wangkun před 4 týdny
revize
973bee2077
100 změnil soubory, kde provedl 10516 přidání a 0 odebrání
  1. 2 0
      .gitattributes
  2. 33 0
      .gitignore
  3. 3 0
      .mvn/wrapper/maven-wrapper.properties
  4. 295 0
      mvnw
  5. 189 0
      mvnw.cmd
  6. 236 0
      pom.xml
  7. 22 0
      src/main/java/com/ymwk/icvp/IcvpApplication.java
  8. 14 0
      src/main/java/com/ymwk/icvp/code/IErrorCode.java
  9. 53 0
      src/main/java/com/ymwk/icvp/component/PageParam.java
  10. 35 0
      src/main/java/com/ymwk/icvp/component/PageResult.java
  11. 99 0
      src/main/java/com/ymwk/icvp/component/ResponseDTO.java
  12. 89 0
      src/main/java/com/ymwk/icvp/component/SmartBeanUtil.java
  13. 130 0
      src/main/java/com/ymwk/icvp/component/SmartPageUtil.java
  14. 320 0
      src/main/java/com/ymwk/icvp/component/SmartStringUtil.java
  15. 90 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/AnnotsDir.java
  16. 181 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/AttachsDir.java
  17. 34 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/DirCollect.java
  18. 482 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/DocDir.java
  19. 82 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/DocObjDump.java
  20. 244 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDCreator.java
  21. 236 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDDir.java
  22. 910 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDPageDrawer.java
  23. 87 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDRender.java
  24. 8 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/OfdResIdDefine.java
  25. 176 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/PageDir.java
  26. 93 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/PagesDir.java
  27. 138 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/ResDir.java
  28. 158 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/SignDir.java
  29. 130 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/SignsDir.java
  30. 226 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/TemplateDir.java
  31. 93 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/TemplatesDir.java
  32. 150 0
      src/main/java/com/ymwk/icvp/pdfToOfdBao/ZipUtil.java
  33. 112 0
      src/main/java/com/ymwk/icvp/pki/controller/CertificateController.java
  34. 42 0
      src/main/java/com/ymwk/icvp/pki/entity/CertificateEntity.java
  35. 14 0
      src/main/java/com/ymwk/icvp/pki/entity/User.java
  36. 9 0
      src/main/java/com/ymwk/icvp/pki/mapper/CertificateMapper.java
  37. 9 0
      src/main/java/com/ymwk/icvp/pki/mapper/UserMapper.java
  38. 12 0
      src/main/java/com/ymwk/icvp/pki/model/CertificateFileInfo.java
  39. 10 0
      src/main/java/com/ymwk/icvp/pki/model/DownloadRequest.java
  40. 14 0
      src/main/java/com/ymwk/icvp/pki/model/DownloadResponse.java
  41. 12 0
      src/main/java/com/ymwk/icvp/pki/model/FreezeCertificateRequest.java
  42. 69 0
      src/main/java/com/ymwk/icvp/pki/model/IssueRequest.java
  43. 14 0
      src/main/java/com/ymwk/icvp/pki/model/Pkcs12DownloadRequest.java
  44. 12 0
      src/main/java/com/ymwk/icvp/pki/model/RenewRequest.java
  45. 12 0
      src/main/java/com/ymwk/icvp/pki/model/RevokeCertificateRequest.java
  46. 15 0
      src/main/java/com/ymwk/icvp/pki/model/certListRequest.java
  47. 40 0
      src/main/java/com/ymwk/icvp/pki/service/CertificateService.java
  48. 8 0
      src/main/java/com/ymwk/icvp/pki/service/UserService.java
  49. 281 0
      src/main/java/com/ymwk/icvp/pki/service/impl/CertificateAssembleService.java
  50. 944 0
      src/main/java/com/ymwk/icvp/pki/service/impl/CertificateServiceImpl.java
  51. 96 0
      src/main/java/com/ymwk/icvp/pki/service/impl/CertificateStorageService.java
  52. 74 0
      src/main/java/com/ymwk/icvp/pki/service/impl/CryptoService.java
  53. 20 0
      src/main/java/com/ymwk/icvp/pki/service/impl/UserServiceImpl.java
  54. 17 0
      src/main/java/com/ymwk/icvp/pki/utils/Base64Utils.java
  55. 55 0
      src/main/java/com/ymwk/icvp/pki/utils/issueCertUtil.java
  56. 132 0
      src/main/java/com/ymwk/icvp/signature/controller/PictureController.java
  57. 113 0
      src/main/java/com/ymwk/icvp/signature/controller/SealController.java
  58. 740 0
      src/main/java/com/ymwk/icvp/signature/controller/SignatureController.java
  59. 18 0
      src/main/java/com/ymwk/icvp/signature/entity/Picture.java
  60. 102 0
      src/main/java/com/ymwk/icvp/signature/entity/Seal.java
  61. 9 0
      src/main/java/com/ymwk/icvp/signature/mapper/PictureMapper.java
  62. 10 0
      src/main/java/com/ymwk/icvp/signature/mapper/SealMapper.java
  63. 38 0
      src/main/java/com/ymwk/icvp/signature/model/PictureGenerateRequest.java
  64. 12 0
      src/main/java/com/ymwk/icvp/signature/model/PictureListRequest.java
  65. 27 0
      src/main/java/com/ymwk/icvp/signature/model/SealAlgorithmEnum.java
  66. 18 0
      src/main/java/com/ymwk/icvp/signature/model/SealBindCertDTO.java
  67. 11 0
      src/main/java/com/ymwk/icvp/signature/model/SealBuildResult.java
  68. 38 0
      src/main/java/com/ymwk/icvp/signature/model/SealCreateDTO.java
  69. 12 0
      src/main/java/com/ymwk/icvp/signature/model/SealDownload.java
  70. 19 0
      src/main/java/com/ymwk/icvp/signature/model/SealExtendDTO.java
  71. 27 0
      src/main/java/com/ymwk/icvp/signature/model/SealFormatEnum.java
  72. 32 0
      src/main/java/com/ymwk/icvp/signature/model/SealQueryDTO.java
  73. 29 0
      src/main/java/com/ymwk/icvp/signature/model/SealStatusEnum.java
  74. 7 0
      src/main/java/com/ymwk/icvp/signature/model/SealType.java
  75. 14 0
      src/main/java/com/ymwk/icvp/signature/service/PictureService.java
  76. 16 0
      src/main/java/com/ymwk/icvp/signature/service/SealBuilder.java
  77. 32 0
      src/main/java/com/ymwk/icvp/signature/service/SealService.java
  78. 12 0
      src/main/java/com/ymwk/icvp/signature/service/SealVerifyService.java
  79. 50 0
      src/main/java/com/ymwk/icvp/signature/service/impl/PictureServiceImpl.java
  80. 172 0
      src/main/java/com/ymwk/icvp/signature/service/impl/SealBuilderImpl.java
  81. 35 0
      src/main/java/com/ymwk/icvp/signature/service/impl/SealGenerateService.java
  82. 394 0
      src/main/java/com/ymwk/icvp/signature/service/impl/SealServiceImpl.java
  83. 85 0
      src/main/java/com/ymwk/icvp/signature/service/impl/SealVerifyServiceImpl.java
  84. 17 0
      src/main/java/com/ymwk/icvp/signature/utils/Base64Utils.java
  85. 41 0
      src/main/java/com/ymwk/icvp/signature/utils/Constans.java
  86. 39 0
      src/main/java/com/ymwk/icvp/signature/utils/DataResponse.java
  87. 46 0
      src/main/java/com/ymwk/icvp/signature/utils/FileUtil.java
  88. 127 0
      src/main/java/com/ymwk/icvp/signature/utils/FileUtils.java
  89. 462 0
      src/main/java/com/ymwk/icvp/signature/utils/GraphicsUtil.java
  90. 32 0
      src/main/java/com/ymwk/icvp/signature/utils/ObjectUtils.java
  91. 108 0
      src/main/java/com/ymwk/icvp/signature/utils/PfxUtils.java
  92. 52 0
      src/main/java/com/ymwk/icvp/task/ExpiryStatusTask.java
  93. 248 0
      src/main/java/com/ymwk/icvp/utils/MinioClientService.java
  94. 22 0
      src/main/java/com/ymwk/icvp/utils/MinioConfig.java
  95. 48 0
      src/main/java/com/ymwk/icvp/utils/fileConvert.java
  96. binární
      src/main/resources/SealBuilder.p12
  97. 39 0
      src/main/resources/application.yaml
  98. binární
      src/main/resources/simsun.ttc
  99. 102 0
      src/test/java/com/ymwk/icvp/IcvpApplicationTests.java
  100. binární
      src/test/resources/input.ofd

+ 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/

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

@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip

+ 295 - 0
mvnw

@@ -0,0 +1,295 @@
+#!/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.4
+#
+# 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:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+  if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+    actualDistributionDir="$distributionUrlNameMain"
+  fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+  # enable globbing to iterate over items
+  set +f
+  for dir in "$TMP_DOWNLOAD_DIR"/*; do
+    if [ -d "$dir" ]; then
+      if [ -f "$dir/bin/$MVN_CMD" ]; then
+        actualDistributionDir="$(basename "$dir")"
+        break
+      fi
+    fi
+  done
+  set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+  verbose "Contents of $TMP_DOWNLOAD_DIR:"
+  verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+  die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"

+ 189 - 0
mvnw.cmd

@@ -0,0 +1,189 @@
+<# : 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.4
+@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+    New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+  $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+  $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+  $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+  Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+    $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+    if (Test-Path -Path $testPath -PathType Leaf) {
+      $actualDistributionDir = $_.Name
+    }
+  }
+}
+
+if (!$actualDistributionDir) {
+  Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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"

+ 236 - 0
pom.xml

@@ -0,0 +1,236 @@
+<?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>4.1.1</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.ymwk</groupId>
+    <artifactId>icvpServer</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>ICVP</name>
+    <description>ICVP</description>
+    <url/>
+    <licenses>
+        <license/>
+    </licenses>
+    <developers>
+        <developer/>
+    </developers>
+    <scm>
+        <connection/>
+        <developerConnection/>
+        <tag/>
+        <url/>
+    </scm>
+    <properties>
+        <java.version>21</java.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webmvc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jdbc-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-session-jdbc-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webmvc-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+            <version>3.5.17</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>8.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>2.2.54</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.itextpdf</groupId>
+            <artifactId>itextpdf</artifactId>
+            <version>5.5.13.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-core</artifactId>
+            <version>7.4.7.Final</version>
+            <type>pom</type>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+            <version>9.1.3.Final</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+            <version>2.0.1.Final</version>
+            <scope>compile</scope>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+            <version>4.6.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <!-- Source: https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.47</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk18on</artifactId>
+            <version>1.84</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk18on</artifactId>
+            <version>1.84</version>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.pdfbox</groupId>
+            <artifactId>pdfbox</artifactId>
+            <version>2.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.22.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.ofdrw</groupId>
+            <artifactId>ofdrw-full</artifactId>
+            <version>2.4.0</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-slf4j-impl</artifactId>
+                </exclusion>
+                <!-- 顺便排除 log4j-api 和 log4j-core,防止残留 -->
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </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-compiler-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-compile</id>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                        <configuration>
+                            <annotationProcessorPaths>
+                                <path>
+                                    <groupId>org.projectlombok</groupId>
+                                    <artifactId>lombok</artifactId>
+                                </path>
+                            </annotationProcessorPaths>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>default-testCompile</id>
+                        <phase>test-compile</phase>
+                        <goals>
+                            <goal>testCompile</goal>
+                        </goals>
+                        <configuration>
+                            <annotationProcessorPaths>
+                                <path>
+                                    <groupId>org.projectlombok</groupId>
+                                    <artifactId>lombok</artifactId>
+                                </path>
+                            </annotationProcessorPaths>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 22 - 0
src/main/java/com/ymwk/icvp/IcvpApplication.java

@@ -0,0 +1,22 @@
+package com.ymwk.icvp;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.security.Security;
+
+@SpringBootApplication
+@MapperScan("com.ymwk.icvp.**.mapper")
+@EnableScheduling
+public class IcvpApplication {
+
+    public static void main(String[] args) {
+        Security.addProvider(new BouncyCastleProvider());
+        SpringApplication.run(IcvpApplication.class, args);
+    }
+
+
+}

+ 14 - 0
src/main/java/com/ymwk/icvp/code/IErrorCode.java

@@ -0,0 +1,14 @@
+package com.ymwk.icvp.code;
+
+/**
+ * @author autwindx
+ */
+public interface IErrorCode {
+    int getCode();
+
+    /**
+     * 错误消息
+     *
+     */
+    String getMsg();
+}

+ 53 - 0
src/main/java/com/ymwk/icvp/component/PageParam.java

@@ -0,0 +1,53 @@
+package com.ymwk.icvp.component;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Data
+public class PageParam {
+
+    @Schema(description = "页码(不能为空)", example = "1")
+    @NotNull(message = "分页参数不能为空")
+    private Long pageNum;
+
+    @Schema(description = "每页数量(不能为空)", example = "10")
+    @NotNull(message = "每页数量不能为空")
+    @Max(value = 500, message = "每页最大为500")
+    private Long pageSize;
+
+    @Schema(description = "是否查询总条数")
+    protected Boolean searchCount;
+
+    @Schema(description = "排序字段集合")
+    @Size(max = 10, message = "排序字段最多10")
+    @Valid
+    private List<SortItem> sortItemList;
+
+    /**
+     * 排序DTO类
+     */
+    @Data
+    public static class SortItem {
+
+        @Schema(description = "true正序|false倒序")
+        @NotNull(message = "排序规则不能为空")
+        private Boolean isAsc;
+
+        @Schema(description = "排序字段")
+        @NotBlank(message = "排序字段不能为空")
+        @Length(max = 30, message = "排序字段最多30")
+        private String column;
+    }
+}

+ 35 - 0
src/main/java/com/ymwk/icvp/component/PageResult.java

@@ -0,0 +1,35 @@
+package com.ymwk.icvp.component;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Data
+public class PageResult<T> {
+    /**
+     * 当前页
+     */
+    private Long pageNum;
+    /**
+     * 每页的数量
+     */
+    private Long pageSize;
+    /**
+     * 总记录数
+     */
+    private Long total;
+    /**
+     * 总页数
+     */
+    private Long pages;
+    /**
+     * 结果集
+     */
+    private List<T> list;
+
+    private Boolean emptyFlag;
+
+}

+ 99 - 0
src/main/java/com/ymwk/icvp/component/ResponseDTO.java

@@ -0,0 +1,99 @@
+package com.ymwk.icvp.component;
+
+import com.ymwk.icvp.code.IErrorCode;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author autwindx
+ */
+@Data
+@Schema
+public class ResponseDTO<T> {
+    public static final int OK_CODE = 100000;
+    public static final int ERROR_CODE = 100001;
+
+    public static final String OK_MSG = "操作成功";
+
+    @Schema(description = "返回码")
+    private Integer code;
+
+    private String message;
+
+    @Schema(description = "返回数据")
+    private T data;
+
+
+    public ResponseDTO(Integer code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public ResponseDTO(IErrorCode IErrorCode, String msg, T data) {
+        this.code = IErrorCode.getCode();
+        if (StringUtils.isNotBlank(msg)) {
+            this.message = msg;
+        } else {
+            this.message = IErrorCode.getMsg();
+        }
+        this.data = data;
+    }
+
+    public ResponseDTO(IErrorCode IErrorCode, boolean ok, String msg, T data) {
+        this.code = IErrorCode.getCode();
+
+        if (StringUtils.isNotBlank(msg)) {
+            this.message = msg;
+        } else {
+            this.message = IErrorCode.getMsg();
+        }
+        this.data = data;
+    }
+
+    public static <T> ResponseDTO<T> ok() {
+        return new ResponseDTO<>(OK_CODE, OK_MSG, null);
+    }
+
+    public static <T> ResponseDTO<T> ok(T data) {
+        return new ResponseDTO<>(OK_CODE, OK_MSG, data);
+    }
+
+    public static <T> ResponseDTO<T> okMsg(String msg) {
+        return new ResponseDTO<>(OK_CODE, msg, null);
+    }
+
+
+
+    // -------------------------------------------- 错误码 --------------------------------------------
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode) {
+        return new ResponseDTO<>(IErrorCode, null, null);
+    }
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode, boolean ok) {
+        return new ResponseDTO<>(IErrorCode, null, null);
+    }
+
+    public static <T>  ResponseDTO<T> error(ResponseDTO<?> responseDTO) {
+        return new ResponseDTO<>(responseDTO.getCode(), responseDTO.getMessage(), null);
+    }
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode, String msg) {
+        return new ResponseDTO<>(IErrorCode, msg, null);
+    }
+
+    public static <T> ResponseDTO<T> errorData(IErrorCode IErrorCode, T data) {
+        return new ResponseDTO<>(IErrorCode, null, data);
+    }
+
+    public static <T> ResponseDTO<T> error(int i, String string) {
+        return new ResponseDTO<>(i, string, null);
+    }
+
+    public static <T> ResponseDTO<T> error(String msg, T data) {
+        return new ResponseDTO<>(ERROR_CODE, msg, data);
+    }
+
+}

+ 89 - 0
src/main/java/com/ymwk/icvp/component/SmartBeanUtil.java

@@ -0,0 +1,89 @@
+package com.ymwk.icvp.component;
+
+import org.springframework.beans.BeanUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author autwindx
+ */
+public class SmartBeanUtil {
+
+    /**
+     * 验证器
+     */
+    private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
+
+    /**
+     * 复制bean的属性
+     *
+     * @param source 源 要复制的对象
+     * @param target 目标 复制到此对象
+     */
+    public static void copyProperties(Object source, Object target) {
+        BeanUtils.copyProperties(source, target);
+    }
+
+    /**
+     * 复制对象
+     *
+     * @param source 源 要复制的对象
+     * @param target 目标 复制到此对象
+     * @param <T>
+     * @return
+     */
+    public static <T> T copy(Object source, Class<T> target) {
+        if (source == null || target == null) {
+            return null;
+        }
+        try {
+            T newInstance = target.newInstance();
+            BeanUtils.copyProperties(source, newInstance);
+            return newInstance;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 复制list
+     *
+     * @param source
+     * @param target
+     * @param <T>
+     * @param <K>
+     * @return
+     */
+    public static <T, K> List<K> copyList(List<T> source, Class<K> target) {
+        if (null == source || source.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return source.stream().map(e -> copy(e, target)).collect(Collectors.toList());
+    }
+
+    /**
+     * 手动验证对象 Model的属性
+     * 需要配合 hibernate-validator 校验注解
+     *
+     * @param t
+     * @return String 返回null代表验证通过,否则返回错误的信息
+     */
+    public static <T> String verify(T t) {
+        // 获取验证结果
+        Set<ConstraintViolation<T>> validate = VALIDATOR.validate(t);
+        if (validate.isEmpty()) {
+            // 验证通过
+            return null;
+        }
+        // 返回错误信息
+        List<String> messageList = validate.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
+        return messageList.toString();
+    }
+}
+

+ 130 - 0
src/main/java/com/ymwk/icvp/component/SmartPageUtil.java

@@ -0,0 +1,130 @@
+package com.ymwk.icvp.component;
+
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.google.common.collect.Lists;
+import com.ymwk.icvp.component.PageParam;
+import com.ymwk.icvp.component.PageResult;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Slf4j
+public class SmartPageUtil {
+
+    /**
+     * 转换为查询参数
+     */
+    public static Page<?> convert2PageQuery(PageParam pageParam) {
+        Page<?> page = new Page<>(pageParam.getPageNum(), pageParam.getPageSize());
+
+        List<PageParam.SortItem> sortItemList = pageParam.getSortItemList();
+        if (CollectionUtils.isEmpty(sortItemList)) {
+            return page;
+        }
+
+        // 设置排序字段并检测是否含有sql注入
+        List<OrderItem> orderItemList = new ArrayList<>();
+        for (PageParam.SortItem sortItem : sortItemList) {
+
+            if (SmartStringUtil.isEmpty(sortItem.getColumn())) {
+                continue;
+            }
+
+            if (SqlInjectionUtils.check(sortItem.getColumn())) {
+                log.error("《存在SQL注入:》 : {}", sortItem.getColumn());
+                // throw new BusinessException("存在SQL注入风险,请联系技术工作人员!");
+            }
+
+            orderItemList.add(new OrderItem().setColumn(sortItem.getColumn()).setAsc(sortItem.getIsAsc()));
+        }
+        page.setOrders(orderItemList);
+        return page;
+    }
+
+    /**
+     * 转换为 PageResult 对象
+     */
+    public static <T, E> PageResult<T> convert2PageResult(Page<?> page, List<E> sourceList, Class<T> targetClazz) {
+        return convert2PageResult(page, SmartBeanUtil.copyList(sourceList, targetClazz));
+    }
+
+    /**
+     * 转换为 PageResult 对象
+     */
+    public static <E> PageResult<E> convert2PageResult(Page<?> page, List<E> sourceList) {
+        PageResult<E> pageResult = new PageResult<>();
+        pageResult.setPageNum(page.getCurrent());
+        pageResult.setPageSize(page.getSize());
+        pageResult.setTotal(page.getTotal());
+        pageResult.setPages(page.getPages());
+        pageResult.setList(sourceList);
+        pageResult.setEmptyFlag(CollectionUtils.isEmpty(sourceList));
+        return pageResult;
+    }
+
+    /**
+     * 转换分页结果对象
+     */
+    public static <E, T> PageResult<T> convert2PageResult(PageResult<E> pageResult, Class<T> targetClazz) {
+        PageResult<T> newPageResult = new PageResult<>();
+        newPageResult.setPageNum(pageResult.getPageNum());
+        newPageResult.setPageSize(pageResult.getPageSize());
+        newPageResult.setTotal(pageResult.getTotal());
+        newPageResult.setPages(pageResult.getPages());
+        newPageResult.setEmptyFlag(pageResult.getEmptyFlag());
+        newPageResult.setList(SmartBeanUtil.copyList(pageResult.getList(), targetClazz));
+        return newPageResult;
+    }
+
+//    /**
+//     * 转换分页结果对象
+//     * @param page 分页对象
+//     * @param targetClazz 目标类型
+//     * @return PageResult
+//     * @param <E> 分页泛型
+//     * @param <T> PageResult泛型
+//     */
+//    public static <E, T> PageResult<T> cover2PageResult(Page<E> page, Class<T> targetClazz){
+//        PageResult<T> pageResult = new PageResult<>();
+//        pageResult.setPageNum(page.getCurrent());
+//        pageResult.setPageSize(page.getSize());
+//        pageResult.setTotal(page.getTotal());
+//        pageResult.setPages(page.getPages());
+//        pageResult.setEmptyFlag(CollUtil.isEmpty(page.getRecords()));
+//        pageResult.setList(SmartBeanUtil.copyList(page.getRecords(), targetClazz));
+//        return pageResult;
+//    }
+
+    public static <T> PageResult subListPage(Integer pageNum, Integer pageSize, List<T> list) {
+        PageResult<T> pageRet = new PageResult<T>();
+        //总条数
+        int count = list.size();
+        int pages = count % pageSize == 0 ? count / pageSize : (count / pageSize + 1);
+        int fromIndex = (pageNum - 1) * pageSize;
+        int toIndex = Math.min(pageNum * pageSize, count);
+
+        if (pageNum > pages) {
+            pageRet.setList(Lists.newLinkedList());
+            pageRet.setPageNum(pageNum.longValue());
+            pageRet.setPages((long) pages);
+            pageRet.setTotal((long) count);
+            return pageRet;
+        }
+        List<T> pageList = list.subList(fromIndex, toIndex);
+        pageRet.setList(pageList);
+        pageRet.setPageNum(pageNum.longValue());
+        pageRet.setPages((long) pages);
+        pageRet.setTotal((long) count);
+        return pageRet;
+    }
+}

+ 320 - 0
src/main/java/com/ymwk/icvp/component/SmartStringUtil.java

@@ -0,0 +1,320 @@
+package com.ymwk.icvp.component;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.util.*;
+
+/**
+ * @author autwindx
+ */
+public class SmartStringUtil extends StrUtil {
+
+    // ===============split =======================
+
+    public static Set<String> splitConvertToSet(String str, String split) {
+        if (isEmpty(str)) {
+            return new HashSet<String>();
+        }
+        String[] splitArr = str.split(split);
+        HashSet<String> set = new HashSet<String>(splitArr.length);
+        Collections.addAll(set, splitArr);
+        return set;
+    }
+
+    public static List<String> splitConvertToList(String str, String split) {
+        if (isEmpty(str)) {
+            return new ArrayList<String>();
+        }
+        String[] splitArr = str.split(split);
+        ArrayList<String> list = new ArrayList<String>(splitArr.length);
+        list.addAll(Arrays.asList(splitArr));
+        return list;
+    }
+
+    // ===============split Integer=======================
+
+    public static List<Integer> splitConvertToIntList(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Integer>();
+        }
+        String[] strArr = str.split(split);
+        List<Integer> list = new ArrayList<Integer>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                int parseInt = Integer.parseInt(strArr[i]);
+                list.add(parseInt);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static Set<Integer> splitConvertToIntSet(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new HashSet<Integer>();
+        }
+        String[] strArr = str.split(split);
+        HashSet<Integer> set = new HashSet<Integer>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                int parseInt = Integer.parseInt(strArr[i]);
+                set.add(parseInt);
+            } catch (NumberFormatException e) {
+                set.add(defaultVal);
+                continue;
+            }
+        }
+        return set;
+    }
+
+    public static Set<Integer> splitConvertToIntSet(String str, String split) {
+        return splitConvertToIntSet(str, split, 0);
+    }
+
+    public static List<Integer> splitConvertToIntList(String str, String split) {
+        return splitConvertToIntList(str, split, 0);
+    }
+
+    public static int[] splitConvertToIntArray(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new int[0];
+        }
+        String[] strArr = str.split(split);
+        int[] result = new int[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Integer.parseInt(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static int[] splitConvertToIntArray(String str, String split) {
+        return splitConvertToIntArray(str, split, 0);
+    }
+
+    // ===============split 2 Long=======================
+
+    public static List<Long> splitConvertToLongList(String str, String split, long defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Long>();
+        }
+        String[] strArr = str.split(split);
+        List<Long> list = new ArrayList<Long>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                long parseLong = Long.parseLong(strArr[i]);
+                list.add(parseLong);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Long> splitConvertToLongList(String str, String split) {
+        return splitConvertToLongList(str, split, 0L);
+    }
+
+    public static long[] splitConvertToLongArray(String str, String split, long defaultVal) {
+        if (isEmpty(str)) {
+            return new long[0];
+        }
+        String[] strArr = str.split(split);
+        long[] result = new long[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Long.parseLong(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static long[] splitConvertToLongArray(String str, String split) {
+        return splitConvertToLongArray(str, split, 0L);
+    }
+
+    // ===============split convert byte=======================
+
+    public static List<Byte> splitConvertToByteList(String str, String split, byte defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Byte>();
+        }
+        String[] strArr = str.split(split);
+        List<Byte> list = new ArrayList<Byte>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                byte parseByte = Byte.parseByte(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Byte> splitConvertToByteList(String str, String split) {
+        return splitConvertToByteList(str, split, (byte) 0);
+    }
+
+    public static byte[] splitConvertToByteArray(String str, String split, byte defaultVal) {
+        if (isEmpty(str)) {
+            return new byte[0];
+        }
+        String[] strArr = str.split(split);
+        byte[] result = new byte[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Byte.parseByte(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static byte[] splitConvertToByteArray(String str, String split) {
+        return splitConvertToByteArray(str, split, (byte) 0);
+    }
+
+    // ===============split convert double=======================
+
+    public static List<Double> splitConvertToDoubleList(String str, String split, double defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Double>();
+        }
+        String[] strArr = str.split(split);
+        List<Double> list = new ArrayList<Double>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                double parseByte = Double.parseDouble(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Double> splitConvertToDoubleList(String str, String split) {
+        return splitConvertToDoubleList(str, split, 0);
+    }
+
+    public static double[] splitConvertToDoubleArray(String str, String split, double defaultVal) {
+        if (isEmpty(str)) {
+            return new double[0];
+        }
+        String[] strArr = str.split(split);
+        double[] result = new double[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Double.parseDouble(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static double[] splitConvertToDoubleArray(String str, String split) {
+        return splitConvertToDoubleArray(str, split, 0);
+    }
+
+    // ===============split convert float=======================
+
+    public static List<Float> splitConvertToFloatList(String str, String split, float defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Float>();
+        }
+        String[] strArr = str.split(split);
+        List<Float> list = new ArrayList<Float>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                float parseByte = Float.parseFloat(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Float> splitConvertToFloatList(String str, String split) {
+        return splitConvertToFloatList(str, split, 0f);
+    }
+
+    public static float[] splitConvertToFloatArray(String str, String split, float defaultVal) {
+        if (isEmpty(str)) {
+            return new float[0];
+        }
+        String[] strArr = str.split(split);
+        float[] result = new float[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Float.parseFloat(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static float[] splitConvertToFloatArray(String str, String split) {
+        return splitConvertToFloatArray(str, split, 0f);
+    }
+
+
+    public static String upperCaseFirstChar(String str) {
+        if (str != null && !str.isEmpty()) {
+            char firstChar = str.charAt(0);
+            if (Character.isUpperCase(firstChar)) {
+                return str;
+            } else {
+                char[] values = str.toCharArray();
+                values[0] = Character.toUpperCase(firstChar);
+                return new String(values);
+            }
+        } else {
+            return str;
+        }
+    }
+
+    public static String replace(String content, int begin, int end, String newStr) {
+        if (begin < content.length() && begin >= 0) {
+            if (end <= content.length() && end >= 0) {
+                if (begin > end) {
+                    return content;
+                } else {
+                    StringBuilder starStr = new StringBuilder();
+
+                    for (int i = begin; i < end; ++i) {
+                        starStr.append(newStr);
+                    }
+
+                    return content.substring(0, begin) + starStr + content.substring(end);
+                }
+            } else {
+                return content;
+            }
+        } else {
+            return content;
+        }
+    }
+
+
+}

+ 90 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/AnnotsDir.java

@@ -0,0 +1,90 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.annotation.Annotations;
+import org.ofdrw.core.annotation.pageannot.PageAnnot;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * @author 权观宇
+ * @time 2020/10/1
+ */
+public class AnnotsDir implements DirCollect {
+    /**
+     * 注释文档
+     */
+    private Annotations annotations;
+    
+    /**
+     * 注释项文档
+     */
+    private PageAnnot pageAnnot;
+
+	@Override
+	public Path collect(String base) throws IOException {
+		// TODO Auto-generated method stub
+		
+        Path path = Paths.get(base, "Annots");
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+        
+        if (annotations != null) {
+        	DocObjDump.dump(annotations, Paths.get(dir, "Annotations.xml"));
+        }
+        
+        if (this.pageAnnot != null) {
+        	DocObjDump.dump(pageAnnot, Paths.get(dir, "Annotation.xml"));
+        }
+        
+		return path;
+	}
+
+	@Override
+	public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+		Path path = Paths.get(base, "Annots");
+		String dir = path.toString();
+
+		if (annotations != null) {
+			DocObjDump.dump(annotations, Paths.get(dir, "Annotations.xml").toString(), virtualFileMap);
+		}
+
+		if (this.pageAnnot != null) {
+			DocObjDump.dump(pageAnnot, Paths.get(dir, "Annotation.xml").toString(), virtualFileMap);
+		}
+
+		return virtualFileMap;
+	}
+
+	/**
+     * 注释文档
+     */
+	public Annotations getAnnotations() {
+		return annotations;
+	}
+
+    /**
+     * 注释文档
+     */
+	public void setAnnotations(Annotations annotations) {
+		this.annotations = annotations;
+	}
+
+    /**
+     * 注释项文档
+     */
+	public PageAnnot getPageAnnot() {
+		return pageAnnot;
+	}
+
+    /**
+     * 注释项文档
+     */
+	public void setPageAnnot(PageAnnot pageAnnot) {
+		this.pageAnnot = pageAnnot;
+	}
+
+}

+ 181 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/AttachsDir.java

@@ -0,0 +1,181 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.apache.commons.io.FileUtils;
+import org.dom4j.Element;
+import org.ofdrw.core.attachment.Attachments;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author 权观宇
+ * @time 2020/10/1
+ */
+public class AttachsDir implements DirCollect {
+	
+	/**
+	 * 附件描述文件Attachments.xml
+	 */
+	private Attachments attachments;
+	
+	/**
+	 * xml格式附件文件
+	 */
+	private Map<String, Element> attachXmlMap;
+	
+	/**
+	 * path指定的附件文件
+	 */
+	private Map<String, Path> attachPathMap;
+	
+	/**
+	 * 二进制格式的附件文件
+	 */
+	private Map<String, byte[]> attachDataMap;
+	
+	
+	public AttachsDir() {
+		attachDataMap = new HashMap<>();
+		attachPathMap = new HashMap<>();
+		attachXmlMap = new HashMap<>();
+	}
+	
+	/**
+	 * 附件描述文件Attachments.xml
+	 * @return
+	 */
+	public Attachments getAttachments() {
+		return this.attachments;
+	}
+	
+	/**
+	 * 附件描述文件Attachments.xml
+	 * @param attachments
+	 * @return
+	 */
+	public AttachsDir setAttachments(Attachments attachments) {
+		this.attachments = attachments;
+		return this;
+	}
+	
+	/**
+	 * 添加xml格式的附件
+	 * @param name            附件名
+	 * @param attach          xml格式的附件
+	 * @return
+	 * @throws Exception 
+	 */
+	public AttachsDir addAttachment(String name, Element attach) {	
+		this.attachXmlMap.put(name, attach);
+		return this;
+	}
+	
+	/**
+	 * 添加二进制格式的附件
+	 * @param name           附件名称
+	 * @param attach         附件二进制内容
+	 * @return
+	 * @throws Exception 
+	 */
+	public AttachsDir addAttachment(String name, byte[] attach) {	
+		attachDataMap.put(name, attach);
+		return this;
+	}
+	
+	/**
+	 * 添加Path路径指定的附件
+	 * @param name
+	 * @param attach
+	 * @return
+	 * @throws Exception 
+	 */
+	public AttachsDir addAttachment(String name, Path attach) {		
+		attachPathMap.put(name, attach);
+		return this;
+	}
+	
+	@Override
+	public Path collect(String base) throws IOException {
+        if (attachments == null) {
+            throw new IllegalArgumentException("文档根节点(Attachments.xml)为空");
+        }
+        
+        Path path = Paths.get(base, "Attachs");
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();  
+        
+        //1. 添加Attachments.xml
+        DocObjDump.dump(attachments, Paths.get(dir, "Attachments.xml"));
+        
+        //2. 添加附件
+        //2.1. xml附件
+        if (attachXmlMap != null) {
+        	for (Map.Entry<String, Element> entry : attachXmlMap.entrySet()) {
+        		//System.out.println(entry.getValue().asXML());
+        		DocObjDump.dump(entry.getValue(), Paths.get(dir, entry.getKey()));
+        	}
+        }
+        
+        //2.2 path指定的附件文件
+        if (attachPathMap != null) {
+        	for (Map.Entry<String, Path> entry : attachPathMap.entrySet()) {
+        		Files.copy(entry.getValue(), Paths.get(dir, entry.getKey()));
+        	}
+        }
+        
+        //2.3 二进制文件
+        if (attachDataMap != null) {
+        	for (Map.Entry<String, byte[]> entry : attachDataMap.entrySet()) {
+        		Files.write(Paths.get(dir, entry.getKey()), entry.getValue());
+        	}
+        }
+        
+		return path;
+	}
+
+	@Override
+	public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+		if (attachments == null) {
+			throw new IllegalArgumentException("文档根节点(Attachments.xml)为空");
+		}
+
+		Path path = Paths.get(base, "Attachs");
+		String dir = path.toString();
+
+		//1. 添加Attachments.xml
+		DocObjDump.dump(attachments, Paths.get(dir, "Attachments.xml").toString(), virtualFileMap);
+
+		//2. 添加附件
+		//2.1. xml附件
+		if (attachXmlMap != null) {
+			for (Map.Entry<String, Element> entry : attachXmlMap.entrySet()) {
+				//System.out.println(entry.getValue().asXML());
+				DocObjDump.dump(entry.getValue(), Paths.get(dir, entry.getKey()).toString(), virtualFileMap);
+			}
+		}
+
+		//2.2 path指定的附件文件
+		if (attachPathMap != null) {
+			for (Map.Entry<String, Path> entry : attachPathMap.entrySet()) {
+//				Files.copy(entry.getValue(), Paths.get(dir, entry.getKey()));
+				virtualFileMap.put(Paths.get(dir, entry.getKey()).toString(), FileUtils.readFileToByteArray(new File(entry.getKey())));
+			}
+		}
+
+		//2.3 二进制文件
+		if (attachDataMap != null) {
+			for (Map.Entry<String, byte[]> entry : attachDataMap.entrySet()) {
+//				Files.write(Paths.get(dir, entry.getKey()), entry.getValue());
+				virtualFileMap.put(Paths.get(dir, entry.getKey()).toString(), entry.getValue());
+			}
+		}
+
+		return virtualFileMap;
+	}
+
+}

+ 34 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/DirCollect.java

@@ -0,0 +1,34 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+
+/**
+ * 创建目录
+ *
+ * @author 权观宇
+ * @since 2020-01-20 13:06:26
+ */
+public interface DirCollect {
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    Path collect(String base) throws IOException;
+
+    /**
+     * 创建虚拟目录并推入文件数据
+     *
+     * @Author: Lianglx
+     *
+     * @param base
+     * @param virtualFileMap
+     * @return
+     * @throws IOException
+     */
+    Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException;
+}

+ 482 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/DocDir.java

@@ -0,0 +1,482 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.annotation.Annotations;
+import org.ofdrw.core.annotation.pageannot.PageAnnot;
+import org.ofdrw.core.basicStructure.doc.Document;
+import org.ofdrw.core.basicStructure.pageObj.CT_TemplatePage;
+import org.ofdrw.core.basicStructure.res.Res;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+public class DocDir implements DirCollect {
+
+    /**
+     * 表示第几份文档,从0开始
+     */
+    private Integer index = 0;
+    
+    /**
+     * 文档的根节点
+     */
+    private Document document;
+    
+    /**
+     * 注释文档
+     */
+    private Annotations annotations;
+    
+    /**
+     * 注释项文档
+     */
+    private PageAnnot pageAnnot;
+        
+    /**
+     * 文档公共资源索引
+     */
+    private Res publicRes;
+
+    /**
+     * 文档自身资源索引
+     */
+    private Res documentRes;
+
+    /**
+     * 资源文件夹
+     */
+    private ResDir res;
+    /**
+     * 资源文件夹
+     */
+    private TemplatesDir tpls;
+
+    /**
+     * 数字签名存储目录
+     */
+    private SignsDir signs;
+
+    /**
+     * 页面存储目录
+     */
+    private PagesDir pages;
+    
+    
+    private CT_TemplatePage templateRes;
+    
+    /**
+     * 附件存储目录
+     */
+    private AttachsDir attachs;
+    
+    /**
+     * 注释目录
+     */
+    private AnnotsDir annotsDir;
+    
+    public DocDir() {
+
+    }
+
+    /**
+     * @return 文档编号(用于表示第几个) ,从0 起
+     */
+    public Integer getIndex() {
+        return index;
+    }
+
+    /**
+     * 设值 文档编号(用于表示第几个)
+     *
+     * @param index 第几个
+     * @return this
+     */
+    public DocDir setIndex(Integer index) {
+        this.index = index;
+        return this;
+    }
+
+    /**
+     * @return 文档的根节点
+     */
+    public Document getDocument() {
+        return document;
+    }
+
+    /**
+     * 设置 文档的根节点
+     *
+     * @param document 文档的根节点
+     * @return this
+     */
+    public DocDir setDocument(Document document) {
+        this.document = document;
+        return this;
+    }
+    
+    /**
+     * 注释文档
+     * @return
+     */
+    public Annotations getAnnotations() {
+    	return annotations;
+    }
+    
+    /**
+     * 注释文档
+     * @param annotations
+     * @return
+     */
+    public DocDir setAnnotations(Annotations annotations) {
+    	this.annotations = annotations;
+    	return this;
+    }
+    
+    /**
+     * 注释项文档
+     * @return
+     */
+    public PageAnnot getPageAnnot() {
+    	return pageAnnot;
+    }
+    
+    /**
+     * 注释项文档
+     * @param annot
+     * @return
+     */
+    public DocDir setPageAnnot(PageAnnot annot) {
+    	this.pageAnnot = annot;
+    	return this;
+    }
+
+    /**
+     * @return 文档公共资源索引
+     */
+    public Res getPublicRes() {
+        return publicRes;
+    }
+
+    /**
+     * 设置 文档公共资源索引
+     *
+     * @param publicRes 文档公共资源索引
+     * @return this
+     */
+    public DocDir setPublicRes(Res publicRes) {
+        this.publicRes = publicRes;
+        return this;
+    }
+
+    /**
+     * @return 文档自身资源索引
+     */
+    public Res getDocumentRes() {
+        return documentRes;
+    }
+
+    /**
+     * 设置 文档自身资源索引
+     *
+     * @param documentRes 文档自身资源索引
+     * @return this
+     */
+    public DocDir setDocumentRes(Res documentRes) {
+        this.documentRes = documentRes;
+        return this;
+    }
+
+    /**
+     * @return 资源文件夹
+     */
+    public ResDir getRes() {
+        return res;
+    }
+
+    /**
+     * 设置 资源文件夹
+     *
+     * @param res 资源文件夹
+     * @return this
+     */
+    public DocDir setRes(ResDir res) {
+        this.res = res;
+        return this;
+    }
+
+    /**
+     * @return 数字签名存储目录
+     */
+    public SignsDir getSigns() {
+        return signs;
+    }
+
+    /**
+     * 设置 数字签名存储目录
+     *
+     * @param signs 数字签名存储目录
+     * @return this
+     */
+    public DocDir setSigns(SignsDir signs) {
+        this.signs = signs;
+        return this;
+    }
+
+    /**
+     * @return 页面存储目录
+     */
+    public PagesDir getPages() {
+        return pages;
+    }
+
+    /**
+     * 设置 页面存储目录
+     *
+     * @param pages 页面存储目录
+     * @return this
+     */
+    public DocDir setPages(PagesDir pages) {
+        this.pages = pages;
+        return this;
+    }
+
+    
+    /**
+     * @return 页面存储目录
+     */
+    public TemplatesDir getTemplates() {
+        return tpls;
+    }
+
+    /**
+     * 设置 页面存储目录
+     *
+     * @param pages 页面存储目录
+     * @return this
+     */
+    public DocDir setPages(TemplatesDir tpls) {
+        this.tpls = tpls;
+        return this;
+    }
+    
+    /**
+     * 增加资源(资源文件路径)
+     *
+     * @param resource 资源
+     * @return this
+     */
+    public DocDir addResource(Path resource) {
+        if (this.res == null) {
+            this.res = new ResDir();
+        }
+        this.res.add(resource);
+        return this;
+    }
+    
+    /**
+     * 添加资源(资源文件二进制内容)
+     * @param name
+     * @param resData
+     * @return
+     */
+    public DocDir addResource(String name, byte[] resData) {
+        if (res == null) {
+            res = new ResDir();
+        }  
+        
+        res.add(name, resData);
+        return this;
+    }
+    
+    /**
+     * 增加资源
+     *
+     * @param resource 资源
+     * @return this
+     */
+    public DocDir addResources(List<Path> resources) {
+        if (this.res == null) {
+            this.res = new ResDir();
+        }
+        for (Path path : resources) {
+        	this.res.add(path);
+		}
+        return this;
+    }
+    
+
+    /**
+     * 获取资源
+     *
+     * @param name 资源名称(包含后缀名称)
+     * @return 资源,不存在则返还null
+     */
+    public Path getResource(String name) {
+        if (this.res == null) {
+            return null;
+        }
+        return this.res.get(name);
+    }
+    
+    
+	public CT_TemplatePage getTemplateRes() {
+		return templateRes;
+	}
+
+	public DocDir setTemplateRes(CT_TemplatePage templateRes) {
+		this.templateRes = templateRes;
+		return this;
+	}
+
+	public TemplatesDir getTpls() {
+		return tpls;
+	}
+
+	public DocDir setTpls(TemplatesDir tpls) {
+		this.tpls = tpls;
+		return this;
+	}
+	
+    /**
+     * 附件存储目录
+     * @return
+     */
+    public AttachsDir getAttachs() {
+    	return attachs;
+    }
+    
+    /**
+     * 附件存储目录
+     * @param attachs
+     * @return
+     */
+    public DocDir setAttachs(AttachsDir attachs) {
+    	this.attachs = attachs;
+    	return this;
+    }
+
+	/**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        if (document == null) {
+            throw new IllegalArgumentException("文档根节点(document)为空");
+        }
+
+        Path path = Paths.get(base, "Doc_" + index);
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+
+        DocObjDump.dump(document, Paths.get(dir, "Document.xml"));
+        
+        if (annotations != null) {
+        	DocObjDump.dump(annotations, Paths.get(dir, "Annotations.xml"));
+        }
+        
+        if (this.pageAnnot != null) {
+        	DocObjDump.dump(pageAnnot, Paths.get(dir, "Annotation.xml"));
+        }
+        
+        if (signs != null) {
+            signs.collect(dir);
+        }
+        if (pages != null) {
+            pages.collect(dir);
+        }
+        if (publicRes != null) {
+            DocObjDump.dump(publicRes, Paths.get(dir, "PublicRes.xml"));
+        }
+        if (documentRes != null) {
+            DocObjDump.dump(documentRes, Paths.get(dir, "DocumentRes.xml"));
+        }
+        if (res != null) {
+            res.collect(dir);
+        }
+        if(tpls !=null) {
+        	tpls.collect(dir);
+        }
+        
+        if (attachs != null) {
+        	attachs.collect(dir);
+        }
+        
+        if (annotsDir != null) {
+        	annotsDir.collect(dir);
+        }
+        
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (document == null) {
+            throw new IllegalArgumentException("文档根节点(document)为空");
+        }
+
+        Path path = Paths.get(base, "Doc_" + index);
+        String dir = path.toString();
+
+        DocObjDump.dump(document, Paths.get(dir, "Document.xml").toString(), virtualFileMap);
+
+        if (annotations != null) {
+            DocObjDump.dump(annotations, Paths.get(dir, "Annotations.xml").toString(), virtualFileMap);
+        }
+
+        if (this.pageAnnot != null) {
+            DocObjDump.dump(pageAnnot, Paths.get(dir, "Annotation.xml").toString(), virtualFileMap);
+        }
+
+        if (signs != null) {
+            signs.collect(dir, virtualFileMap);
+        }
+        if (pages != null) {
+            pages.collect(dir, virtualFileMap);
+        }
+        if (publicRes != null) {
+            DocObjDump.dump(publicRes, Paths.get(dir, "PublicRes.xml").toString(), virtualFileMap);
+        }
+        if (documentRes != null) {
+            DocObjDump.dump(documentRes, Paths.get(dir, "DocumentRes.xml").toString(), virtualFileMap);
+        }
+        if (res != null) {
+            res.collect(dir, virtualFileMap);
+        }
+        if(tpls !=null) {
+            tpls.collect(dir, virtualFileMap);
+        }
+
+        if (attachs != null) {
+            attachs.collect(dir, virtualFileMap);
+        }
+
+        if (annotsDir != null) {
+            annotsDir.collect(dir, virtualFileMap);
+        }
+
+        return virtualFileMap;
+    }
+
+    /**
+     * 注释目录
+     */
+	public AnnotsDir getAnnotsDir() {
+		return annotsDir;
+	}
+
+    /**
+     * 注释目录
+     */
+	public void setAnnotsDir(AnnotsDir annotsDir) {
+		this.annotsDir = annotsDir;
+	}
+    
+    
+}

+ 82 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/DocObjDump.java

@@ -0,0 +1,82 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.io.XMLWriter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+/**
+ * 对象序列化
+ *
+ * @author 权观宇
+ * @since 2020-01-20 14:45:02
+ */
+public class DocObjDump {
+
+    /**
+     * 序列化文档对象
+     *
+     * @param e  文档对象
+     * @param to 目标目录
+     * @throws IOException IO异常
+     */
+    public static void dump(Element e, Path to) throws IOException {
+        if (e == null) {
+            return;
+        }
+        if (to == null) {
+            throw new IllegalArgumentException("文档元素序列化目标目录(to)为空");
+        }
+        if (Files.notExists(to)) {
+            if (Files.notExists(to.getParent())) {
+                Files.createDirectories(to.getParent());
+            }
+            Files.createFile(to);
+        }
+
+        Document doc = DocumentHelper.createDocument();
+        //System.out.println(e.asXML());
+        //System.out.println(doc.asXML());
+        doc.add(e);
+        try (OutputStream out = Files.newOutputStream(to)) {
+            XMLWriter writeToFile = new XMLWriter(out);
+            writeToFile.write(doc);
+            writeToFile.close();
+        }
+    }
+
+    /**
+     * 序列化文档对象
+     *
+     * @Author: Lianglx
+     * @Description: 把文档对象的二进制数据dump到map中,不写临时文件
+     *
+     * @param e
+     * @param loc
+     * @param virtualFileMap
+     * @throws IOException
+     */
+    public static void dump(Element e, String loc, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (e == null) {
+            return;
+        }
+
+        Document doc = DocumentHelper.createDocument();
+        //System.out.println(e.asXML());
+        //System.out.println(doc.asXML());
+        doc.add(e);
+        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            XMLWriter writeToFile = new XMLWriter(bos);
+            writeToFile.write(doc);
+            writeToFile.close();
+            virtualFileMap.put(loc, bos.toByteArray());
+        }
+    }
+}

+ 244 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDCreator.java

@@ -0,0 +1,244 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.basicStructure.doc.CT_CommonData;
+import org.ofdrw.core.basicStructure.doc.CT_PageArea;
+import org.ofdrw.core.basicStructure.doc.Document;
+import org.ofdrw.core.basicStructure.ofd.DocBody;
+import org.ofdrw.core.basicStructure.ofd.OFD;
+import org.ofdrw.core.basicStructure.ofd.docInfo.CT_DocInfo;
+import org.ofdrw.core.basicStructure.pageObj.Content;
+import org.ofdrw.core.basicStructure.pageObj.layer.CT_Layer;
+import org.ofdrw.core.basicStructure.pageTree.Page;
+import org.ofdrw.core.basicStructure.pageTree.Pages;
+import org.ofdrw.core.basicStructure.res.CT_MultiMedia;
+import org.ofdrw.core.basicStructure.res.MediaType;
+import org.ofdrw.core.basicStructure.res.Res;
+import org.ofdrw.core.basicStructure.res.resources.ColorSpaces;
+import org.ofdrw.core.basicStructure.res.resources.Fonts;
+import org.ofdrw.core.basicStructure.res.resources.MultiMedias;
+import org.ofdrw.core.basicType.ST_ID;
+import org.ofdrw.core.basicType.ST_Loc;
+import org.ofdrw.core.basicType.ST_RefID;
+import org.ofdrw.core.pageDescription.color.colorSpace.BitsPerComponent;
+import org.ofdrw.core.pageDescription.color.colorSpace.CT_ColorSpace;
+import org.ofdrw.core.pageDescription.color.colorSpace.OFDColorSpaceType;
+import org.ofdrw.core.text.font.CT_Font;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class OFDCreator {
+    private Map<String, String> fontMap;
+    private Map<String, String> imageMap;
+    private OFDDir ofdDir;
+
+    private DocDir docDir;
+
+    private long localRID = OfdResIdDefine.RID_VarStart;
+
+    public long getNextRid() {
+        return ++localRID;
+    }
+
+    public long getCurrRid() {
+        return localRID;
+    }
+
+    public Map<String, String> getFontMap() {
+        return fontMap;
+    }
+
+    public Map<String, String> getImageMap() {
+        return imageMap;
+    }
+
+    private MultiMedias mms;
+
+    public OFDCreator() {
+        fontMap = new HashMap<>();
+        imageMap = new HashMap<>();
+        ofdDir = new OFDDir();
+
+        //生成ofd.xml文件
+        OFD ofd = new OFD();
+        ofd.attribute("Version").setValue("1.1");
+
+        DocBody docBody = new DocBody();
+        docBody.setDocRoot(new ST_Loc("Doc_0/Document.xml"));
+
+        CT_DocInfo docInfo = new CT_DocInfo();
+        docInfo.setDocID(UUID.randomUUID());
+        docInfo.setCreatorVersion("1.0");
+        docInfo.setAuthor("OFD");
+        docInfo.setCreationDate(LocalDate.now());
+        docInfo.setCreator("OFD");
+        docBody.setDocInfo(docInfo);
+        ofd.addDocBody(docBody);
+        ofdDir.add(this.genDocDir());
+        ofdDir.setOfd(ofd);
+    }
+
+    private DocDir genDocDir() {
+        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        docDir = new DocDir();
+        docDir.setPublicRes(genPublicRes());
+        docDir.setDocumentRes(genDocumentRes());
+        Document doc = genDoc();
+        docDir.setDocument(doc);
+
+        PagesDir pagesDir = new PagesDir();
+        docDir.setPages(pagesDir);
+        return docDir;
+    }
+
+    private Res genPublicRes() {
+        Res ret = new Res();
+        ret.setBaseLoc(new ST_Loc("Res"));
+        CT_ColorSpace clrSpace = new CT_ColorSpace(OFDColorSpaceType.RGB, OfdResIdDefine.RID_ColorSpace);
+        clrSpace.setBitsPerComponent(BitsPerComponent.BIT_8);
+        ColorSpaces clrSpaces = new ColorSpaces();
+        clrSpaces.addColorSpace(clrSpace);
+        ret.addResource(clrSpaces);
+
+        Fonts fonts = new Fonts();
+        ret.addResource(fonts);
+
+        return ret;
+    }
+
+    private Res genDocumentRes() {
+        Res ret = new Res();
+        ret.setBaseLoc(new ST_Loc("Res"));
+
+        mms = new MultiMedias();
+        ret.add(mms);
+
+        return ret;
+    }
+
+    public Document genDoc() {
+        Document doc = new Document();
+
+        //1. 创建commonData
+        CT_CommonData commonData = new CT_CommonData();
+        commonData.setDefaultCS(new ST_RefID(OfdResIdDefine.RID_ColorSpace));
+
+        //1.1页面区域
+        CT_PageArea pageArea = new CT_PageArea();
+        pageArea.setPhysicalBox(0, 0, 210, 297);
+        pageArea.setApplicationBox(0, 0, 210, 297);
+        pageArea.setContentBox(0, 0, 210, 297);
+        commonData.setPageArea(pageArea);
+        commonData.setPublicRes(new ST_Loc("PublicRes.xml"));
+        commonData.setDocumentRes(new ST_Loc("DocumentRes.xml"));
+        doc.setCommonData(commonData);
+
+        Pages pages = new Pages();
+        doc.setPages(pages);
+        return doc;
+    }
+
+    public String putFont(String familyName, String fontName, byte[] fontBytes, String suffix) {
+        String fontId = this.fontMap.get(fontName);
+        if (this.fontMap.get(fontName) == null) {
+            long currentId = this.getNextRid();
+            CT_Font fntKt = new CT_Font();
+            fntKt.setFamilyName(familyName);
+            fntKt.setFontName(fontName);
+            fntKt.setID(currentId);
+            if (fontBytes != null) {
+                fntKt.setFontFile(new ST_Loc(fontName + suffix));
+                docDir.addResource(fontName + suffix, fontBytes);
+            }
+            Fonts fonts = this.ofdDir.getDocDefault().getPublicRes().getFonts().get(0);
+            if (fonts != null) {
+                this.ofdDir.getDocDefault().getPublicRes().getFonts().get(0).addFont(fntKt);
+            }
+            fontId = String.valueOf(currentId);
+            this.fontMap.put(fontName, fontId);
+        }
+        return fontId;
+    }
+
+    // 以hashcode作为文件名,防止字体重名
+    public String putFont(String fontHash, String familyName, String fontName, byte[] fontBytes, String suffix) {
+        String fontId = this.fontMap.get(fontHash);
+        if (fontId == null) {
+            long currentId = this.getNextRid();
+            CT_Font fntKt = new CT_Font();
+            fntKt.setFamilyName(familyName);
+            fntKt.setFontName(fontName);
+            fntKt.setID(currentId);
+            if (fontBytes != null) {
+                fntKt.setFontFile(new ST_Loc("font_" + currentId + suffix));
+                docDir.addResource("font_" + currentId + suffix, fontBytes);
+            }
+            Fonts fonts = this.ofdDir.getDocDefault().getPublicRes().getFonts().get(0);
+            if (fonts != null) {
+                this.ofdDir.getDocDefault().getPublicRes().getFonts().get(0).addFont(fntKt);
+            }
+            fontId = String.valueOf(currentId);
+            this.fontMap.put(fontHash, fontId);
+        }
+        return fontId;
+    }
+
+    public void putImage(String name, byte[] imageBytes, String suffix) {
+        if (this.imageMap.get(name) == null) {
+            long currentId = this.getNextRid();
+            CT_MultiMedia mmEwm = new CT_MultiMedia();
+            mmEwm.setID(new ST_ID(currentId));
+            mmEwm.setType(MediaType.Image);
+            mmEwm.setFormat(suffix.toUpperCase());
+            mmEwm.setMediaFile(new ST_Loc(name));
+            mms.addMultiMedia(mmEwm);
+            docDir.addResource(name, imageBytes);
+            this.imageMap.put(name, String.valueOf(currentId));
+        }
+    }
+
+    public void addPage(int idx) {
+        Page page = new Page(getNextRid(), String.format("Pages/Page_%d/Content.xml", idx));
+        docDir.getDocument().getPages().addPage(page);
+    }
+
+
+    public CT_Layer createLayer() {
+        CT_Layer layerInv = new CT_Layer();
+        layerInv.setObjID(new ST_ID(getNextRid()));
+        return layerInv;
+    }
+
+    public void addPageContent(int idx, CT_Layer ctLayer, float width, float height) {
+        PageDir pageDirInv = new PageDir();
+        pageDirInv.setIndex(idx);
+        org.ofdrw.core.basicStructure.pageObj.Page pageInv = new org.ofdrw.core.basicStructure.pageObj.Page();
+        CT_PageArea areaInv = new CT_PageArea();
+        areaInv.setPhysicalBox(0, 0, width, height);
+        pageInv.setArea(areaInv);
+        Content contentInv = new Content();
+        contentInv.addLayer(ctLayer);
+        pageInv.setContent(contentInv);
+        pageDirInv.setContent(pageInv);
+        docDir.getPages().add(pageDirInv);
+    }
+
+    public byte[] jar() throws IOException {
+        docDir.getDocument().getCommonData().setMaxUnitID(getCurrRid());
+        Map<String, byte[]> virtualFileMap = new ConcurrentHashMap<>(64);
+        return ofdDir.jar(virtualFileMap);
+    }
+
+    public void jar(OutputStream output) throws IOException {
+        docDir.getDocument().getCommonData().setMaxUnitID(getCurrRid());
+        Map<String, byte[]> virtualFileMap = new ConcurrentHashMap<>(64);
+        ofdDir.jar(virtualFileMap, output);
+    }
+}

+ 236 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDDir.java

@@ -0,0 +1,236 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+
+import org.ofdrw.core.basicStructure.ofd.OFD;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * OFD文档对象
+ * <p>
+ * 1. 创建目录
+ * 2. 复制文件
+ * 3. 序列化文件
+ *
+ * @author 权观宇
+ * @since 2020-01-18 13:00:45
+ */
+public class OFDDir implements DirCollect {
+    /**
+     * 文档容器列表
+     */
+    private List<DocDir> container;
+
+    /**
+     * 文档主入口文件对象
+     */
+    private OFD ofd;
+
+    public OFDDir() {
+        container = new ArrayList<>(1);
+    }
+
+    /**
+     * @return 文档主入口文件对象
+     */
+    public OFD getOfd() {
+        return ofd;
+    }
+
+    /**
+     * 设置  文档主入口文件对象
+     *
+     * @param ofd 文档主入口文件对象
+     * @return this
+     */
+    public OFDDir setOfd(OFD ofd) {
+        this.ofd = ofd;
+        return this;
+    }
+
+    /**
+     * 增加文档容器
+     *
+     * @param docDir 文档容器
+     * @return this
+     */
+    public OFDDir add(DocDir docDir) {
+        if (docDir == null) {
+            return this;
+        }
+        if (container == null) {
+            container = new ArrayList<>(1);
+        }
+        container.add(docDir);
+        return this;
+    }
+
+    /**
+     * 获取文档容器
+     *
+     * @param numberOf 第几个
+     * @return 文档容器
+     */
+    public DocDir getDoc(Integer numberOf) {
+        if (container == null) {
+            return null;
+        }
+        for (DocDir docDir : this.container) {
+            if (docDir.getIndex().equals(numberOf)) {
+                return docDir;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取第一个文档容器作为默认
+     *
+     * @return 第一个文档容器
+     */
+    public DocDir getDocDefault() {
+        return getDoc(0);
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param dir 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String dir) throws IOException {
+        if (ofd == null) {
+            throw new IllegalArgumentException("文件主入口文件(ofd)为空");
+        }
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("文档(doc)为空");
+        }
+
+        Path path = Paths.get(dir);
+        Files.createDirectories(path);
+        for (DocDir doc : container) {
+            doc.collect(dir);
+        }
+        DocObjDump.dump(ofd, Paths.get(dir, "OFD.xml"));
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (ofd == null) {
+            throw new IllegalArgumentException("文件主入口文件(ofd)为空");
+        }
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("文档(doc)为空");
+        }
+
+        for (DocDir doc : container) {
+            doc.collect(base, virtualFileMap);
+        }
+        DocObjDump.dump(ofd, "OFD.xml", virtualFileMap);
+
+        return virtualFileMap;
+    }
+
+
+    /**
+     * 打包成OFD
+     * <p>
+     * 1. 创建文件夹复制文件
+     * 2. 打包
+     * 3. 删除临时文件
+     * <p>
+     * 使用操作系统临时文件夹作为生成OFD文件的临时路径
+     *
+     * @param fileName OFD文件名称路径(含后缀名)
+     * @throws IOException IO异常
+     */
+    public void jar(String fileName) throws IOException {
+        String tmp = System.getProperty("java.io.tmpdir");
+        jar(tmp, fileName);
+    }
+
+    /**
+     * 打包成OFD
+     * <p>
+     * 1. 创建文件夹复制文件
+     * 2. 打包
+     * 3. 删除临时文件
+     *
+     * @param base     临时文件生成目录
+     * @param fileName OFD文件名称路径(含后缀名)
+     * @throws IOException IO异常
+     */
+    public void jar(String base, String fileName) throws IOException {
+        Path tmpPath = null;
+        try {
+            if (fileName == null || fileName.trim().length() == 0) {
+                throw new IllegalArgumentException("OFD文件名(fileName)不能为空");
+            }
+            Path target = Paths.get(fileName);
+            if (Files.exists(target)) {
+                Files.delete(target);
+            }
+
+            Path basePath = Paths.get(base);
+            if (Files.notExists(basePath)) {
+                // 基础目录不存在则创建
+                basePath = Files.createDirectories(basePath);
+            }
+            // 在基础目录下创建临时目录用于生成OFD
+            tmpPath = Files.createTempDirectory(basePath, "ofd-tmp-");
+            String workDir = tmpPath.toAbsolutePath().toString();
+            // 创建文件夹复制文件
+            Path workDirPath = this.collect(workDir);
+            // 打包OFD文件
+            ZipUtil.zip(workDirPath.toFile(), fileName);
+        } finally {
+            if (tmpPath != null) {
+                try (Stream<Path> walk = Files.walk(tmpPath)) {
+                    walk.sorted(Comparator.reverseOrder())
+                            .map(Path::toFile)
+                            .forEach(File::delete);
+                }
+            }
+        }
+    }
+
+    /**
+     * 打包成OFD数据
+     *
+     * @param virtualFileMap
+     * @return
+     * @throws IOException
+     */
+    public byte[] jar(Map<String, byte[]> virtualFileMap) throws IOException {
+        virtualFileMap = this.collect("", virtualFileMap);
+        // 打包OFD文件
+        return ZipUtil.zip(virtualFileMap);
+    }
+
+    /**
+     * 打包成OFD数据
+     *
+     * @param virtualFileMap
+     * @return
+     * @throws IOException
+     */
+    public void jar(Map<String, byte[]> virtualFileMap, OutputStream output) throws IOException {
+        virtualFileMap = this.collect("", virtualFileMap);
+        // 打包OFD文件
+        ZipUtil.zip(virtualFileMap, output);
+    }
+
+}

+ 910 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDPageDrawer.java

@@ -0,0 +1,910 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+
+import org.apache.commons.io.IOUtils;
+import org.apache.fontbox.ttf.CmapLookup;
+import org.apache.fontbox.ttf.TrueTypeFont;
+import org.apache.fontbox.util.BoundingBox;
+import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDStream;
+import org.apache.pdfbox.pdmodel.font.*;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
+import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
+import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
+import org.apache.pdfbox.text.TextPosition;
+import org.apache.pdfbox.util.Matrix;
+import org.apache.pdfbox.util.Vector;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.MD5Digest;
+import org.dom4j.Element;
+import org.ofdrw.core.OFDElement;
+import org.ofdrw.core.basicStructure.pageObj.layer.CT_Layer;
+import org.ofdrw.core.basicStructure.pageObj.layer.block.ImageObject;
+import org.ofdrw.core.basicStructure.pageObj.layer.block.PathObject;
+import org.ofdrw.core.basicStructure.pageObj.layer.block.TextObject;
+import org.ofdrw.core.basicType.ST_Array;
+import org.ofdrw.core.basicType.ST_ID;
+import org.ofdrw.core.basicType.ST_RefID;
+import org.ofdrw.core.graph.pathObj.AbbreviatedData;
+import org.ofdrw.core.graph.pathObj.CT_Path;
+import org.ofdrw.core.pageDescription.clips.CT_Clip;
+import org.ofdrw.core.pageDescription.clips.Clips;
+import org.ofdrw.core.pageDescription.color.color.CT_Color;
+import org.ofdrw.core.text.CT_CGTransform;
+import org.ofdrw.core.text.TextCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.imageio.ImageIO;
+import java.awt.geom.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class OFDPageDrawer extends PDFGraphicsStreamEngine {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    private GeneralPath linePath = new GeneralPath();
+    private int clipWindingRule = -1;
+
+    private OFDCreator ofdCreator;
+    private CT_Layer ctLayer;
+    private float scale;
+    private PDPage page;
+    private float PX2MM = 2.834175f;
+    private int pageRotation;
+
+    private Area lastClip;
+
+    public CT_Layer getCtLayer() {
+        // 获取时,从队列添加
+        for (Element ele : queue) {
+            ctLayer.add(ele);
+        }
+        return ctLayer;
+    }
+
+    protected OFDPageDrawer(int idx, PDPage page, OFDCreator ofdCreator, float scale) throws IOException {
+        super(page);
+        this.page = page;
+        this.ofdCreator = ofdCreator;
+        ctLayer = this.ofdCreator.createLayer();
+        this.scale = scale;
+    }
+
+    public void drawPage() throws IOException {
+        processPage(this.getPage());
+    }
+
+
+    @Override
+    public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException {
+        linePath.moveTo((float) p0.getX(), (float) p0.getY());
+        linePath.lineTo((float) p1.getX(), (float) p1.getY());
+        linePath.lineTo((float) p2.getX(), (float) p2.getY());
+        linePath.lineTo((float) p3.getX(), (float) p3.getY());
+        linePath.closePath();
+    }
+
+    @Override
+    public void drawImage(PDImage pdImage) throws IOException {
+        ByteArrayOutputStream bosImage = new ByteArrayOutputStream();
+        String suffix = "png";
+        ImageIO.write(pdImage.getImage(), suffix, bosImage);
+        String name = String.format("%s.%s", bcMD5(bosImage.toByteArray()), suffix);
+        ofdCreator.putImage(name, bosImage.toByteArray(), suffix);
+
+        Matrix ctmNew = this.getGraphicsState().getCurrentTransformationMatrix();
+        float imageXScale = ctmNew.getScalingFactorX();
+        float imageYScale = ctmNew.getScalingFactorY();
+        double x = ctmNew.getTranslateX() * scale;
+        double y = (page.getCropBox().getHeight() - ctmNew.getTranslateY() - imageYScale) * scale;
+        double w = imageXScale * scale;
+        double h = imageYScale * scale;
+        ImageObject imageObject = new ImageObject(ofdCreator.getNextRid());
+        imageObject.setBoundary(x, y, w, h);
+        imageObject.setResourceID(new ST_RefID(ST_ID.getInstance(ofdCreator.getImageMap().get(name))));
+        imageObject.setCTM(ST_Array.getInstance(String.format("%.0f 0 0 %.0f 0 0", w, h)));
+        setImageClip(imageObject, x, y, w, h);
+        ctLayer.add(imageObject);
+    }
+
+    private String getPointsFromClipArea(Area area, double x, double y, double w, double h) {
+        String abbreviatedData = "";
+        double[] coords = new double[6];
+        for (PathIterator pi = area.getPathIterator(null);
+             !pi.isDone();
+             pi.next()) {
+            switch (pi.currentSegment(coords)) {
+                case PathIterator.SEG_MOVETO:
+                    abbreviatedData += String.format("M %.1f %.1f ", (coords[0] * scale - x) / w, ((page.getCropBox().getHeight() - coords[1]) * scale - y) / h);
+                    break;
+                case PathIterator.SEG_LINETO:
+                    abbreviatedData += String.format("L %.1f %.1f ", (coords[0] * scale - x) / w, ((page.getCropBox().getHeight() - coords[1]) * scale - y) / h);
+                    break;
+                case PathIterator.SEG_CUBICTO:
+                    abbreviatedData += String.format("B %.1f %.1f %.1f %.1f %.1f %.1f ", (coords[0] * scale - x) / w, ((page.getCropBox().getHeight() - coords[1]) * scale - y) / h, (coords[2] * scale - x) / w, ((page.getCropBox().getHeight() - coords[3]) * scale - y) / h, (coords[4] * scale - x) / w, ((page.getCropBox().getHeight() - coords[5]) * scale - y) / h);
+                    break;
+                case PathIterator.SEG_CLOSE:
+                    abbreviatedData += " C";
+            }
+        }
+        return abbreviatedData;
+    }
+
+    private void setImageClip(ImageObject imageObject, double ix, double iy, double iw, double ih) {
+        Area clippingPath = this.getGraphicsState().getCurrentClippingPath();
+        if (clippingPath != lastClip) {
+            if (clippingPath.getPathIterator((AffineTransform) null).isDone()) {
+                return;
+            } else {
+                double w = (clippingPath.getBounds().width * scale);
+                double h = (clippingPath.getBounds().height * scale);
+                double x = (clippingPath.getBounds().x * scale);
+                double y = (page.getCropBox().getHeight() - clippingPath.getBounds().y - clippingPath.getBounds().height) * scale;
+                x = Double.parseDouble(String.format("%.2f", x));
+                y = Double.parseDouble(String.format("%.2f", y));
+                w = Double.parseDouble(String.format("%.0f", w));
+                h = Double.parseDouble(String.format("%.0f", h));
+                double clipX = Math.abs(x - ix) / iw;
+                double clipY = Math.abs(y - iy) / ih;
+                double clipW = w / iw;
+                double clipH = h / ih;
+                lastClip = clippingPath;
+                String abbreviatedData = getPointsFromClipArea(clippingPath, x, y, w, h);
+                if (abbreviatedData.contains("B") || (clipX >= 0 && clipY >= 0 && (clipW >= 0 && clipW <= 1) && (clipH >= 0 && clipH <= 1))) {
+                    OFDElement abbreviatedDataEle = OFDElement.getInstance("AbbreviatedData");
+                    abbreviatedDataEle.setText(abbreviatedData);
+                    CT_Path ctPath = new CT_Path();
+                    ctPath.setFill(true);
+                    ctPath.setStroke(false);
+                    ctPath.setBoundary(clipX, clipY, clipW, clipH);
+                    ctPath.add(abbreviatedDataEle);
+                    org.ofdrw.core.pageDescription.clips.Area area = new org.ofdrw.core.pageDescription.clips.Area();
+                    area.add(ctPath);
+                    CT_Clip ct_clip = new CT_Clip();
+                    ct_clip.addArea(area);
+                    Clips clips = new Clips();
+                    clips.addClip(ct_clip);
+                    imageObject.setClips(clips);
+                }
+            }
+        }
+    }
+
+    private String bcMD5(byte[] imageBytes) {
+        Digest digest = new MD5Digest();
+        digest.update(imageBytes, 0, imageBytes.length);
+        byte[] md5Bytes = new byte[digest.getDigestSize()];
+        digest.doFinal(md5Bytes, 0);
+        return org.bouncycastle.util.encoders.Hex.toHexString(md5Bytes);
+    }
+
+    @Override
+    public void clip(int windingRule) throws IOException {
+        clipWindingRule = windingRule;
+    }
+
+    @Override
+    public void moveTo(float x, float y) throws IOException {
+        linePath.moveTo(x, y);
+    }
+
+    @Override
+    public void lineTo(float x, float y) throws IOException {
+        linePath.lineTo(x, y);
+    }
+
+    @Override
+    public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException {
+        linePath.curveTo(x1, y1, x2, y2, x3, y3);
+    }
+
+    @Override
+    public Point2D getCurrentPoint() throws IOException {
+        return linePath.getCurrentPoint();
+    }
+
+    @Override
+    public void closePath() throws IOException {
+        linePath.closePath();
+    }
+
+    @Override
+    public void endPath() throws IOException {
+        if (clipWindingRule != -1) {
+            linePath.setWindingRule(clipWindingRule);
+            getGraphicsState().intersectClippingPath(linePath);
+            clipWindingRule = -1;
+        }
+        linePath.reset();
+    }
+
+    @Override
+    public void strokePath() throws IOException {
+        ctLayer.add(getPathObject(false));
+        linePath.reset();
+    }
+
+    @Override
+    public void fillPath(int i) throws IOException {
+        ctLayer.add(getPathObject(true));
+        linePath.reset();
+    }
+
+    /**
+     * 获取路径对象
+     */
+    private PathObject getPathObject(boolean fill) throws IOException {
+        double x = 0, y = 0;
+        float w = page.getCropBox().getWidth() * scale;
+        float h = page.getCropBox().getHeight() * scale;
+        double lineWidth = getGraphicsState().getLineWidth() * scale;
+
+        PathObject path = new PathObject(ST_ID.getInstance(String.valueOf(ofdCreator.getNextRid())));
+        path.setLineWidth(lineWidth);
+        path.setBoundary(x, y, w, h);
+        AbbreviatedData data = new AbbreviatedData();
+        drawLine(linePath.getPathIterator(null), data, h);
+        if (fill) {
+            CT_Color nonStrokeColor = getNonStrokeColor();
+            if (nonStrokeColor != null) {
+                path.setFillColor(nonStrokeColor);
+                path.setFill(true);
+                path.setStroke(false);
+            }
+        } else {
+            CT_Color strokeColor = getStrokeColor();
+            if (strokeColor != null) {
+                path.setStroke(true);
+                path.setFill(false);
+                path.setStrokeColor(strokeColor);
+            } else {
+                path.setStroke(false);
+            }
+        }
+        path.setAbbreviatedData(data);
+        return path;
+    }
+
+    /**
+     * 获取非stroke颜色
+     */
+    private CT_Color getNonStrokeColor() throws IOException {
+        PDColor color = getGraphicsState().getNonStrokingColor();
+        if (color == null) {
+            return null;
+        }
+        return getColor(color);
+    }
+
+    /**
+     * 获取stroke颜色
+     */
+    private CT_Color getStrokeColor() throws IOException {
+        PDColor color = getGraphicsState().getStrokingColor();
+        if (color == null) {
+            return null;
+        }
+        return getColor(color);
+    }
+
+    /**
+     * 画线
+     */
+    private void drawLine(PathIterator iterator, AbbreviatedData data, float height) throws IOException {
+        double[] coords = new double[6];
+        while (!iterator.isDone()) {
+            switch (iterator.currentSegment(coords)) {
+                case PathIterator.SEG_MOVETO:
+                    data.moveTo(coords[0] * scale, height - coords[1] * scale);
+                    break;
+                case PathIterator.SEG_LINETO:
+                    data.lineTo(coords[0] * scale, height - coords[1] * scale);
+                    break;
+                case PathIterator.SEG_CUBICTO:
+                    data.B(coords[0] * scale, height - coords[1] * scale,
+                            coords[2] * scale, height - coords[3] * scale,
+                            coords[4] * scale, height - coords[5] * scale);
+                    break;
+                case PathIterator.SEG_CLOSE:
+                    data.close();
+                    closePath();
+                    break;
+                default:
+                    break;
+            }
+            iterator.next();
+        }
+    }
+
+    @Override
+    public void fillAndStrokePath(int windingRule) throws IOException {
+        GeneralPath path = (GeneralPath) this.linePath.clone();
+        this.fillPath(windingRule);
+        this.linePath = path;
+        this.strokePath();
+    }
+
+    @Override
+    public void shadingFill(COSName cosName) throws IOException {
+
+    }
+
+    private TextObject $textObj = null;
+    private TextCode $textCode = null;
+    private Double $deltaX = 0d;
+    private List<String> $deltaXList = null;
+    private Double $deltaY = 0d;
+    private List<String> $deltaYList = null;
+    private StringBuffer $text = null;
+    // 字符索引
+    private int $stringIndex = 0;
+    private List<String> $glyphs = null;
+    private CmapLookup $cmap = null;
+    private byte[] textByte;
+    // 字体转换索引
+    private List<String> $cgTransforms = null;
+    /**
+     * 元素队列
+     */
+    private List<Element> queue = new ArrayList<Element>();
+
+    @Override
+    protected void showText(byte[] string) throws IOException {
+        // 初始化
+        $stringIndex = 0;
+        $textObj = null;
+        $textCode = null;
+        $deltaX = 0d;
+        $deltaY = 0d;
+        $text = new StringBuffer();
+        $glyphs = new ArrayList<String>();
+        $deltaXList = new ArrayList<String>();
+        $deltaYList = new ArrayList<String>();
+        $cgTransforms = new ArrayList<String>();
+        $cmap = null;
+        textByte = string;
+        // 处理文字
+        super.showText(string);
+
+        // 完成添加文字
+        String text = $text.toString();
+        if (null == text || "".equals(text)) {
+            return;
+        }
+        if ($glyphs != null && $glyphs.size() > 0) {
+            // 添加字符变换
+            addCGTransform(text);
+        }
+
+        if ($deltaXList != null && $deltaXList.size() > 0) {
+            // 宽度根据所有字体累加
+            Double deltaWidth = $textObj.getBoundary().getWidth();
+            for (String w : $deltaXList) {
+                deltaWidth += Double.valueOf(w);
+            }
+            $textObj.setBoundary($textObj.getBoundary().setWidth(deltaWidth));
+
+            $textCode.setDeltaX(new ST_Array($deltaXList.toArray(new String[$deltaXList.size()])));
+        }
+
+        if ($deltaYList != null && $deltaYList.size() > 0) {
+            // 高度根据所有字体累加
+            Double deltaHeight = $textObj.getBoundary().getHeight();
+            for (String h : $deltaYList) {
+                deltaHeight += Double.valueOf(h);
+            }
+            $textObj.setBoundary($textObj.getBoundary().setHeight(deltaHeight));
+            $textCode.setDeltaY(new ST_Array($deltaYList.toArray(new String[$deltaYList.size()])));
+        }
+
+        // 扩展
+        drawTextParam();
+        $textCode.setContent(text);
+        $textObj.addTextCode($textCode);
+        queue.add($textObj);
+    }
+
+    /**
+     * Glyph bounding boxes.
+     */
+    @Override
+    protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code,
+                             Vector displacement) throws IOException {
+        String unicode = font.toUnicode(code);
+        boolean isUnicode = false;
+        if (null == unicode || "".equals(unicode)) {
+            unicode = "¤";// 占位符
+            isUnicode = true;
+        }
+
+        $cgTransforms.add($stringIndex + "-" + unicode.length());
+        PDGraphicsState state = getGraphicsState();
+        RenderingMode renderingMode = state.getTextState().getRenderingMode();
+        Matrix ctm = state.getCurrentTransformationMatrix();
+        float fontSize = state.getTextState().getFontSize();
+        float horizontalScaling = state.getTextState().getHorizontalScaling()
+                / 100f;
+        Matrix textMatrix = getTextMatrix();
+
+        double angle = getRotate(textRenderingMatrix);
+        BoundingBox bbox = font.getBoundingBox();
+        if (bbox.getLowerLeftY() < Short.MIN_VALUE) {
+            // PDFBOX-2158 and PDFBOX-3130
+            // files by Salmat eSolutions / ClibPDF Library
+            bbox.setLowerLeftY(-(bbox.getLowerLeftY() + 65536));
+        }
+        // 1/2 the bbox is used as the height todo: why?
+        float glyphHeight = bbox.getHeight() / 2;
+
+        // sometimes the bbox has very high values, but CapHeight is OK
+        PDFontDescriptor fontDescriptor = font.getFontDescriptor();
+        if (fontDescriptor != null) {
+            float capHeight = fontDescriptor.getCapHeight();
+            if (Float.compare(capHeight, 0) != 0 && (capHeight < glyphHeight
+                    || Float.compare(glyphHeight, 0) == 0)) {
+                glyphHeight = capHeight;
+            }
+            // PDFBOX-3464, PDFBOX-4480, PDFBOX-4553:
+            // sometimes even CapHeight has very high value, but Ascent and
+            // Descent are ok
+            float ascent = fontDescriptor.getAscent();
+            float descent = fontDescriptor.getDescent();
+            if (capHeight > ascent && ascent > 0 && descent < 0
+                    && ((ascent - descent) / 2 < glyphHeight
+                    || Float.compare(glyphHeight, 0) == 0)) {
+                glyphHeight = (ascent - descent) / 2;
+            }
+        }
+
+        // transformPoint from glyph space -> text space
+        float height;
+        if (font instanceof PDType3Font) {
+            height = font.getFontMatrix().transformPoint(0, glyphHeight).y;
+        } else {
+            height = glyphHeight / 1000;
+        }
+
+        float displacementX = displacement.getX();
+        // the sorting algorithm is based on the width of the character. As the
+        // displacement
+        // for vertical characters doesn't provide any suitable value for it, we
+        // have to
+        // calculate our own
+        TrueTypeFont ttf = null;
+        if (font instanceof PDTrueTypeFont) {
+            ttf = ((PDTrueTypeFont) font).getTrueTypeFont();
+        } else if (font instanceof PDType0Font) {
+            PDCIDFont cidFont = ((PDType0Font) font).getDescendantFont();
+            if (cidFont instanceof PDCIDFontType2) {
+                ttf = ((PDCIDFontType2) cidFont).getTrueTypeFont();
+            }
+        }
+
+        if (font.isVertical()) {
+            displacementX = font.getWidth(code) / 1000;
+            // there may be an additional scaling factor for true type fonts
+            if (ttf != null && ttf.getUnitsPerEm() != 1000) {
+                displacementX *= 1000f / ttf.getUnitsPerEm();
+            }
+        }
+
+        //
+        // legacy calculations which were previously in PDFStreamEngine
+        //
+        // DO NOT USE THIS CODE UNLESS YOU ARE WORKING WITH PDFTextStripper.
+        // THIS CODE IS DELIBERATELY INCORRECT
+        //
+
+        // (modified) combined displacement, this is calculated *without* taking
+        // the character
+        // spacing and word spacing into account, due to legacy code in
+        // TextStripper
+        float tx = displacementX * fontSize * horizontalScaling;
+        float ty = displacement.getY() * fontSize;
+
+        // (modified) combined displacement matrix
+        Matrix td = Matrix.getTranslateInstance(tx, ty);
+
+        // (modified) text rendering matrix
+        Matrix nextTextRenderingMatrix = td.multiply(textMatrix).multiply(ctm); // text
+        // space
+        // ->
+        // device
+        // space
+        float nextX = nextTextRenderingMatrix.getTranslateX();
+        float nextY = nextTextRenderingMatrix.getTranslateY();
+
+        float dxDisplay = nextX - textRenderingMatrix.getTranslateX();
+        float dyDisplay = height * textRenderingMatrix.getScalingFactorY();
+
+        float glyphSpaceToTextSpaceFactor = 1 / 1000f;
+        if (font instanceof PDType3Font) {
+            glyphSpaceToTextSpaceFactor = font.getFontMatrix().getScaleX();
+        }
+
+        float spaceWidthText = 0;
+        try {
+            spaceWidthText = font.getSpaceWidth() * glyphSpaceToTextSpaceFactor;
+        } catch (Throwable exception) {
+            logger.error(exception.getMessage(), exception);
+        }
+
+        if (spaceWidthText == 0) {
+            spaceWidthText = font.getAverageFontWidth()
+                    * glyphSpaceToTextSpaceFactor;
+            spaceWidthText *= .80f;
+        }
+        if (spaceWidthText == 0) {
+            spaceWidthText = 1.0f; // if could not find font, use a generic
+            // value
+        }
+
+        // the space width has to be transformed into display units
+        float spaceWidthDisplay = spaceWidthText
+                * textRenderingMatrix.getScalingFactorX();
+
+        if (angle == 0d) {
+            fontSize = textRenderingMatrix.getScaleY() / PX2MM;
+        }
+        // 修复字体水平旋转,大小为空
+        if (fontSize == 0) {
+            fontSize = dyDisplay / PX2MM;
+        }
+
+        TextPosition textPosition = new TextPosition(pageRotation,
+                page.getCropBox().getWidth(), page.getCropBox().getHeight(), textRenderingMatrix,
+                nextX, nextY, Math.abs(dyDisplay), dxDisplay,
+                Math.abs(spaceWidthDisplay), unicode, new int[]{code}, font,
+                fontSize, (int) (fontSize * textMatrix.getScalingFactorX()));
+        String fontName = "";
+        if (font.getFontDescriptor() != null) {
+            fontName = font.getFontDescriptor().getFontFamily();
+            if (null == fontName || "".equals(fontName)) {
+                fontName = font.getName();
+            }
+        }
+        boolean willBeSubset = font.isEmbedded() || font.willBeSubset();
+        String text = unicode;
+        $text.append(text);
+        int fontInt = 1;
+        if ($stringIndex == 0) {
+            long textID = ofdCreator.getNextRid();
+            $textObj = new TextObject(textID);
+
+            String fontHash = "" + font.hashCode();
+            String fontId = ofdCreator.getFontMap().get(fontHash);
+            if (fontId == null) {
+                byte[] fontBytes = writeFont(font);
+                fontId = ofdCreator.putFont(fontHash, fontName, fontName, fontBytes, ".otf");
+            }
+            fontInt = Integer.valueOf(fontId);
+
+            $textCode = new TextCode();
+            AffineTransform transform = new AffineTransform();
+            transform.rotate(Math.toRadians(angle), textPosition.getWidth(),
+                    textPosition.getHeight());
+
+            if (angle == 0)
+			{
+				$textCode.setX(0d);
+				$textCode.setY((double)fontSize);
+				$textObj.setBoundary(
+								textPosition.getX() / PX2MM,
+						Math.abs(
+								textPosition.getY() / PX2MM
+										- fontSize),
+						textRenderingMatrix.getScaleX()
+								/ PX2MM,
+						dyDisplay).setFont(fontInt)
+						.setSize((double)fontSize);
+			}
+			else
+			{
+				$textCode.setX(0d);
+				$textCode.setY(0d);
+				$textObj.setBoundary(0, 0, textPosition.getX(),
+						textPosition.getY()).setFont(fontInt).setSize(fontSize / (double)PX2MM);
+			}
+
+        }
+
+        Integer glyphId = code;
+        if (font instanceof PDTrueTypeFont) {
+            PDTrueTypeFont ttfFont = (PDTrueTypeFont) font;
+            glyphId = ttfFont.codeToGID(code);
+        } else if (font instanceof PDType1CFont) {
+            PDType1CFont type1CFont = (PDType1CFont) font;
+            String name = type1CFont.codeToName(code);
+            glyphId = type1CFont.getCFFType1Font().nameToGID(name);
+        }
+        if ($stringIndex == 0 && ttf != null && willBeSubset) {
+            try {
+                $cmap = ttf.getUnicodeCmapLookup();
+            } catch (Exception e) {
+                //logger.warn("加载字体异常",e);
+            }
+            /**
+             * 存在cmap,从cmap中获取glyphId,否则code即为glyphId<br>
+             * 详见PDFDebugger查看pdf内部结构方法源码 <br>
+             *
+             * @link org.apache.pdfbox.debugger.PDFDebugger
+             */
+            if ($cmap != null && ttf != null && willBeSubset) {
+                glyphId = $cmap.getGlyphId(text.codePointAt(0));
+            }
+        }
+        // unicode编码字体
+        if (isUnicode || glyphId == 0) {
+            glyphId = code;
+        }
+        if (willBeSubset) {
+            $glyphs.add("" + glyphId);
+        }
+
+        Double topLeftX = textPosition.getX() / (double) PX2MM;
+        if ($stringIndex > 0 && $deltaX.compareTo(topLeftX) != 0) {
+            Double w = topLeftX - $deltaX;
+            $deltaXList.add("" + w);
+        }
+        $deltaX = topLeftX;
+
+        Double topLeftY = textPosition.getY() / (double) PX2MM;
+
+        if ($stringIndex > 0 && $deltaY.compareTo(topLeftY) != 0) {
+            Double h = topLeftY - $deltaY;
+            $deltaYList.add("" + h);
+        }
+        $deltaY = topLeftY;
+
+        Float scaleX = 1f;
+        Float scaleY = 1f;
+        Float lineWidth = state.getLineWidth();
+        // 粗体
+        if (renderingMode.isStroke() && lineWidth != null) {
+            // minimum line width as used by Adobe Reader
+            if (lineWidth < 0.25) {
+                lineWidth = 0.25f;
+            }
+            $textObj.setLineWidth(
+                    lineWidth.doubleValue() / PX2MM);
+        }
+
+        if (angle == 0d) {
+            $textObj.setCTM(
+                    new ST_Array(scaleX, ctm.getShearX() / PX2MM,
+                            ctm.getShearY() / PX2MM, scaleY, 0, 0));
+        } else {
+            $textObj.setCTM(new ST_Array(
+                    nextTextRenderingMatrix.getScaleX(),
+                    nextTextRenderingMatrix.getShearX(),
+                    nextTextRenderingMatrix.getShearY(),
+                    nextTextRenderingMatrix.getScaleY(),
+                    textPosition.getX() / PX2MM,
+                    textPosition.getY() / PX2MM));
+        }
+        if (nextTextRenderingMatrix.getScaleX() != nextTextRenderingMatrix.getScaleY()) {
+            $textObj.setHScale((double) nextTextRenderingMatrix.getScaleX());
+        }
+        $stringIndex++;
+    }
+
+    private byte[] writeFont(PDFont font) {
+        byte[] fontBytes = null;
+        InputStream is = null;
+        try {
+            if (font instanceof PDTrueTypeFont) {
+                PDTrueTypeFont f = (PDTrueTypeFont) font;
+                is = f.getTrueTypeFont().getOriginalData();
+            } else if (font instanceof PDType0Font) {
+                PDType0Font type0Font = (PDType0Font) font;
+                if (type0Font.getDescendantFont() instanceof PDCIDFontType2) {
+                    PDCIDFontType2 ff = (PDCIDFontType2) type0Font
+                            .getDescendantFont();
+                    is = ff.getTrueTypeFont().getOriginalData();
+                } else if (type0Font
+                        .getDescendantFont() instanceof PDCIDFontType0) {
+                    // a Type0 CIDFont contains CFF font
+                    PDCIDFontType0 cidType0Font = (PDCIDFontType0) type0Font
+                            .getDescendantFont();
+                    PDFontDescriptor s = cidType0Font.getFontDescriptor();
+                    PDStream s1 = s.getFontFile();
+                    if (s1 != null) {
+                        is = s1.createInputStream();
+                    }
+                    PDStream s2 = s.getFontFile2();
+                    if (s2 != null) {
+                        is = s2.createInputStream();
+                    }
+                    PDStream s3 = s.getFontFile3();
+                    if (s3 != null) {
+                        is = s3.createInputStream();
+                    }
+                } else {
+                    PDType0Font f = (PDType0Font) font;
+                    PDFontDescriptor s = f.getFontDescriptor();
+                    PDStream s1 = s.getFontFile();
+                    if (s1 != null) {
+                        is = s1.createInputStream();
+                    }
+                    PDStream s2 = s.getFontFile2();
+                    if (s2 != null) {
+                        is = s2.createInputStream();
+                    }
+                    PDStream s3 = s.getFontFile3();
+                    if (s3 != null) {
+                        is = s3.createInputStream();
+                    }
+                }
+            } else if (font instanceof PDType1Font) {
+                PDType1Font f = (PDType1Font) font;
+                PDFontDescriptor s = f.getFontDescriptor();
+                PDStream s1 = s.getFontFile();
+                if (s1 != null) {
+                    is = s1.createInputStream();
+                }
+                PDStream s2 = s.getFontFile2();
+                if (s2 != null) {
+                    is = s2.createInputStream();
+                }
+                PDStream s3 = s.getFontFile3();
+                if (s3 != null) {
+                    is = s3.createInputStream();
+                }
+            } else if (font instanceof PDType1CFont) {
+                PDType1CFont f = (PDType1CFont) font;
+                PDFontDescriptor s = f.getFontDescriptor();
+                PDStream s1 = s.getFontFile();
+                if (s1 != null) {
+                    is = s1.createInputStream();
+                }
+                PDStream s2 = s.getFontFile2();
+                if (s2 != null) {
+                    is = s2.createInputStream();
+                }
+                PDStream s3 = s.getFontFile3();
+                if (s3 != null) {
+                    is = s3.createInputStream();
+                }
+            } else if (font instanceof PDType3Font) {
+                PDType3Font f = (PDType3Font) font;
+                PDFontDescriptor s = f.getFontDescriptor();
+                PDStream s1 = s.getFontFile();
+                if (s1 != null) {
+                    is = s1.createInputStream();
+                }
+                PDStream s2 = s.getFontFile2();
+                if (s2 != null) {
+                    is = s2.createInputStream();
+                }
+                PDStream s3 = s.getFontFile3();
+                if (s3 != null) {
+                    is = s3.createInputStream();
+                }
+            }
+            if (is != null) {
+                fontBytes = IOUtils.toByteArray(is);
+            } else {
+                logger.error("生成字体文件异常:" + font.getClass());
+            }
+        } catch (Exception e) {
+            logger.error("生成字体文件异常:" + font.getName(), e);
+        } finally {
+			IOUtils.closeQuietly(is);
+		}
+        return fontBytes;
+    }
+
+    /**
+     * 绘画文字扩展属性<br/>
+     * <p>
+     * 绘画文字扩展属性
+     * </p>
+     *
+     * @throws IOException 异常
+     */
+    private void drawTextParam() throws IOException {
+        PDGraphicsState state = getGraphicsState();
+        RenderingMode renderingMode = state.getTextState().getRenderingMode();
+
+        setColor($textObj, state.getNonStrokingColor(), true);
+        if (renderingMode.isFill()) {
+            $textObj.setFill(true);
+        }
+        setColor($textObj, state.getStrokingColor(), false);
+        if (renderingMode.isStroke()) {
+            $textObj.setStroke(true);
+        }
+    }
+
+    /**
+     * 计算旋转角度<br/>
+     * <p>
+     * 计算旋转角度
+     * </p>
+     *
+     * @param textMatrix
+     * @return
+     */
+    private double getRotate(Matrix textMatrix) {
+        double angle = 180 * Math.atan2(Float.valueOf(textMatrix.getShearX()),
+                Float.valueOf(textMatrix.getScaleY())) / Math.PI;
+        if (angle < 0) {
+            angle = 360 + angle;
+        }
+        return angle;
+    }
+
+    private void setColor(Object object, PDColor color,
+                          boolean noStroke) throws IOException {
+        if (color == null) {
+            return;
+        }
+        CT_Color ctColor = getColor(color);
+        if (noStroke) {
+            ctColor.setOFDName("FillColor");
+            if (object instanceof TextObject) {
+                TextObject o = (TextObject) object;
+                o.setFillColor(ctColor);
+            } else if (object instanceof PathObject) {
+                PathObject o = (PathObject) object;
+                o.setFillColor(ctColor);
+            } else if (object instanceof CT_Path) {
+                CT_Path o = (CT_Path) object;
+                o.setFillColor(ctColor);
+            }
+        } else {
+            ctColor.setOFDName("StrokeColor");
+            if (object instanceof TextObject) {
+                TextObject o = (TextObject) object;
+                o.setStrokeColor(ctColor);
+            } else if (object instanceof PathObject) {
+                PathObject o = (PathObject) object;
+                o.setStrokeColor(ctColor);
+            } else if (object instanceof CT_Path) {
+                CT_Path o = (CT_Path) object;
+                o.setStrokeColor(ctColor);
+            }
+        }
+    }
+
+    private CT_Color getColor(PDColor color) throws IOException {
+        CT_Color ctColor = new CT_Color();
+        if (color != null) {
+            PDColorSpace colorSpace = color.getColorSpace();
+            float[] rgb = colorSpace.toRGB(color.getComponents());
+            ctColor = CT_Color.rgb(toRgbNumber(rgb[0]), toRgbNumber(rgb[1]), toRgbNumber(rgb[2]));
+        }
+        return ctColor;
+    }
+
+    /**
+	 * 添加字符变换
+	 * 
+	 * @param text
+	 */
+	private void addCGTransform(String text) {
+		int total = text.length();
+        CT_CGTransform cgTransform = new CT_CGTransform();
+        cgTransform.setCodePosition(0);
+        cgTransform.setGlyphCount(total);
+        cgTransform.setCodeCount(total);
+        cgTransform.setGlyphs(new ST_Array($glyphs.toArray(new String[$glyphs.size()])));
+        $textObj.addCGTransform(cgTransform);
+	}
+
+    /**
+     * 转换为rgb
+     */
+    private int toRgbNumber(float color) {
+        return color < 0 ? 0 : (int) ((color > 1 ? 1 : color) * 255);
+    }
+}

+ 87 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/OFDRender.java

@@ -0,0 +1,87 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+public class OFDRender {
+    private static final Logger logger = LoggerFactory.getLogger(OFDRender.class);
+
+    /**
+     * user space units per millimeter
+     */
+    private static final float POINTS_PER_MM = 1 / (10 * 2.54f) * 72;
+
+    public static void convertPdfToOfd(InputStream input, OutputStream output) throws IOException {
+        long start;
+        long end;
+        start = System.currentTimeMillis();
+        try (PDDocument doc = PDDocument.load(input)) {
+            OFDCreator ofdCreator = new OFDCreator();
+            for (int i = 0; i < doc.getNumberOfPages(); i++) {
+                ofdCreator.addPage(i);
+                PDRectangle cropBox = doc.getPage(i).getCropBox();
+                float widthPt = cropBox.getWidth();
+                float heightPt = cropBox.getHeight();
+                OFDPageDrawer ofdPageDrawer = new OFDPageDrawer(i, doc.getPage(i), ofdCreator, 1 / POINTS_PER_MM);
+                ofdPageDrawer.drawPage();
+                ofdCreator.addPageContent(i, ofdPageDrawer.getCtLayer(), widthPt / POINTS_PER_MM, heightPt / POINTS_PER_MM);
+            }
+            ofdCreator.jar(output);
+        }
+        end = System.currentTimeMillis();
+        logger.info("gen ofd speed time {}", end - start);
+    }
+
+    public static byte[] convertPdfToOfd(byte[] pdfBytes) {
+        long start;
+        long end;
+
+        String tempFilePath = generateTempFilePath();
+        PDDocument doc = null;
+        try {
+            FileUtils.writeByteArrayToFile(new File(tempFilePath), pdfBytes);
+            doc = PDDocument.load(new File(tempFilePath));
+            start = System.currentTimeMillis();
+            OFDCreator ofdCreator = new OFDCreator();
+            for (int i = 0; i < doc.getNumberOfPages(); i++) {
+                ofdCreator.addPage(i);
+                PDRectangle cropBox = doc.getPage(i).getCropBox();
+                float widthPt = cropBox.getWidth();
+                float heightPt = cropBox.getHeight();
+                OFDPageDrawer ofdPageDrawer = new OFDPageDrawer(i, doc.getPage(i), ofdCreator, 1 / POINTS_PER_MM);
+                ofdPageDrawer.drawPage();
+                ofdCreator.addPageContent(i, ofdPageDrawer.getCtLayer(), widthPt / POINTS_PER_MM, heightPt / POINTS_PER_MM);
+            }
+            end = System.currentTimeMillis();
+            logger.info("parse speed time {}", end - start);
+            byte[] ofdBytes = ofdCreator.jar();
+            logger.info("gen ofd speed time {}", end - start);
+            return ofdBytes;
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (doc != null) {
+                    doc.close();
+                }
+                FileUtils.forceDeleteOnExit(new File(tempFilePath));
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    private static String generateTempFilePath() {
+        return System.getProperty("java.io.tmpdir") + "/" + UUID.randomUUID().toString();
+    }
+}

+ 8 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/OfdResIdDefine.java

@@ -0,0 +1,8 @@
+package com.ymwk.icvp.pdfToOfdBao;
+public class OfdResIdDefine {
+
+	public static final long RID_ColorSpace = 1;
+
+	public static final long RID_VarStart = 100;
+
+}

+ 176 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/PageDir.java

@@ -0,0 +1,176 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.basicStructure.pageObj.Page;
+import org.ofdrw.core.basicStructure.res.Res;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * 页面目录
+ *
+ * @author 权观宇
+ * @since 2020-01-18 03:05:23
+ */
+public class PageDir implements DirCollect {
+    /**
+     * 代表OFD中第几页
+     * <p>
+     * index 从 0 开始取
+     */
+    private Integer index = 0;
+
+    /**
+     * 页面资源
+     * <p>
+     * 记录了资源的目录
+     */
+    private Res pageRes;
+
+    /**
+     * 资源容器
+     */
+    private ResDir res;
+
+    /**
+     * 页面描述
+     */
+    private Page content;
+
+    public PageDir() {
+    }
+
+    public PageDir(Integer index, Res pageRes, ResDir res, Page content) {
+        this.index = index;
+        this.pageRes = pageRes;
+        this.res = res;
+        this.content = content;
+    }
+
+    /**
+     * @return 页码
+     */
+    public Integer getIndex() {
+        return index;
+    }
+
+    /**
+     * 设置页码
+     *
+     * @param index 页码
+     * @return this
+     */
+    public PageDir setIndex(Integer index) {
+        this.index = index;
+        return this;
+    }
+
+    /**
+     * @return 资源
+     */
+    public Res getPageRes() {
+        return pageRes;
+    }
+
+    public PageDir setPageRes(Res pageRes) {
+        this.pageRes = pageRes;
+        return this;
+    }
+
+    /**
+     * 向页面中增加页面资源
+     *
+     * @param resource 资源
+     * @return this
+     */
+    public PageDir add(Path resource) {
+        if (res == null) {
+            res = new ResDir();
+        }
+        this.res.add(resource);
+        return this;
+    }
+
+    /**
+     * 获取页面资源
+     *
+     * @param name 资源名称,包含后缀
+     * @return 资源路径,如果资源不存在则为null
+     */
+    public Path get(String name) {
+        if (this.res == null) {
+            return null;
+        }
+        return this.res.get(name);
+    }
+
+    /**
+     * @return 获取资源目录
+     */
+    public ResDir getResDir() {
+        return this.res;
+    }
+
+    /**
+     * @return 页面描述
+     */
+    public Page getContent() {
+        return content;
+    }
+
+    /**
+     * 设置页面描述
+     *
+     * @param content 页面描述
+     * @return this
+     */
+    public PageDir setContent(Page content) {
+        this.content = content;
+        return this;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        Path path = Paths.get(base, "Page_" + index);
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+
+        if (content != null) {
+            DocObjDump.dump(this.content, Paths.get(dir, "Content.xml"));
+        }
+        if (res != null) {
+            res.collect(dir);
+        }
+        if (pageRes != null) {
+            DocObjDump.dump(this.pageRes, Paths.get(dir, "PageRes.xml"));
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        Path path = Paths.get(base, "Page_" + index);
+        String dir = path.toString();
+
+        if (content != null) {
+            DocObjDump.dump(this.content, Paths.get(dir, "Content.xml").toString(), virtualFileMap);
+        }
+        if (res != null) {
+            res.collect(dir, virtualFileMap);
+        }
+        if (pageRes != null) {
+            DocObjDump.dump(this.pageRes, Paths.get(dir, "PageRes.xml").toString(), virtualFileMap);
+        }
+        return virtualFileMap;
+    }
+}

+ 93 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/PagesDir.java

@@ -0,0 +1,93 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 页面容器
+ *
+ * @author 权观宇
+ * @since 2020-01-18 03:34:34
+ */
+public class PagesDir implements DirCollect {
+
+    /**
+     * 容器
+     */
+    private List<PageDir> container;
+
+    public PagesDir() {
+        this.container = new ArrayList<>(5);
+    }
+
+    /**
+     * 增加页面
+     *
+     * @param page 页面容器
+     * @return this
+     */
+    public PagesDir add(PageDir page) {
+        if (container == null) {
+            container = new ArrayList<>(5);
+        }
+        this.container.add(page);
+        return this;
+    }
+
+    /**
+     * 获取指定页面容器
+     *
+     * @param index 页码(从1开始)
+     * @return this
+     */
+    public PageDir get(Integer index) {
+        if (container == null) {
+            return null;
+        }
+        for (PageDir page : container) {
+            if (page.getIndex().equals(index)) {
+                return page;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少页面");
+        }
+        Path path = Paths.get(base, "Pages");
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+        for (PageDir p :container) {
+            p.collect(dir);
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少页面");
+        }
+        Path path = Paths.get(base, "Pages");
+        String dir = path.toString();
+        for (PageDir p :container) {
+            p.collect(dir, virtualFileMap);
+        }
+        return virtualFileMap;
+    }
+}

+ 138 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/ResDir.java

@@ -0,0 +1,138 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 资源目录
+ *
+ * @author 权观宇
+ * @since 2020-01-18 02:49:24
+ */
+public class ResDir implements DirCollect {
+    /**
+     * 目录内容(文件内容)
+     */
+    private Map<String, Path> content;
+    
+    /**
+     * 目录内容(文件的二进制内容)
+     */
+    private Map<String, byte[]> contentOfData;
+    
+    public ResDir() {
+        content = new HashMap<>(5);
+        contentOfData = new HashMap<>();
+    }
+
+    /**
+     * 向目录中加入资源
+     *
+     * @param res 资源
+     * @return this
+     */
+    public ResDir add(Path res) {
+        if (res == null) {
+            return this;
+        }
+        if (Files.notExists(res)) {
+            throw new IllegalArgumentException("加入的资源不存在: " + res.toAbsolutePath().toString());
+        }
+        this.content.put(res.getFileName().toString(), res);
+        return this;
+    }
+    
+    /**
+     * 向目录中添加资源(资源文件二进制内容)
+     * @param name            资源名称
+     * @param resData         资源文件二进制内容
+     * @return
+     */
+    public ResDir add(String name, byte[] resData) {
+    	if (name == null || name.length() == 0 || resData == null && resData.length == 0) {
+    		return this;
+    	}
+    	
+    	contentOfData.put(name, resData);
+    	
+    	return this;
+    }
+
+    /**
+     * 获取资源
+     *
+     * @param name 资源名称(包含后缀)
+     * @return 资源路径
+     */
+    public Path get(String name) {
+        return content.get(name);
+    }
+
+    /**
+     * 创建目录并且复制文件
+     *
+     * @param baseDir 基础路径
+     * @return 创建的目录路径
+     * @throws IOException 文件复制异常
+     */
+    @Override
+    public Path collect(String baseDir) throws IOException {
+        // 1. 根据基础路径创建目录
+        Path path = Paths.get(baseDir, "Res");
+        //System.out.println("baseDir:" + baseDir);
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+        if (content == null) {
+            return path;
+        }
+        for (Map.Entry<String, Path> e : content.entrySet()) {
+            // 判断被复制文件是否存在
+            if (!Files.exists(e.getValue())) {
+                continue;
+            }
+            // 2. 复制文件
+            Files.copy(e.getValue(), Paths.get(dir, e.getKey()));
+            
+        }
+        
+        //写入二进制文件
+        for (Map.Entry<String, byte[]> e : contentOfData.entrySet()) {
+        	Files.write(Paths.get(dir, e.getKey()), e.getValue());
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        // 1. 根据基础路径创建目录
+        Path path = Paths.get(base, "Res");
+        String dir = path.toString();
+        if (content == null) {
+            return virtualFileMap;
+        }
+        for (Map.Entry<String, Path> entry : content.entrySet()) {
+//            // 判断被复制文件是否存在
+//            if (!Files.exists(e.getValue())) {
+//                continue;
+//            }
+//            // 2. 复制文件
+//            Files.copy(e.getValue(), Paths.get(dir, e.getKey()));
+
+            virtualFileMap.put(Paths.get(dir, entry.getKey()).toString(), FileUtils.readFileToByteArray(new File(entry.getKey())));
+        }
+
+        //写入二进制文件
+        for (Map.Entry<String, byte[]> entry : contentOfData.entrySet()) {
+//            Files.write(Paths.get(dir, e.getKey()), e.getValue());
+            virtualFileMap.put(Paths.get(dir, entry.getKey()).toString(), entry.getValue());
+        }
+        return virtualFileMap;
+    }
+}

+ 158 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/SignDir.java

@@ -0,0 +1,158 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.signatures.sig.Signature;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * 签名资源容器
+ *
+ * @author 
+ * @since 2020-01-18 03:43:52
+ */
+public class SignDir implements DirCollect {
+
+    /**
+     * 表示第几个签名
+     */
+    private Integer index = 0;
+
+    /**
+     * 签名/签章 描述文件
+     */
+    private Signature signature;
+
+    /**
+     * 电子印章文件
+     * <p>
+     * Seal.esl
+     */
+    private Path seal;
+
+    /**
+     * 签名值文件
+     */
+    private Path signedValue;
+
+    public SignDir() {
+
+    }
+
+    /**
+     * @return 第几个签名,从1开始
+     */
+    public Integer getIndex() {
+        return index;
+    }
+
+
+    /**
+     * 设置页码
+     *
+     * @param index 第几个签名
+     */
+    public SignDir setIndex(Integer index) {
+        this.index = index;
+        return this;
+    }
+
+    /**
+     * @return 签名/签章 描述文件
+     */
+    public Signature getSignature() {
+        return signature;
+    }
+
+    /**
+     * 设置 签名/签章 描述文件
+     *
+     * @param signature 签名/签章 描述文件
+     * @return this
+     */
+    public SignDir setSignature(Signature signature) {
+        this.signature = signature;
+        return this;
+    }
+
+    /**
+     * @return 电子印章文件
+     */
+    public Path getSeal() {
+        return seal;
+    }
+
+    /**
+     * 设置电子印章文件
+     *
+     * @param seal 电子印章文件
+     * @return this
+     */
+    public SignDir setSeal(Path seal) {
+        this.seal = seal;
+        return this;
+    }
+
+    /**
+     * @return 签名值文件
+     */
+    public Path getSignedValue() {
+        return signedValue;
+    }
+
+    /**
+     * 设置签名值文件
+     *
+     * @param signedValue 签名值文件
+     * @return this
+     */
+    public SignDir setSignedValue(Path signedValue) {
+        this.signedValue = signedValue;
+        return this;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        Path path = Paths.get(base, "Sign_" + index);
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+        if (signature == null) {
+            throw new IllegalArgumentException("缺少签名/签章描述文件(signature)");
+        }
+        DocObjDump.dump(signature, Paths.get(dir, "Signature.xml"));
+        if (seal != null) {
+            Files.copy(seal, Paths.get(dir, "Seal.esl"));
+        }
+        if (signedValue != null) {
+            Files.copy(signedValue, Paths.get(dir, "SignedValue.dat"));
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        Path path = Paths.get(base, "Sign_" + index);
+        String dir = path.toString();
+        if (signature == null) {
+            throw new IllegalArgumentException("缺少签名/签章描述文件(signature)");
+        }
+        DocObjDump.dump(signature, Paths.get(dir, "Signature.xml").toString(), virtualFileMap);
+//        if (seal != null) {
+//            Files.copy(seal, Paths.get(dir, "Seal.esl"));
+//        }
+//        if (signedValue != null) {
+//            Files.copy(signedValue, Paths.get(dir, "SignedValue.dat"));
+//        }
+        return virtualFileMap;
+    }
+}

+ 130 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/SignsDir.java

@@ -0,0 +1,130 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.ofdrw.core.signatures.Signatures;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 签名容器
+ *
+ * @author 
+ * @since 2020-01-18 03:34:34
+ */
+public class SignsDir implements DirCollect {
+
+    /**
+     * 容器
+     */
+    private List<SignDir> container;
+
+    /**
+     * 签名列表文件
+     */
+    private Signatures signatures;
+
+    public SignsDir() {
+        this.container = new ArrayList<>(5);
+    }
+
+    /**
+     * @return 签名列表文件
+     */
+    public Signatures getSignatures() {
+        return signatures;
+    }
+
+    /**
+     * 设置 签名列表文件
+     *
+     * @param signatures 签名列表文件
+     * @return this
+     */
+    public SignsDir setSignatures(Signatures signatures) {
+        this.signatures = signatures;
+        return this;
+    }
+
+    /**
+     * 增加页面签名
+     *
+     * @param page 页面容器
+     * @return this
+     */
+    public SignsDir add(SignDir page) {
+        if (container == null) {
+            container = new ArrayList<>(5);
+        }
+        this.container.add(page);
+        return this;
+    }
+
+    /**
+     * 获取指定签名容器
+     *
+     * @param numberOfSign 第几个签名
+     * @return this
+     */
+    public SignDir get(Integer numberOfSign) {
+        if (container == null) {
+            return null;
+        }
+        for (SignDir sign : container) {
+            if (sign.getIndex().equals(numberOfSign)) {
+                return sign;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少签名文件(SignDir)");
+        }
+        if (signatures == null) {
+            throw new IllegalArgumentException("缺少签名列表文件(signatures)");
+        }
+
+        Path path = Paths.get(base, "Signs");
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+
+        DocObjDump.dump(this.signatures, Paths.get(dir, "Signatures.xml"));
+        for (SignDir p : container) {
+            p.collect(dir);
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少签名文件(SignDir)");
+        }
+        if (signatures == null) {
+            throw new IllegalArgumentException("缺少签名列表文件(signatures)");
+        }
+
+        Path path = Paths.get(base, "Signs");
+        String dir = path.toString();
+
+        DocObjDump.dump(this.signatures, Paths.get(dir, "Signatures.xml").toString(), virtualFileMap);
+        for (SignDir p : container) {
+            p.collect(dir, virtualFileMap);
+        }
+        return virtualFileMap;
+    }
+}

+ 226 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/TemplateDir.java

@@ -0,0 +1,226 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.ofdrw.core.basicStructure.pageObj.Template;
+import org.ofdrw.core.basicStructure.res.Res;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+
+/**
+ * 页面目录
+ *
+ * @author 权观宇
+ * @since 2020-01-18 03:05:23
+ */
+public class TemplateDir implements DirCollect {
+    /**
+     * 代表OFD中第几页
+     * <p>
+     * index 从 0 开始取
+     */
+    private Integer index = 0;
+
+    /**
+     * 页面资源
+     * <p>
+     * 记录了资源的目录
+     */
+    private Res pageRes;
+
+    /**
+     * 资源容器
+     */
+    private ResDir res;
+
+    /**
+     * 页面描述
+     */
+    private Template content;
+    
+    /**
+     * 页面描述Stream
+     */
+    private InputStream contentOfStream;
+    
+    /**
+     * 页面描述文件path,若设置了content或contentOfStream的则无效
+     */
+    private Path contentOfPath;
+
+    public TemplateDir() {
+    }
+
+    public TemplateDir(Integer index, Res pageRes, ResDir res, Template content) {
+        this.index = index;
+        this.pageRes = pageRes;
+        this.res = res;
+        this.content = content;
+    }
+
+    /**
+     * @return 页码
+     */
+    public Integer getIndex() {
+        return index;
+    }
+
+    /**
+     * 设置页码
+     *
+     * @param index 页码
+     * @return this
+     */
+    public TemplateDir setIndex(Integer index) {
+        this.index = index;
+        return this;
+    }
+
+    /**
+     * @return 资源
+     */
+    public Res getPageRes() {
+        return pageRes;
+    }
+
+    public TemplateDir setPageRes(Res pageRes) {
+        this.pageRes = pageRes;
+        return this;
+    }
+
+    /**
+     * 向页面中增加页面资源
+     *
+     * @param resource 资源
+     * @return this
+     */
+    public TemplateDir add(Path resource) {
+        if (res == null) {
+            res = new ResDir();
+        }
+        this.res.add(resource);
+        return this;
+    }
+
+    /**
+     * 获取页面资源
+     *
+     * @param name 资源名称,包含后缀
+     * @return 资源路径,如果资源不存在则为null
+     */
+    public Path get(String name) {
+        if (this.res == null) {
+            return null;
+        }
+        return this.res.get(name);
+    }
+
+    /**
+     * @return 获取资源目录
+     */
+    public ResDir getResDir() {
+        return this.res;
+    }
+
+    /**
+     * @return 页面描述
+     */
+    public Template getContent() {
+        return content;
+    }
+
+    /**
+     * 设置页面描述
+     *
+     * @param content 页面描述
+     * @return this
+     */
+    public TemplateDir setContent(Template content) {
+        this.content = content;
+        return this;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        Path path = Paths.get(base, "Tpl_" + index);
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+
+        if (content != null) {
+            DocObjDump.dump(this.content, Paths.get(dir, "Content.xml"));
+        } else if (contentOfStream != null) {
+        	Files.copy(contentOfStream, Paths.get(dir, "Content.xml"));
+        } else if (contentOfPath != null) {
+        	Files.copy(contentOfPath, Paths.get(dir, "Content.xml"));
+        }
+        
+        
+        if (res != null) {
+            res.collect(dir);
+        }
+        if (pageRes != null) {
+            DocObjDump.dump(this.pageRes, Paths.get(dir, "PageRes.xml"));
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        Path path = Paths.get(base, "Tpl_" + index);
+        String dir = path.toString();
+
+        if (content != null) {
+            DocObjDump.dump(this.content, Paths.get(dir, "Content.xml").toString(), virtualFileMap);
+        } else if (contentOfStream != null) {
+//            Files.copy(contentOfStream, Paths.get(dir, "Content.xml"));
+            virtualFileMap.put(Paths.get(dir, "Content.xml").toString(), IOUtils.toByteArray(contentOfStream));
+        } else if (contentOfPath != null) {
+//            Files.copy(contentOfPath, Paths.get(dir, "Content.xml"));
+            virtualFileMap.put(Paths.get(dir, "Content.xml").toString(), FileUtils.readFileToByteArray(contentOfPath.toFile()));
+        }
+
+
+        if (res != null) {
+            res.collect(dir);
+        }
+        if (pageRes != null) {
+            DocObjDump.dump(this.pageRes, Paths.get(dir, "PageRes.xml").toString(), virtualFileMap);
+        }
+        return virtualFileMap;
+    }
+
+    /**
+     * 页面描述文件path,若设置了content的则无效
+     */
+	public Path getContentOfPath() {
+		return contentOfPath;
+	}
+
+    /**
+     * 页面描述文件path,若设置了content的则无效
+     */
+	public void setContentOfPath(Path contentOfPath) {
+		this.contentOfPath = contentOfPath;
+	}
+
+	public InputStream getContentOfStream() {
+		return contentOfStream;
+	}
+
+	public void setContentOfStream(InputStream contentOfStream) {
+		this.contentOfStream = contentOfStream;
+	}
+}

+ 93 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/TemplatesDir.java

@@ -0,0 +1,93 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 页面容器
+ *
+ * @author 权观宇
+ * @since 2020-01-18 03:34:34
+ */
+public class TemplatesDir implements DirCollect {
+
+    /**
+     * 容器
+     */
+    private List<TemplateDir> container;
+
+    public TemplatesDir() {
+        this.container = new ArrayList<>(5);
+    }
+
+    /**
+     * 增加页面
+     *
+     * @param template 页面容器
+     * @return this
+     */
+    public TemplatesDir add(TemplateDir template) {
+        if (container == null) {
+            container = new ArrayList<>(5);
+        }
+        this.container.add(template);
+        return this;
+    }
+
+    /**
+     * 获取指定页面容器
+     *
+     * @param index 页码(从1开始)
+     * @return this
+     */
+    public TemplateDir get(Integer index) {
+        if (container == null) {
+            return null;
+        }
+        for (TemplateDir template : container) {
+            if (template.getIndex().equals(index)) {
+                return template;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 创建目录并复制文件
+     *
+     * @param base 基础路径
+     * @return 创建的目录路径
+     * @throws IOException IO异常
+     */
+    @Override
+    public Path collect(String base) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少页面");
+        }
+        Path path = Paths.get(base, "Tpls");
+        path = Files.createDirectories(path);
+        String dir = path.toAbsolutePath().toString();
+        for (TemplateDir p :container) {
+            p.collect(dir);
+        }
+        return path;
+    }
+
+    @Override
+    public Map<String, byte[]> collect(String base, Map<String, byte[]> virtualFileMap) throws IOException {
+        if (container == null || container.isEmpty()) {
+            throw new IllegalArgumentException("缺少页面");
+        }
+        Path path = Paths.get(base, "Tpls");
+        String dir = path.toString();
+        for (TemplateDir p : container) {
+            p.collect(dir, virtualFileMap);
+        }
+        return virtualFileMap;
+    }
+}

+ 150 - 0
src/main/java/com/ymwk/icvp/pdfToOfdBao/ZipUtil.java

@@ -0,0 +1,150 @@
+package com.ymwk.icvp.pdfToOfdBao;
+
+import org.apache.commons.compress.archivers.zip.Zip64Mode;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author: Llx
+ * @date: 2020/8/12
+ * @description:
+ **/
+public class ZipUtil {
+
+
+    /**
+     * 打包OFD文件
+     *
+     * @param workDirPath OFD内容工作目录
+     * @param zipFileName 文件名称(包含后缀的路径)
+     * @throws IOException IO异常
+     * @Author: Lianglx
+     * @Description: 使用commons-compress的压缩方式重写了原使用zip4j的压缩方式,为了删除zip4j依赖,统一使用commons-compress
+     */
+    public static void zip(File workDirPath, String zipFileName) throws IOException {
+        ZipArchiveOutputStream zaos = null;
+        try {
+            File zipFile = new File(zipFileName);
+            zaos = new ZipArchiveOutputStream(zipFile);
+            //Use Zip64 extensions for all entries where they are required
+            zaos.setUseZip64(Zip64Mode.AsNeeded);
+
+            //将每个文件用ZipArchiveEntry封装
+            //再用ZipArchiveOutputStream写到压缩文件中
+            for (File file : allFiles(workDirPath)) {
+                if (file != null) {
+                    ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(file, file.getAbsolutePath().replace(workDirPath.getAbsolutePath(), "").substring(1));
+                    zaos.putArchiveEntry(zipArchiveEntry);
+                    InputStream is = null;
+                    try {
+                        if (file.isDirectory()) continue;
+                        is = new BufferedInputStream(new FileInputStream(file));
+                        byte[] buffer = new byte[1024 * 4];
+                        int len = -1;
+                        while ((len = is.read(buffer)) != -1) {
+                            //把缓冲区的字节写入到ZipArchiveEntry
+                            zaos.write(buffer, 0, len);
+                        }
+                        //Writes all necessary data for this entry.
+                        zaos.closeArchiveEntry();
+                    } catch (Exception e) {
+                        throw new IOException(e);
+                    } finally {
+                        if (is != null)
+                            is.close();
+                    }
+
+                }
+            }
+            zaos.finish();
+        } catch (Exception e) {
+            throw new IOException(e);
+        } finally {
+            if (zaos != null) {
+                zaos.close();
+            }
+        }
+    }
+
+    private static File[] allFiles(File file) {
+        List<File> fileList = new ArrayList<>();
+
+        if (file.exists()) {
+            LinkedList<File> list = new LinkedList<>();
+            File[] files = file.listFiles();
+
+            for (File file2 : files) {
+                if (file2.isDirectory()) {
+                    list.add(file2);
+                } else {
+                    fileList.add(file2);
+                }
+            }
+            File temp_file;
+            while (!list.isEmpty()) {
+                temp_file = list.removeFirst();
+                files = temp_file.listFiles();
+                for (File file2 : files) {
+                    if (file2.isDirectory()) {
+                        list.add(file2);
+                    } else {
+                        fileList.add(file2);
+                    }
+                }
+            }
+        }
+
+        File[] files = new File[fileList.size()];
+
+        return fileList.toArray(files);
+    }
+
+    /**
+     * 打包OFD文件包二进制数据
+     *
+     * @param virtualFileMap
+     * @return
+     * @throws IOException
+     * @Author Lianglx
+     */
+    public static byte[] zip(Map<String, byte[]> virtualFileMap) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ZipArchiveOutputStream zaos = new ZipArchiveOutputStream(bos);
+
+        for (Map.Entry<String, byte[]> entry : virtualFileMap.entrySet()) {
+            zaos.putArchiveEntry(new ZipArchiveEntry(entry.getKey()));
+            zaos.write(entry.getValue());
+            zaos.closeArchiveEntry();
+        }
+
+        zaos.finish();
+
+        return bos.toByteArray();
+    }
+
+    /**
+     * 打包OFD文件包二进制数据
+     *
+     * @param virtualFileMap
+     * @return
+     * @throws IOException
+     * @Author Lianglx
+     */
+    public static void zip(Map<String, byte[]> virtualFileMap,OutputStream output) throws IOException {
+        ZipArchiveOutputStream zaos = new ZipArchiveOutputStream(output);
+
+        for (Map.Entry<String, byte[]> entry : virtualFileMap.entrySet()) {
+            zaos.putArchiveEntry(new ZipArchiveEntry(entry.getKey()));
+            zaos.write(entry.getValue());
+            zaos.closeArchiveEntry();
+        }
+
+        zaos.finish();
+    }
+}

+ 112 - 0
src/main/java/com/ymwk/icvp/pki/controller/CertificateController.java

@@ -0,0 +1,112 @@
+package com.ymwk.icvp.pki.controller;
+
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.model.*;
+import com.ymwk.icvp.pki.service.CertificateService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+
+@RestController
+@RequestMapping("/api/v1/certificate")
+public class CertificateController {
+
+    @Autowired
+    private  CertificateService certificateService;
+
+
+    //证书列表
+    @PostMapping("/list")
+    public ResponseDTO<PageResult<CertificateEntity>> getCertificateList(@RequestBody certListRequest certListRequest) {
+        return certificateService.getCertificateList(certListRequest);
+    }
+    //证书延期
+    @PostMapping("/renew")
+    public ResponseDTO<CertificateEntity> renewCertificate(@RequestBody RenewRequest request) throws Exception {
+        return certificateService.renewCertificate(request);
+    }
+    //证书冻结
+    @PostMapping("/freeze")
+    public ResponseDTO<CertificateEntity> freezeCertificate(@RequestBody FreezeCertificateRequest request) {
+        return certificateService.freezeCertificate(request);
+    }
+    //证书解冻
+    @PostMapping("/unfreeze")
+    public ResponseDTO<CertificateEntity> unfreezeCertificate(@RequestBody FreezeCertificateRequest request) throws Exception {
+        return certificateService.unfreezeCertificate(request);
+    }
+    //证书吊销
+    @PostMapping("/revoke")
+    public ResponseDTO<CertificateEntity> revokeCertificate(@RequestBody RevokeCertificateRequest request) throws Exception {
+        return certificateService.revokeCertificate(request);
+    }
+    //下载crl
+    @PostMapping("/crl")
+    public ResponseEntity<String> downloadCrl(@RequestBody DownloadRequest request, HttpServletResponse response) {
+        return certificateService.downloadCrl(request.getId(), response);
+    }
+    // 上传根证书(接收合并 PEM:cert [+ chain] + private key)
+    @PostMapping("/uploadRoot")
+    public ResponseDTO<String> uploadRootCertificate(@RequestParam("file") MultipartFile file,
+                                                          @RequestParam(value = "certName", required = false) String certName) throws Exception {
+        String content = new String(file.getBytes(), StandardCharsets.UTF_8);
+        return certificateService.uploadRootCertificate(content, certName);
+    }
+    //生成根证书
+    @PostMapping("/issueRootCertificate")
+    public ResponseDTO<CertificateEntity> issueRootCertificate(@RequestBody IssueRequest issueRequest) throws Exception {
+
+            return certificateService.issueRootCertificate(issueRequest);
+    }
+    // 生成普通证书(由已有 Root CA 签发)
+    @PostMapping("/issuePersonalCertificate")
+    public ResponseDTO<CertificateEntity> issuePersonalCertificate(@RequestBody IssueRequest issueRequest) throws Exception {
+        return certificateService.issuePersonalCertificate(issueRequest);
+    }
+    /**
+     * 下载PEM格式(三份分开)
+     */
+    @PostMapping("/download/pem-separate")
+    public ResponseEntity<Map<String, String>> downloadPemSeparate(@RequestBody DownloadRequest downloadRequest) {
+
+        return certificateService.downloadPemSeparate(downloadRequest.getId());
+    }
+
+    /**
+     * 下载合并的PEM(cert + private-key)
+     * GET /api/v1/ca/certificate/{serialNumber}/download/pem-combined
+     */
+    @PostMapping("/download/pem-combined")
+    public ResponseEntity<byte[]> downloadPemCombined(@RequestBody DownloadRequest downloadRequest, HttpServletResponse response) {
+       return certificateService.downloadPemCombined(downloadRequest.getId(), response);
+    }
+
+    /**
+     * 下载PKCS#12格式(需要提供密码)
+     * POST /api/v1/ca/certificate/{serialNumber}/download/pkcs12
+     */
+    @PostMapping("/download/pkcs12")
+    public ResponseEntity<byte[]> downloadPkcs12(@RequestBody Pkcs12DownloadRequest request, HttpServletResponse response) {
+      return certificateService.downloadPkcs12(request, response);
+    }
+
+    /**
+     * 下载证书链
+     * GET /api/v1/ca/certificate/{serialNumber}/download/chain
+     */
+    @PostMapping("/download/chain")
+    public ResponseEntity<String> downloadChain(@RequestBody String id, HttpServletResponse response) {
+       return certificateService.downloadChain(id, response);
+
+    }
+
+
+}

+ 42 - 0
src/main/java/com/ymwk/icvp/pki/entity/CertificateEntity.java

@@ -0,0 +1,42 @@
+package com.ymwk.icvp.pki.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("t_certificate")
+public class CertificateEntity {
+
+    //TODO id不传,数据库自增
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    private String certName;
+    private String certSerialNumber;
+    private String certSubject;
+    private String certAlgo;
+    private String certUsage;
+    private String status;
+    private String url;
+    private LocalDateTime validFrom;
+    private LocalDateTime validTo;
+    private LocalDateTime revokedTime;
+    private String revokeReason;
+
+    private String isCa;
+    private String isRoot;
+    private String parentSerial;
+
+    private String certFilePath;
+    private String privateKeyPath;
+    private String publicKeyPath;
+    private String csrFilePath;
+    private String chainPath;
+
+    private LocalDateTime createdTime;
+
+}

+ 14 - 0
src/main/java/com/ymwk/icvp/pki/entity/User.java

@@ -0,0 +1,14 @@
+package com.ymwk.icvp.pki.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("user")
+public class User {
+    @TableId
+    private Long id;
+    private String name;
+    private Integer age;
+}

+ 9 - 0
src/main/java/com/ymwk/icvp/pki/mapper/CertificateMapper.java

@@ -0,0 +1,9 @@
+package com.ymwk.icvp.pki.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CertificateMapper extends BaseMapper<CertificateEntity> {
+}

+ 9 - 0
src/main/java/com/ymwk/icvp/pki/mapper/UserMapper.java

@@ -0,0 +1,9 @@
+package com.ymwk.icvp.pki.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.icvp.pki.entity.User;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserMapper extends BaseMapper<User> {
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/pki/model/CertificateFileInfo.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.pki.model;
+
+import lombok.Data;
+
+@Data
+public class CertificateFileInfo {
+    private String serialNumber;
+    private String certPath;
+    private String privateKeyPath;
+    private String publicKeyPath;
+    private String chainPath;
+}

+ 10 - 0
src/main/java/com/ymwk/icvp/pki/model/DownloadRequest.java

@@ -0,0 +1,10 @@
+package com.ymwk.icvp.pki.model;
+
+import lombok.Data;
+
+@Data
+public class DownloadRequest {
+
+    private Long id;
+
+}

+ 14 - 0
src/main/java/com/ymwk/icvp/pki/model/DownloadResponse.java

@@ -0,0 +1,14 @@
+package com.ymwk.icvp.pki.model;
+
+import lombok.Data;
+
+@Data
+public class DownloadResponse {
+    private String certContent;      // PEM文本内容
+    private String privateKeyContent;
+    private String publicKeyContent;
+    private byte[] binaryContent;    // PKCS#12二进制
+    private String format;           // PEM_SEPARATE, PEM_COMBINED, PKCS12
+    private String fileName;
+    private String password;         // PKCS#12密码
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/pki/model/FreezeCertificateRequest.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.pki.model;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class FreezeCertificateRequest {
+    @NotNull
+    private Long id;
+
+    private String reason;
+}

+ 69 - 0
src/main/java/com/ymwk/icvp/pki/model/IssueRequest.java

@@ -0,0 +1,69 @@
+package com.ymwk.icvp.pki.model;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class IssueRequest {
+
+    private String certName;
+
+    @NotBlank(message = "通用名称不能为空")
+    private String commonName;
+
+    /**
+     * 组织名称(O)
+     */
+    private String organization;
+
+    /**
+     * 国家代码(C)
+     */
+    private String country ;
+
+    /**
+     * 省份/州(ST)
+     */
+    private String state;
+
+    /**
+     * 城市/地区(L)
+     */
+    private String locality;
+
+    /**
+     * 部门(OU)
+     */
+    private String organizationalUnit;
+
+    private String phone;
+
+    @NotBlank(message = "证书用途不能为空")
+    private String certUsage;
+
+    @Min(1)
+    @Max(3650)
+    private Integer validityDays = 365;
+
+    /**
+     * 密钥算法
+     * RSA: 默认
+     * EC: 椭圆曲线
+     */
+    private String keyAlgorithm;
+
+    private Integer keySize = 2048;
+
+    private Integer isCa;
+
+    private Integer isRoot;
+
+    private Long issuerId;
+
+    private String remark;
+}

+ 14 - 0
src/main/java/com/ymwk/icvp/pki/model/Pkcs12DownloadRequest.java

@@ -0,0 +1,14 @@
+package com.ymwk.icvp.pki.model;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class Pkcs12DownloadRequest {
+    @NotBlank(message = "密码不能为空")
+    @Size(min = 6, message = "密码至少6位")
+    private String password;
+
+    private Long id;
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/pki/model/RenewRequest.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.pki.model;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+@Data
+public class RenewRequest {
+
+    @NotNull
+    private Long id;
+    @Min(1)
+    private Integer validityDays = 365;
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/pki/model/RevokeCertificateRequest.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.pki.model;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class RevokeCertificateRequest {
+    @NotNull
+    private Long id;
+
+    private String reason;
+}

+ 15 - 0
src/main/java/com/ymwk/icvp/pki/model/certListRequest.java

@@ -0,0 +1,15 @@
+package com.ymwk.icvp.pki.model;
+
+import com.ymwk.icvp.component.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class certListRequest extends PageParam {
+
+
+    private String serialNumberStr;
+    private String certUsage;
+    private String status;
+}

+ 40 - 0
src/main/java/com/ymwk/icvp/pki/service/CertificateService.java

@@ -0,0 +1,40 @@
+package com.ymwk.icvp.pki.service;
+
+
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.model.*;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.ResponseEntity;
+
+import java.util.Map;
+
+public interface CertificateService {
+
+    ResponseDTO<PageResult<CertificateEntity>>  getCertificateList(certListRequest certListRequest);
+
+    ResponseDTO<CertificateEntity> issuePersonalCertificate(IssueRequest issueRequest) throws Exception;
+
+    ResponseDTO<CertificateEntity> issueRootCertificate(IssueRequest issueRequest) throws Exception;
+
+    ResponseEntity<Map<String, String>> downloadPemSeparate(Long id);
+
+    ResponseEntity<byte[]> downloadPemCombined(Long id, HttpServletResponse response);
+
+    ResponseEntity<byte[]> downloadPkcs12(Pkcs12DownloadRequest request, HttpServletResponse response);
+
+    ResponseEntity<String> downloadChain(java.lang.String id, jakarta.servlet.http.HttpServletResponse response);
+
+    ResponseDTO<CertificateEntity> renewCertificate(RenewRequest request) throws Exception;
+
+    ResponseDTO<CertificateEntity> freezeCertificate(FreezeCertificateRequest request);
+
+    ResponseDTO<CertificateEntity> unfreezeCertificate(FreezeCertificateRequest request);
+
+    ResponseDTO<CertificateEntity> revokeCertificate(RevokeCertificateRequest request);
+
+    ResponseEntity<String> downloadCrl(Long id, HttpServletResponse response);
+
+    ResponseDTO<String> uploadRootCertificate(String content, String certName) throws Exception;
+}

+ 8 - 0
src/main/java/com/ymwk/icvp/pki/service/UserService.java

@@ -0,0 +1,8 @@
+package com.ymwk.icvp.pki.service;
+
+import com.ymwk.icvp.pki.entity.User;
+import java.util.List;
+
+public interface UserService {
+    List<User> listAll();
+}

+ 281 - 0
src/main/java/com/ymwk/icvp/pki/service/impl/CertificateAssembleService.java

@@ -0,0 +1,281 @@
+package com.ymwk.icvp.pki.service.impl;
+
+import com.ymwk.icvp.pki.model.DownloadResponse;
+import com.ymwk.icvp.utils.MinioClientService;
+import io.minio.MinioClient;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+
+@Service
+@Slf4j
+public class CertificateAssembleService {
+
+    @Autowired
+    private CertificateStorageService storageService;
+
+    @Autowired
+    private MinioClientService minioUtil;
+
+    /**
+     * 下载PEM格式(三份分开)
+     */
+    public DownloadResponse downloadPemSeparate(String serialNumber) throws Exception {
+        String certPem = storageService.readCertificateFile(serialNumber);
+        String privateKeyPem = storageService.readPrivateKeyFile(serialNumber);
+        String publicKeyPem = storageService.readPublicKeyFile(serialNumber);
+
+        DownloadResponse response = new DownloadResponse();
+        response.setCertContent(certPem);
+        response.setPrivateKeyContent(privateKeyPem);
+        response.setPublicKeyContent(publicKeyPem);
+        response.setFormat("PEM_SEPARATE");
+        response.setFileName(serialNumber + ".pem");
+
+        return response;
+    }
+
+    /**
+     * 下载合并的PEM(cert + private-key)
+     * 适用于Nginx/Apache配置
+     */
+    public DownloadResponse downloadPemCombined(String serialNumber) throws Exception {
+        String certPem = storageService.readCertificateFile(serialNumber);
+        String privateKeyPem = storageService.readPrivateKeyFile(serialNumber);
+        String chainPem = storageService.readChainFile(serialNumber);
+
+        StringBuilder combined = new StringBuilder();
+        combined.append(certPem).append("\n");
+
+        String normalizedChain = normalizePemChain(certPem, chainPem);
+        if (!normalizedChain.trim().isEmpty()) {
+            combined.append(normalizedChain).append("\n");
+        }
+
+        combined.append(privateKeyPem);
+
+        DownloadResponse response = new DownloadResponse();
+        response.setCertContent(combined.toString());
+        response.setFormat("PEM_COMBINED");
+        response.setFileName(serialNumber + "_full.pem");
+        return response;
+    }
+    private String normalizePemChain(String certPem, String chainPem) {
+        if (chainPem == null || chainPem.trim().isEmpty()) {
+            return "";
+        }
+        String normalized = chainPem.replace(certPem, "").trim();
+        normalized = normalized.replaceAll("(?s)\\n{3,}", "\n\n");
+        return normalized;
+    }
+
+    /**
+     * 下载PKCS#12格式(.p12/.pfx)
+     * 适用于浏览器导入、Windows/IIS
+     */
+    public DownloadResponse downloadPkcs12(String serialNumber, String password) throws Exception {
+        // 1. 从MinIO读取证书和私钥
+        String certPem = storageService.readCertificateFile(serialNumber);
+        String privateKeyPem = storageService.readPrivateKeyFile(serialNumber);
+        String chainPem = storageService.readChainFile(serialNumber);
+
+        // 2. 解析PEM为Java对象
+        X509Certificate certificate = parseCertificate(certPem);
+        PrivateKey privateKey = parsePrivateKey(
+                privateKeyPem, certificate.getPublicKey().getAlgorithm());
+
+        // 3. 解析证书链(如果有)
+        List<X509Certificate> chain = parseCertificateChain(chainPem);
+
+        // 调试信息
+        System.out.println("=== Certificate Info ===");
+        System.out.println("Subject: " + certificate.getSubjectDN());
+        System.out.println("Issuer: " + certificate.getIssuerDN());
+        System.out.println("Is Self-Signed: " + certificate.getSubjectDN().equals(certificate.getIssuerDN()));
+        System.out.println("Chain size: " + chain.size());
+
+        // 4. 构建PKCS#12 KeyStore
+        byte[] pkcs12Data = buildPkcs12(certificate, privateKey, chain, password);
+
+        DownloadResponse response = new DownloadResponse();
+        response.setBinaryContent(pkcs12Data);
+        response.setFormat("PKCS12");
+        response.setFileName(serialNumber + ".p12");
+        response.setPassword(password);
+
+        return response;
+    }
+
+    /**
+     * 构建PKCS#12
+     */
+    private byte[] buildPkcs12(X509Certificate certificate, PrivateKey privateKey,
+                               List<X509Certificate> chain, String password) throws Exception {
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        keyStore.load(null, null);
+
+        X509Certificate[] chainArray;
+        if (certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal())) {
+            chainArray = new X509Certificate[]{certificate};
+        } else {
+            List<X509Certificate> fullChain = new ArrayList<>();
+            fullChain.add(certificate);
+            if (chain != null && !chain.isEmpty()) {
+                for (X509Certificate c : chain) {
+                    if (c == null) continue;
+                    if (isSameCertificate(c, certificate)) continue;
+                    if (containsCertificate(fullChain, c)) continue;
+                    fullChain.add(c);
+                }
+            }
+            chainArray = fullChain.toArray(new X509Certificate[0]);
+        }
+
+        keyStore.setKeyEntry("certificate", privateKey, password.toCharArray(), chainArray);
+
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            keyStore.store(baos, password.toCharArray());
+            return baos.toByteArray();
+        }
+    }
+    private boolean isSameCertificate(X509Certificate a, X509Certificate b) {
+        return a.getSerialNumber().equals(b.getSerialNumber())
+                && a.getSubjectX500Principal().equals(b.getSubjectX500Principal())
+                && a.getIssuerX500Principal().equals(b.getIssuerX500Principal());
+    }
+
+    private boolean containsCertificate(List<X509Certificate> certs, X509Certificate target) {
+        for (X509Certificate cert : certs) {
+            if (isSameCertificate(cert, target)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    /**
+     * 解析PEM证书
+     */
+    private X509Certificate parseCertificate(String certPem) throws Exception {
+        java.security.cert.CertificateFactory cf =
+                java.security.cert.CertificateFactory.getInstance("X.509", "BC");
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(certPem.getBytes())) {
+            return (X509Certificate) cf.generateCertificate(bais);
+        }
+    }
+
+    /**
+     * 解析PEM私钥
+     */
+    private PrivateKey parsePrivateKey(String privateKeyPem) throws Exception {
+        return parsePrivateKey(privateKeyPem, "RSA");
+    }
+
+    private PrivateKey parsePrivateKey(String privateKeyPem, String algorithm) throws Exception {
+        boolean pkcs1RsaKey = privateKeyPem.contains("-----BEGIN RSA PRIVATE KEY-----");
+        String base64 = privateKeyPem
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replace("-----BEGIN RSA PRIVATE KEY-----", "")
+                .replace("-----END RSA PRIVATE KEY-----", "")
+                .replaceAll("\\s", "");
+
+        byte[] keyBytes = Base64.getDecoder().decode(base64);
+
+        try {
+            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+            boolean ecKey = isEcKeyAlgorithm(algorithm);
+            String keyAlgorithm = ecKey
+                    ? "EC" : "RSA";
+            return KeyFactory.getInstance(keyAlgorithm, "BC").generatePrivate(spec);
+        } catch (Exception e) {
+            if (isEcKeyAlgorithm(algorithm) || !pkcs1RsaKey) {
+                throw e;
+            }
+            org.bouncycastle.asn1.pkcs.RSAPrivateKey key =
+                    org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(keyBytes);
+
+            java.security.spec.RSAPrivateCrtKeySpec spec =
+                    new java.security.spec.RSAPrivateCrtKeySpec(
+                            key.getModulus(),
+                            key.getPublicExponent(),
+                            key.getPrivateExponent(),
+                            key.getPrime1(),
+                            key.getPrime2(),
+                            key.getExponent1(),
+                            key.getExponent2(),
+                            key.getCoefficient()
+                    );
+            return KeyFactory.getInstance("RSA", "BC").generatePrivate(spec);
+        }
+    }
+
+    private boolean isEcKeyAlgorithm(String algorithm) {
+        return "2".equalsIgnoreCase(algorithm)
+                || "SM2".equalsIgnoreCase(algorithm)
+                || "EC".equalsIgnoreCase(algorithm);
+    }
+
+    /**
+     * 解析证书链
+     */
+    private List<X509Certificate> parseCertificateChain(String chainPem) throws Exception {
+        List<X509Certificate> chain = new ArrayList<>();
+
+        // 如果链为空,直接返回空列表
+        if (chainPem == null || chainPem.trim().isEmpty()) {
+            System.out.println("No certificate chain provided");
+            return chain;
+        }
+
+        java.security.cert.CertificateFactory cf =
+                java.security.cert.CertificateFactory.getInstance("X.509", "BC");
+
+        String[] certs = chainPem.split("-----BEGIN CERTIFICATE-----");
+        for (String cert : certs) {
+            if (cert.trim().isEmpty()) continue;
+            String pem = "-----BEGIN CERTIFICATE-----" + cert;
+            try (ByteArrayInputStream bais = new ByteArrayInputStream(pem.getBytes())) {
+                X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(bais);
+                chain.add(x509Cert);
+                System.out.println("Added chain certificate: " + x509Cert.getSubjectDN());
+            }
+        }
+
+        System.out.println("Total chain certificates parsed: " + chain.size());
+        return chain;
+    }
+
+    /**
+     * 根据序列号获取 X509 证书(供制章等内部流程使用)
+     */
+    public X509Certificate getCertificate(String serialNumber) throws Exception {
+        String certPem = storageService.readCertificateFile(serialNumber);
+        return parseCertificate(certPem);
+    }
+
+    /**
+     * 根据序列号获取私钥(签章阶段使用)
+     */
+    public PrivateKey getPrivateKey(String serialNumber) throws Exception {
+        return getPrivateKey(serialNumber, "RSA");
+    }
+
+    public PrivateKey getPrivateKey(String serialNumber, String algorithm) throws Exception {
+        String privateKeyPem = storageService.readPrivateKeyFile(serialNumber);
+        return parsePrivateKey(privateKeyPem, algorithm);
+    }
+
+}

+ 944 - 0
src/main/java/com/ymwk/icvp/pki/service/impl/CertificateServiceImpl.java

@@ -0,0 +1,944 @@
+package com.ymwk.icvp.pki.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.component.SmartPageUtil;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.mapper.CertificateMapper;
+import com.ymwk.icvp.pki.model.*;
+import com.ymwk.icvp.pki.service.CertificateService;
+
+import com.ymwk.icvp.pki.utils.issueCertUtil;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.*;
+import org.bouncycastle.cert.X509CRLHolder;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v2CRLBuilder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+
+@Service
+@Slf4j
+public class CertificateServiceImpl implements CertificateService {
+
+    @Resource
+    private CertificateMapper certificateMapper;
+
+    @Resource
+    private CertificateStorageService certificateStorageService;
+
+    @Resource
+    private CertificateAssembleService assembleService;
+    @Resource
+    private CertificateStorageService storageService;
+
+    @Override
+    public ResponseDTO<PageResult<CertificateEntity>> getCertificateList(certListRequest certListRequest) {
+        LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
+
+        wrapper.eq(certListRequest.getCertUsage() != null, CertificateEntity::getCertUsage, certListRequest.getCertUsage());
+        wrapper.like(certListRequest.getSerialNumberStr() != null, CertificateEntity::getCertSerialNumber, certListRequest.getSerialNumberStr());
+        wrapper.eq(certListRequest.getStatus() != null, CertificateEntity::getStatus, certListRequest.getStatus());
+        Page<CertificateEntity> page = new Page<>(certListRequest.getPageNum(), certListRequest.getPageSize());
+        Page<CertificateEntity> resultPage = certificateMapper.selectPage(page, wrapper);
+        Long count = certificateMapper.selectCount(wrapper);
+        PageResult<CertificateEntity> pageResult = SmartPageUtil.convert2PageResult(resultPage, resultPage.getRecords());
+        pageResult.setTotal(count);
+        return ResponseDTO.ok(pageResult);
+    }
+
+    @Override
+    public ResponseDTO<CertificateEntity> issuePersonalCertificate(IssueRequest request) throws Exception {
+        // 1. 生成密钥对(申请者自己的密钥对)
+        KeyPair keyPair = issueCertUtil.generateKeyPair(request.getKeyAlgorithm(), request.getKeySize());
+
+        // 2. 找到用于签发的 Issuer(这里默认用 isRoot=1 的 Root CA,取第一个有效的)
+        Long issuerId = request.getIssuerId();
+        if (issuerId == null) {
+            throw new IllegalArgumentException("issuerId is required");
+        }
+        CertificateEntity issuerEntity = certificateMapper.selectById(issuerId);
+        if (issuerEntity == null) {
+            throw new IllegalArgumentException("Issuer certificate not found: " + issuerId);
+        }
+// 可选:校验 issuer 是否为有效 CA
+        if (!"1".equals(issuerEntity.getIsCa()) && !"1".equals(issuerEntity.getIsRoot())) {
+            throw new IllegalArgumentException("Issuer is not a CA");
+        }
+        if ("3".equals(issuerEntity.getStatus())) {
+            throw new IllegalStateException("冻结证书不能用于签发");
+        }
+
+        // 3. 从存储读取 Issuer 的证书与私钥(privateKey 已在存储中加密,readPrivateKeyFile 会解密)
+        String issuerSerial = issuerEntity.getCertSerialNumber();
+        String issuerCertPem = storageService.readCertificateFile(issuerSerial);
+        String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
+
+        // 4. 解析 Issuer 证书与私钥
+        java.security.cert.CertificateFactory cf =
+                java.security.cert.CertificateFactory.getInstance("X.509", "BC");
+        X509Certificate issuerCert;
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
+            issuerCert = (X509Certificate) cf.generateCertificate(bais);
+        }
+        PrivateKey issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuerEntity.getCertAlgo());
+
+        // 5. 构造 Subject DN(和根证书构造类似,但 BasicConstraints(false))
+        X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+        if (request.getCountry() != null) x500NameBuilder.addRDN(BCStyle.C, request.getCountry());
+        if (request.getOrganization() != null) x500NameBuilder.addRDN(BCStyle.O, request.getOrganization());
+        if (request.getOrganizationalUnit() != null) x500NameBuilder.addRDN(BCStyle.OU, request.getOrganizationalUnit());
+        if (request.getState() != null) x500NameBuilder.addRDN(BCStyle.ST, request.getState());
+        if (request.getLocality() != null) x500NameBuilder.addRDN(BCStyle.L, request.getLocality());
+        if (request.getPhone() != null) x500NameBuilder.addRDN(BCStyle.TELEPHONE_NUMBER, request.getPhone());
+        x500NameBuilder.addRDN(BCStyle.CN, request.getCommonName());
+        String serialNumberStr = UUID.randomUUID().toString().replace("-", "").toUpperCase();
+        x500NameBuilder.addRDN(BCStyle.SERIALNUMBER, serialNumberStr);
+        X500Name subjectDN = x500NameBuilder.build();
+
+        BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
+        Date notBefore = new Date();
+        Date notAfter = Date.from(Instant.now().plusSeconds(request.getValidityDays() * 86400L));
+
+        // 6. 构建证书(Issuer = issuerCert.getSubjectX500Principal())
+        X500Name issuerDN = X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded());
+        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+                issuerDN,
+                serialNumber,
+                notBefore,
+                notAfter,
+                subjectDN,
+                keyPair.getPublic()
+        );
+
+        // BasicConstraints:普通证书 false
+        certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
+        // KeyUsage:普通证书常见 digitalSignature + keyEncipherment
+        certBuilder.addExtension(Extension.keyUsage, true,
+                new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
+
+        // ExtendedKeyUsage:根据请求 certUsage 可定制;这里默认 serverAuth + clientAuth
+        KeyPurposeId[] purposes = new KeyPurposeId[] {
+                KeyPurposeId.id_kp_serverAuth,
+                KeyPurposeId.id_kp_clientAuth
+        };
+        ExtendedKeyUsage eku = new ExtendedKeyUsage(purposes);
+        certBuilder.addExtension(Extension.extendedKeyUsage, false, eku);
+
+        // 7. 签名(用 Issuer 的私钥和 Issuer 的签名算法)
+        ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuerEntity.getCertAlgo()))
+                .setProvider("BC")
+                .build(issuerPrivateKey);
+
+        X509CertificateHolder certHolder = certBuilder.build(signer);
+        X509Certificate issuedCert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
+
+        // 验证(可选)
+        issuedCert.verify(issuerCert.getPublicKey());
+
+        // 8. 转 PEM 并存储(同根证书逻辑)
+        String certPem = convertCertificateToPem(issuedCert);
+        String privateKeyPem = convertPrivateKeyToPem(keyPair.getPrivate());
+        String publicKeyPem = convertPublicKeyToPem(keyPair.getPublic());
+       // String chainPem = certPem; // 如果需要完整链,可把 issuerCert prepended;此处简单只放自身证书
+        // 若想把链做成 [issuedCert + issuerCert], 使用:
+        String  chainPem = certPem + convertCertificateToPem(issuerCert);
+
+        CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
+                serialNumberStr,
+                certPem,
+                privateKeyPem,
+                publicKeyPem,
+                chainPem
+        );
+
+        // 9. 保存到数据库
+        CertificateEntity entity = new CertificateEntity();
+        // 不设置 id(数据库自增)
+        entity.setCertName(request.getCertName());
+        entity.setCertSerialNumber(serialNumberStr);
+        entity.setCertSubject(issuedCert.getSubjectX500Principal().getName());
+        entity.setCertAlgo(request.getKeyAlgorithm());
+        entity.setValidFrom(LocalDateTime.ofInstant(issuedCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
+        entity.setValidTo(LocalDateTime.ofInstant(issuedCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
+        entity.setStatus("1");
+        entity.setIsCa("0");
+        entity.setIsRoot("0");
+        entity.setParentSerial(issuerSerial);
+        entity.setCertUsage(request.getCertUsage() == null ? "PERSONAL" : request.getCertUsage());
+        entity.setCertFilePath(fileInfo.getCertPath());
+        entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
+        entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
+        entity.setChainPath(fileInfo.getChainPath());
+
+        certificateMapper.insert(entity);
+
+        log.info("普通证书签发成功: serial={}", serialNumberStr);
+        return ResponseDTO.ok(entity);
+    }
+
+
+    @Override
+    public ResponseDTO<CertificateEntity> issueRootCertificate(IssueRequest request) throws Exception {
+        //  生成密钥对
+        KeyPair keyPair = issueCertUtil.generateKeyPair(request.getKeyAlgorithm(), request.getKeySize());
+
+        // 生成序列号
+        String serialNumberStr = UUID.randomUUID()
+                .toString()
+                .replace("-", "")
+                .toUpperCase();
+
+        BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
+        // 生成证书请求
+        X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+        x500NameBuilder.addRDN(BCStyle.C, request.getCountry());
+        x500NameBuilder.addRDN(BCStyle.CN, request.getCommonName());
+        x500NameBuilder.addRDN(BCStyle.OU, request.getOrganizationalUnit());
+        x500NameBuilder.addRDN(BCStyle.O, request.getOrganization());
+        x500NameBuilder.addRDN(BCStyle.ST, request.getState());
+        x500NameBuilder.addRDN(BCStyle.L, request.getLocality());
+        x500NameBuilder.addRDN(BCStyle.SERIALNUMBER, serialNumberStr);
+        x500NameBuilder.addRDN(BCStyle.TELEPHONE_NUMBER, request.getPhone());
+        X500Name subjectDN = x500NameBuilder.build();
+        // 设置证书有效期
+        Date notBefore = new Date();
+        Date notAfter = Date.from(
+                Instant.now().plusSeconds(request.getValidityDays() * 86400L)
+        );
+
+        // 创建证书构建器(颁发者=主体,因为是自签名)
+        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+                subjectDN,          // 颁发者(Issuer)= 主体(Subject)
+                serialNumber,             // 序列号
+                notBefore,          // 生效时间
+                notAfter,           // 过期时间
+                subjectDN,          // 主体
+                keyPair.getPublic() // 公钥
+        );
+        // 基本约束:标记为CA证书(true表示是CA)
+        certBuilder.addExtension(
+                Extension.basicConstraints,
+                true,  // critical = true
+                new BasicConstraints(true) // true = 是CA,可以签发子证书
+        );
+
+        // 密钥用途:CA需要证书签名和CRL签名能力
+        certBuilder.addExtension(
+                Extension.keyUsage,
+                true,  // critical = true
+                new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
+        );
+
+        // 5.3 使用私钥签名证书
+        ContentSigner signer = new JcaContentSignerBuilder(
+                getSignatureAlgorithm(request.getKeyAlgorithm())
+        )
+                .setProvider("BC")
+                .build(keyPair.getPrivate());
+
+        X509CertificateHolder certHolder = certBuilder.build(signer);
+        X509Certificate rootCert = new JcaX509CertificateConverter()
+                .setProvider("BC")
+                .getCertificate(certHolder);
+
+        // ====== 6. 验证自签名证书(可选但推荐) ======
+        rootCert.verify(keyPair.getPublic());
+
+        // ====== 7. 转换为PEM格式 ======
+        String chainPem = convertCertificateToPem(rootCert);
+        String privateKeyPem = convertPrivateKeyToPem(keyPair.getPrivate());
+        String publicKeyPem = convertPublicKeyToPem(keyPair.getPublic());
+
+        // 根证书的证书链就是自己
+
+        // ====== 8. 存储到MinIO(分开存储,私钥加密) ======
+        CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
+                serialNumberStr,
+                chainPem,
+                privateKeyPem,
+                publicKeyPem,
+                chainPem
+        );
+
+        // ====== 9. 保存到数据库 ======
+        CertificateEntity entity = new CertificateEntity();
+        entity.setCertName(request.getCertName());
+        entity.setCertSerialNumber(serialNumberStr);
+        entity.setCertSubject(rootCert.getSubjectX500Principal().getName());
+        entity.setCertAlgo(request.getKeyAlgorithm());
+        entity.setValidFrom(LocalDateTime.ofInstant(rootCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
+        entity.setValidTo(LocalDateTime.ofInstant(rootCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
+        entity.setStatus("1");
+        entity.setIsCa("1");
+        entity.setIsRoot("1");
+        entity.setParentSerial(null);
+        entity.setCertUsage("ROOT_CA");
+
+        // 只存储文件路径,不存储下载URL(动态生成)
+        entity.setCertFilePath(fileInfo.getCertPath());
+        entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
+        entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
+        entity.setChainPath(fileInfo.getChainPath());
+
+        certificateMapper.insert(entity);
+
+        log.info("根证书签发成功: serial={}", serialNumberStr);
+
+        return ResponseDTO.ok(entity);
+
+    }
+    @Override
+    public ResponseDTO<CertificateEntity> renewCertificate(RenewRequest request) throws Exception {
+        Long id = request.getId();
+        int validityDays = request.getValidityDays() == null ? 365 : request.getValidityDays();
+
+        // 1. 查询证书数据库记录
+        CertificateEntity entity = certificateMapper.selectById(id);
+        if (entity == null) {
+            throw new IllegalArgumentException("证书未找到: id=" + id);
+        }
+        if ("3".equals(entity.getStatus())) {
+            throw new IllegalStateException("冻结状态证书不能续期");
+        }
+        String serial = entity.getCertSerialNumber();
+
+        // 2. 读取原有证书文件、公私钥文件(复用原有密钥)
+        String subjectCertPem = storageService.readCertificateFile(serial);
+        String subjectPublicKeyPem = storageService.readPublicKeyFile(serial);
+        String subjectPrivateKeyPem = storageService.readPrivateKeyFile(serial);
+
+        // 3. 解析旧证书 & 公钥
+        CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
+        X509Certificate subjectOldCert;
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(subjectCertPem.getBytes(StandardCharsets.UTF_8))) {
+            subjectOldCert = (X509Certificate) cf.generateCertificate(bais);
+        }
+        X500Name subjectDN = X500Name.getInstance(subjectOldCert.getSubjectX500Principal().getEncoded());
+        PublicKey subjectPublicKey = parsePublicKeyPem(subjectPublicKeyPem, entity.getCertAlgo());
+
+        // 4. 获取签发者信息:根证书自签 / 非根证书找父签发者
+        X509Certificate issuerCert;
+        PrivateKey issuerPrivateKey;
+        X500Name issuerDN;
+        String issuerAlgo = entity.getCertAlgo();
+
+        if ("1".equals(entity.getIsRoot())) {
+            // 根证书:使用自身作为签发者,自签名
+            issuerCert = subjectOldCert;
+            issuerPrivateKey = parsePrivateKeyPem(subjectPrivateKeyPem, entity.getCertAlgo());
+            issuerDN = subjectDN;
+            issuerAlgo = entity.getCertAlgo();
+        } else {
+            // 子证书:根据 parentSerial 查找签发CA证书
+            String issuerSerial = entity.getParentSerial();
+            if (issuerSerial == null) {
+                throw new IllegalArgumentException("证书缺少父级序列号,无法续期");
+            }
+            CertificateEntity issuerEntity = certificateMapper.selectOne(
+                    new LambdaQueryWrapper<CertificateEntity>()
+                            .eq(CertificateEntity::getCertSerialNumber, issuerSerial)
+            );
+            if (issuerEntity == null) {
+                throw new IllegalArgumentException("签发者未找到: serial=" + issuerSerial);
+            }
+            String issuerCertPem = storageService.readCertificateFile(issuerSerial);
+            String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
+            try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
+                issuerCert = (X509Certificate) cf.generateCertificate(bais);
+            }
+            issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuerEntity.getCertAlgo());
+            issuerDN = X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded());
+            issuerAlgo = issuerEntity.getCertAlgo();
+        }
+
+        // 5. 构建新证书:沿用原序列号,更新有效期
+        BigInteger serialNumber = new BigInteger(serial, 16);
+        Date notBefore = new Date();
+        Date notAfter = Date.from(Instant.now().plusSeconds((long) validityDays * 86400L));
+
+        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+                issuerDN,
+                serialNumber,
+                notBefore,
+                notAfter,
+                subjectDN,
+                subjectPublicKey
+        );
+
+        // 设置证书扩展项:CA证书 / 业务证书区分
+        if ("1".equals(entity.getIsCa()) || "1".equals(entity.getIsRoot())) {
+            certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
+            certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
+        } else {
+            certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
+            certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
+            KeyPurposeId[] purposes = new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth};
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(purposes));
+        }
+
+        // 使用签发者私钥签名
+        ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuerAlgo))
+                .setProvider("BC")
+                .build(issuerPrivateKey);
+
+        X509CertificateHolder certHolder = certBuilder.build(signer);
+        X509Certificate newCert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
+
+        // 校验新证书签名合法性
+        newCert.verify(issuerCert.getPublicKey());
+
+        // 6. PEM格式化,文件存储:**复用原有公私钥,只更新证书文件与证书链**
+        String newCertPem = convertCertificateToPem(newCert);
+        String newPublicKeyPem = subjectPublicKeyPem;
+        String newPrivateKeyPem = subjectPrivateKeyPem;
+
+        String chainPem;
+        if ("1".equals(entity.getIsRoot())) {
+            chainPem = newCertPem;
+        } else {
+            String issuerCertPem = storageService.readCertificateFile(entity.getParentSerial());
+            chainPem = newCertPem + issuerCertPem;
+        }
+
+        // MinIO:相同serial覆盖原有文件
+        CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
+                serial,
+                newCertPem,
+                newPrivateKeyPem,
+                newPublicKeyPem,
+                chainPem
+        );
+
+        // 7. 更新数据库:只更新有效期与文件路径,其余字段不变
+        entity.setValidFrom(LocalDateTime.ofInstant(newCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
+        entity.setValidTo(LocalDateTime.ofInstant(newCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
+        entity.setCertFilePath(fileInfo.getCertPath());
+        entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
+        entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
+        entity.setChainPath(fileInfo.getChainPath());
+        certificateMapper.updateById(entity);
+
+        log.info("证书续期成功: serial={}, id={}", serial, id);
+        return ResponseDTO.ok(entity);
+    }
+
+    @Override
+    public ResponseDTO<CertificateEntity> freezeCertificate(FreezeCertificateRequest request) {
+        if (request == null || request.getId() == null) {
+            throw new IllegalArgumentException("证书ID不能为空");
+        }
+
+        CertificateEntity entity = certificateMapper.selectById(request.getId());
+        if (entity == null) {
+            throw new IllegalArgumentException("证书未找到: id=" + request.getId());
+        }
+
+        // 防止重复冻结
+        if ("3".equals(entity.getStatus())) {
+            return ResponseDTO.ok(entity);
+        }
+
+        entity.setStatus("3");
+
+        certificateMapper.updateById(entity);
+
+        log.info("证书已冻结: id={}, serial={}", entity.getId(), entity.getCertSerialNumber());
+        return ResponseDTO.ok(entity);
+    }
+    @Override
+    public ResponseDTO<CertificateEntity> unfreezeCertificate(FreezeCertificateRequest request) {
+        if (request == null || request.getId() == null) {
+            throw new IllegalArgumentException("证书ID不能为空");
+        }
+
+        CertificateEntity entity = certificateMapper.selectById(request.getId());
+        if (entity == null) {
+            throw new IllegalArgumentException("证书未找到: id=" + request.getId());
+        }
+
+        if (!"3".equals(entity.getStatus())) {
+            return ResponseDTO.ok(entity);
+        }
+
+        entity.setStatus("1");
+
+
+        certificateMapper.updateById(entity);
+
+        log.info("证书已解冻: id={}, serial={}", entity.getId(), entity.getCertSerialNumber());
+        return ResponseDTO.ok(entity);
+    }
+
+    @Override
+    public ResponseDTO<CertificateEntity> revokeCertificate(RevokeCertificateRequest request) {
+        if (request == null || request.getId() == null) {
+            throw new IllegalArgumentException("证书ID不能为空");
+        }
+
+        CertificateEntity cert = certificateMapper.selectById(request.getId());
+        if (cert == null) {
+            throw new IllegalArgumentException("证书未找到: id=" + request.getId());
+        }
+
+        if ("4".equals(cert.getStatus())) {
+            return ResponseDTO.ok(cert);
+        }
+
+        cert.setStatus("4");
+        cert.setRevokedTime(LocalDateTime.now());
+        cert.setRevokeReason(
+                request.getReason() == null || request.getReason().trim().isEmpty()
+                        ? "certificate revoked"
+                        : request.getReason()
+        );
+
+        certificateMapper.updateById(cert);
+
+        log.info("证书已吊销: id={}, serial={}", cert.getId(), cert.getCertSerialNumber());
+        return ResponseDTO.ok(cert);
+    }
+
+    @Override
+    public ResponseEntity<String> downloadCrl(Long issuerId, HttpServletResponse response) {
+        try {
+            CertificateEntity issuer = certificateMapper.selectById(issuerId);
+            if (issuer == null) {
+                return ResponseEntity.notFound().build();
+            }
+
+            String issuerSerial = issuer.getCertSerialNumber();
+
+            List<CertificateEntity> revokedList = certificateMapper.selectList(
+                    new LambdaQueryWrapper<CertificateEntity>()
+                            .eq(CertificateEntity::getStatus, "2")
+                            .and(w -> w.eq(CertificateEntity::getParentSerial, issuerSerial)
+                                    .or()
+                                    .eq(CertificateEntity::getCertSerialNumber, issuerSerial))
+            );
+
+            String issuerCertPem = storageService.readCertificateFile(issuerSerial);
+            String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
+
+            CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
+            X509Certificate issuerCert;
+            try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
+                issuerCert = (X509Certificate) cf.generateCertificate(bais);
+            }
+
+            PrivateKey issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuer.getCertAlgo());
+
+            X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(
+                    X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded()),
+                    new Date()
+            );
+            crlBuilder.setNextUpdate(Date.from(Instant.now().plusSeconds(86400L)));
+
+            for (CertificateEntity revoked : revokedList) {
+                if (revoked.getRevokedTime() == null) {
+                    continue;
+                }
+                BigInteger serial = new BigInteger(revoked.getCertSerialNumber(), 16);
+                Date revocationDate = Date.from(revoked.getRevokedTime().atZone(ZoneId.systemDefault()).toInstant());
+                crlBuilder.addCRLEntry(serial, revocationDate, CRLReason.privilegeWithdrawn);
+            }
+
+            ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuer.getCertAlgo()))
+                    .setProvider("BC")
+                    .build(issuerPrivateKey);
+
+            X509CRLHolder crlHolder = crlBuilder.build(signer);
+            X509CRL crl = new JcaX509CRLConverter().setProvider("BC").getCRL(crlHolder);
+
+            String crlPem = convertCrlToPem(crl);
+
+            response.setContentType("application/pkix-crl");
+            response.setHeader("Content-Disposition", "attachment; filename=" + issuerSerial + "_crl.pem");
+
+            return ResponseEntity.ok()
+                    .header("Content-Disposition", "attachment; filename=" + issuerSerial + "_crl.pem")
+                    .header("Content-Type", "application/pkix-crl")
+                    .body(crlPem);
+
+        } catch (Exception e) {
+            log.error("生成CRL失败", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+        }
+    }
+
+    @Override
+    public ResponseDTO<String> uploadRootCertificate(String pemCombined, String certName) throws Exception {
+        if (pemCombined == null || pemCombined.trim().isEmpty()) {
+            throw new IllegalArgumentException("上传文件内容为空");
+        }
+
+        // 提取第一个证书 PEM
+        int certBegin = pemCombined.indexOf("-----BEGIN CERTIFICATE-----");
+        int certEnd = pemCombined.indexOf("-----END CERTIFICATE-----", certBegin);
+        if (certBegin < 0 || certEnd < 0) {
+            throw new IllegalArgumentException("未找到证书内容(PEM 格式)");
+        }
+        String certPem = pemCombined.substring(certBegin, certEnd + "-----END CERTIFICATE-----".length()).trim();
+
+        // 提取私钥 PEM(支持 PKCS#8 与 RSA PKCS#1)
+        String privateKeyPem = null;
+        String[][] privMarkers = {
+                {"-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"},
+                {"-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----"}
+        };
+        for (String[] m : privMarkers) {
+            int b = pemCombined.indexOf(m[0]);
+            if (b >= 0) {
+                int e = pemCombined.indexOf(m[1], b);
+                if (e > b) {
+                    privateKeyPem = pemCombined.substring(b, e + m[1].length()).trim();
+                    break;
+                }
+            }
+        }
+        if (privateKeyPem == null) {
+            throw new IllegalArgumentException("未找到私钥内容(PEM 格式),上传文件必须包含私钥");
+        }
+
+        // 解析证书对象
+        java.security.cert.CertificateFactory cf =
+                java.security.cert.CertificateFactory.getInstance("X.509", "BC");
+        X509Certificate cert;
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(certPem.getBytes(StandardCharsets.UTF_8))) {
+            cert = (X509Certificate) cf.generateCertificate(bais);
+        }
+
+        // 构造序列号(与系统其它处一致的十六进制大写)
+        String serialHex = cert.getSerialNumber().toString(16).toUpperCase();
+
+        // 唯一性检查:若已有该序列的记录,幂等返回
+        CertificateEntity exist = certificateMapper.selectOne(
+                new LambdaQueryWrapper<CertificateEntity>().eq(CertificateEntity::getCertSerialNumber, serialHex)
+        );
+        if (exist != null) {
+            log.info("根证书已存在,返回已有记录: serial={}", serialHex);
+            return ResponseDTO.error(100001,"证书已存在");
+        }
+
+        // 推断算法:EC/SM2 -> "2",否则视作 RSA 标识 "RSA"
+        String pubAlgo = cert.getPublicKey().getAlgorithm();
+        String certAlgo = ("EC".equalsIgnoreCase(pubAlgo) || "SM2".equalsIgnoreCase(pubAlgo)) ? "2" : "RSA";
+
+        // 公钥 PEM
+        String publicKeyPem = convertPublicKeyToPem(cert.getPublicKey());
+
+        // 证书链:这里以单个根证书为链(如需要可从 pemCombined 提取更多证书)
+        String chainPem = certPem;
+
+        // 存储文件(MinIO),storeCertificateFiles 对私钥做加密存储
+        CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
+                serialHex,
+                certPem,
+                privateKeyPem,
+                publicKeyPem,
+                chainPem
+        );
+
+        // 保存到数据库(字段与 issueRootCertificate 保持一致)
+        CertificateEntity entity = new CertificateEntity();
+        entity.setCertName((certName != null && !certName.trim().isEmpty()) ? certName : cert.getSubjectX500Principal().getName());
+        entity.setCertSerialNumber(serialHex);
+        entity.setCertSubject(cert.getSubjectX500Principal().getName());
+        entity.setCertAlgo(certAlgo);
+        entity.setValidFrom(LocalDateTime.ofInstant(cert.getNotBefore().toInstant(), ZoneId.systemDefault()));
+        entity.setValidTo(LocalDateTime.ofInstant(cert.getNotAfter().toInstant(), ZoneId.systemDefault()));
+        entity.setStatus("1");
+        entity.setIsCa("1");
+        entity.setIsRoot("1");
+        entity.setParentSerial(null);
+        entity.setCertUsage("ROOT_CA");
+        entity.setCertFilePath(fileInfo.getCertPath());
+        entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
+        entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
+        entity.setChainPath(fileInfo.getChainPath());
+
+        certificateMapper.insert(entity);
+
+        log.info("上传根证书并保存成功: serial={}", serialHex);
+        return ResponseDTO.ok();
+    }
+    @Override
+    public ResponseEntity<Map<String, String>> downloadPemSeparate(Long id) {
+        try {
+            CertificateEntity cert = certificateMapper.selectById(id);
+            if (cert == null) {
+                return ResponseEntity.notFound().build();
+            }
+
+            DownloadResponse response = assembleService.downloadPemSeparate(cert.getCertSerialNumber());
+
+            Map<String, String> result = new HashMap<>();
+            result.put("certificate", response.getCertContent());
+            result.put("privateKey", response.getPrivateKeyContent());
+            result.put("publicKey", response.getPublicKeyContent());
+
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("下载PEM失败", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+        }
+    }
+
+    @Override
+    public ResponseEntity<byte[]> downloadPemCombined(Long id, HttpServletResponse response) {
+        try {
+            CertificateEntity cert = certificateMapper.selectById(id);
+            if (cert == null) {
+                return ResponseEntity.notFound().build();
+            }
+
+            DownloadResponse downloadResponse = assembleService.downloadPemCombined(cert.getCertSerialNumber());
+
+            response.setContentType("application/x-pem-file");
+            response.setHeader("Content-Disposition",
+                    "attachment; filename=" + cert.getCertSerialNumber() + "_full.pem");
+
+
+            return ResponseEntity.ok()
+                    .header("Content-Disposition",
+                            "attachment; filename=" + cert.getCertSerialNumber() + "_full.pem")
+                    .header("Content-Type", "application/x-pem-file")
+                    .body(downloadResponse.getCertContent().getBytes(StandardCharsets.UTF_8));
+
+        } catch (Exception e) {
+            log.error("下载合并PEM失败", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+        }
+    }
+
+    @Override
+    public ResponseEntity<byte[]> downloadPkcs12(Pkcs12DownloadRequest request, HttpServletResponse response) {
+        try {
+            CertificateEntity cert = certificateMapper.selectById(request.getId());
+            if (cert == null) {
+                return ResponseEntity.notFound().build();
+            }
+
+            // 验证密码强度
+            String password = request.getPassword();
+            if (password == null || password.length() < 6) {
+                return ResponseEntity.badRequest()
+                        .header("X-Error", "密码至少6位")
+                        .build();
+            }
+
+            DownloadResponse downloadResponse = assembleService.downloadPkcs12(
+                    cert.getCertSerialNumber(),
+                    password
+            );
+
+            // 设置响应头
+            response.setContentType("application/x-pkcs12");
+            response.setHeader("Content-Disposition",
+                    "attachment; filename=" + cert.getCertSerialNumber() + ".p12");
+            response.setHeader("X-Password", password); // 返回密码给用户(可选)
+
+            return ResponseEntity.ok()
+                    .header("Content-Disposition",
+                            "attachment; filename=" + cert.getCertSerialNumber() + ".p12")
+                    .header("Content-Type", "application/x-pkcs12")
+                    .body(downloadResponse.getBinaryContent());
+
+        } catch (Exception e) {
+            log.error("下载PKCS#12失败", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                    .header("X-Error", e.getMessage())
+                    .build();
+        }
+    }
+
+    @Override
+    public ResponseEntity<String> downloadChain(String id, HttpServletResponse response) {
+        try {
+            CertificateEntity cert = certificateMapper.selectById(id);
+            if (cert == null) {
+                return ResponseEntity.notFound().build();
+            }
+
+            // 从MinIO读取证书链
+            String chain = storageService.readChainFile(id);
+            if (chain == null || chain.isEmpty()) {
+                // 如果没有证书链,返回证书本身
+                chain = storageService.readCertificateFile(id);
+            }
+
+            response.setContentType("application/x-pem-file");
+            response.setHeader("Content-Disposition",
+                    "attachment; filename=" + id + "_chain.pem");
+
+            return ResponseEntity.ok()
+                    .header("Content-Disposition",
+                            "attachment; filename=" + id + "_chain.pem")
+                    .header("Content-Type", "application/x-pem-file")
+                    .body(chain);
+
+        } catch (Exception e) {
+            log.error("下载证书链失败", e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+        }
+    }
+
+    private String getSignatureAlgorithm(String keyAlgorithm) {
+        if ("2".equalsIgnoreCase(keyAlgorithm)) {
+            return "SM3withSM2";
+        } else {
+            return "SHA256WithRSA"; // RSA默认
+        }
+    }
+    /**
+     * 证书转PEM
+     */
+    private String convertCertificateToPem(X509Certificate cert) throws Exception {
+        String base64 = Base64.getEncoder().encodeToString(cert.getEncoded());
+        StringBuilder pem = new StringBuilder();
+        pem.append("-----BEGIN CERTIFICATE-----\n");
+        for (int i = 0; i < base64.length(); i += 64) {
+            int end = Math.min(i + 64, base64.length());
+            pem.append(base64, i, end).append("\n");
+        }
+        pem.append("-----END CERTIFICATE-----\n");
+        return pem.toString();
+    }
+
+    /**
+     * 公钥转PEM
+     */
+    private String convertPublicKeyToPem(PublicKey publicKey) throws Exception {
+        String base64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
+        StringBuilder pem = new StringBuilder();
+        pem.append("-----BEGIN PUBLIC KEY-----\n");
+        for (int i = 0; i < base64.length(); i += 64) {
+            int end = Math.min(i + 64, base64.length());
+            pem.append(base64, i, end).append("\n");
+        }
+        pem.append("-----END PUBLIC KEY-----\n");
+        return pem.toString();
+    }
+
+    /**
+     * 私钥转PEM
+     */
+    private String convertPrivateKeyToPem(PrivateKey privateKey) throws Exception {
+        String base64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
+        StringBuilder pem = new StringBuilder();
+        pem.append("-----BEGIN PRIVATE KEY-----\n");
+        for (int i = 0; i < base64.length(); i += 64) {
+            int end = Math.min(i + 64, base64.length());
+            pem.append(base64, i, end).append("\n");
+        }
+        pem.append("-----END PRIVATE KEY-----\n");
+        return pem.toString();
+    }
+    /**
+     * 解析 PEM 私钥(支持 PKCS8 与 RSA PKCS#1),根据算法选择 KeyFactory(RSA 或 EC)
+     */
+    private PrivateKey parsePrivateKeyPem(String privateKeyPem, String algoHint) throws Exception {
+        String base64 = privateKeyPem
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replace("-----BEGIN RSA PRIVATE KEY-----", "")
+                .replace("-----END RSA PRIVATE KEY-----", "")
+                .replaceAll("\\s", "");
+        byte[] keyBytes = Base64.getDecoder().decode(base64);
+        boolean ecKey = isEcKeyAlgorithm(algoHint);
+
+        try {
+            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+            if (ecKey) {
+                return KeyFactory.getInstance("EC", "BC").generatePrivate(spec);
+            } else {
+                return KeyFactory.getInstance("RSA", "BC").generatePrivate(spec);
+            }
+        } catch (Exception e) {
+            if (ecKey) {
+                throw e;
+            }
+
+            org.bouncycastle.asn1.pkcs.RSAPrivateKey rsa = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(keyBytes);
+            java.security.spec.RSAPrivateCrtKeySpec crtSpec =
+                    new java.security.spec.RSAPrivateCrtKeySpec(
+                            rsa.getModulus(),
+                            rsa.getPublicExponent(),
+                            rsa.getPrivateExponent(),
+                            rsa.getPrime1(),
+                            rsa.getPrime2(),
+                            rsa.getExponent1(),
+                            rsa.getExponent2(),
+                            rsa.getCoefficient()
+                    );
+            return KeyFactory.getInstance("RSA", "BC").generatePrivate(crtSpec);
+        }
+    }
+
+    /**
+     * PEM公钥解析工具
+     */
+    private PublicKey parsePublicKeyPem(String publicKeyPem, String algoHint) throws Exception {
+        String base64 = publicKeyPem
+                .replace("-----BEGIN PUBLIC KEY-----", "")
+                .replace("-----END PUBLIC KEY-----", "")
+                .replaceAll("\\s", "");
+        byte[] keyBytes = Base64.getDecoder().decode(base64);
+        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+        if (isEcKeyAlgorithm(algoHint)) {
+            return KeyFactory.getInstance("EC", "BC").generatePublic(spec);
+        } else {
+            return KeyFactory.getInstance("RSA", "BC").generatePublic(spec);
+        }
+    }
+
+    private boolean isEcKeyAlgorithm(String algorithm) {
+        return "2".equalsIgnoreCase(algorithm)
+                || "SM2".equalsIgnoreCase(algorithm)
+                || "EC".equalsIgnoreCase(algorithm);
+    }
+
+    private String convertCrlToPem(X509CRL crl) throws Exception {
+        String base64 = Base64.getEncoder().encodeToString(crl.getEncoded());
+        StringBuilder pem = new StringBuilder();
+        pem.append("-----BEGIN X509 CRL-----\n");
+        for (int i = 0; i < base64.length(); i += 64) {
+            int end = Math.min(i + 64, base64.length());
+            pem.append(base64, i, end).append("\n");
+        }
+        pem.append("-----END X509 CRL-----\n");
+        return pem.toString();
+    }
+}

+ 96 - 0
src/main/java/com/ymwk/icvp/pki/service/impl/CertificateStorageService.java

@@ -0,0 +1,96 @@
+package com.ymwk.icvp.pki.service.impl;
+
+import com.ymwk.icvp.pki.model.CertificateFileInfo;
+import com.ymwk.icvp.utils.MinioClientService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+
+@Component
+@Slf4j
+public class CertificateStorageService {
+
+    @Autowired
+    private MinioClientService minioUtil;
+
+    @Autowired
+    private CryptoService cryptoService; // 加密服务
+
+    /**
+     * 存储证书相关文件(分开存储)
+     */
+    public CertificateFileInfo storeCertificateFiles(
+            String serialNumber,
+            String certPem,
+            String privateKeyPem,
+            String publicKeyPem,
+            String chainPem) throws Exception {
+
+        CertificateFileInfo fileInfo = new CertificateFileInfo();
+        fileInfo.setSerialNumber(serialNumber);
+
+        // 1. 存储证书文件(不加密)
+        String certPath = String.format("certs/%s/cert.pem", serialNumber);
+        minioUtil.uploadObject(certPath, certPem, "application/x-pem-file");
+        fileInfo.setCertPath(certPath);
+        log.info("证书文件已存储: {}", certPath);
+
+        // 2. 存储私钥文件(AES加密)
+        String encryptedPrivateKey = cryptoService.encrypt(privateKeyPem);
+        String privateKeyPath = String.format("certs/%s/private-key.enc", serialNumber);
+        minioUtil.uploadObject(privateKeyPath, encryptedPrivateKey, "application/octet-stream");
+        fileInfo.setPrivateKeyPath(privateKeyPath);
+        log.info("私钥文件已加密存储: {}", privateKeyPath);
+
+        // 3. 存储公钥文件(不加密)
+        String publicKeyPath = String.format("certs/%s/public-key.pem", serialNumber);
+        minioUtil.uploadObject(publicKeyPath, publicKeyPem, "application/x-pem-file");
+        fileInfo.setPublicKeyPath(publicKeyPath);
+        log.info("公钥文件已存储: {}", publicKeyPath);
+
+        // 4. 存储证书链(如果有)
+        if (chainPem != null && !chainPem.isEmpty()) {
+            String chainPath = String.format("certs/%s/chain.pem", serialNumber);
+            minioUtil.uploadObject(chainPath, chainPem, "application/x-pem-file");
+            fileInfo.setChainPath(chainPath);
+            log.info("证书链文件已存储: {}", chainPath);
+        }
+
+        return fileInfo;
+    }
+
+    /**
+     * 读取证书文件
+     */
+    public String readCertificateFile(String serialNumber) throws Exception {
+        String path = String.format("certs/%s/cert.pem", serialNumber);
+        return minioUtil.downloadFileAsString(path);
+    }
+
+    /**
+     * 读取并解密私钥文件
+     */
+    public String readPrivateKeyFile(String serialNumber) throws Exception {
+        String path = String.format("certs/%s/private-key.enc", serialNumber);
+        String encrypted = minioUtil.downloadFileAsString(path);
+        return cryptoService.decrypt(encrypted);
+    }
+
+    /**
+     * 读取公钥文件
+     */
+    public String readPublicKeyFile(String serialNumber) throws Exception {
+        String path = String.format("certs/%s/public-key.pem", serialNumber);
+        return minioUtil.downloadFileAsString(path);
+    }
+
+    /**
+     * 读取证书链
+     */
+    public String readChainFile(String serialNumber) throws Exception {
+        String path = String.format("certs/%s/chain.pem", serialNumber);
+        return minioUtil.downloadFileAsString(path);
+    }
+}

+ 74 - 0
src/main/java/com/ymwk/icvp/pki/service/impl/CryptoService.java

@@ -0,0 +1,74 @@
+package com.ymwk.icvp.pki.service.impl;
+
+import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+@Component
+@Slf4j
+public class CryptoService {
+
+
+    private final String masterKey = "12345678901234567890123456789012"; // 32字节的主密钥
+
+    private static final String ALGORITHM = "AES/GCM/NoPadding";
+    private static final int GCM_IV_LENGTH = 12;
+    private static final int GCM_TAG_LENGTH = 128;
+
+    /**
+     * AES-GCM加密
+     */
+    public String encrypt(String plaintext) throws Exception {
+        byte[] iv = new byte[GCM_IV_LENGTH];
+        SecureRandom random = new SecureRandom();
+        random.nextBytes(iv);
+
+        Cipher cipher = Cipher.getInstance(ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(
+                masterKey.getBytes(StandardCharsets.UTF_8),
+                "AES"
+        );
+        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
+
+        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
+
+        // 将IV和密文合并(Base64编码)
+        byte[] combined = new byte[iv.length + ciphertext.length];
+        System.arraycopy(iv, 0, combined, 0, iv.length);
+        System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
+
+        return Base64.getEncoder().encodeToString(combined);
+    }
+
+    /**
+     * AES-GCM解密
+     */
+    public String decrypt(String ciphertextBase64) throws Exception {
+        byte[] combined = Base64.getDecoder().decode(ciphertextBase64);
+
+        // 提取IV和密文
+        byte[] iv = new byte[GCM_IV_LENGTH];
+        byte[] ciphertext = new byte[combined.length - GCM_IV_LENGTH];
+        System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
+        System.arraycopy(combined, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
+
+        Cipher cipher = Cipher.getInstance(ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(
+                masterKey.getBytes(StandardCharsets.UTF_8),
+                "AES"
+        );
+        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
+
+        byte[] plaintext = cipher.doFinal(ciphertext);
+        return new String(plaintext, StandardCharsets.UTF_8);
+    }
+}

+ 20 - 0
src/main/java/com/ymwk/icvp/pki/service/impl/UserServiceImpl.java

@@ -0,0 +1,20 @@
+package com.ymwk.icvp.pki.service.impl;
+
+import com.ymwk.icvp.pki.entity.User;
+import com.ymwk.icvp.pki.mapper.UserMapper;
+import com.ymwk.icvp.pki.service.UserService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class UserServiceImpl implements UserService {
+    @Resource
+    private UserMapper userMapper;
+
+    @Override
+    public List<User> listAll() {
+        return userMapper.selectList(null);
+    }
+}

+ 17 - 0
src/main/java/com/ymwk/icvp/pki/utils/Base64Utils.java

@@ -0,0 +1,17 @@
+package com.ymwk.icvp.pki.utils;
+
+
+import java.io.IOException;
+import java.util.Base64;
+
+
+public class Base64Utils {
+
+    public static byte[] decode(String str) throws IOException{
+		return Base64.getDecoder().decode(str);
+    }
+
+    public static String encode(byte[] bytes){
+		return new String(Base64.getEncoder().encode(bytes));
+    }
+}

+ 55 - 0
src/main/java/com/ymwk/icvp/pki/utils/issueCertUtil.java

@@ -0,0 +1,55 @@
+package com.ymwk.icvp.pki.utils;
+
+import com.ymwk.icvp.pki.model.IssueRequest;
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import java.security.spec.ECGenParameterSpec;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+
+public class issueCertUtil {
+
+    public static KeyPair generateKeyPair(String algorithm, Integer keySize) throws Exception {
+        KeyPairGenerator generator;
+
+        if ("2".equalsIgnoreCase(algorithm)) {
+            // 1. SM2密钥生成(使用 BouncyCastle 提供者)
+            generator = KeyPairGenerator.getInstance("EC", "BC");
+            // SM2 算法使用特定椭圆曲线参数: sm2p256v1
+            ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
+            generator.initialize(sm2Spec, new SecureRandom());
+
+        } else { // 默认为 RSA
+            // 2. RSA 密钥生成(使用 BouncyCastle 提供者)
+            generator = KeyPairGenerator.getInstance("RSA", "BC");
+            int strength = (keySize != null && keySize >= 2048) ? keySize : 2048;
+            generator.initialize(strength, new SecureRandom());
+        }
+
+        return generator.generateKeyPair();
+    }
+    /**
+     * 构建主题DN
+     */
+    public static String buildSubjectDn(IssueRequest request) {
+        StringBuilder dn = new StringBuilder();
+        dn.append("CN=").append(request.getCommonName());
+        if (request.getOrganization() != null && !request.getOrganization().isEmpty()) {
+            dn.append(", O=").append(request.getOrganization());
+        }
+        if (request.getOrganizationalUnit() != null && !request.getOrganizationalUnit().isEmpty()) {
+            dn.append(", OU=").append(request.getOrganizationalUnit());
+        }
+        if (request.getLocality() != null && !request.getLocality().isEmpty()) {
+            dn.append(", L=").append(request.getLocality());
+        }
+        if (request.getState() != null && !request.getState().isEmpty()) {
+            dn.append(", ST=").append(request.getState());
+        }
+        if (request.getCountry() != null && !request.getCountry().isEmpty()) {
+            dn.append(", C=").append(request.getCountry());
+        }
+        return dn.toString();
+    }
+}

+ 132 - 0
src/main/java/com/ymwk/icvp/signature/controller/PictureController.java

@@ -0,0 +1,132 @@
+package com.ymwk.icvp.signature.controller;
+
+import com.ymwk.icvp.component.PageParam;
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.mapper.PictureMapper;
+import com.ymwk.icvp.signature.model.PictureGenerateRequest;
+import com.ymwk.icvp.signature.model.PictureListRequest;
+import com.ymwk.icvp.signature.service.PictureService;
+import com.ymwk.icvp.signature.service.impl.SealGenerateService;
+import com.ymwk.icvp.utils.MinioClientService;
+import com.ymwk.icvp.utils.MinioConfig;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1/pictures")
+public class PictureController {
+
+    @Autowired
+    private  PictureService pictureService;
+    @Autowired
+    private PictureMapper pictureMapper;
+    @Autowired
+    private  SealGenerateService sealGenerateService;
+    @Autowired
+    private  MinioClientService minioClientService;
+    @Autowired
+    private MinioConfig minioConfig;
+
+
+
+
+
+    //印模列表
+    @PostMapping("/list")
+    public ResponseDTO<PageResult<Picture>> getPictureList(@RequestBody PictureListRequest pictureListRequest) {
+        return pictureService.getPictureList(pictureListRequest);
+    }
+    //生成印模图片并上传到MinIO
+    @ResponseBody
+    @RequestMapping("/generate")
+    public ResponseDTO<Map<String, String>> generateSeal(
+            @RequestBody PictureGenerateRequest request
+    ) throws Exception {
+
+        // 1. 生成图片
+        BufferedImage image = sealGenerateService.generate(request);
+
+        // 2. 将BufferedImage转换为MultipartFile
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", baos);
+        baos.flush();
+        byte[] imageBytes = baos.toByteArray();
+        baos.close();
+
+        // 创建MultipartFile对象
+        MultipartFile multipartFile = new MockMultipartFile(
+                "seal.png",
+                "seal.png",
+                "image/png",
+                imageBytes
+        );
+
+        // 3. 上传到MinIO
+        String fileName = minioClientService.uploadFile(multipartFile);
+
+        // 4. 生成下载链接
+        String downloadUrl = minioConfig.getEndpoint() + "/" +
+                minioConfig.getBucketName() + "/" +
+                fileName;
+
+
+        Picture picture = new Picture();
+        picture.setName(request.getName() != null && !request.getName().isEmpty()
+                ? request.getName() : fileName);
+        picture.setUrl(downloadUrl);
+        picture.setCreateTime(new Date());
+        pictureMapper.insert(picture);   // 或 pictureService.save(picture)
+
+        // 5. 返回下载链接
+        Map<String, String> response = new HashMap<>();
+        response.put("downloadUrl", downloadUrl);
+        return ResponseDTO.ok(response);
+    }
+    //下载印模图片
+    //TODO 下载出来0kb的空文件
+    @GetMapping("/download")
+    public void downloadSeal(@RequestParam String fileName, HttpServletResponse response) {
+        try (InputStream is = minioClientService.downloadFile(fileName);
+             OutputStream os = response.getOutputStream()) {
+
+            response.setContentType("application/octet-stream");
+            response.setHeader("Content-Disposition",
+                    "attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + "\"");
+
+            byte[] buffer = new byte[8192];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                os.write(buffer, 0, len);
+            }
+            os.flush();
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        }
+    }
+    //删除印模图片
+    @PostMapping("/delete")
+    public ResponseDTO<String> deleteSeal(@RequestParam Integer id) {
+
+
+            return pictureService.deletePicture(id);
+
+    }
+
+}

+ 113 - 0
src/main/java/com/ymwk/icvp/signature/controller/SealController.java

@@ -0,0 +1,113 @@
+package com.ymwk.icvp.signature.controller;
+
+
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.model.*;
+import com.ymwk.icvp.signature.service.SealService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/seal")
+@RequiredArgsConstructor
+public class SealController {
+
+    private final SealService sealService;
+
+    /**
+     * 印章列表(条件查询)
+     */
+    @PostMapping("/list")
+    public ResponseDTO<PageResult<Seal>> list(@RequestBody SealQueryDTO queryDTO) {
+        return sealService.pageSeal(queryDTO);
+    }
+
+    /**
+     * 印章详情
+     */
+    @GetMapping("/detail/{id}")
+    public ResponseDTO<Seal> detail(@PathVariable Long id) {
+        return sealService.getSealDetail(id);
+    }
+
+    /**
+     * 生成印章
+     */
+    @PostMapping("/create")
+    public ResponseDTO<Long> create(@Validated @RequestBody SealCreateDTO createDTO) {
+        return sealService.createSeal(createDTO);
+    }
+
+    /**
+     * 冻结印章
+     */
+    @PostMapping("/freeze/{id}")
+    public ResponseDTO<Void> freeze(@PathVariable Long id) {
+        return sealService.freeze(id);
+    }
+
+    /**
+     * 解冻印章
+     */
+    @PostMapping("/unfreeze/{id}")
+    public ResponseDTO<Void> unfreeze(@PathVariable Long id) {
+        return sealService.unfreeze(id);
+    }
+
+    /**
+     * 销毁印章
+     */
+    @PostMapping("/destroy/{id}")
+    public ResponseDTO<Void> destroy(@PathVariable Long id) {
+        return sealService.destroy(id);
+    }
+
+    /**
+     * 印章延期
+     */
+    @PostMapping("/extend")
+    public ResponseDTO<Void> extend(@Validated @RequestBody SealExtendDTO extendDTO) {
+        return sealService.extend(extendDTO);
+    }
+
+    /**
+     * 绑定/换绑证书
+     */
+    @PostMapping("/bindCert")
+    public ResponseDTO<Void> bindCert(@Validated @RequestBody SealBindCertDTO bindCertDTO) {
+        return sealService.bindCert(bindCertDTO);
+    }
+
+    /**
+     * 印章有效性验证
+     */
+    @PostMapping("/verify/{id}")
+    public ResponseDTO<Boolean> verify(@PathVariable Long id) {
+        return sealService.verify(id);
+    }
+
+    /**
+     * 下载印章文件:SM2 返回 ESL,RSA 返回包含印模和证书的 ZIP
+     */
+    @GetMapping("/download/{id}")
+    public ResponseEntity<?> download(@PathVariable Long id) {
+        ResponseDTO<SealDownload> result = sealService.download(id);
+        if (!Integer.valueOf(ResponseDTO.OK_CODE).equals(result.getCode())
+                || result.getData() == null) {
+            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
+        }
+
+        SealDownload download = result.getData();
+        return ResponseEntity.ok()
+                .header("Content-Disposition", "attachment; filename=\"" + download.getFileName() + "\"")
+                .contentType(MediaType.parseMediaType(download.getContentType()))
+                .body(download.getData());
+    }
+}

+ 740 - 0
src/main/java/com/ymwk/icvp/signature/controller/SignatureController.java

@@ -0,0 +1,740 @@
+package com.ymwk.icvp.signature.controller;
+
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.PdfReader;
+import com.itextpdf.text.pdf.PdfSignatureAppearance;
+import com.itextpdf.text.pdf.PdfStamper;
+import com.itextpdf.text.Image;
+import com.itextpdf.text.pdf.security.*;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.mapper.CertificateMapper;
+import com.ymwk.icvp.pki.service.impl.CertificateAssembleService;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.mapper.PictureMapper;
+import com.ymwk.icvp.signature.mapper.SealMapper;
+import com.ymwk.icvp.signature.model.SealFormatEnum;
+import com.ymwk.icvp.signature.utils.*;
+import com.ymwk.icvp.utils.MinioClientService;
+import com.ymwk.icvp.utils.fileConvert;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
+import org.apache.pdfbox.pdmodel.interactive.form.PDField;
+import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
+import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.Store;
+import org.apache.commons.io.FileUtils;
+import org.ofdrw.core.basicType.ST_Box;
+import org.ofdrw.core.basicType.ST_Loc;
+import org.ofdrw.core.signatures.range.Reference;
+import org.ofdrw.core.signatures.range.References;
+import org.ofdrw.core.signatures.sig.SignedInfo;
+import org.ofdrw.gm.ses.v4.SESeal;
+import org.ofdrw.gm.ses.v4.SES_Signature;
+import org.ofdrw.reader.OFDReader;
+import org.ofdrw.sign.NumberFormatAtomicSignID;
+import org.ofdrw.sign.OFDSigner;
+import org.ofdrw.sign.SignMode;
+import org.ofdrw.sign.signContainer.SESV4Container;
+import org.ofdrw.sign.stamppos.NormalStampPos;
+import org.ofdrw.sign.verify.container.SESV4ValidateContainer;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.cert.CertificateFactory;
+import java.security.PrivateKey;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.zip.ZipException;
+
+@RestController
+@RequestMapping("/api/v1/signature")
+@RequiredArgsConstructor
+@Slf4j
+public class SignatureController {
+
+
+  private  final MinioClientService minioClientService;
+  private final SealMapper sealMapper;
+  private final PictureMapper pictureMapper;
+  private final CertificateMapper certificateMapper;
+  private final CertificateAssembleService certificateAssembleService;
+    /**
+     * PDF 转 OFD
+     */
+    @PostMapping(
+            value = "/pdf-to-ofd",
+            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
+            produces = "application/ofd"
+    )
+    public ResponseEntity<byte[]> pdfToOfd(
+            @RequestParam("file") MultipartFile file) throws Exception {
+
+        validateFile(file);
+
+        try (InputStream input = file.getInputStream();
+             InputStream output = fileConvert.pdfToOfdEx(input)) {
+
+            if (output == null) {
+                throw new IllegalStateException("PDF 转 OFD 失败");
+            }
+
+            byte[] data = output.readAllBytes();
+
+            return ResponseEntity.ok()
+                    .header(
+                            HttpHeaders.CONTENT_DISPOSITION,
+                            ContentDisposition.attachment()
+                                    .filename("converted.ofd")
+                                    .build()
+                                    .toString()
+                    )
+                    .contentType(MediaType.parseMediaType("application/ofd"))
+                    .contentLength(data.length)
+                    .body(data);
+        }
+    }
+
+    /**
+     * OFD 转 PDF
+     */
+    @PostMapping(
+            value = "/ofd-to-pdf",
+            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
+            produces = MediaType.APPLICATION_PDF_VALUE
+    )
+    public ResponseEntity<byte[]> ofdToPdf(
+            @RequestParam("file") MultipartFile file) throws Exception {
+
+        validateFile(file);
+
+        try (InputStream input = file.getInputStream();
+             InputStream output = fileConvert.ofdToPdfEx(input)) {
+
+            if (output == null) {
+                throw new IllegalStateException("OFD 转 PDF 失败");
+            }
+
+            byte[] data = output.readAllBytes();
+
+            return ResponseEntity.ok()
+                    .header(
+                            HttpHeaders.CONTENT_DISPOSITION,
+                            ContentDisposition.attachment()
+                                    .filename("converted.pdf")
+                                    .build()
+                                    .toString()
+                    )
+                    .contentType(MediaType.APPLICATION_PDF)
+                    .contentLength(data.length)
+                    .body(data);
+        }
+    }
+
+    private void validateFile(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("上传文件不能为空");
+        }
+    }
+
+    @PostMapping(value = "signatureByPosition", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Map<String, Object> signatureByPosition(@RequestParam("sealId") Long sealId,
+                                                   @RequestParam("x") Integer x,
+                                                   @RequestParam("y") Integer y,
+                                                   @RequestParam("signType") Integer signType,
+                                                   @RequestParam("file") MultipartFile file) {
+        Map<String, Object> resultMap = new HashMap<String, Object>();
+        try {
+            if (sealId == null || x == null || y == null || signType == null) {
+                resultMap.put("code", DataResponse.CODE_NULL_PARAM);
+                resultMap.put("message", "签章参数不能为空");
+                return resultMap;
+            }
+            if (file == null || file.isEmpty()) {
+                resultMap.put("code", DataResponse.CODE_NULL_PARAM);
+                resultMap.put("message", "签章文件不能为空");
+                return resultMap;
+            }
+            if (signType != 0 && signType != 1) {
+                resultMap.put("code", DataResponse.CODE_ERROR_PARAM);
+                resultMap.put("message", "signType仅支持0(第一页)或1(每一页)");
+                return resultMap;
+            }
+            if (x < 0 || y < 0 || x > 50000 || y > 50000) {
+                resultMap.put("code", DataResponse.CODE_NULL_PARAM);
+                resultMap.put("message", "签章坐标需在0到50000之间");
+                return resultMap;
+            }
+
+            Seal seal = sealMapper.selectById(sealId);
+            if (seal == null) {
+                resultMap.put("code", DataResponse.CODE_NULL_PARAM);
+                resultMap.put("message", "印章不存在");
+                return resultMap;
+            }
+            if (seal.getImpressionId() == null || seal.getCertId() == null) {
+                resultMap.put("code", DataResponse.CODE_ERROR);
+                resultMap.put("message", "印章未绑定印模或数字证书");
+                return resultMap;
+            }
+
+            Picture picture = pictureMapper.selectById(seal.getImpressionId());
+            if (picture == null || picture.getUrl() == null || picture.getUrl().isBlank()) {
+                resultMap.put("code", DataResponse.CODE_ERROR);
+                resultMap.put("message", "关联印模或印模下载地址不存在");
+                return resultMap;
+            }
+
+            CertificateEntity certificate = certificateMapper.selectById(seal.getCertId());
+            if (certificate == null || certificate.getCertSerialNumber() == null) {
+                resultMap.put("code", DataResponse.CODE_ERROR);
+                resultMap.put("message", "关联数字证书或证书序列号不存在");
+                return resultMap;
+            }
+
+            byte[] signatureImage;
+            try (InputStream imageStream = minioClientService.getObject(extractObjectName(picture.getUrl()))) {
+                signatureImage = imageStream.readAllBytes();
+            }
+            X509Certificate x509Certificate =
+                    certificateAssembleService.getCertificate(certificate.getCertSerialNumber());
+            PrivateKey privateKey =
+                    certificateAssembleService.getPrivateKey(
+                            certificate.getCertSerialNumber(), certificate.getCertAlgo());
+
+            byte[] signedPdf = file.getBytes();
+            int pageCount = getPdfPageCount(signedPdf);
+            int lastPage = signType == 0 ? 1 : pageCount;
+            for (int page = 1; page <= lastPage; page++) {
+                signedPdf = signPdfPage(signedPdf, page, x, y, signatureImage, x509Certificate, privateKey);
+            }
+
+            String objectName = "signed/" + UUID.randomUUID() + ".pdf";
+            minioClientService.uploadBytes(objectName, signedPdf, MediaType.APPLICATION_PDF_VALUE);
+            String pdfUrl = minioClientService.getPresignedUrl(objectName, 60);
+
+            resultMap.put("code", DataResponse.CODE_SUCCESS);
+            resultMap.put("message", "签章成功");
+            Map<String, Object> dataMap = new HashMap<String, Object>();
+            dataMap.put("pdfIden", objectName);
+            dataMap.put("pdfUrl", pdfUrl);
+            resultMap.put("data", dataMap);
+            return resultMap;
+        } catch (Exception e) {
+            log.error("PDF坐标签章失败", e);
+            resultMap.put("code", DataResponse.CODE_EXCEPTION);
+            resultMap.put("message", "签章发生异常 " + e.getMessage());
+            return resultMap;
+        }
+    }
+
+    private int getPdfPageCount(byte[] pdfBytes) throws Exception {
+        PdfReader reader = new PdfReader(pdfBytes);
+        try {
+            return reader.getNumberOfPages();
+        } finally {
+            reader.close();
+        }
+    }
+
+    private byte[] signPdfPage(byte[] pdfBytes, int page, int x, int y, byte[] signatureImage,
+                               X509Certificate certificate, PrivateKey privateKey) throws Exception {
+        PdfReader reader = new PdfReader(pdfBytes);
+        try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
+            PdfStamper stamper = PdfStamper.createSignature(reader, signedPdfOutput, '\0', null, true);
+            try {
+                float pageWidth = reader.getPageSize(page).getWidth();
+                float pageHeight = reader.getPageSize(page).getHeight();
+                float positionX = pageWidth / 50000 * x;
+                float positionY = pageHeight / 50000 * y;
+
+                PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
+                appearance.setSignDate(new GregorianCalendar());
+                appearance.setReason("Document Sign");
+                appearance.setLocation("System");
+                appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
+                appearance.setSignatureGraphic(Image.getInstance(signatureImage));
+                appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
+                appearance.setVisibleSignature(
+                        new Rectangle(positionX - 75, positionY - 75, positionX + 75, positionY + 75),
+                        page,
+                        UUID.randomUUID().toString().replace("-", ""));
+
+                Certificate[] chain = {certificate};
+                ExternalSignature signature = new PrivateKeySignature(privateKey, "SHA256", "BC");
+                ExternalDigest digest = new BouncyCastleDigest();
+                MakeSignature.signDetached(appearance, digest, signature, chain,
+                        null, null, null, 0, MakeSignature.CryptoStandard.CMS);
+            } finally {
+                stamper.close();
+            }
+            return signedPdfOutput.toByteArray();
+        } finally {
+            reader.close();
+        }
+    }
+
+    private String extractObjectName(String url) {
+        String objectName = url.substring(url.lastIndexOf('/') + 1);
+        int queryIndex = objectName.indexOf('?');
+        return queryIndex >= 0 ? objectName.substring(0, queryIndex) : objectName;
+    }
+
+    @PostMapping(value = "signatrueOFD", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Map<String, Object> signatrueOFD(@RequestParam("sealId") Long sealId,
+                                            @RequestParam("x") Integer x,
+                                            @RequestParam("y") Integer y,
+                                            @RequestParam("signType") Integer signType,
+                                            @RequestParam("file") MultipartFile file,
+                                            @RequestParam(defaultValue = "40") Double width,
+                                            @RequestParam(defaultValue = "40") Double height) {
+        Map<String, Object> result = new HashMap<>();
+        if (sealId == null || x == null || y == null || signType == null || file == null || file.isEmpty()) {
+            return ofdSignatureResult(result, DataResponse.CODE_NULL_PARAM,
+                    "印章ID、坐标、签章类型和OFD文件不能为空", null);
+        }
+        if (signType != 0 && signType != 1) {
+            return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                    "signType仅支持0(首页签章)或1(每页签章)", null);
+        }
+        if (x < 0 || y < 0 || x > 50000 || y > 50000) {
+            return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                    "签章坐标需在0到50000之间", null);
+        }
+        if (width == null || height == null || width <= 0 || height <= 0) {
+            return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM, "印章宽度和高度必须大于0", null);
+        }
+
+        Path workingDirectory = null;
+        try {
+            Seal sealRecord = sealMapper.selectById(sealId);
+            if (sealRecord == null) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR, "印章不存在", null);
+            }
+            if (!SealFormatEnum.ESL.getCode().equalsIgnoreCase(sealRecord.getSealFormat())
+                    || sealRecord.getSealFilePath() == null || sealRecord.getSealFilePath().isBlank()) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR,
+                        "当前印章没有可用的ESL印章文件", null);
+            }
+            if (sealRecord.getCertId() == null) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR, "印章未绑定数字证书", null);
+            }
+            CertificateEntity certificateEntity = certificateMapper.selectById(sealRecord.getCertId());
+            if (certificateEntity == null || certificateEntity.getCertSerialNumber() == null) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR, "关联数字证书不存在", null);
+            }
+
+            workingDirectory = Files.createTempDirectory("icvp-ofd-sign-");
+            Path ofdFilePath = workingDirectory.resolve("source.ofd");
+            Path sealFilePath = workingDirectory.resolve("seal.esl");
+            Path out = workingDirectory.resolve("signed.ofd");
+            try (InputStream ofdInput = file.getInputStream()) {
+                Files.copy(ofdInput, ofdFilePath);
+            }
+            if (!hasZipSignature(ofdFilePath)) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                        "上传文件不是有效的OFD文件;如果是PDF,请先转换为OFD再签章", null);
+            }
+            downloadMinioObject(extractObjectName(sealRecord.getSealFilePath()), sealFilePath);
+
+            PrivateKey privateKey = certificateAssembleService.getPrivateKey(
+                    certificateEntity.getCertSerialNumber(), certificateEntity.getCertAlgo());
+            Certificate certificate = certificateAssembleService.getCertificate(
+                    certificateEntity.getCertSerialNumber());
+            SESeal eslSeal = SESeal.getInstance(Files.readAllBytes(sealFilePath));
+
+            try (OFDReader reader = new OFDReader(ofdFilePath);
+                 OFDSigner signer = new OFDSigner(reader, out, new NumberFormatAtomicSignID())) {
+                SESV4Container signContainer = new SESV4Container(privateKey, eslSeal, certificate);
+                signer.setSignMode(SignMode.WholeProtected);
+                signer.setSignContainer(signContainer);
+
+                int lastPage = signType == 0 ? 1 : reader.getNumberOfPages();
+                for (int pageNumber = 1; pageNumber <= lastPage; pageNumber++) {
+                    ST_Box pageSize = reader.getPageSize(reader.getPage(pageNumber));
+                    double positionX = pageSize.getTopLeftX()
+                            + pageSize.getWidth() * x / 50000.0 - width / 2;
+                    double positionY = pageSize.getTopLeftY()
+                            + pageSize.getHeight() * y / 50000.0 - height / 2;
+                    signer.addApPos(new NormalStampPos(pageNumber, positionX, positionY, width, height));
+                }
+                signer.exeSign();
+            }
+
+            String objectName = "signed/" + UUID.randomUUID().toString().replace("-", "") + ".ofd";
+            minioClientService.uploadBytes(objectName, Files.readAllBytes(out), "application/ofd");
+            String downloadUrl = minioClientService.getPresignedUrl(objectName, 60);
+            Map<String, Object> data = new HashMap<>();
+            data.put("ofdIden", objectName);
+            data.put("ofdUrl", downloadUrl);
+            return ofdSignatureResult(result, DataResponse.CODE_SUCCESS, "签章成功", data);
+        } catch (ZipException e) {
+            log.warn("上传文件不是有效的OFD压缩包", e);
+            return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                    "上传文件不是有效的OFD文件或文件已损坏,请检查文件格式", null);
+        } catch (Exception e) {
+            log.error("OFD签章失败", e);
+            return ofdSignatureResult(result, DataResponse.CODE_EXCEPTION, "OFD签章失败: " + e.getMessage(), null);
+        } finally {
+            if (workingDirectory != null) {
+                try {
+                    FileUtils.deleteDirectory(workingDirectory.toFile());
+                } catch (IOException e) {
+                    log.warn("清理OFD签章临时目录失败: {}", workingDirectory, e);
+                }
+            }
+        }
+    }
+
+    @PostMapping(value = "verifyOFDSignature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Map<String, Object> verifyOFDSignature(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        if (file == null || file.isEmpty()) {
+            return ofdSignatureResult(result, DataResponse.CODE_NULL_PARAM, "待验签OFD文件不能为空", null);
+        }
+
+        Path workingDirectory = null;
+        try {
+            workingDirectory = Files.createTempDirectory("icvp-ofd-verify-");
+            Path ofdFilePath = workingDirectory.resolve("source.ofd");
+            try (InputStream ofdInput = file.getInputStream()) {
+                Files.copy(ofdInput, ofdFilePath);
+            }
+            if (!hasZipSignature(ofdFilePath)) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                        "上传文件不是有效的OFD文件;如果是PDF,请先转换为OFD再验签", null);
+            }
+
+            Map<String, Object> verification = new HashMap<>();
+            try (OFDReader reader = new OFDReader(ofdFilePath)) {
+                var signatureList = reader.getDefaultSignatures();
+                List<org.ofdrw.core.signatures.Signature> signatureRecords = signatureList == null
+                        || signatureList.getSignatures() == null
+                        ? Collections.emptyList() : signatureList.getSignatures();
+                List<Map<String, Object>> signatures = new ArrayList<>();
+                boolean allValid = !signatureRecords.isEmpty();
+                SESV4ValidateContainer signatureValidator = new SESV4ValidateContainer();
+
+                for (int i = 0; i < signatureRecords.size(); i++) {
+                    org.ofdrw.core.signatures.Signature signatureRecord = signatureRecords.get(i);
+                    Map<String, Object> signatureResult = new HashMap<>();
+                    signatureResult.put("name", signatureRecord.getID() == null
+                            ? "signature-" + (i + 1) : signatureRecord.getID());
+                    signatureResult.put("type", signatureRecord.getType() == null
+                            ? "Seal" : signatureRecord.getType().name());
+
+                    var resourceLocator = reader.getResourceLocator();
+                    boolean contentIntegrityValid = false;
+                    boolean cryptographicSignatureValid = false;
+                    Boolean sealMatched = null;
+                    try {
+                        ST_Loc signaturesPath = reader.getDefaultDocSignaturesPath();
+                        resourceLocator.save();
+                        try {
+                            resourceLocator.cd("/");
+                            resourceLocator.cd(signaturesPath.parent());
+                            ST_Loc signaturePath = signatureRecord.getBaseLoc();
+                            Path signatureFile = resourceLocator.getFile(signaturePath);
+                            var signatureDescription = resourceLocator.get(
+                                    signaturePath,
+                                    org.ofdrw.core.signatures.sig.Signature::new);
+                            SignedInfo signedInfo = signatureDescription.getSignedInfo();
+                            signatureResult.put("structureValid",
+                                    signatureDescription.getSignedValue() != null
+                                            && signedInfo.getReferences() != null);
+                            signatureResult.put("signatureMethod", signedInfo.getSignatureMethod());
+                            if (signedInfo.getSignatureDateTime() != null) {
+                                signatureResult.put("signingTime", signedInfo.getSignatureDateTime());
+                            }
+
+                            resourceLocator.save();
+                            try {
+                                resourceLocator.cd(signaturePath.parent());
+                                References references = signedInfo.getReferences();
+                                MessageDigest digest = MessageDigest.getInstance(
+                                        references.getCheckMethod(), new BouncyCastleProvider());
+                                contentIntegrityValid = true;
+                                for (Reference reference : references.getReferences()) {
+                                    Path referencedFile = resourceLocator.getFile(reference.getFileRef());
+                                    byte[] actualHash;
+                                    try (InputStream input = Files.newInputStream(referencedFile)) {
+                                        digest.reset();
+                                        byte[] buffer = new byte[8192];
+                                        int read;
+                                        while ((read = input.read(buffer)) != -1) {
+                                            digest.update(buffer, 0, read);
+                                        }
+                                        actualHash = digest.digest();
+                                    }
+                                    if (!MessageDigest.isEqual(reference.getCheckValue(), actualHash)) {
+                                        contentIntegrityValid = false;
+                                        break;
+                                    }
+                                }
+
+                                Path signedValuePath = resourceLocator.getFile(
+                                        signatureDescription.getSignedValue());
+                                byte[] signedValue = Files.readAllBytes(signedValuePath);
+                                signatureValidator.validate(
+                                        signatureRecord.getType(),
+                                        signedInfo.getSignatureMethod(),
+                                        Files.readAllBytes(signatureFile),
+                                        signedValue);
+                                cryptographicSignatureValid = true;
+
+                                var seal = signedInfo.getSeal();
+                                if (seal != null) {
+                                    Path sealPath = resourceLocator.getFile(seal.getBaseLoc());
+                                    SES_Signature sesSignature = SES_Signature.getInstance(signedValue);
+                                    SESeal embeddedSeal = sesSignature.getToSign().getEseal();
+                                    sealMatched = MessageDigest.isEqual(
+                                            embeddedSeal.getEncoded("DER"),
+                                            Files.readAllBytes(sealPath));
+                                    if (!sealMatched) {
+                                        throw new GeneralSecurityException(
+                                                "OFD中的Seal.esl与电子签章数据中的印章不匹配");
+                                    }
+                                    byte[] certificateBytes = sesSignature.getCert().getOctets();
+                                    var certificate = (java.security.cert.X509Certificate)
+                                            CertificateFactory.getInstance(
+                                                    "X.509", new BouncyCastleProvider())
+                                                    .generateCertificate(new java.io.ByteArrayInputStream(
+                                                            certificateBytes));
+                                    signatureResult.put("signerName",
+                                            certificate.getSubjectX500Principal().getName());
+                                }
+                            } finally {
+                                resourceLocator.restore();
+                            }
+                        } finally {
+                            resourceLocator.restore();
+                        }
+                        signatureResult.put("contentIntegrityValid", contentIntegrityValid);
+                        signatureResult.put("cryptographicSignatureValid",
+                                cryptographicSignatureValid);
+                        signatureResult.put("sealMatched", sealMatched);
+                        signatureResult.put("valid", contentIntegrityValid
+                                && cryptographicSignatureValid
+                                && Boolean.TRUE.equals(signatureResult.get("structureValid"))
+                                && (sealMatched == null || sealMatched)
+                                && (signatureRecord.getType() == null
+                                    || signatureRecord.getType()
+                                        == org.ofdrw.core.signatures.SigType.Seal));
+                        if (!contentIntegrityValid) {
+                            signatureResult.put("message", "签章引用的文档内容摘要校验失败");
+                        }
+                    } catch (Exception e) {
+                        signatureResult.put("contentIntegrityValid", contentIntegrityValid);
+                        signatureResult.put("cryptographicSignatureValid",
+                                cryptographicSignatureValid);
+                        signatureResult.put("sealMatched", sealMatched);
+                        signatureResult.putIfAbsent("structureValid", false);
+                        signatureResult.put("valid", false);
+                        signatureResult.put("message", e.getMessage());
+                    }
+
+                    boolean signatureValid = Boolean.TRUE.equals(signatureResult.get("valid"));
+                    signatures.add(signatureResult);
+                    allValid &= signatureValid;
+                }
+
+                verification.put("valid", allValid);
+                verification.put("signatureCount", signatures.size());
+                verification.put("signatures", signatures);
+                verification.put("trustChecked", false);
+                verification.put("message", signatureRecords.isEmpty()
+                        ? "OFD文件中没有电子签章"
+                        : allValid ? "所有OFD电子签章均通过校验" : "存在无效电子签章");
+            }
+            return ofdSignatureResult(result, DataResponse.CODE_SUCCESS, "验签完成", verification);
+        } catch (Exception e) {
+            log.error("OFD电子签章验签失败", e);
+            return ofdSignatureResult(result, DataResponse.CODE_EXCEPTION, "验签处理失败: " + e.getMessage(), null);
+        } finally {
+            if (workingDirectory != null) {
+                try {
+                    FileUtils.deleteDirectory(workingDirectory.toFile());
+                } catch (IOException e) {
+                    log.warn("清理OFD验签临时目录失败: {}", workingDirectory, e);
+                }
+            }
+        }
+    }
+
+    @PostMapping(value = "verifyPDFSignature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public Map<String, Object> verifyPDFSignature(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        if (file == null || file.isEmpty()) {
+            return ofdSignatureResult(result, DataResponse.CODE_NULL_PARAM, "待验签PDF文件不能为空", null);
+        }
+
+        try {
+            byte[] pdfBytes = file.getBytes();
+            if (!hasPdfSignature(pdfBytes)) {
+                return ofdSignatureResult(result, DataResponse.CODE_ERROR_PARAM,
+                        "上传文件不是有效的PDF文件", null);
+            }
+
+            try (PDDocument document = PDDocument.load(pdfBytes)) {
+                PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
+                List<PDSignatureField> signatureFields = new ArrayList<>();
+                if (acroForm != null) {
+                    for (PDField field : acroForm.getFieldTree()) {
+                        if (field instanceof PDSignatureField signatureField
+                                && signatureField.getSignature() != null) {
+                            signatureFields.add(signatureField);
+                        }
+                    }
+                }
+                List<Map<String, Object>> signatures = new ArrayList<>();
+                boolean allValid = !signatureFields.isEmpty();
+                BouncyCastleProvider provider = new BouncyCastleProvider();
+
+                for (PDSignatureField signatureField : signatureFields) {
+                    String signatureName = signatureField.getFullyQualifiedName();
+                    Map<String, Object> signatureResult = new HashMap<>();
+                    signatureResult.put("name", signatureName);
+                    PDSignature pdfSignature = signatureField.getSignature();
+                    int[] byteRange = pdfSignature.getByteRange();
+                    boolean coversWholeDocument = byteRange != null
+                            && byteRange.length == 4
+                            && byteRange[0] == 0
+                            && byteRange[1] >= 0
+                            && byteRange[2] >= byteRange[1]
+                            && byteRange[3] >= 0
+                            && (long) byteRange[2] + byteRange[3] == pdfBytes.length;
+                    signatureResult.put("coversWholeDocument", coversWholeDocument);
+                    try {
+                        byte[] signedContent = pdfSignature.getSignedContent(pdfBytes);
+                        byte[] signatureContent = pdfSignature.getContents(pdfBytes);
+                        CMSSignedData cmsSignedData = new CMSSignedData(
+                                new CMSProcessableByteArray(signedContent), signatureContent);
+                        Store<X509CertificateHolder> certificateStore = cmsSignedData.getCertificates();
+                        Collection<SignerInformation> signerInfos = cmsSignedData.getSignerInfos().getSigners();
+                        boolean valid = !signerInfos.isEmpty();
+                        String signerName = pdfSignature.getName();
+
+                        for (SignerInformation signerInfo : signerInfos) {
+                            Collection<X509CertificateHolder> matchingCertificates =
+                                    certificateStore.getMatches(signerInfo.getSID());
+                            boolean signerValid = false;
+                            for (X509CertificateHolder certificateHolder : matchingCertificates) {
+                                var certificate = new JcaX509CertificateConverter()
+                                        .setProvider(provider)
+                                        .getCertificate(certificateHolder);
+                                if (signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder()
+                                        .setProvider(provider)
+                                        .build(certificate))) {
+                                    signerValid = true;
+                                    signerName = certificate.getSubjectX500Principal().getName();
+                                    break;
+                                }
+                            }
+                            valid &= signerValid;
+                        }
+
+                        signatureResult.put("valid", valid);
+                        signatureResult.put("signerName", signerName);
+                        if (pdfSignature.getSignDate() != null) {
+                            signatureResult.put("signingTime", pdfSignature.getSignDate().getTime());
+                        }
+                        if (!valid) {
+                            signatureResult.put("message", "签名摘要或签名值校验失败");
+                            allValid = false;
+                        } else if (!coversWholeDocument) {
+                            signatureResult.put("message", "签名有效,但未覆盖整个PDF文档,签名后可能有内容变更");
+                            allValid = false;
+                        }
+                    } catch (Exception e) {
+                        signatureResult.put("valid", false);
+                        signatureResult.put("message", e.getMessage());
+                        allValid = false;
+                    }
+                    signatures.add(signatureResult);
+                }
+
+                Map<String, Object> verification = new HashMap<>();
+                verification.put("valid", allValid);
+                verification.put("signatureCount", signatures.size());
+                verification.put("signatures", signatures);
+                verification.put("message", signatureFields.isEmpty()
+                        ? "PDF文件中没有电子签名"
+                        : allValid
+                            ? "所有PDF签名均通过完整性校验"
+                            : "存在无效签名或签名后文档内容发生变化");
+                verification.put("trustChecked", false);
+                return ofdSignatureResult(result, DataResponse.CODE_SUCCESS, "验签完成", verification);
+            }
+        } catch (Exception e) {
+            log.error("PDF电子签章验签失败", e);
+            return ofdSignatureResult(result, DataResponse.CODE_EXCEPTION, "验签处理失败: " + e.getMessage(), null);
+        }
+    }
+
+    private boolean hasPdfSignature(byte[] content) {
+        byte[] marker = "%PDF-".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+        int searchLength = Math.min(content.length, 1024);
+        for (int i = 0; i <= searchLength - marker.length; i++) {
+            boolean matches = true;
+            for (int j = 0; j < marker.length; j++) {
+                if (content[i + j] != marker[j]) {
+                    matches = false;
+                    break;
+                }
+            }
+            if (matches) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Map<String, Object> ofdSignatureResult(Map<String, Object> result, int code, String message, Object data) {
+        result.put("code", code);
+        result.put("message", message);
+        result.put("data", data);
+        return result;
+    }
+
+    private void downloadMinioObject(String objectName, Path target) throws Exception {
+        try (InputStream inputStream = minioClientService.getObject(objectName)) {
+            Files.copy(inputStream, target);
+        }
+    }
+
+    private boolean hasZipSignature(Path file) throws IOException {
+        try (InputStream inputStream = Files.newInputStream(file)) {
+            byte[] header = inputStream.readNBytes(4096);
+            for (int i = 0; i <= header.length - 4; i++) {
+                if (header[i] == 'P' && header[i + 1] == 'K'
+                        && ((header[i + 2] == 3 && header[i + 3] == 4)
+                        || (header[i + 2] == 5 && header[i + 3] == 6)
+                        || (header[i + 2] == 7 && header[i + 3] == 8))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}

+ 18 - 0
src/main/java/com/ymwk/icvp/signature/entity/Picture.java

@@ -0,0 +1,18 @@
+package com.ymwk.icvp.signature.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.util.Date;
+
+@Data
+@TableName("t_picture")
+public class Picture {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    private String name;
+    private String url;
+    private Date createTime;
+}

+ 102 - 0
src/main/java/com/ymwk/icvp/signature/entity/Seal.java

@@ -0,0 +1,102 @@
+package com.ymwk.icvp.signature.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serial;
+import com.baomidou.mybatisplus.annotation.*;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 电子印章实体
+ */
+@Data
+@TableName("t_seal")
+public class Seal implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 印章别名/名称,唯一标识 */
+    @TableField("seal_alias")
+    private String sealAlias;
+
+    /** 关联印模ID */
+    @TableField("impression_id")
+    private Long impressionId;
+
+    /** 印模别名(冗余,便于展示) */
+    @TableField("impression_alias")
+    private String impressionAlias;
+
+    /** 绑定的数字证书ID */
+    @TableField("cert_id")
+    private Long certId;
+
+    /** 证书别名(冗余,便于展示) */
+    @TableField("cert_alias")
+    private String certAlias;
+
+    /** 签名算法:SM2 / RSA */
+    @TableField("algorithm")
+    private String algorithm;
+
+    /** 印章载体格式:ESL(SM2) / CMS(RSA) */
+    @TableField("seal_format")
+    private String sealFormat;
+
+    /** 印章状态:1-正常 2-冻结 3-销毁 4-过期 */
+    @TableField("seal_status")
+    private Integer sealStatus;
+
+    /** 有效期开始时间 */
+    @TableField("valid_start_time")
+    private LocalDateTime validStartTime;
+
+    /** 有效期结束时间 */
+    @TableField("valid_end_time")
+    private LocalDateTime validEndTime;
+
+    /** 印章校验值 */
+    @TableField("seal_check_value")
+    private String sealCheckValue;
+
+    /** 印章文件存储路径 */
+    @TableField("seal_file_path")
+    private String sealFilePath;
+
+    /** 印章结构体数据(SM2 为 .esl 二进制,RSA 可为空) */
+    @TableField("seal_file_data")
+    private byte[] sealFileData;
+
+    /** 创建时间 */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /** 创建人 */
+    @TableField("create_by")
+    private String createBy;
+
+    /** 更新人 */
+    @TableField("update_by")
+    private String updateBy;
+
+    /** 备注 */
+    @TableField("remark")
+    private String remark;
+
+    /** 逻辑删除:0-未删除 1-已删除 */
+    @TableLogic
+    @TableField("del_flag")
+    private Integer delFlag;
+}

+ 9 - 0
src/main/java/com/ymwk/icvp/signature/mapper/PictureMapper.java

@@ -0,0 +1,9 @@
+package com.ymwk.icvp.signature.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.icvp.signature.entity.Picture;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PictureMapper extends BaseMapper<Picture> {
+}

+ 10 - 0
src/main/java/com/ymwk/icvp/signature/mapper/SealMapper.java

@@ -0,0 +1,10 @@
+package com.ymwk.icvp.signature.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.icvp.signature.entity.Seal;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SealMapper extends BaseMapper<Seal> {
+}

+ 38 - 0
src/main/java/com/ymwk/icvp/signature/model/PictureGenerateRequest.java

@@ -0,0 +1,38 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.Data;
+
+@Data
+public class PictureGenerateRequest {
+
+    /**
+     * 印模类型:ROUND / ELLIPSE / SQUARE
+     */
+    private SealType type;
+
+    /**
+     * 圆章 / 椭圆章
+     */
+    private Integer width;
+    private Integer height;
+
+    /**
+     * 方章
+     */
+    private Integer sideLength;
+
+    /**
+     * 印模文字
+     */
+    private String message;
+
+    /**
+     * 中心文字(圆章 / 椭圆章)
+     */
+    private String centerName;
+
+    /**
+     * 印模别名
+     */
+    private String name;
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/signature/model/PictureListRequest.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.signature.model;
+
+import com.ymwk.icvp.component.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class PictureListRequest extends PageParam {
+    private String name;
+
+}

+ 27 - 0
src/main/java/com/ymwk/icvp/signature/model/SealAlgorithmEnum.java

@@ -0,0 +1,27 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 签名算法
+ */
+@Getter
+@AllArgsConstructor
+public enum SealAlgorithmEnum {
+
+    SM2("SM2", "SM2"),
+    RSA("RSA", "RSA");
+
+    private final String code;
+    private final String desc;
+
+    public static SealAlgorithmEnum of(String code) {
+        for (SealAlgorithmEnum e : values()) {
+            if (e.code.equalsIgnoreCase(code)) {
+                return e;
+            }
+        }
+        return null;
+    }
+}

+ 18 - 0
src/main/java/com/ymwk/icvp/signature/model/SealBindCertDTO.java

@@ -0,0 +1,18 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 印章换绑证书请求
+ */
+@Data
+public class SealBindCertDTO {
+
+    @NotNull(message = "印章ID不能为空")
+    private Long sealId;
+
+    @NotNull(message = "证书ID不能为空")
+    private Long certId;
+}

+ 11 - 0
src/main/java/com/ymwk/icvp/signature/model/SealBuildResult.java

@@ -0,0 +1,11 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.Data;
+
+@Data
+public class SealBuildResult {
+    /** .esl 二进制数据 */
+    private byte[] fileData;
+    /** Minio 下载地址 */
+    private String filePath;
+}

+ 38 - 0
src/main/java/com/ymwk/icvp/signature/model/SealCreateDTO.java

@@ -0,0 +1,38 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.Data;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * 生成印章请求
+ */
+@Data
+public class SealCreateDTO {
+
+    /** 印章别名 */
+    @NotBlank(message = "印章别名不能为空")
+    private String sealAlias;
+
+    /** 印模ID */
+    @NotNull(message = "请选择印模")
+    private Long impressionId;
+
+    /** 数字证书ID */
+    @NotNull(message = "请选择数字证书")
+    private Long certId;
+
+    /** 算法:SM2 / RSA */
+    @NotBlank(message = "请选择印章算法")
+    private String algorithm;
+
+    /** 有效期开始时间 */
+    private LocalDateTime validStartTime;
+
+    /** 有效期结束时间 */
+    private LocalDateTime validEndTime;
+
+    /** 备注 */
+    private String remark;
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/signature/model/SealDownload.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class SealDownload {
+    private byte[] data;
+    private String fileName;
+    private String contentType;
+}

+ 19 - 0
src/main/java/com/ymwk/icvp/signature/model/SealExtendDTO.java

@@ -0,0 +1,19 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * 印章延期请求
+ */
+@Data
+public class SealExtendDTO {
+
+    @NotNull(message = "印章ID不能为空")
+    private Long sealId;
+
+    @NotNull(message = "延期时间不能为空")
+    private LocalDateTime newValidEndTime;
+}

+ 27 - 0
src/main/java/com/ymwk/icvp/signature/model/SealFormatEnum.java

@@ -0,0 +1,27 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 印章载体格式
+ */
+@Getter
+@AllArgsConstructor
+public enum SealFormatEnum {
+
+    ESL("ESL", "国密电子印章文件"),
+    CMS("CMS", "PKCS#7/CMS 签名数据");
+
+    private final String code;
+    private final String desc;
+
+    public static SealFormatEnum of(String code) {
+        for (SealFormatEnum e : values()) {
+            if (e.code.equalsIgnoreCase(code)) {
+                return e;
+            }
+        }
+        return null;
+    }
+}

+ 32 - 0
src/main/java/com/ymwk/icvp/signature/model/SealQueryDTO.java

@@ -0,0 +1,32 @@
+package com.ymwk.icvp.signature.model;
+
+
+import com.ymwk.icvp.component.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 印章查询条件
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class SealQueryDTO extends PageParam {
+
+    /** 印章别名(模糊) */
+    private String sealAlias;
+
+    /** 印章状态 */
+    private Integer sealStatus;
+
+    /** 算法 */
+    private String algorithm;
+
+    /** 创建时间起 */
+    private LocalDateTime createTimeStart;
+
+    /** 创建时间止 */
+    private LocalDateTime createTimeEnd;
+
+}

+ 29 - 0
src/main/java/com/ymwk/icvp/signature/model/SealStatusEnum.java

@@ -0,0 +1,29 @@
+package com.ymwk.icvp.signature.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 印章状态
+ */
+@Getter
+@AllArgsConstructor
+public enum SealStatusEnum {
+
+    NORMAL(1, "正常"),
+    FROZEN(2, "冻结"),
+    DESTROYED(3, "销毁"),
+    EXPIRED(4, "过期");
+
+    private final Integer code;
+    private final String desc;
+
+    public static SealStatusEnum of(Integer code) {
+        for (SealStatusEnum e : values()) {
+            if (e.code.equals(code)) {
+                return e;
+            }
+        }
+        return null;
+    }
+}

+ 7 - 0
src/main/java/com/ymwk/icvp/signature/model/SealType.java

@@ -0,0 +1,7 @@
+package com.ymwk.icvp.signature.model;
+
+public enum SealType {
+    ROUND,     // 圆章
+    ELLIPSE,   // 椭圆章
+    SQUARE     // 方章
+}

+ 14 - 0
src/main/java/com/ymwk/icvp/signature/service/PictureService.java

@@ -0,0 +1,14 @@
+package com.ymwk.icvp.signature.service;
+
+import com.ymwk.icvp.component.PageParam;
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.model.PictureListRequest;
+
+
+public interface PictureService {
+    ResponseDTO<PageResult<Picture>> getPictureList(PictureListRequest pictureListRequest);
+
+    ResponseDTO<String> deletePicture(Integer id);
+}

+ 16 - 0
src/main/java/com/ymwk/icvp/signature/service/SealBuilder.java

@@ -0,0 +1,16 @@
+package com.ymwk.icvp.signature.service;
+
+
+
+import com.ymwk.icvp.signature.model.SealBuildResult;
+import com.ymwk.icvp.signature.model.SealCreateDTO;
+import com.ymwk.icvp.signature.entity.Seal;
+
+public interface SealBuilder {
+
+    /** 生成 SM2 .esl */
+    SealBuildResult buildEsl(Seal seal, SealCreateDTO dto);
+
+    /** 重新生成 .esl(延期、换绑证书时用) */
+    SealBuildResult rebuildEsl(Seal seal);
+}

+ 32 - 0
src/main/java/com/ymwk/icvp/signature/service/SealService.java

@@ -0,0 +1,32 @@
+package com.ymwk.icvp.signature.service;
+
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.model.SealBindCertDTO;
+import com.ymwk.icvp.signature.model.SealCreateDTO;
+import com.ymwk.icvp.signature.model.SealDownload;
+import com.ymwk.icvp.signature.model.SealExtendDTO;
+import com.ymwk.icvp.signature.model.SealQueryDTO;
+
+public interface SealService {
+    ResponseDTO<PageResult<Seal>> pageSeal(SealQueryDTO queryDTO);
+
+    ResponseDTO<Seal> getSealDetail(Long id);
+
+    ResponseDTO<Long> createSeal(SealCreateDTO createDTO);
+
+    ResponseDTO<Void> freeze(Long id);
+
+    ResponseDTO<Void> unfreeze(Long id);
+
+    ResponseDTO<Void> destroy(Long id);
+
+    ResponseDTO<Void> extend(SealExtendDTO extendDTO);
+
+    ResponseDTO<Void> bindCert(SealBindCertDTO bindCertDTO);
+
+    ResponseDTO<Boolean> verify(Long id);
+
+    ResponseDTO<SealDownload> download(Long id);
+}

+ 12 - 0
src/main/java/com/ymwk/icvp/signature/service/SealVerifyService.java

@@ -0,0 +1,12 @@
+package com.ymwk.icvp.signature.service;
+
+import com.ymwk.icvp.signature.entity.Seal;
+
+public interface SealVerifyService {
+
+    /** 计算印章校验值 */
+    String calcCheckValue(Seal seal);
+
+    /** 验证印章有效性 */
+    boolean verify(Seal seal);
+}

+ 50 - 0
src/main/java/com/ymwk/icvp/signature/service/impl/PictureServiceImpl.java

@@ -0,0 +1,50 @@
+package com.ymwk.icvp.signature.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.component.SmartPageUtil;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.mapper.PictureMapper;
+import com.ymwk.icvp.signature.model.PictureListRequest;
+import com.ymwk.icvp.signature.service.PictureService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class PictureServiceImpl implements PictureService {
+    @Resource
+    private PictureMapper pictureMapper;
+
+    @Override
+    public ResponseDTO<PageResult<Picture>> getPictureList(PictureListRequest pictureListRequest) {
+
+        LambdaQueryWrapper<Picture> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(pictureListRequest.getName() != null, Picture::getName, pictureListRequest.getName());
+
+
+        Page<Picture> page = new Page<>(pictureListRequest.getPageNum(),pictureListRequest.getPageSize());
+        List<Picture> examRecordList = pictureMapper.selectList(page,wrapper);
+
+        PageResult<Picture> pageResult = SmartPageUtil.convert2PageResult(page, examRecordList);
+
+
+        return ResponseDTO.ok(pageResult);
+    }
+
+    @Override
+    public ResponseDTO<String> deletePicture(Integer id) {
+        Optional<Picture> pictureOptional = Optional.ofNullable(pictureMapper.selectById(id));
+        if (pictureOptional.isPresent()) {
+            pictureMapper.deleteById(id);
+            return ResponseDTO.ok("删除成功");
+        } else {
+            return ResponseDTO.error(100001,"图片不存在");
+        }
+    }
+
+}

+ 172 - 0
src/main/java/com/ymwk/icvp/signature/service/impl/SealBuilderImpl.java

@@ -0,0 +1,172 @@
+package com.ymwk.icvp.signature.service.impl;
+
+
+import cn.hutool.core.date.DateUtil;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.mapper.CertificateMapper;
+import com.ymwk.icvp.pki.service.impl.CertificateAssembleService;
+import com.ymwk.icvp.utils.MinioClientService;
+import org.ofdrw.gm.cert.PKCS12Tools;
+import org.ofdrw.gm.ses.v1.SES_ESPictrueInfo;
+import org.ofdrw.gm.ses.v1.SES_Header;
+import org.ofdrw.gm.ses.v4.SES_CertList;
+import org.ofdrw.gm.ses.v4.SES_ESPropertyInfo;
+import org.ofdrw.gm.ses.v4.SES_SealInfo;
+import org.ofdrw.gm.ses.v4.SESeal;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.mapper.PictureMapper;
+import com.ymwk.icvp.signature.model.SealCreateDTO;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.service.SealBuilder;
+import com.ymwk.icvp.signature.model.SealBuildResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.PrivateKey;
+import java.security.Signature;
+
+import java.security.cert.Certificate;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SealBuilderImpl implements SealBuilder {
+
+
+    private final CertificateMapper certificateMapper;
+    private final PictureMapper pictureMapper;
+    private final MinioClientService minioClientService;
+    private final CertificateAssembleService certificateAssembleService;
+
+    @Value("${seal.sealer-p12-path}")
+    private String sealerP12Path;
+
+    @Value("${seal.sealer-p12-password}")
+    private String sealerP12Password;
+
+    @Override
+    public SealBuildResult buildEsl(Seal seal, SealCreateDTO dto) {
+        try {
+            // 1. 制章人证书 + 私钥(固定路径固定密码)
+            Path sealerPath = Paths.get(sealerP12Path);
+            Certificate sealerCert = PKCS12Tools.ReadUserCert(sealerPath, "Sig", sealerP12Password);
+            PrivateKey sealerPrvKey = PKCS12Tools.ReadPrvKey(sealerPath, "Sig", sealerP12Password);
+
+            // 2. 签章人证书
+            Certificate userCert = loadUserCert(seal.getCertId());
+
+            // 3. 印模图片
+            byte[] pictureBytes = loadPictureBytes(seal.getImpressionId());
+
+            // 4. 组装 SES_SealInfo
+            ASN1EncodableVector v = new ASN1EncodableVector();
+            v.add(new DEROctetString(userCert.getEncoded()));
+            SES_Header header = new SES_Header(SES_Header.V4, new DERIA5String(seal.getSealAlias()));
+            SES_ESPropertyInfo propertyInfo = new SES_ESPropertyInfo()
+                    .setType(new ASN1Integer(3))
+                    .setName(new DERUTF8String(seal.getSealAlias()))
+                    .setCertListType(SES_ESPropertyInfo.CertListType)
+                    .setCertList(SES_CertList.getInstance(SES_ESPropertyInfo.CertListType, new DERSequence(v)))
+                    .setCreateDate(new ASN1GeneralizedTime(new Date()))
+                    .setValidStart(new ASN1GeneralizedTime(toDate(seal.getValidStartTime())))
+                    .setValidEnd(new ASN1GeneralizedTime(toDate(seal.getValidEndTime())));
+            SES_ESPictrueInfo pictrueInfo = new SES_ESPictrueInfo()
+                    .setType("PNG")
+                    .setData(pictureBytes)
+                    .setWidth(40)
+                    .setHeight(40);
+            SES_SealInfo sesSealInfo = new SES_SealInfo()
+                    .setHeader(header)
+                    .setEsID(new DERIA5String(UUID.randomUUID().toString().replace("-", "")))
+                    .setProperty(propertyInfo)
+                    .setPicture(pictrueInfo);
+
+            // 5. 制章人私钥签名
+            Signature sg = Signature.getInstance("SM3WithSM2", new BouncyCastleProvider());
+            sg.initSign(sealerPrvKey);
+            sg.update(sesSealInfo.getEncoded("DER"));
+            byte[] sigVal = sg.sign();
+
+            // 6. 组装 SESeal
+            SESeal esl = new SESeal()
+                    .seteSealInfo(sesSealInfo)
+                    .setCert(sealerCert)
+                    .setSignAlgID(GMObjectIdentifiers.sm2sign_with_sm3)
+                    .setSignedValue(sigVal);
+            byte[] eslBytes = esl.getEncoded("DER");
+
+            // 7. 上传 Minio
+            String objectName = UUID.randomUUID().toString().replace("-", "") + ".esl";
+            minioClientService.uploadBytes(objectName, eslBytes, "application/octet-stream");
+            String url = minioClientService.getPublicUrl(objectName);
+
+            SealBuildResult result = new SealBuildResult();
+            result.setFileData(eslBytes);
+            result.setFilePath(url);
+            return result;
+
+        } catch (Exception e) {
+            log.error("构建 .esl 失败", e);
+            throw new RuntimeException("构建 .esl 失败:" + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public SealBuildResult rebuildEsl(Seal seal) {
+        SealCreateDTO dto = new SealCreateDTO();
+        dto.setSealAlias(seal.getSealAlias());
+        dto.setImpressionId(seal.getImpressionId());
+        dto.setCertId(seal.getCertId());
+        dto.setAlgorithm(seal.getAlgorithm());
+        return buildEsl(seal, dto);
+    }
+
+    // ---------------- 私有辅助 ----------------
+
+    private Certificate loadUserCert(Long certId) {
+        CertificateEntity cert = certificateMapper.selectById(certId);
+        if (cert == null) {
+            throw new RuntimeException("证书不存在:" + certId);
+        }
+        try {
+            return certificateAssembleService.getCertificate(cert.getCertSerialNumber());
+        } catch (Exception e) {
+            throw new RuntimeException("读取签章人证书失败:" + e.getMessage(), e);
+        }
+    }
+
+    private byte[] loadPictureBytes(Long pictureId) {
+        Picture picture = pictureMapper.selectById(pictureId);
+        if (picture == null) {
+            throw new RuntimeException("印模不存在:" + pictureId);
+        }
+        try (InputStream in = minioClientService.downloadFile(extractObjectName(picture.getUrl()))) {
+            return in.readAllBytes();
+        } catch (Exception e) {
+            throw new RuntimeException("下载印模图片失败:" + e.getMessage(), e);
+        }
+    }
+
+    private String extractObjectName(String url) {
+        int idx = url.lastIndexOf('/');
+        return idx >= 0 ? url.substring(idx + 1) : url;
+    }
+
+    private Date toDate(java.time.LocalDateTime time) {
+        return time == null ? new Date()
+                : Date.from(time.atZone(java.time.ZoneId.systemDefault()).toInstant());
+    }
+}

+ 35 - 0
src/main/java/com/ymwk/icvp/signature/service/impl/SealGenerateService.java

@@ -0,0 +1,35 @@
+package com.ymwk.icvp.signature.service.impl;
+
+import com.ymwk.icvp.signature.utils.GraphicsUtil;
+import com.ymwk.icvp.signature.model.PictureGenerateRequest;
+import org.springframework.stereotype.Service;
+
+import java.awt.image.BufferedImage;
+
+@Service
+public class SealGenerateService {
+
+    public BufferedImage generate(PictureGenerateRequest req) {
+
+        return switch (req.getType()) {
+
+            //圆章
+            case ROUND -> GraphicsUtil.startChapterGraphics2D(
+                    req.getWidth(),
+                    req.getHeight(),
+                    req.getMessage(),
+                    req.getCenterName()
+            );
+            //椭圆章
+            case ELLIPSE -> GraphicsUtil.startEllipseGraphics2D(
+                    req.getMessage(),
+                    req.getCenterName()
+            );
+            //方章
+            case SQUARE -> GraphicsUtil.startSquareGraphics2D(
+                    req.getSideLength(),
+                    req.getMessage()
+            );
+        };
+    }
+}

+ 394 - 0
src/main/java/com/ymwk/icvp/signature/service/impl/SealServiceImpl.java

@@ -0,0 +1,394 @@
+package com.ymwk.icvp.signature.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ymwk.icvp.component.PageResult;
+import com.ymwk.icvp.component.ResponseDTO;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.mapper.CertificateMapper;
+import com.ymwk.icvp.signature.entity.Picture;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.mapper.PictureMapper;
+import com.ymwk.icvp.signature.mapper.SealMapper;
+import com.ymwk.icvp.signature.model.*;
+import com.ymwk.icvp.signature.service.SealBuilder;
+import com.ymwk.icvp.signature.service.SealService;
+import com.ymwk.icvp.signature.service.SealVerifyService;
+import com.ymwk.icvp.utils.MinioClientService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.LocalDateTime;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+@Service
+@RequiredArgsConstructor
+public class SealServiceImpl implements SealService {
+
+
+    private final SealMapper sealMapper;
+    private final SealBuilder sealBuilder;
+    private final SealVerifyService sealVerifyService;
+    private final PictureMapper pictureMapper;
+    private final CertificateMapper certificateMapper;
+    private final MinioClientService minioClientService;
+
+
+    @Override
+    public ResponseDTO<PageResult<Seal>> pageSeal(SealQueryDTO queryDTO) {
+        LambdaQueryWrapper<Seal> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getSealAlias()),
+                        Seal::getSealAlias, queryDTO.getSealAlias())
+                .eq(queryDTO.getSealStatus() != null,
+                        Seal::getSealStatus, queryDTO.getSealStatus())
+                .eq(StringUtils.hasText(queryDTO.getAlgorithm()),
+                        Seal::getAlgorithm, queryDTO.getAlgorithm())
+                .ge(queryDTO.getCreateTimeStart() != null,
+                        Seal::getCreateTime, queryDTO.getCreateTimeStart())
+                .le(queryDTO.getCreateTimeEnd() != null,
+                        Seal::getCreateTime, queryDTO.getCreateTimeEnd())
+                .orderByDesc(Seal::getCreateTime);
+
+        Page<Seal> page = sealMapper.selectPage(
+                new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()), wrapper);
+
+        PageResult<Seal> result = new PageResult<>();
+        result.setTotal(page.getTotal());
+        result.setList(page.getRecords());
+        return ResponseDTO.ok(result);
+    }
+
+    @Override
+    public ResponseDTO<Seal> getSealDetail(Long id) {
+        Seal seal = sealMapper.selectById(id);
+   /*     if (seal == null) {
+            return ResponseDTO.error(100001,"印章不存在");
+        }*/
+        return ResponseDTO.ok(seal);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Long> createSeal(SealCreateDTO createDTO) {
+        // 1. 校验印章别名唯一
+        Long count = sealMapper.selectCount(new LambdaQueryWrapper<Seal>()
+                .eq(Seal::getSealAlias, createDTO.getSealAlias()));
+        if (count > 0) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "印章别名已存在:" + createDTO.getSealAlias());
+        }
+        // 2. 校验算法
+        SealAlgorithmEnum algorithm = SealAlgorithmEnum.of(createDTO.getAlgorithm());
+        if (algorithm == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "不支持的印章算法:" + createDTO.getAlgorithm());
+        }
+        // 3. 查询印模和证书
+        Picture picture = pictureMapper.selectById(createDTO.getImpressionId());
+        if (picture == null) {
+            return ResponseDTO.error(
+                    ResponseDTO.ERROR_CODE,
+                    "印模不存在:" + createDTO.getImpressionId()
+            );
+        }
+
+        CertificateEntity certificate = certificateMapper.selectById(createDTO.getCertId());
+        if (certificate == null) {
+            return ResponseDTO.error(
+                    ResponseDTO.ERROR_CODE,
+                    "证书不存在:" + createDTO.getCertId()
+            );
+        }
+
+
+        // 4. 组装印章基础信息
+        Seal seal = new Seal();
+        seal.setSealAlias(createDTO.getSealAlias());
+        seal.setImpressionId(createDTO.getImpressionId());
+        seal.setImpressionAlias(picture.getName());
+        seal.setCertId(createDTO.getCertId());
+        seal.setCertAlias(certificate.getCertName());
+        seal.setAlgorithm(algorithm.getCode());
+        seal.setSealStatus(SealStatusEnum.NORMAL.getCode());
+        seal.setValidStartTime(createDTO.getValidStartTime() != null
+                ? createDTO.getValidStartTime()
+                : LocalDateTime.now());
+        seal.setValidEndTime(createDTO.getValidEndTime());
+        seal.setRemark(createDTO.getRemark());
+        seal.setCreateBy(getCurrentUser());
+        seal.setUpdateBy(getCurrentUser());
+
+        LocalDateTime now = LocalDateTime.now();
+        seal.setCreateTime(now);
+        seal.setUpdateTime(now);
+
+        // 4. 按算法分流构建印章结构体
+        if (SealAlgorithmEnum.SM2 == algorithm) {
+            seal.setSealFormat(SealFormatEnum.ESL.getCode());
+            SealBuildResult buildResult = sealBuilder.buildEsl(seal, createDTO);
+            seal.setSealFileData(buildResult.getFileData());
+            seal.setSealFilePath(buildResult.getFilePath());
+        } else {
+            seal.setSealFormat(SealFormatEnum.CMS.getCode());
+        }
+
+        // 5. 计算印章校验值
+        seal.setSealCheckValue(sealVerifyService.calcCheckValue(seal));
+
+        // 6. 保存
+        sealMapper.insert(seal);
+
+        return ResponseDTO.ok(seal.getId());
+    }
+
+    private String getCurrentUser() {
+        // TODO 从 SecurityContext / ThreadLocal 获取当前登录用户
+        return "system";
+    }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Void> freeze(Long id) {
+        Seal seal = sealMapper.selectById(id);
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "印章不存在");
+        }
+        // 只有正常状态才允许冻结
+        if (!SealStatusEnum.NORMAL.getCode().equals(seal.getSealStatus())) {
+            SealStatusEnum current = SealStatusEnum.of(seal.getSealStatus());
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "当前状态不允许冻结:" + (current != null ? current.getDesc() : "未知"));
+        }
+
+        Seal update = new Seal();
+        update.setId(id);
+        update.setSealStatus(SealStatusEnum.FROZEN.getCode());
+        update.setUpdateBy(getCurrentUser());
+        sealMapper.updateById(update);
+
+        return ResponseDTO.ok();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Void> unfreeze(Long id) {
+        Seal seal = sealMapper.selectById(id);
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        // 只有冻结状态才允许解冻
+        if (!SealStatusEnum.FROZEN.getCode().equals(seal.getSealStatus())) {
+            SealStatusEnum current = SealStatusEnum.of(seal.getSealStatus());
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"当前状态不允许解冻:" + (current != null ? current.getDesc() : "未知"));
+        }
+
+        Seal update = new Seal();
+        update.setId(id);
+        update.setSealStatus(SealStatusEnum.NORMAL.getCode());
+        update.setUpdateBy(getCurrentUser());
+        sealMapper.updateById(update);
+
+        return ResponseDTO.ok();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Void> destroy(Long id) {
+        Seal seal = sealMapper.selectById(id);
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        // 已销毁的不允许重复销毁
+        if (SealStatusEnum.DESTROYED.getCode().equals(seal.getSealStatus())) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章已销毁,不可重复操作");
+        }
+
+        Seal update = new Seal();
+        update.setId(id);
+        update.setSealStatus(SealStatusEnum.DESTROYED.getCode());
+        update.setUpdateBy(getCurrentUser());
+        sealMapper.updateById(update);
+
+        return ResponseDTO.ok();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Void> extend(SealExtendDTO extendDTO) {
+        Seal seal = sealMapper.selectById(extendDTO.getSealId());
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        if (!SealStatusEnum.NORMAL.getCode().equals(seal.getSealStatus())) {
+            SealStatusEnum current = SealStatusEnum.of(seal.getSealStatus());
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"当前状态不允许延期:" + (current != null ? current.getDesc() : "未知"));
+        }
+        if (seal.getValidEndTime() != null
+                && !extendDTO.getNewValidEndTime().isAfter(seal.getValidEndTime())) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"延期时间必须晚于原有效期");
+        }
+
+        Seal update = new Seal();
+        update.setId(seal.getId());
+        update.setValidEndTime(extendDTO.getNewValidEndTime());
+
+        // 用于重算校验值的对象
+        Seal checkSeal = new Seal();
+        checkSeal.setSealAlias(seal.getSealAlias());
+        checkSeal.setCertId(seal.getCertId());
+        checkSeal.setImpressionId(seal.getImpressionId());
+        checkSeal.setAlgorithm(seal.getAlgorithm());
+        checkSeal.setValidEndTime(extendDTO.getNewValidEndTime());
+
+        if (SealAlgorithmEnum.SM2.getCode().equals(seal.getAlgorithm())) {
+            // SM2:重新生成 .esl
+            seal.setValidEndTime(extendDTO.getNewValidEndTime());
+            SealBuildResult buildResult = sealBuilder.rebuildEsl(seal);
+            update.setSealFileData(buildResult.getFileData());
+            update.setSealFilePath(buildResult.getFilePath());
+            checkSeal.setSealFileData(buildResult.getFileData());
+        }
+        // RSA:无需重建,checkSeal.sealFileData 为 null,走元数据摘要
+
+        update.setSealCheckValue(sealVerifyService.calcCheckValue(checkSeal));
+        update.setUpdateBy(getCurrentUser());
+        sealMapper.updateById(update);
+
+        return ResponseDTO.ok();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseDTO<Void> bindCert(SealBindCertDTO bindCertDTO) {
+        Seal seal = sealMapper.selectById(bindCertDTO.getSealId());
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        if (!SealStatusEnum.NORMAL.getCode().equals(seal.getSealStatus())) {
+            SealStatusEnum current = SealStatusEnum.of(seal.getSealStatus());
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"当前状态不允许绑定证书:" + (current != null ? current.getDesc() : "未知"));
+        }
+
+        CertificateEntity certificate = certificateMapper.selectById(bindCertDTO.getCertId());
+        if (certificate == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "证书不存在:" + bindCertDTO.getCertId());
+        }
+
+        Long boundSealCount = sealMapper.selectCount(new LambdaQueryWrapper<Seal>()
+                .eq(Seal::getCertId, bindCertDTO.getCertId())
+                .ne(Seal::getId, seal.getId()));
+        if (boundSealCount != null && boundSealCount > 0) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "该证书已绑定其他印章");
+        }
+
+        Seal update = new Seal();
+        update.setId(seal.getId());
+        update.setCertId(bindCertDTO.getCertId());
+        update.setCertAlias(certificate.getCertName());
+
+        // 用于重算校验值的对象
+        Seal checkSeal = new Seal();
+        checkSeal.setSealAlias(seal.getSealAlias());
+        checkSeal.setCertId(bindCertDTO.getCertId());   // 新证书
+        checkSeal.setImpressionId(seal.getImpressionId());
+        checkSeal.setAlgorithm(seal.getAlgorithm());
+        checkSeal.setValidEndTime(seal.getValidEndTime());
+
+        if (SealAlgorithmEnum.SM2.getCode().equals(seal.getAlgorithm())) {
+            // SM2:证书在 .esl 结构体内,必须重新生成
+            seal.setCertId(bindCertDTO.getCertId());
+            seal.setCertAlias(certificate.getCertName());
+            SealBuildResult buildResult = sealBuilder.rebuildEsl(seal);
+            update.setSealFileData(buildResult.getFileData());
+            update.setSealFilePath(buildResult.getFilePath());
+            checkSeal.setSealFileData(buildResult.getFileData());
+        }
+        // RSA:只改 cert_id,无需重建
+
+        update.setSealCheckValue(sealVerifyService.calcCheckValue(checkSeal));
+        update.setUpdateBy(getCurrentUser());
+        sealMapper.updateById(update);
+
+        return ResponseDTO.ok();
+    }
+
+    @Override
+    public ResponseDTO<Boolean> verify(Long id) {
+        Seal seal = sealMapper.selectById(id);
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        boolean valid = sealVerifyService.verify(seal);
+        return ResponseDTO.ok(valid);
+    }
+
+    @Override
+    public ResponseDTO<SealDownload> download(Long id) {
+        Seal seal = sealMapper.selectById(id);
+        if (seal == null) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章不存在");
+        }
+        if (SealStatusEnum.DESTROYED.getCode().equals(seal.getSealStatus())) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章已销毁,不可下载");
+        }
+
+        // SM2:返回 .esl 字节
+        if (SealAlgorithmEnum.SM2.getCode().equals(seal.getAlgorithm())) {
+            byte[] data = seal.getSealFileData();
+            if (data == null || data.length == 0) {
+                return ResponseDTO.error(ResponseDTO.ERROR_CODE,"印章文件不存在");
+            }
+            return ResponseDTO.ok(new SealDownload(
+                    data, "seal.esl", "application/octet-stream"));
+        }
+
+        if (SealAlgorithmEnum.RSA.getCode().equals(seal.getAlgorithm())) {
+            return downloadRsaSeal(seal);
+        }
+
+        return ResponseDTO.error(ResponseDTO.ERROR_CODE, "不支持的印章算法:" + seal.getAlgorithm());
+    }
+
+    private ResponseDTO<SealDownload> downloadRsaSeal(Seal seal) {
+        Picture picture = pictureMapper.selectById(seal.getImpressionId());
+        if (picture == null || !StringUtils.hasText(picture.getUrl())) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "印章关联的印模文件不存在");
+        }
+
+        CertificateEntity certificate = certificateMapper.selectById(seal.getCertId());
+        if (certificate == null || !StringUtils.hasText(certificate.getCertFilePath())) {
+            return ResponseDTO.error(ResponseDTO.ERROR_CODE, "印章关联的证书文件不存在");
+        }
+
+        String pictureObjectName = extractObjectName(picture.getUrl());
+        try (InputStream pictureStream = minioClientService.downloadFile(pictureObjectName);
+             InputStream certificateStream = minioClientService.downloadFile(certificate.getCertFilePath());
+             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+             ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
+            addZipEntry(zipOutputStream, "impression.png", pictureStream.readAllBytes());
+            addZipEntry(zipOutputStream, "certificate.pem", certificateStream.readAllBytes());
+            zipOutputStream.finish();
+            return ResponseDTO.ok(new SealDownload(
+                    outputStream.toByteArray(), "seal_" + seal.getId() + ".zip", "application/zip"));
+        } catch (IOException e) {
+            throw new RuntimeException("打包RSA印章下载文件失败", e);
+        }
+    }
+
+    private String extractObjectName(String url) {
+        String objectName = url.substring(url.lastIndexOf('/') + 1);
+        int queryIndex = objectName.indexOf('?');
+        return queryIndex >= 0 ? objectName.substring(0, queryIndex) : objectName;
+    }
+
+    private void addZipEntry(ZipOutputStream zipOutputStream, String fileName, byte[] content)
+            throws java.io.IOException {
+        zipOutputStream.putNextEntry(new ZipEntry(fileName));
+        zipOutputStream.write(content);
+        zipOutputStream.closeEntry();
+    }
+}

+ 85 - 0
src/main/java/com/ymwk/icvp/signature/service/impl/SealVerifyServiceImpl.java

@@ -0,0 +1,85 @@
+package com.ymwk.icvp.signature.service.impl;
+
+
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.model.SealAlgorithmEnum;
+import com.ymwk.icvp.signature.service.SealVerifyService;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.ofdrw.gm.ses.v4.SESeal;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Arrays;
+
+@Slf4j
+@Service
+public class SealVerifyServiceImpl implements SealVerifyService {
+
+    @Override
+    public String calcCheckValue(Seal seal) {
+        try {
+            byte[] input;
+            if (seal.getSealFileData() != null && seal.getSealFileData().length > 0) {
+                // SM2:对 .esl 结构体算摘要
+                input = seal.getSealFileData();
+            } else {
+                // RSA:对证书 + 印模 + 元数据组合算摘要
+                String raw = seal.getCertId() + "|" + seal.getImpressionId()
+                        + "|" + seal.getAlgorithm() + "|" + seal.getSealAlias()
+                        + "|" + seal.getValidEndTime();
+                input = raw.getBytes(StandardCharsets.UTF_8);
+            }
+            return SmUtil.sm3(Arrays.toString(input));
+        } catch (Exception e) {
+            log.error("计算印章校验值失败", e);
+            throw new RuntimeException("计算印章校验值失败:" + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean verify(Seal seal) {
+        // 第一层:完整性校验
+        String current = calcCheckValue(seal);
+        if (current == null || !current.equals(seal.getSealCheckValue())) {
+            log.warn("印章[{}]完整性校验失败", seal.getId());
+            return false;
+        }
+        // 第二层:SM2 验签
+        if (SealAlgorithmEnum.SM2.getCode().equals(seal.getAlgorithm())
+                && seal.getSealFileData() != null) {
+            return verifyEslSignature(seal.getSealFileData());
+        }
+        return true;
+    }
+
+    /**
+     * 解析 .esl 并用其中的制章人证书验签
+     */
+    private boolean verifyEslSignature(byte[] eslBytes) {
+        try {
+            SESeal esl = SESeal.getInstance(eslBytes);
+
+            // getCert() 返回 ASN1OctetString,转成 JDK Certificate
+            ASN1OctetString certOctet = esl.getCert();
+            CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
+            Certificate sealerCert = cf.generateCertificate(
+                    new ByteArrayInputStream(certOctet.getOctets()));
+
+            Signature sg = Signature.getInstance("SM3WithSM2", new BouncyCastleProvider());
+            sg.initVerify(sealerCert.getPublicKey());
+            sg.update(esl.geteSealInfo().getEncoded("DER"));
+            return sg.verify(esl.getSignedValue().getBytes());
+        } catch (Exception e) {
+            log.error("SM2 印章验签失败", e);
+            return false;
+        }
+    }
+}

+ 17 - 0
src/main/java/com/ymwk/icvp/signature/utils/Base64Utils.java

@@ -0,0 +1,17 @@
+package com.ymwk.icvp.signature.utils;
+
+
+import java.io.IOException;
+import java.util.Base64;
+
+
+public class Base64Utils {
+
+    public static byte[] decode(String str) throws IOException{
+		return Base64.getDecoder().decode(str);
+    }
+
+    public static String encode(byte[] bytes){
+		return new String(Base64.getEncoder().encode(bytes));
+    }
+}

+ 41 - 0
src/main/java/com/ymwk/icvp/signature/utils/Constans.java

@@ -0,0 +1,41 @@
+package com.ymwk.icvp.signature.utils;
+
+import org.bouncycastle.util.encoders.Hex;
+
+public class Constans {
+
+	//文件存放地址
+	public static final String FileInputPdfAddress = "pdfInput";// 客户PDF原文件存储地址
+	public static final String FileOutputPdfAddress = "pdfOutput";// 客户PDF输出文件存储地址
+	public static final String FilePicAddress = "pic";
+
+
+	public final static String rootCaPrivateKey = "MIIEowIBAAKCAQEAnO3QCpbf4f+n1OFvk/jebMmZNqCQOcoK5EmfPDFgYfsLLfSGif6GYjH2sirbzCB/fMjo2FtQg1n5+uRz6eumPL0sgq3XR5MI6OhzU+0ouAWjkASCTNoNJ8Ma1tRUzE3KpEOxLIuDA5mX2m2h3KaVTJcT8NaUc846LRbZyka8d9mtlNUo72rMHMYbCI66a96ERYGym7+RK2O8BN0LNPWm7jQfL0SBsaBLI0Y3JBZkuCsbjXlHm5hvDI1t8a4+kD+k3J2ZR5YQrdWUSpXaxTAg2Lh59nqtlZwvNltaa1Dni3EKgSA+bo3FLInIQunR3oSYifvudJKreNQyRP44qYAZUwIDAQABAoIBAADIe7QitroWgAcU4ntinfLgs1ffR5czIKTmAdMlzCfyzEZt0wXV1CtMgN24pHpF6wXYAino6WqrueQHB0eAvCokp8KZMLdXqznE/sF0THmqn8KA9CVKXtbFA+CplH4WhKD65UdJzA6/fGvLImEndaqAWWYSHrwXqIrGupVmcqLjI3q7fsGON1o1tKwa5xg+2W325A+TuGv+ye5NXDlTrhbhbY4BWXjZE5UMgm4mVNoz+sJExcSTLn7daPIIf3KAI33AOm2aaP0zU6B5wqV//Na3MzGKP24UEE6Z8EoZ6zdVF8h3E+Vq/244nfcz5hOEPy8GL5rx66z8WMcVwt0kBYkCgYEA2pKU5I5cIY4s1ScZW8EnhTb8mhGRdIDYl/fDBI6hOJKQkbXyL/sQ/qi/FQVUTaW6kHQvkiM+oVf4Ze9C/GKr18UiktbrA53+yemSfwM52AT6eUbZaxD83ACoUd7lvSG2sW7QGrGszQbSOJ63fMZ5VFZ5oam07m5YbwIo/Nim2gsCgYEAt80AQ+eUgArg8VGXvbKK5TK1x6586KmYjHnZTIPgm336njmEIMSGLYyW71WxbvkWsDcYMZEtWXmwiD/omXR6itzSEuDmmP/DW22JzrQz2yCudmNFh1FmtZ57PWVWACpujI58j15ABF6PGouPN4DD95W28l7IujNixQ3LJDyFktkCgYAHkvLD5Mb+hq25CSzQyCW1cQ1ZzWgL8HcQ9Q3193dQvYty8SoeTYXuPSy57sBYHHkVjmEjoEMIVkMOTTc2BblMtdEX44irTGtK1eQ0sIjtmmaPk0vvcNxCR2eNxrqRTjzFc0CU3DlPtX/NZHqZQUttB74ZQwc5B+valOmUsXE+twKBgBgdBwnhmfPqeXRvde7nO52yN3MJC04i4eacHcwSCSedMYIMna8G2WukZDDmFy8t8rIPqFboawCwHF7js4peY61QJyde5iHwfBrEli3OKnd3JCxX/ckw/r6ZGiYF43CGIJbEIsdUYCWymierY5CujgcvVx8ZHFCBaVXc52fLoOKRAoGBALh2/VV5b3aNFIm7ShDZjgGz0lJHfKcg60EVKnAkqP2bn6z9D6gFjb6HK508ITRyYrPAZQKkkldPtf615OcnrKYQH/IwC+W5k6kOl9ykYLYKNui2kJsy57ut9YwGOTIWqjN7oNgF3Mi1V7z5h7lYopGchkt3Gc3TrhidQGJlayPd";
+	public final static String rootCaPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnO3QCpbf4f+n1OFvk/jebMmZNqCQOcoK5EmfPDFgYfsLLfSGif6GYjH2sirbzCB/fMjo2FtQg1n5+uRz6eumPL0sgq3XR5MI6OhzU+0ouAWjkASCTNoNJ8Ma1tRUzE3KpEOxLIuDA5mX2m2h3KaVTJcT8NaUc846LRbZyka8d9mtlNUo72rMHMYbCI66a96ERYGym7+RK2O8BN0LNPWm7jQfL0SBsaBLI0Y3JBZkuCsbjXlHm5hvDI1t8a4+kD+k3J2ZR5YQrdWUSpXaxTAg2Lh59nqtlZwvNltaa1Dni3EKgSA+bo3FLInIQunR3oSYifvudJKreNQyRP44qYAZUwIDAQAB";
+	public final static String rootCaCert = "MIIDLTCCAhWgAwIBAgIIF0MHxDCGSyswDQYJKoZIhvcNAQELBQAwJDELMAkGA1UEBhMCQ04xFTATBgNVBAMMDE1vYmlsZVJvb3RDQTAeFw0yMDA5MTIwNDExNTFaFw00MDA5MDcwNDExNTFaMCQxCzAJBgNVBAYTAkNOMRUwEwYDVQQDDAxNb2JpbGVSb290Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc7dAKlt/h/6fU4W+T+N5syZk2oJA5ygrkSZ88MWBh+wst9IaJ/oZiMfayKtvMIH98yOjYW1CDWfn65HPp66Y8vSyCrddHkwjo6HNT7Si4BaOQBIJM2g0nwxrW1FTMTcqkQ7Esi4MDmZfabaHcppVMlxPw1pRzzjotFtnKRrx32a2U1SjvaswcxhsIjrpr3oRFgbKbv5ErY7wE3Qs09abuNB8vRIGxoEsjRjckFmS4KxuNeUebmG8MjW3xrj6QP6TcnZlHlhCt1ZRKldrFMCDYuHn2eq2VnC82W1prUOeLcQqBID5ujcUsichC6dHehJiJ++50kqt41DJE/jipgBlTAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSP0cdt44cS1uI0ZigeOdyaTzjJljAfBgNVHSMEGDAWgBSP0cdt44cS1uI0ZigeOdyaTzjJljANBgkqhkiG9w0BAQsFAAOCAQEAjGlo+6TsEEToAbA6oghNc9o4Ad5Fvl9G7IjzxjWrniyNxlV/qTJo1iHtzuoFeBL72rIL9GNv5V8f5Hf4veQH3F9JC4TM+EP/OSwcfP0zA26zpG4yepzayhapIBbNg57uexeHaOXdfuw314MSMOKmd2FX03eDaL7T6J1/N078cgy1ZbIl0nMRK838ca3iMF74BD1e3PSCDT9BbOQxDRxGhsZ8czof0ARbwMPzei2MK4+jWsLRtRnGB7cjjUO2mNp1JSamW4NTSkWXuXqJNmt1ao1LIWEfEI9KTOoxPJuuKCJ3ThYkqiRdnaNqoDtKHdPMUWon3dYyuBVlPtC55Z3Usw";
+
+
+	// yumawk
+	public final static String rootYumawkKey = "MIIEowIBAAKCAQEAr1AwdkdQEhMhrUTtmKRqlf+OZ4vd5OTT/Jn2AKGl9715XoRj0JZPInfDNZ8ssEMohEFvgcQbIxI9U8Pf+JNAeqb6wJQjNQXBZg831TuiSVTjXIsPBUz9odzcCzfClGhSX8815IVdmu1uIndp8QRakyCvP9ehPJhcwGasLLOD4Fkj4qIFuSKT+g+ZaaeuNfrK4GsbKPp7GpXBUU0t0ewqY3L28oTefkJRwFPff3/HtQtY043j6ggLziwFdrAiac5fkQ/2WsJXT+GSDVHgLco/Ar1+Xi5Ps/A2MEYDfv+GFDEMU80PQyN7PhT0DfdvPHG9iV4NfDu+Sc8hdKoBLF5hRQIDAQABAoIBAEzNfZ2jmkGA3iURaGGRzhwOjC1yyfAoC3LDZFFcf+ArwssUhsHvx5VbEEsig6pokYx3OkmIRzczG2jiC/HTWNEi7AGIJHBrKBH1IsmoyW0E4mI9jYQN+aN6pW/IqtbnaQcOORb3Ek34LGKvG9t5w9Bm5j+60KlCOogTsuQWdPTIfVsFVm6a5CnWahAZmzo1esGkFr3gM0Dka9uHfg8/qN+oezIRTZMEBgi0dGblYE6rleFdBn3ldxjLpHeuk5yOOQcHcEq9uwJM0iZOtqwG1go39bmFD4qOX6YXBpWTVug6LlGoJPqMlD8rML6wVVJ6c/mXo6oGlstMk7hZF/UfS5UCgYEA8MHrqOGtsQW86D+gOD5FfvnZeUFq0kOhHR79ZTYzQsKQeSimd8BvDbXh74MW/cNhOVVdKVP8vhRqA1u4G3AFN0JI9uxNNVj+CWNCkf9zM8S+76L/C99vFYRh429P7aoAtheam+qIs5Zac2QbOHEHNANsrRLyb2BxaXIG/oQ+38sCgYEAummUjCcc+zx80HA/w+Yi8TpwNc+RSkMbJ1ucCCPn1wFgFK03PTJrlb6dgtrAcdA/JcVDn8QiNcPKifZZP+EY/XZDnHLMtlMoXWH7ALnnkegevxSuTk9x8Ot1l0umUWYxhtujA9b9fuPtrwn82R7AUXlbNmZlKjHV69MNJiRqgS8CgYBL3C6PFh0OjEpLu9CWKDjpm3xE0rH/8sNPU7ZwL7XTMU8ofgCF9yKG5Wp9d4bFCs0VraofHZGjqvcEe9/09UUPdllbRHXHe3UALECLlDwOcc3f4DIQ4aQ02XOmoCpFLq3usl1LB4lvXM6/USmqjSLB5tpVZoTxRyg1G3LMu4q8JQKBgQCvJskGrzGR9pGoH/AqFzC5zBjm1rNDPa5j2rXPCD2sTYSDqQWAIMHpn4LMEtJDnlHXIXSax2UFc4P5AVl2pgAfF2ILu+oRXi1NIOUBF6w38gwBqmLk/cyne/WvRMtvLEBY4EJZN0ZUVcFceFv1feliJE7J3Igtj0nbHSD7t7fCzQKBgBqq3abyyH50CKnb/55+3JWehirnYDZ6beVh2+ZVCYluxHHgcGWvxWbszT1MjPzipub8MyvS7iy26rwH0z9ozSNn3hItKpDVfOL6hMzhDhnsgv9lZCXmIFXxGqxrQJl8aNLvzMSkNRw5IogFyJRwaL6Uyqj+zEQUAcIFTIZFg6md";
+	public final static String rootYumawkCert = "MIID/zCCAuegAwIBAgIIOsRbrJh6TqAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkNOMQ8wDQYDVQQDDAZZVU1BV0sxDzANBgNVBAsMBllVTUFXSzESMBAGA1UEBwwJ6ZW/5rKZ5biCMRIwEAYDVQQIDAnmuZbljZfnnIExMzAxBgNVBAoMKua5luWNl+W+oeeggee9keaOp+S/oeaBr+aKgOacr+aciemZkOWFrOWPuDAeFw0yMTA3MjQwNDMzMjVaFw00MTA3MTkwNDMzMjVaMIGMMQswCQYDVQQGEwJDTjEPMA0GA1UEAwwGWVVNQVdLMQ8wDQYDVQQLDAZZVU1BV0sxEjAQBgNVBAcMCemVv+aymeW4gjESMBAGA1UECAwJ5rmW5Y2X55yBMTMwMQYDVQQKDCrmuZbljZflvqHnoIHnvZHmjqfkv6Hmga/mioDmnK/mnInpmZDlhazlj7gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvUDB2R1ASEyGtRO2YpGqV/45ni93k5NP8mfYAoaX3vXlehGPQlk8id8M1nyywQyiEQW+BxBsjEj1Tw9/4k0B6pvrAlCM1BcFmDzfVO6JJVONciw8FTP2h3NwLN8KUaFJfzzXkhV2a7W4id2nxBFqTIK8/16E8mFzAZqwss4PgWSPiogW5IpP6D5lpp641+srgaxso+nsalcFRTS3R7CpjcvbyhN5+QlHAU99/f8e1C1jTjePqCAvOLAV2sCJpzl+RD/ZawldP4ZINUeAtyj8CvX5eLk+z8DYwRgN+/4YUMQxTzQ9DI3s+FPQN9288cb2JXg18O75JzyF0qgEsXmFFAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBQRDwbZjKGF1BEyGVISxc+IGfbNBjAfBgNVHSMEGDAWgBQRDwbZjKGF1BEyGVISxc+IGfbNBjANBgkqhkiG9w0BAQsFAAOCAQEAe0YSoBlrdlAA6sNTyJPbYfaU8tdqLKHzyaWJWJxruU6jbOUEcdZk8De9ntIffPL57LKkEJ8L4/a/2wirNAgZz4KlG0R7jFDy0V1kOKY4g9kX2AjcaA+dC31YcYvYzUNGbuRD8A0cotDtajSB3hMNHPEnNghtE0ukqC5Mixat2jd9pcw8BOoIALn9SWBPqDa4hg6pyDqzGcOQUsusY7fxjns9pWjnfLMnLO7NMbN9qjqyi4qJj9mfnvN/visxYhGGq4YSK7xA4CZmgvy02aqcqeJy+ugxrqxtwYIWBDSfuWCS3HxwKJ7xauvEDV0noKM6iwPYKVoocat4MParlm+HQg";
+
+
+	public static final String KEY = "eEoGbv";//eEoGbv2ngA0f1USj
+
+
+	public static final byte[] AES_KEY = Hex.decode("255ace472f7df7a4d5ea6034742acc82");
+
+	public static final String AES_OFFSET = "ecde992f8c8be08c3215880dbb77e90b";
+
+	public static final String XJ_REQ_CERT_URL = "https://open.silksign.cn/api/app.php";
+	public static final String SM4_KEY = "31774747f3be6084dc5f0214034fa45a";
+	public static final String SM4_IV = "cb9173157a841eeb29314fc9deb45c65";
+
+
+
+
+
+
+
+
+}

+ 39 - 0
src/main/java/com/ymwk/icvp/signature/utils/DataResponse.java

@@ -0,0 +1,39 @@
+package com.ymwk.icvp.signature.utils;
+
+public class DataResponse {
+
+	// 成功
+	public static final int CODE_SUCCESS  = 100000;
+	// 失败
+	public static final int CODE_ERROR = 100001;
+	// 未知错误
+	public static final int CODE_UNKNOWN = 100002;
+	// 参数为空
+	public static final int CODE_NULL_PARAM = 100003;
+	// 参数错误
+	public static final int CODE_ERROR_PARAM = 100004;
+	// 内部错误
+	public static final int CODE_EXCEPTION = 100005;
+	// 网络异常
+	public static final int CODE_ERROR_NET = 100006;
+
+	private int code;
+	private String message;
+
+	public int getCode() {
+		return code;
+	}
+
+	public void setCode(int code) {
+		this.code = code;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public void setMessage(String message) {
+		this.message = message;
+	}
+
+}

+ 46 - 0
src/main/java/com/ymwk/icvp/signature/utils/FileUtil.java

@@ -0,0 +1,46 @@
+package com.ymwk.icvp.signature.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+
+public class FileUtil {
+
+	public static byte[] toByteArray(InputStream inputStream) throws IOException{
+		ByteArrayOutputStream output = new ByteArrayOutputStream();
+	    byte[] buffer = new byte[4096];
+	    int n = 0;
+	    while (-1 != (n = inputStream.read(buffer))) {
+	        output.write(buffer, 0, n);
+	    }
+	    return output.toByteArray();
+	}
+
+    public static void writeFile(String filePath, byte[] data) throws IOException {
+        RandomAccessFile raf = null;
+        try {
+            raf = new RandomAccessFile(filePath, "rw");
+            raf.write(data);
+        } finally {
+            if (raf != null) {
+                raf.close();
+            }
+        }
+    }
+
+    public static byte[] readFile(String filePath) throws IOException {
+        RandomAccessFile raf = null;
+        byte[] data;
+        try {
+            raf = new RandomAccessFile(filePath, "r");
+            data = new byte[(int) raf.length()];
+            raf.read(data);
+            return data;
+        } finally {
+            if (raf != null) {
+                raf.close();
+            }
+        }
+    }
+}

+ 127 - 0
src/main/java/com/ymwk/icvp/signature/utils/FileUtils.java

@@ -0,0 +1,127 @@
+package com.ymwk.icvp.signature.utils;
+
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+public class FileUtils {
+
+	/**
+	 * 获取文件名的后缀
+	 * @param fileName
+	 * @return
+	 */
+	public static String getFileNameSuffix(String fileName) {
+		int lastIndexOf = fileName.lastIndexOf(".");
+		if (lastIndexOf > -1) {
+			String subString = fileName.substring(lastIndexOf + 1);
+			return subString;
+		} else {
+			return "";
+		}
+	}
+
+	/**
+	 * 重新定义文件名
+	 * @param fileName
+	 * @return
+	 */
+	public static String renameFileName(String fileName) {
+		String fileSuffix = getFileNameSuffix(fileName);
+		if (ObjectUtils.isEmpty(fileSuffix)) {
+			return UUID.randomUUID().toString().replace("-","");
+		} else {
+			return UUID.randomUUID().toString().replace("-","") + "." + fileSuffix;
+		}
+	}
+	
+	
+	/***
+	 * 传入文件转换成base
+	 * 返回字符串
+	 * @param in
+	 * @return
+	 */
+	public static String base64InputStreamToString(InputStream in) {
+        // 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
+        byte[] data = null;
+        // 读取图片字节数组
+        try {
+            ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
+            byte[] buff = new byte[100];
+            int rc = 0;
+            while ((rc = in.read(buff, 0, 100)) > 0) {
+                swapStream.write(buff, 0, rc);
+            }
+            data = swapStream.toByteArray();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return Base64Utils.encode(data);
+    }
+	
+	/***
+	 * 传入文件转换成base
+	 * 返回字节数组
+	 * @param in
+	 * @return
+	 */
+	public static byte[] base64InputStreamToByte(InputStream in) {
+        // 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
+        byte[] data = null;
+        // 读取图片字节数组
+        try {
+            ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
+            byte[] buff = new byte[100];
+            int rc = 0;
+            while ((rc = in.read(buff, 0, 100)) > 0) {
+                swapStream.write(buff, 0, rc);
+            }
+            data = swapStream.toByteArray();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return data;
+    }
+	
+	/* 
+	 *将InputStream写入本地文件
+     * @param destination 写入本地目录
+     * @param input    输入流
+     * @throws IOException
+     */
+    public static void writeToLocal(String destination, InputStream input)
+            throws IOException {
+        int index;
+        byte[] bytes = new byte[1024];
+        FileOutputStream downloadFile = new FileOutputStream(destination);
+        while ((index = input.read(bytes)) != -1) {
+            downloadFile.write(bytes, 0, index);
+            downloadFile.flush();
+        }
+        downloadFile.close();
+        input.close();
+    }
+	
+	
+}

+ 462 - 0
src/main/java/com/ymwk/icvp/signature/utils/GraphicsUtil.java

@@ -0,0 +1,462 @@
+package com.ymwk.icvp.signature.utils;
+
+import org.springframework.core.io.ClassPathResource;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.font.FontRenderContext;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Date;
+
+public class GraphicsUtil {
+	private static final int width = 160 * 2;// 图片宽度
+	private static final int height = 110 + 50;// 图片高度
+	private static String message = " 白居易";
+	private static String centerName = "测试用章";
+	private static Font font;
+	// private static String year = "2020年06月30日";
+
+	//private static String fontPath = System.getProperty("user.dir")+File.separator+"webapps"+File.separator+"simsun.ttc";
+	//private static String fontPath = System.getProperty("user.dir")+File.separator+"simsun.ttc";
+
+
+	static {
+		ClassPathResource resource = new ClassPathResource("simsun.ttc");
+		try {
+			font = Font.createFont(Font.TRUETYPE_FONT, resource.getInputStream());
+		} catch (FontFormatException e) {
+			e.printStackTrace();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	public static void main(String[] args) throws Exception {
+		BufferedImage image = startChapterGraphics2D(message, centerName);
+		//BufferedImage image = startRectangleGraphics2D(15, centerName,(byte)1,(byte)1);
+		// BufferedImage image = startSquareGraphics2D(500, message);
+		//BufferedImage image = startEllipseGraphics2D(message, centerName);
+		try {
+
+			String filePath = "D:\\\\" +"temp\\"+ new Date().getTime() + ".png";
+			//ByteArrayOutputStream out = new ByteArrayOutputStream();
+			ImageIO.write(image, "png", new FileOutputStream(filePath)); // 将其保存在D:\\下,得有这个目录
+			//ImageIO.write(image, "png", out); // 将其保存在D:\\下,得有这个目录
+			//System.out.println(Base64.toBase64String(out.toByteArray()));
+		} catch (Exception ex) {
+			ex.printStackTrace();
+		}
+
+	}
+
+
+
+
+
+
+
+	/**
+	 * 椭圆章 前面不加空格
+	 *
+	 * @param message
+	 * @param centerName
+	 * @return
+	 */
+	public static BufferedImage startEllipseGraphics2D(String message, String centerName) {
+		// 定义图像buffer
+		BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+		GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+		font = font.deriveFont(Font.BOLD, 21);
+		Font f = font.deriveFont(Font.BOLD, 30);
+		localGraphicsEnvironment.registerFont(font);
+		Graphics2D g = localGraphicsEnvironment.createGraphics(buffImg);
+		// 增加下面代码使得背景透明
+		buffImg = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
+		g.dispose();
+		g = buffImg.createGraphics();
+		// 背景透明代码结束
+		g.setColor(Color.RED);
+		// 设置锯齿圆滑
+		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		g.setStroke(new BasicStroke(3));
+		g.drawOval(2, 2, width - 4, height - 10);
+		g.setFont(font);
+		g.drawString(centerName, (width - 21 * centerName.trim().length()) / 2, height / 2 + 20);
+		// 根据输入字符串得到字符数组
+		String[] messages2 = message.split("", 0);
+		String[] messages = new String[messages2.length];
+		System.arraycopy(messages2, 0, messages, 0, messages2.length);
+		int fontsize = 30;
+		int radius = height / 2;// 周半径
+		int CENTERX = width / 2;// 画图所出位置
+		int CENTERY = height / 2;// 画图所处位置
+		int ilength = messages.length;
+
+
+		FontRenderContext context = g.getFontRenderContext();
+		Rectangle2D bounds = f.getStringBounds(message, context);
+
+		// 字符宽度=字符串长度/字符数
+		double char_interval = (bounds.getWidth() / ilength);
+		// 上坡度
+		double ascent = -bounds.getY();
+		double ascentx = -bounds.getX();
+
+		int first = 0, second = 0;
+		boolean odd = false;
+		if (ilength % 2 == 1) {
+			first = (ilength - 1) / 2;
+			odd = true;
+		} else {
+			first = (ilength) / 2 - 1;
+			second = (ilength) / 2;
+			odd = false;
+		}
+
+		double radius2 = radius + ascentx + 60 + 50;
+		double x0 = CENTERX;
+		double y0 = CENTERY - radius + ascent + 10;
+		// 旋转角度
+		double a = 2 * Math.asin(char_interval / (2 * radius2));
+
+		if (odd) {
+			g.setFont(f);
+			g.drawString(messages[first], (float) (x0 - char_interval / 2), (float) y0);
+
+			// 中心点的右边
+			for (int i = first + 1; i < ilength; i++) {
+				double aa = (i - first) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(aa);// ,x0 + ax, y0 + ay);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 + ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay - char_interval / 2 * Math.sin(aa)));
+			}
+			// 中心点的左边
+			for (int i = first - 1; i > -1; i--) {
+				double aa = (first - i) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(-aa);// ,x0 + ax, y0 + ay);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 - ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay + char_interval / 2 * Math.sin(aa)));
+			}
+
+		} else {
+			// 中心点的右边
+			for (int i = second; i < ilength; i++) {
+				double aa = (i - second + 0.5) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(aa);// ,x0 + ax, y0 + ay);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 + ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay - char_interval / 2 * Math.sin(aa)));
+			}
+
+			// 中心点的左边
+			for (int i = first; i > -1; i--) {
+				double aa = (first - i + 0.5) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(-aa);// ,x0 + ax, y0 + ay);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 - ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay + char_interval / 2 * Math.sin(aa)));
+			}
+		}
+
+		return buffImg;
+
+	}
+
+
+	/**
+	 * 文字转图片
+	 * @param fontSize
+	 * @param message
+	 * @return
+	 */
+	public static BufferedImage startRectangleGraphics2D(int fontSize, String message,byte colorType,byte blod) {
+		int width = fontSize*message.length();
+		int height = fontSize;
+		// 定义图像buffer
+		BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+		GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+		font = font.deriveFont(Font.PLAIN, fontSize);
+		localGraphicsEnvironment.registerFont(font);
+		Graphics2D g = localGraphicsEnvironment.createGraphics(buffImg);
+		// 增加下面代码使得背景透明
+		buffImg = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
+		g.dispose();
+		g = buffImg.createGraphics();
+		// 背景透明代码结束
+		if(colorType == 1){
+			g.setColor(Color.BLACK);
+		}else{
+			g.setColor(Color.RED);
+		}
+		// 设置锯齿圆滑
+//		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+//		g.setStroke(new BasicStroke(fontSize));
+		if(blod == 1){
+			g.setFont(font.deriveFont(Font.BOLD, fontSize));// 设置字体信息
+		}else{
+			g.setFont(font.deriveFont(Font.PLAIN, fontSize));// 设置字体信息
+		}
+		String[] messages = message.split("", 0);
+		for(int i= 0; i< messages.length; i++){
+			g.drawString(messages[i], 0+(fontSize)*i, fontSize);
+		}
+		return buffImg;
+	}
+
+
+	public static BufferedImage startSquareGraphics2D( String message) {
+		return startSquareGraphics2D(200,message);
+	}
+
+	/**
+	 * 方章
+	 *
+	 * @param sideLength
+	 * @param message
+	 * @return
+	 */
+	public static BufferedImage startSquareGraphics2D(int sideLength, String message) {
+		int height = sideLength;
+		if (message.trim().length() <= 2) {
+			height /= 2;
+		}
+		// 定义图像buffer
+		BufferedImage buffImg = new BufferedImage(sideLength, height, BufferedImage.TYPE_INT_RGB);
+		GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+		font = font.deriveFont(Font.BOLD, 21);
+		localGraphicsEnvironment.registerFont(font);
+		Graphics2D g = localGraphicsEnvironment.createGraphics(buffImg);
+		// 增加下面代码使得背景透明
+		buffImg = g.getDeviceConfiguration().createCompatibleImage(sideLength, height, Transparency.TRANSLUCENT);
+		g.dispose();
+		g = buffImg.createGraphics();
+		// 背景透明代码结束
+		g.setColor(Color.RED);
+		// 设置锯齿圆滑
+		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		g.setStroke(new BasicStroke(10));
+		g.drawRect(0, 0, sideLength - 2, height - 2);
+		int fontWidth = sideLength / 2 - 5; // 计算文字合适大小
+		int messageWidth = fontWidth * 2; // 计算出文字的总宽度
+		int CENTERX = (sideLength - messageWidth) / 2;// 计算出字体开始的位置
+		g.setFont(font.deriveFont(Font.LAYOUT_LEFT_TO_RIGHT, fontWidth));// 设置字体信息
+		int CENTERY = fontWidth - sideLength / 22;
+		if (message.trim().length() <= 2) {
+			CENTERY = fontWidth - sideLength / 15;
+		}
+		String[] messages2 = message.split("", 0);
+		String[] messages = new String[messages2.length];
+        System.arraycopy(messages2, 0, messages, 0, messages2.length);
+        for (int i = 0; i < 2; i++) {// 画出前面两个字符
+			try{
+				if (message.trim().length() > 2) {
+					g.drawString(messages[i], CENTERX + fontWidth, CENTERY + fontWidth * i);
+				} else {
+					g.drawString(messages[i], CENTERX + fontWidth * i, CENTERY);
+				}
+			}catch(Exception e){
+				e.printStackTrace();
+			}
+		}
+		if (message.trim().length() > 2) {
+			for (int i = 2; i < message.length(); i++) {
+				// g.drawString(messages[i], CENTERX + fontWidth * (i - 2), CENTERY+ fontWidth);
+				g.drawString(messages[i], CENTERX, CENTERY + fontWidth * (i - 2));
+			}
+		}
+		return buffImg;
+	}
+
+	/**
+	 * 圆章
+	 *
+	 * @param message
+	 * @param centerName
+	 * @return
+	 */
+	public static BufferedImage startChapterGraphics2D(String message, String centerName) {
+		return startChapterGraphics2D(365, 365, message, centerName);
+	}
+
+	/**
+	 * 圆章
+	 *
+	 * @param width
+	 * @param height
+	 * @param message
+	 * @param centerName
+	 * @return
+	 */
+	public static BufferedImage startChapterGraphics2D(int width, int height, String message, String centerName) {
+		// 定义图像buffer
+		BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+		GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+
+		localGraphicsEnvironment.registerFont(font);
+		Graphics2D g = localGraphicsEnvironment.createGraphics(buffImg);
+		// 增加下面代码使得背景透明
+		buffImg = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
+		g.dispose();
+		g = buffImg.createGraphics();
+		// 背景透明代码结束
+		g.setColor(Color.RED);
+		// 设置锯齿圆滑
+		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		// 绘制圆
+		int radius = height / 2-40;// 周半径
+		int CENTERX = width / 2;// 画图所出位置
+		int CENTERY = height / 2;// 画图所处位置
+
+		Ellipse2D circle = new Ellipse2D.Double();
+		circle.setFrameFromCenter(CENTERX, CENTERY, CENTERX + radius+15, CENTERY + radius+15);
+		g.setStroke(new BasicStroke(8));// 设置圆的宽度
+		g.draw(circle);
+
+		// 绘制中间的五角星
+		g.setFont( font.deriveFont(Font.BOLD, 120));
+		g.drawString("★", CENTERX - (120 / 2), CENTERY + (120 / 3));
+
+		// 添加姓名
+		AffineTransform affineTransform = new AffineTransform();
+		affineTransform.scale(1, 1.3); // 第一个参数为水平缩放因子,第二个为垂直缩放因子
+
+		// 应用变换到字体
+		Font stretchedFont = font.deriveFont(affineTransform);
+		g.setFont(stretchedFont.deriveFont(Font.BOLD, 30));// 写入签名
+
+		//计算字数获取需要的宽度,再计算出中间位置
+		int centerNameWidth = centerName.length() * 30;
+
+		g.drawString(centerName, radius-centerNameWidth/2 + 40, CENTERY + (30 + 80));
+
+		// 添加年份
+		// g.setFont(new Font("宋体", Font.LAYOUT_LEFT_TO_RIGHT, 20));// 写入签名
+		// g.drawString(year, CENTERX -(60), CENTERY +(30+80));
+
+		// 根据输入字符串得到字符数组
+		String[] messages2 = message.split("", 0);
+		String[] messages = new String[messages2.length];
+		System.arraycopy(messages2, 0, messages, 0, messages2.length);
+//	        for (int i = 0; i < messages.length; i++) {
+//	        	 System.out.println(messages[i]);
+//			}
+		// 输入的字数
+		int ilength = messages.length;
+		if(ilength>30){
+			throw new IllegalArgumentException("长度大于30个字!");
+		}
+		// 设置字体属性
+		int fontsize = 50;
+		// 字体大小自适应
+		if(ilength>10) {
+			fontsize-=(ilength-8)*1.4;
+			if (fontsize < 24) fontsize = 24;  // 设置一个最小字体大小
+		}
+		Font f = font.deriveFont(Font.PLAIN, fontsize);
+		FontRenderContext context = g.getFontRenderContext();
+		Rectangle2D bounds = f.getStringBounds(message, context);
+		// 字符宽度=字符串长度/字符数
+		double char_interval = (bounds.getWidth() / ilength);
+		// 上坡度
+		double ascent = -bounds.getY();
+
+		int first = 0, second = 0;
+		boolean odd = false;
+		if (ilength % 2 == 1) {
+			first = (ilength - 1) / 2;
+			odd = true;
+		} else {
+			first = (ilength) / 2 - 1;
+			second = (ilength) / 2;
+			odd = false;
+		}
+
+		double radius2 = radius - ascent;
+		double x0 = CENTERX ;
+		double y0 = CENTERY - radius + ascent;
+		// 旋转角度
+		double a = 2 * Math.asin(char_interval / (2 * radius2));
+
+		if (odd) {
+			AffineTransform tr = new AffineTransform();
+			tr.scale(1,1.3);
+			Font ff = f.deriveFont(tr);
+			g.setFont(f);
+			g.drawString(messages[first], (float) (x0 - char_interval / 2), (float) y0);
+
+			// 中心点的右边
+			for (int i = first + 1; i < ilength; i++) {
+				double aa = (i - first) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(aa);// ,x0 + ax, y0 + ay);
+				transform.scale(1, 1.3);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 + ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay - char_interval / 2 * Math.sin(aa)));
+			}
+			// 中心点的左边
+			for (int i = first - 1; i > -1; i--) {
+				double aa = (first - i) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(-aa);// ,x0 + ax, y0 + ay);
+				transform.scale(1, 1.3);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 - ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay + char_interval / 2 * Math.sin(aa)));
+			}
+
+		} else {
+			// 中心点的右边
+			for (int i = second; i < ilength; i++) {
+				double aa = (i - second + 0.5) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(aa);// ,x0 + ax, y0 + ay);
+				transform.scale(1, 1.3);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 + ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay - char_interval / 2 * Math.sin(aa)));
+			}
+
+			// 中心点的左边
+			for (int i = first; i > -1; i--) {
+				double aa = (first - i + 0.5) * a;
+				double ax = radius2 * Math.sin(aa);
+				double ay = radius2 - radius2 * Math.cos(aa);
+				AffineTransform transform = AffineTransform.getRotateInstance(-aa);// ,x0 + ax, y0 + ay);
+				transform.scale(1, 1.3);
+				Font f2 = f.deriveFont(transform);
+				g.setFont(f2);
+				g.drawString(messages[i], (float) (x0 - ax - char_interval / 2 * Math.cos(aa)),
+						(float) (y0 + ay + char_interval / 2 * Math.sin(aa)));
+			}
+		}
+
+		return buffImg;
+	}
+
+}

+ 32 - 0
src/main/java/com/ymwk/icvp/signature/utils/ObjectUtils.java

@@ -0,0 +1,32 @@
+package com.ymwk.icvp.signature.utils;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Map;
+
+public class ObjectUtils {
+	
+	public static boolean isEmpty(Object obj) {
+		if (obj == null) {
+			return true;
+		}
+		if (obj instanceof CharSequence) {
+			return ((CharSequence) obj).length() == 0;
+		}
+		if (obj.getClass().isArray()) {
+			return Array.getLength(obj) == 0;
+		}
+		if (obj instanceof Collection) {
+			return ((Collection) obj).isEmpty();
+		}
+		if (obj instanceof Map) {
+			return ((Map) obj).isEmpty();
+		}
+		return false;
+	}
+
+	public static boolean isNotEmpty(Object obj) {
+		return !isEmpty(obj);
+	}
+}
+

+ 108 - 0
src/main/java/com/ymwk/icvp/signature/utils/PfxUtils.java

@@ -0,0 +1,108 @@
+package com.ymwk.icvp.signature.utils;
+
+import org.bouncycastle.asn1.DERBMPString;
+import org.bouncycastle.asn1.pkcs.ContentInfo;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.crypto.engines.DESedeEngine;
+import org.bouncycastle.crypto.engines.RC2Engine;
+import org.bouncycastle.crypto.modes.CBCBlockCipher;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.InputDecryptorProvider;
+import org.bouncycastle.pkcs.*;
+import org.bouncycastle.pkcs.bc.BcPKCS12MacCalculatorBuilder;
+import org.bouncycastle.pkcs.bc.BcPKCS12PBEOutputEncryptorBuilder;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS12SafeBagBuilder;
+import org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+public class PfxUtils {
+
+    public static PKCS12PfxPdu makePfx(PrivateKey privKey, PublicKey pubKey, X509Certificate cert, String passwd)
+        throws NoSuchAlgorithmException, IOException, PKCSException {
+        JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+
+        PKCS12SafeBagBuilder eeCertBagBuilder = new JcaPKCS12SafeBagBuilder(cert);
+        eeCertBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
+            new DERBMPString("User Key"));
+        eeCertBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
+            extUtils.createSubjectKeyIdentifier(pubKey));
+
+        char[] passwdChars = passwd.toCharArray();
+        PKCS12SafeBagBuilder keyBagBuilder = new JcaPKCS12SafeBagBuilder(privKey,
+            new BcPKCS12PBEOutputEncryptorBuilder(
+                PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC,
+                new CBCBlockCipher(new DESedeEngine())).build(passwdChars));
+        keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
+            new DERBMPString("User Key"));
+        keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
+            extUtils.createSubjectKeyIdentifier(pubKey));
+
+        PKCS12PfxPduBuilder pfxPduBuilder = new PKCS12PfxPduBuilder();
+        PKCS12SafeBag[] certs = new PKCS12SafeBag[1];
+        certs[0] = eeCertBagBuilder.build();
+        pfxPduBuilder.addEncryptedData(new BcPKCS12PBEOutputEncryptorBuilder(
+                PKCSObjectIdentifiers.pbeWithSHAAnd40BitRC2_CBC,
+                new CBCBlockCipher(new RC2Engine())).build(passwdChars),
+            certs);
+        pfxPduBuilder.addData(keyBagBuilder.build());
+        return pfxPduBuilder.build(new BcPKCS12MacCalculatorBuilder(), passwdChars);
+    }
+
+    public static PrivateKey getPrivateKeyFromPfx(byte[] pfxDER, String passwd) throws Exception {
+		InputDecryptorProvider inputDecryptorProvider = new JcePKCSPBEInputDecryptorProviderBuilder()
+				.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passwd.toCharArray());
+		PKCS12PfxPdu pfx = new PKCS12PfxPdu(pfxDER);
+
+		ContentInfo[] infos = pfx.getContentInfos();
+		if (infos.length != 2) {
+			throw new Exception("Only support one pair ContentInfo");
+		}
+
+		for (int i = 0; i != infos.length; i++) {
+			if (!infos[i].getContentType().equals(PKCSObjectIdentifiers.encryptedData)) {
+				PKCS12SafeBagFactory dataFact = new PKCS12SafeBagFactory(infos[i]);
+				PKCS12SafeBag[] bags = dataFact.getSafeBags();
+				PKCS8EncryptedPrivateKeyInfo encInfo = (PKCS8EncryptedPrivateKeyInfo) bags[0].getBagValue();
+				PrivateKeyInfo info = encInfo.decryptPrivateKeyInfo(inputDecryptorProvider);
+				PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(info.getEncoded());
+				KeyFactory kf = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
+				PrivateKey privateKey = kf.generatePrivate(peks);
+				return privateKey;
+			}
+		}
+		throw new Exception("Not found Private Key in this pfx");
+	}
+
+	public static X509Certificate getX509CertificateFromPfx(byte[] pfxDER, String passwd) throws Exception {
+		InputDecryptorProvider inputDecryptorProvider = new JcePKCSPBEInputDecryptorProviderBuilder()
+				.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passwd.toCharArray());
+		PKCS12PfxPdu pfx = new PKCS12PfxPdu(pfxDER);
+		ContentInfo[] infos = pfx.getContentInfos();
+		if (infos.length != 2) {
+			throw new Exception("Only support one pair ContentInfo");
+		}
+		for (int i = 0; i != infos.length; i++) {
+			if (infos[i].getContentType().equals(PKCSObjectIdentifiers.encryptedData)) {
+				PKCS12SafeBagFactory dataFact = new PKCS12SafeBagFactory(infos[i], inputDecryptorProvider);
+				PKCS12SafeBag[] bags = dataFact.getSafeBags();
+				X509CertificateHolder certHoler = (X509CertificateHolder) bags[0].getBagValue();
+				ByteArrayInputStream bais = new ByteArrayInputStream(certHoler.getEncoded());
+				CertificateFactory cf = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
+				return (X509Certificate) cf.generateCertificate(bais);
+			}
+		}
+		throw new Exception("Not found X509Certificate in this pfx");
+	}
+}

+ 52 - 0
src/main/java/com/ymwk/icvp/task/ExpiryStatusTask.java

@@ -0,0 +1,52 @@
+package com.ymwk.icvp.task;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.ymwk.icvp.pki.entity.CertificateEntity;
+import com.ymwk.icvp.pki.mapper.CertificateMapper;
+import com.ymwk.icvp.signature.entity.Seal;
+import com.ymwk.icvp.signature.mapper.SealMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Component
+public class ExpiryStatusTask {
+
+    private final CertificateMapper certificateMapper;
+    private final SealMapper sealMapper;
+
+    public ExpiryStatusTask(CertificateMapper certificateMapper, SealMapper sealMapper) {
+        this.certificateMapper = certificateMapper;
+        this.sealMapper = sealMapper;
+    }
+
+    @Scheduled(initialDelay = 0, fixedDelay = 3_600_000)
+    public void updateExpiredStatuses() {
+        LocalDateTime now = LocalDateTime.now();
+
+        int expiredCertificates = certificateMapper.update(
+                null,
+                new LambdaUpdateWrapper<CertificateEntity>()
+                        .set(CertificateEntity::getStatus, "2")
+                        .in(CertificateEntity::getStatus, "1", "3")
+                        .lt(CertificateEntity::getValidTo, now)
+        );
+
+        int expiredSeals = sealMapper.update(
+                null,
+                new LambdaUpdateWrapper<Seal>()
+                        .set(Seal::getSealStatus, 4)
+                        .set(Seal::getUpdateTime, now)
+                        .set(Seal::getUpdateBy, "system")
+                        .in(Seal::getSealStatus, 1, 2)
+                        .lt(Seal::getValidEndTime, now)
+        );
+
+        if (expiredCertificates > 0 || expiredSeals > 0) {
+            log.info("过期状态更新完成: certificates={}, seals={}", expiredCertificates, expiredSeals);
+        }
+    }
+}

+ 248 - 0
src/main/java/com/ymwk/icvp/utils/MinioClientService.java

@@ -0,0 +1,248 @@
+package com.ymwk.icvp.utils;
+
+
+import io.minio.*;
+import io.minio.http.Method;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Slf4j
+public class MinioClientService {
+
+    @Value("${minio.endpoint}")
+    public String endpoint;
+
+    @Value("${minio.access-key}")
+    private String accessKey;
+
+    @Value("${minio.secret-key}")
+    private String secretKey;
+
+    @Value("${minio.bucket-name}")
+    private String bucketName;
+
+    private MinioClient minioClient;
+
+    @PostConstruct
+    public void init() {
+        try {
+            minioClient = MinioClient.builder()
+                    .endpoint(endpoint)
+                    .credentials(accessKey, secretKey)
+                    .build();
+
+            // 确保桶存在
+            boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+            if (!isExist) {
+                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("MinIO client initialization failed", e);
+        }
+    }
+
+    /**
+     * 上传文件到 MinIO
+     * @param file 上传的文件
+     * @return 生成的文件名(用于后续下载)
+     */
+    public String uploadFile(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("File cannot be null or empty");
+        }
+
+        String fileName = generateUniqueFileName(file.getOriginalFilename());
+
+        try (InputStream inputStream = file.getInputStream()) {
+            PutObjectArgs args = PutObjectArgs.builder()
+                    .bucket(bucketName)
+                    .object(fileName)
+                    .stream(inputStream, file.getSize(), -1)
+                    .contentType(file.getContentType())
+                    .build();
+
+            minioClient.putObject(args);
+            return fileName;
+
+        } catch (Exception e) {
+            throw new RuntimeException("Error uploading file: " + e.getMessage(), e);
+        }
+    }
+    /**
+     * 上传证书文件(PEM格式)
+     * @param serialNumber 证书序列号(用作文件名)
+     * @param content 证书内容
+     * @param fileType 文件类型(cert / private-key / chain)
+     * @return 文件路径
+     */
+    public String uploadCertificate(String serialNumber, String content, String fileType) throws Exception {
+
+
+
+        // 构建文件路径: certs/{serialNumber}/cert.pem
+        String objectName = String.format("certs/%s/%s.pem", serialNumber, fileType);
+
+        // 转换为字节流
+        byte[] contentBytes = content.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(contentBytes)) {
+
+            // 设置文件元数据
+            Map<String, String> userMetadata = new HashMap<>();
+            userMetadata.put("serial-number", serialNumber);
+            userMetadata.put("file-type", fileType);
+            userMetadata.put("upload-time", LocalDateTime.now().toString());
+
+            // 上传文件
+            minioClient.putObject(
+                    PutObjectArgs.builder()
+                            .bucket(bucketName)
+                            .object(objectName)
+                            .stream(inputStream, contentBytes.length, -1)
+                            .contentType("application/x-pem-file")
+                            .userMetadata(userMetadata)
+                            .build()
+            );
+
+            log.info("证书文件上传成功: {}", objectName);
+            return objectName;
+        }
+    }
+    public void uploadObject(String objectName, String content, String contentType) throws Exception {
+        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
+            minioClient.putObject(
+                    PutObjectArgs.builder()
+                            .bucket(bucketName)
+                            .object(objectName)
+                            .stream(inputStream, bytes.length, -1)
+                            .contentType(contentType)
+                            .build()
+            );
+        }
+    }
+    /**
+     * 下载文件(返回输入流,用于Web下载)
+     * @param fileName 文件名
+     * @return 文件输入流
+     */
+    public InputStream downloadFile(String fileName) {
+        if (fileName == null || fileName.trim().isEmpty()) {
+            throw new IllegalArgumentException("File name cannot be null or empty");
+        }
+
+        try {
+            GetObjectArgs args = GetObjectArgs.builder()
+                    .bucket(bucketName)
+                    .object(fileName)
+                    .build();
+
+            return minioClient.getObject(args);
+        } catch (Exception e) {
+            throw new RuntimeException("Error downloading file: " + e.getMessage(), e);
+        }
+    }
+    /**
+     * 获取文件输入流(用于压缩包下载)
+     * @param fileKey 文件在 MinIO 中的 key
+     * @return 文件输入流
+     */
+    public GetObjectResponse getObject(String fileKey) throws Exception {
+        return minioClient.getObject(
+                GetObjectArgs.builder()
+                        .bucket(bucketName)
+                        .object(fileKey)
+                        .build()
+        );
+    }
+    /**
+     * 获取文件访问URL(带临时签名)
+     */
+    public String getPresignedUrl(String objectName, int expiryMinutes) throws Exception {
+        return minioClient.getPresignedObjectUrl(
+                GetPresignedObjectUrlArgs.builder()
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .method(Method.GET)
+                        .expiry(expiryMinutes, TimeUnit.MINUTES)
+                        .build()
+        );
+    }
+    /**
+     * 获取永久下载URL(如果MinIO配置了公开访问)
+     */
+    public String getPublicUrl(String objectName) {
+        return String.format("%s/%s/%s",
+                endpoint,
+                bucketName,
+                objectName
+        );
+    }
+    /**
+     * 删除证书文件
+     */
+    public void deleteFile(String objectName) throws Exception {
+        minioClient.removeObject(
+                RemoveObjectArgs.builder()
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .build()
+        );
+    }
+    /**
+     * 下载文件为字符串
+     */
+    public String downloadFileAsString(String objectName) throws Exception {
+        try (InputStream inputStream = minioClient.getObject(
+                GetObjectArgs.builder()
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .build()
+        )) {
+            byte[] content = inputStream.readAllBytes();
+            return new String(content, StandardCharsets.UTF_8);
+        }
+    }
+
+    /**
+     * 生成唯一文件名
+     */
+    private String generateUniqueFileName(String originalFileName) {
+        String extension = "";
+        if (originalFileName != null && originalFileName.contains(".")) {
+            extension = originalFileName.substring(originalFileName.lastIndexOf("."));
+        }
+        return UUID.randomUUID() + extension;
+    }
+
+    /**
+     * 上传字节数组到 MinIO
+     * @param objectName 对象名
+     * @param bytes 字节数据
+     * @param contentType MIME 类型
+     */
+    public void uploadBytes(String objectName, byte[] bytes, String contentType) throws Exception {
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
+            minioClient.putObject(
+                    PutObjectArgs.builder()
+                            .bucket(bucketName)
+                            .object(objectName)
+                            .stream(inputStream, bytes.length, -1)
+                            .contentType(contentType)
+                            .build()
+            );
+        }
+    }
+}

+ 22 - 0
src/main/java/com/ymwk/icvp/utils/MinioConfig.java

@@ -0,0 +1,22 @@
+package com.ymwk.icvp.utils;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MinioConfig {
+
+    @Value("${minio.endpoint}")
+    private String endpoint;
+
+    @Value("${minio.bucket-name}")
+    private String bucketName;
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public String getBucketName() {
+        return bucketName;
+    }
+}

+ 48 - 0
src/main/java/com/ymwk/icvp/utils/fileConvert.java

@@ -0,0 +1,48 @@
+package com.ymwk.icvp.utils;
+
+import com.ymwk.icvp.pdfToOfdBao.OFDRender;
+import lombok.extern.slf4j.Slf4j;
+import org.ofdrw.converter.ConvertHelper;
+import org.ofdrw.converter.FontLoader;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+@Component
+@Slf4j
+public class fileConvert {
+
+
+    public static InputStream pdfToOfdEx(InputStream in) {
+        ByteArrayOutputStream ofdOut = new ByteArrayOutputStream();
+        try {
+            OFDRender.convertPdfToOfd(in, ofdOut);
+            return new ByteArrayInputStream(ofdOut.toByteArray());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static InputStream ofdToPdfEx(InputStream in) {
+        ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
+        try {
+            FontLoader.getInstance()
+                    .addAliasMapping(null, "小标宋体", "方正小标宋简体", "方正小标宋简体")
+                    .addAliasMapping(null, "KaiTi_GB2312", "楷体", "楷体")
+                    .addSimilarFontReplaceRegexMapping(null, ".*Kai.*", null, "楷体")
+                    .addSimilarFontReplaceRegexMapping(null, ".*SimSun.*", null, "SimSun")
+                    .addSimilarFontReplaceRegexMapping(null, ".*Song.*", null, "宋体")
+                    .addSimilarFontReplaceRegexMapping(null, ".*MinionPro.*", null, "SimSun");
+
+            FontLoader.setSimilarFontReplace(true);
+            ConvertHelper.toPdf(in, pdfOut);
+            return new ByteArrayInputStream(pdfOut.toByteArray());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

binární
src/main/resources/SealBuilder.p12


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

@@ -0,0 +1,39 @@
+spring:
+  application:
+    name: ICVP
+
+  datasource:
+    # 驱动类 (使用 MySQL 8.0 及以上版本推荐驱动)
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    # 数据库连接 URL,注意替换库名、时区和编码设置
+    url: jdbc:mysql://10.10.10.30:3306/icvp?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
+    # 数据库用户名
+    username: icvp
+    # 数据库密码
+    password: 4RtmJimtscsK7XiN
+
+minio:
+  endpoint: http://10.10.10.25:9020
+  bucket-name: test
+  access-key: minio_bxM8k5
+  secret-key: minio_mP6zC4
+
+seal:
+  #sealer-p12-path: /opt/icvp/seal/SealBuilder.p12
+  sealer-p12-path: D:/IdeaProject/icvpServer/src/main/resources/SealBuilder.p12
+  sealer-p12-password: 12345678
+
+# 2. MyBatis-Plus 专属配置
+mybatis-plus:
+  # 指定 XML 映射文件的位置 (用于自定义 SQL)
+  # 默认值为 classpath*:/mapper/**/*.xml,通常无需修改
+  mapper-locations: classpath*:/mapper/**/*.xml
+  # 设置实体类的别名包,这样在 XML 中可以直接使用类名,无需全限定名
+  type-aliases-package: com.ymwk.icvp.entity
+  # 原生 MyBatis 的详细配置
+  configuration:
+    # 开启下划线到驼峰的自动映射 (例如: user_name -> userName)
+    # 默认为 true,建议保持开启
+    map-underscore-to-camel-case: true
+    # 将执行的 SQL 语句输出到控制台 (开发时很有用)
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

binární
src/main/resources/simsun.ttc


+ 102 - 0
src/test/java/com/ymwk/icvp/IcvpApplicationTests.java

@@ -0,0 +1,102 @@
+package com.ymwk.icvp;
+
+import com.ymwk.icvp.utils.fileConvert;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FileConvertTest {
+
+    /**
+     * 可以通过 JVM 参数覆盖:
+     * -Dpdf.input=D:\test\input.pdf
+     * -Dofd.input=D:\test\input.ofd
+     */
+    private static final Path PDF_INPUT = Paths.get(
+            System.getProperty("pdf.input", "src/test/resources/input.pdf")
+    );
+
+    private static final Path OFD_INPUT = Paths.get(
+            System.getProperty("ofd.input", "src/test/resources/input.ofd")
+    );
+
+    private static final Path OFD_OUTPUT = Paths.get(
+            System.getProperty("ofd.output", "target/test-output/result.ofd")
+    );
+
+    private static final Path PDF_OUTPUT = Paths.get(
+            System.getProperty("pdf.output", "target/test-output/result.pdf")
+    );
+
+    @Test
+    void pdfToOfdTest() throws Exception {
+        assertInputFileExists(PDF_INPUT);
+
+        createParentDirectory(OFD_OUTPUT);
+
+        try (InputStream input = Files.newInputStream(PDF_INPUT);
+             InputStream output = fileConvert.pdfToOfdEx(input)) {
+
+            assertTrue(output != null, "PDF 转 OFD 失败,转换方法返回 null");
+
+            Files.copy(
+                    output,
+                    OFD_OUTPUT,
+                    java.nio.file.StandardCopyOption.REPLACE_EXISTING
+            );
+        }
+
+        assertOutputFileExists(OFD_OUTPUT);
+    }
+
+    @Test
+    void ofdToPdfTest() throws Exception {
+        assertInputFileExists(OFD_INPUT);
+
+        createParentDirectory(PDF_OUTPUT);
+
+        try (InputStream input = Files.newInputStream(OFD_INPUT);
+             InputStream output = fileConvert.ofdToPdfEx(input)) {
+
+            assertTrue(output != null, "OFD 转 PDF 失败,转换方法返回 null");
+
+            Files.copy(
+                    output,
+                    PDF_OUTPUT,
+                    java.nio.file.StandardCopyOption.REPLACE_EXISTING
+            );
+        }
+
+        assertOutputFileExists(PDF_OUTPUT);
+    }
+
+    private static void assertInputFileExists(Path inputPath) {
+        assertTrue(
+                Files.isRegularFile(inputPath),
+                "输入文件不存在,请检查路径:" + inputPath.toAbsolutePath()
+        );
+    }
+
+    private static void assertOutputFileExists(Path outputPath) throws Exception {
+        assertTrue(
+                Files.isRegularFile(outputPath),
+                "输出文件未生成:" + outputPath.toAbsolutePath()
+        );
+        assertTrue(
+                Files.size(outputPath) > 0,
+                "输出文件为空:" + outputPath.toAbsolutePath()
+        );
+    }
+
+    private static void createParentDirectory(Path path) throws Exception {
+        Path parent = path.toAbsolutePath().getParent();
+        if (parent != null) {
+            Files.createDirectories(parent);
+        }
+    }
+}

binární
src/test/resources/input.ofd


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů