weijianghai vor 1 Tag
Commit
7ce88ce0cf
100 geänderte Dateien mit 6756 neuen und 0 gelöschten Zeilen
  1. 734 0
      .gitignore
  2. 4 0
      .mvn/maven.config
  3. 19 0
      .mvn/wrapper/maven-wrapper.properties
  4. 371 0
      doc/dingtalk.sql
  5. 1625 0
      doc/钉钉机器人接口.md
  6. 259 0
      mvnw
  7. 149 0
      mvnw.cmd
  8. 203 0
      pom.xml
  9. 2 0
      readme.md
  10. 13 0
      src/main/java/com/nokia/dingtalk_api/DingtalkApiApplication.java
  11. 15 0
      src/main/java/com/nokia/dingtalk_api/annotation/RequiredHeader.java
  12. 24 0
      src/main/java/com/nokia/dingtalk_api/annotation/ValidCron.java
  13. 24 0
      src/main/java/com/nokia/dingtalk_api/annotation/ValidRegex.java
  14. 58 0
      src/main/java/com/nokia/dingtalk_api/auth/MyExceptionHandling.java
  15. 29 0
      src/main/java/com/nokia/dingtalk_api/auth/UserDetailServiceImpl.java
  16. 41 0
      src/main/java/com/nokia/dingtalk_api/auth/token/AuthenticationToken.java
  17. 156 0
      src/main/java/com/nokia/dingtalk_api/auth/token/TokenAuthenticationFilter.java
  18. 100 0
      src/main/java/com/nokia/dingtalk_api/common/R.java
  19. 7 0
      src/main/java/com/nokia/dingtalk_api/common/exception/BizException.java
  20. 27 0
      src/main/java/com/nokia/dingtalk_api/common/exception/MyRuntimeException.java
  21. 18 0
      src/main/java/com/nokia/dingtalk_api/config/ApiDocConfig.java
  22. 67 0
      src/main/java/com/nokia/dingtalk_api/config/AppConfig.java
  23. 23 0
      src/main/java/com/nokia/dingtalk_api/config/DescNullsLastInterceptor.java
  24. 24 0
      src/main/java/com/nokia/dingtalk_api/config/MybatisPlusConfig.java
  25. 17 0
      src/main/java/com/nokia/dingtalk_api/config/TaskConfig.java
  26. 244 0
      src/main/java/com/nokia/dingtalk_api/config/security/SecurityBeanConfig.java
  27. 88 0
      src/main/java/com/nokia/dingtalk_api/config/security/SecurityConfig.java
  28. 102 0
      src/main/java/com/nokia/dingtalk_api/config/web/ControllerExceptionHandler.java
  29. 29 0
      src/main/java/com/nokia/dingtalk_api/config/web/MyWebMvcConfigurer.java
  30. 186 0
      src/main/java/com/nokia/dingtalk_api/config/web/RequestLogHandlerInterceptor.java
  31. 25 0
      src/main/java/com/nokia/dingtalk_api/config/web/ValidatorConfig.java
  32. 59 0
      src/main/java/com/nokia/dingtalk_api/controller/AppController.java
  33. 52 0
      src/main/java/com/nokia/dingtalk_api/controller/AppRobotCommandController.java
  34. 63 0
      src/main/java/com/nokia/dingtalk_api/controller/AppRobotController.java
  35. 62 0
      src/main/java/com/nokia/dingtalk_api/controller/AppSftpController.java
  36. 81 0
      src/main/java/com/nokia/dingtalk_api/controller/AppTaskController.java
  37. 41 0
      src/main/java/com/nokia/dingtalk_api/controller/AuthController.java
  38. 149 0
      src/main/java/com/nokia/dingtalk_api/controller/OpenController.java
  39. 52 0
      src/main/java/com/nokia/dingtalk_api/controller/RobotController.java
  40. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IAppRobotService.java
  41. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IAppService.java
  42. 13 0
      src/main/java/com/nokia/dingtalk_api/dao/IAppSftpService.java
  43. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IAppTaskLogService.java
  44. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IAppTaskService.java
  45. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IReceiveMessageLogService.java
  46. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IRequestLogService.java
  47. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IRobotCommandService.java
  48. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IRobotService.java
  49. 14 0
      src/main/java/com/nokia/dingtalk_api/dao/IUserService.java
  50. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AppRobotServiceImpl.java
  51. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AppServiceImpl.java
  52. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AppSftpServiceImpl.java
  53. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AppTaskLogServiceImpl.java
  54. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/AppTaskServiceImpl.java
  55. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/ReceiveMessageLogServiceImpl.java
  56. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/RequestLogServiceImpl.java
  57. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/RobotCommandServiceImpl.java
  58. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/RobotServiceImpl.java
  59. 18 0
      src/main/java/com/nokia/dingtalk_api/dao/impl/UserServiceImpl.java
  60. 68 0
      src/main/java/com/nokia/dingtalk_api/filter/SessionRefreshFilter.java
  61. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/AppMapper.java
  62. 117 0
      src/main/java/com/nokia/dingtalk_api/mapper/AppRobotMapper.java
  63. 15 0
      src/main/java/com/nokia/dingtalk_api/mapper/AppSftpMapper.java
  64. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/AppTaskLogMapper.java
  65. 159 0
      src/main/java/com/nokia/dingtalk_api/mapper/AppTaskMapper.java
  66. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/ReceiveMessageLogMapper.java
  67. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/RequestLogMapper.java
  68. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/RobotCommandMapper.java
  69. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/RobotMapper.java
  70. 16 0
      src/main/java/com/nokia/dingtalk_api/mapper/UserMapper.java
  71. 120 0
      src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java
  72. 22 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/PageDto.java
  73. 15 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppDto.java
  74. 23 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppRobotCommandDto.java
  75. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppRobotDto.java
  76. 28 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppSftpDto.java
  77. 50 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java
  78. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddRobotDto.java
  79. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppDto.java
  80. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppRobotCommandDto.java
  81. 18 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppRobotDto.java
  82. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppSftpDto.java
  83. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppTaskDto.java
  84. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteRobotDto.java
  85. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/EnableAppTaskDto.java
  86. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/GetAppRobotOptionsDto.java
  87. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/GetAppSftpOptionsDto.java
  88. 21 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppDto.java
  89. 26 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppRobotCommandDto.java
  90. 25 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppRobotDto.java
  91. 29 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppSftpDto.java
  92. 43 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppTaskDto.java
  93. 50 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppTaskLogDto.java
  94. 25 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAvailableRobotDto.java
  95. 21 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListRobotDto.java
  96. 15 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/LoginDto.java
  97. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ResetAppSecretDto.java
  98. 16 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/RunAppTaskDto.java
  99. 12 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/StopAppTaskDto.java
  100. 15 0
      src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/UpdateAppDto.java

+ 734 - 0
.gitignore

@@ -0,0 +1,734 @@
+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/
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Maven template
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+# JDT-specific (Eclipse Java Development Tools)
+
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains+all template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+
+# AWS User-specific
+
+# Generated files
+
+# Sensitive or high-churn files
+
+# Gradle
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+
+# Mongo Explorer plugin
+
+# File-based project format
+
+# IntelliJ
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Cursive Clojure plugin
+
+# SonarLint plugin
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+# Editor-based Rest Client
+
+# Android studio 3.1+ serialized cache file
+
+### Eclipse template
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+.apt_generated_test/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+# Uncomment this line if you wish to ignore the project description file.
+# Typically, this file would be tracked if it contains build/dependency configurations:
+#.project
+
+### Example user template template
+### Example user template
+
+# IntelliJ project files
+out
+gen
+### VisualStudio template
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp_proj
+*_wpftmp.csproj
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+*.code-workspace
+
+# Local History for Visual Studio Code
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+### JetBrains+iml template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+
+# AWS User-specific
+
+# Generated files
+
+# Sensitive or high-churn files
+
+# Gradle
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+
+# Mongo Explorer plugin
+
+# File-based project format
+
+# IntelliJ
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Cursive Clojure plugin
+
+# SonarLint plugin
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+# Editor-based Rest Client
+
+# Android studio 3.1+ serialized cache file
+
+### Kotlin template
+# Compiled class file
+
+# Log file
+
+# BlueJ files
+
+# Mobile Tools for Java (J2ME)
+
+# Package Files #
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+

+ 4 - 0
.mvn/maven.config

@@ -0,0 +1,4 @@
+-U
+-T1C
+-Dmaven.test.skip=true
+-Dmaven.compile.fork=true

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

@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip

+ 371 - 0
doc/dingtalk.sql

