Compare commits
5 Commits
3c1076834b
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1832e29672 | |||
| 11ef997085 | |||
| bd1e67c481 | |||
| 81af4244e2 | |||
| 04b3213ced |
@@ -1,44 +0,0 @@
|
||||
# Copilot Instructions for Jiscuss
|
||||
|
||||
## Build, test, and run commands
|
||||
|
||||
This repository is a Maven Spring Boot project (Java 17+).
|
||||
|
||||
- Full compile: `mvn -DskipTests compile`
|
||||
- Full test suite: `mvn test`
|
||||
- Run one test class: `mvn -Dtest=ClassName test`
|
||||
- Run one test method: `mvn -Dtest=ClassName#methodName test`
|
||||
- Run app (default H2 profile): `mvn spring-boot:run`
|
||||
- Run app (MySQL profile): `mvn spring-boot:run -Dspring-boot.run.profiles=mysql`
|
||||
- Run app on a non-80 port: `mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8080"`
|
||||
|
||||
There is currently no dedicated lint plugin configured in `pom.xml` (no Checkstyle/SpotBugs/PMD task).
|
||||
|
||||
## High-level architecture
|
||||
|
||||
Jiscuss is a Spring Boot monolith with a server-rendered web UI plus REST APIs:
|
||||
|
||||
- **Web MVC + templates**: `controller/` classes return FreeMarker views from `src/main/resources/templates/`, with frontend assets under `src/main/resources/static/` (Semantic UI + custom JS).
|
||||
- **REST APIs**: `controller/api/` exposes JSON endpoints (`/user_api/**`, `/other_api/**`) and is documented by SpringDoc OpenAPI (`/swagger-ui.html`, `/v3/api-docs`).
|
||||
- **Domain and persistence**: `entity/` models map to relational tables; `repository/` uses Spring Data JPA (plus a few native queries); `service/impl/` contains business logic and transaction boundaries.
|
||||
- **Security and RBAC**: `WebSecurityConfig` wires Spring Security 6; auth uses `UserDetailServiceImpl`; path checks delegate to `RbacPermission`; roles come from `rbac_role` / `rbac_user_role` (Flyway migration `V4__rbac_admin_console.sql`).
|
||||
- **Admin console and auditing**: `/admin/**` endpoints in `AdminSystemController` manage users/roles/content and write operation history via `AuditLogService`.
|
||||
- **Database lifecycle**: Flyway owns schema/data evolution (`src/main/resources/db/migration/*`), with profile-specific datasource config in `application-h2.yml` and `application-mysql.yml`.
|
||||
|
||||
## Key repository conventions
|
||||
|
||||
- **Flyway is the source of truth for DDL**: keep schema changes in new migration files; do not reintroduce `schema.sql`/`data.sql` initialization paths.
|
||||
- **Boot 3 / Jakarta imports only**: use `jakarta.*` APIs (not legacy `javax.*` servlet/validation/persistence types).
|
||||
- **Password handling is BCrypt-only**: DB passwords are expected to be BCrypt hashes (`UserDetailServiceImpl`, migration notes in `V2__security_updates.sql`); do not add plaintext comparisons in SQL/repositories.
|
||||
- **Role naming convention**: security authorities are `ROLE_*`; database role codes are plain (`ADMIN`, `USER`) and are prefixed in the auth layer.
|
||||
- **Controller split matters**:
|
||||
- `controller/` for page flows and model population
|
||||
- `controller/api/` for REST endpoints
|
||||
- `AdminSystemController` for admin workflows under `/admin/**`
|
||||
- **Current user lookup pattern**: controllers extending `BaseController` use `getUserInfo(HttpServletRequest)` (reads `SPRING_SECURITY_CONTEXT` from session).
|
||||
- **Response wrappers are mixed**: newer global exception flow returns `ApiResponse`; legacy endpoints still return entities or `ResponseResult`. Keep changes consistent with the style already used in the touched controller package.
|
||||
- **Caching is selective**: user read paths in `UsersServiceImpl` use cache names (`user`, `userList`) backed by Ehcache JCache (`ehcache3.xml`).
|
||||
- **Profile behavior**:
|
||||
- default/dev flow is H2 profile
|
||||
- MySQL requires explicit profile activation and datasource credentials
|
||||
- admin-only tooling endpoints include `/admin/**`, `/actuator/**`, `/druid/**` (and H2 console when enabled)
|
||||
@@ -18,12 +18,11 @@
|
||||
|
||||
|
||||
|
||||
# 预览
|
||||
[](https://imgchr.com/i/sykhy4)
|
||||
# 预览(预留)
|
||||
|
||||
|
||||
# 简介
|
||||
基于JAVA,使用SpringBoot + H2 Database + Semantic-UI + Freemarker 构建,官网:[[www.jiscuss.com]](http://www.jiscuss.com/)
|
||||
基于JAVA 构建,官网:[[www.jiscuss.com]](http://www.jiscuss.com/)
|
||||
|
||||
# 在线体验
|
||||
|
||||
@@ -44,7 +43,7 @@
|
||||
```
|
||||
|
||||
|
||||
## [三]、运行启动(默认使用H2数据库,可以在配置文件切换MYSQL)
|
||||
## [三]、运行启动(默认使用MYSQL数据库)
|
||||
|
||||
```
|
||||
// 1、代码导入IDE
|
||||
@@ -57,178 +56,6 @@
|
||||
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 已移除;如后续确实需要实时通信,再按功能重新引入。
|
||||
|
||||
|
||||
## [四]、开源协议
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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,169 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" 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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.13</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>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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>
|
||||
|
||||
<properties>
|
||||
<!-- Java 17 matches the installed JDK (openjdk-17). Upgrade to 21 when JDK 21 is installed. -->
|
||||
<java.version>17</java.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
</properties>
|
||||
<groupId>com.yaoyuan</groupId>
|
||||
<artifactId>jiscuss</artifactId>
|
||||
<version>2.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</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>
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.4.3</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
|
||||
<!-- H2 (dev/test only) -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<!-- MySQL 8 connector (updated groupId from mysql:mysql-connector-java) -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI 2 — replaces discontinued springfox-swagger2, compatible with Spring Boot 3 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon.auth</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon.view.beetl</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct for type-safe DTO mapping -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon.banner</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid Spring Boot 3 starter (Spring Boot 3 compatible version) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<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.logging.logback</artifactId>
|
||||
</dependency>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,18 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.yaoyuan.jiscuss;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableCaching
|
||||
@EnableAsync
|
||||
public class JiccussApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(JiccussApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
* @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 ipRegion;
|
||||
|
||||
private String browser;
|
||||
|
||||
private Integer likeCount;
|
||||
|
||||
private Integer dislikeCount;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
* @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);
|
||||
// floor number
|
||||
if (mapObj.get("number") != null) {
|
||||
postCustom.setNumber(Integer.parseInt(String.valueOf(mapObj.get("number"))));
|
||||
}
|
||||
// create user id
|
||||
if (mapObj.get("create_id") != null) {
|
||||
postCustom.setCreateId(Integer.parseInt(String.valueOf(mapObj.get("create_id"))));
|
||||
}
|
||||
// ip address
|
||||
if (mapObj.get("ip_address") != null) {
|
||||
postCustom.setIpAddress(String.valueOf(mapObj.get("ip_address")));
|
||||
}
|
||||
// ip region (may be null if column not yet populated)
|
||||
if (mapObj.get("ip_region") != null) {
|
||||
postCustom.setIpRegion(String.valueOf(mapObj.get("ip_region")));
|
||||
}
|
||||
// browser info (user_agent stored but only display-parsed browser is used)
|
||||
if (mapObj.get("browser") != null) {
|
||||
postCustom.setBrowser(String.valueOf(mapObj.get("browser")));
|
||||
}
|
||||
// vote counters
|
||||
if (mapObj.get("like_count") != null) {
|
||||
postCustom.setLikeCount(Integer.parseInt(String.valueOf(mapObj.get("like_count"))));
|
||||
}
|
||||
if (mapObj.get("dislike_count") != null) {
|
||||
postCustom.setDislikeCount(Integer.parseInt(String.valueOf(mapObj.get("dislike_count"))));
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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/");
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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")));
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
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.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico",
|
||||
"/login/**", "/register", "/registerDo", "/initUserData",
|
||||
"/", "/main", "/index", "/getdiscussionsbyid", "/search", "/profile",
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.entity.Role;
|
||||
import com.yaoyuan.jiscuss.entity.UpgradeLog;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.entity.UserRole;
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsTagsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.RoleRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UpgradeLogRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UserRoleRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||
import com.yaoyuan.jiscuss.repository.AuditLogRepository;
|
||||
import com.yaoyuan.jiscuss.service.AuditLogService;
|
||||
import org.springframework.boot.SpringBootVersion;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.ModelMap;
|
||||
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.RequestParam;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* 后台系统控制器
|
||||
*/
|
||||
@Controller
|
||||
public class AdminSystemController extends BaseController {
|
||||
|
||||
private final UsersRepository usersRepository;
|
||||
private final DiscussionsRepository discussionsRepository;
|
||||
private final PostsRepository postsRepository;
|
||||
private final DiscussionsTagsRepository discussionsTagsRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
private final UserRoleRepository userRoleRepository;
|
||||
private final UpgradeLogRepository upgradeLogRepository;
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
private final AuditLogService auditLogService;
|
||||
private final com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||
|
||||
public AdminSystemController(
|
||||
UsersRepository usersRepository,
|
||||
DiscussionsRepository discussionsRepository,
|
||||
PostsRepository postsRepository,
|
||||
DiscussionsTagsRepository discussionsTagsRepository,
|
||||
RoleRepository roleRepository,
|
||||
UserRoleRepository userRoleRepository,
|
||||
UpgradeLogRepository upgradeLogRepository,
|
||||
AuditLogRepository auditLogRepository,
|
||||
AuditLogService auditLogService,
|
||||
com.yaoyuan.jiscuss.service.IScoreService scoreService) {
|
||||
this.usersRepository = usersRepository;
|
||||
this.discussionsRepository = discussionsRepository;
|
||||
this.postsRepository = postsRepository;
|
||||
this.discussionsTagsRepository = discussionsTagsRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRoleRepository = userRoleRepository;
|
||||
this.upgradeLogRepository = upgradeLogRepository;
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
this.auditLogService = auditLogService;
|
||||
this.scoreService = scoreService;
|
||||
}
|
||||
|
||||
@GetMapping("/admin/home")
|
||||
public String home(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "home");
|
||||
|
||||
map.put("userCount", usersRepository.count());
|
||||
map.put("discussionCount", discussionsRepository.count());
|
||||
map.put("postCount", postsRepository.count());
|
||||
map.put("roleCount", roleRepository.count());
|
||||
|
||||
map.put("sys", buildSystemMetrics());
|
||||
map.put("deps", buildDependencyVersions());
|
||||
|
||||
return "admin/home";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/users")
|
||||
public String users(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "users");
|
||||
|
||||
List<User> users = usersRepository.findAll(Sort.by(Sort.Direction.ASC, "id"));
|
||||
List<Role> roles = roleRepository.findAllByOrderByIdAsc();
|
||||
Map<String, Set<Integer>> userRoleIds = new HashMap<>();
|
||||
for (UserRole ur : userRoleRepository.findAll()) {
|
||||
userRoleIds.computeIfAbsent(String.valueOf(ur.getUserId()), key -> new HashSet<>()).add(ur.getRoleId());
|
||||
}
|
||||
|
||||
map.put("users", users);
|
||||
map.put("roles", roles);
|
||||
map.put("userRoleIds", userRoleIds);
|
||||
return "admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/users/{id}/roles")
|
||||
@Transactional
|
||||
public String updateUserRoles(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id,
|
||||
@RequestParam(value = "roleIds", required = false) List<Integer> roleIds) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
userRoleRepository.deleteByUserId(id);
|
||||
if (roleIds != null) {
|
||||
Date now = new Date();
|
||||
Set<Integer> uniqueRoleIds = new HashSet<>(roleIds);
|
||||
Set<Integer> validEnabledRoleIds = roleRepository.findAllById(uniqueRoleIds).stream()
|
||||
.filter(role -> role.getEnabled() != null && role.getEnabled() == 1)
|
||||
.map(Role::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (Integer roleId : validEnabledRoleIds) {
|
||||
UserRole ur = new UserRole();
|
||||
ur.setUserId(id);
|
||||
ur.setRoleId(roleId);
|
||||
ur.setCreateTime(now);
|
||||
userRoleRepository.save(ur);
|
||||
}
|
||||
}
|
||||
auditLogService.log(currentAdmin, "update_user_roles", "user", id,
|
||||
"Updated user role assignments. Role count: " + (roleIds == null ? 0 : roleIds.size()));
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/roles")
|
||||
public String createRole(HttpServletRequest request,
|
||||
@RequestParam("code") String code,
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "description", required = false) String description) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
String normalizedCode = code == null ? "" : code.trim().toUpperCase();
|
||||
if (normalizedCode.isEmpty()) {
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
Role exists = roleRepository.findByCodeIgnoreCase(normalizedCode);
|
||||
if (exists == null) {
|
||||
Date now = new Date();
|
||||
Role role = new Role();
|
||||
role.setCode(normalizedCode);
|
||||
role.setName(name == null ? "" : name.trim());
|
||||
role.setDescription(description);
|
||||
role.setEnabled(1);
|
||||
role.setCreateTime(now);
|
||||
role.setUpdateTime(now);
|
||||
roleRepository.save(role);
|
||||
auditLogService.log(currentAdmin, "create_role", "role", role.getId(),
|
||||
"Created role: " + normalizedCode);
|
||||
} else {
|
||||
auditLogService.log(currentAdmin, "create_role_duplicate", "role", exists.getId(),
|
||||
"Attempted to create duplicate role: " + normalizedCode, "ignored");
|
||||
}
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/roles/{id}/toggle")
|
||||
public String toggleRole(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Role role = roleRepository.findById(id).orElse(null);
|
||||
if (role != null) {
|
||||
int nextStatus = role.getEnabled() != null && role.getEnabled() == 1 ? 0 : 1;
|
||||
// Keep built-in ADMIN role enabled to avoid locking out management access.
|
||||
if ("ADMIN".equalsIgnoreCase(role.getCode()) && nextStatus == 0) {
|
||||
auditLogService.log(currentAdmin, "toggle_role_blocked", "role", id,
|
||||
"Attempted to disable built-in ADMIN role", "blocked");
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
role.setEnabled(nextStatus);
|
||||
role.setUpdateTime(new Date());
|
||||
roleRepository.save(role);
|
||||
auditLogService.log(currentAdmin, "toggle_role", "role", id,
|
||||
"Toggled role status to " + (nextStatus == 1 ? "enabled" : "disabled"));
|
||||
}
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/discussions")
|
||||
public String discussions(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "discussions");
|
||||
List<Discussion> discussions = discussionsRepository.findAll(Sort.by(Sort.Direction.DESC, "createTime"));
|
||||
map.put("discussions", discussions);
|
||||
return "admin/discussions";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/discussions/{id}/delete")
|
||||
@Transactional
|
||||
public String deleteDiscussion(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Discussion discussion = discussionsRepository.findById(id).orElse(null);
|
||||
String title = discussion == null ? "unknown" : discussion.getTitle();
|
||||
discussionsTagsRepository.deleteByDiscussionId(id);
|
||||
postsRepository.deleteByDiscussionId(id);
|
||||
discussionsRepository.deleteById(id);
|
||||
auditLogService.log(currentAdmin, "delete_discussion", "discussion", id,
|
||||
"Deleted discussion: " + title);
|
||||
return "redirect:/admin/discussions";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/posts")
|
||||
public String posts(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "posts");
|
||||
List<Post> posts = postsRepository.findAll(Sort.by(Sort.Direction.DESC, "createTime"));
|
||||
map.put("posts", posts);
|
||||
|
||||
Set<Integer> userIds = posts.stream().map(Post::getCreateId).filter(v -> v != null).collect(Collectors.toSet());
|
||||
Set<Integer> discussionIds = posts.stream().map(Post::getDiscussionId).filter(v -> v != null).collect(Collectors.toSet());
|
||||
|
||||
Map<String, String> userNames = usersRepository.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(user -> String.valueOf(user.getId()), User::getUsername));
|
||||
Map<String, String> discussionNames = discussionsRepository.findAllById(discussionIds).stream()
|
||||
.collect(Collectors.toMap(discussion -> String.valueOf(discussion.getId()), Discussion::getTitle));
|
||||
|
||||
map.put("userNames", userNames);
|
||||
map.put("discussionNames", discussionNames);
|
||||
return "admin/posts";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/posts/{id}/delete")
|
||||
public String deletePost(HttpServletRequest request,
|
||||
@PathVariable("id") Integer id) {
|
||||
UserInfo currentAdmin = getUserInfo(request);
|
||||
Post post = postsRepository.findById(id).orElse(null);
|
||||
String content = post == null ? "unknown" : (post.getContent() == null ? "" : post.getContent());
|
||||
if (content.length() > 50) {
|
||||
content = content.substring(0, 50) + "...";
|
||||
}
|
||||
postsRepository.deleteById(id);
|
||||
auditLogService.log(currentAdmin, "delete_post", "post", id,
|
||||
"Deleted post: " + content);
|
||||
return "redirect:/admin/posts";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/upgrade-logs")
|
||||
public String upgradeLogs(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "upgradeLogs");
|
||||
map.put("logs", upgradeLogRepository.findAllByOrderByCreateTimeDesc());
|
||||
return "admin/upgrade-logs";
|
||||
}
|
||||
|
||||
@PostMapping("/admin/upgrade-logs")
|
||||
public String createUpgradeLog(HttpServletRequest request,
|
||||
@RequestParam("version") String version,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "type", defaultValue = "feature") String type,
|
||||
@RequestParam("content") String content) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
UpgradeLog log = new UpgradeLog();
|
||||
log.setVersion(version);
|
||||
log.setTitle(title);
|
||||
log.setType(type);
|
||||
log.setContent(content);
|
||||
log.setCreatedBy(user == null ? null : user.getId());
|
||||
log.setCreateTime(new Date());
|
||||
upgradeLogRepository.save(log);
|
||||
auditLogService.log(user, "create_upgrade_log", "upgrade_log", log.getId(),
|
||||
"Created upgrade log: " + version + " - " + title);
|
||||
return "redirect:/admin/upgrade-logs";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/plugins")
|
||||
public String plugins(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "plugins");
|
||||
return "admin/plugins";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/audit-logs")
|
||||
public String auditLogs(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "auditLogs");
|
||||
List<AuditLog> logs = auditLogRepository.findAllByOrderByCreateTimeDesc();
|
||||
map.put("logs", logs);
|
||||
return "admin/audit-logs";
|
||||
}
|
||||
|
||||
@GetMapping("/admin/themes")
|
||||
public String themes(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "themes");
|
||||
return "admin/themes";
|
||||
}
|
||||
|
||||
/** 积分管理 — list all users sorted by score desc. */
|
||||
@GetMapping("/admin/scores")
|
||||
public String scores(HttpServletRequest request, ModelMap map) {
|
||||
fillCommon(request, map, "scores");
|
||||
List<User> users = usersRepository.findAll(Sort.by(Sort.Direction.DESC, "score"));
|
||||
map.put("users", users);
|
||||
return "admin/scores";
|
||||
}
|
||||
|
||||
/** Manually adjust a user's score (admin operation). */
|
||||
@PostMapping("/admin/scores/{id}/adjust")
|
||||
public String adjustScore(@PathVariable Integer id,
|
||||
@RequestParam int delta,
|
||||
HttpServletRequest request) {
|
||||
UserInfo operator = getUserInfo(request);
|
||||
scoreService.addScore(id, delta);
|
||||
auditLogService.log(operator, "score_adjust", "user", id,
|
||||
"score " + (delta >= 0 ? "+" : "") + delta);
|
||||
return "redirect:/admin/scores";
|
||||
}
|
||||
|
||||
private void fillCommon(HttpServletRequest request, ModelMap map, String active) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
map.put("adminName", user == null ? "admin" : user.getUsername());
|
||||
map.put("active", active);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSystemMetrics() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||
|
||||
long totalMb = bytesToMb(rt.totalMemory());
|
||||
long freeMb = bytesToMb(rt.freeMemory());
|
||||
long usedMb = totalMb - freeMb;
|
||||
long maxMb = bytesToMb(rt.maxMemory());
|
||||
long heapUsedMb = bytesToMb(memoryMXBean.getHeapMemoryUsage().getUsed());
|
||||
|
||||
result.put("totalMb", totalMb);
|
||||
result.put("freeMb", freeMb);
|
||||
result.put("usedMb", usedMb);
|
||||
result.put("maxMb", maxMb);
|
||||
result.put("heapUsedMb", heapUsedMb);
|
||||
result.put("uptimeMs", ManagementFactory.getRuntimeMXBean().getUptime());
|
||||
result.put("cpuLoad", readCpuLoadPercent());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> buildDependencyVersions() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("springBoot", SpringBootVersion.getVersion());
|
||||
result.put("java", System.getProperty("java.version"));
|
||||
result.put("spring", implVersion("org.springframework.core.SpringVersion"));
|
||||
result.put("hibernate", implVersion("org.hibernate.Version"));
|
||||
result.put("flyway", mavenVersion("org.flywaydb", "flyway-core",
|
||||
implVersion("org.flywaydb.core.Flyway")));
|
||||
result.put("druid", mavenVersion("com.alibaba", "druid-spring-boot-3-starter",
|
||||
mavenVersion("com.alibaba", "druid",
|
||||
implVersion("com.alibaba.druid.pool.DruidDataSource"))));
|
||||
result.put("ehcache", implVersion("org.ehcache.CacheManager"));
|
||||
result.put("springdoc", implVersion("org.springdoc.core.configuration.SpringDocConfiguration"));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Read version from META-INF/maven pom.properties (works in fat JARs). Falls back to {@code fallback}. */
|
||||
private String mavenVersion(String groupId, String artifactId, String fallback) {
|
||||
String path = "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties";
|
||||
try (java.io.InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
java.util.Properties props = new java.util.Properties();
|
||||
props.load(is);
|
||||
String v = props.getProperty("version");
|
||||
if (v != null && !v.isBlank()) return v;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String implVersion(String className) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
Package pkg = clazz.getPackage();
|
||||
String version = pkg == null ? null : pkg.getImplementationVersion();
|
||||
return version == null ? "unknown" : version;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private String readCpuLoadPercent() {
|
||||
try {
|
||||
java.lang.management.OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
|
||||
double value = sunOsBean.getCpuLoad();
|
||||
if (value >= 0) {
|
||||
return String.format("%.2f%%", value * 100);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
private long bytesToMb(long bytes) {
|
||||
return bytes / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
/**
|
||||
* Injects common model attributes (e.g. isAdmin) into every rendered view.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalModelAdvice {
|
||||
|
||||
@ModelAttribute("isAdmin")
|
||||
public boolean isAdmin(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) return false;
|
||||
Object ctx = session.getAttribute("SPRING_SECURITY_CONTEXT");
|
||||
if (!(ctx instanceof SecurityContext sc)) return false;
|
||||
Authentication auth = sc.getAuthentication();
|
||||
if (auth == null || !(auth.getPrincipal() instanceof UserInfo)) return false;
|
||||
return auth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.anyMatch(a -> a.equals("ROLE_ADMIN"));
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.dto.InboxItem;
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* 站内私信与通知控制器
|
||||
*/
|
||||
@Controller
|
||||
public class UserMsgController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
/** 收件箱:对话列表 */
|
||||
@GetMapping("/messages")
|
||||
public String inbox(HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
List<Message> inboxList = messageService.getInbox(user.getId());
|
||||
|
||||
List<InboxItem> items = new ArrayList<>();
|
||||
for (Message msg : inboxList) {
|
||||
Integer otherId = msg.getSenderId().equals(user.getId()) ? msg.getReceiverId() : msg.getSenderId();
|
||||
User other = usersService.findOne(otherId);
|
||||
String rawName = (other != null) ? other.getUsername() : null;
|
||||
String otherName = (rawName != null && !rawName.isBlank()) ? rawName : "用户" + otherId;
|
||||
boolean hasUnread = !Boolean.TRUE.equals(msg.getIsRead()) && msg.getReceiverId().equals(user.getId());
|
||||
items.add(new InboxItem(otherId, otherName, msg.getContent(), msg.getCreateTime(), hasUnread));
|
||||
}
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("currentUserId", user.getId());
|
||||
map.put("inboxItems", items);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(user.getId()));
|
||||
return "messages";
|
||||
}
|
||||
|
||||
/** 对话详情页 */
|
||||
@GetMapping("/messages/{otherId}")
|
||||
public String conversation(@PathVariable Integer otherId, HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
messageService.markConversationRead(user.getId(), otherId);
|
||||
|
||||
List<Message> messages = messageService.getConversation(user.getId(), otherId);
|
||||
User other = usersService.findOne(otherId);
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("currentUserId", user.getId());
|
||||
map.put("otherId", otherId);
|
||||
map.put("otherUsername", other != null ? other.getUsername() : "用户" + otherId);
|
||||
map.put("messages", messages);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(user.getId()));
|
||||
return "conversation";
|
||||
}
|
||||
|
||||
/** 通知列表页 */
|
||||
@GetMapping("/notifications")
|
||||
public String notifications(@RequestParam(defaultValue = "0") int page,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return "redirect:/login";
|
||||
|
||||
notificationService.markAllRead(user.getId());
|
||||
Page<Notification> notifs = notificationService.listByUser(user.getId(), page, 20);
|
||||
|
||||
List<String> fromUsernames = new ArrayList<>();
|
||||
for (Notification n : notifs.getContent()) {
|
||||
if (n.getFromUserId() != null) {
|
||||
User from = usersService.findOne(n.getFromUserId());
|
||||
fromUsernames.add(from != null ? from.getUsername() : "用户" + n.getFromUserId());
|
||||
} else {
|
||||
fromUsernames.add("系统");
|
||||
}
|
||||
}
|
||||
|
||||
map.put("username", user.getUsername());
|
||||
map.put("notifications", notifs.getContent());
|
||||
map.put("fromUsernames", fromUsernames);
|
||||
map.put("totalPages", notifs.getTotalPages());
|
||||
map.put("currentPage", page);
|
||||
map.put("unreadMsgCount", messageService.countUnread(user.getId()));
|
||||
map.put("unreadNotifCount", 0L);
|
||||
return "notifications";
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* 其他控制器——积分/权限等
|
||||
*/
|
||||
@Controller
|
||||
public class UserOtherController extends BaseController {
|
||||
|
||||
/**
|
||||
* 用户积分获取
|
||||
*/
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
*/
|
||||
@Controller
|
||||
public class UserPageController extends BaseController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserPageController.class);
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private LikeCollectRepository likeCollectRepository;
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
/**
|
||||
* 用户个人主页
|
||||
*/
|
||||
@RequestMapping("/user")
|
||||
public String user(@RequestParam(defaultValue = "discussion") String type,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
|
||||
UserInfo currentUser = getUserInfo(request);
|
||||
if (currentUser == null) return "redirect:/login";
|
||||
|
||||
User userEntity = usersService.findOne(currentUser.getId());
|
||||
long liveDiscCount = discussionsRepository.countByStartUserId(currentUser.getId());
|
||||
long liveReplyCount = postsRepository.countByCreateId(currentUser.getId());
|
||||
map.put("username", currentUser.getUsername());
|
||||
map.put("userEntity", userEntity);
|
||||
map.put("liveDiscCount", liveDiscCount);
|
||||
map.put("liveReplyCount", liveReplyCount);
|
||||
map.put("type", type);
|
||||
map.put("unreadMsgCount", messageService.countUnread(currentUser.getId()));
|
||||
map.put("unreadNotifCount", notificationService.countUnread(currentUser.getId()));
|
||||
|
||||
if ("discussion".equals(type)) {
|
||||
Page<Discussion> discussions = discussionsRepository.findByStartUserIdOrderByStartTimeDesc(
|
||||
currentUser.getId(), PageRequest.of(0, 20));
|
||||
map.put("discussions", discussions.getContent());
|
||||
} else if ("reply".equals(type)) {
|
||||
Page<Post> posts = postsRepository.findByCreateIdOrderByCreateTimeDesc(
|
||||
currentUser.getId(), PageRequest.of(0, 20));
|
||||
map.put("posts", posts.getContent());
|
||||
// Attach discussion titles
|
||||
List<String> discTitles = new ArrayList<>();
|
||||
for (Post p : posts.getContent()) {
|
||||
if (p.getDiscussionId() != null) {
|
||||
Discussion d = discussionsRepository.findById(p.getDiscussionId()).orElse(null);
|
||||
discTitles.add(d != null ? d.getTitle() : "");
|
||||
} else {
|
||||
discTitles.add("");
|
||||
}
|
||||
}
|
||||
map.put("discTitles", discTitles);
|
||||
} else if ("like".equals(type)) {
|
||||
List<Integer> likedIds = likeCollectRepository.findLikedDiscussionIdsByUser(currentUser.getId());
|
||||
List<Discussion> liked = new ArrayList<>();
|
||||
for (Integer id : likedIds) {
|
||||
Discussion d = discussionsRepository.findById(id).orElse(null);
|
||||
if (d != null) liked.add(d);
|
||||
}
|
||||
map.put("likedDiscussions", liked);
|
||||
}
|
||||
|
||||
map.put("isOwn", true);
|
||||
return "user";
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开用户主页 /profile?uid=xxx (无需登录)
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
public String profile(@RequestParam Integer uid,
|
||||
@RequestParam(defaultValue = "discussion") String type,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
User userEntity = usersService.findOne(uid);
|
||||
if (userEntity == null) return "redirect:/";
|
||||
|
||||
long liveDiscCount = discussionsRepository.countByStartUserId(uid);
|
||||
long liveReplyCount = postsRepository.countByCreateId(uid);
|
||||
map.put("username", userEntity.getUsername());
|
||||
map.put("userEntity", userEntity);
|
||||
map.put("liveDiscCount", liveDiscCount);
|
||||
map.put("liveReplyCount", liveReplyCount);
|
||||
map.put("type", type);
|
||||
map.put("isOwn", false);
|
||||
|
||||
if ("discussion".equals(type)) {
|
||||
Page<Discussion> discussions = discussionsRepository.findByStartUserIdOrderByStartTimeDesc(
|
||||
uid, PageRequest.of(0, 20));
|
||||
map.put("discussions", discussions.getContent());
|
||||
} else if ("reply".equals(type)) {
|
||||
Page<Post> posts = postsRepository.findByCreateIdOrderByCreateTimeDesc(
|
||||
uid, PageRequest.of(0, 20));
|
||||
map.put("posts", posts.getContent());
|
||||
List<String> discTitles = new ArrayList<>();
|
||||
for (Post p : posts.getContent()) {
|
||||
Discussion d = p.getDiscussionId() != null
|
||||
? discussionsRepository.findById(p.getDiscussionId()).orElse(null) : null;
|
||||
discTitles.add(d != null ? d.getTitle() : "");
|
||||
}
|
||||
map.put("discTitles", discTitles);
|
||||
} else if ("like".equals(type)) {
|
||||
List<Integer> likedIds = likeCollectRepository.findLikedDiscussionIdsByUser(uid);
|
||||
List<Discussion> liked = new ArrayList<>();
|
||||
for (Integer id : likedIds) {
|
||||
Discussion d = discussionsRepository.findById(id).orElse(null);
|
||||
if (d != null) liked.add(d);
|
||||
}
|
||||
map.put("likedDiscussions", liked);
|
||||
}
|
||||
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
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.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
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 com.yaoyuan.jiscuss.service.IpRegionService;
|
||||
import com.yaoyuan.jiscuss.util.IpUtils;
|
||||
import com.yaoyuan.jiscuss.util.UserAgentUtils;
|
||||
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.PageRequest;
|
||||
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 Chuyaoyuan
|
||||
* 主题帖子评论控制器
|
||||
*/
|
||||
@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;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private IpRegionService ipRegionService;
|
||||
|
||||
@Autowired
|
||||
private com.yaoyuan.jiscuss.service.INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||
|
||||
|
||||
/**
|
||||
* 首页查看主题列表(各种条件筛选,最热最新标签等)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 查看主题详情
|
||||
* @param request
|
||||
* @param map
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping("/getdiscussionsbyid")
|
||||
public String getDiscussionsById(HttpServletRequest request, ModelMap map,
|
||||
@RequestParam("id") Integer id,
|
||||
@RequestParam(defaultValue = "newest") String sort) {
|
||||
logger.info(">>> getDiscussionsById{} sort={}", id, sort);
|
||||
|
||||
// Atomically increment view count before loading the discussion
|
||||
discussionsService.incrementViewCount(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, sort);
|
||||
map.put("tags", tags);
|
||||
map.put("discussions", newdd);
|
||||
map.put("posts", postsObj);
|
||||
map.put("sort", sort);
|
||||
|
||||
// Sidebar: author stats and top discussions
|
||||
if (newdd.getStartUserId() != null) {
|
||||
Integer authorId = newdd.getStartUserId();
|
||||
long authorDiscCount = discussionsRepository.countByStartUserId(authorId);
|
||||
long authorReplyCount = postsRepository.countByCreateId(authorId);
|
||||
java.util.List<Discussion> authorTopDiscs = discussionsRepository
|
||||
.findTopByStartUserIdOrderByLikes(authorId, PageRequest.of(0, 5));
|
||||
map.put("authorDiscCount", authorDiscCount);
|
||||
map.put("authorReplyCount", authorReplyCount);
|
||||
map.put("authorTopDiscs", authorTopDiscs);
|
||||
}
|
||||
|
||||
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);
|
||||
// Award points for creating a discussion
|
||||
if (user != null) {
|
||||
scoreService.addScore(user.getId(), 10);
|
||||
}
|
||||
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());
|
||||
|
||||
// Capture client IP, region, and browser
|
||||
String clientIp = IpUtils.getClientIp(request);
|
||||
post.setIpAddress(clientIp);
|
||||
post.setIpRegion(ipRegionService.getRegion(clientIp));
|
||||
post.setBrowser(UserAgentUtils.parseBrowser(request.getHeader("User-Agent")));
|
||||
|
||||
if (null != post.getParentId()) {
|
||||
Post temp = postsService.findOneByid(post.getParentId());
|
||||
post.setUserId(temp.getCreateId());
|
||||
}
|
||||
|
||||
Post savePost = postsService.insert(post);
|
||||
// Notify the discussion starter about a reply (async, ignores self-reply)
|
||||
if (post.getDiscussionId() != null && user != null) {
|
||||
Discussion disc = discussionsService.findOne(post.getDiscussionId());
|
||||
if (disc != null && disc.getStartUserId() != null) {
|
||||
notificationService.notifyReply(disc.getStartUserId(), user.getId(),
|
||||
disc.getId(), disc.getTitle());
|
||||
}
|
||||
}
|
||||
Map<String, Object> resultobj = new HashMap<>();
|
||||
logger.info(">>>{}", savePost);
|
||||
resultobj.put("msg", "添加评论成功");
|
||||
resultobj.put("flag", true);
|
||||
// Award points for posting a reply
|
||||
if (user != null) {
|
||||
scoreService.addScore(user.getId(), 5);
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
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.repository.DiscussionsRepository;
|
||||
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.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
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 org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* 首页页面系统控制器
|
||||
*/
|
||||
@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;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
/**
|
||||
* 首页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());
|
||||
map.put("pageNumAll", allDiscussionsPage.getTotalPages());
|
||||
map.put("topUsers", usersService.getTopActiveUsers(10));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Integer> userIds = new HashSet<>();
|
||||
for (Discussion dd : allDiscussions) {
|
||||
if (dd.getStartUserId() != null) {
|
||||
userIds.add(dd.getStartUserId());
|
||||
}
|
||||
if (dd.getLastUserId() != null) {
|
||||
userIds.add(dd.getLastUserId());
|
||||
}
|
||||
}
|
||||
Map<Integer, User> userMap = usersService.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(User::getId, u -> u));
|
||||
|
||||
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 = userMap.get(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 = userMap.get(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";
|
||||
}
|
||||
|
||||
@GetMapping("/favicon.ico")
|
||||
public RedirectView favicon() {
|
||||
return new RedirectView("/static/assets/images/logo.png");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册提交
|
||||
* @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(passwordEncoder.encode(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";
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索页
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public String search(@RequestParam(defaultValue = "") String q,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
HttpServletRequest request, ModelMap map) {
|
||||
String keyword = q.trim();
|
||||
int pageSize = 10;
|
||||
int page = Math.max(pageNum - 1, 0);
|
||||
Page<Discussion> resultPage = keyword.isEmpty()
|
||||
? Page.empty()
|
||||
: discussionsRepository.searchByKeyword(keyword,
|
||||
org.springframework.data.domain.PageRequest.of(page, pageSize));
|
||||
|
||||
List<DiscussionCustom> results = new ArrayList<>();
|
||||
if (!keyword.isEmpty()) {
|
||||
setTagAndUserList(results, resultPage.getContent());
|
||||
}
|
||||
|
||||
map.put("q", keyword);
|
||||
map.put("results", results);
|
||||
map.put("totalPages", resultPage.getTotalPages());
|
||||
map.put("totalElements", resultPage.getTotalElements());
|
||||
map.put("pageNum", pageNum);
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user != null) map.put("username", user.getUsername());
|
||||
return "search";
|
||||
}
|
||||
|
||||
//获取设置信息
|
||||
|
||||
//获取首页统计
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller.api;
|
||||
|
||||
import com.yaoyuan.jiscuss.controller.BaseController;
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST API for private messages and notifications.
|
||||
*/
|
||||
@Tag(name = "Message API", description = "Private messages and notifications")
|
||||
@RestController
|
||||
@RequestMapping("/msg_api")
|
||||
public class RestMsgController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IMessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private IUsersService usersService;
|
||||
|
||||
// ─── User card (public — accessible to any authenticated user) ────────
|
||||
|
||||
/** Returns basic user info for the hover card (discussionsCount, commentsCount). */
|
||||
@Operation(summary = "Get user card info")
|
||||
@GetMapping("/user-card/{id}")
|
||||
public ApiResponse<Map<String, Object>> getUserCard(@PathVariable Integer id, HttpServletRequest request) {
|
||||
UserInfo currentUser = getUserInfo(request);
|
||||
if (currentUser == null) return ApiResponse.fail(401, "请先登录");
|
||||
User user = usersService.findOne(id);
|
||||
if (user == null) return ApiResponse.fail(404, "用户不存在");
|
||||
return ApiResponse.ok(Map.of(
|
||||
"id", user.getId(),
|
||||
"username", user.getUsername() != null ? user.getUsername() : "",
|
||||
"discussionsCount", user.getDiscussionsCount() != null ? user.getDiscussionsCount() : 0,
|
||||
"commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0,
|
||||
"level", user.getLevel() != null ? user.getLevel() : 0,
|
||||
"score", user.getScore() != null ? user.getScore() : 0
|
||||
));
|
||||
}
|
||||
|
||||
// ─── Notifications ────────────────────────────────────────────────────
|
||||
|
||||
@Operation(summary = "Get unread notification + message counts")
|
||||
@GetMapping("/unread-counts")
|
||||
public ApiResponse<Map<String, Long>> unreadCounts(HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.ok(Map.of("notifications", 0L, "messages", 0L));
|
||||
return ApiResponse.ok(Map.of(
|
||||
"notifications", notificationService.countUnread(user.getId()),
|
||||
"messages", messageService.countUnread(user.getId())
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "Mark a notification as read")
|
||||
@PostMapping("/notifications/{id}/read")
|
||||
public ApiResponse<Void> markNotifRead(@PathVariable Integer id, HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
notificationService.markOneRead(id, user.getId());
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "Mark all notifications as read")
|
||||
@PostMapping("/notifications/read-all")
|
||||
public ApiResponse<Void> markAllNotifRead(HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
notificationService.markAllRead(user.getId());
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
// ─── Messages ─────────────────────────────────────────────────────────
|
||||
|
||||
@Operation(summary = "Send a private message")
|
||||
@PostMapping("/messages/send")
|
||||
public ApiResponse<Message> sendMessage(
|
||||
@RequestParam Integer receiverId,
|
||||
@RequestParam String content,
|
||||
HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
if (user.getId().equals(receiverId)) return ApiResponse.fail(400, "不能给自己发消息");
|
||||
if (content == null || content.isBlank()) return ApiResponse.fail(400, "消息内容不能为空");
|
||||
Message msg = messageService.send(user.getId(), receiverId, content.trim());
|
||||
return ApiResponse.ok(msg);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get conversation with another user")
|
||||
@GetMapping("/messages/conversation/{otherId}")
|
||||
public ApiResponse<List<Message>> getConversation(@PathVariable Integer otherId, HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) return ApiResponse.fail(401, "请先登录");
|
||||
messageService.markConversationRead(user.getId(), otherId);
|
||||
return ApiResponse.ok(messageService.getConversation(user.getId(), otherId));
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller.api;
|
||||
|
||||
import com.yaoyuan.jiscuss.dto.UserCreateRequest;
|
||||
import com.yaoyuan.jiscuss.dto.UserMapper;
|
||||
import com.yaoyuan.jiscuss.dto.UserResponse;
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.exception.BaseException;
|
||||
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||
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 jakarta.validation.Valid;
|
||||
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;
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@PostMapping("/user")
|
||||
@Operation(summary = "新增用户")
|
||||
public ApiResponse<UserResponse> save(@Valid @RequestBody UserCreateRequest request) {
|
||||
User user = userMapper.fromCreateRequest(request);
|
||||
User saved = usersService.insert(user);
|
||||
if (saved != null) {
|
||||
return ApiResponse.ok(userMapper.toResponse(saved));
|
||||
}
|
||||
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||
}
|
||||
|
||||
@DeleteMapping("/user/{id}")
|
||||
@Operation(summary = "删除用户")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Integer id) {
|
||||
usersService.remove(id);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@PutMapping("/user/{id}")
|
||||
@Operation(summary = "修改用户")
|
||||
public ApiResponse<UserResponse> update(@RequestBody User user, @PathVariable Integer id) {
|
||||
User updated = usersService.update(user, id);
|
||||
if (updated != null) {
|
||||
return ApiResponse.ok(userMapper.toResponse(updated));
|
||||
}
|
||||
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||
}
|
||||
|
||||
@GetMapping("/user/{id}")
|
||||
@Operation(summary = "获取用户")
|
||||
public ApiResponse<UserResponse> getUser(@PathVariable Integer id) {
|
||||
User user = usersService.findOne(id);
|
||||
logger.info("获取用户==>:{}", user);
|
||||
return ApiResponse.ok(userMapper.toResponse(user));
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
@Operation(summary = "获取全部用户")
|
||||
public ApiResponse<List<UserResponse>> getAllUsers() {
|
||||
List<UserResponse> list = usersService.getAllList().stream()
|
||||
.map(userMapper::toResponse)
|
||||
.toList();
|
||||
return ApiResponse.ok(list);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.controller.api;
|
||||
|
||||
import com.yaoyuan.jiscuss.controller.BaseController;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||
import com.yaoyuan.jiscuss.service.ILikeCollectService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST API for voting (like / dislike) on discussions and posts.
|
||||
*/
|
||||
@Tag(name = "Vote API", description = "Like and dislike discussions and posts")
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class VoteController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private ILikeCollectService likeCollectService;
|
||||
|
||||
@Operation(summary = "Vote on a discussion")
|
||||
@PostMapping("/discussions/{id}/vote")
|
||||
public ApiResponse<Map<String, String>> voteDiscussion(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam String action,
|
||||
HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) {
|
||||
return ApiResponse.fail(401, "请先登录");
|
||||
}
|
||||
if (!"like".equals(action) && !"dislike".equals(action)) {
|
||||
return ApiResponse.fail(400, "action 参数必须是 like 或 dislike");
|
||||
}
|
||||
String result = likeCollectService.voteDiscussion(user.getId(), id, action);
|
||||
return ApiResponse.ok(Map.of("action", result));
|
||||
}
|
||||
|
||||
@Operation(summary = "Vote on a post (reply)")
|
||||
@PostMapping("/posts/{id}/vote")
|
||||
public ApiResponse<Map<String, String>> votePost(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam String action,
|
||||
HttpServletRequest request) {
|
||||
UserInfo user = getUserInfo(request);
|
||||
if (user == null) {
|
||||
return ApiResponse.fail(401, "请先登录");
|
||||
}
|
||||
if (!"like".equals(action) && !"dislike".equals(action)) {
|
||||
return ApiResponse.fail(400, "action 参数必须是 like 或 dislike");
|
||||
}
|
||||
String result = likeCollectService.votePost(user.getId(), id, action);
|
||||
return ApiResponse.ok(Map.of("action", result));
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/** View model for one inbox entry (latest message in a conversation). */
|
||||
@Data
|
||||
public class InboxItem {
|
||||
private final Integer otherId;
|
||||
private final String otherUsername;
|
||||
private final String lastContent;
|
||||
private final Date lastTime;
|
||||
private final boolean hasUnread;
|
||||
|
||||
public InboxItem(Integer otherId, String otherUsername, String lastContent, Date lastTime, boolean hasUnread) {
|
||||
this.otherId = otherId;
|
||||
this.otherUsername = otherUsername;
|
||||
this.lastContent = lastContent;
|
||||
this.lastTime = lastTime;
|
||||
this.hasUnread = hasUnread;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
) {}
|
||||
@@ -1,54 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
) {}
|
||||
@@ -1,47 +0,0 @@
|
||||
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 = "audit_log")
|
||||
public class AuditLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "admin_id")
|
||||
private Integer adminId;
|
||||
|
||||
@Column(name = "admin_name")
|
||||
private String adminName;
|
||||
|
||||
@Column(name = "action_type")
|
||||
private String actionType;
|
||||
|
||||
@Column(name = "target_type")
|
||||
private String targetType;
|
||||
|
||||
@Column(name = "target_id")
|
||||
private Integer targetId;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "status")
|
||||
private String status;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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;
|
||||
|
||||
@Column(name = "dislike_count")
|
||||
private Integer dislikeCount;
|
||||
|
||||
/** 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;
|
||||
|
||||
@Column(name = "view_count")
|
||||
private Integer viewCount = 0;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
*/
|
||||
@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;
|
||||
|
||||
/** Vote action: 'like' or 'dislike'. */
|
||||
@Column(name = "action")
|
||||
private String action;
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "message")
|
||||
public class Message implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "sender_id", nullable = false)
|
||||
private Integer senderId;
|
||||
|
||||
@Column(name = "receiver_id", nullable = false)
|
||||
private Integer receiverId;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT", nullable = false)
|
||||
private String content;
|
||||
|
||||
@Column(name = "is_read")
|
||||
private Boolean isRead = false;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* Conversation identifier = "min(a,b)_max(a,b)", groups all messages
|
||||
* between two users into a single conversation thread.
|
||||
*/
|
||||
@Column(name = "conversation_id", nullable = false)
|
||||
private String conversationId;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notification")
|
||||
public class Notification implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/** Receiver user ID. */
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Integer userId;
|
||||
|
||||
/** Sender user ID (null for system notifications). */
|
||||
@Column(name = "from_user_id")
|
||||
private Integer fromUserId;
|
||||
|
||||
/** Notification type: reply / like / system. */
|
||||
@Column(name = "type", nullable = false)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/** Related entity ID (e.g., discussion or post id). */
|
||||
@Column(name = "related_id")
|
||||
private Integer relatedId;
|
||||
|
||||
/** Type of related entity: discussion / post. */
|
||||
@Column(name = "related_type")
|
||||
private String relatedType;
|
||||
|
||||
@Column(name = "is_read")
|
||||
private Boolean isRead = false;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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;
|
||||
|
||||
/** Province/region derived from IP (e.g. "北京", "广东"). Stored on post creation. */
|
||||
@Column(name = "ip_region")
|
||||
private String ipRegion;
|
||||
|
||||
/** Parsed browser name+version (e.g. "Chrome 120"). Derived from User-Agent on post creation. */
|
||||
@Column(name = "browser")
|
||||
private String browser;
|
||||
|
||||
@Column(name = "copyright")
|
||||
private String copyright;
|
||||
|
||||
@Column(name = "is_approved")
|
||||
private Integer isApproved;
|
||||
|
||||
@Column(name = "like_count")
|
||||
private Integer likeCount;
|
||||
|
||||
@Column(name = "dislike_count")
|
||||
private Integer dislikeCount;
|
||||
|
||||
@Column(name = "create_id")
|
||||
private Integer createId;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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 = "rbac_role")
|
||||
public class Role implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "code")
|
||||
private String code;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private Integer enabled;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 = "upgrade_log")
|
||||
public class UpgradeLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "version")
|
||||
private String version;
|
||||
|
||||
@Column(name = "title")
|
||||
private String title;
|
||||
|
||||
@Column(name = "content")
|
||||
private String content;
|
||||
|
||||
@Column(name = "type")
|
||||
private String type;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Integer createdBy;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
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). Never serialized to JSON. */
|
||||
@JsonIgnore
|
||||
@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;
|
||||
|
||||
@Column(name = "score")
|
||||
private Integer score;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
* @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 score;
|
||||
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{" +
|
||||
"username='" + username + '\'' +
|
||||
", id='" + id + '\'' +
|
||||
", authorities=" + authorities +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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 = "rbac_user_role")
|
||||
public class UserRole implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Integer userId;
|
||||
|
||||
@Column(name = "role_id")
|
||||
private Integer roleId;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
* @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;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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 Chuyaoyuan
|
||||
* @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;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.entity.custom;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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 org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
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("没有权限访问该资源"));
|
||||
}
|
||||
|
||||
/** 404 mappings/resources should not be logged as system errors. */
|
||||
@ExceptionHandler({NoHandlerFoundException.class, NoResourceFoundException.class})
|
||||
public ResponseEntity<ApiResponse<Void>> handleNotFound(Exception ex) {
|
||||
log.warn("Not found: {}", ex.getMessage());
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.NOT_FOUND)
|
||||
.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));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface AuditLogRepository extends JpaRepository<AuditLog, Integer> {
|
||||
List<AuditLog> findAllByOrderByCreateTimeDesc();
|
||||
|
||||
Page<AuditLog> findAllByOrderByCreateTimeDesc(Pageable pageable);
|
||||
|
||||
List<AuditLog> findByTargetTypeAndTargetIdOrderByCreateTimeDesc(String targetType, Integer targetId);
|
||||
|
||||
List<AuditLog> findByAdminIdOrderByCreateTimeDesc(Integer adminId);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@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);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Discussion d SET d.viewCount = d.viewCount + 1 WHERE d.id = :id")
|
||||
void incrementViewCount(@org.springframework.data.repository.query.Param("id") Integer id);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Discussion d SET d.commentsCount = COALESCE(d.commentsCount, 0) + 1, d.lastTime = :lastTime, d.lastUserId = :userId WHERE d.id = :id")
|
||||
void incrementCommentCount(
|
||||
@org.springframework.data.repository.query.Param("id") Integer id,
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
@org.springframework.data.repository.query.Param("lastTime") Date lastTime);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Discussion d SET d.likeCount = COALESCE(d.likeCount, 0) + :delta WHERE d.id = :id")
|
||||
void adjustLikeCount(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||
@org.springframework.data.repository.query.Param("delta") int delta);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Discussion d SET d.dislikeCount = COALESCE(d.dislikeCount, 0) + :delta WHERE d.id = :id")
|
||||
void adjustDislikeCount(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||
@org.springframework.data.repository.query.Param("delta") int delta);
|
||||
|
||||
/** Discussions started by a given user, most recent first. */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.startUserId = :userId ORDER BY d.startTime DESC")
|
||||
org.springframework.data.domain.Page<Discussion> findByStartUserIdOrderByStartTimeDesc(
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
org.springframework.data.domain.Pageable pageable);
|
||||
long countByStartUserId(Integer startUserId);
|
||||
|
||||
/** Author's top discussions ordered by likeCount (for sidebar). */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.startUserId = :userId ORDER BY COALESCE(d.likeCount, 0) DESC, d.startTime DESC")
|
||||
List<Discussion> findTopByStartUserIdOrderByLikes(
|
||||
@org.springframework.data.repository.query.Param("userId") Integer userId,
|
||||
Pageable pageable);
|
||||
|
||||
/** Full-text search by title or content. */
|
||||
@Query("SELECT d FROM Discussion d WHERE d.title LIKE %:q% OR d.content LIKE %:q% ORDER BY d.startTime DESC")
|
||||
Page<Discussion> searchByKeyword(@org.springframework.data.repository.query.Param("q") String q, Pageable pageable);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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> {
|
||||
void deleteByDiscussionId(Integer discussionId);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.LikeCollect;
|
||||
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.Optional;
|
||||
|
||||
@Repository
|
||||
public interface LikeCollectRepository extends JpaRepository<LikeCollect, Integer> {
|
||||
|
||||
/** Find an existing vote record for a user on a discussion. */
|
||||
@Query("FROM LikeCollect lc WHERE lc.userId = :userId AND lc.discussionId = :discussionId AND lc.type = 'discussion'")
|
||||
Optional<LikeCollect> findDiscussionVote(@Param("userId") Integer userId, @Param("discussionId") Integer discussionId);
|
||||
|
||||
/** Find an existing vote record for a user on a post. */
|
||||
@Query("FROM LikeCollect lc WHERE lc.userId = :userId AND lc.postId = :postId AND lc.type = 'post'")
|
||||
Optional<LikeCollect> findPostVote(@Param("userId") Integer userId, @Param("postId") Integer postId);
|
||||
|
||||
/** Discussions liked by a user (action = 'like'). */
|
||||
@Query("SELECT lc.discussionId FROM LikeCollect lc WHERE lc.userId = :userId AND lc.type = 'discussion' AND lc.action = 'like'")
|
||||
java.util.List<Integer> findLikedDiscussionIdsByUser(@Param("userId") Integer userId);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, Integer> {
|
||||
|
||||
/** Latest messages in a conversation, most recent first. */
|
||||
List<Message> findByConversationIdOrderByCreateTimeDesc(String conversationId);
|
||||
|
||||
/** All messages sent by user. */
|
||||
List<Message> findBySenderIdOrderByCreateTimeDesc(Integer senderId);
|
||||
|
||||
/** All messages received by user. */
|
||||
List<Message> findByReceiverIdOrderByCreateTimeDesc(Integer receiverId);
|
||||
|
||||
long countByReceiverIdAndIsReadFalse(Integer receiverId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Message m SET m.isRead = true WHERE m.conversationId = :convId AND m.receiverId = :userId")
|
||||
void markConversationReadForUser(@Param("convId") String convId, @Param("userId") Integer userId);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
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.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, Integer> {
|
||||
|
||||
Page<Notification> findByUserIdOrderByCreateTimeDesc(Integer userId, Pageable pageable);
|
||||
|
||||
long countByUserIdAndIsReadFalse(Integer userId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.userId = :userId")
|
||||
void markAllReadByUserId(@Param("userId") Integer userId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.id = :id AND n.userId = :userId")
|
||||
void markReadById(@Param("id") Integer id, @Param("userId") Integer userId);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public interface PostsRepository extends JpaRepository<Post, Integer> {
|
||||
|
||||
void deleteByDiscussionId(Integer discussionId);
|
||||
|
||||
/**
|
||||
* @param dId
|
||||
* @return
|
||||
*/
|
||||
@Query("from Post where discussionId = :dId")
|
||||
List<Post> findOneBy(@Param("dId") Integer dId);
|
||||
|
||||
/** Returns the current max floor number for a discussion (0 if no posts yet). */
|
||||
@Query("SELECT COALESCE(MAX(p.number), 0) FROM Post p WHERE p.discussionId = :dId")
|
||||
int findMaxNumberByDiscussionId(@Param("dId") Integer dId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Post p SET p.likeCount = COALESCE(p.likeCount, 0) + :delta WHERE p.id = :id")
|
||||
void adjustLikeCount(@Param("id") Integer id, @Param("delta") int delta);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE Post p SET p.dislikeCount = COALESCE(p.dislikeCount, 0) + :delta WHERE p.id = :id")
|
||||
void adjustDislikeCount(@Param("id") Integer id, @Param("delta") int delta);
|
||||
|
||||
@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 asc"
|
||||
, nativeQuery = true)
|
||||
List<Map<String, Object>> findAllByDIdAndparentIdNullOldest(@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>> findAllByDIdAndparentIdNullNewest(@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.like_count desc, p.create_time desc"
|
||||
, nativeQuery = true)
|
||||
List<Map<String, Object>> findAllByDIdAndparentIdNullHot(@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);
|
||||
|
||||
/** Posts (replies) created by a given user, most recent first. */
|
||||
@Query("SELECT p FROM Post p WHERE p.createId = :userId ORDER BY p.createTime DESC")
|
||||
org.springframework.data.domain.Page<Post> findByCreateIdOrderByCreateTimeDesc(
|
||||
@Param("userId") Integer userId, org.springframework.data.domain.Pageable pageable);
|
||||
|
||||
long countByCreateId(Integer createId);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface RoleRepository extends JpaRepository<Role, Integer> {
|
||||
Role findByCode(String code);
|
||||
|
||||
Role findByCodeIgnoreCase(String code);
|
||||
|
||||
List<Role> findAllByOrderByIdAsc();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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> {
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.UpgradeLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface UpgradeLogRepository extends JpaRepository<UpgradeLog, Integer> {
|
||||
List<UpgradeLog> findAllByOrderByCreateTimeDesc();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.UserRole;
|
||||
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 UserRoleRepository extends JpaRepository<UserRole, Integer> {
|
||||
List<UserRole> findByUserId(Integer userId);
|
||||
|
||||
void deleteByUserId(Integer userId);
|
||||
|
||||
@Query(value = "select r.code from rbac_user_role ur join rbac_role r on ur.role_id = r.id where ur.user_id = :userId and r.enabled = 1",
|
||||
nativeQuery = true)
|
||||
List<String> findRoleCodesByUserId(@Param("userId") Integer userId);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.repository;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE User u SET u.commentsCount = COALESCE(u.commentsCount, 0) + 1 WHERE u.id = :id")
|
||||
void incrementCommentCount(@Param("id") Integer id);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE User u SET u.discussionsCount = COALESCE(u.discussionsCount, 0) + 1 WHERE u.id = :id")
|
||||
void incrementDiscussionCount(@Param("id") Integer id);
|
||||
|
||||
/** Top active users by comment count (for the ranking sidebar). Shows all users if counts are 0. */
|
||||
@Query("FROM User u ORDER BY COALESCE(u.commentsCount, 0) DESC")
|
||||
List<User> findTop10ActiveUsers(org.springframework.data.domain.Pageable pageable);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE User u SET u.score = GREATEST(0, COALESCE(u.score, 0) + :delta), " +
|
||||
"u.level = CASE " +
|
||||
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 2000 THEN 5 " +
|
||||
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 1000 THEN 4 " +
|
||||
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 500 THEN 3 " +
|
||||
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 200 THEN 2 " +
|
||||
" WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 50 THEN 1 " +
|
||||
" ELSE 0 END " +
|
||||
"WHERE u.id = :id")
|
||||
void addScore(@Param("id") Integer id, @Param("delta") int delta);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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> fail(int code, String msg) {
|
||||
return new ApiResponse<>(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(ResponseCode.SERVICE_ERROR.getCode(), message, null);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.AuditLog;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.repository.AuditLogRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Service for recording admin operations in audit log.
|
||||
*/
|
||||
@Service
|
||||
public class AuditLogService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
|
||||
public AuditLogService(AuditLogRepository auditLogRepository) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(Integer adminId, String adminName, String actionType, String targetType,
|
||||
Integer targetId, String description, String status) {
|
||||
AuditLog log = new AuditLog();
|
||||
log.setAdminId(adminId);
|
||||
log.setAdminName(adminName);
|
||||
log.setActionType(actionType);
|
||||
log.setTargetType(targetType);
|
||||
log.setTargetId(targetId);
|
||||
log.setDescription(description);
|
||||
log.setStatus(status == null ? "success" : status);
|
||||
log.setCreateTime(new Date());
|
||||
auditLogRepository.save(log);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(Integer adminId, String adminName, String actionType, String targetType,
|
||||
Integer targetId, String description) {
|
||||
log(adminId, adminName, actionType, targetType, targetId, description, "success");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(UserInfo user, String actionType, String targetType,
|
||||
Integer targetId, String description) {
|
||||
Integer userId = user == null ? null : user.getId();
|
||||
String userName = user == null ? "unknown" : user.getUsername();
|
||||
log(userId, userName, actionType, targetType, targetId, description, "success");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void log(UserInfo user, String actionType, String targetType,
|
||||
Integer targetId, String description, String status) {
|
||||
Integer userId = user == null ? null : user.getId();
|
||||
String userName = user == null ? "unknown" : user.getUsername();
|
||||
log(userId, userName, actionType, targetType, targetId, description, status);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
|
||||
void incrementViewCount(Integer id);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
/**
|
||||
* Vote (like / dislike) service for discussions and posts.
|
||||
*
|
||||
* <p>Toggle semantics:
|
||||
* <ul>
|
||||
* <li>If the user has not voted → create a new vote with the requested action.</li>
|
||||
* <li>If the user voted with the same action → cancel the vote (toggle off).</li>
|
||||
* <li>If the user voted with a different action → switch the vote.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the user's current action after the operation: "like", "dislike", or "none" (cancelled)
|
||||
*/
|
||||
public interface ILikeCollectService {
|
||||
|
||||
/** Vote on a discussion. {@code action} must be "like" or "dislike". */
|
||||
String voteDiscussion(Integer userId, Integer discussionId, String action);
|
||||
|
||||
/** Vote on a post (reply). {@code action} must be "like" or "dislike". */
|
||||
String votePost(Integer userId, Integer postId, String action);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IMessageService {
|
||||
|
||||
/** Send a message from sender to receiver. */
|
||||
Message send(Integer senderId, Integer receiverId, String content);
|
||||
|
||||
/** All messages in a conversation (ordered oldest→newest). */
|
||||
List<Message> getConversation(Integer userId, Integer otherId);
|
||||
|
||||
/** Inbox: latest message per conversation, sorted by latest activity (max 50). */
|
||||
List<Message> getInbox(Integer userId);
|
||||
|
||||
/** Unread message count for a user. */
|
||||
long countUnread(Integer userId);
|
||||
|
||||
/** Mark an entire conversation as read for the current user. */
|
||||
void markConversationRead(Integer userId, Integer otherId);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
public interface INotificationService {
|
||||
|
||||
/** Create a notification for a user. */
|
||||
Notification create(Integer toUserId, Integer fromUserId, String type, String title, String content,
|
||||
Integer relatedId, String relatedType);
|
||||
|
||||
/** Convenience: create a "reply" notification. */
|
||||
void notifyReply(Integer toUserId, Integer fromUserId, Integer discussionId, String discussionTitle);
|
||||
|
||||
/** Convenience: create a "like" notification for a post. */
|
||||
void notifyLike(Integer toUserId, Integer fromUserId, Integer postId, Integer discussionId);
|
||||
|
||||
/** Unread notification count for a user. */
|
||||
long countUnread(Integer userId);
|
||||
|
||||
/** Paged notification list for a user. */
|
||||
Page<Notification> listByUser(Integer userId, int page, int size);
|
||||
|
||||
void markAllRead(Integer userId);
|
||||
|
||||
void markOneRead(Integer notificationId, Integer userId);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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 findPostCustomById(Integer id, String sort);
|
||||
|
||||
Post findOneByid(Integer parentId);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
/**
|
||||
* Service for awarding activity points and recomputing user levels.
|
||||
*
|
||||
* <p>Level thresholds (score → level):
|
||||
* <ul>
|
||||
* <li>0 – 49 → 0 新手</li>
|
||||
* <li>50 – 199 → 1 学徒</li>
|
||||
* <li>200 – 499 → 2 熟手</li>
|
||||
* <li>500 – 999 → 3 达人</li>
|
||||
* <li>1000 – 1999 → 4 专家</li>
|
||||
* <li>2000+ → 5 大神</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface IScoreService {
|
||||
|
||||
/** Award (or deduct) {@code delta} points to the user and refresh their level. */
|
||||
void addScore(Integer userId, int delta);
|
||||
|
||||
/** Compute the level (0-5) corresponding to a given score. */
|
||||
static int computeLevel(int score) {
|
||||
if (score >= 2000) return 5;
|
||||
if (score >= 1000) return 4;
|
||||
if (score >= 500) return 3;
|
||||
if (score >= 200) return 2;
|
||||
if (score >= 50) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Human-readable name for a level (0-5). */
|
||||
static String levelName(int level) {
|
||||
return switch (level) {
|
||||
case 1 -> "学徒";
|
||||
case 2 -> "熟手";
|
||||
case 3 -> "达人";
|
||||
case 4 -> "专家";
|
||||
case 5 -> "大神";
|
||||
default -> "新手";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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);
|
||||
|
||||
List<User> findAllById(Iterable<Integer> ids);
|
||||
|
||||
User update(User user, Integer id);
|
||||
|
||||
List<User> getTopActiveUsers(int limit);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Lightweight IP region service.
|
||||
*
|
||||
* <p>Current implementation distinguishes local/private addresses from external ones.
|
||||
* To display accurate province-level location (e.g. "北京", "广东"), replace the
|
||||
* {@code lookupExternal} method with an ip2region XDB lookup or similar offline database.
|
||||
*
|
||||
* <p>Upgrade path:
|
||||
* <ol>
|
||||
* <li>Add {@code org.lionsoul:ip2region:2.7.0} to pom.xml</li>
|
||||
* <li>Place {@code ip2region.xdb} (from the ip2region GitHub release) under
|
||||
* {@code src/main/resources/}</li>
|
||||
* <li>Replace {@code lookupExternal()} with:
|
||||
* {@code Searcher.newWithFileOnly(xdbPath).searchByStr(ip)}</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Service
|
||||
public class IpRegionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(IpRegionService.class);
|
||||
|
||||
private static final Set<String> LOCAL_PREFIXES = Set.of(
|
||||
"127.", "0.", "::1", "0:0:0:0:0:0:0:1"
|
||||
);
|
||||
|
||||
private static final Set<String> PRIVATE_PREFIXES = Set.of(
|
||||
"10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.",
|
||||
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
|
||||
"172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31."
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a display-friendly region label for the given IP address.
|
||||
*
|
||||
* @param ip client IP string (IPv4 or IPv6)
|
||||
* @return region label, e.g. "本地", "局域网", or "未知地区"
|
||||
*/
|
||||
public String getRegion(String ip) {
|
||||
if (ip == null || ip.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
for (String prefix : LOCAL_PREFIXES) {
|
||||
if (ip.startsWith(prefix)) {
|
||||
return "本地";
|
||||
}
|
||||
}
|
||||
for (String prefix : PRIVATE_PREFIXES) {
|
||||
if (ip.startsWith(prefix)) {
|
||||
return "局域网";
|
||||
}
|
||||
}
|
||||
return lookupExternal(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to integrate an offline IP database (e.g. ip2region).
|
||||
* Default implementation returns "未知地区" as a placeholder.
|
||||
*/
|
||||
protected String lookupExternal(String ip) {
|
||||
// TODO: integrate ip2region XDB for accurate province-level lookup
|
||||
return "未知地区";
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||
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;
|
||||
|
||||
@Autowired
|
||||
private UsersRepository usersRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<Discussion> getAllList() {
|
||||
return discussionsRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Discussion insert(Discussion discussion) {
|
||||
Discussion saved = discussionsRepository.save(discussion);
|
||||
// Maintain user discussions count
|
||||
if (discussion.getCreateId() != null) {
|
||||
usersRepository.incrementDiscussionCount(discussion.getCreateId());
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Discussion findOne(Integer id) {
|
||||
return discussionsRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void incrementViewCount(Integer id) {
|
||||
discussionsRepository.incrementViewCount(id);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.LikeCollect;
|
||||
import com.yaoyuan.jiscuss.entity.Post;
|
||||
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.service.ILikeCollectService;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import com.yaoyuan.jiscuss.entity.Discussion;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class LikeCollectServiceImpl implements ILikeCollectService {
|
||||
|
||||
@Autowired
|
||||
private LikeCollectRepository likeCollectRepository;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private com.yaoyuan.jiscuss.service.IScoreService scoreService;
|
||||
|
||||
@Autowired
|
||||
private com.yaoyuan.jiscuss.repository.UsersRepository usersRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public String voteDiscussion(Integer userId, Integer discussionId, String action) {
|
||||
// Look up discussion author once for score updates
|
||||
com.yaoyuan.jiscuss.entity.Discussion disc = discussionsRepository.findById(discussionId).orElse(null);
|
||||
Integer authorId = (disc != null) ? disc.getStartUserId() : null;
|
||||
|
||||
Optional<LikeCollect> existing = likeCollectRepository.findDiscussionVote(userId, discussionId);
|
||||
if (existing.isPresent()) {
|
||||
LikeCollect vote = existing.get();
|
||||
if (vote.getAction().equals(action)) {
|
||||
// Toggle off: cancel the same action
|
||||
likeCollectRepository.delete(vote);
|
||||
adjustDiscussion(discussionId, action, -1);
|
||||
// Reverse score for the author (only likes affect score)
|
||||
if ("like".equals(action) && authorId != null && !authorId.equals(userId)) {
|
||||
scoreService.addScore(authorId, -2);
|
||||
}
|
||||
return "none";
|
||||
} else {
|
||||
// Switch action: undo old, apply new
|
||||
adjustDiscussion(discussionId, vote.getAction(), -1);
|
||||
vote.setAction(action);
|
||||
likeCollectRepository.save(vote);
|
||||
adjustDiscussion(discussionId, action, +1);
|
||||
// Switched from like→dislike: deduct; dislike→like: award
|
||||
if (authorId != null && !authorId.equals(userId)) {
|
||||
if ("like".equals(action)) {
|
||||
scoreService.addScore(authorId, +2);
|
||||
} else {
|
||||
scoreService.addScore(authorId, -2);
|
||||
}
|
||||
}
|
||||
return action;
|
||||
}
|
||||
} else {
|
||||
// New vote
|
||||
LikeCollect vote = new LikeCollect();
|
||||
vote.setUserId(userId);
|
||||
vote.setDiscussionId(discussionId);
|
||||
vote.setType("discussion");
|
||||
vote.setAction(action);
|
||||
vote.setCreateId(userId);
|
||||
vote.setCreateTime(new Date());
|
||||
likeCollectRepository.save(vote);
|
||||
adjustDiscussion(discussionId, action, +1);
|
||||
// Award score to discussion author on new like
|
||||
if ("like".equals(action) && authorId != null && !authorId.equals(userId)) {
|
||||
scoreService.addScore(authorId, +2);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public String votePost(Integer userId, Integer postId, String action) {
|
||||
Optional<LikeCollect> existing = likeCollectRepository.findPostVote(userId, postId);
|
||||
if (existing.isPresent()) {
|
||||
LikeCollect vote = existing.get();
|
||||
if (vote.getAction().equals(action)) {
|
||||
likeCollectRepository.delete(vote);
|
||||
adjustPost(postId, action, -1);
|
||||
// Reverse score on unlike
|
||||
if ("like".equals(action)) {
|
||||
Post post = postsRepository.findById(postId).orElse(null);
|
||||
if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) {
|
||||
scoreService.addScore(post.getCreateId(), -2);
|
||||
}
|
||||
}
|
||||
return "none";
|
||||
} else {
|
||||
adjustPost(postId, vote.getAction(), -1);
|
||||
vote.setAction(action);
|
||||
likeCollectRepository.save(vote);
|
||||
adjustPost(postId, action, +1);
|
||||
// Switched vote direction: adjust score accordingly
|
||||
Post post = postsRepository.findById(postId).orElse(null);
|
||||
if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) {
|
||||
if ("like".equals(action)) {
|
||||
scoreService.addScore(post.getCreateId(), +2);
|
||||
} else {
|
||||
scoreService.addScore(post.getCreateId(), -2);
|
||||
}
|
||||
}
|
||||
return action;
|
||||
}
|
||||
} else {
|
||||
LikeCollect vote = new LikeCollect();
|
||||
vote.setUserId(userId);
|
||||
vote.setPostId(postId);
|
||||
vote.setType("post");
|
||||
vote.setAction(action);
|
||||
vote.setCreateId(userId);
|
||||
vote.setCreateTime(new Date());
|
||||
likeCollectRepository.save(vote);
|
||||
adjustPost(postId, action, +1);
|
||||
// Award score to post author on new like; also send notification
|
||||
if ("like".equals(action)) {
|
||||
Post post = postsRepository.findById(postId).orElse(null);
|
||||
if (post != null && post.getCreateId() != null) {
|
||||
notificationService.notifyLike(post.getCreateId(), userId, postId, post.getDiscussionId());
|
||||
if (!post.getCreateId().equals(userId)) {
|
||||
scoreService.addScore(post.getCreateId(), +2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustDiscussion(Integer id, String action, int delta) {
|
||||
if ("like".equals(action)) {
|
||||
discussionsRepository.adjustLikeCount(id, delta);
|
||||
} else {
|
||||
discussionsRepository.adjustDislikeCount(id, delta);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustPost(Integer id, String action, int delta) {
|
||||
if ("like".equals(action)) {
|
||||
postsRepository.adjustLikeCount(id, delta);
|
||||
} else {
|
||||
postsRepository.adjustDislikeCount(id, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Message;
|
||||
import com.yaoyuan.jiscuss.repository.MessageRepository;
|
||||
import com.yaoyuan.jiscuss.service.IMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MessageServiceImpl implements IMessageService {
|
||||
|
||||
@Autowired
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
private String buildConversationId(Integer a, Integer b) {
|
||||
return Math.min(a, b) + "_" + Math.max(a, b);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Message send(Integer senderId, Integer receiverId, String content) {
|
||||
Message msg = new Message();
|
||||
msg.setSenderId(senderId);
|
||||
msg.setReceiverId(receiverId);
|
||||
msg.setContent(content);
|
||||
msg.setIsRead(false);
|
||||
msg.setCreateTime(new Date());
|
||||
msg.setConversationId(buildConversationId(senderId, receiverId));
|
||||
return messageRepository.save(msg);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<Message> getConversation(Integer userId, Integer otherId) {
|
||||
String convId = buildConversationId(userId, otherId);
|
||||
List<Message> msgs = messageRepository.findByConversationIdOrderByCreateTimeDesc(convId);
|
||||
// reverse to get oldest-first order
|
||||
java.util.Collections.reverse(msgs);
|
||||
return msgs;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<Message> getInbox(Integer userId) {
|
||||
// Fetch sent and received separately, merge, then deduplicate by conversationId
|
||||
List<Message> sent = messageRepository.findBySenderIdOrderByCreateTimeDesc(userId);
|
||||
List<Message> received = messageRepository.findByReceiverIdOrderByCreateTimeDesc(userId);
|
||||
|
||||
// Merge both lists, sort by createTime descending
|
||||
java.util.List<Message> all = new java.util.ArrayList<>();
|
||||
all.addAll(sent);
|
||||
all.addAll(received);
|
||||
all.sort((a, b) -> {
|
||||
if (a.getCreateTime() == null) return 1;
|
||||
if (b.getCreateTime() == null) return -1;
|
||||
return b.getCreateTime().compareTo(a.getCreateTime());
|
||||
});
|
||||
|
||||
// Keep only the latest message per conversation
|
||||
java.util.LinkedHashMap<String, Message> seen = new java.util.LinkedHashMap<>();
|
||||
for (Message m : all) {
|
||||
String cid = (m.getConversationId() != null && !m.getConversationId().isEmpty())
|
||||
? m.getConversationId()
|
||||
: Math.min(m.getSenderId(), m.getReceiverId()) + "_" + Math.max(m.getSenderId(), m.getReceiverId());
|
||||
seen.putIfAbsent(cid, m);
|
||||
}
|
||||
return new java.util.ArrayList<>(seen.values());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public long countUnread(Integer userId) {
|
||||
return messageRepository.countByReceiverIdAndIsReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markConversationRead(Integer userId, Integer otherId) {
|
||||
messageRepository.markConversationReadForUser(buildConversationId(userId, otherId), userId);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.Notification;
|
||||
import com.yaoyuan.jiscuss.repository.NotificationRepository;
|
||||
import com.yaoyuan.jiscuss.service.INotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class NotificationServiceImpl implements INotificationService {
|
||||
|
||||
@Autowired
|
||||
private NotificationRepository notificationRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Notification create(Integer toUserId, Integer fromUserId, String type, String title,
|
||||
String content, Integer relatedId, String relatedType) {
|
||||
// Do not notify yourself
|
||||
if (toUserId != null && toUserId.equals(fromUserId)) {
|
||||
return null;
|
||||
}
|
||||
Notification n = new Notification();
|
||||
n.setUserId(toUserId);
|
||||
n.setFromUserId(fromUserId);
|
||||
n.setType(type);
|
||||
n.setTitle(title);
|
||||
n.setContent(content);
|
||||
n.setRelatedId(relatedId);
|
||||
n.setRelatedType(relatedType);
|
||||
n.setIsRead(false);
|
||||
n.setCreateTime(new Date());
|
||||
return notificationRepository.save(n);
|
||||
}
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void notifyReply(Integer toUserId, Integer fromUserId, Integer discussionId, String discussionTitle) {
|
||||
create(toUserId, fromUserId, "reply",
|
||||
"有人回复了你的帖子",
|
||||
"在话题《" + discussionTitle + "》中回复了你",
|
||||
discussionId, "discussion");
|
||||
}
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void notifyLike(Integer toUserId, Integer fromUserId, Integer postId, Integer discussionId) {
|
||||
create(toUserId, fromUserId, "like",
|
||||
"有人赞了你的回复",
|
||||
null, postId, "post");
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public long countUnread(Integer userId) {
|
||||
return notificationRepository.countByUserIdAndIsReadFalse(userId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Page<Notification> listByUser(Integer userId, int page, int size) {
|
||||
return notificationRepository.findByUserIdOrderByCreateTimeDesc(
|
||||
userId, PageRequest.of(page, size));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markAllRead(Integer userId) {
|
||||
notificationRepository.markAllReadByUserId(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void markOneRead(Integer notificationId, Integer userId) {
|
||||
notificationRepository.markReadById(notificationId, userId);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
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.DiscussionsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.PostsRepository;
|
||||
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class PostsServiceImpl implements IPostsService {
|
||||
|
||||
@Autowired
|
||||
private PostsRepository postsRepository;
|
||||
|
||||
@Autowired
|
||||
private DiscussionsRepository discussionsRepository;
|
||||
|
||||
@Autowired
|
||||
private UsersRepository usersRepository;
|
||||
|
||||
@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, String sort) {
|
||||
// Choose sort order for top-level posts
|
||||
List<Map<String, Object>> firstposts = switch (sort == null ? "newest" : sort) {
|
||||
case "oldest" -> postsRepository.findAllByDIdAndparentIdNullOldest(id);
|
||||
case "hot" -> postsRepository.findAllByDIdAndparentIdNullHot(id);
|
||||
default -> postsRepository.findAllByDIdAndparentIdNullNewest(id); // newest (root only, create_time desc)
|
||||
};
|
||||
List<PostCustom> firstpostCustomList = PostCommonUtil.getNewPostsObjMap(firstposts);
|
||||
|
||||
//查询id为1且parentId不为null的评论
|
||||
List<Map<String, Object>> thenposts = postsRepository.findAllByDIdAndparentIdNotNull(id);
|
||||
List<PostCustom> thenpostCustomList = PostCommonUtil.getNewPostsObjMap(thenposts);
|
||||
|
||||
//新建一个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);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Post insert(Post post) {
|
||||
// Auto-assign floor number within the same transaction to ensure consistency
|
||||
if (post.getDiscussionId() != null) {
|
||||
int nextNumber = postsRepository.findMaxNumberByDiscussionId(post.getDiscussionId()) + 1;
|
||||
post.setNumber(nextNumber);
|
||||
}
|
||||
Post saved = postsRepository.save(post);
|
||||
|
||||
// Maintain discussion comment count and last activity
|
||||
if (post.getDiscussionId() != null) {
|
||||
discussionsRepository.incrementCommentCount(post.getDiscussionId(), post.getCreateId(), new Date());
|
||||
}
|
||||
// Maintain user comment count
|
||||
if (post.getCreateId() != null) {
|
||||
usersRepository.incrementCommentCount(post.getCreateId());
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.repository.UsersRepository;
|
||||
import com.yaoyuan.jiscuss.service.IScoreService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ScoreServiceImpl implements IScoreService {
|
||||
|
||||
@Autowired
|
||||
private UsersRepository usersRepository;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@CacheEvict(value = "user", key = "#userId")
|
||||
public void addScore(Integer userId, int delta) {
|
||||
if (userId == null || delta == 0) return;
|
||||
usersRepository.addScore(userId, delta);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.service.impl;
|
||||
|
||||
import com.yaoyuan.jiscuss.entity.User;
|
||||
import com.yaoyuan.jiscuss.entity.UserInfo;
|
||||
import com.yaoyuan.jiscuss.repository.UserRoleRepository;
|
||||
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;
|
||||
|
||||
@Autowired
|
||||
private UserRoleRepository userRoleRepository;
|
||||
|
||||
@Override
|
||||
public UserInfo loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User userInfo = userInfoService.getByUsername(username);
|
||||
if (userInfo == null) {
|
||||
throw new UsernameNotFoundException("用户不存在: " + username);
|
||||
}
|
||||
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
List<String> roleCodes = userRoleRepository.findRoleCodesByUserId(userInfo.getId());
|
||||
if (roleCodes == null || roleCodes.isEmpty()) {
|
||||
// Compatibility fallback for legacy data before RBAC migration.
|
||||
String role = (userInfo.getLevel() != null && userInfo.getLevel() >= 1) ? "ADMIN" : "USER";
|
||||
roleCodes = List.of(role);
|
||||
}
|
||||
for (String roleCode : roleCodes) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));
|
||||
}
|
||||
|
||||
return new UserInfo(
|
||||
authorities,
|
||||
userInfo.getId(),
|
||||
userInfo.getPassword(), // Must be a BCrypt hash (see V2 migration)
|
||||
userInfo.getUsername(),
|
||||
userInfo.getPhone()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
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.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<User> findAllById(Iterable<Integer> ids) {
|
||||
return usersRepository.findAllById(ids);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public List<User> getTopActiveUsers(int limit) {
|
||||
return usersRepository.findTop10ActiveUsers(
|
||||
org.springframework.data.domain.PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.util;
|
||||
|
||||
/**
|
||||
* @author Chuyaoyuan
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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,58 +0,0 @@
|
||||
package com.yaoyuan.jiscuss.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Lightweight User-Agent parser.
|
||||
* Extracts a human-readable browser name and major version without
|
||||
* importing a heavy third-party library.
|
||||
*
|
||||
* <p>Detection order matters: Edge must be checked before Chrome, and
|
||||
* Chrome before Safari, because UA strings commonly contain multiple tokens.
|
||||
*/
|
||||
public final class UserAgentUtils {
|
||||
|
||||
private UserAgentUtils() {}
|
||||
|
||||
private static final Pattern EDGE = Pattern.compile("Edg(?:e|A|iOS)?/([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FIREFOX = Pattern.compile("Firefox/([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern CHROME = Pattern.compile("Chrome/([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SAFARI = Pattern.compile("Version/([\\d.]+).*Safari", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern OPERA = Pattern.compile("OPR/([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern IE = Pattern.compile("(?:MSIE |Trident/.*rv:)([\\d.]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Returns a short browser description, e.g. "Chrome 120", "Firefox 121", "Safari 17".
|
||||
* Returns "未知浏览器" when detection fails.
|
||||
*
|
||||
* @param userAgent the raw User-Agent header value
|
||||
* @return display string, never null
|
||||
*/
|
||||
public static String parseBrowser(String userAgent) {
|
||||
if (userAgent == null || userAgent.isBlank()) {
|
||||
return "未知浏览器";
|
||||
}
|
||||
// Order matters: Edge contains 'Chrome', Chrome contains 'Safari'
|
||||
String r;
|
||||
if ((r = match(EDGE, userAgent, "Edge")) != null) return r;
|
||||
if ((r = match(OPERA, userAgent, "Opera")) != null) return r;
|
||||
if ((r = match(FIREFOX, userAgent, "Firefox")) != null) return r;
|
||||
if ((r = match(CHROME, userAgent, "Chrome")) != null) return r;
|
||||
if ((r = match(SAFARI, userAgent, "Safari")) != null) return r;
|
||||
if ((r = match(IE, userAgent, "IE")) != null) return r;
|
||||
return "未知浏览器";
|
||||
}
|
||||
|
||||
private static String match(Pattern pattern, String ua, String name) {
|
||||
Matcher m = pattern.matcher(ua);
|
||||
if (m.find()) {
|
||||
String version = m.group(1);
|
||||
// Only keep major version number
|
||||
int dotIdx = version.indexOf('.');
|
||||
String major = dotIdx > 0 ? version.substring(0, dotIdx) : version;
|
||||
return name + " " + major;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<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>
|
||||
@@ -0,0 +1,8 @@
|
||||
server.port: 80
|
||||
|
||||
solon.app:
|
||||
name: jiscuss
|
||||
group: jiscuss
|
||||
|
||||
# 可选: *,disk,cpu,jvm,memory,os,qps
|
||||
solon.health.detector: "*"
|
||||
@@ -1,63 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
@@ -1,57 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
@@ -1,3 +0,0 @@
|
||||
# Activate h2 (dev) profile by default.
|
||||
# Override via --spring.profiles.active=mysql for production.
|
||||
spring.profiles.active=h2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user