Initial project

This commit is contained in:
Timi
2025-07-08 16:31:30 +08:00
parent 1c6a45c8c2
commit ae0f56a6dc
356 changed files with 21123 additions and 109 deletions

129
.gitignore vendored
View File

@ -1,98 +1,43 @@
# ---> JetBrains /config
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider /data
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 /logs
/target
# User-specific stuff multilingualField/
.idea/**/workspace.xml !.mvn/wrapper/maven-wrapper.jar
.idea/**/tasks.xml !**/src/main/**/multilingualField/
.idea/**/usage.statistics.xml !**/src/test/**/multilingualField/
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific ### IntelliJ IDEA ###
.idea/**/aws.xml .idea/modules.xml
.idea/jarRepositories.xml
# Generated files .idea/compiler.xml
.idea/**/contentModel.xml .idea/libraries/
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws *.iws
*.iml
*.ipr
# IntelliJ ### Eclipse ###
out/ .apt_generated
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath .classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

7
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" native2AsciiForPropertiesFiles="true">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

58
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,58 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="jakarta.validation.constraints.NotNull" />
<option name="myNullables">
<value>
<list size="14">
<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="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="4" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="7" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="8" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="9" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="10" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="11" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="12" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="13" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="15">
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="1" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="2" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="5" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="6" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="7" class="java.lang.String" itemvalue="jakarta.validation.constraints.NotNull" />
<item index="8" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" />
<item index="9" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="10" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="12" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="14" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="openjdk-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

32
LICENSE
View File

@ -1,18 +1,20 @@
MIT License The MIT License (MIT)
Copyright © 2021 imyeyu.com
Copyright (c) 2025 timi Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and The above copyright notice and this permission notice shall be included in
associated documentation files (the "Software"), to deal in the Software without restriction, including all copies or substantial portions of the Software.
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
portions of the Software. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE THE SOFTWARE.
USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +1,7 @@
# TimiServerAPI ## Timi 服务器数据中心
Timi 总后端 [我的博客](https://www.imyeyu.net)、[timi-icon](https://icon.imyeyu.net) 等我开发的软件可能会使用此数据接口
技术栈SpringBoot MariaDB MyBatis Redis
[timijava](https://github.com/imyeyu/timijava)

181
pom.xml Normal file
View File

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
<properties>
<springboot.version>3.4.0</springboot.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.multilingualField>21</maven.compiler.multilingualField>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>apache-maven</id>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
<build>
<defaultGoal>compile</defaultGoal>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<excludeDevtools>true</excludeDevtools>
<mainClass>com.imyeyu.server.TimiServerAPI</mainClass>
<finalName>${project.artifactId}</finalName>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.imyeyu.network</groupId>
<artifactId>timi-network</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.imyeyu.lang</groupId>
<artifactId>timi-lang</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${springboot.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.7.0.202309050840-r</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.archive</artifactId>
<version>6.7.0.202309050840-r</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.1</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.4</version>
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.8.0</version>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.20</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec</artifactId>
<version>0.2.5</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.17.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,75 @@
package com.imyeyu.server;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.utils.OS;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.io.File;
/**
* 夜雨综合数据中心接口
*
* <p>本端所有接口面向用户,不做管理接口,数据管理将使用 JavaFX
*
* @author 夜雨
* @since 2021-02-23 21:35
*/
@Slf4j
@SpringBootApplication(scanBasePackages = {"com.imyeyu.server", "com.imyeyu.spring"})
@EnableTransactionManagement
public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware {
private static final String DEV_LANG_CONFIG = "dev.lang";
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
TimiServerAPI.applicationContext = applicationContext;
}
public static Language getUserLanguage() {
Language userLanguage = TimiSpring.getLanguage();
Environment env = applicationContext.getBean(Environment.class);
if (env.containsProperty(DEV_LANG_CONFIG)) {
String property = env.getProperty(DEV_LANG_CONFIG);
if (TimiJava.isNotEmpty(property)) {
userLanguage = Ref.toType(Language.class, property);
}
}
return userLanguage;
}
public static void main(String[] args) {
try {
{
// 导出配置
String[] files = {"application.yml", "logback.xml"};
for (int i = 0; i < files.length; i++) {
File file = new File("config" + SEP + files[i]);
if (!file.exists() || !file.isFile()) {
log.info("exporting default config at {}", file.getAbsolutePath());
IO.resourceToDisk(TimiServerAPI.class, files[i], file.getAbsolutePath());
}
}
}
// 启动 SpringBoot
SpringApplication.run(TimiServerAPI.class, args);
} catch (Exception e) {
log.error("launch error", e);
}
}
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.server.annotation;
import com.imyeyu.server.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();
}

View File

@ -0,0 +1,70 @@
package com.imyeyu.server.annotation;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.util.CaptchaManager;
import com.imyeyu.spring.bean.CaptchaData;
import org.aspectj.lang.JoinPoint;
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.stereotype.Component;
import java.lang.reflect.Method;
/**
* 图形验证码校验注解处理器
*
* @author 夜雨
* @since 2023-07-15 10:01
*/
@Slf4j
@Aspect
@Component
public class CaptchaValidInterceptor {
@Value("${spring.profiles.active}")
private String env;
@Autowired
private CaptchaManager captchaManager;
/** 注入注解 */
@Pointcut("@annotation(com.imyeyu.server.annotation.CaptchaValid)")
public void captchaPointCut() {
}
/**
* 执行前
*
* @param joinPoint 切入点
*/
@Before("captchaPointCut()")
public void doBefore(JoinPoint joinPoint) {
try {
if (env.startsWith("dev")) {
// 开发环境不校验
return;
}
if (joinPoint.getSignature() instanceof MethodSignature ms) {
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");
}
}
}

View File

@ -0,0 +1,25 @@
package com.imyeyu.server.annotation;
import com.imyeyu.server.modules.common.bean.SettingKey;
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:00
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableSetting {
/** @return 配置键 */
SettingKey value();
/** @return 未启用配置时响应消息语言映射键 */
String message() default "service.offline";
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.service.SettingService;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 启用配置注解处理器
*
* @author 夜雨
* @since 2023-07-15 10:01
*/
@Component
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class EnableSettingInterceptor implements HandlerInterceptor {
private final SettingService service;
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (handler instanceof HandlerMethod handlerMethod) {
EnableSetting annotation = handlerMethod.getMethodAnnotation(EnableSetting.class);
if (annotation == null) {
return true;
}
if (service.is(annotation.value())) {
return true;
}
throw new TimiException(TimiCode.ERROR_SERVICE_OFF, annotation.message());
}
return true;
}
}

View File

@ -0,0 +1,49 @@
package com.imyeyu.server.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.spring.TimiSpring;
import com.imyeyu.spring.annotation.RequestRateLimitAbstractInterceptor;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* 请求频率限制处理器
*
* @author 夜雨
* @since 2021-08-16 18:07
*/
@Slf4j
@Component
public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterceptor {
@Autowired
@Qualifier("redisRateLimit")
private Redis<String, Integer> redisRequestRateLimit;
@Override
public boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit) {
// 键
String key = "TimiServerAPI." + TimiSpring.getRequestIP() + "." + id;
if (redisRequestRateLimit.has(key)) {
Integer count = redisRequestRateLimit.get(key);
if (count != null) {
if (count < limit) {
redisRequestRateLimit.setAndKeepTTL(key, ++count);
} else {
log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit);
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("request_rate_limit");
}
}
return true;
}
redisRequestRateLimit.set(key, 0, Time.S * cycle);
return true;
}
}

View File

@ -0,0 +1,39 @@
package com.imyeyu.server.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.server.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;
}
}

View File

