Compare commits

...
100 Commits
Author SHA1 Message Date
timi 9555474b0b Merge pull request 'v1.0.19' (#28) from dev into master
Reviewed-on: #28
2026-08-21 05:52:36 +00:00
Timi 022185079c v1.0.19
CI / build-deploy (pull_request) Successful in 28s
CI / notify-on-failure (pull_request) Skipped
2026-08-21 13:52:05 +08:00
Timi acc3f05f12 support pinyin search 2026-08-21 13:51:26 +08:00
timi a150f9e017 Merge pull request 'v1.0.18' (#27) from dev into master
Reviewed-on: #27
2026-08-20 17:40:22 +00:00
Timi a523df6a07 v1.0.18
CI / build-deploy (pull_request) Successful in 39s
CI / notify-on-failure (pull_request) Skipped
2026-08-21 01:39:14 +08:00
Timi 31c583eeb5 add internal notify 2026-08-21 01:38:51 +08:00
Timi e3ef3e5275 implement notify 2026-08-20 23:14:54 +08:00
Timi 84190b9773 point tier rule allow greater than 0 for min 2026-08-20 16:09:46 +08:00
Timi 7808d22775 support tag for customer create 2026-08-20 14:57:29 +08:00
Timi fba559cd22 fix customer delete 2026-08-20 11:19:24 +08:00
timi ecd2fa1c33 Merge pull request 'v1.0.17' (#26) from dev into master
Reviewed-on: #26
2026-08-19 17:30:35 +00:00
Timi dcc75efa87 v1.0.17
CI / build-deploy (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Skipped
2026-08-20 01:29:48 +08:00
Timi a9465299a9 add feedback 2026-08-20 00:22:19 +08:00
Timi 42152c7b79 fix event record DAILY_HALF_DAY 2026-08-19 17:56:05 +08:00
Timi 1a896ef457 add point rule sort 2026-08-19 16:12:23 +08:00
Timi 9d8990a028 add permission list and role list for user detail 2026-08-19 10:38:36 +08:00
Timi 17d88fc338 add event value chart 2026-08-18 17:26:56 +08:00
Timi bb40f347a3 support consume point 2026-08-18 16:46:30 +08:00
timi e3236d262d Merge pull request 'v1.0.16' (#25) from dev into master
Reviewed-on: #25
2026-08-16 03:37:08 +00:00
Timi 3d4e32e5da v1.0.16
CI / build-deploy (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Skipped
2026-08-16 11:36:02 +08:00
timi c8e2e16ba5 Merge pull request 'v1.0.15' (#24) from dev into master
Reviewed-on: #24
2026-08-15 17:49:51 +00:00
Timi bdc4cbb240 v1.0.15
CI / build-deploy (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Skipped
2026-08-16 01:49:18 +08:00
timi 99b44faf09 Merge pull request 'v1.0.15' (#23) from dev into master
Reviewed-on: #23
2026-08-15 17:45:26 +00:00
Timi 7f6cebd4e2 v1.0.15
CI / build-deploy (pull_request) Failing after 6s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-16 01:44:45 +08:00
Timi 75fd4bed18 fix event save fail for limit == 1 2026-08-15 15:01:08 +08:00
timi b3e3cbf84b Merge pull request 'v1.0.14' (#22) from dev into master
Reviewed-on: #22
2026-08-14 17:31:11 +00:00
Timi 8db6f962c9 v1.0.14
CI / build-deploy (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Skipped
2026-08-15 01:30:51 +08:00
timi 0aee11d824 Merge pull request 'v1.0.14' (#21) from dev into master
Reviewed-on: #21
2026-08-14 17:26:10 +00:00
Timi e35cf3aa59 1.0.14
CI / build-deploy (pull_request) Failing after 10s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-15 01:25:08 +08:00
Timi 457e716b38 add totalNumberValue for rank 2026-08-14 18:58:04 +08:00
Timi ab4a164680 return NumberValueTotal for list 2026-08-14 18:44:53 +08:00
Timi 9dfca25f8c update point tier rule 2026-08-14 18:26:38 +08:00
Timi e369a38e2e add Gao user invite 2026-08-14 11:57:10 +08:00
Timi f1e10df6a7 rename Gao GlobalManager role name 2026-08-14 11:31:25 +08:00
timi fcf358a768 Merge pull request 'v1.0.13' (#20) from dev into master
Reviewed-on: #20
2026-08-13 09:52:26 +00:00
Timi 36e2a37af2 v1.0.13
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Skipped
2026-08-13 17:52:03 +08:00
Timi 348f7396a1 add point log 2026-08-13 14:04:07 +08:00
timi 619369104c Merge pull request 'v1.0.12' (#19) from dev into master
Reviewed-on: #19
2026-08-12 17:02:43 +00:00
Timi c0b04fc554 v1.0.12
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-13 01:02:22 +08:00
Timi 5e5748b521 add customer in gao point ledger list api 2026-08-13 00:46:32 +08:00
Timi 67d79f7527 fix gao event rank 2026-08-13 00:24:39 +08:00
Timi ee530d2a8c refactor mapper xml 2026-08-12 16:11:08 +08:00
Timi 5f36844279 update GaoStore update 2026-08-12 12:10:21 +08:00
Timi 5b9391a50a add GaoStore.autoClosedDay 2026-08-12 12:03:30 +08:00
Timi 6bc73414d9 remove @RequireGaoRole 2026-08-12 11:06:48 +08:00
timi 4f1408b9d0 Merge pull request 'v1.0.11' (#18) from dev into master
Reviewed-on: #18
2026-08-11 16:49:08 +00:00
Timi d555aad9ac v1.0.11
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-12 00:48:54 +08:00
timi 029eea70cc Merge pull request 'v1.0.10' (#17) from dev into master
Reviewed-on: #17
2026-08-11 16:45:36 +00:00
Timi 2f42204794 v1.0.10
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-12 00:42:35 +08:00
timi 22b1e65385 Merge pull request 'v1.0.9' (#16) from dev into master
Reviewed-on: #16
2026-08-11 16:01:10 +00:00
Timi 72bdb8d49b v1.0.9
CI / build-deploy (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-11 23:58:00 +08:00
Timi 5ec6bc472f add chart api 2026-08-11 23:58:00 +08:00
Timi 81b7356d67 nullable point account detail api 2026-08-11 23:58:00 +08:00
Timi 44af16f451 fix new permission D authorize fail 2026-08-11 23:57:59 +08:00
Timi c6157db0de add gao point 2026-08-11 23:57:59 +08:00
Timi 39ac73dc66 refactor permission system 2026-08-11 23:57:59 +08:00
Timi 6e43ff349d fix gao store id 2026-08-07 19:10:25 +08:00
Timi ab79391cdc fix user login ip and time 2026-08-07 15:28:16 +08:00
Timi 67bc9382c5 add store sort api 2026-08-07 11:49:17 +08:00
Timi 7e40e0202e add store 2026-08-07 11:37:54 +08:00
Timi 7ffdd715ee refactor gao module 2026-08-06 13:16:52 +08:00
Timi 1709316ce1 refactor gao module 2026-08-06 10:52:50 +08:00
timi 2170c48763 Merge pull request 'v1.0.8' (#15) from dev into master
Reviewed-on: #15
2026-08-03 16:36:19 +00:00
Timi 568563475e v1.0.8
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-04 00:35:57 +08:00
Timi 8a92e46eb1 unlimit attach read rate 2026-08-03 23:46:12 +08:00
Timi 1875cec8a5 add registration event rank 2026-08-03 23:45:56 +08:00
timi c2d1c48aae Merge pull request 'v0.0.18' (#14) from dev into master
Reviewed-on: #14
2026-08-03 11:59:20 +00:00
Timi 4c162dad34 v0.0.18
CI / build-deploy (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-03 19:59:08 +08:00
timi e14958c643 Merge pull request 'v0.0.18' (#13) from dev into master
Reviewed-on: #13
2026-08-03 11:58:16 +00:00
Timi c49686c411 v0.0.18
CI / build-deploy (pull_request) Failing after 8s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-03 19:57:44 +08:00
Timi 65161a7fad fix customer update 2026-08-03 19:21:12 +08:00
Timi 187f270866 add customer lunar birthdate 2026-08-03 18:54:17 +08:00
Timi 50d4bba045 batch customer event 2026-08-03 17:30:33 +08:00
Timi 6eedc06a07 update GAO logger 2026-08-03 15:34:01 +08:00
Timi b55b43eba4 add customer/event record export permission 2026-08-03 13:00:40 +08:00
Timi 20289a3168 add GAO logger 2026-08-03 12:22:48 +08:00
Timi d45bf11477 add GaoRegistrationEvent status and valid date range 2026-08-03 11:41:33 +08:00
Timi 7370b2fc98 rename registration event limit type 2026-08-03 10:48:43 +08:00
timi da288338cd Merge pull request 'v1.0.7' (#12) from dev into master
Reviewed-on: #12
2026-08-01 09:34:18 +00:00
Timi 2f366fe219 v1.0.7
CI / build-deploy (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 17:34:06 +08:00
timi ea430f2468 Merge pull request 'v1.0.6' (#11) from dev into master
Reviewed-on: #11
2026-08-01 09:03:48 +00:00
Timi f1715a3e32 v1.0.6
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 17:03:28 +08:00
timi f76f79744a Merge pull request 'v1.0.5' (#10) from dev into master
Reviewed-on: #10
2026-08-01 08:52:52 +00:00
Timi 833a303bb6 v1.0.5
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 16:51:53 +08:00
timi 19daaff907 Merge pull request 'v1.0.4' (#9) from dev into master
Reviewed-on: #9
2026-08-01 03:13:00 +00:00
Timi 1658be7d8d v1.0.4
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 11:11:53 +08:00
timi db94d8f932 Merge pull request 'v1.0.3' (#8) from dev into master
Reviewed-on: #8
2026-07-31 17:19:48 +00:00
Timi 6c537852a0 v1.0.3
CI / build-deploy (pull_request) Successful in 1m8s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 01:19:21 +08:00
timi 53618fe362 Merge pull request 'v1.0.3' (#7) from dev into master
Reviewed-on: #7
2026-07-31 17:13:37 +00:00
Timi 15bf9bd688 v1.0.3
CI / build-deploy (pull_request) Failing after 2m52s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-01 01:13:08 +08:00
Timi ce22661a9b v1.0.3 2026-07-31 18:36:37 +08:00
Timi 22e415dd7b Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	pom.xml
#	src/main/java/com/imyeyu/api/annotation/EnableSetting.java
#	src/main/java/com/imyeyu/api/annotation/EnableSettingInterceptor.java
#	src/main/java/com/imyeyu/api/modules/blog/util/UserToken.java
#	src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java
#	src/main/java/com/imyeyu/api/modules/common/controller/CommentController.java
#	src/main/java/com/imyeyu/api/modules/common/controller/CommonController.java
#	src/main/java/com/imyeyu/api/modules/common/controller/UserController.java
#	src/main/java/com/imyeyu/api/modules/common/entity/Setting.java
#	src/main/java/com/imyeyu/api/modules/common/mapper/SettingMapper.java
#	src/main/java/com/imyeyu/api/modules/common/service/SettingService.java
#	src/main/java/com/imyeyu/api/modules/common/service/implement/MultilingualServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/common/service/implement/SettingServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/common/task/MultilingualTranslateTask.java
#	src/main/java/com/imyeyu/api/modules/git/bean/gitea/GiteaAPI.java
#	src/main/java/com/imyeyu/api/modules/git/controller/DeveloperController.java
#	src/main/java/com/imyeyu/api/modules/git/service/implement/RepositoryServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/journal/controller/JournalController.java
#	src/main/java/com/imyeyu/api/modules/journal/controller/ToolController.java
#	src/main/java/com/imyeyu/api/modules/journal/util/JournalAPIInterceptor.java
#	src/main/java/com/imyeyu/api/modules/minecraft/annotation/RequiredFMCServerTokenInterceptor.java
#	src/main/java/com/imyeyu/api/modules/minecraft/controller/MinecraftController.java
#	src/main/java/com/imyeyu/api/modules/minecraft/controller/PlayerController.java
#	src/main/java/com/imyeyu/api/modules/minecraft/service/implement/PlayerServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/music/runner/ControllerBootstrapRunner.java
#	src/main/java/com/imyeyu/api/modules/music/runner/PlayerBootstrapRunner.java
#	src/main/java/com/imyeyu/api/modules/system/bean/ServerFile.java
#	src/main/java/com/imyeyu/api/modules/system/controller/FileController.java
#	src/main/java/com/imyeyu/api/modules/system/controller/SystemController.java
#	src/main/java/com/imyeyu/api/modules/system/service/implement/FileServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/system/service/implement/TerminalServiceImplement.java
#	src/main/java/com/imyeyu/api/modules/system/task/ServerStatusTask.java
#	src/main/java/com/imyeyu/api/modules/system/task/TerminalTask.java
#	src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java
#	src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java
#	src/main/java/com/imyeyu/api/modules/system/util/SystemAPIInterceptor.java
#	src/main/java/com/imyeyu/api/util/InitApplication.java
2026-07-31 15:18:12 +08:00
Timi 844c5381c4 v1.0.3 2026-07-31 15:16:55 +08:00
Timi e45809a0db refactor all 2026-07-31 01:03:53 +08:00
timi 523edffc4e Merge pull request 'v1.0.2' (#6) from dev into master
Reviewed-on: #6
2026-04-13 10:28:11 +00:00
timi c7ddb1a8b0 Merge pull request 'v1.0.1' (#5) from dev into master
Reviewed-on: #5
2026-04-12 16:17:02 +00:00
timi 45c9fc814a Merge pull request 'v1.0.0' (#4) from dev into master
Reviewed-on: #4
2026-04-09 05:16:04 +00:00
timi 407dc13ac4 Merge pull request 'v1.0.0' (#3) from dev into master
Reviewed-on: #3
2026-04-09 04:09:16 +00:00
timi 0c06bf16c2 Merge pull request 'v1.0.0' (#2) from dev into master
Reviewed-on: #2
2026-04-09 04:03:43 +00:00
timi 1db39e77d3 Merge pull request 'v1.0.0' (#1) from dev into master
Reviewed-on: #1
2026-04-08 08:48:12 +00:00
633 changed files with 24319 additions and 13180 deletions
+6 -1
View File
@@ -14,7 +14,7 @@ jobs:
outputs: outputs:
deployment_status: ${{ steps.set_status.outputs.status }} deployment_status: ${{ steps.set_status.outputs.status }}
env: env:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk JAVA_HOME: /usr/lib/jvm/java-25-openjdk
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -46,6 +46,11 @@ jobs:
<username>${{ vars.TIMI_NEXUS_USERNAME }}</username> <username>${{ vars.TIMI_NEXUS_USERNAME }}</username>
<password>${{ vars.TIMI_NEXUS_PASSWORD }}</password> <password>${{ vars.TIMI_NEXUS_PASSWORD }}</password>
</server> </server>
<server>
<id>e2ins_nexus</id>
<username>${{ vars.E2INS_NEXUS_USERNAME }}</username>
<password>${{ vars.E2INS_NEXUS_PASSWORD }}</password>
</server>
</servers> </servers>
</settings> </settings>
EOF EOF
-3
View File
@@ -1,11 +1,8 @@
/config /config
/data /data
/docs
/logs /logs
/target /target
/temp /temp
/.claude
/CLAUDE.md
/AGENTS.md /AGENTS.md
multilingualField/ multilingualField/
+3
View File
@@ -1,5 +1,8 @@
# Default ignored files # Default ignored files
/shelf/ /shelf/
/workspace.xml /workspace.xml
/httpRequests
/dataSources
/dataSources.local.xml
CopilotChatHistory.xml CopilotChatHistory.xml
developer-tools.xml developer-tools.xml
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="vm.imyeyu.develop" uuid="05742bdc-a11a-4147-9168-ef976a70258a">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://vm.imyeyu.develop:3306/timi_server</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>
+12
View File
@@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="customHeaders">
<set>
<option value="Token" />
</set>
</option>
</inspection_tool>
</profile>
</component>
+35 -29
View File
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager"> <component name="MavenProjectsManager">
@@ -10,49 +11,54 @@
<component name="NullableNotNullManager"> <component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" /> <option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="jakarta.validation.constraints.NotNull" /> <option name="myDefaultNotNull" value="jakarta.validation.constraints.NotNull" />
<option name="myOrdered" value="true" />
<option name="myNullables"> <option name="myNullables">
<value> <value>
<list size="14"> <list size="16">
<item index="0" class="java.lang.String" itemvalue="com.android.annotations.Nullable" /> <item index="0" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jspecify.nullness.Nullable" /> <item index="1" class="java.lang.String" itemvalue="org.jspecify.nullness.Nullable" />
<item index="2" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" /> <item index="2" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" /> <item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" /> <item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" /> <item index="5" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" /> <item index="6" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="7" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" /> <item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="android.support.annotation.Nullable" /> <item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" /> <item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="10" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" /> <item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="11" class="java.lang.String" itemvalue="javax.annotation.Nullable" /> <item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="12" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" /> <item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" /> <item index="13" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="14" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
</list> </list>
</value> </value>
</option> </option>
<option name="myNotNulls"> <option name="myNotNulls">
<value> <value>
<list size="15"> <list size="17">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" /> <item index="0" class="java.lang.String" itemvalue="jakarta.validation.constraints.NotNull" />
<item index="1" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" /> <item index="1" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="2" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" /> <item index="2" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" /> <item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" /> <item index="4" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" />
<item index="5" class="java.lang.String" itemvalue="lombok.NonNull" /> <item index="5" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="6" class="java.lang.String" itemvalue="android.support.annotation.NonNull" /> <item index="6" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="7" class="java.lang.String" itemvalue="jakarta.validation.constraints.NotNull" /> <item index="7" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="8" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" /> <item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="9" class="java.lang.String" itemvalue="com.android.annotations.NonNull" /> <item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="10" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" /> <item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" /> <item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="12" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" /> <item index="12" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nonnull" /> <item index="13" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="14" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" /> <item index="14" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="15" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="16" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
</list> </list>
</value> </value>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="openjdk-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="temurin-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V1__init_schema.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V2__init_system_data.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml" dialect="MariaDB" />
<file url="PROJECT" dialect="MariaDB" />
</component>
</project>
+2
View File
@@ -0,0 +1,2 @@
/cache
/project.local.yml
+1
View File
@@ -0,0 +1 @@
TimiServerAPI 是一个基于 Spring Boot 的高耦合业务后端服务,代码位于 src/main/java/com/imyeyu/api,下分多个业务模块,如 blog、common、gitea、forevermc、journal、minecraft、music、system 等。项目使用多数据源 MySQLtimiserver、gitea、forevermc)、MongoDB、Redis、MyBatis,并引入 Flyway 做数据库迁移。运行环境为 Windows,JDK 21。
+1
View File
@@ -0,0 +1 @@
项目约定:全部沟通、注释、文档使用中文;Java 使用 tab 缩进;注释要求符合 javadoc;优先使用官方 API,不随意引入外部依赖;字符串优先用 formatted(),长动态字符串用 StringBuilder;优先用代码级工具做精确修改;避免盲目通读大文件,先缩小目录和符号范围。
+1
View File
@@ -0,0 +1 @@
Windows / PowerShell 常用命令:`Get-ChildItem -Force` 查看目录,`rg --files` 列文件,`rg -n "PATTERN" PATH` 搜索文本,`mvn -q -DskipTests compile` 做基础编译验证,`git diff -- PATH` 查看改动。配置文件主要在 `src/main/resources`Java 源码在 `src/main/java`
+1
View File
@@ -0,0 +1 @@
完成任务后,优先做最小范围验证。Java 侧先尝试 `mvn -q -DskipTests compile`;如果失败,区分是本次改动导致还是项目已有编译问题。不要回滚用户已有改动,不要做破坏性 git 操作。多数据源场景要特别检查 `@Primary``@Qualifier` 和自动装配框架(如 Flyway)的绑定关系。
+154
View File
@@ -0,0 +1,154 @@
# the name by which the project can be referenced within Serena
project_name: "TimiServerAPI"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# haxe java julia kotlin lua
# markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- java
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project based on the project name or path.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
# for example by saying that the information retrieved from a memory file is no longer correct
# or no longer relevant for the project.
# * `edit_memory`: Replaces content matching a regular expression in a memory.
# * `execute_shell_command`: Executes a shell command.
# * `find_file`: Finds files in the given relative paths
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
# * `find_symbol`: Performs a global (or local) search using the language server backend.
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
# for clients that do not read the initial instructions when the MCP server is connected.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
# is relevant to the current task. You can infer whether the information
# is relevant from the memory file name.
# You should not read the same memory file multiple times in the same conversation.
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
# For JB, we use a separate tool.
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
# * `safe_delete_symbol`:
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
# The memory name should be meaningful.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
+163
View File
@@ -0,0 +1,163 @@
# HTTP 调试与文档规范
本目录下的 `.http` 文件同时承担接口调试和接口文档职责,也要保证 AIAgent 能稳定读取
## 目录分工
### `.http`
`.http` 只描述接口级信息:
1. 接口中文名
2. 权限要求
3. 前置条件
4. 请求参数对应的对象说明
5. 返回参数对应的对象说明
`.http` 不重复展开对象字段明细,复杂字段、嵌套结构、枚举值统一写到 `./object`
### `./object`
`./object` 只描述对象级信息:
1. 字段名
2. 字段类型
3. 字段说明
4. 枚举值及含义
所有模块都可能复用的通用对象必须写在 `./object/common.md`
按业务领域拆分对象说明文件,例如:
1. `./object/common.md`
2. `./object/user.md`
3. `./object/role.md`
4. `./object/permission.md`
不要把同一个对象的字段说明散落到多个文件里
## 全局约定
### 返回包装
除使用 `IgnoreGlobalReturn` 跳过全局包装的方法外,接口默认都会经过
[GlobalReturnHandler](E:/IDEAProject/timi-spring/src/main/java/com/imyeyu/spring/util/GlobalReturnHandler.java)
统一包装返回
因此 `.http` 里的 `返回参数` 默认只描述业务 `data` 对象,不再重复赘述全局包装结构
全局返回包装、通用成功码、错误信息、常见分页壳子等内容统一维护在 `./object/common.md`
如果接口使用
[IgnoreGlobalReturn](E:/IDEAProject/timi-spring/src/main/java/com/imyeyu/spring/annotation/IgnoreGlobalReturn.java)
跳过全局包装,必须在对应接口的 `前置条件` 或补充说明里明确写出“该接口不走全局返回包装”
### 文档真实性
1. 文档只记录服务端真实行为
2. 不写猜测
3. 不写“看代码”
4. 接口改了就同步更新 `.http``./object`
### 变量复用
能复用的变量统一放在文件顶部,例如 `@user_token``@userId`
## `.http` 模板
每个接口块固定使用下面格式,不要擅自增删字段名:
```http
### <>
###
###
###
###
POST {{baseUrl}}/path
Content-Type: application/json
Token: {{user_token}}
{}
```
## `.http` 填写要求
### 接口中文名
直接写接口用途,别写成控制器方法名
### 权限
1. 优先直接翻译控制器上的真实注解
2. 同时存在角色和权限时一起写,例如 `需要 ADMIN 角色 + USER_UPDATE 权限`
3. 只有 Token 要求时写 `需要有效 Token`
4. 无需登录时直接写 `无需登录`
### 前置条件
这里只写接口调用前必须知道的约束,例如:
1. 需要先登录并回填 `user_token`
2. 需要先创建用户再传入 `userId`
3. 重复调用可能因为唯一约束失败
4. 接口当前实现为空
5. 该接口不走全局返回包装
没有额外限制时写 `无`
### 请求参数
只写请求对象名和对象说明文件,不在 `.http` 内展开字段
推荐格式:
1. `无`
2. `UserCreateRequest,详见 ./object/user.md`
3. `Page<UserQuery>,详见 ./object/common.md 和 ./object/user.md`
### 返回参数
只写业务返回对象名和对象说明文件,不在 `.http` 内展开字段
推荐格式:
1. `无`
2. `User,详见 ./object/user.md`
3. `PageResult<User>,详见 ./object/common.md 和 ./object/user.md`
4. `二进制数据流,该接口不走全局返回包装`
## `./object` 编写要求
对象说明统一使用 Markdown 表格,推荐结构如下
```md
## User
| 字段 | 类型 | 说明 |
|---|---|---|
| name | String | 登录名,系统内唯一 |
## RoleCode
| 枚举 | 说明 |
|---|---|
| SYSTEM | 系统 |
| ADMIN | 管理员 |
```
### 对象说明规则
1. 一个对象一个标题,标题名必须和 `.http` 里引用的对象名一致
2. 字段说明只写真实存在且调用方需要关心的内容
3. 枚举单独列出,不要把枚举值混进普通字段表
4. 嵌套对象、列表元素对象也要有独立标题
5. 通用壳子对象统一写在 `./object/common.md`
## 维护要求
1. 一个控制器通常对应一个 `.http` 文件
2. 新增接口时,同步补齐对应 `.http`
3. 新增或修改对象字段时,同步更新对应 `./object/*.md`
4. 注释语言统一用中文
5. 示例请求优先保证可调试,其次才是看起来完整
+200
View File
@@ -0,0 +1,200 @@
### AttachmentController 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@attachmentId =
@attachmentBizType = TEMP_FILE
@attachmentBizId = demo-biz-id
@attachmentAttachType = SOURCE
@attachmentName = demo.png
@attachmentFilePath = E:/Temp/demo.png
@attachmentTempFileId =
### 查询附件详情
### 前置条件:`id` 必须传有效附件 ID
### 请求参数:URL 参数 `id` 为附件 ID
### 返回参数:Attachment,详见 ./object/common.md
POST {{baseUrl}}/attach/detail?id={{attachmentId}}
Token: {{user_token}}
### 创建附件
### 前置条件:回填 `attachmentFilePath` 为本地文件绝对路径
### 请求参数:multipart/form-data
### 返回参数:Attachment,详见 ./object/common.md
POST {{baseUrl}}/attach/create
Content-Type: multipart/form-data; boundary=AttachmentCreateBoundary
Token: {{user_token}}
--AttachmentCreateBoundary
Content-Disposition: form-data; name="bizType"
{{attachmentBizType}}
--AttachmentCreateBoundary
Content-Disposition: form-data; name="bizId"
{{attachmentBizId}}
--AttachmentCreateBoundary
Content-Disposition: form-data; name="attachType"
{{attachmentAttachType}}
--AttachmentCreateBoundary
Content-Disposition: form-data; name="name"
{{attachmentName}}
--AttachmentCreateBoundary
Content-Disposition: form-data; name="file"; filename="demo.png"
Content-Type: application/octet-stream
< {{attachmentFilePath}}
--AttachmentCreateBoundary--
### 更新附件元数据
### 前置条件:`id` 必须传有效附件 ID
### 请求参数:multipart/form-data
### 返回参数:无
POST {{baseUrl}}/attach/update
Content-Type: multipart/form-data; boundary=AttachmentUpdateMetaBoundary
Token: {{user_token}}
--AttachmentUpdateMetaBoundary
Content-Disposition: form-data; name="id"
{{attachmentId}}
--AttachmentUpdateMetaBoundary
Content-Disposition: form-data; name="name"
{{attachmentName}}-updated
--AttachmentUpdateMetaBoundary
Content-Disposition: form-data; name="attachType"
{{attachmentAttachType}}
--AttachmentUpdateMetaBoundary--
### 更新附件文件
### 前置条件:`id` 必须传有效附件 ID`attachmentFilePath` 必须传本地文件绝对路径
### 请求参数:multipart/form-data
### 返回参数:无
POST {{baseUrl}}/attach/update
Content-Type: multipart/form-data; boundary=AttachmentUpdateFileBoundary
Token: {{user_token}}
--AttachmentUpdateFileBoundary
Content-Disposition: form-data; name="id"
{{attachmentId}}
--AttachmentUpdateFileBoundary
Content-Disposition: form-data; name="name"
{{attachmentName}}-file-updated
--AttachmentUpdateFileBoundary
Content-Disposition: form-data; name="file"; filename="demo.png"
Content-Type: application/octet-stream
< {{attachmentFilePath}}
--AttachmentUpdateFileBoundary--
### 删除附件
### 前置条件:`id` 必须传有效附件 ID
### 请求参数:URL 参数 `id` 为附件 ID
### 返回参数:无
POST {{baseUrl}}/attach/delete?id={{attachmentId}}
Token: {{user_token}}
### 按业务查询单个附件
### 前置条件:`bizType` 和 `bizId` 必须传有效值
### 请求参数:URL 参数 `bizType`、`bizId`
### 返回参数:Attachment,详见 ./object/common.md
GET {{baseUrl}}/attach/detail/byBiz?bizType={{attachmentBizType}}&bizId={{attachmentBizId}}
Token: {{user_token}}
### 按业务和附件类型查询单个附件
### 前置条件:`bizType`、`bizId`、`attachType` 必须传有效值
### 请求参数:URL 参数 `bizType`、`bizId`、`attachType`
### 返回参数:Attachment,详见 ./object/common.md
GET {{baseUrl}}/attach/detail/byAttachType?bizType={{attachmentBizType}}&bizId={{attachmentBizId}}&attachType={{attachmentAttachType}}
Token: {{user_token}}
### 按业务查询附件列表
### 前置条件:`bizType` 和 `bizId` 必须传有效值
### 请求参数:URL 参数 `bizType`、`bizId`,可重复传 `attachTypeList`
### 返回参数:Attachment[]
GET {{baseUrl}}/attach/list/byBiz?bizType={{attachmentBizType}}&bizId={{attachmentBizId}}&attachTypeList={{attachmentAttachType}}
Token: {{user_token}}
### 按业务统计附件数量
### 前置条件:`bizType` 和 `bizId` 必须传有效值
### 请求参数:URL 参数 `bizType`、`bizId`,可重复传 `attachTypeList`
### 返回参数:number
GET {{baseUrl}}/attach/count/byBiz?bizType={{attachmentBizType}}&bizId={{attachmentBizId}}&attachTypeList={{attachmentAttachType}}
Token: {{user_token}}
### 按业务分页查询附件
### 前置条件:`bizType` 和 `bizId` 必须传有效值
### 请求参数:URL 参数 `bizType`、`bizId`,可重复传 `attachTypeList`,请求体为 Page
### 返回参数:PageResult<Attachment>
POST {{baseUrl}}/attach/page/byBiz?bizType={{attachmentBizType}}&bizId={{attachmentBizId}}&attachTypeList={{attachmentAttachType}}
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 10
}
### 按业务差分更新附件
### 前置条件:新增项需要传有效 `tempFileId`
### 请求参数:BizUpdateReq
### 返回参数:无
POST {{baseUrl}}/attach/update/byBiz
Content-Type: application/json
Token: {{user_token}}
{
"bizType": "{{attachmentBizType}}",
"bizId": "{{attachmentBizId}}",
"items": [
{
"id": "{{attachmentId}}",
"attachType": "{{attachmentAttachType}}",
"name": "{{attachmentName}}-biz-updated"
},
{
"tempFileId": "{{attachmentTempFileId}}",
"attachType": "{{attachmentAttachType}}",
"name": "{{attachmentName}}-from-temp"
}
]
}
### 按业务删除全部附件
### 前置条件:`bizType` 和 `bizId` 必须传有效值
### 请求参数:BizDeleteReq
### 返回参数:无
POST {{baseUrl}}/attach/delete/byBiz
Content-Type: application/json
Token: {{user_token}}
{
"bizType": "{{attachmentBizType}}",
"bizId": "{{attachmentBizId}}"
}
### 读取附件原图
### 前置条件:`attachmentId` 必须传有效附件 ID
### 请求参数:路径参数 `id`
### 返回参数:附件二进制流
GET {{baseUrl}}/attach/read/{{attachmentId}}
Token: {{user_token}}
### 读取附件缩略图
### 前置条件:`attachmentId` 必须传有效图片或视频附件 ID
### 请求参数:路径参数 `id`URL 参数 `thumbWidth`、`thumbHeight`
### 返回参数:附件二进制流
GET {{baseUrl}}/attach/read/{{attachmentId}}?thumbWidth=200&thumbHeight=200
Token: {{user_token}}
### 下载附件
### 前置条件:`attachmentId` 必须传有效附件 ID
### 请求参数:路径参数 `id`
### 返回参数:附件二进制流
GET {{baseUrl}}/attach/download/{{attachmentId}}
Token: {{user_token}}
+66
View File
@@ -0,0 +1,66 @@
### MultilingualController 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@multilingualId =
@multilingualZhCN = 测试简体中文
@multilingualZhTW = 測試繁體中文
@multilingualEnUS = Test English
@multilingualRuRU = Тест
@multilingualJaJP = テスト
@multilingualDeDE = Test Deutsch
@multilingualKoKR = 테스트
### 查询多语言详情
### 权限:需要 ADMIN 角色 + MULTILINGUAL_READ 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效多语言 ID
### 请求参数:无请求体,URL 参数:`id` 为多语言 ID
### 返回参数:Multilingual,详见 ./object/common.md
POST {{baseUrl}}/system/multilingual/detail?id={{multilingualId}}
Token: {{user_token}}
### 创建多语言
### 权限:需要 ADMIN 角色 + MULTILINGUAL_CREATE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Multilingual,详见 ./object/common.md
### 返回参数:Multilingual,详见 ./object/common.md
POST {{baseUrl}}/system/multilingual/create
Content-Type: application/json
Token: {{user_token}}
{
"zhCN": "{{multilingualZhCN}}",
"zhTW": "{{multilingualZhTW}}",
"enUS": "{{multilingualEnUS}}",
"ruRU": "{{multilingualRuRU}}",
"jaJP": "{{multilingualJaJP}}",
"deDE": "{{multilingualDeDE}}",
"koKR": "{{multilingualKoKR}}"
}
### 更新多语言
### 权限:需要 ADMIN 角色 + MULTILINGUAL_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token``id` 必须存在
### 请求参数:Multilingual,详见 ./object/common.md
### 返回参数:无
POST {{baseUrl}}/system/multilingual/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{multilingualId}}",
"zhCN": "{{multilingualZhCN}}-已更新",
"zhTW": "{{multilingualZhTW}}-已更新",
"enUS": "{{multilingualEnUS}}-updated",
"ruRU": "{{multilingualRuRU}}-updated",
"jaJP": "{{multilingualJaJP}}-updated",
"deDE": "{{multilingualDeDE}}-updated",
"koKR": "{{multilingualKoKR}}-updated"
}
### 删除多语言
### 权限:需要 ADMIN 角色 + MULTILINGUAL_DELETE 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效多语言 ID
### 请求参数:无请求体,URL 参数:`id` 为多语言 ID
### 返回参数:无
POST {{baseUrl}}/system/multilingual/delete?id={{multilingualId}}
Token: {{user_token}}
+60
View File
@@ -0,0 +1,60 @@
### GaoRegisterAction 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@actionId =
### 分页查询登记行为
POST {{baseUrl}}/gao/action/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 16
}
### 查询启用行为
POST {{baseUrl}}/gao/action/enabled/list
Token: {{user_token}}
### 查询行为详情
POST {{baseUrl}}/gao/action/detail?id={{actionId}}
Token: {{user_token}}
### 创建登记行为
POST {{baseUrl}}/gao/action/create
Content-Type: application/json
Token: {{user_token}}
{
"name": "签到",
"description": "客户到店签到",
"enabled": true,
"requirePhoto": false,
"sort": 10,
"permissionCodeList": [
"RECORD:CREATE"
]
}
### 更新登记行为
POST {{baseUrl}}/gao/action/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{actionId}}",
"name": "物品领取",
"description": "登记客户领取物品",
"enabled": true,
"requirePhoto": true,
"sort": 20,
"permissionCodeList": [
"RECORD:CREATE",
"STAT:READ"
]
}
### 删除登记行为
POST {{baseUrl}}/gao/action/delete?id={{actionId}}
Token: {{user_token}}
+119
View File
@@ -0,0 +1,119 @@
### GaoCustomer 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@customerId =
@introducerCustomerId =
@customerCode = GAO-0001
### 分页查询客户
POST {{baseUrl}}/gao/customer/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 16
}
### 搜索客户
POST {{baseUrl}}/gao/customer/search
Content-Type: application/json
Token: {{user_token}}
{
"keyword": "张",
"introducerCustomerId": "{{introducerCustomerId}}"
}
### 查询客户详情
POST {{baseUrl}}/gao/customer/detail?id={{customerId}}
Token: {{user_token}}
### 按客户编码查询
POST {{baseUrl}}/gao/customer/detail/code?code={{customerCode}}
Token: {{user_token}}
### 查询某客户邀请了哪些人
POST {{baseUrl}}/gao/customer/invited/list?introducerCustomerId={{customerId}}
Token: {{user_token}}
### 创建客户
POST {{baseUrl}}/gao/customer/create
Content-Type: application/json
Token: {{user_token}}
{
"code": "{{customerCode}}",
"name": "张三",
"age": 56,
"telephone": "020-88886666",
"address": "广州市天河区某街道",
"remark": "首诊客户",
"disease": "肩颈不适",
"conditioningPart": [
{
"code": "neck",
"name": "颈部"
},
{
"code": "shoulder",
"name": "肩部"
}
],
"introducerCustomerId": "{{introducerCustomerId}}"
}
### 更新客户
POST {{baseUrl}}/gao/customer/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{customerId}}",
"code": "{{customerCode}}",
"name": "张三",
"age": 57,
"telephone": "020-88886667",
"address": "广州市天河区某街道 88 号",
"remark": "复诊客户",
"disease": "腰背调理",
"conditioningPart": {
"parts": [
"waist",
"back"
]
},
"introducerCustomerId": "{{introducerCustomerId}}"
}
### 删除客户
POST {{baseUrl}}/gao/customer/delete?id={{customerId}}
Token: {{user_token}}
### 创建客户邀请关系
POST {{baseUrl}}/gao/customer/invite/create
Content-Type: application/json
Token: {{user_token}}
{
"customerId": "{{customerId}}",
"introducerCustomerId": "{{introducerCustomerId}}"
}
### 查询客户邀请关系详情
POST {{baseUrl}}/gao/customer/invite/detail?customerId={{customerId}}
Token: {{user_token}}
### 更新客户邀请关系
POST {{baseUrl}}/gao/customer/invite/update
Content-Type: application/json
Token: {{user_token}}
{
"customerId": "{{customerId}}",
"introducerCustomerId": "{{introducerCustomerId}}"
}
### 删除客户邀请关系
POST {{baseUrl}}/gao/customer/invite/delete?customerId={{customerId}}
Token: {{user_token}}
+86
View File
@@ -0,0 +1,86 @@
### GaoRegisterRecord 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@recordId =
@customerId =
@customerCode = GAO-0001
@actionId =
### 分页查询登记记录
POST {{baseUrl}}/gao/record/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 16
}
### 查询登记记录详情
POST {{baseUrl}}/gao/record/detail?id={{recordId}}
Token: {{user_token}}
### 创建登记记录,直接传客户 ID
POST {{baseUrl}}/gao/record/create
Content-Type: application/json
Token: {{user_token}}
{
"customerId": "{{customerId}}",
"actionId": "{{actionId}}",
"remark": "客户到店签到"
}
### 创建登记记录,扫码后传客户编码
POST {{baseUrl}}/gao/record/create
Content-Type: application/json
Token: {{user_token}}
{
"customerCode": "{{customerCode}}",
"actionId": "{{actionId}}",
"remark": "扫码快速登记"
}
### 更新登记记录
POST {{baseUrl}}/gao/record/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{recordId}}",
"customerId": "{{customerId}}",
"actionId": "{{actionId}}",
"remark": "补充备注",
"registeredAt": 1785110400
}
### 删除登记记录
POST {{baseUrl}}/gao/record/delete?id={{recordId}}
Token: {{user_token}}
### 按日查询某个行为记录
POST {{baseUrl}}/gao/record/period/list
Content-Type: application/json
Token: {{user_token}}
{
"actionId": "{{actionId}}",
"periodType": "DAY",
"periodValue": 20260727
}
### 按月统计某个行为记录
POST {{baseUrl}}/gao/record/period/stat
Content-Type: application/json
Token: {{user_token}}
{
"actionId": "{{actionId}}",
"periodType": "MONTH",
"periodValue": 202607
}
### 查询客户所有登记统计
POST {{baseUrl}}/gao/record/customer/stat?customerId={{customerId}}
Token: {{user_token}}
+8
View File
@@ -0,0 +1,8 @@
{
"dev": {
"baseUrl": "http://localhost:8091"
},
"prod": {
"baseUrl": "https://api.example.com"
}
}
+77
View File
@@ -0,0 +1,77 @@
# 通用对象说明
## TimiResponse<T>
| 字段 | 类型 | 说明 |
|---|---|---|
| code | Integer | 响应码,`20000` 为成功 |
| msg | String | 响应消息 |
| data | T | 业务数据体,具体结构由接口返回对象决定 |
## Page<T>
| 字段 | 类型 | 说明 |
|---|---|---|
| index | int | 页码下标,从 `0` 开始 |
| size | long | 每页数量 |
| equalsExample | T | 精确匹配示例对象,非空字段参与精确查询 |
| likesExample | T | 模糊匹配示例对象,非空字段参与模糊查询 |
| equalsLogic | Logic | 精确匹配字段之间的连接逻辑,默认 `AND` |
| likesLogic | Logic | 模糊匹配字段之间的连接逻辑,默认 `OR` |
| orderMap | LinkedHashMap<String, OrderType> | 排序字段映射,键通常为字段名,值为排序方向 |
## PageResult<T>
| 字段 | 类型 | 说明 |
|---|---|---|
| total | long | 总数据量 |
| pages | int | 总页数 |
| list | List<T> | 当前页数据列表 |
## UUIDEntity
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 主键 IDUUID 字符串 |
| createdAt | Long | 创建时间,Unix 时间戳毫秒 |
| updatedAt | Long | 更新时间,Unix 时间戳毫秒 |
| deletedAt | Long | 删除时间,Unix 时间戳毫秒,未删除时通常为空 |
## Multilingual
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 多语言 ID |
| zhCN | String | 简体中文文本 |
| zhTW | String | 繁体中文文本 |
| enUS | String | 英文文本 |
| ruRU | String | 俄文文本 |
| jaJP | String | 日文文本 |
| deDE | String | 德文文本 |
| koKR | String | 韩文文本 |
| lastAccessAt | Long | 最后访问时间,Unix 时间戳毫秒 |
| createdAt | Long | 创建时间,Unix 时间戳毫秒 |
| updatedAt | Long | 更新时间,Unix 时间戳毫秒 |
| deletedAt | Long | 删除时间,Unix 时间戳毫秒,未删除时通常为空 |
## Logic
| 枚举 | 说明 |
|---|---|
| AND | 多个条件同时满足 |
| OR | 多个条件满足其一 |
## OrderType
| 枚举 | 说明 |
|---|---|
| ASC | 升序 |
| DESC | 降序 |
## ImageType
| 枚举 | 说明 |
|---|---|
| AUTO | 双线性 |
| SMOOTH | 模糊 |
| PIXELATED | 像素 |
+31
View File
@@ -0,0 +1,31 @@
# 权限对象说明
## Permission
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 权限 ID |
| createdAt | Long | 创建时间,Unix 时间戳毫秒 |
| updatedAt | Long | 更新时间,Unix 时间戳毫秒 |
| deletedAt | Long | 删除时间,Unix 时间戳毫秒 |
| code | String | 权限代码,需与数据库 `permission.code` 保持一致 |
| nameLangId | String | 权限名称的多语言键 |
| description | String | 权限说明 |
| name | String | 当前语言下的权限名称 |
## BatchCreateReq
| 字段 | 类型 | 说明 |
|---|---|---|
| prefixCode | String | 权限代码前缀,例如 `ROLE` |
| prefixName | String | 权限名称前缀,例如 `角色` |
| types | Type[] | 需要批量生成的权限类型列表,详见 `BatchCreateType` |
## BatchCreateType
| 枚举 | 说明 |
|---|---|
| CREATE | 创建权限 |
| READ | 读取权限 |
| UPDATE | 更新权限 |
| DELETE | 删除权限 |
+34
View File
@@ -0,0 +1,34 @@
# 角色对象说明
## Role
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 角色 ID |
| createdAt | Long | 创建时间,Unix 时间戳毫秒 |
| updatedAt | Long | 更新时间,Unix 时间戳毫秒 |
| deletedAt | Long | 删除时间,Unix 时间戳毫秒 |
| code | String | 角色代码,常见值见 `RoleCode` |
| nameLangId | String | 角色名称的多语言键 |
| description | String | 角色说明 |
| name | String | 当前语言下的角色名称 |
| permissionList | List<Permission> | 角色自身直接绑定的权限列表 |
| allPermissionList | List<Permission> | 角色全部权限列表,包含继承自子角色的权限 |
| childRoleList | List<Role> | 子角色列表 |
| permissionIdList | Set<String> | 权限 ID 列表,创建或更新角色时使用 |
| childRoleIdList | Set<String> | 子角色 ID 列表,创建或更新角色时使用 |
## UserRoleAuthorizeReq
| 字段 | 类型 | 说明 |
|---|---|---|
| userId | String | 目标用户 ID |
| roleIdList | List<String> | 需要授权的角色 ID 列表 |
| roleCodeList | List<String> | 需要授权的角色代码列表 |
## RoleCode
| 枚举 | 说明 |
|---|---|
| SYSTEM | 系统 |
| ADMIN | 管理员 |
+73
View File
@@ -0,0 +1,73 @@
# 用户对象说明
## User
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 用户 ID |
| createdAt | Long | 创建时间,Unix 时间戳毫秒 |
| updatedAt | Long | 更新时间,Unix 时间戳毫秒 |
| deletedAt | Long | 删除时间,Unix 时间戳毫秒 |
| name | String | 登录名,系统内唯一 |
| nick | String | 用户昵称 |
| password | String | 明文密码,请求时填写,响应通常不返回 |
| email | String | 邮箱地址 |
| emailVerifyAt | Long | 邮箱验证时间,Unix 时间戳毫秒 |
| phoneNo | String | 手机号,11 位大陆手机号 |
| phoneNoVerifyAt | Long | 手机号验证时间,Unix 时间戳毫秒 |
| wrapperType | ImageType | 背景图渲染算法,详见 `ImageType` |
| avatarType | ImageType | 头像渲染算法,详见 `ImageType` |
| exp | Integer | 经验值 |
| sex | Integer | 性别,当前代码未定义枚举说明 |
| birthdate | Long | 出生日期,Unix 时间戳毫秒 |
| qq | String | QQ 号 |
| description | String | 个人说明 |
| lastLoginIP | String | 上次登录 IP |
| lastLoginAt | Long | 上次登录时间,Unix 时间戳毫秒 |
| unmuteAt | Long | 解除禁言时间,Unix 时间戳毫秒 |
| unbanAt | Long | 解除封禁时间,Unix 时间戳毫秒 |
| roleList | List<String> | 角色代码列表 |
| permissionList | List<String> | 权限代码列表 |
| attachmentList | List<Attachment> | 附件列表 |
| settingList | List<Setting> | 设置列表 |
## LoginRequest
| 字段 | 类型 | 说明 |
|---|---|---|
| user | String | 用户标识,可能是 UID、邮箱或用户名 |
| password | String | 明文密码 |
## LoginResponse
| 字段 | 类型 | 说明 |
|---|---|---|
| token | String | 登录令牌 |
| expireAt | Long | 令牌过期时间,Unix 时间戳毫秒 |
| user | User | 当前登录用户 |
## UpdatePasswordRequest
| 字段 | 类型 | 说明 |
|---|---|---|
| oldValue | String | 原密码 |
| newValue | String | 新密码 |
## PhoneVerifyCodeSendRequest
| 字段 | 类型 | 说明 |
|---|---|---|
| phoneNo | String | 手机号,必须为 11 位大陆手机号 |
## PhoneVerifyRequest
| 字段 | 类型 | 说明 |
|---|---|---|
| phoneNo | String | 手机号,必须为 11 位大陆手机号 |
| verifyCode | String | 6 位数字验证码 |
## CancelRequest
| 字段 | 类型 | 说明 |
|---|---|---|
| password | String | 当前密码,用于确认注销 |
+84
View File
@@ -0,0 +1,84 @@
### PermissionController 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@user_token =
@permissionId =
@permissionCode = TEST_PERMISSION
@permissionNameLangId = permission.test.name
@permissionDescription = 测试权限
@moduleCode = CORE
### 分页查询权限
### 权限:需要 PERMISSION_READ 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Page<Permission>,详见 ./http/object/permission.md
### 返回参数:PageResult<Permission>,详见 ./http/object/permission.md
POST {{baseUrl}}/user/permission/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 10,
"likesExample": {
"code": "{{permissionCode}}",
"moduleCode": "{{moduleCode}}"
}
}
### 创建权限
### 权限:当前代码要求 PERMISSION_READ 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Permission,详见 ./http/object/permission.md
### 返回参数:无
POST {{baseUrl}}/user/permission/create
Content-Type: application/json
Token: {{user_token}}
{
"moduleCode": "{{moduleCode}}",
"code": "{{permissionCode}}",
"nameLangId": "{{permissionNameLangId}}",
"description": "{{permissionDescription}}"
}
### 批量创建权限
### 权限:需要 PERMISSION_CREATE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:BatchCreateReq,详见 ./http/object/permission.md
### 返回参数:无
POST {{baseUrl}}/user/permission/create/batch
Content-Type: application/json
Token: {{user_token}}
{
"moduleCode": "{{moduleCode}}",
"prefixCode": "ROLE",
"prefixName": "角色",
"types": ["CREATE", "READ", "UPDATE", "DELETE"]
}
### 更新权限
### 权限:需要 PERMISSION_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token``id` 必须存在
### 请求参数:Permission,详见 ./http/object/permission.md
### 返回参数:无
POST {{baseUrl}}/user/permission/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{permissionId}}",
"moduleCode": "{{moduleCode}}",
"code": "{{permissionCode}}",
"nameLangId": "{{permissionNameLangId}}",
"description": "测试权限-已更新"
}
### 删除权限
### 权限:需要 PERMISSION_DELETE 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效权限 ID
### 请求参数:无请求体,URL 参数:`id` 为权限 ID
### 返回参数:无
POST {{baseUrl}}/user/permission/delete?id={{permissionId}}
Token: {{user_token}}
+113
View File
@@ -0,0 +1,113 @@
### RoleController 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@permissionId =
@roleId =
@roleCode = TEST_ROLE
@roleNameLangId = role.test.name
@roleDescription = 测试角色
@childRoleId =
@authorizedUserId =
@moduleCode = CORE
### 分页查询角色
### 权限:需要 ADMIN 角色 + ROLE_READ 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Page<Role>,详见 ./http/object/role.md
### 返回参数:PageResult<Role>,详见 ./http/object/role.md
POST {{baseUrl}}/user/role/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 16,
"likesExample": {
"moduleCode": "{{moduleCode}}"
}
}
### 查询角色详情
### 权限:需要 ADMIN 角色 + ROLE_READ 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效角色 ID
### 请求参数:无请求体,URL 参数:`id` 为角色 ID
### 返回参数:Role,详见 ./http/object/role.md
POST {{baseUrl}}/user/role/detail?id={{roleId}}
Token: {{user_token}}
### 创建角色
### 权限:需要 ADMIN 角色 + ROLE_CREATE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Role,详见 ./http/object/role.md
### 返回参数:无
POST {{baseUrl}}/user/role/create
Content-Type: application/json
Token: {{user_token}}
{
"moduleCode": "{{moduleCode}}",
"code": "{{roleCode}}",
"nameLangId": "{{roleNameLangId}}",
"description": "{{roleDescription}}",
"permissionIdList": ["{{permissionId}}"],
"childRoleIdList": []
}
### 更新角色
### 权限:需要 ADMIN 角色 + ROLE_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token``id` 必须存在
### 请求参数:Role,详见 ./http/object/role.md
### 返回参数:无
POST {{baseUrl}}/user/role/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{roleId}}",
"moduleCode": "{{moduleCode}}",
"code": "{{roleCode}}",
"nameLangId": "{{roleNameLangId}}",
"description": "测试角色-已更新",
"permissionIdList": ["{{permissionId}}"],
"childRoleIdList": ["{{childRoleId}}"]
}
### 删除角色
### 权限:需要 ADMIN 角色 + ROLE_DELETE 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效角色 ID
### 请求参数:无请求体,URL 参数:`id` 为角色 ID
### 返回参数:无
POST {{baseUrl}}/user/role/delete?id={{roleId}}
Token: {{user_token}}
### 查询用户角色
### 权限:需要 ADMIN 角色 + USER_ROLE_READ 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `userId` 必须传有效用户 ID
### 请求参数:无请求体,URL 参数:`userId` 为用户 ID
### 返回参数:List<Role>,详见 ./http/object/role.md
POST {{baseUrl}}/user/role/authorized/list?userId={{authorizedUserId}}
Token: {{user_token}}
### 设置用户角色
### 权限:需要 ADMIN 角色 + 同时具备 USER_ROLE_CREATE 和 USER_ROLE_DELETE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:UserRoleAuthorizeReq,详见 ./http/object/role.md
### 返回参数:无
POST {{baseUrl}}/user/role/authorized/create
Content-Type: application/json
Token: {{user_token}}
{
"userId": "{{authorizedUserId}}",
"moduleCode": "{{moduleCode}}",
"roleIdList": ["{{roleId}}"],
"roleCodeList": ["{{roleCode}}"]
}
### 查询用户已授权权限
### 权限:需要 ADMIN 角色 + USER_ROLE_READ 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `userId` 必须传有效用户 ID
### 请求参数:无请求体,URL 参数:`userId` 为用户 ID
### 返回参数:List<Permission>,详见 ./http/object/permission.md
POST {{baseUrl}}/user/role/authorized/permission?userId={{authorizedUserId}}
Token: {{user_token}}
+186
View File
@@ -0,0 +1,186 @@
### UserController 接口测试
### 在 IDEA 中选择 dev 或 prod 环境后执行
@userId =
@name = System
@password = u#HA027xq$i9]P}uB]LweJspTb!3oS^.
@newPassword = qwe123
@nick = 测试用户
@email = test@example.com
### 分页查询用户
### 权限:需要 ADMIN 角色 + USER_READ 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:Page<User>,详见 ./http/object/user.md
### 返回参数:PageResult<User>,详见 ./http/object/user.md
POST {{baseUrl}}/user/list
Content-Type: application/json
Token: {{user_token}}
{
"index": 0,
"size": 16
}
### 查询用户详情
### 权限:需要 ADMIN 角色 + USER_READ 权限
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效用户 ID
### 请求参数:无请求体,URL 参数:`id` 为用户 ID
### 返回参数:User,详见 ./http/object/user.md
POST {{baseUrl}}/user/detail?id={{userId}}
Token: {{user_token}}
### 创建用户
### 权限:需要 ADMIN 角色 + USER_CREATE 权限
### 前置条件:需要先登录并回填 `user_token`,重复用户名通常会失败
### 请求参数:User,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/create
Content-Type: application/json
Token: {{user_token}}
{
"name": "{{name}}",
"nick": "{{nick}}",
"email": "{{email}}",
"phoneNo": "13800138000",
"password": "{{password}}"
}
### 更新用户
### 权限:需要 ADMIN 角色 + USER_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token``id` 必须存在
### 请求参数:User,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/update
Content-Type: application/json
Token: {{user_token}}
{
"id": "{{userId}}",
"name": "{{name}}",
"nick": "{{nick}}",
"email": "{{email}}",
"phoneNo": "13800138000"
}
### 删除用户
### 权限:需要 ADMIN 角色
### 前置条件:需要先登录并回填 `user_token`URL 参数 `id` 必须传有效用户 ID
### 请求参数:无请求体,URL 参数:`id` 为用户 ID
### 返回参数:无
POST {{baseUrl}}/user/delete?id={{userId}}
Token: {{user_token}}
### 注册
### 权限:无需登录
### 前置条件:重复用户名通常会失败
### 请求参数:User,详见 ./http/object/user.md
### 返回参数:LoginResponse,详见 ./http/object/user.md
POST {{baseUrl}}/user/register
Content-Type: application/json
{
"name": "Test",
"nick": "测试用户",
"phoneNo": "13800138000",
"password": "qwe123"
}
### 登录
### 权限:无需登录
### 前置条件:成功后需要把响应中的 token 回填到 `user_token`
### 请求参数:LoginRequest,详见 ./http/object/user.md
### 返回参数:LoginResponse,详见 ./http/object/user.md
POST {{baseUrl}}/user/login
Content-Type: application/json
{
"user": "{{name}}",
"password": "{{password}}"
}
> {%
client.global.set("user_token", response.body?.data?.token)
%}
### 令牌登录
### 权限:需要有效 Token
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:无
### 返回参数:LoginResponse,详见 ./http/object/user.md
GET {{baseUrl}}/user/login/token
Token: {{user_token}}
### 登出
### 权限:当前控制器未声明 RequiredToken/权限注解,按服务端实际会话处理
### 前置条件:当前接口通常用于退出当前登录态
### 请求参数:无
### 返回参数:无
POST {{baseUrl}}/user/logout
Token: {{user_token}}
### 修改当前用户密码
### 权限:需要 USER_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:UpdatePasswordRequest,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/update/password
Content-Type: application/json
Token: {{user_token}}
{
"oldValue": "{{password}}",
"newValue": "{{newPassword}}"
}
### 修改当前用户邮箱
### 权限:需要 USER_UPDATE 权限
### 前置条件:当前方法体为空,接口尚未实现
### 请求参数:无
### 返回参数:无
POST {{baseUrl}}/user/update/email
Content-Type: application/json
Token: {{user_token}}
{}
### 发送当前用户手机号验证码
### 权限:需要 USER_UPDATE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:PhoneVerifyCodeSendRequest,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/phone/verify/send
Content-Type: application/json
Token: {{user_token}}
{
"phoneNo": "13800138000"
}
### 验证当前用户手机号
### 权限:需要 USER_UPDATE 权限
### 前置条件:需要先调用手机号验证码发送接口获取验证码
### 请求参数:PhoneVerifyRequest,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/phone/verify
Content-Type: application/json
Token: {{user_token}}
{
"phoneNo": "13800138000",
"verifyCode": "123456"
}
### 注销当前用户
### 权限:需要 USER_DELETE 权限
### 前置条件:需要先登录并回填 `user_token`
### 请求参数:CancelRequest,详见 ./http/object/user.md
### 返回参数:无
POST {{baseUrl}}/user/cancel
Content-Type: application/json
Token: {{user_token}}
{
"password": "{{password}}"
}
+110 -12
View File
@@ -11,15 +11,16 @@
<groupId>com.imyeyu.timiserverapi</groupId> <groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId> <artifactId>TimiServerAPI</artifactId>
<version>1.0.1</version> <version>1.0.19</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TimiServerAPI</name> <name>TimiServerAPI</name>
<description>imyeyu.com API</description> <description>imyeyu.com API</description>
<properties> <properties>
<java.version>25</java.version>
<springboot.version>3.5.11</springboot.version> <springboot.version>3.5.11</springboot.version>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>25</maven.compiler.source>
<maven.compiler.multilingualField>21</maven.compiler.multilingualField> <maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -91,6 +92,47 @@
<build> <build>
<defaultGoal>compile</defaultGoal> <defaultGoal>compile</defaultGoal>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<proc>full</proc>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.46</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<configuration>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.46</version>
</dependency>
</dependencies>
</plugin>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
@@ -125,28 +167,53 @@
<enabled>true</enabled> <enabled>true</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository>
<id>e2ins_nexus</id>
<url>https://nexus.e2ins.com/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories> </repositories>
<dependencies> <dependencies>
<dependency>
<groupId>com.imyeyu.java</groupId>
<artifactId>timi-java</artifactId>
<version>0.0.3</version>
</dependency>
<dependency> <dependency>
<groupId>com.imyeyu.spring</groupId> <groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId> <artifactId>timi-spring</artifactId>
<version>0.0.12</version> <version>0.0.19</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.imyeyu.network</groupId> <groupId>com.imyeyu.network</groupId>
<artifactId>timi-network</artifactId> <artifactId>timi-network</artifactId>
<version>0.0.8</version> <version>0.0.14</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.imyeyu.lang</groupId> <groupId>com.imyeyu.lang</groupId>
<artifactId>timi-lang</artifactId> <artifactId>timi-lang</artifactId>
<version>0.0.2</version> <version>0.0.5</version>
</dependency>
<dependency>
<groupId>com.imyeyu.java</groupId>
<artifactId>timi-java</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>com.imyeyu.utils</groupId>
<artifactId>timi-utils</artifactId>
<version>0.0.13</version>
</dependency>
<dependency>
<groupId>me.majiajie</groupId>
<artifactId>tinypinyin</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.imyeyu.font.icon</groupId>
<artifactId>timi-icon-font</artifactId>
<version>0.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -165,11 +232,37 @@
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
<version>3.5.3</version> <version>3.5.3</version>
</dependency> </dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.e2ins.ms.notify</groupId>
<artifactId>e2ins-msg-notify-service-sdk</artifactId>
<version>0.0.9</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>3.1.1281</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId> <artifactId>spring-boot-starter-freemarker</artifactId>
<version>${springboot.version}</version> <version>${springboot.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId> <artifactId>spring-boot-starter-data-mongodb</artifactId>
@@ -192,7 +285,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.34</version> <version>1.18.46</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -268,6 +361,11 @@
<artifactId>jsoup</artifactId> <artifactId>jsoup</artifactId>
<version>1.18.1</version> <version>1.18.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
@@ -1,22 +0,0 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.bean.CaptchaFrom;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 图形验证码校验注解
*
* @author 夜雨
* @since 2023-07-15 10:09
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CaptchaValid {
/** @return 验证码来源 */
CaptchaFrom value();
}
@@ -1,20 +1,13 @@
package com.imyeyu.api.annotation; package com.imyeyu.api.annotation;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.util.CaptchaManager; import com.imyeyu.api.util.CaptchaManager;
import com.imyeyu.spring.bean.CaptchaData; import com.imyeyu.spring.annotation.CaptchaValidAbstractInterceptor;
import org.aspectj.lang.JoinPoint; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/** /**
* 图形验证码校验注解处理器 * 图形验证码校验注解处理器
* *
@@ -24,47 +17,19 @@ import java.lang.reflect.Method;
@Slf4j @Slf4j
@Aspect @Aspect
@Component @Component
public class CaptchaValidInterceptor { @RequiredArgsConstructor
public class CaptchaValidInterceptor extends CaptchaValidAbstractInterceptor {
@Value("${spring.profiles.active}") @Value("${spring.profiles.active}")
private String env; private String env;
@Autowired private final CaptchaManager captchaManager;
private CaptchaManager captchaManager;
/** 注入注解 */ @Override
@Pointcut("@annotation(com.imyeyu.api.annotation.CaptchaValid)") protected void verify(String captchaId, String captcha) {
public void captchaPointCut() { if (!env.equals("prod")) {
}
/**
* 执行前
*
* @param joinPoint 切入点
*/
@Before("captchaPointCut()")
public void doBefore(JoinPoint joinPoint) {
try {
if (env.startsWith("dev")) {
// 开发环境不校验
return; return;
} }
if (joinPoint.getSignature() instanceof MethodSignature ms) { captchaManager.verify(captchaId, captcha);
Method method = joinPoint.getTarget().getClass().getMethod(ms.getName(), ms.getParameterTypes());
CaptchaValid annotation = method.getAnnotation(CaptchaValid.class);
CaptchaFrom from = annotation.value();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof CaptchaData<?> captchaData) {
// 校验请求参数的验证码
captchaManager.test(captchaData.getCaptcha(), from.toString());
break;
}
}
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("TODO CaptchaValidInterceptor error");
}
} }
} }
@@ -1,7 +1,5 @@
package com.imyeyu.api.annotation; package com.imyeyu.api.annotation;
import com.imyeyu.api.modules.common.entity.Setting;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@@ -30,20 +28,6 @@ public @interface EnableSetting {
OR OR
} }
Setting.Module.User[] user() default {};
Setting.Module.Comment[] comment() default {};
Setting.Module.ForeverMC[] foreverMC() default {};
Setting.Module.Music[] music() default {};
Setting.Module.Journal[] journal() default {};
Setting.Module.TempFile[] tempFile() default {};
Setting.Module.System[] system() default {};
Logic logic() default Logic.AND; Logic logic() default Logic.AND;
/** @return 未启用配置时响应消息语言映射键 */ /** @return 未启用配置时响应消息语言映射键 */
@@ -41,7 +41,7 @@ public class EnableSettingInterceptor implements HandlerInterceptor {
} }
Enum<?>[] enums = (Enum<?>[]) method.invoke(annotation); Enum<?>[] enums = (Enum<?>[]) method.invoke(annotation);
for (Enum<?> anEnum : enums) { for (Enum<?> anEnum : enums) {
valueList.add(service.is(anEnum)); valueList.add(service.getSystem(anEnum).isTrue());
} }
} }
if (logic == EnableSetting.Logic.AND) { if (logic == EnableSetting.Logic.AND) {
@@ -0,0 +1,31 @@
package com.imyeyu.api.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记需要在响应阶段自动回填的多语言字段
*
* @author 夜雨
* @since 2026-07-17 00:00
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultilingualField {
/**
* 多语言 ID 来源字段名
*
* @return 来源字段名
*/
String value();
/**
* 模板参数来源字段名
*
* @return 同类中的 Map 字段名
*/
String argMap() default "";
}
@@ -23,27 +23,33 @@ import org.springframework.stereotype.Component;
@Component @Component
public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterceptor { public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterceptor {
private static final String KEY_PREFIX = "REQUEST:RATE_LIMIT:";
@Autowired @Autowired
@Qualifier("redisRateLimit") @Qualifier("redisRateLimit")
private Redis<String, Integer> redisRequestRateLimit; private Redis<String, Integer> redis;
@Override @Override
public boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit) { public boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit) {
// 键 // 键
String key = "TimiServerAPI." + TimiSpring.getRequestIP() + "." + id; String key = getKey(id);
if (redisRequestRateLimit.has(key)) { if (redis.has(key)) {
Integer count = redisRequestRateLimit.get(key); Integer count = redis.get(key);
if (count != null) { if (count != null) {
if (count < limit) { if (count < limit) {
redisRequestRateLimit.setAndKeepTTL(key, ++count); redis.setAndKeepTTL(key, ++count);
} else { } else {
log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit); log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit);
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("request_rate_limit"); throw new TimiException(TimiCode.REQUEST_BAD, "request_rate_limit");
} }
} }
return true; return true;
} }
redisRequestRateLimit.set(key, 0, Time.S * cycle); redis.set(key, 0, Time.S * cycle);
return true; return true;
} }
private String getKey(String id) {
return "%sTIMISERVERAPI:%s:%s".formatted(KEY_PREFIX, TimiSpring.getRequestIP(), id);
}
} }
@@ -0,0 +1,29 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.bean.CorePermissionCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
///
/// 要求 CORE 模块权限
///
/// @author Codex
/// @since 2026-07-26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireCorePermission {
/// 所需权限
///
/// @return 权限列表
CorePermissionCode[] value();
/// 匹配模式
///
/// @return 匹配模式
PermissionMatchMode mode() default PermissionMatchMode.ALL;
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.java.TimiJava;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
///
/// CORE 权限注解处理器
///
/// @author Codex
/// @since 2026-07-27
@Component
public class RequireCorePermissionInterceptor implements HandlerInterceptor {
private final PermissionChecker permissionChecker;
public RequireCorePermissionInterceptor(PermissionChecker permissionChecker) {
this.permissionChecker = permissionChecker;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequireCorePermission annotation = handlerMethod.getMethodAnnotation(RequireCorePermission.class);
if (annotation == null || TimiJava.isEmpty(annotation.value())) {
return true;
}
String[] permissionCodeList = java.util.Arrays.stream(annotation.value()).map(CorePermissionCode::getValue).toArray(String[]::new);
if (annotation.mode() == PermissionMatchMode.ANY) {
permissionChecker.checkAny(ModuleCode.CORE, permissionCodeList);
} else {
permissionChecker.checkAll(ModuleCode.CORE, permissionCodeList);
}
return true;
}
}
@@ -1,39 +0,0 @@
package com.imyeyu.api.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.blog.util.UserToken;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.annotation.RequiredTokenAbstractInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 令牌验证注解处理器
*
* @author 夜雨
* @since 2021-08-16 18:07
*/
@Slf4j
@Component
public class RequiredTokenInterceptor extends RequiredTokenAbstractInterceptor<RequiredToken> {
@Autowired
private UserToken userToken;
public RequiredTokenInterceptor() {
super(RequiredToken.class);
}
@Override
protected boolean beforeRun(HttpServletRequest req, HttpServletResponse resp) {
if (userToken.isInvalid(TimiSpring.getToken())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal");
}
return true;
}
}
@@ -1,34 +0,0 @@
package com.imyeyu.api.bean;
/**
* 验证码来源
*
* @author 夜雨
* @since 2023-07-16 10:12
*/
public enum CaptchaFrom {
/** 注册 */
REGISTER,
/** 登录 */
LOGIN,
/** 忘记密码 */
RESET_PASSWORD,
/** 评论 */
COMMENT,
/** 评论回复 */
COMMENT_REPLY,
/** Git 反馈 */
GIT_ISSUE,
/** Git 合并请求 */
GIT_MERGE,
/** 歌词修正申请 */
LYRIC_CORRECT
}
@@ -0,0 +1,96 @@
package com.imyeyu.api.bean;
import com.imyeyu.api.modules.user.bean.BuiltinPermissionCode;
import lombok.AllArgsConstructor;
///
/// CORE 模块权限枚举
///
/// @author Codex
/// @since 2026-07-26
@AllArgsConstructor
public enum CorePermissionCode implements BuiltinPermissionCode {
PERMISSION_CREATE("PERMISSION:CREATE", "权限创建", false, true),
PERMISSION_READ("PERMISSION:READ", "权限读取", false, true),
PERMISSION_UPDATE("PERMISSION:UPDATE", "权限修改", false, true),
PERMISSION_DELETE("PERMISSION:DELETE", "权限删除", false, true),
ROLE_CREATE("ROLE:CREATE", "角色创建", false, true),
ROLE_READ("ROLE:READ", "角色读取", false, true),
ROLE_UPDATE("ROLE:UPDATE", "角色修改", false, true),
ROLE_DELETE("ROLE:DELETE", "角色删除", false, true),
ROLE_PERMISSION_CREATE("ROLE_PERMISSION:CREATE", "角色权限创建", false, true),
ROLE_PERMISSION_READ("ROLE_PERMISSION:READ", "角色权限读取", false, true),
ROLE_PERMISSION_DELETE("ROLE_PERMISSION:DELETE", "角色权限删除", false, true),
ROLE_PERMISSION_INHERIT_CREATE("ROLE_PERMISSION_INHERIT:CREATE", "角色权限继承创建", false, true),
ROLE_PERMISSION_INHERIT_READ("ROLE_PERMISSION_INHERIT:READ", "角色权限继承读取", false, true),
ROLE_PERMISSION_INHERIT_DELETE("ROLE_PERMISSION_INHERIT:DELETE", "角色权限继承删除", false, true),
SETTING_CREATE("SETTING:CREATE", "设置创建", false, true),
SETTING_READ("SETTING:READ", "设置读取", false, true),
SETTING_UPDATE("SETTING:UPDATE", "设置修改", false, true),
SETTING_DELETE("SETTING:DELETE", "设置删除", false, true),
MULTILINGUAL_CREATE("MULTILINGUAL:CREATE", "多语言创建", false, true),
MULTILINGUAL_READ("MULTILINGUAL:READ", "多语言读取", false, true),
MULTILINGUAL_UPDATE("MULTILINGUAL:UPDATE", "多语言修改", false, true),
MULTILINGUAL_DELETE("MULTILINGUAL:DELETE", "多语言删除", false, true),
TAG_CREATE("TAG:CREATE", "标签创建", false, true),
TAG_READ("TAG:READ", "标签读取", false, true),
TAG_APPLY("TAG:APPLY", "标签应用", false, true),
TAG_UPDATE("TAG:UPDATE", "标签修改", false, true),
TAG_DELETE("TAG:DELETE", "标签删除", false, true),
USER_CREATE("USER:CREATE", "用户创建", false, true),
USER_READ("USER:READ", "用户读取", false, true),
USER_UPDATE("USER:UPDATE", "用户修改", false, true),
USER_DELETE("USER:DELETE", "用户删除", false, true),
FEEDBACK_CREATE("FEEDBACK:CREATE", "反馈创建", false, true),
FEEDBACK_READ("FEEDBACK:READ", "反馈读取", false, true),
FEEDBACK_UPDATE("FEEDBACK:UPDATE", "反馈修改", false, true),
FEEDBACK_DELETE("FEEDBACK:DELETE", "反馈删除", false, true),
NOTIFY_CREATE("NOTIFY:CREATE", "通知创建", false, true),
NOTIFY_READ("NOTIFY:READ", "通知读取", false, true),
NOTIFY_DELETE("NOTIFY:DELETE", "通知删除", false, true),
NOTIFY_CANCEL("NOTIFY:CANCEL", "通知取消", false, true),
USER_ROLE_CREATE("USER_ROLE:CREATE", "用户角色创建", true, true),
USER_ROLE_READ("USER_ROLE:READ", "用户角色读取", true, true),
USER_ROLE_DELETE("USER_ROLE:DELETE", "用户角色删除", true, true);
final String value;
final String defaultName;
final boolean grantToSystem;
final boolean grantToAdmin;
@Override
public ModuleCode getModuleCode() {
return ModuleCode.CORE;
}
@Override
public String getValue() {
return value;
}
@Override
public String getDefaultName() {
return defaultName;
}
@Override
public boolean isGrantToSystem() {
return grantToSystem;
}
@Override
public boolean isGrantToAdmin() {
return grantToAdmin;
}
}
@@ -0,0 +1,42 @@
package com.imyeyu.api.bean;
import com.imyeyu.api.modules.user.bean.BuiltinRoleCode;
import lombok.AllArgsConstructor;
///
/// CORE 模块角色枚举
///
/// @author Codex
/// @since 2026-07-26
@AllArgsConstructor
public enum CoreRoleCode implements BuiltinRoleCode {
SYSTEM("系统", true, null),
ADMIN("管理员", false, SYSTEM.name());
final String defaultName;
final boolean protectedRole;
final String parentCode;
@Override
public ModuleCode getModuleCode() {
return ModuleCode.CORE;
}
@Override
public String getDefaultName() {
return defaultName;
}
@Override
public boolean isProtected() {
return protectedRole;
}
@Override
public String getParentCode() {
return parentCode;
}
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.bean;
///
/// 模块代码
///
/// @author Codex
/// @since 2026-07-26
public enum ModuleCode {
CORE,
GAO,
}
@@ -1,26 +0,0 @@
package com.imyeyu.api.bean;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 夜雨
* @since 2023-10-25 10:10
*/
public interface MultilingualHandler {
/**
*
*
* @author 夜雨
* @since 2023-10-25 10:25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@interface MultilingualField {
String[] args() default {};
}
}
@@ -0,0 +1,16 @@
package com.imyeyu.api.bean;
/**
* 权限匹配模式
*
* @author 夜雨
* @since 2026-06-05 17:45
*/
public enum PermissionMatchMode {
/** 全部满足 */
ALL,
/** 满足任意一个 */
ANY,
}
@@ -0,0 +1,16 @@
package com.imyeyu.api.bean;
/**
* 角色匹配模式
*
* @author 夜雨
* @since 2026-06-05 17:45
*/
public enum RoleMatchMode {
/** 全部满足 */
ALL,
/** 满足任意一个 */
ANY,
}
@@ -1,8 +1,14 @@
package com.imyeyu.api.config; package com.imyeyu.api.config;
import com.e2ins.ms.notify.SmsNotifyService;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -17,11 +23,19 @@ public class BeanConfig {
@Bean @Bean
public ObjectMapper jackson() { public ObjectMapper jackson() {
ObjectMapper mapper = new ObjectMapper(); return JsonMapper.builder()
mapper.setDateFormat(Time.dateTime); .addModule(new JavaTimeModule())
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); .serializationInclusion(JsonInclude.Include.NON_NULL)
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); .defaultDateFormat(Time.dateTime)
return mapper; .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(MapperFeature.USE_STD_BEAN_NAMING)
.build();
}
@Bean
public SmsNotifyService smsNotifyService() {
// TODO
return new SmsNotifyService(null, null);
} }
@Bean @Bean
@@ -1,8 +1,8 @@
package com.imyeyu.api.config; package com.imyeyu.api.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.common.entity.Multilingual; import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache;
import com.imyeyu.spring.bean.RedisConfigParams; import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.config.AbstractRedisConfig; import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.spring.util.Redis; import com.imyeyu.spring.util.Redis;
@@ -11,8 +11,8 @@ import com.imyeyu.utils.Time;
import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.api.StatefulConnection;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.KeyGenerator;
@@ -36,11 +36,11 @@ import java.time.Duration;
@Configuration @Configuration
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@EnableAutoConfiguration @EnableAutoConfiguration
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "spring.redis") @ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig extends AbstractRedisConfig { public class RedisConfig extends AbstractRedisConfig {
private final ObjectMapper jackson; @Autowired
private ObjectMapper jackson;
// ---------- 连接配置 ---------- // ---------- 连接配置 ----------
@@ -53,57 +53,8 @@ public class RedisConfig extends AbstractRedisConfig {
/** 密码 */ /** 密码 */
private String password; private String password;
/** 数据库 */ /** Redis 逻辑库 */
private Database database; private int database;
/**
* 数据库
*
* @author 夜雨
* @since 2023-08-21 16:25
*/
@Data
public static class Database {
/** 分布式锁 */
private int locker;
/** 多语言环境 */
private int language;
/** 多语言键环境 */
private int languageMap;
/** 文章排位 */
private int articleRanking;
/** 文章阅读记录 */
private int articleRead;
/** 用户登录令牌 */
private int userToken;
/** 用户经验值标记 */
private int userExpFlag;
/** 用户邮箱验证 */
private int userEmailVerify;
/** 用户重置密码验证 */
private int userResetPWVerify;
/** 访问频率控制 */
private int rateLimit;
/** 系统配置 */
private int setting;
/** Minecraft 登录 */
private int fmcPlayerToken;
/** 共享剪切板 */
private int clipboard;
}
@Override @Override
protected RedisConfigParams configParams() { protected RedisConfigParams configParams() {
@@ -149,13 +100,13 @@ public class RedisConfig extends AbstractRedisConfig {
/** @return 分布式锁, ID: 尝试加锁次数 */ /** @return 分布式锁, ID: 尝试加锁次数 */
@Bean("redisLocker") @Bean("redisLocker")
public Redis<String, Integer> getLockerRedisTemplate() { public Redis<String, Integer> getLockerRedisTemplate() {
return getRedis(database.locker, RedisSerializers.STRING, RedisSerializers.INTEGER); return getRedis(database, RedisSerializers.STRING, RedisSerializers.INTEGER);
} }
/** @return 多语言环境,ID: {@link Multilingual} */ /** @return 多语言环境,ID: {@link Multilingual} */
@Bean("redisLanguage") @Bean("redisLanguage")
public Redis<Long, Multilingual> getLanguageRedisTemplate() { public Redis<String, Multilingual> getLanguageRedisTemplate() {
return getRedis(database.language, RedisSerializers.LONG, new RedisSerializer<>() { return getRedis(database, RedisSerializers.STRING, new RedisSerializer<>() {
public Multilingual deserialize(@Nullable byte[] bytes) { public Multilingual deserialize(@Nullable byte[] bytes) {
if (bytes == null || bytes.length == 0) { if (bytes == null || bytes.length == 0) {
@@ -182,63 +133,80 @@ public class RedisConfig extends AbstractRedisConfig {
}); });
} }
@Bean("redisCaptcha")
public Redis<String, String> getCaptchaRedisTemplate() {
return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
}
/** @return 文章访问记录,IP: [文章 ID] */ /** @return 文章访问记录,IP: [文章 ID] */
@Bean("redisLanguageMap") @Bean("redisLanguageMap")
public Redis<String, Long> getLanguageMapRedisTemplate() { public Redis<String, Long> getLanguageMapRedisTemplate() {
return getRedis(database.languageMap, RedisSerializers.STRING, RedisSerializers.LONG); return getRedis(database, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 文章访问统计,文章 ID: {@link ArticleRanking}(JSON) */
@Bean("redisArticleRanking")
public Redis<Long, ArticleRanking> getArticleRankingRedisTemplate() {
return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.jacksonSerializer(jackson, ArticleRanking.class));
} }
/** @return 文章访问记录,IP: [文章 ID] */ /** @return 文章访问记录,IP: [文章 ID] */
@Bean("redisArticleRead") @Bean("redisArticleRead")
public Redis<String, Long> getArticleReadRedisTemplate() { public Redis<String, Long> getArticleReadRedisTemplate() {
return getRedis(database.articleRead, RedisSerializers.STRING, RedisSerializers.LONG); return getRedis(database, RedisSerializers.STRING, RedisSerializers.LONG);
} }
/** @return 用户登录经验标记,UID: NULL,暂时没有值,数据死亡时间为次日零时 */ /** @return 用户登录经验标记,UID: NULL,暂时没有值,数据死亡时间为次日零时 */
@Bean("redisUserExpFlag") @Bean("redisUserExpFlag")
public Redis<Long, String> getUserExpFlagRedisTemplate() { public Redis<Long, String> getUserExpFlagRedisTemplate() {
return getRedis(database.userExpFlag, RedisSerializers.LONG, RedisSerializers.STRING); return getRedis(database, RedisSerializers.LONG, RedisSerializers.STRING);
}
/** @return 用户登录令牌,令牌: 用户 ID */
@Bean("redisUserToken")
public Redis<String, String> getUserTokenRedisTemplate() {
return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
} }
/** @return 用户邮箱验证密钥,密钥: UID */ /** @return 用户邮箱验证密钥,密钥: UID */
@Bean("redisUserEmailVerify") @Bean("redisUserEmailVerify")
public Redis<String, Long> getUserEmailVerifyRedisTemplate() { public Redis<String, Long> getUserEmailVerifyRedisTemplate() {
return getRedis(database.userEmailVerify, RedisSerializers.STRING, RedisSerializers.LONG); return getRedis(database, RedisSerializers.STRING, RedisSerializers.LONG);
} }
/** @return 用户重置密码密钥,密钥: UID */ /** @return 用户重置密码密钥,密钥: UID */
@Bean("redisUserResetPWVerify") @Bean("redisUserResetPWVerify")
public Redis<String, Long> getUserResetPWVerifyRedisTemplate() { public Redis<String, Long> getUserResetPWVerifyRedisTemplate() {
return getRedis(database.userResetPWVerify, RedisSerializers.STRING, RedisSerializers.LONG); return getRedis(database, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 用户手机号验证码,手机号: 验证码 */
@Bean("redisUserPhoneVerify")
public Redis<String, String> getUserPhoneVerifyRedisTemplate() {
return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
} }
/** @return 接口访问控制,IP#方法: 生命周期内访问次数 */ /** @return 接口访问控制,IP#方法: 生命周期内访问次数 */
@Bean("redisRateLimit") @Bean("redisRateLimit")
public Redis<String, Integer> getRateLimitRedisTemplate() { public Redis<String, Integer> getRateLimitRedisTemplate() {
return getRedis(database.rateLimit, RedisSerializers.STRING, RedisSerializers.INTEGER); return getRedis(database, RedisSerializers.STRING, RedisSerializers.INTEGER);
} }
/** @return 系统配置,Key 枚举: String 配置值 */ /** @return 系统配置,Key 枚举: String 配置值 */
@Bean("redisSetting") @Bean("redisSetting")
public Redis<String, String> getSettingRedisTemplate() { public Redis<String, String> getSettingRedisTemplate() {
return getRedis(database.setting, RedisSerializers.STRING, RedisSerializers.STRING); return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
} }
/** @return Minecraft 登录,令牌: 玩家 ID */ /** @return Minecraft 登录,令牌: 玩家 ID */
@Bean("redisMCPlayerToken") @Bean("redisMCPlayerToken")
public Redis<String, Long> getMCPlayerLoginRedisTemplate() { public Redis<String, Long> getMCPlayerLoginRedisTemplate() {
return getRedis(database.fmcPlayerToken, RedisSerializers.STRING, RedisSerializers.LONG); return getRedis(database, RedisSerializers.STRING, RedisSerializers.LONG);
} }
/** @return 共享剪切板,会话 ID: 内容 */ /** @return 共享剪切板,会话 ID: 内容 */
@Bean("redisClipboard") @Bean("redisClipboard")
public Redis<String, String> getClipboardRedisTemplate() { public Redis<String, String> getClipboardRedisTemplate() {
return getRedis(database.clipboard, RedisSerializers.STRING, RedisSerializers.STRING); return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
}
/// @return GAO 用户邀请,code: 邀请缓存
@Bean("redisGaoUserInvite")
public Redis<String, GaoUserInviteCache> getGaoUserInviteRedisTemplate() {
return getRedis(database, RedisSerializers.STRING, RedisSerializers.jacksonSerializer(jackson, GaoUserInviteCache.class));
} }
} }
@@ -5,11 +5,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.api.annotation.EnableSettingInterceptor; import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequiredTokenInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor;
import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor; import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor;
import com.imyeyu.api.modules.minecraft.annotation.RequiredFMCServerTokenInterceptor;
import com.imyeyu.api.modules.system.util.SystemAPIInterceptor; import com.imyeyu.api.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.spring.annotation.RequestBodyValueArgumentResolver; import com.imyeyu.spring.annotation.RequestBodyValueArgumentResolver;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
@@ -39,10 +40,10 @@ public class WebConfig implements WebMvcConfigurer {
private final ObjectMapper jackson; private final ObjectMapper jackson;
private final SystemAPIInterceptor systemAPIInterceptor; private final SystemAPIInterceptor systemAPIInterceptor;
private final JournalAPIInterceptor journalAPIInterceptor; private final JournalAPIInterceptor journalAPIInterceptor;
private final RequiredTokenInterceptor requiredTokenInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor; private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestRateLimitInterceptor requestRateLimitInterceptor; private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequiredFMCServerTokenInterceptor requiredFMCServerTokenInterceptor; private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor;
private final RequireCorePermissionInterceptor requireCorePermissionInterceptor;
/** /**
* 过滤器 * 过滤器
@@ -53,9 +54,9 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH); registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH);
registry.addInterceptor(requiredFMCServerTokenInterceptor).addPathPatterns("/fmc/server/**");
registry.addInterceptor(requiredTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**"); registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**"); registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
} }
@@ -72,6 +73,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
JsonMapper jsonMapper = JsonMapper.builder() JsonMapper jsonMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
// 设置默认属性包含规则:忽略 null 值 // 设置默认属性包含规则:忽略 null 值
.serializationInclusion(JsonInclude.Include.NON_NULL) .serializationInclusion(JsonInclude.Include.NON_NULL)
// 日期格式化 // 日期格式化
@@ -30,7 +30,6 @@ public class ForeverMCDBConfig {
public static final String ROLLBACKER = "foreverMCTransactionManager"; public static final String ROLLBACKER = "foreverMCTransactionManager";
@Bean(name = "foreverMCDataSource") @Bean(name = "foreverMCDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.forevermc") @ConfigurationProperties(prefix = "spring.datasource.forevermc")
public DataSource getPrimaryDateSource() throws SQLException { public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource(); HikariDataSource dataSource = new HikariDataSource();
@@ -50,7 +49,6 @@ public class ForeverMCDBConfig {
@Bean(name = "foreverMCSqlSessionFactory") @Bean(name = "foreverMCSqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("foreverMCDataSource") DataSource datasource) throws Exception { public SqlSessionFactory primarySqlSessionFactory(@Qualifier("foreverMCDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true); config.setUseGeneratedKeys(true);
@@ -64,7 +62,6 @@ public class ForeverMCDBConfig {
} }
@Bean("foreverMCSqlSessionTemplate") @Bean("foreverMCSqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("foreverMCSqlSessionFactory") SqlSessionFactory sessionfactory) { public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("foreverMCSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory); return new SqlSessionTemplate(sessionfactory);
} }
@@ -38,7 +38,6 @@ public class GiteaDBConfig {
public static final String ROLLBACKER = "giteaTransactionManager"; public static final String ROLLBACKER = "giteaTransactionManager";
@Bean(name = "giteaDataSource") @Bean(name = "giteaDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.gitea") @ConfigurationProperties(prefix = "spring.datasource.gitea")
public DataSource dateSource() throws SQLException { public DataSource dateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource(); HikariDataSource dataSource = new HikariDataSource();
@@ -58,7 +57,6 @@ public class GiteaDBConfig {
@Bean(name = "giteaSqlSessionFactory") @Bean(name = "giteaSqlSessionFactory")
@Primary
public SqlSessionFactory sessionFactory(@Qualifier("giteaDataSource") DataSource datasource) throws Exception { public SqlSessionFactory sessionFactory(@Qualifier("giteaDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true); config.setUseGeneratedKeys(true);
@@ -69,7 +67,7 @@ public class GiteaDBConfig {
{ {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>(); List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/gitea/**/*.xml"); // mapperLocations.add("classpath:mapper/gitea/**/*.xml");
for (String mapperLocation : mapperLocations) { for (String mapperLocation : mapperLocations) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocation))); resources.addAll(List.of(resourceResolver.getResources(mapperLocation)));
} }
@@ -92,7 +90,6 @@ public class GiteaDBConfig {
@Bean("giteaSqlSessionTemplate") @Bean("giteaSqlSessionTemplate")
@Primary
public SqlSessionTemplate sessionTemplate(@Qualifier("giteaSqlSessionFactory") SqlSessionFactory sessionfactory) { public SqlSessionTemplate sessionTemplate(@Qualifier("giteaSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory); return new SqlSessionTemplate(sessionfactory);
} }
@@ -8,6 +8,7 @@ import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.flyway.FlywayDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -32,7 +33,9 @@ import java.util.List;
@Configuration @Configuration
@MapperScan(basePackages = { @MapperScan(basePackages = {
"com.imyeyu.api.modules.git.mapper", "com.imyeyu.api.modules.git.mapper",
"com.imyeyu.api.modules.gao.mapper",
"com.imyeyu.api.modules.bill.mapper", "com.imyeyu.api.modules.bill.mapper",
"com.imyeyu.api.modules.user.mapper",
"com.imyeyu.api.modules.blog.mapper", "com.imyeyu.api.modules.blog.mapper",
"com.imyeyu.api.modules.lyric.mapper", "com.imyeyu.api.modules.lyric.mapper",
"com.imyeyu.api.modules.mirror.mapper", "com.imyeyu.api.modules.mirror.mapper",
@@ -47,6 +50,7 @@ public class TimiServerDBConfig {
@Bean(name = "timiServerDataSource") @Bean(name = "timiServerDataSource")
@Primary @Primary
@FlywayDataSource
@ConfigurationProperties(prefix = "spring.datasource.timiserver") @ConfigurationProperties(prefix = "spring.datasource.timiserver")
public DataSource getPrimaryDateSource() throws SQLException { public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource(); HikariDataSource dataSource = new HikariDataSource();
@@ -77,19 +81,16 @@ public class TimiServerDBConfig {
{ {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>(); List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/git/**/*.xml"); mapperLocations.add("classpath:mapper/timi-server/**/*.xml");
mapperLocations.add("classpath:mapper/blog/**/*.xml");
mapperLocations.add("classpath:mapper/common/**/*.xml");
mapperLocations.add("classpath:mapper/system/**/*.xml");
mapperLocations.add("classpath:mapper/journal/**/*.xml");
mapperLocations.add("classpath:mapper/minecraft/**/*.xml");
for (String mapperLocation : mapperLocations) { for (String mapperLocation : mapperLocations) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocation))); resources.addAll(List.of(resourceResolver.getResources(mapperLocation)));
} }
} }
String[] typeAliases = { String[] typeAliases = {
"com.imyeyu.api.modules.git.entity", "com.imyeyu.api.modules.git.entity",
"com.imyeyu.api.modules.gao.entity",
"com.imyeyu.api.modules.bill.entity", "com.imyeyu.api.modules.bill.entity",
"com.imyeyu.api.modules.user.entity",
"com.imyeyu.api.modules.blog.entity", "com.imyeyu.api.modules.blog.entity",
"com.imyeyu.api.modules.lyric.entity", "com.imyeyu.api.modules.lyric.entity",
"com.imyeyu.api.modules.mirror.entity", "com.imyeyu.api.modules.mirror.entity",
@@ -1,93 +0,0 @@
package com.imyeyu.api.modules.blog.controller;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.service.ArticleService;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.modules.blog.vo.article.KeywordPage;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
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;
/**
* 文章接口
*
* @author 夜雨
* @since 2021-02-17 17:47
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
public class ArticleController {
private final ArticleService service;
/**
* 查看
*
* @param id 文章 ID
* @return 文章
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/{id}")
public ArticleView view(@Min(1) @NotNull @PathVariable Long id) {
return service.view(id);
}
/**
* 喜欢文章
*
* @param id 文章 ID
* @return 最新喜欢数量
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/like/{id}")
public int like(@Min(1) @NotNull @PathVariable Long id) {
return service.like(id);
}
/**
* 主列表
*
* @param page 页面参数
* @return 文章列表
*/
@RequestRateLimit
@RequestMapping("/list")
public PageResult<Article> list(@Valid @RequestBody Page page) {
return service.page(page);
}
/**
* 根据关键字获取列表
*
* @param page 关键字页面参数
* @return 文章列表
*/
@RequestRateLimit
@RequestMapping("/list/search")
public PageResult<Article> listByKeyword(@Valid @RequestBody KeywordPage page) {
return service.pageByKeyword(page);
}
/** @return 每周访问排位 */
@RequestMapping("/list/ranking")
public List<ArticleRanking> ranking() {
return service.listRanking();
}
}
@@ -1,32 +0,0 @@
package com.imyeyu.api.modules.blog.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.modules.blog.entity.Friend;
import com.imyeyu.api.modules.blog.service.FriendService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 主控
*
* @author 夜雨
* @since 2023-02-04 10:28
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class BlogController {
private final FriendService friendService;
/** @return 所有友链列表 */
@GetMapping("/friend")
public List<Friend> friend() {
return friendService.listAll();
}
}
@@ -1,92 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.spring.entity.Destroyable;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 文章
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Article extends Entity implements CommentSupport, Destroyable {
/**
* 文章渲染类型,对应前端模板
*
* @author 夜雨
* @since 2021-07-04 09:23
*/
public enum Type {
/** 公版 */
PUBLIC,
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
/** 标题 */
protected String title;
/** 类型 */
protected Type type;
/** 摘要 */
protected String digest;
/** 数据 */
protected String data;
/** 扩展数据 */
protected JsonNode extendData;
/** 阅读数量 */
protected int reads;
/** 喜欢数量 */
protected int likes;
/** true 为显示评论 */
protected boolean showComment;
/** true 为可评论 */
protected boolean canComment;
/** true 为可排位 */
protected boolean canRanking;
/** true 为可通过列表查询 */
protected boolean canList;
/** @return true 为可评论 */
@Override
public boolean canComment() {
return canComment;
}
/** @return true 为不可评论 */
@Override
public boolean canNotComment() {
return !canComment;
}
/** @return true 为可排位 */
public boolean canRanking() {
return canRanking;
}
/** @return true 为不可排位 */
public boolean canNotRanking() {
return !canRanking;
}
}
@@ -1,35 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 访问排行(每周)
* 只记录访问次数、标题和最近访问,具体文章由 Redis key 记录
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ArticleRanking extends Entity {
private String title;
private Article.Type type;
private int count = 1;
private Long recentAt; // 最近访问
public ArticleRanking(Long id, String title, Article.Type type) {
setId(id);
this.title = title;
this.type = type;
}
/** 访问计数 + 1 */
public void read() {
count++;
}
}
@@ -1,36 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 评论回复提醒队列,和 CommentReplyRecord 不一样,本队列在推送消息后就删除,而后者会持久保存
*
* <p>基本逻辑:
* <pre>
* 触发:用户回复一条评论
* 条件:被回复者是注册用户 && 不是回复自己 && 邮箱已验证 && 接收回复提醒邮件
* 事件:添加本对象到队列列表,等待邮件推送服务调度,邮件推送服务
* </pre>
* 会针对用户收集本队列消息组合成邮件再一并推送
*
* @author 夜雨
* @since 2021-08-25 00:00
*/
@Data
@NoArgsConstructor
public class CommentRemindQueue {
@Id
@AutoUUID
private String UUID;
private Long userId;
private Long replyId;
private CommentReply reply;
}
@@ -1,21 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 夜雨 创建于 2021-07-15 15:59
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Friend extends Entity {
private String icon;
private String name;
private String link;
}
@@ -1,33 +0,0 @@
package com.imyeyu.api.modules.blog.mapper;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 文章
*
* @author 夜雨
* @since 2021-02-23 21:34
*/
public interface ArticleMapper extends BaseMapper<Article, Long> {
@Override
long countByPage(Page page);
@Override
List<Article> selectByPage(Page page);
long countByKeyword(String keyword);
List<Article> selectByKeyword(String keyword, Long offset, long limit);
@Select("UPDATE `article` SET `likes` = `likes` + 1 WHERE `id` = #{articleId}")
void like(Long articleId);
@Select("UPDATE `article` SET `reads` = `reads` + 1 WHERE `id` = #{articleId}")
void read(Long articleId);
}
@@ -1,20 +0,0 @@
package com.imyeyu.api.modules.blog.mapper;
import com.imyeyu.api.modules.blog.entity.Friend;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 友链
*
* @author 夜雨
* @since 2021-07-15 16:11
*/
public interface FriendMapper extends BaseMapper<Friend, Long> {
@Select("SELECT * FROM friend WHERE 1 = 1" + NOT_DELETE)
List<Friend> listAll();
}
@@ -1,48 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.modules.blog.vo.article.KeywordPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
import java.util.List;
/**
* 文章服务
*
* @author 夜雨
* @since 2021-02-23 21:33
*/
public interface ArticleService extends GettableService<Article, Long>, PageableService<Article> {
/**
* 获取文章,此方法触发阅读计数,包括触发每周热门排行统计,同一 IP 3 小时内访问多次的文章只计一次
*
* @param id 文章 ID
* @throws TimiException 服务异常
*/
ArticleView view(long id);
PageResult<Article> pageByKeyword(KeywordPage page);
/**
* 获取每周阅读排行
*
* @return 热门文章列表
* @throws TimiException 服务异常
*/
List<ArticleRanking> listRanking();
/**
* 喜欢文章
*
* @param articleId 文章 ID
* @return 最新喜欢数量
* @throws TimiException 服务异常
*/
int like(Long articleId);
}
@@ -1,41 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.spring.service.CreatableService;
import java.util.List;
/**
* 评论回复队列服务
*
* @author 夜雨
* @since 2021-08-25 00:11
*/
public interface CommentRemindQueueService extends CreatableService<CommentRemindQueue> {
/**
* 根据用户 ID 获取
*
* @param userId 用户 ID
* @return 回复提醒列表
* @throws TimiException 服务异常
*/
List<CommentRemindQueue> listByUserId(Long userId);
/**
* 根据用户 ID 移出队列
*
* @param uid 用户 ID
* @throws TimiException 服务异常
*/
void destroyByUserId(Long uid);
/**
* 根据回复 ID 移出队列
*
* @param rid 回复 ID
* @throws TimiException 服务异常
*/
void destroyByReplyId(Long rid);
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.api.modules.blog.entity.Friend;
import java.util.List;
/**
* 友链服务
*
* @author 夜雨
* @since 2021-07-15 16:04
*/
public interface FriendService {
List<Friend> listAll();
}
@@ -1,122 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.mapper.ArticleMapper;
import com.imyeyu.api.modules.blog.service.ArticleService;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.modules.blog.vo.article.KeywordPage;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.mapper.CommentMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Comparator;
import java.util.List;
/**
* 文章服务实现
*
* @author 夜雨
* @since 2021-02-17 17:48
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleServiceImplement extends AbstractEntityService<Article, Long> implements ArticleService {
private final TagService tagService;
private final AttachmentService attachmentService;
private final ArticleMapper mapper;
private final CommentMapper commentMapper;
private final Redis<String, Long> redisArticleRead;
private final Redis<Long, ArticleRanking> redisArticleRanking;
@Override
protected BaseMapper<Article, Long> mapper() {
return mapper;
}
@Override
public PageResult<Article> page(Page page) {
PageResult<Article> result = new PageResult<>();
result.setList(mapper.selectByPage(page));
result.setTotal(mapper.countByPage(page));
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public ArticleView view(long id) {
String ip = TimiSpring.getRequestIP();
Article article = get(id);
TimiException.required(article, "article.not_found");
// 计数
if (!redisArticleRead.contains(ip, article.getId())) {
// 3 小时内访问记录
redisArticleRead.add(ip, article.getId());
redisArticleRead.setExpire(ip, Time.H * 3);
mapper.read(article.getId());
// 每周访问计数
if (article.canRanking()) {
ArticleRanking ranking = redisArticleRanking.get(article.getId());
if (ranking == null) {
ranking = new ArticleRanking(article.getId(), article.getTitle(), article.getType());
ranking.setRecentAt(Time.now());
redisArticleRanking.set(article.getId(), ranking, Time.D * 7);
} else {
ranking.read();
ranking.setRecentAt(Time.now());
redisArticleRanking.setAndKeepTTL(article.getId(), ranking);
}
}
}
ArticleView view = new ArticleView();
BeanUtils.copyProperties(article, view);
view.setComments(commentMapper.countAll(Comment.BizType.ARTICLE, article.getId()));
view.setTagList(tagService.listByBizID(Tag.BizType.ARTICLE, String.valueOf(article.getId())));
view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
return view;
}
@Override
public PageResult<Article> pageByKeyword(KeywordPage page) {
PageResult<Article> result = new PageResult<>();
result.setList(mapper.selectByKeyword(page.getKeyword(), page.getOffset(), page.getLimit()));
result.setTotal(mapper.countByKeyword(page.getKeyword()));
return result;
}
@Override
public List<ArticleRanking> listRanking() {
List<ArticleRanking> list = redisArticleRanking.values();
list.sort(Comparator.comparing(ArticleRanking::getCount).reversed());
return list.subList(0, Math.min(10, list.size()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public int like(Long articleId) {
mapper.like(articleId);
return get(articleId).getLikes();
}
}
@@ -1,52 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.api.modules.blog.service.CommentRemindQueueService;
import com.imyeyu.api.modules.common.mapper.CommentRemindQueueMapper;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 评论回复队列服务实现
*
* @author 夜雨
* @since 2021-08-25 00:11
*/
@Service
@RequiredArgsConstructor
public class CommentRemindQueueServiceImplement extends AbstractEntityService<CommentRemindQueue, String> implements CommentRemindQueueService {
private final CommentRemindQueueMapper mapper;
@Override
protected BaseMapper<CommentRemindQueue, String> mapper() {
return mapper;
}
@Override
public List<CommentRemindQueue> listByUserId(Long userId) {
return mapper.listByUserId(userId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByUserId(Long userId) {
CommentRemindQueue example = new CommentRemindQueue();
example.setUserId(userId);
mapper.deleteAllByExample(example);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByReplyId(Long replyId) {
CommentRemindQueue example = new CommentRemindQueue();
example.setReplyId(replyId);
mapper.destroyAllByExample(example);
}
}
@@ -1,27 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.blog.entity.Friend;
import com.imyeyu.api.modules.blog.mapper.FriendMapper;
import com.imyeyu.api.modules.blog.service.FriendService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 友链服务实现
*
* @author 夜雨
* @since 2021-07-15 16:05
*/
@Service
@RequiredArgsConstructor
public class FriendServiceImplement implements FriendService {
private final FriendMapper mapper;
@Override
public List<Friend> listAll() {
return mapper.listAll();
}
}
@@ -1,147 +0,0 @@
package com.imyeyu.api.modules.blog.util;
import com.imyeyu.api.config.RedisConfig;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import com.imyeyu.utils.Time;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* Redis 令牌缓存
*
* <p>一级缓存 Session,二级缓存 Redis,有效期为 {@link com.imyeyu.api.modules.common.entity.Setting.Module.User#TOKEN_TTL} 天,每次触发
* 二级缓存获取时会刷新这个时间,即指定天数内不再访问则视为登出
*
* @author 夜雨
* @since 2023-07-17 16:58
*/
@Component
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserToken {
private final RedisConfig redisConfig;
private final UserService userService;
private final SettingService settingService;
private Redis<String, Long> redis;
@PostConstruct
private void postConstruct() {
redis = redisConfig.getRedis(redisConfig.getDatabase().getUserToken(), RedisSerializers.STRING, RedisSerializers.LONG);
}
public Long set(String token, Long userId) {
long ttl = settingService.getAsTime(Setting.Module.User.TOKEN_TTL);
// 会话
TimiSpring.setSessionAttr(token, userId);
// 跨站 Cookie
Cookie cookie = Objects.requireNonNullElse(TimiSpring.getCookie("Token"), new Cookie("Token", token));
cookie.setDomain(settingService.getAsString(Setting.Module.Domain.ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge((int) (ttl / 1000));
TimiSpring.addCookie(cookie);
// Redis
redis.set(token, userId, ttl);
return Time.now() + ttl;
}
public Long getExpireAt(String token) {
return Time.now() + redis.getExpire(token);
}
/**
* 获取令牌是否有效
*
* @param token 令牌
* @return true 为有效
*/
public boolean isValid(String token) {
return getUserId(token) != null;
}
/**
* 获取令牌是否无效({@link #isValid(String)} 取反)
*
* @param token 令牌
* @return true 为无效
*/
public boolean isInvalid(String token) {
return !isValid(token);
}
/**
* 获取用户 ID
* <p>一级缓存 Session,二级缓存 Redis,每次触发二级缓存获取时会刷新这个时间
* <p>Session 存键为 token,值为 UserIdRedis 也相同
*
* @param token 令牌
* @return 用户 ID
* @throws TimiException 无效 token
*/
public @Nullable Long getRequiredUserId(String token) throws TimiException {
return TimiException.required(getUserId(token), "invalid token");
}
/**
* 获取用户 ID
* <p>一级缓存 Session,二级缓存 Redis,每次触发二级缓存获取时会刷新这个时间
* <p>Session 存键为 token,值为 UserIdRedis 也相同
*
* @param token 令牌
* @return 用户 IDtoken 无效时为 null
*/
public @Nullable Long getUserId(String token) {
if (TimiJava.isEmpty(token)) {
return null;
}
Long userId;
// Session
if (TimiSpring.getSessionAttr(token) instanceof Long sessionUserId) {
userId = sessionUserId;
} else {
// Redis
userId = redis.get(token);
}
if (TimiJava.isEmpty(userId)) {
return null;
}
// 刷新
set(token, userId);
return userId;
}
public @NotNull User getUser(String token) {
return userService.get(getRequiredUserId(token));
}
public void clear(String token) {
// 会话
TimiSpring.removeSessionAttr(token);
// 清除跨站 Cookie
Cookie cookie = new Cookie("Token", "DIED");
cookie.setDomain(settingService.getAsString(Setting.Module.Domain.ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(0);
TimiSpring.addCookie(cookie);
// Redis
redis.destroy(token);
}
}
@@ -1,24 +0,0 @@
package com.imyeyu.api.modules.blog.vo.article;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Tag;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-07 17:19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ArticleView extends Article {
private long comments;
private List<Tag> tagList;
private List<Attachment> attachmentList;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.blog.vo.article;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:52
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ClassPage extends Page {
private long classId;
}
@@ -1,18 +0,0 @@
package com.imyeyu.api.modules.blog.vo.article;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KeywordPage extends Page {
@NotBlank(message = "article.keyword.empty")
private String keyword;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.blog.vo.article;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class LabelPage extends Page {
private long labelId;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.common.bean;
/**
* 支持评论的实体
*
* @author 夜雨
* @since 2023-10-10 11:39
*/
public interface CommentSupport {
/** @return true 为可评论 */
boolean canComment();
/** @return true 为不可评论 */
boolean canNotComment();
}
@@ -1,10 +1,13 @@
package com.imyeyu.api.modules.common.bean; package com.imyeyu.api.modules.common.bean;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* * 设置模块
* *
* @author 夜雨 * @author 夜雨
* @since 2026-04-24 00:59 * @since 2026-04-24 00:59
@@ -13,23 +16,37 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum SettingModule { public enum SettingModule {
COMMON("通用"), COMMON(Setting.Module.Common.class),
DOMAIN("域名"), DOMAIN(Setting.Module.Domain.class),
FOREVER_MC("Forever MC"), FOREVER_MC(Setting.Module.ForeverMC.class),
MULTILINGUAL("多语言环境"), MULTILINGUAL(Setting.Module.Multilingual.class),
GIT("Git"), GITEA(Setting.Module.Gitea.class),
MUSIC("远程音乐"), MUSIC(Setting.Module.Music.class),
JOURNAL("糕雨日记"), JOURNAL(Setting.Module.Journal.class),
TEMP_FILE("临时文件"), TEMP_FILE(Setting.Module.TempFile.class),
SYSTEM("系统"); USER(Setting.Module.User.class),
final String title; SYSTEM(Setting.Module.System.class),
SYSTEM_STATUS(Setting.Module.SystemStatus.class);
/** 模块枚举类 */
final Class<? extends Enum<?>> clazz;
public static <T extends Enum<?>> SettingModule fromClass(Class<T> clazz) {
for (SettingModule value : values()) {
if (value.clazz.equals(clazz)) {
return value;
}
}
throw new TimiException(TimiCode.ERROR, "not found System Module: %s".formatted(clazz.getName()));
}
} }
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
/**
* 附件访问密钥 VO
*
* @author 夜雨
* @since 2026-01-13 10:05
*/
@Data
public class AccessKey {
/** 访问密钥 */
private String value;
/** 附件 ID */
private String attachmentId;
/** true 为一次性访问 */
private boolean isDisposable;
/** 过期时间戳 */
private Long expiredAt;
/** 创建时间戳 */
private Long createdAt;
}
@@ -1,4 +1,4 @@
package com.imyeyu.api.modules.common.bean; package com.imyeyu.api.modules.common.bean.attachment;
/** /**
* @author 夜雨 * @author 夜雨
@@ -1,4 +1,4 @@
package com.imyeyu.api.modules.common.bean; package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -33,10 +33,16 @@ public class Metadata {
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public static class ThumbImage extends Image { public static class ThumbImage extends Image {
private long sourceId; private String sourceId;
private String sourceMongoId; private String sourceMongoId;
private String sourceMimeType; private String sourceMimeType;
/** 请求宽度(缓存键之一,null 表示未指定) */
private Integer requestWidth;
/** 请求高度(缓存键之一,null 表示未指定) */
private Integer requestHeight;
} }
} }
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.common.bean.attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import java.util.List;
/**
*
*
* @author 夜雨
* @since 2026-05-13 17:27
*/
public interface SettingSupport {
String getId();
List<Setting> getSettingList();
}
@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
/**
* 临时文件元数据
*
* @author 夜雨
* @since 2025-09-27 01:47
*/
@Data
public class TempFileMetaData {
/** 附件 ID */
private String id;
/** 文件名 */
private String name;
/** 原始文件名 */
private String originalName;
/** 储存 ID */
private String mongoId;
}
@@ -0,0 +1,68 @@
package com.imyeyu.api.modules.common.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/// 通知消息队列配置
///
/// @author 夜雨
/// @since 2026-08-20
@Data
@Configuration
@ConfigurationProperties(prefix = "notify.queue")
public class NotifyQueueConfig {
private Mail mail = new Mail();
private Sms sms = new Sms();
private Wechat wechat = new Wechat();
private HTTP http = new HTTP();
/// 邮件队列配置
@Data
@EqualsAndHashCode(callSuper = true)
public static class Mail extends Queue {
private String defaultFrom;
}
/// 短信队列配置
@Data
@EqualsAndHashCode(callSuper = true)
public static class Sms extends Queue {
private String appId;
private String sign;
private String secretId;
private String secretKey;
}
/// 微信队列配置
@Data
@EqualsAndHashCode(callSuper = true)
public static class Wechat extends Queue {
}
/// HTTP 队列配置
@Data
@EqualsAndHashCode(callSuper = true)
public static class HTTP extends Queue {
}
/// 通用队列配置
@Data
public static class Queue {
/// true 为启用
private boolean enable;
private String cron = "0/30 * * * * ?";
}
}
@@ -0,0 +1,254 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.UnsupportedEncodingException;
import java.util.List;
///
///
/// @author 夜雨
/// @since 2026-07-27 19:26
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/attach")
public class AttachmentController {
private final TempFileService tempFileService;
private final AttachmentService service;
/**
* 查询附件详情
*
* @param id 附件 ID
* @return 附件详情
*/
@RequestRateLimit
@PostMapping("/detail")
public Attachment detail(@RequestParam String id) {
return service.get(id);
}
/**
* 创建附件
*
* @param attachment 附件
* @return 创建结果
*/
@AOPLog
@RequestRateLimit
@PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Attachment create(Attachment attachment) {
service.create(attachment);
return attachment;
}
/**
* 更新附件
*
* @param attachment 附件
*/
@AOPLog
@RequestRateLimit
@PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void update(Attachment attachment) {
service.update(attachment);
}
/**
* 删除附件
*
* @param id 附件 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
/**
* 按业务查询单个附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @return 附件详情
*/
@RequestRateLimit
@GetMapping("/detail/byBiz")
public Attachment detailByBiz(@RequestParam Attachment.BizType bizType, @RequestParam String bizId) {
return service.getByBizId(bizType, bizId);
}
/**
* 按业务和附件类型查询单个附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachType 附件类型
* @return 附件详情
*/
@RequestRateLimit
@GetMapping("/detail/byAttachType")
public Attachment detailByAttachType(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam String attachType
) {
return service.getByAttachType(bizType, bizId, attachType);
}
/**
* 按业务查询附件列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @return 附件列表
*/
@RequestRateLimit
@GetMapping("/list/biz")
public List<Attachment> listByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList
) {
List<String> types = TimiJava.defaultIfNull(attachTypeList, List.of());
return service.listByBizId(bizType, bizId, types.toArray(String[]::new));
}
/**
* 按业务统计附件数量
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @return 附件数量
*/
@RequestRateLimit
@GetMapping("/count/biz")
public long countByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList
) {
List<String> types = TimiJava.defaultIfNull(attachTypeList, List.of());
return service.countByBizId(bizType, bizId, types.toArray(String[]::new));
}
/**
* 按业务分页查询附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @param page 分页参数
* @return 分页结果
*/
@RequestRateLimit
@PostMapping("/page/biz")
public PageResult<Attachment> pageByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList,
@RequestBody Page<Attachment> page
) {
return service.pageByBizId(bizType, bizId, attachTypeList, page);
}
@AOPLog
@IgnoreGlobalReturn
@GetMapping({"/read/{id}", "/download/{id}"})
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (attach == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
responseAttachment(attach, thumbWidth, thumbHeight);
}
@AOPLog
@PostMapping("/temp/upload")
public List<TempFileResp> upload(@RequestParam("file") List<MultipartFile> files, @RequestParam(value = "ttl", required = false) String ttl) {
return tempFileService.create(files, Time.D);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping({"/temp/read", "/temp/download"})
public void read(@RequestParam String id) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (TimiSpring.getRequest().getRequestURI().endsWith("/download")) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
response(attach);
}
private void responseAttachment(Attachment attach, Integer thumbWidth, Integer thumbHeight) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
if (attach != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attach.getName() != null) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
responseThumb(attach, thumbWidth, thumbHeight);
}
protected void responseThumb(Attachment attachment, Integer thumbWidth, Integer thumbHeight) {
HttpServletResponse resp = TimiSpring.getResponse();
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (thumbWidth == null && thumbHeight == null) {
// 原图
response(attachment);
return;
}
// 缩略图
Attachment thumb = service.fetchThumb(attachment, thumbWidth, thumbHeight);
response(TimiJava.defaultIfNull(thumb, attachment));
}
protected void response(Attachment attachment) {
HttpServletResponse resp = TimiSpring.getResponse();
try {
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(attachment.getMimeType());
attachment.doResponse();
} catch (Exception e) {
log.error("response attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
@@ -1,87 +1,264 @@
package com.imyeyu.api.modules.common.controller; package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid; import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.entity.Comment; import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply; import com.imyeyu.api.modules.user.entity.UserBehaviorRecord;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.common.service.CommentService; import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.vo.comment.CommentCreateReq;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerQueryReq;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerResp;
import com.imyeyu.api.modules.common.vo.comment.CommentNodeResp;
import com.imyeyu.api.modules.common.vo.comment.CommentReplyReq;
import com.imyeyu.api.modules.common.vo.comment.CommentThreadResp;
import com.imyeyu.api.modules.common.vo.comment.FlatCommentListResp;
import com.imyeyu.api.modules.common.vo.comment.NestedCommentListResp;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
* 评论操作接口 * 评论操作接口
* <p>*评论回复只依赖评论而不含业务关联,评论有关联业务,所以此接口是通用接口
* *
* @author 夜雨 * @author 夜雨
* @since 2021-02-23 21:36 * @since 2021-02-23 21:36
*/ */
@Slf4j @Slf4j
@Validated
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/comment") @RequestMapping("/comment")
public class CommentController { public class CommentController {
private final CommentService service; private final CommentService service;
private final CommentReplyService replyService;
/**
* 创建主评论
*
* @param req 创建请求
* @return 评论树详情
*/
@AOPLog @AOPLog
@CaptchaValid(CaptchaFrom.COMMENT) @JsonView(ResponseView.Public.class)
@EnableSetting(comment = Setting.Module.Comment.ENABLE, message = "comment.off_service")
@RequestRateLimit @RequestRateLimit
@PostMapping("/create") @PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<Comment> captchaData) { public Comment create(@RequestBody @Valid CommentCreateReq req) {
service.create(captchaData.getData()); Comment entity = new Comment();
} entity.setBizType(req.getBizType());
entity.setBizId(req.getBizId());
@RequestRateLimit entity.setUserId(req.getUserId());
@PostMapping("/list") entity.setUserNick(req.getUserNick());
public PageResult<Comment> list(@Valid @RequestBody Page<Comment> page) { entity.setContent(req.getContent());
return service.page(page); entity.setIp(TimiSpring.getRequestIP());
service.create(entity);
return service.detailTree(entity.getId(), req.getUserId());
} }
/** /**
* 创建评论回复 * 回复评论
* *
* @param request 回复数据 * @param req 回复请求
* @return 评论树详情
*/ */
@AOPLog @AOPLog
@CaptchaValid(CaptchaFrom.COMMENT_REPLY) @JsonView(ResponseView.Public.class)
@EnableSetting(comment = Setting.Module.Comment.ENABLE, message = "comment.off_service")
@RequestRateLimit @RequestRateLimit
@PostMapping("/reply/create") @PostMapping("/reply")
public void createReply(@Valid @RequestBody CaptchaData<CommentReply> request) { public Comment reply(@RequestBody @Valid CommentReplyReq req) {
replyService.create(request.getData()); Comment entity = new Comment();
entity.setParentId(req.getParentId());
entity.setUserId(req.getUserId());
entity.setUserNick(req.getUserNick());
entity.setContent(req.getContent());
entity.setIp(TimiSpring.getRequestIP());
service.create(entity);
return service.detailTree(entity.getId(), req.getUserId());
} }
/** /**
* 获取回复列表 * 删除当前用户自己的评论
* *
* @param page 页面参数 * @param id 评论 ID
* @return 回复列表 * @param userId 用户 ID
*/ */
@AOPLog
@RequestRateLimit @RequestRateLimit
@RequestMapping("/reply/list") @PostMapping("/delete")
public PageResult<CommentReply> pageReplies(@Valid @RequestBody Page<CommentReply> page) { public void delete(@RequestParam String id, @RequestParam String userId) {
// 通用接口,只允许查询评论的回复 service.deleteOwned(id, userId);
CommentReply example = new CommentReply();
if (page.getEqualsExample() != null) {
example.setCommentId(page.getEqualsExample().getCommentId());
} }
page.setEqualsExample(example);
page.setLikesExample(null); /**
return replyService.page(page); * 点赞评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/like")
public void like(@RequestParam String commentId, @RequestParam String userId) {
service.like(commentId, userId);
}
/**
* 不喜欢评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/unlike")
public void unlike(@RequestParam String commentId, @RequestParam String userId) {
service.unlike(commentId, userId);
}
/**
* 查询用户点赞评论记录
*
* @param page 分页参数
* @param userId 用户 ID
* @return 点赞评论记录分页
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/like/list")
public PageResult<UserBehaviorRecord> listLikedRecord(@RequestBody Page<UserBehaviorRecord> page, @RequestParam String userId) {
return service.listLikedRecord(page, userId);
}
/**
* 查询扁平主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 扁平主评论列表
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/flat/list")
public FlatCommentListResp listFlatRoot(
@RequestParam String bizType,
@RequestParam String bizId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.listFlatRoot(bizType, bizId, offset, currentUserId);
}
/**
* 查询扁平评论线程
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 评论线程
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/flat/thread")
public CommentThreadResp getFlatThread(
@RequestParam String commentId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.getUserThread(commentId, offset, currentUserId);
}
/**
* 查询层级主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 层级主评论列表
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/list")
public NestedCommentListResp listNestedRoot(
@RequestParam String bizType,
@RequestParam String bizId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.listNestedRoot(bizType, bizId, offset, currentUserId);
}
/**
* 查询评论节点
*
* @param commentId 评论 ID
* @param currentUserId 当前用户 ID,可为 null
* @return 评论节点
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/detail")
public CommentNodeResp getNestedDetail(@RequestParam String commentId, @RequestParam(required = false) String currentUserId) {
return service.getUserNode(commentId, currentUserId);
}
/**
* 查询直属子评论分页
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 子评论分页
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/children")
public CommentLayerResp getNestedChildren(
@RequestParam String commentId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.getUserChildren(commentId, offset, currentUserId);
}
/**
* 查询单层评论列表
*
* @param req 查询请求
* @param currentUserId 当前用户 ID,可为 null
* @return 评论层
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/layer/list")
public CommentLayerResp listLayer(@RequestBody CommentLayerQueryReq req, @RequestParam(required = false) String currentUserId) {
return service.listSdkLayer(req, currentUserId);
}
/**
* 查询评论树详情
*
* @param id 评论 ID
* @param currentUserId 当前用户 ID,可为 null
* @return 评论树详情
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/detail/tree")
public Comment detailTree(@RequestParam String id, @RequestParam(required = false) String currentUserId) {
return service.detailTree(id, currentUserId);
} }
} }
@@ -1,75 +1,23 @@
package com.imyeyu.api.modules.common.controller; package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.ImageType;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.Task;
import com.imyeyu.api.modules.common.entity.Template;
import com.imyeyu.api.modules.common.entity.Version;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.ClipboardService;
import com.imyeyu.api.modules.common.service.FeedbackService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TaskService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.service.TemplateService;
import com.imyeyu.api.modules.common.service.VersionService;
import com.imyeyu.api.modules.common.vo.ClipboardRequest;
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.api.modules.system.util.ResourceHandler;
import com.imyeyu.api.util.CaptchaManager; import com.imyeyu.api.util.CaptchaManager;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn; import com.imyeyu.utils.Decoder;
import com.imyeyu.spring.annotation.RequestRateLimit; import jakarta.servlet.ServletOutputStream;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.RequestRange;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.yaml.snakeyaml.Yaml;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* 系统接口 * 系统接口
@@ -84,20 +32,7 @@ import java.util.Map;
@RequestMapping("/") @RequestMapping("/")
public class CommonController { public class CommonController {
private final TaskService taskService;
private final VersionService versionService;
private final SettingService settingService;
private final FeedbackService feedbackService;
private final TemplateService templateService;
private final TempFileService tempFileService;
private final AttachmentService attachmentService;
private final ClipboardService clipboardService;
private final ObjectMapper jackson;
private final Yaml yaml;
private final GridFSBucket gridFSBucket;
private final CaptchaManager captchaManager; private final CaptchaManager captchaManager;
private final ResourceHandler resourceHandler;
@AOPLog @AOPLog
@RequestMapping("") @RequestMapping("")
@@ -105,375 +40,56 @@ public class CommonController {
return "IT WORKING! " + TimiSpring.getRequestIP(); return "IT WORKING! " + TimiSpring.getRequestIP();
} }
/**
* 获取共享剪切板内容
*
* @param id 会话 ID
* @return 剪切板内容
*/
@AOPLog
@RequestRateLimit
@GetMapping("/clipboard/{id}")
public String clipboardGet(@Valid @NotBlank @PathVariable("id") String id) {
return clipboardService.getContent(id);
}
/**
* 设置共享剪切板内容
*
* @param id 会话 ID
* @param request 请求内容
*/
@AOPLog
@RequestRateLimit
@PostMapping("/clipboard/{id}")
public void clipboardSet(@Valid @NotBlank @PathVariable("id") String id, @Valid @RequestBody ClipboardRequest request) {
clipboardService.setContent(id, request.getContent());
}
/**
* 订阅共享剪切板实时更新
*
* @param id 会话 ID
* @return SSE 发射器
*/
@AOPLog
@IgnoreGlobalReturn
@GetMapping(value = "/clipboard/stream/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter clipboardStream(@Valid @NotBlank @PathVariable("id") String id) {
return clipboardService.subscribe(id);
}
/** /**
* 获取验证码 * 获取验证码
* *
* @param width 宽度 * @param width 宽度
* @param height 高度 * @param height 高度
* @param from 来源 * @return Base64 编码的图形验证码和唯一识别 ID
* @param response 返回对象
*/ */
@IgnoreGlobalReturn
@GetMapping("/captcha") @GetMapping("/captcha")
public void captcha(int width, int height, CaptchaFrom from, HttpServletResponse response) { public CaptchaManager.Result captcha(Integer width, Integer height) {
// 返回图像流 TimiException.requiredTrue(width != null && 64 < width, "not found width or less than 64");
response.setHeader("Pragma", "no-cache"); TimiException.requiredTrue(height != null && 19 < height, "not found height or less than 19");
response.setHeader("Cache-Control", "no-cache"); // 禁止缓存 return captchaManager.generate(width, height);
response.setDateHeader("Expires", -1); }
response.setContentType("image/jpg");
/**
* 获取验证码(返回图形数据流,唯一识别 ID 在返回头 X-Captcha-ID
*
* @param width 宽度
* @param height 高度
*/
@GetMapping("/captcha/img")
public void captchaImg(Integer width, Integer height, HttpServletResponse resp) {
CaptchaManager.Result result = captchaManager.generate(width, height);
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("X-Captcha-ID", result.id());
resp.setHeader("Access-Control-Expose-Headers", "X-Captcha-ID");
resp.setDateHeader("Expires", -1);
resp.setContentType("image/png");
try { try {
// 宽度 ServletOutputStream os = resp.getOutputStream();
if (width < 64) { if (width < 64) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream()); ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "png", os);
return; return;
} }
// 高度 // 高度
if (height < 19) { if (height < 19) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream()); ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "png", os);
return;
}
// 来自
if (TimiJava.isEmpty(from)) {
ImageIO.write(captchaManager.error(TimiCode.ARG_MISS), "jpg", response.getOutputStream());
return; return;
} }
// 输出图像流 // 输出图像流
ImageIO.write(captchaManager.generate(from, width, height), "jpg", response.getOutputStream()); IO.toOutputStream(os, Decoder.base64(result.data().split(",")[1]));
} catch (Exception e) { } catch (IOException e) {
log.error("CommonController.getCaptcha", e); log.error("captcha img error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try { try {
ImageIO.write(captchaManager.error(TimiCode.ERROR), "jpg", response.getOutputStream()); ImageIO.write(captchaManager.error(TimiCode.ERROR), "png", resp.getOutputStream());
} catch (IOException subE) { } catch (IOException subE) {
log.error("write error image fail", subE); log.error("write error image fail", subE);
} }
} }
} }
/**
* 获取软件最新版本状态
*
* @param name 软件名称
* @return 最新版本状态
* @deprecated 兼容旧程序
*/
@AOPLog
@GetMapping("/versions/{name}")
@Deprecated
public Version versions(@Valid @NotBlank @PathVariable("name") String name) {
return versionService.getByName(name);
}
/**
* 获取软件最新版本状态
*
* @param name 软件名称
* @return 最新版本状态
*/
@AOPLog
@GetMapping("/version/{name}")
public Version version(@Valid @NotBlank @PathVariable("name") String name) {
return versionService.getByName(name);
}
@AOPLog
@RequestRateLimit
@PostMapping("/feedback")
public void createFeedback(@Valid @NotNull @RequestBody CaptchaData<FeedbackRequest> request) {
captchaManager.test(request.getCaptcha(), request.getCaptchaId());
feedbackService.create(request.getData());
}
/** @return 公开任务信息 */
@AOPLog
@RequestRateLimit
@GetMapping("/tasklist")
public List<Task> getTasks() {
return taskService.listAll4Public();
}
@RequestRateLimit
@GetMapping("/template")
public String viewTemplate(@RequestParam Template.BizType bizType, @RequestParam String bizCode) {
return templateService.get(bizType, bizCode).getData();
}
@RequestRateLimit
@GetMapping("/setting")
public String settingByKey(@RequestParam String module, @RequestParam String key) throws ClassNotFoundException {
Setting setting = settingService.getByKey(SettingService.fromModuleKey(module, key));
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
return setting.getValue();
}
@RequestRateLimit
@PostMapping("/setting/map")
public Map<String, Map<String, String>> mapSettingByKeys(@RequestBody Map<String, List<String>> moduleKeyMap) {
List<Enum<?>> keyList = new ArrayList<>();
moduleKeyMap.forEach((k, v) -> {
try {
for (String key : v) {
keyList.add(SettingService.fromModuleKey(k, key));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
Map<String, Map<String, String>> result = new HashMap<>();
List<Setting> settingList = settingService.listByKeys(keyList);
for (Setting setting : settingList) {
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
if (!result.containsKey(setting.getModule())) {
result.put(setting.getKey(), new HashMap<>());
}
result.get(setting.getModule()).put(setting.getKey(), setting.getValue());
}
return result;
}
@AOPLog
@RequestRateLimit
@GetMapping("/attachment/{mongoId}")
public Attachment getAttachment(@PathVariable String mongoId) {
return attachmentService.getByMongoId(mongoId);
}
@AOPLog
@IgnoreGlobalReturn
@GetMapping("/attachment/read/{mongoId}")
public void readAttachment(
@PathVariable String mongoId,
@RequestParam(name = "size", required = false) Integer size,
@RequestParam(name = "type", required = false) String type,
HttpServletRequest req,
HttpServletResponse resp
) {
try {
GridFSFile file = attachmentService.readByMongoId(mongoId);
if (file == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
return;
}
GridFSDownloadStream mimeReadStream = gridFSBucket.openDownloadStream(file.getObjectId());
String mimeType = new Tika().detect(mimeReadStream);
if (TimiJava.isNotEmpty(mimeType)) {
resp.setContentType(mimeType);
}
if (size != null) {
String fileType = switch (mimeType) {
case "image/png" -> "png";
case "image/jpeg" -> "jpg";
default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType");
};
switch (mimeType) {
case "image/png", "image/jpeg" -> {
// 图片缩放
GridFSDownloadStream stream = gridFSBucket.openDownloadStream(file.getObjectId());
byte[] bytes = IO.toBytes(stream);
BufferedImage imgSrc = ImageIO.read(new ByteArrayInputStream(bytes));
double scale;
if (imgSrc.getHeight() < imgSrc.getWidth()) {
// 横向
scale = 1D * size / imgSrc.getWidth();
} else {
scale = 1D * size / imgSrc.getHeight();
}
int width = (int) (imgSrc.getWidth() * scale);
int height = (int) (imgSrc.getHeight() * scale);
BufferedImage imgResult = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = imgResult.createGraphics();
if (ImageType.PIXELATED == Ref.toType(ImageType.class, type)) {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
} else {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.drawImage(imgSrc, 0, 0, width, height, null);
ImageIO.write(imgResult, fileType, resp.getOutputStream());
}
default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType");
}
} else {
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
GridFsResource gridFsResource = new GridFsResource(file, downloadStream);
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.MONGO);
req.setAttribute(ResourceHandler.ATTR_VALUE, gridFsResource);
resourceHandler.handleRequest(req, resp);
}
} catch (Exception e) {
log.error("read attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/attachment/download/{mongoId}")
public void downloadAttachment(@PathVariable String mongoId, HttpServletResponse resp) {
try {
Attachment attachment = attachmentService.getByMongoId(mongoId);
GridFSFile file = attachmentService.readByMongoId(mongoId);
if (file == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
return;
}
{
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
String mimeType = new Tika().detect(downloadStream);
if (TimiJava.isNotEmpty(mimeType)) {
resp.setContentType(mimeType);
}
}
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getFilename()));
resp.setHeader("Content-Range", String.valueOf(attachment.getSize()));
resp.setHeader("Accept-Ranges", "bytes");
resp.setContentLengthLong(attachment.getSize());
IO.toOutputStream(downloadStream, resp.getOutputStream());
resp.flushBuffer();
} catch (Exception e) {
log.error("read attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
/**
* 上传临时文件
*
* @param files 文件列表
* @param ttl 缓存时长(毫秒),最长 3 天(259200000ms
* @return 临时文件响应列表
*/
@AOPLog
@PostMapping("/temp/file/upload")
public List<TempFileResponse> tempFileUpload(@RequestParam("file") List<MultipartFile> files, @RequestParam(value = "ttl", required = false) Long ttl) {
return tempFileService.store(files, ttl);
}
/**
* 读取临时文件
*
* @param mongoId mongoId
* @param req 请求
* @param resp 返回
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/temp/file/read/{mongoId}")
public void tempFileRead(@PathVariable String mongoId, HttpServletRequest req, HttpServletResponse resp) {
try {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
GridFSFile file = attachmentService.readByMongoId(attach.getMongoId());
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
GridFsResource gridFsResource = new GridFsResource(file, downloadStream);
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.MONGO);
req.setAttribute(ResourceHandler.ATTR_VALUE, gridFsResource);
resp.setContentType(attach.getMimeType());
resourceHandler.handleRequest(req, resp);
} catch (Exception e) {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 下载临时文件
*
* @param mongoId
* @param resp
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequestMapping("/temp/file/download/{mongoId}")
public void tempFileDownload(@PathVariable String mongoId, HttpServletResponse resp) {
try {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(attach.getMimeType());
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
resp.setHeader("Accept-Ranges", "bytes");
GridFSFile file = attachmentService.readByMongoId(mongoId);
@Cleanup
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
RequestRange range = TimiSpring.getRequestRange(attach.getSize());
if (range == null) {
// 完整文件
resp.setContentLengthLong(attach.getSize());
resp.setStatus(HttpServletResponse.SC_OK);
IO.toOutputStream(downloadStream, resp.getOutputStream());
} else {
// 分片文件
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), attach.getSize()));
resp.setContentLengthLong(range.getLength());
IO.toOutputStream(downloadStream, resp.getOutputStream(), range.getStart(), range.getEnd());
resp.flushBuffer();
}
} catch (Exception e) {
log.error("download error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
} }
@@ -0,0 +1,100 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Feedback;
import com.imyeyu.api.modules.common.service.FeedbackService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.CaptchaValid;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// 通用反馈接口
///
/// @author Codex
/// @since 2026-08-19
@RestController
@RequiredArgsConstructor
@RequestMapping("/feedback")
public class FeedbackController {
private final FeedbackService service;
private final UserLoginService userLoginService;
/// 提交反馈。登录用户和匿名用户均需提交图形验证码,匿名用户按 IP 限流
///
/// @param captchaData 带图形验证码的反馈请求
@AOPLog
@JsonView(ResponseView.Public.class)
@CaptchaValid
@RequestRateLimit(value = 3, inSeconds = 60)
@PostMapping("/create")
public void create(@RequestBody @Valid CaptchaData<Feedback> captchaData) {
Feedback feedback = captchaData.getData();
feedback.setIp(TimiSpring.getRequestIP());
if (userLoginService.isLogged()) {
feedback.setUserId(userLoginService.getRequireLoginUserId());
}
service.create(feedback);
}
/// 查询反馈列表
///
/// @param page 分页参数
/// @return 反馈分页
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.FEEDBACK_READ)
@PostMapping("/list")
public PageResult<Feedback> list(@RequestBody Page<Feedback> page) {
return service.page(page);
}
/// 查询反馈详情
///
/// @param id 反馈 ID
/// @return 反馈详情
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.FEEDBACK_READ)
@PostMapping("/detail")
public Feedback detail(@RequestParam String id) {
return service.get(id);
}
/// 更新反馈
///
/// @param feedback 反馈
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.FEEDBACK_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Feedback feedback) {
feedback.setRespondedBy(userLoginService.getRequireLoginUserId());
service.update(feedback);
}
/// 删除反馈
///
/// @param id 反馈 ID
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.FEEDBACK_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
}
@@ -1,26 +1,19 @@
package com.imyeyu.api.modules.common.controller; package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Icon;
import com.imyeyu.api.modules.common.service.IconService; import com.imyeyu.api.modules.common.service.IconService;
import com.imyeyu.api.modules.common.vo.icon.AllResponse; import com.imyeyu.network.Network;
import com.imyeyu.api.modules.common.vo.icon.NamePage; import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.api.modules.common.vo.icon.UnicodePage; import jakarta.servlet.http.HttpServletResponse;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* timi-icon 前端接口 * timi-icon 前端接口
@@ -38,60 +31,38 @@ public class IconController {
private final IconService service; private final IconService service;
/** /**
* 获取图标列表 * 查询全部图标名称
* *
* @param page 查询列表参数 * @return 图标名称列表
* @return 图标列表
*/ */
@RequestRateLimit @GetMapping("/name/list")
@PostMapping("/list") public List<String> listName() {
public PageResult<Icon> list(@RequestBody Page page) { return service.listName();
return service.page(page); }
@GetMapping("/svg/map")
public Map<String, String> mapSvg() {
return service.mapSvg();
}
@GetMapping("/unicode/map")
public Map<String, String> mapUnicode() {
return service.mapUnicode();
} }
/** /**
* 获取所有图标,为了减小传输数据,此接口只返回 name 名称、Unicode 代码和 SVG 路径 * 导出 TTF 字体文件
* *
* @param latest 请求缓存的最新数据时间 * @param resp HTTP 响应
* @return 所有图标,如果请求缓存的最新时间等于数据库的最新数据时间,不返回任何数据 * @throws IOException 写出字体文件失败
*/ */
@RequestRateLimit @IgnoreGlobalReturn
@GetMapping("/list/all") @GetMapping(value = "/export/ttf", produces = "font/ttf")
public AllResponse listAll(@Valid @RequestParam Long latest) { public void exportTtf(HttpServletResponse resp) throws IOException {
AllResponse resp = service.listAll(latest); byte[] body = service.exportTtf();
List<Icon> icons = resp.getIcons(); resp.setContentType("font/ttf");
for (int i = 0; i < icons.size(); i++) { resp.setContentLength(body.length);
Icon icon = icons.get(i); resp.setHeader("Content-Disposition", Network.getFileDownloadHeader("timi-icon.ttf"));
icon.setId(null); resp.getOutputStream().write(body);
icon.setCreatedAt(null);
icon.setUpdatedAt(null);
}
return resp;
}
/**
* 根据名称获取查询列表参数列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list/name")
public PageResult<Icon> listByName(@Valid @RequestBody NamePage page) {
return service.pageByName(page);
}
/**
* 根据 Unicode 获取图标列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list/unicode")
public PageResult<Icon> listByUnicode(@Valid @RequestBody UnicodePage page) {
return service.pageByUnicode(page);
} }
} }
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequiredToken;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
/**
*
*
* @author 夜雨
* @since 2026-06-13 09:31
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/log")
public class LoggerController {
private final LoggerService service;
@AOPLog
@RequiredToken
@PostMapping("/create")
public void create(@Valid @RequestBody Logger logger) {
service.create(logger);
}
}
@@ -0,0 +1,88 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 多语言管理接口
*
* @author Codex
* @since 2026-07-27
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/multilingual")
public class MultilingualController {
private final MultilingualService service;
/**
* 查询多语言详情
*
* @param id 多语言 ID
* @return 多语言详情
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_READ)
@PostMapping("/detail")
public Multilingual detail(@RequestParam String id) {
return service.get(id);
}
/**
* 创建多语言
*
* @param multilingual 多语言
* @return 创建结果
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_CREATE)
@PostMapping("/create")
public Multilingual create(@RequestBody Multilingual multilingual) {
service.create(multilingual);
return multilingual;
}
/**
* 更新多语言
*
* @param multilingual 多语言
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Multilingual multilingual) {
service.update(multilingual);
}
/**
* 删除多语言
*
* @param id 多语言 ID
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
}
@@ -0,0 +1,174 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/// 通知接口
///
/// @author 夜雨
/// @since 2026-08-20
@RestController
@RequiredArgsConstructor
@RequestMapping("/notify")
public class NotifyController {
private final UserService userService;
private final NotifyService service;
private final UserLoginService userLoginService;
private final NotifyDetailService detailService;
/// 创建通知,同时创建通知详情
///
/// @param notify 通知及通知详情
/// @return 创建后的通知
@RequireCorePermission(CorePermissionCode.NOTIFY_CREATE)
@RequiredToken
@PostMapping("/create")
public Notify create(@RequestBody Notify notify) {
service.create(notify);
return notify;
}
/// 查询通知详情
///
/// @param id 通知 ID
/// @return 通知及通知详情
@JsonView(ResponseView.Public.class)
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@RequestMapping("/detail")
public Notify detail(@RequestParam String id) {
Notify notify = service.get(id);
{
List<NotifyDetail> detailList = detailService.listByNotifyId(id);
Set<String> userIdSet = detailList.stream()
.filter(item -> item.getMsgType() == NotifyDetail.MsgType.INTERNAL)
.flatMap(item -> Stream.of(item.getSendFrom(), item.getSendTo()))
.collect(Collectors.toSet());
Map<String, User> userMap = userService.mapByIdList(userIdSet);
for (NotifyDetail detail : detailList) {
if (TimiJava.isNotEmpty(detail.getSendFrom())) {
detail.setFromUser(userMap.get(detail.getSendFrom()));
}
if (TimiJava.isNotEmpty(detail.getSendTo())) {
detail.setToUser(userMap.get(detail.getSendTo()));
}
}
notify.setDetailList(detailList);
}
return notify;
}
/// 删除通知及其通知详情
///
/// @param id 通知 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_DELETE)
@RequiredToken
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
/// 取消尚未成功发送的通知详情
///
/// @param id 通知 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_CANCEL)
@RequiredToken
@PostMapping("/cancel")
public void cancel(@RequestParam String id) {
service.cancel(id);
}
/// 查询当前用户的站内通知
///
/// @param page 分页参数
/// @param unreadOnly true 为只查询未读通知
/// @return 站内通知分页结果
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/internal")
public PageResult<NotifyDetail> listInternal(
@RequestBody(required = false) Page<NotifyDetail> page,
@RequestParam(defaultValue = "false") boolean unreadOnly
) {
return detailService.pageInternalByUserId(page, userLoginService.getRequireLoginUserId(), unreadOnly);
}
@JsonView(ResponseView.Public.class)
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/detail")
public NotifyDetail internalDetail(@RequestParam String id) {
NotifyDetail detail = detailService.getInternalByUserId(id, userLoginService.getRequireLoginUserId());
if (TimiJava.isNotEmpty(detail.getSendFrom())) {
detail.setFromUser(userService.get(detail.getSendFrom()));
}
if (TimiJava.isNotEmpty(detail.getSendTo())) {
detail.setToUser(userService.get(detail.getSendTo()));
}
return detail;
}
/// 删除当前用户的一条站内通知
///
/// @param id 通知详情 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/delete")
public void deleteInternal(@RequestParam String id) {
detailService.deleteInternalByUserId(id, userLoginService.getRequireLoginUserId());
}
/// 查询当前用户未读通知数量
///
/// @return 未读通知数量
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/unread/count")
public long countUnreadSite() {
return detailService.countUnreadSiteByUserId(userLoginService.getRequireLoginUserId());
}
/// 标记当前用户的一条站内通知已读
///
/// @param id 通知详情 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/read")
public void markSiteRead(@RequestParam String id) {
detailService.markSiteRead(userLoginService.getRequireLoginUserId(), id);
}
/// 标记当前用户全部站内通知已读
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/read/all")
public void markAllSiteRead() {
detailService.markAllSiteRead(userLoginService.getRequireLoginUserId());
}
}
@@ -0,0 +1,100 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
/**
*
*
* @author 夜雨
* @since 2026-04-23 16:31
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/setting")
public class SettingController {
private final SettingService service;
/**
* 查询配置列表
*
* @param page 分页查询
* @return 配置列表
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_READ)
@PostMapping("/list")
public PageResult<Setting> list(@RequestBody Page<Setting> page) {
return service.page(page);
}
/**
* 查询配置详情
*
* @param id 配置 ID
* @return 配置详情
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@PostMapping("/detail")
public Setting detail(@RequestBody String id) {
return service.get(id);
}
/**
* 创建配置
*
* @param setting 配置
* @return 配置
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@PostMapping("/create")
public Setting create(@RequestBody Setting setting) {
service.create(setting);
return setting;
}
/**
* 更新配置
*
* @param setting 配置
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Setting setting) {
service.update(setting);
}
/**
* 删除配置
*
* @param id 配置 ID
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_DELETE)
@PostMapping("/delete")
public void delete(@RequestBody String id) {
service.delete(id);
}
}
@@ -0,0 +1,111 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// 标签接口
///
/// @author 夜雨
/// @since 2026-08-12 15:09
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/tag")
public class TagController {
private final TagService service;
/// 查询标签列表
///
/// @param page 分页参数
/// @return 标签分页
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/list")
public PageResult<Tag> list(@RequestBody Page<Tag> page) {
return service.page(page);
}
/// 查询标签详情
///
/// @param id 标签 ID
/// @return 标签详情
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/detail")
public Tag detail(@RequestParam String id) {
return service.get(id);
}
/// 创建标签
///
/// @param tag 标签
/// @return 标签
@AOPLog
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/create")
public Tag create(@RequestBody Tag tag) {
service.create(tag);
return tag;
}
/// 更新标签
///
/// @param tag 标签
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Tag tag) {
service.update(tag);
}
/// 删除标签
///
/// @param id 标签 ID
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
/// 按业务查询标签
///
/// @param bizType 业务类型
/// @param bizId 业务 ID
/// @return 标签列表
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@GetMapping("/list/biz/id")
public List<Tag> listByBizType(@RequestParam TagApply.BizType bizType, @RequestParam String bizId) {
return service.listByBizType(bizType, bizId);
}
/// 差分保存业务标签
///
/// @param apply 标签请求
@AOPLog
@RequestRateLimit
@PostMapping("/apply")
public void apply(@RequestBody TagApply apply) {
service.apply(apply.getBizType(), apply.getBizId(), apply.getTagIdList());
}
}
@@ -1,316 +0,0 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.service.UserConfigService;
import com.imyeyu.api.modules.common.service.UserPrivacyService;
import com.imyeyu.api.modules.common.service.UserProfileService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.common.vo.user.EmailVerifyCallbackRequest;
import com.imyeyu.api.modules.common.vo.user.LoginRequest;
import com.imyeyu.api.modules.common.vo.user.LoginResponse;
import com.imyeyu.api.modules.common.vo.user.RegisterRequest;
import com.imyeyu.api.modules.common.vo.user.UpdatePasswordByKeyRequest;
import com.imyeyu.api.modules.common.vo.user.UpdatePasswordRequest;
import com.imyeyu.api.modules.common.vo.user.UserRequest;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户接口
*
* @author 夜雨
* @since 2021-02-23 21:38
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController implements TimiJava {
private final UserService service;
private final CommentService commentService;
private final UserConfigService configService;
private final UserProfileService profileService;
private final UserPrivacyService privacyService;
private final CommentReplyService commentReplyService;
/**
* 注册。执行成功会自动登录
*
* @param request 注册请求
* @return 登录数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.REGISTER)
@EnableSetting(user = Setting.Module.User.ENABLE_REGISTER, message = "user.register.off_service")
@RequestRateLimit(value = 1, lifeCycle = 60)
@PostMapping("/register")
public LoginResponse register(@Valid @RequestBody CaptchaData<RegisterRequest> request) {
return service.register(request.getData());
}
/**
* 登录
*
* @param request 登录请求
* @return 登录数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.LOGIN)
@EnableSetting(user = Setting.Module.User.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody CaptchaData<LoginRequest> request) {
return service.login(request.getData());
}
/**
* 根据令牌登录,请求头携带 Token 参数,通常用于延续登录令牌
*
* @return 登录数据
*/
@AOPLog
@EnableSetting(user = Setting.Module.User.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login/token")
public LoginResponse login4Token() {
return service.login4Token();
}
/** 登出 */
@AOPLog
@RequestRateLimit
@PostMapping("/logout")
public void logout() {
service.logout();
}
/** 发送邮箱验证邮件 */
@AOPLog
@RequiredToken
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/email/verify")
public void sendEmailVerify() {
service.sendEmailVerify();
}
/**
* 邮箱验证邮件回调,验证请求的密钥来源于 {@link #sendEmailVerify()} 接口发送的邮件
*
* @param request 邮箱验证请求
*/
@AOPLog
@RequiredToken
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/email/verify/callback")
public void emailVerifyCallback(@Valid @RequestBody EmailVerifyCallbackRequest request) {
service.emailVerifyCallback(request.getKey());
}
/**
* 修改密码,需要已登录状态,使用旧密码修改
*
* @param request 修改密码请求
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/password/update")
public void updatePassword(@Valid @RequestBody UpdatePasswordRequest request) {
service.updatePassword(request.getOldValue(), request.getNewValue());
}
/**
* 发送用于重置密码的忘记密码邮件,入参数据可能是 UID、邮箱或用户名,该数据目标用户的邮箱需要通过验证
*
* @param request 忘记密码邮件请求
*/
@AOPLog
@CaptchaValid(CaptchaFrom.RESET_PASSWORD)
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/password/forget")
public void sendPasswordForgetVerify(@Valid @RequestBody CaptchaData<String> request) {
service.sendPasswordForgetVerify(request.getData());
}
/**
* 修改密码,不需要登录状态,入参数据的密钥来源于 {@link #sendPasswordForgetVerify(CaptchaData)} 接口发送的邮件
*
* @param request 重置密码请求
*/
@AOPLog
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/password/reset")
public void resetPasswordByKey(@Valid @RequestBody UpdatePasswordByKeyRequest request) {
service.resetPasswordByKey(request.getKey(), request.getNewPassword());
}
/**
* 注销账号,此操作将会标记此用户的所有数据为删除状态
*
* @param password 密码
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/cancel")
public void cancel(@RequestBody String password) {
service.cancel(password);
}
/**
* 获取用户资料
*
* @param userId 目标用户 ID
* @return 用户资料
*/
@AOPLog
@RequestRateLimit
@PostMapping("/view/{userId}")
public UserView view(@Min(1) @NotNull @PathVariable Long userId) throws Exception {
return service.view(userId).doFilter();
}
/**
* 更新用户数据
*
* @param data 用户数据(包括账号数据)
*/
@AOPLog
@RequiredToken
@EnableSetting(user = Setting.Module.User.ENABLE_PROFILE_UPDATE, message = "user.data.off_service")
@RequestRateLimit
@PostMapping("/profile/update")
public void updateProfile(@Valid UserRequest data) {
profileService.update(data);
}
/**
* 获取用户隐私控制
*
* @return 用户资料
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/privacy")
public UserPrivacy privacy() {
return privacyService.get(service.getLoginUser().getId());
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/privacy/update")
public void updatePrivacy(@Valid @RequestBody UserPrivacy privacy) {
privacyService.update(privacy);
}
/**
* 获取用户设置
*
* @return 用户设置
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/config")
public UserConfig config() {
return configService.get(service.getLoginUser().getId());
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/config/update")
public void updateConfig(@Valid @RequestBody UserConfig config) {
configService.update(config);
}
/**
* 获取用户评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/list")
public PageResult<Comment> listComment(@Valid @RequestBody Page<Comment> page) {
Comment example = new Comment();
example.setUserId(service.getLoginUser().getId());
page.setEqualsExample(example);
return commentService.page(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/delete")
public void deleteComment(@RequestBody Long commentId) {
commentService.get(commentId);
commentService.delete(commentId);
}
/**
* 获取用户被回复的评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/list")
public PageResult<CommentReply> listCommentReply(@Valid @RequestBody Page<CommentReply> page) {
CommentReply example = new CommentReply();
example.setReceiverId(service.getLoginUser().getId());
page.setEqualsExample(example);
return commentReplyService.page(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/delete")
public void deleteCommentReply(@RequestBody Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getSenderId().equals(service.getLoginUser().getId()), "user.comment.reply.delete.not_owner");
commentReplyService.delete(replyId);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/ignore")
public void ignoreCommentReply(@RequestBody Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getReceiverId().equals(service.getLoginUser().getId()), "user.comment.reply.ignore.not_owner");
reply.setIgnoredAt(Time.now());
commentReplyService.update(reply);
}
}
@@ -0,0 +1,104 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 文章
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Article extends UUIDEntity implements SettingSupport {
/**
* 类型
*
* @author 夜雨
* @since 2026-06-05 15:55
*/
public enum BizType {
/** 博客 */
BLOG,
/// 反馈
FEEDBACK
}
/**
* 正文类型
*
* @author 夜雨
* @since 2026-06-05 15:56
*/
public enum ContentType {
/** 纯文本 */
TEXT,
/** Markdown */
MARKDOWN,
/** TipTap 富文本 */
TIP_TAP
}
/// 文章状态
public enum Status {
/// 草稿
DART,
/// 正常
NORMAL,
/// 隐藏
HIDDEN
}
/** 业务类型 */
protected BizType bizType;
/** 业务 ID */
protected String bizId;
/** 标题 */
protected String title;
/** 描述 */
protected String description;
/** 正文类型 */
protected ContentType contentType;
/** 内容 */
@PageIgnore
protected String content;
/** 阅读数量 */
protected Integer reads;
/** 喜欢数量 */
protected Integer likes;
/** 文章状态 */
protected Status status;
@Transient
protected List<Setting> settingList;
@Transient
protected List<Comment> commentList;
@Transient
protected List<Attachment> attachmentList;
}
@@ -1,17 +1,19 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.TimiServerAPI; import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.bean.MultilingualHandler; import com.imyeyu.api.modules.common.bean.attachment.AccessKey;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.ref.Ref; import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
@@ -20,68 +22,42 @@ import java.io.InputStream;
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Attachment extends Entity implements MultilingualHandler { public class Attachment extends UUIDEntity {
/** ///
* 附件类型 ///
* /// @author 夜雨
* @author 夜雨 /// @since 2026-07-17 20:52
* @since 2023-08-21 16:32
*/
@Getter
@AllArgsConstructor
public enum BizType { public enum BizType {
/** 用户 */
USER,
/** 文章 */
ARTICLE, ARTICLE,
/** Git */ USER,
GIT,
/** 歌词 */ GAO_CUSTOMER,
LYRIC,
/** ForeverMC */ GAO_EVENT_RECORD,
FMC,
/** 镜像 */ TEMP_FILE,
MIRROR,
/** 日记 */ MIRROR
JOURNAL,
/** 日记出行 */
JOURNAL_TRAVEL,
/** 日记瞬间 */
JOURNAL_MOMENT,
/** 系统 */
SYSTEM,
/** 临时文件 */
TEMP_FILE
} }
protected BizType bizType; protected BizType bizType;
protected Long bizId; protected String bizId;
protected String attachType; protected String attachType;
protected String mongoId; protected String mongoId;
@MultilingualField
protected String title; protected String title;
protected String name; protected String name;
protected String mimeType; protected String mimeType;
protected JsonNode metadata; protected ObjectNode metadata;
protected Long size; protected Long size;
@@ -93,9 +69,30 @@ public class Attachment extends Entity implements MultilingualHandler {
protected Long destroyAt; protected Long destroyAt;
/** 数据流 */
@Transient @Transient
protected InputStream inputStream; protected InputStream inputStream;
/** 保存自临时文件 ID */
@Transient
protected String tempFileId;
/** 访问密钥 */
@Transient
protected AccessKey accessKey;
/** 请求文件 */
@Transient
protected MultipartFile file;
public String buildMongoName() {
StringBuilder result = new StringBuilder(bizType.name());
if (TimiJava.isNotEmpty(attachType)) {
result.append('_').append(attachType.toUpperCase());
}
return result.append('_').append(name).toString();
}
public InputStream openInputStream() { public InputStream openInputStream() {
AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class); AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class);
return service.getInputStreamByMongoId(mongoId); return service.getInputStreamByMongoId(mongoId);
@@ -108,4 +105,9 @@ public class Attachment extends Entity implements MultilingualHandler {
public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) { public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) {
return Ref.toType(attachTypeClass, attachType); return Ref.toType(attachTypeClass, attachType);
} }
/** 执行接口流式返回 */
public void doResponse() throws IOException {
TimiSpring.responseRangeStream(openInputStream(), size);
}
} }
@@ -0,0 +1,60 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Destroyable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
import lombok.NoArgsConstructor;
/// 通知基础队列实体
///
/// @author 夜雨
/// @since 2026-08-20
@Data
@NoArgsConstructor
public class BaseQueueEntity implements IDEntity<String>, Creatable, Destroyable {
/** 主键 */
@Id
@AutoUUID
protected String id;
/** 所属通知详情 ID */
protected String notifyDetailId;
/** 内容 */
protected String data;
/** 参数 */
protected String args;
/** 发送去向 */
protected String sendTo;
/** 发送时间 */
protected Long sendAt;
/** 剩余重试次数 */
protected Integer retry;
/** 超时时间 */
protected Long timeoutAt;
/** 创建时间 */
protected Long createdAt;
/// 从通知详情创建队列实体
///
/// @param detail 通知详情
public BaseQueueEntity(NotifyDetail detail) {
notifyDetailId = detail.getId();
data = detail.getData();
args = detail.getArgs();
sendTo = detail.getSendTo();
sendAt = detail.getSendAt();
retry = detail.getRetry();
timeoutAt = detail.getTimeoutAt();
}
}
@@ -1,20 +1,11 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.blog.entity.Article; import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.blog.service.implement.ArticleServiceImplement;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.api.modules.git.service.implement.IssueServiceImplement;
import com.imyeyu.api.modules.git.service.implement.MergeServiceImplement;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity; import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.service.GettableService;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
@@ -23,71 +14,94 @@ import java.util.List;
* 评论 * 评论
* *
* @author 夜雨 * @author 夜雨
* @since 2021-02-25 14:46 * @since 2026-05-27 17:20
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Comment extends Entity { public class Comment extends UUIDEntity {
/** /**
* 关联业务类型 * 评论状态
* <p>
* TODO 添加模块名称以便区别邮件通知推送来源,使用多语言键
* *
* @author 夜雨 * @author 夜雨
* @since 2023-08-06 23:42 * @since 2026-05-27 17:20
*/ */
@Getter public enum Status {
@AllArgsConstructor
public enum BizType {
ARTICLE(ArticleServiceImplement.class), /** 正常 */
NORMAL,
GIT_ISSUE(IssueServiceImplement.class), /** 隐藏 */
HIDDEN
GIT_MERGE(MergeServiceImplement.class);
final Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass;
} }
/** 关联业务类型 */ /** 业务类型 */
private BizType bizType; protected String bizType;
/** 关联业务 ID */ /** 业务 ID */
private Long bizId; protected String bizId;
/** 发送用户 ID,登录用户评论有值,游客无 */ /** 父评论 ID,主评论为 null */
private Long userId; protected String parentId;
/** 发送用户昵称,游客评论有值,登录用户无 */ /** 根评论 ID,主评论为自身 ID */
private String nick; protected String rootId;
/** 评论数据 */ /** 评论用户 ID */
protected String userId;
/** 评论用户昵称,游客评论时使用 */
protected String userNick;
/** 被回复用户 ID */
protected String replyUserId;
/** 被回复用户昵称,被回复人为游客时使用 */
protected String replyUserNick;
/** 评论层级,1 为主评论 */
protected Integer level;
/** 评论内容 */
@NotBlank @NotBlank
private String content; protected String content;
/** 发送用户 IP */ /** IP 地址 */
private String ip; protected String ip;
/** 回复数 */ /** 回复数 */
protected Integer replyCount;
/** 点赞数 */
protected Integer likeCount;
/** 评论状态 */
protected Status status;
/** true 为置顶 */
protected Boolean isTop;
/** 最后回复时间 */
protected Long lastReplyAt;
/** 评论用户信息 */
@Transient @Transient
private long repliesLength; protected User user;
/** 发送用户 */ /** 被回复用户信息 */
@Transient @Transient
private UserView user; protected User replyUser;
/** 关联文章 */ /** 子评论列表 */
@Transient @Transient
private Article article; protected List<Comment> replyList;
/** 关联仓库 */ /** true 为当前用户已点赞 */
@Transient @Transient
private Repository repository; protected Boolean liked;
/** 回复列表 */ /** true 为当前用户已不喜欢 */
@Transient @Transient
private List<CommentReply> replies; protected Boolean unliked;
} }
@@ -1,59 +0,0 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 评论回复
*
* @author 夜雨
* @since 2021-03-01 17:11
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CommentReply extends Entity {
/** 所属评论 ID */
private Long commentId;
/** 被回复的回复,回复主评论时为 NULL */
private Long replyId;
/** 发送用户 ID,登录用户回复有值,游客无 */
private Long senderId;
/** 回复用户 ID,系统用户回复有值,游客无 */
private Long receiverId;
/** 发送用户昵称,游客回复有值,登录用户无 */
private String senderNick;
/** 回复用户昵称,游客回复有值,系统用户无 */
private String receiverNick;
/** 回复数据 */
private String content;
/** 发送用户 IP */
private String ip;
/** 被回复用户忽略该回复的时间 */
private Long ignoredAt;
/** 所属评论 */
@Transient
private Comment comment;
/** 发送用户 */
@Transient
private UserView sender;
/** 回复用户 */
@Transient
private UserView receiver;
}
@@ -0,0 +1,47 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 数据字典
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DataDict extends UUIDEntity {
/** 业务类型 */
private String bizType;
/** 代码 */
private String code;
/** 父级代码 */
private String parentCode;
/** 字典名称语言键 */
private String nameLangId;
/** 字典名称(读取) */
@Transient
protected String name;
/** 搜索名称 */
@Transient
protected String searchName;
/** 字典中文名称(写入) */
@Transient
protected String zhCN;
/** 选项列表 */
@Transient
protected List<DataDictOption> dataDictOptionList;
}
@@ -0,0 +1,50 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 数据字典选项
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DataDictOption extends UUIDEntity {
/** 所属字典 ID */
private String dictId;
/** 值代码 */
private String valueCode;
/** 父级值代码 */
private String valueParentCode;
/** 值语言键 */
private String valueLangId;
/** 值(读取) */
@Transient
protected String value;
/** 中文值(写入) */
@Transient
protected String zhCN;
@Transient
protected String searchValue;
/** 所属字典 */
@Transient
protected DataDict dataDict;
/** 附加参数设置 */
@Transient
protected List<Setting> settingList;
}
@@ -0,0 +1,34 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
/**
* 邮件过滤
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
public class EmailFilter implements IDEntity<String>, Creatable, Deletable {
/** 主键 */
@Id
@AutoUUID
private String id;
/** 邮箱 */
private String email;
/** 创建时间 */
private Long createdAt;
/** 删除时间 */
@DeleteColumn
private Long deletedAt;
}
@@ -1,43 +0,0 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
/**
* 邮件队列
*
* @author 夜雨
* @since 2021-08-24 14:59
*/
@Data
public class EmailQueue {
/**
* 业务类型
*
* @author 夜雨
* @since 2021-08-24 15:54
*/
public enum BizType {
/** 回复提醒 */
REPLY_REMINAD,
/** 邮箱验证 */
EMAIL_VERIFY,
/** 重置密码 */
RESET_PASSWORD
}
@Id
@AutoUUID
private String UUID;
private BizType bizType;
private Long bizId;
private Long sendAt;
}
@@ -1,23 +0,0 @@
package com.imyeyu.api.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* @author 夜雨
* @since 2021-08-24 18:00
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailQueueLog extends Entity {
private String UUID;
private EmailQueue.BizType bizType;
private Long bizId;
private String sendTo;
private Long sendAt;
private Boolean isSent;
private String exceptionMsg;
}
@@ -1,21 +1,79 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.util.ResponseView;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity; import lombok.NoArgsConstructor;
/** /// 通用反馈
* 反馈 ///
* /// @author Codex
* @author 夜雨 /// @since 2026-08-19
* @since 2021-11-16 22:22
*/
@Data @Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Feedback extends Entity { public class Feedback extends UUIDEntity {
private String from; /// 反馈处理状态
private String email; public enum Status {
private String data;
/// 待处理
PENDING,
/// 已回复
REPLIED,
/// 已关闭
CLOSED
}
/// 所属模块
@JsonView(ResponseView.Public.class)
private ModuleCode module;
/// 提交用户 ID匿名反馈为 null
@JsonView(ResponseView.Admin.class)
private String userId;
/// 反馈内容文章 ID
@JsonView(ResponseView.Public.class)
private String contentId;
/// 联系方式
@JsonView(ResponseView.Public.class)
private String contact;
/// 提交 IP
@JsonView(ResponseView.Admin.class)
private String ip; private String ip;
/// 处理状态
@JsonView(ResponseView.Public.class)
private Status status;
/// 管理员回复文章 ID
@JsonView(ResponseView.Public.class)
private String responseId;
/// 回复用户 ID
@JsonView(ResponseView.Admin.class)
private String respondedBy;
/// 回复时间
@JsonView(ResponseView.Public.class)
private Long respondedAt;
/// 管理员回复文章
@Transient
@JsonView(ResponseView.Public.class)
private Article content;
/// 管理员回复文章
@Transient
@JsonView(ResponseView.Public.class)
private Article response;
} }

Some files were not shown because too many files have changed in this diff Show More