first commit
This commit is contained in:
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Doghouse - 基于 Springboot 的个人博客系统
|
||||
|
||||
## 本地 avif 和 webp 转换
|
||||
|
||||
下载对应二进制文件,并放在系统环境变量下,确保能通过 CLI 访问即可。
|
||||
|
||||
## 本地 Mermaid 流程图转换
|
||||
|
||||
安装 node,而后
|
||||
|
||||
```
|
||||
npm i -g @mermaid-js/mermaid-cli
|
||||
```
|
||||
|
||||
## 疑难解决
|
||||
|
||||
如果出现:`url` 未配置之类的,可能是 `devtools` 热重载出问题,需要 maven clean 以后再 maven install;
|
||||
|
||||
如果出现 cglib 相关的 java.lang.IncompatibleClassChangeError,请检查 cglib 支持的最高版本的 asm,并手动指定在 dependency 中。有时候 spring-boot 会指定额外的 asm 版本,导致不兼容。
|
||||
|
||||
如果出现 InaccessibleObjectException: Unable to make protected final java.lang.Class,请添加 --add-opens=java.base/java.lang=ALL-UNNAMED 在运行前。
|
||||
|
||||
338
pom.xml
Normal file
338
pom.xml
Normal file
@@ -0,0 +1,338 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>me.qwq</groupId>
|
||||
<artifactId>doghouse</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Doghouse</name>
|
||||
<description>Private Blog System for Doghole</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>22</java.version>
|
||||
<maven.compiler.source>22</maven.compiler.source>
|
||||
<maven.compiler.target>22</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring session redis 依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson</artifactId>
|
||||
<version>3.27.1</version>
|
||||
</dependency>
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.26.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
<scope>compile</scope>
|
||||
<exclusions>
|
||||
<!-- 解决 Invalid value type for attribute 'factoryBeanObjectType':
|
||||
java.lang.String 问题 -->
|
||||
<exclusion>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<groupId>org.mybatis</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.yulichang</groupId>
|
||||
<artifactId>mybatis-plus-join-boot-starter</artifactId>
|
||||
<version>1.4.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>1.11.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.14.0</version><!--$NO-MVN-MAN-VER$-->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-all</artifactId>
|
||||
<version>0.62.2</version>
|
||||
<exclusions>
|
||||
<!-- 移除不需要的组件 -->
|
||||
<exclusion>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-pdf-converter</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.23</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<version>2.14.1</version><!--$NO-MVN-MAN-VER$-->
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15to18 -->
|
||||
<!-- sha224 哈希套件 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<version>1.77</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>com.github.penggle</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp -->
|
||||
<!-- WEBP 图像读取支持,用于判断上传文件的长宽 -->
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-webp</artifactId>
|
||||
<version>3.10.1</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.17.0</version><!--$NO-MVN-MAN-VER$-->
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.rometools/rome -->
|
||||
<!-- Atom/RSS -->
|
||||
<dependency>
|
||||
<groupId>com.rometools</groupId>
|
||||
<artifactId>rome</artifactId>
|
||||
<version>1.16.0</version>
|
||||
</dependency>
|
||||
<!-- 邮件 -->
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>javax.mail</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
<!-- quartz 定时任务 -->
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<groupId>org.slf4j</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 给头像做反代 -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 给头像做反代 -->
|
||||
<dependency>
|
||||
<groupId>me.qwq</groupId>
|
||||
<artifactId>dogface</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.4.8-jre</version>
|
||||
</dependency>
|
||||
<!-- IP 归属地 -->
|
||||
<dependency>
|
||||
<groupId>com.maxmind.geoip2</groupId>
|
||||
<artifactId>geoip2</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-info</id>
|
||||
<goals>
|
||||
<goal>build-info</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<additionalProperties>
|
||||
<java.version>${java.version}</java.version>
|
||||
<description>${project.description}</description>
|
||||
</additionalProperties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>
|
||||
--add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
--add-opens=java.base/java.util=ALL-UNNAMED
|
||||
</argLine>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<excludes>
|
||||
<exclude>/static/blog/plugins/doghouse/js/scrollLock/demo/**</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
</project>
|
||||
40
src/main/java/me/qwq/doghouse/BlogApplication.java
Normal file
40
src/main/java/me/qwq/doghouse/BlogApplication.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package me.qwq.doghouse;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
|
||||
@MapperScan("me.qwq.doghouse.dao")
|
||||
@SpringBootApplication
|
||||
@EnableAsync(proxyTargetClass=true)
|
||||
@EnableCaching(proxyTargetClass=true)
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
public class BlogApplication {
|
||||
|
||||
static Boolean isDebug = null;
|
||||
|
||||
public static boolean isDebug () {
|
||||
|
||||
if (isDebug == null) {
|
||||
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
|
||||
isDebug = runtimeMXBean.getInputArguments().toString().contains("-agentlib:jdwp");
|
||||
}
|
||||
return isDebug;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
boolean isDebug = isDebug();
|
||||
|
||||
// 根据是否调试加载额外的 application-*.yml
|
||||
SpringApplication app = new SpringApplication(BlogApplication.class);
|
||||
app.setAdditionalProfiles(isDebug ? "debug" : "prod");
|
||||
|
||||
app.run(args);
|
||||
}
|
||||
}
|
||||
50
src/main/java/me/qwq/doghouse/annotation/CacheEvictTags.java
Normal file
50
src/main/java/me/qwq/doghouse/annotation/CacheEvictTags.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package me.qwq.doghouse.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 清除缓存的 Tags。注解在 String field 上,自动扫描所有以该 key 为注解的 @Cacheable 注解。
|
||||
* 注解在方法上,更新时自动驱逐。目前支持的有 Post、Comment。
|
||||
* 该方法并不影响用原生的 @Caching、@CacheEvict 来驱逐缓存
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CacheEvictTags {
|
||||
|
||||
Tag[] value();
|
||||
|
||||
public static enum Tag {
|
||||
/**
|
||||
* 更新 Post 时更新(指定 PostId)
|
||||
*/
|
||||
PostId,
|
||||
/**
|
||||
* 更新 Post 时更新(指定 PostPageName)
|
||||
*/
|
||||
PostName,
|
||||
|
||||
Next,
|
||||
|
||||
Prev,
|
||||
/**
|
||||
* 任意 Post 更新时都更新
|
||||
*/
|
||||
Posts,
|
||||
/**
|
||||
* 任意 Comment 更新时都更新
|
||||
*/
|
||||
Comments,
|
||||
/**
|
||||
* 任意 Meta 更新时都更新
|
||||
*/
|
||||
Metas,
|
||||
/**
|
||||
* 每日更新
|
||||
*/
|
||||
Daily;
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/me/qwq/doghouse/annotation/ConfigInfo.java
Normal file
33
src/main/java/me/qwq/doghouse/annotation/ConfigInfo.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package me.qwq.doghouse.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 将该注解用在配置上,给后台 ConfigController 自动渲染配置页用
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigInfo {
|
||||
/**
|
||||
* @return 存储在数据库中的配置键名
|
||||
*/
|
||||
String field();
|
||||
/**
|
||||
* @return 名称
|
||||
*/
|
||||
String name() default "配置";
|
||||
/**
|
||||
* <p>为 true 时,最终会调用其无参构造方法,若需要填充默认值的,请在无参构造器中填充</p>
|
||||
* @return 是否初始化默认值
|
||||
*/
|
||||
boolean initDefault() default false;
|
||||
/**
|
||||
* @return 是否提供后台控制
|
||||
*/
|
||||
boolean managed() default true;
|
||||
}
|
||||
15
src/main/java/me/qwq/doghouse/annotation/RequireSession.java
Normal file
15
src/main/java/me/qwq/doghouse/annotation/RequireSession.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package me.qwq.doghouse.annotation;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.annotation.ElementType;
|
||||
|
||||
/**
|
||||
* 注解在方法上确保该方法由请求链调用
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireSession {
|
||||
boolean create() default false;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package me.qwq.doghouse.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 注入前端的注解
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>
|
||||
* 注解在类上,以便在前端使用类的静态方法或常量值
|
||||
* <hr>
|
||||
* <code>
|
||||
* {@code @StaticAttribute}<br>
|
||||
* <b>public class</b> EncryptUtils {<br>
|
||||
* <b>public static</b> String <i>DEFAULT_ENC</i> = "md5";<br>
|
||||
* <b>public static</b> String md5(String plain);<br>
|
||||
* }
|
||||
* </code>
|
||||
* <hr>
|
||||
* 即可在 thymeleaf html 中使用该类的静态方法:
|
||||
* <br>
|
||||
* <br>
|
||||
* <code>
|
||||
* <input th:value="${EncryptUtils.DEFAULT_ENC + EncryptUtils.md5(password)}"/>
|
||||
* </code><br>
|
||||
* <br>
|
||||
* </li>
|
||||
* <li>
|
||||
* 又如:某类内成员字段是不可修改的枚举<p>
|
||||
* <hr>
|
||||
* <code>
|
||||
* {@code @Data}<br>
|
||||
* {@code @Component}<br>
|
||||
* <b>public class</b> Network {<br>
|
||||
* {@code @StaticAttribute("ProxyTypeEnum")}<br>
|
||||
* <b>private</b> Proxy.Type <i>proxyType</i> = Proxy.Type.<i><b>DIRECT</b></i>;<br>
|
||||
* }
|
||||
* </code>
|
||||
* <hr>
|
||||
* 即可在 thymeleaf html 中使用该枚举:<br>
|
||||
* <br>
|
||||
* <code>
|
||||
<option value="DIRECT" th:selected="${@network.proxyType == ProxyTypeEnum.DIRECT}">无</option><br>
|
||||
</code>
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.FIELD})
|
||||
public @interface StaticAttribute {
|
||||
String value() default "";
|
||||
}
|
||||
15
src/main/java/me/qwq/doghouse/annotation/UpdatePosts.java
Normal file
15
src/main/java/me/qwq/doghouse/annotation/UpdatePosts.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package me.qwq.doghouse.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface UpdatePosts {
|
||||
|
||||
boolean postViews() default false;
|
||||
boolean commentsCount() default false;
|
||||
|
||||
}
|
||||
146
src/main/java/me/qwq/doghouse/cache/CacheConstants.java
vendored
Normal file
146
src/main/java/me/qwq/doghouse/cache/CacheConstants.java
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import me.qwq.doghouse.annotation.CacheEvictTags;
|
||||
import me.qwq.doghouse.annotation.CacheEvictTags.Tag;
|
||||
|
||||
public class CacheConstants {
|
||||
|
||||
public static class Statistics {
|
||||
/** 每日更新、更新 Post 和 Comment 时更新 */
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments, Tag.Daily})
|
||||
public static final String DAILY_POST_AND_COMMENT_COUNTS = "StatisticsService::getPostAndCommentCountsWithDate";
|
||||
/** 每日更新 */
|
||||
@CacheEvictTags(Tag.Daily)
|
||||
public static final String DAY_SINCE = "StatisticsService::daySince";
|
||||
/** 更新 Post 时更新 */
|
||||
@CacheEvictTags(Tag.Posts)
|
||||
public static final String TOTAL_RELEASE_POSTS_COUNT = "StatisticsService::totalReleasedPostsCount";
|
||||
/** 更新 Post 和 Comment 时更新 */
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String TOTAL_COMMENTS_COUNT = "StatisticsService::totalCommentsCount";
|
||||
}
|
||||
|
||||
public static class Posts {
|
||||
static final String ID = "Posts::";
|
||||
|
||||
// Markdown
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String MARKDOWN_CONTENT_RENDERING = ID + "Markdown::renderPostContent";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String MARKDOWN_PREFACE_RENDERING = ID + "Markdown::renderPostPreface";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_CONTENT_TEXT = ID + "Markdown::getPostContentText";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_PREFACE_TEXT = ID + "Markdown::getPostPrefaceText";
|
||||
|
||||
// Statistic
|
||||
public static final String POST_STATISTIC = ID + "PostStatistic::getPostStatistic";
|
||||
|
||||
// CondictionPage
|
||||
/** 更新 Post/Meta 时更新 */
|
||||
@CacheEvictTags(Tag.Posts)
|
||||
public static final String CONDITION_PAGE = ID + "ConditionPage::conditionPage";
|
||||
|
||||
// PostService
|
||||
/// 与 ID 或 Name 有关(指定)
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_BY_ID = ID + "PostService::getById";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_BY_ID_LOGINED = ID + "PostService::getByIdLogined";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_BY_ID_UNLOGINED = ID + "PostService::getByIdUnlogined";
|
||||
|
||||
@CacheEvictTags(Tag.PostName)
|
||||
public static final String GET_BY_NAME_LOGINED = ID + "PostService::getByNameLogined";
|
||||
|
||||
@CacheEvictTags(Tag.PostName)
|
||||
public static final String GET_BY_NAME_UNLOGINED = ID + "PostService::getByNameUnlogined";
|
||||
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_SEO_DESCRIPTION = ID + "PostService::getSeoDescription";
|
||||
|
||||
/** 更新 Post 时更新(指定 PostID 的 Next 的 PostID) */
|
||||
@CacheEvictTags(Tag.Next)
|
||||
public static final String GET_PREVIOUS_LOGINED = ID + "PostService::getPreviousLogined";
|
||||
|
||||
/** 更新 Post 时更新(指定 PostID 的 Next 的 PostID) */
|
||||
@CacheEvictTags(Tag.Next)
|
||||
public static final String GET_PREVIOUS_UNLOGINED = ID + "PostService::getPreviousUnlogined";
|
||||
|
||||
/** 更新 Post 时更新(指定 PostID 的 Previous 的 PostID) */
|
||||
@CacheEvictTags(Tag.Prev)
|
||||
public static final String GET_NEXT_LOGINED = ID + "PostService::getNextLogined";
|
||||
|
||||
/** 更新 Post 时更新(指定 PostID 的 Previous 的 PostID) */
|
||||
@CacheEvictTags(Tag.Prev)
|
||||
public static final String GET_NEXT_UNLOGINED = ID + "PostService::getNextUnlogined";
|
||||
|
||||
/** 仅在更新 Post 浏览数时更新 **/
|
||||
@CacheEvictTags(Tag.PostId)
|
||||
public static final String GET_POST_VIEWS = ID + "PostService::getPostViews";
|
||||
|
||||
/// 与 ID 或 Name 无关(批量)
|
||||
|
||||
@CacheEvictTags(Tag.Posts)
|
||||
public static final String GET_LATEST_POSTS_LOGINED = ID + "PostService::getLatestPostsLogined";
|
||||
|
||||
@CacheEvictTags(Tag.Posts)
|
||||
public static final String GET_LATEST_POSTS_UNLOGINED = ID + "PostService::getLatestPostsUnlogined";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_POPULAR_POSTS_LOGINED = ID + "PostService::getPopularPostsLogined";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_POPULAR_POSTS_UNLOGINED = ID + "PostService::getPopularPostsUnlogined";
|
||||
|
||||
@CacheEvictTags(Tag.Posts)
|
||||
public static final String GET_JUMP_TO_PAGES_URL = ID + "PostService::getJumpToPagesUrl";
|
||||
|
||||
}
|
||||
|
||||
public static class Comments {
|
||||
static final String ID = "Comments::";
|
||||
|
||||
// Markdown
|
||||
@CacheEvictTags(Tag.Comments)
|
||||
public static final String MARKDOWN_CONTENT_RENDERING = ID + "Markdown::renderCommentContent";
|
||||
|
||||
@CacheEvictTags(Tag.Comments)
|
||||
public static final String GET_CONTENT_TEXT = ID + "Markdown::getCommentContentText";
|
||||
|
||||
// CommentService
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_BY_ID = ID + "CommentService::getById";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_LATEST_COMMENTS = ID + "CommentService::getLatestComments";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_LATEST_COMMENTS_WITH_LEVEL = ID + "CommentService::getLatestCommentsWithLevel";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String LIST_FROM_CHILD_TO_PARENT = ID + "CommentService::listFromChildToParent";
|
||||
|
||||
@CacheEvictTags({Tag.Posts, Tag.Comments})
|
||||
public static final String GET_JUMP_TO_URL = ID + "CommentService::getJumpToUrl";
|
||||
|
||||
// Other
|
||||
@CacheEvictTags(Tag.Comments)
|
||||
public static final String GET_POST_COMMENT_COUNT = ID + "CommentService::getPostCommentCount";
|
||||
}
|
||||
|
||||
public static class Metas {
|
||||
static final String ID = "Metas::";
|
||||
|
||||
// MetaService
|
||||
/** 更新 Post 和 Meta 时更新(批量) */
|
||||
@CacheEvictTags({Tag.Posts, Tag.Metas})
|
||||
public static final String LIST_META_COUNT = ID + "MetaService::listMetaCount";
|
||||
}
|
||||
|
||||
}
|
||||
263
src/main/java/me/qwq/doghouse/cache/CacheEvictTagsAspect.java
vendored
Normal file
263
src/main/java/me/qwq/doghouse/cache/CacheEvictTagsAspect.java
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.annotation.CacheEvictTags;
|
||||
import me.qwq.doghouse.annotation.CacheEvictTags.Tag;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CacheEvictTagsAspect {
|
||||
|
||||
@Autowired
|
||||
PostService postService;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
@Autowired
|
||||
CacheManager cacheManager;
|
||||
|
||||
List<Field> fields = new ArrayList<>();
|
||||
|
||||
Map<Tag, Set<String>> tagKeyCache = new HashMap<>();
|
||||
|
||||
Map<CacheEvictTags, Map<Tag, Boolean>> containsTagCache = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
// 使用 Reflections 查找所有添加了 CacheEvictTags 的 field
|
||||
Set<Field> fields = reflections.getFieldsAnnotatedWith(CacheEvictTags.class);
|
||||
this.fields = fields.stream()
|
||||
.filter(f -> f.getType().equals(String.class))
|
||||
.filter(f -> Modifier.isPublic(f.getModifiers()))
|
||||
.filter(f -> Modifier.isStatic(f.getModifiers())).toList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Around("@annotation(annotation)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, CacheEvictTags annotation) throws Throwable {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
if (args.length != 1) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
MethodSignature ms = (MethodSignature)joinPoint.getSignature();
|
||||
Method method = ms.getMethod();
|
||||
|
||||
ResolvableType argType = ResolvableType.forMethodParameter(method, 0);
|
||||
Class<?> rawClass = argType.resolve();
|
||||
|
||||
|
||||
|
||||
// 缓存 postName (若有)
|
||||
List<String> postNames = new ArrayList<>();
|
||||
List<Post> posts = new ArrayList<>();
|
||||
|
||||
if (rawClass == Post.class) {
|
||||
// 单 Post,驱逐 PostId, PostName, Prev, Next
|
||||
postNames.add(((Post)args[0]).getPostPageName());
|
||||
posts.add((Post)args[0]);
|
||||
}
|
||||
else if (Collection.class.isAssignableFrom(rawClass)) {
|
||||
rawClass = argType.getGeneric(0).resolve();
|
||||
if (rawClass == Post.class) {
|
||||
// 批量
|
||||
posts = new ArrayList<>((Collection<Post>)args[0]);
|
||||
postNames.addAll(posts.stream().map(p -> p.getPostPageName()).toList());
|
||||
}
|
||||
}
|
||||
else if (Wrapper.class.isAssignableFrom(rawClass)) {
|
||||
rawClass = argType.getGeneric(0).resolve();
|
||||
if (rawClass == Post.class) {
|
||||
// 批量
|
||||
posts = postService.list((Wrapper<Post>)args[0]);
|
||||
postNames.addAll(posts.stream().map(p -> p.getPostPageName()).toList());
|
||||
}
|
||||
}
|
||||
|
||||
Object result = joinPoint.proceed(args);
|
||||
|
||||
if ((method.getReturnType() == Boolean.class || method.getReturnType() == boolean.class) && (Boolean)result) {
|
||||
// 清除批量类缓存
|
||||
batchEvictIfContainsTag(annotation, Tag.Comments);
|
||||
batchEvictIfContainsTag(annotation, Tag.Posts);
|
||||
batchEvictIfContainsTag(annotation, Tag.Metas);
|
||||
evictPosts(posts, annotation);
|
||||
evictPostNames(postNames, annotation);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 Post 有关缓存, 包括 ID, Prev 和 Next, 在更新成功后驱逐
|
||||
* @param posts
|
||||
*/
|
||||
void evictPosts(Collection<Post> posts, CacheEvictTags anno) {
|
||||
|
||||
if (posts == null || posts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean containsPostId = containsTag(anno, Tag.PostId);
|
||||
boolean containsPrev = containsTag(anno, Tag.Prev);
|
||||
boolean containsNext = containsTag(anno, Tag.Next);
|
||||
|
||||
Set<String> postIdKeys = containsPostId ? getKeysWithTag(Tag.PostId) : Set.of();
|
||||
Set<String> prevKeys = containsPrev ? getKeysWithTag(Tag.Prev) : Set.of();
|
||||
Set<String> nextKeys = containsNext ? getKeysWithTag(Tag.Next) : Set.of();
|
||||
|
||||
|
||||
|
||||
for (Post post : posts) {
|
||||
for (String key : postIdKeys) {
|
||||
cacheManager.getCache(key).evictIfPresent(post.getPostId());
|
||||
}
|
||||
if (containsPrev) {
|
||||
for (Post prev : Arrays.asList(
|
||||
postService.cacheableGetPreviousLogined(post.getPostId()),
|
||||
postService.cacheableGetPreviousUnlogined(post.getPostId()))) {
|
||||
if (prev != null) {
|
||||
for (String key : prevKeys) {
|
||||
cacheManager.getCache(key).evictIfPresent(prev.getPostId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containsNext) {
|
||||
for (Post next : Arrays.asList(
|
||||
postService.cacheableGetNextLogined(post.getPostId()),
|
||||
postService.cacheableGetNextUnlogined(post.getPostId()))) {
|
||||
if (next != null) {
|
||||
for (String key : nextKeys) {
|
||||
cacheManager.getCache(key).evictIfPresent(next.getPostId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 PostName 有关缓存,在更新成功后驱逐,但其名称需要在更新前存储
|
||||
* @param postNames
|
||||
*/
|
||||
void evictPostNames(Collection<String> postNames, CacheEvictTags anno) {
|
||||
|
||||
if (postNames == null || postNames.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containsTag(anno, Tag.PostName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> postNameKeys = getKeysWithTag(Tag.PostName);
|
||||
|
||||
for (String postName : postNames) {
|
||||
for (String key : postNameKeys) {
|
||||
cacheManager.getCache(key).evictIfPresent(postName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清除指定 tag 的缓存
|
||||
* @param tag
|
||||
*/
|
||||
void batchEvictIfContainsTag(CacheEvictTags anno, Tag tag) {
|
||||
if (!containsTag(anno, tag)) return;
|
||||
Set<String> keys = getKeysWithTag(tag);
|
||||
for (String key : keys) {
|
||||
cacheManager.getCache(key).clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定 tag 获取被其注解的缓存 key
|
||||
* @param tag
|
||||
* @return
|
||||
*/
|
||||
Set<String> getKeysWithTag(Tag tag) {
|
||||
|
||||
if (tagKeyCache.containsKey(tag)) {
|
||||
return tagKeyCache.get(tag);
|
||||
}
|
||||
|
||||
Set<String> keys = new HashSet<>();
|
||||
|
||||
for (Field field : fields) {
|
||||
CacheEvictTags anno = field.getAnnotation(CacheEvictTags.class);
|
||||
if (!containsTag(anno, tag)) continue;
|
||||
try {
|
||||
String key = (String)field.get(null);
|
||||
if (key == null) continue;
|
||||
keys.add(key);
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
log.warn("无法获取被 Tag: {} 注解的 field: {}", tag, field);
|
||||
}
|
||||
}
|
||||
tagKeyCache.put(tag, keys);
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定 CacheEvictTags 是否包含 Tag
|
||||
* @param anno
|
||||
* @param tag
|
||||
* @return
|
||||
*/
|
||||
boolean containsTag(CacheEvictTags anno, Tag tag) {
|
||||
if (anno == null || tag == null) return false;
|
||||
return containsTagCache.computeIfAbsent(anno,
|
||||
m -> new HashMap<Tag, Boolean>()
|
||||
).computeIfAbsent(tag, b -> {
|
||||
Set<Tag> setA = EnumSet.copyOf(Arrays.asList(anno.value()));
|
||||
return !setA.isEmpty() && setA.contains(tag);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
93
src/main/java/me/qwq/doghouse/cache/CacheEvictionJob.java
vendored
Normal file
93
src/main/java/me/qwq/doghouse/cache/CacheEvictionJob.java
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.redis.cache.CacheKeyPrefix;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.enums.CacheEvictionSpan;
|
||||
import me.qwq.doghouse.task.TaskConstants;
|
||||
|
||||
/**
|
||||
* 定时清除缓存任务
|
||||
* @author Doghole
|
||||
* @since 2024-01-30
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CacheEvictionJob implements Job {
|
||||
|
||||
@Autowired
|
||||
RedissonClient redis;
|
||||
|
||||
@Autowired
|
||||
CacheProperties cacheProperties;
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
static CacheKeyPrefix prefix;
|
||||
|
||||
static final CacheEvictionTask DAILY_ANNO = new CacheEvictionTask() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return CacheEvictionTask.class;
|
||||
}
|
||||
@Override
|
||||
public CacheEvictionSpan value() {
|
||||
return CacheEvictionSpan.DAILY;
|
||||
}
|
||||
};
|
||||
|
||||
@PostConstruct
|
||||
void onPostConstruct() {
|
||||
prefix = CacheKeyPrefix.prefixed(
|
||||
cacheProperties.getRedis().getKeyPrefix());
|
||||
}
|
||||
|
||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
||||
String triggerName = jobExecutionContext.getTrigger().getKey().getName();
|
||||
|
||||
Set<Method> methods = null;
|
||||
|
||||
if (TaskConstants.CACHE_DAILY_EVICTION_TRIGGER.equals(triggerName)) {
|
||||
methods = reflections.getMethodsAnnotatedWith(DAILY_ANNO);
|
||||
}
|
||||
|
||||
if (methods != null) {
|
||||
for (Method method : methods) {
|
||||
Cacheable cacheable = method.getAnnotation(Cacheable.class);
|
||||
if (cacheable == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] values = cacheable.value();
|
||||
if (values.length == 0) {
|
||||
values = cacheable.cacheNames();
|
||||
}
|
||||
for (String cacheName : values) {
|
||||
|
||||
String redisCacheName = prefix.compute(cacheName) + "*";
|
||||
|
||||
for (String name : redis.getKeys().getKeysByPattern(redisCacheName)) {
|
||||
redis.getBucket(name).deleteAsync();
|
||||
log.debug("Scheduler clear cacheName {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/java/me/qwq/doghouse/cache/CacheEvictionTask.java
vendored
Normal file
17
src/main/java/me/qwq/doghouse/cache/CacheEvictionTask.java
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import me.qwq.doghouse.enums.CacheEvictionSpan;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Repeatable(CacheEvictionTasks.class)
|
||||
public @interface CacheEvictionTask {
|
||||
|
||||
CacheEvictionSpan value() default CacheEvictionSpan.DAILY;
|
||||
}
|
||||
13
src/main/java/me/qwq/doghouse/cache/CacheEvictionTasks.java
vendored
Normal file
13
src/main/java/me/qwq/doghouse/cache/CacheEvictionTasks.java
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface CacheEvictionTasks {
|
||||
|
||||
CacheEvictionTask[] value();
|
||||
}
|
||||
65
src/main/java/me/qwq/doghouse/cache/PostViewsAndCommentsCountAspect.java
vendored
Normal file
65
src/main/java/me/qwq/doghouse/cache/PostViewsAndCommentsCountAspect.java
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package me.qwq.doghouse.cache;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import me.qwq.doghouse.annotation.UpdatePosts;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.pojo.dto.PageQuery;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
|
||||
/**
|
||||
* 对需要更新 PostViews/CommentsCount 的返回值进行修改注入<br>
|
||||
* 适用于:频繁更新某个特定值(此处为 PostViews 和 CommentsCount)后更新 Post
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class PostViewsAndCommentsCountAspect {
|
||||
|
||||
@Autowired
|
||||
PostService postService;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Around("@annotation(annotation)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, UpdatePosts annotation) throws Throwable {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
Object result = joinPoint.proceed(args);
|
||||
if (result == null) return result;
|
||||
if (!annotation.commentsCount() && !annotation.postViews()) return result;
|
||||
if (result.getClass() == PageQuery.class) {
|
||||
PageQuery<?> pageQuery = (PageQuery<?>)result;
|
||||
pageQuery.getRecords().forEach(post -> updatePost(post, annotation));
|
||||
return result;
|
||||
}
|
||||
else if (result instanceof List) {
|
||||
List<?> list = (List<?>)result;
|
||||
if (list.isEmpty() || list.get(0).getClass() != Post.class) return result;
|
||||
list.forEach(post -> updatePost(post, annotation));
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updatePost(Object post, UpdatePosts annotation) {
|
||||
if (annotation.commentsCount()) {
|
||||
Long postCommentCount = commentService.getPostCommentCount(((Post)post).getPostId());
|
||||
((Post)post).setCommentCount(postCommentCount);
|
||||
}
|
||||
if (annotation.postViews()) {
|
||||
Long postViews = postService.getPostViews(
|
||||
((Post)post).getPostId());
|
||||
((Post)post).setPostViews(postViews);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.config.DoghouseProperties;
|
||||
|
||||
/**
|
||||
* 重写数据库位置
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@DependsOn("doghouseProperties")
|
||||
public class DataSourcePostProcessor implements BeanPostProcessor {
|
||||
|
||||
private static final String RESOURCE_PATH = "blog.db";
|
||||
private static final String PREFIX = "jdbc:sqlite:";
|
||||
|
||||
@Autowired
|
||||
DoghouseProperties doghouseProperties;
|
||||
|
||||
/**
|
||||
* 配置数据库连接
|
||||
* <p>
|
||||
* application 指定的 jdbc url 位置存在则使用对应位置,且后续一直以该路径为准(包括调试时修改的数据)。
|
||||
* 若不存在则从 classpath 中取初始数据库复制到 jdbc url 指定的位置
|
||||
* @param dataSource
|
||||
*/
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof HikariDataSource hikariDataSource) {
|
||||
|
||||
String filePath = hikariDataSource.getJdbcUrl();
|
||||
if (filePath == null || !filePath.startsWith(PREFIX)) {
|
||||
log.warn(
|
||||
"Unable to obtain a valid SQLite JDBC URL from HikariDataSource; "
|
||||
+ "the data source may fail to load.\r\n"
|
||||
+ "A valid JDBC URL is typically specified in application.yml "
|
||||
+ "or application.properties and should begin with \"jdbc:sqlite:\". "
|
||||
+ "Current url: {}", filePath);
|
||||
return bean;
|
||||
}
|
||||
filePath = filePath.substring(PREFIX.length()).trim();
|
||||
log.debug("Retrieved HikariDataSource SQLite location: {}", filePath);
|
||||
|
||||
if (doghouseProperties.getUseRelativeDbPath()) {
|
||||
filePath = Paths.get(doghouseProperties.getWorkPath(), filePath).toString();
|
||||
log.debug("Use relative db path with work path: {}, so the db path will be: {}",
|
||||
doghouseProperties.getWorkPath(), filePath);
|
||||
hikariDataSource.setJdbcUrl(PREFIX + filePath);
|
||||
}
|
||||
|
||||
File dest = new File(filePath);
|
||||
if (dest.exists()) {
|
||||
log.debug("Retrieved HikariDataSource SQLite file exists, will be used as database.");
|
||||
return bean;
|
||||
}
|
||||
|
||||
ClassPathResource original = new ClassPathResource(RESOURCE_PATH);
|
||||
if (!original.exists()) {
|
||||
log.warn("Cannot find classpath resource: {}", RESOURCE_PATH);
|
||||
return bean;
|
||||
}
|
||||
// 复制到外部 yml 指定路径,已存在则不复制
|
||||
File parentDir = dest.getParentFile();
|
||||
String destAbsolutePath = dest.getAbsolutePath();
|
||||
if (!parentDir.exists() && !parentDir.mkdirs()) {
|
||||
// 无法创建放置 SQLite 文件的位置
|
||||
log.warn("Failed to create the directory in which the SQLite file should be placed: {}", parentDir.getAbsolutePath());
|
||||
return bean;
|
||||
}
|
||||
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE_PATH)) {
|
||||
if (in == null) {
|
||||
log.warn("Cannot read SQLite resource from classpath: {}", RESOURCE_PATH);
|
||||
return bean;
|
||||
}
|
||||
Files.copy(in, dest.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("SQLite database file has been copy to: {}", destAbsolutePath);
|
||||
} catch (Exception e) {
|
||||
log.warn("Copt SQLite databse file failed", e);
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.service.UserService;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
|
||||
|
||||
// 每次启动后
|
||||
@Component
|
||||
@DependsOn("rk")
|
||||
@Slf4j
|
||||
public class DogfacePreparation {
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
UserService userService;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
Long regeneratedCount = commentService.reprepareDogHash() + userService.reprepareDogHash();
|
||||
log.info("重新生成 {} 个邮箱的 Doghash", regeneratedCount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* 异常处理
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
public class DoghouseExceptionHandler {
|
||||
|
||||
@Autowired
|
||||
HttpServletResponse response;
|
||||
|
||||
@Autowired
|
||||
HttpServletRequest request;
|
||||
|
||||
@ExceptionHandler({
|
||||
BindException.class,
|
||||
MethodArgumentNotValidException.class})
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public <Ex extends BindException> R<?> handleBindException(Ex ex) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
ex.getBindingResult().getAllErrors()
|
||||
.forEach(error -> message.append(error.getDefaultMessage()).append("\n"));
|
||||
log.warn("Resolved Exception {}", message.substring(0, message.length() - 1));
|
||||
log.warn(httpServletRequestToString(request));
|
||||
return bodyOrPage(HttpStatus.BAD_REQUEST, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(LoginException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
|
||||
public R<?> handleLoginException(LoginException ex) {
|
||||
return bodyOrPage(HttpStatus.NOT_ACCEPTABLE, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RException.class)
|
||||
@ResponseBody
|
||||
public R<?> handleRException(RException ex) {
|
||||
response.setStatus(ex.getHttpStatus().value());
|
||||
if (ex.getLogRequest()) {
|
||||
log.warn(httpServletRequestToString(request));
|
||||
}
|
||||
return bodyOrPage(ex.getHttpStatus(), ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ServletException.class)
|
||||
@ResponseBody
|
||||
public R<?> handleServletException(ServletException ex) {
|
||||
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException)
|
||||
httpStatus = HttpStatus.METHOD_NOT_ALLOWED;
|
||||
response.setStatus(httpStatus.value());
|
||||
return bodyOrPage(httpStatus, ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R<?> handleException(Exception ex) {
|
||||
if (ex instanceof PageNotFoundException) {
|
||||
throw (PageNotFoundException)ex;
|
||||
}
|
||||
log.warn("Resolved exception {}", ex.getMessage());
|
||||
log.warn(httpServletRequestToString(request));
|
||||
return bodyOrPage(HttpStatus.INTERNAL_SERVER_ERROR, ex);
|
||||
}
|
||||
|
||||
private R<?> bodyOrPage(HttpStatus httpStatus, Exception ex) {
|
||||
boolean isPage = (ex instanceof RException || ex instanceof LoginException) ?
|
||||
false : isPage();
|
||||
if (isPage) {
|
||||
if (ex instanceof NoResourceFoundException nrfe) {
|
||||
if (StringUtils.isNotEmpty(nrfe.getMessage())
|
||||
&& nrfe.getMessage().endsWith(" .well-known/appspecific/com.chrome.devtools.json.")) {
|
||||
// 傻逼 Chrome 开发工具默认调用该地址
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw ex == null ?
|
||||
new RuntimeException("Page exception raised") :
|
||||
new RuntimeException(ex);
|
||||
}
|
||||
ex.printStackTrace();
|
||||
return ex != null ? R.status(httpStatus, ex.getMessage()) : R.status(httpStatus);
|
||||
}
|
||||
/**
|
||||
* 根据 @Autowired request 等自动判断当前请求是 text/html 还是 application/json
|
||||
* @return
|
||||
*/
|
||||
private boolean isPage() {
|
||||
boolean isPage = true;
|
||||
String accept;
|
||||
// 匹配当前 request 在控制层的方法,根据是否是
|
||||
// @ResponseBody 注解、@RestController 注解、
|
||||
// 已知的返回类型来判断返回的是否是页面
|
||||
Object handler = request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
|
||||
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
Method method = handlerMethod.getMethod();
|
||||
Class<?> controllerClass = handlerMethod.getBeanType();
|
||||
|
||||
boolean hasResponseBody =
|
||||
method.isAnnotationPresent(ResponseBody.class) ||
|
||||
controllerClass.isAnnotationPresent(ResponseBody.class) ||
|
||||
controllerClass.isAnnotationPresent(RestController.class);
|
||||
|
||||
Class<?> returnType = method.getReturnType();
|
||||
boolean isDtoReturnType =
|
||||
returnType.equals(R.class) ||
|
||||
returnType.equals(LayPageResp.class) ||
|
||||
JsonNode.class.isAssignableFrom(returnType);
|
||||
|
||||
if (hasResponseBody || isDtoReturnType) {
|
||||
isPage = false;
|
||||
}
|
||||
}
|
||||
if (isPage && (accept = request.getHeader("Accept")) != null) {
|
||||
int indexOfHtml = accept.indexOf("text/html"), indexOfJson = accept.indexOf("application/json");
|
||||
if (indexOfHtml == -1 && indexOfJson != -1) {
|
||||
isPage = false;
|
||||
} else if (indexOfHtml != -1 && indexOfJson != -1) {
|
||||
isPage = indexOfHtml < indexOfJson;
|
||||
}
|
||||
}
|
||||
return isPage;
|
||||
}
|
||||
|
||||
private static String httpServletRequestToString(HttpServletRequest request) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Request Method = \"" + request.getMethod() + "\", ");
|
||||
sb.append("Request URL Path = \"" + request.getRequestURL() + "\", ");
|
||||
|
||||
String headers =
|
||||
Collections.list(request.getHeaderNames()).stream()
|
||||
.map(headerName -> headerName + " : " + Collections.list(request.getHeaders(headerName)) )
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
if (headers.isEmpty()) {
|
||||
sb.append("Request headers: NONE,");
|
||||
} else {
|
||||
sb.append("Request headers: ["+headers+"],");
|
||||
}
|
||||
|
||||
String parameters =
|
||||
Collections.list(request.getParameterNames()).stream()
|
||||
.map(p -> {
|
||||
String[] values = request.getParameterValues(p);
|
||||
return p + ": [" + StringUtils.join(values, ", ") + "]";
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
if (parameters.isEmpty()) {
|
||||
sb.append("Request parameters: NONE.");
|
||||
} else {
|
||||
sb.append("Request parameters: [" + parameters + "].");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
116
src/main/java/me/qwq/doghouse/component/PostTypeComponent.java
Normal file
116
src/main/java/me/qwq/doghouse/component/PostTypeComponent.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.service.ThemeService;
|
||||
import me.qwq.doghouse.service.post.AbstractPostService;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* 提供通过 PostType 获取一些配置的能力
|
||||
*/
|
||||
@Component
|
||||
public class PostTypeComponent {
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
@Autowired
|
||||
ThemeService themeService;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
Set<Class<? extends IPostTypeConfig>> postTypeConfigClassCache = new HashSet<>();
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
Set<Class<? extends AbstractPostService>> abstractPostServiceClassCache = new HashSet<>();
|
||||
|
||||
Map<PostTypeEnum, IPostTypeConfig<?>> postTypeConfigCache = new HashMap<>();
|
||||
|
||||
Map<PostTypeEnum, AbstractPostService<?, ?>> abstractPostServiceCache = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
postTypeConfigClassCache =
|
||||
reflections.getSubTypesOf(IPostTypeConfig.class);
|
||||
abstractPostServiceClassCache =
|
||||
reflections.getSubTypesOf(AbstractPostService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定模式的 Post 配置
|
||||
* @param <T>
|
||||
* @param postType
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends IPostTypeConfig<T>> T getPostTypeConfig(PostTypeEnum postType) {
|
||||
return (T) postTypeConfigCache.computeIfAbsent(postType, key -> {
|
||||
for (@SuppressWarnings("rawtypes") Class<? extends IPostTypeConfig> clazz : postTypeConfigClassCache) {
|
||||
T instance = (T) SpringContextHolder.getBean(clazz);
|
||||
if (instance.getPostType() == key) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定模式的 PostService.
|
||||
* <p>注意在和 AbstractPostService 有关泛型的类内调用时不要指定其返回为泛型, 而是使用
|
||||
AbstractPostService<?, ?> 作为返回类型, 避免强制转换为泛型。除非很确定返回类型
|
||||
就是类内指定的泛型类型
|
||||
* @param <C>
|
||||
* @param <T>
|
||||
* @param postType
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <C extends IPostTypeConfig<C>, T extends AbstractPostService<T, C>> T getPostTypeService(PostTypeEnum postType) {
|
||||
return (T) abstractPostServiceCache.computeIfAbsent(postType, key -> {
|
||||
for (@SuppressWarnings("rawtypes") Class<? extends AbstractPostService> clazz : abstractPostServiceClassCache) {
|
||||
T instance = (T) SpringContextHolder.getBean(clazz);
|
||||
ResolvableType type = ResolvableType.forClass(clazz).as(AbstractPostService.class);
|
||||
ResolvableType iPageConfigType = type.getGeneric(1);
|
||||
Class<C> iPageConfigClass = (Class<C>) iPageConfigType.resolve();
|
||||
C pageConfig = SpringContextHolder.getBean(iPageConfigClass);
|
||||
if (pageConfig.getPostType() == key) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定模式的模板路径
|
||||
* @param postType
|
||||
* @param fileNameSegment
|
||||
* @return
|
||||
*/
|
||||
public String getTemplatePath(PostTypeEnum postType, String fileNameSegment) {
|
||||
return themeService.getCurrentTheme().getTemplateString(
|
||||
getPostTypeConfig(postType).getTemplatePrefix(),
|
||||
fileNameSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定模式是否打开
|
||||
* @param postType
|
||||
* @return
|
||||
*/
|
||||
public boolean checkModeEnable(PostTypeEnum postType) {
|
||||
return getPostTypeConfig(postType).getEnabled();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import me.qwq.doghouse.annotation.RequireSession;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class RequireSessionAspect {
|
||||
|
||||
@Before("@annotation(requireSession)")
|
||||
public void verifySession(JoinPoint joinPoint, RequireSession requireSession) {
|
||||
|
||||
// 检查是否在 DispatcherServlet 控制下
|
||||
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
|
||||
if (!(attrs instanceof ServletRequestAttributes servletAttrs)) {
|
||||
throw new IllegalStateException(
|
||||
joinPoint.getSignature() +
|
||||
" 必须在 Web 请求上下文中调用(未检测到 HttpServletRequest)");
|
||||
}
|
||||
|
||||
HttpServletRequest request = servletAttrs.getRequest();
|
||||
HttpSession session = request.getSession(requireSession.create());
|
||||
|
||||
if (session == null) {
|
||||
throw new IllegalStateException(
|
||||
joinPoint.getSignature() +
|
||||
" 调用时不允许创建 Session,但未检测到有效 Session");
|
||||
}
|
||||
}
|
||||
}
|
||||
241
src/main/java/me/qwq/doghouse/component/Rk.java
Normal file
241
src/main/java/me/qwq/doghouse/component/Rk.java
Normal file
@@ -0,0 +1,241 @@
|
||||
package me.qwq.doghouse.component;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.client.codec.StringCodec;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.command.CommandAsyncExecutor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.sqids.Sqids;
|
||||
|
||||
import me.qwq.doghouse.entity.Comment;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.util.Cryptos;
|
||||
/**
|
||||
* Redis Key Tool
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class Rk {
|
||||
|
||||
static SiteConfig siteConfig;
|
||||
|
||||
static RedissonClient redisson;
|
||||
|
||||
|
||||
public static String SYSTEM_PREFIX;
|
||||
|
||||
static final String NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events",
|
||||
x = "x", E = "E";
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setService(SiteConfig siteConfig, RedissonClient redisson) {
|
||||
Rk.siteConfig = siteConfig;
|
||||
Rk.redisson = redisson;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
|
||||
SYSTEM_PREFIX = "doghouse:" + siteConfig.getAddress().hashCode() + ":";
|
||||
|
||||
// 使用 Redisson 设置 redis 服务端的 x(expired) 和 E(evicted) 事件
|
||||
Redisson redissonImpl = (Redisson) redisson;
|
||||
CommandAsyncExecutor executor = redissonImpl.getCommandExecutor();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> config = (Map<String, String>) executor
|
||||
.readAsync("default", StringCodec.INSTANCE, RedisCommands.CONFIG_GET_MAP, NOTIFY_KEYSPACE_EVENTS)
|
||||
.toCompletableFuture().join();
|
||||
|
||||
String keyEvents = config.getOrDefault(NOTIFY_KEYSPACE_EVENTS, "");
|
||||
boolean changed = false;
|
||||
if (!keyEvents.contains(x)) {
|
||||
keyEvents += x;
|
||||
changed = true;
|
||||
}
|
||||
if (!keyEvents.contains(E)) {
|
||||
keyEvents += E;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
log.debug("Redis setConfig {} value {}", NOTIFY_KEYSPACE_EVENTS, keyEvents);
|
||||
executor
|
||||
.writeAsync("default", StringCodec.INSTANCE, RedisCommands.CONFIG_SET, NOTIFY_KEYSPACE_EVENTS, keyEvents)
|
||||
.toCompletableFuture()
|
||||
.join();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Mail {
|
||||
|
||||
public static final String ID = "mail:";
|
||||
|
||||
private static String getMailKey(String subfix) {
|
||||
return SYSTEM_PREFIX + ID + subfix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有过期事件的 key
|
||||
* @return
|
||||
*/
|
||||
public static Iterable<String> getAllExpirationTaskKeys() {
|
||||
return redisson.getKeys()
|
||||
.getKeysByPattern(getMailKey("task-"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件推送任务的 Bucket(不包括管理员邮件)
|
||||
* @param commentId
|
||||
* @return
|
||||
*/
|
||||
public static RBucket<Object> getTaskBucket(Long commentId) {
|
||||
return redisson.getBucket(getMailKey("task-" + commentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件推送任务的 Comment 缓存 Bucket(不包括管理员邮件)
|
||||
* @param commentId
|
||||
* @return
|
||||
*/
|
||||
public static RBucket<Comment> getTaskCommentBucket(Long commentId) {
|
||||
return redisson.getBucket(getMailKey("task-" + commentId + "-comment"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户邮件订阅管理 UUID 缓存 Bucket
|
||||
* @param mail
|
||||
* @return 包含 uuid 的 Bucket,用以建立 email → uuid 的映射关系
|
||||
*/
|
||||
public static RBucket<String> getMgrBucket(String mail) {
|
||||
return redisson.getBucket(getMailKey("mgr-addr-" + mail));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户邮件订阅管理邮箱地址缓存 Bucket
|
||||
* @param uuid
|
||||
* @return 包含 email 的 Bucket,用以建立 uuid → email 的映射关系
|
||||
*/
|
||||
public static RBucket<String> getMgrUuidBucket(String uuid) {
|
||||
return redisson.getBucket(getMailKey("mgr-uuid-" + uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个随机 Key 的用于过期事件的 Bucket
|
||||
*/
|
||||
public static RBucket<Object> getRandomExpirationBucket(){
|
||||
List<Long> list = new ArrayList<>();
|
||||
SecureRandom random = new SecureRandom();
|
||||
list.add(Math.abs(random.nextLong()));
|
||||
list.add(Math.abs(random.nextLong()));
|
||||
return redisson.getBucket(getMailKey(Sqids.builder().build().encode(list)));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Gravatar {
|
||||
|
||||
public static final String ID = "gravatar:";
|
||||
|
||||
public static final String GRAVATAR_PREFIX = "image:";
|
||||
|
||||
public static final String EMAIL_HASH_PREFIX = "email-hash:";
|
||||
|
||||
public static final String PRIVATE_HASH_PREFIX = "private-hash:";
|
||||
|
||||
public static final String SALT = "~D09H0U5E-9R4V474R^_^";
|
||||
|
||||
private static String getGravatarKey(String subfix) {
|
||||
return SYSTEM_PREFIX + ID + subfix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应 hash 和尺寸 size 的头像缓存 Bucket
|
||||
* @param email
|
||||
* @param size
|
||||
* @return
|
||||
*/
|
||||
public static RBucket<Object> getGravatarBucket(String dogHash, int size) {
|
||||
return redisson.getBucket(getGravatarKey(GRAVATAR_PREFIX + dogHash + ":" + size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 gravatar 图片缓存
|
||||
*/
|
||||
public static void clearGravatarCache() {
|
||||
for (String name : redisson.getKeys().getKeysByPattern(getGravatarKey(GRAVATAR_PREFIX + "*"))) {
|
||||
redisson.getBucket(name).deleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应 email 的 hash,并且提前在 Redis 中存储对应关系
|
||||
* @param email
|
||||
* @return
|
||||
*/
|
||||
public static String prepareDogHash(String email) {
|
||||
|
||||
email = email.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
RBucket<String> gravatarHashBucket = redisson.getBucket(getGravatarKey(EMAIL_HASH_PREFIX + email));
|
||||
if (gravatarHashBucket.isExists()) {
|
||||
return gravatarHashBucket.get();
|
||||
}
|
||||
|
||||
byte[] hash = Cryptos.sha3((email + SALT).getBytes(), 256);
|
||||
|
||||
List<Long> list = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Long x = 0L;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
x |= (((long) hash[i * 8 + j]) & 0xFFL) << ((7 - j) * 8);
|
||||
}
|
||||
i++;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
x ^= (((long) hash[i * 8 + j]) & 0xFFL) << ((7 - j) * 8);
|
||||
}
|
||||
if (x < 0) {
|
||||
list.add(0L);
|
||||
list.add(Math.abs(x));
|
||||
}
|
||||
else {
|
||||
list.add(x);
|
||||
}
|
||||
}
|
||||
|
||||
String dogHash = Sqids.builder().build().encode(list);
|
||||
gravatarHashBucket.set(dogHash);
|
||||
String realHash = Cryptos.sha256(email);
|
||||
|
||||
redisson.getBucket(getGravatarKey(PRIVATE_HASH_PREFIX + dogHash))
|
||||
.set(realHash);
|
||||
|
||||
log.info("Email: {}, real hash: {}, dog hash: {}", email, realHash, dogHash);
|
||||
|
||||
return dogHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 hash 获取 gravatar md5
|
||||
* @param dogHash
|
||||
* @return
|
||||
*/
|
||||
public static String getRealHash(String dogHash) {
|
||||
RBucket<String> email = redisson.getBucket(getGravatarKey(PRIVATE_HASH_PREFIX + dogHash));
|
||||
return email.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
|
||||
/**
|
||||
* 实现自动化注册 Config
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor {
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigInfo.class));
|
||||
|
||||
scanner.findCandidateComponents("me.qwq.doghouse.entity.config").forEach(beanDefinition -> {
|
||||
String className = beanDefinition.getBeanClassName();
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
|
||||
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(ConfigServiceFactoryBean.class)
|
||||
.addConstructorArgValue(clazz);
|
||||
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition());
|
||||
log.info("Config part {} registered", beanName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("Failed to load class: " + className, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.service.ConfigService;
|
||||
|
||||
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T> {
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
public ConfigServiceFactoryBean(Class<T> targetClass) {
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getObject() throws Exception {
|
||||
return configService.getConfig(targetClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
17
src/main/java/me/qwq/doghouse/config/CustomMimeMapping.java
Normal file
17
src/main/java/me/qwq/doghouse/config/CustomMimeMapping.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class CustomMimeMapping implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableServletWebServerFactory factory) {
|
||||
MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
|
||||
mappings.add("avif", "image/avif");
|
||||
factory.setMimeMappings(mappings);
|
||||
}
|
||||
}
|
||||
23
src/main/java/me/qwq/doghouse/config/DogfaceConfig.java
Normal file
23
src/main/java/me/qwq/doghouse/config/DogfaceConfig.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import me.qwq.dogface.Dogface;
|
||||
|
||||
/**
|
||||
* Reflections
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class DogfaceConfig {
|
||||
|
||||
@Bean("dogface")
|
||||
Dogface dogface() throws IOException {
|
||||
return new Dogface();
|
||||
}
|
||||
|
||||
}
|
||||
93
src/main/java/me/qwq/doghouse/config/DoghouseConfig.java
Normal file
93
src/main/java/me/qwq/doghouse/config/DoghouseConfig.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
|
||||
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||
|
||||
import me.qwq.doghouse.interceptions.BlogInterceptor;
|
||||
import me.qwq.doghouse.interceptions.ThemeStaticFilter;
|
||||
import me.qwq.doghouse.service.ConfigService;
|
||||
|
||||
/**
|
||||
* Doghouse 配置项
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DoghouseProperties.class)
|
||||
@Import(ConfigAutoRegistrar.class)
|
||||
public class DoghouseConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
BlogInterceptor blogInterceptor;
|
||||
|
||||
@Autowired
|
||||
DoghouseProperties doghouseProperties;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
/**
|
||||
* 配置项注入拦截器
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(blogInterceptor)
|
||||
.addPathPatterns("/**");
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件资源映射、外部主题静态资源映射
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry
|
||||
.addResourceHandler(doghouseProperties.getFileSqlPath() + "**")
|
||||
.addResourceLocations("file:" + doghouseProperties.getFileUploadPath());
|
||||
|
||||
// URL: /themes/bleaching/css/main.css
|
||||
// FS: workPath/themes/bleaching/static/css/main.css
|
||||
registry
|
||||
.addResourceHandler("/themes/**")
|
||||
.addResourceLocations("file:" + doghouseProperties.getWorkPath() + "/themes/");
|
||||
|
||||
registry
|
||||
.addResourceHandler("/tmp/themes/**")
|
||||
.addResourceLocations("file:" + doghouseProperties.getWorkPath() + "/tmp/themes/");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<ThemeStaticFilter> themeStaticFilter() {
|
||||
FilterRegistrationBean<ThemeStaticFilter> bean = new FilterRegistrationBean<>();
|
||||
bean.setFilter(new ThemeStaticFilter());
|
||||
bean.addUrlPatterns("/themes/*");
|
||||
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ITemplateResolver fileTemplateResolver() {
|
||||
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
|
||||
resolver.setResolvablePatterns(Set.of("file:*"));
|
||||
resolver.setPrefix(""); // 不要自动加前缀
|
||||
resolver.setSuffix(".html"); // 自动补 .html
|
||||
resolver.setTemplateMode("HTML");
|
||||
resolver.setCharacterEncoding("UTF-8");
|
||||
resolver.setOrder(1); // 比默认的 templateResolver 优先
|
||||
resolver.setCheckExistence(true); // 文件不存在就跳过,交给下一个 resolver
|
||||
return resolver;
|
||||
}
|
||||
}
|
||||
84
src/main/java/me/qwq/doghouse/config/DoghouseProperties.java
Normal file
84
src/main/java/me/qwq/doghouse/config/DoghouseProperties.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.BlogApplication;
|
||||
import me.qwq.doghouse.entity.Asset;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ConfigurationProperties(prefix = "doghouse")
|
||||
@Component("doghouseProperties")
|
||||
public class DoghouseProperties {
|
||||
|
||||
@Autowired
|
||||
BuildProperties buildProperties;
|
||||
|
||||
String workPath = "/tmp/doghouse/";
|
||||
|
||||
/**
|
||||
* 文件上传路径。默认是相对于 workPath 的路径。如需绝对路径请在前加 absolute:
|
||||
*/
|
||||
String fileUploadPath = "/upload/";
|
||||
|
||||
/**
|
||||
* 文件在 SQL 中存储的路径前缀
|
||||
* <p>
|
||||
* 如:文件名为 filename.file, 则设置为 /upload/ 时, 可通过 http(s)://siteAddress/upload/{年份}/{月份}/filename.file 访问到文件
|
||||
*/
|
||||
String fileSqlPath = "/upload/";
|
||||
|
||||
/**
|
||||
* 数据库 blog.db 路径是否使用相对路径, 默认是
|
||||
*/
|
||||
Boolean useRelativeDbPath = true;
|
||||
|
||||
public String getFileUploadPath() {
|
||||
String finalPath;
|
||||
if (StringUtils.startsWith(fileUploadPath, "absolute:")) {
|
||||
finalPath = Paths.get(fileUploadPath.substring("absolute:".length())).toString();
|
||||
}
|
||||
else {
|
||||
finalPath = Paths.get(workPath, fileUploadPath).toString();
|
||||
}
|
||||
return finalPath + Paths.get("/").toString();
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return buildProperties.getVersion();
|
||||
}
|
||||
|
||||
public boolean isDebug() {
|
||||
return BlogApplication.isDebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将资产的 sql 路径转换为实际路径
|
||||
* @param asset
|
||||
* @return 资产在部署机器上的实际路径
|
||||
*/
|
||||
public String getFileUploadPath(@NonNull Asset asset) {
|
||||
String path = asset.getPath();
|
||||
if (StringUtils.isNoneBlank(path) && path.startsWith(getFileSqlPath())) {
|
||||
path = path.replaceFirst(getFileSqlPath(), getFileUploadPath());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getFileSqlPath(@NonNull String path) {
|
||||
if (StringUtils.isNoneBlank(path) && path.startsWith(getFileUploadPath())) {
|
||||
path = path.replaceFirst(getFileUploadPath(), getFileSqlPath());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/me/qwq/doghouse/config/KaptchaConfig.java
Normal file
30
src/main/java/me/qwq/doghouse/config/KaptchaConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package me.qwq.doghouse.config;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
import com.google.code.kaptcha.util.Config;
|
||||
|
||||
@Configuration
|
||||
public class KaptchaConfig {
|
||||
|
||||
@Bean(name = "kaptchaProperties")
|
||||
@ConfigurationProperties
|
||||
Properties getKaptchaProperties() {
|
||||
return new Properties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("kaptchaProperties")
|
||||
DefaultKaptcha getDefaultKaptcha(@Autowired Properties kaptchaProperties) {
|
||||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||
Config config = new Config(kaptchaProperties);
|
||||
defaultKaptcha.setConfig(config);
|
||||
|
||||
return defaultKaptcha;
|
||||
}
|
||||
}
|
||||
32
src/main/java/me/qwq/doghouse/config/MybatisPlusConfig.java
Normal file
32
src/main/java/me/qwq/doghouse/config/MybatisPlusConfig.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* MybatisPlus 配置
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@EnableTransactionManagement
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页器插件
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor(){
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
30
src/main/java/me/qwq/doghouse/config/QuartzConfig.java
Normal file
30
src/main/java/me/qwq/doghouse/config/QuartzConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.quartz.Scheduler;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||
|
||||
import me.qwq.doghouse.task.TaskSchedulerFactory;
|
||||
|
||||
|
||||
@Configuration
|
||||
public class QuartzConfig {
|
||||
@Autowired
|
||||
private TaskSchedulerFactory taskSchedulerFactory;
|
||||
|
||||
@Bean("schedulerFactoryBean")
|
||||
SchedulerFactoryBean schedulerFactoryBean() {
|
||||
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
|
||||
schedulerFactoryBean.setJobFactory( taskSchedulerFactory);
|
||||
return schedulerFactoryBean;
|
||||
}
|
||||
|
||||
@Bean("scheduler")
|
||||
@DependsOn("schedulerFactoryBean")
|
||||
Scheduler scheduler(@Autowired SchedulerFactoryBean schedulerFactoryBean) {
|
||||
return schedulerFactoryBean.getScheduler();
|
||||
}
|
||||
}
|
||||
28
src/main/java/me/qwq/doghouse/config/ReflectionsConfig.java
Normal file
28
src/main/java/me/qwq/doghouse/config/ReflectionsConfig.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Reflections
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class ReflectionsConfig {
|
||||
|
||||
@Bean("reflections")
|
||||
Reflections reflections() {
|
||||
return new Reflections(new ConfigurationBuilder()
|
||||
.addScanners(
|
||||
Scanners.MethodsAnnotated,
|
||||
Scanners.SubTypes,
|
||||
Scanners.TypesAnnotated,
|
||||
Scanners.FieldsAnnotated)
|
||||
.forPackages("me.qwq.doghouse"));
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/java/me/qwq/doghouse/config/SecurityConfig.java
Normal file
67
src/main/java/me/qwq/doghouse/config/SecurityConfig.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import me.qwq.doghouse.service.UserService;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.cacheControl(cache -> cache.disable())
|
||||
.frameOptions(frame -> frame.sameOrigin()))
|
||||
.csrf(CsrfConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/admin/layuiadmin/**").permitAll()
|
||||
.requestMatchers("/admin/v2/**").authenticated()
|
||||
.requestMatchers("/admin/*/login").permitAll()
|
||||
.requestMatchers("/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// TODO 移除 formLogin,改为 JsonLogin
|
||||
.formLogin(form -> form // 开启表单登录,并指定登录页
|
||||
.loginPage("/admin/v2/login") // 指定登录页
|
||||
.loginProcessingUrl("/admin/v2/doLogin") // 处理登录请求的 URL
|
||||
.defaultSuccessUrl("/admin/v2/", false) // 登录成功后默认跳转
|
||||
.permitAll())
|
||||
.logout(logout -> logout.logoutUrl("/admin/v2/logout").logoutSuccessUrl("/admin/v2/login")
|
||||
.invalidateHttpSession(true).permitAll());
|
||||
;
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(@Autowired PasswordEncoder passwordEncoder) {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userService);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
return new ProviderManager(provider);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package me.qwq.doghouse.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
|
||||
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400) //session 过期时间 24h
|
||||
@ConditionalOnProperty(prefix = "doghouse", name = "spring-session-open", havingValue = "true")
|
||||
public class SpringSessionConfig {
|
||||
|
||||
}
|
||||
35
src/main/java/me/qwq/doghouse/constants/UsefulRegex.java
Normal file
35
src/main/java/me/qwq/doghouse/constants/UsefulRegex.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package me.qwq.doghouse.constants;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UsefulRegex {
|
||||
|
||||
public static final String IGNORE_CASE = "(?i)";
|
||||
|
||||
public static final String BLANK = "^\\s*$";
|
||||
|
||||
public static final String OR = "|";
|
||||
|
||||
public static final String DOMAIN = "^(https?://)?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$";
|
||||
|
||||
public static final String DOMAIN_WITHOUT_SCHEME = "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$";
|
||||
|
||||
public static final String URL = "^((https?:)?//)?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(/.*?)?$";
|
||||
|
||||
public static final String URL_WITH_SCHEME = "^https?://([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(/.*?)?$";
|
||||
|
||||
public static final String URL_ENDS_WITH_SLASH = "^((https?:)?//)?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(/.*?)?/$";
|
||||
|
||||
public static final String EMAIL = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$";
|
||||
|
||||
public static final String IPv4 = "^((?:(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))\\.){3}(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))))$";
|
||||
|
||||
public static final String IPv4_WITH_WILECARD = "^((?:(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)|\\*))\\.){3}(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)|\\*)))$";
|
||||
|
||||
public static final String ANY_CHINESE = ".*?[\\u4E00-\\u9FA5]+.*?";
|
||||
|
||||
public static final Pattern ANY_CHINESE_PATTERN = Pattern.compile(UsefulRegex.ANY_CHINESE, Pattern.DOTALL);
|
||||
|
||||
public static final Pattern ADMIN_PATTERN = Pattern.compile("^[/\\\\]*admin[/\\\\]+(?<version>v\\d+)[/\\\\]*(?<action>.*?)$");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
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.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.User;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.UserService;
|
||||
import me.qwq.doghouse.util.CookieUtils;
|
||||
import me.qwq.doghouse.util.Cryptos;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2")
|
||||
public class AdminController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
UserService adminUserService;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "admin/index";
|
||||
}
|
||||
|
||||
@GetMapping("/getCurrentUserInfo")
|
||||
@ResponseBody
|
||||
public R<?> getCurrentUserInfo() {
|
||||
User user = SessionUtils.getUser(session);
|
||||
user.setPassword(null);
|
||||
return R.ok(user);
|
||||
}
|
||||
|
||||
static final String EMPTY_PASSWORD = Cryptos.sha3("", 224);
|
||||
|
||||
static boolean passwordIsNotEmpty(String password) {
|
||||
return StringUtils.isNotEmpty(password) &&
|
||||
!password.equalsIgnoreCase(EMPTY_PASSWORD);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/setCurrentUserInfo")
|
||||
@ResponseBody
|
||||
public R<?> setCurrentUserInfo(@Validated User changedUser, BindingResult bindingResult, String newPassword) {
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage())
|
||||
.setLogRequest(true);
|
||||
}
|
||||
|
||||
User user = SessionUtils.getUser(session);
|
||||
boolean hasNewPass = passwordIsNotEmpty(newPassword);
|
||||
if (hasNewPass && !user.getPassword().equals(changedUser.getPassword())) {
|
||||
throw RException.badRequest("旧密码不正确");
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<User> uw = new LambdaUpdateWrapper<User>();
|
||||
|
||||
uw.set(User::getEmail, changedUser.getEmail())
|
||||
.set(User::getNickname, changedUser.getNickname())
|
||||
.set(User::getSite, changedUser.getSite());
|
||||
|
||||
if (hasNewPass) {
|
||||
uw.set(User::getPassword, newPassword);
|
||||
}
|
||||
|
||||
if (adminUserService.update(uw)) {
|
||||
user = adminUserService.getById(user.getUserId());
|
||||
SessionUtils.setUser(session, user);
|
||||
CookieUtils.setUserToCookie(response, user);
|
||||
return R.ok(user.setPassword(null));
|
||||
}
|
||||
throw RException.internalServerError();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import com.alibaba.fastjson2.util.DateUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import jodd.io.FileUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.config.DoghouseProperties;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Asset;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.FileTypeEnum;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.AssetService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.sqids.Sqids;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
|
||||
/**
|
||||
* 资产控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/asset")
|
||||
@Slf4j
|
||||
public class AssetController extends BaseController {
|
||||
|
||||
final private static SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
final private static Sqids SQIDS = Sqids.builder().build();
|
||||
|
||||
@Autowired
|
||||
DoghouseProperties doghouseProperties;
|
||||
|
||||
@Autowired
|
||||
AssetService assetService;
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
/**
|
||||
* 主页
|
||||
* @return
|
||||
*/
|
||||
@GetMapping({"", "/", "/index"})
|
||||
String index() {
|
||||
return "admin/asset/index";
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/list")
|
||||
public LayPageResp<?> getAssetList(LayPageReq<Asset> pageReq) {
|
||||
|
||||
MPJLambdaWrapper<Asset> ew = new MPJLambdaWrapper<Asset>()
|
||||
.selectAll(Asset.class)
|
||||
.leftJoin(Post.class, Post::getPostId, Asset::getFromPostId)
|
||||
.orderByDesc(Asset::getCreateTime);
|
||||
|
||||
IPage<Asset> page = assetService.page(pageReq, ew);
|
||||
|
||||
LayPageResp<Asset> result = new LayPageResp<>(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/uploadFile")
|
||||
R<?> uploadFile(
|
||||
@RequestParam(name = "file") MultipartFile file,
|
||||
@RequestParam(name = "postId", required = false) Long postId) {
|
||||
|
||||
String msg = "文件上传失败";
|
||||
String suffix = getSuffix(file);
|
||||
|
||||
if (suffix == null) {
|
||||
return R.badRequest(msg);
|
||||
}
|
||||
|
||||
String prefix = generateFilePrefix();
|
||||
String filename = prefix + '.' + suffix;
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String originalPrefix = originalFilename.substring(0, originalFilename.length() -
|
||||
suffix.length() - 1);
|
||||
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String yearAndMonth = String.format("%d/%02d", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1);
|
||||
Path filePlacedDirPath = Paths.get(doghouseProperties.getFileUploadPath(), yearAndMonth);
|
||||
|
||||
File dir = filePlacedDirPath.toFile();
|
||||
File destFile = filePlacedDirPath.resolve(filename).toFile();
|
||||
Asset asset = new Asset();
|
||||
try {
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
msg = "创建文件夹 " + dir + " 失败";
|
||||
log.error(msg);
|
||||
return R.internalServerError(msg);
|
||||
}
|
||||
|
||||
// file.transferTo(destFile); // 解决 jetty 上传文件问题
|
||||
FileUtil.writeBytes(destFile, file.getBytes());
|
||||
String fileUrl =
|
||||
Paths.get(doghouseProperties.getFileSqlPath(), yearAndMonth, filename)
|
||||
.toString().replace('\\', '/');
|
||||
|
||||
FileTypeEnum fileType = FileTypeEnum.getFileType(suffix);
|
||||
|
||||
asset
|
||||
.setFromPostId(postId)
|
||||
.setTitle(file.getOriginalFilename())
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setFileType(fileType)
|
||||
.setFileSize(destFile.length())
|
||||
.setUpdateTime(LocalDateTime.now())
|
||||
.setPath(fileUrl);
|
||||
|
||||
if (fileType == FileTypeEnum.IMAGE) {
|
||||
BufferedImage image = ImageIO.read(destFile);
|
||||
if (image != null) {
|
||||
asset.setImageHeight(image.getHeight())
|
||||
.setImageWidth(image.getWidth());
|
||||
|
||||
if (siteConfig.getImageCompress()) {
|
||||
|
||||
File avifFile = getAvif(suffix, destFile.toString(), prefix, filePlacedDirPath);
|
||||
File webpFile = getWebp(suffix, destFile.toString(), prefix, filePlacedDirPath);
|
||||
|
||||
File finalFile = null;
|
||||
String finalSuffix = null;
|
||||
|
||||
if (webpFile != null && avifFile != null) {
|
||||
if (webpFile.length() >= avifFile.length()) {
|
||||
finalFile = webpFile;
|
||||
finalSuffix = ".webp";
|
||||
avifFile.delete();
|
||||
}
|
||||
else {
|
||||
finalFile = avifFile;
|
||||
finalSuffix = ".avif";
|
||||
webpFile.delete();
|
||||
}
|
||||
}
|
||||
else if (webpFile == null && avifFile != null) {
|
||||
finalFile = avifFile;
|
||||
finalSuffix = ".avif";
|
||||
}
|
||||
else if (webpFile != null && avifFile == null) {
|
||||
finalFile = webpFile;
|
||||
finalSuffix = ".webp";
|
||||
}
|
||||
|
||||
if (finalFile != null) {
|
||||
|
||||
fileUrl = Paths.get(doghouseProperties.getFileSqlPath(), yearAndMonth, finalFile.getName())
|
||||
.toString().replace('\\', '/');
|
||||
|
||||
asset
|
||||
.setPath(fileUrl)
|
||||
.setFileSize(finalFile.length())
|
||||
.setTitle(originalPrefix + finalSuffix);
|
||||
|
||||
destFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn("Unsupported image format {}", suffix);
|
||||
}
|
||||
}
|
||||
if (assetService.save(asset)) {
|
||||
return R.ok(asset);
|
||||
}
|
||||
throw RException.badRequest("文件上传成功但保存失败");
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error(msg, e);
|
||||
}
|
||||
return R.internalServerError(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件基本信息
|
||||
* @param asset
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/update")
|
||||
R<?> update(Asset asset) {
|
||||
if (Objects.isNull(asset) || Objects.isNull(asset.getAssetId())) {
|
||||
throw RException.badRequest("提供的资产不能为空!");
|
||||
}
|
||||
LambdaUpdateWrapper<Asset> uw = new LambdaUpdateWrapper<>();
|
||||
uw.eq(Asset::getAssetId, asset.getAssetId())
|
||||
.set(Asset::getTitle, asset.getTitle())
|
||||
.set(Asset::getDescription, asset.getDescription());
|
||||
return R.judge(assetService.update(uw), "更新资产信息失败!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除资产
|
||||
* @param assetId
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/delete")
|
||||
R<?> delete(Long assetId) {
|
||||
if (Objects.isNull(assetId)) {
|
||||
throw RException.badRequest("提供的资产不能为空!");
|
||||
}
|
||||
Asset asset = assetService.getById(assetId);
|
||||
if (Objects.isNull(asset)) {
|
||||
log.info("试图删除已不存在于数据库内的资产");
|
||||
return R.ok();
|
||||
}
|
||||
String uploadPath = doghouseProperties.getFileUploadPath(asset);
|
||||
File file = new File(uploadPath);
|
||||
if (file.exists()) {
|
||||
return R.judge(file.delete() && assetService.removeById(assetId), "删除文件失败");
|
||||
}
|
||||
return R.judge(assetService.removeById(assetId), "删除文件失败");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解绑资产
|
||||
* @param assetId
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/unlink")
|
||||
R<?> unlink(Long assetId) {
|
||||
if (Objects.isNull(assetId)) {
|
||||
throw RException.badRequest("提供的资产不能为空!");
|
||||
}
|
||||
Asset asset = assetService.getById(assetId);
|
||||
if (Objects.isNull(asset)) {
|
||||
log.info("试图操作已不存在于数据库内的资产");
|
||||
return R.ok();
|
||||
}
|
||||
if (Objects.isNull(asset.getFromPostId())) {
|
||||
return R.ok("该资产并未绑定 Post");
|
||||
}
|
||||
|
||||
return R.judge(
|
||||
assetService.update(new LambdaUpdateWrapper<Asset>()
|
||||
.eq(Asset::getAssetId, asset.getAssetId())
|
||||
.set(Asset::getFromPostId, null)), "解绑失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀,不包含".", 返回小写
|
||||
*/
|
||||
public static String getSuffix(MultipartFile file){
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
String fileName = file.getOriginalFilename();
|
||||
if (StringUtils.isEmpty(fileName)) {
|
||||
return null;
|
||||
}
|
||||
int lastIndexOfDot = fileName.lastIndexOf('.');
|
||||
if (lastIndexOfDot == -1 || lastIndexOfDot == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(lastIndexOfDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用随机 Long + Sqids 作为文件名
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public static String generateFilePrefix() {
|
||||
String dateStr = DateUtils.format(LocalDateTime.now(), "yyyyMMdd_HHmmssSSS_");
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(dateStr);
|
||||
|
||||
List<Long> list = new ArrayList<>();
|
||||
list.add(Math.abs(SECURE_RANDOM.nextLong()));
|
||||
|
||||
String id = SQIDS.encode(list);
|
||||
sb.append(id);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static File getAvif(String suffix,
|
||||
String originalFilePath, String finalPrefix, Path filePlacedDirPath) {
|
||||
|
||||
if (!"jpg".equals(suffix) && !"jpeg".equals(suffix) && !"png".equals(suffix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String avifFilename = finalPrefix + ".avif";
|
||||
String avifPath = filePlacedDirPath.resolve(avifFilename).toString();
|
||||
List<String> avifCommand = List.of(
|
||||
"avifenc",
|
||||
"-q", "60",
|
||||
"--qalpha", "80",
|
||||
"-s", "6",
|
||||
"-j", "all",
|
||||
originalFilePath,
|
||||
avifPath);
|
||||
|
||||
return executeFile(avifCommand, avifPath);
|
||||
}
|
||||
|
||||
|
||||
private static File getWebp(String suffix,
|
||||
String originalFilePath, String finalPrefix, Path filePlacedDirPath) {
|
||||
|
||||
|
||||
if (!"jpg".equals(suffix) && !"jpeg".equals(suffix) && !"png".equals(suffix) && !"gif".equals(suffix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String webpFilename = finalPrefix + ".webp";
|
||||
String webpPath = filePlacedDirPath.resolve(webpFilename).toString();
|
||||
List<String> webpCommand =
|
||||
!"gif".equals(suffix) ?
|
||||
List.of("cwebp",
|
||||
"-z", "9",
|
||||
"-q", "100",
|
||||
"-near_lossless", "60",
|
||||
"-m", "6",
|
||||
"-mt", originalFilePath,
|
||||
"-o", webpPath):
|
||||
List.of("gif2webp",
|
||||
"-q", "90",
|
||||
"-lossy", "-min_size",
|
||||
"-m", "0",
|
||||
"-mt", originalFilePath,
|
||||
"-o", webpPath);
|
||||
return executeFile(webpCommand, webpPath);
|
||||
}
|
||||
|
||||
private static File executeFile(List<String> command, String filePath) {
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(true);
|
||||
try {
|
||||
Process process = pb.start();
|
||||
|
||||
try (var reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
log.debug("[img-encode] {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor(15, TimeUnit.SECONDS) ? process.exitValue() : -1;
|
||||
|
||||
if (exitCode == 0) {
|
||||
File out = FileUtil.file(filePath);
|
||||
if (!out.exists()) {
|
||||
log.warn("Command finished with code 0, but output file not found: {}", filePath);
|
||||
return null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
log.warn("Execution failed with exit code: {}, command {}", exitCode, command);
|
||||
return null;
|
||||
}
|
||||
catch (IOException | InterruptedException e) {
|
||||
log.warn("Cannot execute command {}, {}", command, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
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.bind.annotation.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Cate;
|
||||
import me.qwq.doghouse.entity.Meta;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.MetaService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/category")
|
||||
public class CategoryController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
MetaService metaService;
|
||||
|
||||
@GetMapping({"", "/"})
|
||||
private String index() {
|
||||
request.setAttribute("title", "分类管理");
|
||||
return "admin/category/index";
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ResponseBody
|
||||
private LayPageResp<Cate> getCategoryList(
|
||||
LayPageReq<Cate> pageReq,
|
||||
@RequestParam(required=false) Long parentId,
|
||||
@RequestParam(name="unfolded[]", required=false) Long[] unfolded){
|
||||
List<Long> unfoldedId;
|
||||
if (unfolded != null && unfolded.length > 0) {
|
||||
unfoldedId = Arrays.asList(unfolded);
|
||||
}
|
||||
else {
|
||||
unfoldedId = new ArrayList<>();
|
||||
}
|
||||
if (parentId == null) {
|
||||
IPage<Cate> page = metaService.pageTreeViewMetaWithChildren(Cate.class, pageReq, unfoldedId);
|
||||
return new LayPageResp<Cate>(page);
|
||||
}
|
||||
else {
|
||||
List<Cate> listCate = metaService.listTreeViewMetaWithChildren(Cate.class, parentId, unfoldedId);
|
||||
return new LayPageResp<Cate>().setData(listCate).setCount(listCate.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或编辑分类
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@ResponseBody
|
||||
private R<?> update(@Valid Cate cate, BindingResult bindingResult) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
if (cate == null) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "分类不允许为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(cate.getFriendlyName())) {
|
||||
cate.setFriendlyName(cate.getName().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
// 查询友好名称是否重复
|
||||
// 新增
|
||||
long count = metaService.count(new MPJLambdaWrapper<Meta>()
|
||||
.eq(Meta::getType, MetaTypeEnum.CATEGORY)
|
||||
.eq(Meta::getFriendlyName, cate.getFriendlyName()));
|
||||
if (count >= 1) {
|
||||
if (cate.getMid() == null) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "分类友好名称与其他分类重复");
|
||||
}
|
||||
else {
|
||||
Cate existCate = metaService.getOneJoin(Cate.class,
|
||||
new MPJLambdaWrapper<Cate>(Cate.class).eq(Cate::getFriendlyName, cate.getFriendlyName()));
|
||||
if (!existCate.getMid().equals(cate.getMid())) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "分类友好名称与其他分类重复");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(cate.insertOrUpdate()) {
|
||||
return R.ok();
|
||||
}
|
||||
throw new RException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作,支持批量删除、批量移动
|
||||
* @param ids
|
||||
* @param op 操作
|
||||
* @param to 移动到
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/batchOp")
|
||||
@ResponseBody
|
||||
private R<?> batchOp(
|
||||
@RequestParam(value="ids[]", required=true) Long[] ids,
|
||||
@RequestParam(value="op", required=false) String op,
|
||||
@RequestParam(value="to", required=false) Long to) {
|
||||
List<Long> idArray = Arrays.asList(ids);
|
||||
if (op == null) {
|
||||
// 批量删除
|
||||
metaService.removeByIds(idArray);
|
||||
return R.ok();
|
||||
}
|
||||
else if (to == null) {
|
||||
// 批量移动到顶层
|
||||
LambdaUpdateWrapper<Meta> uw = new UpdateWrapper<Meta>()
|
||||
.lambda().in(Meta::getMid, idArray)
|
||||
.set(Meta::getParentId, 0L);
|
||||
metaService.update(uw);
|
||||
return R.ok();
|
||||
}
|
||||
else {
|
||||
// 批量移动到指定分类下
|
||||
// 检查父子从属关系
|
||||
List<Meta> childToParent = metaService.listFromChildToParent(to);
|
||||
if (childToParent.isEmpty()) {
|
||||
throw RException.badRequest("指定移动至的目标分类不存在");
|
||||
}
|
||||
// 如果传过来的 ids 中有 id 处在父子从属列表中
|
||||
// 则表明是想将父类移动到子类,或者想将自己移动到自己
|
||||
if (childToParent.stream().mapToLong(Meta::getMid).anyMatch(id -> idArray.contains(id))) {
|
||||
throw RException.badRequest("不允许将父级或本级移动到子级或本级");
|
||||
}
|
||||
LambdaUpdateWrapper<Meta> uw = new UpdateWrapper<Meta>()
|
||||
.lambda().in(Meta::getMid, idArray)
|
||||
.set(Meta::getParentId, to);
|
||||
metaService.update(uw);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定 id 的分类
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
private R<?> delete(Long id) {
|
||||
metaService.removeById(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.User;
|
||||
import me.qwq.doghouse.entity.Comment;
|
||||
import me.qwq.doghouse.entity.config.CommentConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.CommentAction;
|
||||
import me.qwq.doghouse.enums.CommentStatusEnum;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.bo.IpInfo;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.IpService;
|
||||
import me.qwq.doghouse.service.MailService;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
import me.qwq.doghouse.util.GeoIPUtil;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* 评论管理
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/comment")
|
||||
public class CommentController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private PostService postService;
|
||||
|
||||
@Autowired
|
||||
private CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
CommentConfig commentConfig;
|
||||
|
||||
@Autowired
|
||||
MailService mailService;
|
||||
|
||||
@Autowired
|
||||
IpService ipService;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index(Long postId) {
|
||||
request.setAttribute("title", "评论管理");
|
||||
if (postId != null) {
|
||||
Post post = postService.getById(postId);
|
||||
if (post != null) {
|
||||
request.setAttribute("postId", postId);
|
||||
request.setAttribute("title", post.getPostTitle() + " 的评论管理");
|
||||
}
|
||||
}
|
||||
return "admin/comment/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评论
|
||||
* @param comment
|
||||
* @param bindingResult
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/update")
|
||||
public R<String> update(Comment comment, BindingResult bindingResult) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
if (Objects.isNull(comment) || Objects.isNull(comment.getId())) {
|
||||
return R.status(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<Comment> uw = new LambdaUpdateWrapper<>();
|
||||
uw.eq(Comment::getId, comment.getId())
|
||||
.set(Comment::getAuthor, comment.getAuthor())
|
||||
.set(Comment::getEmail, comment.getEmail())
|
||||
.set(Comment::getUrl, comment.getUrl())
|
||||
.set(Comment::getContent, comment.getContent())
|
||||
.set(Comment::isMarkdown, comment.isMarkdown());
|
||||
|
||||
if (commentService.update(uw)) {
|
||||
comment = commentService.getById(comment.getId());
|
||||
mailService.addOrUpdateSendQueue(comment);
|
||||
return R.status(HttpStatus.OK);
|
||||
}
|
||||
return R.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员回复
|
||||
* @param id
|
||||
* @param content
|
||||
* @param markdown
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/reply")
|
||||
public R<?> reply(Long id, String content, Boolean markdown) {
|
||||
if (StringUtils.isBlank(content) || id == null) {
|
||||
throw RException.badRequest();
|
||||
}
|
||||
User user = SessionUtils.getUser(session);
|
||||
Comment comment = commentService.getById(id);
|
||||
if (comment == null) {
|
||||
return R.ok("回复的评论已不存在");
|
||||
}
|
||||
|
||||
if (markdown == null) markdown = false;
|
||||
|
||||
if (markdown && !commentConfig.isMarkdownOn()) {
|
||||
throw RException.badRequest("当前评论设置未开启 Markdown,请先开启 Markdown");
|
||||
}
|
||||
|
||||
Comment myComment = new Comment()
|
||||
.setAgent(request.getHeader("User-Agent"))
|
||||
.setAuthor(user.getNickname())
|
||||
.setCommentStatus(CommentStatusEnum.RELEASED)
|
||||
.setContent(content)//StringEscapeUtils.escapeEcmaScript())
|
||||
.setEmail(user.getEmail())
|
||||
.setIp(ipService.getClientIp(request))
|
||||
.setPostId(comment.getPostId())
|
||||
.setReplyTo(id)
|
||||
.setUrl(user.getSite())
|
||||
.setMarkdown(commentConfig.isMarkdownOn() && markdown);
|
||||
|
||||
if (commentService.save(myComment)) {
|
||||
mailService.addOrUpdateSendQueue(myComment);
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.internalServerError("回复失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 转到对应 Post 的评论页
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/jumpTo")
|
||||
public String jumpToPostPage(Long id) {
|
||||
return "redirect:" + commentService.getJumpToUrl(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论分页列表
|
||||
*
|
||||
* @param pageReq 分页参数
|
||||
* @param condition 筛选条件
|
||||
* @return {@code me.qwq.doghouse.pojo.dto.LayPageResp<?>}
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping("/list")
|
||||
public LayPageResp<?> getCommentList(
|
||||
LayPageReq<Comment> pageReq, Comment condition) {
|
||||
|
||||
MPJLambdaWrapper<Comment> ew = new MPJLambdaWrapper<Comment>()
|
||||
.selectAll(Comment.class)
|
||||
.selectAll(Post.class)
|
||||
.leftJoin(Post.class, Post::getPostId, Comment::getPostId)
|
||||
// TODO xxx
|
||||
//.likeOrEqNotNull(condition)
|
||||
.eq(Objects.nonNull(condition.getPostId()), Comment::getPostId, condition.getPostId())
|
||||
.eq(Objects.nonNull(condition.getCommentStatus()), Comment::getCommentStatus, condition.getCommentStatus())
|
||||
|
||||
.eq(Comment::getIsDeleted, false)
|
||||
.orderByDesc(Comment::getCreateTime);
|
||||
|
||||
Page<Comment> page = commentService.page(pageReq, ew);
|
||||
|
||||
// 查询 IP 归属地
|
||||
for (Comment comment : page.getRecords()) {
|
||||
IpInfo ipInfo = new IpInfo().setIp(comment.getIp());
|
||||
GeoIPUtil.queryIpInfoGeoLite(ipInfo);
|
||||
comment.setIpInfo(ipInfo);
|
||||
}
|
||||
|
||||
LayPageResp<Comment> result = new LayPageResp<Comment>(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 评论批量操作
|
||||
*
|
||||
* @param blogInfo
|
||||
* @return me.qwq.doghouse.pojo.dto.R
|
||||
* @date 2019/8/29 12:22
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/batchOp")
|
||||
public R<?> batchOp(
|
||||
@RequestParam("ids[]")
|
||||
Long[] ids, CommentStatusEnum op) {
|
||||
|
||||
if (Objects.isNull(ids) || ids.length == 0) {
|
||||
throw RException.badRequest("提供的评论 ID 不能为空");
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<Comment> uw =
|
||||
new LambdaUpdateWrapper<Comment>()
|
||||
.in(Comment::getId, (Object[])ids);
|
||||
|
||||
if (Objects.isNull(op)) {
|
||||
uw.set(Comment::getIsDeleted, true);
|
||||
}
|
||||
else {
|
||||
uw.set(Comment::getCommentStatus, op);
|
||||
}
|
||||
if (commentService.update(uw)) {
|
||||
for (Comment comment : commentService.list(
|
||||
new LambdaQueryWrapper<Comment>()
|
||||
.in(Comment::getId, (Object[])ids))) {
|
||||
mailService.addOrUpdateSendQueue(comment);
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.internalServerError("批量操作失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置封禁 IP 或 emial
|
||||
* @param ip
|
||||
* @param email
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/block")
|
||||
public R<?> block(@RequestParam(required=false) String ip, @RequestParam(required=false) String email) {
|
||||
if (StringUtils.isAllBlank(ip, email)) {
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
commentConfig
|
||||
.addBlockEmail(email)
|
||||
.addBlockIp(ip);
|
||||
if (commentConfig.saveOrUpdate()) {
|
||||
if (commentConfig.getBlockEmailAction() == CommentAction.NONE || commentConfig.getBlockIpAction() == CommentAction.NONE) {
|
||||
return R.ok(
|
||||
"封禁成功,但您未设置触发封禁后的动作<br>" +
|
||||
"请前往 <b>设置</b> >> <b>评论设置</b> 进行设置");
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.internalServerError("设置封禁失败");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.ConfigService;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* 配置项控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/config")
|
||||
@Slf4j
|
||||
public class ConfigController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
/**
|
||||
* 前端设置页面接口,其设置项必须实现 ConfigInterface 接口,且 resources/templates/admin/config 下有对应的 html 模板,例:
|
||||
* <ol>
|
||||
* <li>假设 <code>ConfigInfo</code> 注解 field 为 wiki,则其 html 模板路径为 resources/templates/admin/config/wiki/index(.html)</li>
|
||||
* <li>假设 <code>ConfigInfo</code> 注解 field 为 mail,则其 html 模板路径为 resources/templates/admin/config/mail/index(.html)</li>
|
||||
* <li>假设 <code>ConfigInfo</code> 注解 field 为空,配置类名为 SiteConfig,则其 html 模板路径为 resources/templates/admin/config/site/index(.html)</li>
|
||||
* </ol>
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
@GetMapping({"/{configField}", "/{configField}/", "/{configField}/index"})
|
||||
public String configPage(@PathVariable String configField)
|
||||
{
|
||||
|
||||
ConfigInfo info = configService.getConfigInfoByField(configField);
|
||||
if (info == null || !info.managed()) {
|
||||
log.info("Try to enter management of {} but ConfigInfo is null or not managed", configField);
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
request.setAttribute("title", info.name());
|
||||
return "admin/config/" + configField + "/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置项统一接口
|
||||
*/
|
||||
@PostMapping({"/{configField}", "/{configField}/", "/{configField}/index"})
|
||||
@ResponseBody
|
||||
public <T extends IConfig<T>> R<?> saveConfig(@PathVariable String configField, @RequestBody JSONObject config) {
|
||||
|
||||
Class<T> clazz = configService.getConfigClassByField(configField);
|
||||
if (clazz == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
return R.judge(
|
||||
configService.saveOrUpdate((T)config.to(clazz)),
|
||||
"保存失败");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import me.qwq.doghouse.config.DoghouseProperties;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Link;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.LinkService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* 友链控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/link")
|
||||
public class LinkController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
DoghouseProperties doghouseProperties;
|
||||
|
||||
@Autowired
|
||||
LinkService linkService;
|
||||
|
||||
/**
|
||||
* 主页
|
||||
* @return
|
||||
*/
|
||||
@GetMapping({"", "/", "/index"})
|
||||
String index() {
|
||||
return "admin/link/index";
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/list")
|
||||
public LayPageResp<?> getLinkList(LayPageReq<Link> pageReq) {
|
||||
|
||||
LambdaQueryWrapper<Link> ew = new LambdaQueryWrapper<Link>()
|
||||
.eq(Link::getIsDeleted, false)
|
||||
.orderByDesc(Link::getHealth, Link::getCreateTime);
|
||||
|
||||
IPage<Link> page = linkService.page(pageReq, ew);
|
||||
LayPageResp<Link> result = new LayPageResp<>(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新文件基本信息
|
||||
* @param asset
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/update")
|
||||
R<?> update(Link link) {
|
||||
if (linkService.saveOrUpdate(link)) {
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.internalServerError("保存失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param assetId
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/delete")
|
||||
R<?> delete(Long id) {
|
||||
if (Objects.isNull(id)) {
|
||||
throw RException.badRequest("友链 ID 不允许为空!");
|
||||
}
|
||||
if (linkService.update(new LambdaUpdateWrapper<Link>()
|
||||
.eq(Link::getId, id)
|
||||
.set(Link::getIsDeleted, true))) {
|
||||
return R.ok();
|
||||
}
|
||||
throw RException.internalServerError("删除友链失败");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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.ResponseBody;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
|
||||
import jodd.util.Base64;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.User;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.UserService;
|
||||
import me.qwq.doghouse.util.CookieUtils;
|
||||
import me.qwq.doghouse.util.Cryptos;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2")
|
||||
public class LoginController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private UserService adminUserService;
|
||||
|
||||
|
||||
@GetMapping("/login")
|
||||
public String loginPage() {
|
||||
|
||||
if (isLogin()) {
|
||||
return "redirect:/admin/v2/index";
|
||||
}
|
||||
|
||||
return "admin/login";
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@ResponseBody
|
||||
public R<?> login(String username, String password, String captcha, String redirect) throws LoginException {
|
||||
if (StringUtils.isBlank(captcha)) {
|
||||
throw new LoginException("验证码不能为空");
|
||||
}
|
||||
Object sessionCaptcha = session.getAttribute(SessionUtils.CAPTCHA);
|
||||
if (Objects.isNull(sessionCaptcha) ||
|
||||
!captcha.equalsIgnoreCase(sessionCaptcha.toString())) {
|
||||
throw new LoginException("验证码错误");
|
||||
}
|
||||
if (StringUtils.isBlank(username) || !passwordIsNotEmpty(password)) {
|
||||
throw new LoginException("用户名和密码不能为空");
|
||||
}
|
||||
User adminUser = adminUserService.getOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getPassword, password));
|
||||
if (adminUser != null) {
|
||||
SessionUtils.setUser(session, adminUser);
|
||||
CookieUtils.setUserToCookie(response, adminUser);
|
||||
|
||||
String to = "/admin/v2";
|
||||
if (StringUtils.isNotEmpty(redirect)) {
|
||||
to = Base64.decodeToString(redirect);
|
||||
}
|
||||
session.removeAttribute(SessionUtils.CAPTCHA);
|
||||
return R.ok(to);
|
||||
} else {
|
||||
// 清除 session 中验证码,避免单次输入验证码后的碰撞
|
||||
// 直到下一次请求 kaptcha 接口再重新设置
|
||||
session.removeAttribute(SessionUtils.CAPTCHA);
|
||||
throw new LoginException("用户名和/或密码错误");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/logout")
|
||||
public String logout() {
|
||||
session.removeAttribute(SessionUtils.LOGIN_USER);
|
||||
return "redirect:/admin/v2/login";
|
||||
}
|
||||
|
||||
|
||||
static final String EMPTY_PASSWORD = Cryptos.sha3("", 224);
|
||||
|
||||
static boolean passwordIsNotEmpty(String password) {
|
||||
return StringUtils.isNotEmpty(password) &&
|
||||
!password.equalsIgnoreCase(EMPTY_PASSWORD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
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.ResponseBody;
|
||||
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Meta;
|
||||
import me.qwq.doghouse.entity.Tag;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.MetaService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/tag")
|
||||
public class TagController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
MetaService metaService;
|
||||
|
||||
@GetMapping({"", "/"})
|
||||
private String index() {
|
||||
request.setAttribute("title", "标签管理");
|
||||
return "admin/tag/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供建议的 Tag,一般用于输入提示
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/suggest")
|
||||
@ResponseBody
|
||||
private List<Tag> suggest(String input){
|
||||
List<Tag> list = new ArrayList<>();
|
||||
if (StringUtils.isBlank(input)) return list;
|
||||
list = metaService.listJoin(Tag.class, new MPJLambdaWrapper<Tag>(Tag.class)
|
||||
.selectAll(Tag.class)
|
||||
.eq(Tag::getType, MetaTypeEnum.TAG)
|
||||
.nested(i -> i
|
||||
.like(Tag::getName, input).or()
|
||||
.like(Tag::getFriendlyName, input))
|
||||
.last(" LIMIT 5"));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增或编辑分类
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@ResponseBody
|
||||
private R<?> update(@Valid Tag tag, BindingResult bindingResult) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
if (tag == null) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "分类不允许为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(tag.getFriendlyName())) {
|
||||
tag.setFriendlyName(tag.getName().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
// 查询友好名称是否重复
|
||||
long count = metaService.count(new MPJLambdaWrapper<Meta>()
|
||||
.eq(Meta::getType, MetaTypeEnum.TAG)
|
||||
.eq(Meta::getFriendlyName, tag.getFriendlyName()));
|
||||
if (count >= 1) {
|
||||
if (tag.getMid() == null) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "友好名称与其他标签重复");
|
||||
}
|
||||
else {
|
||||
Tag existTag = metaService.getOneJoin(Tag.class,
|
||||
new MPJLambdaWrapper<Tag>(Tag.class).eq(Tag::getFriendlyName, tag.getFriendlyName()));
|
||||
if (!existTag.getMid().equals(tag.getMid())) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "友好名称与其他标签重复");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(metaService.saveOrUpdate(tag)) {
|
||||
return R.ok();
|
||||
}
|
||||
throw new RException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定 id 的分类
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/deleteUnusedTags")
|
||||
@ResponseBody
|
||||
private R<?> deleteUnusedTags() {
|
||||
metaService.removeUnusedTags();
|
||||
return R.ok();
|
||||
}
|
||||
/**
|
||||
* 删除指定 id 的分类
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@ResponseBody
|
||||
private R<?> delete(Long id) {
|
||||
metaService.removeById(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package me.qwq.doghouse.controller.admin;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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.ResponseBody;
|
||||
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.ThemeService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/themes")
|
||||
public class ThemeController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
@Autowired
|
||||
ThemeService themeService;
|
||||
|
||||
@GetMapping({"", "/", "/index"})
|
||||
public String index() {
|
||||
return "admin/themes/index";
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/setTheme")
|
||||
public R<?> setTheme(String identity) {
|
||||
return R.judge(siteConfig.setThemeIdentity(identity).saveOrUpdate());
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/extractTheme")
|
||||
public R<?> extractTheme(String identity) {
|
||||
return R.judge(() -> themeService.copyInternalToExternal(identity));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package me.qwq.doghouse.controller.admin.post;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.component.PostTypeComponent;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Asset;
|
||||
import me.qwq.doghouse.entity.Cate;
|
||||
import me.qwq.doghouse.entity.Meta;
|
||||
import me.qwq.doghouse.entity.Tag;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.PostStatusEnum;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.AssetService;
|
||||
import me.qwq.doghouse.service.MetaService;
|
||||
import me.qwq.doghouse.service.RelationshipService;
|
||||
import me.qwq.doghouse.service.post.AbstractPostService;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* Post 后台控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
public abstract class AbstractAdminPostController<C extends IPostTypeConfig<C>, S extends AbstractPostService<S, C>> extends BaseController {
|
||||
|
||||
@Autowired
|
||||
AssetService assetService;
|
||||
@Autowired
|
||||
MetaService metaService;
|
||||
@Autowired
|
||||
RelationshipService relationshipService;
|
||||
@Autowired
|
||||
PostTypeComponent postTypeComponent;
|
||||
|
||||
C postTypeConfig;
|
||||
|
||||
/**
|
||||
* 注意本方法不可信,尤其是某些 PostTypeEnum 未重写本抽象类内的方法的时候。能用 postTypeComponent.getPostTypeConfig(postTypeEnum)
|
||||
* 来获取的时候尽量不用本方法,本方法仅在无法确认 postTypeEnum 的时候,默认其类型是继承了本抽象类的 PostTypeService 对应的类型
|
||||
* @return
|
||||
*/
|
||||
C getPostTypeConfig() {
|
||||
if (postTypeConfig != null) return postTypeConfig;
|
||||
ResolvableType type = ResolvableType.forClass(this.getClass()).as(AbstractAdminPostController.class);
|
||||
ResolvableType iPageConfigType = type.getGeneric(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<C> iPageConfigClass = (Class<C>) iPageConfigType.resolve();
|
||||
postTypeConfig = SpringContextHolder.getBean(iPageConfigClass);
|
||||
return postTypeConfig;
|
||||
}
|
||||
|
||||
S postService;
|
||||
|
||||
/**
|
||||
* 注意本方法不可信,尤其是某些 PostTypeEnum 未重写本抽象类内的方法的时候。能用 postTypeComponent.getPostTypeService(postTypeEnum)
|
||||
* 来获取的时候尽量不用本方法,本方法仅在无法确认 postTypeEnum 的时候,默认其类型是继承了本抽象类的 PostTypeService 对应的类型
|
||||
* @return
|
||||
*/
|
||||
S getPostService() {
|
||||
if (postService != null) return postService;
|
||||
ResolvableType type = ResolvableType.forClass(this.getClass()).as(AbstractAdminPostController.class);
|
||||
ResolvableType postServiceType = type.getGeneric(1);
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<S> postServiceClass = (Class<S>) postServiceType.resolve();
|
||||
postService = SpringContextHolder.getBean(postServiceClass);
|
||||
return postService;
|
||||
}
|
||||
|
||||
@GetMapping({"", "index", "/index"})
|
||||
public String nonSlashIndex() {
|
||||
|
||||
RequestMapping requestMapping = this.getClass().getAnnotation(RequestMapping.class);
|
||||
if (requestMapping == null || requestMapping.value().length == 0) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
String uri = requestMapping.value()[0];
|
||||
uri = uri.replaceAll("/+$", "");
|
||||
return "redirect:" + uri + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表界面
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index(
|
||||
@RequestParam(required = false) Long categoryId,
|
||||
@RequestParam(required = false) Long tagId) {
|
||||
|
||||
PostTypeEnum postType = getPostTypeConfig().getPostType();
|
||||
|
||||
request.setAttribute("title", String.format("%s管理", postType.getNote()));
|
||||
request.setAttribute("manageTitle", String.format("%s列表", postType.getNote()));
|
||||
if (categoryId != null) {
|
||||
Meta cate = metaService.getOne(new MPJLambdaWrapper<Meta>().eq(Meta::getMid, categoryId));
|
||||
request.setAttribute("title", String.format("分类 %s 下的%s管理", cate.getName(), postType.getNote()));
|
||||
request.setAttribute("manageTitle", String.format("分类 %s 下的%s列表", cate.getName(), postType.getNote()));
|
||||
}
|
||||
if (tagId != null) {
|
||||
Meta tag = metaService.getOne(new MPJLambdaWrapper<Meta>().eq(Meta::getMid, tagId));;
|
||||
request.setAttribute("title", String.format("标签 %s 下的%s管理", tag.getName(), postType.getNote()));
|
||||
request.setAttribute("manageTitle", String.format("标签 %s 下的%s列表", tag.getName(), postType.getNote()));
|
||||
}
|
||||
return "admin/" + postType.toLowerString() + "/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
@GetMapping("/edit")
|
||||
public String edit(@RequestParam(required = false) Long postId) {
|
||||
|
||||
PostTypeEnum postType = getPostTypeConfig().getPostType();
|
||||
|
||||
Post post = getPostService().getById(postId);
|
||||
if (post != null) {
|
||||
request.setAttribute("post", post);
|
||||
request.setAttribute("title", "编辑 - " + post.getPostTitle());
|
||||
List<Asset> assets = assetService.list(
|
||||
new LambdaQueryWrapper<Asset>().eq(Asset::getFromPostId, postId));
|
||||
request.setAttribute("assets", StringEscapeUtils.escapeEcmaScript(JSONObject.toJSONString(assets)));
|
||||
}
|
||||
else {
|
||||
request.setAttribute("title", "新增" + postType.getNote());
|
||||
}
|
||||
return "admin/" + postType.toLowerString() + "/edit";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @param post 文章实体模型
|
||||
* @param assets 包含的资产
|
||||
* @param tagsName 标签
|
||||
* @param categoriesId 分类
|
||||
* @param postType 文章类型 - 文章|页面|Wiki
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/edit")
|
||||
public R<?> savePost(@Valid Post post,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(name="assets[]", required=false) Long[] assets,
|
||||
@RequestParam(name="_tagsName[]", required=false) String[] tagsName,
|
||||
@RequestParam(name="_categoriesId[]", required=false) Long[] categoriesId
|
||||
) {
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
|
||||
if (ObjectUtils.isEmpty(post)) {
|
||||
throw RException.badRequest("无法识别的请求");
|
||||
}
|
||||
Post existsPost = post.getPostId() != null ? getPostService().getById(post.getPostId()) : null;
|
||||
Boolean isUpdate = existsPost != null;
|
||||
|
||||
getPostService().beforeSaving(post, assets, tagsName, categoriesId, isUpdate);
|
||||
|
||||
if (!getPostService().saveOrUpdate(post)) {
|
||||
throw RException.internalServerError("保存失败");
|
||||
}
|
||||
|
||||
getPostService().afterSaving(post, assets, tagsName, categoriesId, isUpdate);
|
||||
|
||||
Long postId = post.getPostId();
|
||||
post = getPostService().getById(postId);
|
||||
return R.ok(post);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pageReq 分页请求模型
|
||||
* @param condition 查询条件
|
||||
* @param categoryId 分类 ID,可指定分类查询
|
||||
* @param tagId 标签 ID,可指定标签查询
|
||||
* @return Post 结果列表分页模型
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping("/list")
|
||||
public LayPageResp<?> getPostList(
|
||||
LayPageReq<Post> pageReq,
|
||||
Post condition,
|
||||
Long categoryId,
|
||||
Long tagId,
|
||||
String keywords) {
|
||||
|
||||
MPJLambdaWrapper<Post> ew = getPostService().getPostCommonWrapper()
|
||||
// TODO
|
||||
//.likeOrEqNotNull(condition) // postType 以当前控制器为准
|
||||
.eq(Post::getType, getPostTypeConfig().getPostType())
|
||||
.eq(Post::getIsDeleted, false)
|
||||
.eq(Objects.nonNull(categoryId), Cate::getMid, categoryId)
|
||||
.eq(Objects.nonNull(tagId), Tag::getMid, tagId)
|
||||
.and(StringUtils.isNotBlank(keywords), o -> {
|
||||
o.like(Post::getPostTitle, keywords)
|
||||
.or().like(Post::getPostPreface, keywords)
|
||||
.or().like(Post::getPostContent, keywords)
|
||||
.or().like(Post::getKeywords, keywords);
|
||||
})
|
||||
.orderByDesc(Post::getCreateTime);
|
||||
IPage<Post> page = getPostService().page(new Page<>(pageReq.getPage(), pageReq.getLimit()), ew);
|
||||
|
||||
LayPageResp<Post> result = new LayPageResp<>(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对 Post 批量操作
|
||||
* @param ids 批量操作的 Post ID 集合
|
||||
* @param op 将要转换为的 PostStatusEnum
|
||||
* @param to 移动至(针对 Wiki)
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/batchOp")
|
||||
public R<?> batchOp(
|
||||
@RequestParam(value="ids[]", required=true)
|
||||
Long[] ids, PostStatusEnum postStatus) {
|
||||
|
||||
if (Objects.isNull(ids) || ids.length == 0) {
|
||||
throw RException.badRequest("提供的批量操作 ID 不能为空");
|
||||
}
|
||||
List<Long> idArray = Arrays.asList(ids);
|
||||
LambdaQueryWrapper<Post> qw = new LambdaQueryWrapper<Post>().in(Post::getPostId, idArray);
|
||||
List<Post> waitForUpdates = postService.list(qw);
|
||||
|
||||
// 看下这批的 postType 是否一致, 不一致直接不允许, 简化操作
|
||||
boolean allEqual = waitForUpdates.stream()
|
||||
.map(Post::getType)
|
||||
.distinct()
|
||||
.count() <= 1;
|
||||
if (!allEqual) {
|
||||
throw RException.badRequest("批量操作类型不一致");
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<Post> uw =
|
||||
new LambdaUpdateWrapper<Post>()
|
||||
.in(Post::getPostId, idArray);
|
||||
|
||||
boolean delete = Objects.isNull(postStatus);
|
||||
// 逻辑删除
|
||||
uw.set(delete, Post::getIsDeleted, true);
|
||||
// 更改 Post 状态
|
||||
uw.set(!delete, Post::getPostStatus, postStatus);
|
||||
uw.set(!delete && postStatus != PostStatusEnum.RELEASED, Post::getSearchable, false);
|
||||
return R.judge(
|
||||
getPostService().update(uw),
|
||||
"批量操作保存失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章
|
||||
* @param postId
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/delete")
|
||||
public R<?> delete(@RequestParam Long postId) {
|
||||
Post post = postService.getById(postId);
|
||||
if (post == null || post.getIsDeleted()) {
|
||||
throw RException.badRequest("删除失败, 请检查是否已删除或不存在");
|
||||
}
|
||||
AbstractPostService<?, ?> postTypeService = postTypeComponent.getPostTypeService(post.getType());
|
||||
LambdaUpdateWrapper<Post> uw = new LambdaUpdateWrapper<Post>()
|
||||
.eq(Post::getPostId, postId)
|
||||
.set(Post::getIsDeleted, true)
|
||||
.set(Post::getUpdateTime, LocalDateTime.now());
|
||||
return R.judge(postTypeService.update(uw));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.qwq.doghouse.controller.admin.post;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.PageConfig;
|
||||
import me.qwq.doghouse.service.post.PageService;
|
||||
|
||||
|
||||
/**
|
||||
* Post 后台控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/page")
|
||||
@Slf4j
|
||||
public class AdminPageController extends AbstractAdminPostController<PageConfig, PageService> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package me.qwq.doghouse.controller.admin.post;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Post 后台控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/post")
|
||||
@Slf4j
|
||||
public class AdminPostController extends AbstractAdminPostController<SiteConfig, PostService> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.qwq.doghouse.controller.admin.post;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.TweetConfig;
|
||||
import me.qwq.doghouse.service.post.TweetService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Post 后台控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/tweet")
|
||||
@Slf4j
|
||||
public class AdminTweetController extends AbstractAdminPostController<TweetConfig, TweetService> {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package me.qwq.doghouse.controller.admin.post;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.Cate;
|
||||
import me.qwq.doghouse.entity.Tag;
|
||||
import me.qwq.doghouse.entity.config.WikiConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.PostStatusEnum;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageReq;
|
||||
import me.qwq.doghouse.pojo.dto.LayPageResp;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.post.WikiService;
|
||||
import me.qwq.doghouse.util.EscapeUnescapeUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
|
||||
|
||||
/**
|
||||
* Post 后台控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/v2/wiki")
|
||||
@Slf4j
|
||||
public class AdminWikiController extends AbstractAdminPostController<WikiConfig, WikiService> {
|
||||
|
||||
@Autowired
|
||||
WikiService wikiService;
|
||||
|
||||
/**
|
||||
* 重写 Wiki 列表渲染
|
||||
*/
|
||||
@Override
|
||||
public LayPageResp<?> getPostList(
|
||||
LayPageReq<Post> pageReq,
|
||||
Post condition,
|
||||
Long categoryId,
|
||||
Long tagId,
|
||||
String keywords) {
|
||||
|
||||
if (condition != null) {
|
||||
List<Long> unfolded = WikiService.getUnfoldedWikis(request);
|
||||
if (unfolded == null) {
|
||||
Cookie unfoldedCookie = new Cookie(WikiService.UNFOLDED_WIKIS,
|
||||
EscapeUnescapeUtils.escape("[]"));
|
||||
unfoldedCookie.setPath("/");
|
||||
response.addCookie(unfoldedCookie);
|
||||
}
|
||||
|
||||
MPJLambdaWrapper<Post> ew = wikiService.getWikiDefaultWrapper(true)
|
||||
// TODO
|
||||
//.likeOrEqNotNull(condition)
|
||||
.eq(Objects.nonNull(condition.getParentId()), Post::getParentId, condition.getParentId())
|
||||
.eq(Objects.nonNull(condition.getType()), Post::getType, condition.getType())
|
||||
.eq(Post::getIsDeleted, false)
|
||||
.eq(Objects.nonNull(categoryId), Cate::getMid, categoryId)
|
||||
.eq(Objects.nonNull(tagId), Tag::getMid, tagId)
|
||||
.and(StringUtils.isNotBlank(keywords), o -> {
|
||||
o.like(Post::getPostTitle, keywords)
|
||||
.or().like(Post::getPostPreface, keywords)
|
||||
.or().like(Post::getPostContent, keywords)
|
||||
.or().like(Post::getKeywords, keywords);
|
||||
});
|
||||
if (condition.getParentId() == null) {
|
||||
// 分页模型
|
||||
IPage<Post> page = wikiService.pageWikiWithChildren(pageReq, ew, unfolded);
|
||||
LayPageResp<Post> result = new LayPageResp<>(page);
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
// 单独查询
|
||||
List<Post> listPost = wikiService.listWikiWithChildren(ew, unfolded);
|
||||
return new LayPageResp<Post>().setData(listPost).setCount(listPost.size());
|
||||
}
|
||||
}
|
||||
else {
|
||||
return super.getPostList(pageReq, condition, categoryId, tagId, keywords);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<?> batchOp(
|
||||
@RequestParam(value="ids[]", required=true)
|
||||
Long[] ids, PostStatusEnum postStatus) {
|
||||
|
||||
if (Objects.isNull(ids) || ids.length == 0) {
|
||||
throw RException.badRequest("提供的批量操作 ID 不能为空");
|
||||
}
|
||||
|
||||
// 接口方法未实现 to 参数接收, 这里手动从 request 里面取
|
||||
Long to = null;
|
||||
|
||||
try {
|
||||
String toString = request.getParameter("to");
|
||||
if (!StringUtils.isBlank(toString)) {
|
||||
to = Long.valueOf(toString);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
throw RException.badRequest("错误的移动层级");
|
||||
}
|
||||
|
||||
List<Long> idArray = Arrays.asList(ids);
|
||||
|
||||
LambdaUpdateWrapper<Post> uw =
|
||||
new LambdaUpdateWrapper<Post>()
|
||||
.in(Post::getPostId, idArray);
|
||||
|
||||
boolean delete = Objects.isNull(postStatus) && Objects.isNull(to); // 删除
|
||||
boolean changeStatus = !delete && Objects.nonNull(postStatus); // 更新状态
|
||||
boolean move = !delete && !changeStatus && to != null; // 移动层级
|
||||
|
||||
if (!(delete || changeStatus || move)) {
|
||||
throw RException.badRequest("非法操作");
|
||||
}
|
||||
|
||||
if (!move) {
|
||||
uw.set(delete, Post::getIsDeleted, true);
|
||||
uw.set(changeStatus, Post::getPostStatus, postStatus);
|
||||
uw.set(changeStatus && postStatus != PostStatusEnum.RELEASED, Post::getSearchable, false);
|
||||
if (wikiService.update(uw)) {
|
||||
// 更新可能包含的子级
|
||||
boolean subOperationFlag = true;
|
||||
for (Long id : idArray) {
|
||||
Post post = wikiService.getById(id);
|
||||
if (delete ?
|
||||
wikiService.deleteChildren(post) :
|
||||
wikiService.resetSearchableStatus(post)) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
subOperationFlag = false;
|
||||
log.warn(delete ?
|
||||
"删除 postId={}, title={} 子项失败":
|
||||
"重设 postId={}, title={} Wiki 子项可搜索状态失败",
|
||||
id, post.getTitle());
|
||||
}
|
||||
}
|
||||
// TODO 这里应该做成事务
|
||||
return R.judge(subOperationFlag,
|
||||
delete ?
|
||||
"删除部分子项失败, 请查看输出日志" :
|
||||
"重设部分子项可搜索状态失败,请查看输出日志");
|
||||
}
|
||||
else {
|
||||
throw RException.internalServerError("批量操作保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
// to
|
||||
if (to < -1L) {
|
||||
// 移动到顶级
|
||||
to = -1L;
|
||||
}
|
||||
else if (to >= 0) {
|
||||
|
||||
// 检查父子从属关系
|
||||
List<Post> childToParent = wikiService.listFromChildToParent(to, true, true);
|
||||
if (childToParent.isEmpty()) {
|
||||
throw RException.badRequest("指定移动至的目标 Wiki 不存在");
|
||||
}
|
||||
// 如果传过来的 ids 中有 id 处在父子从属列表中
|
||||
// 则表明是想将父类移动到子类,或者想将自己移动到自己
|
||||
if (childToParent.stream().mapToLong(Post::getPostId).anyMatch(id -> idArray.contains(id))) {
|
||||
throw RException.badRequest("不允许将父级或本级移动到子级或本级");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取欲移动的 Post 的原 parentId,待移动成功后重排这些 parent 下的子级
|
||||
List<Long> originalParentIds = getPostService().getParentIds(idArray);
|
||||
|
||||
try {
|
||||
if (!getPostService().batchMoveTo(idArray, to)) {
|
||||
throw RException.badRequest("重设排序失败");
|
||||
}
|
||||
|
||||
for (Long parentId : originalParentIds) {
|
||||
if (!wikiService.reorderChildren(parentId)) {
|
||||
log.warn(
|
||||
"重排原 parentId = {} 下子级失败,但移动将继续", parentId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 移动后重设可搜索状态
|
||||
if (to != -1L) {
|
||||
Post toPost = getPostService().getById(to);
|
||||
if (!wikiService.resetSearchableStatus(toPost)) {
|
||||
log.warn("重设 postId={}, title={} Wiki 子项可搜索状态失败", to, toPost.getTitle());
|
||||
throw RException.internalServerError("重设部分子项可搜索状态失败,请查看输出日志");
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 判断原本是否有密码
|
||||
uw.set(Post::getSearchable, true)
|
||||
.eq(Post::getPassword, "");
|
||||
if (!getPostService().update(uw)) {
|
||||
throw RException.internalServerError("重设移动到顶级的可搜索状态失败");
|
||||
}
|
||||
boolean resetSearchableOk = true;
|
||||
for (Long id : idArray) {
|
||||
Post post = getPostService().getById(id);
|
||||
if (wikiService.resetSearchableStatus(post)) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
resetSearchableOk = false;
|
||||
log.warn("重设 postId={}, title={} Wiki 子项可搜索状态失败", id, post.getTitle());
|
||||
}
|
||||
}
|
||||
if (resetSearchableOk)
|
||||
return R.ok();
|
||||
else {
|
||||
throw RException.internalServerError("重设部分子项可搜索状态失败,请查看输出日志");
|
||||
}
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (e instanceof RException) {
|
||||
throw e;
|
||||
}
|
||||
log.error("批量移动层级失败", e);
|
||||
}
|
||||
throw RException.internalServerError("批量操作失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重排, 只有 Wiki 有这个操作
|
||||
* <p>TODO 前端使用拖拽来实现重排
|
||||
* @param postId
|
||||
* @param up
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping("/reorder")
|
||||
public R<?> reorder(@RequestParam Long postId, boolean up) {
|
||||
try {
|
||||
if (!getPostService().reorder(postId, up)) {
|
||||
throw RException.badRequest("重设排序失败");
|
||||
}
|
||||
} catch (RException e) {
|
||||
throw e;
|
||||
}
|
||||
return R.ok(postId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package me.qwq.doghouse.controller.api.v1;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jdom2.DocType;
|
||||
import org.jdom2.Document;
|
||||
import org.jdom2.Element;
|
||||
import org.jdom2.Namespace;
|
||||
import org.jdom2.output.Format;
|
||||
import org.jdom2.output.XMLOutputter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.dogface.Dogface;
|
||||
import me.qwq.doghouse.component.Rk;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.StatisticsService;
|
||||
import me.qwq.doghouse.util.CompressUtils;
|
||||
import me.qwq.doghouse.util.GravatarUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
@Slf4j
|
||||
public class ApiControllerV1 {
|
||||
|
||||
private static final Namespace SVG_NAMESPACE = Namespace.getNamespace("http://www.w3.org/2000/svg");
|
||||
|
||||
private static final String[] POST_FILLS_VAR = {"var(--post-fill-0)", "var(--post-fill-1)", "var(--post-fill-2)", "var(--post-fill-3)", "var(--post-fill-4)"};
|
||||
private static final String[] POST_STROKES_VAR = {"var(--post-stroke-0)", "var(--post-stroke-1)", "var(--post-stroke-2)", "var(--post-stroke-3)", "var(--post-stroke-4)"};
|
||||
private static final String[] COMMENT_FILLS_VAR = {"var(--comment-fill-0)", "var(--comment-fill-1)", "var(--comment-fill-2)", "var(--comment-fill-3)", "var(--comment-fill-4)"};
|
||||
private static final String[] COMMENT_STROKES_VAR = {"var(--comment-stroke-0)", "var(--comment-stroke-1)", "var(--comment-stroke-2)", "var(--comment-stroke-3)", "var(--comment-stroke-4)"};
|
||||
|
||||
@Autowired
|
||||
StatisticsService statisticsService;
|
||||
|
||||
@Autowired
|
||||
GravatarUtils gravatarUtils;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
Dogface dogface;
|
||||
|
||||
@Autowired
|
||||
HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
HttpServletResponse response;
|
||||
|
||||
@GetMapping(value={
|
||||
"/gravatar/{hash}",
|
||||
"/gravatar/{hash}/{size}"}, produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getGravatar(@PathVariable(value="hash") String hash,
|
||||
@PathVariable(value="size", required=false) Integer size,
|
||||
@RequestParam(required=false) String nocache) {
|
||||
|
||||
if (size == null || size <= 0) size = 36;
|
||||
CompletableFuture<byte[]> future =
|
||||
gravatarUtils.getReversedProxyGravatarAsync(hash, size, StringUtils.isBlank(nocache));
|
||||
|
||||
byte[] avatar;
|
||||
try {
|
||||
avatar = future.get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
if (avatar == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
try {
|
||||
response.setHeader("Content-Encoding", "gzip");
|
||||
return CompressUtils.compressGZ(avatar);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("GZIP 压缩 gravatar 头像错误", e);
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value={
|
||||
"/dogavatar/{hash}",
|
||||
"/dogavatar/{hash}/{size}"}, produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public byte[] getDogavatar(@PathVariable(value="hash") String hash,
|
||||
@PathVariable(value="size", required=false) Integer size) throws IOException {
|
||||
|
||||
if (size == null || size <= 0) size = 36;
|
||||
byte[] avatar = dogface.getDogavatar(hash, size);
|
||||
if (avatar == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
try {
|
||||
response.setHeader("Content-Encoding", "gzip");
|
||||
return CompressUtils.compressGZ(avatar);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("GZIP 压缩 gravatar 头像错误", e);
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
}
|
||||
@GetMapping(value={
|
||||
"/gravatars/clearCache"})
|
||||
public R<?> clearGravatarCache() {
|
||||
|
||||
Rk.Gravatar.clearGravatarCache();
|
||||
return R.ok();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static String colorClassifier(int count, String[] colorSet) {
|
||||
if (count < 0) count = 0;
|
||||
if (count < 4) return colorSet[count];
|
||||
return colorSet[4];
|
||||
}
|
||||
|
||||
@GetMapping(path="/getPassDaysStatistics", produces="application/json")
|
||||
public JSONObject getPassDaysStatistics(Integer passDays) {
|
||||
return statisticsService.getPostAndCommentCountsWithDates(passDays, null);
|
||||
}
|
||||
|
||||
public String gitLikeStatisticsSvg(Integer cols, Integer rows) {
|
||||
|
||||
if (cols == null || cols < 1) {
|
||||
cols = 1;
|
||||
}
|
||||
if (rows == null || rows < 1) {
|
||||
rows = 1;
|
||||
}
|
||||
|
||||
int span = 2, size = 8, stroke = 1;
|
||||
|
||||
Integer total = cols * rows;
|
||||
|
||||
|
||||
Element svgRoot = new Element("svg");
|
||||
// 设置命名空间
|
||||
svgRoot.setNamespace(SVG_NAMESPACE);
|
||||
|
||||
int
|
||||
w = (span + size) * cols - span + stroke * 2,
|
||||
h = (span + size) * rows - span + stroke * 2;
|
||||
|
||||
svgRoot.setAttribute("width", "100%");
|
||||
svgRoot.setAttribute("height", "100%");
|
||||
svgRoot.setAttribute("viewBox", String.format("0 0 %d %d", w, h));
|
||||
|
||||
// 获取 n 日以来的文章和评论统计
|
||||
JSONObject jo = statisticsService.getPostAndCommentCountsWithDates(total, null);
|
||||
|
||||
List<String> dates = jo.getList("dates", String.class);
|
||||
List<Integer> postCounts = jo.getList("postCounts", Integer.class);
|
||||
List<Integer> commentCounts = jo.getList("commentCounts", Integer.class);
|
||||
|
||||
for (int col = 0; col < cols; col++) {
|
||||
for (int row = 0; row < rows; row++) {
|
||||
int offset = col * rows + row;
|
||||
String date = dates.get(offset);
|
||||
Integer posts = postCounts.get(offset);
|
||||
Integer comments = commentCounts.get(offset);
|
||||
Boolean hasPost = posts != 0;
|
||||
Boolean hasComment = comments != 0;
|
||||
|
||||
int x = stroke + (span + size) * col;
|
||||
int y = stroke + (span + size) * row;
|
||||
|
||||
Element postRect = new Element("rect");
|
||||
postRect.setAttribute("x", Integer.toString(x));
|
||||
postRect.setAttribute("y", Integer.toString(y));
|
||||
|
||||
postRect.setAttribute("stroke-width", "1.2");
|
||||
postRect.setAttribute("stroke",
|
||||
!hasComment ?
|
||||
colorClassifier(posts, POST_STROKES_VAR) :
|
||||
colorClassifier(comments, COMMENT_STROKES_VAR));
|
||||
|
||||
postRect.setAttribute("width", Integer.toString(size));
|
||||
postRect.setAttribute("height", Integer.toString(size));
|
||||
postRect.setAttribute("rx", Integer.toString(stroke));
|
||||
postRect.setAttribute("ry", Integer.toString(stroke));
|
||||
postRect.setAttribute("fill",
|
||||
(hasComment && !hasPost) ?
|
||||
colorClassifier(comments, COMMENT_FILLS_VAR):
|
||||
colorClassifier(posts, POST_FILLS_VAR));
|
||||
|
||||
postRect.setAttribute("data-posts", posts.toString());
|
||||
postRect.setAttribute("data-comments", comments.toString());
|
||||
postRect.setAttribute("data-date", date);
|
||||
postRect.setNamespace(SVG_NAMESPACE);
|
||||
|
||||
// 将矩形元素添加到 <svg> 元素中
|
||||
svgRoot.addContent(postRect);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建文档对象
|
||||
Document svgDoc = new Document(svgRoot);
|
||||
DocType svgDocType = new DocType("svg");
|
||||
svgDoc.setDocType(svgDocType);
|
||||
|
||||
// 输出SVG文档到控制台
|
||||
XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
|
||||
return xmlOutputter.outputString(svgDoc);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package me.qwq.doghouse.controller.api.v2;
|
||||
|
||||
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 com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import me.qwq.dogface.Dogface;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.Comment;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.StatisticsService;
|
||||
import me.qwq.doghouse.util.GravatarUtils;
|
||||
import me.qwq.doghouse.util.MarkDownUtils;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v2/comment")
|
||||
public class CommentApiControllerV2 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
StatisticsService statisticsService;
|
||||
|
||||
@Autowired
|
||||
GravatarUtils gravatarUtils;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
Dogface dogface;
|
||||
|
||||
@PostMapping("/preview")
|
||||
public R<?> preview(@RequestBody JSONObject jo) {
|
||||
String markDown = MarkDownUtils.commentToHtml(jo.getString("content"));
|
||||
return R.ok(markDown);
|
||||
}
|
||||
|
||||
@GetMapping("/getContentSource/{id}")
|
||||
public R<?> getContentSource(@PathVariable("id") Long id) {
|
||||
if (!isLogin() && !SessionUtils.containsCommentId(session, id)) {
|
||||
throw RException.unauthorized("非法请求");
|
||||
}
|
||||
Comment commentForEdit = commentService.getById(id);
|
||||
if (commentForEdit == null) {
|
||||
// 这一般不会进来,除非是管理员
|
||||
throw RException.badRequest("非法请求");
|
||||
}
|
||||
|
||||
JSONObject jo = new JSONObject();
|
||||
jo.put("author", commentForEdit.getAuthor());
|
||||
jo.put("isMarkdown", commentForEdit.isMarkdown());
|
||||
jo.put("email", commentForEdit.getEmail());
|
||||
jo.put("url", commentForEdit.getUrl());
|
||||
jo.put("content", commentForEdit.getContent());
|
||||
jo.put("subscribeReply", commentForEdit.isSubscribeReply());
|
||||
jo.put("commentStatu", commentForEdit.getCommentStatus());
|
||||
jo.put("id", commentForEdit.getId());
|
||||
|
||||
return R.ok(jo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package me.qwq.doghouse.controller.api.v2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.dogface.Dogface;
|
||||
import me.qwq.doghouse.component.Rk;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.CommentService;
|
||||
import me.qwq.doghouse.service.StatisticsService;
|
||||
import me.qwq.doghouse.util.CompressUtils;
|
||||
import me.qwq.doghouse.util.GravatarUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v2/gravatar")
|
||||
@Slf4j
|
||||
public class GravatarApiControllerV2 extends BaseController {
|
||||
|
||||
@Autowired
|
||||
StatisticsService statisticsService;
|
||||
|
||||
@Autowired
|
||||
GravatarUtils gravatarUtils;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
Dogface dogface;
|
||||
|
||||
@GetMapping(value={
|
||||
"/get/{hash}",
|
||||
"/get/{hash}/{size}"}, produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getGravatar(@PathVariable(value="hash") String hash,
|
||||
@PathVariable(value="size", required=false) Integer size,
|
||||
@RequestParam(required=false) String nocache) {
|
||||
|
||||
if (size == null || size <= 0) size = 36;
|
||||
CompletableFuture<byte[]> future =
|
||||
gravatarUtils.getReversedProxyGravatarAsync(hash, size, StringUtils.isBlank(nocache));
|
||||
|
||||
byte[] avatar;
|
||||
try {
|
||||
avatar = future.get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
if (avatar == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
try {
|
||||
response.setHeader("Content-Encoding", "gzip");
|
||||
return CompressUtils.compressGZ(avatar);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("GZIP 压缩 gravatar 头像错误", e);
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value={
|
||||
"/dog/{seed}",
|
||||
"/dog/{seed}/{size}"}, produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public byte[] getDogavatar(@PathVariable(value="seed") String seed,
|
||||
@PathVariable(value="size", required=false) Integer size) throws IOException {
|
||||
|
||||
if (size == null || size <= 0) size = 36;
|
||||
byte[] avatar = dogface.getDogavatar(seed, size);
|
||||
if (avatar == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
try {
|
||||
response.setHeader("Content-Encoding", "gzip");
|
||||
return CompressUtils.compressGZ(avatar);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("GZIP 压缩 gravatar 头像错误", e);
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
}
|
||||
@GetMapping(value={
|
||||
"/clearCache"})
|
||||
public R<?> clearGravatarCache() {
|
||||
|
||||
Rk.Gravatar.clearGravatarCache();
|
||||
return R.ok();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package me.qwq.doghouse.controller.blog;
|
||||
|
||||
|
||||
import com.rometools.rome.feed.synd.*;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.service.*;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Feed
|
||||
*
|
||||
* @author Doghole
|
||||
* @date 2021/12/23
|
||||
*/
|
||||
@Controller
|
||||
public class FeedController extends BaseController {
|
||||
|
||||
public final static String ATOM_1_0 = "atom_1.0";
|
||||
public final static String RSS_2_0 = "rss_2.0";
|
||||
public final static String CATEGORY_PATH = "/category/";
|
||||
public final static String COMMA = ",";
|
||||
public final static String FEED_PATH = "/feed.xml";
|
||||
public final static String TEXT_HTML = "text/html";
|
||||
public final static String POST_ENCRYPTED = "加密文章 ";
|
||||
public final static String POST_ENCRYPTED_DESCRIPTION = "该文章已加密,请输入密码查看";
|
||||
public final static String RSS_LANGUAGE = "zh-cn";
|
||||
public final static String SELF = "self";
|
||||
public final static String SLASH = "/";
|
||||
public final static String UTF8 = "utf-8";
|
||||
public final static String MORE = "<!--more-->";
|
||||
|
||||
@Autowired
|
||||
PostService postService;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
@Autowired
|
||||
MarkdownService markdownService;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@GetMapping(value=FEED_PATH, produces = "application/xml")
|
||||
@ResponseBody
|
||||
public String feed() throws FeedException {
|
||||
|
||||
if (!siteConfig.getRssOn()) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
final String siteAddr = !siteConfig.getAddress().endsWith(SLASH) ?
|
||||
siteConfig.getAddress() : siteConfig.getAddress().substring(0, siteConfig.getAddress().length());
|
||||
|
||||
SyndLink rssLink = new SyndLinkImpl() {{
|
||||
setHref(new StringBuilder(siteAddr).append(FEED_PATH).toString());
|
||||
setRel(SELF);
|
||||
}};
|
||||
|
||||
SyndLink siteLink = new SyndLinkImpl() {{setHref(siteAddr);}};
|
||||
|
||||
List<SyndLink> links = new ArrayList<SyndLink>() {{
|
||||
add(rssLink);add(siteLink);
|
||||
}};
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType(ATOM_1_0);
|
||||
feed.setCopyright(siteConfig.getCopyright());
|
||||
feed.setDescription(siteConfig.getDescription());
|
||||
feed.setEncoding(UTF8);
|
||||
feed.setLinks(links);
|
||||
feed.setLanguage(RSS_LANGUAGE);
|
||||
feed.setTitle(siteConfig.getName());
|
||||
|
||||
List<SyndEntry> entries = new ArrayList<>();
|
||||
List<Post> posts = postService.getLatestPosts(siteConfig.getRecentSize());
|
||||
for (Post post : posts) {
|
||||
boolean hasPassword = StringUtils.isNotEmpty(post.getPassword());
|
||||
|
||||
SyndEntry entry = new SyndEntryImpl();
|
||||
entry.setTitle(hasPassword?POST_ENCRYPTED + post.getPostId():post.getPostTitle());
|
||||
// Add content if it's not encrypted
|
||||
if (!hasPassword) {
|
||||
SyndContent content = new SyndContentImpl();
|
||||
content.setType(TEXT_HTML);
|
||||
content.setValue(markdownService.renderPostContent(post));
|
||||
entry.setContents(new ArrayList<SyndContent>(){{add(content);}});
|
||||
}
|
||||
|
||||
// Add description if it's present
|
||||
if (hasPassword) {
|
||||
SyndContent description = new SyndContentImpl();
|
||||
description.setType(TEXT_HTML);
|
||||
description.setValue(POST_ENCRYPTED_DESCRIPTION);
|
||||
entry.setDescription(description);
|
||||
}
|
||||
else {
|
||||
SyndContent description = new SyndContentImpl();
|
||||
description.setType(TEXT_HTML);
|
||||
description.setValue(markdownService.renderPostPreface(post));
|
||||
entry.setDescription(description);
|
||||
}
|
||||
|
||||
entry.setLink(new StringBuilder(siteAddr).append(post.getLink()).toString());
|
||||
entry.setPublishedDate(Date.from(post.getCreateTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
entry.setUpdatedDate(Date.from(post.getUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
|
||||
if (StringUtils.isBlank(post.getPassword()) && StringUtils.isNotBlank(post.getCategoriesName())) {
|
||||
List<SyndCategory> categories = new ArrayList<>();
|
||||
String[] categoriesFriendlyNames = post.getCategoriesFriendlyName().split(COMMA);
|
||||
String[] categoriesNames = post.getCategoriesName().split(COMMA);
|
||||
for (int i = 0; i < categoriesFriendlyNames.length; i++) {
|
||||
String cateName = categoriesNames[i],
|
||||
cateFriendlyName = categoriesFriendlyNames[i];
|
||||
try {
|
||||
categories.add(new SyndCategoryImpl() {{
|
||||
this.setLabel(cateFriendlyName);
|
||||
this.setName(cateName);
|
||||
this.setTaxonomyUri(
|
||||
new StringBuilder()
|
||||
.append(siteAddr)
|
||||
.append(CATEGORY_PATH)
|
||||
.append(URLEncoder.encode(cateFriendlyName, UTF8)).toString());
|
||||
}});
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!categories.isEmpty()) {
|
||||
entry.setCategories(categories);
|
||||
}
|
||||
}
|
||||
entries.add(entry);
|
||||
}
|
||||
feed.setEntries(entries);
|
||||
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
return output.outputString(feed);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package me.qwq.doghouse.controller.blog;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
public class KaptchaController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
DefaultKaptcha kaptcha;
|
||||
|
||||
@GetMapping(value="/get", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public byte[] getCaptcha() throws Exception {
|
||||
String createText = kaptcha.createText();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
session.setAttribute(SessionUtils.CAPTCHA, createText);
|
||||
ImageIO.write(kaptcha.createImage(createText), "jpg", os);
|
||||
byte[] result = os.toByteArray();
|
||||
os.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package me.qwq.doghouse.controller.blog;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.component.PostTypeComponent;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.*;
|
||||
import me.qwq.doghouse.entity.config.CommentConfig;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.PostStatusEnum;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.exception.RException;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.pojo.bo.PostPassJar;
|
||||
import me.qwq.doghouse.pojo.dto.R;
|
||||
import me.qwq.doghouse.service.*;
|
||||
import me.qwq.doghouse.service.post.AbstractPostService;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
import me.qwq.doghouse.util.Cryptos;
|
||||
import me.qwq.doghouse.util.IPUtils;
|
||||
import me.qwq.doghouse.util.MarkDownUtils;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
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.ResponseBody;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* 前端控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
public class PostFrontendController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
PostTypeComponent postTypeComponent;
|
||||
|
||||
@Autowired
|
||||
PostService postService;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
MarkdownService markdownService;
|
||||
|
||||
@Autowired
|
||||
MailService mailService;
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
@Autowired
|
||||
CommentConfig commentConfig;
|
||||
|
||||
@Autowired
|
||||
IpService ipService;
|
||||
|
||||
@Autowired
|
||||
ThemeService themeService;
|
||||
|
||||
/**
|
||||
* 文章详情
|
||||
*/
|
||||
@GetMapping({
|
||||
"/p/{postId}", "/p/{postId}/{page}",
|
||||
"/n/{postName}", "/n/{postName}/{page}",
|
||||
"/{postName}.html", "/{postName}.html/{page}"})
|
||||
public String detail(
|
||||
@PathVariable(value="postId", required=false) Long postId,
|
||||
@PathVariable(value="postName", required=false) String postName,
|
||||
@PathVariable(value="page", required=false) Long commentPage) {
|
||||
|
||||
Boolean isLogin = isLogin();
|
||||
// 获得文章info
|
||||
Post post = postService.getByIdOrName(postId, postName, isLogin);
|
||||
|
||||
if (post == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
IPostTypeConfig<?> postTypeConfig = postTypeComponent.getPostTypeConfig(post.getType());
|
||||
AbstractPostService<?, ?> postTypeService = postTypeComponent.getPostTypeService(post.getType());
|
||||
|
||||
if (!postTypeConfig.getEnabled()) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
// <link rel="canonical" href="https://example.com/dresses/green-dresses" />
|
||||
// 检查规范网页
|
||||
// TODO 使用 UriComponentsBuilder
|
||||
String canonical = post.getLink();
|
||||
String requestUri = request.getRequestURI();
|
||||
if (!requestUri.startsWith(canonical)) {
|
||||
|
||||
String[] split = requestUri.split("/");
|
||||
|
||||
// 获取除却 2 级 uri 后的参数并做重定向
|
||||
requestUri = requestUri.replace(String.format("/%s/%s", split[1], split[2]), canonical);
|
||||
return "redirect:" + requestUri;
|
||||
}
|
||||
|
||||
request.setAttribute("canonical", siteConfig.getAddress() + canonical);
|
||||
|
||||
// 文章校验、清除
|
||||
Optional<String> detailCheck = postTypeService.detailCheckAndJump(post);
|
||||
if (detailCheck.isPresent()) {
|
||||
return detailCheck.get();
|
||||
}
|
||||
|
||||
request.setAttribute("title",
|
||||
new StringBuilder()
|
||||
.append(postTypeService.getFrontEndPostTitle(post))
|
||||
.append(" - ")
|
||||
.append(postTypeConfig.getName())
|
||||
.toString());
|
||||
|
||||
request.setAttribute("post", post);
|
||||
|
||||
// 评论
|
||||
IPage<Comment> page = commentService.getPostCommentPage(
|
||||
commentPage, postTypeConfig.getCommentPageSize(),
|
||||
post.getPostId());
|
||||
request.setAttribute("commentPaginationPrefix", post.getLink() + "/");
|
||||
request.setAttribute("commentPage", page);
|
||||
if (commentPage != null && commentPage != 1L && page.getRecords().size() == 0) {
|
||||
// 页数不为首页且评论数为 0,一般是属于本来有那么多页但是最终删除了
|
||||
String uri = request.getRequestURI();
|
||||
return "redirect:" + request.getRequestURI().substring(0, uri.lastIndexOf('/'));
|
||||
}
|
||||
|
||||
|
||||
String targetTemplet = postTypeComponent.getTemplatePath(post.getType(), "detail");
|
||||
|
||||
if (!StringUtils.isEmpty(post.getTemplate())) {
|
||||
targetTemplet = themeService
|
||||
.getCurrentTheme()
|
||||
.getTemplateString("template", post.getTemplate());
|
||||
}
|
||||
|
||||
return targetTemplet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码
|
||||
* @param postPageName
|
||||
* @param postId
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
@PostMapping({
|
||||
"/p/{postId}/password",
|
||||
"/n/{postPageName}/password"})
|
||||
@ResponseBody
|
||||
public R<?> checkPassword(
|
||||
@PathVariable(value="postPageName", required=false) String postPageName,
|
||||
@PathVariable(value="postId", required=false) Long postId,
|
||||
String password) {
|
||||
boolean postIdGiven = Objects.nonNull(postId);
|
||||
boolean postPageNameGiven = StringUtils.isNoneBlank(postPageName);
|
||||
if (!postIdGiven && !postPageNameGiven) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "非法请求");
|
||||
}
|
||||
|
||||
Boolean isLogin = isLogin();
|
||||
Post post = postService.getOne(new LambdaQueryWrapper<Post>()
|
||||
.eq(!isLogin, Post::getPostStatus, PostStatusEnum.RELEASED)
|
||||
.eq(Post::getIsDeleted, false)
|
||||
.eq(postIdGiven, Post::getPostId, postId)
|
||||
.eq(postPageNameGiven, Post::getPostPageName, postPageName));
|
||||
if (post == null) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "非法操作");
|
||||
}
|
||||
if (isLogin || StringUtils.isBlank(post.getPassword())) { //
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
if (!postTypeComponent.checkModeEnable(post.getType())) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, "非法操作");
|
||||
}
|
||||
|
||||
if (Cryptos.sha3(post.getPassword(), 224).equals(password)) {
|
||||
// 有密码
|
||||
PostPassJar.addToSession(session, post, password);
|
||||
return R.ok();
|
||||
}
|
||||
throw new RException(HttpStatus.FORBIDDEN, "密码输入错误");
|
||||
}
|
||||
|
||||
|
||||
@GetMapping({
|
||||
"/p/{postId}/lock",
|
||||
"/n/{postPageName}/lock"})
|
||||
public String lock(
|
||||
@PathVariable(value="postPageName", required=false) String postPageName,
|
||||
@PathVariable(value="postId", required=false) Long postId,
|
||||
String password) {
|
||||
|
||||
Boolean isLogin = isLogin();
|
||||
Post post = postService.getByIdOrName(postId, postPageName, isLogin);
|
||||
if (post == null) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
PostPassJar.removeFromSession(session, post);
|
||||
return "redirect:" + post.getLink();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 评论页元素
|
||||
*/
|
||||
@GetMapping({
|
||||
"/{postName}.html/comments/{page}",
|
||||
"/p/{postId}/comments/{page}",
|
||||
"/n/{postName}/comments/{page}"
|
||||
})
|
||||
public String commentPage(
|
||||
@PathVariable(value="postId", required=false) Long postId,
|
||||
@PathVariable(value="postName", required=false) String postName,
|
||||
@PathVariable(value="page") Long commentPage) {
|
||||
|
||||
|
||||
Boolean isLogin = isLogin();
|
||||
|
||||
Post post = postService.getOne(
|
||||
new LambdaQueryWrapper<Post>()
|
||||
.eq(!isLogin, Post::getPostStatus, PostStatusEnum.RELEASED)
|
||||
.eq(Post::getIsDeleted, false)
|
||||
// 是哪种页面,如何定位
|
||||
.and(!Objects.isNull(postId), i -> i.eq(Post::getPostId, postId))
|
||||
.and(!StringUtils.isEmpty(postName), i -> i.eq(Post::getPostPageName, postName))
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (post == null) {
|
||||
// 如果 POST 不存在
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
IPostTypeConfig<?> postTypeConfig = postTypeComponent.getPostTypeConfig(post.getType());
|
||||
|
||||
if (!postTypeConfig.getEnabled()) {
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
|
||||
String targetTemplet = postTypeComponent.getTemplatePath(post.getType(), "comment-only");
|
||||
|
||||
// 文章加密状态校验、清除
|
||||
if (!PostPassJar.validFromSession(session, post)) {
|
||||
return targetTemplet;
|
||||
}
|
||||
|
||||
// 评论
|
||||
IPage<Comment> page = commentService.getPostCommentPage(
|
||||
commentPage, postTypeConfig.getCommentPageSize(),
|
||||
post.getPostId());
|
||||
response.addHeader("Post-Id", post.getPostId().toString());
|
||||
request.setAttribute("post", post);
|
||||
request.setAttribute("enableComment", post.getEnableComment());
|
||||
request.setAttribute("commentPaginationPrefix", post.getLink() + "/");
|
||||
request.setAttribute("commentPage", page);
|
||||
if (commentPage != null && commentPage != 1L && page.getRecords().size() == 0) {
|
||||
// 页数不为首页且评论数为 0,一般是属于本来有那么多页但是最终删除了
|
||||
String uri = request.getRequestURI();
|
||||
return "redirect:" + request.getRequestURI().substring(0, uri.lastIndexOf('/'));
|
||||
}
|
||||
|
||||
return targetTemplet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论
|
||||
*/
|
||||
@PostMapping({
|
||||
"/{postPageName}.html/reply",
|
||||
"/p/{postId}/reply",
|
||||
"/n/{postPageName}/reply"
|
||||
})
|
||||
@ResponseBody
|
||||
public R<?> reply(@Validated Comment comment, BindingResult bindingResult) {
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage())
|
||||
.setLogRequest(true);
|
||||
}
|
||||
if (commentService.saveOrUpdate(comment)) {
|
||||
mailService.sendAdminNotification(comment);
|
||||
mailService.addOrUpdateSendQueue(comment);
|
||||
|
||||
comment = commentService.getById(comment.getId())
|
||||
.setIsDeleted(null)
|
||||
.setIp(null)
|
||||
.setAgent(null)
|
||||
.setEditable(true);
|
||||
|
||||
SessionUtils.addCommentId(request, comment.getId());
|
||||
|
||||
if (commentConfig.isMarkdownOn() && comment.isMarkdown()) {
|
||||
comment.setContent(MarkDownUtils.commentToHtml(comment.getContent()));
|
||||
}
|
||||
return R.ok(comment);
|
||||
}
|
||||
throw RException.internalServerError();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 假评论
|
||||
*/
|
||||
@PostMapping({
|
||||
"/{postPageName}.html/honeyReply",
|
||||
"/p/{postId}/honeyReply",
|
||||
"/n/{postPageName}/honeyReply"
|
||||
})
|
||||
@ResponseBody
|
||||
public R<?> honeyReply(
|
||||
@Validated Comment comment, BindingResult bindingResult) {
|
||||
|
||||
String ip = ipService.getClientIp(request);
|
||||
Boolean isLocalhost = IPUtils.isLocalhost(ip);
|
||||
|
||||
// 添加入 ip 黑名单
|
||||
if (!isLocalhost) {
|
||||
commentConfig.addBlockIp(ip).saveOrUpdate();
|
||||
}
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
//log.info("触发测试评论蜜罐, ip: {}, 但表单验证不通过", IPUtils.getClientIP(request));
|
||||
throw new RException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage())
|
||||
.setLogRequest(false);
|
||||
}
|
||||
|
||||
log.debug("触发测试评论蜜罐, ip: {}, {}", ip, comment.toString());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/comments/go/{commentId}")
|
||||
public String commentsGo(@PathVariable("commentId") Long id) {
|
||||
return "redirect:" + commentService.getJumpToUrl(id);
|
||||
}
|
||||
|
||||
@GetMapping("/posts/gotoPage/{postId}")
|
||||
public String postsGo(@PathVariable("postId") Long id) {
|
||||
return "redirect:" + postService.getJumpToPagesUrl(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package me.qwq.doghouse.controller.blog;
|
||||
|
||||
|
||||
import com.alibaba.fastjson2.util.DateUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import me.qwq.doghouse.component.PostTypeComponent;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.*;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.enums.CommentStatusEnum;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
import me.qwq.doghouse.enums.PostStatusEnum;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.pojo.dto.PageQuery;
|
||||
import me.qwq.doghouse.service.*;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jdom2.Document;
|
||||
import org.jdom2.Element;
|
||||
import org.jdom2.Namespace;
|
||||
import org.jdom2.Text;
|
||||
import org.jdom2.output.Format;
|
||||
import org.jdom2.output.XMLOutputter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sitemap Controller
|
||||
*
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
public class SitemapController extends BaseController {
|
||||
|
||||
public final static String CATEGORY_PATH = "/category/";
|
||||
public final static String TAG_PATH = "/tag/";
|
||||
public final static String SITEMAP_PATH = "/sitemap.xml";
|
||||
public final static String URL = "url";
|
||||
public final static String LOC = "loc";
|
||||
public final static String LASTMOD = "lastmod";
|
||||
public final static String UTF8 = "UTF-8";
|
||||
public final static String DATE_FORMAT = "yyyy-MM-dd";
|
||||
public final static Format FORMAT = Format.getCompactFormat().setEncoding(UTF8);
|
||||
public final static Namespace NAMESPACE = Namespace.getNamespace("http://www.sitemaps.org/schemas/sitemap/0.9");
|
||||
|
||||
@Autowired
|
||||
PostService postService;
|
||||
|
||||
@Autowired
|
||||
MetaService metaService;
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
@Autowired
|
||||
PostTypeComponent postTypeComponent;
|
||||
|
||||
@GetMapping(value=SITEMAP_PATH, produces = "application/xml")
|
||||
@ResponseBody
|
||||
public String feed() {
|
||||
|
||||
String siteAddress = siteConfig.getAddress();
|
||||
Document doc = new Document();
|
||||
|
||||
Element urlset = new Element("urlset", NAMESPACE);
|
||||
LambdaQueryWrapper<Post> ew = new LambdaQueryWrapper<Post>()
|
||||
.eq(Post::getPostStatus, PostStatusEnum.RELEASED)
|
||||
.eq(Post::getIsDeleted, false)
|
||||
.eq(Post::getSearchable, true)
|
||||
.orderByDesc(Post::getType, Post::getCreateTime);
|
||||
|
||||
for (PostTypeEnum e : PostTypeEnum.values()) {
|
||||
IPostTypeConfig<?> postTypeConfig = postTypeComponent.getPostTypeConfig(e);
|
||||
if (!postTypeConfig.getEnabled() || postTypeConfig.getAddToSitemap()) {
|
||||
ew.ne(Post::getType, e);
|
||||
}
|
||||
}
|
||||
|
||||
List<Post> releasePosts = postService.list(ew);
|
||||
|
||||
List<Element> urls = new ArrayList<>();
|
||||
|
||||
for (Post post : releasePosts) {
|
||||
if (post.getType() == PostTypeEnum.WIKI && StringUtils.isEmpty(post.getPostContent())) {
|
||||
// 空 WIKI 文章不加入 Sitemap
|
||||
continue;
|
||||
}
|
||||
urls.add(getPostUrlElement(post, siteAddress));
|
||||
}
|
||||
|
||||
List<Cate> releaseCates = metaService.listJoin(Cate.class,
|
||||
metaService.getCountDefaultWrapper(MetaTypeEnum.CATEGORY)
|
||||
.selectMax(Post::getUpdateTime, Cate::getUpdateTime)
|
||||
//.selectFuncs(Post::getUpdateTime, Cate::getUpdateTime, FunctionPool.MAX)
|
||||
);
|
||||
List<Tag> releaseTags = metaService.listJoin(Tag.class,
|
||||
metaService.getCountDefaultWrapper(MetaTypeEnum.TAG)
|
||||
.selectMax(Post::getUpdateTime, Tag::getUpdateTime)
|
||||
//.selectFuncs(Post::getUpdateTime, Cate::getUpdateTime, FunctionPool.MAX)
|
||||
);
|
||||
for (Cate cate : releaseCates) {
|
||||
if (cate.getPostCount() == 0) continue;
|
||||
urls.add(getMetaUrlElement(cate, siteAddress));
|
||||
}
|
||||
for (Tag tag : releaseTags) {
|
||||
if (tag.getPostCount() == 0) continue;
|
||||
urls.add(getMetaUrlElement(tag, siteAddress));
|
||||
}
|
||||
|
||||
// 首页的 lastmod 一般应该由以下几个部分组成:
|
||||
// 1. 首页文章的 lastmod
|
||||
// 2. 最新评论的 lastmod
|
||||
LocalDateTime date = LocalDateTime.of(1991, 8, 6, 0, 0);
|
||||
PageQuery<?> indexPage = postService.conditionPage(new PageQuery<>(PostTypeEnum.POST));
|
||||
for (Post post : indexPage.getRecords()) {
|
||||
if (date.isBefore(post.getUpdateTime())) {
|
||||
date = post.getUpdateTime();
|
||||
}
|
||||
}
|
||||
List<Comment> latestComments = commentService.getLatestComments(1L);
|
||||
if (!latestComments.isEmpty()) {
|
||||
if (date.isBefore(latestComments.get(0).getCreateTime())) {
|
||||
date = latestComments.get(0).getCreateTime();
|
||||
}
|
||||
}
|
||||
|
||||
Element url = new Element(URL, NAMESPACE);
|
||||
Element loc = new Element(LOC, NAMESPACE);
|
||||
Element lastmod = new Element(LASTMOD, NAMESPACE);
|
||||
Text locNode = new Text(
|
||||
String.format("%s", siteAddress));
|
||||
loc.addContent(locNode);
|
||||
Text lastmodNode = new Text(
|
||||
DateUtils.format(date, DATE_FORMAT));
|
||||
lastmod.addContent(lastmodNode);
|
||||
url.addContent(loc);
|
||||
url.addContent(lastmod);
|
||||
|
||||
urlset.addContent(url);
|
||||
|
||||
urlset.addContent(urls);
|
||||
|
||||
doc.addContent(urlset);
|
||||
XMLOutputter outputter = new XMLOutputter(FORMAT);
|
||||
return outputter.outputString(doc);
|
||||
}
|
||||
|
||||
private Element getPostUrlElement(Post post, String siteAddress) {
|
||||
Element url = new Element(URL, NAMESPACE);
|
||||
Element loc = new Element(LOC, NAMESPACE);
|
||||
|
||||
// For test
|
||||
/*Element name = new Element("name", NAMESPACE);
|
||||
name.addContent(new Text(post.getPostTitle()));
|
||||
url.addContent(name);*/
|
||||
|
||||
Element lastmod = new Element(LASTMOD, NAMESPACE);
|
||||
Text locNode = new Text(
|
||||
String.format("%s%s", siteAddress, post.getLink()));
|
||||
loc.addContent(locNode);
|
||||
|
||||
LocalDateTime updateTime = post.getUpdateTime();
|
||||
// getLatestComment
|
||||
|
||||
Comment comment = commentService.getOne(new QueryWrapper<Comment>()
|
||||
.lambda()
|
||||
.eq(Comment::getPostId, post.getPostId())
|
||||
.eq(Comment::getCommentStatus, CommentStatusEnum.RELEASED)
|
||||
.eq(Comment::getIsDeleted, false)
|
||||
.orderByAsc(Comment::getCreateTime).last("LIMIT 1"));
|
||||
|
||||
if (comment != null && updateTime.isBefore(comment.getCreateTime())) {
|
||||
updateTime = comment.getCreateTime();
|
||||
}
|
||||
|
||||
Text lastmodNode = new Text(
|
||||
DateUtils.format(updateTime, DATE_FORMAT));
|
||||
lastmod.addContent(lastmodNode);
|
||||
url.addContent(loc);
|
||||
url.addContent(lastmod);
|
||||
return url;
|
||||
}
|
||||
|
||||
private static Element getMetaUrlElement(Meta meta, String siteAddress) {
|
||||
Element url = new Element(URL, NAMESPACE);
|
||||
Element loc = new Element(LOC, NAMESPACE);
|
||||
Element lastmod = new Element(LASTMOD, NAMESPACE);
|
||||
Text locNode = new Text(
|
||||
String.format("%s%s%s", siteAddress, meta.getType() == MetaTypeEnum.TAG ? TAG_PATH : CATEGORY_PATH,
|
||||
UriUtils.encode(meta.getFriendlyName(), StandardCharsets.UTF_8)));
|
||||
loc.addContent(locNode);
|
||||
Text lastmodNode = new Text(
|
||||
DateUtils.format(meta.getUpdateTime(), DATE_FORMAT));
|
||||
lastmod.addContent(lastmodNode);
|
||||
url.addContent(loc);
|
||||
url.addContent(lastmod);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package me.qwq.doghouse.controller.blog;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
|
||||
import me.qwq.doghouse.component.Rk;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.*;
|
||||
import me.qwq.doghouse.entity.config.MailConfig;
|
||||
import me.qwq.doghouse.service.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* 用户功能控制器
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/system")
|
||||
public class VisitorController extends BaseController {
|
||||
|
||||
static final String TOKEN_ERROR = "token 不存在或已过期";
|
||||
|
||||
static final String MAIL_DISABLED = "站点未开启邮件功能";
|
||||
|
||||
static final String PATH = "system/unsubscribe-mail";
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
MailConfig mailConfig;
|
||||
|
||||
@RequestMapping("/unsubscribe-mail")
|
||||
private String unsubscribeMailPost(String token) {
|
||||
|
||||
String method = request.getMethod();
|
||||
|
||||
if (!mailConfig.isMailOn()) {
|
||||
return render(MAIL_DISABLED, null);
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(token)) {
|
||||
return render(TOKEN_ERROR, null);
|
||||
}
|
||||
|
||||
RBucket<String> uuidBucket = Rk.Mail.getMgrUuidBucket(token);
|
||||
String mail = uuidBucket.get();
|
||||
if (StringUtils.isEmpty(mail)) {
|
||||
return render(TOKEN_ERROR, null);
|
||||
}
|
||||
|
||||
if ("GET".equals(method)) {
|
||||
return render(null, mail);
|
||||
}
|
||||
|
||||
if ("POST".equals(method)) {
|
||||
uuidBucket.delete();
|
||||
commentService.update(new LambdaUpdateWrapper<Comment>()
|
||||
.eq(Comment::getEmail, mail.toLowerCase(Locale.ROOT))
|
||||
.set(Comment::isSubscribeReply, false));
|
||||
|
||||
return render("操作成功", null);
|
||||
}
|
||||
|
||||
return render("非法操作请求: " + method, null);
|
||||
}
|
||||
|
||||
String render(String note, String mail) {
|
||||
request.setAttribute("note", note);
|
||||
request.setAttribute("mail", mail);
|
||||
return PATH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package me.qwq.doghouse.controller.blog.post;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.component.PostTypeComponent;
|
||||
import me.qwq.doghouse.controller.common.BaseController;
|
||||
import me.qwq.doghouse.entity.*;
|
||||
import me.qwq.doghouse.entity.config.CommentConfig;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.exception.PageNotFoundException;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.pojo.dto.PageQuery;
|
||||
import me.qwq.doghouse.service.*;
|
||||
import me.qwq.doghouse.service.post.AbstractPostService;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
|
||||
/**
|
||||
* 前端分页控制器, 不同 PostType 如需分页, 可继承该方法
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
public abstract class AbstractPaginationController<T extends IPostTypeConfig<T>, S extends AbstractPostService<S, T>> extends BaseController {
|
||||
|
||||
@Autowired
|
||||
CommentService commentService;
|
||||
|
||||
@Autowired
|
||||
MarkdownService markdownService;
|
||||
|
||||
@Autowired
|
||||
CommentConfig commentConfig;
|
||||
|
||||
@Autowired
|
||||
PostTypeComponent postTypeComponent;
|
||||
|
||||
@Autowired
|
||||
ThemeService themeService;
|
||||
|
||||
@Autowired
|
||||
SiteConfig siteConfig;
|
||||
|
||||
T postTypeConfig;
|
||||
|
||||
T getPostTypeConfig() {
|
||||
if (postTypeConfig != null) return postTypeConfig;
|
||||
ResolvableType type = ResolvableType.forClass(this.getClass()).as(AbstractPaginationController.class);
|
||||
ResolvableType iPageConfigType = type.getGeneric(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> iPageConfigClass = (Class<T>) iPageConfigType.resolve();
|
||||
postTypeConfig = SpringContextHolder.getBean(iPageConfigClass);
|
||||
return postTypeConfig;
|
||||
}
|
||||
|
||||
PostTypeEnum getPostType() {
|
||||
return getPostTypeConfig().getPostType();
|
||||
}
|
||||
|
||||
S postTypeService;
|
||||
|
||||
S getPostTypeService() {
|
||||
if (postTypeService != null) return postTypeService;
|
||||
ResolvableType type = ResolvableType.forClass(this.getClass()).as(AbstractPaginationController.class);
|
||||
ResolvableType postServiceType = type.getGeneric(1);
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<S> postServiceClass = (Class<S>) postServiceType.resolve();
|
||||
postTypeService = SpringContextHolder.getBean(postServiceClass);
|
||||
return postTypeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
@GetMapping({
|
||||
"", "/"})
|
||||
public String index() {
|
||||
|
||||
return this.page(
|
||||
new PageQuery<>(getPostType())
|
||||
.setTitle(getPostTypeConfig().getIndexTitle()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页无查询条件
|
||||
* @param pageQuery
|
||||
* @return
|
||||
*/
|
||||
@GetMapping({"/page", "/page/{current}"})
|
||||
public String indexPage(@PathVariable(name="current", required=false) Long current) {
|
||||
return page(new PageQuery<>(getPostType()).setCurrent(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类
|
||||
*/
|
||||
@GetMapping({"/category/{friendlyName}", "/category/{friendlyName}/{current}"})
|
||||
public String category(
|
||||
@PathVariable("friendlyName") String friendlyName,
|
||||
@PathVariable(name="current", required=false) Long current) {
|
||||
|
||||
Cate cate = new Cate();
|
||||
cate.setFriendlyName(friendlyName);
|
||||
|
||||
return this.page(new PageQuery<Cate>(getPostType())
|
||||
.setQuery(cate)
|
||||
.setCurrent(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@GetMapping({"/tag/{friendlyName}", "/tag/{friendlyName}/{current}"})
|
||||
public String tag(
|
||||
@PathVariable("friendlyName") String friendlyName,
|
||||
@PathVariable(name="current", required=false) Long current) {
|
||||
Tag tag = new Tag();
|
||||
tag.setFriendlyName(friendlyName);
|
||||
return this.page(new PageQuery<Tag>(getPostType())
|
||||
.setQuery(tag)
|
||||
.setCurrent(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索,适用于伪静态的情况
|
||||
*/
|
||||
@GetMapping({"/search/{keyword}", "/search/{keyword}/{page}"})
|
||||
public String search(@PathVariable("keyword") String keyword, @PathVariable(name="page", required=false) Long c) {
|
||||
|
||||
String prefix = "redirect:" + getPostIndexPrefix();
|
||||
if (StringUtils.isEmpty(keyword)) return prefix;
|
||||
try {
|
||||
keyword = URLDecoder.decode(keyword,"UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
log.warn("搜索 keyword 编码转换失败,将跳转到首页");
|
||||
return prefix;
|
||||
}
|
||||
return this.page(new PageQuery<String>(getPostType())
|
||||
.setQuery(keyword)
|
||||
.setCurrent(c)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索,适用于 QueryParam 的情况:.../search?keyword=xxx
|
||||
*/
|
||||
@GetMapping({"/search"})
|
||||
public String search(@RequestParam(name="keyword", required=false) String keyword) {
|
||||
|
||||
String prefix = "redirect:" + getPostIndexPrefix();
|
||||
if (StringUtils.isEmpty(keyword)) return prefix;
|
||||
try {
|
||||
return prefix + "search/" + URLEncoder.encode(keyword,"UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
log.warn("搜索 keyword 编码转换失败,将跳转到首页");
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通用分页<p>
|
||||
* 注意:此处的 PageQuery 需要在调用方法内自行实例化,
|
||||
* 不能在映射方法接口上自动装填,否则经由 postService
|
||||
* 的 conditionPage 方法取缓存后,自动装填对象未被更改,
|
||||
* 会导致返回的 PageQuery 是空结果
|
||||
*/
|
||||
protected String page(PageQuery<?> pageQuery) {
|
||||
|
||||
pageQuery = getPostTypeService().conditionPage(pageQuery);
|
||||
pageQuery.checkPassword(session);
|
||||
|
||||
request.setAttribute("pageQuery", pageQuery);
|
||||
setTitle(pageQuery.getTitle());
|
||||
|
||||
request.setAttribute("canonical", siteConfig.getAddress() + "/");
|
||||
|
||||
String targetTemplet = postTypeComponent
|
||||
.getTemplatePath(
|
||||
getPostType(),
|
||||
"index");
|
||||
|
||||
return targetTemplet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应的路由前缀,该前缀由继承了本类的子类上的 @RequestMapping
|
||||
* 注解指定,仅取第一个路径。
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>@RequestMapping({"/", ""}) 返回 "/"
|
||||
* <li>@RequestMapping("/wiki") 返回 "/wiki/"
|
||||
* <li>@RequestMapping("/wiki/") 返回 "/wiki/"
|
||||
* <li>无注解返回 "/"
|
||||
*
|
||||
* @param postType
|
||||
* @return
|
||||
*/
|
||||
private String getPostIndexPrefix() {
|
||||
|
||||
RequestMapping requestMapping = null;
|
||||
try {
|
||||
requestMapping = this.getClass().getAnnotation(RequestMapping.class);
|
||||
}
|
||||
catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new PageNotFoundException();
|
||||
}
|
||||
if (requestMapping == null ||
|
||||
requestMapping.value().length == 0) {
|
||||
return "/";
|
||||
}
|
||||
String uri = requestMapping.value()[0];
|
||||
if (uri == null || uri.isBlank()) return "/";
|
||||
// 去掉前后斜杠
|
||||
uri = uri.replaceAll("^/+|/+$", "");
|
||||
// 去掉中间重复斜杠
|
||||
uri = uri.replaceAll("/+", "/");
|
||||
return "/" + uri + "/";
|
||||
}
|
||||
|
||||
private void setTitle(String title) {
|
||||
request.setAttribute("title", title);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package me.qwq.doghouse.controller.blog.post;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.SiteConfig;
|
||||
import me.qwq.doghouse.service.post.PostService;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
|
||||
/**
|
||||
* PostTypeEnum.POST 前端分页控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
public class PostPaginationController extends AbstractPaginationController<SiteConfig, PostService> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.qwq.doghouse.controller.blog.post;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.TweetConfig;
|
||||
import me.qwq.doghouse.service.post.TweetService;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
|
||||
/**
|
||||
* PostTypeEnum.TWEET 前端分页控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
@RequestMapping("/tweet")
|
||||
public class TweetPaginationController extends AbstractPaginationController<TweetConfig, TweetService> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package me.qwq.doghouse.controller.blog.post;
|
||||
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.entity.config.WikiConfig;
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
import me.qwq.doghouse.pojo.bo.PostPassJar;
|
||||
import me.qwq.doghouse.service.post.WikiService;
|
||||
import me.qwq.doghouse.util.EscapeUnescapeUtils;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* PostTypeEnum.WIKI 前端分页控制器
|
||||
* @author Doghole
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
@RequestMapping("/wiki")
|
||||
public class WikiPaginationController extends AbstractPaginationController<WikiConfig, WikiService> {
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
public String index() {
|
||||
// 先找最顶层的一篇文章,如果有,跳转到顶层文章
|
||||
Post post = getPostTypeService().getOne(getPostTypeService().getWikiDefaultWrapper(isLogin()).last("LIMIT 1"));
|
||||
if (post != null) {
|
||||
return "redirect:" + post.getLink();
|
||||
}
|
||||
return super.index();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wiki 树形菜单点击
|
||||
* @param postId
|
||||
* @return
|
||||
*/
|
||||
@GetMapping({"/list", "/list/", "/list/{postId}"})
|
||||
public String listFrontWikisOnly(@PathVariable(name="postId", required=false) Long postId) {
|
||||
request.setAttribute("wikis", listFrontWikis(postId));
|
||||
return postTypeComponent.getTemplatePath(getPostType(), "side-only");
|
||||
}
|
||||
|
||||
/**
|
||||
* 该方法不应通过 Web 接口调用,否则会暴露完整的 POST(WIKI),包括密码
|
||||
* 放在控制器这里只是方便前端模板调用,并且能获得 service 的缓存能力
|
||||
* @param postId
|
||||
* @return
|
||||
*/
|
||||
public List<Post> listFrontWikis(Long postId) {
|
||||
List<Long> unfolded = WikiService.getUnfoldedWikis(request);
|
||||
String unfoldedStr = unfolded == null ? "[]" : JSON.toJSONString(unfolded);
|
||||
unfoldedStr = EscapeUnescapeUtils.escape(unfoldedStr);
|
||||
Cookie unfoldedCookie = new Cookie(WikiService.UNFOLDED_WIKIS, unfoldedStr);
|
||||
unfoldedCookie.setPath("/");
|
||||
response.addCookie(unfoldedCookie);
|
||||
// 清洗 Password
|
||||
Post post = getPostTypeService().getOne(getPostTypeService().getWikiDefaultWrapper(isLogin()).eq(Post::getPostId, postId));
|
||||
if (post != null && (!PostPassJar.validFromSession(session, post))) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return getPostTypeService().listFrontWikis(postId, unfolded);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package me.qwq.doghouse.controller.common;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import me.qwq.doghouse.util.SessionUtils;
|
||||
|
||||
@Controller
|
||||
public abstract class BaseController {
|
||||
|
||||
@Autowired
|
||||
protected HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
protected HttpServletResponse response;
|
||||
|
||||
@Autowired
|
||||
protected HttpSession session;
|
||||
|
||||
protected Boolean isLogin() {
|
||||
return SessionUtils.isLogin(session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package me.qwq.doghouse.controller.common;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import me.qwq.doghouse.service.ThemeService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("${server.error.path:${error.path:/error}}")
|
||||
public class ErrorPageController implements ErrorController {
|
||||
|
||||
@Autowired
|
||||
ErrorAttributes errorAttributes;
|
||||
|
||||
@Autowired
|
||||
ThemeService themeService;
|
||||
|
||||
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
|
||||
public String errorHtml(HttpServletRequest request) {
|
||||
HttpStatus status = getStatus(request);
|
||||
return themeService.getCurrentTheme().getErrorTemplateString(status);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
|
||||
boolean include = Boolean.parseBoolean(request.getParameter("trace"));
|
||||
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
|
||||
if (include) {
|
||||
options = options.including(Include.STACK_TRACE);
|
||||
}
|
||||
|
||||
WebRequest webRequest = new ServletWebRequest(request);
|
||||
Map<String, Object> body = errorAttributes.getErrorAttributes(webRequest, options);
|
||||
|
||||
return ResponseEntity.status(getStatus(request)).body(body);
|
||||
}
|
||||
|
||||
private HttpStatus getStatus(HttpServletRequest request) {
|
||||
Object value = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
if (value instanceof Integer code) {
|
||||
HttpStatus status = HttpStatus.resolve(code);
|
||||
return (status != null ? status : HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
12
src/main/java/me/qwq/doghouse/dao/AssetMapper.java
Normal file
12
src/main/java/me/qwq/doghouse/dao/AssetMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Asset;
|
||||
|
||||
/**
|
||||
* 资产 Mapper
|
||||
*/
|
||||
public interface AssetMapper extends MPJBaseMapper<Asset> {
|
||||
|
||||
}
|
||||
12
src/main/java/me/qwq/doghouse/dao/CommentMapper.java
Normal file
12
src/main/java/me/qwq/doghouse/dao/CommentMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Comment;
|
||||
|
||||
/**
|
||||
* 评论 Mapper
|
||||
*/
|
||||
public interface CommentMapper extends MPJBaseMapper<Comment> {
|
||||
|
||||
}
|
||||
12
src/main/java/me/qwq/doghouse/dao/ConfigMapper.java
Normal file
12
src/main/java/me/qwq/doghouse/dao/ConfigMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.RawConfig;
|
||||
|
||||
/**
|
||||
* 配置项 Mapper
|
||||
*/
|
||||
public interface ConfigMapper extends MPJBaseMapper<RawConfig> {
|
||||
|
||||
}
|
||||
13
src/main/java/me/qwq/doghouse/dao/LinkMapper.java
Normal file
13
src/main/java/me/qwq/doghouse/dao/LinkMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Link;
|
||||
|
||||
/**
|
||||
* 友情链接 Mapper
|
||||
*
|
||||
*/
|
||||
public interface LinkMapper extends MPJBaseMapper<Link> {
|
||||
|
||||
}
|
||||
13
src/main/java/me/qwq/doghouse/dao/MetaMapper.java
Normal file
13
src/main/java/me/qwq/doghouse/dao/MetaMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Meta;
|
||||
|
||||
/**
|
||||
* Meta Mapper
|
||||
*
|
||||
*/
|
||||
public interface MetaMapper extends MPJBaseMapper<Meta> {
|
||||
|
||||
}
|
||||
16
src/main/java/me/qwq/doghouse/dao/PostMapper.java
Normal file
16
src/main/java/me/qwq/doghouse/dao/PostMapper.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Post;
|
||||
|
||||
/**
|
||||
* Post Mapper
|
||||
*
|
||||
*/
|
||||
public interface PostMapper extends MPJBaseMapper<Post> {
|
||||
|
||||
List<Long> getParentIds(Collection<Long> childrenIds);
|
||||
}
|
||||
13
src/main/java/me/qwq/doghouse/dao/RelationshipMapper.java
Normal file
13
src/main/java/me/qwq/doghouse/dao/RelationshipMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.Relationship;
|
||||
|
||||
/**
|
||||
* Meta Relationship Mapper
|
||||
*
|
||||
*/
|
||||
public interface RelationshipMapper extends MPJBaseMapper<Relationship> {
|
||||
|
||||
}
|
||||
20
src/main/java/me/qwq/doghouse/dao/StatisticsMapper.java
Normal file
20
src/main/java/me/qwq/doghouse/dao/StatisticsMapper.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
|
||||
public interface StatisticsMapper {
|
||||
|
||||
|
||||
String getDatabaseVersion();
|
||||
|
||||
/**
|
||||
* 获取过去 passDay 日的每日文章和评论数统计
|
||||
* @param passDays
|
||||
* @param postType
|
||||
* @return
|
||||
*/
|
||||
Map<String, String> getPostAndCommentCountsWithDates(Integer passDays, PostTypeEnum postType);
|
||||
|
||||
}
|
||||
13
src/main/java/me/qwq/doghouse/dao/UserMapper.java
Normal file
13
src/main/java/me/qwq/doghouse/dao/UserMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package me.qwq.doghouse.dao;
|
||||
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
|
||||
import me.qwq.doghouse.entity.User;
|
||||
|
||||
/**
|
||||
* 管理员 Mapper
|
||||
*
|
||||
*/
|
||||
public interface UserMapper extends MPJBaseMapper<User> {
|
||||
|
||||
}
|
||||
105
src/main/java/me/qwq/doghouse/entity/Asset.java
Normal file
105
src/main/java/me/qwq/doghouse/entity/Asset.java
Normal file
@@ -0,0 +1,105 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.FileTypeEnum;
|
||||
|
||||
/**
|
||||
* 资产实体类
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain=true)
|
||||
public class Asset extends Model<Post> implements Serializable {
|
||||
|
||||
/**
|
||||
* 资产 ID
|
||||
*/
|
||||
@TableId(value="asset_id", type=IdType.AUTO)
|
||||
private Long assetId;
|
||||
|
||||
/**
|
||||
* 资产路径
|
||||
*/
|
||||
@TableField("path")
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 资产标题(名称)
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 获取扩展名
|
||||
* @return
|
||||
*/
|
||||
public String getExt() {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
return "";
|
||||
}
|
||||
int lastIndexOfDot = path.lastIndexOf('.');
|
||||
if (lastIndexOfDot == -1 || lastIndexOfDot == path.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
return path.substring(lastIndexOfDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
@TableField("file_type")
|
||||
private FileTypeEnum fileType;
|
||||
|
||||
@TableField("file_size")
|
||||
private Long fileSize;
|
||||
|
||||
@TableField("image_width")
|
||||
private Integer imageWidth;
|
||||
|
||||
@TableField("image_height")
|
||||
private Integer imageHeight;
|
||||
|
||||
/**
|
||||
* 资产描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField("update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableField("from_post_id")
|
||||
private Long fromPostId;
|
||||
|
||||
@TableField(exist=false)
|
||||
private String fromPostTitle;
|
||||
|
||||
}
|
||||
91
src/main/java/me/qwq/doghouse/entity/Cate.java
Normal file
91
src/main/java/me/qwq/doghouse/entity/Cate.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
|
||||
import java.util.List;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IFriendlyName;
|
||||
import me.qwq.doghouse.validator.FriendlyNameValid;
|
||||
/**
|
||||
* Cate for category
|
||||
*
|
||||
* @author Doghole
|
||||
* @since 2021-12-01
|
||||
* @see Meta
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("meta")
|
||||
@FriendlyNameValid
|
||||
public class Cate extends Meta implements Serializable, ITreeView<Cate>, IFriendlyName {
|
||||
|
||||
@TableId(value = "mid", type=IdType.AUTO)
|
||||
private Long mid;
|
||||
|
||||
@TableField("name")
|
||||
@NotBlank(message = "分类名称不允许为空")
|
||||
private String name;
|
||||
|
||||
@TableField("type")
|
||||
private MetaTypeEnum type = MetaTypeEnum.CATEGORY;
|
||||
|
||||
@TableField("parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@TableField("friendly_name")
|
||||
private String friendlyName;
|
||||
|
||||
@TableField("description")
|
||||
private String description;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@TableField("no")
|
||||
private Integer no;
|
||||
|
||||
/*
|
||||
* FOR TREEVIEW USE.
|
||||
*/
|
||||
|
||||
@TableField(exist=false)
|
||||
private List<Cate> children;
|
||||
|
||||
public String getTitle() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return getMid();
|
||||
}
|
||||
|
||||
public Boolean getHasChildren() {
|
||||
return childrenCount > 0;
|
||||
}
|
||||
|
||||
@TableField(exist=false)
|
||||
private Boolean checked = false;
|
||||
|
||||
@TableField(exist=false)
|
||||
private Boolean spread = false;
|
||||
|
||||
@TableField(value = "post_count", exist = false)
|
||||
private Long postCount = 0L;
|
||||
|
||||
|
||||
@TableField(value = "children_count", exist = false)
|
||||
private Long childrenCount = 0L;
|
||||
|
||||
}
|
||||
201
src/main/java/me/qwq/doghouse/entity/Comment.java
Normal file
201
src/main/java/me/qwq/doghouse/entity/Comment.java
Normal file
@@ -0,0 +1,201 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.constants.UsefulRegex;
|
||||
import me.qwq.doghouse.enums.CommentStatusEnum;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IAvatarUrl;
|
||||
import me.qwq.doghouse.interfaces.IPostPassMess;
|
||||
import me.qwq.doghouse.pojo.bo.IpInfo;
|
||||
import me.qwq.doghouse.validator.CommentValid;
|
||||
import me.qwq.doghouse.validator.CommentValidator;
|
||||
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.constraints.Pattern.Flag;
|
||||
|
||||
/**
|
||||
*
|
||||
* Comment entity
|
||||
*
|
||||
* @author Doghole
|
||||
* @since 2021-11-19
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@CommentValid
|
||||
public class Comment extends Model<Comment> implements Serializable, IPostPassMess<Comment>, IAvatarUrl {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
/**
|
||||
* 主键id
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的blog主键
|
||||
*/
|
||||
@TableField("post_id")
|
||||
private Long postId;
|
||||
|
||||
/**
|
||||
* 关联 Post 标题
|
||||
*/
|
||||
@TableField(exist=false)
|
||||
private String postTitle;
|
||||
/**
|
||||
* 关联 Post 类型
|
||||
*/
|
||||
@TableField(exist=false)
|
||||
private PostTypeEnum type;
|
||||
/**
|
||||
* 关联 Post 密码
|
||||
*/
|
||||
@TableField(exist=false)
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 自定义路径
|
||||
*/
|
||||
@TableField(exist=false)
|
||||
private String postPageName;
|
||||
|
||||
/**
|
||||
* 评论者名称
|
||||
*/
|
||||
@TableField("author")
|
||||
@NotBlank(message = "请输入称呼")
|
||||
@Length(max = 10, message = CommentValidator.AUTHOR_LENGTH_OVERFLOW)
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 评论人的邮箱
|
||||
*/
|
||||
@TableField("email")
|
||||
@NotBlank(message = CommentValidator.CONTENT_CANNOT_BE_EMPTY)
|
||||
@Email(message = CommentValidator.EMAIL_INVALID)
|
||||
@Size(max = 100, message = CommentValidator.EMAIL_LENGTH_OVERFLOW)
|
||||
private String email;
|
||||
|
||||
public Comment setEmail(String email) {
|
||||
if (email != null) email = email.toLowerCase(Locale.ENGLISH);
|
||||
this.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网址
|
||||
*/
|
||||
@TableField("url")
|
||||
@Pattern(regexp = UsefulRegex.BLANK + UsefulRegex.OR + UsefulRegex.DOMAIN,
|
||||
flags = Flag.CASE_INSENSITIVE,
|
||||
message = CommentValidator.URL_INVALID)
|
||||
@Size(max = 50, message = CommentValidator.URL_LENGTH_OVERFLOW)
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
@TableField("content")
|
||||
@NotBlank(message = "请输入评论内容")
|
||||
@Length(min = 1, max = 6000, message = CommentValidator.CONTENT_LENGTH_MISMATCH)
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 评论提交时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 评论时的ip地址
|
||||
*/
|
||||
@TableField("ip")
|
||||
private String ip;
|
||||
|
||||
@TableField("agent")
|
||||
private String agent;
|
||||
|
||||
/**
|
||||
* 回复内容
|
||||
*/
|
||||
@TableField("reply_to")
|
||||
private Long replyTo;
|
||||
|
||||
/**
|
||||
* 回复层级
|
||||
*/
|
||||
@TableField("level")
|
||||
private Integer level;
|
||||
|
||||
/**
|
||||
* 是否审核通过 0-未审核 1-审核通过
|
||||
*/
|
||||
@TableField("comment_status")
|
||||
private CommentStatusEnum commentStatus;
|
||||
|
||||
public String getCommentStatusNote() {
|
||||
return commentStatus.getNote();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否删除 0-未删除 1-已删除
|
||||
*/
|
||||
@TableField("is_deleted")
|
||||
private Boolean isDeleted = false;
|
||||
|
||||
@TableField("is_markdown")
|
||||
private boolean markdown = false;
|
||||
|
||||
@TableField("subscribe_reply")
|
||||
private boolean subscribeReply = false;
|
||||
|
||||
@TableField("mail_sent")
|
||||
private boolean mailSent = false;
|
||||
|
||||
@TableField(exist=false)
|
||||
private List<Comment> subComments;
|
||||
|
||||
@TableField(exist=false)
|
||||
private IpInfo ipInfo;
|
||||
|
||||
/**
|
||||
* 是否可编辑
|
||||
*/
|
||||
@TableField(exist=false)
|
||||
private boolean editable = false;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder()
|
||||
.append("Comment[id=").append(id).append(", ")
|
||||
.append("author=").append(author).append(", ")
|
||||
.append("email").append(email).append(", ")
|
||||
.append("url=").append(url).append(", ")
|
||||
.append("content=").append(content).append(", ")
|
||||
.append("replyTo=").append(replyTo).append(", ")
|
||||
.append("isMarkdown=").append(markdown).append(", ")
|
||||
.append("subscribeReply=").append(subscribeReply).append("]")
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/java/me/qwq/doghouse/entity/ITreeView.java
Normal file
18
src/main/java/me/qwq/doghouse/entity/ITreeView.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
public interface ITreeView<T> extends Serializable {
|
||||
|
||||
T setChildren(List<T> children);
|
||||
Long getChildrenCount();
|
||||
T setChildrenCount(Long childrenCount);
|
||||
Long getParentId();
|
||||
String getTitle();
|
||||
Boolean getHasChildren();
|
||||
Boolean getChecked();
|
||||
T setChecked(Boolean checked);
|
||||
Boolean getSpread();
|
||||
T setSpread(Boolean spread);
|
||||
}
|
||||
90
src/main/java/me/qwq/doghouse/entity/Link.java
Normal file
90
src/main/java/me/qwq/doghouse/entity/Link.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.LinkTypeEnum;
|
||||
|
||||
/**
|
||||
* 友链
|
||||
* @author Doghole
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class Link implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 友链表主键id
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 友链类别
|
||||
*/
|
||||
@TableField
|
||||
private LinkTypeEnum type;
|
||||
|
||||
/**
|
||||
* 网站名称
|
||||
*/
|
||||
@TableField
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 网站链接
|
||||
*/
|
||||
@TableField
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 网站描述
|
||||
*/
|
||||
@TableField
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 用于列表排序
|
||||
*/
|
||||
@TableField
|
||||
private Integer health;
|
||||
|
||||
/**
|
||||
* 是否删除 0-未删除 1-已删除
|
||||
*/
|
||||
@TableField
|
||||
private Boolean isDeleted;
|
||||
|
||||
/**
|
||||
* 有效性检测
|
||||
*/
|
||||
@TableField
|
||||
private Boolean detect;
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
@TableField
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 检测时间
|
||||
*/
|
||||
@TableField
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime detectiveTime;
|
||||
|
||||
|
||||
}
|
||||
69
src/main/java/me/qwq/doghouse/entity/Meta.java
Normal file
69
src/main/java/me/qwq/doghouse/entity/Meta.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
|
||||
/**
|
||||
* Meta, super class of Cate and Tag
|
||||
*
|
||||
* @author Doghole
|
||||
* @since 2021-12-01
|
||||
* @see Cate
|
||||
* @see Tag
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class Meta extends Model<Meta> implements Serializable {
|
||||
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
@TableId(value = "mid", type=IdType.AUTO)
|
||||
private Long mid;
|
||||
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
@TableField("type")
|
||||
private MetaTypeEnum type;
|
||||
|
||||
@TableField("parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@TableField("friendly_name")
|
||||
private String friendlyName;
|
||||
|
||||
public String getUrlEncodedFriendlyName() {
|
||||
return UriUtils.encode(getFriendlyName(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
@TableField(value = "post_count", exist = false)
|
||||
private Long postCount = 0L;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@TableField("no")
|
||||
private Integer no;
|
||||
|
||||
@TableField(exist=false)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
311
src/main/java/me/qwq/doghouse/entity/Post.java
Normal file
311
src/main/java/me/qwq/doghouse/entity/Post.java
Normal file
@@ -0,0 +1,311 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.PostStatusEnum;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IAvatarUrl;
|
||||
import me.qwq.doghouse.interfaces.IFriendlyName;
|
||||
import me.qwq.doghouse.interfaces.IPostPassMess;
|
||||
import me.qwq.doghouse.validator.FriendlyNameValid;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 文章/页面/Wiki
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@FriendlyNameValid
|
||||
public class Post extends Model<Post> implements Serializable, IPostPassMess<Post>, ITreeView<Post>, IAvatarUrl, IFriendlyName {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String URI_ID_TAG = "p";
|
||||
public static final String URI_PAGENAME_TAG = "n";
|
||||
|
||||
public static final String URL_ID_FORMAT = "/" + URI_ID_TAG + "/%s";
|
||||
public static final String URL_PAGENAME_FORMAT = "/" + URI_PAGENAME_TAG + "/%s";
|
||||
public static final String URL_INDIVIDUAL_PAGE_FORMAT = "/%s.html";
|
||||
|
||||
/**
|
||||
*表主键id
|
||||
*/
|
||||
@TableId(value = "post_id", type = IdType.AUTO)
|
||||
private Long postId;
|
||||
|
||||
/**
|
||||
* 类型,0 - 文章,1 - 独立页面
|
||||
*/
|
||||
@TableField("type")
|
||||
private PostTypeEnum type;
|
||||
|
||||
/**
|
||||
* 模板
|
||||
*/
|
||||
@TableField("template")
|
||||
private String template;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@TableField("post_title")
|
||||
private String postTitle;
|
||||
|
||||
/**
|
||||
* 自定义路径url
|
||||
*/
|
||||
@TableField("post_page_name")
|
||||
private String postPageName;
|
||||
|
||||
/**
|
||||
* 前言
|
||||
*/
|
||||
@TableField("post_preface")
|
||||
private String postPreface;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@TableField("post_content")
|
||||
private String postContent;
|
||||
|
||||
/**
|
||||
* 关键词
|
||||
*/
|
||||
@TableField("keywords")
|
||||
private String keywords;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 分类id(冗余字段)
|
||||
*/
|
||||
@TableField(value = "categories_id", exist = false)
|
||||
private String categoriesId;
|
||||
|
||||
/**
|
||||
* 分类(冗余字段)
|
||||
*/
|
||||
@TableField(value = "categories_name", exist = false)
|
||||
private String categoriesName;
|
||||
|
||||
/**
|
||||
* 分类友好名称(冗余字段)
|
||||
*/
|
||||
@TableField(value = "categories_friendly_name", exist = false)
|
||||
private String categoriesFriendlyName;
|
||||
|
||||
/**
|
||||
* 前端遍历用
|
||||
* @return
|
||||
*/
|
||||
public List<String> getCategoriesFriendlyNamesEncoded() {
|
||||
if (categoriesFriendlyName == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return Arrays.asList(
|
||||
categoriesFriendlyName.split(","))
|
||||
.stream().map(i -> UriUtils.encode(i, StandardCharsets.UTF_8))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签id
|
||||
*/
|
||||
@TableField(value = "tags_id", exist = false)
|
||||
private String tagsId;
|
||||
|
||||
|
||||
/**
|
||||
* 标签名(冗余字段)
|
||||
*/
|
||||
@TableField(value = "tags_name", exist = false)
|
||||
private String tagsName;
|
||||
|
||||
/**
|
||||
* 标签友好名称(冗余字段)
|
||||
*/
|
||||
@TableField(value = "tags_friendly_name", exist = false)
|
||||
private String tagsFriendlyName;
|
||||
|
||||
/**
|
||||
* 前端遍历用
|
||||
* @return
|
||||
*/
|
||||
public List<String> getTagsFriendlyNamesEncoded() {
|
||||
if (tagsFriendlyName == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return Arrays.asList(
|
||||
tagsFriendlyName.split(","))
|
||||
.stream().map(i -> UriUtils.encode(i, StandardCharsets.UTF_8))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建者的用户 ID
|
||||
*/
|
||||
@TableField(value = "created_by")
|
||||
private Integer createdBy;
|
||||
|
||||
/**
|
||||
* 创建者昵称
|
||||
*/
|
||||
@TableField(value = "creator_nickname", exist = false)
|
||||
private String creatorNickname;
|
||||
|
||||
/**
|
||||
* 创建者邮箱地址
|
||||
*/
|
||||
@TableField(value = "email", exist = false)
|
||||
private String email;
|
||||
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
|
||||
@TableField("post_status")
|
||||
private PostStatusEnum postStatus;
|
||||
|
||||
/**
|
||||
* 阅读量
|
||||
*/
|
||||
@TableField("post_views")
|
||||
private Long postViews;
|
||||
|
||||
/**
|
||||
* 评论量
|
||||
*/
|
||||
@TableField(value = "comment_count", exist=false)
|
||||
private Long commentCount = 0L;
|
||||
|
||||
/**
|
||||
* 0-允许评论 1-不允许评论
|
||||
*/
|
||||
@TableField("enable_comment")
|
||||
private Boolean enableComment;
|
||||
|
||||
/**
|
||||
* 是否删除 0=否 1=是
|
||||
*/
|
||||
@TableField("is_deleted")
|
||||
private Boolean isDeleted;
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField("update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 获取相对链接
|
||||
*/
|
||||
public String getLink() {
|
||||
return getLink(type, postId, postPageName);
|
||||
}
|
||||
|
||||
public static String getLink(PostTypeEnum type, Long postId, String postPageName) {
|
||||
if (StringUtils.isBlank(postPageName)) {
|
||||
return String.format(URL_ID_FORMAT, postId);
|
||||
}
|
||||
else {
|
||||
postPageName = UriUtils.encode(postPageName, StandardCharsets.UTF_8);
|
||||
if (type == PostTypeEnum.PAGE) {
|
||||
return String.format(URL_INDIVIDUAL_PAGE_FORMAT, postPageName);
|
||||
}
|
||||
else {
|
||||
return String.format(URL_PAGENAME_FORMAT, postPageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FOR WIKI
|
||||
*/
|
||||
@TableField(value="parent_id")
|
||||
private Long parentId = -1L;
|
||||
|
||||
@TableField(value="no")
|
||||
private Integer no;
|
||||
|
||||
@TableField(value="level")
|
||||
private Integer level;
|
||||
|
||||
@TableField(value="searchable")
|
||||
private Boolean searchable;
|
||||
|
||||
/*
|
||||
* FOR TREEVIEW USE.
|
||||
*/
|
||||
|
||||
@TableField(exist=false)
|
||||
private List<Post> children;
|
||||
|
||||
public String getTitle() {
|
||||
return getPostTitle();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return getPostId();
|
||||
}
|
||||
|
||||
public Boolean getHasChildren() {
|
||||
return childrenCount > 0 || !CollectionUtils.isEmpty(children);
|
||||
}
|
||||
|
||||
@TableField(exist=false)
|
||||
private Boolean checked = false;
|
||||
|
||||
@TableField(exist=false)
|
||||
private Boolean spread = false;
|
||||
|
||||
@TableField(value = "post_count", exist = false)
|
||||
private Long postCount = 0L;
|
||||
|
||||
@TableField(value = "children_count", exist = false)
|
||||
private Long childrenCount = 0L;
|
||||
|
||||
public String getFriendlyName() {
|
||||
return getPostPageName();
|
||||
}
|
||||
|
||||
}
|
||||
54
src/main/java/me/qwq/doghouse/entity/RawConfig.java
Normal file
54
src/main/java/me/qwq/doghouse/entity/RawConfig.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 配置项
|
||||
* @author Doghole
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class RawConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 2194163620574826637L;
|
||||
|
||||
/**
|
||||
* 字段名
|
||||
*/
|
||||
@TableId("config_field")
|
||||
protected String configField;
|
||||
|
||||
/**
|
||||
* 配置名
|
||||
*/
|
||||
@TableField("config_name")
|
||||
protected String configName;
|
||||
|
||||
/**
|
||||
* 配置项的值
|
||||
*/
|
||||
@TableField("config_value")
|
||||
protected String configValue;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
protected LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@TableField("update_time")
|
||||
protected LocalDateTime updateTime;
|
||||
|
||||
|
||||
}
|
||||
41
src/main/java/me/qwq/doghouse/entity/Relationship.java
Normal file
41
src/main/java/me/qwq/doghouse/entity/Relationship.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
|
||||
/**
|
||||
* Mapping relationship between Meta and Post
|
||||
*
|
||||
* @author Doghole
|
||||
* @since 2021-12-01
|
||||
* @see Post
|
||||
* @see Meta
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class Relationship extends Model<Relationship> implements Serializable {
|
||||
|
||||
@TableId(value="relationship_id", type=IdType.AUTO)
|
||||
private Long relationshipId;
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
@TableField("mid")
|
||||
private Long mid;
|
||||
|
||||
@TableField("post_id")
|
||||
private Long postId;
|
||||
|
||||
}
|
||||
60
src/main/java/me/qwq/doghouse/entity/Tag.java
Normal file
60
src/main/java/me/qwq/doghouse/entity/Tag.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.enums.MetaTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IFriendlyName;
|
||||
import me.qwq.doghouse.validator.FriendlyNameValid;
|
||||
/**
|
||||
* Tag
|
||||
*
|
||||
* @author Doghole
|
||||
* @since 2021-12-01
|
||||
* @see Meta
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("meta")
|
||||
@FriendlyNameValid
|
||||
public class Tag extends Meta implements Serializable, IFriendlyName {
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
@TableId(value = "mid", type=IdType.AUTO)
|
||||
private Long mid;
|
||||
|
||||
@TableField("name")
|
||||
@NotBlank(message = "标签名称不允许为空")
|
||||
private String name;
|
||||
|
||||
@TableField("type")
|
||||
private MetaTypeEnum type = MetaTypeEnum.TAG;
|
||||
|
||||
|
||||
@TableField("parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@TableField("friendly_name")
|
||||
private String friendlyName;
|
||||
|
||||
@TableField("description")
|
||||
private String description;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@TableField("no")
|
||||
private Integer no;
|
||||
|
||||
}
|
||||
100
src/main/java/me/qwq/doghouse/entity/User.java
Normal file
100
src/main/java/me/qwq/doghouse/entity/User.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package me.qwq.doghouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.Pattern.Flag;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.component.Rk;
|
||||
import me.qwq.doghouse.util.GravatarUtils;
|
||||
|
||||
/**
|
||||
* 后台管理员信息
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@Component
|
||||
public class User implements Serializable {
|
||||
|
||||
static final long serialVersionUID=1L;
|
||||
|
||||
static final String USERNAME_CANNOT_BE_EMPTY = "Username cannot be empty/用户昵称不允许为空";
|
||||
static final String USERNAME_LENGTH_OVERFLOW = "Username length overflow/用户昵称过长";
|
||||
static final String EMAIL_CANNOT_BE_EMPTY = "Email cannot be empty/邮箱地址不允许为空";
|
||||
static final String EMAIL_INVALID = "Email is invaild/邮箱地址非法";
|
||||
static final String EMAIL_LENGTH_OVERFLOW = "Email length overflow/邮箱地址过长";
|
||||
static final String INVALID_REQUEST = "Invalid request/非法请求";
|
||||
static final String INTERNAL_ERROR = "Internal error/内部错误";
|
||||
static final String URL_INVALID = "Url is invaild/网站地址非法";
|
||||
static final String URL_LENGTH_OVERFLOW = "Url length overflow/网站地址过长";
|
||||
|
||||
/**
|
||||
* 管理员id
|
||||
*/
|
||||
@TableId(value = "user_id", type = IdType.AUTO)
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 管理员登陆名称
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 管理员登陆密码
|
||||
*/
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 管理员显示昵称
|
||||
*/
|
||||
@TableField("nickname")
|
||||
@NotBlank(message = USERNAME_CANNOT_BE_EMPTY)
|
||||
@Size(max = 25, message = USERNAME_LENGTH_OVERFLOW)
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 管理员显示昵称
|
||||
*/
|
||||
@TableField("email")
|
||||
@NotBlank(message = EMAIL_CANNOT_BE_EMPTY)
|
||||
@Email(
|
||||
flags = Flag.CASE_INSENSITIVE,
|
||||
message = EMAIL_INVALID)
|
||||
private String email;
|
||||
|
||||
@TableField("site")
|
||||
private String site;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
* @return
|
||||
*/
|
||||
public String getAvatarUrl() {
|
||||
return GravatarUtils.getProxyAvatarUrl(
|
||||
Rk.Gravatar.prepareDogHash(getEmail()), 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像
|
||||
* @return
|
||||
*/
|
||||
public String getAvatarUrl(Integer size) {
|
||||
return GravatarUtils.getProxyAvatarUrl(
|
||||
Rk.Gravatar.prepareDogHash(getEmail()), size);
|
||||
}
|
||||
|
||||
}
|
||||
373
src/main/java/me/qwq/doghouse/entity/config/CommentConfig.java
Normal file
373
src/main/java/me/qwq/doghouse/entity/config/CommentConfig.java
Normal file
@@ -0,0 +1,373 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.constants.UsefulRegex;
|
||||
import me.qwq.doghouse.entity.Comment;
|
||||
import me.qwq.doghouse.enums.CommentAction;
|
||||
import me.qwq.doghouse.enums.CommentActionAfter;
|
||||
import me.qwq.doghouse.enums.CommentStatusEnum;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.util.MarkDownUtils;
|
||||
|
||||
/**
|
||||
* 评论设置
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="comment", name="评论设置", initDefault = true)
|
||||
@Slf4j
|
||||
public class CommentConfig implements Serializable, IConfig<CommentConfig> {
|
||||
|
||||
/**
|
||||
* 默认构造器
|
||||
*/
|
||||
public CommentConfig() {
|
||||
this.markdownOn = false;
|
||||
this.ruleOn = true;
|
||||
this.maxLevel = 6;
|
||||
this.blockEmailAction = CommentAction.NONE;
|
||||
this.blockEmailActionAfter = CommentActionAfter.NONE;
|
||||
this.blockIpAction = CommentAction.NONE;
|
||||
this.blockIpActionAfter = CommentActionAfter.NONE;
|
||||
this.nonChineseAction = CommentAction.NONE;
|
||||
this.nonChineseActionAfter = CommentActionAfter.NONE;
|
||||
this.blockWordAction = CommentAction.NONE;
|
||||
this.blockWordActionAfter = CommentActionAfter.NONE;
|
||||
this.sensitiveWordAction = CommentAction.NONE;
|
||||
this.sensitiveWordActionAfter = CommentActionAfter.NONE;
|
||||
this.htmlWaitingOn = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持 Markdown
|
||||
*/
|
||||
private boolean markdownOn;
|
||||
|
||||
/**
|
||||
* 规则是否开启
|
||||
*/
|
||||
private Boolean ruleOn;
|
||||
|
||||
/**
|
||||
* 最大回复层级,0 则不限层级
|
||||
*/
|
||||
private Integer maxLevel;
|
||||
|
||||
/**
|
||||
* 封禁 IP 列表
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<String> blockIpList;
|
||||
|
||||
/**
|
||||
* 增加 IP 到 Block IP 列表
|
||||
* @param ip
|
||||
* @return
|
||||
*/
|
||||
public CommentConfig addBlockIp(String ip) {
|
||||
if (StringUtils.isBlank(ip)) {
|
||||
return this;
|
||||
}
|
||||
if (blockIpList == null) {
|
||||
blockIpList = new ArrayList<>();
|
||||
}
|
||||
ip = ip.trim().toLowerCase();
|
||||
if (!blockIpList.contains(ip)) {
|
||||
blockIpList.add(ip);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 IP Block 列表移除指定 IP
|
||||
* @param ip
|
||||
* @return
|
||||
*/
|
||||
public CommentConfig removeBlockIp(String ip) {
|
||||
if (blockIpList == null || blockIpList.size() == 0 || StringUtils.isBlank(ip)) {
|
||||
return this;
|
||||
}
|
||||
ip = ip.trim().toLowerCase();
|
||||
blockIpList.remove(ip.trim());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封禁 IP 列表转换为换行符分隔的文本,一般用于前台显示,下同
|
||||
* @return
|
||||
*/
|
||||
public String getBlockIpListText() {
|
||||
if (CollectionUtils.isEmpty(blockIpList)) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\r\n", blockIpList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置封禁 IP 列表转换为换行符分隔的文本,一般用于接收前台数据后封装,下同
|
||||
* @return
|
||||
*/
|
||||
public CommentConfig setBlockIpListText(String text) {
|
||||
blockIpList = new ArrayList<>(Arrays.asList(text.split("\r\n")));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对封禁 IP 的动作
|
||||
*/
|
||||
private CommentAction blockIpAction;
|
||||
|
||||
/**
|
||||
* 触发封禁 IP 动作为:SPAM/FAIL 后的进一步动作
|
||||
*/
|
||||
private CommentActionAfter blockIpActionAfter;
|
||||
|
||||
@JsonIgnore
|
||||
private List<String> blockEmailList;
|
||||
|
||||
public String getBlockEmailListText() {
|
||||
if (CollectionUtils.isEmpty(blockEmailList)) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\r\n", blockEmailList);
|
||||
}
|
||||
|
||||
public CommentConfig setBlockEmailListText(String text) {
|
||||
blockEmailList = new ArrayList<>(Arrays.asList(text.split("\r\n")));
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 增加 email 到 Block Email 列表
|
||||
* @param email
|
||||
* @return
|
||||
*/
|
||||
public CommentConfig addBlockEmail(String email) {
|
||||
if (StringUtils.isBlank(email)) {
|
||||
return this;
|
||||
}
|
||||
if (blockEmailList == null) {
|
||||
blockEmailList = new ArrayList<>();
|
||||
}
|
||||
email = "^" + email.trim().toLowerCase().replace(".", "\\.").replace("-", "\\-") + "$";
|
||||
if (!blockEmailList.contains(email)) {
|
||||
blockEmailList.add(email);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 IP Block 列表移除指定 IP
|
||||
* @param ip
|
||||
* @return
|
||||
*/
|
||||
public CommentConfig removeBlockEmail(String email) {
|
||||
if (blockEmailList == null || blockEmailList.size() == 0 || StringUtils.isBlank(email)) {
|
||||
return this;
|
||||
}
|
||||
email = "^" + email.trim().toLowerCase().replace(".", "\\.").replace("-", "\\-") + "$";
|
||||
blockEmailList.remove(email);
|
||||
return this;
|
||||
}
|
||||
|
||||
private CommentAction blockEmailAction;
|
||||
|
||||
private CommentActionAfter blockEmailActionAfter;
|
||||
|
||||
|
||||
|
||||
private CommentAction nonChineseAction;
|
||||
|
||||
private CommentActionAfter nonChineseActionAfter;
|
||||
|
||||
@JsonIgnore
|
||||
private List<String> blockWordList;
|
||||
|
||||
public String getBlockWordListText() {
|
||||
if (CollectionUtils.isEmpty(blockWordList)) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\r\n", blockWordList);
|
||||
}
|
||||
|
||||
public CommentConfig setBlockWordListText(String text) {
|
||||
blockWordList = new ArrayList<>(Arrays.asList(text.split("\r\n")));
|
||||
return this;
|
||||
}
|
||||
|
||||
private CommentAction blockWordAction;
|
||||
|
||||
private CommentActionAfter blockWordActionAfter;
|
||||
|
||||
@JsonIgnore
|
||||
private List<String> sensitiveWordList;
|
||||
|
||||
public String getSensitiveWordListText() {
|
||||
if (CollectionUtils.isEmpty(sensitiveWordList)) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\r\n", sensitiveWordList);
|
||||
}
|
||||
|
||||
public CommentConfig setSensitiveWordListText(String text) {
|
||||
sensitiveWordList = new ArrayList<>(Arrays.asList(text.split("\r\n")));
|
||||
return this;
|
||||
}
|
||||
|
||||
private CommentAction sensitiveWordAction;
|
||||
|
||||
private CommentActionAfter sensitiveWordActionAfter;
|
||||
|
||||
private Boolean htmlWaitingOn;
|
||||
|
||||
public CommentAction filter(Comment comment) {
|
||||
if (!ruleOn) {
|
||||
return CommentAction.NONE;
|
||||
}
|
||||
// IP Filter, support IPv6
|
||||
if (!CollectionUtils.isEmpty(blockIpList) && StringUtils.isNotBlank(comment.getIp())) {
|
||||
String ip = comment.getIp().trim().toLowerCase();
|
||||
for(String ipBlockRule : blockIpList) {
|
||||
Boolean fullMatched = !ipBlockRule.contains("?") && !ipBlockRule.contains("*");
|
||||
String regexForIP = ipBlockRule
|
||||
.replace(".", "\\.");
|
||||
if (!fullMatched) {
|
||||
regexForIP = regexForIP.replace("?", ".").replace("*", ".*?");
|
||||
}
|
||||
String prompt = fullMatched ? "完全" : "模糊";
|
||||
if (ip.matches(regexForIP)) {
|
||||
log.info("评论触发了 IP 地址黑名单过滤({}匹配)", prompt);
|
||||
if (!processComment(comment, blockIpAction, blockIpActionAfter)) {
|
||||
log.info("评论触发了 IP 地址黑名单过滤({}匹配)的 {} 操作", prompt, blockIpAction.getNote());
|
||||
return blockIpAction;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Email Filter
|
||||
if (!CollectionUtils.isEmpty(blockEmailList)) {
|
||||
for(String blockEmailRule : blockEmailList) {
|
||||
if (StringUtils.isBlank(blockEmailRule)) {
|
||||
continue;
|
||||
}
|
||||
Pattern pattern = Pattern.compile(blockEmailRule, Pattern.CASE_INSENSITIVE);
|
||||
if (pattern.matcher(comment.getEmail()).find()) {
|
||||
log.info("评论触发了邮箱地址黑名单过滤");
|
||||
if (!processComment(comment, blockEmailAction, blockEmailActionAfter)) {
|
||||
log.info("评论触发了邮箱地址黑名单过滤的 {} 操作", blockEmailAction.getNote());
|
||||
return blockEmailAction;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Non-Chinese filter
|
||||
if (!UsefulRegex.ANY_CHINESE_PATTERN.matcher(comment.getContent()).find()) {
|
||||
log.info("评论触发了非中文过滤");
|
||||
if (!processComment(comment, nonChineseAction, nonChineseActionAfter)) {
|
||||
log.info("评论触发了非中文过滤的 {} 操作", nonChineseAction.getNote());
|
||||
return nonChineseAction;
|
||||
}
|
||||
}
|
||||
|
||||
// Block Word Filter
|
||||
if (!CollectionUtils.isEmpty(blockWordList)) {
|
||||
for(String blockWordRule : blockWordList) {
|
||||
if (StringUtils.isBlank(blockWordRule)) {
|
||||
continue;
|
||||
}
|
||||
Pattern pattern = Pattern.compile(blockWordRule, Pattern.CASE_INSENSITIVE);
|
||||
if (pattern.matcher(comment.getAuthor()).find() || pattern.matcher(comment.getContent()).find()) {
|
||||
log.info("评论触发了违禁词过滤");
|
||||
if (!processComment(comment, blockWordAction, blockWordActionAfter)) {
|
||||
log.info("评论触发了违禁词过滤的 {} 操作", blockWordAction.getNote());
|
||||
return blockWordAction;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sensitive Word Filter
|
||||
if (!CollectionUtils.isEmpty(sensitiveWordList)) {
|
||||
for(String sensitiveWordRule : sensitiveWordList) {
|
||||
if (StringUtils.isBlank(sensitiveWordRule)) {
|
||||
continue;
|
||||
}
|
||||
Pattern pattern = Pattern.compile(sensitiveWordRule, Pattern.CASE_INSENSITIVE);
|
||||
if (pattern.matcher(comment.getAuthor()).find() || pattern.matcher(comment.getContent()).find()) {
|
||||
log.info("评论触发了敏感词过滤");
|
||||
if (!processComment(comment, sensitiveWordAction, sensitiveWordActionAfter)) {
|
||||
log.info("评论触发了敏感词过滤的 {} 操作", sensitiveWordAction.getNote());
|
||||
return sensitiveWordAction;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTML a tag & img tag
|
||||
if (markdownOn == true && htmlWaitingOn == true) {
|
||||
String html = MarkDownUtils.commentToHtml(comment.getContent());
|
||||
Document jsoupDoc = Jsoup.parseBodyFragment(html);
|
||||
if (jsoupDoc.select("a,img").size() > 0) {
|
||||
processComment(comment, CommentAction.WAITING, null);
|
||||
return CommentAction.WAITING;
|
||||
}
|
||||
}
|
||||
|
||||
return CommentAction.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否进行下一步
|
||||
* @param comment
|
||||
* @param action
|
||||
* @param actionAfter
|
||||
* @return
|
||||
*/
|
||||
private boolean processComment(Comment comment, CommentAction action, CommentActionAfter actionAfter) {
|
||||
if (action == CommentAction.NONE) {
|
||||
return true;
|
||||
}
|
||||
if (action == CommentAction.WAITING) {
|
||||
comment.setCommentStatus(CommentStatusEnum.WAITING);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
if (actionAfter == CommentActionAfter.BLOCK_EMAIL || actionAfter == CommentActionAfter.BOTH) {
|
||||
addBlockEmail(comment.getEmail());
|
||||
}
|
||||
if (actionAfter == CommentActionAfter.BLOCK_IP || actionAfter == CommentActionAfter.BOTH) {
|
||||
addBlockIp(comment.getIp());
|
||||
}
|
||||
comment.setCommentStatus(CommentStatusEnum.SPAM);
|
||||
if (action == CommentAction.FAIL) {
|
||||
comment.setIsDeleted(true);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
81
src/main/java/me/qwq/doghouse/entity/config/MailConfig.java
Normal file
81
src/main/java/me/qwq/doghouse/entity/config/MailConfig.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.constants.UsefulRegex;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.service.MailService;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="mail", name="邮件设置", initDefault=true)
|
||||
public class MailConfig implements Serializable, IConfig<MailConfig> {
|
||||
|
||||
private static final Boolean DEFAULT_MAIL_ON = false;
|
||||
private static final String EMPTY_STRING = "";
|
||||
private static final String DEFAULT_SUBJECT = "您在 Doghouse 的留言有新回复啦";
|
||||
|
||||
|
||||
@NotBlank
|
||||
private boolean mailOn = DEFAULT_MAIL_ON;
|
||||
|
||||
@NotBlank
|
||||
private String username = EMPTY_STRING;
|
||||
|
||||
private String password = EMPTY_STRING;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = UsefulRegex.DOMAIN_WITHOUT_SCHEME)
|
||||
private String smtpHost = EMPTY_STRING;
|
||||
|
||||
@NotNull
|
||||
@Size(min=1, max=65535)
|
||||
private Integer smtpPort = 25;
|
||||
|
||||
@NotBlank
|
||||
private String subject = DEFAULT_SUBJECT;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = UsefulRegex.EMAIL)
|
||||
private String fromAddress = EMPTY_STRING;
|
||||
|
||||
private String fromUser = EMPTY_STRING;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = UsefulRegex.EMAIL)
|
||||
private String adminAddress = EMPTY_STRING;
|
||||
|
||||
private boolean tlsEnable = false;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 0, max = 1440)
|
||||
private Long sendingDelay = 5L;
|
||||
|
||||
/**
|
||||
* 获取用于 Redisson 过期的 Duration
|
||||
* @return
|
||||
*/
|
||||
public Duration getMailDuration() {
|
||||
if (sendingDelay == null || sendingDelay <= 0L) {
|
||||
return Duration.ofSeconds(1L);
|
||||
}
|
||||
return Duration.ofMinutes(sendingDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSaving() {
|
||||
SpringContextHolder.getBean(MailService.class).resetTaskDelay();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
|
||||
import jodd.io.FileUtil;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="markdown", name="Markdown 设置", initDefault=true)
|
||||
@Slf4j
|
||||
public class MarkdownConfig implements Serializable, IConfig<MarkdownConfig> {
|
||||
|
||||
|
||||
private static final String DEFAULT_MERMAID_CONFIG = "";
|
||||
|
||||
private String mermaidConfig = DEFAULT_MERMAID_CONFIG;
|
||||
|
||||
public void afterSaving() {
|
||||
if (StringUtils.isNotBlank(mermaidConfig)) {
|
||||
try {
|
||||
JSONObject jo = JSONObject.parse(mermaidConfig);
|
||||
FileUtil.writeString("./conf/mermaid/config.json", jo.toJSONString());
|
||||
} catch (Exception e) {
|
||||
log.warn("Save mermaid self-defined css failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
128
src/main/java/me/qwq/doghouse/entity/config/NetworkConfig.java
Normal file
128
src/main/java/me/qwq/doghouse/entity/config/NetworkConfig.java
Normal file
@@ -0,0 +1,128 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
import me.qwq.doghouse.constants.UsefulRegex;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.pojo.bo.CidrBlock;
|
||||
import me.qwq.doghouse.util.IPUtils;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="network", name="网络设置", initDefault=true)
|
||||
public class NetworkConfig implements Serializable, IConfig<NetworkConfig> {
|
||||
|
||||
@StaticAttribute("ProxyTypeEnum")
|
||||
private Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
@Pattern(regexp = UsefulRegex.BLANK + UsefulRegex.OR + UsefulRegex.IPv4,
|
||||
message = "非法 Host")
|
||||
private String proxyHost = "";
|
||||
|
||||
@Min(1)
|
||||
@Max(65535)
|
||||
private Integer proxyPort = 1;
|
||||
|
||||
@NotNull
|
||||
private Boolean ignoreHttpsVerification = false;
|
||||
|
||||
@Pattern(regexp = "^((\\s*|" + UsefulRegex.IPv4 + ")(\\r?\\n?))*$")
|
||||
private String trustedProxies;
|
||||
|
||||
@JsonIgnore
|
||||
public List<String> getTrustedProxiesList() {
|
||||
if (StringUtils.isBlank(trustedProxies)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(trustedProxies.lines().toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置获取代理
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public Proxy getProxy () {
|
||||
if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) {
|
||||
return new Proxy(getProxyType(),
|
||||
new InetSocketAddress(getProxyHost(), getProxyPort()));
|
||||
}
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置获取代理 Url
|
||||
* @return
|
||||
*/
|
||||
public String getProxyUrl() {
|
||||
Proxy.Type type = getProxyType();
|
||||
String host = getProxyHost();
|
||||
Integer port = getProxyPort();
|
||||
if (ObjectUtils.anyNull(type, host, port)) {
|
||||
return null;
|
||||
}
|
||||
if (type == Proxy.Type.DIRECT) return null;
|
||||
StringBuilder sb = new StringBuilder()
|
||||
.append(type == Proxy.Type.SOCKS ? "socks5://" : "http://")
|
||||
.append(host).append(':').append(port);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
private Set<String> trustedIpList;
|
||||
|
||||
@JsonIgnore
|
||||
public Set<String> getTrustedIpList() {
|
||||
if (trustedIpList == null || trustedIpList.isEmpty()) {
|
||||
// 从 trustedProxies 中恢复
|
||||
trustedIpList = new HashSet<>();
|
||||
for (String potentialIp : getTrustedProxiesList()) {
|
||||
if (IPUtils.isIP(potentialIp)) {
|
||||
trustedIpList.add(potentialIp);
|
||||
}
|
||||
}
|
||||
}
|
||||
return trustedIpList;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
private Set<CidrBlock> trustedCidrBlockList;
|
||||
|
||||
@JsonIgnore
|
||||
public Set<CidrBlock> getTrustedCidrBlockList() {
|
||||
if (trustedCidrBlockList == null || trustedCidrBlockList.isEmpty()) {
|
||||
// 从 trustedProxies 中恢复
|
||||
trustedCidrBlockList = new HashSet<>();
|
||||
for (String potentialCidr : getTrustedProxiesList()) {
|
||||
if (IPUtils.isCIDR(potentialCidr)) {
|
||||
trustedCidrBlockList.add(new CidrBlock(potentialCidr));
|
||||
}
|
||||
}
|
||||
}
|
||||
return trustedCidrBlockList;
|
||||
}
|
||||
|
||||
public void afterSaving() {
|
||||
trustedIpList = null;
|
||||
trustedCidrBlockList = null;
|
||||
}
|
||||
}
|
||||
93
src/main/java/me/qwq/doghouse/entity/config/PageConfig.java
Normal file
93
src/main/java/me/qwq/doghouse/entity/config/PageConfig.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.cache.CacheConstants;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="page", name="独立页面设置", initDefault=true, managed=false)
|
||||
public class PageConfig implements Serializable, IConfig<PageConfig>, IPostTypeConfig<PageConfig> {
|
||||
|
||||
private static final String DEFAULT_NAME = "Doghouse";
|
||||
private static final String DEFAULT_DESCRIPTION = "Doghouse 个人博客系统";
|
||||
private static final Long DEFAULT_COMMENT_PAGE_SIZE = 5L;
|
||||
private static final Long DEFAULT_RECENT_SIZE = 6L;
|
||||
private static final Long DEFAULT_POPULAR_SIZE = 6L;
|
||||
private static final Long DEFAULT_TAG_SIZE = 8L;
|
||||
private static final Long DEFAULT_RECENT_COMMENT_SIZE = 6L;
|
||||
private static final Integer DEFAULT_RECENT_COMMENT_LEVEL = 3;
|
||||
|
||||
@NotBlank
|
||||
private String name = DEFAULT_NAME;
|
||||
|
||||
@NotBlank
|
||||
private String description = DEFAULT_DESCRIPTION;
|
||||
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long commentPageSize = DEFAULT_COMMENT_PAGE_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentSize = DEFAULT_RECENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long popularSize = DEFAULT_POPULAR_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long tagSize = DEFAULT_TAG_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentCommentSize = DEFAULT_RECENT_COMMENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Integer recentCommentLevel = DEFAULT_RECENT_COMMENT_LEVEL;
|
||||
|
||||
private Boolean addToSitemap = true;
|
||||
|
||||
private Boolean addToFeed = false;
|
||||
|
||||
private Boolean rssOn = false;
|
||||
|
||||
private Boolean imageCompress = false;
|
||||
|
||||
@Override
|
||||
public void afterSaving() {
|
||||
// 缓存驱逐:GET_JUMP_TO_URL
|
||||
// 因为具体是哪一页是和每页评论数有关的,每页评论数又在 SiteConfig 里定义
|
||||
// 一旦更改,那么评论对应页的缓存得驱逐
|
||||
((CacheManager)SpringContextHolder.getBean("cacheManager")).getCache(CacheConstants.Comments.GET_JUMP_TO_URL).clear();
|
||||
}
|
||||
|
||||
public PostTypeEnum getPostType() {
|
||||
return PostTypeEnum.PAGE;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 独立页面不需要 PageSize
|
||||
*/
|
||||
public Long getPageSize() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
public String getMappingPrefix() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
124
src/main/java/me/qwq/doghouse/entity/config/SiteConfig.java
Normal file
124
src/main/java/me/qwq/doghouse/entity/config/SiteConfig.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.Pattern.Flag;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.cache.CacheConstants;
|
||||
import me.qwq.doghouse.constants.UsefulRegex;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
import me.qwq.doghouse.util.SpringContextHolder;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="site", name="基本设置", initDefault=true)
|
||||
public class SiteConfig implements Serializable, IConfig<SiteConfig>, IPostTypeConfig<SiteConfig> {
|
||||
|
||||
private static final String GRAVATAR_URL_ADDRESS_INVALID = "Gravatar URL 地址不正确";
|
||||
private static final String ADDRESS_INVALID = "站点地址不正确";
|
||||
|
||||
private static final String DEFAULT_NAME = "Doghouse";
|
||||
private static final String DEFAULT_DESCRIPTION = "Doghouse 个人博客系统";
|
||||
private static final String DEFAULT_ADDRESS = "http://localhost";
|
||||
private static final String DEFAULT_KEYWORDS = "Doghouse,个人博客,博客,狗洞,日记本";
|
||||
private static final String DEFAULT_COPYRIGHT = "Doghouse";
|
||||
private static final String DEFAULT_BEIAN_INFO = "";
|
||||
private static final Long DEFAULT_PAGE_SIZE = 3L;
|
||||
private static final Long DEFAULT_COMMENT_PAGE_SIZE = 5L;
|
||||
private static final Long DEFAULT_RECENT_SIZE = 6L;
|
||||
private static final Long DEFAULT_POPULAR_SIZE = 6L;
|
||||
private static final Long DEFAULT_TAG_SIZE = 8L;
|
||||
private static final Long DEFAULT_RECENT_COMMENT_SIZE = 6L;
|
||||
private static final Integer DEFAULT_RECENT_COMMENT_LEVEL = 3;
|
||||
|
||||
@NotBlank
|
||||
private String name = DEFAULT_NAME;
|
||||
|
||||
private String themeIdentity;
|
||||
|
||||
@NotBlank
|
||||
private String description = DEFAULT_DESCRIPTION;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = UsefulRegex.URL_WITH_SCHEME,
|
||||
flags = Flag.CASE_INSENSITIVE,
|
||||
message = ADDRESS_INVALID)
|
||||
private String address = DEFAULT_ADDRESS;
|
||||
|
||||
private String keywords = DEFAULT_KEYWORDS;
|
||||
|
||||
private String copyright = DEFAULT_COPYRIGHT;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = UsefulRegex.URL_ENDS_WITH_SLASH,
|
||||
flags = Flag.CASE_INSENSITIVE,
|
||||
message = GRAVATAR_URL_ADDRESS_INVALID)
|
||||
private String gravatarUrl = "//sdn.geekzu.org/avatar/";
|
||||
|
||||
private String beiAnInfo = DEFAULT_BEIAN_INFO;
|
||||
|
||||
@NotNull
|
||||
@Size(min=1)
|
||||
private Long pageSize = DEFAULT_PAGE_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long commentPageSize = DEFAULT_COMMENT_PAGE_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentSize = DEFAULT_RECENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long popularSize = DEFAULT_POPULAR_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long tagSize = DEFAULT_TAG_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentCommentSize = DEFAULT_RECENT_COMMENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Integer recentCommentLevel = DEFAULT_RECENT_COMMENT_LEVEL;
|
||||
|
||||
private Boolean addToSitemap = true;
|
||||
|
||||
private Boolean addToFeed = true;
|
||||
|
||||
private Boolean rssOn = false;
|
||||
|
||||
/**
|
||||
* 是否启用图片压缩
|
||||
*/
|
||||
private Boolean imageCompress = false;
|
||||
|
||||
@Override
|
||||
public void afterSaving() {
|
||||
// 缓存驱逐:GET_JUMP_TO_URL
|
||||
// 因为具体是哪一页是和每页评论数有关的,每页评论数又在 SiteConfig 里定义
|
||||
// 一旦更改,那么评论对应页的缓存得驱逐
|
||||
((CacheManager)SpringContextHolder.getBean("cacheManager")).getCache(CacheConstants.Comments.GET_JUMP_TO_URL).clear();
|
||||
}
|
||||
|
||||
public PostTypeEnum getPostType() {
|
||||
return PostTypeEnum.POST;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getMappingPrefix() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
89
src/main/java/me/qwq/doghouse/entity/config/TweetConfig.java
Normal file
89
src/main/java/me/qwq/doghouse/entity/config/TweetConfig.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ConfigInfo(field = "tweet", name = "说说设置", initDefault = true)
|
||||
public class TweetConfig implements Serializable, IConfig<TweetConfig>, IPostTypeConfig<TweetConfig> {
|
||||
|
||||
private static final Boolean DEFAULT_TWEET_MODE = false;
|
||||
private static final String DEFAULT_NAME = "DogTweet";
|
||||
private static final String DEFAULT_DESCRIPTION = "Doghouse 说说";
|
||||
private static final String DEFAULT_KEYWORDS = "Doghouse,doghouse tweet,dogutweet,tweet,碎碎念,说说,心情,微博";
|
||||
private static final String DEFAULT_COPYRIGHT = "Doghouse";
|
||||
private static final Long DEFAULT_PAGE_SIZE = 3L;
|
||||
private static final Long DEFAULT_COMMENT_SIZE = 5L;
|
||||
private static final Long DEFAULT_RECENT_SIZE = 6L;
|
||||
private static final Long DEFAULT_POPULAR_SIZE = 6L;
|
||||
private static final Long DEFAULT_TAG_SIZE = 8L;
|
||||
private static final Long DEFAULT_RECENT_COMMENT_SIZE = 6L;
|
||||
private static final Integer DEFAULT_RECENT_COMMENT_LEVEL = 3;
|
||||
|
||||
@NotBlank
|
||||
private Boolean tweetMode = DEFAULT_TWEET_MODE;
|
||||
|
||||
@NotBlank
|
||||
private String name = DEFAULT_NAME;
|
||||
|
||||
@NotBlank
|
||||
private String description = DEFAULT_DESCRIPTION;
|
||||
|
||||
private String keywords = DEFAULT_KEYWORDS;
|
||||
|
||||
private String copyright = DEFAULT_COPYRIGHT;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long pageSize = DEFAULT_PAGE_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long commentPageSize = DEFAULT_COMMENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long recentSize = DEFAULT_RECENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long popularSize = DEFAULT_POPULAR_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long tagSize = DEFAULT_TAG_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Long recentCommentSize = DEFAULT_RECENT_COMMENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min = 0)
|
||||
private Integer recentCommentLevel = DEFAULT_RECENT_COMMENT_LEVEL;
|
||||
|
||||
private Boolean addToSitemap = true;
|
||||
|
||||
private Boolean addToFeed = false;
|
||||
|
||||
public PostTypeEnum getPostType() {
|
||||
return PostTypeEnum.TWEET;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return getTweetMode();
|
||||
}
|
||||
|
||||
public String getMappingPrefix() {
|
||||
return "/tweet";
|
||||
}
|
||||
|
||||
public String getTemplatePrefix() {
|
||||
return "tweet";
|
||||
}
|
||||
}
|
||||
91
src/main/java/me/qwq/doghouse/entity/config/WikiConfig.java
Normal file
91
src/main/java/me/qwq/doghouse/entity/config/WikiConfig.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package me.qwq.doghouse.entity.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.qwq.doghouse.annotation.ConfigInfo;
|
||||
import me.qwq.doghouse.enums.PostTypeEnum;
|
||||
import me.qwq.doghouse.interfaces.IConfig;
|
||||
import me.qwq.doghouse.interfaces.IPostTypeConfig;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@ConfigInfo(field="wiki", name="Wiki 设置", initDefault=true)
|
||||
public class WikiConfig implements Serializable, IConfig<WikiConfig>, IPostTypeConfig<WikiConfig> {
|
||||
|
||||
private static final Boolean DEFAULT_WIKI_MODE = false;
|
||||
private static final String DEFAULT_NAME = "DoguWiki";
|
||||
private static final String DEFAULT_DESCRIPTION = "Doghouse 个人 Wiki 系统";
|
||||
private static final String DEFAULT_KEYWORDS = "Doghouse, doghouse wiki, doguwiki,维基,知识库,狗洞";
|
||||
private static final String DEFAULT_COPYRIGHT = "Doghouse";
|
||||
private static final Long DEFAULT_PAGE_SIZE = 3L;
|
||||
private static final Long DEFAULT_COMMENT_PAGE_SIZE = 5L;
|
||||
private static final Long DEFAULT_RECENT_SIZE = 6L;
|
||||
private static final Long DEFAULT_POPULAR_SIZE = 6L;
|
||||
private static final Long DEFAULT_TAG_SIZE = 8L;
|
||||
private static final Long DEFAULT_RECENT_COMMENT_SIZE = 6L;
|
||||
private static final Integer DEFAULT_RECENT_COMMENT_LEVEL = 3;
|
||||
|
||||
|
||||
@NotBlank
|
||||
private Boolean wikiMode = DEFAULT_WIKI_MODE;
|
||||
|
||||
@NotBlank
|
||||
private String name = DEFAULT_NAME;
|
||||
|
||||
@NotBlank
|
||||
private String description = DEFAULT_DESCRIPTION;
|
||||
|
||||
private String keywords = DEFAULT_KEYWORDS;
|
||||
|
||||
private String copyright = DEFAULT_COPYRIGHT;
|
||||
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long commentPageSize = DEFAULT_COMMENT_PAGE_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentSize = DEFAULT_RECENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long popularSize = DEFAULT_POPULAR_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long tagSize = DEFAULT_TAG_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Long recentCommentSize = DEFAULT_RECENT_COMMENT_SIZE;
|
||||
@NotNull
|
||||
@Size(min=0)
|
||||
private Integer recentCommentLevel = DEFAULT_RECENT_COMMENT_LEVEL;
|
||||
|
||||
private Boolean addToSitemap = true;
|
||||
|
||||
private Boolean addToFeed = false;
|
||||
|
||||
public PostTypeEnum getPostType() {
|
||||
return PostTypeEnum.WIKI;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return getWikiMode();
|
||||
}
|
||||
|
||||
public Long getPageSize() {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
public String getMappingPrefix() {
|
||||
return "/wiki";
|
||||
}
|
||||
|
||||
public String getTemplatePrefix() {
|
||||
return "wiki";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
public enum CacheEvictionSpan {
|
||||
|
||||
HOURLY, DAILY, WEEKLY, MONTHLY, SEASONLY, YEARLY
|
||||
|
||||
}
|
||||
34
src/main/java/me/qwq/doghouse/enums/CommentAction.java
Normal file
34
src/main/java/me/qwq/doghouse/enums/CommentAction.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
/**
|
||||
* 评论触发动作
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum CommentAction implements IQueryableEnum {
|
||||
|
||||
NONE(0,"无动作"),
|
||||
WAITING(1,"标记为待审"),
|
||||
SPAM(2,"标记为垃圾"),
|
||||
FAIL(3,"评论失败");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
private final String note;
|
||||
|
||||
CommentAction(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
34
src/main/java/me/qwq/doghouse/enums/CommentActionAfter.java
Normal file
34
src/main/java/me/qwq/doghouse/enums/CommentActionAfter.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
/**
|
||||
* 标记为垃圾或失败后的动作
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum CommentActionAfter implements IQueryableEnum {
|
||||
|
||||
NONE(0,"无动作"),
|
||||
BLOCK_IP(1,"添加到屏蔽IP列表"),
|
||||
BLOCK_EMAIL(2,"添加到屏蔽邮箱列表"),
|
||||
BOTH(3, "屏蔽IP和邮箱");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
private final String note;
|
||||
|
||||
CommentActionAfter(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
33
src/main/java/me/qwq/doghouse/enums/CommentStatusEnum.java
Normal file
33
src/main/java/me/qwq/doghouse/enums/CommentStatusEnum.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
/**
|
||||
* 评论状态
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum CommentStatusEnum implements IQueryableEnum {
|
||||
|
||||
RELEASED(0,"审核通过"),
|
||||
WAITING(1,"审核中"),
|
||||
SPAM(2,"垃圾");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
private final String note;
|
||||
|
||||
CommentStatusEnum(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
161
src/main/java/me/qwq/doghouse/enums/FileTypeEnum.java
Normal file
161
src/main/java/me/qwq/doghouse/enums/FileTypeEnum.java
Normal file
@@ -0,0 +1,161 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
import me.qwq.doghouse.entity.Asset;
|
||||
|
||||
/**
|
||||
* 文件类型枚举
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum FileTypeEnum implements IQueryableEnum {
|
||||
|
||||
OTHER(0, "其他"),
|
||||
IMAGE(1, "图片"),
|
||||
ARCHIVE(2, "压缩文件"),
|
||||
DOCUMENT(3, "文档"),
|
||||
MEDIA(4, "媒体文件"),
|
||||
APPLICATION(5, "应用程序");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
private final String note;
|
||||
|
||||
FileTypeEnum(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FileTypeEnum getFileType(Asset asset) {
|
||||
if (Objects.isNull(asset)) {
|
||||
return OTHER;
|
||||
}
|
||||
return getFileType(asset.getExt());
|
||||
}
|
||||
|
||||
public static FileTypeEnum getFileType(String ext) {
|
||||
if (StringUtils.isEmpty(ext)) {
|
||||
return OTHER;
|
||||
}
|
||||
ext = ext.toLowerCase();
|
||||
switch (ext) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "gif":
|
||||
case "png":
|
||||
case "apng":
|
||||
case "bmp":
|
||||
case "svg":
|
||||
case "ico":
|
||||
case "webp":
|
||||
case "psd":
|
||||
case "tif":
|
||||
case "raw":
|
||||
case "avif":
|
||||
return IMAGE;
|
||||
case "zip":
|
||||
case "rar":
|
||||
case "7z":
|
||||
case "001":
|
||||
case "iso":
|
||||
case "xz":
|
||||
case "txz":
|
||||
case "lzma":
|
||||
case "tar":
|
||||
case "cpio":
|
||||
case "bz2":
|
||||
case "bzip2":
|
||||
case "tbz2":
|
||||
case "tbz":
|
||||
case "gz":
|
||||
case "gzip":
|
||||
case "tgz":
|
||||
case "tpz":
|
||||
case "z":
|
||||
case "taz":
|
||||
case "lzh":
|
||||
case "lha":
|
||||
case "rpm":
|
||||
case "deb":
|
||||
case "arj":
|
||||
case "vhd":
|
||||
case "wim":
|
||||
case "swm":
|
||||
case "fat":
|
||||
case "ntfs":
|
||||
case "dmg":
|
||||
case "hfs":
|
||||
case "xar":
|
||||
case "squashfs":
|
||||
return ARCHIVE;
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "dot":
|
||||
case "dotx":
|
||||
case "ppt":
|
||||
case "pptx":
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case "pdf":
|
||||
case "txt":
|
||||
case "rtf":
|
||||
case "md":
|
||||
return DOCUMENT;
|
||||
case "mp3":
|
||||
case "aac":
|
||||
case "ac3":
|
||||
case "amr":
|
||||
case "ogg":
|
||||
case "ogm":
|
||||
case "ogv":
|
||||
case "dts":
|
||||
case "m1a":
|
||||
case "m2a":
|
||||
case "m4a":
|
||||
case "mp2":
|
||||
case "mpa":
|
||||
case "mpc":
|
||||
case "flac":
|
||||
case "mid":
|
||||
case "wav":
|
||||
case "wma":
|
||||
case "wmv":
|
||||
case "mp4":
|
||||
case "webm":
|
||||
case "swf":
|
||||
case "rm":
|
||||
case "rmvb":
|
||||
case "mov":
|
||||
case "mpeg":
|
||||
case "mkv":
|
||||
case "m4b":
|
||||
case "m4v":
|
||||
case "m4p":
|
||||
case "f4v":
|
||||
case "flv":
|
||||
case "3gp":
|
||||
return MEDIA;
|
||||
case "exe":
|
||||
case "msi":
|
||||
case "bat":
|
||||
case "cmd":
|
||||
case "apk":
|
||||
return APPLICATION;
|
||||
default:
|
||||
return OTHER;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/java/me/qwq/doghouse/enums/FuncsEnum.java
Normal file
22
src/main/java/me/qwq/doghouse/enums/FuncsEnum.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.github.yulichang.wrapper.enums.BaseFuncEnum;
|
||||
|
||||
public enum FuncsEnum implements BaseFuncEnum {
|
||||
|
||||
COUNT_DISTINCT("COUNT(DISTINCT %s)"),
|
||||
SELECT_META("GROUP_CONCAT(DISTINCT %s ORDER BY %s DESC, %s ASC)"),
|
||||
DISTINCT("DISTINCT %s");
|
||||
|
||||
private final String sql;
|
||||
|
||||
FuncsEnum(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSql() {
|
||||
return this.sql;
|
||||
}
|
||||
|
||||
}
|
||||
12
src/main/java/me/qwq/doghouse/enums/IQueryableEnum.java
Normal file
12
src/main/java/me/qwq/doghouse/enums/IQueryableEnum.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
public interface IQueryableEnum {
|
||||
|
||||
public default String getName() {
|
||||
return name();
|
||||
}
|
||||
String name();
|
||||
public int getStatus();
|
||||
public String getNote();
|
||||
|
||||
}
|
||||
35
src/main/java/me/qwq/doghouse/enums/LinkTypeEnum.java
Normal file
35
src/main/java/me/qwq/doghouse/enums/LinkTypeEnum.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
/**
|
||||
* 链接类型枚举
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum LinkTypeEnum implements IQueryableEnum {
|
||||
|
||||
FRIEND(0, "朋友"),
|
||||
RECOMMEND(1, "推荐"),
|
||||
NAVI(2, "导航");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
@JsonValue
|
||||
private final String note;
|
||||
|
||||
LinkTypeEnum(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
29
src/main/java/me/qwq/doghouse/enums/MetaTypeEnum.java
Normal file
29
src/main/java/me/qwq/doghouse/enums/MetaTypeEnum.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
@StaticAttribute
|
||||
public enum MetaTypeEnum implements IQueryableEnum {
|
||||
|
||||
CATEGORY(0,"分类"),
|
||||
TAG(1,"标签");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
private final String note;
|
||||
|
||||
MetaTypeEnum(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
36
src/main/java/me/qwq/doghouse/enums/PostStatusEnum.java
Normal file
36
src/main/java/me/qwq/doghouse/enums/PostStatusEnum.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package me.qwq.doghouse.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import me.qwq.doghouse.annotation.StaticAttribute;
|
||||
|
||||
/**
|
||||
* 文章状态枚举
|
||||
**/
|
||||
@StaticAttribute
|
||||
public enum PostStatusEnum implements IQueryableEnum {
|
||||
|
||||
RELEASED(0, "公开"),
|
||||
DRAFT(1, "草稿"),
|
||||
HIDDEN(2, "隐藏"),
|
||||
PRIVATE(3, "私密");
|
||||
|
||||
@EnumValue
|
||||
private final int status;
|
||||
@JsonValue
|
||||
private final String note;
|
||||
|
||||
PostStatusEnum(int status, String note) {
|
||||
this.status = status;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user