@ -0,0 +1,34 @@
package com.imyeyu.server.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
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.bean;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
/**
* @author 夜雨
* @since 2025-01-13 11:42
*/
@Component
public class IOCBeans {
@Bean
public Yaml yaml() {
return new Yaml();
}
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.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 {};
}
}

View File

@ -0,0 +1,30 @@
package com.imyeyu.server.bean;
import lombok.Data;
import com.imyeyu.utils.OS;
import java.io.InputStream;
/**
* 资源文件
*
* @author 夜雨
* @since 2021-07-31 15:31
*/
@Data
public class ResourceFile implements OS.FileSystem {
/** 服务器文件所在路径 */
private String path;
/** 文件名 */
private String name;
/** 文件数据流 */
private InputStream inputStream;
/** @return 完整绝对路径 */
public String getFullPath() {
return path + SEP + name;
}
}

View File

@ -0,0 +1,35 @@
package com.imyeyu.server.config;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.Arrays;
/**
* 异步线程池配置
*
* @author 夜雨
* @since 2023-08-21 16:22
*/
@Slf4j
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public @NotNull AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (e, method, obj) -> {
log.info("Exception message - {}", e.getMessage());
log.info("Method name - {}", method.getName());
log.info("Parameter values - {}", Arrays.toString(obj));
if (e instanceof Exception exception) {
log.info("exception: {}", exception.getMessage());
}
log.error("async uncaught error", e);
};
}
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.config;
import com.google.gson.Gson;
import org.springframework.context.annotation.Configuration;
/**
* @author 夜雨
* @since 2025-05-16 18:53
*/
@Configuration
public class BeanConfig {
public Gson gson() {
return new Gson();
}
}

View File