@@ -0,0 +1,371 @@
+-- ----------------------------
+-- Table structure for app
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."app";
+CREATE TABLE "dingtalk"."app" (
+  "app_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "app_secret" text COLLATE "pg_catalog"."default",
+  "app_name" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "deleted" int4 DEFAULT 0
+)
+;
+COMMENT ON COLUMN "dingtalk"."app"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."app"."app_secret" IS '应用密钥';
+COMMENT ON COLUMN "dingtalk"."app"."app_name" IS '应用名称';
+COMMENT ON COLUMN "dingtalk"."app"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."app"."update_time" IS '更新时间';
+COMMENT ON COLUMN "dingtalk"."app"."deleted" IS '是否删除,0否1是';
+COMMENT ON TABLE "dingtalk"."app" IS '应用';
+
+-- ----------------------------
+-- Table structure for app_robot
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."app_robot";
+CREATE TABLE "dingtalk"."app_robot" (
+  "app_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "robot_code" text COLLATE "pg_catalog"."default" NOT NULL
+)
+;
+COMMENT ON COLUMN "dingtalk"."app_robot"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."app_robot"."robot_code" IS '机器人编码';
+COMMENT ON TABLE "dingtalk"."app_robot" IS '应用机器人关联';
+
+-- ----------------------------
+-- Table structure for app_sftp
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."app_sftp";
+CREATE TABLE "dingtalk"."app_sftp" (
+  "sftp_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "app_id" text COLLATE "pg_catalog"."default",
+  "host" text COLLATE "pg_catalog"."default",
+  "port" int4,
+  "username" text COLLATE "pg_catalog"."default",
+  "password" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP
+)
+;
+COMMENT ON COLUMN "dingtalk"."app_sftp"."sftp_id" IS 'sftp id';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."host" IS '主机';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."port" IS '端口';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."username" IS '用户名';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."password" IS '密码';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."app_sftp"."update_time" IS '更新时间';
+COMMENT ON TABLE "dingtalk"."app_sftp" IS '应用sftp账号';
+
+-- ----------------------------
+-- Table structure for app_task
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."app_task";
+CREATE TABLE "dingtalk"."app_task" (
+  "task_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "task_name" text COLLATE "pg_catalog"."default",
+  "description" text COLLATE "pg_catalog"."default",
+  "status" int4,
+  "cron" text COLLATE "pg_catalog"."default",
+  "app_id" text COLLATE "pg_catalog"."default",
+  "robot_code" text COLLATE "pg_catalog"."default",
+  "conversation_type" int4,
+  "open_conversation_id" text COLLATE "pg_catalog"."default",
+  "phones" text COLLATE "pg_catalog"."default",
+  "user_ids" text COLLATE "pg_catalog"."default",
+  "sftp_id" text COLLATE "pg_catalog"."default",
+  "data_dir" text COLLATE "pg_catalog"."default",
+  "file_pattern" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP
+)
+;
+COMMENT ON COLUMN "dingtalk"."app_task"."task_id" IS '任务id';
+COMMENT ON COLUMN "dingtalk"."app_task"."task_name" IS '任务名称';
+COMMENT ON COLUMN "dingtalk"."app_task"."description" IS '任务描述';
+COMMENT ON COLUMN "dingtalk"."app_task"."status" IS '任务状态,0已停止1运行中2已删除';
+COMMENT ON COLUMN "dingtalk"."app_task"."cron" IS 'cron表达式';
+COMMENT ON COLUMN "dingtalk"."app_task"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."app_task"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."app_task"."conversation_type" IS '会话类型:1:单聊,2:群聊';
+COMMENT ON COLUMN "dingtalk"."app_task"."open_conversation_id" IS '群id';
+COMMENT ON COLUMN "dingtalk"."app_task"."phones" IS '接收机器人消息的用户的手机号列表';
+COMMENT ON COLUMN "dingtalk"."app_task"."user_ids" IS '接收机器人消息的用户的userId列表';
+COMMENT ON COLUMN "dingtalk"."app_task"."sftp_id" IS 'sftp id';
+COMMENT ON COLUMN "dingtalk"."app_task"."data_dir" IS '数据文件夹';
+COMMENT ON COLUMN "dingtalk"."app_task"."file_pattern" IS '文件名匹配正则表达式';
+COMMENT ON COLUMN "dingtalk"."app_task"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."app_task"."update_time" IS '更新时间';
+COMMENT ON TABLE "dingtalk"."app_task" IS '应用定时任务';
+
+-- ----------------------------
+-- Table structure for app_task_log
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."app_task_log";
+CREATE TABLE "dingtalk"."app_task_log" (
+  "id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "task_id" text COLLATE "pg_catalog"."default",
+  "task_name" text COLLATE "pg_catalog"."default",
+  "app_id" text COLLATE "pg_catalog"."default",
+  "app_name" text COLLATE "pg_catalog"."default",
+  "robot_code" text COLLATE "pg_catalog"."default",
+  "robot_name" text COLLATE "pg_catalog"."default",
+  "conversation_type" int4,
+  "open_conversation_id" text COLLATE "pg_catalog"."default",
+  "phones" text COLLATE "pg_catalog"."default",
+  "user_ids" text COLLATE "pg_catalog"."default",
+  "sftp_id" text COLLATE "pg_catalog"."default",
+  "host" text COLLATE "pg_catalog"."default",
+  "port" int4,
+  "data_dir" text COLLATE "pg_catalog"."default",
+  "file_pattern" text COLLATE "pg_catalog"."default",
+  "file_path" text COLLATE "pg_catalog"."default",
+  "status" int4,
+  "detail" text COLLATE "pg_catalog"."default"
+)
+;
+COMMENT ON COLUMN "dingtalk"."app_task_log"."id" IS '日志id';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."create_time" IS '执行时间';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."task_id" IS '任务id';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."task_name" IS '任务名称';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."app_name" IS '应用名称';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."robot_name" IS '机器人名称';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."conversation_type" IS '会话类型:1:单聊,2:群聊';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."open_conversation_id" IS '群id';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."phones" IS '接收机器人消息的用户的手机号列表';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."user_ids" IS '接收机器人消息的用户的userId列表';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."sftp_id" IS 'sftp id';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."host" IS 'sftp ip';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."port" IS 'sftp端口';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."data_dir" IS '数据文件夹';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."file_pattern" IS '文件名匹配正则表达式';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."file_path" IS '文件路径';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."status" IS '任务执行状态,0失败1成功';
+COMMENT ON COLUMN "dingtalk"."app_task_log"."detail" IS '详情';
+COMMENT ON TABLE "dingtalk"."app_task_log" IS '应用定时任务日志';
+
+-- ----------------------------
+-- Table structure for receive_message_log
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."receive_message_log";
+CREATE TABLE "dingtalk"."receive_message_log" (
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "robot_name" text COLLATE "pg_catalog"."default",
+  "robot_code" text COLLATE "pg_catalog"."default",
+  "sender_staff_id" text COLLATE "pg_catalog"."default",
+  "sender_nick" text COLLATE "pg_catalog"."default",
+  "content" text COLLATE "pg_catalog"."default",
+  "msg_type" text COLLATE "pg_catalog"."default",
+  "conversation_type" text COLLATE "pg_catalog"."default",
+  "conversation_title" text COLLATE "pg_catalog"."default",
+  "conversation_id" text COLLATE "pg_catalog"."default",
+  "at_users" text COLLATE "pg_catalog"."default",
+  "chatbot_corp_id" text COLLATE "pg_catalog"."default",
+  "chatbot_user_id" text COLLATE "pg_catalog"."default",
+  "msg_id" text COLLATE "pg_catalog"."default",
+  "is_admin" text COLLATE "pg_catalog"."default",
+  "session_webhook_expired_time" text COLLATE "pg_catalog"."default",
+  "create_at" text COLLATE "pg_catalog"."default",
+  "sender_corp_id" text COLLATE "pg_catalog"."default",
+  "sender_id" text COLLATE "pg_catalog"."default",
+  "is_in_at_list" text COLLATE "pg_catalog"."default",
+  "session_webhook" text COLLATE "pg_catalog"."default"
+)
+;
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."robot_name" IS '机器人名称';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."sender_staff_id" IS '发送者 userId';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."sender_nick" IS '发送者昵称';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."content" IS '内容';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."msg_type" IS '消息类型';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."conversation_type" IS '会话类型:1:单聊2:群聊';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."conversation_title" IS '群聊时才有的会话标题';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."conversation_id" IS '会话 ID';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."at_users" IS '被@人的信息:dingtalkId:加密的发送者 ID。staffId:当前企业内部群中员工 userId 值。';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."chatbot_corp_id" IS '机器人所在的企业 corpId';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."chatbot_user_id" IS '加密的机器人 ID';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."msg_id" IS '加密的消息 ID';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."is_admin" IS '是否为管理员';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."session_webhook_expired_time" IS '当前会话的 Webhook 地址过期时间';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."create_at" IS '消息的时间戳,单位毫秒';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."sender_corp_id" IS '企业内部群有的发送者当前群的企业 corpId';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."sender_id" IS '加密的发送者 ID';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."is_in_at_list" IS '是否在@列表中';
+COMMENT ON COLUMN "dingtalk"."receive_message_log"."session_webhook" IS '当前会话的 Webhook 地址';
+COMMENT ON TABLE "dingtalk"."receive_message_log" IS '钉钉机器人接收消息日志';
+
+-- ----------------------------
+-- Table structure for request_log
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."request_log";
+CREATE TABLE "dingtalk"."request_log" (
+  "trace_id" text COLLATE "pg_catalog"."default",
+  "request_time" timestamp(6),
+  "ip" text COLLATE "pg_catalog"."default",
+  "api" text COLLATE "pg_catalog"."default",
+  "request_parameters" text COLLATE "pg_catalog"."default",
+  "headers" text COLLATE "pg_catalog"."default",
+  "open" int4,
+  "username" text COLLATE "pg_catalog"."default",
+  "app_name" text COLLATE "pg_catalog"."default",
+  "app_id" text COLLATE "pg_catalog"."default",
+  "time_stamp" timestamp(6),
+  "token" text COLLATE "pg_catalog"."default"
+)
+;
+COMMENT ON COLUMN "dingtalk"."request_log"."trace_id" IS '请求序列号';
+COMMENT ON COLUMN "dingtalk"."request_log"."request_time" IS '请求时间';
+COMMENT ON COLUMN "dingtalk"."request_log"."ip" IS 'ip';
+COMMENT ON COLUMN "dingtalk"."request_log"."api" IS '接口url';
+COMMENT ON COLUMN "dingtalk"."request_log"."request_parameters" IS '请求参数';
+COMMENT ON COLUMN "dingtalk"."request_log"."headers" IS '请求头';
+COMMENT ON COLUMN "dingtalk"."request_log"."open" IS '是否开放接口,0否1是';
+COMMENT ON COLUMN "dingtalk"."request_log"."username" IS '管理后台用户名';
+COMMENT ON COLUMN "dingtalk"."request_log"."app_name" IS '应用名称';
+COMMENT ON COLUMN "dingtalk"."request_log"."app_id" IS '应用id';
+COMMENT ON COLUMN "dingtalk"."request_log"."time_stamp" IS '时间戳';
+COMMENT ON COLUMN "dingtalk"."request_log"."token" IS '访问令牌';
+COMMENT ON TABLE "dingtalk"."request_log" IS '访问日志';
+
+-- ----------------------------
+-- Table structure for robot
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."robot";
+CREATE TABLE "dingtalk"."robot" (
+  "robot_code" text COLLATE "pg_catalog"."default" NOT NULL,
+  "robot_secret" text COLLATE "pg_catalog"."default",
+  "robot_name" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "deleted" int4 DEFAULT 0
+)
+;
+COMMENT ON COLUMN "dingtalk"."robot"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."robot"."robot_secret" IS '机器人密钥';
+COMMENT ON COLUMN "dingtalk"."robot"."robot_name" IS '机器人名称';
+COMMENT ON COLUMN "dingtalk"."robot"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."robot"."update_time" IS '更新时间';
+COMMENT ON COLUMN "dingtalk"."robot"."deleted" IS '是否删除,0否1是';
+COMMENT ON TABLE "dingtalk"."robot" IS '钉钉机器人';
+
+-- ----------------------------
+-- Table structure for robot_command
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."robot_command";
+CREATE TABLE "dingtalk"."robot_command" (
+  "app_id" text COLLATE "pg_catalog"."default" NOT NULL,
+  "robot_code" text COLLATE "pg_catalog"."default" NOT NULL,
+  "command" text COLLATE "pg_catalog"."default" NOT NULL,
+  "url" text COLLATE "pg_catalog"."default",
+  "description" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP
+)
+;
+COMMENT ON COLUMN "dingtalk"."robot_command"."app_id" IS '应用编码';
+COMMENT ON COLUMN "dingtalk"."robot_command"."robot_code" IS '机器人编码';
+COMMENT ON COLUMN "dingtalk"."robot_command"."command" IS '指令';
+COMMENT ON COLUMN "dingtalk"."robot_command"."url" IS '应用方接口';
+COMMENT ON COLUMN "dingtalk"."robot_command"."description" IS '说明';
+COMMENT ON COLUMN "dingtalk"."robot_command"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."robot_command"."update_time" IS '更新时间';
+COMMENT ON TABLE "dingtalk"."robot_command" IS '机器人指令';
+
+-- ----------------------------
+-- Table structure for user
+-- ----------------------------
+DROP TABLE IF EXISTS "dingtalk"."user";
+CREATE TABLE "dingtalk"."user" (
+  "username" text COLLATE "pg_catalog"."default" NOT NULL,
+  "password" text COLLATE "pg_catalog"."default",
+  "create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "update_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
+  "enable" int4 DEFAULT 1
+)
+;
+COMMENT ON COLUMN "dingtalk"."user"."username" IS '用户名';
+COMMENT ON COLUMN "dingtalk"."user"."password" IS '密码';
+COMMENT ON COLUMN "dingtalk"."user"."create_time" IS '创建时间';
+COMMENT ON COLUMN "dingtalk"."user"."update_time" IS '更新时间';
+COMMENT ON COLUMN "dingtalk"."user"."enable" IS '是否启用,0否1是';
+COMMENT ON TABLE "dingtalk"."user" IS '用户';
+
+-- ----------------------------
+-- Primary Key structure for table app
+-- ----------------------------
+ALTER TABLE "dingtalk"."app" ADD CONSTRAINT "app_pk" PRIMARY KEY ("app_id");
+
+-- ----------------------------
+-- Primary Key structure for table app_robot
+-- ----------------------------
+ALTER TABLE "dingtalk"."app_robot" ADD CONSTRAINT "app_robot_pk" PRIMARY KEY ("app_id", "robot_code");
+
+-- ----------------------------
+-- Indexes structure for table app_sftp
+-- ----------------------------
+CREATE INDEX "app_sftp_app_id_idx" ON "dingtalk"."app_sftp" USING btree (
+  "app_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Primary Key structure for table app_sftp
+-- ----------------------------
+ALTER TABLE "dingtalk"."app_sftp" ADD CONSTRAINT "app_sftp_pk" PRIMARY KEY ("sftp_id");
+
+-- ----------------------------
+-- Indexes structure for table app_task
+-- ----------------------------
+CREATE INDEX "app_task_app_id_idx" ON "dingtalk"."app_task" USING btree (
+  "app_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Primary Key structure for table app_task
+-- ----------------------------
+ALTER TABLE "dingtalk"."app_task" ADD CONSTRAINT "app_task_pk" PRIMARY KEY ("task_id");
+
+-- ----------------------------
+-- Indexes structure for table app_task_log
+-- ----------------------------
+CREATE INDEX "app_task_log_create_time_idx" ON "dingtalk"."app_task_log" USING btree (
+  "create_time" "pg_catalog"."timestamp_ops" DESC NULLS FIRST,
+  "task_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Indexes structure for table receive_message_log
+-- ----------------------------
+CREATE INDEX "receive_message_log_create_time_idx" ON "dingtalk"."receive_message_log" USING btree (
+  "create_time" "pg_catalog"."timestamp_ops" DESC NULLS FIRST,
+  "robot_code" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST,
+  "sender_staff_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST,
+  "sender_nick" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Indexes structure for table request_log
+-- ----------------------------
+CREATE INDEX "request_log_request_time_idx" ON "dingtalk"."request_log" USING btree (
+  "request_time" "pg_catalog"."timestamp_ops" DESC NULLS FIRST,
+  "app_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST,
+  "app_name" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Primary Key structure for table robot
+-- ----------------------------
+ALTER TABLE "dingtalk"."robot" ADD CONSTRAINT "robot_pk" PRIMARY KEY ("robot_code");
+
+-- ----------------------------
+-- Primary Key structure for table robot_command
+-- ----------------------------
+ALTER TABLE "dingtalk"."robot_command" ADD CONSTRAINT "robot_command_pk" PRIMARY KEY ("app_id", "robot_code", "command");
+
+-- ----------------------------
+-- Primary Key structure for table user
+-- ----------------------------
+ALTER TABLE "dingtalk"."user" ADD CONSTRAINT "user_pk" PRIMARY KEY ("username");

+ 1625 - 0
doc/钉钉机器人接口.md

@@ -0,0 +1,1625 @@
+# 钉钉机器人接口
+
+
+**简介**:钉钉机器人接口
+
+
+**HOST**:http://localhost:10101
+
+
+**联系人**:
+
+
+**Version**:1.0
+
+
+**接口路径**:/v3/api-docs
+
+
+[TOC]
+
+# 认证鉴权
+
+接口安全通过鉴权进行保障,能力调用鉴权采用如下方式:
+
+1. 对调用方的身份鉴权。每个接入平台的应用都需要在平台进行注册,注册完成后平台分配给应用APP_ID和APP_SECRET;
+
+2. 平台提供TOKEN算法。应用的请求中必须在请求头加上APP_ID、TIMESTAMP、TRACE_ID和TOKEN参数,用于应用的身份验证。TOKEN算法如下:
+
+TOKEN原始内容为json字符串,使用AES/ECB/PKCS5Padding加密算法和APP_SECRET进行加密,最后将加密内容转成base64,TOKEN需包含以下参数:
+
+| 参数名称 | 参数说明 |
+| -------- | -------- |
+|APP_ID|应用编码|
+|TIMESTAMP|当前的系统时间戳,单位为毫秒,格式为“yyyy-MM-dd HH:mm:ss.SSS”;例如:"2018-05-10 15:05:58.174"|
+|TRACE_ID|序列号,根据上述时间戳和随机数生成,生成的长度为23的字符串,格式为 :yyyyMMddhhmmssSSS+6位随机数。例如:“20180510150558174549793”|
+
+TOKEN原始内容json字符串示例:
+
+```json
+{"APP_ID":"APP_ID","TIMESTAMP":"2018-05-10 15:05:58.174","TRACE_ID":"20180510150558174549793"}
+```
+
+加密TOKEN代码示例:
+
+```java
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 加密解密工具
+ */
+@Slf4j
+public class AesUtil {
+    /**
+     * 加密算法
+     */
+    private static final String ALGORITHM = "AES";
+    /**
+     * 加密算法填充方式
+     */
+    private static final String TRANSFORMATION = ALGORITHM + "/ECB/PKCS5Padding";
+
+    public static void main(String[] args) {
+        try {
+            // 应用编码
+            String appId = "";
+            // 应用密钥
+            String appSecret = "";
+            // 生成token
+            String token = generateToken(appId, appSecret);
+            System.out.println(token);
+        } catch (Exception e) {
+            log.error(e.toString(), e);
+        }
+    }
+
+    /**
+     * 加密
+     * @param text 内容
+     * @param key 密钥
+     */
+    public static String encrypt(String text, String key) throws NoSuchPaddingException, NoSuchAlgorithmException,
+            IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
+        // 获取Cipher实例并设置为加密模式
+        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(Base64.getDecoder().decode(key), ALGORITHM));
+        // 执行加密操作并返回Base64编码的字符串
+        return Base64.getEncoder().encodeToString(cipher.doFinal(text.getBytes()));
+    }
+
+    /**
+     * 解密
+     * @param text 加密内容
+     * @param key 密钥
+     */
+    public static String decrypt(String text, String key) throws NoSuchPaddingException, NoSuchAlgorithmException,
+            InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+        // 获取Cipher实例并设置为解密模式
+        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
+        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(Base64.getDecoder().decode(key), ALGORITHM));
+        // 解码Base64字符串并执行解密操作
+        return new String(cipher.doFinal(Base64.getDecoder().decode(text)));
+    }
+
+    /**
+     * 生成token
+     * @param appId 应用编码
+     * @param appSecret 应用密钥
+     */
+    public static String generateToken(String appId, String appSecret) throws JsonProcessingException,
+            NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException,
+            InvalidKeyException {
+        // 获取当前时间
+        LocalDateTime now = LocalDateTime.now();
+        // 格式化时间戳
+        String timestamp = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
+        // 生成追踪ID
+        String traceId = generateTraceId(now);
+        // 创建对象映射器用于处理JSON
+        ObjectMapper objectMapper = new ObjectMapper();
+        // 创建Map存储需要加密的信息
+        Map<String, String> m = new HashMap<>();
+        m.put("APP_ID", appId);
+        m.put("TIMESTAMP", timestamp);
+        m.put("TRACE_ID", traceId);
+        // 将Map转换为JSON字符串并进行加密
+        String s = objectMapper.writeValueAsString(m);
+        return encrypt(s, appSecret);
+    }
+
+    /**
+     * 生成traceId
+     * @param time 时间
+     */
+    public static String generateTraceId(LocalDateTime time) {
+        return time.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
+                + ThreadLocalRandom.current().nextInt(100000, 1000000);
+    }
+}
+```
+
+
+
+# 应用机器人操作命令接口规范
+
+用户向机器人发送操作命令,机器人收到操作命令,本系统将操作命令转发给相关应用接口,具体的后续操作由相关应用自行定义,应用接口需符合以下规范:
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "senderStaffId": "xxxx",
+  "conversationType": "1",
+  "conversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "parameter": "202401"
+}
+```
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|当前的系统时间戳,单位为毫秒,格式为“yyyy-MM-dd HH:mm:ss.SSS”;例如:"2018-05-10 15:05:58.174"|header|true|||
+|TRACE_ID|序列号,根据上述时间戳和随机数生成,生成的长度为23的字符串,格式为 :yyyyMMddhhmmssSSS+6位随机数。例如:“20180510150558174549793”|header|true|||
+|TOKEN|访问令牌|header|true|||
+|dto||body|true|||
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;senderStaffId|企业内部群中@该机器人的用户ID||true|string||
+|conversationType|会话类型:1:单聊,2:群聊||true|string||
+|conversationId|会话ID||true|string||
+|parameter| 参数                                                         ||true|string||
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| ------ | ---- | ------ |
+| 200    | OK   |        |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型    | schema |
+| -------- | -------- | ------- | ------ |
+| success  | 是否成功 | boolean |        |
+| message  | 提示信息 | string  |        |
+
+**响应示例**:
+
+```javascript
+{
+	"success": true,
+	"message": "成功",
+}
+```
+
+# 钉钉机器人接口
+
+## 上传媒体文件
+
+
+**接口地址**:`/api/open/upload`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`multipart/form-data`
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|file|文件,最大20MB|multipart/form-data|true|file||
+|robotCode|机器人的编码 base64|multipart/form-data|true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RUploadVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||UploadVo|UploadVo|
+|&emsp;&emsp;mediaId|媒体文件上传后获取的唯一标识|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"mediaId": "@#lAzPDgCwPn1mJiDOQoLpxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 企业机器人撤回内部群消息
+
+
+**接口地址**:`/api/open/recallGroupMessages`
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "processQueryKeys": []
+}
+```
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|recallGroupMessagesDto|RecallGroupMessagesDto|body|true|RecallGroupMessagesDto|RecallGroupMessagesDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+|&emsp;&emsp;processQueryKeys|消息唯一标识列表,每次最多传20个,在发送消息24小时内可以通过processQueryKey撤回消息,超过24小时则无法撤回消息||true|array|string|
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RRecallGroupMessagesVo|
+
+**响应头**:
+
+
+| 参数名称 | 参数说明 |
+| -------- | -------- |
+|APP_ID|应用编码|
+|TIMESTAMP|时间戳|
+|TRACE_ID|序列号|
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||RecallGroupMessagesVo|RecallGroupMessagesVo|
+|&emsp;&emsp;successResult|撤回成功的消息发送任务ID列表|array|string|
+|&emsp;&emsp;failedResult|撤回失败的消息发送任务ID列表及对应的失败原因|object||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"successResult": [],
+		"failedResult": {}
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人发送群聊文本类型消息
+
+
+**接口地址**:`/api/open/groupSendSampleText`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "content": "content",
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx=="
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|groupSendSampleTextDto|GroupSendSampleTextDto|body|true|GroupSendSampleTextDto|GroupSendSampleTextDto|
+|&emsp;&emsp;content|内容||true|string||
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人发送群聊Markdown类型消息
+
+
+**接口地址**:`/api/open/groupSendSampleMarkdown`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "title": "title",
+  "text": "text",
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx=="
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|groupSendSampleMarkdownDto|GroupSendSampleMarkdownDto|body|true|GroupSendSampleMarkdownDto|GroupSendSampleMarkdownDto|
+|&emsp;&emsp;title|标题||true|string||
+|&emsp;&emsp;text|内容||true|string||
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人发送群聊图片类型消息
+
+
+**接口地址**:`/api/open/groupSendSampleImageMsg`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+**接口描述**:
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "mediaId": "@#lAzPDgCwPn1mJiDOQoLpxxxx"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|groupSendSampleImageMsgDto|GroupSendSampleImageMsgDto|body|true|GroupSendSampleImageMsgDto|GroupSendSampleImageMsgDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+|&emsp;&emsp;mediaId|媒体文件上传后获取的唯一标识||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人发送群聊文件类型消息
+
+
+**接口地址**:`/api/open/groupSendSampleFile`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "mediaId": "@#lAzPDgCwPn1mJiDOQoLpxxxx",
+  "filename": "a.xlsx"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|groupSendSampleFileDto|GroupSendSampleFileDto|body|true|GroupSendSampleFileDto|GroupSendSampleFileDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+|&emsp;&emsp;mediaId|媒体文件上传后获取的唯一标识||true|string||
+|&emsp;&emsp;filename|文件名||true|string||
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+**响应示例**:
+
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 根据手机号获取用户的userId
+
+
+**接口地址**:`/api/open/getUserIdByMobile`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "mobile": "12345678901"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|getUserIdByMobileDto|GetUserIdByMobileDto|body|true|GetUserIdByMobileDto|GetUserIdByMobileDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;mobile|用户的手机号||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGetUserIdByMobileVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GetUserIdByMobileVo|GetUserIdByMobileVo|
+|&emsp;&emsp;userId|员工的userId|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"userId": "xxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人批量发送文本类型消息
+
+
+**接口地址**:`/api/open/batchSendOtoSampleText`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "content": "content",
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "phones": [],
+  "userIds": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|batchSendSampleTextDto|BatchSendSampleTextDto|body|true|BatchSendSampleTextDto|BatchSendSampleTextDto|
+|&emsp;&emsp;content|内容||true|string||
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||true|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||true|array|string|
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人批量发送Markdown类型消息
+
+
+**接口地址**:`/api/open/batchSendOtoSampleMarkdown`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "title": "title",
+  "text": "content",
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "phones": [],
+  "userIds": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|batchSendSampleMarkdownDto|BatchSendSampleMarkdownDto|body|true|BatchSendSampleMarkdownDto|BatchSendSampleMarkdownDto|
+|&emsp;&emsp;title|标题||true|string||
+|&emsp;&emsp;text|内容||true|string||
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||true|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||true|array|string|
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人批量发送图片类型消息
+
+
+**接口地址**:`/api/open/batchSendOtoSampleImageMsg`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "mediaId": "@#lAzPDgCwPn1mJiDOQoLpxxxx",
+  "phones": [],
+  "userIds": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|batchSendOtoSampleImageMsgDto|BatchSendOtoSampleImageMsgDto|body|true|BatchSendOtoSampleImageMsgDto|BatchSendOtoSampleImageMsgDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;mediaId|媒体文件上传后获取的唯一标识||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||false|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||false|array|string|
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人批量发送文件类型消息
+
+
+**接口地址**:`/api/open/batchSendOtoSampleFile`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "mediaId": "@#lAzPDgCwPn1mJiDOQoLpxxxx",
+  "phones": [],
+  "userIds": [],
+  "filename": "a.xlsx"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|batchSendOtoSampleFileDto|BatchSendOtoSampleFileDto|body|true|BatchSendOtoSampleFileDto|BatchSendOtoSampleFileDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;mediaId|媒体文件上传后获取的唯一标识||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||false|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||false|array|string|
+|&emsp;&emsp;filename|文件名||true|string||
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 批量撤回人与机器人会话中机器人消息
+
+
+**接口地址**:`/api/open/batchRecallOtoMessages`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "processQueryKeys": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|batchRecallOtoMessagesDto|BatchRecallOtoMessagesDto|body|true|BatchRecallOtoMessagesDto|BatchRecallOtoMessagesDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;processQueryKeys|消息唯一标识列表,每次最多传20个,在发送消息24小时内可以通过processQueryKey撤回消息,超过24小时则无法撤回消息||true|array|string|
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchRecallOtoMessagesVo|
+
+**响应头**:
+
+
+| 参数名称  | 参数说明 |
+| --------- | -------- |
+| APP_ID    | 应用编码 |
+| TIMESTAMP | 时间戳   |
+| TRACE_ID  | 序列号   |
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchRecallOtoMessagesVo|BatchRecallOtoMessagesVo|
+|&emsp;&emsp;successResult|撤回成功的消息发送任务ID列表|array|string|
+|&emsp;&emsp;failedResult|撤回失败的消息发送任务ID列表及对应的失败原因|object||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"successResult": [],
+		"failedResult": {}
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+## 机器人通过sftp下载图片并发送群聊图片类型消息
+
+
+**接口地址**:`/api/open/sftpGroupSendSampleImageMsg`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+**响应数据类型**:`application/json`
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "sftpId": "xxxx",
+  "filePath": "/data/a.png"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|sftpGroupSendSampleImageMsgDto|SftpGroupSendSampleImageMsgDto|body|true|SftpGroupSendSampleImageMsgDto|SftpGroupSendSampleImageMsgDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+|&emsp;&emsp;sftpId|sftp id||true|string||
+|&emsp;&emsp;filePath|图片绝对路径||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人通过sftp下载文件并发送群聊文件类型消息
+
+
+**接口地址**:`/api/open/sftpGroupSendSampleFile`
+
+
+**请求方式**:`POST`
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "openConversationId": "cid6KeBBLoveMJOGXoYKF5x7Eeixxxx==",
+  "sftpId": "xxxx",
+  "filePath": "/data/a.pdf"
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|sftpGroupSendSampleFileDto|SftpGroupSendSampleFileDto|body|true|SftpGroupSendSampleFileDto|SftpGroupSendSampleFileDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;openConversationId|会话ID||true|string||
+|&emsp;&emsp;sftpId|sftp id||true|string||
+|&emsp;&emsp;filePath|文件绝对路径||true|string||
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RGroupSendVo|
+
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||GroupSendVo|GroupSendVo|
+|&emsp;&emsp;processQueryKey|加密消息id,根据此id可查询消息已读状态和撤回消息|string||
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx"
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人通过sftp下载图片并批量发送图片类型消息
+
+
+**接口地址**:`/api/open/sftpBatchSendOtoSampleImageMsg`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "sftpId": "xxxx",
+  "filePath": "/data/a.png",
+  "phones": [],
+  "userIds": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|sftpBatchSendOtoSampleImageMsgDto|SftpBatchSendOtoSampleImageMsgDto|body|true|SftpBatchSendOtoSampleImageMsgDto|SftpBatchSendOtoSampleImageMsgDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;sftpId|sftp id||true|string||
+|&emsp;&emsp;filePath|图片绝对路径||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||false|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||false|array|string|
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+
+
+## 机器人通过sftp下载文件并批量发送文件类型消息
+
+
+**接口地址**:`/api/open/sftpBatchSendOtoSampleFile`
+
+
+**请求方式**:`POST`
+
+
+**请求数据类型**:`application/json`
+
+
+**响应数据类型**:`application/json`
+
+
+**接口描述**:
+
+
+**请求示例**:
+
+
+```javascript
+{
+  "robotCode": "dingue4kfzdxbynxxxxxx",
+  "sftpId": "xxxx",
+  "filePath": "/data/a.pdf",
+  "phones": [],
+  "userIds": []
+}
+```
+
+
+**请求参数**:
+
+
+| 参数名称 | 参数说明 | 请求类型    | 是否必须 | 数据类型 | schema |
+| -------- | -------- | ----- | -------- | -------- | ------ |
+|APP_ID|应用编码|header|true|||
+|TIMESTAMP|时间戳|header|true|||
+|TRACE_ID|序列号|header|true|||
+|TOKEN|访问令牌|header|true|||
+|sftpBatchSendOtoSampleFileDto|SftpBatchSendOtoSampleFileDto|body|true|SftpBatchSendOtoSampleFileDto|SftpBatchSendOtoSampleFileDto|
+|&emsp;&emsp;robotCode|机器人的编码||true|string||
+|&emsp;&emsp;sftpId|sftp id||true|string||
+|&emsp;&emsp;filePath|文件绝对路径||true|string||
+|&emsp;&emsp;phones|接收机器人消息的用户的手机号列表,每次最多传20个||false|array|string|
+|&emsp;&emsp;userIds|接收机器人消息的用户的userId列表,每次最多传20个||false|array|string|
+
+
+**响应状态**:
+
+
+| 状态码 | 说明 | schema |
+| -------- | -------- | ----- | 
+|200|OK|RBatchSendVo|
+
+
+**响应参数**:
+
+
+| 参数名称 | 参数说明 | 类型 | schema |
+| -------- | -------- | ----- |----- | 
+|success|是否成功|boolean||
+|code|错误码|integer(int32)|integer(int32)|
+|message|提示信息|string||
+|data||BatchSendVo|BatchSendVo|
+|&emsp;&emsp;processQueryKey|消息id,根据此id,可用于查询消息是否已读和撤回消息|string||
+|&emsp;&emsp;failPhones|失败的手机号列表和对应原因|object||
+|&emsp;&emsp;invalidStaffIdList|无效的用户userId列表|array|string|
+|&emsp;&emsp;flowControlledStaffIdList|被限流的userId列表|array|string|
+|traceId|请求跟踪id|string||
+
+
+**响应示例**:
+```javascript
+{
+	"success": true,
+	"code": 0,
+	"message": "成功",
+	"data": {
+		"processQueryKey": "jkasdfb8va9hndjksnxxxx",
+		"failPhones": {},
+		"invalidStaffIdList": [],
+		"flowControlledStaffIdList": []
+	},
+	"traceId": "20231231235959436337954"
+}
+```
+

+ 259 - 0
mvnw

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

+ 149 - 0
mvnw.cmd

@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

+ 203 - 0
pom.xml

@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.3.3</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.nokia</groupId>
+    <artifactId>dingtalk_api</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>dingtalk_api</name>
+    <description>dingtalk_api</description>
+    <url/>
+    <licenses>
+        <license/>
+    </licenses>
+    <developers>
+        <developer/>
+    </developers>
+    <scm>
+        <connection/>
+        <developerConnection/>
+        <tag/>
+        <url/>
+    </scm>
+    <properties>
+        <java.version>17</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>${project.build.sourceEncoding}</project.reporting.outputEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.aliyun/dingtalk -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dingtalk</artifactId>
+            <version>2.1.46</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.aliyun/alibaba-dingtalk-service-sdk -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
+            <version>2.0.0</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <version>4.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <version>3.5.7</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+            <version>3.5.7</version>
+            <scope>test</scope>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.32</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-freemarker -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-freemarker</artifactId>
+            <version>3.2.3</version>
+            <scope>test</scope>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>3.1.8</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.dingtalk.open/app-stream-client -->
+        <dependency>
+            <groupId>com.dingtalk.open</groupId>
+            <artifactId>app-stream-client</artifactId>
+            <version>1.3.7</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.springframework.integration/spring-integration-sftp -->
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-sftp</artifactId>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- https://mvnrepository.com/artifact/com.aliyun/tea -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>tea</artifactId>
+                <version>1.3.0</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>credentials-java</artifactId>
+                <version>0.3.3</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/com.aliyun/tea-util -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>tea-util</artifactId>
+                <version>0.2.23</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/org.jacoco/org.jacoco.agent -->
+            <dependency>
+                <groupId>org.jacoco</groupId>
+                <artifactId>org.jacoco.agent</artifactId>
+                <version>0.8.4</version>
+            </dependency>
+            <!-- https://mvnrepository.com/artifact/com.aliyun/alibabacloud-gateway-spi -->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>alibabacloud-gateway-spi</artifactId>
+                <version>0.0.2</version>
+            </dependency>
+            <dependency>
+                <groupId>org.checkerframework</groupId>
+                <artifactId>checker-qual</artifactId>
+                <version>3.42.0</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 2 - 0
readme.md

@@ -0,0 +1,2 @@
+# 钉钉消息推送后端
+

+ 13 - 0
src/main/java/com/nokia/dingtalk_api/DingtalkApiApplication.java

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

+ 15 - 0
src/main/java/com/nokia/dingtalk_api/annotation/RequiredHeader.java

@@ -0,0 +1,15 @@
+package com.nokia.dingtalk_api.annotation;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Parameter(name = "APP_ID", description = "应用编码", in = ParameterIn.HEADER, required = true)
+@Parameter(name = "TIMESTAMP", description = "时间戳", in = ParameterIn.HEADER, required = true)
+@Parameter(name = "TRACE_ID", description = "序列号", in = ParameterIn.HEADER, required = true)
+@Parameter(name = "TOKEN", description = "访问令牌", in = ParameterIn.HEADER, required = true)
+public @interface RequiredHeader {
+}

+ 24 - 0
src/main/java/com/nokia/dingtalk_api/annotation/ValidCron.java

@@ -0,0 +1,24 @@
+package com.nokia.dingtalk_api.annotation;
+
+import com.nokia.dingtalk_api.validator.CronValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 校验cron表达式
+ */
+@Constraint(validatedBy = CronValidator.class)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValidCron {
+    String message() default "cron表达式错误";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}

+ 24 - 0
src/main/java/com/nokia/dingtalk_api/annotation/ValidRegex.java

@@ -0,0 +1,24 @@
+package com.nokia.dingtalk_api.annotation;
+
+import com.nokia.dingtalk_api.validator.RegexValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 校验正则表达式
+ */
+@Constraint(validatedBy = RegexValidator.class)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValidRegex {
+    String message() default "正则表达式错误";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}

+ 58 - 0
src/main/java/com/nokia/dingtalk_api/auth/MyExceptionHandling.java

@@ -0,0 +1,58 @@
+package com.nokia.dingtalk_api.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nokia.dingtalk_api.common.R;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 为Spring Security提供了统一的JSON响应格式,当出现未认证或无权限访问时,向客户端返回具体的错误信息和HTTP状态码
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class MyExceptionHandling implements AuthenticationEntryPoint, AccessDeniedHandler {
+    private final ObjectMapper objectMapper;
+    /**
+     * 当用户尝试访问一个需要身份验证但尚未认证的资源时,此方法会被调用
+     */
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+        log.warn(authException.toString());
+        String s = objectMapper.writeValueAsString(R.error().message("认证失败").code(401));
+        response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getOutputStream().write(s.getBytes());
+        log.warn("响应内容: {}", s);
+        log.warn("响应状态: {}", response.getStatus());
+    }
+
+    /**
+     * 当用户已经通过身份验证但没有足够的权限访问请求的资源时,此方法会被调用
+     */
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
+        log.warn(accessDeniedException.toString());
+        String s = objectMapper.writeValueAsString(R.error().message("禁止访问").code(403));
+        response.setStatus(HttpStatus.FORBIDDEN.value());
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getOutputStream().write(s.getBytes());
+        log.warn("响应内容: {}", s);
+        log.warn("响应状态: {}", response.getStatus());
+    }
+}

+ 29 - 0
src/main/java/com/nokia/dingtalk_api/auth/UserDetailServiceImpl.java

@@ -0,0 +1,29 @@
+package com.nokia.dingtalk_api.auth;
+
+import com.nokia.dingtalk_api.dao.IUserService;
+import com.nokia.dingtalk_api.pojos.po.UserPo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class UserDetailServiceImpl implements UserDetailsService {
+
+    private final IUserService iUserService;
+
+    /**
+     * 加载用户信息
+     */
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        return loadUserPoByUsername(username);
+    }
+
+    public UserPo loadUserPoByUsername(String username) {
+        // 使用iUserService获取指定用户名的用户信息,如果找不到用户,则使用orElseThrow抛出UsernameNotFoundException异常。
+        return iUserService.getOptById(username).orElseThrow(() -> new UsernameNotFoundException("用户名不存在"));
+    }
+}

+ 41 - 0
src/main/java/com/nokia/dingtalk_api/auth/token/AuthenticationToken.java

@@ -0,0 +1,41 @@
+package com.nokia.dingtalk_api.auth.token;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serial;
+import java.util.Collection;
+
+public class AuthenticationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private final String principal;
+
+    private String credentials;
+
+    public AuthenticationToken(String principal, String credentials) {
+        super(null);
+        this.principal = principal;
+        this.credentials = credentials;
+        setAuthenticated(false);
+    }
+
+    public AuthenticationToken(String principal, String credentials,
+                               Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        this.credentials = credentials;
+        super.setAuthenticated(true);
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.credentials;
+    }
+}

