Browse Source

feat(项目初始化):

autumnal_wind 2 months ago
commit
32d60bc74c

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+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/

+ 19 - 0
.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,19 @@
+# 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
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip

+ 1 - 0
ArcFacePro64.dat

@@ -0,0 +1 @@
+EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTICAVBKEOARCYHKADCIASHXGBJJALDMBVGLBGHOGQCZDYHCATHIAUFJFLCCAEGLIHIYGUGPHTFLAOEEAIHDCMBHALACFNINGMIXGJIMFLEGGVBYJODSEKBYJFGUJGBDILJHDBGKEAGZFXDMARCJILGBFJCZBJBIFJBYIBBIAQHDIZBCIDDRIUCFDFFPIZGYGTFZJCHZGQISEZANCOEIEHGUFYDWGNHTEJBEJRJFEZGPEUGNJOFMFBEJIVCHAWBGHKDMABHCBSFGDDFBJMILBAHFHJAZCWDMHBBRBLEKFSIPHYADDCFWGVFGDNELHVFQHDHGFBAVIBFFBCCQHJJVAZFDDNICHEHRFEFTDJDJDTIBCZBJJPIYDMHOIHFUFRBZASAWIECICKAKEAHKJKBLFUCGHBEGHLILBRAGGCFKEOJJIMCWJUECDMDSBJCZDEDZHOBJDDEPJKBKIEBRARBEIXGKBUIIHHEKCPBLDYBRCZGXCLBPEYHNIBHVEAGAASBFEYBMERCLBEFFBGASHNAZANFFIHHGCUCJDICTCRBGJMDDEWIZDDDVHKFPBNJIBWHWIUIVFSBJGHEEHVJIIQGQFDAKHLATAHDKFLCEIZIPHVHUEAEYFZDLEQHFDNFZGDCNIBFMICHXEZGIGJDEIZDBHAJEBEFPFBHJAQGWJMJMHPHLCEESADGRDEFBDOBKFAFRIGHRFNCSIWICIRDHGUEHGIIIETDZGRCCIKAQJEBKCNCPERIVIVEQAVCZCBBWCZJQHEJFCDJIFLHFHJDHAVGUCFJMEMAQDKEWBSBQCPEHCHEBDRDNFXJOIJIDEADGDTDNHCCBIJFLBOHWDIHHICDYHIBBEWCEGNCTDVELHZCTAXCLBRAMEICCJSEIHECWCMDSDGGAHVAMJUGDABIVEOBHGBHSIIJRHCEHJQAVEVHGACALFYFGDCAYGWHKHSHVHLHRBNGIFKAREHBEJHHMIOCSDGFYARATBXABESDEHPEVJSIGHHFTFAFBDZDHGZHQEECHETGTGYDVCNBFFVHDIRGZCBBGCFEPENHXFVDVCUFEBODHAEDKEZAQAXGYGNGKJNJSASAYGBEOHSJVHMDOIDDOGSJOHDGSJIJAAE

+ 259 - 0
mvnw

@@ -0,0 +1,259 @@
+#!/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
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+#   JAVA_HOME - location of a JDK home dir, required when download maven via java source
+#   MVNW_REPOURL - repo url base for downloading maven distribution
+#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+  [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+  native_path() { cygpath --path --windows "$1"; }
+  ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+  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"
+      JAVACCMD="$JAVA_HOME/jre/sh/javac"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+      JAVACCMD="$JAVA_HOME/bin/javac"
+
+      if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+        echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+        echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+        return 1
+      fi
+    fi
+  else
+    JAVACMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v java
+    )" || :
+    JAVACCMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v javac
+    )" || :
+
+    if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+      echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+      return 1
+    fi
+  fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+  str="${1:-}" h=0
+  while [ -n "$str" ]; do
+    char="${str%"${str#?}"}"
+    h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+    str="${str#?}"
+  done
+  printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+  printf %s\\n "$1" >&2
+  exit 1
+}
+
+trim() {
+  # MWRAPPER-139:
+  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+  #   Needed for removing poorly interpreted newline sequences when running in more
+  #   exotic environments such as mingw bash on Windows.
+  printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+  case "${key-}" in
+  distributionUrl) distributionUrl=$(trim "${value-}") ;;
+  distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+  esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+  case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+  *)
+    echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+    distributionPlatform=linux-amd64
+    ;;
+  esac
+  distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+  ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+  exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+  verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+  clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+  trap clean HUP INT TERM EXIT
+else
+  die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+  distributionUrl="${distributionUrl%.zip}.tar.gz"
+  distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+  verbose "Found wget ... using wget"
+  wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+  verbose "Found curl ... using curl"
+  curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+  verbose "Falling back to use Java to download"
+  javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+  targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+  cat >"$javaSource" <<-END
+	public class Downloader extends java.net.Authenticator
+	{
+	  protected java.net.PasswordAuthentication getPasswordAuthentication()
+	  {
+	    return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+	  }
+	  public static void main( String[] args ) throws Exception
+	  {
+	    setDefault( new Downloader() );
+	    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+	  }
+	}
+	END
+  # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+  verbose " - Compiling Downloader.java ..."
+  "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+  verbose " - Running Downloader.java ..."
+  "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+  distributionSha256Result=false
+  if [ "$MVN_CMD" = mvnd.sh ]; then
+    echo "Checksum validation is not supported for maven-mvnd." >&2
+    echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  elif command -v sha256sum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  elif command -v shasum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  else
+    echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+    echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  fi
+  if [ $distributionSha256Result = false ]; then
+    echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+    echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+    exit 1
+  fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+  unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+  tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"

+ 149 - 0
mvnw.cmd

@@ -0,0 +1,149 @@
+<# : batch portion
+@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    http://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.3.2
+@REM
+@REM Optional ENV vars
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

+ 101 - 0
pom.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.2.6</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.dt</groupId>
+    <artifactId>ykt-face</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>ykt-face</name>
+    <description>ykt-face</description>
+    <properties>
+        <java.version>17</java.version>
+        <hutool.version>5.8.27</hutool.version>
+        <org.apache.commons.version>3.18.0</org.apache.commons.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                    <groupId>org.springframework.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- web 容器使用 undertow 性能更强 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-undertow</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.arcsoft.face</groupId>
+            <artifactId>arcsoft-sdk-face</artifactId>
+            <version>4.1.1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-json</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>${org.apache.commons.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+    <repositories>
+        <repository>
+            <id>public</id>
+            <name>huawei nexus</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+    </repositories>
+</project>

+ 13 - 0
src/main/java/com/dt/ykt/face/YktFaceApplication.java

@@ -0,0 +1,13 @@
+package com.dt.ykt.face;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class YktFaceApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(YktFaceApplication.class, args);
+    }
+
+}

+ 100 - 0
src/main/java/com/dt/ykt/face/business/FaceBusiness.java

@@ -0,0 +1,100 @@
+package com.dt.ykt.face.business;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ObjectUtil;
+import com.dt.ykt.face.common.domain.R;
+import com.dt.ykt.face.service.IFaceAlgorithm;
+import com.dt.ykt.face.utils.FileUtils;
+import com.dt.ykt.face.utils.SpringUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+
+/**
+ * name: FaceBusiness
+ * package: com.dt.ykt.face.business
+ * description: 人脸业务处理类
+ * date: 2025-08-07 09:51:07 09:51
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class FaceBusiness {
+    // 文件上传路径
+    @Value("${upload.upload-path}/")
+    private String uploadPath;
+    // 用户头像路径
+    @Value("${upload.image.user}/")
+    private String userPath;
+    /**
+     * 提取指定图片的人脸特征码
+     * 1.根据照片数据进行人脸识别与特征码提取
+     * 2.根据照片数据生成图片片在本地硬盘存储
+     *
+     * @param userId 图片标识(用户Id)
+     * @param imageData     用户图片数据
+     * @param faceAlgorithm 使用的人脸算法
+     * @return 成功时返回提取到的特征码数据,失败时返回相应错误
+     */
+    public R<Void> extractUserFaces(String userId, String imageData, String faceAlgorithm) {
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[照片处理失败]-[无图片数据]");
+        }
+        // 提取特征码
+        String faceAlgorithmName = faceAlgorithm + "_algorithm";
+        IFaceAlgorithm useFaceAlgorithm = SpringUtils.getBean(faceAlgorithmName, IFaceAlgorithm.class);
+        R<String> createResult = useFaceAlgorithm.extractFeatures(imageData);
+        if (R.isError(createResult)) {
+            return R.fail(createResult.getMsg());
+        }
+        // 保存文件到物理路径
+        R<Void> result = this.saveImageData(Long.valueOf(userId), imageData);
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        return R.ok(createResult.getData());
+    }
+    /**
+     * 保存人脸照片到物理硬盘
+     *
+     * @param userId    用户Id
+     * @param imageData 用户图片数据
+     * @return 保存结果
+     */
+    public R<Void> saveImageData(Long userId, String imageData) {
+        try {
+            // 对图片进行压缩处理
+            byte[] imageBytes = Base64.decode(imageData);
+            imageBytes = FileUtils.imgCompression(imageBytes);
+
+            // 保存图片到物理目录
+            String fileName = uploadPath + userPath + userId + ".jpg";
+            FileUtils.saveFileToDisk(fileName, imageBytes);
+            return R.ok();
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 检测图片是否达到人脸识别的要求.
+     * @param userId 图片标识
+     * @param imageData 图片数据
+     * @param faceAlgorithm 人脸识别算法
+     * @return 检查结果
+     */
+    public R<Void> detectFaces(String userId, String imageData, String faceAlgorithm) {
+        String faceAlgorithmName = faceAlgorithm + "_algorithm";
+        IFaceAlgorithm useFaceAlgorithm = SpringUtils.getBean(faceAlgorithmName, IFaceAlgorithm.class);
+
+        return useFaceAlgorithm.detectFaces(imageData);
+    }
+}

+ 27 - 0
src/main/java/com/dt/ykt/face/common/constant/AlgorithmConstants.java

@@ -0,0 +1,27 @@
+package com.dt.ykt.face.common.constant;
+
+/**
+ * 算法常量定义
+ * 本接口用于存放算法相关的常量字符串,便于统一管理和维护。
+ *
+ * @author luoyibo
+ * @date 2025-07-13
+ * @since JDK17
+ */
+public interface AlgorithmConstants {
+
+    /**
+     * 虹软人脸
+     */
+    String ARC_FACE = "arc_face";
+
+    /**
+     * 虹软人脸算法
+     */
+    String ARC_FACE_ALGORITHM = "arc_face_algorithm";
+
+    /**
+     * 虹软人脸数据转换
+     */
+    String ARC_FACE_CONVERT = "arc_face_convert";
+}

+ 93 - 0
src/main/java/com/dt/ykt/face/common/constant/HttpStatus.java

@@ -0,0 +1,93 @@
+package com.dt.ykt.face.common.constant;
+
+/**
+ * 返回状态码
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+    /**
+     * 操作成功
+     */
+    int SUCCESS = 200;
+
+    /**
+     * 对象创建成功
+     */
+    int CREATED = 201;
+
+    /**
+     * 请求已经被接受
+     */
+    int ACCEPTED = 202;
+
+    /**
+     * 操作已经执行成功,但是没有返回数据
+     */
+    int NO_CONTENT = 204;
+
+    /**
+     * 资源已被移除
+     */
+    int MOVED_PERM = 301;
+
+    /**
+     * 重定向
+     */
+    int SEE_OTHER = 303;
+
+    /**
+     * 资源没有被修改
+     */
+    int NOT_MODIFIED = 304;
+
+    /**
+     * 参数列表错误(缺少,格式不匹配)
+     */
+    int BAD_REQUEST = 400;
+
+    /**
+     * 未授权
+     */
+    int UNAUTHORIZED = 401;
+
+    /**
+     * 访问受限,授权过期
+     */
+    int FORBIDDEN = 403;
+
+    /**
+     * 资源,服务未找到
+     */
+    int NOT_FOUND = 404;
+
+    /**
+     * 不允许的http方法
+     */
+    int BAD_METHOD = 405;
+
+    /**
+     * 资源冲突,或者资源被锁
+     */
+    int CONFLICT = 409;
+
+    /**
+     * 不支持的数据,媒体类型
+     */
+    int UNSUPPORTED_TYPE = 415;
+
+    /**
+     * 系统内部错误
+     */
+    int ERROR = 500;
+
+    /**
+     * 接口未实现
+     */
+    int NOT_IMPLEMENTED = 501;
+
+    /**
+     * 系统警告消息
+     */
+    int WARN = 601;
+}

+ 120 - 0
src/main/java/com/dt/ykt/face/common/domain/R.java

@@ -0,0 +1,120 @@
+package com.dt.ykt.face.common.domain;
+
+import com.dt.ykt.face.common.constant.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R<T> implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 成功
+     */
+    public static final int SUCCESS = 200;
+
+    /**
+     * 失败
+     */
+    public static final int FAIL = 500;
+
+    /**
+     * 消息状态码
+     */
+    private int code;
+
+    /**
+     * 消息内容
+     */
+    private String msg;
+
+    /**
+     * 数据对象
+     */
+    private T data;
+
+    public static <T> R<T> ok() {
+        return restResult(null, SUCCESS, "操作成功");
+    }
+
+    public static <T> R<T> ok(T data) {
+        return restResult(data, SUCCESS, "操作成功");
+    }
+
+    public static <T> R<T> ok(String msg) {
+        return restResult(null, SUCCESS, msg);
+    }
+
+    public static <T> R<T> ok(String msg, T data) {
+        return restResult(data, SUCCESS, msg);
+    }
+
+    public static <T> R<T> fail() {
+        return restResult(null, FAIL, "操作失败");
+    }
+
+    public static <T> R<T> fail(String msg) {
+        return restResult(null, FAIL, msg);
+    }
+
+    public static <T> R<T> fail(T data) {
+        return restResult(data, FAIL, "操作失败");
+    }
+
+    public static <T> R<T> fail(String msg, T data) {
+        return restResult(data, FAIL, msg);
+    }
+
+    public static <T> R<T> fail(int code, String msg) {
+        return restResult(null, code, msg);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @return 警告消息
+     */
+    public static <T> R<T> warn(String msg) {
+        return restResult(null, HttpStatus.WARN, msg);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @param data 数据对象
+     * @return 警告消息
+     */
+    public static <T> R<T> warn(String msg, T data) {
+        return restResult(data, HttpStatus.WARN, msg);
+    }
+
+    private static <T> R<T> restResult(T data, int code, String msg) {
+        R<T> r = new R<>();
+        r.setCode(code);
+        r.setData(data);
+        r.setMsg(msg);
+        return r;
+    }
+
+    public static <T> Boolean isError(R<T> ret) {
+        return !isSuccess(ret);
+    }
+
+    public static <T> Boolean isSuccess(R<T> ret) {
+        return R.SUCCESS == ret.getCode();
+    }
+
+}

+ 28 - 0
src/main/java/com/dt/ykt/face/config/arcface/ArcFaceConfig.java

@@ -0,0 +1,28 @@
+package com.dt.ykt.face.config.arcface;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 虹软人脸算法配置
+ * 本接口用于存放算法相关的常量字符串,便于统一管理和维护。
+ *
+ * @author luoyibo
+ * @date 2025-07-13
+ * @since JDK17
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "face.arc")
+public class ArcFaceConfig {
+    // 获取配置参数
+    private String sdkLibPath = "";
+    private String appId = "";
+    private String sdkKey = "";
+    private String activeKey = "";
+    private Integer detectPooSize = 5;
+    private Integer comparePooSize = 5;
+    private Float faceQuality = 0.6F;
+    private boolean faceMask = false;
+}

+ 74 - 0
src/main/java/com/dt/ykt/face/config/arcface/ArcFaceEngineFactory.java