@ -0,0 +1,55 @@
package com.imyeyu.server.config;
import jakarta.servlet.Filter;
import lombok.Data;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
/**
* 跨域控制
*
* @author 夜雨
* @since 2021-05-14 09:21
*/
@Data
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "cors")
public class CORSConfig {
/** 允许跨域的地址 */
private String[] allowOrigin;
/** 是否允许请求带有验证信息 */
private boolean allowCredentials;
/** 允许请求的方法 */
private String allowMethods;
/** 允许服务端访问的客户端请求头 */
private String allowHeaders;
@Bean
public FilterRegistrationBean<Filter> corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader(allowHeaders);
config.addAllowedMethod(allowMethods);
config.setAllowCredentials(allowCredentials);
config.setAllowedOriginPatterns(Arrays.asList(allowOrigin));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.server.config;
import com.mongodb.client.MongoClient;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 夜雨
* @since 2024-02-23 10:55
*/
@Configuration
public class MongoConfig {
@Value("${spring.data.mongodb.database}")
private String db;
@Bean
public GridFSBucket gridFSBucket(MongoClient mongoClient) {
return GridFSBuckets.create(mongoClient.getDatabase(db));
}
}

View File

@ -0,0 +1,268 @@
package com.imyeyu.server.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.common.entity.Multilingual;
import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* Redis 配置
*
* @author 夜雨
* @since 2021-02-23 21:36
*/
@Data
@Configuration
@EqualsAndHashCode(callSuper = true)
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig extends AbstractRedisConfig {
// ---------- 连接配置 ----------
/** 地址 */
private String host;
/** 端口 */
private int port;
/** 密码 */
private String password;
/** 超时(毫秒) */
private int timeout;
/** 连接池 */
private Lettuce lettuce;
/** 数据库 */
private Database database;
/**
* 连接池
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Lettuce {
/** 配置 */
private Pool pool;
/**
* 配置
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Pool {
/** 最大活跃连接 */
private int maxActive;
/** 最小空闲连接 */
private int minIdle;
/** 最大空闲连接 */
private int maxIdle;
/** 最大等待时间(秒) */
private int maxWait;
}
}
/**
* 数据库
*
* @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;
}
@Override
protected RedisConfigParams configParams() {
return new RedisConfigParams() {{
setHost(host);
setPort(port);
setPassword(password);
setTimeout(timeout);
setMaxActive(lettuce.pool.maxActive);
setMinIdle(lettuce.pool.minIdle);
setMaxIdle(lettuce.pool.maxIdle);
}};
}
/** @return 连接池配置 */
@Bean
@Override
public GenericObjectPoolConfig<?> getPoolConfig() {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(lettuce.pool.maxActive);
config.setMinIdle(lettuce.pool.minIdle);
config.setMaxIdle(lettuce.pool.maxIdle);
config.setMaxWait(Duration.ofMillis(lettuce.pool.maxWait));
return config;
}
/** @return key 生成策略 */
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
/** @return 分布式锁, ID: 尝试加锁次数 */
@Bean("redisLocker")
public Redis<String, Integer> getLockerRedisTemplate() {
return getRedis(database.locker, RedisSerializers.STRING, RedisSerializers.INTEGER);
}
/** @return 多语言环境ID: {@link Multilingual} */
@Bean("redisLanguage")
public Redis<Long, Multilingual> getLanguageRedisTemplate() {
return getRedis(database.language, RedisSerializers.LONG, new RedisSerializer<>() {
public Multilingual deserialize(@Nullable byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return (Multilingual) new DeserializingConverter().convert(bytes);
} catch (Exception var3) {
throw new SerializationException("Cannot deserialize", var3);
}
}
public byte[] serialize(@Nullable Multilingual multilingual) {
if (multilingual == null) {
return new byte[0];
} else {
try {
return new SerializingConverter().convert(multilingual);
} catch (Exception var3) {
throw new SerializationException("Cannot serialize", var3);
}
}
}
});
}
/** @return 文章访问记录IP: [文章 ID] */
@Bean("redisLanguageMap")
public Redis<String, Long> getLanguageMapRedisTemplate() {
return getRedis(database.languageMap, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 文章访问统计,文章 ID: {@link ArticleRanking}(JSON) */
@Bean("redisArticleRanking")
public Redis<Long, ArticleRanking> getArticleRankingRedisTemplate() {
return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.gsonSerializer(ArticleRanking.class));
}
/** @return 文章访问记录IP: [文章 ID] */
@Bean("redisArticleRead")
public Redis<String, Long> getArticleReadRedisTemplate() {
return getRedis(database.articleRead, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 用户登录经验标记UID: NULL暂时没有值数据死亡时间为次日零时 */
@Bean("redisUserExpFlag")
public Redis<Long, String> getUserExpFlagRedisTemplate() {
return getRedis(database.userExpFlag, RedisSerializers.LONG, RedisSerializers.STRING);
}
/** @return 用户邮箱验证密钥,密钥: UID */
@Bean("redisUserEmailVerify")
public Redis<String, Long> getUserEmailVerifyRedisTemplate() {
return getRedis(database.userEmailVerify, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 用户重置密码密钥,密钥: UID */
@Bean("redisUserResetPWVerify")
public Redis<String, Long> getUserResetPWVerifyRedisTemplate() {
return getRedis(database.userResetPWVerify, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 接口访问控制IP#方法: 生命周期内访问次数 */
@Bean("redisRateLimit")
public Redis<String, Integer> getRateLimitRedisTemplate() {
return getRedis(database.rateLimit, RedisSerializers.STRING, RedisSerializers.INTEGER);
}
/** @return 系统配置Key 枚举: String 配置值 */
@Bean("redisSetting")
public Redis<String, String> getSettingRedisTemplate() {
return getRedis(database.setting, RedisSerializers.STRING, RedisSerializers.STRING);
}
/** @return Minecraft 登录,令牌: 玩家 ID */
@Bean("redisMCPlayerToken")
public Redis<String, Long> getMCPlayerLoginRedisTemplate() {
return getRedis(database.fmcPlayerToken, RedisSerializers.STRING, RedisSerializers.LONG);
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
*
*
* @author 夜雨
* @since 2024-12-19 23:04
*/
@Configuration
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(32);
scheduler.initialize();
return scheduler;
}
@Bean
public ScheduledTaskRegistrar scheduleCronTask(TaskScheduler taskScheduler) {
ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
registrar.setTaskScheduler(taskScheduler);
registrar.afterPropertiesSet();
return registrar;
}
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.server.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author 夜雨
* @since 2023-08-21 16:31
*/
@Data
@Slf4j
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "spring.async.thread-pool")
public class ThreadPoolConfig {
/** 核心数量 */
private int corePoolSize;
/** 最大数量 */
private int maxPoolSize;
/** 等待区容量 */
private int queueCapacity;
/** 最大保持活跃时间(秒) */
private int keepAliveSeconds;
/** 最大等待时间(秒) */
private int awaitTerminationSeconds;
/** 线程名称前缀 */
private String threadNamePrefix;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,94 @@
package com.imyeyu.server.config;
import com.google.gson.GsonBuilder;
import com.imyeyu.server.annotation.EnableSettingInterceptor;
import com.imyeyu.server.annotation.RequestRateLimitInterceptor;
import com.imyeyu.server.annotation.RequiredTokenInterceptor;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.vo.user.UserProfileView;
import com.imyeyu.server.modules.common.vo.user.UserView;
import com.imyeyu.server.modules.minecraft.annotation.RequiredFMCServerTokenInterceptor;
import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer;
import com.imyeyu.server.modules.mirror.vo.MirrorView;
import com.imyeyu.server.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.server.util.GsonSerializerAdapter;
import com.imyeyu.spring.annotation.RequestSingleParamResolver;
import jakarta.validation.constraints.NotNull;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.List;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 16:44
*/
@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final SystemAPIInterceptor systemAPIInterceptor;
private final GsonSerializerAdapter gsonSerializerAdapter;
private final RequiredTokenInterceptor requiredTokenInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestSingleParamResolver requestSingleParamResolver;
private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequiredFMCServerTokenInterceptor requiredFMCServerTokenInterceptor;
/**
* 过滤器
*
* @param registry 注册表
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(requiredFMCServerTokenInterceptor).addPathPatterns("/fmc/server/**");
registry.addInterceptor(requiredTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(requestSingleParamResolver);
}
/**
* 通信消息转换
*
* @param converters 转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter() {
@Override
protected void writeInternal(@NotNull Object object, Type type, @NonNull Writer writer) {
// 忽略参数类型,因为接口返回对象会被全局返回处理器包装为 TimiResponse否则会序列化转型错误
getGson().toJson(object, writer);
}
};
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Attachment.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MirrorView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserProfileView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MinecraftPlayer.class, gsonSerializerAdapter);
converter.setGson(builder.create());
converters.add(converter);
}
}

View File

@ -0,0 +1,76 @@
package com.imyeyu.server.config.dbsource;
import com.zaxxer.hikari.HikariDataSource;
import com.imyeyu.utils.Time;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* ForeverMC 登录校验数据源
*
* @author 夜雨
* @since 2022-11-29 22:39
*/
@Configuration
@MapperScan(basePackages = "com.imyeyu.server.modules.forevermc.mapper", sqlSessionFactoryRef = "foreverMCSqlSessionFactory")
public class ForeverMCDBConfig {
public static final String ROLLBACKER = "foreverMCTransactionManager";
@Bean(name = "foreverMCDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.forevermc")
public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "foreverMCSqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("foreverMCDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setTypeAliasesPackage("com.imyeyu.server.modules.forevermc.entity");
bean.setConfiguration(config);
return bean.getObject();
}
@Bean("foreverMCSqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("foreverMCSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "foreverMCTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("foreverMCDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,104 @@
package com.imyeyu.server.config.dbsource;
import com.imyeyu.utils.Time;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.EnumTypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Gitea 数据源
*
* @author 夜雨
* @since 2022-11-29 22:40
*/
@Configuration
@MapperScan(basePackages = {
"com.imyeyu.server.modules.gitea.mapper",
}, sqlSessionFactoryRef = "giteaSqlSessionFactory")
public class GiteaDBConfig {
public static final String ROLLBACKER = "giteaTransactionManager";
@Bean(name = "giteaDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.gitea")
public DataSource dateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "giteaSqlSessionFactory")
@Primary
public SqlSessionFactory sessionFactory(@Qualifier("giteaDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
config.setDefaultEnumTypeHandler(EnumTypeHandler.class);
List<Resource> resources = new ArrayList<>();
{
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/gitea/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
}
}
String[] typeAliases = {
"com.imyeyu.server.modules.gitea.entity",
};
String[] typeHandlers = {
"com.imyeyu.server.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setConfiguration(config);
bean.setMapperLocations(resources.toArray(new Resource[0]));
bean.setTypeAliasesPackage(String.join(",", typeAliases));
bean.setTypeHandlersPackage(String.join(",", typeHandlers));
return bean.getObject();
}
@Bean("giteaSqlSessionTemplate")
@Primary
public SqlSessionTemplate sessionTemplate(@Qualifier("giteaSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "giteaTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("giteaDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,122 @@
package com.imyeyu.server.config.dbsource;
import com.zaxxer.hikari.HikariDataSource;
import com.imyeyu.utils.Time;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.EnumTypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* TimiServer 数据源
*
* @author 夜雨
* @since 2022-11-29 22:40
*/
@Configuration
@MapperScan(basePackages = {
"com.imyeyu.server.modules.git.mapper",
"com.imyeyu.server.modules.bill.mapper",
"com.imyeyu.server.modules.blog.mapper",
"com.imyeyu.server.modules.lyric.mapper",
"com.imyeyu.server.modules.mirror.mapper",
"com.imyeyu.server.modules.system.mapper",
"com.imyeyu.server.modules.common.mapper",
"com.imyeyu.server.modules.minecraft.mapper"
}, sqlSessionFactoryRef = "timiServerSqlSessionFactory")
public class TimiServerDBConfig {
public static final String ROLLBACKER = "timiServerTransactionManager";
@Bean(name = "timiServerDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.timiserver")
public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "timiServerSqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("timiServerDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
config.setDefaultEnumTypeHandler(EnumTypeHandler.class);
List<Resource> resources = new ArrayList<>();
{
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/git/**/*.xml");
mapperLocations.add("classpath:mapper/blog/**/*.xml");
mapperLocations.add("classpath:mapper/common/**/*.xml");
mapperLocations.add("classpath:mapper/system/**/*.xml");
mapperLocations.add("classpath:mapper/minecraft/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
}
}
String[] typeAliases = {
"com.imyeyu.server.modules.git.entity",
"com.imyeyu.server.modules.bill.entity",
"com.imyeyu.server.modules.blog.entity",
"com.imyeyu.server.modules.lyric.entity",
"com.imyeyu.server.modules.mirror.entity",
"com.imyeyu.server.modules.system.entity",
"com.imyeyu.server.modules.common.entity",
"com.imyeyu.server.modules.minecraft.entity"
};
String[] typeHandlers = {
"com.imyeyu.server.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setConfiguration(config);
bean.setMapperLocations(resources.toArray(new Resource[0]));
bean.setTypeAliasesPackage(String.join(",", typeAliases));
bean.setTypeHandlersPackage(String.join(",", typeHandlers));
return bean.getObject();
}
@Bean("timiServerSqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("timiServerSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "timiServerTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("timiServerDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.server.handler;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.imyeyu.java.TimiJava;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MySQL JSON 数据类型处理器
*
* @author 夜雨
* @since 2021-07-04 09:36
*/
public class GsonHandler extends BaseTypeHandler<JsonElement> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JsonElement parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, String.valueOf(parameter.toString()));
}
@Override
public JsonElement getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toElement(rs.getString(columnName));
}
@Override
public JsonElement getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toElement(rs.getString(columnIndex));
}
@Override
public JsonElement getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toElement(cs.getNString(columnIndex));
}
private JsonElement toElement(String json) {
if (TimiJava.isNotEmpty(json)) {
JsonElement el = JsonParser.parseString(json);
if (el.isJsonObject()) {
return el.getAsJsonObject();
}
if (el.isJsonArray()) {
return el.getAsJsonArray();
}
if (el.isJsonPrimitive()) {
return el.getAsJsonPrimitive();
}
}
return null;
}
}

View File

@ -0,0 +1,49 @@
package com.imyeyu.server.modules.bill.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.server.modules.bill.service.BillService;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
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 2023-02-04 01:02
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/bill")
public class BillController {
private final BillService service;
private final SettingService settingService;
/**
* 创建收支帐单
*
* @param bill 账单
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public void createREBill(@Valid @RequestBody Bill bill) {
if (!settingService.getAsString(SettingKey.BILL_API_TOKEN).equals(TimiSpring.getToken())) {
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("token.illegal");
}
service.create(bill);
}
}

View File

@ -0,0 +1,120 @@
package com.imyeyu.server.modules.bill.entity;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import com.imyeyu.spring.entity.Entity;
/**
* 收支账单
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Bill extends Entity {
/**
* 类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum Type {
/** 收入 */
REVENUE,
/** 支出 */
EXPENDITURE
}
/**
* 收入类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum RevenueType {
/** 工作 */
WORK,
/** 退款 */
REFUND,
/** 其他 */
OTHER
}
/**
* 支出类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum ExpenditureType {
/** 饮食 */
FOOD,
/** 生活 */
LIFE,
/** 通信 */
COMMUNICATION,
/** 交通 */
TRAFFIC,
/** 娱乐 */
GAME,
/** 工作 */
WORK,
/** 服饰 */
CLOTHES,
/** 医疗 */
HEALTH,
/** 其他 */
OTHER
}
/** 收入类型 */
private RevenueType revenueType;
/** 支出类型 */
private ExpenditureType expenditureType;
/** 描述 */
@NotBlank(message = "bill.description.empty")
private String description;
/** 金额(未确保计算精度,放大了 100 倍) */
@NotNull(message = "bill.decimal.empty")
@DecimalMin(value = "0", message = "bill.decimal.limit")
private Long decimal;
/** 备注 */
private String remarks;
/** @return true 为收入账单 */
public boolean isRevenue() {
return revenueType != null;
}
/** @return true 为支出账单 */
public boolean isExpenditure() {
return expenditureType != null;
}
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.bill.mapper;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 收支帐单表
*
* @author 夜雨
* @since 2022-04-01 16:26
*/
public interface BillMapper extends BaseMapper<Bill, Long> {
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.bill.service;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.spring.service.CreatableService;
/**
* 收支帐单服务
*
* @author 夜雨
* @since 2022-04-01 16:24
*/
public interface BillService extends CreatableService<Bill> {
}

View File

@ -0,0 +1,27 @@
package com.imyeyu.server.modules.bill.service.implement;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.server.modules.bill.mapper.BillMapper;
import com.imyeyu.server.modules.bill.service.BillService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 收支账单服务
*
* @author 夜雨
* @since 2022-04-01 16:25
*/
@Service
@RequiredArgsConstructor
public class BillServiceImplement extends AbstractEntityService<Bill, Long> implements BillService {
private final BillMapper mapper;
@Override
protected BaseMapper<Bill, Long> mapper() {
return mapper;
}
}

View File

@ -0,0 +1,93 @@
package com.imyeyu.server.modules.blog.controller;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.service.ArticleService;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.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();
}
}

View File

@ -0,0 +1,32 @@
package com.imyeyu.server.modules.blog.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.server.modules.blog.entity.Friend;
import com.imyeyu.server.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();
}
}

View File

@ -0,0 +1,95 @@
package com.imyeyu.server.modules.blog.entity;
import com.google.gson.JsonElement;
import com.imyeyu.server.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 {
/** 关于 */
ABOUT,
/** 公版 */
PUBLIC,
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
/** 标题 */
protected String title;
/** 类型 */
protected Type type;
/** 分类 ID */
protected long classId;
/** 摘要 */
protected String digest;
/** 数据 */
protected String data;
/** 扩展数据 */
protected JsonElement extendData;
/** 阅读数量 */
protected int reads;
/** 喜欢数量 */
protected int likes;
/** 是否显示评论 */
protected boolean showComment;
/** true 为可评论 */
protected boolean canComment;
/** true 为可排位 */
protected boolean canRanking;
/** @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;
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.blog.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 访问排行(每周)
* 只记录访问次数、标题和最近访问,具体文章由 Redis key 记录
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@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++;
}
}

View File

@ -0,0 +1,36 @@
package com.imyeyu.server.modules.blog.entity;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
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 CommentReplyView reply;
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.modules.blog.mapper;
import com.imyeyu.server.modules.blog.entity.Article;
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> {
long countByKeyword(String keyword);
List<Article> selectByKeyword(String keyword, Long offset, int 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);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.server.modules.blog.mapper;
import com.imyeyu.server.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();
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.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);
}

View File

@ -0,0 +1,41 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.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);
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.server.modules.blog.entity.Friend;
import java.util.List;
/**
* 友链服务
*
* @author 夜雨
* @since 2021-07-15 16:04
*/
public interface FriendService {
List<Friend> listAll();
}

View File

@ -0,0 +1,113 @@
package com.imyeyu.server.modules.blog.service.implement;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.config.dbsource.TimiServerDBConfig;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.mapper.ArticleMapper;
import com.imyeyu.server.modules.blog.service.ArticleService;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.modules.blog.vo.article.KeywordPage;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.entity.Tag;
import com.imyeyu.server.modules.common.mapper.CommentMapper;
import com.imyeyu.server.modules.common.service.AttachmentService;
import com.imyeyu.server.modules.common.service.TagService;
import com.imyeyu.spring.TimiSpring;
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;
}
@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();
}
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.blog.service.implement;
import com.imyeyu.server.config.dbsource.TimiServerDBConfig;
import com.imyeyu.server.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.server.modules.blog.service.CommentRemindQueueService;
import com.imyeyu.server.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) {
mapper.destroyByUserId(userId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByReplyId(Long replyId) {
mapper.destroyByReplyId(replyId);
}
}

View File

@ -0,0 +1,27 @@
package com.imyeyu.server.modules.blog.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.server.modules.blog.entity.Friend;
import com.imyeyu.server.modules.blog.mapper.FriendMapper;
import com.imyeyu.server.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();
}
}

