Compare commits
8 Commits
3eb651e039
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fae09f42f | |||
| 7db42c6c30 | |||
| 304c458436 | |||
| b7afe9496a | |||
| 7380f783ee | |||
| 0a126eb520 | |||
| de52e2816c | |||
| 894a1c5d07 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -65,4 +65,5 @@ Desktop.ini
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
application-prod.yml
|
application-prod.yml
|
||||||
application-dev.yml
|
application-dev.yml
|
||||||
.env
|
.env
|
||||||
|
/.idea/.cache/.Apifox_Helper/.toolWindow.db
|
||||||
|
|||||||
9
.idea/ApifoxUploaderProjectSetting.xml
generated
9
.idea/ApifoxUploaderProjectSetting.xml
generated
@@ -1,6 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ApifoxUploaderProjectSetting">
|
<component name="ApifoxUploaderProjectSetting">
|
||||||
<option name="apiAccessToken" value="APS-mJ5jVj0KjHRVPvJnChI91r8WFqR0oXhE" />
|
<option name="apiAccessToken" value="APS-nkhftrUwkg4bhzK4DRYUNguJFix8j1fd" />
|
||||||
|
<option name="apiProjectIds">
|
||||||
|
<array>
|
||||||
|
<option value="<byte-array>rO0ABXNyADZjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50LlByb2plY3RBbmRNb2R1bGUAAAAAAAAAAQIAFVoABmVuYWJsZUwACG1vZHVsZUlkdAASTGphdmEvbGFuZy9TdHJpbmc7TAAGb3RoZXIxcQB+AAFMAAdvdGhlcjEwcQB+AAFMAAdvdGhlcjExcQB+AAFMAAdvdGhlcjEycQB+AAFMAAZvdGhlcjJxAH4AAUwABm90aGVyM3EAfgABTAAGb3RoZXI0cQB+AAFMAAZvdGhlcjVxAH4AAUwABm90aGVyNnEAfgABTAAGb3RoZXI3cQB+AAFMAAZvdGhlcjhxAH4AAUwABm90aGVyOXEAfgABTAAKcGF0aEJlZm9yZXEAfgABTAANcHJvamVjdEZvbGRlcnEAfgABTAAPcHJvamVjdEZvbGRlcklkcQB+AAFMAAlwcm9qZWN0SWRxAH4AAUwAC3Byb2plY3ROYW1lcQB+AAFMAAxzY2hlbWFGb2xkZXJxAH4AAUwACHNjaGVtYUlkcQB+AAF4cAF0ACp3ZWJsb2ctc3ByaW5nYm9vdC53ZWJsb2ctbW9kdWxlLWFkbWluLm1haW50AAc3MjIwMjQ1cHBwdAAHNjY1NzM5NHQAC2JyYW5jaC1tYWludAAM6buY6K6k5qih5Z2XcHBwcHB0AAB0AAnmoLnnm67lvZV0AAEwdAAHNzQ4NDkxM3QABVdCbG9ncQB+AAlxAH4ACg==</byte-array>" />
|
||||||
|
</array>
|
||||||
|
</option>
|
||||||
|
<option name="treeNodes" value="<byte-array>rO0ABXNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAnQABzM0MjU5MzBzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgAQTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAFGJyYW5jaEFuZFZlcnNpb25JdGVtdABLW0xjb20vaXRhbmdjZW50L2lkZWEvcGx1Z2luL2RpYWxvZy9jb21wb25lbnQvYWNjb3VudC9BY2NvdW50UmlnaHRQYW5lbEl0ZW07TAAUYnJhbmNoSWRBbmRWZXJzaW9uSWRxAH4ABUwACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAKZm9sZGVyVHlwZXEAfgAFTAAIZnVsbFBhdGhxAH4ABUwAA2tleXEAfgAFWwAJbW9kZWxJdGVtcQB+AAZMAAhtb2R1bGVJZHEAfgAFTAAEbmFtZXEAfgAFTAAIcGFyZW50SWRxAH4ABUwACXByb2plY3RJZHEAfgAFTAALcHJvamVjdE5hbWVxAH4ABUwABnRlYW1JZHEAfgAFTAAIdGVhbU5hbWVxAH4ABUwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQADOS4quS6uuWboumYn3Bwc3EAfgAAP0AAAAAAAAx3CAAAABAAAAABdAAHNzQ4NDkxM3NxAH4ABHQAEuS4quS6uuWboumYny9XQmxvZ3Bwc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcHEAfgAMcHB0AA9XQmxvZyAoNzQ4NDkxMyl0AAczNDI1OTMwcQB+AAx0AAVXQmxvZ3EAfgARcH5yAC5jb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50Lk5vZGVUeXBlAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdAAHUFJPSkVDVHgAcHBxAH4AA3BwcQB+AApwcHBxAH4AA3EAfgAKfnEAfgATdAAEVEVBTXQABzM5NjY0MDlzcQB+AAR0AAlIYW5zZXJEZXZwcHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAXQABzc0NjI0NzJzcQB+AAR0ABlIYW5zZXJEZXYvSU4tQUktaW50ZXJ2aWV3cHBzcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4AHBwcQB+AB1wcHQAGUlOLUFJLWludGVydmlldyAoNzQ2MjQ3Mil0AAczOTY2NDA5cQB+AB10AA9JTi1BSS1pbnRlcnZpZXdxAH4AInBxAH4AFXgAcHBxAH4AGXBwcQB+ABtwcHBxAH4AGXEAfgAbcQB+ABd4AA==</byte-array>" />
|
||||||
|
<option name="treeNodesJTree" value="<byte-array>rO0ABXNyACFqYXZheC5zd2luZy50cmVlLkRlZmF1bHRUcmVlTW9kZWynvpEmGsXl2QMAA1oAEmFza3NBbGxvd3NDaGlsZHJlbkwADGxpc3RlbmVyTGlzdHQAJUxqYXZheC9zd2luZy9ldmVudC9FdmVudExpc3RlbmVyTGlzdDtMAARyb290dAAbTGphdmF4L3N3aW5nL3RyZWUvVHJlZU5vZGU7eHAAc3IAI2phdmF4LnN3aW5nLmV2ZW50LkV2ZW50TGlzdGVuZXJMaXN0kUjMLXPfDt4DAAB4cHB4c3IAJ2phdmF4LnN3aW5nLnRyZWUuRGVmYXVsdE11dGFibGVUcmVlTm9kZcRYv/zyqHHgAwADWgAOYWxsb3dzQ2hpbGRyZW5MAAhjaGlsZHJlbnQAEkxqYXZhL3V0aWwvVmVjdG9yO0wABnBhcmVudHQAIkxqYXZheC9zd2luZy90cmVlL011dGFibGVUcmVlTm9kZTt4cAFzcgAQamF2YS51dGlsLlZlY3RvctmXfVuAO68BAwADSQARY2FwYWNpdHlJbmNyZW1lbnRJAAxlbGVtZW50Q291bnRbAAtlbGVtZW50RGF0YXQAE1tMamF2YS9sYW5nL09iamVjdDt4cAAAAAAAAAACdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAACnNxAH4ABgFzcQB+AAoAAAAAAAAAAXVxAH4ADQAAAApzcQB+AAYBcHEAfgAPdXEAfgANAAAAAnQACnVzZXJPYmplY3RzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgAQTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAFGJyYW5jaEFuZFZlcnNpb25JdGVtdABLW0xjb20vaXRhbmdjZW50L2lkZWEvcGx1Z2luL2RpYWxvZy9jb21wb25lbnQvYWNjb3VudC9BY2NvdW50UmlnaHRQYW5lbEl0ZW07TAAUYnJhbmNoSWRBbmRWZXJzaW9uSWRxAH4AFkwACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAKZm9sZGVyVHlwZXEAfgAWTAAIZnVsbFBhdGhxAH4AFkwAA2tleXEAfgAWWwAJbW9kZWxJdGVtcQB+ABdMAAhtb2R1bGVJZHEAfgAWTAAEbmFtZXEAfgAWTAAIcGFyZW50SWRxAH4AFkwACXByb2plY3RJZHEAfgAWTAALcHJvamVjdE5hbWVxAH4AFkwABnRlYW1JZHEAfgAWTAAIdGVhbU5hbWVxAH4AFkwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQAEuS4quS6uuWboumYny9XQmxvZ3VyAEtbTGNvbS5pdGFuZ2NlbnQuaWRlYS5wbHVnaW4uZGlhbG9nLmNvbXBvbmVudC5hY2NvdW50LkFjY291bnRSaWdodFBhbmVsSXRlbTspvFKeKrgMqQIAAHhwAAAAAXNyAEhjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmRpYWxvZy5jb21wb25lbnQuYWNjb3VudC5BY2NvdW50UmlnaHRQYW5lbEl0ZW0AAAAAAAAAAQIABFoAD2lzTWFpbk9yRGVmYXVsdEwACGljb25UeXBlcQB+ABZMAAJpZHEAfgAWTAAEbmFtZXEAfgAWeHABdAAGYnJhbmNodAAHNzIyMDI0NXQABG1haW5wc3IAF2phdmEudXRpbC5MaW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeABwcHQABzc0ODQ5MTN1cQB+ABwAAAABc3EAfgAeAXQABW1vZGVsdAAHNjY1NzM5NHQADOm7mOiupOaooeWdl3B0AA9XQmxvZyAoNzQ4NDkxMyl0AAczNDI1OTMwdAAHNzQ4NDkxM3QABVdCbG9ndAAHMzQyNTkzMHB+cgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5Ob2RlVHlwZQAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQAB1BST0pFQ1R4cHBwcHBwcHBweHEAfgAJdXEAfgANAAAAAnEAfgAUc3EAfgAVdAAM5Liq5Lq65Zui6ZifcHBzcQB+ACM/QAAAAAAAAHcIAAAAEAAAAAB4AHBwdAAHMzQyNTkzMHBwdAAM5Liq5Lq65Zui6ZifcHBwdAAHMzQyNTkzMHQADOS4quS6uuWboumYn35xAH4AMXQABFRFQU14c3EAfgAGAXNxAH4ACgAAAAAAAAABdXEAfgANAAAACnNxAH4ABgFwcQB+AD91cQB+AA0AAAACcQB+ABRzcQB+ABV0ABlIYW5zZXJEZXYvSU4tQUktaW50ZXJ2aWV3cHBzcQB+ACM/QAAAAAAAAHcIAAAAEAAAAAB4AHBwdAAHNzQ2MjQ3MnBwdAAZSU4tQUktaW50ZXJ2aWV3ICg3NDYyNDcyKXQABzM5NjY0MDl0AAc3NDYyNDcydAAPSU4tQUktaW50ZXJ2aWV3dAAHMzk2NjQwOXBxAH4AM3hwcHBwcHBwcHB4cQB+AAl1cQB+AA0AAAACcQB+ABRzcQB+ABV0AAlIYW5zZXJEZXZwcHNxAH4AIz9AAAAAAAAAdwgAAAAQAAAAAHgAcHB0AAczOTY2NDA5cHB0AAlIYW5zZXJEZXZwcHB0AAczOTY2NDA5dAAJSGFuc2VyRGV2cQB+AD14cHBwcHBwcHB4cHVxAH4ADQAAAAJxAH4AFHNxAH4AFXQABFJvb3RwcHBwcHQAATBwcHEAfgBXcHBwcHBxAH4APXhzcQB+AAoAAAAAAAAAAnVxAH4ADQAAAAp0AARyb290cQB+AAlwcHBwcHBwcHh4</byte-array>" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@@ -8,10 +8,12 @@
|
|||||||
<processorPath useClasspath="false">
|
<processorPath useClasspath="false">
|
||||||
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.42/8365263844ebb62398e0dc33057ba10ba472d3b8/lombok-1.18.42.jar" />
|
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.42/8365263844ebb62398e0dc33057ba10ba472d3b8/lombok-1.18.42.jar" />
|
||||||
</processorPath>
|
</processorPath>
|
||||||
|
<module name="weblog-springboot.weblog-module-jwt.main" />
|
||||||
<module name="weblog-springboot.weblog-web.test" />
|
<module name="weblog-springboot.weblog-web.test" />
|
||||||
<module name="weblog-springboot.weblog-module-admin.test" />
|
|
||||||
<module name="weblog-springboot.weblog-module-common.test" />
|
<module name="weblog-springboot.weblog-module-common.test" />
|
||||||
<module name="weblog-springboot.weblog-module-admin.main" />
|
<module name="weblog-springboot.weblog-module-admin.main" />
|
||||||
|
<module name="weblog-springboot.weblog-module-jwt.test" />
|
||||||
|
<module name="weblog-springboot.weblog-module-admin.test" />
|
||||||
<module name="weblog-springboot.weblog-module-common.main" />
|
<module name="weblog-springboot.weblog-module-common.main" />
|
||||||
<module name="weblog-springboot.weblog-web.main" />
|
<module name="weblog-springboot.weblog-web.main" />
|
||||||
</profile>
|
</profile>
|
||||||
|
|||||||
18
.idea/dataSources.xml
generated
Normal file
18
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="weblog@127.0.0.1" uuid="bb8330e4-9a89-4978-ad63-ad6402096c16">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<imported>true</imported>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://127.0.0.1:5432/weblog</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/data_source_mapping.xml
generated
Normal file
8
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bb8330e4-9a89-4978-ad63-ad6402096c16/console.sql" value="bb8330e4-9a89-4978-ad63-ad6402096c16" />
|
||||||
|
<file url="file://$PROJECT_DIR$/sql/createTable.sql" value="bb8330e4-9a89-4978-ad63-ad6402096c16" />
|
||||||
|
<file url="file://$PROJECT_DIR$/weblog-module-common/src/main/java/com/hanserwei/common/domain/dataobject/Category.java" value="bb8330e4-9a89-4978-ad63-ad6402096c16" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -10,6 +10,7 @@
|
|||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/weblog-module-admin" />
|
<option value="$PROJECT_DIR$/weblog-module-admin" />
|
||||||
<option value="$PROJECT_DIR$/weblog-module-common" />
|
<option value="$PROJECT_DIR$/weblog-module-common" />
|
||||||
|
<option value="$PROJECT_DIR$/weblog-module-jwt" />
|
||||||
<option value="$PROJECT_DIR$/weblog-web" />
|
<option value="$PROJECT_DIR$/weblog-web" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
@@ -3,7 +3,9 @@
|
|||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/weblog-springboot.iml" filepath="$PROJECT_DIR$/weblog-springboot.iml" />
|
<module fileurl="file://$PROJECT_DIR$/weblog-springboot.iml" filepath="$PROJECT_DIR$/weblog-springboot.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-admin/weblog-springboot.weblog-module-admin.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-admin/weblog-springboot.weblog-module-admin.main.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-jwt/weblog-springboot.weblog-module-jwt.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-jwt/weblog-springboot.weblog-module-jwt.main.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/sql/createTable.sql" dialect="PostgreSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
gradle/libs.versions.toml
Normal file
10
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[versions]
|
||||||
|
# 定义版本号
|
||||||
|
jjwt = "0.13.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
# 定义依赖包的别名 (bundles 是可选的,libraries 是必须的)
|
||||||
|
# 对应 Maven 的 <groupId>io.jsonwebtoken</groupId>
|
||||||
|
jjwt-api = { group = "io.jsonwebtoken", name = "jjwt-api", version.ref = "jjwt" }
|
||||||
|
jjwt-impl = { group = "io.jsonwebtoken", name = "jjwt-impl", version.ref = "jjwt" }
|
||||||
|
jjwt-jackson = { group = "io.jsonwebtoken", name = "jjwt-jackson", version.ref = "jjwt" }
|
||||||
@@ -2,4 +2,5 @@ rootProject.name = "weblog-springboot"
|
|||||||
|
|
||||||
include("weblog-web")
|
include("weblog-web")
|
||||||
include("weblog-module-admin")
|
include("weblog-module-admin")
|
||||||
include("weblog-module-common")
|
include("weblog-module-common")
|
||||||
|
include("weblog-module-jwt")
|
||||||
322
sql/createTable.sql
Normal file
322
sql/createTable.sql
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- 1. 创建一个函数,用于在数据更新时自动修改 update_time 字段
|
||||||
|
CREATE OR REPLACE FUNCTION set_update_time()
|
||||||
|
RETURNS TRIGGER AS
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
NEW.update_time = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
-- 2. 创建表(使用 BOOLEAN 替代 SMALLINT for is_deleted)
|
||||||
|
CREATE TABLE t_user
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(60) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(60) NOT NULL,
|
||||||
|
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
-- WITH TIME ZONE 是更严谨的选择
|
||||||
|
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
-- 使用 BOOLEAN 逻辑删除,DEFAULT FALSE 对应 '0:未删除'
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
-- 3. 创建触发器,在每次 UPDATE 操作前调用函数
|
||||||
|
CREATE TRIGGER set_t_user_update_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON t_user
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_update_time();
|
||||||
|
-- 添加注释
|
||||||
|
COMMENT ON TABLE t_user IS '用户表(优化版)';
|
||||||
|
COMMENT ON COLUMN t_user.is_deleted IS '逻辑删除:FALSE:未删除 TRUE:已删除';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_user_role
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(60) NOT NULL,
|
||||||
|
role_name VARCHAR(60) NOT NULL, -- 重命名为 role_name 避免关键字冲突
|
||||||
|
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_username ON t_user_role (username);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN t_user_role.role_name IS '角色名称';
|
||||||
|
-- 为 t_user_role 表创建触发器
|
||||||
|
CREATE TRIGGER set_t_user_role_update_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON t_user_role
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_update_time();
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_category
|
||||||
|
(
|
||||||
|
-- id:对应 MySQL 的 bigint(20) unsigned NOT NULL AUTO_INCREMENT
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- 分类名称:VARCHAR(60) NOT NULL DEFAULT '',同时是 UNIQUE 约束
|
||||||
|
"name" VARCHAR(60) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- 创建时间
|
||||||
|
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- 最后一次更新时间
|
||||||
|
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- 逻辑删除标志位:tinyint(2) NOT NULL DEFAULT '0',改为 BOOLEAN
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- UNIQUE KEY uk_name (`name`)
|
||||||
|
CONSTRAINT uk_name UNIQUE ("name")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 添加非唯一索引(对应 MySQL 的 KEY `idx_create_time`)
|
||||||
|
CREATE INDEX idx_create_time ON t_category (create_time);
|
||||||
|
|
||||||
|
-- 可选:添加注释
|
||||||
|
COMMENT ON TABLE t_category IS '文章分类表';
|
||||||
|
COMMENT ON COLUMN t_category.id IS '分类id';
|
||||||
|
COMMENT ON COLUMN t_category.name IS '分类名称';
|
||||||
|
COMMENT ON COLUMN t_category.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN t_category.update_time IS '最后一次更新时间';
|
||||||
|
COMMENT ON COLUMN t_category.is_deleted IS '逻辑删除标志位:FALSE:未删除 TRUE:已删除';
|
||||||
|
-- 为 t_category 表创建触发器
|
||||||
|
CREATE TRIGGER set_t_category_update_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON t_category
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_update_time();
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- 1. 创建表结构
|
||||||
|
CREATE TABLE t_tag
|
||||||
|
(
|
||||||
|
-- id: 使用 BIG SERIAL,自动创建序列,性能优异
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- name: 保持 VARCHAR(60),但在 PG 中 TEXT 和 VARCHAR 性能一样,
|
||||||
|
-- 这里为了保留原表 "60字符限制" 的业务逻辑,继续使用 VARCHAR(60)
|
||||||
|
name VARCHAR(60) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- create_time: 使用带时区的时间戳,更标准严谨
|
||||||
|
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- update_time: 同上
|
||||||
|
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- is_deleted: 使用原生 BOOLEAN 类型,存储效率高且语义明确
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- 约束:显式命名约束,方便后续维护(如报错时能看到具体约束名)
|
||||||
|
CONSTRAINT uk_tag_name UNIQUE (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 创建普通索引
|
||||||
|
-- 对应 MySQL 的 KEY `idx_create_time`
|
||||||
|
CREATE INDEX idx_tag_create_time ON t_tag (create_time);
|
||||||
|
|
||||||
|
-- 3. 添加注释 (PostgresSQL 标准方式)
|
||||||
|
COMMENT ON TABLE t_tag IS '文章标签表';
|
||||||
|
COMMENT ON COLUMN t_tag.id IS '标签id';
|
||||||
|
COMMENT ON COLUMN t_tag.name IS '标签名称';
|
||||||
|
COMMENT ON COLUMN t_tag.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN t_tag.update_time IS '最后一次更新时间';
|
||||||
|
COMMENT ON COLUMN t_tag.is_deleted IS '逻辑删除标志位:FALSE:未删除 TRUE:已删除';
|
||||||
|
|
||||||
|
-- 4. 应用自动更新时间戳触发器 (体现 PostgresSQL 强大的过程语言优势)
|
||||||
|
-- 前提:您之前已经执行过 CREATE FUNCTION set_update_time() ...
|
||||||
|
CREATE TRIGGER set_t_tag_update_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON t_tag
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_update_time();
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_blog_settings
|
||||||
|
(
|
||||||
|
-- id: 使用 BIG SERIAL 自动管理序列
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- logo: 图片路径可能很长,使用 TEXT 替代 VARCHAR(120),无性能损耗
|
||||||
|
logo TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- name: 博客名称通常较短,保留 VARCHAR 限制也是一种合理的业务约束
|
||||||
|
name VARCHAR(60) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- author: 作者名同上
|
||||||
|
author VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- introduction: 介绍语可能会变长,使用 TEXT 更灵活
|
||||||
|
introduction TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- avatar: 头像路径,使用 TEXT
|
||||||
|
avatar TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- 下面的主页链接:原 MySQL 定义 varchar(60) 风险很高,
|
||||||
|
-- 现在的 URL 很容易超过 60 字符,PG 使用 TEXT 完美解决
|
||||||
|
github_homepage TEXT NOT NULL DEFAULT '',
|
||||||
|
csdn_homepage TEXT NOT NULL DEFAULT '',
|
||||||
|
gitee_homepage TEXT NOT NULL DEFAULT '',
|
||||||
|
zhihu_homepage TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 添加注释
|
||||||
|
COMMENT ON TABLE t_blog_settings IS '博客设置表';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.id IS 'id';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.logo IS '博客Logo';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.name IS '博客名称';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.author IS '作者名';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.introduction IS '介绍语';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.avatar IS '作者头像';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.github_homepage IS 'GitHub 主页访问地址';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.csdn_homepage IS 'CSDN 主页访问地址';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.gitee_homepage IS 'Gitee 主页访问地址';
|
||||||
|
COMMENT ON COLUMN t_blog_settings.zhihu_homepage IS '知乎主页访问地址';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_article
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- 标题:保持限制,适合作为索引或列表显示
|
||||||
|
title VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- 封面:使用 TEXT,不再担心 URL 超长
|
||||||
|
cover TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
-- 摘要:使用 TEXT,不再受 160 字限制,前端截取即可
|
||||||
|
summary TEXT DEFAULT '',
|
||||||
|
|
||||||
|
-- 阅读量:使用 INTEGER 配合 CHECK 约束模拟 unsigned
|
||||||
|
read_num INTEGER NOT NULL DEFAULT 1 CHECK (read_num >= 0),
|
||||||
|
|
||||||
|
-- 时间与逻辑删除
|
||||||
|
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_article_create_time ON t_article (create_time);
|
||||||
|
|
||||||
|
-- 自动更新时间戳触发器
|
||||||
|
CREATE TRIGGER set_t_article_update_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON t_article
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_update_time();
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE t_article IS '文章表';
|
||||||
|
COMMENT ON COLUMN t_article.read_num IS '被阅读次数 (>=0)';
|
||||||
|
COMMENT ON COLUMN t_article.cover IS '文章封面';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_article_content
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- 外键关联字段
|
||||||
|
article_id BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- 正文:PG 的 TEXT 能够容纳海量文字
|
||||||
|
content TEXT,
|
||||||
|
|
||||||
|
-- 显式外键约束:确保 article_id 必须存在于 t_article 表中
|
||||||
|
-- ON DELETE CASCADE: 如果物理删除了 t_article,对应的内容也会被自动删除
|
||||||
|
CONSTRAINT fk_article_content_article_id
|
||||||
|
FOREIGN KEY (article_id)
|
||||||
|
REFERENCES t_article (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_article_content_article_id ON t_article_content (article_id);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE t_article_content IS '文章内容表';
|
||||||
|
COMMENT ON COLUMN t_article_content.content IS '教程正文';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_article_category_rel
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- 文章 ID:添加外键,删除文章时自动删除此关联
|
||||||
|
article_id BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- 分类 ID:添加外键,删除分类时... (通常分类不轻易删,或者策略不同,这里设为级联删除)
|
||||||
|
category_id BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- 约束:保证 article_id 唯一 (对应 MySQL 的 UNIQUE KEY)
|
||||||
|
CONSTRAINT uni_article_category_rel_article_id UNIQUE (article_id),
|
||||||
|
|
||||||
|
-- 外键定义
|
||||||
|
CONSTRAINT fk_rel_cat_article
|
||||||
|
FOREIGN KEY (article_id)
|
||||||
|
REFERENCES t_article (id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_rel_cat_category
|
||||||
|
FOREIGN KEY (category_id)
|
||||||
|
REFERENCES t_category (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引:category_id 需要索引以便反向查询(查询某分类下有哪些文章)
|
||||||
|
CREATE INDEX idx_rel_cat_category_id ON t_article_category_rel (category_id);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE t_article_category_rel IS '文章所属分类关联表';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
|
CREATE TABLE t_article_tag_rel
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
article_id BIGINT NOT NULL,
|
||||||
|
tag_id BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- 外键定义:级联删除
|
||||||
|
CONSTRAINT fk_rel_tag_article
|
||||||
|
FOREIGN KEY (article_id)
|
||||||
|
REFERENCES t_article (id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_rel_tag_tag
|
||||||
|
FOREIGN KEY (tag_id)
|
||||||
|
REFERENCES t_tag (id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- ⚡ 优化:防止同一篇文章被打上重复的标签
|
||||||
|
CONSTRAINT uk_article_tag_rel_unique_pair UNIQUE (article_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX idx_rel_tag_article_id ON t_article_tag_rel (article_id);
|
||||||
|
CREATE INDEX idx_rel_tag_tag_id ON t_article_tag_rel (tag_id);
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON TABLE t_article_tag_rel IS '文章对应标签关联表';
|
||||||
|
-- ====================================================================================================================
|
||||||
|
-- ====================================================================================================================
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
`java-library`
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Test
|
api("org.springframework.boot:spring-boot-starter-security")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
|
|
||||||
implementation(project(":weblog-module-common"))
|
implementation(project(":weblog-module-common"))
|
||||||
}
|
|
||||||
|
api(project(":weblog-module-jwt"))
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.hanserwei.admin.config;
|
||||||
|
|
||||||
|
import com.hanserwei.jwt.config.JwtAuthenticationSecurityConfig;
|
||||||
|
import com.hanserwei.jwt.handler.RestAccessDeniedHandler;
|
||||||
|
import com.hanserwei.jwt.handler.RestAuthenticationEntryPoint;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
||||||
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RestAccessDeniedHandler restAccessDeniedHandler;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
// 1. 禁用 CSRF
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
// 2. 禁用表单登录
|
||||||
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
|
// 3. 授权规则配置
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/admin/**").authenticated() // 保护 /admin/**
|
||||||
|
.anyRequest().permitAll() // 其他放行
|
||||||
|
)
|
||||||
|
// 4. 会话管理:无状态
|
||||||
|
.sessionManagement(session -> session
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
)
|
||||||
|
// 5. 自定义未登录/权限不足的响应
|
||||||
|
.exceptionHandling(exception -> exception
|
||||||
|
.authenticationEntryPoint(restAuthenticationEntryPoint)
|
||||||
|
.accessDeniedHandler(restAccessDeniedHandler)
|
||||||
|
)
|
||||||
|
// 6. 应用自定义配置 (核心变化)
|
||||||
|
.with(jwtAuthenticationSecurityConfig, Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义 AuthenticationProvider Bean。
|
||||||
|
* Spring Security 会自动将此 Provider 注入到 AuthenticationManager 中。
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
|
||||||
|
PasswordEncoder passwordEncoder) {
|
||||||
|
// 修正点:直接在构造函数中传入 userDetailsService
|
||||||
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||||
|
|
||||||
|
// 设置密码编码器 (PasswordEncoder 依然使用 setter 设置)
|
||||||
|
authProvider.setPasswordEncoder(passwordEncoder);
|
||||||
|
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.article.*;
|
||||||
|
import com.hanserwei.admin.service.AdminArticleService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端文章控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/article")
|
||||||
|
public class AdminArticleController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminArticleService articleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布文章
|
||||||
|
*/
|
||||||
|
@PostMapping("/publish")
|
||||||
|
@ApiOperationLog(description = "文章发布")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
public Response<?> publishArticle(@RequestBody @Validated PublishArticleReqVO publishArticleReqVO) {
|
||||||
|
return articleService.publishArticle(publishArticleReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文章
|
||||||
|
*/
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@ApiOperationLog(description = "文章删除")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
public Response<?> deleteArticle(@RequestBody @Validated DeleteArticleReqVO deleteArticleReqVO) {
|
||||||
|
return articleService.deleteArticle(deleteArticleReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章分页数据
|
||||||
|
*/
|
||||||
|
@PostMapping("/list")
|
||||||
|
@ApiOperationLog(description = "查询文章分页数据")
|
||||||
|
public PageResponse<FindArticlePageListRspVO> findArticlePageList(@RequestBody @Validated FindArticlePageListReqVO findArticlePageListReqVO) {
|
||||||
|
return articleService.findArticlePageList(findArticlePageListReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章详情
|
||||||
|
*/
|
||||||
|
@PostMapping("/detail")
|
||||||
|
@ApiOperationLog(description = "查询文章详情")
|
||||||
|
public Response<FindArticleDetailRspVO> findArticleDetail(@RequestBody @Validated FindArticleDetailReqVO findArticlePageListReqVO) {
|
||||||
|
return articleService.findArticleDetail(findArticlePageListReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文章
|
||||||
|
*/
|
||||||
|
@PostMapping("/update")
|
||||||
|
@ApiOperationLog(description = "更新文章")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
public Response<?> updateArticle(@RequestBody @Validated UpdateArticleReqVO updateArticleReqVO) {
|
||||||
|
return articleService.updateArticle(updateArticleReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
|
||||||
|
import com.hanserwei.admin.service.AdminBlogSettingsService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端博客设置控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/blog/settings")
|
||||||
|
public class AdminBlogSettingsController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminBlogSettingsService blogSettingsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客基础信息修改
|
||||||
|
*/
|
||||||
|
@PostMapping("/update")
|
||||||
|
@ApiOperationLog(description = "博客基础信息修改")
|
||||||
|
public Response<?> updateBlogSettings(@RequestBody @Validated UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
|
||||||
|
return blogSettingsService.updateBlogSettings(updateBlogSettingsReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取博客设置详情
|
||||||
|
*/
|
||||||
|
@PostMapping("/detail")
|
||||||
|
@ApiOperationLog(description = "获取博客设置详情")
|
||||||
|
public Response<FindBlogSettingsRspVO> findDetail() {
|
||||||
|
return blogSettingsService.findDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.category.*;
|
||||||
|
import com.hanserwei.admin.service.AdminCategoryService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端分类控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
public class AdminCategoryController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminCategoryService adminCategoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加分类
|
||||||
|
*/
|
||||||
|
@PostMapping("/category/add")
|
||||||
|
@ApiOperationLog(description = "添加分类")
|
||||||
|
public Response<?> addCategory(@RequestBody @Validated AddCategoryReqVO addCategoryReqVO) {
|
||||||
|
return adminCategoryService.addCategory(addCategoryReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据获取
|
||||||
|
*/
|
||||||
|
@PostMapping("/category/list")
|
||||||
|
@ApiOperationLog(description = "分类分页数据获取")
|
||||||
|
public PageResponse<FindCategoryPageListRspVO> findCategoryList(@RequestBody @Validated FindCategoryPageListReqVO findCategoryPageListReqVO) {
|
||||||
|
return adminCategoryService.findCategoryList(findCategoryPageListReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
@PostMapping("/category/delete")
|
||||||
|
@ApiOperationLog(description = "删除分类")
|
||||||
|
public Response<?> deleteCategory(@RequestBody @Validated DeleteCategoryReqVO deleteCategoryReqVO) {
|
||||||
|
return adminCategoryService.deleteCategory(deleteCategoryReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类下拉列表
|
||||||
|
*/
|
||||||
|
@PostMapping("/category/select/list")
|
||||||
|
@ApiOperationLog(description = "分类 Select 下拉列表数据获取")
|
||||||
|
public Response<List<SelectRspVO>> findCategorySelectList() {
|
||||||
|
return adminCategoryService.findCategorySelectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据分类ID查询
|
||||||
|
*/
|
||||||
|
@GetMapping("/category/find/{id}")
|
||||||
|
@ApiOperationLog(description = "根据分类ID查询")
|
||||||
|
public Response<FindCategoryByIdRspVO> findCategoryById(@PathVariable Long id) {
|
||||||
|
return adminCategoryService.findCategoryById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
|
||||||
|
import com.hanserwei.admin.service.AdminFileService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端文件控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
public class AdminFileController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminFileService fileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传
|
||||||
|
*/
|
||||||
|
@PostMapping("/file/upload")
|
||||||
|
@ApiOperationLog(description = "文件上传")
|
||||||
|
public Response<UploadFileRspVO> uploadFile(@RequestParam("file") MultipartFile file) {
|
||||||
|
return fileService.uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.tag.*;
|
||||||
|
import com.hanserwei.admin.service.AdminTagService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端标签控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
public class AdminTagController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminTagService adminTagService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加标签
|
||||||
|
*/
|
||||||
|
@PostMapping("/tag/add")
|
||||||
|
@ApiOperationLog(description = "添加标签")
|
||||||
|
public Response<?> addTag(@RequestBody @Validated AddTagReqVO addTagReqVO) {
|
||||||
|
return adminTagService.addTag(addTagReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签分页数据获取
|
||||||
|
*/
|
||||||
|
@PostMapping("/tag/list")
|
||||||
|
@ApiOperationLog(description = "标签分页数据获取")
|
||||||
|
public PageResponse<FindTagPageListRspVO> findTagList(@RequestBody @Validated FindTagPageListReqVO findTagPageListReqVO) {
|
||||||
|
return adminTagService.findTagList(findTagPageListReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除标签
|
||||||
|
*/
|
||||||
|
@PostMapping("/tag/delete")
|
||||||
|
@ApiOperationLog(description = "删除标签")
|
||||||
|
public Response<?> deleteTag(@RequestBody @Validated DeleteTagReqVO deleteTagReqVO) {
|
||||||
|
return adminTagService.deleteTag(deleteTagReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签模糊查询
|
||||||
|
*/
|
||||||
|
@PostMapping("/tag/search")
|
||||||
|
@ApiOperationLog(description = "标签模糊查询")
|
||||||
|
public Response<List<SelectRspVO>> searchTag(@RequestBody @Validated SearchTagReqVO searchTagReqVO) {
|
||||||
|
return adminTagService.searchTag(searchTagReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID列表获取标签
|
||||||
|
*/
|
||||||
|
@PostMapping("/tag/list/ids")
|
||||||
|
@ApiOperationLog(description = "根据ID列表获取标签")
|
||||||
|
public Response<List<FindTagsByIdsRspVO>> findTagsByIds(@RequestBody @Validated FindTagsByIdsReqVO findTagsByIdsReqVO) {
|
||||||
|
return adminTagService.findTagsByIds(findTagsByIdsReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.hanserwei.admin.controller;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.user.UpdateAdminUserPasswordReqVO;
|
||||||
|
import com.hanserwei.admin.service.AdminUserService;
|
||||||
|
import com.hanserwei.common.aspect.ApiOperationLog;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端用户控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
public class AdminUserController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminUserService adminUserService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户密码
|
||||||
|
*/
|
||||||
|
@PostMapping("/password/update")
|
||||||
|
@ApiOperationLog(description = "修改用户密码")
|
||||||
|
public Response<?> updatePassword(@RequestBody @Validated UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO) {
|
||||||
|
return adminUserService.updatePassword(updateAdminUserPasswordReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/user/info")
|
||||||
|
@ApiOperationLog(description = "获取用户信息")
|
||||||
|
public Response<FindUserInfoRspVO> findUserInfo() {
|
||||||
|
return adminUserService.findUserInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DeleteArticleReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "文章 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindArticleDetailReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章 ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "文章 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindArticleDetailRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章封面
|
||||||
|
*/
|
||||||
|
private String cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类 ID
|
||||||
|
*/
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签 ID 集合
|
||||||
|
*/
|
||||||
|
private List<Long> tagIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摘要
|
||||||
|
*/
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import com.hanserwei.common.model.BasePageQuery;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindArticlePageListReqVO extends BasePageQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布的起始日期
|
||||||
|
*/
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布的结束日期
|
||||||
|
*/
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindArticlePageListRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章封面
|
||||||
|
*/
|
||||||
|
private String cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 发布文章请求参数
|
||||||
|
*/
|
||||||
|
public class PublishArticleReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章标题不能为空")
|
||||||
|
@Length(min = 1, max = 40, message = "文章标题字数需大于 1 小于 40")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章内容
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章内容不能为空")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章封面图片URL
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章封面不能为空")
|
||||||
|
private String cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章摘要
|
||||||
|
*/
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章分类ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "文章分类不能为空")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标签列表
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "文章标签不能为空")
|
||||||
|
private List<String> tags;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.article;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文章请求参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class UpdateArticleReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "文章 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标题
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章标题不能为空")
|
||||||
|
@Length(min = 1, max = 40, message = "文章标题字数需大于 1 小于 40")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章内容
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章内容不能为空")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章封面图片URL
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "文章封面不能为空")
|
||||||
|
private String cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章摘要
|
||||||
|
*/
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章分类ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "文章分类不能为空")
|
||||||
|
private Long categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章标签列表
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "文章标签不能为空")
|
||||||
|
private List<String> tags;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.category;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class AddCategoryReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "分类名称不能为空")
|
||||||
|
@Length(min = 1, max = 10, message = "分类名称字数限制 1 ~ 10 之间")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.category;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DeleteCategoryReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "分类 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.category;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindCategoryByIdRspVO {
|
||||||
|
/**
|
||||||
|
* 分类 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.category;
|
||||||
|
|
||||||
|
import com.hanserwei.common.model.BasePageQuery;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindCategoryPageListReqVO extends BasePageQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建的起始日期
|
||||||
|
*/
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建的结束日期
|
||||||
|
*/
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.category;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindCategoryPageListRspVO {
|
||||||
|
/**
|
||||||
|
* 分类 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.file;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class UploadFileRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件的访问链接
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.setting;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询博客设置响应 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindBlogSettingsRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客 Logo
|
||||||
|
*/
|
||||||
|
private String logo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者名称
|
||||||
|
*/
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客介绍
|
||||||
|
*/
|
||||||
|
private String introduction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者头像
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub 主页地址
|
||||||
|
*/
|
||||||
|
private String githubHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSDN 主页地址
|
||||||
|
*/
|
||||||
|
private String csdnHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee 主页地址
|
||||||
|
*/
|
||||||
|
private String giteeHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知乎主页地址
|
||||||
|
*/
|
||||||
|
private String zhihuHomepage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.setting;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新博客设置请求 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class UpdateBlogSettingsReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客 LOGO
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "博客 LOGO 不能为空")
|
||||||
|
private String logo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客名称
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "博客名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客作者
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "博客作者不能为空")
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客介绍语
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "博客介绍语不能为空")
|
||||||
|
private String introduction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客头像
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "博客头像不能为空")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub 主页
|
||||||
|
*/
|
||||||
|
private String githubHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSDN 主页
|
||||||
|
*/
|
||||||
|
private String csdnHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee 主页
|
||||||
|
*/
|
||||||
|
private String giteeHomepage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知乎主页
|
||||||
|
*/
|
||||||
|
private String zhihuHomepage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class AddTagReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签集合
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "标签集合 不能为空")
|
||||||
|
private List<String> tags;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DeleteTagReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签 ID
|
||||||
|
*/
|
||||||
|
@NotNull(message = "标签 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import com.hanserwei.common.model.BasePageQuery;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindTagPageListReqVO extends BasePageQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建的起始日期
|
||||||
|
*/
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建的结束日期
|
||||||
|
*/
|
||||||
|
private LocalDate endDate;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindTagPageListRspVO {
|
||||||
|
/**
|
||||||
|
* 标签 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindTagsByIdsReqVO {
|
||||||
|
/**
|
||||||
|
* 标签 ID 集合
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "标签 ID 集合不能为空")
|
||||||
|
private List<Long> tagIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class FindTagsByIdsRspVO {
|
||||||
|
/**
|
||||||
|
* 标签 ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 标签名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.tag;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class SearchTagReqVO {
|
||||||
|
|
||||||
|
@NotEmpty(message = "标签查询关键词不能为空!")
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindUserInfoRspVO {
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.hanserwei.admin.model.vo.user;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class UpdateAdminUserPasswordReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新密码
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.article.*;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
|
||||||
|
public interface AdminArticleService {
|
||||||
|
/**
|
||||||
|
* 发布文章
|
||||||
|
*
|
||||||
|
* @param publishArticleReqVO 文章发布参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<?> publishArticle(PublishArticleReqVO publishArticleReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文章
|
||||||
|
*
|
||||||
|
* @param deleteArticleReqVO 删除文章参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<?> deleteArticle(DeleteArticleReqVO deleteArticleReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章分页数据
|
||||||
|
*
|
||||||
|
* @param findArticlePageListReqVO 查询文章分页请求参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
PageResponse<FindArticlePageListRspVO> findArticlePageList(FindArticlePageListReqVO findArticlePageListReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询文章详情
|
||||||
|
*
|
||||||
|
* @param findArticleDetailReqVO 查询文章详情参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<FindArticleDetailRspVO> findArticleDetail(FindArticleDetailReqVO findArticleDetailReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文章
|
||||||
|
*
|
||||||
|
* @param updateArticleReqVO 更新文章参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<?> updateArticle(UpdateArticleReqVO updateArticleReqVO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
|
||||||
|
public interface AdminBlogSettingsService {
|
||||||
|
/**
|
||||||
|
* 更新博客设置信息
|
||||||
|
*
|
||||||
|
* @param updateBlogSettingsReqVO 博客设置信息
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取博客设置详情
|
||||||
|
*
|
||||||
|
* @return 博客设置详情
|
||||||
|
*/
|
||||||
|
Response<FindBlogSettingsRspVO> findDetail();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.category.*;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface AdminCategoryService {
|
||||||
|
/**
|
||||||
|
* 添加分类
|
||||||
|
*
|
||||||
|
* @param addCategoryReqVO 添加分类请求参数
|
||||||
|
* @return 添加结果
|
||||||
|
*/
|
||||||
|
Response<?> addCategory(AddCategoryReqVO addCategoryReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据查询
|
||||||
|
*
|
||||||
|
* @param findCategoryPageListReqVO 分页查询分类参数
|
||||||
|
* @return 查询结果
|
||||||
|
*/
|
||||||
|
PageResponse<FindCategoryPageListRspVO> findCategoryList(FindCategoryPageListReqVO findCategoryPageListReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*
|
||||||
|
* @param deleteCategoryReqVO 删除分类参数
|
||||||
|
* @return 删除结果
|
||||||
|
*/
|
||||||
|
Response<?> deleteCategory(DeleteCategoryReqVO deleteCategoryReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文章分类的 Select 列表数据
|
||||||
|
*
|
||||||
|
* @return Select 列表数据
|
||||||
|
*/
|
||||||
|
Response<List<SelectRspVO>> findCategorySelectList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据分类 ID 查询分类
|
||||||
|
*
|
||||||
|
* @param id 分类 ID
|
||||||
|
* @return 查询结果
|
||||||
|
*/
|
||||||
|
Response<FindCategoryByIdRspVO> findCategoryById(Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public interface AdminFileService {
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*
|
||||||
|
* @param file 文件
|
||||||
|
* @return 访问地址
|
||||||
|
*/
|
||||||
|
Response<UploadFileRspVO> uploadFile(MultipartFile file);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.tag.*;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface AdminTagService {
|
||||||
|
/**
|
||||||
|
* 添加标签
|
||||||
|
*
|
||||||
|
* @param addTagReqVO 添加标签请求参数
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
Response<?> addTag(AddTagReqVO addTagReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签分页数据获取
|
||||||
|
*
|
||||||
|
* @param findTagPageListReqVO 标签分页数据获取请求参数
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
PageResponse<FindTagPageListRspVO> findTagList(FindTagPageListReqVO findTagPageListReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除标签
|
||||||
|
*
|
||||||
|
* @param deleteTagReqVO 删除标签请求参数
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
Response<?> deleteTag(DeleteTagReqVO deleteTagReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签下拉列表数据获取
|
||||||
|
*
|
||||||
|
* @param searchTagReqVO 搜索标签请求参数
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
Response<List<SelectRspVO>> searchTag(SearchTagReqVO searchTagReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 列表获取标签
|
||||||
|
*
|
||||||
|
* @param findTagsByIdsReqVO 根据 ID 列表获取标签请求参数
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
Response<List<FindTagsByIdsRspVO>> findTagsByIds(FindTagsByIdsReqVO findTagsByIdsReqVO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.admin.service;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.user.UpdateAdminUserPasswordReqVO;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
|
||||||
|
public interface AdminUserService {
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param updateAdminUserPasswordReqVO 修改密码参数
|
||||||
|
* @return 修改密码结果
|
||||||
|
*/
|
||||||
|
Response<?> updatePassword(UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
* @return 当前登录用户信息
|
||||||
|
*/
|
||||||
|
Response<FindUserInfoRspVO> findUserInfo();
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.article.*;
|
||||||
|
import com.hanserwei.admin.service.AdminArticleService;
|
||||||
|
import com.hanserwei.common.domain.dataobject.*;
|
||||||
|
import com.hanserwei.common.domain.repository.*;
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.exception.BizException;
|
||||||
|
import com.hanserwei.common.utils.PageHelper;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AdminArticleServiceImpl implements AdminArticleService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ArticleRepository articleRepository;
|
||||||
|
@Resource
|
||||||
|
private ArticleTagRelRepository articleTagRelRepository;
|
||||||
|
@Resource
|
||||||
|
private ArticleCategoryRelRepository articleCategoryRelRepository;
|
||||||
|
@Resource
|
||||||
|
private ArticleContentRepository articleContentRepository;
|
||||||
|
@Resource
|
||||||
|
private CategoryRepository categoryRepository;
|
||||||
|
@Resource
|
||||||
|
private TagRepository tagRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Response<?> publishArticle(PublishArticleReqVO publishArticleReqVO) {
|
||||||
|
Article article = new Article();
|
||||||
|
BeanUtils.copyProperties(publishArticleReqVO, article);
|
||||||
|
Article saved = articleRepository.save(article);
|
||||||
|
// 拿到保存后的文章id
|
||||||
|
Long articleId = saved.getId();
|
||||||
|
ArticleContent articleContent = ArticleContent.builder()
|
||||||
|
.articleId(articleId)
|
||||||
|
.content(publishArticleReqVO.getContent())
|
||||||
|
.build();
|
||||||
|
articleContentRepository.save(articleContent);
|
||||||
|
// 处理分类
|
||||||
|
Long categoryId = publishArticleReqVO.getCategoryId();
|
||||||
|
categoryRepository.findById(categoryId)
|
||||||
|
.ifPresentOrElse(p -> {
|
||||||
|
ArticleCategoryRel articleCategoryRel = ArticleCategoryRel.builder()
|
||||||
|
.articleId(articleId)
|
||||||
|
.categoryId(categoryId)
|
||||||
|
.build();
|
||||||
|
articleCategoryRelRepository.save(articleCategoryRel);
|
||||||
|
}, () -> {
|
||||||
|
log.warn("==>文章分类不存在: {}", categoryId);
|
||||||
|
throw new BizException(ResponseCodeEnum.CATEGORY_NOT_EXISTED);
|
||||||
|
});
|
||||||
|
// 保存标签
|
||||||
|
List<String> tags = publishArticleReqVO.getTags();
|
||||||
|
insertTags(articleId, tags);
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Response<?> deleteArticle(DeleteArticleReqVO deleteArticleReqVO) {
|
||||||
|
Long id = deleteArticleReqVO.getId();
|
||||||
|
articleRepository.deleteById(id);
|
||||||
|
articleCategoryRelRepository.deleteByArticleId(id);
|
||||||
|
articleTagRelRepository.deleteByArticleId(id);
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResponse<FindArticlePageListRspVO> findArticlePageList(FindArticlePageListReqVO findArticlePageListReqVO) {
|
||||||
|
return PageHelper.findPageList(
|
||||||
|
articleRepository,
|
||||||
|
findArticlePageListReqVO,
|
||||||
|
findArticlePageListReqVO.getTitle(),
|
||||||
|
"title",
|
||||||
|
findArticlePageListReqVO.getStartDate(),
|
||||||
|
findArticlePageListReqVO.getEndDate(),
|
||||||
|
article -> FindArticlePageListRspVO.builder()
|
||||||
|
.id(article.getId())
|
||||||
|
.title(article.getTitle())
|
||||||
|
.cover(article.getCover())
|
||||||
|
.createTime(LocalDateTime.ofInstant(article.getCreateTime(), ZoneId.systemDefault()))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<FindArticleDetailRspVO> findArticleDetail(FindArticleDetailReqVO findArticleDetailReqVO) {
|
||||||
|
Long id = findArticleDetailReqVO.getId();
|
||||||
|
Article article = articleRepository.findById(id)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
log.warn("==>文章不存在: {}", id);
|
||||||
|
return new BizException(ResponseCodeEnum.ARTICLE_NOT_EXIST);
|
||||||
|
});
|
||||||
|
// 文章正文详情
|
||||||
|
ArticleContent articleContent = articleContentRepository.findByArticleId(id);
|
||||||
|
// 所属分类
|
||||||
|
ArticleCategoryRel articleCategoryRel = articleCategoryRelRepository.findByArticleId(id);
|
||||||
|
// 对应标签集合
|
||||||
|
List<ArticleTagRel> articleTagRelList = articleTagRelRepository.findByArticleId(id);
|
||||||
|
List<Long> tags = articleTagRelList.stream().map(ArticleTagRel::getTagId).toList();
|
||||||
|
// 封装响应结果
|
||||||
|
FindArticleDetailRspVO findArticleDetailRspVO = new FindArticleDetailRspVO();
|
||||||
|
BeanUtils.copyProperties(article, findArticleDetailRspVO);
|
||||||
|
findArticleDetailRspVO.setTagIds(tags);
|
||||||
|
findArticleDetailRspVO.setCategoryId(articleCategoryRel.getCategoryId());
|
||||||
|
findArticleDetailRspVO.setContent(articleContent.getContent());
|
||||||
|
return Response.success(findArticleDetailRspVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Response<?> updateArticle(UpdateArticleReqVO updateArticleReqVO) {
|
||||||
|
Long id = updateArticleReqVO.getId();
|
||||||
|
Article article = articleRepository.findById(id)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
log.warn("==>欲更新的文章不存在: {}", id);
|
||||||
|
return new BizException(ResponseCodeEnum.ARTICLE_NOT_EXIST);
|
||||||
|
});
|
||||||
|
BeanUtils.copyProperties(updateArticleReqVO, article);
|
||||||
|
// 更新文章正文
|
||||||
|
ArticleContent articleContent = articleContentRepository.findByArticleId(id);
|
||||||
|
articleContent.setContent(updateArticleReqVO.getContent());
|
||||||
|
articleContentRepository.save(articleContent);
|
||||||
|
// 更新分类
|
||||||
|
Long categoryId = updateArticleReqVO.getCategoryId();
|
||||||
|
// 验证该ID的分类是否存在
|
||||||
|
categoryRepository.findById(categoryId).orElseThrow(() -> {
|
||||||
|
log.warn("==>欲更新的文章其分类不存在: {}", categoryId);
|
||||||
|
return new BizException(ResponseCodeEnum.CATEGORY_NOT_EXISTED);
|
||||||
|
});
|
||||||
|
// 先删除关联的分类记录,再插入新的关联记录
|
||||||
|
articleCategoryRelRepository.deleteByArticleId(id);
|
||||||
|
ArticleCategoryRel articleCategoryRel = ArticleCategoryRel.builder()
|
||||||
|
.articleId(id)
|
||||||
|
.categoryId(categoryId)
|
||||||
|
.build();
|
||||||
|
articleCategoryRelRepository.save(articleCategoryRel);
|
||||||
|
// 删除该文章的标签关联
|
||||||
|
articleTagRelRepository.deleteByArticleId(id);
|
||||||
|
insertTags(id, updateArticleReqVO.getTags());
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入标签
|
||||||
|
*
|
||||||
|
* @param articleId 文章ID
|
||||||
|
* @param tags 标签列表(可能是ID字符串或标签名称)
|
||||||
|
*/
|
||||||
|
private void insertTags(Long articleId, List<String> tags) {
|
||||||
|
if (CollectionUtils.isEmpty(tags)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String tagStr : tags) {
|
||||||
|
Long tagId;
|
||||||
|
|
||||||
|
// 判断是否为数字ID
|
||||||
|
try {
|
||||||
|
tagId = Long.parseLong(tagStr);
|
||||||
|
// 验证该ID的标签是否存在
|
||||||
|
Optional<Tag> tagOptional = tagRepository.findById(tagId);
|
||||||
|
if (tagOptional.isEmpty()) {
|
||||||
|
log.warn("==>标签ID不存在: {}", tagId);
|
||||||
|
throw new BizException(ResponseCodeEnum.TAG_NOT_EXISTED);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 不是数字,说明是新标签名称,需要先保存标签
|
||||||
|
Tag newTag = Tag.builder()
|
||||||
|
.name(tagStr)
|
||||||
|
.build();
|
||||||
|
Tag savedTag = tagRepository.save(newTag);
|
||||||
|
tagId = savedTag.getId();
|
||||||
|
log.info("==>新增标签: {}, ID: {}", tagStr, tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文章-标签关联关系
|
||||||
|
ArticleTagRel articleTagRel = ArticleTagRel.builder()
|
||||||
|
.articleId(articleId)
|
||||||
|
.tagId(tagId)
|
||||||
|
.build();
|
||||||
|
articleTagRelRepository.save(articleTagRel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
|
||||||
|
import com.hanserwei.admin.service.AdminBlogSettingsService;
|
||||||
|
import com.hanserwei.common.domain.dataobject.BlogSettings;
|
||||||
|
import com.hanserwei.common.domain.repository.BlogSettingsRepository;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminBlogSettingsServiceImpl implements AdminBlogSettingsService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private BlogSettingsRepository blogSettingsRepository;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
|
||||||
|
// 保存或更新博客设置
|
||||||
|
blogSettingsRepository.findById(1L)
|
||||||
|
.ifPresentOrElse(existingSettings -> {
|
||||||
|
// 如果存在,则更新现有记录
|
||||||
|
BeanUtils.copyProperties(updateBlogSettingsReqVO, existingSettings);
|
||||||
|
blogSettingsRepository.saveAndFlush(existingSettings);
|
||||||
|
}, () -> {
|
||||||
|
// 如果不存在,则创建新记录
|
||||||
|
BlogSettings blogSettings = new BlogSettings();
|
||||||
|
BeanUtils.copyProperties(updateBlogSettingsReqVO, blogSettings);
|
||||||
|
blogSettings.setId(1L);
|
||||||
|
blogSettingsRepository.saveAndFlush(blogSettings);
|
||||||
|
});
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<FindBlogSettingsRspVO> findDetail() {
|
||||||
|
return blogSettingsRepository.findById(1L)
|
||||||
|
.map(e -> {
|
||||||
|
FindBlogSettingsRspVO findBlogSettingsRspVO = new FindBlogSettingsRspVO();
|
||||||
|
BeanUtils.copyProperties(e, findBlogSettingsRspVO);
|
||||||
|
return Response.success(findBlogSettingsRspVO);
|
||||||
|
})
|
||||||
|
.orElse(Response.success(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.category.*;
|
||||||
|
import com.hanserwei.admin.service.AdminCategoryService;
|
||||||
|
import com.hanserwei.common.domain.dataobject.ArticleCategoryRel;
|
||||||
|
import com.hanserwei.common.domain.dataobject.Category;
|
||||||
|
import com.hanserwei.common.domain.repository.ArticleCategoryRelRepository;
|
||||||
|
import com.hanserwei.common.domain.repository.CategoryRepository;
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.exception.BizException;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageHelper;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminCategoryServiceImpl implements AdminCategoryService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CategoryRepository categoryRepository;
|
||||||
|
@Resource
|
||||||
|
private ArticleCategoryRelRepository articleCategoryRelRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> addCategory(AddCategoryReqVO addCategoryReqVO) {
|
||||||
|
String categoryName = addCategoryReqVO.getName();
|
||||||
|
// 先判断是否存在
|
||||||
|
if (categoryRepository.existsCategoryByName(categoryName)) {
|
||||||
|
throw new BizException(ResponseCodeEnum.CATEGORY_NAME_IS_EXISTED);
|
||||||
|
}
|
||||||
|
// 构造Category对象
|
||||||
|
Category category = Category.builder()
|
||||||
|
.name(categoryName)
|
||||||
|
.build();
|
||||||
|
categoryRepository.save(category);
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResponse<FindCategoryPageListRspVO> findCategoryList(FindCategoryPageListReqVO findCategoryPageListReqVO) {
|
||||||
|
return PageHelper.findPageList(
|
||||||
|
categoryRepository,
|
||||||
|
findCategoryPageListReqVO,
|
||||||
|
findCategoryPageListReqVO.getName(),
|
||||||
|
findCategoryPageListReqVO.getStartDate(),
|
||||||
|
findCategoryPageListReqVO.getEndDate(),
|
||||||
|
category -> FindCategoryPageListRspVO.builder()
|
||||||
|
.id(category.getId())
|
||||||
|
.name(category.getName())
|
||||||
|
.createTime(LocalDateTime.ofInstant(category.getCreateTime(), ZoneId.systemDefault()))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> deleteCategory(DeleteCategoryReqVO deleteCategoryReqVO) {
|
||||||
|
List<ArticleCategoryRel> articleTagRelList = articleCategoryRelRepository.findByCategoryId((deleteCategoryReqVO.getId()));
|
||||||
|
if (!CollectionUtils.isEmpty(articleTagRelList)) {
|
||||||
|
throw new BizException(ResponseCodeEnum.CATEGORY_HAS_ARTICLE);
|
||||||
|
}
|
||||||
|
return categoryRepository.findById(deleteCategoryReqVO.getId())
|
||||||
|
.map(tag -> {
|
||||||
|
tag.setIsDeleted(true);
|
||||||
|
categoryRepository.save(tag);
|
||||||
|
return Response.success();
|
||||||
|
})
|
||||||
|
.orElse(Response.fail(ResponseCodeEnum.CATEGORY_NOT_EXIST));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<List<SelectRspVO>> findCategorySelectList() {
|
||||||
|
List<Category> categoryList = categoryRepository.findAll();
|
||||||
|
// DO 转 VO
|
||||||
|
List<SelectRspVO> selectRspVOS = null;
|
||||||
|
if (!CollectionUtils.isEmpty(categoryList)) {
|
||||||
|
selectRspVOS = categoryList.stream()
|
||||||
|
.map(category -> SelectRspVO.builder()
|
||||||
|
.label(category.getName())
|
||||||
|
.value(category.getId())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return Response.success(selectRspVOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<FindCategoryByIdRspVO> findCategoryById(Long id) {
|
||||||
|
Category category = categoryRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BizException(ResponseCodeEnum.CATEGORY_NOT_EXIST));
|
||||||
|
return Response.success(FindCategoryByIdRspVO.builder()
|
||||||
|
.id(category.getId())
|
||||||
|
.name(category.getName())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
|
||||||
|
import com.hanserwei.admin.service.AdminFileService;
|
||||||
|
import com.hanserwei.admin.utils.RustfsUtils;
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.exception.BizException;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AdminFileServiceImpl implements AdminFileService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RustfsUtils rustfsUtils;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<UploadFileRspVO> uploadFile(MultipartFile file) {
|
||||||
|
try {
|
||||||
|
// 上传文件
|
||||||
|
String url = rustfsUtils.uploadFile(file);
|
||||||
|
return Response.success(UploadFileRspVO.builder().url(url).build());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("==> 上传文件异常:{} ...", e.getMessage());
|
||||||
|
throw new BizException(ResponseCodeEnum.FILE_UPLOAD_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.tag.*;
|
||||||
|
import com.hanserwei.admin.service.AdminTagService;
|
||||||
|
import com.hanserwei.common.domain.dataobject.ArticleTagRel;
|
||||||
|
import com.hanserwei.common.domain.dataobject.Tag;
|
||||||
|
import com.hanserwei.common.domain.repository.ArticleTagRelRepository;
|
||||||
|
import com.hanserwei.common.domain.repository.TagRepository;
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.exception.BizException;
|
||||||
|
import com.hanserwei.common.model.vo.SelectRspVO;
|
||||||
|
import com.hanserwei.common.utils.PageHelper;
|
||||||
|
import com.hanserwei.common.utils.PageResponse;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminTagServiceImpl implements AdminTagService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TagRepository tagRepository;
|
||||||
|
@Resource
|
||||||
|
private ArticleTagRelRepository articleTagRelRepository;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加标签
|
||||||
|
*
|
||||||
|
* @param addTagReqVO 添加标签请求对象
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Response<?> addTag(AddTagReqVO addTagReqVO) {
|
||||||
|
// 获取标签列表
|
||||||
|
List<String> tagList = addTagReqVO.getTags();
|
||||||
|
|
||||||
|
// 对标签进行清洗:去除空格、过滤空字符串、去重
|
||||||
|
List<String> names = tagList.stream()
|
||||||
|
.map(String::trim) // 去除首尾空格
|
||||||
|
.filter(s -> !s.isEmpty()) // 过滤空字符串
|
||||||
|
.distinct() // 去重
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 查询数据库中已存在的标签名称
|
||||||
|
List<String> exists = tagRepository.findByNameIn(names).stream()
|
||||||
|
.map(Tag::getName)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 筛选出需要新建的标签,并构建标签对象
|
||||||
|
List<Tag> toCreate = names.stream()
|
||||||
|
.filter(n -> !exists.contains(n)) // 过滤掉已存在的标签
|
||||||
|
.map(n -> Tag.builder().name(n).build()) // 构建标签对象
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 批量保存新标签到数据库
|
||||||
|
if (!toCreate.isEmpty()) {
|
||||||
|
tagRepository.saveAllAndFlush(toCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResponse<FindTagPageListRspVO> findTagList(FindTagPageListReqVO findTagPageListReqVO) {
|
||||||
|
return PageHelper.findPageList(
|
||||||
|
tagRepository,
|
||||||
|
findTagPageListReqVO,
|
||||||
|
findTagPageListReqVO.getName(),
|
||||||
|
findTagPageListReqVO.getStartDate(),
|
||||||
|
findTagPageListReqVO.getEndDate(),
|
||||||
|
tag -> FindTagPageListRspVO.builder()
|
||||||
|
.id(tag.getId())
|
||||||
|
.name(tag.getName())
|
||||||
|
.createTime(LocalDateTime.ofInstant(tag.getCreateTime(), ZoneId.systemDefault()))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> deleteTag(DeleteTagReqVO deleteTagReqVO) {
|
||||||
|
// 检查是否有关联的文章
|
||||||
|
List<ArticleTagRel> articleTagRelsList = articleTagRelRepository.findByTagId(deleteTagReqVO.getId());
|
||||||
|
if (!articleTagRelsList.isEmpty()) {
|
||||||
|
throw new BizException(ResponseCodeEnum.TAG_HAS_ARTICLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagRepository.findById(deleteTagReqVO.getId())
|
||||||
|
.map(tag -> {
|
||||||
|
tag.setIsDeleted(true);
|
||||||
|
tagRepository.save(tag);
|
||||||
|
return Response.success();
|
||||||
|
})
|
||||||
|
.orElse(Response.fail(ResponseCodeEnum.TAG_NOT_EXIST));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<List<SelectRspVO>> searchTag(SearchTagReqVO searchTagReqVO) {
|
||||||
|
// 使用模糊查询获取标签列表
|
||||||
|
List<Tag> tags = tagRepository.findByNameContaining(searchTagReqVO.getKey());
|
||||||
|
|
||||||
|
// 将标签转换为下拉列表格式
|
||||||
|
List<SelectRspVO> vos = tags.stream()
|
||||||
|
.map(tag -> SelectRspVO.builder()
|
||||||
|
.label(tag.getName())
|
||||||
|
.value(tag.getId())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Response.success(vos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<List<FindTagsByIdsRspVO>> findTagsByIds(FindTagsByIdsReqVO findTagsByIdsReqVO) {
|
||||||
|
List<Tag> tags = tagRepository.queryAllByIdIn(findTagsByIdsReqVO.getTagIds());
|
||||||
|
List<FindTagsByIdsRspVO> vos = tags.stream()
|
||||||
|
.map(tag -> FindTagsByIdsRspVO.builder()
|
||||||
|
.id(tag.getId())
|
||||||
|
.name(tag.getName())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
return Response.success(vos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.hanserwei.admin.service.impl;
|
||||||
|
|
||||||
|
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.hanserwei.admin.model.vo.user.UpdateAdminUserPasswordReqVO;
|
||||||
|
import com.hanserwei.admin.service.AdminUserService;
|
||||||
|
import com.hanserwei.common.domain.repository.UserRepository;
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.exception.BizException;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminUserServiceImpl implements AdminUserService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserRepository userRepository;
|
||||||
|
@Resource
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> updatePassword(UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO) {
|
||||||
|
// 拿到用户名密码
|
||||||
|
String username = updateAdminUserPasswordReqVO.getUsername();
|
||||||
|
String password = updateAdminUserPasswordReqVO.getPassword();
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
String encodePassword = passwordEncoder.encode(password);
|
||||||
|
|
||||||
|
int updatedRows = userRepository.updatePasswordByUsername(username, encodePassword);
|
||||||
|
|
||||||
|
if (updatedRows == 0) {
|
||||||
|
// 如果更新行数为 0,说明用户名不存在
|
||||||
|
throw new BizException(ResponseCodeEnum.USER_NOT_EXIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<FindUserInfoRspVO> findUserInfo() {
|
||||||
|
// 获取存储在 ThreadLocal 中的用户信息
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
// 拿到用户名
|
||||||
|
String username = authentication.getName();
|
||||||
|
return Response.success(new FindUserInfoRspVO(username));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
package com.hanserwei.admin.utils;
|
||||||
|
|
||||||
|
import com.hanserwei.common.config.RustfsProperties;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RustfsUtils {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RustfsProperties rustfsProperties;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private S3Client s3Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*
|
||||||
|
* @param file 文件
|
||||||
|
* @return 访问地址
|
||||||
|
* @throws Exception 异常
|
||||||
|
*/
|
||||||
|
public String uploadFile(MultipartFile file) throws Exception {
|
||||||
|
// 判断文件是否为空
|
||||||
|
if (file == null || file.getSize() == 0) {
|
||||||
|
log.error("==> 上传文件异常:文件大小为空 ...");
|
||||||
|
throw new RuntimeException("文件大小不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件的原始名称
|
||||||
|
String originalFileName = file.getOriginalFilename();
|
||||||
|
// 文件的 Content-Type
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
|
||||||
|
// 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
|
||||||
|
String key = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
// 获取文件的后缀,如 .jpg
|
||||||
|
String suffix = null;
|
||||||
|
if (originalFileName != null) {
|
||||||
|
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接上文件后缀,即为要存储的文件名
|
||||||
|
String objectName = String.format("%s%s", key, suffix);
|
||||||
|
|
||||||
|
log.info("==> 开始上传文件至 Rustfs, ObjectName: {}", objectName);
|
||||||
|
|
||||||
|
// 上传文件至 Rustfs
|
||||||
|
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
|
||||||
|
.key(objectName)
|
||||||
|
.bucket(rustfsProperties.getBucketName())
|
||||||
|
.contentType(contentType)
|
||||||
|
.contentLength(file.getSize())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
|
||||||
|
|
||||||
|
// 返回文件的访问链接
|
||||||
|
String url = String.format("%s/%s/%s", rustfsProperties.getEndpoint(), rustfsProperties.getBucketName(), objectName);
|
||||||
|
log.info("==> 上传文件至 Rustfs 成功,访问路径: {}", url);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
`java-library`
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// guava
|
|
||||||
implementation("com.google.guava:guava:33.5.0-jre")
|
implementation("com.google.guava:guava:33.5.0-jre")
|
||||||
// commons-lang3
|
|
||||||
implementation("org.apache.commons:commons-lang3:3.20.0")
|
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||||
// test
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
// jackson
|
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind")
|
implementation("com.fasterxml.jackson.core:jackson-databind")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-core")
|
implementation("com.fasterxml.jackson.core:jackson-core")
|
||||||
// aop
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
api("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
// web
|
api("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
api("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
api("org.springframework.boot:spring-boot-starter-aop")
|
||||||
|
api("software.amazon.awssdk:s3:2.40.1")
|
||||||
|
|
||||||
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ public class JacksonConfig {
|
|||||||
|
|
||||||
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
|
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
|
||||||
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
// 忽略未知属性
|
||||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
return objectMapper;
|
return objectMapper;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.hanserwei.common.config;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RustfsClientConfig {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RustfsProperties rustfsProperties;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public S3Client s3Client() {
|
||||||
|
return S3Client.builder()
|
||||||
|
.endpointOverride(URI.create(rustfsProperties.getEndpoint()))
|
||||||
|
.region(Region.US_EAST_1)
|
||||||
|
.credentialsProvider(
|
||||||
|
StaticCredentialsProvider.create(
|
||||||
|
AwsBasicCredentials.create(rustfsProperties.getAccessKey(), rustfsProperties.getSecretKey())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.forcePathStyle(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.hanserwei.common.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Data
|
||||||
|
@ConfigurationProperties(prefix = "rustfs")
|
||||||
|
public class RustfsProperties {
|
||||||
|
private String endpoint;
|
||||||
|
private String accessKey;
|
||||||
|
private String secretKey;
|
||||||
|
private String bucketName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_article", indexes = {
|
||||||
|
@Index(name = "idx_article_create_time", columnList = "create_time")
|
||||||
|
})
|
||||||
|
@SQLRestriction("is_deleted = false")
|
||||||
|
@SQLDelete(sql = "update t_article set is_deleted = true where id = ?")
|
||||||
|
public class Article implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 120)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String cover = "";
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String summary = "";
|
||||||
|
|
||||||
|
@Column(name = "read_num", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer readNum = 1;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
@Column(name = "is_deleted", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean isDeleted = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_article_category_rel",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_rel_cat_category_id", columnList = "category_id")
|
||||||
|
})
|
||||||
|
public class ArticleCategoryRel implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章ID (Unique)
|
||||||
|
*/
|
||||||
|
@Column(name = "article_id", nullable = false, unique = true)
|
||||||
|
private Long articleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类ID
|
||||||
|
*/
|
||||||
|
@Column(name = "category_id", nullable = false)
|
||||||
|
private Long categoryId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_article_content", indexes = {
|
||||||
|
@Index(name = "idx_article_content_article_id", columnList = "article_id")
|
||||||
|
})
|
||||||
|
public class ArticleContent implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联 Article 的 ID
|
||||||
|
* 这里只存储 ID,不建立对象关联,以实现轻量级操作
|
||||||
|
*/
|
||||||
|
@Column(name = "article_id", nullable = false)
|
||||||
|
private Long articleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正文内容
|
||||||
|
* 映射为 TEXT 类型,支持大文本
|
||||||
|
*/
|
||||||
|
@Column(name = "content", columnDefinition = "TEXT")
|
||||||
|
private String content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_article_tag_rel",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_rel_tag_article_id", columnList = "article_id"),
|
||||||
|
@Index(name = "idx_rel_tag_tag_id", columnList = "tag_id")
|
||||||
|
},
|
||||||
|
uniqueConstraints = {
|
||||||
|
// 对应数据库中的联合唯一约束
|
||||||
|
@UniqueConstraint(name = "uk_article_tag_rel_unique_pair", columnNames = {"article_id", "tag_id"})
|
||||||
|
})
|
||||||
|
public class ArticleTagRel implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章ID
|
||||||
|
*/
|
||||||
|
@Column(name = "article_id", nullable = false)
|
||||||
|
private Long articleId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签ID
|
||||||
|
*/
|
||||||
|
@Column(name = "tag_id", nullable = false)
|
||||||
|
private Long tagId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客设置表
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_blog_settings")
|
||||||
|
public class BlogSettings implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客Logo
|
||||||
|
* 数据库类型为 TEXT,Java中映射为 String 即可
|
||||||
|
*/
|
||||||
|
@Column(name = "logo", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String logo = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 博客名称
|
||||||
|
*/
|
||||||
|
@Column(name = "name", length = 60, nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者名
|
||||||
|
*/
|
||||||
|
@Column(name = "author", length = 20, nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private String author = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 介绍语
|
||||||
|
*/
|
||||||
|
@Column(name = "introduction", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String introduction = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者头像
|
||||||
|
*/
|
||||||
|
@Column(name = "avatar", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String avatar = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub 主页访问地址
|
||||||
|
*/
|
||||||
|
@Column(name = "github_homepage", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String githubHomepage = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSDN 主页访问地址
|
||||||
|
*/
|
||||||
|
@Column(name = "csdn_homepage", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String csdnHomepage = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee 主页访问地址
|
||||||
|
*/
|
||||||
|
@Column(name = "gitee_homepage", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String giteeHomepage = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知乎主页访问地址
|
||||||
|
*/
|
||||||
|
@Column(name = "zhihu_homepage", nullable = false, columnDefinition = "TEXT")
|
||||||
|
@Builder.Default
|
||||||
|
private String zhihuHomepage = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*; // 使用 Jakarta Persistence API (JPA 3.0+)
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import org.hibernate.annotations.*;
|
||||||
|
import lombok.*; // 引入所有 Lombok 注解
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章分类表(t_category 对应实体)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_category",
|
||||||
|
uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "uk_name", columnNames = {"name"})
|
||||||
|
},
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_create_time", columnList = "create_time")
|
||||||
|
})
|
||||||
|
@SQLRestriction("is_deleted = false")
|
||||||
|
public class Category implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类ID (BIG SERIAL PRIMARY KEY)
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类名称 (VARCHAR(60) NOT NULL DEFAULT '')
|
||||||
|
*/
|
||||||
|
@Column(name = "name", length = 60, nullable = false)
|
||||||
|
private String name = ""; // 对应数据库的 DEFAULT ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL)
|
||||||
|
*/
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后一次更新时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL)
|
||||||
|
* 配合数据库触发器或 ORM 框架自动更新
|
||||||
|
*/
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除标志位:FALSE:未删除 TRUE:已删除 (BOOLEAN NOT NULL DEFAULT FALSE)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "is_deleted", nullable = false)
|
||||||
|
private Boolean isDeleted = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_tag", indexes = {
|
||||||
|
@Index(name = "idx_tag_create_time", columnList = "create_time")
|
||||||
|
})
|
||||||
|
@SQLRestriction("is_deleted = false")
|
||||||
|
public class Tag implements Serializable {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 60, unique = true)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
@Column(name = "is_deleted", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean isDeleted = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*; // 使用 Jakarta Persistence API (JPA 3.0+)
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant; // 推荐用于 TIMESTAMP WITH TIME ZONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户表(t_user 对应实体)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@Table(name = "t_user")
|
||||||
|
@SQLRestriction("is_deleted = false")
|
||||||
|
@SQLDelete(sql = "UPDATE t_user SET is_deleted = true WHERE id = ?")
|
||||||
|
public class User implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID (BIG SERIAL)
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名 (VARCHAR(60) NOT NULL UNIQUE)
|
||||||
|
*/
|
||||||
|
@Column(name = "username", length = 60, nullable = false, unique = true)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码 (VARCHAR(60) NOT NULL)
|
||||||
|
*/
|
||||||
|
@Column(name = "password", length = 60, nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间 (TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW())
|
||||||
|
* 使用 Hibernate 的 @CreationTimestamp 确保创建时自动赋值
|
||||||
|
*/
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后一次更新时间 (TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW())
|
||||||
|
* 使用 Hibernate 的 @UpdateTimestamp 确保更新时自动赋值
|
||||||
|
* 注意:虽然数据库有触发器,但使用此注解可保持 ORM 层的同步性
|
||||||
|
*/
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "update_time", nullable = false)
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除:FALSE:未删除 TRUE:已删除 (BOOLEAN NOT NULL DEFAULT FALSE)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "is_deleted", nullable = false)
|
||||||
|
private Boolean isDeleted = false; // 对应数据库默认值
|
||||||
|
|
||||||
|
public User() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public User(Long id, String username, String password, Instant createTime, Instant updateTime, Boolean isDeleted) {
|
||||||
|
this.id = id;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.createTime = createTime;
|
||||||
|
this.updateTime = updateTime;
|
||||||
|
this.isDeleted = isDeleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.hanserwei.common.domain.dataobject;
|
||||||
|
|
||||||
|
import jakarta.persistence.*; // 使用 Jakarta Persistence API (JPA 3.0+)
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户角色表(t_user_role 对应实体)
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "t_user_role",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_username", columnList = "username") // 映射数据库中的 idx_username 索引
|
||||||
|
})
|
||||||
|
public class UserRole implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID (BIG SERIAL PRIMARY KEY)
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名 (VARCHAR(60) NOT NULL)
|
||||||
|
*/
|
||||||
|
@Column(name = "username", length = 60, nullable = false)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色名称 (VARCHAR(60) NOT NULL)
|
||||||
|
*/
|
||||||
|
@Column(name = "role_name", length = 60, nullable = false)
|
||||||
|
private String roleName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP)
|
||||||
|
*/
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
// --- 构造函数 (Constructor) ---
|
||||||
|
|
||||||
|
public UserRole() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRole(String username, String roleName) {
|
||||||
|
this.username = username;
|
||||||
|
this.roleName = roleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.ArticleCategoryRel;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ArticleCategoryRelRepository extends JpaRepository<ArticleCategoryRel, Long> {
|
||||||
|
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||||
|
@Query("delete from ArticleCategoryRel acr where acr.articleId = :articleId")
|
||||||
|
void deleteByArticleId(@Param("articleId") Long articleId);
|
||||||
|
|
||||||
|
ArticleCategoryRel findByArticleId(Long articleId);
|
||||||
|
|
||||||
|
List<ArticleCategoryRel> findByCategoryId(Long categoryId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.ArticleContent;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ArticleContentRepository extends JpaRepository<ArticleContent, Long> {
|
||||||
|
ArticleContent findByArticleId(Long articleId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.Article;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface ArticleRepository extends JpaRepository<Article, Long>, JpaSpecificationExecutor<Article> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.ArticleTagRel;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ArticleTagRelRepository extends JpaRepository<ArticleTagRel, Long> {
|
||||||
|
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||||
|
@Query("delete from ArticleTagRel atr where atr.articleId = :articleId")
|
||||||
|
void deleteByArticleId(@Param("articleId") Long articleId);
|
||||||
|
|
||||||
|
List<ArticleTagRel> findByArticleId(Long articleId);
|
||||||
|
|
||||||
|
List<ArticleTagRel> findByTagId(Long tagId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.BlogSettings;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface BlogSettingsRepository extends JpaRepository<BlogSettings, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.Category;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
|
||||||
|
boolean existsCategoryByName(String name);
|
||||||
|
|
||||||
|
Page<Category> findAll(Specification<Category> specification, Pageable pageable);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.Tag;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface TagRepository extends JpaRepository<Tag, Long>, JpaSpecificationExecutor<Tag> {
|
||||||
|
Collection<Tag> findByNameIn(List<String> names);
|
||||||
|
|
||||||
|
Page<Tag> findAll(Specification<Tag> specification, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标签名称模糊查询
|
||||||
|
* @param name 标签名称关键词
|
||||||
|
* @return 标签列表
|
||||||
|
*/
|
||||||
|
List<Tag> findByNameContaining(String name);
|
||||||
|
|
||||||
|
List<Tag> queryAllByIdIn(Collection<Long> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
User getUsersByUsername(String username);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query("UPDATE User u SET u.password = :newPassword WHERE u.username = :username")
|
||||||
|
int updatePasswordByUsername(@Param("username") String username,
|
||||||
|
@Param("newPassword") String newPassword);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.hanserwei.common.domain.repository;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.UserRole;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
|
||||||
|
|
||||||
|
List<UserRole> queryAllByUsername(String username);
|
||||||
|
}
|
||||||
@@ -13,7 +13,20 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
|||||||
PARAM_NOT_VALID("10001", "参数错误"),
|
PARAM_NOT_VALID("10001", "参数错误"),
|
||||||
|
|
||||||
// ----------- 业务异常状态码 -----------
|
// ----------- 业务异常状态码 -----------
|
||||||
;
|
LOGIN_FAIL("20000", "登录失败"),
|
||||||
|
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
|
||||||
|
UNAUTHORIZED("20002", "无访问权限,请先登录!"),
|
||||||
|
FORBIDDEN("20004", "演示账号仅支持查询操作!"),
|
||||||
|
USER_NOT_EXIST("2005", "有户不存在!"),
|
||||||
|
CATEGORY_NAME_IS_EXISTED("20005", "该分类已存在,请勿重复添加!"),
|
||||||
|
TAG_NOT_EXIST("20006", "标签不存在!"),
|
||||||
|
CATEGORY_NOT_EXIST("20007", "分类不存在!"),
|
||||||
|
FILE_UPLOAD_FAILED("20008", "上传文件失败!"),
|
||||||
|
CATEGORY_NOT_EXISTED("20009", "提交的分类不存在!"),
|
||||||
|
TAG_NOT_EXISTED("20010", "标签ID不存在或已删除!"),
|
||||||
|
ARTICLE_NOT_EXIST("20011", "文章不存在!"),
|
||||||
|
TAG_HAS_ARTICLE("20012", "该标签有关联文章,无法删除!"),
|
||||||
|
CATEGORY_HAS_ARTICLE("20013","该分类下有关联文章,无法删除!" );
|
||||||
|
|
||||||
// 异常码
|
// 异常码
|
||||||
private final String errorCode;
|
private final String errorCode;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
|
|||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
@@ -76,4 +77,11 @@ public class GlobalExceptionHandler {
|
|||||||
return Response.fail(errorCode, errorMessage);
|
return Response.fail(errorCode, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({ AccessDeniedException.class })
|
||||||
|
public void throwAccessDeniedException(AccessDeniedException e) throws AccessDeniedException {
|
||||||
|
// 捕获到鉴权失败异常,主动抛出,交给 RestAccessDeniedHandler 去处理
|
||||||
|
log.info("============= 捕获到 AccessDeniedException");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.hanserwei.common.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BasePageQuery {
|
||||||
|
/**
|
||||||
|
* 当前页码, 默认第一页
|
||||||
|
*/
|
||||||
|
private Long current = 1L;
|
||||||
|
/**
|
||||||
|
* 每页展示的数据数量,默认每页展示 10 条数据
|
||||||
|
*/
|
||||||
|
private Long size = 10L;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hanserwei.common.model.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class SelectRspVO {
|
||||||
|
/**
|
||||||
|
* Select 下拉列表的展示文字
|
||||||
|
*/
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select 下拉列表的 value 值,如 ID 等
|
||||||
|
*/
|
||||||
|
private Object value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.hanserwei.common.utils;
|
||||||
|
|
||||||
|
import com.hanserwei.common.model.BasePageQuery;
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询工具类
|
||||||
|
* 用于抽取通用的分页查询逻辑
|
||||||
|
*/
|
||||||
|
public class PageHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行带条件的分页查询
|
||||||
|
*
|
||||||
|
* @param repository JPA Repository
|
||||||
|
* @param pageQuery 分页查询参数
|
||||||
|
* @param searchText 搜索文本(用于模糊查询)
|
||||||
|
* @param searchField 搜索字段名称(如 "name"、"title" 等)
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @param converter DO 到 VO 的转换函数
|
||||||
|
* @param <T> 实体类型
|
||||||
|
* @param <R> 响应VO类型
|
||||||
|
* @return 分页响应
|
||||||
|
*/
|
||||||
|
public static <T, R> PageResponse<R> findPageList(
|
||||||
|
JpaSpecificationExecutor<T> repository,
|
||||||
|
BasePageQuery pageQuery,
|
||||||
|
String searchText,
|
||||||
|
String searchField,
|
||||||
|
LocalDate startDate,
|
||||||
|
LocalDate endDate,
|
||||||
|
Function<T, R> converter) {
|
||||||
|
|
||||||
|
// 构建分页参数
|
||||||
|
Pageable pageable = PageRequest.of(
|
||||||
|
pageQuery.getCurrent().intValue() - 1,
|
||||||
|
pageQuery.getSize().intValue(),
|
||||||
|
Sort.by(Sort.Direction.DESC, "createTime")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
Specification<T> specification = (root, query, criteriaBuilder) -> {
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
// 搜索文本模糊查询
|
||||||
|
if (StringUtils.hasText(searchText) && StringUtils.hasText(searchField)) {
|
||||||
|
predicates.add(
|
||||||
|
criteriaBuilder.like(root.get(searchField), "%" + searchText.trim() + "%")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始日期查询
|
||||||
|
if (Objects.nonNull(startDate)) {
|
||||||
|
predicates.add(
|
||||||
|
criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), startDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束日期查询
|
||||||
|
if (Objects.nonNull(endDate)) {
|
||||||
|
predicates.add(
|
||||||
|
criteriaBuilder.lessThan(root.get("createTime"), endDate.plusDays(1).atStartOfDay())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
Page<T> page = repository.findAll(specification, pageable);
|
||||||
|
|
||||||
|
// DO 转 VO
|
||||||
|
List<R> vos = page.getContent().stream()
|
||||||
|
.map(converter)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return PageResponse.success(page, vos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行带条件的分页查询(使用默认字段名 "name")
|
||||||
|
*
|
||||||
|
* @param repository JPA Repository
|
||||||
|
* @param pageQuery 分页查询参数
|
||||||
|
* @param name 名称(用于模糊查询)
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @param converter DO 到 VO 的转换函数
|
||||||
|
* @param <T> 实体类型
|
||||||
|
* @param <R> 响应VO类型
|
||||||
|
* @return 分页响应
|
||||||
|
*/
|
||||||
|
public static <T, R> PageResponse<R> findPageList(
|
||||||
|
JpaSpecificationExecutor<T> repository,
|
||||||
|
BasePageQuery pageQuery,
|
||||||
|
String name,
|
||||||
|
LocalDate startDate,
|
||||||
|
LocalDate endDate,
|
||||||
|
Function<T, R> converter) {
|
||||||
|
return findPageList(repository, pageQuery, name, "name", startDate, endDate, converter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.hanserwei.common.utils;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class PageResponse<T> extends Response<List<T>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总记录数
|
||||||
|
*/
|
||||||
|
private long total = 0L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页显示的记录数,默认每页显示 10 条
|
||||||
|
*/
|
||||||
|
private long size = 10L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前页码 (JPA Page 从 0 开始, 这里为方便前端, 统一改为从 1 开始)
|
||||||
|
*/
|
||||||
|
private long current;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总页数
|
||||||
|
*/
|
||||||
|
private long pages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应
|
||||||
|
*
|
||||||
|
* @param page Spring Data JPA 提供的分页接口
|
||||||
|
* @param <T> 响应数据类型
|
||||||
|
*/
|
||||||
|
public static <T> PageResponse<T> success(Page<T> page) {
|
||||||
|
PageResponse<T> response = new PageResponse<>();
|
||||||
|
response.setSuccess(true);
|
||||||
|
|
||||||
|
if (Objects.nonNull(page)) {
|
||||||
|
// JPA Page 的 getNumber() 是当前页码 (从 0 开始), 我们在返回时通常习惯改为从 1 开始
|
||||||
|
response.setCurrent(page.getNumber() + 1);
|
||||||
|
// JPA Page 的 getSize() 是每页大小
|
||||||
|
response.setSize(page.getSize());
|
||||||
|
// JPA Page 的 getTotalPages() 是总页数
|
||||||
|
response.setPages(page.getTotalPages());
|
||||||
|
// JPA Page 的 getTotalElements() 是总记录数
|
||||||
|
response.setTotal(page.getTotalElements());
|
||||||
|
// JPA Page 的 getContent() 是当前页的数据列表
|
||||||
|
response.setData(page.getContent());
|
||||||
|
} else {
|
||||||
|
// 如果传入的 page 为 null,设置默认值
|
||||||
|
response.setCurrent(1L);
|
||||||
|
response.setSize(10L);
|
||||||
|
response.setPages(0L);
|
||||||
|
response.setTotal(0L);
|
||||||
|
response.setData(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果您需要处理分页结果DTO(例如实体转DTO),可以添加一个重载方法:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应 (适用于将实体 Page<E> 转换为 DTO PageResponse<D>)
|
||||||
|
*
|
||||||
|
* @param page Spring Data JPA 提供的实体分页接口
|
||||||
|
* @param data 经过转换后的 DTO 列表
|
||||||
|
* @param <E> 实体类型
|
||||||
|
* @param <D> DTO 类型
|
||||||
|
*/
|
||||||
|
public static <E, D> PageResponse<D> success(Page<E> page, List<D> data) {
|
||||||
|
PageResponse<D> response = new PageResponse<>();
|
||||||
|
response.setSuccess(true);
|
||||||
|
|
||||||
|
if (Objects.nonNull(page)) {
|
||||||
|
response.setCurrent(page.getNumber() + 1);
|
||||||
|
response.setSize(page.getSize());
|
||||||
|
response.setPages(page.getTotalPages());
|
||||||
|
response.setTotal(page.getTotalElements());
|
||||||
|
response.setData(data); // 使用传入的 DTO 列表
|
||||||
|
} else {
|
||||||
|
response.setCurrent(1L);
|
||||||
|
response.setSize(10L);
|
||||||
|
response.setPages(0L);
|
||||||
|
response.setTotal(0L);
|
||||||
|
response.setData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
weblog-module-jwt/build.gradle.kts
Normal file
14
weblog-module-jwt/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation(project(":weblog-module-common"))
|
||||||
|
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||||
|
|
||||||
|
// jwt
|
||||||
|
api(libs.jjwt.api)
|
||||||
|
runtimeOnly(libs.jjwt.impl)
|
||||||
|
runtimeOnly(libs.jjwt.jackson)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.hanserwei.jwt.config;
|
||||||
|
|
||||||
|
import com.hanserwei.jwt.filter.JwtAuthenticationFilter;
|
||||||
|
import com.hanserwei.jwt.filter.TokenAuthenticationFilter;
|
||||||
|
import com.hanserwei.jwt.handler.RestAuthenticationFailureHandler;
|
||||||
|
import com.hanserwei.jwt.handler.RestAuthenticationSuccessHandler;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(HttpSecurity httpSecurity) {
|
||||||
|
// 1. 实例化自定义过滤器
|
||||||
|
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
|
||||||
|
|
||||||
|
// 2. 这里的 AuthenticationManager 是从外部传入的 (httpSecurity sharedObject)
|
||||||
|
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
|
||||||
|
|
||||||
|
// 3. 设置成功/失败处理器
|
||||||
|
filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
|
||||||
|
filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);
|
||||||
|
|
||||||
|
// 4. 将过滤器添加到 UsernamePasswordAuthenticationFilter 之前
|
||||||
|
// (JWT 校验通常在用户名密码校验之前,或者用来替换它,addFilterBefore 是最稳妥的)
|
||||||
|
httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
// 5. 在链路中加入 Token 校验过滤器,确保携带 Token 的请求被识别为已登录
|
||||||
|
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.jwt.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class PasswordEncoderConfig {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||||
|
System.out.println(encoder.encode("hanserwei"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.hanserwei.jwt.exception;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
|
public class UsernameOrPasswordNullException extends AuthenticationException {
|
||||||
|
public UsernameOrPasswordNullException(String msg, Throwable cause) {
|
||||||
|
super(msg, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UsernameOrPasswordNullException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.hanserwei.jwt.filter;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.hanserwei.jwt.exception.UsernameOrPasswordNullException;
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
|
||||||
|
|
||||||
|
public JwtAuthenticationFilter() {
|
||||||
|
super(new RegexRequestMatcher("/login", HttpMethod.POST.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
// 解析提交的JSON数据
|
||||||
|
JsonNode jsonNode = mapper.readTree(request.getInputStream());
|
||||||
|
JsonNode usernameNode = jsonNode.get("username");
|
||||||
|
JsonNode passwordNode = jsonNode.get("password");
|
||||||
|
// 判断用户名、密码是否为空
|
||||||
|
if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode)
|
||||||
|
|| StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) {
|
||||||
|
throw new UsernameOrPasswordNullException("用户名或密码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = usernameNode.textValue();
|
||||||
|
String password = passwordNode.textValue();
|
||||||
|
|
||||||
|
// 将用户名、密码封装到 Token 中
|
||||||
|
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
|
||||||
|
= new UsernamePasswordAuthenticationToken(username, password);
|
||||||
|
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.hanserwei.jwt.filter;
|
||||||
|
|
||||||
|
import com.hanserwei.jwt.utils.JwtTokenHelper;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import jakarta.annotation.Nonnull;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JwtTokenHelper jwtTokenHelper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
@Nonnull HttpServletResponse response,
|
||||||
|
@Nonnull FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
// 1. 从请求头中获取 Authorization
|
||||||
|
String header = request.getHeader("Authorization");
|
||||||
|
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.info("Request URI: {}", requestURI);
|
||||||
|
if (requestURI.startsWith("/admin")) {
|
||||||
|
// 2. 校验头格式 (必须以 Bearer 开头)
|
||||||
|
if (header != null && header.startsWith("Bearer ")) {
|
||||||
|
String token = StringUtils.substring(header, 7);
|
||||||
|
log.info("JWT Token: {}", token);
|
||||||
|
if (StringUtils.isNotBlank(token)) {
|
||||||
|
try {
|
||||||
|
// 3. 解析 Token (核心步骤)
|
||||||
|
// 注意:JwtTokenHelper.parseToken 方法内部已经处理了 JWT 格式校验和过期校验,
|
||||||
|
// 并将 JJWT 异常转换为了 Spring Security 的 AuthenticationException 抛出。
|
||||||
|
Jws<Claims> claimsJws = jwtTokenHelper.parseToken(token);
|
||||||
|
|
||||||
|
// 4. 获取用户名
|
||||||
|
// JJWT 0.12+ 建议使用 getPayload() 替代 getBody()
|
||||||
|
// 在 Helper 中生成 Token 时使用的是 .subject(username),所以这里取 Subject
|
||||||
|
String username = claimsJws.getPayload().getSubject();
|
||||||
|
|
||||||
|
// 5. 组装认证信息 (如果当前上下文没有认证信息)
|
||||||
|
if (StringUtils.isNotBlank(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
|
||||||
|
|
||||||
|
// 查询数据库获取用户完整信息 (包含权限)
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
|
// 构建 Spring Security 的认证 Token
|
||||||
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||||
|
userDetails,
|
||||||
|
null,
|
||||||
|
userDetails.getAuthorities()
|
||||||
|
);
|
||||||
|
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
|
// 6. 将认证信息存入 SecurityContext
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
// 7. 异常处理
|
||||||
|
// 捕获 JwtTokenHelper 抛出的 BadCredentialsException 或 CredentialsExpiredException
|
||||||
|
// 如果 Token 存在但是无效/过期,直接交给 EntryPoint 处理响应 (通常返回 401)
|
||||||
|
// 并 return 结束当前过滤器,不再往下执行
|
||||||
|
authenticationEntryPoint.commence(request, response, e);
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 处理其他未预料到的异常
|
||||||
|
log.error("JWT处理过程中发生未知错误", e);
|
||||||
|
authenticationEntryPoint.commence(request, response, new AuthenticationException("系统内部认证错误") {
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 放行请求 (如果没有 Token 或者 Token 校验通过,继续执行下一个过滤器)
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.hanserwei.jwt.handler;
|
||||||
|
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import com.hanserwei.jwt.utils.ResultUtil;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RestAccessDeniedHandler implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
|
||||||
|
log.warn("登录成功访问收保护的资源,但是权限不够: ", accessDeniedException);
|
||||||
|
// 预留,后面引入多角色时会用到
|
||||||
|
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.FORBIDDEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.hanserwei.jwt.handler;
|
||||||
|
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import com.hanserwei.jwt.utils.ResultUtil;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||||
|
log.warn("用户未登录访问受保护的资源: ", authException);
|
||||||
|
if (authException instanceof InsufficientAuthenticationException) {
|
||||||
|
ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(ResponseCodeEnum.UNAUTHORIZED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(authException.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.hanserwei.jwt.handler;
|
||||||
|
|
||||||
|
import com.hanserwei.common.enums.ResponseCodeEnum;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import com.hanserwei.jwt.exception.UsernameOrPasswordNullException;
|
||||||
|
import com.hanserwei.jwt.utils.ResultUtil;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
|
||||||
|
log.warn("AuthenticationException: ", exception);
|
||||||
|
if (exception instanceof UsernameOrPasswordNullException) {
|
||||||
|
// 用户名或密码为空
|
||||||
|
ResultUtil.fail(response, Response.fail(exception.getMessage()));
|
||||||
|
return;
|
||||||
|
} else if (exception instanceof BadCredentialsException) {
|
||||||
|
// 用户名或密码错误
|
||||||
|
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录失败
|
||||||
|
ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hanserwei.jwt.handler;
|
||||||
|
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import com.hanserwei.jwt.model.LoginRspVO;
|
||||||
|
import com.hanserwei.jwt.utils.JwtTokenHelper;
|
||||||
|
import com.hanserwei.jwt.utils.ResultUtil;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JwtTokenHelper jwtTokenHelper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
|
||||||
|
// 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
|
||||||
|
// 通过用户名生成 Token
|
||||||
|
String username = userDetails.getUsername();
|
||||||
|
String token = jwtTokenHelper.generateToken(username);
|
||||||
|
|
||||||
|
// 返回 Token
|
||||||
|
LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build();
|
||||||
|
|
||||||
|
ResultUtil.ok(response, Response.success(loginRspVO));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.hanserwei.jwt.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class LoginRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 值
|
||||||
|
*/
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.hanserwei.jwt.service;
|
||||||
|
|
||||||
|
import com.hanserwei.common.domain.dataobject.User;
|
||||||
|
import com.hanserwei.common.domain.dataobject.UserRole;
|
||||||
|
import com.hanserwei.common.domain.repository.UserRepository;
|
||||||
|
import com.hanserwei.common.domain.repository.UserRoleRepository;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class UserDetailServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserRoleRepository userRoleRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
// 从数据库查询用户信息
|
||||||
|
User user = userRepository.getUsersByUsername(username);
|
||||||
|
// 判断用户是否存在
|
||||||
|
if (Objects.isNull(user)) {
|
||||||
|
throw new UsernameNotFoundException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserRole> userRoles = userRoleRepository.queryAllByUsername(username);
|
||||||
|
// 转换为数组
|
||||||
|
String[] roleArr = new String[0];
|
||||||
|
if (!CollectionUtils.isEmpty(userRoles)) {
|
||||||
|
roleArr = userRoles.stream()
|
||||||
|
.map(UserRole::getRoleName)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorities 用于指定角色,这里写死为 ADMIN 管理员
|
||||||
|
return org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
|
||||||
|
.password(user.getPassword())
|
||||||
|
.authorities(roleArr)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.hanserwei.jwt.utils;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.io.Decoders;
|
||||||
|
import io.jsonwebtoken.io.Encoders;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtTokenHelper implements InitializingBean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签发人
|
||||||
|
*/
|
||||||
|
@Value("${jwt.issuer}")
|
||||||
|
private String issuer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 秘钥 (JJWT 0.12+ 强制要求使用 SecretKey 接口,且长度必须符合算法安全标准)
|
||||||
|
*/
|
||||||
|
private SecretKey key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 解析器 (线程安全,建议复用)
|
||||||
|
*/
|
||||||
|
private JwtParser jwtParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具方法:生成一个符合 HS512 标准的安全 Base64 秘钥
|
||||||
|
* 用于生成后填入 application.yml
|
||||||
|
*/
|
||||||
|
public static String generateBase64Key() {
|
||||||
|
SecretKey secretKey = Jwts.SIG.HS512.key().build();
|
||||||
|
return Encoders.BASE64.encode(secretKey.getEncoded()); // Fixed line
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String key = generateBase64Key();
|
||||||
|
System.out.println("请将此 Key 复制到配置文件中 (HS512需要长Key): " + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Base64 秘钥
|
||||||
|
* 官方建议:HS512 算法至少需要 512 bit (64字节) 的秘钥长度,否则会抛出 WeakKeyException
|
||||||
|
*/
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
public void setBase64Key(String base64Key) {
|
||||||
|
// 使用 JJWT 提供的 Decoders 工具,或者使用 java.util.Base64 均可
|
||||||
|
byte[] keyBytes = Decoders.BASE64.decode(base64Key);
|
||||||
|
this.key = Keys.hmacShaKeyFor(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 JwtParser
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() {
|
||||||
|
// 0.13.0 标准写法:
|
||||||
|
// 1. 使用 parser() 而不是 parserBuilder()
|
||||||
|
// 2. 使用 verifyWith() 设置验签秘钥
|
||||||
|
jwtParser = Jwts.parser()
|
||||||
|
.requireIssuer(issuer)
|
||||||
|
.verifyWith(key)
|
||||||
|
// 允许的时钟偏差 (防止服务器时间不一致导致验证失败),默认单位秒
|
||||||
|
.clockSkewSeconds(10)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Token
|
||||||
|
*/
|
||||||
|
public String generateToken(String username) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant expireTime = now.plus(30, ChronoUnit.DAYS);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.header().add("type", "JWT").and() // 推荐添加 header
|
||||||
|
.subject(username)
|
||||||
|
.issuer(issuer)
|
||||||
|
.issuedAt(Date.from(now))
|
||||||
|
.expiration(Date.from(expireTime))
|
||||||
|
// 0.13.0 标准:使用 Jwts.SIG 中的常量指定算法
|
||||||
|
.signWith(key, Jwts.SIG.HS512)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Token
|
||||||
|
*/
|
||||||
|
public Jws<Claims> parseToken(String token) {
|
||||||
|
try {
|
||||||
|
// 0.13.0 标准:解析已签名的 JWS 使用 parseSignedClaims
|
||||||
|
return jwtParser.parseSignedClaims(token);
|
||||||
|
} catch (MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
|
||||||
|
// 注意:SignatureException 现在属于 io.jsonwebtoken.security 包
|
||||||
|
throw new BadCredentialsException("Token 不可用或签名无效", e);
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
throw new CredentialsExpiredException("Token 已失效", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.hanserwei.jwt.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.hanserwei.common.utils.Response;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
public class ResultUtil {
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse对象
|
||||||
|
* @param result 响应数据
|
||||||
|
* @throws IOException IO异常
|
||||||
|
*/
|
||||||
|
public static void ok(HttpServletResponse response, Response<?> result) throws IOException {
|
||||||
|
writeResponse(response, HttpStatus.OK.value(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败响应
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse对象
|
||||||
|
* @param result 响应数据
|
||||||
|
* @throws IOException IO异常
|
||||||
|
*/
|
||||||
|
public static void fail(HttpServletResponse response, Response<?> result) throws IOException {
|
||||||
|
writeResponse(response, HttpStatus.OK.value(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败响应
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse对象
|
||||||
|
* @param status HTTP状态码
|
||||||
|
* @param result 响应数据
|
||||||
|
* @throws IOException IO异常
|
||||||
|
*/
|
||||||
|
public static void fail(HttpServletResponse response, int status, Response<?> result) throws IOException {
|
||||||
|
writeResponse(response, status, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入响应数据
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse对象
|
||||||
|
* @param status HTTP状态码
|
||||||
|
* @param result 响应数据
|
||||||
|
* @throws IOException IO异常
|
||||||
|
*/
|
||||||
|
private static void writeResponse(HttpServletResponse response, int status, Response<?> result) throws IOException {
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setStatus(status);
|
||||||
|
response.setContentType("application/json");
|
||||||
|
|
||||||
|
try (PrintWriter writer = response.getWriter()) {
|
||||||
|
writer.write(objectMapper.writeValueAsString(result));
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ dependencies {
|
|||||||
// Spring Boot Web(示例)
|
// Spring Boot Web(示例)
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.hanserwei.web;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(scanBasePackages = "com.hanserwei")
|
||||||
@ComponentScan({"com.hanserwei.*"})
|
@EnableJpaRepositories(basePackages = "com.hanserwei.common.domain.repository")
|
||||||
|
@EntityScan(basePackages = "com.hanserwei.common.domain.dataobject")
|
||||||
public class WeblogWebApplication {
|
public class WeblogWebApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(WeblogWebApplication.class, args);
|
SpringApplication.run(WeblogWebApplication.class, args);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user