Compare commits
5 Commits
master
..
388a34837f
| Author | SHA1 | Date | |
|---|---|---|---|
| 388a34837f | |||
| 2adefb5207 | |||
| ac6442a452 | |||
| 543c4d9096 | |||
| 07e34a1fe8 |
@@ -18,11 +18,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 预览(预留)
|
# 预览
|
||||||
|
[](https://imgchr.com/i/sykhy4)
|
||||||
|
|
||||||
|
|
||||||
# 简介
|
# 简介
|
||||||
基于JAVA 构建,官网:[[www.jiscuss.com]](http://www.jiscuss.com/)
|
基于JAVA,使用SpringBoot + H2 Database + Semantic-UI + Freemarker 构建,官网:[[www.jiscuss.com]](http://www.jiscuss.com/)
|
||||||
|
|
||||||
# 在线体验
|
# 在线体验
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## [三]、运行启动(默认使用MYSQL数据库)
|
## [三]、运行启动(默认使用H2数据库,可以在配置文件切换MYSQL)
|
||||||
|
|
||||||
```
|
```
|
||||||
// 1、代码导入IDE
|
// 1、代码导入IDE
|
||||||
@@ -56,6 +57,178 @@
|
|||||||
http://localhost
|
http://localhost
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 说明:以上为原始快速启动说明。当前升级后的 v2.0 分支已迁移到 Spring Boot 3,推荐使用 JDK 17 运行;旧版本中的 JDK 1.8 说明仅适用于早期分支。
|
||||||
|
|
||||||
|
|
||||||
|
## 当前升级版本说明
|
||||||
|
|
||||||
|
当前代码已完成 Spring Boot 3 兼容升级,主要技术栈如下:
|
||||||
|
|
||||||
|
- Java 17
|
||||||
|
- Spring Boot 3.3.5
|
||||||
|
- Spring Security 6
|
||||||
|
- Spring Data JPA / Hibernate 6
|
||||||
|
- FreeMarker
|
||||||
|
- H2 Database(开发环境默认)
|
||||||
|
- MySQL 8(生产/外部数据库环境)
|
||||||
|
- Flyway(数据库版本迁移)
|
||||||
|
- SpringDoc OpenAPI(替代 Springfox Swagger)
|
||||||
|
- Ehcache 3 + JCache
|
||||||
|
- Druid Spring Boot 3 Starter
|
||||||
|
- MapStruct
|
||||||
|
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- JDK:17+
|
||||||
|
- Maven:3.8+
|
||||||
|
- MySQL:8.x(仅 mysql profile 需要)
|
||||||
|
|
||||||
|
检查本地环境:
|
||||||
|
|
||||||
|
```
|
||||||
|
java -version
|
||||||
|
javac -version
|
||||||
|
mvn -version
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 构建与启动
|
||||||
|
|
||||||
|
### 使用默认 H2 开发环境
|
||||||
|
|
||||||
|
项目默认激活 `h2` profile:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
如果本机 Linux 普通用户无法绑定 80 端口,可以临时指定其他端口:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 使用 MySQL 环境
|
||||||
|
|
||||||
|
1. 创建数据库:
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATE DATABASE jiscuss DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 修改配置文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/resources/application-mysql.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
根据实际环境修改:
|
||||||
|
|
||||||
|
- `spring.datasource.url`
|
||||||
|
- `spring.datasource.username`
|
||||||
|
- `spring.datasource.password`
|
||||||
|
|
||||||
|
3. 启动 mysql profile:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn spring-boot:run -Dspring-boot.run.profiles=mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 数据库迁移说明
|
||||||
|
|
||||||
|
项目已使用 Flyway 接管数据库初始化和后续变更,迁移脚本位于:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/resources/db/migration/
|
||||||
|
```
|
||||||
|
|
||||||
|
当前迁移文件:
|
||||||
|
|
||||||
|
- `V1__init_schema.sql`:初始化表结构和基础数据
|
||||||
|
- `V2__security_updates.sql`:安全相关字段/数据升级
|
||||||
|
|
||||||
|
注意:旧的 `schema.sql` 和 `data.sql` 已不再使用,避免与 Flyway 重复初始化。
|
||||||
|
|
||||||
|
|
||||||
|
## 安全与管理端点
|
||||||
|
|
||||||
|
- 登录认证已迁移到 Spring Security 6 配置方式。
|
||||||
|
- `/admin/**`、`/actuator/**`、`/druid/**` 需要管理员角色访问。
|
||||||
|
- H2 Console 默认关闭;如需开发调试,可显式开启:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn spring-boot:run -Dspring-boot.run.arguments="--spring.h2.console.enabled=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
- OpenAPI 文档地址:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost/swagger-ui.html
|
||||||
|
http://localhost/v3/api-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 网络不佳时的构建建议
|
||||||
|
|
||||||
|
如果依赖已经下载到本地 Maven 仓库,可以使用离线模式:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn -o -DskipTests compile
|
||||||
|
```
|
||||||
|
|
||||||
|
如果首次构建尚未下载 Spring Boot 3.3.5 等依赖,离线模式会失败,需要在网络可用时先执行一次:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn -DskipTests compile
|
||||||
|
```
|
||||||
|
|
||||||
|
国内网络环境可配置 Maven 镜像源后再构建。依赖下载完成后,后续即可使用 `mvn -o` 离线构建。
|
||||||
|
|
||||||
|
|
||||||
|
## 常用检查命令
|
||||||
|
|
||||||
|
编译检查:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn -DskipTests compile
|
||||||
|
```
|
||||||
|
|
||||||
|
打包:
|
||||||
|
|
||||||
|
```
|
||||||
|
mvn -DskipTests package
|
||||||
|
```
|
||||||
|
|
||||||
|
运行 jar:
|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar target/jiscuss-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 升级注意事项
|
||||||
|
|
||||||
|
- Spring Boot 3 使用 `jakarta.*` 命名空间,旧的 `javax.servlet`、`javax.persistence`、`javax.validation` 不再使用。
|
||||||
|
- Hibernate 6 可自动识别数据库方言,配置中无需手动指定 `database-platform`。
|
||||||
|
- MySQL 连接已移除不推荐的 `autoReconnect=true`。
|
||||||
|
- JSON 响应优先使用 Spring MVC + Jackson 自动序列化,不再依赖 `org.json`。
|
||||||
|
- WebSocket starter 已移除;如后续确实需要实时通信,再按功能重新引入。
|
||||||
|
|
||||||
|
|
||||||
## [四]、开源协议
|
## [四]、开源协议
|
||||||
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
INFO 2023-08-07 20:18:44.163 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Start loading
|
|
||||||
INFO 2023-08-07 20:18:44.175 [-Solon-executor-1][*][o.noear.solon.Solon]:
|
|
||||||
Logging: console {level=TRACE, enable=true}
|
|
||||||
INFO 2023-08-07 20:18:44.203 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Plugin starting
|
|
||||||
INFO 2023-08-07 20:18:44.315 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: BeetlRender
|
|
||||||
INFO 2023-08-07 20:18:44.316 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: org.noear.solon.view.beetl.BeetlRender
|
|
||||||
INFO 2023-08-07 20:18:44.316 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .htm=BeetlRender
|
|
||||||
INFO 2023-08-07 20:18:44.317 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .btl=BeetlRender
|
|
||||||
INFO 2023-08-07 20:18:44.326 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Bean scanning
|
|
||||||
INFO 2023-08-07 20:18:44.352 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:18:44.352 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @type_json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:18:44.365 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Connector:main: jlhttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:80}
|
|
||||||
INFO 2023-08-07 20:18:44.365 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Started (jlhttp 2.6/2.4.2) @10ms
|
|
||||||
INFO 2023-08-07 20:18:44.366 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: End loading elapsed=838ms pid=28784 v=2.4.2
|
|
||||||
INFO 2023-08-07 20:21:20.710 [-Thread-2][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Has Stopped (jlhttp 2.6/2.4.2)
|
|
||||||
INFO 2023-08-07 20:21:27.896 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Start loading
|
|
||||||
INFO 2023-08-07 20:21:27.912 [-Solon-executor-1][*][o.noear.solon.Solon]:
|
|
||||||
Logging: console {level=TRACE, enable=true}
|
|
||||||
INFO 2023-08-07 20:21:27.946 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Plugin starting
|
|
||||||
INFO 2023-08-07 20:21:28.195 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:28.198 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: org.noear.solon.view.beetl.BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:28.198 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .htm=BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:28.198 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .btl=BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:28.213 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Bean scanning
|
|
||||||
INFO 2023-08-07 20:21:28.260 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:21:28.261 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @type_json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:21:28.282 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Connector:main: jlhttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:80}
|
|
||||||
INFO 2023-08-07 20:21:28.283 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Started (jlhttp 2.6/2.4.2) @16ms
|
|
||||||
INFO 2023-08-07 20:21:28.285 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: End loading elapsed=1027ms pid=32548 v=2.4.2
|
|
||||||
INFO 2023-08-07 20:21:30.257 [-Thread-2][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Has Stopped (jlhttp 2.6/2.4.2)
|
|
||||||
INFO 2023-08-07 20:21:35.121 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Start loading
|
|
||||||
INFO 2023-08-07 20:21:35.135 [-Solon-executor-1][*][o.noear.solon.Solon]:
|
|
||||||
Logging: console {level=TRACE, enable=true}
|
|
||||||
INFO 2023-08-07 20:21:35.160 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Plugin starting
|
|
||||||
INFO 2023-08-07 20:21:35.266 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:35.266 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: load: org.noear.solon.view.beetl.BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:35.266 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .htm=BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:35.266 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: .btl=BeetlRender
|
|
||||||
INFO 2023-08-07 20:21:35.275 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: Bean scanning
|
|
||||||
INFO 2023-08-07 20:21:35.312 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:21:35.312 [-main][*][o.noear.solon.Solon]:
|
|
||||||
View: mapping: @type_json=StringSerializerRender#SnackSerializer
|
|
||||||
INFO 2023-08-07 20:21:35.330 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Connector:main: jlhttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:80}
|
|
||||||
INFO 2023-08-07 20:21:35.331 [-main][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Started (jlhttp 2.6/2.4.2) @14ms
|
|
||||||
INFO 2023-08-07 20:21:35.332 [-main][*][o.noear.solon.Solon]:
|
|
||||||
App: End loading elapsed=796ms pid=29432 v=2.4.2
|
|
||||||
INFO 2023-08-07 20:21:40.308 [-jlhttp-1][*][c.j.c.DemoController]:
|
|
||||||
test
|
|
||||||
INFO 2023-08-07 20:21:42.146 [-jlhttp-1][*][c.j.c.DemoController]:
|
|
||||||
test
|
|
||||||
INFO 2023-08-07 20:21:42.929 [-jlhttp-1][*][c.j.c.DemoController]:
|
|
||||||
test
|
|
||||||
INFO 2023-08-07 20:21:43.975 [-jlhttp-1][*][c.j.c.DemoController]:
|
|
||||||
test
|
|
||||||
INFO 2023-08-07 20:21:51.555 [-Thread-2][*][o.noear.solon.Solon]:
|
|
||||||
Server:main: jlhttp: Has Stopped (jlhttp 2.6/2.4.2)
|
|
||||||
@@ -1,68 +1,169 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
<groupId>com.yaoyuan</groupId>
|
||||||
|
<artifactId>jiscuss</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>jiccuss</name>
|
||||||
|
<description>Jiscuss forum project for Spring Boot 3</description>
|
||||||
|
|
||||||
<groupId>com.yaoyuan</groupId>
|
<properties>
|
||||||
<artifactId>jiscuss</artifactId>
|
<!-- Java 17 matches the installed JDK (openjdk-17). Upgrade to 21 when JDK 21 is installed. -->
|
||||||
<version>2.0</version>
|
<java.version>17</java.version>
|
||||||
<packaging>jar</packaging>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<parent>
|
<dependencies>
|
||||||
<groupId>org.noear</groupId>
|
<!-- Spring Boot Starters -->
|
||||||
<artifactId>solon-parent</artifactId>
|
<dependency>
|
||||||
<version>2.4.3</version>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<relativePath />
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</parent>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Spring Security -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Bean Validation (input validation) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Actuator for health checks and metrics -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Cache -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<properties>
|
<!-- H2 (dev/test only) -->
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<dependency>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<groupId>com.h2database</groupId>
|
||||||
</properties>
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependencies>
|
<!-- MySQL 8 connector (updated groupId from mysql:mysql-connector-java) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.noear</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>solon-api</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
</dependency>
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- SpringDoc OpenAPI 2 — replaces discontinued springfox-swagger2, compatible with Spring Boot 3 -->
|
||||||
<groupId>org.noear</groupId>
|
<dependency>
|
||||||
<artifactId>solon.auth</artifactId>
|
<groupId>org.springdoc</groupId>
|
||||||
</dependency>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- Lombok -->
|
||||||
<groupId>org.noear</groupId>
|
<dependency>
|
||||||
<artifactId>solon.view.beetl</artifactId>
|
<groupId>org.projectlombok</groupId>
|
||||||
</dependency>
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- MapStruct for type-safe DTO mapping -->
|
||||||
<groupId>org.noear</groupId>
|
<dependency>
|
||||||
<artifactId>solon.banner</artifactId>
|
<groupId>org.mapstruct</groupId>
|
||||||
</dependency>
|
<artifactId>mapstruct</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- Druid Spring Boot 3 starter (Spring Boot 3 compatible version) -->
|
||||||
<groupId>org.noear</groupId>
|
<dependency>
|
||||||
<artifactId>solon.logging.logback</artifactId>
|
<groupId>com.alibaba</groupId>
|
||||||
</dependency>
|
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||||
|
<version>1.2.23</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Ehcache 3 with Jakarta namespace (for Spring Boot 3 / JCache / JSR-107) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ehcache</groupId>
|
||||||
|
<artifactId>ehcache</artifactId>
|
||||||
|
<version>3.10.8</version>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
|
</dependency>
|
||||||
|
<!-- JCache API (JSR-107) required for Ehcache 3 Spring integration -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.cache</groupId>
|
||||||
|
<artifactId>cache-api</artifactId>
|
||||||
|
<version>1.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Flyway for database schema version management -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Flyway MySQL dialect support (required for MySQL 8+) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-mysql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<!-- Ensure Lombok and MapStruct annotation processors cooperate -->
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.noear</groupId>
|
|
||||||
<artifactId>solon.health.detector</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<finalName>${project.artifactId}</finalName>
|
|
||||||
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.noear</groupId>
|
|
||||||
<artifactId>solon-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.jiscuss;
|
|
||||||
|
|
||||||
import com.jiscuss.dso.AuthProcessorImpl;
|
|
||||||
import org.noear.solon.annotation.Bean;
|
|
||||||
import org.noear.solon.annotation.Configuration;
|
|
||||||
import org.noear.solon.auth.AuthUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author cyy 2023/7/12 created
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class Config {
|
|
||||||
@Bean
|
|
||||||
public void authAdapter(){
|
|
||||||
AuthUtil.adapter()
|
|
||||||
.processor(new AuthProcessorImpl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.jiscuss;
|
|
||||||
|
|
||||||
import org.noear.solon.Solon;
|
|
||||||
import org.noear.solon.annotation.SolonMain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author cyy 2023/7/12 created
|
|
||||||
*/
|
|
||||||
@SolonMain
|
|
||||||
public class JiscussApp {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
Solon.start(JiscussApp.class, args)
|
|
||||||
.onError(e -> e.printStackTrace());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.jiscuss.controller;
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Controller;
|
|
||||||
import org.noear.solon.annotation.Mapping;
|
|
||||||
import org.noear.solon.core.handle.ModelAndView;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author cyy 2023/7/12 created
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
public class DemoController {
|
|
||||||
|
|
||||||
static Logger log = LoggerFactory.getLogger(DemoController.class);
|
|
||||||
|
|
||||||
@Mapping("/")
|
|
||||||
public Object test(){
|
|
||||||
log.info("test");
|
|
||||||
ModelAndView model = new ModelAndView("index.htm");
|
|
||||||
model.put("title","jiscuss");
|
|
||||||
model.put("message","你好 jiscuss!");
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.jiscuss.dso;
|
|
||||||
|
|
||||||
import org.noear.solon.auth.AuthProcessorBase;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author cyy 2023/7/12 created
|
|
||||||
*/
|
|
||||||
public class AuthProcessorImpl extends AuthProcessorBase {
|
|
||||||
@Override
|
|
||||||
public boolean verifyLogined() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> getPermissions() {
|
|
||||||
List<String> list = new ArrayList<>();
|
|
||||||
|
|
||||||
list.add("user:add");
|
|
||||||
list.add("user:demo");
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> getRoles() {
|
|
||||||
List<String> list = new ArrayList<>();
|
|
||||||
|
|
||||||
list.add("admin1");
|
|
||||||
list.add("admin2");
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yaoyuan.jiscuss;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
|
public class JiccussApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(JiccussApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.yaoyuan.jiscuss.common;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.PostCustom;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.common
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/8/17 14:51
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Node {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Node.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node
|
||||||
|
* 空方法
|
||||||
|
**/
|
||||||
|
public Node() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private Integer discussionId;
|
||||||
|
|
||||||
|
private Integer number;
|
||||||
|
|
||||||
|
private Date time;
|
||||||
|
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private Integer parentId;
|
||||||
|
|
||||||
|
private Date editTime;
|
||||||
|
|
||||||
|
private Integer editUserId;
|
||||||
|
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
private String copyright;
|
||||||
|
|
||||||
|
private Integer isApproved;
|
||||||
|
|
||||||
|
private Integer createId;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* avatar
|
||||||
|
* 用户表相关
|
||||||
|
**/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private String realname;
|
||||||
|
|
||||||
|
private String avatarReply;
|
||||||
|
|
||||||
|
private String usernameReply;
|
||||||
|
|
||||||
|
private String realnameReply;
|
||||||
|
|
||||||
|
//下一条回复
|
||||||
|
private List<Node> nextNodes = new ArrayList<Node>();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个node添加到链表中
|
||||||
|
*
|
||||||
|
* @param list
|
||||||
|
* @param node
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean addNode(List<Node> list, Node node) {
|
||||||
|
for (Node node1 : list) { //循环添加
|
||||||
|
if (node1.getId().equals(node.getParentId())) { //判断留言的上一段是都是这条留言
|
||||||
|
node1.getNextNodes().add(node); //是,添加,返回true;
|
||||||
|
logger.debug("添加了一个");
|
||||||
|
return true;
|
||||||
|
} else { //否则递归继续判断
|
||||||
|
if (node1.getNextNodes().size() != 0) {
|
||||||
|
if (Node.addNode(node1.getNextNodes(), node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将查出来的lastId不为null的回复都添加到第一层Node集合中
|
||||||
|
*
|
||||||
|
* @param firstList
|
||||||
|
* @param thenList
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static List addAllNode(List<Node> firstList, List<PostCustom> thenList) {
|
||||||
|
while (thenList.size() != 0) {
|
||||||
|
int size = thenList.size();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
Node node = new Node();
|
||||||
|
BeanUtils.copyProperties(thenList.get(i), node);
|
||||||
|
if (Node.addNode(firstList, node)) {
|
||||||
|
thenList.remove(i);
|
||||||
|
i--;
|
||||||
|
size--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印
|
||||||
|
* show
|
||||||
|
**/
|
||||||
|
public static void show(List<Node> list) {
|
||||||
|
for (Node node : list) {
|
||||||
|
logger.debug("{} 用户回复了你:{}", node.getUserId(), node.getContent());
|
||||||
|
if (node.getNextNodes().size() != 0) {
|
||||||
|
Node.show(node.getNextNodes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.yaoyuan.jiscuss.common;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.PostCustom;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.sql.Clob;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title: 工具类
|
||||||
|
* @Package com.yaoyuan.jiscuss.common
|
||||||
|
* @Description: 通用工具类
|
||||||
|
* @date 2020/9/10 15:47
|
||||||
|
*/
|
||||||
|
public class PostCommonUtil {
|
||||||
|
|
||||||
|
public static List<PostCustom> getNewPostsObjMap(List<Map<String, Object>> posts) {
|
||||||
|
List<PostCustom> postCustomList = new ArrayList<>();
|
||||||
|
for (Map<String, Object> mapObj : posts) {
|
||||||
|
PostCustom postCustom = new PostCustom();
|
||||||
|
postCustom.setId(Integer.parseInt(String.valueOf(mapObj.get("id"))));
|
||||||
|
postCustom.setParentId(mapObj.get("parent_id") != null ? Integer.parseInt(String.valueOf(mapObj.get("parent_id"))) : null);
|
||||||
|
if (null != mapObj.get("create_time")) {
|
||||||
|
postCustom.setCreateTime((Date) mapObj.get("create_time"));
|
||||||
|
}
|
||||||
|
String content = "";
|
||||||
|
if (mapObj.get("content") != null) {
|
||||||
|
if (mapObj.get("content") instanceof Clob) {
|
||||||
|
Clob clob = (Clob) mapObj.get("content");
|
||||||
|
content = PostCommonUtil.clobToString(clob);
|
||||||
|
} else if (mapObj.get("content") instanceof String) {
|
||||||
|
content = String.valueOf(mapObj.get("content"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postCustom.setContent(content);
|
||||||
|
String avatar = "";
|
||||||
|
if (mapObj.get("create_avatar") != null) {
|
||||||
|
if (mapObj.get("create_avatar") instanceof Clob) {
|
||||||
|
Clob clob = (Clob) mapObj.get("create_avatar");
|
||||||
|
avatar = PostCommonUtil.clobToString(clob);
|
||||||
|
} else if (mapObj.get("create_avatar") instanceof String) {
|
||||||
|
avatar = String.valueOf(mapObj.get("create_avatar"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postCustom.setAvatar(avatar);
|
||||||
|
postCustom.setUsername(mapObj.get("create_username") != null ? String.valueOf(mapObj.get("create_username")) : null);
|
||||||
|
postCustom.setRealname(mapObj.get("create_realname") != null ? String.valueOf(mapObj.get("create_realname")) : null);
|
||||||
|
String avatarReply = "";
|
||||||
|
if (mapObj.get("user_avatar") != null) {
|
||||||
|
if (mapObj.get("user_avatar") instanceof Clob) {
|
||||||
|
Clob clob = (Clob) mapObj.get("user_avatar");
|
||||||
|
avatarReply = PostCommonUtil.clobToString(clob);
|
||||||
|
} else if (mapObj.get("user_avatar") instanceof String) {
|
||||||
|
avatarReply = String.valueOf(mapObj.get("user_avatar"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postCustom.setAvatarReply(avatarReply);
|
||||||
|
postCustom.setUsernameReply(mapObj.get("user_username") != null ? String.valueOf(mapObj.get("user_username")) : null);
|
||||||
|
postCustom.setRealnameReply(mapObj.get("user_realname") != null ? String.valueOf(mapObj.get("user_realname")) : null);
|
||||||
|
|
||||||
|
postCustomList.add(postCustom);
|
||||||
|
}
|
||||||
|
return postCustomList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取post
|
||||||
|
* @param postCustomList
|
||||||
|
* @return List<PostCustom>
|
||||||
|
*/
|
||||||
|
public static List<PostCustom> getNewPostsObjCustom(List<PostCustom> postCustomList) {
|
||||||
|
List<PostCustom> mainList = new ArrayList<>();
|
||||||
|
Map<String, Object> postMap = new LinkedHashMap<>();
|
||||||
|
for (PostCustom mapObj : postCustomList) {
|
||||||
|
if (null == mapObj.getParentId()) {
|
||||||
|
mainList.add(mapObj);
|
||||||
|
} else {
|
||||||
|
List<PostCustom> tempList = new ArrayList<>();
|
||||||
|
if (postMap.containsKey(String.valueOf(mapObj.getParentId()))) {
|
||||||
|
tempList = (List<PostCustom>) postMap.get(String.valueOf(mapObj.getParentId()));
|
||||||
|
tempList.add(mapObj);
|
||||||
|
postMap.put(String.valueOf(mapObj.getParentId()), tempList);
|
||||||
|
} else {
|
||||||
|
tempList.add(mapObj);
|
||||||
|
postMap.put(String.valueOf(mapObj.getParentId()), tempList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PostCustom> mainListResult = new ArrayList<>();
|
||||||
|
for (PostCustom mapObj : mainList) {
|
||||||
|
if (postMap.containsKey(String.valueOf(mapObj.getId()))) {
|
||||||
|
PostCustom newObj = new PostCustom();
|
||||||
|
BeanUtils.copyProperties(mapObj, newObj);
|
||||||
|
newObj.setChild((List<PostCustom>) postMap.get(String.valueOf(mapObj.getId())));
|
||||||
|
mainListResult.add(newObj);
|
||||||
|
} else {
|
||||||
|
mainListResult.add(mapObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mainListResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clob类型转换成String类型
|
||||||
|
* @param clob
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String clobToString(final Clob clob) {
|
||||||
|
if (clob == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Reader is = null;
|
||||||
|
try {
|
||||||
|
is = clob.getCharacterStream();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
BufferedReader br = new BufferedReader(is);
|
||||||
|
String str = null;
|
||||||
|
try {
|
||||||
|
str = br.readLine(); // 读取第一行
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
while (str != null) { // 如果没有到达流的末尾,则继续读取下一行
|
||||||
|
sb.append(str);
|
||||||
|
try {
|
||||||
|
str = br.readLine();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String returnString = sb.toString();
|
||||||
|
return returnString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.yaoyuan.jiscuss.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MyMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
// SpringDoc OpenAPI serves its own UI resources — no manual mapping needed.
|
||||||
|
// Static application assets:
|
||||||
|
registry.addResourceHandler("/static/**")
|
||||||
|
.addResourceLocations("classpath:/static/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.yaoyuan.jiscuss.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpringDoc OpenAPI 2.x configuration.
|
||||||
|
*
|
||||||
|
* <p>Replaces discontinued springfox-swagger2 (incompatible with Spring Boot 3).
|
||||||
|
* UI is available at /swagger-ui/index.html
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SwaggerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI jiscussOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("Jiscuss REST APIs")
|
||||||
|
.description("Jiscuss forum system API documentation")
|
||||||
|
.version("1.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Jiscuss Team")
|
||||||
|
.email("developer@mail.com")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.yaoyuan.jiscuss.config;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.handler.CustomAccessDeniedHandler;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler;
|
||||||
|
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security 6 configuration (Spring Boot 3 compatible).
|
||||||
|
*
|
||||||
|
* <p>Breaking changes from Spring Boot 2.x:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code WebSecurityConfigurerAdapter} removed → use {@code SecurityFilterChain} bean</li>
|
||||||
|
* <li>{@code antMatchers} → {@code requestMatchers}</li>
|
||||||
|
* <li>{@code @EnableGlobalMethodSecurity} → {@code @EnableMethodSecurity}</li>
|
||||||
|
* <li>{@code @Configurable} (AspectJ) corrected to {@code @Configuration}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
|
||||||
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
|
private final CustomAccessDeniedHandler customAccessDeniedHandler;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
/** Controlled by spring.h2.console.enabled — only true in dev (h2) profile. */
|
||||||
|
@Value("${spring.h2.console.enabled:false}")
|
||||||
|
private boolean h2ConsoleEnabled;
|
||||||
|
|
||||||
|
public WebSecurityConfig(
|
||||||
|
CustomAccessDeniedHandler customAccessDeniedHandler,
|
||||||
|
@Qualifier("userDetailServiceImpl") UserDetailsService userDetailsService,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
this.customAccessDeniedHandler = customAccessDeniedHandler;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude truly static assets from the security filter chain entirely.
|
||||||
|
* H2 console and Druid monitoring are NOT excluded here — they are secured via filterChain.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
|
return web -> web.ignoring().requestMatchers(
|
||||||
|
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main security filter chain.
|
||||||
|
*
|
||||||
|
* <p>Security decisions:
|
||||||
|
* <ul>
|
||||||
|
* <li>H2 console: only accessible when enabled (dev profile) and requires ROLE_ADMIN</li>
|
||||||
|
* <li>Druid monitoring: requires ROLE_ADMIN through Spring Security</li>
|
||||||
|
* <li>Admin/Actuator paths: requires ROLE_ADMIN</li>
|
||||||
|
* <li>SpringDoc UI paths: public (API docs are for developers)</li>
|
||||||
|
* <li>All other authenticated requests: evaluated by {@code RbacPermission.hasPermission}</li>
|
||||||
|
* <li>X-Frame-Options: SAMEORIGIN (prevents clickjacking while allowing H2 console iframe)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
// Fix: SAMEORIGIN instead of disable() — prevents clickjacking, allows H2 iframe in same origin
|
||||||
|
http.headers(headers -> headers
|
||||||
|
.frameOptions(frame -> frame.sameOrigin())
|
||||||
|
);
|
||||||
|
|
||||||
|
// CSRF: disable for H2 console path only (H2 web console does not send CSRF tokens)
|
||||||
|
if (h2ConsoleEnabled) {
|
||||||
|
http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the RBAC expression manager with ApplicationContext so @bean references work
|
||||||
|
DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
|
||||||
|
expressionHandler.setApplicationContext(applicationContext);
|
||||||
|
WebExpressionAuthorizationManager rbacManager = new WebExpressionAuthorizationManager(
|
||||||
|
"@rbacPermission.hasPermission(request, authentication)");
|
||||||
|
rbacManager.setExpressionHandler(expressionHandler);
|
||||||
|
|
||||||
|
http.authorizeHttpRequests(auth -> {
|
||||||
|
// Publicly accessible
|
||||||
|
auth.requestMatchers(
|
||||||
|
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||||
|
"/", "/main", "/index", "/getdiscussionsbyid",
|
||||||
|
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
||||||
|
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
||||||
|
).permitAll();
|
||||||
|
|
||||||
|
// H2 console: admin-only, dev profile only
|
||||||
|
if (h2ConsoleEnabled) {
|
||||||
|
auth.requestMatchers("/h2-console/**").hasRole("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Druid monitoring, Actuator, Admin UI: restricted to ROLE_ADMIN
|
||||||
|
auth.requestMatchers("/druid/**").hasRole("ADMIN");
|
||||||
|
auth.requestMatchers("/actuator/**").hasRole("ADMIN");
|
||||||
|
auth.requestMatchers("/admin/**").hasRole("ADMIN");
|
||||||
|
|
||||||
|
// All remaining requests evaluated by RBAC permission bean
|
||||||
|
auth.anyRequest().access(rbacManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
http.formLogin(form -> form
|
||||||
|
.loginPage("/login")
|
||||||
|
.loginProcessingUrl("/login")
|
||||||
|
.usernameParameter("username")
|
||||||
|
.passwordParameter("password")
|
||||||
|
.defaultSuccessUrl("/index")
|
||||||
|
);
|
||||||
|
|
||||||
|
http.logout(logout -> logout
|
||||||
|
.logoutSuccessUrl("/login?logout")
|
||||||
|
);
|
||||||
|
|
||||||
|
http.exceptionHandling(ex -> ex
|
||||||
|
.accessDeniedHandler(customAccessDeniedHandler)
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication manager wired with BCrypt — replaces the old
|
||||||
|
* {@code configure(AuthenticationManagerBuilder)} override.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(BCryptPasswordEncoder passwordEncoder) {
|
||||||
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
|
provider.setUserDetailsService(userDetailsService);
|
||||||
|
provider.setPasswordEncoder(passwordEncoder);
|
||||||
|
return new ProviderManager(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** BCrypt password encoder bean. */
|
||||||
|
@Bean
|
||||||
|
public BCryptPasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* 后台系统控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class AdminSystemController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台登录
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台退出
|
||||||
|
*/
|
||||||
|
@RequestMapping("/admin/logout")
|
||||||
|
public String logout(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||||
|
return "admin/logout";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台设置管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台页面
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping("/admin/home")
|
||||||
|
public String home(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||||
|
return "admin/home";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base controller exposing helpers shared by web controllers.
|
||||||
|
*
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
*/
|
||||||
|
public class BaseController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current authenticated user, or {@code null} when not logged in.
|
||||||
|
*/
|
||||||
|
protected UserInfo getUserInfo(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object ctx = session.getAttribute("SPRING_SECURITY_CONTEXT");
|
||||||
|
if (!(ctx instanceof SecurityContext securityContext)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Authentication authentication = securityContext.getAuthentication();
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof UserInfo userInfo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* 用户消息控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class UserMsgController extends BaseController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页最新消息
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* 其他控制器——积分/权限等
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class UserOtherController extends BaseController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户积分获取
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.controller
|
||||||
|
* @Description:
|
||||||
|
* @date
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class UserPageController extends BaseController {
|
||||||
|
|
||||||
|
private static Logger logger = LoggerFactory.getLogger(UserPageController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUsersService usersService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDiscussionsService discussionsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ITagsService tagsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户页面
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping("/user")
|
||||||
|
public String user(@RequestParam(defaultValue = "discussion") String type, HttpServletRequest request, ModelMap map) {
|
||||||
|
|
||||||
|
// type 判断
|
||||||
|
if (type.equals("discussion")) {
|
||||||
|
map.put("discussion", "active");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.equals("change")) {
|
||||||
|
map.put("change", "active");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.equals("like")) {
|
||||||
|
map.put("like", "active");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
map.put("username", user.getUsername());
|
||||||
|
map.put("data", "Jiscuss 用户:" + user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import com.yaoyuan.jiscuss.entity.DiscussionTag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.Post;
|
||||||
|
import com.yaoyuan.jiscuss.entity.Tag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.IPostsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* 主题帖子评论控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class UserPostController extends BaseController {
|
||||||
|
|
||||||
|
private static Logger logger = LoggerFactory.getLogger(UserPostController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDiscussionsService discussionsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ITagsService tagsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDiscussionsTagsService iDiscussionsTagsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPostsService postsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUsersService usersService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页查看主题列表(各种条件筛选,最热最新标签等)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看主题详情
|
||||||
|
* @param request
|
||||||
|
* @param map
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping("/getdiscussionsbyid")
|
||||||
|
public String getDiscussionsById(HttpServletRequest request, ModelMap map, @RequestParam("id") Integer id) {
|
||||||
|
logger.info(">>> getDiscussionsById{}", id);
|
||||||
|
|
||||||
|
Discussion discussion = discussionsService.findOne(id);
|
||||||
|
// 获取此主题下的评论
|
||||||
|
List<Post> posts = postsService.findOneBy(id);
|
||||||
|
|
||||||
|
DiscussionCustom newdd = new DiscussionCustom();
|
||||||
|
BeanUtils.copyProperties(discussion, newdd);
|
||||||
|
if (null != newdd.getStartUserId()) {
|
||||||
|
User startUser = usersService.findOne(newdd.getStartUserId());
|
||||||
|
newdd.setAvatar(startUser.getAvatar());
|
||||||
|
newdd.setRealname(startUser.getRealname());
|
||||||
|
newdd.setUsername(startUser.getUsername());
|
||||||
|
}
|
||||||
|
if (null != newdd.getLastUserId()) {
|
||||||
|
User lastUser = usersService.findOne(newdd.getLastUserId());
|
||||||
|
newdd.setAvatarLast(lastUser.getAvatar());
|
||||||
|
newdd.setRealnameLast(lastUser.getRealname());
|
||||||
|
newdd.setUsernameLast(lastUser.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Tag> tags = tagsService.findByDId(id);
|
||||||
|
List postsObj = postsService.findPostCustomById(id);
|
||||||
|
map.put("tags", tags);
|
||||||
|
map.put("discussions", newdd);
|
||||||
|
map.put("posts", postsObj);
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
map.put("username", user.getUsername());
|
||||||
|
}
|
||||||
|
return "discussions";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建主题
|
||||||
|
* @param discussion
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = {"/newDiscussions", "/newdiscussions"})
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> newDiscussions(@RequestBody DiscussionCustom discussion) {
|
||||||
|
logger.info(">>> newPost" + discussion);
|
||||||
|
|
||||||
|
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
HttpServletRequest request = servletRequestAttributes.getRequest();
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
discussion.setStartUserId(user.getId());
|
||||||
|
discussion.setLastUserId(user.getId());
|
||||||
|
discussion.setCreateId(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
discussion.setCreateTime(new Date());
|
||||||
|
discussion.setStartTime(new Date());
|
||||||
|
discussion.setLastTime(new Date());
|
||||||
|
|
||||||
|
Discussion discussionOne = new Discussion();
|
||||||
|
BeanUtils.copyProperties(discussion, discussionOne);
|
||||||
|
Discussion saveDiscussion = discussionsService.insert(discussionOne);
|
||||||
|
|
||||||
|
if (null != discussion.getTag()) {
|
||||||
|
//执行组装标签
|
||||||
|
String[] strArray = discussion.getTag().split(",");
|
||||||
|
for (String str : strArray) {
|
||||||
|
DiscussionTag dtag = new DiscussionTag();
|
||||||
|
dtag.setDiscussionId(saveDiscussion.getId());
|
||||||
|
dtag.setTagId(Integer.parseInt(str));
|
||||||
|
iDiscussionsTagsService.insert(dtag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> resultobj = new HashMap<>();
|
||||||
|
logger.info(">>>{}", saveDiscussion);
|
||||||
|
resultobj.put("msg", "添加主题成功");
|
||||||
|
resultobj.put("flag", true);
|
||||||
|
return resultobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑主题
|
||||||
|
* @param post
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除主题
|
||||||
|
* @param post
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建评论
|
||||||
|
* @param post
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/newPost")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> newPosts(@RequestBody Post post) {
|
||||||
|
logger.info(">>> newpost" + post);
|
||||||
|
|
||||||
|
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
HttpServletRequest request = servletRequestAttributes.getRequest();
|
||||||
|
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
post.setCreateId(user.getId());
|
||||||
|
}
|
||||||
|
post.setCreateTime(new Date());
|
||||||
|
if (null != post.getParentId()) {
|
||||||
|
Post temp = postsService.findOneByid(post.getParentId());
|
||||||
|
post.setUserId(temp.getCreateId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Post savePost = postsService.insert(post);
|
||||||
|
Map<String, Object> resultobj = new HashMap<>();
|
||||||
|
logger.info(">>>{}", savePost);
|
||||||
|
resultobj.put("msg", "添加评论成功");
|
||||||
|
resultobj.put("flag", true);
|
||||||
|
return resultobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
* @param tag
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建标签
|
||||||
|
* @param tag
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/newtags")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> newTags(@RequestBody Tag tag) {
|
||||||
|
logger.info(">>> newTags" + tag);
|
||||||
|
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
HttpServletRequest request = servletRequestAttributes.getRequest();
|
||||||
|
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
tag.setCreateId(user.getId());
|
||||||
|
}
|
||||||
|
tag.setCreateTime(new Date());
|
||||||
|
|
||||||
|
Tag saveTag = tagsService.insert(tag);
|
||||||
|
Map<String, Object> resultobj = new HashMap<>();
|
||||||
|
logger.info(">>>{}", saveTag);
|
||||||
|
resultobj.put("msg", "添加标签成功");
|
||||||
|
resultobj.put("flag", true);
|
||||||
|
return resultobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排行榜等
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建主题页
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping({"/newDiscussionsPage"})
|
||||||
|
public String newdiccuss(HttpServletRequest request, ModelMap map) {
|
||||||
|
//获取所有标签(以后尝试去缓存中取)
|
||||||
|
List<Tag> allTags = tagsService.getAllListDiscussions();
|
||||||
|
map.put("allTags", allTags);
|
||||||
|
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
map.put("username", user.getUsername());
|
||||||
|
map.put("data", "Jiscuss 用户:" + user.getUsername());
|
||||||
|
}
|
||||||
|
return "newdiccuss";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建标签页
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping({"/newTagPage"})
|
||||||
|
public String newtag(HttpServletRequest request, ModelMap map) {
|
||||||
|
|
||||||
|
//获取所有标签(以后尝试去缓存中取)
|
||||||
|
List<Tag> allTags = tagsService.getAllList();
|
||||||
|
map.put("allTags", allTags);
|
||||||
|
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
map.put("username", user.getUsername());
|
||||||
|
map.put("data", "Jiscuss 用户:" + user.getUsername());
|
||||||
|
}
|
||||||
|
return "newtag";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import com.yaoyuan.jiscuss.entity.Tag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.DiscussionCustom;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import com.yaoyuan.jiscuss.util.DelTagsUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* 首页页面系统控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class UserSystemController extends BaseController {
|
||||||
|
private static Logger logger = LoggerFactory.getLogger(UserSystemController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUsersService usersService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDiscussionsService discussionsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ITagsService tagsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页index
|
||||||
|
* @param tag
|
||||||
|
* @param type
|
||||||
|
* @param pageNum
|
||||||
|
* @param request
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping({"/", "/main", "/index"})
|
||||||
|
public String home(@RequestParam(defaultValue = "all") String tag, @RequestParam(defaultValue = "all") String type,
|
||||||
|
@RequestParam(defaultValue = "1") Integer pageNum, HttpServletRequest request, ModelMap map) {
|
||||||
|
logger.info(">>> index");
|
||||||
|
logger.info(">>> tag:" + tag);
|
||||||
|
logger.info(">>> pageNum:" + pageNum);
|
||||||
|
int pageSiz = 10;
|
||||||
|
int pageNumNew = pageNum - 1;
|
||||||
|
Discussion discussion = new Discussion();
|
||||||
|
//分页获取主题帖子-默认
|
||||||
|
Page<Discussion> allDiscussionsPage = discussionsService.queryAllDiscussionsList(discussion, pageNumNew, pageSiz, tag, type);
|
||||||
|
|
||||||
|
List<Discussion> allDiscussions = allDiscussionsPage.getContent();
|
||||||
|
List<DiscussionCustom> newAllD = new ArrayList<>();
|
||||||
|
setTagAndUserList(newAllD, allDiscussions);
|
||||||
|
logger.info("全部主题==>:{}", allDiscussions);
|
||||||
|
|
||||||
|
Long total = allDiscussionsPage.getTotalElements();
|
||||||
|
|
||||||
|
//获取主题帖子的分页数据(以后尝试去缓存中取)
|
||||||
|
List<String> pageNumList = getPageNumList(allDiscussionsPage.getTotalPages());
|
||||||
|
|
||||||
|
//获取所有标签(以后尝试去缓存中取)
|
||||||
|
List<Tag> allTags = tagsService.getAllList();
|
||||||
|
logger.info("全部标签==>:{}", allTags);
|
||||||
|
|
||||||
|
if (!tag.equals("all")) {
|
||||||
|
//获取是否有子标签
|
||||||
|
List<Tag> allChildTags = tagsService.findByParentId(tag);
|
||||||
|
map.put("allChildTags", allChildTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.put("data", "Jiscuss 用户");
|
||||||
|
map.put("allDiscussions", newAllD);
|
||||||
|
map.put("allUser", usersService.getAllList());
|
||||||
|
map.put("pageDiscussions", pageNumList);
|
||||||
|
map.put("allTags", allTags);
|
||||||
|
map.put("tag", tag);
|
||||||
|
map.put("type", type);
|
||||||
|
|
||||||
|
if (type.equals("all")) {
|
||||||
|
map.put("typeAll", "active");
|
||||||
|
}
|
||||||
|
if (type.equals("hot")) {
|
||||||
|
map.put("typeHot", "active");
|
||||||
|
}
|
||||||
|
if (type.equals("new")) {
|
||||||
|
map.put("typeNew", "active");
|
||||||
|
}
|
||||||
|
map.put("pageSize", pageSiz);
|
||||||
|
map.put("pageTotal", total);
|
||||||
|
map.put("pageNum", pageNum);
|
||||||
|
map.put("pageTotalPages", allDiscussionsPage.getTotalPages());
|
||||||
|
UserInfo user = getUserInfo(request);
|
||||||
|
if (user != null) {
|
||||||
|
map.put("username", user.getUsername());
|
||||||
|
map.put("data", "Jiscuss 用户:" + user.getUsername());
|
||||||
|
}
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装DiscussionCustom
|
||||||
|
*
|
||||||
|
* @param newAllD
|
||||||
|
* @param allDiscussions
|
||||||
|
*/
|
||||||
|
private void setTagAndUserList(List<DiscussionCustom> newAllD, List<Discussion> allDiscussions) {
|
||||||
|
|
||||||
|
List<Integer> discussionIdLsit = new ArrayList<>();
|
||||||
|
for (Discussion dd : allDiscussions) {
|
||||||
|
discussionIdLsit.add(dd.getId());
|
||||||
|
}
|
||||||
|
List<TagCustom> TagCustomList = tagsService.findByDiscussionIdlistId(discussionIdLsit);
|
||||||
|
|
||||||
|
Map<Integer, List<TagCustom>> tagMap = new HashMap<>();
|
||||||
|
for (TagCustom tc : TagCustomList) {
|
||||||
|
List<TagCustom> tagCustomListTemp = new ArrayList<>();
|
||||||
|
if (tagMap.containsKey(tc.getDiscussionId())) {
|
||||||
|
tagCustomListTemp = tagMap.get(tc.getDiscussionId());
|
||||||
|
tagCustomListTemp.add(tc);
|
||||||
|
tagMap.put(tc.getDiscussionId(), tagCustomListTemp);
|
||||||
|
} else {
|
||||||
|
tagCustomListTemp.add(tc);
|
||||||
|
tagMap.put(tc.getDiscussionId(), tagCustomListTemp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Discussion dd : allDiscussions) {
|
||||||
|
DiscussionCustom newdd = new DiscussionCustom();
|
||||||
|
BeanUtils.copyProperties(dd, newdd);
|
||||||
|
String newCon = "";
|
||||||
|
String content = DelTagsUtil.getTextFromHtml(newdd.getContent());
|
||||||
|
if (null != content && content.length() > 30) {
|
||||||
|
newCon = content.substring(0, 29);
|
||||||
|
} else {
|
||||||
|
newCon = content;
|
||||||
|
}
|
||||||
|
newdd.setContent(newCon);
|
||||||
|
if (null != newdd.getStartUserId()) {
|
||||||
|
User startUser = usersService.findOne(newdd.getStartUserId());
|
||||||
|
newdd.setAvatar(startUser != null && startUser.getAvatar() != null ? startUser.getAvatar() : "");
|
||||||
|
newdd.setRealname(startUser != null && startUser.getRealname() != null ? startUser.getRealname() : "");
|
||||||
|
newdd.setUsername(startUser != null && startUser.getUsername() != null ? startUser.getUsername() : "");
|
||||||
|
}
|
||||||
|
if (null != newdd.getLastUserId()) {
|
||||||
|
User lastUser = usersService.findOne(newdd.getLastUserId());
|
||||||
|
newdd.setAvatarLast(lastUser != null && lastUser.getAvatar() != null ? lastUser.getAvatar() : "");
|
||||||
|
newdd.setRealnameLast(lastUser != null && lastUser.getRealname() != null ? lastUser.getRealname() : "");
|
||||||
|
newdd.setUsernameLast(lastUser != null && lastUser.getUsername() != null ? lastUser.getUsername() : "");
|
||||||
|
}
|
||||||
|
// 组装tag
|
||||||
|
newdd.setTagList(tagMap.get(dd.getId()));
|
||||||
|
newAllD.add(newdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页信息
|
||||||
|
*
|
||||||
|
* @param size
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private List<String> getPageNumList(int size) {
|
||||||
|
List<String> pageNumList = new ArrayList<String>();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
pageNumList.add("" + (i + 1));
|
||||||
|
}
|
||||||
|
return pageNumList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录页
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
* @param logout
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping("/login")
|
||||||
|
public String login(@RequestParam(value = "error", required = false) String error,
|
||||||
|
@RequestParam(value = "logout", required = false) String logout, ModelMap map) {
|
||||||
|
if (error != null) {
|
||||||
|
map.put("msg", "您输入的用户名密码错误!");
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
if (logout != null) {
|
||||||
|
map.put("msg", "退出成功!");
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册提交
|
||||||
|
* @param username
|
||||||
|
* @param email
|
||||||
|
* @param password
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/registerDo")
|
||||||
|
public String registerDo(@RequestParam String username, @RequestParam String email,
|
||||||
|
@RequestParam String password, ModelMap map) {
|
||||||
|
//验证用户名是否存在
|
||||||
|
if (null != usersService.getByUsername(username)) {
|
||||||
|
map.put("msg", "用户已存在,请重试!");
|
||||||
|
return "register";
|
||||||
|
} else {
|
||||||
|
User user = new User();
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setPassword(password);
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setRealname(username);
|
||||||
|
user.setJoinTime(new Date());
|
||||||
|
User userInsert = usersService.insert(user);
|
||||||
|
map.put("msg", "注册成功,请登陆!");
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册页面
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping("/register")
|
||||||
|
public String register() {
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取设置信息
|
||||||
|
|
||||||
|
//获取首页统计
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller.api;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import com.yaoyuan.jiscuss.exception.BaseException;
|
||||||
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "/other_api", produces = "application/json;charset=utf-8")
|
||||||
|
@Tag(name = "主题帖子(其他)接口管理", description = "主题相关 REST API")
|
||||||
|
public class RestPostController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RestPostController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDiscussionsService discussionsService;
|
||||||
|
|
||||||
|
@PostMapping("/discussion")
|
||||||
|
@Operation(summary = "新增主题")
|
||||||
|
public Discussion save(@RequestBody Discussion discussion) {
|
||||||
|
Discussion saveDiscussion = discussionsService.insert(discussion);
|
||||||
|
if (saveDiscussion != null) {
|
||||||
|
return saveDiscussion;
|
||||||
|
} else {
|
||||||
|
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/discussion/{id}")
|
||||||
|
@Operation(summary = "获取主题")
|
||||||
|
public Discussion getDiscussions(@PathVariable Integer id) {
|
||||||
|
return discussionsService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/discussions")
|
||||||
|
@Operation(summary = "获取全部主题")
|
||||||
|
public List<Discussion> getAllDiscussions() {
|
||||||
|
List<Discussion> allDiscussions = discussionsService.getAllList();
|
||||||
|
logger.info("全部主题==>:{}", allDiscussions);
|
||||||
|
return allDiscussions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.yaoyuan.jiscuss.controller.api;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import com.yaoyuan.jiscuss.exception.BaseException;
|
||||||
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user_api")
|
||||||
|
@Tag(name = "用户接口管理", description = "用户 CRUD API")
|
||||||
|
public class RestUserController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RestUserController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUsersService usersService;
|
||||||
|
|
||||||
|
@PostMapping("/user")
|
||||||
|
@Operation(summary = "新增用户")
|
||||||
|
public User save(@RequestBody User user) {
|
||||||
|
User saveUser = usersService.insert(user);
|
||||||
|
if (saveUser != null) {
|
||||||
|
return saveUser;
|
||||||
|
} else {
|
||||||
|
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/user/{id}")
|
||||||
|
@Operation(summary = "删除用户")
|
||||||
|
public Boolean delete(@PathVariable Integer id) {
|
||||||
|
usersService.remove(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/user/{id}")
|
||||||
|
@Operation(summary = "修改用户")
|
||||||
|
public User update(@RequestBody User user, @PathVariable Integer id) {
|
||||||
|
User updateuser = usersService.update(user, id);
|
||||||
|
if (updateuser != null) {
|
||||||
|
return updateuser;
|
||||||
|
} else {
|
||||||
|
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user/{id}")
|
||||||
|
@Operation(summary = "获取用户")
|
||||||
|
public User getUser(@PathVariable Integer id) {
|
||||||
|
User user = usersService.findOne(id);
|
||||||
|
logger.info("获取用户==>:{}", user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user")
|
||||||
|
@Operation(summary = "获取全部用户")
|
||||||
|
public List<User> getAllUsers() {
|
||||||
|
return usersService.getAllList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request payload for creating a new user account.
|
||||||
|
* Validated at the controller boundary before reaching the service layer.
|
||||||
|
*/
|
||||||
|
public record UserCreateRequest(
|
||||||
|
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Size(min = 3, max = 50, message = "用户名长度必须在 3-50 个字符之间")
|
||||||
|
String username,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw password — will be BCrypt-hashed before persistence.
|
||||||
|
* Never log or return this field.
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
@Size(min = 8, max = 100, message = "密码长度至少 8 个字符")
|
||||||
|
String password,
|
||||||
|
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
@Size(max = 100)
|
||||||
|
String email,
|
||||||
|
|
||||||
|
@Size(max = 50)
|
||||||
|
String realname
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.MappingConstants;
|
||||||
|
import org.mapstruct.ReportingPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapStruct mapper between {@link User} entity and its DTOs.
|
||||||
|
*
|
||||||
|
* <p>The {@code password} field is intentionally never copied into a response and
|
||||||
|
* is left {@code null} when constructing a {@link User} from a create request —
|
||||||
|
* the caller (service layer) is responsible for BCrypt-hashing and setting it.
|
||||||
|
*/
|
||||||
|
@Mapper(
|
||||||
|
componentModel = MappingConstants.ComponentModel.SPRING,
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||||
|
)
|
||||||
|
public interface UserMapper {
|
||||||
|
|
||||||
|
/** Map a {@link User} entity to a safe {@link UserResponse} (no password). */
|
||||||
|
default UserResponse toResponse(User user) {
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new UserResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealname(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getAvatar(),
|
||||||
|
user.getGender(),
|
||||||
|
user.getPhone(),
|
||||||
|
user.getAge(),
|
||||||
|
user.getDiscussionsCount(),
|
||||||
|
user.getCommentsCount(),
|
||||||
|
user.getJoinTime(),
|
||||||
|
user.getLastSeenTime(),
|
||||||
|
user.getLevel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map a create request to a new {@link User} entity. Password must be set by the caller. */
|
||||||
|
default User fromCreateRequest(UserCreateRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(request.username());
|
||||||
|
user.setEmail(request.email());
|
||||||
|
user.setRealname(request.realname());
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only user projection exposed to the frontend / API consumers.
|
||||||
|
* Never exposes the {@code password} field.
|
||||||
|
*/
|
||||||
|
public record UserResponse(
|
||||||
|
Integer id,
|
||||||
|
String username,
|
||||||
|
String realname,
|
||||||
|
String email,
|
||||||
|
String avatar,
|
||||||
|
String gender,
|
||||||
|
String phone,
|
||||||
|
Integer age,
|
||||||
|
Integer discussionsCount,
|
||||||
|
Integer commentsCount,
|
||||||
|
Date joinTime,
|
||||||
|
Date lastSeenTime,
|
||||||
|
Integer level
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "discussion")
|
||||||
|
public class Discussion implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id", unique = true)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 200)
|
||||||
|
@Column(name = "title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Column(name = "comments_count")
|
||||||
|
private Integer commentsCount;
|
||||||
|
|
||||||
|
@Column(name = "participants_count")
|
||||||
|
private Integer participantsCount;
|
||||||
|
|
||||||
|
@Column(name = "number_index")
|
||||||
|
private Integer numberIndex;
|
||||||
|
|
||||||
|
@Column(name = "start_time")
|
||||||
|
private Date startTime;
|
||||||
|
|
||||||
|
@Column(name = "start_user_id")
|
||||||
|
private Integer startUserId;
|
||||||
|
|
||||||
|
@Column(name = "start_post_id")
|
||||||
|
private Integer startPostId;
|
||||||
|
|
||||||
|
@Column(name = "last_time")
|
||||||
|
private Date lastTime;
|
||||||
|
|
||||||
|
@Column(name = "last_user_id")
|
||||||
|
private Integer lastUserId;
|
||||||
|
|
||||||
|
@Column(name = "last_post_id")
|
||||||
|
private Integer lastPostId;
|
||||||
|
|
||||||
|
@Column(name = "last_post_number")
|
||||||
|
private Integer lastPostNumber;
|
||||||
|
|
||||||
|
@Column(name = "is_approved")
|
||||||
|
private Integer isApproved;
|
||||||
|
|
||||||
|
@Column(name = "like_count")
|
||||||
|
private Integer likeCount;
|
||||||
|
|
||||||
|
/** IP is resolved server-side via IpUtils.getClientIp() — never trusted from X-Forwarded-For alone. */
|
||||||
|
@Column(name = "ip_address")
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Column(name = "create_id")
|
||||||
|
private Integer createId;
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "discussiontag")
|
||||||
|
public class DiscussionTag implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "discussion_id")
|
||||||
|
private Integer discussionId;
|
||||||
|
|
||||||
|
@Column(name = "tag_id")
|
||||||
|
private Integer tagId;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "likecollect")
|
||||||
|
public class LikeCollect implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "discussion_id")
|
||||||
|
private Integer discussionId;
|
||||||
|
|
||||||
|
@Column(name = "discussion_name")
|
||||||
|
private String discussionName;
|
||||||
|
|
||||||
|
@Column(name = "tag_id")
|
||||||
|
private Integer tagId;
|
||||||
|
|
||||||
|
@Column(name = "post_id")
|
||||||
|
private Integer postId;
|
||||||
|
|
||||||
|
@Column(name = "post_content")
|
||||||
|
private String postContent;
|
||||||
|
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Column(name = "user_name")
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Column(name = "type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Column(name = "like_type")
|
||||||
|
private String likeType;
|
||||||
|
|
||||||
|
@Column(name = "collect_type")
|
||||||
|
private String collectType;
|
||||||
|
|
||||||
|
@Column(name = "create_id")
|
||||||
|
private Integer createId;
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "post")
|
||||||
|
public class Post implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "discussion_id")
|
||||||
|
private Integer discussionId;
|
||||||
|
|
||||||
|
@Column(name = "number")
|
||||||
|
private Integer number;
|
||||||
|
|
||||||
|
@Column(name = "time")
|
||||||
|
private Date time;
|
||||||
|
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Column(name = "type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Column(name = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Column(name = "parent_id")
|
||||||
|
private Integer parentId;
|
||||||
|
|
||||||
|
@Column(name = "edit_time")
|
||||||
|
private Date editTime;
|
||||||
|
|
||||||
|
@Column(name = "edit_user_id")
|
||||||
|
private Integer editUserId;
|
||||||
|
|
||||||
|
/** IP resolved server-side via IpUtils.getClientIp(). */
|
||||||
|
@Column(name = "ip_address")
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Column(name = "copyright")
|
||||||
|
private String copyright;
|
||||||
|
|
||||||
|
@Column(name = "is_approved")
|
||||||
|
private Integer isApproved;
|
||||||
|
|
||||||
|
@Column(name = "create_id")
|
||||||
|
private Integer createId;
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "setting")
|
||||||
|
public class Setting implements Serializable {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "setting_key")
|
||||||
|
private String settingKey;
|
||||||
|
|
||||||
|
@Column(name = "setting_value")
|
||||||
|
private String settingValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "tag")
|
||||||
|
public class Tag implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Column(name = "name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "color")
|
||||||
|
private String color;
|
||||||
|
|
||||||
|
@Column(name = "icon")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
@Column(name = "position")
|
||||||
|
private Integer position;
|
||||||
|
|
||||||
|
@Column(name = "parent_id")
|
||||||
|
private Integer parentId;
|
||||||
|
|
||||||
|
@Column(name = "discussions_count")
|
||||||
|
private String discussionsCount;
|
||||||
|
|
||||||
|
@Column(name = "last_time")
|
||||||
|
private Date lastTime;
|
||||||
|
|
||||||
|
@Column(name = "last_discussion_id")
|
||||||
|
private Integer lastDiscussionId;
|
||||||
|
|
||||||
|
@Column(name = "create_id")
|
||||||
|
private Integer createId;
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user")
|
||||||
|
public class User implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
|
@Column(name = "username")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Size(max = 50)
|
||||||
|
@Column(name = "realname")
|
||||||
|
private String realname;
|
||||||
|
|
||||||
|
@Email
|
||||||
|
@Size(max = 100)
|
||||||
|
@Column(name = "email")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/** BCrypt-hashed password. Column length must be >= 68 (see V2 migration). */
|
||||||
|
@Column(name = "password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(name = "join_time")
|
||||||
|
private Date joinTime;
|
||||||
|
|
||||||
|
@Column(name = "age")
|
||||||
|
private Integer age;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^(男|女|其他)?$")
|
||||||
|
@Column(name = "gender")
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Column(name = "avatar")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Size(max = 20)
|
||||||
|
@Column(name = "phone")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "discussions_count")
|
||||||
|
private Integer discussionsCount;
|
||||||
|
|
||||||
|
@Column(name = "comments_count")
|
||||||
|
private Integer commentsCount;
|
||||||
|
|
||||||
|
@Column(name = "last_seen_time")
|
||||||
|
private Date lastSeenTime;
|
||||||
|
|
||||||
|
@Column(name = "flag")
|
||||||
|
private Integer flag;
|
||||||
|
|
||||||
|
@Column(name = "level")
|
||||||
|
private Integer level;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.entity
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/7/16 14:55
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserInfo implements UserDetails {
|
||||||
|
|
||||||
|
private Collection<GrantedAuthority> authorities;
|
||||||
|
private String password;
|
||||||
|
private String username;
|
||||||
|
private String phone;
|
||||||
|
private Integer age;
|
||||||
|
private Integer id;
|
||||||
|
private String realname;
|
||||||
|
private String email;
|
||||||
|
private String gender;
|
||||||
|
private Integer level;
|
||||||
|
private Integer flag;
|
||||||
|
|
||||||
|
public UserInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserInfo(Collection<GrantedAuthority> authorities, Integer id, String password, String username, String phone) {
|
||||||
|
this.authorities = authorities;
|
||||||
|
this.id = id;
|
||||||
|
this.password = password;
|
||||||
|
this.username = username;
|
||||||
|
this.phone = phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonLocked() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCredentialsNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "UserInfo{" +
|
||||||
|
"authorities=" + authorities +
|
||||||
|
", password='" + password + '\'' +
|
||||||
|
", username='" + username + '\'' +
|
||||||
|
", id='" + id + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity.custom;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.entity.custom
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/9/9 14:44
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public class DiscussionCustom extends Discussion {
|
||||||
|
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private String realname;
|
||||||
|
|
||||||
|
private String avatarLast;
|
||||||
|
|
||||||
|
private String usernameLast;
|
||||||
|
|
||||||
|
private String realnameLast;
|
||||||
|
|
||||||
|
private String tag;
|
||||||
|
|
||||||
|
private List<TagCustom> tagList;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity.custom;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Post;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.entity.custom
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/9/9 14:44
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public class PostCustom extends Post {
|
||||||
|
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private String realname;
|
||||||
|
|
||||||
|
private String avatarReply;
|
||||||
|
|
||||||
|
private String usernameReply;
|
||||||
|
|
||||||
|
private String realnameReply;
|
||||||
|
|
||||||
|
private List<PostCustom> child;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.yaoyuan.jiscuss.entity.custom;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title:
|
||||||
|
* @Package com.yaoyuan.jiscuss.entity.custom
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/10/21 10:05
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public class TagCustom {
|
||||||
|
|
||||||
|
@Column(name = "name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "color")
|
||||||
|
private String color;
|
||||||
|
|
||||||
|
@Column(name = "icon")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
@Column(name = "discussion_id")
|
||||||
|
private Integer discussionId;
|
||||||
|
|
||||||
|
|
||||||
|
public TagCustom(String name, String color, String icon, String description, Integer discussionId) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.icon = icon;
|
||||||
|
this.description = description;
|
||||||
|
this.discussionId = discussionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.yaoyuan.jiscuss.exception;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常类,继承运行时异常,确保事务正常回滚
|
||||||
|
*
|
||||||
|
* @author NULL
|
||||||
|
* @since 2019-07-16
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
public class BaseException extends RuntimeException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private ResponseCode code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BaseException
|
||||||
|
* ResponseCode code
|
||||||
|
**/
|
||||||
|
public BaseException(ResponseCode code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BaseException
|
||||||
|
* ResponseCode code
|
||||||
|
* Throwable cause
|
||||||
|
**/
|
||||||
|
public BaseException(Throwable cause, ResponseCode code) {
|
||||||
|
super(cause);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.yaoyuan.jiscuss.exception;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||||
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global exception handler.
|
||||||
|
*
|
||||||
|
* <p>Catches {@link BaseException}, validation errors, and unexpected exceptions so they
|
||||||
|
* are returned as structured JSON instead of Spring's default error page.
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
/** Business logic exceptions (already typed with a ResponseCode). */
|
||||||
|
@ExceptionHandler(BaseException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleBaseException(BaseException ex) {
|
||||||
|
log.warn("Business exception: code={}, msg={}", ex.getCode().getCode(), ex.getCode().getMsg());
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.fail(ex.getCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Valid / @Validated failures on @RequestBody. */
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||||
|
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||||
|
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
log.warn("Validation failed: {}", message);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.error(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Validated failures on @RequestParam / @PathVariable. */
|
||||||
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException ex) {
|
||||||
|
String message = ex.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
log.warn("Constraint violation: {}", message);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.error(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security access-denied exceptions re-thrown by method security.
|
||||||
|
* Note: exceptions raised before method invocation are handled by {@code CustomAccessDeniedHandler}.
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleAccessDenied(AccessDeniedException ex) {
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("没有权限访问该资源"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Catch-all for any unhandled exception — never expose stack traces to clients. */
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleGeneral(Exception ex) {
|
||||||
|
log.error("Unexpected error", ex);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ApiResponse.fail(ResponseCode.SERVICE_ERROR));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.yaoyuan.jiscuss.handler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles 403 access-denied responses.
|
||||||
|
*
|
||||||
|
* <p>Behavior:
|
||||||
|
* <ul>
|
||||||
|
* <li>AJAX requests → JSON body with {@code code/msg}</li>
|
||||||
|
* <li>Regular page requests → simple HTML 403 response (no forward to /403,
|
||||||
|
* which previously caused a 404)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||||
|
if (response.isCommitted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Access denied for {} → {}", request.getRequestURI(), accessDeniedException.getMessage());
|
||||||
|
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
if (isAjaxRequest(request)) {
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
try (PrintWriter writer = response.getWriter()) {
|
||||||
|
writer.write("{\"code\":403,\"msg\":\"没有权限\"}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.setContentType(MediaType.TEXT_HTML_VALUE);
|
||||||
|
try (PrintWriter writer = response.getWriter()) {
|
||||||
|
writer.write("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>403 Forbidden</title></head>"
|
||||||
|
+ "<body><h1>403 Forbidden</h1><p>您没有权限访问该资源。</p></body></html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAjaxRequest(HttpServletRequest request) {
|
||||||
|
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.yaoyuan.jiscuss.handler;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RBAC (Role-Based Access Control) permission evaluator.
|
||||||
|
*
|
||||||
|
* <p>Rules:
|
||||||
|
* <ul>
|
||||||
|
* <li>Anonymous / unauthenticated: denied</li>
|
||||||
|
* <li>ROLE_ADMIN: full access (all paths)</li>
|
||||||
|
* <li>ROLE_USER: access to all non-admin paths</li>
|
||||||
|
* <li>Admin-only paths ({@code /admin/**}, {@code /druid/**}, etc.) require ROLE_ADMIN</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Previously this method unconditionally returned {@code true}, giving every user
|
||||||
|
* unrestricted access regardless of role — a critical security vulnerability now fixed.
|
||||||
|
*/
|
||||||
|
@Component("rbacPermission")
|
||||||
|
public class RbacPermission {
|
||||||
|
|
||||||
|
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||||
|
|
||||||
|
/** URL patterns that require ROLE_ADMIN. */
|
||||||
|
private static final String[] ADMIN_ONLY_PATTERNS = {
|
||||||
|
"/admin/**",
|
||||||
|
"/druid/**",
|
||||||
|
"/actuator/**",
|
||||||
|
"/h2-console/**",
|
||||||
|
"/user_api/**",
|
||||||
|
"/other_api/**"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether the authenticated principal is permitted to access the requested URL.
|
||||||
|
*
|
||||||
|
* @param request the current HTTP request
|
||||||
|
* @param authentication the current authentication token
|
||||||
|
* @return {@code true} when access is granted
|
||||||
|
*/
|
||||||
|
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
|
||||||
|
if (authentication == null
|
||||||
|
|| !authentication.isAuthenticated()
|
||||||
|
|| "anonymousUser".equals(authentication.getPrincipal())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||||
|
|
||||||
|
boolean isAdmin = authorities.stream()
|
||||||
|
.anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));
|
||||||
|
|
||||||
|
// Admin-only paths: only ROLE_ADMIN may proceed
|
||||||
|
for (String pattern : ADMIN_ONLY_PATTERNS) {
|
||||||
|
if (PATH_MATCHER.match(pattern, uri)) {
|
||||||
|
return isAdmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other paths are accessible by any authenticated user
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DiscussionsRepository extends JpaRepository<Discussion, Integer> {
|
||||||
|
|
||||||
|
@Query(value = "SELECT * from discussion where id in (\n" +
|
||||||
|
"SELECT discussion_id from discussiontag where tag_id = ?1 ) ", nativeQuery = true)
|
||||||
|
Page<Discussion> findByQuery(String tagId, Pageable pageable);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.DiscussionTag;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DiscussionsTagsRepository extends JpaRepository<DiscussionTag, Integer> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Post;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PostsRepository extends JpaRepository<Post, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Query("from Post where discussionId = :dId")
|
||||||
|
List<Post> findOneBy(@Param("dId") Integer dId);
|
||||||
|
|
||||||
|
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
|
||||||
|
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
|
||||||
|
"from post p \n" +
|
||||||
|
"left join user u on p.user_id = u.id \n" +
|
||||||
|
"left join user u2 on p.create_id = u2.id \n" +
|
||||||
|
"where p.discussion_id = :dId order by p.create_time desc"
|
||||||
|
, nativeQuery = true)
|
||||||
|
List<Map<String, Object>> findPostCustomById(@Param("dId") Integer dId);
|
||||||
|
|
||||||
|
|
||||||
|
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
|
||||||
|
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
|
||||||
|
"from post p \n" +
|
||||||
|
"left join user u on p.user_id = u.id \n" +
|
||||||
|
"left join user u2 on p.create_id = u2.id \n" +
|
||||||
|
"where p.discussion_id = :dId and p.parent_id is null order by p.create_time desc"
|
||||||
|
, nativeQuery = true)
|
||||||
|
List<Map<String, Object>> findAllByDIdAndparentIdNull(@Param("dId") Integer dId);
|
||||||
|
|
||||||
|
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
|
||||||
|
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
|
||||||
|
"from post p \n" +
|
||||||
|
"left join user u on p.user_id = u.id \n" +
|
||||||
|
"left join user u2 on p.create_id = u2.id \n" +
|
||||||
|
"where p.discussion_id = :dId and p.parent_id is not null order by p.create_time desc"
|
||||||
|
, nativeQuery = true)
|
||||||
|
List<Map<String, Object>> findAllByDIdAndparentIdNotNull(@Param("dId") Integer dId);
|
||||||
|
|
||||||
|
|
||||||
|
// @Query("from Post where parentId is null and discussionId = :dId")
|
||||||
|
// List<Post> findAllByDIdAndparentIdNull(@Param("dId")Integer dId);
|
||||||
|
//
|
||||||
|
// @Query("from Post where parentId is not null and discussionId = :dId")
|
||||||
|
// List<Post> findAllByDIdAndparentIdNotNull(@Param("dId")Integer dId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Setting;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SettingsRepository extends JpaRepository<Setting, Integer> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Tag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TagsRepository extends JpaRepository<Tag, Integer> {
|
||||||
|
|
||||||
|
@Query(value = "FROM Tag a, DiscussionTag b WHERE a.id = b.tagId and b.discussionId = :dId")
|
||||||
|
List<Tag> findByDId(@Param("dId") Integer dId);
|
||||||
|
|
||||||
|
@Query("select new com.yaoyuan.jiscuss.entity.custom.TagCustom(" +
|
||||||
|
"u.name,u.color,u.icon,u.description, d.discussionId" +
|
||||||
|
") " +
|
||||||
|
"from Tag u, DiscussionTag d " +
|
||||||
|
"where u.id=d.tagId and d.discussionId in (:discussionIdlist)")
|
||||||
|
List<TagCustom> findByDiscussionIdlistId(@Param(value = "discussionIdlist") List<Integer> discussionIdlist);
|
||||||
|
|
||||||
|
List<Tag> findByParentId(Integer tagId);
|
||||||
|
|
||||||
|
@Query(value = "FROM Tag a WHERE a.parentId is null")
|
||||||
|
List<Tag> findAllIsNull();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yaoyuan.jiscuss.repository;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UsersRepository extends JpaRepository<User, Integer> {
|
||||||
|
/**
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Query("from User where username like %:username%")
|
||||||
|
List<User> getByUsernameIsLike(@Param("username") String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
User getById(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Query("from User where username = :username")
|
||||||
|
User getByUsername(String username);
|
||||||
|
|
||||||
|
// REMOVED: checkByUsernameAndPassword — never compare passwords at the SQL layer.
|
||||||
|
// Password validation must go through BCryptPasswordEncoder.matches() in the service/security layer.
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.yaoyuan.jiscuss.response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified API response wrapper.
|
||||||
|
*
|
||||||
|
* @param <T> payload type
|
||||||
|
*/
|
||||||
|
public record ApiResponse<T>(int code, String msg, T data) {
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> ok(T data) {
|
||||||
|
return new ApiResponse<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> fail(ResponseCode responseCode) {
|
||||||
|
return new ApiResponse<>(responseCode.getCode(), responseCode.getMsg(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message) {
|
||||||
|
return new ApiResponse<>(ResponseCode.SERVICE_ERROR.getCode(), message, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.yaoyuan.jiscuss.response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回状态码
|
||||||
|
*
|
||||||
|
* @author NULL
|
||||||
|
* @date 2019-12-16
|
||||||
|
*/
|
||||||
|
public enum ResponseCode {
|
||||||
|
/**
|
||||||
|
* 成功返回的状态码
|
||||||
|
*/
|
||||||
|
SUCCESS(10000, "success"),
|
||||||
|
/**
|
||||||
|
* 资源不存在的状态码
|
||||||
|
*/
|
||||||
|
RESOURCES_NOT_EXIST(10001, "资源不存在"),
|
||||||
|
/**
|
||||||
|
* 所有无法识别的异常默认的返回状态码
|
||||||
|
*/
|
||||||
|
SERVICE_ERROR(50000, "服务器异常");
|
||||||
|
/**
|
||||||
|
* 状态码
|
||||||
|
*/
|
||||||
|
private int code;
|
||||||
|
/**
|
||||||
|
* 返回信息
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
ResponseCode(int code, String msg) {
|
||||||
|
this.code = code;
|
||||||
|
this.msg = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMsg() {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.yaoyuan.jiscuss.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的公共响应体
|
||||||
|
*
|
||||||
|
* @author NULL
|
||||||
|
* @date 2019-12-16
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ResponseResult implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serialVersionUID
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回状态码
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
/**
|
||||||
|
* 返回信息
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
/**
|
||||||
|
* 数据
|
||||||
|
*/
|
||||||
|
private Object data;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IDiscussionsService {
|
||||||
|
|
||||||
|
List<Discussion> getAllList();
|
||||||
|
|
||||||
|
Discussion insert(Discussion discussion);
|
||||||
|
|
||||||
|
Discussion findOne(Integer id);
|
||||||
|
|
||||||
|
Page<Discussion> queryAllDiscussionsList(Discussion discussion, int pageNumNew, int pageSiz, String tag, String type);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.DiscussionTag;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IDiscussionsTagsService {
|
||||||
|
|
||||||
|
List<DiscussionTag> getAllList();
|
||||||
|
|
||||||
|
DiscussionTag insert(DiscussionTag discussionTag);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Post;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.PostCustom;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IPostsService {
|
||||||
|
List<Post> getAllList();
|
||||||
|
|
||||||
|
Post insert(Post post);
|
||||||
|
|
||||||
|
List<Post> findOneBy(Integer id);
|
||||||
|
|
||||||
|
List<PostCustom> findPostCustomById(Integer id);
|
||||||
|
|
||||||
|
Post findOneByid(Integer parentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Setting;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ISettingsService {
|
||||||
|
|
||||||
|
List<Setting> getAllList();
|
||||||
|
|
||||||
|
Setting insert(Setting setting);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Tag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ITagsService {
|
||||||
|
|
||||||
|
List<Tag> getAllList();
|
||||||
|
|
||||||
|
Tag insert(Tag tag);
|
||||||
|
|
||||||
|
List<Tag> findByDId(Integer id);
|
||||||
|
|
||||||
|
List<TagCustom> findByDiscussionIdlistId(List<Integer> discussionIdLsit);
|
||||||
|
|
||||||
|
List<Tag> findByParentId(String tag);
|
||||||
|
|
||||||
|
List<Tag> getAllListDiscussions();
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IUsersService {
|
||||||
|
|
||||||
|
List<User> getAllList();
|
||||||
|
|
||||||
|
Page<User> queryAllUsersList(int pageNum, int pageSize);
|
||||||
|
|
||||||
|
//Cacheable
|
||||||
|
List<User> getByUsernameIsLike(String name);
|
||||||
|
|
||||||
|
//@Cacheable("myCache")
|
||||||
|
User findOne(Integer id);
|
||||||
|
|
||||||
|
User insert(User user);
|
||||||
|
|
||||||
|
void remove(Integer id);
|
||||||
|
|
||||||
|
void deleteAll();
|
||||||
|
|
||||||
|
User getByUsername(String username);
|
||||||
|
|
||||||
|
User update(User user, Integer id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||||
|
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Example;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DiscussionsServiceImpl implements IDiscussionsService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DiscussionsRepository discussionsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Discussion> getAllList() {
|
||||||
|
return discussionsRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Discussion insert(Discussion discussion) {
|
||||||
|
return discussionsRepository.save(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Discussion findOne(Integer id) {
|
||||||
|
// getReferenceById replaces removed JpaRepository.getOne()
|
||||||
|
return discussionsRepository.getReferenceById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Page<Discussion> queryAllDiscussionsList(Discussion discussion, int pageNum, int pageSize, String tag, String type) {
|
||||||
|
Sort sort = switch (type) {
|
||||||
|
case "hot" -> Sort.by(Sort.Direction.DESC, "likeCount");
|
||||||
|
case "new" -> Sort.by(Sort.Direction.DESC, "startTime");
|
||||||
|
default -> Sort.by(Sort.Direction.DESC, "id");
|
||||||
|
};
|
||||||
|
Pageable pageable = PageRequest.of(pageNum, pageSize, sort);
|
||||||
|
Example<Discussion> example = Example.of(discussion);
|
||||||
|
if (tag != null && !"all".equals(tag)) {
|
||||||
|
return discussionsRepository.findByQuery(tag, pageable);
|
||||||
|
} else {
|
||||||
|
return discussionsRepository.findAll(example, pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.DiscussionTag;
|
||||||
|
import com.yaoyuan.jiscuss.repository.DiscussionsTagsRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DiscussionsTagsServiceImpl implements IDiscussionsTagsService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DiscussionsTagsRepository discussionsTagsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<DiscussionTag> getAllList() {
|
||||||
|
return discussionsTagsRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public DiscussionTag insert(DiscussionTag discussionTag) {
|
||||||
|
return discussionsTagsRepository.save(discussionTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.common.Node;
|
||||||
|
import com.yaoyuan.jiscuss.common.PostCommonUtil;
|
||||||
|
import com.yaoyuan.jiscuss.entity.Post;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.PostCustom;
|
||||||
|
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.IPostsService;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Example;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PostsServiceImpl implements IPostsService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostsRepository postsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Post> getAllList() {
|
||||||
|
return postsRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Post> findOneBy(Integer id) {
|
||||||
|
List<Post> posts = postsRepository.findOneBy(id);
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Post findOneByid(Integer id) {
|
||||||
|
Post post = new Post();
|
||||||
|
post.setId(id);
|
||||||
|
Example<Post> example = Example.of(post);
|
||||||
|
Optional<Post> postRes = postsRepository.findOne(example);
|
||||||
|
return postRes.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List findPostCustomById(Integer id) {
|
||||||
|
//查询id为1且parentId为null的评论
|
||||||
|
List<Map<String, Object>> firstposts = postsRepository.findAllByDIdAndparentIdNull(id);
|
||||||
|
List<PostCustom> firstpostCustomList = PostCommonUtil.getNewPostsObjMap(firstposts);
|
||||||
|
// List<PostCustom> firstpostCustomListNew = PostCommonUtil.getNewPostsObjCustom(firstpostCustomList);
|
||||||
|
|
||||||
|
//查询id为1且parentId不为null的评论
|
||||||
|
List<Map<String, Object>> thenposts = postsRepository.findAllByDIdAndparentIdNotNull(id);
|
||||||
|
List<PostCustom> thenpostCustomList = PostCommonUtil.getNewPostsObjMap(thenposts);
|
||||||
|
|
||||||
|
// List<PostCustom> thenpostCustomListNew = PostCommonUtil.getNewPostsObjCustom(thenpostCustomList);
|
||||||
|
//新建一个Node集合。
|
||||||
|
ArrayList<Node> nodes = new ArrayList<>();
|
||||||
|
//将第一层评论都添加都Node集合中
|
||||||
|
for (PostCustom post : firstpostCustomList) {
|
||||||
|
Node node = new Node();
|
||||||
|
BeanUtils.copyProperties(post, node);
|
||||||
|
nodes.add(node);
|
||||||
|
}
|
||||||
|
//将回复添加到对应的位置
|
||||||
|
List list = Node.addAllNode(nodes, thenpostCustomList);
|
||||||
|
//打印回复链表
|
||||||
|
Node.show(list);
|
||||||
|
|
||||||
|
// List<Map<String, Object>> posts = postsRepository.findPostCustomById(id);
|
||||||
|
// List<PostCustom> postCustomList = PostCommonUtil.getNewPostsObjMap(posts);
|
||||||
|
// List<PostCustom> postCustomListNew = PostCommonUtil.getNewPostsObjCustom(postCustomList);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Post insert(Post post) {
|
||||||
|
return postsRepository.save(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Setting;
|
||||||
|
import com.yaoyuan.jiscuss.repository.SettingsRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.ISettingsService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SettingsServiceImpl implements ISettingsService {
|
||||||
|
@Autowired
|
||||||
|
private SettingsRepository settingsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Setting> getAllList() {
|
||||||
|
return settingsRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Setting insert(Setting setting) {
|
||||||
|
return settingsRepository.save(setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.Tag;
|
||||||
|
import com.yaoyuan.jiscuss.entity.custom.TagCustom;
|
||||||
|
import com.yaoyuan.jiscuss.repository.TagsRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.ITagsService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TagsServiceImpl implements ITagsService {
|
||||||
|
@Autowired
|
||||||
|
private TagsRepository tagsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Tag> getAllList() {
|
||||||
|
return tagsRepository.findAllIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Tag> getAllListDiscussions() {
|
||||||
|
return tagsRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Tag insert(Tag tag) {
|
||||||
|
return tagsRepository.save(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Tag> findByDId(Integer id) {
|
||||||
|
return tagsRepository.findByDId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<TagCustom> findByDiscussionIdlistId(List<Integer> discussionIdLsit) {
|
||||||
|
return tagsRepository.findByDiscussionIdlistId(discussionIdLsit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<Tag> findByParentId(String tag) {
|
||||||
|
int tagId = Integer.parseInt(tag);
|
||||||
|
return tagsRepository.findByParentId(tagId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security {@code UserDetailsService} implementation.
|
||||||
|
*
|
||||||
|
* <p>Role assignment: users with {@code level >= 1} receive {@code ROLE_ADMIN};
|
||||||
|
* all others receive {@code ROLE_USER}.
|
||||||
|
*
|
||||||
|
* <p>Password note: passwords stored in the database MUST be BCrypt-hashed.
|
||||||
|
* See Flyway migration {@code V2__security_updates.sql} for the one-time migration
|
||||||
|
* that converts legacy plaintext passwords to BCrypt hashes and expands the column.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class UserDetailServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUsersService userInfoService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserInfo loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
User userInfo = userInfoService.getByUsername(username);
|
||||||
|
if (userInfo == null) {
|
||||||
|
throw new UsernameNotFoundException("用户不存在: " + username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign role based on user level: level >= 1 → ADMIN, else → USER
|
||||||
|
String role = (userInfo.getLevel() != null && userInfo.getLevel() >= 1) ? "ADMIN" : "USER";
|
||||||
|
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
|
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
||||||
|
|
||||||
|
return new UserInfo(
|
||||||
|
authorities,
|
||||||
|
userInfo.getId(),
|
||||||
|
userInfo.getPassword(), // Must be a BCrypt hash (see V2 migration)
|
||||||
|
userInfo.getUsername(),
|
||||||
|
userInfo.getPhone()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package com.yaoyuan.jiscuss.service.impl;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||||
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.CachePut;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.cache.annotation.Caching;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UsersServiceImpl implements IUsersService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UsersRepository usersRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全部用户
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Cacheable(value = "userList")
|
||||||
|
@Override
|
||||||
|
public List<User> getAllList() {
|
||||||
|
return usersRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @param pageNum
|
||||||
|
* @param pageSize
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Page<User> queryAllUsersList(int pageNum, int pageSize) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "id"));
|
||||||
|
return usersRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称模糊查询
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public List<User> getByUsernameIsLike(String name) {
|
||||||
|
return usersRepository.getByUsernameIsLike(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Cacheable(value = "user", key = "#id")
|
||||||
|
@Override
|
||||||
|
public User findOne(Integer id) {
|
||||||
|
return usersRepository.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@CachePut(value = "user", key = "#user.id")
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
|
@Override
|
||||||
|
public User insert(User user) {
|
||||||
|
return usersRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新修改
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@CachePut(value = "user", key = "#user.id")
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
|
@Override
|
||||||
|
public User update(User user, Integer id) {
|
||||||
|
return usersRepository.saveAndFlush(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Caching(evict = {
|
||||||
|
@CacheEvict(value = "user", key = "#id"),
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
|
})
|
||||||
|
@Override
|
||||||
|
public void remove(Integer id) {
|
||||||
|
usersRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除所有
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void deleteAll() {
|
||||||
|
usersRepository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称查询
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public User getByUsername(String username) {
|
||||||
|
return usersRepository.getByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.yaoyuan.jiscuss.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yaoyuan2.chu
|
||||||
|
* @Title: 去除内容页代码里的HTML标签
|
||||||
|
* @Package com.yaoyuan.jiscuss.util
|
||||||
|
* @Description:
|
||||||
|
* @date 2020/8/31 20:52
|
||||||
|
*/
|
||||||
|
public class DelTagsUtil {
|
||||||
|
/**
|
||||||
|
* 去除html代码中含有的标签
|
||||||
|
*
|
||||||
|
* @param htmlStr
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String delHtmlTags(String htmlStr) {
|
||||||
|
//定义script的正则表达式,去除js可以防止注入
|
||||||
|
String scriptRegex = "<script[^>]*?>[\\s\\S]*?<\\/script>";
|
||||||
|
//定义style的正则表达式,去除style样式,防止css代码过多时只截取到css样式代码
|
||||||
|
String styleRegex = "<style[^>]*?>[\\s\\S]*?<\\/style>";
|
||||||
|
//定义HTML标签的正则表达式,去除标签,只提取文字内容
|
||||||
|
String htmlRegex = "<[^>]+>";
|
||||||
|
//定义空格,回车,换行符,制表符
|
||||||
|
String spaceRegex = "\\s*|\t|\r|\n";
|
||||||
|
|
||||||
|
// 过滤script标签
|
||||||
|
htmlStr = htmlStr.replaceAll(scriptRegex, "");
|
||||||
|
// 过滤style标签
|
||||||
|
htmlStr = htmlStr.replaceAll(styleRegex, "");
|
||||||
|
// 过滤html标签
|
||||||
|
htmlStr = htmlStr.replaceAll(htmlRegex, "");
|
||||||
|
// 过滤空格等
|
||||||
|
htmlStr = htmlStr.replaceAll(spaceRegex, "");
|
||||||
|
return htmlStr.trim(); // 返回文本字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取HTML代码里的内容
|
||||||
|
*
|
||||||
|
* @param htmlStr
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getTextFromHtml(String htmlStr) {
|
||||||
|
//去除html标签
|
||||||
|
htmlStr = delHtmlTags(htmlStr);
|
||||||
|
//去除空格" "
|
||||||
|
htmlStr = htmlStr.replaceAll(" ", "");
|
||||||
|
return htmlStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.yaoyuan.jiscuss.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secure IP address extraction utility.
|
||||||
|
*
|
||||||
|
* <p>Defends against IP spoofing via forged {@code X-Forwarded-For} headers by:
|
||||||
|
* <ul>
|
||||||
|
* <li>Treating the first non-internal IP in the proxy chain as the client IP</li>
|
||||||
|
* <li>Falling back to {@code HttpServletRequest.getRemoteAddr()} when no trusted
|
||||||
|
* proxy header is present</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><strong>Important:</strong> Only trust forwarded headers when the application
|
||||||
|
* runs behind a known, trusted reverse proxy. Consider configuring
|
||||||
|
* {@code server.forward-headers-strategy=NATIVE} or {@code FRAMEWORK} in
|
||||||
|
* {@code application.yml} to let Spring handle this automatically.
|
||||||
|
*/
|
||||||
|
public final class IpUtils {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IpUtils.class);
|
||||||
|
|
||||||
|
private static final String UNKNOWN = "unknown";
|
||||||
|
private static final int MAX_IP_LENGTH = 15; // IPv4
|
||||||
|
|
||||||
|
private IpUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the best-effort client IP address from the request.
|
||||||
|
*
|
||||||
|
* @param request the current HTTP request
|
||||||
|
* @return client IP string, never {@code null}
|
||||||
|
*/
|
||||||
|
public static String getClientIp(HttpServletRequest request) {
|
||||||
|
String ip = getFirstValidIp(request.getHeader("X-Forwarded-For"));
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("X-Real-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("Proxy-Client-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
return ip != null ? sanitize(ip) : UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the first entry from a comma-separated {@code X-Forwarded-For} chain.
|
||||||
|
* Returns {@code null} if the header is absent/empty/unknown.
|
||||||
|
*/
|
||||||
|
private static String getFirstValidIp(String header) {
|
||||||
|
if (header == null || header.isBlank() || UNKNOWN.equalsIgnoreCase(header)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// X-Forwarded-For: client, proxy1, proxy2 — take the leftmost (client) entry
|
||||||
|
String[] parts = header.split(",");
|
||||||
|
return parts[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValid(String ip) {
|
||||||
|
return ip != null && !ip.isBlank() && !UNKNOWN.equalsIgnoreCase(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Truncate excessively long values to prevent log injection / storage attacks. */
|
||||||
|
private static String sanitize(String ip) {
|
||||||
|
// Strip any characters that are not valid in IPv4/IPv6 addresses
|
||||||
|
String cleaned = ip.replaceAll("[^0-9a-fA-F:.\\[\\]]", "");
|
||||||
|
if (cleaned.length() > 45) { // max IPv6 length
|
||||||
|
log.warn("Suspiciously long IP value truncated: {}", ip);
|
||||||
|
return cleaned.substring(0, 45);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>${title}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
jiscss::${message}
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>权限(user:add)</td>
|
|
||||||
<td>
|
|
||||||
<#authPermissions name="user:add">
|
|
||||||
有
|
|
||||||
</#authPermissions>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>权限(user:del)</td><td>
|
|
||||||
<#authPermissions name="user:del">
|
|
||||||
有
|
|
||||||
</#authPermissions>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>权限(user:clear)</td><td>
|
|
||||||
<#authPermissions name="user:clear">
|
|
||||||
有
|
|
||||||
</#authPermissions>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>角色(admin1)</td><td>
|
|
||||||
<#authRoles name="admin1">
|
|
||||||
有
|
|
||||||
</#authRoles>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>角色(admin2)</td><td>
|
|
||||||
<#authRoles name="admin2">
|
|
||||||
有
|
|
||||||
</#authRoles>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>角色(admin3)</td><td>
|
|
||||||
<#authRoles name="admin3">
|
|
||||||
有
|
|
||||||
</#authRoles>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
server.port: 80
|
|
||||||
|
|
||||||
solon.app:
|
|
||||||
name: jiscuss
|
|
||||||
group: jiscuss
|
|
||||||
|
|
||||||
# 可选: *,disk,cpu,jvm,memory,os,qps
|
|
||||||
solon.health.detector: "*"
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# H2 / development profile.
|
||||||
|
# Activate with: --spring.profiles.active=h2
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
# Flyway owns DDL. Disable Hibernate validation in H2 because H2 reports
|
||||||
|
# some MySQL-compatible legacy column types differently from MySQL/Hibernate.
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
# H2 uses the same SQL dialect as MySQL in MySQL compatibility mode
|
||||||
|
|
||||||
|
# H2 console — enabled in dev ONLY.
|
||||||
|
# Access restricted to ROLE_ADMIN via Spring Security (see WebSecurityConfig).
|
||||||
|
# web-allow-others=false prevents remote access; rely on security for protection-in-depth.
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
path: /h2-console
|
||||||
|
# Default OFF — enable explicitly at startup: --spring.h2.console.enabled=true
|
||||||
|
enabled: false
|
||||||
|
settings:
|
||||||
|
web-allow-others: false
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
# H2 2.x reserves USER/VALUE; keep them usable for the legacy schema names.
|
||||||
|
url: jdbc:h2:~/testjiscuss;MODE=MySQL;NON_KEYWORDS=VALUE,USER
|
||||||
|
username: sa
|
||||||
|
password:
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
min-idle: 1
|
||||||
|
initial-size: 2
|
||||||
|
max-active: 5
|
||||||
|
max-wait: 3000
|
||||||
|
validation-query: SELECT 1
|
||||||
|
filter:
|
||||||
|
stat:
|
||||||
|
enabled: true
|
||||||
|
db-type: h2
|
||||||
|
log-slow-sql: true
|
||||||
|
slow-sql-millis: 1000
|
||||||
|
web-stat-filter:
|
||||||
|
enabled: true
|
||||||
|
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
|
||||||
|
stat-view-servlet:
|
||||||
|
enabled: true
|
||||||
|
url-pattern: /druid/*
|
||||||
|
# Druid UI gated by Spring Security ROLE_ADMIN; built-in basic auth disabled
|
||||||
|
reset-enable: false
|
||||||
|
allow: 127.0.0.1
|
||||||
|
|
||||||
|
# Flyway: run V1 + V2 migrations on H2 in-memory DB on startup
|
||||||
|
flyway:
|
||||||
|
locations: classpath:db/migration
|
||||||
|
url: ${spring.datasource.url}
|
||||||
|
user: ${spring.datasource.username}
|
||||||
|
password: ${spring.datasource.password}
|
||||||
|
|
||||||
|
# Override default port for dev profile
|
||||||
|
server:
|
||||||
|
port: 80
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# MySQL / production profile.
|
||||||
|
# Activate with: --spring.profiles.active=mysql
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
database: MYSQL
|
||||||
|
show-sql: false
|
||||||
|
hibernate:
|
||||||
|
# Flyway owns all DDL; Hibernate only validates
|
||||||
|
ddl-auto: validate
|
||||||
|
|
||||||
|
# H2 console is OFF in production
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
# Updated URL for MySQL 8: removed deprecated useSSL=false; use requireSSL for prod
|
||||||
|
url: jdbc:mysql://127.0.0.1:3306/jiscuss?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
|
||||||
|
username: root
|
||||||
|
password: changeme_production
|
||||||
|
# Updated driver class name for MySQL Connector/J 8+
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
min-idle: 5
|
||||||
|
initial-size: 10
|
||||||
|
max-active: 50
|
||||||
|
max-wait: 5000
|
||||||
|
validation-query: SELECT 1
|
||||||
|
filter:
|
||||||
|
stat:
|
||||||
|
enabled: true
|
||||||
|
db-type: mysql
|
||||||
|
log-slow-sql: true
|
||||||
|
slow-sql-millis: 2000
|
||||||
|
web-stat-filter:
|
||||||
|
enabled: true
|
||||||
|
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
|
||||||
|
stat-view-servlet:
|
||||||
|
enabled: true
|
||||||
|
url-pattern: /druid/*
|
||||||
|
reset-enable: false
|
||||||
|
# Druid UI is gated by Spring Security ROLE_ADMIN; built-in basic auth disabled
|
||||||
|
allow: 127.0.0.1
|
||||||
|
|
||||||
|
# Flyway for MySQL
|
||||||
|
flyway:
|
||||||
|
locations: classpath:db/migration
|
||||||
|
url: ${spring.datasource.url}
|
||||||
|
user: ${spring.datasource.username}
|
||||||
|
password: ${spring.datasource.password}
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 80
|
||||||
|
forward-headers-strategy: FRAMEWORK
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Activate h2 (dev) profile by default.
|
||||||
|
# Override via --spring.profiles.active=mysql for production.
|
||||||
|
spring.profiles.active=h2
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Default configuration — shared across all profiles.
|
||||||
|
# Profile-specific overrides live in application-h2.yml and application-mysql.yml.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
spring:
|
||||||
|
# Ehcache 3 via JCache (JSR-107) — replaces Ehcache 2 (net.sf.ehcache)
|
||||||
|
cache:
|
||||||
|
type: jcache
|
||||||
|
jcache:
|
||||||
|
config: classpath:ehcache3.xml
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
generate-ddl: false
|
||||||
|
show-sql: false
|
||||||
|
hibernate:
|
||||||
|
# ddl-auto=validate lets Hibernate check that the schema matches entities
|
||||||
|
# without modifying anything; Flyway owns all DDL.
|
||||||
|
ddl-auto: validate
|
||||||
|
|
||||||
|
freemarker:
|
||||||
|
suffix: .ftl
|
||||||
|
content-type: text/html
|
||||||
|
charset: UTF-8
|
||||||
|
cache: false
|
||||||
|
template-loader-path:
|
||||||
|
- classpath:/templates
|
||||||
|
settings:
|
||||||
|
classic_compatible: true
|
||||||
|
|
||||||
|
mvc:
|
||||||
|
static-path-pattern: /static/**
|
||||||
|
|
||||||
|
# Disable legacy spring.datasource.schema / spring.datasource.data initialisation;
|
||||||
|
# Flyway handles all schema setup in db/migration/.
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
mode: never
|
||||||
|
|
||||||
|
# Expose Flyway information via Actuator (read-only)
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
baseline-on-migrate: true # safe for existing databases without flyway_schema_history
|
||||||
|
|
||||||
|
# Actuator: expose health + info publicly; restrict all other endpoints to ROLE_ADMIN
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info,metrics,flyway
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: when-authorized
|
||||||
|
|
||||||
|
# Forward header strategy — set to NATIVE when running behind a trusted reverse proxy
|
||||||
|
# so Spring uses X-Forwarded-* headers for request URL/IP resolution.
|
||||||
|
server:
|
||||||
|
port: 80
|
||||||
|
forward-headers-strategy: NONE # change to NATIVE behind a trusted proxy
|
||||||
|
|
||||||
|
# SpringDoc OpenAPI — UI at /swagger-ui/index.html
|
||||||
|
springdoc:
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
| |__| | \__ \ (__| |_| \__ \__ \
|
| |__| | \__ \ (__| |_| \__ \__ \
|
||||||
\____/|_|___/\___|\__,_|___/___/
|
\____/|_|___/\___|\__,_|___/___/
|
||||||
=========================================================
|
=========================================================
|
||||||
:: Solon :: (v${solon.version})
|
:: Spring Boot :: (v${spring-boot.version})
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- V1: Initial schema — mirrors the existing schema.sql / data.sql.
|
||||||
|
-- Managed by Flyway; do NOT modify after first deployment.
|
||||||
|
-- Subsequent schema changes belong in V2, V3, ...
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- User table
|
||||||
|
CREATE TABLE IF NOT EXISTS user
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50),
|
||||||
|
realname VARCHAR(50),
|
||||||
|
email VARCHAR(100),
|
||||||
|
password VARCHAR(255), -- must hold BCrypt hashes (>=68 chars); V2 migrates existing data
|
||||||
|
join_time DATETIME,
|
||||||
|
age INTEGER,
|
||||||
|
avatar TEXT,
|
||||||
|
gender CHAR(10),
|
||||||
|
phone VARCHAR(20),
|
||||||
|
discussions_count INTEGER,
|
||||||
|
comments_count INTEGER,
|
||||||
|
last_seen_time DATETIME,
|
||||||
|
flag INTEGER,
|
||||||
|
level INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Discussion (thread) table
|
||||||
|
CREATE TABLE IF NOT EXISTS discussion
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
title VARCHAR(200),
|
||||||
|
content TEXT,
|
||||||
|
comments_count INTEGER,
|
||||||
|
participants_count INTEGER,
|
||||||
|
number_index INTEGER,
|
||||||
|
start_time DATETIME,
|
||||||
|
start_user_id INTEGER,
|
||||||
|
start_post_id INTEGER,
|
||||||
|
last_time DATETIME,
|
||||||
|
last_user_id INTEGER,
|
||||||
|
last_post_id INTEGER,
|
||||||
|
last_post_number INTEGER,
|
||||||
|
is_approved INTEGER,
|
||||||
|
like_count INTEGER,
|
||||||
|
ip_address VARCHAR(200),
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Discussion ↔ Tag association table
|
||||||
|
CREATE TABLE IF NOT EXISTS discussiontag
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Post (reply/comment) table
|
||||||
|
CREATE TABLE IF NOT EXISTS post
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER,
|
||||||
|
number INTEGER,
|
||||||
|
time DATETIME,
|
||||||
|
user_id INTEGER,
|
||||||
|
type VARCHAR(20),
|
||||||
|
content TEXT,
|
||||||
|
edit_time DATETIME,
|
||||||
|
edit_user_id INTEGER,
|
||||||
|
parent_id INTEGER,
|
||||||
|
ip_address VARCHAR(200),
|
||||||
|
copyright VARCHAR(200),
|
||||||
|
is_approved INTEGER,
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Settings table
|
||||||
|
CREATE TABLE IF NOT EXISTS setting
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
setting_key VARCHAR(50),
|
||||||
|
setting_value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tag table
|
||||||
|
CREATE TABLE IF NOT EXISTS tag
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(200),
|
||||||
|
description VARCHAR(200),
|
||||||
|
color VARCHAR(200),
|
||||||
|
icon VARCHAR(200),
|
||||||
|
position INTEGER,
|
||||||
|
parent_id INTEGER,
|
||||||
|
discussions_count TEXT,
|
||||||
|
last_time DATETIME,
|
||||||
|
last_discussion_id INTEGER,
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Like / Collect table
|
||||||
|
CREATE TABLE IF NOT EXISTS likecollect
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER,
|
||||||
|
discussion_name VARCHAR(200),
|
||||||
|
tag_id INTEGER,
|
||||||
|
post_id INTEGER,
|
||||||
|
post_content TEXT,
|
||||||
|
user_id INTEGER,
|
||||||
|
user_name VARCHAR(200),
|
||||||
|
type VARCHAR(20),
|
||||||
|
like_type VARCHAR(20),
|
||||||
|
collect_type VARCHAR(20),
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ─── Seed data (dev/test) ────────────────────────────────────────────────────
|
||||||
|
-- Passwords below are BCrypt hashes of '123456'.
|
||||||
|
-- Generated with: BCrypt.encode("123456")
|
||||||
|
-- DO NOT use these credentials in production.
|
||||||
|
INSERT INTO user (id, username, realname, email, password, join_time, age, avatar, gender, phone,
|
||||||
|
discussions_count, comments_count, last_seen_time, flag, level)
|
||||||
|
VALUES (1, 'admin', '管理员', 'admin@jiscuss.local',
|
||||||
|
'$2a$10$7EqJtq98hPqEX7fNZaFWoOziYXMEIUBm..wTkPE3bsUJn4Lm7oHVC',
|
||||||
|
'2019-09-09 00:00:00', 31, '', '男', '13800000001', 0, 0, '2019-09-09 00:00:00', 1, 1);
|
||||||
|
|
||||||
|
INSERT INTO user (id, username, realname, email, password, join_time, age, avatar, gender, phone,
|
||||||
|
discussions_count, comments_count, last_seen_time, flag, level)
|
||||||
|
VALUES (2, 'test', '测试用户', 'test@jiscuss.local',
|
||||||
|
'$2a$10$7EqJtq98hPqEX7fNZaFWoOziYXMEIUBm..wTkPE3bsUJn4Lm7oHVC',
|
||||||
|
'2019-09-09 00:00:00', 31, '', '男', '13800000002', 0, 0, '2019-09-09 00:00:00', 1, 0);
|
||||||
|
|
||||||
|
INSERT INTO discussion (id, title, content, start_time, start_user_id, last_time, last_user_id, create_id, create_time)
|
||||||
|
VALUES (1, '测试主题1', '测试内容1', '2020-09-19 00:00:00', 1, '2020-09-29 00:00:00', 2, 1, '2020-09-09 00:00:00');
|
||||||
|
|
||||||
|
INSERT INTO tag (id, name, icon, position)
|
||||||
|
VALUES (1, '测试标签1', 'edit', 1);
|
||||||
|
|
||||||
|
INSERT INTO post (id, discussion_id, number, time, user_id, content, create_id, create_time)
|
||||||
|
VALUES (1, 1, 1, '2020-02-09 00:00:00', 1, '评论内容222', 1, '2020-08-09 00:00:00');
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- V2: Security hardening — password column expansion + BCrypt migration.
|
||||||
|
--
|
||||||
|
-- BACKGROUND:
|
||||||
|
-- The original schema used VARCHAR(20) for the password column, which is too
|
||||||
|
-- short to store BCrypt hashes (60–68 characters). This migration:
|
||||||
|
-- 1. Expands the column to VARCHAR(255).
|
||||||
|
-- 2. Provides UPDATE statements to replace any remaining plaintext passwords
|
||||||
|
-- with BCrypt hashes (useful when applying this migration to an existing
|
||||||
|
-- database that has not yet been migrated).
|
||||||
|
--
|
||||||
|
-- NOTE: The example BCrypt hash below encodes the string '123456'.
|
||||||
|
-- For a real production migration, generate individual hashes per user
|
||||||
|
-- using a one-off script and run this migration during a maintenance window.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Step 1: Expand the password column so it can hold BCrypt hashes
|
||||||
|
ALTER TABLE user
|
||||||
|
MODIFY COLUMN password VARCHAR(255);
|
||||||
|
|
||||||
|
-- Step 2: Identify and log any users whose passwords look like plaintext
|
||||||
|
-- (BCrypt hashes always start with '$2a$' or '$2b$')
|
||||||
|
-- Run this SELECT manually before Step 3 to audit affected rows:
|
||||||
|
--
|
||||||
|
-- SELECT id, username FROM user WHERE password NOT LIKE '$2%';
|
||||||
|
|
||||||
|
-- Step 3: Replace all non-BCrypt passwords with the hash of a known reset value
|
||||||
|
-- ('changeme123' hashed below) and force a password reset via application logic.
|
||||||
|
-- Replace the hash value with one generated by your BCryptPasswordEncoder.
|
||||||
|
--
|
||||||
|
-- UPDATE user
|
||||||
|
-- SET password = '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
|
-- WHERE password NOT LIKE '$2%';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Reset development seed account passwords to a known BCrypt value.
|
||||||
|
-- Plaintext password for both seeded accounts: 123456
|
||||||
|
-- Do not use these credentials in production.
|
||||||
|
|
||||||
|
UPDATE user
|
||||||
|
SET password = '$2a$10$R4prDK/CfI0Hjrcz5dS8cOHClOIHIHlk8SoGmVY1iGWYSPxBGo7nm'
|
||||||
|
WHERE username IN ('admin', 'test');
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!-- ehcache配置 -->
|
||||||
|
<ehcache
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
|
||||||
|
updateCheck="false">
|
||||||
|
<!--缓存路径,用户目录下的base_ehcache目录-->
|
||||||
|
<diskStore path="user.home/base_ehcache"/>
|
||||||
|
|
||||||
|
<defaultCache
|
||||||
|
maxElementsInMemory="20000"
|
||||||
|
eternal="false"
|
||||||
|
timeToIdleSeconds="120"
|
||||||
|
timeToLiveSeconds="120"
|
||||||
|
overflowToDisk="true"
|
||||||
|
maxElementsOnDisk="10000000"
|
||||||
|
diskPersistent="false"
|
||||||
|
diskExpiryThreadIntervalSeconds="120"
|
||||||
|
memoryStoreEvictionPolicy="LRU"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
缓存文件名:user,同样的可以配置多个缓存
|
||||||
|
maxElementsInMemory:内存中最多存储
|
||||||
|
eternal:外部存储
|
||||||
|
overflowToDisk:超出缓存到磁盘
|
||||||
|
diskPersistent:磁盘持久化
|
||||||
|
timeToLiveSeconds:缓存时间
|
||||||
|
diskExpiryThreadIntervalSeconds:磁盘过期时间
|
||||||
|
-->
|
||||||
|
<cache name="user"
|
||||||
|
maxElementsInMemory="20000"
|
||||||
|
eternal="true"
|
||||||
|
overflowToDisk="true"
|
||||||
|
diskPersistent="false"
|
||||||
|
timeToLiveSeconds="0"
|
||||||
|
diskExpiryThreadIntervalSeconds="120"/>
|
||||||
|
|
||||||
|
</ehcache>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Ehcache 3 configuration (JCache / JSR-107 format).
|
||||||
|
Referenced by: spring.cache.jcache.config=classpath:ehcache3.xml
|
||||||
|
|
||||||
|
Two caches:
|
||||||
|
"user" — single User entities keyed by ID (e.g. @Cacheable(value="user", key="#id"))
|
||||||
|
"userList" — full user list (e.g. @Cacheable(value="userList"))
|
||||||
|
|
||||||
|
TTI (time-to-idle) evicts entries not accessed within the specified window,
|
||||||
|
preventing stale data accumulation and cache stampede from eternal entries.
|
||||||
|
-->
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.ehcache.org/v3"
|
||||||
|
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
|
||||||
|
|
||||||
|
<!-- Single user entity cache: up to 5 000 entries, evict if idle for 5 minutes -->
|
||||||
|
<cache alias="user">
|
||||||
|
<expiry>
|
||||||
|
<tti unit="minutes">5</tti>
|
||||||
|
</expiry>
|
||||||
|
<heap unit="entries">5000</heap>
|
||||||
|
</cache>
|
||||||
|
|
||||||
|
<!-- Full user-list cache: small number of keys, evict after 2 minutes (reduces stampede risk) -->
|
||||||
|
<cache alias="userList">
|
||||||
|
<expiry>
|
||||||
|
<ttl unit="minutes">2</ttl>
|
||||||
|
</expiry>
|
||||||
|
<heap unit="entries">10</heap>
|
||||||
|
</cache>
|
||||||
|
|
||||||
|
</config>
|
||||||
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This is the JRebel configuration file. It maps the running application to your IDE workspace, enabling JRebel reloading for this project.
|
||||||
|
Refer to https://manuals.zeroturnaround.com/jrebel/standalone/config.html for more information.
|
||||||
|
-->
|
||||||
|
<application generated-by="eclipse" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.zeroturnaround.com"
|
||||||
|
xsi:schemaLocation="http://www.zeroturnaround.com http://update.zeroturnaround.com/jrebel/rebel-2_1.xsd">
|
||||||
|
|
||||||
|
<classpath>
|
||||||
|
<dir name="E:/chuyaoyuan/jiscuss/workspace/jiccuss/target/classes">
|
||||||
|
</dir>
|
||||||
|
</classpath>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
function massage(msg, type, title) {
|
||||||
|
layx.msg(msg, {dialogIcon: type});
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
let discussionsId = null;
|
||||||
|
|
||||||
|
function setDiscussionsId(id) {
|
||||||
|
console.log('setDiscussionsId:' + id);
|
||||||
|
discussionsId = id; // id
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#addPost").click(function () {
|
||||||
|
console.log("点击 addPost");
|
||||||
|
console.log(username);
|
||||||
|
console.log(discussionsId);
|
||||||
|
let postId = $("#postId").val();
|
||||||
|
console.log(postId);
|
||||||
|
if (username && username != null) {
|
||||||
|
var token = $("meta[name='_csrf']").attr("content");
|
||||||
|
var header = $("meta[name='_csrf_header']").attr("content");
|
||||||
|
console.warn(header)
|
||||||
|
console.warn(token)
|
||||||
|
var content = $("#postContent").val();
|
||||||
|
console.log(content);
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/newPost",
|
||||||
|
data: JSON.stringify({
|
||||||
|
content: content,
|
||||||
|
discussionId: discussionsId,
|
||||||
|
parentId: postId
|
||||||
|
}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
beforeSend: function (request) {
|
||||||
|
request.setRequestHeader(header, token); // 添加 CSRF Token
|
||||||
|
},
|
||||||
|
dataType: "JSON",
|
||||||
|
success: function (data) {
|
||||||
|
console.log(data);
|
||||||
|
if (data.flag) {
|
||||||
|
massage(content + ',添加成功!', 'success', '');
|
||||||
|
location.reload();
|
||||||
|
massage('评论添加成功!', 'success', '');
|
||||||
|
} else {
|
||||||
|
massage(data.msg, 'error', '');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
massage('您未登录,请先登录!', 'error', '');
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回复
|
||||||
|
function replyThis(username, postId) {
|
||||||
|
console.warn(username)
|
||||||
|
console.warn(postId)
|
||||||
|
$("#postId").val(postId);
|
||||||
|
var content = $("#postContent").val();
|
||||||
|
if (content) content += '\n';
|
||||||
|
$("#postContent").val(content + "@" + username + " ");
|
||||||
|
$("#postContent").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
let username = null;
|
||||||
|
|
||||||
|
function setusername(name) {
|
||||||
|
console.log('已经登陆:' + name);
|
||||||
|
username = name; // name
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
let pageNum = null;
|
||||||
|
let pageAll = null;
|
||||||
|
let pageUrl = '/';
|
||||||
|
|
||||||
|
|
||||||
|
$("#newdiscussions").click(function () {
|
||||||
|
|
||||||
|
var token = $("meta[name='_csrf']").attr("content");
|
||||||
|
var header = $("meta[name='_csrf_header']").attr("content");
|
||||||
|
console.warn(header)
|
||||||
|
console.warn(token)
|
||||||
|
var title = $("#discussionstitle").val();
|
||||||
|
var tag = $("#selectTag").val();
|
||||||
|
// $("#discussionscontent").val();
|
||||||
|
console.log(tinyMCE.editors[0].getContent());
|
||||||
|
var content = tinyMCE.editors[0].getContent();
|
||||||
|
console.log(title);
|
||||||
|
console.log(content);
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/newdiscussions",
|
||||||
|
data: JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
tag: Array.isArray(tag) ? tag.join(",") : tag
|
||||||
|
}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
beforeSend: function (request) {
|
||||||
|
request.setRequestHeader(header, token); // 添加 CSRF Token
|
||||||
|
},
|
||||||
|
dataType: "JSON",
|
||||||
|
success: function (data) {
|
||||||
|
console.log(data);
|
||||||
|
if (data.flag) {
|
||||||
|
massage(title + ',添加成功!','success', '');
|
||||||
|
$('.ui.modal.createNewDiccuss').modal('hide');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
massage(data.msg,'error', '');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#cancelnewtags").click(function () {
|
||||||
|
$('#createNewtagsDiv').hide();
|
||||||
|
$('#tagDescriptionDiv').hide();
|
||||||
|
$('#parentTagDiv').hide();
|
||||||
|
$('#colorIconDiv').hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#newtags").click(function () {
|
||||||
|
debugger
|
||||||
|
$('#createNewtagsDiv').show();
|
||||||
|
$('#tagDescriptionDiv').show();
|
||||||
|
$('#colorIconDiv').show();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$("#commitnewtags").click(function () {
|
||||||
|
|
||||||
|
var header = $("meta[name='_csrf_header']").attr("content");
|
||||||
|
var token = $("meta[name='_csrf']").attr("content");
|
||||||
|
|
||||||
|
var name = $("#tagsname").val();
|
||||||
|
|
||||||
|
var tagColor = $("#tagColor").val();
|
||||||
|
var tagIcon = $("#tagIcon").val();
|
||||||
|
var parentTag = $("#parentTag").val();
|
||||||
|
|
||||||
|
let coloricon = tagColor + ',' + tagIcon
|
||||||
|
var tagdescription = $("#tagdescription").val();
|
||||||
|
|
||||||
|
console.log(tagColor);
|
||||||
|
console.log(tagIcon);
|
||||||
|
console.log(parentTag);
|
||||||
|
console.log(tagdescription);
|
||||||
|
console.log(name);
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/newtags",
|
||||||
|
data: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
color: coloricon,
|
||||||
|
description: tagdescription,
|
||||||
|
parentId: parentTag
|
||||||
|
}),
|
||||||
|
contentType: 'application/json',
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(header, token);
|
||||||
|
},
|
||||||
|
dataType: "JSON",
|
||||||
|
success: function (data) {
|
||||||
|
console.log(data);
|
||||||
|
if (data.flag) {
|
||||||
|
massage(name + ',添加成功!', 'success','');
|
||||||
|
$("#selectTag").html("");
|
||||||
|
} else {
|
||||||
|
massage(name + ',添加失败!', 'error','');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
let pageNum = null;
|
||||||
|
let pageAll = null;
|
||||||
|
let pageUrl = '/';
|
||||||
|
|
||||||
|
function setpageNum(data, allList) {
|
||||||
|
var url = window.location.href;
|
||||||
|
console.log('当前url:' + url)
|
||||||
|
if (url.indexOf('index') > 0) {
|
||||||
|
pageUrl = '/index';
|
||||||
|
}
|
||||||
|
console.log('当前页码:' + data)
|
||||||
|
pageNum = parseInt(data) // pageNum
|
||||||
|
pageAll = parseInt(allList)
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#upPage").click(function () {
|
||||||
|
console.log("点击 upPage");
|
||||||
|
console.log('当前页码:' + pageNum);
|
||||||
|
if (pageNum > 1) {
|
||||||
|
let page = pageNum - 1
|
||||||
|
window.location.href = pageUrl + "?pageNum=" + page;
|
||||||
|
} else {
|
||||||
|
massage('已经是首页!', 'warn', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#nextPage").click(function () {
|
||||||
|
console.log("点击 nextPage");
|
||||||
|
console.log('当前页码:' + pageNum);
|
||||||
|
console.log('当前pageAll页码:' + pageAll);
|
||||||
|
if ((pageNum + 1) <= pageAll) {
|
||||||
|
let page = pageNum + 1
|
||||||
|
window.location.href = pageUrl + "?pageNum=" + page;
|
||||||
|
} else {
|
||||||
|
massage('已经是尾页!', 'warn', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$("#createNewDiccuss2").click(function () {
|
||||||
|
console.log("点击 createNewDiccuss2");
|
||||||
|
console.log(username);
|
||||||
|
|
||||||
|
if (username && username != null) {
|
||||||
|
|
||||||
|
layx.iframe('createNewDiccussContent', '新建主题', './newDiscussionsPage', {
|
||||||
|
shadable: 0.8,
|
||||||
|
event: {
|
||||||
|
ondestroy: {
|
||||||
|
before: function (layxWindow, winform, inside, escKey) {
|
||||||
|
massage('操作成功!', 'success', '')
|
||||||
|
console.log(new Date() + "关闭之前~")
|
||||||
|
console.log(winform);
|
||||||
|
console.log("=============分割线===============")
|
||||||
|
},
|
||||||
|
after: function () {
|
||||||
|
location.reload();
|
||||||
|
console.log(new Date() + "关闭之后~")
|
||||||
|
massage('操作成功!', 'success', '')
|
||||||
|
console.log("=============分割线===============")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
massage('您未登录,请先登录!', 'error', '');
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function onTags(data) {
|
||||||
|
console.log('点击标签跳转' + data);
|
||||||
|
if (username && username != null) {
|
||||||
|
window.location.href = '/main?tag=' + data
|
||||||
|
} else {
|
||||||
|
window.location.href = '/?tag=' + data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#menu')
|
||||||
|
.sticky({
|
||||||
|
context: '#container'
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
$('#context2 .menu .item')
|
||||||
|
.tab({
|
||||||
|
// special keyword works same as above
|
||||||
|
context: 'parent'
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
$('.ui.dropdown')
|
||||||
|
.dropdown()
|
||||||
|
;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
$("#userButton").click(function () {
|
||||||
|
console.log("点击 userButton");
|
||||||
|
$('#userButtonsidebar')
|
||||||
|
.sidebar({
|
||||||
|
transition: 'scale down' //默认uncover,可以取值push\overlay\slide along\slide out\scale down
|
||||||
|
})
|
||||||
|
.sidebar('toggle')
|
||||||
|
;
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.nullright {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||