View File

@ -0,0 +1,148 @@
package com.imyeyu.server.modules.blog.util;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.config.RedisConfig;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.User;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.server.modules.common.service.UserService;
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 SettingKey#TTL_USER_TOKEN} 天,每次触发
* 二级缓存获取时会刷新这个时间,即指定天数内不再访问则视为登出
*
* @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 = Time.D * settingService.getAsInt(SettingKey.TTL_USER_TOKEN);
// 会话
TimiSpring.setSessionAttr(token, userId);
// 跨站 Cookie
Cookie cookie = Objects.requireNonNullElse(TimiSpring.getCookie("Token"), new Cookie("Token", token));
cookie.setDomain(settingService.getAsString(SettingKey.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(SettingKey.DOMAIN_ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(0);
TimiSpring.addCookie(cookie);
// Redis
redis.destroy(token);
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.server.modules.blog.vo.article;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.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;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.common.bean;
/**
* 支持评论的实体
*
* @author 夜雨
* @since 2023-10-10 11:39
*/
public interface CommentSupport {
/** @return true 为可评论 */
boolean canComment();
/** @return true 为不可评论 */
boolean canNotComment();
}

View File

@ -0,0 +1,30 @@
package com.imyeyu.server.modules.common.bean;
import lombok.Getter;
import lombok.Setter;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 邮件服务异常
*
* @author 夜雨
* @since 2021-10-03 11:14
*/
public class EmailException extends TimiException {
/** 邮箱 */
@Setter
@Getter
private String email;
public EmailException(TimiCode code, String email) {
super(code);
this.email = email;
}
public EmailException(TimiCode code, String msg, String email) {
super(code, msg);
this.email = email;
}
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.modules.common.bean;
/**
*
* @author 夜雨
* @since 2021-09-20 11:49
*/
public enum ImageType {
/** 双线性 */
AUTO,
/** 模糊 */
SMOOTH,
/** 像素 */
PIXELATED
}

View File

@ -0,0 +1,156 @@
package com.imyeyu.server.modules.common.bean;
/**
* 系统设置
*
* @author 夜雨
* @since 2021-07-20 21:46
*/
public enum SettingKey {
// ---------- 通用 ----------
RUN_ENV,
PUBLIC_RESOURCES,
/** 启用注册 */
ENABLE_REGISTER,
/** 启用登录 */
ENABLE_LOGIN,
/** 启用评论 */
ENABLE_COMMENT,
/** 启用测试 */
ENABLE_DEBUG,
/** 启用账号数据更新User 和 UserProfile */
ENABLE_USER_UPDATE,
/** 启用灰色滤镜 */
ENABLE_GRAY_FILTER,
// ---------- ICP 备案号 ----------
ICP_IMYEYU_COM,
// ---------- 域名 ----------
DOMAIN_ROOT,
DOMAIN_API,
DOMAIN_GIT,
DOMAIN_BLOG,
DOMAIN_SPACE,
DOMAIN_RESOURCE,
DOMAIN_DOWNLOAD,
DOMAIN_FOREVER_MC,
// ---------- ForeverMC ----------
/** 启用登录服务 */
FMC_PLAYER_LOGIN_ENABLE,
/** 最多绑定玩家数量 */
FMC_MAX_BIND,
/** 闪烁标语 */
FMC_SPLASHES,
/** 启动器背景 */
FMC_BG,
FMC_BGM,
FMC_BG_SWIPER,
/** JRE 列表 */
FMC_JRE,
/** 辅助登录模组 */
FMC_LOGIN_FABRIC,
/** 启用图片地图上传 */
FMC_ENABLE_IMAGE_MAP_UPLOAD,
/** 玩家登录令牌有效期(天) */
FMC_PLAYER_LOGIN_TOKEN_TTL,
/** 服务器与数据中心的通信令牌 */
FMC_SERVER_TOKEN,
// ---------- 生存时间 ----------
TTL_USER_TOKEN,
TTL_SETTING,
TTL_MULTILINGUAL,
// ---------- 多语言翻译 ----------
MULTILINGUAL_TRANSLATE_API,
MULTILINGUAL_TRANSLATE_APP_ID,
MULTILINGUAL_TRANSLATE_KEY,
// ---------- 账单 ----------
BILL_API_TOKEN,
// ---------- Git ----------
GIT_API,
// ---------- 远程音乐 ----------
MUSIC_MAX_FRAME_LENGTH,
MUSIC_PLAYER_PORT,
MUSIC_PLAYER_IP,
MUSIC_CONTROLLER_PORT,
MUSIC_CONTROLLER_IP,
MUSIC_CONTROLLER_URI,
// ---------- 系统 ----------
SYSTEM_FILE_BASE,
SYSTEM_FILE_TYPE,
SYSTEM_FILE_SYNC,
/** 文件过滤(通过密钥类型) */
SYSTEM_FILE_FILTER,
SYSTEM_STATUS_RATE,
SYSTEM_STATUS_LIMIT,
SYSTEM_STATUS_NETWORK_MAC,
SYSTEM_TERMINAL_TTL,
SYSTEM_TERMINAL_FILTERS,
/** 一般密钥 */
SYSTEM_API_KEY,
/** 超级密钥 */
SYSTEM_API_SUPER_KEY,
SYSTEM_REBOOT_COMMAND,
}

View File

@ -0,0 +1,85 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.server.annotation.CaptchaValid;
import com.imyeyu.server.annotation.EnableSetting;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.service.CommentReplyService;
import com.imyeyu.server.modules.common.service.CommentService;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.server.modules.git.vo.issue.CommentPage;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.PageResult;
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;
/**
* 评论操作接口
* <p>*评论回复只依赖评论而不含业务关联,评论有关联业务,所以此接口是通用接口
*
* @author 夜雨
* @since 2021-02-23 21:36
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService service;
private final CommentReplyService replyService;
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<Comment> captchaData) {
service.create(captchaData.getData());
}
@RequestRateLimit
@PostMapping("/list")
public PageResult<CommentView> list(@Valid @RequestBody CommentPage commentPage) {
return service.pageByBizId(commentPage);
}
/**
* 创建评论回复
*
* @param request 回复数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT_REPLY)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/reply/create")
public void createReply(@Valid @RequestBody CaptchaData<CommentReply> request) {
replyService.create(request.getData());
}
/**
* 获取回复列表
*
* @param page 页面参数
* @return 回复列表
*/
@RequestRateLimit
@RequestMapping("/reply/list")
public PageResult<CommentReplyView> pageCommentReplies(@Valid @RequestBody CommentReplyPage page) {
// 通用接口,只允许查询评论的回复
page.setBizType(CommentReplyPage.BizType.COMMENT);
return replyService.pageByBizType(page);
}
}

View File

@ -0,0 +1,375 @@
package com.imyeyu.server.modules.common.controller;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.Network;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.ImageType;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.entity.Setting;
import com.imyeyu.server.modules.common.entity.Task;
import com.imyeyu.server.modules.common.entity.Template;
import com.imyeyu.server.modules.common.entity.Version;
import com.imyeyu.server.modules.common.service.AttachmentService;
import com.imyeyu.server.modules.common.service.FeedbackService;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.server.modules.common.service.TaskService;
import com.imyeyu.server.modules.common.service.TemplateService;
import com.imyeyu.server.modules.common.service.VersionService;
import com.imyeyu.server.modules.common.vo.FeedbackRequest;
import com.imyeyu.server.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.server.modules.system.util.ResourceHandler;
import com.imyeyu.server.util.CaptchaManager;
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.CaptchaData;
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.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 系统接口
*
* @author 夜雨
* @since 2021-02-23 21:38
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
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 AttachmentService attachmentService;
private final Gson gson;
private final Yaml yaml;
private final GridFSBucket gridFSBucket;
private final CaptchaManager captchaManager;
private final ResourceHandler resourceHandler;
@AOPLog
@RequestMapping("")
public String root() {
return "IT WORKING! " + TimiSpring.getRequestIP();
}
/**
* 获取验证码
*
* @param width 宽度
* @param height 高度
* @param from 来源
* @param response 返回对象
*/
@IgnoreGlobalReturn
@GetMapping("/captcha")
public void captcha(int width, int height, CaptchaFrom from, HttpServletResponse response) {
// 返回图像流
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache"); // 禁止缓存
response.setDateHeader("Expires", -1);
response.setContentType("image/jpg");
try {
// 宽度
if (width < 64) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream());
return;
}
// 高度
if (height < 19) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream());
return;
}
// 来自
if (TimiJava.isEmpty(from)) {
ImageIO.write(captchaManager.error(TimiCode.ARG_MISS), "jpg", response.getOutputStream());
return;
}
// 输出图像流
ImageIO.write(captchaManager.generate(from, width, height), "jpg", response.getOutputStream());
} catch (Exception e) {
log.error("CommonController.getCaptcha", e);
try {
ImageIO.write(captchaManager.error(TimiCode.ERROR), "jpg", response.getOutputStream());
} catch (IOException 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.getFrom());
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/{key}")
public String settingByKey(@PathVariable("key") String key, @RequestParam(value = "as", required = false) Setting.Type asType) {
Setting setting = settingService.getByKey(SettingKey.valueOf(key.toUpperCase()));
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
String result = setting.getValue();
if (asType == null) {
return result;
}
switch (asType) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = yaml.load(setting.getValue());
result = gson.toJson(obj);
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
result = yaml.dump(obj);
}
}
}
return result;
}
@RequestRateLimit
@PostMapping("/setting/map")
public Map<SettingKey, String> mapSettingByKeys(@RequestBody Map<SettingKey, Map<String, Object>> settingMap) {
List<Setting> result = settingService.listByKeys(new ArrayList<>(settingMap.keySet()));
for (int i = 0; i < result.size(); i++) {
Setting setting = result.get(i);
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
Map<String, Object> args = settingMap.get(setting.getKey());
if (args == null) {
continue;
}
if (args.containsKey("as")) {
switch (Ref.toType(Setting.Type.class, args.get("as").toString())) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = new Yaml().load(setting.getValue());
setting.setValue(gson.toJson(obj));
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
setting.setValue(new Yaml().dump(obj));
}
}
}
}
}
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
}
@RequestRateLimit
@GetMapping("/setting/flushCache")
public void settingFlushCache() {
settingService.flushCache();
}
@AOPLog
@RequestRateLimit
@GetMapping("/attachment/{mongoId}")
public AttachmentView getAttachment(@PathVariable String mongoId) {
return attachmentService.viewByMongoId(mongoId);
}
@AOPLog
@RequestRateLimit
@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());
}
}
}