+ 156 - 0
src/main/java/com/nokia/dingtalk_api/auth/token/TokenAuthenticationFilter.java

@@ -0,0 +1,156 @@
+package com.nokia.dingtalk_api.auth.token;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.config.AppConfig;
+import com.nokia.dingtalk_api.dao.IAppService;
+import com.nokia.dingtalk_api.pojos.po.AppPo;
+import com.nokia.dingtalk_api.util.AesUtil;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 自定义的token认证过滤器,用于处理基于token的认证请求。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TokenAuthenticationFilter extends OncePerRequestFilter {
+    public static final String PATTERN = "/api/open/**";
+    private static final RequestMatcher REQUEST_MATCHER = new AntPathRequestMatcher(PATTERN);
+    private final RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository;
+    private final ObjectMapper objectMapper;
+    private final IAppService iAppService;
+    private final AppConfig appConfig;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        if (!REQUEST_MATCHER.matches(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        String traceId = request.getHeader("TRACE_ID");
+        if (!StringUtils.hasText(traceId)) {
+            handleException(request, response, "TRACE_ID不能为空");
+            return;
+        }
+        MDC.put("traceId", traceId);
+        String token = request.getHeader("TOKEN");
+        String appId = request.getHeader("APP_ID");
+        String timestamp = request.getHeader("TIMESTAMP");
+        if (!StringUtils.hasText(token)) {
+            handleException(request, response, "TOKEN不能为空");
+            return;
+        }
+        if (!StringUtils.hasText(appId)) {
+            handleException(request, response, "APP_ID不能为空");
+            return;
+        }
+        if (!StringUtils.hasText(timestamp)) {
+            handleException(request, response, "TIMESTAMP不能为空");
+            return;
+        }
+        response.setHeader("APP_ID", appId);
+        response.setHeader("TIMESTAMP", timestamp);
+        response.setHeader("TRACE_ID", traceId);
+        // 根据appId查询应用信息
+        AppPo appPo = iAppService.getById(appId);
+        if (appPo == null) {
+            handleException(request, response, "APP_ID无效");
+            return;
+        }
+        String appSecret = appPo.getAppSecret();
+        try {
+            // 解密token
+            String decryptedStr = AesUtil.decrypt(token, appSecret);
+            Map<String, String> m = objectMapper.readValue(decryptedStr,
+                    TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class));
+            String APP_ID = m.get("APP_ID");
+            String TIMESTAMP = m.get("TIMESTAMP");
+            String TRACE_ID = m.get("TRACE_ID");
+            if (!appId.equals(APP_ID) || !timestamp.equals(TIMESTAMP) || !traceId.equals(TRACE_ID)) {
+                handleException(request, response, "TOKEN无效");
+                return;
+            }
+            LocalDateTime localDateTime = LocalDateTime.parse(TIMESTAMP,
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
+            //  检查token是否过期
+            if (Math.abs(Duration.between(localDateTime, LocalDateTime.now()).toMinutes()) > appConfig.getAppTokenExpire()) {
+                handleException(request, response, "TOKEN已过期");
+                return;
+            }
+        } catch (Exception e) {
+            log.error(e.toString(), e);
+            handleException(request, response, "TOKEN无效");
+            return;
+        }
+        // 创建认证成功的 token 对象
+        AuthenticationToken t = new AuthenticationToken(appId, null, null);
+        // 设置认证请求的详细信息
+        t.setDetails(appPo.getAppName());
+        SecurityContext context = SecurityContextHolder.createEmptyContext();
+        context.setAuthentication(t);
+        requestAttributeSecurityContextRepository.saveContext(context, request, response);
+        filterChain.doFilter(request, response);
+    }
+
+    private void handleException(HttpServletRequest request, HttpServletResponse response, String message) throws IOException {
+        String traceId = MDC.get("traceId");
+        if (!StringUtils.hasText(traceId)) {
+            traceId = AesUtil.generateTraceId();
+            MDC.put("traceId", traceId);
+        }
+        log.warn("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
+        // 请求头参数
+        Map<String, String> headers = new HashMap<>();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            String k	= headerNames.nextElement();
+            String v = request.getHeader(k);
+            headers.put(k, v);
+        }
+        log.warn("请求头参数: {}", objectMapper.writeValueAsString(headers));
+        // 查询参数
+        Map<String, String> parameters = new HashMap<>();
+        Enumeration<String> parameterNames = request.getParameterNames();
+        while (parameterNames.hasMoreElements()) {
+            String k	= parameterNames.nextElement();
+            String v = request.getParameter(k);
+            parameters.put(k, v);
+        }
+        log.warn("查询参数: {}", objectMapper.writeValueAsString(parameters));
+        String s = objectMapper.writeValueAsString(R.error().message(message).code(401));
+        response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getOutputStream().write(s.getBytes());
+        log.warn("响应内容: {}", s);
+        log.warn("响应状态: {}", response.getStatus());
+    }
+}

+ 100 - 0
src/main/java/com/nokia/dingtalk_api/common/R.java

@@ -0,0 +1,100 @@
+package com.nokia.dingtalk_api.common;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.slf4j.MDC;
+
+/**
+ * 返回值的统一包装
+ */
+@Data
+public class R<T> {
+    @Schema(description = "是否成功", example = "true")
+    private Boolean success;
+    @Schema(description = "错误码", example = "0")
+    private Integer code;
+    @Schema(description = "提示信息", example = "成功")
+    private String message;
+    @Schema(description = "数据")
+    private T data;
+    @Schema(description = "请求跟踪id", example = "20231231235959436337954")
+    private String traceId;
+
+    /**
+     * 私有化构造方法,不允许在外部实例化
+     */
+    private R() {
+    }
+
+    /**
+     * 成功的静态方法
+     *
+     * @return R实例
+     */
+    public static <T> R<T> ok() {
+        R<T> r = new R<>();
+        r.setSuccess(true);
+        r.setCode(0);
+        r.setMessage("成功");
+        r.setTraceId(traceId());
+        return r;
+    }
+
+    public static <T> R<T> ok(T data) {
+        R<T> r = new R<>();
+        r.setSuccess(true);
+        r.setCode(0);
+        r.setMessage("成功");
+        r.setData(data);
+        r.setTraceId(traceId());
+        return r;
+    }
+
+    /**
+     * 失败的静态方法
+     *
+     * @return R实例
+     */
+    public static <T> R<T> error() {
+        R<T> r = new R<>();
+        r.setSuccess(false);
+        r.setCode(1);
+        r.setMessage("失败");
+        r.setTraceId(traceId());
+        return r;
+    }
+
+    public static <T> R<T> error(String message) {
+        R<T> r = new R<>();
+        r.setSuccess(false);
+        r.setCode(1);
+        r.setMessage(message);
+        r.setTraceId(traceId());
+        return r;
+    }
+
+    public R<T> success(Boolean success) {
+        this.setSuccess(success);
+        return this;
+    }
+
+
+    public R<T> code(Integer code) {
+        this.setCode(code);
+        return this;
+    }
+
+    public R<T> data(T object) {
+        this.setData(object);
+        return this;
+    }
+
+    public R<T> message(String message) {
+        this.setMessage(message);
+        return this;
+    }
+
+    public static String traceId() {
+        return MDC.get("traceId");
+    }
+}

+ 7 - 0
src/main/java/com/nokia/dingtalk_api/common/exception/BizException.java

@@ -0,0 +1,7 @@
+package com.nokia.dingtalk_api.common.exception;
+
+public class BizException extends RuntimeException{
+    public BizException(String message) {
+        super(message);
+    }
+}

+ 27 - 0
src/main/java/com/nokia/dingtalk_api/common/exception/MyRuntimeException.java

@@ -0,0 +1,27 @@
+package com.nokia.dingtalk_api.common.exception;
+
+public class MyRuntimeException extends RuntimeException{
+    public MyRuntimeException() {
+    }
+
+    public MyRuntimeException(String message) {
+        super(message);
+    }
+
+    public MyRuntimeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public MyRuntimeException(Throwable cause) {
+        super(cause);
+    }
+
+    public MyRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+    
+    @Override
+    public synchronized Throwable fillInStackTrace() {
+        return this;
+    }
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/config/ApiDocConfig.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * api文档配置
+ *
+ */
+@Configuration
+public class ApiDocConfig {
+    @Bean
+    public OpenAPI openapi() {
+        return new OpenAPI().info(new Info().title("钉钉机器人接口").description("钉钉机器人接口").version("1.0"));
+    }
+}

+ 67 - 0
src/main/java/com/nokia/dingtalk_api/config/AppConfig.java

@@ -0,0 +1,67 @@
+package com.nokia.dingtalk_api.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Data
+@Configuration
+@ConfigurationProperties("app")
+public class AppConfig {
+    public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
+    /**
+     * 应用token过期分钟
+     */
+    private Long appTokenExpire;
+    /**
+     * 钉钉accessToken的过期时间,单位秒
+     */
+    private Long dingtalkAccessTokenExpire;
+    /**
+     * 加密密钥
+     */
+    private String secret;
+    /**
+     * 任务超时分钟
+     */
+    private Integer taskTimeout;
+
+    @Bean
+    public Cache<String, String> dingtalkAccessTokenCache() {
+        return Caffeine.newBuilder()
+                .expireAfterAccess(dingtalkAccessTokenExpire, TimeUnit.SECONDS)
+                .evictionListener((k, v, c) -> log.debug("dingtalkAccessTokenCache evictionListener: {}, {} -> {}", c, k, v))
+                .removalListener((k, v, c) -> log.debug("dingtalkAccessTokenCache removalListener: {}, {} -> {}", c, k, v))
+                .build();
+    }
+
+    @Bean
+    public ObjectMapper objectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setDateFormat(new SimpleDateFormat(DATE_TIME_FORMAT));
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        JavaTimeModule module = new JavaTimeModule();
+        module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
+        module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
+        module.addSerializer(Long.class, ToStringSerializer.instance);
+        mapper.registerModule(module);
+        return mapper;
+    }
+}

+ 23 - 0
src/main/java/com/nokia/dingtalk_api/config/DescNullsLastInterceptor.java

@@ -0,0 +1,23 @@
+package com.nokia.dingtalk_api.config;
+
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+public class DescNullsLastInterceptor implements InnerInterceptor {
+    @Override
+    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
+                            ResultHandler resultHandler, BoundSql boundSql) {
+        String sql = boundSql.getSql();
+        if (!StringUtils.containsIgnoreCase(sql, "desc")) {
+            return;
+        }
+        String sqlNew = sql.replaceAll("(?i)(\\bdesc\\b(?!\\s+nulls\\s+last\\b))(?=[^a-z]|$)", "$1 nulls last");
+        PluginUtils.mpBoundSql(boundSql).sql(sqlNew);
+    }
+}

+ 24 - 0
src/main/java/com/nokia/dingtalk_api/config/MybatisPlusConfig.java

@@ -0,0 +1,24 @@
+package com.nokia.dingtalk_api.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * mybatis +配置
+ *
+ */
+@Configuration
+public class MybatisPlusConfig {
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 添加分页拦截器
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
+        // 添加desc nulls last拦截器
+        interceptor.addInnerInterceptor(new DescNullsLastInterceptor());
+        return interceptor;
+    }
+}

+ 17 - 0
src/main/java/com/nokia/dingtalk_api/config/TaskConfig.java

@@ -0,0 +1,17 @@
+package com.nokia.dingtalk_api.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+public class TaskConfig {
+    @Bean
+    public ThreadPoolTaskScheduler taskScheduler() {
+        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+        scheduler.setPoolSize(100);
+        scheduler.setThreadNamePrefix("task-");
+        scheduler.initialize();
+        return scheduler;
+    }
+}

+ 244 - 0
src/main/java/com/nokia/dingtalk_api/config/security/SecurityBeanConfig.java

@@ -0,0 +1,244 @@
+package com.nokia.dingtalk_api.config.security;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nokia.dingtalk_api.auth.MyExceptionHandling;
+import com.nokia.dingtalk_api.auth.UserDetailServiceImpl;
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.po.RequestLogPo;
+import com.nokia.dingtalk_api.pojos.vo.cms.LoginVo;
+import com.nokia.dingtalk_api.service.RequestLogService;
+import com.nokia.dingtalk_api.util.AesUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AnonymousAuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.context.DelegatingSecurityContextRepository;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.security.web.session.HttpSessionEventPublisher;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class SecurityBeanConfig {
+    private static final String KEY = UUID.randomUUID().toString();
+    private final MyExceptionHandling myExceptionHandling;
+    private final ObjectMapper objectMapper;
+    private final UserDetailServiceImpl userDetailService;
+    private final RequestLogService requestLogService;
+
+    /**
+     * 将 SecurityContext 存储在 HTTP 会话中
+     */
+    @Bean
+    public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
+        return new HttpSessionSecurityContextRepository();
+    }
+
+    /**
+     * 将 SecurityContext 存储在当前请求的属性中
+     */
+    @Bean
+    public RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository() {
+        return new RequestAttributeSecurityContextRepository();
+    }
+
+    /**
+     * 配置密码编码
+     */
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        // 默认使用的密码编码算法标识符
+        String idForEncode = "bcrypt";
+        Map<String, PasswordEncoder> m = new HashMap<>();
+        m.put("bcrypt", new BCryptPasswordEncoder());
+        return new DelegatingPasswordEncoder(idForEncode, m);
+    }
+
+    /**
+     * 配置用户名密码认证
+     */
+    @Bean
+    public DaoAuthenticationProvider daoAuthenticationProvider() {
+        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+        provider.setPasswordEncoder(passwordEncoder());
+        provider.setUserDetailsService(userDetailService);
+        provider.setForcePrincipalAsString(true);
+        return provider;
+    }
+
+    /**
+     * 配置匿名认证
+     */
+    @Bean
+    public AnonymousAuthenticationProvider anonymousAuthenticationProvider() {
+        return new AnonymousAuthenticationProvider(KEY);
+    }
+
+    /**
+     * 配置认证方式
+     */
+    @Bean
+    public AuthenticationManager authenticationManager() {
+        List<AuthenticationProvider> l = new ArrayList<>();
+        l.add(anonymousAuthenticationProvider());
+        l.add(daoAuthenticationProvider());
+        return new ProviderManager(l);
+    }
+
+    /**
+     * 配置认证存储
+     */
+    @Bean
+    public DelegatingSecurityContextRepository delegatingSecurityContextRepository() {
+        List<SecurityContextRepository> l = new ArrayList<>();
+        l.add(httpSessionSecurityContextRepository());
+        l.add(requestAttributeSecurityContextRepository());
+        return new DelegatingSecurityContextRepository(l);
+    }
+
+    /**
+     * 配置session监听
+     */
+    @Bean
+    public HttpSessionEventPublisher httpSessionEventPublisher() {
+        return new HttpSessionEventPublisher();
+    }
+
+    /**
+     * 配置session策略
+     */
+    @Bean
+    public CompositeSessionAuthenticationStrategy compositeSessionStrategy() {
+        List<SessionAuthenticationStrategy> l = new ArrayList<>();
+        l.add(new RegisterSessionAuthenticationStrategy(sessionRegistry()));
+        l.add(new ChangeSessionIdAuthenticationStrategy());
+        return new CompositeSessionAuthenticationStrategy(l);
+    }
+
+    /**
+     * 跟踪和管理用户会话
+     */
+    @Bean
+    public SessionRegistry sessionRegistry() {
+        return new SessionRegistryImpl();
+    }
+
+    /**
+     * 处理用户成功登出后的逻辑
+     */
+    @Bean
+    public LogoutSuccessHandler logoutSuccessHandler() {
+        return (request, response, authentication) -> {
+            String traceId = getTraceId();
+            log.debug("{} logout", authentication.getName());
+            String s = objectMapper.writeValueAsString(R.ok().message("退出成功"));
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.getOutputStream().write(s.getBytes());
+            saveRequestLog(request, authentication, traceId);
+        };
+    }
+
+    /**
+     * 用于处理用户成功登录后的逻辑
+     */
+    @Bean
+    public AuthenticationSuccessHandler authenticationSuccessHandler() {
+        return (request, response, authentication) -> {
+            String traceId = getTraceId();
+            log.debug("{} login", authentication.getName());
+            LoginVo vo = new LoginVo();
+            vo.setName(authentication.getName());
+            String s = objectMapper.writeValueAsString(R.ok(vo).message("登录成功"));
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.getOutputStream().write(s.getBytes());
+            saveRequestLog(request, authentication, traceId);
+        };
+    }
+
+    /**
+     * 处理用户登录失败后的逻辑
+     */
+    @Bean
+    public AuthenticationFailureHandler authenticationFailureHandler() {
+        return myExceptionHandling::commence;
+    }
+
+    /**
+     * 保存请求日志
+     *
+     * @param request        请求
+     * @param authentication 认证信息
+     * @param traceId 请求跟踪id
+     */
+    private void saveRequestLog(HttpServletRequest request, Authentication authentication, String traceId)
+            throws JsonProcessingException {
+        LocalDateTime now = LocalDateTime.now();
+        Map<String, String> headers = new HashMap<>();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            String k	= headerNames.nextElement();
+            String v = request.getHeader(k);
+            headers.put(k, v);
+        }
+        String ip = Optional.ofNullable(request.getHeader("X-Real-IP"))
+                .or(() -> Optional.ofNullable(request.getHeader("X-Forwarded-For")))
+                .orElse(request.getRemoteAddr());
+        String principal = (String) authentication.getPrincipal();
+        RequestLogPo requestLogPo = new RequestLogPo();
+        requestLogPo.setRequestTime(now);
+        requestLogPo.setApi(request.getRequestURI());
+        requestLogPo.setHeaders(objectMapper.writeValueAsString(headers));
+        requestLogPo.setIp(ip);
+        requestLogPo.setApi(request.getRequestURI());
+        requestLogPo.setOpen(0);
+        requestLogPo.setUsername(principal);
+        requestLogPo.setTraceId(traceId);
+        requestLogService.offer(requestLogPo);
+    }
+
+    private static String getTraceId() {
+        String traceId = MDC.get("traceId");
+        if (!StringUtils.hasText(traceId)) {
+            traceId = AesUtil.generateTraceId();
+            MDC.put("traceId", traceId);
+        }
+        return traceId;
+    }
+}