@@ -0,0 +1,74 @@
+package com.dt.ykt.face.config.arcface;
+
+import com.arcsoft.face.EngineConfiguration;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.enums.ErrorInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+
+import java.util.concurrent.ExecutionException;
+
+
+/**
+ * Factory class for creating and managing instances of {@link FaceEngine} using a pool.
+ * This factory is responsible for the creation, initialization, and destruction of {@link FaceEngine} instances,
+ * ensuring that each engine is properly activated and initialized before being used.
+ */
+@Slf4j
+public class ArcFaceEngineFactory extends BasePooledObjectFactory<FaceEngine> {
+    private final String libPath;
+    private final String appId;
+    private final String sdkKey;
+    private final String activeKey;
+    private final EngineConfiguration engineConfiguration;
+
+
+    public ArcFaceEngineFactory(String libPath, String appId, String sdkKey, String activeKey,
+                                EngineConfiguration engineConfiguration) {
+        this.appId = appId;
+        this.sdkKey = sdkKey;
+        this.activeKey = activeKey;
+        this.libPath = libPath;
+        this.engineConfiguration = engineConfiguration;
+    }
+
+    @Override
+    public FaceEngine create() throws Exception {
+        String message;
+        FaceEngine faceEngine = new FaceEngine(this.libPath);
+        try {
+            // sdk4.0以上的激活方式
+            int activeCode = faceEngine.activeOnline(this.appId, this.sdkKey, this.activeKey);
+            if (activeCode != ErrorInfo.MOK.getValue() && activeCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
+                message = String.format("[人脸识别引擎激活]-[%s]", ArcResultCodeEnum.getMessage(activeCode));
+                log.error(message);
+                throw new Exception(message);
+                // throw new (message);
+            }
+            int initCode = faceEngine.init(this.engineConfiguration);
+            if (initCode != ErrorInfo.MOK.getValue()) {
+                message = String.format("[人脸识别引擎初始化]-[%s]", ArcResultCodeEnum.getMessage(initCode));
+                log.error(message);
+                throw new Exception(message);
+            }
+            return faceEngine;
+        } catch (Exception e) {
+            log.error("人脸识别引擎激活失败",e);
+            return null;
+        }
+    }
+
+    @Override
+    public PooledObject<FaceEngine> wrap(FaceEngine faceEngine) {
+        return new DefaultPooledObject<>(faceEngine);
+    }
+
+    @Override
+    public void destroyObject(PooledObject<FaceEngine> pool) throws Exception {
+        FaceEngine faceEngine = pool.getObject();
+        faceEngine.unInit();
+        super.destroyObject(pool);
+    }
+}

+ 60 - 0
src/main/java/com/dt/ykt/face/config/arcface/ArcResultCodeEnum.java

@@ -0,0 +1,60 @@
+package com.dt.ykt.face.config.arcface;
+
+/**
+ * 虹软人脸算结果码法枚举
+ */
+public enum ArcResultCodeEnum {
+    MOK(0, "成功"),
+    MERR_UNKNOWN(1, "错误原因不明"),
+    MERR_INVALID_PARAM(2, "无效的参数"),
+    MERR_UNSUPPORTED(3, "引擎不支持"),
+    MERR_FSDK_FR_INVALID_IMAGE_INFO(73730, "无效的输入图像参数"),
+    MERR_FSDK_FR_INVALID_FACE_INFO(73731, "无效的脸部信息"),
+    MERR_ASF_EX_INVALID_IMAGE_INFO(86021, "无效的输入图像"),
+    MERR_FSDK_MISMATCH_ID_AND_SDK(28676,"SDK_KEY和使用的SDK不匹配,请检查入参"),
+    MERR_FSDK_SYSTEM_VERSION_UNSUPPORTED(28677,"S系统版本不被当前SDK所支持"),
+    MERR_FSDK_LICENCE_EXPIRED(28678,"SDK有效期过期,需要重新下载更新"),
+    MERR_ACTIVEKEY_EXPIRED(98313,"ACTIVEKEY已过期"),
+    MERR_ASF_EX_INVALID_FACE_INFO(86022, "无效的脸部信息");
+
+
+    private final Integer code;
+    private final String message;
+
+    ArcResultCodeEnum(Integer code, String name) {
+        this.code = code;
+        this.message = name;
+    }
+
+    public static String getMessage(String name) {
+        for (ArcResultCodeEnum item : ArcResultCodeEnum.values()) {
+            if (item.name().equals(name)) {
+                return item.message;
+            }
+        }
+        return name;
+    }
+
+    public static String getMessage(Integer code) {
+        for (ArcResultCodeEnum item : ArcResultCodeEnum.values()) {
+            if (item.code().equals(code)) {
+                return item.message;
+            }
+        }
+        return "未知";
+    }
+
+    public Integer code() {
+        return this.code;
+    }
+
+    public String message() {
+        return this.message;
+    }
+
+    @Override
+    public String toString() {
+        return this.name();
+    }
+
+}

+ 80 - 0
src/main/java/com/dt/ykt/face/controller/FaceController.java