View File

@ -0,0 +1,97 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.server.modules.common.entity.Icon;
import com.imyeyu.server.modules.common.service.IconService;
import com.imyeyu.server.modules.common.vo.icon.AllResponse;
import com.imyeyu.server.modules.common.vo.icon.NamePage;
import com.imyeyu.server.modules.common.vo.icon.UnicodePage;
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.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.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;
/**
* timi-icon 前端接口
*
* @author 夜雨
* @since 2022-09-14 23:59
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/icon")
public class IconController {
private final IconService service;
/**
* 获取图标列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@RequestRateLimit
@PostMapping("/list")
public PageResult<Icon> list(@RequestBody Page page) {
return service.page(page);
}
/**
* 获取所有图标,为了减小传输数据,此接口只返回 name 名称、Unicode 代码和 SVG 路径
*
* @param latest 请求缓存的最新数据时间
* @return 所有图标,如果请求缓存的最新时间等于数据库的最新数据时间,不返回任何数据
*/
@RequestRateLimit
@GetMapping("/list/all")
public AllResponse listAll(@Valid @RequestParam Long latest) {
AllResponse resp = service.listAll(latest);
List<Icon> icons = resp.getIcons();
for (int i = 0; i < icons.size(); i++) {
Icon icon = icons.get(i);
icon.setId(null);
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);
}
}