+ 88 - 0
src/main/java/com/nokia/dingtalk_api/config/security/SecurityConfig.java

@@ -0,0 +1,88 @@
+package com.nokia.dingtalk_api.config.security;
+
+import com.nokia.dingtalk_api.auth.MyExceptionHandling;
+import com.nokia.dingtalk_api.auth.token.TokenAuthenticationFilter;
+import com.nokia.dingtalk_api.filter.SessionRefreshFilter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
+import org.springframework.security.web.context.DelegatingSecurityContextRepository;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
+
+@Slf4j
+@RequiredArgsConstructor
+@EnableMethodSecurity
+@EnableWebSecurity
+//@EnableWebSecurity(debug = true)
+@Configuration
+public class SecurityConfig {
+    private final AuthenticationManager authenticationManager;
+    private final MyExceptionHandling myExceptionHandling;
+    private final RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository;
+    private final DelegatingSecurityContextRepository delegatingSecurityContextRepository;
+    private final CompositeSessionAuthenticationStrategy compositeSessionStrategy;
+    private final LogoutSuccessHandler logoutSuccessHandler;
+    private final AuthenticationSuccessHandler authenticationSuccessHandler;
+    private final AuthenticationFailureHandler authenticationFailureHandler;
+    private final SessionRefreshFilter sessionRefreshFilter;
+    private final TokenAuthenticationFilter tokenAuthenticationFilter;
+
+    @Bean
+    @Order(1)
+    public SecurityFilterChain tokenSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
+        httpSecurity
+                .securityMatcher(TokenAuthenticationFilter.PATTERN)
+                .authorizeHttpRequests(t -> t.anyRequest().authenticated())
+                .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+                .exceptionHandling(t -> t.authenticationEntryPoint(myExceptionHandling)
+                        .accessDeniedHandler(myExceptionHandling))
+                .securityContext(t -> t.securityContextRepository(requestAttributeSecurityContextRepository))
+                .sessionManagement(AbstractHttpConfigurer::disable)
+                .csrf(AbstractHttpConfigurer::disable)
+                .formLogin(AbstractHttpConfigurer::disable)
+                .logout(AbstractHttpConfigurer::disable)
+                .rememberMe(AbstractHttpConfigurer::disable)
+                .anonymous(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable);
+        return httpSecurity.build();
+    }
+
+    @Bean
+    public SecurityFilterChain sessionSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
+        String loginUrl = "/api/cms/login";
+        httpSecurity
+                .authorizeHttpRequests(
+                        t -> t.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
+                                .requestMatchers("/error", "/api/public/**", "/v3/**", "/doc.html", "/swagger-ui/**", loginUrl)
+                                .permitAll()
+                                .anyRequest().authenticated())
+                .addFilterAfter(sessionRefreshFilter, AuthorizationFilter.class)
+                .exceptionHandling(t -> t.authenticationEntryPoint(myExceptionHandling)
+                        .accessDeniedHandler(myExceptionHandling))
+                .formLogin(t -> t.successHandler(authenticationSuccessHandler)
+                        .failureHandler(authenticationFailureHandler)
+                        .loginProcessingUrl(loginUrl))
+                .logout(t -> t.logoutSuccessHandler(logoutSuccessHandler).logoutUrl("/api/cms/logout"))
+                .securityContext(t -> t.securityContextRepository(delegatingSecurityContextRepository))
+                .sessionManagement(t -> t.sessionAuthenticationStrategy(compositeSessionStrategy))
+                .authenticationManager(authenticationManager)
+                .csrf(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable);
+        return httpSecurity.build();
+    }
+}

+ 102 - 0
src/main/java/com/nokia/dingtalk_api/config/web/ControllerExceptionHandler.java

@@ -0,0 +1,102 @@
+package com.nokia.dingtalk_api.config.web;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.common.exception.BizException;
+import jakarta.validation.ValidationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageConversionException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MissingRequestValueException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+
+/**
+ * 请求异常处理
+ */
+@Slf4j
+@RestControllerAdvice
+public class ControllerExceptionHandler
+{
+    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+    public ResponseEntity<Object> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(R.error().message(e.getMessage()));
+    }
+
+    @ExceptionHandler(MissingRequestValueException.class)
+    public ResponseEntity<Object> missingRequestValueExceptionHandler(MissingRequestValueException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.error().message(e.getMessage()));
+    }
+
+    @ExceptionHandler(HttpMessageConversionException.class)
+    public ResponseEntity<Object> httpMessageConversionExceptionHandler(HttpMessageConversionException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.error().message(e.getMessage()));
+    }
+
+    @ExceptionHandler(TypeMismatchException.class)
+    public ResponseEntity<Object> typeMismatchExceptionHandler(TypeMismatchException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.error().message(e.getMessage()));
+    }
+
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<Object> bindExceptionHandler(BindException e)
+    {
+        FieldError fieldError = e.getBindingResult().getFieldError();
+        String message = "";
+        if (fieldError != null)
+        {
+            message = fieldError.getDefaultMessage();
+        }
+        log.warn(message);
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.error().message(message));
+    }
+
+    @ExceptionHandler(ValidationException.class)
+    public ResponseEntity<Object> validationExceptionHandler(ValidationException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(R.error().message(e.getMessage()));
+    }
+
+    @ExceptionHandler(AuthenticationException.class)
+    public ResponseEntity<Object> authenticationExceptionHandler(AuthenticationException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.error().message("认证失败").code(401));
+    }
+
+    @ExceptionHandler(AccessDeniedException.class)
+    public ResponseEntity<Object> accessDeniedExceptionHandler(AccessDeniedException e)
+    {
+        log.warn(e.getMessage());
+        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error().message("禁止访问").code(403));
+    }
+
+    @ExceptionHandler(BizException.class)
+    public ResponseEntity<Object> bizExceptionHandler(BizException e)
+    {
+        return ResponseEntity.ok().body(R.error(e.getMessage()));
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<Object> exceptionHandler(Exception e)
+    {
+        log.error("╭( ′• o •′ )╭☞ 发生错误了 {}", e.toString(), e);
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(R.error());
+    }
+}

+ 29 - 0
src/main/java/com/nokia/dingtalk_api/config/web/MyWebMvcConfigurer.java

@@ -0,0 +1,29 @@
+package com.nokia.dingtalk_api.config.web;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class MyWebMvcConfigurer implements WebMvcConfigurer {
+    private final RequestLogHandlerInterceptor requestLogHandlerInterceptor;
+    private final ObjectMapper objectMapper;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 添加请求日志拦截
+        registry.addInterceptor(requestLogHandlerInterceptor)
+                .addPathPatterns("/**")
+                .excludePathPatterns("/webjars/**", "/v3/**", "/doc.html", "/favicon.ico");
+    }
+
+    @Bean
+    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
+        return new MappingJackson2HttpMessageConverter(objectMapper);
+    }
+}

+ 186 - 0
src/main/java/com/nokia/dingtalk_api/config/web/RequestLogHandlerInterceptor.java

@@ -0,0 +1,186 @@
+package com.nokia.dingtalk_api.config.web;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.nokia.dingtalk_api.pojos.po.RequestLogPo;
+import com.nokia.dingtalk_api.service.RequestLogService;
+import com.nokia.dingtalk_api.util.AesUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.StopWatch;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 请求日志拦截器
+ */
+@Slf4j
+@ControllerAdvice
+@RequiredArgsConstructor
+public class RequestLogHandlerInterceptor implements HandlerInterceptor, RequestBodyAdvice, ResponseBodyAdvice<Object> {
+    /**
+     * 计时器线程变量
+     */
+    private static final ThreadLocal<StopWatch> STOP_WATCH_THREAD_LOCAL = new ThreadLocal<>();
+    private static final ThreadLocal<RequestLogPo> REQUEST_LOG_PO_THREAD_LOCAL = new ThreadLocal<>();
+    private final ObjectMapper objectMapper;
+    private final RequestLogService requestLogService;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        StopWatch stopWatch = new StopWatch();
+        stopWatch.start();
+        // 计时器放入线程变量
+        STOP_WATCH_THREAD_LOCAL.set(stopWatch);
+        String traceId = MDC.get("traceId");
+        if (!StringUtils.hasText(traceId)) {
+            traceId = AesUtil.generateTraceId();
+            MDC.put("traceId", traceId);
+        }
+        LocalDateTime now = LocalDateTime.now();
+        log.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
+        // 请求头参数
+        Map<String, String> headers = new HashMap<>();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            String k	= headerNames.nextElement();
+            String v = request.getHeader(k);
+            headers.put(k, v);
+        }
+        log.info("请求头参数: {}", objectMapper.writeValueAsString(headers));
+        // 查询参数
+        Map<String, String> parameters = new HashMap<>();
+        Enumeration<String> parameterNames = request.getParameterNames();
+        while (parameterNames.hasMoreElements()) {
+            String k	= parameterNames.nextElement();
+            String v = request.getParameter(k);
+            parameters.put(k, v);
+        }
+        log.info("查询参数: {}", objectMapper.writeValueAsString(parameters));
+        String ip = Optional.ofNullable(request.getHeader("X-Real-IP"))
+                .or(() -> Optional.ofNullable(request.getHeader("X-Forwarded-For")))
+                .orElse(request.getRemoteAddr());
+        String token = request.getHeader("TOKEN");
+        String timestamp = request.getHeader("TIMESTAMP");
+        LocalDateTime timeStamp = StringUtils.hasText(timestamp)
+                ? LocalDateTime.parse(timestamp, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))
+                : null;
+        SecurityContext context = SecurityContextHolder.getContext();
+        Authentication authentication = context.getAuthentication();
+        String principal = (String) authentication.getPrincipal();
+        log.debug("principal: {}", authentication.getPrincipal());
+        Map<String, String> requestParameters = new HashMap<>();
+        requestParameters.put("request_parameters", objectMapper.writeValueAsString(parameters));
+        boolean open = StringUtils.startsWithIgnoreCase(request.getRequestURI(), "/api/open/");
+        RequestLogPo requestLogPo = new RequestLogPo();
+        REQUEST_LOG_PO_THREAD_LOCAL.set(requestLogPo);
+        requestLogPo.setRequestTime(now);
+        requestLogPo.setIp(ip);
+        requestLogPo.setApi(request.getRequestURI());
+        requestLogPo.setOpen(open ? 1 : 0);
+        requestLogPo.setRequestParameters(objectMapper.writeValueAsString(requestParameters));
+        requestLogPo.setHeaders(objectMapper.writeValueAsString(headers));
+        if (open) {
+            requestLogPo.setAppId(principal);
+            requestLogPo.setAppName(authentication.getDetails().toString());
+        } else {
+            requestLogPo.setUsername(principal);
+        }
+        requestLogPo.setTimeStamp(timeStamp);
+        requestLogPo.setToken(token);
+        requestLogPo.setTraceId(traceId);
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
+                                @Nullable Exception ex) throws Exception {
+        log.info("响应状态: {}", response.getStatus());
+        StopWatch stopWatch = STOP_WATCH_THREAD_LOCAL.get();
+        stopWatch.stop();
+        log.info("耗时: {} ms", stopWatch.getTotalTimeMillis());
+        STOP_WATCH_THREAD_LOCAL.remove();
+        RequestLogPo requestLogPo = REQUEST_LOG_PO_THREAD_LOCAL.get();
+        if (requestLogPo != null) {
+            requestLogService.offer(requestLogPo);
+        }
+        REQUEST_LOG_PO_THREAD_LOCAL.remove();
+    }
+
+    @Override
+    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+        return true;
+    }
+
+    @Override
+    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
+        return inputMessage;
+    }
+
+    @Override
+    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+        try {
+            if (STOP_WATCH_THREAD_LOCAL.get() != null) {
+                String bodyStr = objectMapper.writeValueAsString(body);
+                log.info("请求体参数: {}", bodyStr);
+                RequestLogPo requestLogPo = REQUEST_LOG_PO_THREAD_LOCAL.get();
+                if (requestLogPo != null) {
+                    Map<String, String> requestParameters = objectMapper.readValue(requestLogPo.getRequestParameters(),
+                            TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class));
+                    requestParameters.put("request_body", bodyStr);
+                    requestLogPo.setRequestParameters(objectMapper.writeValueAsString(requestParameters));
+                }
+            }
+        } catch (Exception e) {
+            log.error(e.toString(), e);
+        }
+        return body;
+    }
+
+    @Override
+    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+        return body;
+    }
+
+    @Override
+    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+        return true;
+    }
+
+    @Override
+    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
+        try {
+            if (STOP_WATCH_THREAD_LOCAL.get() != null) {
+                log.info("响应内容: {}", org.apache.commons.lang3.StringUtils.substring(objectMapper.writeValueAsString(body), 0, 500));
+            }
+        } catch (Exception e) {
+            log.error(e.toString(), e);
+        }
+        return body;
+    }
+}

+ 25 - 0
src/main/java/com/nokia/dingtalk_api/config/web/ValidatorConfig.java