@@ -0,0 +1,80 @@
+package com.dt.ykt.face.controller;
+
+import cn.hutool.core.util.ObjUtil;
+import com.dt.ykt.face.business.FaceBusiness;
+import com.dt.ykt.face.common.domain.R;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * name: FaceCountroller
+ * package: com.dt.ykt.face.controller
+ * description: 人脸识别控制器
+ * date: 2025-08-07 09:31:40 09:31
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@RestController("FaceController")
+@RequestMapping("/face")
+@Slf4j
+@RequiredArgsConstructor
+public class FaceController {
+    private final FaceBusiness faceBusiness;
+    /**
+     * 提供给外部进行人脸识别与特征码提取接口
+     *
+     * @param mapInfo 参数map,userId和imageData,imageData去掉了前面的base64标识
+     * @return 操作结果
+     */
+    @RequestMapping(value = "/api/v1/feature", method = RequestMethod.POST)
+    public R<Void> extractFeatures(@RequestBody Map<String, String> mapInfo) {
+        String userId = mapInfo.get("userId");
+        String imageData = mapInfo.get("imageData");
+        String faceAlgorithm = mapInfo.get("faceAlgorithm");
+
+        R<Void> result = this.cheParams(userId, imageData, faceAlgorithm);
+        if (R.isError(result)) {
+            return result;
+        }
+
+        return faceBusiness.extractUserFaces(userId, imageData, faceAlgorithm);
+    }
+    /**
+     * 提供给外部进行人脸识别与特征码提取接口
+     *
+     * @param mapInfo 参数map,userId和imageData,imageData去掉了前面的base64标识
+     * @return 操作结果
+     */
+    @RequestMapping(value = "/api/v1/detect", method = RequestMethod.POST)
+    public R<Void> detectFaces(@RequestBody Map<String, String> mapInfo) {
+        String userId = mapInfo.get("userId");
+        String imageData = mapInfo.get("imageData");
+        String faceAlgorithm = mapInfo.get("faceAlgorithm");
+        R<Void> result = this.cheParams(userId, imageData, faceAlgorithm);
+        if (R.isError(result)) {
+            return result;
+        }
+        return faceBusiness.detectFaces(userId, imageData, faceAlgorithm);
+    }
+
+    private R<Void> cheParams(String userId,String imageData, String faceAlgorithm){
+        if (ObjUtil.isEmpty(userId)) {
+            return R.fail("图片标识不能为空");
+        }
+        if (ObjUtil.isEmpty(imageData)) {
+            return R.fail("图片数据不能为空");
+        }
+        if (ObjUtil.isEmpty(faceAlgorithm)) {
+            return R.fail("人脸算法名称不能为空");
+        }
+        return R.ok();
+    }
+}

+ 61 - 0
src/main/java/com/dt/ykt/face/exception/ServiceException.java