View File

@ -0,0 +1,315 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.annotation.CaptchaValid;
import com.imyeyu.server.annotation.EnableSetting;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.entity.UserConfig;
import com.imyeyu.server.modules.common.entity.UserPrivacy;
import com.imyeyu.server.modules.common.service.CommentReplyService;
import com.imyeyu.server.modules.common.service.CommentService;
import com.imyeyu.server.modules.common.service.UserConfigService;
import com.imyeyu.server.modules.common.service.UserPrivacyService;
import com.imyeyu.server.modules.common.service.UserProfileService;
import com.imyeyu.server.modules.common.service.UserService;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.server.modules.common.vo.comment.UserCommentPage;
import com.imyeyu.server.modules.common.vo.user.EmailVerifyCallbackRequest;
import com.imyeyu.server.modules.common.vo.user.LoginRequest;
import com.imyeyu.server.modules.common.vo.user.LoginResponse;
import com.imyeyu.server.modules.common.vo.user.RegisterRequest;
import com.imyeyu.server.modules.common.vo.user.UpdatePasswordByKeyRequest;
import com.imyeyu.server.modules.common.vo.user.UpdatePasswordRequest;
import com.imyeyu.server.modules.common.vo.user.UserRequest;
import com.imyeyu.server.modules.common.vo.user.UserView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
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(value = SettingKey.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(value = SettingKey.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(value = SettingKey.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(@RequestSingleParam 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(value = SettingKey.ENABLE_USER_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<CommentView> listComment(@Valid @RequestBody UserCommentPage page) {
page.setUserId(service.getLoginUser().getId());
return commentService.pageByUserId(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/delete")
public void deleteComment(@RequestSingleParam Long commentId) {
commentService.get(commentId);
commentService.delete(commentId);
}
/**
* 获取用户被回复的评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/list")
public PageResult<CommentReplyView> listCommentReply(@Valid @RequestBody CommentReplyPage page) {
page.setBizId(service.getLoginUser().getId());
return commentReplyService.pageByBizType(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/delete")
public void deleteCommentReply(@RequestSingleParam 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(@RequestSingleParam 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);
}
}

View File

@ -0,0 +1,74 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.server.bean.MultilingualHandler;
import com.imyeyu.spring.entity.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* @author 夜雨
* @since 2023-08-15 10:17
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Attachment extends Entity implements MultilingualHandler {
/**
* 附件类型
*
* @author 夜雨
* @since 2023-08-21 16:32
*/
@Getter
@AllArgsConstructor
public enum BizType {
/** 用户 */
USER,
/** 文章 */
ARTICLE,
/** Git */
GIT,
/** 歌词 */
LYRIC,
/** ForeverMC */
FMC,
/** 镜像 */
MIRROR,
/** 系统 */
SYSTEM
}
private BizType bizType;
private Long bizId;
private String attachType;
private String mongoId;
@MultilingualField
private String title;
private String name;
private Long size;
public void setAttachTypeValue(Enum<?> attachType) {
this.attachType = attachType.toString();
}
public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) {
return Ref.toType(attachTypeClass, attachType);
}
}

View File

@ -0,0 +1,68 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.spring.service.GettableService;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import com.imyeyu.server.modules.blog.service.implement.ArticleServiceImplement;
import com.imyeyu.server.modules.common.bean.CommentSupport;
import com.imyeyu.server.modules.git.service.implement.IssueServiceImplement;
import com.imyeyu.server.modules.git.service.implement.MergeServiceImplement;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.service.BaseService;
/**
* 评论
*
* @author 夜雨
* @since 2021-02-25 14:46
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Comment extends Entity {
/**
* 关联业务类型
* <p>
* TODO 添加模块名称以便区别邮件通知推送来源,使用多语言键
*
* @author 夜雨
* @since 2023-08-06 23:42
*/
@Getter
@AllArgsConstructor
public enum BizType {
ARTICLE(ArticleServiceImplement.class),
GIT_ISSUE(IssueServiceImplement.class),
GIT_MERGE(MergeServiceImplement.class);
final Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass;
}
/** 关联业务类型 */
private BizType bizType;
/** 关联业务 ID */
private Long bizId;
/** 发送用户 ID登录用户评论有值游客无 */
private Long userId;
/** 发送用户昵称,游客评论有值,登录用户无 */
private String nick;
/** 评论数据 */
@NotBlank
private String content;
/** 发送用户 IP */
private String ip;
}

View File

@ -0,0 +1,45 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 评论回复
*
* @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;
}

View File

@ -0,0 +1,43 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,23 @@
package com.imyeyu.server.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;
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 反馈
*
* @author 夜雨
* @since 2021-11-16 22:22
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Feedback extends Entity {
private String from;
private String email;
private String data;
private String ip;
}

View File

@ -0,0 +1,25 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 字体图标
*
* @author 夜雨
* @since 2022-09-09 10:54
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Icon extends Entity {
/** 名称 */
private String name;
/** Unicode */
private String unicode;
/** SVG 路径 */
private String svg;
}

View File

@ -0,0 +1,67 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.server.TimiServerAPI;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.lang.reflect.Field;
/**
* @author 夜雨
* @since 2023-10-24 16:41
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Multilingual extends Entity {
protected String key;
protected String zhCN;
protected String zhTW;
protected String enUS;
protected String ruRU;
protected String koKR;
protected String jaJP;
protected String deDE;
/** @return 根据用户环境获取语言值 */
public String getValue() {
try {
Field field = Ref.getField(getClass(), TimiServerAPI.getUserLanguage().toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 获取指定语言值
*
* @param language 指定语言
* @return 值
*/
public String getValue(com.imyeyu.java.bean.Language language) {
try {
Field field = Ref.getField(getClass(), language.toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,51 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Updatable;
import lombok.Data;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 21:46
*/
@Data
public class Setting implements Creatable, Updatable {
/**
*
*
* @author 夜雨
* @since 2025-01-10 17:08
*/
public enum Type {
INTEGER,
STRING,
JSON,
YAML,
}
@Id
private SettingKey key;
private String value;
private Type type;
private boolean isPrivate;
private Long createdAt;
private Long updatedAt;
public boolean isPublic() {
return !isPrivate;
}
}

View File

@ -0,0 +1,39 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.bean.MultilingualHandler;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2024-08-28 14:26
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Tag extends Entity implements MultilingualHandler {
/**
*
*
* @author 夜雨
* @since 2024-08-28 14:26
*/
public enum BizType {
ARTICLE,
MUSIC,
SERVER_FILE,
WALLPAPER
}
protected BizType bizType;
protected String bizID;
@MultilingualHandler.MultilingualField
protected String value;
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import com.imyeyu.spring.entity.Entity;
import java.util.List;
/**
* 开发任务
*
* @author 夜雨
* @since 2022-02-26 11:12
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Task extends Entity {
/**
* 任务状态
*
* @author 夜雨
* @since 2022-02-26 11:53
*/
@Getter
public enum Status {
UPDATE, WITH, WAIT, KEEP, DIE;
final int sort = ordinal();
}
private String name;
private Status status;
private String digest;
// 关联数据
private List<TaskDetail> details;
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 开发任务详细信息
*
* @author 夜雨
* @since 2022-02-27 17:58
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class TaskDetail extends Entity {
/**
* 类型
*
* @author 夜雨
* @since 2022-02-27 18:03
*/
@Getter
public enum Type {
BUG, FEATURE
}
/**
* 状态
*
* @author 夜雨
* @since 2022-02-27 18:06
*/
@Getter
public enum Status {
UPDATE, WAIT, FINISH, CLOSE
}
private Long taskId;
private Type type;
private Status status;
private String digest;
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* @author 夜雨
* @since 2023-09-21 00:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Template extends Entity {
/**
*
*
* @author 夜雨
* @since 2023-09-22 16:38
*/
public enum BizType {
GIT,
FOREVER_MC
}
private BizType bizType;
private String bizCode;
private String data;
}

View File

@ -0,0 +1,68 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.utils.Time;
/**
* 用户
*
* @author 夜雨
* @since 2021-03-01 17:11
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Entity {
/**
*
*
* @author 夜雨
* @since 2024-02-21 14:48
*/
public enum AttachType {
AVATAR,
WRAPPER,
LICENSE,
DEFAULT_AVATAR,
DEFAULT_WRAPPER
}
/** 用户名 */
protected String name;
/** 密码 */
protected String password;
/** 邮箱 */
protected String email;
/** 邮箱验证时间 */
protected Long emailVerifyAt;
/** 解除禁言时间 */
protected Long unmuteAt;
/** 解除封禁时间 */
protected Long unbanAt;
/** @return true 为禁言中 */
public boolean isMuting() {
return unmuteAt != null && Time.now() < unmuteAt;
}
/** @return true 为封禁中 */
public boolean isBanning() {
return unbanAt != null && Time.now() < unbanAt;
}
public boolean emailVerified() {
return emailVerifyAt != null;
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 用户设置
*
* @author 夜雨
* @since 2021-08-12 15:06
*/
@Data
public class UserConfig implements Updatable {
@Min(1)
@Id
private Long userId;
private Boolean emailReplyRemind;
private Long updatedAt;
public UserConfig(Long uid) {
this.userId = uid;
emailReplyRemind = true;
}
public Boolean isEmailReplyRemind() {
return emailReplyRemind;
}
}

View File

@ -0,0 +1,51 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Min;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.lang.reflect.Field;
import java.util.List;
/**
* 用户隐私控制
*
* @author 夜雨
* @since 2021-07-27 16:51
*/
@Data
@NoArgsConstructor
public class UserPrivacy implements Updatable {
@Min(1)
@Id
private Long userId;
private boolean email;
private boolean sex;
private boolean birthdate;
private boolean qq;
private boolean lastLoginAt;
private boolean createdAt;
private Long updatedAt;
public UserPrivacy(Long uid) {
this.userId = uid;
}
/** @return 过滤字段列表 */
public List<String> listFilterFields() {
return Ref.listFields(getClass()).stream().filter(f -> {
try {
f.setAccessible(true);
return boolean.class.isAssignableFrom(f.getType()) && !(boolean) f.get(this);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}).map(Field::getName).toList();
}
}

View File

@ -0,0 +1,66 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.modules.common.bean.ImageType;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户数据
*
* @author 夜雨
* @since 2021-05-29 15:58
*/
@Data
@NoArgsConstructor
public class UserProfile implements Updatable {
/** 用户 ID */
@Min(1)
@Id
protected Long userId;
/** 封面类型 */
protected ImageType wrapperType;
/** 头像类型 */
protected ImageType avatarType;
/** 经验值 */
protected Integer exp;
/** 性别 */
@Max(1)
@Min(0)
protected Byte sex;
/** 出生日期 */
@Min(0)
protected Long birthdate;
/** QQ */
@Pattern(regexp = "[1-9]\\d{4,14}")
protected String qq;
/** 说明 */
@Size(max = 240)
protected String description;
/** 最近登录 IP */
protected String lastLoginIP;
/** 最近登录时间 */
protected Long lastLoginAt;
/** 修改时间 */
protected Long updatedAt;
public UserProfile(Long userId) {
this.userId = userId;
}
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 版本管理
*
* @author 夜雨
* @since 2021-06-10 16:01
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Version extends Entity {
private String name;
private String version;
private String content;
private String url;
}

View File

@ -0,0 +1,31 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-15 10:22
*/
public interface AttachmentMapper extends BaseMapper<Attachment, Long> {
/** 有效条件,非删除和销毁 */
String VALID = NOT_DELETE + " AND destroy_at IS NULL";
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID + LIMIT_1)
Attachment selectByBizId(Attachment.BizType bizType, long bizId);
@Select("SELECT * FROM attachment WHERE mongo_id = #{mongoId} " + VALID + LIMIT_1)
Attachment selectByMongoId(String mongoId);
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1)
Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND " + VALID + PAGE)
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit);
List<Attachment> listByAttachType(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 评论
*
* @author 夜雨
* @since 2021-2-23 21:33
*/
public interface CommentMapper extends BaseMapper<Comment, Long> {
@Select("SELECT * FROM comment WHERE id = #{id}" + NOT_DELETE)
Comment select(Long id);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE id = #{id}")
@Override
void delete(Long id);
@Select("SELECT COUNT(1) FROM comment WHERE biz_type = #{bizType} AND biz_id = #{bizId}" + NOT_DELETE)
long count(Comment.BizType bizType, Long bizId);
long countAll(Comment.BizType bizType, Long bizId);
List<CommentView> list(Comment.BizType bizType, Long bizId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
long countByUserId(Long userId);
List<CommentView> listByUserId(Long userId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE user_id = #{userId} ")
void deleteByUserId(Long userId);
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 评论回复提醒队列
*
* @author 夜雨
* @since 2021-08-25 00:15
*/
public interface CommentRemindQueueMapper extends BaseMapper<CommentRemindQueue, String> {
@Select("SELECT * FROM comment_remind_queue WHERE user_id = #{userId}")
List<CommentRemindQueue> listByUserId(Long userId);
@Delete("DELETE FROM comment_remind_queue WHERE user_id = #{userId}")
void destroyByUserId(Long userId);
@Delete("DELETE FROM comment_remind_queue WHERE reply_id = #{replyId}")
void destroyByReplyId(Long replyId);
}

View File

@ -0,0 +1,35 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* 评论回复
*
* @author 夜雨
* @since 2021-08-24 10:36
*/
public interface CommentReplyMapper extends BaseMapper<CommentReply, Long> {
@Select("SELECT * FROM comment_reply WHERE sender_id = #{senderId}" + NOT_DELETE)
List<CommentReply> listAllBySenderId(Long senderId);
@Select("SELECT COUNT(1) FROM comment_reply WHERE ${bizType.column} = #{bizId}" + NOT_DELETE)
long countByBizType(CommentReplyPage.BizType bizType, Long bizId);
@Select("SELECT * FROM comment_reply WHERE ${bizType.column} = #{bizId} AND ignored_at IS NULL" + NOT_DELETE + PAGE)
List<CommentReplyView> listByBizType(CommentReplyPage.BizType bizType, Long bizId, Long offset, int limit);
@Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE sender_id = #{userId} OR receiver_id = #{userId}")
void deleteByUserId(Long userId);
@Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE comment_id = #{commentId}")
void deleteByCommentId(Long commentId);
}

View File

@ -0,0 +1,11 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.EmailQueueLog;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* @author 夜雨
* @since 2023-08-10 10:38
*/
public interface EmailQueueLogMapper extends BaseMapper<EmailQueueLog, Long> {
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.EmailQueue;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 邮件推送队列
*
* @author 夜雨
* @since 2021-08-24 16:22
*/
public interface EmailQueueMapper extends BaseMapper<EmailQueue, String> {
@Select("SELECT * FROM email_queue WHERE biz_type = #{bizType} AND biz_id = #{bizId}")
EmailQueue query(EmailQueue.BizType bizType, Long bizId);
@Select("SELECT * FROM email_queue")
List<EmailQueue> listAll();
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Feedback;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 反馈
*
* @author 夜雨
* @since 2021-11-16 22:28
*/
public interface FeedbackMapper extends BaseMapper<Feedback, Long> {
}

View File

@ -0,0 +1,43 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Icon;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 图标
*
* @author 夜雨
* @since 2022-09-15 00:02
*/
public interface IconMapper extends BaseMapper<Icon, Long> {
@Select("SELECT COUNT(1) FROM icon" + NOT_DELETE)
@Override
long count();
@Select("SELECT * FROM icon LIMIT #{offset}, #{limit}" + NOT_DELETE)
@Override
List<Icon> list(long offset, int limit);
@Select("SELECT * FROM icon WHERE 1 = 1" + NOT_DELETE)
List<Icon> listAll();
@Select("SELECT COUNT(1) FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + NOT_DELETE)
long countByName(String name);
@Select("SELECT * FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + PAGE)
List<Icon> listByName(String name, long offset, int limit);
long countByLabel(String lang, String label);
List<Icon> listByLabel(String lang, String label, long offset, int limit);
@Select("SELECT COUNT(1) FROM icon WHERE unicode = #{unicode}" + NOT_DELETE)
long countByUnicode(String unicode);
@Select("SELECT * FROM icon WHERE unicode = #{unicode}" + PAGE)
List<Icon> listByUnicode(String unicode, long offset, int limit);
}

View File

@ -0,0 +1,32 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Multilingual;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-10-25 10:47
*/
public interface MultilingualMapper extends BaseMapper<Multilingual, Long> {
// 以下临时
@Select("SELECT * FROM multilingual WHERE key LIKE CONCAT('%', #{key}, '%')" + NOT_DELETE)
List<Multilingual> selectByKeyLike(String key);
List<Multilingual> selectByKeyList(List<String> keys);
// 以上临时
@Select("SELECT * FROM multilingual WHERE zh_cn = #{zhCN}" + NOT_DELETE + LIMIT_1)
Multilingual selectByZhCN(String zhCN);
@Select("SELECT * FROM multilingual WHERE en_US IS NULL OR en_US = ''" + NOT_DELETE)
List<Multilingual> selectByNotTranslate();
@Select("SELECT * FROM multilingual WHERE `key` = #{key}" + NOT_DELETE + LIMIT_1)
Multilingual selectByKey(String key);
}

View File

@ -0,0 +1,23 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 22:26
*/
public interface SettingMapper extends BaseMapper<Setting, String> {
@Select("SELECT * FROM `setting` WHERE `key` = #{key}")
Setting selectByKey(SettingKey key);
@Select("SELECT * FROM `setting`")
List<Setting> listAll();
}

View File

@ -0,0 +1,11 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Tag;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* @author 夜雨
* @since 2025-05-30 22:48
*/
public interface TagMapper extends BaseMapper<Tag, Long> {
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Task;
import java.util.List;
/**
* 任务服务
*
* @author 夜雨
* @since 2022-04-03 15:37
*/
public interface TaskMapper {
List<Task> listAll4Public();
}

View File

@ -0,0 +1,15 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Template;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* @author 夜雨
* @since 2023-09-22 16:41
*/
public interface TemplateMapper extends BaseMapper<Template, String> {
@Select("SELECT * FROM template WHERE biz_type = #{bizType} AND biz_code = #{bizCode}" + NOT_DELETE)
Template query(Template.BizType bizType, String bizCode);
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserConfig;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户设置
*
* @author 夜雨
* @since 2021-08-12 16:36
*/
public interface UserConfigMapper extends BaseMapper<UserConfig, Long> {
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.User;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 用户
*
* @author 夜雨
* @since 2021-02-23 21:33
*/
public interface UserMapper extends BaseMapper<User, Long> {
@Select("SELECT * FROM user WHERE BINARY name = #{name}" + NOT_DELETE + LIMIT_1)
User selectByName(String name);
@Select("SELECT * FROM user WHERE BINARY email = #{email} AND email_verify_at IS NOT NULL" + NOT_DELETE + LIMIT_1)
User selectByEmail(String email);
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserPrivacy;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户隐私控制
*
* @author 夜雨
* @since 2021-07-27 17:18
*/
public interface UserPrivacyMapper extends BaseMapper<UserPrivacy, Long> {
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserProfile;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户数据
*
* @author 夜雨
* @since 2021-07-27 17:04
*/
public interface UserProfileMapper extends BaseMapper<UserProfile, Long> {
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Version;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 版本管理
*
* @author 夜雨
* @since 2021-06-10 16:08
*/
public interface VersionMapper extends BaseMapper<Version, String> {
@Select("SELECT * FROM version WHERE name = #{name}" + NOT_DELETE)
Version queryByName(String name);
}

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