@@ -0,0 +1,25 @@
+package com.nokia.dingtalk_api.config.web;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+/**
+ * 校验配置
+ */
+@Configuration
+public class ValidatorConfig
+{
+    @Bean
+    public Validator validator()
+    {
+        try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()
+                .failFast(true).buildValidatorFactory()) {
+            return validatorFactory.getValidator();
+        }
+    }
+}

+ 59 - 0
src/main/java/com/nokia/dingtalk_api/controller/AppController.java

@@ -0,0 +1,59 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddAppDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ResetAppSecretDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.UpdateAppDto;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppVo;
+import com.nokia.dingtalk_api.service.AppService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "app", description = "应用管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/app")
+public class AppController {
+    private final AppService appService;
+
+    @Operation(summary = "查询应用列表")
+    @PostMapping("/listApp")
+    public R<PageVo<ListAppVo>> listApp(@Valid @RequestBody ListAppDto dto) {
+        return appService.listApp(dto);
+    }
+
+    @Operation(summary = "添加应用")
+    @PostMapping("/addApp")
+    public R<Object> addApp(@Valid @RequestBody AddAppDto dto) {
+        return appService.addApp(dto);
+    }
+
+    @Operation(summary = "更新应用")
+    @PostMapping("/updateApp")
+    public R<Object> updateApp(@Valid @RequestBody UpdateAppDto dto) {
+        return appService.updateApp(dto);
+    }
+
+    @Operation(summary = "重置应用密钥")
+    @PostMapping("/resetAppSecret")
+    public R<Object> resetAppSecret(@Valid @RequestBody ResetAppSecretDto dto) {
+        return appService.resetAppSecret(dto);
+    }
+
+    @Operation(summary = "删除应用")
+    @PostMapping("/deleteApp")
+    public R<Object> deleteApp(@Valid @RequestBody DeleteAppDto dto) {
+        return appService.deleteApp(dto);
+    }
+}

+ 52 - 0
src/main/java/com/nokia/dingtalk_api/controller/AppRobotCommandController.java

@@ -0,0 +1,52 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddAppRobotCommandDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppRobotCommandDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppRobotCommandDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.UpdateAppRobotCommandDto;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppRobotCommandVo;
+import com.nokia.dingtalk_api.service.AppRobotCommandService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "appRobotCommand", description = "应用机器人命令管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/appRobotCommand")
+public class AppRobotCommandController {
+    private final AppRobotCommandService appRobotCommandService;
+
+    @Operation(summary = "查询应用机器人命令列表")
+    @PostMapping("/listAppRobotCommand")
+    public R<PageVo<ListAppRobotCommandVo>> listAppRobotCommand(@Valid @RequestBody ListAppRobotCommandDto dto) {
+        return appRobotCommandService.listAppRobotCommand(dto);
+    }
+
+    @Operation(summary = "添加应用机器人命令")
+    @PostMapping("/addAppRobotCommand")
+    public R<Object> addAppRobotCommand(@Valid @RequestBody AddAppRobotCommandDto dto) {
+        return appRobotCommandService.addAppRobotCommand(dto);
+    }
+
+    @Operation(summary = "更新应用机器人命令")
+    @PostMapping("/updateAppRobotCommand")
+    public R<Object> updateAppRobotCommand(@Valid @RequestBody UpdateAppRobotCommandDto dto) {
+        return appRobotCommandService.updateAppRobotCommand(dto);
+    }
+
+    @Operation(summary = "删除应用机器人命令")
+    @PostMapping("/deleteAppRobotCommand")
+    public R<Object> deleteAppRobotCommand(@Valid @RequestBody DeleteAppRobotCommandDto dto) {
+        return appRobotCommandService.deleteAppRobotCommand(dto);
+    }
+}

+ 63 - 0
src/main/java/com/nokia/dingtalk_api/controller/AppRobotController.java

@@ -0,0 +1,63 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddAppRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.GetAppRobotOptionsDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAvailableRobotDto;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAppRobotOptionsVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppRobotVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAvailableRobotVo;
+import com.nokia.dingtalk_api.service.AppRobotService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@Tag(name = "appRobot", description = "应用机器人管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/appRobot")
+public class AppRobotController {
+    private final AppRobotService appRobotService;
+
+    @Operation(summary = "查询应用机器人列表")
+    @PostMapping("/listAppRobot")
+    public R<PageVo<ListAppRobotVo>> listAppRobot(@Valid @RequestBody ListAppRobotDto dto) {
+        return appRobotService.listAppRobot(dto);
+    }
+
+    @Operation(summary = "查询可添加的机器人列表")
+    @PostMapping("/listAvailableRobot")
+    public R<PageVo<ListAvailableRobotVo>> listAvailableRobot(@Valid @RequestBody ListAvailableRobotDto dto) {
+        return appRobotService.listAvailableRobot(dto);
+    }
+
+    @Operation(summary = "添加应用机器人")
+    @PostMapping("/addAppRobot")
+    public R<Object> addAppRobot(@Valid @RequestBody AddAppRobotDto dto) {
+        return appRobotService.addAppRobot(dto);
+    }
+
+    @Operation(summary = "删除应用机器人")
+    @PostMapping("/deleteAppRobot")
+    public R<Object> deleteAppRobot(@Valid @RequestBody DeleteAppRobotDto dto) {
+        return appRobotService.deleteAppRobot(dto);
+    }
+
+    @Operation(summary = "查询应用机器人选项")
+    @PostMapping("/getAppRobotOptions")
+    public R<List<GetAppRobotOptionsVo>> getAppRobotOptions(@Valid @RequestBody GetAppRobotOptionsDto dto) {
+        return appRobotService.getAppRobotOptions(dto);
+    }
+}

+ 62 - 0
src/main/java/com/nokia/dingtalk_api/controller/AppSftpController.java

@@ -0,0 +1,62 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddAppSftpDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppSftpDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.GetAppSftpOptionsDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppSftpDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.UpdateAppSftpDto;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.GetAppSftpOptionsVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppSftpVo;
+import com.nokia.dingtalk_api.service.AppSftpService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@Tag(name = "appSftp", description = "应用sftp管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/appSftp")
+public class AppSftpController {
+    private final AppSftpService appSftpService;
+
+    @Operation(summary = "查询应用sftp列表")
+    @PostMapping("/listAppSftp")
+    public R<PageVo<ListAppSftpVo>> listAppSftp(@Valid @RequestBody ListAppSftpDto dto) {
+        return appSftpService.listAppSftp(dto);
+    }
+
+    @Operation(summary = "添加应用sftp")
+    @PostMapping("/addAppSftp")
+    public R<Object> addAppSftp(@Valid @RequestBody AddAppSftpDto dto) {
+        return appSftpService.addAppSftp(dto);
+    }
+
+    @Operation(summary = "更新应用sftp")
+    @PostMapping("/updateAppSftp")
+    public R<Object> updateAppSftp(@Valid @RequestBody UpdateAppSftpDto dto) {
+        return appSftpService.updateAppSftp(dto);
+    }
+
+    @Operation(summary = "删除应用sftp")
+    @PostMapping("/deleteAppSftp")
+    public R<Object> deleteAppSftp(@Valid @RequestBody DeleteAppSftpDto dto) {
+        return appSftpService.deleteAppSftp(dto);
+    }
+
+    @Operation(summary = "查询应用sftp选项")
+    @PostMapping("/getAppSftpOptions")
+    public R<List<GetAppSftpOptionsVo>> getAppSftpOptions(@Valid @RequestBody GetAppSftpOptionsDto dto) {
+        return appSftpService.getAppSftpOptions(dto);
+    }
+}

+ 81 - 0
src/main/java/com/nokia/dingtalk_api/controller/AppTaskController.java

@@ -0,0 +1,81 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.EnableAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppTaskLogDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.RunAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.StopAppTaskDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.UpdateAppTaskDto;
+import com.nokia.dingtalk_api.pojos.po.AppTaskLogPo;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppTaskVo;
+import com.nokia.dingtalk_api.service.AppTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "appTask", description = "应用任务管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/appTask")
+public class AppTaskController {
+    private final AppTaskService appTaskService;
+
+    @Operation(summary = "查询应用任务列表")
+    @PostMapping("/listAppTask")
+    public R<PageVo<ListAppTaskVo>> listAppTask(@Valid @RequestBody ListAppTaskDto dto) {
+        return appTaskService.listAppTask(dto);
+    }
+
+    @Operation(summary = "添加应用任务")
+    @PostMapping("/addAppTask")
+    public R<Object> addAppTask(@Valid @RequestBody AddAppTaskDto dto) {
+        return appTaskService.addAppTask(dto);
+    }
+
+    @Operation(summary = "更新应用任务")
+    @PostMapping("/updateAppTask")
+    public R<Object> updateAppTask(@Valid @RequestBody UpdateAppTaskDto dto) {
+        return appTaskService.updateAppTask(dto);
+    }
+
+    @Operation(summary = "启用应用任务")
+    @PostMapping("/enableAppTask")
+    public R<Object> enableAppTask(@Valid @RequestBody EnableAppTaskDto dto) {
+        return appTaskService.enableAppTask(dto);
+    }
+
+    @Operation(summary = "停止应用任务")
+    @PostMapping("/stopAppTask")
+    public R<Object> stopAppTask(@Valid @RequestBody StopAppTaskDto dto) {
+        return appTaskService.stopAppTask(dto);
+    }
+
+    @Operation(summary = "删除应用任务")
+    @PostMapping("/deleteAppTask")
+    public R<Object> deleteAppTask(@Valid @RequestBody DeleteAppTaskDto dto) {
+        return appTaskService.deleteAppTask(dto);
+    }
+
+    @Operation(summary = "执行应用任务")
+    @PostMapping("/runAppTask")
+    public R<Object> runAppTask(@Valid @RequestBody RunAppTaskDto dto) {
+        return appTaskService.runAppTask(dto);
+    }
+
+    @Operation(summary = "查询应用任务日志列表")
+    @PostMapping("/listAppTaskLog")
+    public R<PageVo<AppTaskLogPo>> listAppTaskLog(@Valid @RequestBody ListAppTaskLogDto dto) {
+        return appTaskService.listAppTaskLog(dto);
+    }
+}

+ 41 - 0
src/main/java/com/nokia/dingtalk_api/controller/AuthController.java

@@ -0,0 +1,41 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.LoginDto;
+import com.nokia.dingtalk_api.pojos.vo.cms.LoginVo;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "auth", description = "认证")
+@Slf4j
+@RestController
+public class AuthController {
+    @Operation(summary = "登录")
+    @PostMapping("/api/cms/login")
+    public R<LoginVo> login(LoginDto dto) {
+        log.debug("dto: {}", dto);
+        return R.ok();
+    }
+
+    @Operation(summary = "登出")
+    @PostMapping("/api/cms/logout")
+    public R<Object> logout() {
+        return R.ok();
+    }
+
+    @Operation(summary = "获取当前用户信息")
+    @PostMapping("/api/cms/queryCurrentUser")
+    public R<LoginVo> queryCurrentUser() {
+        SecurityContext context = SecurityContextHolder.getContext();
+        Authentication authentication = context.getAuthentication();
+        LoginVo vo = new LoginVo();
+        vo.setName(authentication.getName());
+        return R.ok(vo);
+    }
+}

+ 149 - 0
src/main/java/com/nokia/dingtalk_api/controller/OpenController.java

@@ -0,0 +1,149 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.annotation.RequiredHeader;
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.open.BatchRecallOtoMessagesDto;
+import com.nokia.dingtalk_api.pojos.dto.open.BatchSendOtoSampleFileDto;
+import com.nokia.dingtalk_api.pojos.dto.open.BatchSendOtoSampleImageMsgDto;
+import com.nokia.dingtalk_api.pojos.dto.open.BatchSendSampleMarkdownDto;
+import com.nokia.dingtalk_api.pojos.dto.open.BatchSendSampleTextDto;
+import com.nokia.dingtalk_api.pojos.dto.open.GetUserIdByMobileDto;
+import com.nokia.dingtalk_api.pojos.dto.open.GroupSendSampleFileDto;
+import com.nokia.dingtalk_api.pojos.dto.open.GroupSendSampleImageMsgDto;
+import com.nokia.dingtalk_api.pojos.dto.open.GroupSendSampleMarkdownDto;
+import com.nokia.dingtalk_api.pojos.dto.open.GroupSendSampleTextDto;
+import com.nokia.dingtalk_api.pojos.dto.open.RecallGroupMessagesDto;
+import com.nokia.dingtalk_api.pojos.dto.open.SftpBatchSendOtoSampleFileDto;
+import com.nokia.dingtalk_api.pojos.dto.open.SftpBatchSendOtoSampleImageMsgDto;
+import com.nokia.dingtalk_api.pojos.dto.open.SftpGroupSendSampleFileDto;
+import com.nokia.dingtalk_api.pojos.dto.open.SftpGroupSendSampleImageMsgDto;
+import com.nokia.dingtalk_api.pojos.vo.open.BatchRecallOtoMessagesVo;
+import com.nokia.dingtalk_api.pojos.vo.open.BatchSendVo;
+import com.nokia.dingtalk_api.pojos.vo.open.GetUserIdByMobileVo;
+import com.nokia.dingtalk_api.pojos.vo.open.GroupSendVo;
+import com.nokia.dingtalk_api.pojos.vo.open.RecallGroupMessagesVo;
+import com.nokia.dingtalk_api.pojos.vo.open.UploadVo;
+import com.nokia.dingtalk_api.service.OpenService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@Tag(name = "open", description = "钉钉机器人接口")
+@RequiredHeader
+@Validated
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/open")
+public class OpenController {
+    private final OpenService openService;
+
+    @Operation(summary = "机器人发送群聊文本类型消息")
+    @PostMapping(value = "/groupSendSampleText")
+    public R<GroupSendVo> groupSendSampleText(@Valid @RequestBody GroupSendSampleTextDto dto) {
+        return openService.groupSendSampleText(dto);
+    }
+
+    @Operation(summary = "机器人发送群聊Markdown类型消息")
+    @PostMapping("/groupSendSampleMarkdown")
+    public R<GroupSendVo> groupSendSampleMarkdown(@Valid @RequestBody GroupSendSampleMarkdownDto dto) {
+        return openService.groupSendSampleMarkdown(dto);
+    }
+
+    @Operation(summary = "机器人发送群聊图片类型消息")
+    @PostMapping("/groupSendSampleImageMsg")
+    public R<GroupSendVo> groupSendSampleImageMsg(@Valid @RequestBody GroupSendSampleImageMsgDto dto) {
+        return openService.groupSendSampleImageMsg(dto);
+    }
+
+    @Operation(summary = "机器人发送群聊文件类型消息")
+    @PostMapping("/groupSendSampleFile")
+    public R<GroupSendVo> groupSendSampleFile(@Valid @RequestBody GroupSendSampleFileDto dto) {
+        return openService.groupSendSampleFile(dto);
+    }
+
+    @Operation(summary = "机器人批量发送文本类型消息")
+    @PostMapping("/batchSendOtoSampleText")
+    public R<BatchSendVo> batchSendOtoSampleText(@Valid @RequestBody BatchSendSampleTextDto dto) {
+        return openService.batchSendOtoSampleText(dto);
+    }
+
+    @Operation(summary = "机器人批量发送Markdown类型消息")
+    @PostMapping("/batchSendOtoSampleMarkdown")
+    public R<BatchSendVo> batchSendOtoSampleMarkdown(@Valid @RequestBody BatchSendSampleMarkdownDto dto) {
+        return openService.batchSendOtoSampleMarkdown(dto);
+    }
+
+    @Operation(summary = "机器人批量发送图片类型消息")
+    @PostMapping("/batchSendOtoSampleImageMsg")
+    public R<BatchSendVo> batchSendOtoSampleImageMsg(@Valid @RequestBody BatchSendOtoSampleImageMsgDto dto) {
+        return openService.batchSendOtoSampleImageMsg(dto);
+    }
+
+    @Operation(summary = "机器人批量发送文件类型消息")
+    @PostMapping("/batchSendOtoSampleFile")
+    public R<BatchSendVo> batchSendOtoSampleFile(@Valid @RequestBody BatchSendOtoSampleFileDto dto) {
+        return openService.batchSendOtoSampleFile(dto);
+    }
+
+    @Operation(summary = "批量撤回人与机器人会话中机器人消息")
+    @PostMapping("/batchRecallOtoMessages")
+    public R<BatchRecallOtoMessagesVo> batchRecallOtoMessages(@Valid @RequestBody BatchRecallOtoMessagesDto dto) {
+        return openService.batchRecallOtoMessages(dto);
+    }
+
+    @Operation(summary = "企业机器人撤回内部群消息")
+    @PostMapping("/recallGroupMessages")
+    public R<RecallGroupMessagesVo> recallGroupMessages(@Valid @RequestBody RecallGroupMessagesDto dto) {
+        return openService.recallGroupMessages(dto);
+    }
+
+    @Operation(summary = "根据手机号获取用户的userId")
+    @PostMapping("/getUserIdByMobile")
+    public R<GetUserIdByMobileVo> getUserIdByMobile(@Valid @RequestBody GetUserIdByMobileDto dto) {
+        return openService.getUserIdByMobile(dto);
+    }
+
+    @Operation(summary = "上传媒体文件")
+    @PostMapping("/upload")
+    public R<UploadVo> upload(
+            @NotBlank(message = "robotCode不能为空") @Parameter(description = "机器人的编码 base64", required = true) String robotCode,
+            @Parameter(description = "文件,最大20MB", required = true) @RequestPart("file") MultipartFile file) {
+        return openService.upload(robotCode, file);
+    }
+
+    @Operation(summary = "机器人通过sftp下载图片并发送群聊图片类型消息")
+    @PostMapping("/sftpGroupSendSampleImageMsg")
+    public R<GroupSendVo> sftpGroupSendSampleImageMsg(@Valid @RequestBody SftpGroupSendSampleImageMsgDto dto) {
+        return openService.sftpGroupSendSampleImageMsg(dto);
+    }
+
+    @Operation(summary = "机器人通过sftp下载文件并发送群聊文件类型消息")
+    @PostMapping("/sftpGroupSendSampleFile")
+    public R<GroupSendVo> sftpGroupSendSampleFile(@Valid @RequestBody SftpGroupSendSampleFileDto dto) {
+        return openService.sftpGroupSendSampleFile(dto);
+    }
+
+    @Operation(summary = "机器人通过sftp下载图片并批量发送图片类型消息")
+    @PostMapping("/sftpBatchSendOtoSampleImageMsg")
+    public R<BatchSendVo> sftpBatchSendOtoSampleImageMsg(@Valid @RequestBody SftpBatchSendOtoSampleImageMsgDto dto) {
+        return openService.sftpBatchSendOtoSampleImageMsg(dto);
+    }
+
+    @Operation(summary = "机器人通过sftp下载文件并批量发送文件类型消息")
+    @PostMapping("/sftpBatchSendOtoSampleFile")
+    public R<BatchSendVo> sftpBatchSendOtoSampleFile(@Valid @RequestBody SftpBatchSendOtoSampleFileDto dto) {
+        return openService.sftpBatchSendOtoSampleFile(dto);
+    }
+}

