First commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Eclipse JDK Log ##
|
||||
*pid*.log
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
1
logs/.gitignore
vendored
Normal file
1
logs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/**.log
|
||||
308
mvnw
vendored
Normal file
308
mvnw
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
205
mvnw.cmd
vendored
Normal file
205
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
277
pom.xml
Normal file
277
pom.xml
Normal file
@@ -0,0 +1,277 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="https://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://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.3.0</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>link.at17.mid</groupId>
|
||||
<artifactId>tushare-data-service</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Tushare Data Service</name>
|
||||
<description>Tushare 数据服务,负责更新和提供接口</description>
|
||||
<properties>
|
||||
<java.version>18</java.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 固定 mybatis-spring 版本,解决 Invalid value type for attribute
|
||||
'factoryBeanObjectType':
|
||||
java.lang.String 问题 -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 热重载 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 合法性校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson</artifactId>
|
||||
<version>3.26.0</version>
|
||||
</dependency>
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.26.0</version>
|
||||
</dependency>
|
||||
<!-- jedis依赖的JAR配置start -->
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.42</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.4</version><!--$NO-MVN-MAN-VER$-->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
<!--
|
||||
https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
<!-- Excel 处理组件 -->
|
||||
<dependency>
|
||||
<groupId>com.hynnet</groupId>
|
||||
<artifactId>jxl</artifactId>
|
||||
<version>2.6.12.1</version>
|
||||
</dependency>
|
||||
<!-- Markdown 组件 -->
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-all</artifactId>
|
||||
<version>0.62.2</version>
|
||||
</dependency>
|
||||
<!-- quartz -->
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<groupId>org.slf4j</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15to18 -->
|
||||
<!-- 加密套件 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<version>1.77</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>com.github.penggle</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.4.8-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.37</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,27 @@
|
||||
package link.at17.mid.tushare;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler;
|
||||
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@MapperScan(basePackages = {"link.at17.mid.tushare.dao", "link.at17.mid.tushare.web.mapper"})
|
||||
@EnableCaching(proxyTargetClass=true)
|
||||
public class TushareDataServiceApplication {
|
||||
|
||||
@Autowired
|
||||
DailyUpdateDataScheduler dailyUpdateDataScheduler;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TushareDataServiceApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package link.at17.mid.tushare.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 将该注解注解在 mapper.java 或 dao.java 的批量插入 list 的方法上,以解除数据库对批量插入的限制
|
||||
* <p>例:
|
||||
* <code>
|
||||
* @BatchInsert int insertOrUpdate(@BatchList list)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface BatchInsert {
|
||||
int maxParams() default 65535; // 换库可改
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package link.at17.mid.tushare.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 配合 {@code BatchInsert} 使用
|
||||
* @see BatchInsert
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface BatchList {} // 标记“那个是批量的集合参数”
|
||||
@@ -0,0 +1,59 @@
|
||||
package link.at17.mid.tushare.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 将该注解用在配置上,给后台 ConfigController 自动渲染配置页用
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Bean
|
||||
@Lazy(false)
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigInfo {
|
||||
/**
|
||||
* @return 配置 field 标识,用以自动注入、持久化配置文件名
|
||||
*/
|
||||
String field() default "";
|
||||
|
||||
/**
|
||||
* @return 名称
|
||||
*/
|
||||
String name() default "配置";
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 为 true 时,最终会调用其无参构造方法,若需要填充默认值的,请在无参构造器中填充。
|
||||
* 当为 true 且无法载入配置文件而触发初始化时,若存在 /conf/system/{field}.fallback.json 文件时,从 fallback
|
||||
* 文件中初始化。fallback 仅参与初始化,不参与持久化
|
||||
* </p>
|
||||
*
|
||||
* @return 是否初始化默认值
|
||||
*/
|
||||
boolean initDefault() default false;
|
||||
|
||||
/**
|
||||
* @return 是否提供后台控制
|
||||
*/
|
||||
boolean managed() default true;
|
||||
|
||||
/**
|
||||
* 是否持久化
|
||||
*/
|
||||
boolean save() default true;
|
||||
|
||||
/**
|
||||
* 用于排序场景,如枚举所有配置类列表
|
||||
* @return
|
||||
*/
|
||||
int order() default 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package link.at17.mid.tushare.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 注入前端的注解
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>
|
||||
* 注解在类上,以便在前端使用类的静态方法或常量值
|
||||
* <hr>
|
||||
* <code>
|
||||
* {@code @StaticAttribute}<br>
|
||||
* <b>public class</b> EncryptUtils {<br>
|
||||
* <b>public static</b> String <i>DEFAULT_ENC</i> = "md5";<br>
|
||||
* <b>public static</b> String md5(String plain);<br>
|
||||
* }
|
||||
* </code>
|
||||
* <hr>
|
||||
* 即可在 thymeleaf html 中使用该类的静态方法:
|
||||
* <br>
|
||||
* <br>
|
||||
* <code>
|
||||
* <input th:value="${EncryptUtils.DEFAULT_ENC + EncryptUtils.md5(password)}"/>
|
||||
* </code><br>
|
||||
* <br>
|
||||
* </li>
|
||||
* <li>
|
||||
* 又如:某类内成员字段是不可修改的枚举<p>
|
||||
* <hr>
|
||||
* <code>
|
||||
* {@code @Data}<br>
|
||||
* {@code @Component}<br>
|
||||
* <b>public class</b> Network {<br>
|
||||
* {@code @StaticAttribute("ProxyTypeEnum")}<br>
|
||||
* <b>private</b> Proxy.Type <i>proxyType</i> = Proxy.Type.<i><b>DIRECT</b></i>;<br>
|
||||
* }
|
||||
* </code>
|
||||
* <hr>
|
||||
* 即可在 thymeleaf html 中使用该枚举:<br>
|
||||
* <br>
|
||||
* <code>
|
||||
<option value="DIRECT" th:selected="${@network.proxyType == ProxyTypeEnum.DIRECT}">无</option><br>
|
||||
</code>
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.FIELD})
|
||||
public @interface StaticAttribute {
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package link.at17.mid.tushare.api.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import link.at17.mid.tushare.dao.StockCalendarDao;
|
||||
import link.at17.mid.tushare.enums.StockMarket;
|
||||
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/common/calendar")
|
||||
@Slf4j
|
||||
public class StockCalendarController {
|
||||
|
||||
@Autowired
|
||||
StockCalendarDao stockCalendarDao;
|
||||
|
||||
@GetMapping("todayIsOpen")
|
||||
private String todayIsOpen(@Param("stockMarket") StockMarket stockMarket) {
|
||||
return String.valueOf(stockCalendarDao.isOpen(LocalDateTime.now(), stockMarket));
|
||||
}
|
||||
|
||||
@GetMapping("isOpen")
|
||||
private String isOpen(@Param("stockMarket") StockMarket stockMarket, @Param("date") String date) {
|
||||
LocalDateTime dateTime = LocalDateTimeUtils.parseDate(date);
|
||||
|
||||
return String.valueOf(stockCalendarDao.isOpen(dateTime, stockMarket));
|
||||
}
|
||||
|
||||
}
|
||||
119
src/main/java/link/at17/mid/tushare/cache/CacheConstants.java
vendored
Normal file
119
src/main/java/link/at17/mid/tushare/cache/CacheConstants.java
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
public class CacheConstants {
|
||||
|
||||
public static class Statistics {
|
||||
/** 每日更新、更新 Post 和 Comment 时更新 */
|
||||
public static final String DAILY_POST_AND_COMMENT_COUNTS
|
||||
= "StatisticsService::getPostAndCommentCountsWithDate";
|
||||
/** 每日更新 */
|
||||
public static final String DAY_SINCE
|
||||
= "StatisticsService::daySince";
|
||||
/** 更新 Post 时更新 */
|
||||
public static final String TOTAL_RELEASE_POSTS_COUNT
|
||||
= "StatisticsService::totalReleasedPostsCount";
|
||||
/** 更新 Post 和 Comment 时更新 */
|
||||
public static final String TOTAL_COMMENTS_COUNT
|
||||
= "StatisticsService::totalCommentsCount";
|
||||
}
|
||||
|
||||
public static class Posts {
|
||||
static final String ID = "Posts::";
|
||||
|
||||
// Markdown
|
||||
/** 更新 Post 时更新(指定 PostID) */
|
||||
public static final String MARKDOWN_CONTENT_RENDERING
|
||||
= ID + "Markdown::renderPostContent";
|
||||
/** 更新 Post 时更新(指定 PostID) */
|
||||
public static final String MARKDOWN_PREFACE_RENDERING
|
||||
= ID + "Markdown::renderPostPreface";
|
||||
|
||||
// Statistic
|
||||
public static final String POST_STATISTIC
|
||||
= ID + "PostStatistic::getPostStatistic";
|
||||
|
||||
// CondictionPage
|
||||
/** 更新 Post/Meta 时更新 */
|
||||
public static final String CONDITION_PAGE
|
||||
= ID + "ConditionPage::conditionPage";
|
||||
|
||||
// PostService
|
||||
/// 与 ID 或 Name 有关
|
||||
/** 更新 Post 时更新(指定 PostID) */
|
||||
public static final String GET_BY_ID
|
||||
= ID + "PostService::getById";
|
||||
/** 更新 Post 时更新(指定 PostID) */
|
||||
public static final String GET_BY_ID_LOGINED
|
||||
= ID + "PostService::getByIdLogined";
|
||||
/** 更新 Post 时更新(指定 PostID) */
|
||||
public static final String GET_BY_ID_UNLOGINED
|
||||
= ID + "PostService::getByIdUnlogined";
|
||||
/** 更新 Post 时更新(指定 PostPageName) */
|
||||
public static final String GET_BY_NAME_LOGINED
|
||||
= ID + "PostService::getByNameLogined";
|
||||
/** 更新 Post 时更新(指定 PostPageName) */
|
||||
public static final String GET_BY_NAME_UNLOGINED
|
||||
= ID + "PostService::getByNameUnlogined";
|
||||
|
||||
/** 更新 Post 时更新(指定 PostID 的 Next 的 PostID) */
|
||||
public static final String GET_PREVIOUS_LOGINED
|
||||
= ID + "PostService::getPreviousLogined";
|
||||
/** 更新 Post 时更新(指定 PostID 的 Next 的 PostID) */
|
||||
public static final String GET_PREVIOUS_UNLOGINED
|
||||
= ID + "PostService::getPreviousUnlogined";
|
||||
/** 更新 Post 时更新(指定 PostID 的 Previous 的 PostID) */
|
||||
public static final String GET_NEXT_LOGINED
|
||||
= ID + "PostService::getNextLogined";
|
||||
/** 更新 Post 时更新(指定 PostID 的 Previous 的 PostID) */
|
||||
public static final String GET_NEXT_UNLOGINED
|
||||
= ID + "PostService::getNextUnlogined";
|
||||
|
||||
/// 与 ID 或 Name 无关(批量)
|
||||
/** 更新 Post 时更新 */
|
||||
public static final String GET_LATEST_POSTS_LOGINED
|
||||
= ID + "PostService::getLatestPostsLogined";
|
||||
/** 更新 Post 时更新 */
|
||||
public static final String GET_LATEST_POSTS_UNLOGINED
|
||||
= ID + "PostService::getLatestPostsUnlogined";
|
||||
|
||||
/** 更新 Post 和 Comment 时更新 */
|
||||
public static final String GET_POPULAR_POSTS_LOGINED
|
||||
= ID + "PostService::getPopularPostsLogined";
|
||||
/** 更新 Post 和 Comment 时更新 */
|
||||
public static final String GET_POPULAR_POSTS_UNLOGINED
|
||||
= ID + "PostService::getPopularPostsUnlogined";
|
||||
|
||||
}
|
||||
|
||||
public static class Comments {
|
||||
static final String ID = "Comments::";
|
||||
|
||||
// Markdown
|
||||
public static final String MARKDOWN_CONTENT_RENDERING
|
||||
= ID + "Markdown::renderCommentContent";
|
||||
|
||||
// CommentService
|
||||
public static final String GET_POST_COMMENT_PAGE
|
||||
= ID + "CommentService::getPostCommentPage";
|
||||
public static final String GET_FULL_RELATIVE_COMMENT
|
||||
= ID + "CommentService::getFullRelativeComment";
|
||||
public static final String GET_LATEST_COMMENTS
|
||||
= ID + "CommentService::getLatestComments";
|
||||
public static final String GET_LATEST_COMMENTS_WITH_LEVEL
|
||||
= ID + "CommentService::getLatestCommentsWithLevel";
|
||||
public static final String LIST_FROM_CHILD_TO_PARENT
|
||||
= ID + "CommentService::listFromChildToParent";
|
||||
public static final String GET_JUMP_TO_URL
|
||||
= ID + "CommentService::getJumpToUrl";
|
||||
}
|
||||
|
||||
public static class Metas {
|
||||
static final String ID = "Metas::";
|
||||
|
||||
// MetaService
|
||||
/** 更新 Post 和 Meta 时更新(批量) */
|
||||
public static final String LIST_META_COUNT
|
||||
= ID + "MetaService::listMetaCount";
|
||||
}
|
||||
|
||||
}
|
||||
91
src/main/java/link/at17/mid/tushare/cache/CacheEvictionJob.java
vendored
Normal file
91
src/main/java/link/at17/mid/tushare/cache/CacheEvictionJob.java
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.redis.cache.CacheKeyPrefix;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import link.at17.mid.tushare.task.TaskConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 定时清除缓存任务
|
||||
* @author Doghole
|
||||
* @since 2024-01-30
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CacheEvictionJob implements Job {
|
||||
|
||||
@Autowired
|
||||
RedissonClient redis;
|
||||
|
||||
@Autowired
|
||||
CacheProperties cacheProperties;
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
static CacheKeyPrefix prefix;
|
||||
|
||||
static final CacheEvictionTask DAILY_ANNO = new CacheEvictionTask() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return CacheEvictionTask.class;
|
||||
}
|
||||
@Override
|
||||
public CacheEvictionSpan value() {
|
||||
return CacheEvictionSpan.DAILY;
|
||||
}
|
||||
};
|
||||
|
||||
@PostConstruct
|
||||
void onPostConstruct() {
|
||||
prefix = CacheKeyPrefix.prefixed(
|
||||
cacheProperties.getRedis().getKeyPrefix());
|
||||
}
|
||||
|
||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
||||
String triggerName = jobExecutionContext.getTrigger().getKey().getName();
|
||||
|
||||
Set<Method> methods = null;
|
||||
|
||||
if (TaskConstants.CACHE_DAILY_EVICTION_TRIGGER.equals(triggerName)) {
|
||||
methods = reflections.getMethodsAnnotatedWith(DAILY_ANNO);
|
||||
}
|
||||
|
||||
if (methods != null) {
|
||||
for (Method method : methods) {
|
||||
Cacheable cacheable = method.getAnnotation(Cacheable.class);
|
||||
if (cacheable == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] values = cacheable.value();
|
||||
if (values.length == 0) {
|
||||
values = cacheable.cacheNames();
|
||||
}
|
||||
for (String cacheName : values) {
|
||||
|
||||
String redisCacheName = prefix.compute(cacheName) + "*";
|
||||
redis.getKeys().getKeysByPattern(redisCacheName).forEach(name -> {
|
||||
redis.getBucket(name).deleteAsync();
|
||||
log.debug("Scheduler clear cacheName {}", name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
7
src/main/java/link/at17/mid/tushare/cache/CacheEvictionSpan.java
vendored
Normal file
7
src/main/java/link/at17/mid/tushare/cache/CacheEvictionSpan.java
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
public enum CacheEvictionSpan {
|
||||
|
||||
HOURLY, DAILY, WEEKLY, MONTHLY, SEASONLY, YEARLY
|
||||
|
||||
}
|
||||
15
src/main/java/link/at17/mid/tushare/cache/CacheEvictionTask.java
vendored
Normal file
15
src/main/java/link/at17/mid/tushare/cache/CacheEvictionTask.java
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Repeatable(CacheEvictionTasks.class)
|
||||
public @interface CacheEvictionTask {
|
||||
|
||||
CacheEvictionSpan value() default CacheEvictionSpan.DAILY;
|
||||
}
|
||||
13
src/main/java/link/at17/mid/tushare/cache/CacheEvictionTasks.java
vendored
Normal file
13
src/main/java/link/at17/mid/tushare/cache/CacheEvictionTasks.java
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface CacheEvictionTasks {
|
||||
|
||||
CacheEvictionTask[] value();
|
||||
}
|
||||
18
src/main/java/link/at17/mid/tushare/cache/EvictAfterUpdate.java
vendored
Normal file
18
src/main/java/link/at17/mid/tushare/cache/EvictAfterUpdate.java
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package link.at17.mid.tushare.cache;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 注解用于更新数据后
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface EvictAfterUpdate {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package link.at17.mid.tushare.component;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.reflection.ParamNameResolver;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* 批量插入切片器
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchInsertAspect {
|
||||
|
||||
private final SqlSessionFactory sqlSessionFactory;
|
||||
private final ConcurrentMap<Method, Meta> cache = new ConcurrentHashMap<>();
|
||||
|
||||
@Around("@annotation(batchInsert)")
|
||||
public Object around(ProceedingJoinPoint pjp, BatchInsert batchInsert) throws Throwable {
|
||||
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
|
||||
Object[] args = pjp.getArgs();
|
||||
|
||||
// 定位批量参数索引
|
||||
int listIdx = findBatchListIndex(method);
|
||||
List<?> list = (List<?>) args[listIdx];
|
||||
if (list == null || list.isEmpty()) {
|
||||
return pjp.proceed(); // 无数据,原样调用一次
|
||||
}
|
||||
|
||||
// 计算并缓存批大小
|
||||
Meta meta = cache.computeIfAbsent(method, m -> computeMeta(m, args, listIdx, batchInsert.maxParams()));
|
||||
|
||||
int batchSize = Math.max(1, meta.batchSize);
|
||||
// 分批执行
|
||||
if (method.getReturnType() == Void.TYPE) {
|
||||
for (int i = 0; i < list.size(); i += batchSize) {
|
||||
Object[] cloned = args.clone();
|
||||
cloned[listIdx] = list.subList(i, Math.min(i + batchSize, list.size()));
|
||||
pjp.proceed(cloned);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (method.getReturnType() == int.class || method.getReturnType() == Integer.class) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < list.size(); i += batchSize) {
|
||||
Object[] cloned = args.clone();
|
||||
cloned[listIdx] = list.subList(i, Math.min(i + batchSize, list.size()));
|
||||
total += (int) pjp.proceed(cloned);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
throw new IllegalStateException("不支持的返回类型:" + method.getReturnType());
|
||||
}
|
||||
|
||||
private int findBatchListIndex(Method method) {
|
||||
Annotation[][] pa = method.getParameterAnnotations();
|
||||
for (int i = 0; i < pa.length; i++) {
|
||||
for (Annotation a : pa[i]) {
|
||||
if (a.annotationType() == BatchList.class)
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(method + " 缺少 @BatchList 标注的集合参数");
|
||||
}
|
||||
|
||||
private Meta computeMeta(Method method, Object[] args, int listIdx, int maxParams) {
|
||||
String statementId = method.getDeclaringClass().getName() + "." + method.getName();
|
||||
try (SqlSession session = sqlSessionFactory.openSession()) {
|
||||
MappedStatement ms = session.getConfiguration().getMappedStatement(statementId);
|
||||
|
||||
ParamNameResolver resolver = new ParamNameResolver(ms.getConfiguration(), method);
|
||||
|
||||
// 用真实参数计算 base/perRow:一次放1条,一次放2条,两者相减即为单条参数数量
|
||||
Object[] a1 = args.clone();
|
||||
Object sample = ((List<?>) args[listIdx]).get(0);
|
||||
a1[listIdx] = Collections.singletonList(sample);
|
||||
|
||||
Object p1 = resolver.getNamedParams(a1);
|
||||
BoundSql bs1 = ms.getBoundSql(p1);
|
||||
int s1 = bs1.getParameterMappings().size();
|
||||
|
||||
Object[] a2 = args.clone();
|
||||
a2[listIdx] = Arrays.asList(sample, sample);
|
||||
|
||||
Object p2 = resolver.getNamedParams(a2);
|
||||
BoundSql bs2 = ms.getBoundSql(p2);
|
||||
int s2 = bs2.getParameterMappings().size();
|
||||
|
||||
int perRow = s2 - s1;
|
||||
if (perRow <= 0)
|
||||
throw new IllegalStateException("无法识别每行占位符数量");
|
||||
int base = s1 - perRow;
|
||||
int batch = Math.max(1, (maxParams - base) / perRow);
|
||||
return new Meta(perRow, base, batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Meta {
|
||||
int perRow;
|
||||
int base;
|
||||
int batchSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package link.at17.mid.tushare.component;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
|
||||
import link.at17.mid.tushare.annotation.ConfigInfo;
|
||||
import link.at17.mid.tushare.interfaces.IConfig;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 实现自动化注册 Config
|
||||
*/
|
||||
@Slf4j
|
||||
@DependsOn("configService")
|
||||
public class ConfigAutoRegistrar implements BeanDefinitionRegistryPostProcessor {
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.addIncludeFilter(new AssignableTypeFilter(IConfig.class));
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigInfo.class));
|
||||
|
||||
scanner.findCandidateComponents("link.at17.mid.tushare.system.config").forEach(beanDefinition -> {
|
||||
String className = beanDefinition.getBeanClassName();
|
||||
try {
|
||||
// 确保其 field 规则与 configService 内 field 生成规则一致,即:
|
||||
// 如果 @ConfigInfo 指定了 field 的,使用该 field + "Config"
|
||||
// 作为 beanName,否则使用首字母小写的 simpleClassName 作为
|
||||
// beanName,且 simpleClassName 无论如何必须以 Config 作为结尾。
|
||||
Class<?> clazz = Class.forName(className);
|
||||
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
|
||||
+ clazz.getSimpleName().substring(1);
|
||||
|
||||
if (!IConfig.class.isAssignableFrom(clazz)) {
|
||||
log.warn("Config {} does not implement IConfig, ignore", beanName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!beanName.endsWith("Config")) {
|
||||
log.warn("Config {}'s simple class name does not end with \"Config\", ignore", beanName);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
if (info == null) {
|
||||
log.warn("Config {} does not have @ConfigInfo annotation, ignore", clazz.getName());
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isNotBlank(info.field())) {
|
||||
beanName = info.field() + "Config";
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder factoryBean = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(ConfigServiceFactoryBean.class)
|
||||
.addConstructorArgValue(clazz);
|
||||
// 注意此处注册 factoryBean 不意味着 FactoryBean.getObject() 方法立即被执行,
|
||||
// Spring 管理的 Bean 默认在其被使用时才创建,所以如果 getObject() 调用一些方
|
||||
// 法,这些方法会在初次使用 Bean 时才被创建。如果这些方法对于启动过程很重要,
|
||||
// 需要在对应 Config(Bean) 上加上 @Bean 和 @Lazy(false) 注解,确保一旦准备好
|
||||
// 相应的 Bean 就会被创建。
|
||||
registry.registerBeanDefinition(beanName, factoryBean.getBeanDefinition());
|
||||
log.info("Add config {} to bean register", beanName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("Failed to load class: " + className, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package link.at17.mid.tushare.component;
|
||||
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
|
||||
import link.at17.mid.tushare.config.ConstructionGuard;
|
||||
import link.at17.mid.tushare.interfaces.IConfig;
|
||||
import link.at17.mid.tushare.service.ConfigService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 实现配置项自动载入
|
||||
* @param <T>
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigServiceFactoryBean<T extends IConfig<T>> implements FactoryBean<T>, BeanNameAware {
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
private String beanName;
|
||||
|
||||
@Autowired
|
||||
private AutowireCapableBeanFactory beanFactory;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
public ConfigServiceFactoryBean(Class<T> targetClass) {
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanName(String name) {
|
||||
this.beanName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getObject() throws Exception {
|
||||
ConstructionGuard.enter(targetClass);
|
||||
boolean success = true;
|
||||
try {
|
||||
T bean = configService.getConfig(targetClass);
|
||||
beanFactory.autowireBean(bean);
|
||||
beanFactory.initializeBean(bean, beanName);
|
||||
configService.saveOrUpdate(bean);
|
||||
return bean;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("Fail to load config: " + targetClass.getName(), e);
|
||||
success = false;
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
ConstructionGuard.exit(targetClass);
|
||||
if (success) {
|
||||
log.debug("getObject() for {} success", targetClass.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return targetClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package link.at17.mid.tushare.component;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.reflections.Reflections;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import link.at17.mid.tushare.annotation.StaticAttribute;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
|
||||
/**
|
||||
* 注入拦截器
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class PlatformInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
/**
|
||||
* 静态注入的类缓存
|
||||
*/
|
||||
Map<String, Class<?>> staticAttributeClassCache;
|
||||
|
||||
/**
|
||||
* 静态注入的枚举字段缓存
|
||||
*/
|
||||
Map<String, Map<String, Enum<?>>> options = null;
|
||||
Map<String, String> optionNameMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
injectNecessary(request, handler);
|
||||
request.getSession(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为前端模板注入变量
|
||||
* @param request
|
||||
*/
|
||||
public void injectNecessary(HttpServletRequest request, Object handler) {
|
||||
|
||||
// 只在 Controller(HandlerMethod)里才做注入
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
// 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理,
|
||||
// 这里一律跳过
|
||||
return;
|
||||
}
|
||||
|
||||
// 排除 @ResponseBody/json 接口
|
||||
HandlerMethod hm = (HandlerMethod) handler;
|
||||
if (hm.hasMethodAnnotation(ResponseBody.class)
|
||||
|| hm.getBeanType().isAnnotationPresent(RestController.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
request.setAttribute("request", request); // 把 request 本身放到 attribute 里去,供前端部分位置用
|
||||
|
||||
// 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用
|
||||
if (staticAttributeClassCache == null) {
|
||||
log.debug("init static attribute classes.");
|
||||
staticAttributeClassCache = new HashMap<>();
|
||||
|
||||
|
||||
Set<Class<?>> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class);
|
||||
for (Class<?> staticClass : staticClasses) {
|
||||
StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class);
|
||||
String name = staticClass.getSimpleName();
|
||||
if (StringUtils.isNotBlank(sa.value())) {
|
||||
name = sa.value();
|
||||
}
|
||||
|
||||
if (staticAttributeClassCache.containsKey(name)) {
|
||||
// 缓存中已经存在了这个名字,直接忽略
|
||||
log.warn("StaticAttribute annotation name {} for class {} has been taken, ignore.",
|
||||
name, staticClass.getName());
|
||||
}
|
||||
staticAttributeClassCache.put(name, staticClass);
|
||||
log.debug("{} injected as name {}", staticClass, name);
|
||||
}
|
||||
}
|
||||
else if (staticAttributeClassCache.isEmpty()) {
|
||||
// 跑完一次后,缓存仍然为空,可能不正常,提示一下
|
||||
log.warn("StaticAttributes' staticAttributeClassCache is empty, it's unusual, if you didn't exclude it manually, please check.");
|
||||
}
|
||||
Iterator<Map.Entry<String, Class<?>>> iterator = staticAttributeClassCache.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Class<?>> entry = iterator.next();
|
||||
request.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
if (options == null) {
|
||||
options = new HashMap<>();
|
||||
Set<Field> enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class);
|
||||
for (Field f : enumOptionFields) {
|
||||
if (!f.isAnnotationPresent(StaticAttribute.class)) continue;
|
||||
if (!Enum.class.isAssignableFrom(f.getType())) continue;
|
||||
|
||||
// 拿到注解和名前缀
|
||||
StaticAttribute anno = f.getAnnotation(StaticAttribute.class);
|
||||
String prefix = anno.value().isBlank() ?
|
||||
f.getName() + "Enum" :
|
||||
anno.value();
|
||||
|
||||
prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1);
|
||||
|
||||
if (optionNameMap.containsKey(prefix)) {
|
||||
log.warn("EnumOption name {}:{} has already been taken by {}, please check",
|
||||
prefix, f.getType().getName(), optionNameMap.get(prefix));
|
||||
continue;
|
||||
}
|
||||
|
||||
optionNameMap.put(prefix, f.getType().getName());
|
||||
|
||||
// enum 值列表
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) f.getType();
|
||||
Enum<?>[] constants = enumType.getEnumConstants();
|
||||
|
||||
// 构造 Map<name, constant>
|
||||
Map<String, Enum<?>> optionsMap = new LinkedHashMap<>();
|
||||
for (Enum<?> c : constants) {
|
||||
optionsMap.put(c.name(), c);
|
||||
}
|
||||
|
||||
// 放到 Model 里
|
||||
options.put(prefix, optionsMap);
|
||||
}
|
||||
|
||||
}
|
||||
for (Entry<String, Map<String, Enum<?>>> entry : options.entrySet()) {
|
||||
request.setAttribute(entry.getKey(), entry.getValue());
|
||||
log.debug("Inject enums {}: {} to request {}",
|
||||
entry.getKey(), optionNameMap.get(entry.getKey()),
|
||||
request.getRequestURI());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ConstructionGuard {
|
||||
private static final ThreadLocal<Set<Class<?>>> constructing = ThreadLocal.withInitial(HashSet::new);
|
||||
|
||||
public static boolean isConstructing(Class<?> clazz) {
|
||||
return constructing.get().contains(clazz);
|
||||
}
|
||||
|
||||
public static void enter(Class<?> clazz) {
|
||||
log.debug("Enter construction for {}", clazz.toString());
|
||||
if (isConstructing(clazz)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Class ")
|
||||
.append(clazz.getName())
|
||||
.append(" is being constructed but is seems like circular involving.");
|
||||
String msg = sb.toString();
|
||||
log.error(msg);
|
||||
throw new BeanCreationException(msg);
|
||||
}
|
||||
constructing.get().add(clazz);
|
||||
}
|
||||
|
||||
public static void exit(Class<?> clazz) {
|
||||
constructing.get().remove(clazz);
|
||||
log.debug("Exit construction for {}", clazz.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
import com.google.code.kaptcha.util.Config;
|
||||
|
||||
@Configuration
|
||||
public class KaptchaConfig {
|
||||
|
||||
@Bean(name = "kaptchaProperties")
|
||||
@ConfigurationProperties
|
||||
Properties getKaptchaProperties() {
|
||||
return new Properties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("kaptchaProperties")
|
||||
DefaultKaptcha getDefaultKaptcha(@Autowired Properties kaptchaProperties) {
|
||||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||
Config config = new Config(kaptchaProperties);
|
||||
defaultKaptcha.setConfig(config);
|
||||
|
||||
return defaultKaptcha;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* mybatis-plus分页插件
|
||||
*/
|
||||
@Bean
|
||||
MybatisPlusInterceptor paginationInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/link/at17/mid/tushare/config/QuartzConfig.java
Normal file
30
src/main/java/link/at17/mid/tushare/config/QuartzConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import org.quartz.Scheduler;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||
|
||||
import link.at17.mid.tushare.task.TaskSchedulerFactory;
|
||||
|
||||
@Configuration
|
||||
public class QuartzConfig {
|
||||
@Autowired
|
||||
private TaskSchedulerFactory taskSchedulerFactory;
|
||||
|
||||
@Bean
|
||||
public SchedulerFactoryBean schedulerFactoryBean() {
|
||||
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
|
||||
schedulerFactoryBean.setJobFactory(taskSchedulerFactory);
|
||||
return schedulerFactoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("scheduler")
|
||||
public Scheduler scheduler() {
|
||||
return schedulerFactoryBean().getScheduler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Reflections
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class ReflectionsConfig {
|
||||
|
||||
@Bean("reflections")
|
||||
Reflections reflections() {
|
||||
return new Reflections(new ConfigurationBuilder()
|
||||
.addScanners(
|
||||
Scanners.MethodsAnnotated,
|
||||
Scanners.SubTypes,
|
||||
Scanners.TypesAnnotated,
|
||||
Scanners.FieldsAnnotated)
|
||||
.forPackages("link.at17.mid.tushare"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import link.at17.mid.tushare.web.service.AuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final AuthService userDetailsService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.cacheControl(cache -> cache.disable())
|
||||
.frameOptions(frame -> frame.sameOrigin()))
|
||||
.csrf(CsrfConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/admin/res/**").permitAll()
|
||||
.requestMatchers("/favicon.ico").permitAll()
|
||||
.requestMatchers("/captcha/**").permitAll()
|
||||
.requestMatchers("/api/**").permitAll()
|
||||
.requestMatchers("/admin/login").permitAll()
|
||||
|
||||
// 静态资源
|
||||
.requestMatchers("/js/**").permitAll().requestMatchers("/css/**").permitAll()
|
||||
.requestMatchers("/images/**").permitAll().anyRequest().authenticated())
|
||||
.formLogin(form -> form // 开启表单登录,并指定登录页
|
||||
.loginPage("/admin/login") // 指定登录页
|
||||
.loginProcessingUrl("/admin/doLogin") // 处理登录请求的 URL
|
||||
.defaultSuccessUrl("/admin/manage", false) // 登录成功后默认跳转
|
||||
.permitAll())
|
||||
.logout(logout -> logout.logoutUrl("/admin/logout").logoutSuccessUrl("/admin/login")
|
||||
.invalidateHttpSession(true).permitAll());
|
||||
;
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(@Autowired PasswordEncoder passwordEncoder) {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
return new ProviderManager(provider);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
//@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) //session过期时间 如果部署多机环境,需要打开注释
|
||||
@ConditionalOnProperty(prefix = "littlesweetdog", name = "spring-session-open", havingValue = "true")
|
||||
public class SpringSessionConfig {
|
||||
|
||||
}
|
||||
24
src/main/java/link/at17/mid/tushare/config/VerichConfig.java
Normal file
24
src/main/java/link/at17/mid/tushare/config/VerichConfig.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package link.at17.mid.tushare.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import link.at17.mid.tushare.component.ConfigAutoRegistrar;
|
||||
import link.at17.mid.tushare.component.PlatformInterceptor;
|
||||
|
||||
@Configuration
|
||||
@Import(ConfigAutoRegistrar.class)
|
||||
public class VerichConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
PlatformInterceptor platformInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(platformInterceptor)
|
||||
.addPathPatterns("/**");
|
||||
}
|
||||
}
|
||||
19
src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java
Normal file
19
src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockAdjustDao extends ITsTradeDate {
|
||||
@BatchInsert
|
||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
|
||||
}
|
||||
181
src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java
Normal file
181
src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java
Normal file
@@ -0,0 +1,181 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockCalendar;
|
||||
import link.at17.mid.tushare.enums.StockMarket;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockCalendarDao extends BaseMapper<StockCalendar> {
|
||||
/**
|
||||
* 更新股票日历
|
||||
* @param list
|
||||
*/
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最近一个交易日历
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
||||
* @return
|
||||
*/
|
||||
StockCalendar getLatest(@Nullable StockMarket stockMarket);
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最早一个交易日历的日期
|
||||
* <p>仅为日期,并未指定是否是开市日
|
||||
* @see #getGreatest
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
||||
* @return {@code LocalDate} 或 {@code null}
|
||||
*/
|
||||
default LocalDate getLatestLocalDate(@Nullable StockMarket stockMarket) {
|
||||
StockCalendar stockCalendar = getLatest(stockMarket);
|
||||
if (stockCalendar != null) {
|
||||
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
||||
* <p>仅为日期时间,并未指定是否是开市日
|
||||
* @see #getGreatest
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定)
|
||||
* @return {@code LocalDateTime} 或 {@code null}
|
||||
*/
|
||||
default LocalDateTime getLatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
||||
StockCalendar stockCalendar = getLatest(stockMarket);
|
||||
if (stockCalendar != null) {
|
||||
return stockCalendar.getDate().atStartOfDay();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最早一个交易日历
|
||||
* <p>仅为日期,并未指定是否是开市日
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||
* @return
|
||||
*/
|
||||
StockCalendar getGreatest(@Nullable StockMarket stockMarket);
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最早一个交易日历的日期
|
||||
* <p>仅为日期,并未指定是否是开市日
|
||||
* @see #getGreatest
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||
* @return {@code LocalDate} 或 {@code null}
|
||||
*/
|
||||
default LocalDate getGreatestLocalDate(@Nullable StockMarket stockMarket) {
|
||||
StockCalendar stockCalendar = getGreatest(stockMarket);
|
||||
if (stockCalendar != null) {
|
||||
return stockCalendar.getDate().atStartOfDay().toLocalDate();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库内指定证交所的最早一个交易日历的日期时间
|
||||
* <p>仅为日期,并未指定是否是开市日
|
||||
* @see #getGreatest
|
||||
* @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定)
|
||||
* @return {@code LocalDateTime} 或 {@code null}
|
||||
*/
|
||||
default LocalDateTime getGreatestLocalDateTime(@Nullable StockMarket stockMarket) {
|
||||
StockCalendar stockCalendar = getGreatest(stockMarket);
|
||||
if (stockCalendar != null) {
|
||||
return stockCalendar.getDate().atStartOfDay();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定证交所两个日期(含)之间的交易日个数<br>>
|
||||
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||
* <p>例:
|
||||
* <ul><li>
|
||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||
* 返回 5
|
||||
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||
* 返回 1
|
||||
* <li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||
* <code>startDate : LocalDateTime 2021-08-15 01:20:30</code><br>
|
||||
* <code>endDate : LocalDateTime 2021-08-16 12:34:56</code><br>
|
||||
* 返回 1
|
||||
* </ul>
|
||||
* </p>
|
||||
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重计算
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return
|
||||
|
||||
*/
|
||||
long countOpenDaysBetween(@Param("exchange") StockMarket exchange, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate);
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定证交所两个日期(含)之间的所有交易日<br>
|
||||
* 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate} <br>
|
||||
* 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒
|
||||
* <p>例:
|
||||
* <ul><li><code>countOpenDaysBetween(StockMarket.<b><i>SH</i></b>, startDate, endDate)</code> 其中<br>
|
||||
* <code>startDate : LocalDateTime 2021-08-16 01:20:30</code><br>
|
||||
* <code>endDate : LocalDateTime 2021-08-20 19:20:30</code><br>
|
||||
* 返回:2021-08-16 2021-08-17 2021-08-18 2021-08-19 2021-08-20
|
||||
* </ul>
|
||||
* </p>
|
||||
* @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果
|
||||
* @param startDate 开始日期(含)
|
||||
* @param endDate 结束日期(含)
|
||||
* @return
|
||||
|
||||
*/
|
||||
List<LocalDateTime> getAllOpenDatesBetween(@Param("exchange") StockMarket exchange, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate);
|
||||
|
||||
/**
|
||||
* 获取指定证交所指定日期偏移的指定交易日
|
||||
* @param exchange
|
||||
* @param date
|
||||
* @param offset
|
||||
* @return
|
||||
*/
|
||||
LocalDateTime getOpenDateOffset(@Param("exchange") StockMarket exchange, @Param("date") Temporal date, @Param("offset") long offset);
|
||||
|
||||
/**
|
||||
* 获取指定证交所指定日期(含)以后的所有交易日
|
||||
* @param exchange
|
||||
* @param after 留空则查询所有
|
||||
* @return
|
||||
*/
|
||||
List<LocalDateTime> getAllOpenDate(@Param("exchange") @Nullable StockMarket exchange, @Param("after") @Nullable Temporal after);
|
||||
|
||||
/**
|
||||
* 判断指定日是否为指定交易所的交易日
|
||||
* @param date 会截断时分秒,只取年月日
|
||||
* @param exchange 目前只支持 SSE(深交所)和 SHSE(上交所)
|
||||
* @return
|
||||
* @see StockCalendarDao#isOpen(Temporal, StockMarket)
|
||||
*/
|
||||
boolean isOpen(@Param("date") @NonNull Temporal date, @Param("exchange") @Nullable StockMarket exchange);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockDailyBasicDao extends ITsTradeDate {
|
||||
/**
|
||||
* 批量插入/更新日基本数据
|
||||
* @param list
|
||||
*/
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
}
|
||||
77
src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java
Normal file
77
src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockDailyDao extends ITsTradeDate {
|
||||
/**
|
||||
* 批量插入/更新日线数据
|
||||
* @param list
|
||||
*/
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
/**
|
||||
* 获取除权日线数据 + 基本行情数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param endDate 结束日期(包含),留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValueEx> getExDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
||||
/**
|
||||
* 获取前复权日线数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param startDate 开始日期,留空则为股票上市日起
|
||||
* @param endDate 结束日期,null 则为最新一个交易日
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getQfqDailyTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
/**
|
||||
* 获取前复权日线数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
||||
/**
|
||||
* 获取前复权日线数据 + 基本行情数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValueEx> getExQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after);
|
||||
/**
|
||||
* 获取前复权日线数据 + 基本行情数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,null 则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValueEx> getExQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
||||
/**
|
||||
* 获取前复权日线数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param startDate 开始日期(包含),留空则为上市第一日
|
||||
* @param after 多少个交易日以前,null 则查询 startDate 以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after);
|
||||
|
||||
}
|
||||
28
src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java
Normal file
28
src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.cache.EvictAfterUpdate;
|
||||
import link.at17.mid.tushare.data.models.StockHolder;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
import link.at17.mid.tushare.enums.StockHolderType;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockHolderDao extends ITsTradeDate {
|
||||
@BatchInsert
|
||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list, @NonNull StockHolderType holderType);
|
||||
|
||||
@EvictAfterUpdate("tushare")
|
||||
@Cacheable("stockHolderDao.getAllByStockCode")
|
||||
List<StockHolder> getAllByStockCode(@NonNull String stockCode, @NonNull StockHolderType holderType);
|
||||
}
|
||||
24
src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java
Normal file
24
src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockInfo;
|
||||
import link.at17.mid.tushare.enums.ListStatus;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockInfoDao extends BaseMapper<StockInfo> {
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
StockInfo getStockInfoByStockCode(@Param("stockCode") String stockCode);
|
||||
List<StockInfo> getStockListByListStatus(ListStatus listStatus);
|
||||
}
|
||||
21
src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java
Normal file
21
src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockLimit;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockLimitDao extends BaseMapper<StockLimit>, ITsTradeDate {
|
||||
@BatchInsert
|
||||
int insertOrUpdateListTushare(@BatchList List<JSONObject> list);
|
||||
}
|
||||
77
src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java
Normal file
77
src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||
import link.at17.mid.tushare.enums.StockSpan;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockMinuteDao {
|
||||
/**
|
||||
* 批量插入/更新日线数据
|
||||
* @param list
|
||||
*/
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@NotNull StockSpan stockSpan, @BatchList List<JSONObject> list);
|
||||
|
||||
/**
|
||||
* 获取最新交易日<br/>
|
||||
* 获取到股票的最新交易日
|
||||
* @param stockSpan 分钟线频率
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
LocalDateTime getLatestTradeDate(@NotNull StockSpan stockSpan, ITsStockInfo stockInfo);
|
||||
/**
|
||||
* 获取指定 freq 下的所有交易日
|
||||
* @param stockSpan 分钟线频率
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
List<LocalDateTime> getAllTradeDates(@NotNull StockSpan stockSpan, ITsStockInfo stockInfo);
|
||||
/**
|
||||
* 获取指定 freq 下的数据缺失日,包括分钟数据不全日<br/>
|
||||
* 如:60 分钟频率下,一日内 K 线数应为 240/60 + 1 = 5 条,则小于 5 条的日期都将被列为缺失日期
|
||||
* @param stockSpan 分钟线频率
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
List<LocalDateTime> getAllMissingDates(@NotNull StockSpan stockSpan, ITsStockInfo stockInfo);
|
||||
/**
|
||||
* 获取前复权日线数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param stockSpan 分钟线频率
|
||||
* @param startDate 开始日期,留空则为股票上市日起
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getQfqMinuteTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime startDate, LocalDateTime endDate);
|
||||
/**
|
||||
* 获取前复权日线数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before);
|
||||
/**
|
||||
* 获取前复权日线 Ex 数据
|
||||
* @param stockCode 股票代码,不允许为空
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValueEx> getExQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockThsDailyDao extends ITsTradeDate {
|
||||
/**
|
||||
* 批量插入/更新日线数据
|
||||
* @param list
|
||||
*/
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
/**
|
||||
* 获取板块日线数据
|
||||
* @param stockCode 板块代码,不允许为空
|
||||
* @param endDate 结束日期,留空则为最新一个交易日
|
||||
* @param before 多少个交易日以前,留空则查询上市以来所有数据
|
||||
* @return
|
||||
*/
|
||||
List<StockValue> getDailyBeforeTushare(@Param("stockCode") @NonNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before);
|
||||
}
|
||||
25
src/main/java/link/at17/mid/tushare/dao/StockThsListDao.java
Normal file
25
src/main/java/link/at17/mid/tushare/dao/StockThsListDao.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.ThsStockInfo;
|
||||
import link.at17.mid.tushare.enums.ThsStockMarket;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockThsListDao extends BaseMapper<ThsStockInfo> {
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
default List<ThsStockInfo> listByExchange(ThsStockMarket exchange){
|
||||
return selectList(new LambdaQueryWrapper<ThsStockInfo>().eq(exchange != null, ThsStockInfo::getExchange, exchange));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package link.at17.mid.tushare.dao;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.annotation.BatchInsert;
|
||||
import link.at17.mid.tushare.annotation.BatchList;
|
||||
import link.at17.mid.tushare.data.models.StockInfo;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface StockThsMemberDao {
|
||||
@BatchInsert
|
||||
void insertOrUpdateList(@BatchList List<JSONObject> list);
|
||||
List<StockInfo> getThsMembers(String tsCode);
|
||||
/**
|
||||
* 获取同花顺所属概念
|
||||
* @param tsCode
|
||||
* @return
|
||||
*/
|
||||
String getBelongings(String tsCode);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package link.at17.mid.tushare.data.crawler;
|
||||
|
||||
|
||||
/**
|
||||
* 查询方式
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
public enum QueryWay {
|
||||
/**
|
||||
* 以日期更新,适用于已有从上市开始的部分数据的更新
|
||||
*/
|
||||
ByDateUpdate,
|
||||
/**
|
||||
* 以日期扫描所有,适用于初始化
|
||||
*/
|
||||
ByDateAll,
|
||||
/**
|
||||
* 交叉检查个股和交易日历,而后按照数据缺失日期更新
|
||||
*/
|
||||
ByDateCrossCheck,
|
||||
/**
|
||||
* 以个股最新日数据为起点更新至今,更适用于初始化
|
||||
*/
|
||||
ByStock,
|
||||
/**
|
||||
* 交叉检查个股和交易日历,而后按照数据缺失个股和日期更新
|
||||
*/
|
||||
ByStockCrossCheck,
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package link.at17.mid.tushare.data.crawler;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 线程安全的重试与延迟控制器
|
||||
*/
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
public class RetryAndDelay {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private int retryTime = 5;
|
||||
|
||||
private volatile long delayBase = 500L;
|
||||
|
||||
// 用 AtomicReference<Double> 存储 delayFactor
|
||||
private final AtomicReference<Double> delayFactor = new AtomicReference<>(1d);
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private int threadNum = 1;
|
||||
|
||||
private final AtomicInteger triggerFactorAdd = new AtomicInteger(0);
|
||||
|
||||
// 最大因子限制,避免无限增长
|
||||
@Getter
|
||||
@Setter
|
||||
private double maxDelayFactor = 5.0;
|
||||
|
||||
/**
|
||||
* 计算当前延迟
|
||||
*/
|
||||
public long getDelay() {
|
||||
double factor = delayFactor.get();
|
||||
long delay = (long) Math.floor(delayBase * factor);
|
||||
|
||||
// 衰减回 1
|
||||
if (factor > 1) {
|
||||
double newFactor = Math.max(1d, factor - 0.0005);
|
||||
delayFactor.compareAndSet(factor, newFactor);
|
||||
} else if (factor < 1) {
|
||||
delayFactor.set(1d);
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
public RetryAndDelay setDelay(long delay) {
|
||||
this.delayBase = delay;
|
||||
this.delayFactor.set(1d);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按每分钟请求限制更新
|
||||
*/
|
||||
public RetryAndDelay setDelayByMinLimit(int minLimit) {
|
||||
return setDelayBySpanAndLimit(60000, minLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按每小时请求限制更新
|
||||
*/
|
||||
public RetryAndDelay setDelayByHourLimit(int hourLimit) {
|
||||
return setDelayBySpanAndLimit(3600000, hourLimit);
|
||||
}
|
||||
|
||||
private RetryAndDelay setDelayBySpanAndLimit(int spanMs, int limit) {
|
||||
if (threadNum <= 0) {
|
||||
threadNum = 1;
|
||||
}
|
||||
|
||||
long newDelay = (long) Math.floor(1.0d * spanMs / limit * threadNum);
|
||||
log.debug("Trigger crawler limit = {}/span, delay = {}ms/{} threads", limit, newDelay, threadNum);
|
||||
|
||||
if (delayBase == newDelay) {
|
||||
int count = triggerFactorAdd.incrementAndGet();
|
||||
if (count >= threadNum) {
|
||||
// 增加因子,但不超过最大值
|
||||
delayFactor.updateAndGet(f -> Math.min(maxDelayFactor, f + 0.05));
|
||||
triggerFactorAdd.set(0);
|
||||
}
|
||||
log.debug("Delay is still too short, increase delayFactor = {}, new delay = {}", delayFactor.get(), getDelay());
|
||||
} else {
|
||||
delayBase = newDelay;
|
||||
triggerFactorAdd.set(0);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONException;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Slf4j
|
||||
public class TushareClient {
|
||||
|
||||
private static final String TUSHARE_URL = "http://api.tushare.pro";
|
||||
// private static String token = "83a82fadb0bbb803f008b31ce09479e5107f4aba3f28d5df2174c642";
|
||||
// private static String token = "c473f86ae2f5703f58eecf9864fa9ec91d67edbc01e3294f6a4f9c32";
|
||||
// private static String token = "ec3d7415bf3dfebf0f27b1e5a9805f809b083fb1f8590c8c2bdc633f";
|
||||
private static String token = "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509";
|
||||
|
||||
/**
|
||||
* 返回原始查询结果
|
||||
* @param tr
|
||||
* @return
|
||||
*/
|
||||
public static JSONObject query(TushareRequestBody tr) throws IOException, JSONException {
|
||||
OkHttpClient okHttpClient = new OkHttpClient();
|
||||
tr.setToken(token);
|
||||
Request request = new Request.Builder()
|
||||
.url(TUSHARE_URL)
|
||||
.post(
|
||||
RequestBody.create(
|
||||
tr.toJSONString(),
|
||||
MediaType.parse("application/json; charset=utf-8")))
|
||||
.build();
|
||||
|
||||
final Call call = okHttpClient.newCall(request);
|
||||
Response response = call.execute();
|
||||
JSONObject result = JSONObject.parseObject(response.body().string());
|
||||
if (result.get("data") == null) {
|
||||
response.close();
|
||||
throw new TushareException(result.getString("msg"));
|
||||
}
|
||||
response.close();
|
||||
return result.getJSONObject("data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回封装查询结果
|
||||
* @param tr
|
||||
* @return null 说明请求出错,EmptyList 说明请求成功但结果列表为空
|
||||
* @throws IOException
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static List<JSONObject> queryList(TushareRequestBody tr) throws JSONException, IOException {
|
||||
JSONObject jo = query(tr);
|
||||
if (jo == null) {
|
||||
return null;
|
||||
}
|
||||
List<JSONObject> result = new ArrayList<JSONObject>();
|
||||
JSONArray fields = jo.getJSONArray("fields");
|
||||
JSONArray items = jo.getJSONArray("items");
|
||||
int fieldsCount = fields.size();
|
||||
for (Object o : items) {
|
||||
JSONArray ja = (JSONArray)o;
|
||||
JSONObject item = new JSONObject();
|
||||
for (int i = 0; i < fieldsCount; i++) {
|
||||
item.put(fields.getString(i), ja.get(i));
|
||||
}
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
/*
|
||||
* 测试获取股票列表
|
||||
TushareRequestBody request =
|
||||
new TushareRequestBody("stock_basic")
|
||||
.addParam("list_status", "L")
|
||||
.addFields("ts_code,name,area,industry,market,list_date,delist_date");
|
||||
System.out.println(query(request).toJSONString());
|
||||
*/
|
||||
|
||||
TushareRequestBody request =
|
||||
new TushareRequestBody("daily")
|
||||
.addParam("start_date", "19910101")
|
||||
.addParam("end_date", "20180101")
|
||||
.addFields("ts_code");
|
||||
Queue<TushareRequestBody> requestQueue = new LinkedList<>();
|
||||
requestQueue.add(request);
|
||||
requestQueue.add(request.clone().addParam("start_date", "fuck").addField("fuckyou"));
|
||||
// 测试 clone 是否成功
|
||||
requestQueue.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.alibaba.fastjson2.JSONException;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.dao.StockAdjustDao;
|
||||
import link.at17.mid.tushare.dao.StockCalendarDao;
|
||||
import link.at17.mid.tushare.dao.StockDailyBasicDao;
|
||||
import link.at17.mid.tushare.dao.StockDailyDao;
|
||||
import link.at17.mid.tushare.dao.StockHolderDao;
|
||||
import link.at17.mid.tushare.dao.StockInfoDao;
|
||||
import link.at17.mid.tushare.dao.StockLimitDao;
|
||||
import link.at17.mid.tushare.dao.StockMinuteDao;
|
||||
import link.at17.mid.tushare.dao.StockThsDailyDao;
|
||||
import link.at17.mid.tushare.dao.StockThsListDao;
|
||||
import link.at17.mid.tushare.dao.StockThsMemberDao;
|
||||
import link.at17.mid.tushare.data.crawler.QueryWay;
|
||||
import link.at17.mid.tushare.data.crawler.RetryAndDelay;
|
||||
import link.at17.mid.tushare.data.models.StockInfo;
|
||||
import link.at17.mid.tushare.data.models.ThsStockInfo;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate;
|
||||
import link.at17.mid.tushare.enums.StockHolderType;
|
||||
import link.at17.mid.tushare.enums.StockMarket;
|
||||
import link.at17.mid.tushare.enums.StockSpan;
|
||||
import link.at17.mid.tushare.enums.ThsStockMarket;
|
||||
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Tushare 爬虫
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TushareCrawler {
|
||||
|
||||
|
||||
@Autowired
|
||||
private StockInfoDao stockInfoDao;
|
||||
@Autowired
|
||||
private StockDailyDao stockDailyDao;
|
||||
@Autowired
|
||||
private StockMinuteDao stockMinuteDao;
|
||||
@Autowired
|
||||
private StockDailyBasicDao stockDailyBasicDao;
|
||||
@Autowired
|
||||
private StockCalendarDao stockCalendarDao;
|
||||
@Autowired
|
||||
private StockAdjustDao stockAdjustDao;
|
||||
@Autowired
|
||||
private StockThsListDao stockThsListDao;
|
||||
@Autowired
|
||||
private StockThsMemberDao stockThsMemberDao;
|
||||
@Autowired
|
||||
private StockThsDailyDao stockThsDailyDao;
|
||||
@Autowired
|
||||
private StockHolderDao stockHolderDao;
|
||||
@Autowired
|
||||
private StockLimitDao stockLimitDao;
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redis;
|
||||
|
||||
private static final LocalDateTime THS_DAILY_BEGIN_LOCALDATE = LocalDateTime.of(2007, 8, 1, 0, 0, 0);
|
||||
|
||||
private static final int MAX_THREADS = 5;
|
||||
/**
|
||||
* 根据日期滚动查询日内所有股票相关信息<br>
|
||||
* 该查询的起始日是指交易日历内的最新日,故在调用该方法之前,请确保交易日历已最新,且所有股票已有的数据都是最新日数据<br>
|
||||
*
|
||||
* 该接口使用 trade_date 进行轮询
|
||||
* @param baseRequest
|
||||
* @param ITsTradeDate 实现了 ITsTradeDate 接口的实体
|
||||
* @param processDataFunc 获取 List 以后的处理方法
|
||||
* @param QueryWay 查询方法
|
||||
* @param start 开始日期,留空则默认根据 ITsTradeDate、stockCalendarDao 或个股 listDate 查询
|
||||
* @param useTradeDate 是否使用 trade_date 字段来获取数据,否则使用 start_date 和 end_date 来获取(如分钟数据)
|
||||
* @return
|
||||
* @see TushareCrawler#rollingQueryByStock
|
||||
*/
|
||||
private List<Future<TushareCrawlerResult>> rollingQueryByDate(TushareRequestBody baseRequest, ITsTradeDate iTradeDate,
|
||||
Function<List<JSONObject>, Boolean> processDataFunc, QueryWay queryWay, LocalDateTime start){
|
||||
ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);
|
||||
RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS);
|
||||
List<Future<TushareCrawlerResult>> executeResult = new ArrayList<>();
|
||||
|
||||
// 生成指纹
|
||||
baseRequest.generateFingerPrint();
|
||||
|
||||
if (queryWay != QueryWay.ByDateCrossCheck) {
|
||||
// 查询最新日到当日
|
||||
|
||||
boolean all = queryWay.equals(QueryWay.ByDateAll) || iTradeDate == null;
|
||||
|
||||
if (start == null) {
|
||||
if (!all && queryWay != QueryWay.ByDateUpdate) {
|
||||
queryWay = QueryWay.ByDateCrossCheck;
|
||||
return rollingQueryByDate(baseRequest, iTradeDate, processDataFunc, queryWay, start);
|
||||
}
|
||||
start = stockCalendarDao.getGreatestLocalDateTime(null);
|
||||
}
|
||||
|
||||
if (queryWay == QueryWay.ByDateUpdate && iTradeDate != null) {
|
||||
LocalDateTime iTradeDateLatest = iTradeDate.getLatestTradeDate(null);
|
||||
if (iTradeDateLatest != null &&
|
||||
iTradeDateLatest.toLocalDate().compareTo(start.toLocalDate()) > 0) {
|
||||
// 数据库内最新数据存在,且大于开市日期,说明有一定数据,从数据库内最新日期开始更新
|
||||
start = iTradeDateLatest;
|
||||
}
|
||||
}
|
||||
start = start.toLocalDate().atStartOfDay();
|
||||
LocalDateTime end = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||
|
||||
while(!start.isAfter(end)) {
|
||||
if (all || stockCalendarDao.isOpen(start, (StockMarket)null)) {
|
||||
TushareRequestBody rq = baseRequest.clone()
|
||||
.addDateParam("trade_date", start);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
}
|
||||
start = start.plusDays(1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (iTradeDate == null) {
|
||||
throw new RuntimeException("For queryWay = QueryWay.ByDateCrossCheck, iTradeDate must not be null!");
|
||||
}
|
||||
// 交叉检查
|
||||
final LocalDateTime currentDate = LocalDateTime.now();
|
||||
final List<LocalDateTime> szStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate);
|
||||
final List<LocalDateTime> shStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SH, null, currentDate);
|
||||
final List<LocalDateTime> allStockCalendars = stockCalendarDao.getAllOpenDatesBetween((StockMarket)null, null, currentDate);
|
||||
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
|
||||
TreeSet<LocalDateTime> needUpdates = new TreeSet<>();
|
||||
stockInfos.forEach(stockInfo -> {
|
||||
List<LocalDateTime> stockTradeDates = iTradeDate.getAllTradeDates(stockInfo);
|
||||
StockMarket exchange = stockInfo.getExchange();
|
||||
List<LocalDateTime> crossToList = exchange == StockMarket.SZ ? szStockCalendars :
|
||||
exchange == StockMarket.SH ? shStockCalendars : allStockCalendars;
|
||||
Stream<LocalDateTime> crossTo;
|
||||
LocalDateTime listDate = stockInfo.getListDate(), delistDate = stockInfo.getDelistDate();
|
||||
if (delistDate != null) {
|
||||
crossTo = crossToList.stream().filter(date -> (date.isAfter(listDate) || date.equals(listDate)) && date.isBefore(delistDate));
|
||||
}
|
||||
else {
|
||||
crossTo = crossToList.stream().filter(date -> date.isAfter(listDate) || date.equals(listDate));
|
||||
}
|
||||
|
||||
crossTo.filter(date -> !stockTradeDates.contains(date)).forEach(needUpdates::add);
|
||||
});
|
||||
needUpdates.descendingSet().forEach(date -> {
|
||||
TushareRequestBody rq = baseRequest.clone()
|
||||
.addDateParam("trade_date", date);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
});
|
||||
}
|
||||
es.shutdown();
|
||||
while (!es.isTerminated()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
return executeResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据股票滚动查询,用于获取从上市以来/已有最新记录日期到结束日期的股票的信息轮询<br>
|
||||
* 除最初初始化时以外,不建议采用该方法进行轮询,请采用 rollingQueryByDate 方法进行轮询<br>
|
||||
*
|
||||
* 存储在数据库内相关联的最新日期的字段名必须为 trade_date<br>
|
||||
* 必须是采用 ts_code, start_date 和 end_date 进行查询的接口才可以用
|
||||
* @param baseRequest 基本请求,给定接口名和 fields 就行了
|
||||
* @param stockList
|
||||
* @param iTradeDate 对应的根据 ts_code 获取数据库内最新一条记录的接口实现,可以检查 list_date 和 delist_date,当为空时,只根据 ts_code 轮询
|
||||
* @param afterRespFunc 获取结果后处理的方法,返回是否成功
|
||||
* @param singleMax 单次最大请求条数
|
||||
* @return 每条请求的执行结果
|
||||
* @see TushareCrawler#rollingQueryByDate
|
||||
*/
|
||||
private List<Future<TushareCrawlerResult>> rollingQueryByStock(
|
||||
TushareRequestBody baseRequest,
|
||||
List<? extends ITsStockInfo> stockList,
|
||||
ITsTradeDate iTradeDate,
|
||||
Function<List<JSONObject>, Boolean> afterRespFunc,
|
||||
long singleMax,
|
||||
QueryWay queryWay,
|
||||
StockSpan stockSpan) {
|
||||
|
||||
ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS);
|
||||
RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS);
|
||||
List<Future<TushareCrawlerResult>> executeResult = new ArrayList<>();
|
||||
|
||||
// 生成指纹
|
||||
baseRequest.generateFingerPrint();
|
||||
|
||||
if (stockSpan == null) {
|
||||
stockSpan = StockSpan.Daily;
|
||||
}
|
||||
|
||||
if (queryWay != QueryWay.ByStockCrossCheck) {
|
||||
Assert.isTrue(singleMax > 0, "单次最大请求条数 singleMax 必须大于 0");;
|
||||
|
||||
boolean needCheckDate = iTradeDate != null;
|
||||
|
||||
for (ITsStockInfo stockInfo : stockList) {
|
||||
String stockCode = stockInfo.getTsCode();
|
||||
if (needCheckDate) {
|
||||
LocalDateTime start = iTradeDate.getLatestTradeDate(stockInfo);
|
||||
LocalDateTime end = stockInfo.getDelistDate();
|
||||
// 是否退市
|
||||
if (null == end) {
|
||||
// 未退市或没有 delist_date 字段
|
||||
end = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||
}
|
||||
if (null == start) {
|
||||
// 最新日K为空,则从头开始查询该股所有日K
|
||||
start = stockInfo.getListDate();
|
||||
}
|
||||
else {
|
||||
// 最新日K存在,则查询从最新日开始至今的所有日K并更新
|
||||
start = start.toLocalDate().atStartOfDay();
|
||||
if (start.compareTo(end) == 0) {
|
||||
// 已经是最新了
|
||||
continue;
|
||||
}
|
||||
start = start.plusDays(1);
|
||||
LocalDateTime today = LocalDateTime.now();
|
||||
long todayPassHours = ChronoUnit.HOURS.between(today.toLocalDate().atStartOfDay(), today);
|
||||
if (start.compareTo(end) == 0 && todayPassHours < 15) {
|
||||
// 请求当日数据但未到当日收盘时间
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 各宽限一天,保证数据补缺、分钟数据
|
||||
start = start.minusDays(1);
|
||||
end = end.plusDays(1);
|
||||
TushareRequestBody rq = baseRequest.clone()
|
||||
.addDateParam("start_date", start)
|
||||
.addParam("ts_code", stockCode);
|
||||
|
||||
// TODO: 如果是 BJ?
|
||||
StockMarket exchange = stockCode.endsWith("SZ") ? StockMarket.SZ : StockMarket.SH ; //stockCode.endsWith("SZ") ? "SZSE" : stockCode.endsWith("SH") ? "SSE" : null;
|
||||
|
||||
// 计算 start 和 end 之间一共有多少个交易日
|
||||
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
|
||||
long dataPerDay = 1;
|
||||
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
||||
// 分钟数,重新算下 dataBetween
|
||||
dataPerDay = 240L / stockSpan.getMin() + 1;
|
||||
}
|
||||
dataBetween *= dataPerDay;
|
||||
long daySpan = singleMax / dataPerDay;
|
||||
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
||||
while (dataBetween > singleMax) {
|
||||
rq.addDateParam("end_date",tmpEndDate);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
|
||||
start = tmpEndDate;
|
||||
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
||||
rq.addDateParam("start_date", start);
|
||||
dataBetween -= daySpan * dataPerDay;
|
||||
}
|
||||
rq.addDateParam("end_date", end);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
}
|
||||
else {
|
||||
TushareRequestBody rq = baseRequest.clone()
|
||||
.addParam("ts_code", stockCode);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 交叉检查
|
||||
final List<StockInfo> stockInfos = stockInfoDao.getStockListByListStatus(null);
|
||||
for(StockInfo stockInfo : stockInfos) {
|
||||
StockMarket exchange = stockInfo.getExchange();
|
||||
List<LocalDateTime> missingDates = iTradeDate.getAllMissingDates(stockInfo);
|
||||
if (missingDates.size() > 0) {
|
||||
Collections.sort(missingDates);
|
||||
int i = 0, maxI = missingDates.size() - 1;
|
||||
LocalDateTime start = missingDates.get(0), end = missingDates.get(maxI);
|
||||
end = end.plusDays(1); // end 宽限 1 天,对应分钟数据的情况
|
||||
TushareRequestBody rq = baseRequest.clone()
|
||||
.addDateParam("start_date", start)
|
||||
.addParam("ts_code", stockInfo.getTsCode());
|
||||
|
||||
// 需要注意的是:交叉检查后的 start 和 end 和非交叉检查的不一样
|
||||
// 不应成为一个范围。比如如果只缺失了 start = 2021-01-09 和 end
|
||||
// = 2022-12-06 两天,成为范围以后将会变成更新 start 和 end 之间
|
||||
// 的所有数据,这样将会大大增加不必要的更新请求。
|
||||
|
||||
long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end);
|
||||
long dataPerDay = 1;
|
||||
if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) {
|
||||
// 分钟数,重新算下 dataBetween
|
||||
dataPerDay = 240L / stockSpan.getMin() + 1;
|
||||
}
|
||||
dataBetween *= dataPerDay;
|
||||
long daySpan = singleMax / dataPerDay;
|
||||
|
||||
LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
||||
while (dataBetween > singleMax) {
|
||||
rq.addDateParam("end_date", tmpEndDate);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
|
||||
start = tmpEndDate;
|
||||
while (i < maxI && start.isBefore(missingDates.get(i + 1))) {
|
||||
start = missingDates.get(++i);
|
||||
}
|
||||
tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan);
|
||||
rq.addDateParam("start_date", start);
|
||||
dataBetween -= daySpan * dataPerDay;
|
||||
}
|
||||
rq.addDateParam("end_date", end);
|
||||
executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc)
|
||||
.setRetryAndDelay(retryAndDelay)));
|
||||
}
|
||||
};
|
||||
}
|
||||
es.shutdown();
|
||||
while (!es.isTerminated()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
return executeResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* <a href="https://tushare.pro/document/2?doc_id=25"><b>股票列表</b></a><br>
|
||||
* <b>更新股票列表</b><br/>
|
||||
* 接口:stock_basic,可以通过数据工具调试和查看数据<br>
|
||||
* 描述:获取基础信息数据,包括股票代码、名称、上市日期、退市日期等<br>
|
||||
* 积分:2000积分起<br>
|
||||
* 包括上市、退市和暂停上市,无权限(积分不足)状态下每小时最多访问该接口 1 次
|
||||
*/
|
||||
public boolean updateStockList() {
|
||||
try {
|
||||
List<JSONObject> stockInfos = new ArrayList<>();
|
||||
// Tushare 经常改请求参数的规则,2024/11/13 更新:默认不提供 list_status 时,默认值是 L
|
||||
TushareRequestBody requestBody = new TushareRequestBody("stock_basic")
|
||||
.addFields("ts_code,name,area,industry,market,list_status,list_date,delist_date");
|
||||
|
||||
requestBody.addParam("list_status", "L");
|
||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||
|
||||
requestBody.addParam("list_status", "D");
|
||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||
|
||||
requestBody.addParam("list_status", "P");
|
||||
stockInfos.addAll(TushareClient.queryList(requestBody));
|
||||
|
||||
stockInfoDao.insertOrUpdateList(stockInfos);
|
||||
|
||||
log.info("更新股票列表完成");
|
||||
return true;
|
||||
} catch (JSONException | IOException e) {
|
||||
log.error("更新股票列表时发生错误", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://tushare.pro/document/2?doc_id=259"><b>同花顺概念和行业指数</b></a><br>
|
||||
* 接口:ths_index<br>
|
||||
* 描述:获取同花顺板块指数。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。<br>
|
||||
* 限量:本接口需获得600积分,单次最大5000,一次可提取全部数据,请勿循环提取。
|
||||
* @return
|
||||
*/
|
||||
public boolean updateThsList() {
|
||||
try {
|
||||
List<JSONObject> thsIndexes = TushareClient.queryList(new TushareRequestBody("ths_index"));
|
||||
stockThsListDao.insertOrUpdateList(thsIndexes);
|
||||
log.info("更新同花顺板块列表完成");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("更新同花顺板块列表时发生错误", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://tushare.pro/document/2?doc_id=261"><b>同花顺概念板块成分</b></a><br>
|
||||
* 接口:ths_member<br>
|
||||
* 描述:获取同花顺概念板块成分列表注:数据版权归属同花顺,如做商业用途,请主动联系同花顺。<br>
|
||||
* 限量:用户积累5000积分可调取,可按概念板块代码循环提取所有成分<br>
|
||||
*/
|
||||
public void updateThsMember() {
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("ths_member").addFields("ts_code,con_code,con_name,weight,in_date,out_date,is_new");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockThsMemberDao.insertOrUpdateList(t);
|
||||
return true;
|
||||
};
|
||||
List<ThsStockInfo> stockInfoList = stockThsListDao.listByExchange(null);
|
||||
while (true) {
|
||||
List<Future<TushareCrawlerResult>> executeResult = rollingQueryByStock(baseRequest, stockInfoList, null, function, 5000L, QueryWay.ByStock, null);
|
||||
|
||||
stockInfoList.clear();
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = result.getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("同花顺概念板块成分数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.warn("同花顺概念板块成分数据未获取:{}", request.toJSONString());
|
||||
stockInfoList.add(request.getJSONObject("params").to(ThsStockInfo.class));
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("同花顺概念板块成分数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (stockInfoList.size() == 0) {
|
||||
log.info("同花顺概念板块成分更新完成");
|
||||
break;
|
||||
}
|
||||
log.info("重新获取未获取成功的同花顺概念板块成分");
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://tushare.pro/document/2?doc_id=260"><b>同花顺板块指数行情</b></a><br>
|
||||
* 接口:ths_daily<br>
|
||||
* 描述:获取同花顺板块指数行情。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。<br>
|
||||
* 限量:单次最大3000行数据,可根据指数代码、日期参数循环提取。<br>
|
||||
* @param queryWay
|
||||
*/
|
||||
public void updateThsDaily(QueryWay queryWay) {
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("ths_daily").addFields("ts_code,trade_date,close,open,high,low,pre_close,avg_price,change,pct_change,vol,turnover_rate,total_mv,float_mv");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockThsDailyDao.insertOrUpdateList(t);
|
||||
return true;
|
||||
};
|
||||
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||
executeResult = rollingQueryByStock(baseRequest, stockThsListDao.listByExchange(ThsStockMarket.A), stockThsDailyDao, function, 3000L, queryWay, null);
|
||||
}
|
||||
else {
|
||||
executeResult = rollingQueryByDate(baseRequest, stockThsDailyDao, function, queryWay, THS_DAILY_BEGIN_LOCALDATE);
|
||||
}
|
||||
log.info("同花顺板块指数行情更新完成");
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = result.getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("同花顺板块指数行情数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.warn("同花顺板块指数行情数据未获取:{}", request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("同花顺板块指数行情数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b><a href="https://waditu.com/document/2?doc_id=27">更新日K数据</a></b><br/>
|
||||
* 基础积分每分钟内最多调取500次,每次5000条数据,相当于23年历史,用户获得超过5000积分正常调取无频次限制。
|
||||
* <br>
|
||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票日 K 数据缺失</font>
|
||||
* @param queryWay 滚动查询方式
|
||||
* @see TushareCrawler#rollingQueryByDate
|
||||
* @see TushareCrawler#rollingQueryByStock
|
||||
*/
|
||||
public void updateStockDaily(QueryWay queryWay) {
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("daily")
|
||||
.addFields("ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockDailyDao.insertOrUpdateList(t);
|
||||
return true;
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyDao, function, 5000L, queryWay, null);
|
||||
}
|
||||
else {
|
||||
executeResult = rollingQueryByDate(baseRequest, stockDailyDao, function, queryWay, null);
|
||||
}
|
||||
log.info("日 K 数据更新完成");
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("日 K 数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.warn("日 K 数据未获取:{}", request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查日 K 数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* <b><a href="https://tushare.pro/document/1?doc_id=290">更新分钟K数据</a></b><br/>
|
||||
* <p>有权限时,每分钟500次,每次8000行数据,总量不限制</p>
|
||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票分钟 K 数据缺失</font>
|
||||
* @param queryWay 滚动查询方式,仅支持 StockSpan.ByStock... 系列
|
||||
* @param stockSpan 股票粒度,仅支持 StockSpan.Minute - StockSpan.SixtyMinute
|
||||
* @see TushareCrawler#rollingQueryByDate
|
||||
* @see TushareCrawler#rollingQueryByStock
|
||||
* @see link.at17.mid.tushare.enums.StockSpan
|
||||
*/
|
||||
public void updateStockMinValue(QueryWay queryWay, StockSpan stockSpan) {
|
||||
Assert.isTrue(Objects.nonNull(stockSpan), "stockSpan 不允许为空");
|
||||
Assert.isTrue(Objects.nonNull(stockSpan.getMin()), "不支持的 StockSpan:" + stockSpan + ", 仅支持分钟数据类型");
|
||||
Assert.isTrue(queryWay.compareTo(QueryWay.ByStock) >= 0, "不支持的 QueryWay!");
|
||||
String freq = stockSpan.getMin() + "min";
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("stk_mins")
|
||||
.addFields("ts_code,trade_time,open,close,high,low,vol,amount")
|
||||
.addParam("freq", freq);
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockMinuteDao.insertOrUpdateList(stockSpan, t);
|
||||
return true;
|
||||
};
|
||||
// 特殊重写:获取最新日期和所有交易日,该重写仅对更新数据生效
|
||||
ITsTradeDate minTradeDate = new ITsTradeDate() {
|
||||
@Override
|
||||
public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) {
|
||||
return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo);
|
||||
}
|
||||
@Override
|
||||
public List<LocalDateTime> getAllTradeDates(ITsStockInfo stockInfo) {
|
||||
return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo);
|
||||
}
|
||||
@Override
|
||||
public List<LocalDateTime> getAllMissingDates(ITsStockInfo stockInfo) {
|
||||
return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo);
|
||||
}
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), minTradeDate, function, 8000L, queryWay, stockSpan);
|
||||
log.info("{} 分钟 K 数据更新完成", stockSpan.getMin());
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("{} 分钟 K 数据未获取:{},发生致命错误,将不会重试:{}", stockSpan.getMin(), request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.warn("{} 分钟 K 数据未获取:{}", stockSpan.getMin(), request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查 {} 分钟 K 数据执行结果时发生错误\r\n{}", stockSpan.getMin(), e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取个股每日指标
|
||||
* <blockquote>
|
||||
* 接口:daily_basic<br>
|
||||
* 更新时间:交易日每日15点~17点之间<br>
|
||||
* 描述:获取全部股票每日重要的基本面指标,可用于选股分析、报表展示等。<br>
|
||||
* 积分:用户需要至少600积分才可以调取,具体请参阅
|
||||
* <a href="https://tushare.pro/document/1?doc_id=13">积分获取办法</a><br>
|
||||
* </blockquote>
|
||||
*/
|
||||
public void updateDailyBasic(QueryWay queryWay) {
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("daily_basic");
|
||||
baseRequest.addFields("ts_code,trade_date,close,turnover_rate,turnover_rate_f,volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,dv_ratio,dv_ttm,total_share,float_share,free_share,total_mv,circ_mv");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockDailyBasicDao.insertOrUpdateList(t);
|
||||
return true;
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyBasicDao, function, 5000L, queryWay, null);
|
||||
}
|
||||
else {
|
||||
executeResult = rollingQueryByDate(baseRequest, stockDailyBasicDao, function, queryWay, null);
|
||||
}
|
||||
log.info("每日指标数据更新完成");
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("每日指标数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.warn("每日指标数据未获取:{}", request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查每日指标数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b><a href="https://waditu.com/document/2?doc_id=28">Tushare 复权因子</a></b><br>
|
||||
* <blockquote>
|
||||
* 接口:adj_factor<br>
|
||||
* 更新时间:早上9点30分<br>
|
||||
* 描述:获取股票复权因子,可提取单只股票全部历史复权因子,也可以提取单日全部股票的复权因子。<br>
|
||||
* </blockquote>
|
||||
* 虽然文档没说,但当这个接口请求单支股的所有数据时,也是 5000 条限制<br>
|
||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法,否则可能造成股票复权因子缺失</font>
|
||||
*/
|
||||
public void updateStockAdjustTushare(QueryWay queryWay) {
|
||||
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("adj_factor");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockAdjustDao.insertOrUpdateListTushare(t);
|
||||
return true;
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockAdjustDao, function, 5000L, queryWay, null);
|
||||
}
|
||||
else {
|
||||
executeResult = rollingQueryByDate(baseRequest, stockAdjustDao, function, queryWay, null);
|
||||
}
|
||||
log.info("复权数据更新完成");
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("复权数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.info("复权数据未获取:{}", request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查复权数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b><a href="https://waditu.com/document/2?doc_id=298">Tushare 涨跌停列表</a></b><br>
|
||||
* <ul>
|
||||
* <li>接口:limit_list_d<br>
|
||||
* <li>描述:获取沪深A股每日涨跌停、炸板数据情况,数据从2020年开始<br>
|
||||
* <li>限量:单次最大可以获取500条数据,可通过日期或者股票循环提取<br>
|
||||
* <li>积分:120积分可查看数据,5000积分每分钟可以请求200次,8000积分以上每分钟500次,具体请参阅积分获取办法<br>
|
||||
* </ul>
|
||||
*/
|
||||
public void updateStockLimit(QueryWay queryWay) {
|
||||
|
||||
TushareRequestBody baseRequest = new TushareRequestBody("limit_list_d");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
stockLimitDao.insertOrUpdateListTushare(t);
|
||||
return true;
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
if (queryWay.compareTo(QueryWay.ByStock) >= 0) {
|
||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockLimitDao, function, 500L, queryWay, null);
|
||||
}
|
||||
else {
|
||||
executeResult = rollingQueryByDate(baseRequest, stockLimitDao, function, queryWay, LocalDateTime.of(2019, 11, 28, 0, 0, 0));
|
||||
}
|
||||
log.info("涨跌停数据更新完成");
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("涨跌停数据未获取,发生致命错误,将不会重试:{}", request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.info("涨跌停数据未获取:{}", request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查涨跌停数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* <b><a href="https://tushare.pro/document/2?doc_id=61">前十大股东</a>/<a href="https://tushare.pro/document/2?doc_id=62">前十大流通股东</a></b><br>
|
||||
* <blockquote>
|
||||
* 接口:top10_holders/top10_floatholders<br>
|
||||
* 更新时间:不定时,报告日<br>
|
||||
* 描述:获取上市公司前十大股东数据,包括持有数量和比例等信息/获取上市公司前十大流通股东数据,不包括比例信息<br>
|
||||
* </blockquote>
|
||||
* 文档说单次 100 条限制,但实际测试有单次 5000 条<br>
|
||||
* <font color="red">请先更新交易日历和股票列表后再调用该方法</font><br>
|
||||
* <font color="red">如果是 A+H 或者 A+B 股,可能存在同股票存在多个同名股东,且持股数据不一致。需要仔细研究如何清洗和使用</font>
|
||||
*/
|
||||
public void updateStockHolder(StockHolderType holderType) {
|
||||
|
||||
boolean isFloat = holderType.getIsFloat() == 1;
|
||||
TushareRequestBody baseRequest = new TushareRequestBody(isFloat ? "top10_floatholders" : "top10_holders");
|
||||
Function<List<JSONObject>, Boolean> function = (t) -> {
|
||||
Map<String, JSONObject> map = new HashMap<>();
|
||||
for (JSONObject jo : t) {
|
||||
String holderName = jo.getString("holder_name");
|
||||
String[] temp = {jo.getString("ts_code"), jo.getString("end_date"), holderName, holderType.name()};
|
||||
String key = String.join("_", temp);
|
||||
Integer holderOffset = 1;
|
||||
while (map.containsKey(key)) {
|
||||
log.warn("存在重复的 key {},开始重命名", key);
|
||||
temp[2] = holderName + '^' + holderOffset++;
|
||||
jo.put("holder_name", temp[2]);
|
||||
key = String.join("_", temp);
|
||||
}
|
||||
map.put(key, jo);
|
||||
}
|
||||
stockHolderDao.insertOrUpdateListTushare(t, holderType);
|
||||
return true;
|
||||
};
|
||||
List<Future<TushareCrawlerResult>> executeResult;
|
||||
executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockHolderDao, function, 5000L, QueryWay.ByStock, null);
|
||||
String banner = "十大" + (isFloat ? "流通股东" : "股东");
|
||||
log.info("{}数据更新完成", banner);
|
||||
for (Future<TushareCrawlerResult> f : executeResult) {
|
||||
try {
|
||||
TushareCrawlerResult result = f.get();
|
||||
JSONObject request = f.get().getRequest();
|
||||
if (result.isFatal()) {
|
||||
log.error("{}数据未获取,发生致命错误,将不会重试:{}", banner, request.toJSONString());
|
||||
}
|
||||
else if (!result.isSuccess()) {
|
||||
log.info("{}数据未获取:{}", banner, request.toJSONString());
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("检查" + banner + "数据执行结果时发生错误", e);
|
||||
}
|
||||
}
|
||||
if (baseRequest.hasFingerprint()) {
|
||||
redis.getBucket(baseRequest.getFingerprint()).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>更新交易日历</b>
|
||||
* <p>
|
||||
* 更新当前交易日历到本年度的最后一天
|
||||
* </p>
|
||||
* @param stockMarkets 市场类型,若未提供则默认更新上交所和深交所
|
||||
*/
|
||||
public boolean updateStockCalendar(StockMarket...stockMarkets) {
|
||||
LocalDate lastDayOfThisYear = LocalDateTimeUtils.getLastDayOfThisYear();
|
||||
if (stockMarkets == null || stockMarkets.length == 0) {
|
||||
stockMarkets = new StockMarket[] {StockMarket.SZ, StockMarket.SH};
|
||||
}
|
||||
for (StockMarket stockMarket : stockMarkets) {
|
||||
|
||||
log.debug("正在更新 {} 交易所的交易日历", stockMarket);
|
||||
|
||||
LocalDate latest = stockCalendarDao.getLatestLocalDate(stockMarket);
|
||||
if (latest != null && latest.equals(lastDayOfThisYear)){
|
||||
log.debug("{} 交易所的日历已更新到本年度最后一日,跳过", stockMarket);
|
||||
continue;
|
||||
}
|
||||
TushareRequestBody rq = new TushareRequestBody("trade_cal")
|
||||
.addParam("exchange", stockMarket.getExchangeCode())
|
||||
.addDateParam("end_date", lastDayOfThisYear)
|
||||
.addFields("exchange,cal_date,is_open,pretrade_date");
|
||||
if (latest != null) {
|
||||
rq.addDateParam("start_date", latest);
|
||||
}
|
||||
try {
|
||||
List<JSONObject> list = TushareClient.queryList(rq);
|
||||
stockCalendarDao.insertOrUpdateList(list);
|
||||
} catch (JSONException e) {
|
||||
log.error("JSONException", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
log.error("IOException", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 放置原始请求和爬虫是否成功的类
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Accessors(chain=true)
|
||||
public class TushareCrawlerResult {
|
||||
private TushareRequestBody request;
|
||||
@Builder.Default
|
||||
private boolean isFatal = false;
|
||||
@Builder.Default
|
||||
private boolean isSuccess = false;
|
||||
private String msg;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class TushareException extends RuntimeException {
|
||||
|
||||
public static final String NO_PERMISSION = "抱歉,您没有访问该接口的权限,权限的具体详情访问";
|
||||
|
||||
@Getter
|
||||
private boolean isFatal;
|
||||
|
||||
public TushareException(String message) {
|
||||
super(message);
|
||||
if (message != null && message.contains(NO_PERMISSION)) {
|
||||
isFatal = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Tushare 请求主体封装
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
public class TushareRequestBody extends JSONObject implements Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private List<String> fields = new ArrayList<>();
|
||||
private static final String PARAMS = "params";
|
||||
private static final String API_NAME = "api_name";
|
||||
private static final String FIELDS = "fields";
|
||||
private static final String TOKEN = "token";
|
||||
private static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
private static final DateTimeFormatter DEFAULT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private String fingerprint = null;
|
||||
|
||||
/**
|
||||
* 使用 apiName 实例化
|
||||
* @param apiName
|
||||
*/
|
||||
public TushareRequestBody(String apiName) {
|
||||
put(API_NAME, apiName);
|
||||
put(PARAMS, new JSONObject());
|
||||
put(FIELDS, null);
|
||||
put(TOKEN, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public TushareRequestBody clone() {
|
||||
TushareRequestBody copy = new TushareRequestBody(getString(API_NAME));
|
||||
copy.putAll(JSONObject.parseObject(this.toJSONString()));
|
||||
copy.fields = (List<String>)JSONObject.parseObject(JSONObject.toJSONString(fields), ArrayList.class);
|
||||
|
||||
if (hasFingerprint()) {
|
||||
copy.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加多个查询字段,字段间以逗号分隔
|
||||
* @param fields
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addFields(String fields) {
|
||||
Collections.addAll(this.fields, fields.split(","));
|
||||
this.fields = this.fields.stream().distinct().collect(Collectors.toList());
|
||||
put(FIELDS, this.fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加多个查询字段
|
||||
* @param fields
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addFields(String... fields) {
|
||||
Collections.addAll(this.fields, fields);
|
||||
this.fields = this.fields.stream().distinct().collect(Collectors.toList());
|
||||
put(FIELDS, this.fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个查询字段
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addField(String field) {
|
||||
fields.add(field);
|
||||
fields = fields.stream().distinct().collect(Collectors.toList());
|
||||
put(FIELDS, fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询参数
|
||||
* @param name
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addParam(String name, String value) {
|
||||
((JSONObject)get(PARAMS)).put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日期型查询参数,会截断到日期,格式:yyyyMMdd
|
||||
* @param name 参数名称
|
||||
* @param date 日期
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addDateParam(String name, TemporalAccessor date) {
|
||||
addParam(name, DEFAULT_DATE_FORMATTER.format(date));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日期时间查询参数,格式:yyyy-MM-dd HH:mm:ss
|
||||
* @param name 参数名称
|
||||
* @param dateTime 时间
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addDateTimeParam(String name, TemporalAccessor dateTime) {
|
||||
addParam(name, DEFAULT_TIME_FORMATTER.format(dateTime));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询参数
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody addParams(Map<String, String> params) {
|
||||
((JSONObject)get(PARAMS)).putAll(params);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TushareRequestBody setToken(String token) {
|
||||
put(TOKEN, token);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当前请求参数的 fingerprint
|
||||
* @return
|
||||
*/
|
||||
public TushareRequestBody generateFingerPrint() {
|
||||
if (!hasFingerprint()) {
|
||||
StringBuilder sb = new StringBuilder()
|
||||
.append(API_NAME)
|
||||
.append(':')
|
||||
.append(get(API_NAME))
|
||||
.append('|')
|
||||
.append(PARAMS)
|
||||
.append(':')
|
||||
.append(JSON.toJSONString(get(PARAMS)));
|
||||
fingerprint = sb.toString();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有 fingerprint,用于判断请求是否为同一类型
|
||||
* @return
|
||||
*/
|
||||
public Boolean hasFingerprint () {
|
||||
return StringUtils.isNotEmpty(fingerprint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package link.at17.mid.tushare.data.crawler.tushare;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.data.crawler.RetryAndDelay;
|
||||
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 用来处理请求、请求返回结果,并返回是否执行成功
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class TushareResponseCallable implements Callable<TushareCrawlerResult> {
|
||||
|
||||
private TushareRequestBody request;
|
||||
private RetryAndDelay retryAndDelay = new RetryAndDelay();
|
||||
private Function<List<JSONObject>, Boolean> function;
|
||||
private static final Pattern MIN_LIMIT_PATTERN = Pattern.compile("分钟.*?(\\d+)次");
|
||||
private static final Pattern HOUR_LIMIT_PATTERN = Pattern.compile("小时.*?(\\d+)次");
|
||||
|
||||
/**
|
||||
* 使用 TushareRequestBody 对象和对应 Function 初始化
|
||||
* @param request
|
||||
* @param consumer
|
||||
*/
|
||||
public TushareResponseCallable(TushareRequestBody request, Function<List<JSONObject>, Boolean> function) {
|
||||
this.request = request;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TushareCrawlerResult call() throws Exception {
|
||||
if (!request.hasFingerprint()) {
|
||||
return TushareCrawlerResult.builder()
|
||||
.request(request)
|
||||
.isSuccess(false)
|
||||
.isFatal(true)
|
||||
.msg("请求指纹缺失,请检查")
|
||||
.build();
|
||||
}
|
||||
RedissonClient client = SpringContextHolder.getBean(RedissonClient.class);
|
||||
RBucket<Object> bucket = client.getBucket(request.getFingerprint());
|
||||
int retryTime = retryAndDelay.getRetryTime();
|
||||
while (retryTime-- > 0) {
|
||||
if (bucket.isExists()) {
|
||||
return TushareCrawlerResult.builder()
|
||||
.request(request)
|
||||
.isSuccess(false)
|
||||
.isFatal(true)
|
||||
.msg("Request cancelled for existing fingerprint bucket.")
|
||||
.build();
|
||||
}
|
||||
try {
|
||||
List<JSONObject> list = TushareClient.queryList(request);
|
||||
if (Objects.isNull(list)) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(retryAndDelay.getDelay());
|
||||
return TushareCrawlerResult.builder()
|
||||
.request(request)
|
||||
.isSuccess(list.size() > 0 ? function.apply(list) : true)
|
||||
.build();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.error("Request failed: {}, error={}", request.toJSONString(), ex.getMessage());
|
||||
try {
|
||||
Thread.sleep(5000L);
|
||||
if (ex instanceof TushareException te) {
|
||||
if (te.isFatal()) {
|
||||
// 设置 Redis 缓存,设置 Fatal 指纹
|
||||
bucket.set(1);
|
||||
return TushareCrawlerResult.builder()
|
||||
.request(request)
|
||||
.isSuccess(false)
|
||||
.isFatal(true)
|
||||
.msg(ex.getMessage())
|
||||
.build();
|
||||
}
|
||||
Matcher m = MIN_LIMIT_PATTERN.matcher(ex.getMessage());
|
||||
if (m.find()) {
|
||||
Integer minLimit = Integer.parseInt(m.group(1));
|
||||
retryAndDelay.setDelayByMinLimit(minLimit);
|
||||
continue;
|
||||
}
|
||||
|
||||
m = HOUR_LIMIT_PATTERN.matcher(ex.getMessage());
|
||||
if (m.find()) {
|
||||
Integer hourLimit = Integer.parseInt(m.group(1));
|
||||
retryAndDelay.setDelayByHourLimit(hourLimit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return TushareCrawlerResult.builder().request(request).isSuccess(false).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 成本分布(筹码)
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class CostDistribution {
|
||||
|
||||
private Double[] x;
|
||||
private Double[] y;
|
||||
private LocalDateTime date;
|
||||
private Double benifitPart;
|
||||
private Double avgCost;
|
||||
private PercentChips precentChips90;
|
||||
private PercentChips precentChips70;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public static class PercentChips {
|
||||
private Double priceUpper;
|
||||
private Double priceLower;
|
||||
private Double concentration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import link.at17.mid.tushare.enums.BuyOrSell;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 交易点
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class ExchangePoint<T> implements Comparable<ExchangePoint<?>> {
|
||||
|
||||
private BuyOrSell buyOrSell;
|
||||
private LocalDateTime exchangeDate;
|
||||
private Boolean used = false;
|
||||
|
||||
@Override
|
||||
public int compareTo(ExchangePoint<?> o) {
|
||||
return exchangeDate.compareTo(o.getExchangeDate());
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/link/at17/mid/tushare/data/models/Hold.java
Normal file
29
src/main/java/link/at17/mid/tushare/data/models/Hold.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import link.at17.mid.tushare.system.util.LocalDateTimeUtils;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 个股持仓
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class Hold {
|
||||
|
||||
private int holds = 0;
|
||||
private int canSell = 0;
|
||||
private LocalDateTime date;
|
||||
private StockInfo stockInfo;
|
||||
private Double cost;
|
||||
|
||||
public int getCanSell(LocalDateTime currentDate) {
|
||||
if (!LocalDateTimeUtils.isSameDay(currentDate, date) && currentDate.compareTo(date) == 1) {
|
||||
return holds;
|
||||
}
|
||||
else return canSell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class IndexValue<I extends Comparable<I>, T> implements Comparable<IndexValue<I, T>> {
|
||||
private T value;
|
||||
private I index;
|
||||
|
||||
@Override
|
||||
public int compareTo(IndexValue<I, T> o) {
|
||||
return index.compareTo(o.index);
|
||||
}
|
||||
|
||||
public IndexValue(I index, T value) {
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import link.at17.mid.tushare.enums.PivotPointType;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 中心点、压力位和支撑位
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Getter
|
||||
public class PivotPoint {
|
||||
private Double resistance4;
|
||||
private Double resistance3;
|
||||
private Double resistance2;
|
||||
private Double resistance1;
|
||||
private Double pivotPoint;
|
||||
private Double support1;
|
||||
private Double support2;
|
||||
private Double support3;
|
||||
private Double support4;
|
||||
private PivotPointType pivotPointType;
|
||||
|
||||
public PivotPoint(StockValue stockValue, PivotPointType pivotPointType) {
|
||||
Double high = stockValue.getHigh(), // c
|
||||
low = stockValue.getLow(), // f
|
||||
open = stockValue.getOpen(), // e
|
||||
close = stockValue.getClose(); // h
|
||||
if (pivotPointType.equals(PivotPointType.Classic)) {
|
||||
Double k = (high + low + close) / 3, g = 2 * k - high, bres1 = 2 * k - low;
|
||||
resistance3 = high + 2 * (k - low);
|
||||
resistance2 = k + (bres1 - g);
|
||||
resistance1 = bres1;
|
||||
pivotPoint = k;
|
||||
support1 = g;
|
||||
support2 = k - (bres1 - g);
|
||||
support3 = low - 2 * (high - k);
|
||||
}
|
||||
else if (pivotPointType.equals(PivotPointType.Woodies)) {
|
||||
Double d = (high + low + 2 * close) / 4 ;
|
||||
resistance1 = 2 * d - low;
|
||||
resistance2 = d + high - low;
|
||||
pivotPoint = d;
|
||||
support1 = 2 * d - high;
|
||||
support2 = d - high + low;
|
||||
}
|
||||
else if (pivotPointType.equals(PivotPointType.Camarilla)) {
|
||||
resistance1 = close + ((high - low) * (1.1 / 12));
|
||||
resistance2 = close + ((high - low) * (1.1 / 6));
|
||||
resistance3 = close + ((high - low) * (1.1 / 4));
|
||||
resistance4 = close + ((high - low) * (1.1 / 2));
|
||||
support1 = close - ((high - low) * (1.1 / 12));
|
||||
support2 = close - ((high - low) * (1.1 / 6));
|
||||
support3 = close - ((high - low) * (1.1 / 4));
|
||||
support4 = close - ((high - low) * (1.1 / 2));
|
||||
}
|
||||
else {
|
||||
Double b;
|
||||
if (close < open) {
|
||||
b = high + low * 2 + close;
|
||||
}
|
||||
else if (close > open) {
|
||||
b = high * 2 + low + close;
|
||||
}
|
||||
else {
|
||||
b = high + low + close * 2;
|
||||
}
|
||||
resistance1 = b / 2 - low;
|
||||
support1 = b / 2 - high;
|
||||
pivotPoint = b / 4;
|
||||
}
|
||||
this.pivotPointType = pivotPointType;
|
||||
}
|
||||
}
|
||||
13
src/main/java/link/at17/mid/tushare/data/models/Sector.java
Normal file
13
src/main/java/link/at17/mid/tushare/data/models/Sector.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Sector {
|
||||
String symbol;
|
||||
String name;
|
||||
Double lastPx;
|
||||
Double pxChangeRate;
|
||||
String market;
|
||||
String typeCode;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
|
||||
import link.at17.mid.tushare.enums.StockMarket;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@ToString
|
||||
@TableName("stock_calendar")
|
||||
public class StockCalendar extends Model<StockCalendar> {
|
||||
LocalDate date;
|
||||
StockMarket exchange;
|
||||
LocalDate pretradeDate;
|
||||
Boolean isOpen;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@TableName("stock_holder")
|
||||
public class StockHolder extends Model<StockHolder> {
|
||||
String tsCode;
|
||||
LocalDateTime annDate;
|
||||
LocalDateTime endDate;
|
||||
String holderName;
|
||||
Double holdAmount;
|
||||
Double holdRatio;
|
||||
}
|
||||
102
src/main/java/link/at17/mid/tushare/data/models/StockInfo.java
Normal file
102
src/main/java/link/at17/mid/tushare/data/models/StockInfo.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||
import link.at17.mid.tushare.enums.ListStatus;
|
||||
import link.at17.mid.tushare.enums.StockMarket;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
public class StockInfo implements ITsStockInfo {
|
||||
|
||||
private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(0|1|90)\\.(.*?)$");
|
||||
private static final Pattern STANDARD_CODE_PATTERN = Pattern.compile("^(\\d{6})\\.(SZ|SH|BJ)$");
|
||||
|
||||
private String name;
|
||||
@TableField(exist=false)
|
||||
private String nudeCode;
|
||||
private ListStatus listStatus;
|
||||
private String tsCode;
|
||||
private LocalDateTime listDate;
|
||||
private LocalDateTime delistDate;
|
||||
@TableField(exist=false)
|
||||
private StockMarket exchange;
|
||||
@TableField(exist=false)
|
||||
private String thsBelongings;
|
||||
|
||||
public String getEMCode() {
|
||||
return exchange.getEMCode() + "." + nudeCode;
|
||||
}
|
||||
|
||||
public StockInfo setTsCode(String tsCode) {
|
||||
this.tsCode = tsCode;
|
||||
setStockCode(tsCode);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getStockCode() {
|
||||
return nudeCode + "." + exchange.getStandardCode();
|
||||
}
|
||||
|
||||
public StockInfo setStockCode(String stockCode) {
|
||||
if (StringUtils.isEmpty(stockCode)) {
|
||||
return this;
|
||||
}
|
||||
Matcher m = EM_CODE_PATTERN.matcher(stockCode);
|
||||
if (m.matches()) {
|
||||
String sec = m.group(1);
|
||||
nudeCode = m.group(2);
|
||||
|
||||
if ("1".equals(sec)) {
|
||||
exchange = StockMarket.SH;
|
||||
}
|
||||
else if ("0".equals(sec)) {
|
||||
exchange = StockMarket.SZ;
|
||||
}
|
||||
else if ("BJ".equals(sec)) {
|
||||
exchange = StockMarket.BJ;
|
||||
}
|
||||
else {
|
||||
exchange = StockMarket.BK;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
m = STANDARD_CODE_PATTERN.matcher(stockCode);
|
||||
if (m.matches()) {
|
||||
String sec = m.group(2);
|
||||
nudeCode = m.group(1);
|
||||
|
||||
switch (sec) {
|
||||
case "SH":
|
||||
exchange = StockMarket.SH;
|
||||
break;
|
||||
case "SZ":
|
||||
exchange = StockMarket.SZ;
|
||||
break;
|
||||
case "BJ":
|
||||
default:
|
||||
exchange = StockMarket.BJ;
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否主板
|
||||
* @return
|
||||
*/
|
||||
public boolean isMainBoard() {
|
||||
return exchange != null && (exchange == StockMarket.SH || exchange == StockMarket.SZ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
|
||||
import link.at17.mid.tushare.enums.LimitType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@TableName("stock_limit")
|
||||
public class StockLimit extends Model<StockLimit> {
|
||||
String tsCode;
|
||||
LocalDateTime tradeDate;
|
||||
String industry;
|
||||
String name;
|
||||
Double close;
|
||||
Double pctChg;
|
||||
Double amount; // 成交额
|
||||
Double limitAmount; // 板上成交金额,跌停无此数据
|
||||
Double floatMv;
|
||||
Double totalMv;
|
||||
Double turnoverRatio;
|
||||
Double fdAmount;
|
||||
LocalDateTime firstTime;
|
||||
LocalDateTime lastTime;
|
||||
Integer openTimes;
|
||||
String upStat;
|
||||
Integer limitTimes;
|
||||
@TableField("\"limit\"")
|
||||
LimitType limit;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import link.at17.mid.tushare.enums.StockSpan;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 开/收/高/低/成交量/日期/股票间隔类型
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StockValue {
|
||||
protected StockInfo stockInfo;
|
||||
protected Double open = 0d;
|
||||
protected Double close = 0d;
|
||||
protected Double high = 0d;
|
||||
protected Double low = 0d;
|
||||
protected Double vol = 0d;
|
||||
protected LocalDateTime date;
|
||||
protected StockSpan stockSpan = StockSpan.Daily;
|
||||
protected Double pctChg = null;
|
||||
protected Double pctChange = null;
|
||||
protected Double change = 0d;
|
||||
protected Double amount = 0d;
|
||||
|
||||
public Double getPctChg() {
|
||||
if (pctChg == null && pctChange != null) {
|
||||
return pctChange;
|
||||
}
|
||||
return pctChg;
|
||||
}
|
||||
|
||||
public Double getPctChange() {
|
||||
if (pctChange == null && pctChg != null) {
|
||||
return pctChg;
|
||||
}
|
||||
return pctChange;
|
||||
}
|
||||
|
||||
public String getDateStr(String format) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
|
||||
return formatter.format(getDate());
|
||||
}
|
||||
|
||||
public String getDateStr() {
|
||||
return getDateStr("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
protected void setFreq(int freq) {
|
||||
switch (freq) {
|
||||
case 1:
|
||||
stockSpan = StockSpan.Minute;
|
||||
case 5:
|
||||
stockSpan = StockSpan.Minute5;
|
||||
break;
|
||||
case 15:
|
||||
stockSpan = StockSpan.Minute15;
|
||||
case 30:
|
||||
stockSpan = StockSpan.Minute30;
|
||||
case 60:
|
||||
stockSpan = StockSpan.Minute60;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* StockValue 的扩展,增加换手率、流通股换手率、量比等数据
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StockValueEx extends StockValue {
|
||||
|
||||
private Double turnoverRate;
|
||||
private Double turnoverRateF;
|
||||
private Double volumeRatio;
|
||||
private Double pe;
|
||||
private Double peTtm;
|
||||
private Double pb;
|
||||
private Double ps;
|
||||
private Double psTtm;
|
||||
private Double dvRatio;
|
||||
private Double dvTtm;
|
||||
private Double totalShare;
|
||||
private Double floatShare;
|
||||
private Double freeShare;
|
||||
private Double totalMv;
|
||||
private Double circMv;
|
||||
|
||||
/**
|
||||
* 获取换手率
|
||||
* @return
|
||||
*/
|
||||
public Double getTurnoverRate() {
|
||||
if (turnoverRate == null) {
|
||||
if (totalShare == null || vol == null) return null;
|
||||
turnoverRate = vol * 100 / totalShare;
|
||||
}
|
||||
return turnoverRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取换手率(流通盘)
|
||||
* @return
|
||||
*/
|
||||
public Double getTurnoverRateF() {
|
||||
if (turnoverRateF == null) {
|
||||
if (floatShare == null || vol == null) return null;
|
||||
turnoverRateF = vol * 100 / floatShare;
|
||||
}
|
||||
return turnoverRateF;
|
||||
}
|
||||
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Getter(AccessLevel.NONE)
|
||||
@JsonAnyGetter
|
||||
private Map<String, Object> ext = new HashMap<>();
|
||||
|
||||
public Object get(String key) {
|
||||
return ext.get(key);
|
||||
}
|
||||
|
||||
public StockValueEx put(String key, Object object) {
|
||||
ext.put(key, object);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package link.at17.mid.tushare.data.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo;
|
||||
import link.at17.mid.tushare.enums.ThsStockMarket;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
@Accessors(chain=true)
|
||||
@TableName("stock_ths_list")
|
||||
public class ThsStockInfo implements ITsStockInfo {
|
||||
@JSONField(name="ts_code")
|
||||
private String tsCode;
|
||||
private String name;
|
||||
private Integer count;
|
||||
@JSONField(name="list_date")
|
||||
private LocalDateTime listDate;
|
||||
@JSONField(name="exchange")
|
||||
private ThsStockMarket exchange;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package link.at17.mid.tushare.data.models.interfaces;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 实现基本股票/板块指数查询信息的接口
|
||||
* @see link.at17.mid.tushare.data.models.StockInfo
|
||||
* @see link.at17.mid.tushare.data.models.ThsStockInfo
|
||||
*/
|
||||
public interface ITsStockInfo {
|
||||
/**
|
||||
* 获取 Tushare 代码
|
||||
* @return
|
||||
*/
|
||||
String getTsCode();
|
||||
/**
|
||||
* 获取名称
|
||||
* @return
|
||||
*/
|
||||
String getName();
|
||||
/**
|
||||
* 获取上市(成立)日期
|
||||
* @return
|
||||
*/
|
||||
LocalDateTime getListDate();
|
||||
/**
|
||||
* 获取退市(终止)日期
|
||||
* @return
|
||||
*/
|
||||
default LocalDateTime getDelistDate() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package link.at17.mid.tushare.data.models.interfaces;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 根据 {@code ITsStockInfo} 获取与交易日相关数据的接口,主要用于数据更新轮询时查询给定股票/板块指数在本地数据表记录的交易日情况
|
||||
* @see ITsStockInfo
|
||||
*
|
||||
*/
|
||||
public interface ITsTradeDate {
|
||||
|
||||
/**
|
||||
* 获取指定股票/板块指数在本地存在的最新一交易日
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
LocalDateTime getLatestTradeDate(@Param("stockInfo") ITsStockInfo stockInfo);
|
||||
/**
|
||||
* 获取指定股票/板块指数在本地存在的所有交易日
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
List<LocalDateTime> getAllTradeDates(@Param("stockInfo") ITsStockInfo stockInfo);
|
||||
/**
|
||||
* 交叉对比 stock_calendar 表,获取个股数据缺失的日期<p>
|
||||
* 需要确保 stock_calendar 表所有数据完整无缺失
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
List<LocalDateTime> getAllMissingDates(@Param("stockInfo") ITsStockInfo stockInfo);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package link.at17.mid.tushare.data.typehandler;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.TypeException;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
public class PostgreSQLJsonbTypeHandler extends BaseTypeHandler<JSONObject> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, JSONObject parameter, JdbcType jdbcType) throws SQLException {
|
||||
// 将 JSONObject 转为 String 并存入 jsonb 类型字段
|
||||
ps.setObject(i, parameter.toJSONString(), Types.OTHER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i, JSONObject parameter, JdbcType jdbcType) throws SQLException {
|
||||
if (parameter == null) {
|
||||
if (jdbcType == null) {
|
||||
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
|
||||
}
|
||||
try {
|
||||
ps.setObject(i, "{}", jdbcType.TYPE_CODE);
|
||||
} catch (SQLException e) {
|
||||
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
|
||||
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
|
||||
+ "Cause: " + e, e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
setNonNullParameter(ps, i, parameter, jdbcType);
|
||||
} catch (Exception e) {
|
||||
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
|
||||
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
|
||||
+ "Cause: " + e, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
// 从 jsonb 字段读取并解析为 JSONObject
|
||||
String json = rs.getString(columnName);
|
||||
return json == null ? new JSONObject() : JSONObject.parseObject(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String json = rs.getString(columnIndex);
|
||||
return json == null ? new JSONObject() : JSONObject.parseObject(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String json = cs.getString(columnIndex);
|
||||
return json == null ? new JSONObject() : JSONObject.parseObject(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package link.at17.mid.tushare.data.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
public class CryptoUtil {
|
||||
|
||||
public static String getSHA256Str(String str) {
|
||||
MessageDigest messageDigest;
|
||||
String encdeStr = "";
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
|
||||
encdeStr = Hex.encodeHexString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {}
|
||||
return encdeStr;
|
||||
}
|
||||
}
|
||||
595
src/main/java/link/at17/mid/tushare/data/util/Indicators.java
Normal file
595
src/main/java/link/at17/mid/tushare/data/util/Indicators.java
Normal file
@@ -0,0 +1,595 @@
|
||||
package link.at17.mid.tushare.data.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import link.at17.mid.tushare.data.models.CostDistribution;
|
||||
import link.at17.mid.tushare.data.models.PivotPoint;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
import link.at17.mid.tushare.data.models.StockValueEx;
|
||||
import link.at17.mid.tushare.data.models.CostDistribution.PercentChips;
|
||||
import link.at17.mid.tushare.enums.PivotPointType;
|
||||
|
||||
/**
|
||||
* 指标库
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
public final class Indicators {
|
||||
|
||||
/**
|
||||
* 计算移动平均线
|
||||
* @param values
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getMA(List<T> values, int count) {
|
||||
return getMA(values, v -> v.getClose(), count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算移动平均线
|
||||
* @param values
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static <T> List<Double> getMA(List<T> values, Function<T, Double> valueFunc, int count) {
|
||||
if (Objects.isNull(values) || values.size() < count) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> result = new ArrayList<>(size);
|
||||
double[] tmp = new double[size - count + 1];
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (i > count - 1) {
|
||||
// sum 直接等于之前的 sum - 头天 + 当天
|
||||
double sum =
|
||||
tmp[i - count]
|
||||
- valueFunc.apply(values.get(i - count))
|
||||
+ valueFunc.apply(values.get(i));
|
||||
tmp[i - count + 1] = sum;
|
||||
result.add(sum / count);
|
||||
continue;
|
||||
}
|
||||
if (i < count - 1) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
if (i == count - 1) {
|
||||
double sum = 0;
|
||||
for (int j = 0; j < count; j++) {
|
||||
sum += valueFunc.apply(values.get(i - j));
|
||||
}
|
||||
tmp[i - count + 1] = sum;
|
||||
result.add(sum / count);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 CCI 专用 TP 移动平均线
|
||||
* @param values
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getTPMA(List<T> values, int count) {
|
||||
return getMA(values, v -> (v.getHigh() + v.getLow() + v.getClose()) / 3, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 StockValue 列表获取 CCI
|
||||
* @param values
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getCCI(List<T> values, int count) {
|
||||
if (Objects.isNull(values) || values.size() < count) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> cci = new ArrayList<>(size);
|
||||
List<Double> sma = getTPMA(values, count);
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (i < count - 1) {
|
||||
cci.add(null);
|
||||
continue;
|
||||
}
|
||||
StockValue sv = values.get(i);
|
||||
double tp = getTP(sv), md = 0;
|
||||
for (int j = i - count + 1; j <= i; j++) {
|
||||
md += Math.abs(getTP(values.get(j)) - sma.get(i));
|
||||
}
|
||||
md /= count;
|
||||
cci.add((tp - sma.get(i)) / md / 0.015);
|
||||
}
|
||||
return cci;
|
||||
}
|
||||
|
||||
/** 计算 StockValue 的 高低收 平均值 **/
|
||||
private static double getTP(StockValue value) {
|
||||
return (value.getHigh() + value.getLow() + value.getClose()) / 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 StockValue 列表获取 CYW
|
||||
* @param values
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getCYW(List<T> values, int count) {
|
||||
if (Objects.isNull(values) || values.size() < count) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> cyw = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (i < count - 1) {
|
||||
cyw.add(null);
|
||||
continue;
|
||||
}
|
||||
StockValue sv = values.get(i);
|
||||
double sum = 0;
|
||||
for (int j = i - count + 1; j <= i; j++) {
|
||||
sum += sv.getHigh() > sv.getLow() ? (
|
||||
((sv.getClose() - sv.getLow()) / (sv.getHigh() - sv.getLow()) +
|
||||
(sv.getClose() - sv.getHigh()) / (sv.getHigh() - sv.getLow()))
|
||||
* sv.getVol() / 10000
|
||||
) : 0;
|
||||
}
|
||||
cyw.add(sum);
|
||||
}
|
||||
return cyw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收盘价计算 EXPMA
|
||||
* @param values
|
||||
* @param period
|
||||
* @param multiplier
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getEXPMA(List<T> values, int period) {
|
||||
return getEXPMA(values, v -> v.getClose(), period);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 EXPMA(EMA),提供非主流算法,只需提供好对应获取计算值的方法即可
|
||||
* @param <T>
|
||||
* @param values 值列表
|
||||
* @param valueFunc 获取值的 Function
|
||||
* @param period 周期
|
||||
* @return
|
||||
* 例:获取常规版 EXPMA
|
||||
* <br><code>getEXPMA(values, v -> v.getClose(), period) where values is List<StockValue>
|
||||
*/
|
||||
public static <T> List<Double> getEXPMA(List<T> values, Function<T, Double> valueFunc, int period) {
|
||||
if (Objects.isNull(values)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> ema = new ArrayList<>(size);
|
||||
Double k = 2 / (period + 1.0);
|
||||
ema.add(valueFunc.apply(values.get(0)));
|
||||
for (int i = 1; i < size; i++) {
|
||||
ema.add(valueFunc.apply(values.get(i)) * k + ema.get(i - 1) * (1 - k));
|
||||
}
|
||||
return ema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收盘价计算 SMA
|
||||
* @param values
|
||||
* @param period
|
||||
* @param multiplier
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getSMA(List<T> values, int period, int m) {
|
||||
return getSMA(values, v -> v.getClose(), period, m);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单移动平均线 SMA
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @param valueFunc
|
||||
* @param period 周期
|
||||
* @param m 平滑系数
|
||||
* @return
|
||||
*/
|
||||
public static <T> List<Double> getSMA(List<T> values, Function<T, Double> valueFunc, int period, int m) {
|
||||
Assert.isTrue(period > m, "周期必须大于平滑系数!");
|
||||
if (Objects.isNull(values)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> sma = new ArrayList<>(size);
|
||||
Double k = m * 1.0 / period;
|
||||
sma.add(valueFunc.apply(values.get(0)));
|
||||
for (int i = 1; i < size; i++) {
|
||||
sma.add(valueFunc.apply(values.get(i)) * k + sma.get(i - 1) * (1 - k));
|
||||
}
|
||||
return sma;
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对 StockValue 及其子类的动态异动平均线 DMA
|
||||
* @param <T>
|
||||
* @param values StockValue 或其子类的列表
|
||||
* @param k 权重系数
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getDMA(List<T> values, double k) {
|
||||
return getDMA(values, v -> v.getClose(), k);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态移动平均线 DMA
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @param valueFunc
|
||||
* @param k 权重系数
|
||||
* @return
|
||||
*/
|
||||
public static <T> List<Double> getDMA(List<T> values, Function<T, Double> valueFunc, double k) {
|
||||
Assert.isTrue(k >= 0 && k <= 1, "权重系数须介于 [0, 1] 之间!");
|
||||
if (Objects.isNull(values) || values.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> ema = new ArrayList<>(size);
|
||||
ema.add(valueFunc.apply(values.get(0)));
|
||||
for (int i = 1; i < size; i++) {
|
||||
ema.add(valueFunc.apply(values.get(i)) * k + ema.get(i - 1) * (1 - k));
|
||||
}
|
||||
return ema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通达信 MCST 指标拟合版
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValueEx> List<Double> getMCST(List<T> values) {
|
||||
if (Objects.isNull(values) || values.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> mcst = new ArrayList<>(size);
|
||||
T first = values.get(0);
|
||||
mcst.add((first.getHigh() + first.getLow() + first.getClose())/3);
|
||||
for (int i = 1; i < size; i++) {
|
||||
T curr = values.get(i);
|
||||
double k = curr.getVol() / curr.getFloatShare() / 100;
|
||||
// 成交额单位是千元,成交量单位是手,1000/100 = 10,所以 * 10
|
||||
mcst.add(curr.getAmount() * 10 / curr.getVol() * k + mcst.get(i - 1) * (1 - k));
|
||||
}
|
||||
return mcst;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成交量加权平均线 (VWMA),默认采用 func = CLOSE * VOL 进行计算
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @param period
|
||||
* @return
|
||||
* @see Indicators#getVWMA(List, Function, int)
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getVWMA(List<T> values, int period) {
|
||||
return getVWMA(values, v -> v.getVol() * v.getClose(), period);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成交量加权平均线 (VWMA),可指定 func 进行计算
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @param func
|
||||
* @param period
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getVWMA(List<T> values, Function<T, Double> func, int period) {
|
||||
if (Objects.isNull(values)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> vwma = new ArrayList<>(size);
|
||||
List<Double> closeMulVol = values.stream().map(func).toList();
|
||||
List<Double> vol = values.stream().map(v -> v.getVol()).toList();
|
||||
for (int i = 0; i < size; i++) {
|
||||
vwma.add(closeMulVol.subList(Math.max(0, i - period + 1), i + 1).stream().mapToDouble(v -> v).sum()/
|
||||
vol.subList(Math.max(0, i - period + 1), i + 1).stream().mapToDouble(v -> v).sum());
|
||||
}
|
||||
return vwma;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成交量价格确认指标 VPCI
|
||||
* @param <T>
|
||||
* @param values
|
||||
* @param m
|
||||
* @param n
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getVPCI(List<T> values, int n, int m) {
|
||||
Assert.isTrue(m < n, "m 值必须小于 n 值!");
|
||||
if (Objects.isNull(values) || values.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
/**
|
||||
VWMA:=SUM(C*V,N)/SUM(V,N);
|
||||
VPC:=VWMA - MA(C,N);
|
||||
VPR:=(SUM(C*V,M)/SUM(V,M))/MA(C,M);
|
||||
VM:=MA(V,M)/MA(V,N);
|
||||
VPCI:VPC*VPR*VM,COLORCYAN;
|
||||
*/
|
||||
List<Double> vwma = getVWMA(values, n);
|
||||
List<Double> vpr = getVWMA(values, m);
|
||||
List<Double> vpcma = getMA(values, n);
|
||||
List<Double> vprma = getMA(values, m);
|
||||
List<Double> vmFast = getMA(values, m);
|
||||
List<Double> vmSlow = getMA(values, n);
|
||||
List<Double> vpci = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
Double vwmai = vwma.get(i), vpcmai = vpcma.get(i),
|
||||
vprmai = vprma.get(i), vmFastI = vmFast.get(i),
|
||||
vmSlowI = vmSlow.get(i);
|
||||
Double vpcI = null, vprI = null, vmI = null;
|
||||
if (vwmai != null && vpcmai != null) {
|
||||
vpcI = vwmai - vpcmai;
|
||||
if (vprmai != null) {
|
||||
vprI = vpr.get(i) / vprmai;
|
||||
if (vmFastI != null && vmSlowI != null) {
|
||||
vmI = vmFastI / vmSlowI;
|
||||
if (vmI != null) {
|
||||
vpci.add(vpcI * vprI * vmI * 100);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vpci.add(null);
|
||||
}
|
||||
return vpci;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TRIX 指标
|
||||
* @param values
|
||||
* @param n
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getTRIX(List<T> values, int n) {
|
||||
List<Double> em3 = getEXPMA(getEXPMA(getEXPMA(values, v -> v.getClose(), n), v -> v, n), v -> v, n);
|
||||
if (Objects.isNull(em3)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> trix = new ArrayList<>(size);
|
||||
trix.add(0d);
|
||||
for (int i = 1; i < em3.size(); i++) {
|
||||
trix.add((em3.get(i) - em3.get(i - 1)) / em3.get(i - 1) * 100);
|
||||
}
|
||||
return trix;
|
||||
}
|
||||
|
||||
/**
|
||||
* TRIX 指标
|
||||
* @param values
|
||||
* @param func
|
||||
* @param m
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getTRIX(List<T> values, Function<T, Double> func, int n) {
|
||||
List<Double> em3 = getEXPMA(getEXPMA(getEXPMA(values, func, n), v -> v, n), v -> v, n);
|
||||
if (Objects.isNull(em3)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> trix = new ArrayList<>(size);
|
||||
trix.add(0d);
|
||||
for (int i = 1; i < em3.size(); i++) {
|
||||
trix.add((em3.get(i) - em3.get(i - 1)) / em3.get(i - 1) * 100);
|
||||
}
|
||||
return trix;
|
||||
}
|
||||
|
||||
/**
|
||||
* LON 指标,默认快线 10,慢线 20
|
||||
* @param values
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getLON(List<T> values) {
|
||||
return getLON(values, 10, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* LON 指标
|
||||
* @param values
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getLON(List<T> values, int fast, int slow) {
|
||||
Assert.isTrue(fast < slow, "fast 周期必须小于 slow!");
|
||||
if (Objects.isNull(values)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> lon = new ArrayList<>(size);
|
||||
lon.add(0d);
|
||||
List<Double> rcList = new ArrayList<>();
|
||||
List<Double> longSum = new ArrayList<>();
|
||||
longSum.add(0d);
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
StockValue prev = values.get(i - 1), now = values.get(i);
|
||||
|
||||
Double vid = (prev.getVol() + now.getVol()) / ((Math.max(prev.getHigh(), now.getHigh()) - Math.min(prev.getLow(), now.getLow())) * 100);
|
||||
rcList.add((now.getClose() - prev.getClose()) * vid);
|
||||
longSum.add(rcList.stream().reduce(Double::sum).get());
|
||||
Double dif = getSMA(longSum, v -> v, fast, 1).get(longSum.size() - 1);
|
||||
Double dea = getSMA(longSum, v -> v, slow, 1).get(longSum.size() - 1);
|
||||
lon.add(dif - dea);
|
||||
}
|
||||
return lon;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 非主流 LON 指标
|
||||
* @param values
|
||||
* @return
|
||||
*/
|
||||
public static <T extends StockValue> List<Double> getFZLLON(List<T> values, int fast, int slow) {
|
||||
Assert.isTrue(fast < slow, "fast 周期必须小于 slow!");
|
||||
if (Objects.isNull(values)) {
|
||||
return null;
|
||||
}
|
||||
int size = values.size();
|
||||
List<Double> lon = new ArrayList<>(size);
|
||||
lon.add(0d);
|
||||
List<Double> rcList = new ArrayList<>();
|
||||
List<Double> longSum = new ArrayList<>();
|
||||
longSum.add(0d);
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
StockValue prev = values.get(i - 1), now = values.get(i);
|
||||
PivotPoint prevPivot = new PivotPoint(prev, PivotPointType.DeMarks);
|
||||
PivotPoint nowPivot = new PivotPoint(now, PivotPointType.DeMarks);
|
||||
Double vid = (prev.getVol() + now.getVol()) / ((Math.max(prevPivot.getResistance1(), now.getHigh()) - Math.min(prevPivot.getSupport1(), now.getLow())) * 100);
|
||||
rcList.add((nowPivot.getPivotPoint() - prevPivot.getPivotPoint()) * vid);
|
||||
longSum.add(rcList.stream().reduce(Double::sum).get());
|
||||
Double dif = getSMA(longSum, v -> v, fast, 1).get(longSum.size() - 1);
|
||||
Double dea = getSMA(longSum, v -> v, slow, 1).get(longSum.size() - 1);
|
||||
lon.add(dif - dea);
|
||||
}
|
||||
return lon;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据给定的股价列表,计算指定 index 处的成本分布
|
||||
* @param values 股价列表
|
||||
* @param index 指定的 index
|
||||
* @param factor 系数,指成本分布的最大数量,一般取 150
|
||||
* @param range 取多少个历史交易日进行计算(包括当日),一般取 120
|
||||
* @return
|
||||
*/
|
||||
public static CostDistribution getCostDistribution(List<StockValueEx> values, int index, int factor, int range) {
|
||||
CostDistribution cost = new CostDistribution();
|
||||
StockValueEx current = values.get(index);
|
||||
List<StockValueEx> subValues = values.subList(Math.max(0, index - range + 1), Math.max(1, index + 1));
|
||||
Double historyMax = subValues.stream().mapToDouble(StockValue::getHigh).max().getAsDouble();
|
||||
Double historyMin = subValues.stream().mapToDouble(StockValue::getLow).min().getAsDouble();
|
||||
double normFactor = Math.max(0.01, (historyMax - historyMin) / (factor - 1));
|
||||
double[] distributionY = new double[factor];
|
||||
for (int i = 0; i < factor; i++) {
|
||||
distributionY[i] = historyMin + normFactor * i;
|
||||
}
|
||||
double[] distributionX = new double[factor];
|
||||
for (int i = 0; i < subValues.size(); i++) {
|
||||
StockValueEx stockValueEx = subValues.get(i);
|
||||
double open = stockValueEx.getOpen();
|
||||
double close = stockValueEx.getClose();
|
||||
double high = stockValueEx.getHigh();
|
||||
double low = stockValueEx.getLow();
|
||||
double ohlcAvg = (open + close + high + low) / 4;
|
||||
double turnoverRate = Math.min(1, stockValueEx.getTurnoverRate() / 100);
|
||||
int currDistUpper = (int) Math.floor((high - historyMin) / normFactor);
|
||||
int currDistLower = (int) Math.ceil((low - historyMin) / normFactor);
|
||||
double v0 = high == low ? (factor - 1) : (2 / (high - low));
|
||||
int currDistIndex = (int) Math.floor((ohlcAvg - historyMin) / normFactor);
|
||||
for (int x = 0; x < factor; x++) {
|
||||
distributionX[x] *= 1 - turnoverRate;
|
||||
}
|
||||
if (high == low) {
|
||||
// 一字板
|
||||
distributionX[currDistIndex] += v0 * turnoverRate / 2;
|
||||
}
|
||||
else {
|
||||
for (int I = currDistLower; I <= currDistUpper; I++) {
|
||||
double b = historyMin + normFactor * I;
|
||||
if (b <= ohlcAvg) {
|
||||
if (Math.abs(ohlcAvg - low) < 1e-8) {
|
||||
distributionX[I] += v0 * turnoverRate;
|
||||
}
|
||||
else {
|
||||
distributionX[I] += (b - low) / (ohlcAvg - low) * v0 * turnoverRate;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Math.abs(high - ohlcAvg) < 1e-8) {
|
||||
distributionX[I] += v0 * turnoverRate;
|
||||
}
|
||||
else {
|
||||
distributionX[I] += (high - b) / (high - ohlcAvg) * v0 * turnoverRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double w = current.getClose();
|
||||
double distributionXSum = 0;
|
||||
for (int i = 0; i < factor; i++) {
|
||||
distributionXSum += distributionX[i];
|
||||
}
|
||||
Function<Double, Double> R = (t) -> {
|
||||
Double e = 0d, i = 0d;
|
||||
for (int n = 0; n < factor; n++) {
|
||||
if (t < i + distributionX[n]) {
|
||||
e = historyMin + n * normFactor;
|
||||
break;
|
||||
}
|
||||
i += distributionX[n];
|
||||
}
|
||||
return e;
|
||||
};
|
||||
final double finalSum = distributionXSum;
|
||||
Function<Double, PercentChips> computePercentChips = (t1) -> {
|
||||
double e0 = (1 - t1) / 2,
|
||||
e1 = (1 + t1) / 2,
|
||||
upper = R.apply(finalSum * e1),
|
||||
lower = R.apply(finalSum * e0);
|
||||
return new PercentChips()
|
||||
.setPriceLower(lower)
|
||||
.setPriceUpper(upper)
|
||||
.setConcentration(Math.abs(lower + upper) < 1e-8 ? 0 : ((upper - lower)/(upper + lower)));
|
||||
};
|
||||
Function<Double, Double> getBenifitPart = (t) -> {
|
||||
double e = 0;
|
||||
for (int i = 0; i < factor; i++) {
|
||||
if (historyMin + i * normFactor <= t) {
|
||||
e += distributionX[i];
|
||||
}
|
||||
}
|
||||
return 0 == finalSum ? 0 : (e / finalSum);
|
||||
};
|
||||
cost.setX(ArrayUtils.toObject(distributionX)).setY(ArrayUtils.toObject(distributionY))
|
||||
.setDate(current.getDate())
|
||||
.setBenifitPart(getBenifitPart.apply(w))
|
||||
.setAvgCost(R.apply(.5 * distributionXSum))
|
||||
.setPrecentChips90(computePercentChips.apply(.9))
|
||||
.setPrecentChips70(computePercentChips.apply(.7));
|
||||
return cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据给定的股价列表,计算指定范围的成本分布
|
||||
* @param values 股价列表
|
||||
* @param from 起始索引,包括
|
||||
* @param to 结束索引,不包括
|
||||
* @param factor 系数,指成本分布的最大数量,一般取 150
|
||||
* @param range 取多少个历史交易日进行计算(包括当日),一般取 120
|
||||
* @return
|
||||
*/
|
||||
public static List<CostDistribution> getCostDistributions(List<StockValueEx> values, int from, int to, int factor, int range) {
|
||||
List<CostDistribution> cds = new ArrayList<>();
|
||||
for (int i = from; i < to; i++) {
|
||||
cds.add(getCostDistribution(values, i, factor, range));
|
||||
}
|
||||
return cds;
|
||||
}
|
||||
}
|
||||
495
src/main/java/link/at17/mid/tushare/data/util/PointsUtil.java
Normal file
495
src/main/java/link/at17/mid/tushare/data/util/PointsUtil.java
Normal file
@@ -0,0 +1,495 @@
|
||||
package link.at17.mid.tushare.data.util;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import link.at17.mid.tushare.data.models.IndexValue;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
import link.at17.mid.tushare.enums.CrossType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PointsUtil {
|
||||
|
||||
/**
|
||||
* 获取 StockValue 极大值所在的位置索引通用方法,默认迭代次数:1
|
||||
* @param list
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getStockValueHighestIndex(List<StockValue> list) {
|
||||
return getStockValueHighestIndex(list, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StockValue 极小值所在的位置索引通用方法,默认迭代次数:1
|
||||
* @param list
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getStockLowestIndex(List<StockValue> list) {
|
||||
return getValueLowestIndex(list, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StockValue 极大值所在的位置索引通用方法
|
||||
* @param list
|
||||
* @param generation 迭代次数
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getStockValueHighestIndex(List<StockValue> list, int generation) {
|
||||
if (CollectionUtils.isEmpty(list) || list.size() < 2) {
|
||||
return new ArrayList<Integer>();
|
||||
}
|
||||
List<Integer> indexList = new ArrayList<>();
|
||||
List<StockValue> subList = new ArrayList<>();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
StockValue val = list.get(i);
|
||||
if (i == 0) {
|
||||
if (val.getHigh().compareTo(list.get(1).getHigh()) == 1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i != list.size() - 1 &&
|
||||
val.getHigh().compareTo(list.get(i - 1).getHigh()) == 1 &&
|
||||
val.getHigh().compareTo(list.get(i + 1).getHigh()) == 1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i == list.size() - 1 &&
|
||||
val.getHigh().compareTo(list.get(i - 1).getHigh()) == 1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generation-- > 0) {
|
||||
List<Integer> subGeneration = getStockValueHighestIndex(subList, generation);
|
||||
List<Integer> subResult = new ArrayList<Integer>();
|
||||
for (Integer subLowest : subGeneration) {
|
||||
subResult.add(indexList.get(subLowest));
|
||||
}
|
||||
indexList = subResult;
|
||||
}
|
||||
|
||||
return indexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 StockValue 极小值所在的位置索引通用方法
|
||||
* @param list
|
||||
* @param generation 迭代次数
|
||||
* @param compareTo -1 或 1
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getValueLowestIndex(List<StockValue> list, int generation) {
|
||||
if (CollectionUtils.isEmpty(list) || list.size() < 2) {
|
||||
return new ArrayList<Integer>();
|
||||
}
|
||||
List<Integer> indexList = new ArrayList<>();
|
||||
List<StockValue> subList = new ArrayList<>();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
StockValue val = list.get(i);
|
||||
if (i == 0) {
|
||||
if (val.getLow().compareTo(list.get(1).getLow()) == -1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i != list.size() - 1 &&
|
||||
val.getLow().compareTo(list.get(i - 1).getLow()) == -1 &&
|
||||
val.getLow().compareTo(list.get(i + 1).getLow()) == -1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i == list.size() - 1 &&
|
||||
val.getLow().compareTo(list.get(i - 1).getLow()) == -1) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generation-- > 0) {
|
||||
List<Integer> subGeneration = getValueLowestIndex(subList, generation);
|
||||
List<Integer> subResult = new ArrayList<Integer>();
|
||||
for (Integer subLowest : subGeneration) {
|
||||
subResult.add(indexList.get(subLowest));
|
||||
}
|
||||
indexList = subResult;
|
||||
}
|
||||
|
||||
return indexList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取极值所在的位置索引通用方法
|
||||
* @param list
|
||||
* @param generation 迭代次数
|
||||
* @param compareTo -1 或 1
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getExtremeIndex(List<Double> list, int generation, int compareTo) {
|
||||
if (CollectionUtils.isEmpty(list) || list.size() < 2) {
|
||||
return new ArrayList<Integer>();
|
||||
}
|
||||
List<Integer> indexList = new ArrayList<>();
|
||||
List<Double> subList = new ArrayList<>();
|
||||
for (int i = 0, size = list.size(); i < size; i++) {
|
||||
Double val = list.get(i);
|
||||
if (Objects.isNull(val)) {
|
||||
continue;
|
||||
}
|
||||
if (i == 0) {
|
||||
if(val.compareTo(list.get(1)) == compareTo) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i != size - 1 &&
|
||||
Objects.nonNull(list.get(i - 1)) &&
|
||||
val.compareTo(list.get(i - 1)) == compareTo &&
|
||||
val.compareTo(list.get(i + 1)) == compareTo) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (i == size - 1 &&
|
||||
Objects.nonNull(list.get(i - 1)) &&
|
||||
val.compareTo(list.get(i - 1)) == compareTo) {
|
||||
indexList.add(i);
|
||||
if (generation > 0) {
|
||||
subList.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generation-- > 0) {
|
||||
List<Integer> subGeneration = getExtremeIndex(subList, generation, compareTo);
|
||||
List<Integer> subResult = new ArrayList<Integer>();
|
||||
for (Integer subLowest : subGeneration) {
|
||||
subResult.add(indexList.get(subLowest));
|
||||
}
|
||||
indexList = subResult;
|
||||
}
|
||||
|
||||
return indexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极小值所在的位置索引通用方法,默认迭代次数:1
|
||||
* @param list
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getLowestIndex(List<Double> list) {
|
||||
return getLowestIndex(list, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极小值所在的位置索引通用方法
|
||||
* @param list
|
||||
* @param generation 迭代次数
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getLowestIndex(List<Double> list, int generation) {
|
||||
return getExtremeIndex(list, generation, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极大值所在的位置索引通用方法,默认迭代次数:1
|
||||
* @param list
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getHighestIndex(List<Double> list) {
|
||||
return getHighestIndex(list, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极大值所在的位置索引通用方法
|
||||
* @param list
|
||||
* @param generation 迭代次数
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getHighestIndex(List<Double> list, int generation) {
|
||||
return getExtremeIndex(list, generation, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取金叉和死叉及其所在索引
|
||||
* @param fast 快线
|
||||
* @param slow 慢线
|
||||
* @return
|
||||
*/
|
||||
public static List<IndexValue<Integer, CrossType>> getCross(List<Double> fast, List<Double> slow) {
|
||||
Assert.isTrue(fast != null && slow != null && fast.size() == slow.size(), "快线和慢线不能为 null 且其长度必须相等!");
|
||||
TreeSet<IndexValue<Integer, CrossType>> set = new TreeSet<>();
|
||||
for (int i = 1; i < fast.size(); i++) {
|
||||
Double slowPrev = slow.get(i - 1), slowNow = slow.get(i);
|
||||
Double fastPrev = fast.get(i - 1), fastNow = fast.get(i);
|
||||
if (slowPrev == null || fastPrev == null || slowNow == null || fastNow == null) {
|
||||
continue;
|
||||
}
|
||||
if ((fastPrev < slowPrev && fastNow >= slowNow) || (fastPrev <= slowPrev && fastNow > slowNow)) {
|
||||
set.add(new IndexValue<Integer, CrossType>(i, CrossType.Bullish));
|
||||
}
|
||||
else if ((fastPrev > slowPrev && fastNow <= slowNow) || (fastPrev >= slowPrev && fastNow < slowNow)) {
|
||||
set.add(new IndexValue<Integer, CrossType>(i, CrossType.Bearish));
|
||||
}
|
||||
}
|
||||
return set.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多头排列的 Index<br>
|
||||
* 一般用于寻找买点或上升趋势。寻找买点时建议 satisfiedCount 严格等于提供的 lists 的个数。
|
||||
* @param risePeriod 要连续多少天出现多头才认为是多头,最小 2,默认 2
|
||||
* @param satisfiedCount lists 中有多少个 list 满足上涨和多头排列就认为市场是多头,最小 2,默认:lists 经过筛选后的 size
|
||||
* @param lists 要对比多头的 list,按照快慢排列,其中任意 list 不能为空,且提供的 lists 的个数至少为 2 组
|
||||
* @return
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static List<Integer> getBullishArrange(Integer risePeriod, Integer satisfiedCount, List<Double>... lists){
|
||||
Assert.isTrue(lists != null && lists.length > 1, "lists 不能为空且至少要有 2 组 List<Double> 存在!");
|
||||
if (risePeriod == null || risePeriod < 2) {
|
||||
risePeriod = 2;
|
||||
}
|
||||
List<List<Double>> listList = new ArrayList<>(Arrays.asList(lists));
|
||||
Integer length = null;
|
||||
for (int i = 0; i < listList.size(); i++) {
|
||||
List<Double> list = listList.get(i);
|
||||
if (list == null) {
|
||||
listList.remove(i--);
|
||||
continue;
|
||||
}
|
||||
if (length == null) {
|
||||
length = list.size();
|
||||
}
|
||||
else {
|
||||
Assert.isTrue(length == list.size(), "提供的 lists 内的各 List 长度必须相等!");
|
||||
}
|
||||
}
|
||||
Assert.isTrue(lists.length == listList.size(), "提供的 lists 不允许存在空 list!");
|
||||
if (satisfiedCount == null || satisfiedCount < 2 || satisfiedCount > listList.size()) {
|
||||
satisfiedCount = listList.size();
|
||||
}
|
||||
TreeSet<Integer> set = new TreeSet<>();
|
||||
if (listList.size() == 0) {
|
||||
return set.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
nextPeriod:
|
||||
for (int i = risePeriod - 1; i < length; i++) {
|
||||
final int index = i;
|
||||
// listList 是最终的去除了全 null lists 的 lists
|
||||
// 从中对每个 lists 取对应的 index 索引的值
|
||||
// 得到的就是 index 日的所有线的值
|
||||
List<Double> values = listList.stream().map(list -> list.get(index)).collect(Collectors.toList());
|
||||
// 这里判断 null,判断 list 的个数与 count 有关
|
||||
if (values.subList(0, satisfiedCount).stream().anyMatch(d -> d == null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 当天及前 period 天的多线信息,注意最好往前推,或者往后推时要取最后那一个点,不然属于未来函数了
|
||||
// 这里选择的是往前推 risePeriod days,再从 index - risePeriod 到 index 日,依次放入 nearValuesList
|
||||
List<List<Double>> nearValuesList = new ArrayList<>();
|
||||
for (int j = risePeriod - 1; j > 0; j--) {
|
||||
final int backwardIndex = j;
|
||||
nearValuesList.add(listList.stream().map(list -> list.get(index - backwardIndex)).collect(Collectors.toList()));
|
||||
}
|
||||
nearValuesList.add(values);
|
||||
/** 到这里,假设一开始给定的 List 是 MA5, MA10, MA20,则 nearValuesList 的结构是这样的 **/
|
||||
// List<List<Double>> nearValuesList
|
||||
// Index of nearValuesList : 0 , 1 , ..., period - 1
|
||||
// Structure of ... MA5[0], MA5[1], ..., MA5[period - 1]
|
||||
// MA10[0], MA10[1], ..., MA10[period - 1]
|
||||
// MA20[0], MA20[1], ..., MA20[period - 1]
|
||||
|
||||
// 统计有上涨趋势的线
|
||||
List<Integer> upListIndexes = new ArrayList<>();
|
||||
nextUpJudge:
|
||||
for (int j = 0; j < values.size(); j++) {
|
||||
for (int k = 0; k < risePeriod - 1; k++) {
|
||||
List<Double> prevValues = nearValuesList.get(k);
|
||||
if (prevValues.get(j) == null) {
|
||||
continue;
|
||||
}
|
||||
List<Double> currValues = nearValuesList.get(k + 1);
|
||||
if (currValues.get(j) == null) {
|
||||
continue;
|
||||
}
|
||||
// 只要有一次大于,那这根线就不算上涨趋势了,去判断下一根线
|
||||
if (prevValues.get(j) > currValues.get(j)) {
|
||||
continue nextUpJudge;
|
||||
}
|
||||
}
|
||||
// 一根线跑完一个 period 都没有触发 continue 那这根线认为是上涨趋势
|
||||
// 把这根线的索引加到上涨趋势索引列表里
|
||||
upListIndexes.add(j);
|
||||
}
|
||||
if (upListIndexes.size() < satisfiedCount) {
|
||||
// 当前 risePeriod 内,满足上涨趋势的线的数量不够,直接去下一个 period
|
||||
continue nextPeriod;
|
||||
}
|
||||
|
||||
// 统计多头排列的线
|
||||
// 假设现在有 MA5,MA10,MA20,MA60 四根线
|
||||
// count == 4 时,MA5 ≥ MA10 ≥ MA20 ≥ MA60 才认为是多头
|
||||
// count == 3 时,MA5 ≥ MA10 ≥ MA20 或 MA5 ≥ MA10 ≥ MA60 或 MA5 ≥ MA20 ≥ MA60 或 MA10 ≥ MA20 ≥ MA60 就可认为是多头
|
||||
// 以此类推
|
||||
|
||||
int bullCount = 0;
|
||||
int minBullCount = (satisfiedCount - 1) * satisfiedCount / 2;
|
||||
for (int j = 0; j < values.size() - 1; j++) {
|
||||
slowLineJudge:
|
||||
for (int k = j + 1; k < values.size(); k++) {
|
||||
for (int m = 0; m < risePeriod; m++) {
|
||||
List<Double> allLineOneDayValue = nearValuesList.get(m);
|
||||
Double fastLineValue = allLineOneDayValue.get(j);
|
||||
Double slowLineValue = allLineOneDayValue.get(k);
|
||||
if (fastLineValue == null || slowLineValue == null) {
|
||||
continue;
|
||||
}
|
||||
if (fastLineValue < slowLineValue) {
|
||||
// 对比下一条被对比线
|
||||
continue slowLineJudge;
|
||||
}
|
||||
}
|
||||
// 这条被对比线对比完了,没出现跳到下一个 slowLineJudge 的情况,可认为这两条线满足多头
|
||||
bullCount++;
|
||||
}
|
||||
}
|
||||
if (bullCount < minBullCount) {
|
||||
// 当前 period 内,满足多头排列的线的数量不够,直接去下一个 period
|
||||
continue nextPeriod;
|
||||
}
|
||||
|
||||
// 到这里都满足,加入
|
||||
set.add(i);
|
||||
}
|
||||
|
||||
return set.stream().collect(Collectors.toList());
|
||||
}
|
||||
/**
|
||||
* 获取多头排列的 Index
|
||||
* @param lists
|
||||
* @return
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static List<Integer> getBullishArrange(Integer period, List<Double>... lists){
|
||||
if (period == null || period < 1) {
|
||||
period = 1;
|
||||
}
|
||||
List<List<Double>> listList = new ArrayList<>(Arrays.asList(lists));
|
||||
Integer length = null;
|
||||
for (int i = 0; i < listList.size(); i++) {
|
||||
List<Double> list = listList.get(i);
|
||||
if (list == null) {
|
||||
listList.remove(i--);
|
||||
continue;
|
||||
}
|
||||
if (length == null) {
|
||||
length = list.size();
|
||||
}
|
||||
else {
|
||||
Assert.isTrue(length == list.size(), "提供的 List 长度必须相等!");
|
||||
}
|
||||
}
|
||||
TreeSet<Integer> set = new TreeSet<>();
|
||||
if (listList.size() == 0) {
|
||||
return set.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
valuesLabel:
|
||||
for (int i = period; i < length; i++) {
|
||||
final int index = i;
|
||||
List<Double> values = listList.stream().map(list -> list.get(index)).collect(Collectors.toList());
|
||||
if (values.stream().anyMatch(d -> d == null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 当天及前 period 天的多线信息,注意必须往前推,不然属于未来函数了
|
||||
List<List<Double>> prevValuesList = new ArrayList<>();
|
||||
for (int j = period; j > 0; j--) {
|
||||
final int jIndex = j;
|
||||
prevValuesList.add(listList.stream().map(list -> list.get(index - jIndex)).collect(Collectors.toList()));
|
||||
}
|
||||
prevValuesList.add(values);
|
||||
|
||||
// 上涨趋势
|
||||
for (int j = 0; j < values.size(); j++) {
|
||||
for (int k = 0; k < period - 1; k++) {
|
||||
List<Double> prevValues = prevValuesList.get(k);
|
||||
if (prevValues.get(j) == null) {
|
||||
continue;
|
||||
}
|
||||
List<Double> currValues = prevValuesList.get(k + 1);
|
||||
if (currValues.get(j) == null) {
|
||||
continue;
|
||||
}
|
||||
if (prevValues.get(j) > currValues.get(j)) {
|
||||
continue valuesLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 多头排列
|
||||
for (int j = 1; j < values.size(); j++) {
|
||||
if (j != values.size() - 1) {
|
||||
if (!((values.get(j - 1) > values.get(j) && values.get(j) >= values.get(j + 1)) || (values.get(j - 1) >= values.get(j) && values.get(j) > values.get(j + 1)))) {
|
||||
continue valuesLabel;
|
||||
}
|
||||
}
|
||||
else if (values.get(j - 1) < values.get(j)) {
|
||||
continue valuesLabel;
|
||||
}
|
||||
}
|
||||
set.add(i);
|
||||
}
|
||||
|
||||
return set.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 i 是否位于指定的金叉(死叉)~死叉(金叉)区间内
|
||||
* @param i
|
||||
* @param crossIndexList
|
||||
* @param begin
|
||||
* @return
|
||||
*/
|
||||
public static Boolean inCrossAreaBetween(int i, List<IndexValue<Integer, CrossType>> crossIndexList, CrossType beginCrossType) {
|
||||
for (int k = 0; k < crossIndexList.size(); k++) {
|
||||
IndexValue<Integer, CrossType> cross = crossIndexList.get(k), crossNext = k != crossIndexList.size() - 1 ? crossIndexList.get(k + 1) : null;
|
||||
if (cross.getValue() == beginCrossType && cross.getIndex() >= i && (crossNext != null && crossNext.getIndex() <= k || crossNext == null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
105
src/main/java/link/at17/mid/tushare/data/util/PriceUtil.java
Normal file
105
src/main/java/link/at17/mid/tushare/data/util/PriceUtil.java
Normal file
@@ -0,0 +1,105 @@
|
||||
package link.at17.mid.tushare.data.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import link.at17.mid.tushare.data.models.StockInfo;
|
||||
import link.at17.mid.tushare.data.models.StockValue;
|
||||
|
||||
public class PriceUtil {
|
||||
|
||||
public static final BigDecimal DOT_05 = new BigDecimal("0.05");
|
||||
public static final BigDecimal DOT_1 = new BigDecimal("0.1");
|
||||
public static final BigDecimal DOT_2 = new BigDecimal("0.2");
|
||||
public static final BigDecimal TWO = new BigDecimal("2");
|
||||
|
||||
/**
|
||||
* 根据股票信息获取涨跌幅限制<br/>
|
||||
* 股票以 30 或 68 为开头,或同花顺概念为注册制次新股的,返回 0.2<br/>
|
||||
* 不为以上情况,且为 ST 股票的,返回 0.05<br/>
|
||||
* 不为以上情况的,返回 0.1
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
public static BigDecimal getLimitByStockInfo(StockInfo stockInfo) {
|
||||
if (stockInfo.getNudeCode().startsWith("30")
|
||||
|| stockInfo.getNudeCode().startsWith("68")
|
||||
|| (stockInfo.getThsBelongings() != null && stockInfo.getThsBelongings().contains("注册制"))) {
|
||||
return new BigDecimal("0.2");
|
||||
}
|
||||
else if (stockInfo.getName().contains("ST")) {
|
||||
return new BigDecimal("0.05");
|
||||
}
|
||||
return new BigDecimal("0.1");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前收、现收和股票信息,判断股票是否涨停
|
||||
* @param preClose
|
||||
* @param nowClose
|
||||
* @param stockInfo
|
||||
* @return
|
||||
*/
|
||||
public static boolean isLimit(double preClose, double nowClose, StockInfo stockInfo) {
|
||||
return isLimit(preClose, nowClose, stockInfo, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前收、现收和股票信息,判断股票是否涨/跌停
|
||||
* @param preClose
|
||||
* @param nowClose
|
||||
* @param stockInfo
|
||||
* @param negative 是否跌停
|
||||
* @return
|
||||
*/
|
||||
public static boolean isLimit(double preClose, double nowClose, StockInfo stockInfo, boolean negative) {
|
||||
BigDecimal preCloseDec = new BigDecimal(preClose).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal nowCloseDec = new BigDecimal(nowClose).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal limit = getLimitByStockInfo(stockInfo);
|
||||
if (negative) {
|
||||
limit = limit.negate();
|
||||
}
|
||||
BigDecimal limitUp = preCloseDec.multiply(
|
||||
limit.add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
|
||||
boolean isLimitUp = limitUp.compareTo(nowCloseDec) == 0;
|
||||
if (limit.abs().compareTo(DOT_05) == 0) {
|
||||
limitUp = preCloseDec.multiply(
|
||||
limit.multiply(TWO).add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
|
||||
isLimitUp = isLimitUp || limitUp.compareTo(nowCloseDec) == 0;
|
||||
}
|
||||
return isLimitUp;
|
||||
}
|
||||
/**
|
||||
* 判断是否达到涨跌幅限制
|
||||
* @param preClose 前收
|
||||
* @param nowClose 现收
|
||||
* @param upDownLimitPrecent 涨跌幅限制
|
||||
* @return
|
||||
*/
|
||||
public static boolean isLimit(double preClose, double nowClose, BigDecimal upDownLimitPrecent) {
|
||||
BigDecimal preCloseDec = new BigDecimal(preClose).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal nowCloseDec = new BigDecimal(nowClose).setScale(2, RoundingMode.HALF_UP);
|
||||
BigDecimal limitUp = preCloseDec.multiply(
|
||||
upDownLimitPrecent.add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
|
||||
return limitUp.compareTo(nowCloseDec) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据股价列表获取所有涨/跌停的 Indexes
|
||||
* @param stockValues 股价列表
|
||||
* @param upDownLimitPrecent 涨跌幅
|
||||
* @return
|
||||
*/
|
||||
public static List<Integer> getLimitIndexes(List<? extends StockValue> stockValues, BigDecimal upDownLimitPrecent) {
|
||||
List<Integer> list = new ArrayList<>();
|
||||
for (int i = 0; i < stockValues.size() - 1; i++) {
|
||||
if (isLimit(stockValues.get(i).getClose(), stockValues.get(i + 1).getClose(), upDownLimitPrecent)) {
|
||||
list.add(i + 1);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package link.at17.mid.tushare.data.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class StockCodeUtil {
|
||||
|
||||
private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(\\d{1,2})\\.(.*?)$");
|
||||
|
||||
/**
|
||||
* 转为标准形式的代码,最终仅支持 6 位数字 + .SZ 或 .SH 的形式,否则会抛出异常
|
||||
* @param code
|
||||
* @return
|
||||
* @throws UnsupportedOperationException
|
||||
*/
|
||||
public static String toStandardCode(String code) throws UnsupportedOperationException {
|
||||
if (StringUtils.isEmpty(code)) {
|
||||
return code;
|
||||
}
|
||||
Matcher m = EM_CODE_PATTERN.matcher(code);
|
||||
if (m.matches()) {
|
||||
String sec = m.group(1);
|
||||
String nudeCode = m.group(2);
|
||||
|
||||
if ("1".equals(sec)) {
|
||||
return nudeCode + ".SH";
|
||||
}
|
||||
else if ("0".equals(sec)) {
|
||||
return nudeCode + ".SZ";
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
41
src/main/java/link/at17/mid/tushare/dto/LayPageResp.java
Normal file
41
src/main/java/link/at17/mid/tushare/dto/LayPageResp.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package link.at17.mid.tushare.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 后台查询结果,适配 layui
|
||||
* @author Doghole
|
||||
**/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class LayPageResp<T> implements Serializable {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 3706125181722544872L;
|
||||
|
||||
//状态码
|
||||
private int code;
|
||||
|
||||
//提示消息
|
||||
private String msg;
|
||||
|
||||
//总条数
|
||||
private long count;
|
||||
|
||||
//表格数据
|
||||
private List<T> data;
|
||||
|
||||
public LayPageResp() {}
|
||||
|
||||
public LayPageResp(IPage<T> page) {
|
||||
this.data = page.getRecords();
|
||||
this.count = page.getTotal();
|
||||
}
|
||||
}
|
||||
222
src/main/java/link/at17/mid/tushare/dto/R.java
Normal file
222
src/main/java/link/at17/mid/tushare/dto/R.java
Normal file
@@ -0,0 +1,222 @@
|
||||
package link.at17.mid.tushare.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import link.at17.mid.tushare.web.exception.RException;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* application/json 返回
|
||||
* <p>
|
||||
* 此处返回若非提前指定,虽然 resultCode 可以设置对应 HttpStatus, 但 Response 的 HttpStatus 依然会是
|
||||
* 200.<br>
|
||||
* 需要同时指定 HttpStatus 的,见 {@code RException}
|
||||
* </p>
|
||||
*
|
||||
* @see me.qwq.doghouse.exception.RException
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Slf4j
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -7658884488696622015L;
|
||||
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private int resultCode;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public R() {
|
||||
}
|
||||
|
||||
public R(int resultCode, String message) {
|
||||
this.resultCode = resultCode;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isOk() {
|
||||
return HttpStatus.OK.value() == getResultCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前 R<T> 类型的 T 数据调用 toString(),并将其结果作为生成的 R<String> 的 data, 返回一个相应的
|
||||
* R<String> 对象
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public R<String> toStringifyR() {
|
||||
return new R<String>().setResultCode(resultCode).setMessage(message).setData(data.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定状态且 data 为空(类型为 String)的 R
|
||||
*
|
||||
* @param constants
|
||||
* @return
|
||||
*/
|
||||
public static R<String> status(HttpStatus constants) {
|
||||
return new R<String>().setResultCode(constants.value()).setMessage(constants.getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定状态和 data 的 R
|
||||
*
|
||||
* @param constants
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> status(HttpStatus constants, T data) {
|
||||
return new R<T>().setResultCode(constants.value()).setMessage(constants.getReasonPhrase()).setData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.OK 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> ok(T data) {
|
||||
return status(HttpStatus.OK, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.OK
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> ok() {
|
||||
return new R<>().setResultCode(HttpStatus.OK.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.UNAUTHORIZED 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> unauthorized(T data) {
|
||||
return status(HttpStatus.UNAUTHORIZED, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.UNAUTHORIZED
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> unauthorized() {
|
||||
return status(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.BAD_REQUEST 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> badRequest(T data) {
|
||||
return status(HttpStatus.BAD_REQUEST, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.BAD_REQUEST
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> badRequest() {
|
||||
return status(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.INTERNAL_SERVER_ERROR 状态和指定的 Data
|
||||
*
|
||||
* @param <T>
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<T> internalServerError(T data) {
|
||||
return status(HttpStatus.INTERNAL_SERVER_ERROR, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 HttpStatus.INTERNAL_SERVER_ERROR
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static R<?> internalServerError() {
|
||||
return status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok()<br>
|
||||
* false throw RException.badRequest()
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static R<?> judge(boolean condition) {
|
||||
if (condition) return R.ok();
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judgeNonNull(Object obj) {
|
||||
if (obj != null) return R.ok(obj);
|
||||
throw RException.badRequest();
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judge(ThrowingSupplier<?> supplier) {
|
||||
try {
|
||||
return R.ok(supplier.get());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw RException.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static R<?> judgeThrow(ThrowingSupplier<?> supplier) throws Exception {
|
||||
return R.ok(supplier.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok(T okData)<br>
|
||||
* false throw RException.badRequest(failedMessage)
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static <T> R<?> judge(boolean condition, T okData, String failedMessage) {
|
||||
if (condition) return R.ok(okData);
|
||||
throw RException.badRequest(failedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值判断返回<br>
|
||||
* true 返回 R.ok(null)<br>
|
||||
* false throw RException.badRequest(failedMessage)
|
||||
* @param condition
|
||||
* @return
|
||||
*/
|
||||
public static R<?> judge(boolean condition, String failedMessage) {
|
||||
return R.judge(condition, null, failedMessage);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface ThrowingSupplier<T> {
|
||||
T get() throws Exception;
|
||||
}
|
||||
}
|
||||
13
src/main/java/link/at17/mid/tushare/enums/AdjustType.java
Normal file
13
src/main/java/link/at17/mid/tushare/enums/AdjustType.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
public enum AdjustType {
|
||||
None("0"),
|
||||
Qfq("1"),
|
||||
Hfq("2");
|
||||
|
||||
private String emCode;
|
||||
public String getEmCode() { return emCode; }
|
||||
private AdjustType(String emCode) {
|
||||
this.emCode = emCode;
|
||||
}
|
||||
}
|
||||
6
src/main/java/link/at17/mid/tushare/enums/BuyOrSell.java
Normal file
6
src/main/java/link/at17/mid/tushare/enums/BuyOrSell.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
public enum BuyOrSell {
|
||||
Buy,
|
||||
Sell
|
||||
}
|
||||
6
src/main/java/link/at17/mid/tushare/enums/CrossType.java
Normal file
6
src/main/java/link/at17/mid/tushare/enums/CrossType.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
public enum CrossType {
|
||||
Bullish,
|
||||
Bearish
|
||||
}
|
||||
17
src/main/java/link/at17/mid/tushare/enums/InfoSource.java
Normal file
17
src/main/java/link/at17/mid/tushare/enums/InfoSource.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
/**
|
||||
* 信息来源
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
public enum InfoSource {
|
||||
/** 同花顺 **/
|
||||
THS,
|
||||
/** 东方财富 **/
|
||||
EastMoney,
|
||||
/** tushare **/
|
||||
Tushare,
|
||||
/** 新浪财经 **/
|
||||
Sina
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 开盘啦 特殊指标
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
public enum KaipanlaIndexType {
|
||||
|
||||
/**
|
||||
* 对倒金额
|
||||
*/
|
||||
EXCHANGE_AMOUNT("GetDuiDaoKLine", "DDJE", "exchange_amount"),
|
||||
/**
|
||||
* 竞价成交量
|
||||
*/
|
||||
BID_VOL("GetBidVolKLine", "Vol", "bid_vol"),
|
||||
/**
|
||||
* 压单托单
|
||||
*/
|
||||
HOLD_PRESS_AMOUNT("GetTuoYaDanKLine", "TDJE", "YDJE", "hold_amount", "press_amount"),
|
||||
/**
|
||||
* 资金净额(大单净额,大于 30 万的成交单)
|
||||
*/
|
||||
BIG_AMOUNT("GetDaDanKLine2New", "DDJE", "big_amount");
|
||||
/**
|
||||
* 指标类型,数据库对应字段名
|
||||
*/
|
||||
@Getter
|
||||
private String[] fieldsName;
|
||||
/**
|
||||
* 别名,请求返回字段名
|
||||
*/
|
||||
@Getter
|
||||
private String[] alias;
|
||||
/**
|
||||
* 接口 action 名称
|
||||
*/
|
||||
@Getter String actionName;
|
||||
|
||||
/**
|
||||
* 初始化开盘啦指标
|
||||
* @param actionName 请求名称,即指标对应请求的名称
|
||||
* @param args 参数,填写格式为:请求返回字段1 请求返回字段2 ... 请求返回字段n 数据库对应字段1 数据库对应字段2 ... 数据库对应字段n
|
||||
*/
|
||||
private KaipanlaIndexType(
|
||||
String actionName,
|
||||
String...args
|
||||
) {
|
||||
this.actionName = actionName;
|
||||
Assert.isTrue(args != null && args.length % 2 == 0, "参数错误,参数数目必须为 2 的倍数");
|
||||
int half = args.length / 2;
|
||||
this.alias = new String[half];
|
||||
this.fieldsName = new String[half];
|
||||
for (int i = 0; i < half; i++) {
|
||||
alias[i] = args[i];
|
||||
fieldsName[i] = args[i + half];
|
||||
}
|
||||
}
|
||||
|
||||
private KaipanlaIndexType(
|
||||
String actionName,
|
||||
String[] alias,
|
||||
String[] fieldsName
|
||||
) {
|
||||
this.actionName = actionName;
|
||||
this.alias = alias;
|
||||
this.fieldsName = fieldsName;
|
||||
}
|
||||
}
|
||||
24
src/main/java/link/at17/mid/tushare/enums/LimitType.java
Normal file
24
src/main/java/link/at17/mid/tushare/enums/LimitType.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 涨跌停类型
|
||||
* @author Administrator
|
||||
*
|
||||
*/
|
||||
public enum LimitType {
|
||||
UpLimit("U"),
|
||||
DownLimit("D"),
|
||||
Zha("Z");
|
||||
|
||||
@Getter
|
||||
@EnumValue
|
||||
private String limitType;
|
||||
|
||||
private LimitType(String limitType) {
|
||||
this.limitType = limitType;
|
||||
}
|
||||
}
|
||||
19
src/main/java/link/at17/mid/tushare/enums/ListStatus.java
Normal file
19
src/main/java/link/at17/mid/tushare/enums/ListStatus.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum ListStatus {
|
||||
LIST("L"),
|
||||
DELIST("D"),
|
||||
PAUSE("P");
|
||||
|
||||
@Getter
|
||||
@EnumValue
|
||||
private String listStatus;
|
||||
|
||||
private ListStatus(String listStatus) {
|
||||
this.listStatus = listStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
public enum PivotPointType {
|
||||
/** 经典中心点,提供 Pivot、Resistance/Support 1-3 **/
|
||||
Classic,
|
||||
/** Woodie's 中心点,提供 Pivot、Resistance/Support 1-2 **/
|
||||
Woodies,
|
||||
/** Camarilla 中心点,提供 Resistance/Support 1-4 **/
|
||||
Camarilla,
|
||||
/** DeMark's 中心点,提供 Resistance/Support 1 **/
|
||||
DeMarks
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum StockHolderType {
|
||||
|
||||
|
||||
TOP10(0),
|
||||
TOP10Float(1);
|
||||
|
||||
@EnumValue
|
||||
@Getter
|
||||
private int isFloat;
|
||||
|
||||
private StockHolderType(int isFloat) {
|
||||
this.isFloat = isFloat;
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/link/at17/mid/tushare/enums/StockMarket.java
Normal file
29
src/main/java/link/at17/mid/tushare/enums/StockMarket.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum StockMarket {
|
||||
|
||||
|
||||
BK("BK", "90", null),
|
||||
SZ("SZ", "0", "SZSE"),
|
||||
SH("SH", "1", "SSE"),
|
||||
BJ("BJ", "BJ", "BSE");
|
||||
|
||||
@Getter
|
||||
private String eMCode;
|
||||
@Getter
|
||||
private String standardCode;
|
||||
@EnumValue
|
||||
@Getter
|
||||
private String exchangeCode;
|
||||
|
||||
private StockMarket(String standardCode, String emCode, String exchangeCode) {
|
||||
this.standardCode = standardCode;
|
||||
this.eMCode = emCode;
|
||||
this.exchangeCode = exchangeCode;
|
||||
}
|
||||
|
||||
}
|
||||
227
src/main/java/link/at17/mid/tushare/enums/StockSpan.java
Normal file
227
src/main/java/link/at17/mid/tushare/enums/StockSpan.java
Normal file
@@ -0,0 +1,227 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 股价数据粒度
|
||||
* @author Administrator
|
||||
*
|
||||
*/
|
||||
public enum StockSpan {
|
||||
|
||||
Tick(null),
|
||||
Minute(1),
|
||||
Minute5(5),
|
||||
Minute15(15),
|
||||
Minute30(30),
|
||||
Minute60(60),
|
||||
Minute120(120),
|
||||
Daily(null),
|
||||
Weekly(null),
|
||||
Monthly(null),
|
||||
Seasonly(null),
|
||||
HalfYearly(null),
|
||||
Yearly(null);
|
||||
|
||||
@Getter
|
||||
private Integer min;
|
||||
|
||||
private StockSpan(Integer min) {
|
||||
this.min = min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取东财对应的时间粒度表示
|
||||
* @return
|
||||
*/
|
||||
public String getEastMoneyCode() {
|
||||
|
||||
if (this.min != null) {
|
||||
return String.format("%d", 0);
|
||||
}
|
||||
|
||||
switch(this) {
|
||||
case Daily:
|
||||
return "101";
|
||||
case Weekly:
|
||||
return "102";
|
||||
case Monthly:
|
||||
return "103";
|
||||
case Seasonly:
|
||||
return "104";
|
||||
case HalfYearly:
|
||||
return "105";
|
||||
case Yearly:
|
||||
return "106";
|
||||
default:
|
||||
throw new RuntimeException("东财时间粒度不支持 " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从东财时间粒度转换为 StockSpan
|
||||
* @param eastMoneyCode
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromEastMoneyCode(String eastMoneyCode) {
|
||||
|
||||
switch (eastMoneyCode) {
|
||||
case "1":
|
||||
return Minute;
|
||||
case "5":
|
||||
return Minute5;
|
||||
case "15":
|
||||
return Minute15;
|
||||
case "30":
|
||||
return Minute30;
|
||||
case "60":
|
||||
return Minute60;
|
||||
case "120":
|
||||
return Minute120;
|
||||
case "101":
|
||||
return Daily;
|
||||
case "102":
|
||||
return Weekly;
|
||||
case "103":
|
||||
return Monthly;
|
||||
case "104":
|
||||
return Seasonly;
|
||||
case "105":
|
||||
return HalfYearly;
|
||||
case "106":
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的东财时间粒度 " + eastMoneyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为益盟时间粒度
|
||||
* @return
|
||||
*/
|
||||
public Integer getEmoneyCode() {
|
||||
|
||||
if (this.min != null) {
|
||||
return this.min;
|
||||
}
|
||||
|
||||
switch(this) {
|
||||
case Daily:
|
||||
return 10000;
|
||||
case Weekly:
|
||||
return 20000;
|
||||
case Monthly:
|
||||
return 30000;
|
||||
case Seasonly:
|
||||
return 40000;
|
||||
case HalfYearly:
|
||||
return 50000;
|
||||
case Yearly:
|
||||
return 60000;
|
||||
default:
|
||||
throw new RuntimeException("益盟时间粒度不支持:" + this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从益盟时间粒度转换
|
||||
* @param emoneyDataPeriod
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromEmoneyCode(Integer emoneyDataPeriod) {
|
||||
switch (emoneyDataPeriod) {
|
||||
case 1:
|
||||
return Minute;
|
||||
case 5:
|
||||
return Minute5;
|
||||
case 15:
|
||||
return Minute15;
|
||||
case 30:
|
||||
return Minute30;
|
||||
case 60:
|
||||
return Minute60;
|
||||
case 120:
|
||||
return Minute120;
|
||||
case 10000:
|
||||
return Daily;
|
||||
case 20000:
|
||||
return Weekly;
|
||||
case 30000:
|
||||
return Monthly;
|
||||
case 40000:
|
||||
return Seasonly;
|
||||
case 50000:
|
||||
return HalfYearly;
|
||||
case 60000:
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的益盟时间粒度 " + emoneyDataPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成开盘啦数据粒度
|
||||
* @return
|
||||
*/
|
||||
public String getKaipanlaType() {
|
||||
String type;
|
||||
if (this == StockSpan.Yearly) {
|
||||
type = "y";
|
||||
}
|
||||
else if (this == StockSpan.Monthly) {
|
||||
type = "m";
|
||||
}
|
||||
else if (this == StockSpan.Weekly) {
|
||||
type = "w";
|
||||
}
|
||||
else if (this == StockSpan.Minute60) {
|
||||
type = "60";
|
||||
}
|
||||
else if (this == StockSpan.Minute30) {
|
||||
type = "30";
|
||||
}
|
||||
else if (this == StockSpan.Minute15) {
|
||||
type = "15";
|
||||
}
|
||||
else if (this == StockSpan.Minute5) {
|
||||
type = "5";
|
||||
}
|
||||
else if (this == StockSpan.Daily){
|
||||
type = "d";
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("开盘啦指标不支持该数据粒度 " + this);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从开盘啦时间粒度转换
|
||||
* @param kaipanlaType
|
||||
* @return
|
||||
*/
|
||||
public static StockSpan fromKaipanlaType(String kaipanlaType) {
|
||||
|
||||
switch (kaipanlaType) {
|
||||
case "5":
|
||||
return Minute5;
|
||||
case "15":
|
||||
return Minute15;
|
||||
case "30":
|
||||
return Minute30;
|
||||
case "60":
|
||||
return Minute60;
|
||||
case "d":
|
||||
return Daily;
|
||||
case "w":
|
||||
return Weekly;
|
||||
case "n":
|
||||
return Monthly;
|
||||
case "y":
|
||||
return Yearly;
|
||||
default:
|
||||
throw new RuntimeException("未知的开盘啦时间粒度 " + kaipanlaType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package link.at17.mid.tushare.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum ThsStockMarket {
|
||||
A("A"),
|
||||
US("US"),
|
||||
HK("HK");
|
||||
|
||||
@EnumValue
|
||||
@Getter
|
||||
private String code;
|
||||
|
||||
private ThsStockMarket(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
84
src/main/java/link/at17/mid/tushare/interfaces/IConfig.java
Normal file
84
src/main/java/link/at17/mid/tushare/interfaces/IConfig.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package link.at17.mid.tushare.interfaces;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.copier.CopyOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import link.at17.mid.tushare.service.ConfigService;
|
||||
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* 配置项必须实现该接口
|
||||
*
|
||||
* @author Doghole
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public interface IConfig<T extends IConfig<T>> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public default boolean saveOrUpdate() {
|
||||
return SpringContextHolder.getBean(ConfigService.class).saveOrUpdate((T)this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前处理。无论 @ConfigInfo 是否设置 save = true 都会调用
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T afterSaving() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存后处理。无论 @ConfigInfo 是否设置 save = true 都会调用
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T beforeSaving() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并到 other,且返回合并后的 other
|
||||
* @return
|
||||
*/
|
||||
public default T mergeTo(T other) {
|
||||
if (!Objects.equals(this, other)) {
|
||||
BeanUtil.copyProperties(this, other,
|
||||
CopyOptions.create().setIgnoreNullValue(true));
|
||||
}
|
||||
return other;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并其他,并返回本身
|
||||
* @param other
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public default T mergeFrom(T other) {
|
||||
if (!Objects.equals(this, other)) {
|
||||
BeanUtil.copyProperties(other, this,
|
||||
CopyOptions.create().setIgnoreNullValue(true));
|
||||
}
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化完成之后的方法,会在
|
||||
* <code><i>beanFactory</i>.autowireBean(bean)</code>
|
||||
* 和
|
||||
* <code><i>beanFactory</i>.initializeBean(bean, beanName)</code>
|
||||
* 后执行。<br>
|
||||
* @see quant.rich.emoney.config.ConfigServiceFactoryBean
|
||||
*/
|
||||
@PostConstruct
|
||||
public default void afterBeanInit() {}
|
||||
|
||||
public static class Views {
|
||||
public static class Persistence {}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package link.at17.mid.tushare.robot.feishu.ablilities;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import link.at17.mid.tushare.robot.models.FeishuAppEnv;
|
||||
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJacksonInputMessage;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice("link.at17.mid.tushare.robot.feishu.controller")
|
||||
public class FeishuAppControllerAdvice extends RequestBodyAdviceAdapter {
|
||||
|
||||
@Autowired
|
||||
HttpServletResponse response;
|
||||
|
||||
private static MessageDigest sha256Digest = null;
|
||||
|
||||
@PostConstruct
|
||||
private void init() throws NoSuchAlgorithmException {
|
||||
sha256Digest = MessageDigest.getInstance("SHA-256");
|
||||
}
|
||||
|
||||
private FeishuAppEnv getEnv(Class<? extends FeishuAppEnv> clazz) {
|
||||
return SpringContextHolder.getBean(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书 App 校验错误处理
|
||||
* @param ex
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(value = FeishuAppThrowable.class)
|
||||
@ResponseBody
|
||||
public Serializable feishuAppVerifyExceptionHandler(FeishuAppThrowable ex) {
|
||||
JSONObject respJson = new JSONObject();
|
||||
if (!ex.hasResponse()) {
|
||||
response.setStatus(HttpStatus.BAD_REQUEST.value());
|
||||
respJson.put("error_info", ex.getMessage());
|
||||
return respJson;
|
||||
}
|
||||
else {
|
||||
return ex.getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter methodParameter, Type targetType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// 是否支持注入
|
||||
FeishuAppVerify feishuAppVerify =
|
||||
methodParameter.getMethod()
|
||||
.getAnnotation(FeishuAppVerify.class);
|
||||
// 首先是验证注解不为空,而后获取环境配置的 encryptKey 不为空
|
||||
return Objects.nonNull(feishuAppVerify);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写,用于验证和解密
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Override
|
||||
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
|
||||
HttpHeaders headers = inputMessage.getHeaders();
|
||||
if (!headers.getContentType().toString().startsWith(MediaType.APPLICATION_JSON.toString())
|
||||
) {
|
||||
return inputMessage;
|
||||
}
|
||||
try {
|
||||
|
||||
FeishuAppVerify feishuAppVerify =
|
||||
parameter.getMethod()
|
||||
.getAnnotation(FeishuAppVerify.class);
|
||||
FeishuAppEnv env = getEnv(feishuAppVerify.value());
|
||||
InputStream inputStream = inputMessage.getBody();
|
||||
|
||||
String body = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
|
||||
JSONObject json = JSONObject.parseObject(body);
|
||||
if (env.supportDecryption()) {
|
||||
try {
|
||||
json = JSONObject.parseObject(env.decrypt(JSONObject.parseObject(body).getString("encrypt")));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new FeishuAppThrowable("decrypt error");
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.isNull(json)) {
|
||||
// 本是加密的配置,却传来明文,或者主体根本无法获取
|
||||
throw new FeishuAppThrowable("bad request");
|
||||
}
|
||||
|
||||
// if it's a challenge
|
||||
if (json.containsKey("type") && "url_verification".equals(json.getString("type"))) {
|
||||
final String challenge = json.getString("challenge");
|
||||
throw new FeishuAppThrowable("challenge").setResponse(new JSONObject() {{ put("challenge", challenge);}});
|
||||
}
|
||||
|
||||
// schema verify
|
||||
if (!json.containsKey("schema")) {
|
||||
throw new FeishuAppThrowable("request is not callback event(v2)");
|
||||
}
|
||||
|
||||
// Token verify
|
||||
if (!json.getJSONObject("header").getString("token").equals(env.getVerificationToken())) {
|
||||
throw new FeishuAppThrowable("invalid token");
|
||||
}
|
||||
|
||||
String timestampStr = headers.getFirst("X-Lark-Request-Timestamp");
|
||||
|
||||
// Timestamp expire verify
|
||||
int expireIn = feishuAppVerify.timestampExpireIn();
|
||||
if (expireIn > -1) {
|
||||
int timestampInt = Integer.parseInt(timestampStr);
|
||||
int currentTs = (int) (System.currentTimeMillis() / 1000);
|
||||
if (currentTs - timestampInt > expireIn) {
|
||||
throw new FeishuAppThrowable("timestamp expired");
|
||||
}
|
||||
}
|
||||
|
||||
String nonce = headers.getFirst("X-Lark-Request-Nonce");
|
||||
String signature = headers.getFirst("X-Lark-Signature");
|
||||
// Signature verify
|
||||
String localSignature = Hex.encodeHexString(
|
||||
sha256Digest.digest(
|
||||
(timestampStr + nonce + env.getEncryptKey() + body).getBytes(StandardCharsets.UTF_8)));
|
||||
if (!localSignature.equalsIgnoreCase(signature)) {
|
||||
throw new FeishuAppThrowable("invalid signature in event");
|
||||
}
|
||||
return new MappingJacksonInputMessage(new ByteArrayInputStream(json.toJSONString().getBytes()), inputMessage.getHeaders());
|
||||
}
|
||||
catch (FeishuAppThrowable e) {
|
||||
log.error(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("Error occurred. ", e);
|
||||
throw new FeishuAppThrowable("bad request");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package link.at17.mid.tushare.robot.feishu.ablilities;
|
||||
|
||||
public enum FeishuAppEvents {
|
||||
|
||||
ImReceiveMessageV1("im.message.receive_v1");
|
||||
|
||||
private String value;
|
||||
|
||||
private FeishuAppEvents(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package link.at17.mid.tushare.robot.feishu.ablilities;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class FeishuAppThrowable extends RuntimeException {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain=true)
|
||||
private Serializable response = null;
|
||||
|
||||
public FeishuAppThrowable() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FeishuAppThrowable(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public boolean hasResponse() {
|
||||
return Objects.nonNull(response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package link.at17.mid.tushare.robot.feishu.ablilities;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import link.at17.mid.tushare.robot.models.FeishuAppEnv;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* 飞书请求头校验注解
|
||||
* @author Barry
|
||||
*
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface FeishuAppVerify {
|
||||
|
||||
Class<? extends FeishuAppEnv> value();
|
||||
|
||||
/**
|
||||
* 飞书请求时间戳过期时间(秒),默认 -1,即永不过期
|
||||
* @return
|
||||
*/
|
||||
int timestampExpireIn() default -1;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package link.at17.mid.tushare.robot.feishu.ablilities;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface FeishuRegisterEvent {
|
||||
|
||||
FeishuAppEvents value();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package link.at17.mid.tushare.robot.feishu.controller;
|
||||
|
||||
public class BaseRobotV1 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package link.at17.mid.tushare.robot.feishu.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import link.at17.mid.tushare.robot.feishu.ablilities.FeishuAppEvents;
|
||||
import link.at17.mid.tushare.robot.feishu.ablilities.FeishuAppThrowable;
|
||||
import link.at17.mid.tushare.robot.feishu.ablilities.FeishuAppVerify;
|
||||
import link.at17.mid.tushare.robot.models.SweetDogAppEnv;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/feishu/sweetDogRobotV1")
|
||||
public class SweetDogRobotV1 {
|
||||
|
||||
@Autowired
|
||||
private SweetDogAppEnv sweetDogAppEnv;
|
||||
|
||||
/*
|
||||
* 该接口从飞书网络端获取消息,并且识别消息后回传给飞书端。相当于一个复读机
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@PostMapping("/request")
|
||||
@ResponseBody
|
||||
@FeishuAppVerify(SweetDogAppEnv.class)
|
||||
private JSONObject request(@RequestBody JSONObject json) {
|
||||
|
||||
String eventType = json.getJSONObject("header").getString("event_type");
|
||||
if (eventType.equals(FeishuAppEvents.ImReceiveMessageV1.getValue()) ) {
|
||||
JSONObject message = json.getJSONObject("event").getJSONObject("message");
|
||||
if (!message.getString("message_type").equals("text")) {
|
||||
throw new FeishuAppThrowable("Other types of messages have not been processed yet");
|
||||
}
|
||||
sweetDogAppEnv.getTenantAccessToken().ifPresent((tenantAccessToken) -> {
|
||||
OkHttpClient okHttpClient = new OkHttpClient();
|
||||
JSONObject postJson = new JSONObject() {{
|
||||
put("receive_id", json
|
||||
.getJSONObject("event")
|
||||
.getJSONObject("sender")
|
||||
.getJSONObject("sender_id").getString("open_id"));
|
||||
put("content", message.get("content"));
|
||||
put("msg_type", "text");
|
||||
}};
|
||||
Request request = new Request.Builder()
|
||||
.url(sweetDogAppEnv.getLarkHost() + "/open-apis/im/v1/messages?receive_id_type=open_id")
|
||||
.addHeader("Authorization", "Bearer " + tenantAccessToken)
|
||||
.post(
|
||||
okhttp3.RequestBody.create(
|
||||
postJson.toJSONString(),
|
||||
MediaType.parse("application/json; charset=utf-8")))
|
||||
.build();
|
||||
log.info("tenantAccessToken {}, requestBody {}", tenantAccessToken, postJson);
|
||||
final Call call = okHttpClient.newCall(request);
|
||||
Response resp = null;
|
||||
try {
|
||||
resp = call.execute();
|
||||
JSONObject sendRespJson = JSONObject.parseObject(resp.body().string());
|
||||
if (sendRespJson.getIntValue("code") != 0) {
|
||||
throw new Exception (sendRespJson.getJSONObject("error").toJSONString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("send message error", e);
|
||||
}
|
||||
finally {
|
||||
if (resp != null) resp.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
return new JSONObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package link.at17.mid.tushare.robot.models;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Data
|
||||
@Accessors(chain=true)
|
||||
@Slf4j
|
||||
public abstract class FeishuAppEnv {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
private String verificationToken;
|
||||
|
||||
private String encryptKey = "";
|
||||
|
||||
private String larkHost;
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient String tenantAccessToken = null;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient int tenantExpire = 0;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient byte[] keyBs;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private transient Request accessTokenRequest;
|
||||
|
||||
private static MessageDigest sha256Digest = null;
|
||||
private static Cipher aesCipher = null;
|
||||
private static final String TENANT_ACCESS_TOKEN_URI = "/open-apis/auth/v3/tenant_access_token/internal";
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@PostConstruct
|
||||
private void init() throws NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
sha256Digest = MessageDigest.getInstance("SHA-256");
|
||||
aesCipher = Cipher.getInstance("AES/CBC/NOPADDING");
|
||||
if (!StringUtils.isEmpty(encryptKey)) {
|
||||
keyBs = sha256Digest.digest(encryptKey.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
accessTokenRequest = new Request.Builder()
|
||||
.url(getLarkHost() + TENANT_ACCESS_TOKEN_URI)
|
||||
.post(
|
||||
RequestBody.create(
|
||||
new JSONObject() {{
|
||||
put("app_id", getAppId());
|
||||
put("app_secret", getAppSecret());
|
||||
}}.toJSONString(),
|
||||
MediaType.parse("application/json; charset=utf-8")))
|
||||
.build();
|
||||
}
|
||||
|
||||
public Optional<String> getTenantAccessToken() {
|
||||
int currTs = (int) (System.currentTimeMillis() / 1000);
|
||||
if (StringUtils.isEmpty(tenantAccessToken) || currTs + 1800 > tenantExpire) {
|
||||
updateTenantAccessToken();
|
||||
}
|
||||
return Optional.ofNullable(tenantAccessToken);
|
||||
}
|
||||
|
||||
private void updateTenantAccessToken() {
|
||||
OkHttpClient okHttpClient = new OkHttpClient();
|
||||
final Call call = okHttpClient.newCall(accessTokenRequest);
|
||||
Response resp = null;
|
||||
try {
|
||||
int tenantExpireTemp = (int) (System.currentTimeMillis() / 1000);
|
||||
resp = call.execute();
|
||||
int status = resp.code();
|
||||
if (status != HttpStatus.OK.value()) {
|
||||
throw new Exception("status code " + status);
|
||||
}
|
||||
JSONObject respJson = JSONObject.parseObject(resp.body().string());
|
||||
int code = respJson.getIntValue("code");
|
||||
if (code != 0) {
|
||||
throw new Exception("code = " + code + ", msg = " + respJson.getString("msg"));
|
||||
}
|
||||
tenantExpire = tenantExpireTemp + respJson.getIntValue("expire");
|
||||
tenantAccessToken = respJson.getString("tenant_access_token");
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot get tenant access token", e);
|
||||
}
|
||||
finally {
|
||||
if (resp != null) resp.close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportDecryption() {
|
||||
return StringUtils.isNotEmpty(encryptKey);
|
||||
}
|
||||
|
||||
public String decrypt(String encryption) throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (!supportDecryption() || StringUtils.isBlank(encryption)) {
|
||||
return encryption;
|
||||
}
|
||||
byte[] decode = Base64.getDecoder().decode(encryption);
|
||||
byte[] iv = new byte[16];
|
||||
System.arraycopy(decode, 0, iv, 0, 16);
|
||||
byte[] data = new byte[decode.length - 16];
|
||||
System.arraycopy(decode, 16, data, 0, data.length);
|
||||
aesCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBs, "AES"), new IvParameterSpec(iv));
|
||||
byte[] r = aesCipher.doFinal(data);
|
||||
if (r.length > 0) {
|
||||
int p = r.length - 1;
|
||||
for (; p >= 0 && r[p] <= 16; p--) {
|
||||
}
|
||||
if (p != r.length - 1) {
|
||||
byte[] rr = new byte[p + 1];
|
||||
System.arraycopy(r, 0, rr, 0, p + 1);
|
||||
r = rr;
|
||||
}
|
||||
}
|
||||
return new String(r, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package link.at17.mid.tushare.robot.models;
|
||||
|
||||
public class FeishuRobotEvent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package link.at17.mid.tushare.robot.models;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "sweet-dog-app-env")
|
||||
public class SweetDogAppEnv extends FeishuAppEnv {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package link.at17.mid.tushare.robot.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class FeishuRobotApiService {
|
||||
|
||||
}
|
||||
425
src/main/java/link/at17/mid/tushare/service/ConfigService.java
Normal file
425
src/main/java/link/at17/mid/tushare/service/ConfigService.java
Normal file
@@ -0,0 +1,425 @@
|
||||
package link.at17.mid.tushare.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.BiMap;
|
||||
import com.google.common.collect.HashBiMap;
|
||||
|
||||
import io.micrometer.core.instrument.util.IOUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import link.at17.mid.tushare.TushareDataServiceApplication;
|
||||
import link.at17.mid.tushare.annotation.ConfigInfo;
|
||||
import link.at17.mid.tushare.interfaces.IConfig;
|
||||
import link.at17.mid.tushare.system.util.SmartResourceResolver;
|
||||
import link.at17.mid.tushare.system.util.SmartViewWriter;
|
||||
import link.at17.mid.tushare.system.util.SpringContextHolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.reflections.Reflections;
|
||||
import org.reflections.scanners.Scanners;
|
||||
import org.reflections.util.ConfigurationBuilder;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 配置类服务
|
||||
*
|
||||
* @author Doghole
|
||||
* @see quant.rich.emoney.config.ConfigAutoRegistrar
|
||||
* @see quant.rich.emoney.config.ConfigServiceFactoryBean
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ConfigService implements InitializingBean {
|
||||
|
||||
@Autowired
|
||||
Reflections reflections;
|
||||
|
||||
static final boolean isJar = "jar".equals(TushareDataServiceApplication.class.getProtectionDomain().getCodeSource().getLocation().getProtocol());
|
||||
static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从
|
||||
* <b><code> field </code></b>
|
||||
* 到<b><code> ConfigClass </code></b>的<b>一对一</b>映射关系<br>
|
||||
* 例: <code>"deviceInfo" <--> DeviceInfoConfig.class</code><br>
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
BiMap<String, Class<? extends IConfig>> fieldToClassCache = HashBiMap.create();
|
||||
|
||||
/**
|
||||
* 从
|
||||
* <b><code>ConfigClass</code></b>
|
||||
* 类到对应实例化对象的映射关系。可以理解为对应类的实例运行时缓存。<p>
|
||||
* <b>注意:当且仅当类被实例化后,该缓存才会被填充!</b>由于配置类是 Bean,其生命周期交由
|
||||
* Spring 框架管理,所以实例化的时机是不确定的。如果在这之前需要遍历类,请使用
|
||||
* fieldToClassCache 或其他方式遍历。
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private Map<Class<? extends IConfig>, Object> classToInstanceCache = new HashMap<>();
|
||||
|
||||
static final Pattern FIELD_PATTERN = Pattern.compile("^[a-zA-Z_$][a-zA-Z0-9_$]+$");
|
||||
|
||||
/**
|
||||
* 初始化各项配置缓存,只有添加了 @ConfigInfo 的配置类才会被识别并缓存
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostConstruct
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
if (reflections == null) {
|
||||
reflections = new Reflections(new ConfigurationBuilder()
|
||||
.addScanners(Scanners.MethodsAnnotated, Scanners.SubTypes, Scanners.TypesAnnotated)
|
||||
.forPackages("link.at17.mid.tushare"));
|
||||
}
|
||||
|
||||
Set<Class<?>> configClasses = reflections.getTypesAnnotatedWith(ConfigInfo.class);
|
||||
|
||||
for (Class<?> clazz : configClasses) {
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
String field = info.field();
|
||||
if (StringUtils.isEmpty(field)) {
|
||||
// 如果 ConfigInfo 注解上 field() 为空,则默认从类名初始化该 field
|
||||
// 规则是该配置类名必须以 Config 结尾,否则不合法
|
||||
field = clazz.getSimpleName();
|
||||
int lastIndexOfConfig = field.lastIndexOf("Config");
|
||||
if (lastIndexOfConfig == -1) {
|
||||
log.warn(
|
||||
"Annotation @ConfigInfo doesn't set a field, "
|
||||
+ "so try to init it from className \"{}\", but "
|
||||
+ "it doesn't end with \"Config\", which is invalid, so ignore.", field);
|
||||
continue;
|
||||
}
|
||||
field = field.substring(0, lastIndexOfConfig);
|
||||
field = firstToLower(field);
|
||||
}
|
||||
else if (!FIELD_PATTERN.matcher(field).matches()) {
|
||||
// field 必须是合法的 Java 变量名
|
||||
log.warn("Invalid field name \"{}\", it cannot be a legal java variable name, so ignore.", field);
|
||||
continue;
|
||||
}
|
||||
if (fieldToClassCache.containsKey(field)) {
|
||||
Class<? extends IConfig<?>> existedClass =
|
||||
(Class<? extends IConfig<?>>) fieldToClassCache.get(field);
|
||||
log.warn("Found class {} with expected name {} which is already assigned to class {}, ignored.",
|
||||
clazz.getName(), field, existedClass.getName());
|
||||
continue;
|
||||
}
|
||||
Class<? extends IConfig<?>> configClass = (Class<? extends IConfig<?>>) clazz;
|
||||
fieldToClassCache.put(field, configClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Config 的类名在缓存字典中取其值,这里偷懒了用了运行时缓存,您也可以根据需要换成 Redis 缓存
|
||||
*
|
||||
* @param <Config> 实现了接口 ConfigInterface 的网站配置类泛型
|
||||
* @param clazz 实现了接口 ConfigInterface 的网站配置类
|
||||
* @return <T>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends IConfig<Config>> Config getCache(Class<Config> clazz) {
|
||||
return (Config) classToInstanceCache.get(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将给定的 Config 的实体类缓存到缓存字典中, 这里偷懒了用了运行时缓存, 您也可以根据需要换成 Redis 缓存
|
||||
*
|
||||
* @param <Config> 实现了接口 ConfigInterface 的网站配置类泛型
|
||||
* @param config 实现了接口 ConfigInterface 的网站配置类的实例化对象
|
||||
* @return
|
||||
*/
|
||||
private <Config extends IConfig<Config>> void setCache(Config config) {
|
||||
classToInstanceCache.put(config.getClass(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注释在继承了 ConfigInterface 上的接口
|
||||
*
|
||||
* @param <Config>
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <Config extends IConfig<Config>> ConfigInfo getConfigInfo(Config config) {
|
||||
return getConfigInfo(config.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注释在继承了 ConfigInterface 上的接口
|
||||
*
|
||||
* @param <Config>
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public static <Config extends IConfig<Config>> ConfigInfo getConfigInfo(Class<Config> clazz) {
|
||||
ConfigInfo info = clazz.getAnnotation(ConfigInfo.class);
|
||||
if (info == null) {
|
||||
throw new RuntimeException("Cannot get config info of " + clazz.toString()
|
||||
+ ", please check if @ConfigInfo annotation is set correctly");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过注解 @ConfigInfo 的 field 获取其本身
|
||||
*
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ConfigInfo getConfigInfoByField(String field) {
|
||||
return getConfigInfo(fieldToClassCache.get(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过注解 @ConfigInfo 的 field 从缓存中获取 Config 的类型
|
||||
*
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends IConfig<T>> Class<T> getConfigClassByField(String field) {
|
||||
return (Class<T>) fieldToClassCache.get(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Config
|
||||
* <p>
|
||||
* 当缓存中有时从缓存取, 缓存没有时从数据库取并更新到缓存, 数据库也没有时, 如果指定的 Config 的 @ConfigInfo
|
||||
* 注解开启了 initDefault = true, 则尝试返回一个初始 Config,否则返回 null
|
||||
*
|
||||
* @param <Config>
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public <Config extends IConfig<Config>> Config getConfig(Class<Config> clazz) {
|
||||
if (classToInstanceCache.containsKey(clazz)) {
|
||||
try {
|
||||
return getCache(clazz);
|
||||
} catch (Exception e) {
|
||||
log.warn("Cannot get config info of " + clazz.toString() + " from cache, try to read from database", e);
|
||||
}
|
||||
}
|
||||
String field = fieldToClassCache.inverse().get(clazz);
|
||||
// 和 doghouse 不同,配置不是从数据库拿的,而是从本地文件系统
|
||||
Config config = getOrCreateConfig(field);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("Cannot get or create config field named " + field);
|
||||
}
|
||||
setCache(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化 Config,同时设置缓存、更新注入
|
||||
*
|
||||
* @param <Config>
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@ManagedAttribute
|
||||
public <Config extends IConfig<Config>> boolean saveOrUpdate(Config config) {
|
||||
config.beforeSaving();
|
||||
String field = fieldToClassCache.inverse().get(config.getClass());
|
||||
ConfigInfo info = getConfigInfoByField(field);
|
||||
|
||||
SmartViewWriter writer = new SmartViewWriter();
|
||||
String configJoString = writer.writeWithSmartView(config, IConfig.Views.Persistence.class);
|
||||
|
||||
if (info.save()) {
|
||||
try {
|
||||
String filePath = getConfigFilePath(field, false);
|
||||
SmartResourceResolver.saveText(filePath, configJoString);
|
||||
//Path dirPath = Paths.get(filePath).getParent();
|
||||
//if (Files.notExists(dirPath)) {
|
||||
// Files.createDirectories(dirPath);
|
||||
//}
|
||||
//Files.writeString(Path.of(filePath), configJoString);
|
||||
} catch (IOException e) {
|
||||
log.error("Cannot write config to local file {}.json, error: {}", field, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
config.afterSaving();
|
||||
setCache(config);
|
||||
SpringContextHolder.updateBean(field + "Config", config);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定路径获取配置文件并转换为实例对象
|
||||
* @param <Config>
|
||||
* @param path
|
||||
* @param configClass
|
||||
* @return
|
||||
*/
|
||||
private <Config extends IConfig<Config>> Config getFromFile(String path, Class<Config> configClass) {
|
||||
String configString;
|
||||
Config config = null;
|
||||
|
||||
try {
|
||||
// 此处只是读取文件,并不关心该文件是否可写
|
||||
configString = IOUtils.toString(SmartResourceResolver.loadResource(path), Charset.defaultCharset());
|
||||
} catch (IOException e) {
|
||||
String field = fieldToClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot read config {}.json: {}", field, e.getMessage());
|
||||
return config;
|
||||
}
|
||||
try {
|
||||
config = mapper.readValue(configString, configClass);
|
||||
} catch (Exception e) {
|
||||
String field = fieldToClassCache.inverse().get(configClass);
|
||||
log.warn("Cannot parse configString of {} to Config", field);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 config field 获取 Config,将读取本地存储的配置,若失败则试图初始化并存储
|
||||
*
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends IConfig<Config>> Config getOrCreateConfig(String field) {
|
||||
|
||||
Class<Config> configClass = (Class<Config>) fieldToClassCache.get(field);
|
||||
if (configClass == null) {
|
||||
log.warn("Cannot get class info from fieldClassCache, field name: {}", field);
|
||||
return null;
|
||||
}
|
||||
ConfigInfo info = getConfigInfo(configClass);
|
||||
|
||||
Config config = null;
|
||||
|
||||
// 先从持久化路径拿
|
||||
// fallback 流程:如果持久化文件不存在或者从中反序列化失败,则走 fallback 流程
|
||||
// 如果 fallback 也失败,走初始化流程。但是初始化出的内容不参与 fallback 的持久化
|
||||
// 也就是无论如何,fallback 都不应由程序来写入
|
||||
String filePath = getConfigFilePath(field, false);
|
||||
config = getFromFile(filePath, configClass);
|
||||
if (config == null) {
|
||||
log.info("Cannot init config from local file of {}Config, try fallback", field);
|
||||
// 走 fallback 流程
|
||||
config = getFromFile(getConfigFilePath(field, true), configClass);
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.info("Cannot init config from fallback file of {}Config", field);
|
||||
if (info.initDefault()) {
|
||||
// 还为 null 且允许初始化默认值,则初始化一份
|
||||
log.info("Init {} by default non-args constructor", field);
|
||||
try {
|
||||
config =
|
||||
configClass.getDeclaredConstructor()
|
||||
.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
// 一般是初始化方法内出现未被捕获的错误
|
||||
log.warn("Specific class " + configClass.toString()
|
||||
+ " has @ConfigInfo annotation and enables initDefault, but init failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.warn("Cannot read or init from default/fallback for config {}Config, it will be null", field);
|
||||
}
|
||||
else {
|
||||
saveOrUpdate(config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 config field 生成配置文件路径
|
||||
*
|
||||
* @param configField
|
||||
* @return
|
||||
*/
|
||||
private static String getConfigFilePath(String field, boolean isFallback) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("./conf/system/");
|
||||
sb.append(field);
|
||||
sb.append(isFallback ? ".fallback.json" : ".json");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按默认顺序列出所有已加载 config 的 ConfigInfo,可以用在前端菜单枚举
|
||||
*
|
||||
* @param onlyManaged 是否只列举出 {@code @ConfigInfo(..., managed=true)} 的内容, {@code false} 则全部列出
|
||||
* @return {@code List<ConfigInfo>}
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public List<ConfigInfo> getConfigInfoList(boolean onlyManaged) {
|
||||
List<ConfigInfo> configInfos = new ArrayList<>();
|
||||
|
||||
for (Class<? extends IConfig> configClazz : fieldToClassCache.values()) {
|
||||
ConfigInfo info = getConfigInfo(configClazz);
|
||||
if (onlyManaged && info.managed()) {
|
||||
configInfos.add(info);
|
||||
}
|
||||
else if (!onlyManaged) {
|
||||
configInfos.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(configInfos, CONFIG_INFO_SORTER);
|
||||
|
||||
return configInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigInfo 比较器
|
||||
*/
|
||||
private static class ConfigInfoSorter implements Comparator<ConfigInfo> {
|
||||
|
||||
@Override
|
||||
public int compare(ConfigInfo o1, ConfigInfo o2) {
|
||||
return Integer.compare(o1.order(), o2.order());
|
||||
}
|
||||
}
|
||||
|
||||
private static final ConfigInfoSorter CONFIG_INFO_SORTER = new ConfigInfoSorter();
|
||||
|
||||
/**
|
||||
* 首字母小写
|
||||
*
|
||||
* @param s
|
||||
* @return
|
||||
*/
|
||||
private static String firstToLower(String s) {
|
||||
if (StringUtils.isNotEmpty(s)) {
|
||||
char[] cs = s.toCharArray();
|
||||
if (cs[0] >= 'A' && cs[0] <= 'Z') {
|
||||
cs[0] += 32;
|
||||
}
|
||||
return String.valueOf(cs);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package link.at17.mid.tushare.system.config;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import link.at17.mid.tushare.annotation.ConfigInfo;
|
||||
import link.at17.mid.tushare.annotation.StaticAttribute;
|
||||
import link.at17.mid.tushare.interfaces.IConfig;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@ConfigInfo(field = "system", name = "平台设置", initDefault = true)
|
||||
@Lazy(false)
|
||||
public class SystemConfig implements IConfig<SystemConfig> {
|
||||
|
||||
private String tushareToken;
|
||||
|
||||
@StaticAttribute("ProxyTypeEnum")
|
||||
private Proxy.Type proxyType = Proxy.Type.DIRECT;
|
||||
|
||||
private String proxyHost = "";
|
||||
|
||||
private Integer proxyPort = 1;
|
||||
|
||||
private Boolean ignoreHttpsVerification = false;
|
||||
|
||||
|
||||
public SystemConfig() {
|
||||
tushareToken = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置获取代理
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public Proxy getProxy () {
|
||||
if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) {
|
||||
return new Proxy(getProxyType(),
|
||||
new InetSocketAddress(getProxyHost(), getProxyPort()));
|
||||
}
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置获取代理 Url
|
||||
* @return
|
||||
*/
|
||||
public String getProxyUrl() {
|
||||
if (ObjectUtils.anyNull(getProxyType(), getProxyHost(), getProxyPort())) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (getProxyType() == Proxy.Type.SOCKS) {
|
||||
sb.append("socks5://");
|
||||
}
|
||||
else if (getProxyType() == Proxy.Type.HTTP) {
|
||||
sb.append("http://");
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
sb.append(getProxyHost()).append(':').append(getProxyPort());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user