@@ -0,0 +1,61 @@
+package com.dt.ykt.face.exception;
+
+import lombok.*;
+
+import java.io.Serial;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public final class ServiceException extends RuntimeException {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    @Getter
+    private Integer code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 错误明细,内部调试错误
+     */
+    @Getter
+    private String detailMessage;
+
+    public ServiceException(String message) {
+        this.message = message;
+    }
+
+    public ServiceException(String message, Integer code) {
+        this.message = message;
+        this.code = code;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+    public ServiceException setDetailMessage(String detailMessage) {
+        this.detailMessage = detailMessage;
+        return this;
+    }
+}

+ 31 - 0
src/main/java/com/dt/ykt/face/service/IFaceAlgorithm.java

@@ -0,0 +1,31 @@
+package com.dt.ykt.face.service;
+
+
+import com.dt.ykt.face.common.domain.R;
+
+/**
+ * Defines the contract for a face recognition algorithm.
+ * Implementations of this interface are expected to provide methods for
+ * detecting, recognizing, and processing faces in images or video streams.
+ * This interface serves as a base for various face recognition algorithms,
+ * allowing for flexibility and extensibility in different applications.
+ */
+public interface IFaceAlgorithm {
+
+    /**
+     * Detects faces in the provided image data.
+     *
+     * @param imageData the base64 encoded string of the image to be processed
+     * @return a response object indicating the success or failure of the face detection operation
+     */
+    R<Void> detectFaces(String imageData);
+
+    /**
+     * Extracts facial features from the provided image data.
+     *
+     * @param imageData the base64 encoded string of the image to be processed
+     * @return a response object indicating the success or failure of the feature extraction operation
+     */
+    R<String> extractFeatures(String imageData);
+
+}

+ 186 - 0
src/main/java/com/dt/ykt/face/service/iml/ArcFaceAlgorithmIml.java

@@ -0,0 +1,186 @@
+package com.dt.ykt.face.service.iml;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ObjectUtil;
+import com.arcsoft.face.*;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.DetectOrient;
+import com.arcsoft.face.enums.ExtractType;
+import com.arcsoft.face.toolkit.ImageInfo;
+import com.dt.ykt.face.common.constant.AlgorithmConstants;
+import com.dt.ykt.face.common.domain.R;
+import com.dt.ykt.face.config.arcface.ArcFaceConfig;
+import com.dt.ykt.face.config.arcface.ArcFaceEngineFactory;
+import com.dt.ykt.face.config.arcface.ArcResultCodeEnum;
+import com.dt.ykt.face.service.IFaceAlgorithm;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.arcsoft.face.toolkit.ImageFactory.getRGBData;
+
+/**
+ * name: ArcFaceAlgorithmIml
+ * package: com.dt.ykt.face.service.iml
+ * description: 虹软人脸算法实现
+ * date: 2025-08-07 09:42:42 09:42
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service(AlgorithmConstants.ARC_FACE_ALGORITHM)
+public class ArcFaceAlgorithmIml implements IFaceAlgorithm {
+    private final ArcFaceConfig arcFaceConfig;
+    private GenericObjectPool<FaceEngine> faceEngineGeneralPool;
+
+    /**
+     * 获取引擎配置
+     *
+     * @return 引擎配置
+     */
+    private static EngineConfiguration getEngineConfiguration() {
+        FunctionConfiguration detectFunctionCfg = new FunctionConfiguration();
+        detectFunctionCfg.setSupportFaceDetect(true);
+        detectFunctionCfg.setSupportFaceRecognition(true);
+        detectFunctionCfg.setSupportImageQuality(true);
+        detectFunctionCfg.setSupportLiveness(true);
+        detectFunctionCfg.setSupportIRLiveness(true);
+        detectFunctionCfg.setSupportMaskDetect(true);
+
+        EngineConfiguration detectCfg = new EngineConfiguration();
+        detectCfg.setFunctionConfiguration(detectFunctionCfg);
+        // IMAGE检测模式,用于处理单张的图像数据
+        detectCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
+        // 人脸检测角度,逆时针0度
+        detectCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);
+        detectCfg.setDetectFaceMaxNum(10);
+
+        return detectCfg;
+    }
+
+    /**
+     * 人脸识别
+     * @param imageData base64编码的人脸照片数据
+     * @return 识别结果
+     */
+    @Override
+    public R<Void> detectFaces(String imageData) {
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[图片人脸识别失败]-[无图片信息,请上传图片]");
+        }
+        byte[] imageBytes = Base64.decode(imageData);
+        ImageInfo imageInfo = getRGBData(imageBytes);
+        if (ObjectUtil.isEmpty(imageInfo)) {
+            return R.fail("[图片人脸识别失败]-[无效图片信息,请重新上传图片]");
+        }
+        FaceEngine faceEngine = null;
+        List<FaceInfo> faceInfoList = new ArrayList<>();
+        try {
+            faceEngine = faceEngineGeneralPool.borrowObject();
+            if (ObjectUtil.isEmpty(faceEngine)) {
+                return R.fail("[图片人脸识别失败]-[人脸识别引擎创建错误]");
+            }
+
+            int resultCode = faceEngine.detectFaces(imageInfo, faceInfoList);
+            if (resultCode == ArcResultCodeEnum.MOK.code()) {
+                return R.ok();
+            } else {
+                return R.fail(String.format("[人脸识别失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail("[图片人脸识别失败]-[服务响应异常]");
+        } finally {
+            // 释放人脸引擎
+            if (ObjectUtil.isNotEmpty(faceEngine)) {
+                faceEngineGeneralPool.returnObject(faceEngine);
+            }
+        }
+    }
+
+    /**
+     * 人脸特征提取
+     *
+     * @param imageData base64编码的人脸照片数据
+     * @return 提取的人脸特征串
+     */
+    @Override
+    public R<String> extractFeatures(String imageData) {
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[人脸特征码提取失败]-[无图片信息,请上传图片]");
+        }
+        byte[] imageBytes = Base64.decode(imageData);
+        ImageInfo imageInfo = getRGBData(imageBytes);
+        if (ObjectUtil.isEmpty(imageInfo)) {
+            return R.fail("[人脸特征码提取失败]-[无效图片信息,请重新上传图片]");
+        }
+
+        FaceEngine faceEngine = null;
+        List<FaceInfo> faceInfoList = new ArrayList<>();
+        try {
+            // 创建人脸引擎
+            faceEngine = faceEngineGeneralPool.borrowObject();
+            if (ObjectUtil.isEmpty(faceEngine)) {
+                return R.fail("[人脸特征码提取失败]-[人脸识别引擎创建错误]");
+            }
+            // 检测图片是否满足人脸识别要求
+            int resultCode = faceEngine.detectFaces(imageInfo, faceInfoList);
+            if (resultCode != ArcResultCodeEnum.MOK.code()) {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+            // 图像质量检测
+            ImageQuality imageQuality = new ImageQuality();
+            resultCode = faceEngine.imageQualityDetect(imageInfo, faceInfoList.get(0), 0, imageQuality);
+            if (resultCode != ArcResultCodeEnum.MOK.code() || (imageQuality.getFaceQuality() < this.arcFaceConfig.getFaceQuality())) {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+            // 提取特征码
+            FaceFeature faceFeature = new FaceFeature();
+            if (this.arcFaceConfig.isFaceMask()) {
+                resultCode = faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.REGISTER, 1, faceFeature);
+            } else {
+                resultCode = faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.RECOGNIZE, 0, faceFeature);
+            }
+
+            if (resultCode == ArcResultCodeEnum.MOK.code()) {
+                return R.ok("[人脸特征码提取成功]", Base64.encode(faceFeature.getFeatureData()));
+            } else {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail("[人脸特征码提取失败]-[服务响应异常]");
+        } finally {
+            // 释放人脸引擎
+            if (ObjectUtil.isNotEmpty(faceEngine)) {
+                faceEngineGeneralPool.returnObject(faceEngine);
+            }
+        }
+    }
+
+    /**
+     * 初始化人脸引擎
+     */
+    @PostConstruct
+    public void init() {
+        GenericObjectPoolConfig<FaceEngine> detectPoolConfig = new GenericObjectPoolConfig<>();
+        detectPoolConfig.setMaxIdle(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setMaxTotal(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setMinIdle(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setLifo(false);
+        EngineConfiguration detectCfg = getEngineConfiguration();
+        faceEngineGeneralPool = new GenericObjectPool<>(
+                new ArcFaceEngineFactory(arcFaceConfig.getSdkLibPath(), arcFaceConfig.getAppId(), arcFaceConfig.getSdkKey(),
+                        arcFaceConfig.getActiveKey(), detectCfg), detectPoolConfig);
+    }
+}

+ 184 - 0
src/main/java/com/dt/ykt/face/utils/FileUtils.java

@@ -0,0 +1,184 @@
+package com.dt.ykt.face.utils;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.img.Img;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.ObjectUtil;
+import com.dt.ykt.face.exception.ServiceException;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件处理工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class FileUtils extends FileUtil {
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param response     响应对象
+     * @param realFileName 真实文件名
+     */
+    public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
+        String percentEncodedFileName = percentEncode(realFileName);
+        String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
+        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+        response.setHeader("Content-disposition", contentDispositionValue);
+        response.setHeader("download-filename", percentEncodedFileName);
+    }
+
+    /**
+     * 百分号编码工具方法
+     *
+     * @param s 需要百分号编码的字符串
+     * @return 百分号编码后的字符串
+     */
+    public static String percentEncode(String s) {
+        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
+        return encode.replaceAll("\\+", "%20");
+    }
+
+    /**
+     * 根据旧文件名生成新文件名,使用 uuid 生成
+     *
+     * @param oldName 旧文件名
+     * @return 新文件名
+     */
+    public static String getNewFileName(String oldName) {
+        String extension = StringUtils.substringAfter(oldName, ".");
+        return UUID.randomUUID().toString() + "." + extension;
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param file
+     * @param path
+     * @param fileName
+     * @throws IOException
+     */
+    public static void upload(MultipartFile file, String path, String fileName) throws IOException {
+        // 1. 判断文件夹是否存在,不存在则创建
+        File imageDir = new File(path);
+        if (!imageDir.exists()) {
+            imageDir.mkdirs();
+        }
+
+        // 3. 保存照片
+        file.transferTo(new File(imageDir.getAbsoluteFile(), fileName));
+    }
+
+    public static String toBase64(String filePath) throws Exception {
+        if (ObjectUtil.isEmpty(filePath)) {
+            throw new Exception("[文件转换错误]-[文件路径为空]");
+        } else {
+            return toBase64(new File(filePath));
+        }
+    }
+
+    public static String toBase64(File file) throws Exception {
+        if (ObjectUtil.isEmpty(file)) {
+            throw new Exception("[文件转换错误]-[文件为空]");
+        } else if (!file.exists()) {
+            throw new Exception("[文件转换错误]-[文件不存在]");
+        } else {
+            byte[] data = FileUtil.readBytes(file);
+            return Base64.encode(data);
+        }
+    }
+
+    public static String toBase64(MultipartFile file) throws Exception {
+        if (ObjectUtil.isEmpty(file)) {
+            throw new Exception("[文件转换错误]-[文件为空]");
+        } else {
+            byte[] data = file.getBytes();
+            return Base64.encode(data);
+        }
+    }
+
+    public static byte[] imgCompression(byte[] imageBytes) {
+        // 小于1M就不进行压缩里,浪费执行时间
+        float quality = 0f;
+        if (imageBytes.length > 1024 * 1024 * 10) { // 大于10M
+            quality = 0.1f;
+        } else if (imageBytes.length > 1024 * 1024 * 5) { // 大于5M
+            quality = 0.2f;
+        } else if (imageBytes.length > 1024 * 1024) {// 大于1M
+            quality = 0.5f;
+        }
+
+        if (quality != 0) {
+            ByteArrayInputStream bis = null;
+            ByteArrayOutputStream bos = null;
+            try {
+                bis = new ByteArrayInputStream(imageBytes);
+                bos = new ByteArrayOutputStream();
+                Img.from(bis).setQuality(quality).write(bos);
+                imageBytes = bos.toByteArray();
+            } catch (Exception e) {
+                throw new ServiceException("图片处理失败,请稍后重试!");
+            } finally {
+                if (bis != null) {
+                    try {
+                        bis.close();
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+                if (bos != null) {
+                    try {
+                        bos.close();
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+        return imageBytes;
+    }
+
+    public static void saveFileToDisk(String fileName, byte[] fileData) throws IOException {
+        // 1.上传照片到指定目录
+        ByteArrayInputStream bis = new ByteArrayInputStream(fileData);
+        byte[] bytes = new byte[1024];
+        int index;
+
+        FileOutputStream downloadFile = null;
+        try {
+            downloadFile = new FileOutputStream(fileName);
+            while ((index = bis.read(bytes)) != -1) {
+                downloadFile.write(bytes, 0, index);
+                downloadFile.flush();
+            }
+        } catch (IOException e) {
+            throw new ServiceException("文件处理失败,请稍后重试!");
+        } finally {
+            bis.close();
+            if (downloadFile != null) {
+
+                downloadFile.close();
+            }
+        }
+    }
+
+    // 本地压缩图片
+    public static void main(String[] args) throws IOException {
+        String fileName = "C:\\file\\upload\\image\\avatar\\TeaBaseinfo\\20241022\\1729560631186.jpg";
+        File file = new File(fileName);
+        // 获取bytes
+        FileOutputStream fileOutputStream = new FileOutputStream("C:\\file\\upload\\image\\avatar\\TeaBaseinfo\\20241022\\1729560631186-3.jpg");
+        Img.from(file).setQuality(0.8f).write(fileOutputStream);
+        fileOutputStream.close();
+
+    }
+}

+ 66 - 0
src/main/java/com/dt/ykt/face/utils/SpringUtils.java

@@ -0,0 +1,66 @@
+package com.dt.ykt.face.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.thread.Threading;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring工具类
+ *
+ * @author Lion Li
+ */
+@Component
+public final class SpringUtils extends SpringUtil {
+
+    /**
+     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
+     */
+    public static boolean containsBean(String name) {
+        return getBeanFactory().containsBean(name);
+    }
+
+    /**
+     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
+     * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
+     */
+    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().isSingleton(name);
+    }
+
+    /**
+     * @return Class 注册对象的类型
+     */
+    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().getType(name);
+    }
+
+    /**
+     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
+     */
+    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
+        return getBeanFactory().getAliases(name);
+    }
+
+    /**
+     * 获取aop代理对象
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T getAopProxy(T invoker) {
+        return (T) getBean(invoker.getClass());
+    }
+
+    /**
+     * 获取spring上下文
+     */
+    public static ApplicationContext context() {
+        return getApplicationContext();
+    }
+
+    public static boolean isVirtual() {
+        return Threading.VIRTUAL.isActive(getBean(Environment.class));
+    }
+
+}

+ 323 - 0
src/main/java/com/dt/ykt/face/utils/StringUtils.java

@@ -0,0 +1,323 @@
+package com.dt.ykt.face.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+    public static final String SEPARATOR = ",";
+
+    public static final String SLASH = "/";
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param str defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static String blankToDefault(String str, String defaultValue) {
+        return StrUtil.blankToDefault(str, defaultValue);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str) {
+        return StrUtil.isEmpty(str);
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str) {
+        return StrUtil.trim(str);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start) {
+        return substring(str, start, str.length());
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @param end   结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end) {
+        return StrUtil.sub(str, start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params   参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params) {
+        return StrUtil.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     *
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link) {
+        return Validator.isUrl(link);
+    }
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static Set<String> str2Set(String str, String sep) {
+        return new HashSet<>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str         字符串
+     * @param sep         分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim        去掉首尾空白
+     * @return list集合
+     */
+    public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
+        List<String> list = new ArrayList<>();
+        if (isEmpty(str)) {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && isBlank(str)) {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split) {
+            if (filterBlank && isBlank(string)) {
+                continue;
+            }
+            if (trim) {
+                string = trim(string);
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs                  指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+        return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str) {
+        return StrUtil.toUnderlineCase(str);
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str  验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs) {
+        return StrUtil.equalsAnyIgnoreCase(str, strs);
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name) {
+        return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+    }
+
+    /**
+     * 驼峰式命名法 例如:user_name->userName
+     */
+    public static String toCamelCase(String s) {
+        return StrUtil.toCamelCase(s);
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str  指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs) {
+        if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+            return false;
+        }
+        for (String pattern : strs) {
+            if (isMatch(pattern, str)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url     需要匹配的url
+     */
+    public static boolean isMatch(String pattern, String url) {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num  数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static String padl(final Number num, final int size) {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s    原始字符串
+     * @param size 字符串指定长度
+     * @param c    用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static String padl(final String s, final int size, final char c) {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null) {
+            final int len = s.length();
+            if (s.length() <= size) {
+                sb.append(String.valueOf(c).repeat(size - len));
+                sb.append(s);
+            } else {
+                return s.substring(len - size, len);
+            }
+        } else {
+            sb.append(String.valueOf(c).repeat(Math.max(0, size)));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 切分字符串(分隔符默认逗号)
+     *
+     * @param str 被切分的字符串
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str) {
+        return splitTo(str, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str, String separator) {
+        return splitTo(str, separator, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串自定义转换(分隔符默认逗号)
+     *
+     * @param str    被切分的字符串
+     * @param mapper 自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
+        return splitTo(str, SEPARATOR, mapper);
+    }
+
+    /**
+     * 切分字符串自定义转换
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @param mapper    自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
+        if (isBlank(str)) {
+            return new ArrayList<>(0);
+        }
+        return StrUtil.split(str, separator)
+            .stream()
+            .filter(Objects::nonNull)
+            .map(mapper)
+            .collect(Collectors.toList());
+    }
+
+}

+ 39 - 0
src/main/resources/application.yml

@@ -0,0 +1,39 @@
+server:
+  port: 9800
+  # undertow 配置
+  undertow:
+    # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
+    max-http-post-size: -1
+    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
+    # 每块buffer的空间大小,越小的空间被利用越充分
+    buffer-size: 512
+    # 是否分配的直接内存
+    direct-buffers: true
+    threads:
+      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
+      io: 16
+      # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
+      worker: 256
+spring:
+  application:
+    name: ykt-face
+# 日志配置
+logging:
+  level:
+    org.springframework: warn
+  config: classpath:logback-plus.xml
+# 人脸算法配置
+face:
+  arc:
+    enable: true
+    sdkLibPath: F:/project/lib/arc_face_4.0
+    # 开发测试的配置
+    appId: Fc8uDHCRctVfnaiz3eRK1AnF3sokpb1PzgVBeehRgwzo
+    sdkKey: 87vy2hXC2ZBEEGsefGZa1Gqmx3zMNwBpZxQVfB3Wbqiw
+    activeKey: 86C1-11YN-G13F-CLJR
+    detectPoolSize: 10
+    comparePoolSize: 10
+upload:
+  upload-path: F:/project/002.hnswdx/file/upload
+  image:
+    user: image/avatar  # 用户图片(包含头像和封面)路径

+ 91 - 0
src/main/resources/logback-plus.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="logs" />
+   <!-- 日志输出格式 -->
+    <property name="console.log.pattern"
+              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n%ex{full}"/>
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n%ex{full}"/>
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${console.log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+    </appender>
+
+    <appender name="file_warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/warn.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>WARN</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 控制台输出 -->
+    <appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大 1天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+        </filter>
+    </appender>
+
+    <!-- info异步输出 -->
+    <appender name="async_warn" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>512</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_warn"/>
+    </appender>
+
+    <appender name="async_console" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>512</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_console"/>
+    </appender>
+
+    <include resource="logback-common.xml" />
+
+    <include resource="logback-logstash.xml" />
+
+    <!-- 开启 skywalking 日志收集 -->
+    <include resource="logback-skylog.xml" />
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+        <appender-ref ref="async_warn" />
+        <appender-ref ref="async_console" />
+    </root>
+</configuration>

+ 13 - 0
src/test/java/com/dt/ykt/face/YktFaceApplicationTests.java

@@ -0,0 +1,13 @@
+package com.dt.ykt.face;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class YktFaceApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}