+ 52 - 0
src/main/java/com/nokia/dingtalk_api/controller/RobotController.java

@@ -0,0 +1,52 @@
+package com.nokia.dingtalk_api.controller;
+
+import com.nokia.dingtalk_api.common.R;
+import com.nokia.dingtalk_api.pojos.dto.cms.AddRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.DeleteRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.UpdateRobotDto;
+import com.nokia.dingtalk_api.pojos.vo.PageVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListRobotVo;
+import com.nokia.dingtalk_api.service.RobotService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "robot", description = "机器人管理")
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/robot")
+public class RobotController {
+    private final RobotService robotService;
+
+    @Operation(summary = "查询机器人列表")
+    @PostMapping("/listRobot")
+    public R<PageVo<ListRobotVo>> listRobot(@Valid @RequestBody ListRobotDto dto) {
+        return robotService.listRobot(dto);
+    }
+
+    @Operation(summary = "添加机器人")
+    @PostMapping("/addRobot")
+    public R<Object> addRobot(@Valid @RequestBody AddRobotDto dto) {
+        return robotService.addRobot(dto);
+    }
+
+    @Operation(summary = "更新机器人")
+    @PostMapping("/updateRobot")
+    public R<Object> updateRobot(@Valid @RequestBody UpdateRobotDto dto) {
+        return robotService.updateRobot(dto);
+    }
+
+    @Operation(summary = "删除机器人")
+    @PostMapping("/deleteRobot")
+    public R<Object> deleteRobot(@Valid @RequestBody DeleteRobotDto dto) {
+        return robotService.deleteRobot(dto);
+    }
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAppRobotService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.AppRobotPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 应用机器人关联 服务类
+ * </p>
+ *
+ */
+public interface IAppRobotService extends IService<AppRobotPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAppService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.AppPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 应用 服务类
+ * </p>
+ *
+ */
+public interface IAppService extends IService<AppPo> {
+
+}

+ 13 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAppSftpService.java

@@ -0,0 +1,13 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.nokia.dingtalk_api.pojos.po.AppSftpPo;
+
+/**
+ * <p>
+ * 应用sftp账号 服务类
+ * </p>
+ *
+ */
+public interface IAppSftpService extends IService<AppSftpPo> {
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAppTaskLogService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.AppTaskLogPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 应用定时任务日志 服务类
+ * </p>
+ *
+ */
+public interface IAppTaskLogService extends IService<AppTaskLogPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IAppTaskService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.AppTaskPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 应用定时任务 服务类
+ * </p>
+ *
+ */
+public interface IAppTaskService extends IService<AppTaskPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IReceiveMessageLogService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.ReceiveMessageLogPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 钉钉机器人接收消息日志 服务类
+ * </p>
+ *
+ */
+public interface IReceiveMessageLogService extends IService<ReceiveMessageLogPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IRequestLogService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.RequestLogPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 访问日志 服务类
+ * </p>
+ *
+ */
+public interface IRequestLogService extends IService<RequestLogPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IRobotCommandService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.nokia.dingtalk_api.pojos.po.RobotCommandPo;
+
+/**
+ * <p>
+ * 机器人指令 服务类
+ * </p>
+ *
+ */
+public interface IRobotCommandService extends IService<RobotCommandPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IRobotService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 钉钉机器人 服务类
+ * </p>
+ *
+ */
+public interface IRobotService extends IService<RobotPo> {
+
+}

+ 14 - 0
src/main/java/com/nokia/dingtalk_api/dao/IUserService.java

@@ -0,0 +1,14 @@
+package com.nokia.dingtalk_api.dao;
+
+import com.nokia.dingtalk_api.pojos.po.UserPo;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 用户 服务类
+ * </p>
+ *
+ */
+public interface IUserService extends IService<UserPo> {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AppRobotServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.AppRobotPo;
+import com.nokia.dingtalk_api.mapper.AppRobotMapper;
+import com.nokia.dingtalk_api.dao.IAppRobotService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 应用机器人关联 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AppRobotServiceImpl extends ServiceImpl<AppRobotMapper, AppRobotPo> implements IAppRobotService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AppServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.AppPo;
+import com.nokia.dingtalk_api.mapper.AppMapper;
+import com.nokia.dingtalk_api.dao.IAppService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 应用 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AppServiceImpl extends ServiceImpl<AppMapper, AppPo> implements IAppService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AppSftpServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.nokia.dingtalk_api.dao.IAppSftpService;
+import com.nokia.dingtalk_api.mapper.AppSftpMapper;
+import com.nokia.dingtalk_api.pojos.po.AppSftpPo;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 应用sftp账号 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AppSftpServiceImpl extends ServiceImpl<AppSftpMapper, AppSftpPo> implements IAppSftpService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AppTaskLogServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.AppTaskLogPo;
+import com.nokia.dingtalk_api.mapper.AppTaskLogMapper;
+import com.nokia.dingtalk_api.dao.IAppTaskLogService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 应用定时任务日志 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AppTaskLogServiceImpl extends ServiceImpl<AppTaskLogMapper, AppTaskLogPo> implements IAppTaskLogService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/AppTaskServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.AppTaskPo;
+import com.nokia.dingtalk_api.mapper.AppTaskMapper;
+import com.nokia.dingtalk_api.dao.IAppTaskService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 应用定时任务 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class AppTaskServiceImpl extends ServiceImpl<AppTaskMapper, AppTaskPo> implements IAppTaskService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/ReceiveMessageLogServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.ReceiveMessageLogPo;
+import com.nokia.dingtalk_api.mapper.ReceiveMessageLogMapper;
+import com.nokia.dingtalk_api.dao.IReceiveMessageLogService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 钉钉机器人接收消息日志 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class ReceiveMessageLogServiceImpl extends ServiceImpl<ReceiveMessageLogMapper, ReceiveMessageLogPo> implements IReceiveMessageLogService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/RequestLogServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.RequestLogPo;
+import com.nokia.dingtalk_api.mapper.RequestLogMapper;
+import com.nokia.dingtalk_api.dao.IRequestLogService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 访问日志 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class RequestLogServiceImpl extends ServiceImpl<RequestLogMapper, RequestLogPo> implements IRequestLogService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/RobotCommandServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.nokia.dingtalk_api.dao.IRobotCommandService;
+import com.nokia.dingtalk_api.mapper.RobotCommandMapper;
+import com.nokia.dingtalk_api.pojos.po.RobotCommandPo;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 机器人指令 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class RobotCommandServiceImpl extends ServiceImpl<RobotCommandMapper, RobotCommandPo> implements IRobotCommandService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/RobotServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import com.nokia.dingtalk_api.mapper.RobotMapper;
+import com.nokia.dingtalk_api.dao.IRobotService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 钉钉机器人 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class RobotServiceImpl extends ServiceImpl<RobotMapper, RobotPo> implements IRobotService {
+
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/dao/impl/UserServiceImpl.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.dao.impl;
+
+import com.nokia.dingtalk_api.pojos.po.UserPo;
+import com.nokia.dingtalk_api.mapper.UserMapper;
+import com.nokia.dingtalk_api.dao.IUserService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 用户 服务实现类
+ * </p>
+ *
+ */
+@Service
+public class UserServiceImpl extends ServiceImpl<UserMapper, UserPo> implements IUserService {
+
+}

+ 68 - 0
src/main/java/com/nokia/dingtalk_api/filter/SessionRefreshFilter.java

@@ -0,0 +1,68 @@
+package com.nokia.dingtalk_api.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Slf4j
+@Component
+public class SessionRefreshFilter extends OncePerRequestFilter {
+    /**
+     * Cookie的名称
+     */
+    static final String COOKIE_NAME = "JSESSIONID";
+    /**
+     * 通过@Value注解注入Spring配置中的session超时时间
+     */
+    @Value("${spring.session.timeout}")
+    private Integer sessionTimeout;
+    /**
+     * 通过@Value注解注入Spring配置中的cookie最大存活时间
+     */
+    @Value("${server.servlet.session.cookie.max-age}")
+    private Integer cookieMaxAge;
+    @Value("${server.servlet.session.cookie.http-only}")
+    private Boolean httpOnly;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        // 从SecurityContextHolder中获取当前的Authentication对象
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        // 检查Authentication对象是否存在且已认证
+        if (authentication != null && authentication.isAuthenticated()) {
+            // 尝试从HttpServletRequest中获取HttpSession,如果不存在则返回null
+            HttpSession session = request.getSession(false);
+            // 确保session不为null
+            if (session != null) {
+                // 设置session的超时时间
+                session.setMaxInactiveInterval(sessionTimeout);
+                // 创建一个新的Cookie,名称为COOKIE_NAME,值为session的ID
+                Cookie cookie = new Cookie(COOKIE_NAME, session.getId());
+                // 设置Cookie的最大存活时间
+                cookie.setMaxAge(cookieMaxAge);
+                // 设置Cookie的路径,这里设置为根路径
+                cookie.setPath("/");
+                // 设置Cookie为HttpOnly,增加安全性,防止JavaScript读取
+                cookie.setHttpOnly(httpOnly);
+                // 设置Cookie为Secure,确保Cookie只通过HTTPS协议传输
+//                cookie.setSecure(true);
+                // 将更新后的Cookie添加到HTTP响应中
+                response.addCookie(cookie);
+                log.debug(">>>>>>>>>update session: {}", session.getId());
+            }
+        }
+        // 调用过滤器链中的下一个过滤器
+        filterChain.doFilter(request, response);
+    }
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AppMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.AppPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 应用 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AppMapper extends BaseMapper<AppPo> {
+
+}

+ 117 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AppRobotMapper.java

@@ -0,0 +1,117 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppRobotDto;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAvailableRobotDto;
+import com.nokia.dingtalk_api.pojos.po.AppPo;
+import com.nokia.dingtalk_api.pojos.po.AppRobotPo;
+import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAppRobotVo;
+import com.nokia.dingtalk_api.pojos.vo.cms.ListAvailableRobotVo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 应用机器人关联 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AppRobotMapper extends BaseMapper<AppRobotPo> {
+    /**
+     * 查询应用机器人列表
+     */
+    @Select("""
+<script>
+with t101 as (
+select
+    b.robot_code,
+    b.robot_name,
+    b.robot_secret
+from
+    dingtalk.app_robot a
+join dingtalk.robot b on
+    a.robot_code = b.robot_code
+where
+    a.app_id = #{dto.appId}
+    and b.deleted = 0
+<if test="dto.robotName != null and dto.robotName != ''">
+  and b.robot_name like '%' || #{dto.robotName} || '%'
+</if>
+<if test="dto.robotCode != null and dto.robotCode != ''">
+  and b.robot_code = #{dto.robotCode}
+</if>
+)
+select * from t101
+</script>
+""")
+    List<ListAppRobotVo> listAppRobot(Page<ListAppRobotVo> page, @Param("dto") ListAppRobotDto dto);
+
+    /**
+     * 查询应用可添加的机器人
+     */
+    @Select("""
+<script>
+select
+    *
+from
+    dingtalk.robot a
+where
+    a.deleted = 0
+    and not exists (
+    select
+        1
+    from
+        dingtalk.app_robot b
+    where
+        a.robot_code = b.robot_code
+        and b.app_id = #{dto.appId})
+<if test="dto.robotName != null and dto.robotName != ''">
+  and robot_name like '%' || #{dto.robotName} || '%'
+</if>
+<if test="dto.robotCode != null and dto.robotCode != ''">
+  and robot_code = #{dto.robotCode}
+</if>
+</script>
+""")
+    List<ListAvailableRobotVo> listAvailableRobot(Page<ListAvailableRobotVo> page, ListAvailableRobotDto dto);
+
+    /**
+     * 根据appId和robotCode查询机器人
+     */
+    @Select("""
+select
+    b.*
+from
+    dingtalk.app_robot a
+join dingtalk.robot b on
+    a.robot_code = b.robot_code
+where
+    b.deleted = 0
+    and a.app_id = #{appId}
+    and a.robot_code = #{robotCode}
+""")
+    RobotPo getRobotByAppIdAndRobotCode(@Param("appId") String appId, @Param("robotCode") String robotCode);
+
+    /**
+     * 查询机器人关联的应用列表
+     */
+    @Select("""
+select
+    b.*
+from
+    dingtalk.app_robot a
+join dingtalk.app b on
+    a.app_id = b.app_id
+where
+    b.deleted = 0
+    and a.robot_code = #{robotCode}
+order by b.app_name
+""")
+    List<AppPo> listRobotApp(@Param("robotCode") String robotCode);
+}

+ 15 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AppSftpMapper.java

@@ -0,0 +1,15 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.nokia.dingtalk_api.pojos.po.AppSftpPo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 应用sftp账号 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AppSftpMapper extends BaseMapper<AppSftpPo> {
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AppTaskLogMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.AppTaskLogPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 应用定时任务日志 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AppTaskLogMapper extends BaseMapper<AppTaskLogPo> {
+
+}

+ 159 - 0
src/main/java/com/nokia/dingtalk_api/mapper/AppTaskMapper.java

@@ -0,0 +1,159 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.nokia.dingtalk_api.pojos.bo.AppTaskBo;
+import com.nokia.dingtalk_api.pojos.dto.cms.ListAppTaskDto;
+import com.nokia.dingtalk_api.pojos.po.AppTaskPo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 应用定时任务 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface AppTaskMapper extends BaseMapper<AppTaskPo> {
+
+    /**
+     * 查询运行中的应用任务
+     * @param status 任务状态
+     */
+    @Select("""
+select
+    a.*,
+    b.app_name,
+    c.robot_name,
+    d.host,
+    d.port,
+    d.username,
+    d."password"
+from
+    dingtalk.app_task a
+join dingtalk.app b on
+    a.app_id = b.app_id
+join dingtalk.robot c on
+    a.robot_code = c.robot_code
+join dingtalk.app_sftp d on
+    a.sftp_id = d.sftp_id
+    and a.app_id = d.app_id
+join dingtalk.app_robot e on
+    a.app_id = e.app_id
+    and a.robot_code = e.robot_code
+where
+    b.deleted = 0
+    and c.deleted = 0
+    and a.status = #{status}
+order by a.update_time desc
+""")
+    List<AppTaskBo> getRunningAppTasks(@Param("status") Integer status);
+
+    /**
+     * 查询应用任务
+     */
+    @Select("""
+<script>
+with t101 as (
+select
+    a.*,
+    b.app_name,
+    c.robot_name,
+    d.host,
+    d.port,
+    d.username,
+    d."password"
+from
+    dingtalk.app_task a
+join dingtalk.app b on
+    a.app_id = b.app_id
+join dingtalk.robot c on
+    a.robot_code = c.robot_code
+join dingtalk.app_sftp d on
+    a.sftp_id = d.sftp_id
+    and a.app_id = d.app_id
+join dingtalk.app_robot e on
+    a.app_id = e.app_id
+    and a.robot_code = e.robot_code
+where
+    b.deleted = 0
+    and c.deleted = 0
+    and a.status <![CDATA[ < ]]> 2
+<if test="dto.taskId != null and dto.taskId != ''">
+  and a.task_id = #{dto.taskId}
+</if>
+<if test="dto.taskName != null and dto.taskName != ''">
+  and a.task_name like '%' || #{dto.taskName} || '%'
+</if>
+<if test="dto.status != null">
+  and a.status = #{dto.status}
+</if>
+<if test="dto.appId != null and dto.appId != ''">
+  and a.app_id = #{dto.appId}
+</if>
+<if test="dto.robotCode != null and dto.robotCode != ''">
+  and a.robot_code = #{dto.robotCode}
+</if>
+<if test="dto.robotName != null and dto.robotName != ''">
+  and c.robot_name like '%' || #{dto.robotName} || '%'
+</if>
+<if test="dto.conversationType != null">
+  and a.conversation_type = #{dto.conversationType}
+</if>
+<if test="dto.openConversationId != null and dto.openConversationId != ''">
+  and a.open_conversation_id = #{dto.openConversationId}
+</if>
+<if test="dto.phones != null and dto.phones != ''">
+  and a.phones like '%' || #{dto.phones} || '%'
+</if>
+<if test="dto.userIds != null and dto.userIds != ''">
+  and a.user_ids like '%' || #{dto.userIds} || '%'
+</if>
+<if test="dto.sftpId != null and dto.sftpId != ''">
+  and a.sftp_id = #{dto.sftpId}
+</if>
+<if test="dto.host != null and dto.host != ''">
+  and d.host like '%' || #{dto.host} || '%'
+</if>
+)
+select * from t101
+</script>
+""")
+    List<AppTaskBo> listAppTask(Page<AppTaskBo> page, @Param("dto") ListAppTaskDto dto);
+
+    /**
+     * 根据id查询应用任务
+     * @param taskId 任务id
+     */
+    @Select("""
+select
+    a.*,
+    b.app_name,
+    c.robot_name,
+    d.host,
+    d.port,
+    d.username,
+    d."password"
+from
+    dingtalk.app_task a
+join dingtalk.app b on
+    a.app_id = b.app_id
+join dingtalk.robot c on
+    a.robot_code = c.robot_code
+join dingtalk.app_sftp d on
+    a.sftp_id = d.sftp_id
+    and a.app_id = d.app_id
+join dingtalk.app_robot e on
+    a.app_id = e.app_id
+    and a.robot_code = e.robot_code
+where
+    b.deleted = 0
+    and c.deleted = 0
+    and a.task_id = #{taskId}
+""")
+    AppTaskBo getAppTaskByTaskId(@Param("taskId") String taskId);
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/ReceiveMessageLogMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.ReceiveMessageLogPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 钉钉机器人接收消息日志 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface ReceiveMessageLogMapper extends BaseMapper<ReceiveMessageLogPo> {
+
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/RequestLogMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.RequestLogPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 访问日志 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface RequestLogMapper extends BaseMapper<RequestLogPo> {
+
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/RobotCommandMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.nokia.dingtalk_api.pojos.po.RobotCommandPo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 机器人指令 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface RobotCommandMapper extends BaseMapper<RobotCommandPo> {
+
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/RobotMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.RobotPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 钉钉机器人 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface RobotMapper extends BaseMapper<RobotPo> {
+
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/mapper/UserMapper.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.mapper;
+
+import com.nokia.dingtalk_api.pojos.po.UserPo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * <p>
+ * 用户 Mapper 接口
+ * </p>
+ *
+ */
+@Mapper
+public interface UserMapper extends BaseMapper<UserPo> {
+
+}

+ 120 - 0
src/main/java/com/nokia/dingtalk_api/pojos/bo/AppTaskBo.java

@@ -0,0 +1,120 @@
+package com.nokia.dingtalk_api.pojos.bo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class AppTaskBo {
+    /**
+     * 任务id
+     */
+    private String taskId;
+
+    /**
+     * 任务名称
+     */
+    private String taskName;
+
+    /**
+     * 任务描述
+     */
+    private String description;
+
+    /**
+     * 任务状态,0已停止1运行中2已删除
+     */
+    private Integer status;
+
+    /**
+     * cron表达式
+     */
+    private String cron;
+
+    /**
+     * 应用编码
+     */
+    private String appId;
+
+    /**
+     * 机器人编码
+     */
+    private String robotCode;
+
+    /**
+     * 会话类型:1:单聊,2:群聊
+     */
+    private Integer conversationType;
+
+    /**
+     * 群id
+     */
+    private String openConversationId;
+
+    /**
+     * 接收机器人消息的用户的手机号列表
+     */
+    private String phones;
+
+    /**
+     * 接收机器人消息的用户的userId列表
+     */
+    private String userIds;
+
+    /**
+     * sftp id
+     */
+    private String sftpId;
+
+    /**
+     * 数据文件夹
+     */
+    private String dataDir;
+
+    /**
+     * 文件名匹配正则表达式
+     */
+    private String filePattern;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+    /**
+     * 应用名称
+     */
+    private String appName;
+    /**
+     * 机器人密钥
+     */
+    private String robotSecret;
+
+    /**
+     * 机器人名称
+     */
+    private String robotName;
+    /**
+     * 主机
+     */
+    private String host;
+
+    /**
+     * 端口
+     */
+    private Integer port;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 密码
+     */
+    private String password;
+}

+ 22 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/PageDto.java

@@ -0,0 +1,22 @@
+package com.nokia.dingtalk_api.pojos.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Range;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PageDto {
+    @Schema(description = "页码>=1")
+    @NotNull(message = "页码不能为空")
+    @Range(min = 1, message = "页码错误")
+    private Integer current;
+    @Schema(description = "每页个数,[1,100]")
+    @NotNull(message = "每页个数不能为空")
+    @Range(min = 1, max = 100, message = "每页个数错误")
+    private Integer pageSize;
+}

+ 15 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppDto.java

@@ -0,0 +1,15 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class AddAppDto {
+    @Schema(description = "应用名称")
+    @NotBlank
+    private String appName;
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

+ 23 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppRobotCommandDto.java

@@ -0,0 +1,23 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class AddAppRobotCommandDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+    @Schema(description = "命令")
+    @NotBlank
+    private String command;
+    @Schema(description = "应用方接口")
+    @NotBlank
+    private String url;
+    @Schema(description = "说明")
+    private String description;
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppRobotDto.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.Set;
+
+@Data
+public class AddAppRobotDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    @NotEmpty
+    private Set<String> robotCodes;
+}

+ 28 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppSftpDto.java

@@ -0,0 +1,28 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class AddAppSftpDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "sftp id")
+    @NotBlank
+    private String sftpId;
+    @Schema(description = "主机")
+    @NotBlank
+    private String host;
+    @Schema(description = "端口")
+    @NotNull
+    private Integer port;
+    @Schema(description = "用户名")
+    @NotBlank
+    private String username;
+    @Schema(description = "密码")
+    @NotBlank
+    private String password;
+}

+ 50 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddAppTaskDto.java

@@ -0,0 +1,50 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.annotation.ValidCron;
+import com.nokia.dingtalk_api.annotation.ValidRegex;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class AddAppTaskDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "任务名称")
+    @NotBlank
+    private String taskName;
+    @Schema(description = "任务描述")
+    private String description;
+    @Schema(description = "cron表达式")
+    @NotBlank
+    @ValidCron
+    private String cron;
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+    @Schema(description = "会话类型:1:单聊,2:群聊")
+    @Min(value = 1)
+    @Max(value = 2)
+    @NotNull
+    private Integer conversationType;
+    @Schema(description = "群id")
+    private String openConversationId;
+    @Schema(description = "接收机器人消息的用户的手机号列表")
+    private String phones;
+    @Schema(description = "接收机器人消息的用户的userId列表")
+    private String userIds;
+    @Schema(description = "sftp id")
+    @NotBlank
+    private String sftpId;
+    @Schema(description = "数据文件夹")
+    @NotBlank
+    private String dataDir;
+    @Schema(description = "文件名匹配正则表达式")
+    @ValidRegex
+    @NotBlank
+    private String filePattern;
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/AddRobotDto.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class AddRobotDto {
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+    @Schema(description = "机器人密钥")
+    @NotBlank
+    private String robotSecret;
+    @Schema(description = "机器人名称")
+    @NotBlank
+    private String robotName;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DeleteAppDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppRobotCommandDto.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DeleteAppRobotCommandDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+    @Schema(description = "命令")
+    @NotBlank
+    private String command;
+}

+ 18 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppRobotDto.java

@@ -0,0 +1,18 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.Set;
+
+@Data
+public class DeleteAppRobotDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    @NotEmpty
+    private Set<String> robotCodes;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppSftpDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DeleteAppSftpDto {
+    @Schema(description = "sftp id")
+    @NotBlank
+    private String sftpId;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteAppTaskDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DeleteAppTaskDto {
+    @Schema(description = "任务id")
+    @NotBlank
+    private String taskId;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/DeleteRobotDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DeleteRobotDto {
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/EnableAppTaskDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class EnableAppTaskDto {
+    @Schema(description = "任务id")
+    @NotBlank
+    private String taskId;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/GetAppRobotOptionsDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class GetAppRobotOptionsDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/GetAppSftpOptionsDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class GetAppSftpOptionsDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

+ 21 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppDto.java

@@ -0,0 +1,21 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppDto extends PageDto {
+    @Schema(description = "应用名称")
+    private String appName;
+    @Schema(description = "应用编码")
+    private String appId;
+    @Schema(description = "排序")
+    private Map<ListAppSortEnum, SortEnum> sort;
+}

+ 26 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppRobotCommandDto.java

@@ -0,0 +1,26 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppRobotCommandSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppRobotCommandDto extends PageDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    @NotBlank
+    private String robotCode;
+    @Schema(description = "命令")
+    private String command;
+    @Schema(description = "排序")
+    private Map<ListAppRobotCommandSortEnum, SortEnum> sort;
+}

+ 25 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppRobotDto.java

@@ -0,0 +1,25 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppRobotSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppRobotDto extends PageDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人名称")
+    private String robotName;
+    @Schema(description = "排序")
+    private Map<ListAppRobotSortEnum, SortEnum> sort;
+}

+ 29 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppSftpDto.java

@@ -0,0 +1,29 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppSftpSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppSftpDto extends PageDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "sftp id")
+    private String sftpId;
+    @Schema(description = "主机")
+    private String host;
+    @Schema(description = "端口")
+    private Integer port;
+    @Schema(description = "用户名")
+    private String username;
+    @Schema(description = "排序")
+    private Map<ListAppSftpSortEnum, SortEnum> sort;
+}

+ 43 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppTaskDto.java

@@ -0,0 +1,43 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppTaskSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppTaskDto extends PageDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "任务id")
+    private String taskId;
+    @Schema(description = "任务名称")
+    private String taskName;
+    @Schema(description = "任务状态,0已停止1运行中2已删除")
+    private Integer status;
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人名称")
+    private String robotName;
+    @Schema(description = "会话类型:1:单聊,2:群聊")
+    private Integer conversationType;
+    @Schema(description = "群id")
+    private String openConversationId;
+    @Schema(description = "接收机器人消息的用户的手机号列表")
+    private String phones;
+    @Schema(description = "接收机器人消息的用户的userId列表")
+    private String userIds;
+    @Schema(description = "sftp id")
+    private String sftpId;
+    @Schema(description = "主机")
+    private String host;
+    @Schema(description = "排序")
+    private Map<ListAppTaskSortEnum, SortEnum> sort;
+}

+ 50 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAppTaskLogDto.java

@@ -0,0 +1,50 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAppTaskLogSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAppTaskLogDto extends PageDto {
+    @Schema(description = "日志id")
+    private String id;
+    @Schema(description = "应用编码")
+    private String appId;
+    @Schema(description = "应用名称")
+    private String appName;
+    @Schema(description = "任务id")
+    private String taskId;
+    @Schema(description = "任务名称")
+    private String taskName;
+    @Schema(description = "任务执行状态,0失败1成功")
+    private Integer status;
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人名称")
+    private String robotName;
+    @Schema(description = "会话类型:1:单聊,2:群聊")
+    private Integer conversationType;
+    @Schema(description = "群id")
+    private String openConversationId;
+    @Schema(description = "接收机器人消息的用户的手机号列表")
+    private String phones;
+    @Schema(description = "接收机器人消息的用户的userId列表")
+    private String userIds;
+    @Schema(description = "sftp id")
+    private String sftpId;
+    @Schema(description = "主机")
+    private String host;
+    @Schema(description = "开始时间")
+    private LocalDateTime startTime;
+    @Schema(description = "结束时间")
+    private LocalDateTime endTime;
+    @Schema(description = "排序")
+    private Map<ListAppTaskLogSortEnum, SortEnum> sort;
+}

+ 25 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListAvailableRobotDto.java

@@ -0,0 +1,25 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListAvailableRobotSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListAvailableRobotDto extends PageDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人名称")
+    private String robotName;
+    @Schema(description = "排序")
+    private Map<ListAvailableRobotSortEnum, SortEnum> sort;
+}

+ 21 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ListRobotDto.java

@@ -0,0 +1,21 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import com.nokia.dingtalk_api.pojos.dto.PageDto;
+import com.nokia.dingtalk_api.pojos.enums.ListRobotSortEnum;
+import com.nokia.dingtalk_api.pojos.enums.SortEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ListRobotDto extends PageDto {
+    @Schema(description = "机器人编码")
+    private String robotCode;
+    @Schema(description = "机器人名称")
+    private String robotName;
+    @Schema(description = "排序")
+    private Map<ListRobotSortEnum, SortEnum> sort;
+}

+ 15 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/LoginDto.java

@@ -0,0 +1,15 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class LoginDto {
+    @Schema(description = "用户名")
+    @NotBlank
+    private String username;
+    @Schema(description = "密码")
+    @NotBlank
+    private String password;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/ResetAppSecretDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class ResetAppSecretDto {
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

+ 16 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/RunAppTaskDto.java

@@ -0,0 +1,16 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RunAppTaskDto {
+    @Schema(description = "任务id")
+    @NotBlank
+    private String taskId;
+}

+ 12 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/StopAppTaskDto.java

@@ -0,0 +1,12 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class StopAppTaskDto {
+    @Schema(description = "任务id")
+    @NotBlank
+    private String taskId;
+}

+ 15 - 0
src/main/java/com/nokia/dingtalk_api/pojos/dto/cms/UpdateAppDto.java

@@ -0,0 +1,15 @@
+package com.nokia.dingtalk_api.pojos.dto.cms;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class UpdateAppDto {
+    @Schema(description = "应用名称")
+    @NotBlank
+    private String appName;
+    @Schema(description = "应用编码")
+    @NotBlank
+    private String appId;
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.