Browse Source

对接企业微信在线办公

yingp 5 years ago
commit
cdb13a73c8
100 changed files with 9714 additions and 0 deletions
  1. 34 0
      .gitignore
  2. 28 0
      README.md
  3. 74 0
      build.gradle
  4. 6 0
      dingtalk-sdk/build.gradle
  5. BIN
      gradle/wrapper/gradle-wrapper.jar
  6. 5 0
      gradle/wrapper/gradle-wrapper.properties
  7. 172 0
      gradlew
  8. 84 0
      gradlew.bat
  9. 6 0
      qywx-sdk/build.gradle
  10. 327 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/AddrBookSdk.java
  11. 93 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/BaseSdk.java
  12. 108 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/InvoiceSdk.java
  13. 128 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/MediaSdk.java
  14. 138 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/MessageSdk.java
  15. 79 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/OaSdk.java
  16. 186 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/ScheduleSdk.java
  17. 54 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/config/Agent.java
  18. 40 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/config/QywxProperties.java
  19. 62 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddCalendarReq.java
  20. 16 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddCalendarResp.java
  21. 154 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddScheduleReq.java
  22. 16 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddScheduleResp.java
  23. 45 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/ApplyApprovalReq.java
  24. 25 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BaseResp.java
  25. 41 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchGetInvoiceInfoReq.java
  26. 434 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchGetInvoiceInfoResp.java
  27. 50 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchUpdateInvoiceStatusReq.java
  28. 61 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateChatReq.java
  29. 16 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateChatResp.java
  30. 71 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateDepartmentReq.java
  31. 16 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateDepartmentResp.java
  32. 201 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateUserEvent.java
  33. 221 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateUserReq.java
  34. 25 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetAccessTokenResp.java
  35. 451 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetApprovalTemplateResp.java
  36. 88 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCalendarListResp.java
  37. 67 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetChatResp.java
  38. 55 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinDataReq.java
  39. 175 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinDataResp.java
  40. 25 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinOptionReq.java
  41. 439 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinOptionResp.java
  42. 72 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetDepartmentListResp.java
  43. 435 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetInvoiceInfoResp.java
  44. 19 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetJoinQrCodeResp.java
  45. 16 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetOpenIdResp.java
  46. 49 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleByCalendarReq.java
  47. 190 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleByCalendarResp.java
  48. 181 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleListResp.java
  49. 52 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetSimpleUserListResp.java
  50. 35 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserInfoResp.java
  51. 263 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserListResp.java
  52. 226 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserResp.java
  53. 56 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InviteReq.java
  54. 36 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InviteResp.java
  55. 19 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InvoiceStatus.java
  56. 382 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendChatReq.java
  57. 554 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendMessageReq.java
  58. 62 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendMessageResp.java
  59. 62 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateCalendarReq.java
  60. 74 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateChatReq.java
  61. 69 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateDepartmentReq.java
  62. 159 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateScheduleReq.java
  63. 57 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateTaskCardReq.java
  64. 21 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateTaskCardResp.java
  65. 224 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateUserReq.java
  66. 19 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UploadImageResp.java
  67. 37 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UploadResp.java
  68. 20 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/exception/WeiXinAccessException.java
  69. 19 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/exception/WeiXinInvokeException.java
  70. 174 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/HttpUtils.java
  71. 11 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/QywxConst.java
  72. 35 0
      qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/UrlUtils.java
  73. 90 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/AddrBookSdkTest.java
  74. 42 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/BaseTest.java
  75. 52 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/MessageSdkTest.java
  76. 34 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/OaSdkTest.java
  77. 32 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/ScheduleSdkTest.java
  78. 14 0
      qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/UrlTest.java
  79. 8 0
      settings.gradle
  80. 6 0
      uas-office-core/build.gradle
  81. 35 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/config/RedisConfig.java
  82. 24 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/context/ContextHolder.java
  83. 38 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/context/MasterHolder.java
  84. 29 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/controller/DataCenterController.java
  85. 120 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/dto/Result.java
  86. 64 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/dto/UasEvent.java
  87. 126 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/entity/DataCenter.java
  88. 129 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/entity/Master.java
  89. 22 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/event/DataCenterEvent.java
  90. 22 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/event/MasterEvent.java
  91. 46 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DataSourceBean.java
  92. 36 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DataSourceHolder.java
  93. 172 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSource.java
  94. 81 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSourceConfig.java
  95. 46 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSourceRegister.java
  96. 306 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/SchemaUtils.java
  97. 27 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListener.java
  98. 28 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerAdapter.java
  99. 103 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerFactory.java
  100. 38 0
      uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerProcessor.java

+ 34 - 0
.gitignore

@@ -0,0 +1,34 @@
+.git
+logs
+rebel.xml
+target
+out
+!.mvn/wrapper/maven-wrapper.jar
+### VSCODE ###
+.vscode
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.gradle
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+nbproject/private/
+build/
+nbbuild/
+dist/
+node_modules/
+nbdist/
+.nb-gradle/
+generatorConfig.xml
+
+*.log:

+ 28 - 0
README.md

@@ -0,0 +1,28 @@
+## UAS对接企业微信
+
+### 项目结构
+
+```
+├─uas-qywx-integration
+│  │  
+│  ├─qywx-sdk---------------------------------企业微信api封装sdk
+│  ├─db---------------------------------------数据库脚本
+│  │
+```
+
+### 开发环境配置
+
+| 工具/环境      |  版本  |
+| --------   | :----:  |
+| gradle |  5.4  |
+| idea |  2019.3  |
+| java |  1.8  |
+
+### 本地构建
+
+```
+# build
+gradlew build -x test
+# deploy
+gradlew publish -x test
+```

+ 74 - 0
build.gradle

@@ -0,0 +1,74 @@
+plugins {
+    id "io.spring.dependency-management" version "1.0.7.RELEASE" apply false
+    id "org.springframework.boot" version "2.1.4.RELEASE" apply false
+}
+
+allprojects {
+    group 'com.usoftchina.uas'
+    version '1.0.0-SNAPSHOT'
+}
+
+subprojects { Project subproject ->
+    apply plugin: 'java'
+    apply plugin: 'idea'
+    apply plugin: 'maven'
+    apply plugin: 'maven-publish'
+    apply plugin: 'io.spring.dependency-management'
+
+    sourceCompatibility = 1.8
+    targetCompatibility = 1.8
+
+    [compileJava,compileTestJava,javadoc]*.options*.encoding = 'UTF-8'
+
+    ext {
+        springBootVersion = '2.1.4.RELEASE'
+        // dependencies
+        ojdbc = 'com.oracle:ojdbc6:11.2.0'
+        fastjson = 'com.alibaba:fastjson:1.2.47'
+        
+        repoBaseUrl = "http://maven.ubtob.com/artifactory"
+        snapshotUrl = "$repoBaseUrl/libs-snapshot-local"
+        releaseUrl = "$repoBaseUrl/libs-release-local"
+    }
+
+    repositories {
+        mavenLocal()
+        mavenCentral()
+        maven { url "http://repo.spring.io/libs-milestone" }
+        maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
+        maven { url "http://maven.ubtob.com/artifactory/libs-snapshot-local" }
+    }
+
+    dependencyManagement {
+        imports {
+            mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}"
+        }
+    }
+
+    task sourcesJar(type: Jar) {
+        from sourceSets.main.allJava
+        classifier 'sources'
+    }
+
+    artifacts {
+        archives sourcesJar
+    }
+
+    publishing {
+        publications {
+            plugins(MavenPublication) {
+                from components.java
+                artifact sourcesJar
+            }
+        }
+        repositories {
+            maven {
+                url project.version.endsWith('-SNAPSHOT') ? snapshotUrl : releaseUrl
+                credentials {
+                    username = 'yingp'
+                    password = '111111'
+                }
+            }
+        }
+    }
+}

+ 6 - 0
dingtalk-sdk/build.gradle

@@ -0,0 +1,6 @@
+dependencies {
+    compile "org.springframework:spring-web"
+    compile "org.springframework:spring-context"
+    compile "$fastjson"
+    testCompile "junit:junit"
+}

BIN
gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 6 - 0
qywx-sdk/build.gradle

@@ -0,0 +1,6 @@
+dependencies {
+    compile "org.springframework:spring-web"
+    compile "org.springframework:spring-context"
+    compile "$fastjson"
+    testCompile "junit:junit"
+}

+ 327 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/AddrBookSdk.java

@@ -0,0 +1,327 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.Agent;
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * 通讯录管理
+ *
+ * @author yingp
+ */
+public class AddrBookSdk extends BaseSdk {
+    /**
+     * 通讯录管理私钥
+     * <p>使用应用secret只能进行“查询”、“邀请”等非写操作,而且只能操作应用可见范围内的通讯录</p>
+     */
+    public final static String ADDRESS_BOOK_AGENT_CODE = "AddressBook";
+
+    public AddrBookSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 是否启用
+     *
+     * @return
+     */
+    public boolean enabled() {
+        Agent agent = getAgentMap().get(ADDRESS_BOOK_AGENT_CODE);
+        return null != agent && null != agent.getSecret();
+    }
+
+    /**
+     * sdk是否只有读权限
+     *
+     * @return
+     */
+    public boolean isReadonly() {
+        return enabled() && getAgentMap().get(ADDRESS_BOOK_AGENT_CODE).isReadonly();
+    }
+
+    /**
+     * 创建成员
+     *
+     * @param req
+     */
+    public void createUser(CreateUserReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/user/create?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 读取成员
+     *
+     * @param userId 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节
+     */
+    public GetUserResp getUser(String userId) {
+        ResponseEntity<GetUserResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/get?access_token={access_token}&userid={userid}",
+                GetUserResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)).addAttribute("userid", userId));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    /**
+     * 创建成员
+     *
+     * @param req
+     */
+    public void updateUser(UpdateUserReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/user/update?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 删除成员
+     *
+     * @param userId 成员UserID。对应管理端的帐号
+     */
+    public void deleteUser(String userId) {
+        ResponseEntity<BaseResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/delete?access_token={access_token}&userid={userid}",
+                BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)).addAttribute("userid", userId));
+        assertOK(resp);
+    }
+
+    /**
+     * 批量删除成员
+     *
+     * @param userIdList 成员UserID列表。对应管理端的帐号。最多支持200个。若存在无效UserID,直接返回错误
+     */
+    public void deleteUser(List<String> userIdList) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/user/batchdelete?access_token={access_token}",
+                new ModelMap("useridlist", userIdList),
+                BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取部门成员
+     *
+     * @param departmentId 部门ID
+     * @param fetchChild   是否递归获取子部门下面的成员:1-递归获取,0-只获取本部门
+     */
+    public List<GetSimpleUserListResp.User> getSimpleUserList(Integer departmentId, boolean fetchChild) {
+        ResponseEntity<GetSimpleUserListResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/simplelist?access_token={access_token}&department_id={department_id}&fetch_child={fetch_child}",
+                GetSimpleUserListResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("department_id", departmentId)
+                        .addAttribute("fetch_child", fetchChild ? 1 : 0));
+        assertOK(resp);
+        return resp.getBody().getUserlist();
+    }
+
+    /**
+     * 获取部门成员详情
+     *
+     * @param departmentId 部门ID
+     * @param fetchChild   是否递归获取子部门下面的成员:1-递归获取,0-只获取本部门
+     * @return
+     */
+    public List<GetUserListResp.User> getUserList(Integer departmentId, boolean fetchChild) {
+        ResponseEntity<GetUserListResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/list?access_token={access_token}&department_id={department_id}&fetch_child={fetch_child}",
+                GetUserListResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("department_id", departmentId)
+                        .addAttribute("fetch_child", fetchChild ? 1 : 0));
+        assertOK(resp);
+        return resp.getBody().getUserlist();
+    }
+
+    /**
+     * userid转openid
+     * <p>该接口使用场景为企业支付,在使用企业红包和向员工付款时,需要自行将企业微信的userid转成openid。</p>
+     *
+     * @param userId
+     * @return
+     */
+    public String getOpenId(String userId) {
+        ResponseEntity<GetOpenIdResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/user/convert_to_openid?access_token={access_token}",
+                new ModelMap("userid", userId),
+                GetOpenIdResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getOpenid();
+    }
+
+    /**
+     * 二次验证
+     * <p>
+     * 企业在开启二次验证时,必须在管理端填写企业二次验证页面的url。
+     * 当成员登录企业微信或关注微工作台(原企业号)加入企业时,会自动跳转到企业的验证页面。在跳转到企业的验证页面时,会带上如下参数:code=CODE。
+     * 企业收到code后,使用“通讯录同步助手”调用接口“根据code获取成员信息”获取成员的userid。然后在验证成员信息成功后,调用如下接口即可让成员成功加入企业
+     * </p>
+     *
+     * @param userId
+     */
+    public void authSuccess(String userId) {
+        ResponseEntity<BaseResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/authsucc?access_token={access_token}&userid={userid}",
+                BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("userid", userId));
+        assertOK(resp);
+    }
+
+    /**
+     * 邀请成员
+     * <p>企业可通过接口批量邀请成员使用企业微信,邀请后将通过短信或邮件下发通知</p>
+     *
+     * @param req
+     */
+    public InviteResp invite(InviteReq req) {
+        ResponseEntity<InviteResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/batch/invite?access_token={access_token}",
+                req.build(), InviteResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    /**
+     * 获取加入企业二维码
+     *
+     * @param type 尺寸
+     * @return 二维码链接,有效期7
+     */
+    public String getJoinQrCode(QrCodeType type) {
+        ResponseEntity<GetJoinQrCodeResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/corp/get_join_qrcode?access_token={access_token}&size_type={size_type}",
+                GetJoinQrCodeResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("size_type", type.code));
+        assertOK(resp);
+        return resp.getBody().getJoin_qrcode();
+    }
+
+    /**
+     * qrcode尺寸类型
+     */
+    public enum QrCodeType {
+        T_171_171(1),
+        T_399_399(2),
+        T_741_741(3),
+        T_2052_2052(4);
+
+        private final int code;
+
+        QrCodeType(int code) {
+            this.code = code;
+        }
+
+        public int getCode() {
+            return code;
+        }
+    }
+
+    /**
+     * 创建部门
+     *
+     * @param req
+     * @return 部门ID
+     */
+    public Integer createDepartment(CreateDepartmentReq req) {
+        ResponseEntity<CreateDepartmentResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/department/create?access_token={access_token}",
+                req.build(), CreateDepartmentResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getId();
+    }
+
+    /**
+     * 更新部门
+     *
+     * @param req
+     */
+    public void updateDepartment(UpdateDepartmentReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/department/update?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 删除部门
+     *
+     * @param departmentId
+     */
+    public void deleteDepartment(int departmentId) {
+        ResponseEntity<BaseResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/department/delete?access_token={access_token}&id={id}",
+                BaseResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("id", departmentId));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取部门
+     *
+     * @param departmentId
+     * @return
+     */
+    public GetDepartmentListResp.Department getDepartment(int departmentId) {
+        List<GetDepartmentListResp.Department> departmentList = getDepartmentList(departmentId);
+        if (!CollectionUtils.isEmpty(departmentList)) {
+            for (GetDepartmentListResp.Department department : departmentList) {
+                if(department.getId().equals(departmentId)) {
+                    return department;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取部门,包括下级
+     *
+     * @param departmentId
+     * @return
+     */
+    public List<GetDepartmentListResp.Department> getDepartmentList(int departmentId) {
+        ResponseEntity<GetDepartmentListResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/department/list?access_token={access_token}&id={id}",
+                GetDepartmentListResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE))
+                        .addAttribute("id", departmentId));
+        assertOK(resp);
+        if (!CollectionUtils.isEmpty(resp.getBody().getDepartment())) {
+            return resp.getBody().getDepartment();
+        }
+        return null;
+    }
+
+    /**
+     * 获取部门列表
+     *
+     * @return
+     */
+    public List<GetDepartmentListResp.Department> getDepartmentList() {
+        ResponseEntity<GetDepartmentListResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/department/list?access_token={access_token}",
+                GetDepartmentListResp.class,
+                new ModelMap("access_token", getAccessToken(ADDRESS_BOOK_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getDepartment();
+    }
+
+    /**
+     * 获取访问用户身份
+     *
+     * @param agentCode 应用编号
+     * @param code      授权码
+     */
+    public GetUserInfoResp getUserInfo(String agentCode, String code) {
+        ResponseEntity<GetUserInfoResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/user/getuserinfo?access_token={access_token}&code={code}",
+                GetUserInfoResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode))
+                        .addAttribute("code", code));
+        assertOK(resp);
+        return resp.getBody();
+    }
+}

+ 93 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/BaseSdk.java

@@ -0,0 +1,93 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.Agent;
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.BaseResp;
+import com.usoftchina.qywx.sdk.dto.GetAccessTokenResp;
+import com.usoftchina.qywx.sdk.exception.WeiXinAccessException;
+import com.usoftchina.qywx.sdk.exception.WeiXinInvokeException;
+import com.usoftchina.qywx.sdk.util.HttpUtils;
+import com.usoftchina.qywx.sdk.util.QywxConst;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public abstract class BaseSdk {
+    protected String baseUrl = QywxConst.API_BASE_URL;
+    private QywxProperties properties;
+    /**
+     * <私钥, Token>
+     */
+    private final Map<String, AccessToken> accessTokenMap = new HashMap<>();
+    protected final static RestTemplate restTemplate = HttpUtils.createTemplate();
+
+    public BaseSdk(QywxProperties properties) {
+        this.properties = properties;
+    }
+
+    public String getCorpId() {
+        return properties.getCorpId();
+    }
+
+    protected Map<String, Agent> getAgentMap() {
+        return properties.getAgentMap();
+    }
+
+    protected void assertOK(ResponseEntity<? extends BaseResp> resp) {
+        if (resp.getStatusCode() != HttpStatus.OK) {
+            throw new WeiXinAccessException(resp);
+        }
+        if (resp.getBody().getErrcode() != 0) {
+            throw new WeiXinInvokeException(resp.getBody());
+        }
+    }
+
+    /**
+     * @param agentCode
+     * @return
+     */
+    protected synchronized String getAccessToken(String agentCode) {
+        Agent agent = getAgentMap().get(agentCode);
+        if (null == agent) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        if (!accessTokenMap.containsKey(agent.getSecret()) || accessTokenMap.get(agent.getSecret()).expired()) {
+            ResponseEntity<GetAccessTokenResp> resp = restTemplate.getForEntity(
+                    baseUrl + "/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}",
+                    GetAccessTokenResp.class,
+                    new ModelMap("corpid", getCorpId()).addAttribute("corpsecret", agent.getSecret()));
+            assertOK(resp);
+            accessTokenMap.put(agent.getSecret(), new AccessToken(resp.getBody()));
+        }
+        return accessTokenMap.get(agent.getSecret()).getToken();
+    }
+
+    public boolean isAgentEnabled(String agentCode) {
+        return null != getAgentMap() && getAgentMap().containsKey(agentCode) && getAgentMap().get(agentCode).getSecret() != null;
+    }
+
+    class AccessToken {
+        private final String token;
+        private final long endTime;
+
+        public AccessToken(GetAccessTokenResp resp) {
+            this.token = resp.getAccess_token();
+            this.endTime = System.currentTimeMillis() + resp.getExpires_in() * 1000 - 5000;
+        }
+
+        public boolean expired() {
+            return System.currentTimeMillis() > endTime;
+        }
+
+        public String getToken() {
+            return token;
+        }
+    }
+}

+ 108 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/InvoiceSdk.java

@@ -0,0 +1,108 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+
+import java.util.List;
+
+/**
+ * 电子发票
+ * <p>
+ * 报销发票接口及jsapi用于在应用中选择微信卡包中的电子发票实现电子化报销,该接口仅对认证的企业微信账号开放。具体操作步骤如下:
+ * <ol>
+ * <li>用户在应用中发票报销,选择电子发票类型;</li>
+ * <li>应用调用JS-SDK“chooseInvoice接口”拉起微信卡包,展示发票列表</li>
+ * <li>用户选择电子发票提交报销;</li>
+ * <li>报销方调用API接口查询发票信息,核实后锁定电子发票</li>
+ * <li>报销方打款给用户后,调用接口核销电子发票</li>
+ * </ol>
+ *
+ * @author yingp
+ */
+public class InvoiceSdk extends BaseSdk {
+    public InvoiceSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 查询电子发票
+     * 报销方在获得用户选择的电子发票标识参数后,可以通过该接口查询电子发票的结构化信息,并获取发票PDF文件。
+     * 仅认证的企业微信账号有接口权限
+     *
+     * @param agentCode   应用code
+     * @param cardId      发票id
+     * @param encryptCode 加密code
+     */
+    public GetInvoiceInfoResp getInvoiceInfo(String agentCode, String cardId, String encryptCode) {
+        ResponseEntity<GetInvoiceInfoResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token={access_token}",
+                new ModelMap("card_id", cardId).addAttribute("encrypt_code", encryptCode),
+                GetInvoiceInfoResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    /**
+     * 批量查询电子发票
+     * 报销方在获得用户选择的电子发票标识参数后,可以通过该接口批量查询电子发票的结构化信息。
+     *
+     * @param agentCode   应用code
+     * @param req
+     * @return
+     */
+    public List<BatchGetInvoiceInfoResp.Invoice> batchGetInvoiceInfo(String agentCode, BatchGetInvoiceInfoReq req) {
+        ResponseEntity<BatchGetInvoiceInfoResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token={access_token}",
+                req.build(), BatchGetInvoiceInfoResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody().getItem_list();
+    }
+
+    /**
+     * 更新发票状态
+     * <p>
+     * 报销企业和报销服务商可以通过该接口对某一张发票进行锁定、解锁和报销操作。各操作的业务含义及在用户端的表现如下:
+     * 锁定:电子发票进入了企业的报销流程时应该执行锁定操作,执行锁定操作后的电子发票仍然会存在于用户卡包内,但无法重复提交报销。
+     * 解锁:当电子发票由于各种原因,无法完成报销流程时,应执行解锁操作。执行锁定操作后的电子发票将恢复可以被提交的状态。
+     * 报销:当电子发票报销完成后,应该使用本接口执行报销操作。执行报销操作后的电子发票将从用户的卡包中移除,用户可以在卡包的消息中查看到电子发票的核销信息。注意,报销为不可逆操作,请开发者慎重调用
+     * </p>
+     *
+     * @param agentCode   应用code
+     * @param cardId      发票id
+     * @param encryptCode 加密code
+     * @param status      发票状态
+     */
+    public void updateInvoiceStatus(String agentCode, String cardId, String encryptCode, InvoiceStatus status) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token={access_token}",
+                new ModelMap("card_id", cardId)
+                        .addAttribute("encrypt_code", encryptCode)
+                        .addAttribute("reimburse_status", status.name()),
+                BaseResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+    }
+
+    /**
+     * 批量更新发票状态
+     * <p>
+     * 发票平台可以通过该接口对某个成员的一批发票进行锁定、解锁和报销操作。注意,报销状态为不可逆状态,请开发者慎重调用。
+     * 报销方须保证在报销、锁定、解锁后及时将状态同步至微信侧,保证用户发票可以正常使用
+     * 批量更新发票状态接口为事务性操作,如果其中一张发票更新失败,列表中的其它发票状态更新也会无法执行,恢复到接口调用前的状态
+     * </p>
+     *
+     * @param agentCode   应用code
+     * @param req
+     */
+    public void batchUpdateInvoiceStatus(String agentCode, BatchUpdateInvoiceStatusReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+    }
+}

+ 128 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/MediaSdk.java

@@ -0,0 +1,128 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.UploadImageResp;
+import com.usoftchina.qywx.sdk.dto.UploadResp;
+import com.usoftchina.qywx.sdk.exception.WeiXinAccessException;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.*;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * 素材管理
+ *
+ * @author yingp
+ */
+public class MediaSdk extends BaseSdk {
+    public MediaSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 上传临时素材
+     * <p>
+     * 所有文件size必须大于5个字节
+     * 图片(image):2MB,支持JPG,PNG格式
+     * 语音(voice) :2MB,播放长度不超过60s,仅支持AMR格式
+     * 视频(video) :10MB,支持MP4格式
+     * 普通文件(file):20MB
+     * </p>
+     *
+     * @param agentCode
+     * @param data
+     * @param name
+     * @param type
+     * @return mediaId
+     */
+    public String upload(String agentCode, byte[] data, String name, FileType type) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
+        form.add("name", "media");
+        form.add("media", new ByteArrayResource(data));
+        form.add("filename", name);
+        form.add("filelength", data.length);
+        HttpEntity<MultiValueMap<String, Object>> req = new HttpEntity<>(form, headers);
+        ResponseEntity<UploadResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/media/upload?access_token={access_token}&type={type}",
+                req, UploadResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode))
+                        .addAttribute("type", type.code));
+        assertOK(resp);
+        return resp.getBody().getMedia_id();
+    }
+
+    public enum FileType {
+        /**
+         * 图片
+         */
+        IMAGE("image"),
+        /**
+         * 语音
+         */
+        VOICE("voice"),
+        /**
+         * 视频
+         */
+        VIDEO("video"),
+        /**
+         * 普通文件
+         */
+        FILE("file");
+
+        private final String code;
+
+        FileType(String code) {
+            this.code = code;
+        }
+    }
+
+    /**
+     * 上传图片
+     * <p>
+     * 上传图片得到图片URL,该URL永久有效
+     * 返回的图片URL,仅能用于图文消息正文中的图片展示;若用于非企业微信域名下的页面,图片将被屏蔽。
+     * 每个企业每天最多可上传100张图片
+     * 图片文件大小应在 5B ~ 2MB 之间
+     * </p>
+     *
+     * @param agentCode
+     * @param data
+     * @param name
+     * @param type
+     * @return
+     */
+    public String uploadImage(String agentCode, byte[] data, String name, MediaType type) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(type);
+        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
+        form.add("name", "file");
+        form.add("file", new ByteArrayResource(data));
+        form.add("filename", name);
+        HttpEntity<MultiValueMap<String, Object>> req = new HttpEntity<>(form, headers);
+        ResponseEntity<UploadImageResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/media/uploadimg?access_token={access_token}",
+                req, UploadImageResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody().getUrl();
+    }
+
+    /**
+     * 获取临时素材
+     *
+     * @param agentCode
+     * @param mediaId
+     * @return
+     */
+    public byte[] getMedia(String agentCode, String mediaId) {
+        ResponseEntity<byte[]> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/media/get?access_token={access_token}&media_id={media_id}",
+                byte[].class,
+                new ModelMap("access_token", getAccessToken(agentCode))
+                        .addAttribute("media_id", mediaId));
+        if (resp.getStatusCode() != HttpStatus.OK) {
+            throw new WeiXinAccessException(resp);
+        }
+        return resp.getBody();
+    }
+}

+ 138 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/MessageSdk.java

@@ -0,0 +1,138 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.Agent;
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+
+/**
+ * 消息推送
+ *
+ * @author yingp
+ */
+public class MessageSdk extends BaseSdk {
+
+    public MessageSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 发送消息
+     * 应用支持推送文本、图片、视频、文件、图文等类型
+     *
+     * @param agentCode 应用code
+     * @param req
+     */
+    public SendMessageResp send(String agentCode, SendMessageReq req) {
+        Agent agent = getAgentMap().get(agentCode);
+        if (null == agent) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        req.agentId(agent.getId());
+        ResponseEntity<SendMessageResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/message/send?access_token={access_token}",
+                req.build(), SendMessageResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    /**
+     * 更新任务卡片消息状态
+     * 应用可以发送任务卡片消息,发送之后可再通过接口更新用户任务卡片消息的选择状态。
+     *
+     * @param agentCode 应用code
+     * @param req
+     * @return
+     */
+    public UpdateTaskCardResp updateTaskCard(String agentCode, UpdateTaskCardReq req) {
+        Agent agent = getAgentMap().get(agentCode);
+        if (null == agent) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        req.agentId(agent.getId());
+        ResponseEntity<UpdateTaskCardResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/message/update_taskcard?access_token={access_token}",
+                req.build(), UpdateTaskCardResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    /**
+     * 创建群聊会话
+     * 只允许企业自建应用调用,且应用的可见范围必须是根部门;
+     * 群成员人数不可超过管理端配置的“群成员人数上限”,且最大不可超过500人;
+     * 每企业创建群数不可超过1000/天;
+     *
+     * @param agentCode 应用code
+     * @param req
+     * @return 会话ID
+     */
+    public String createChat(String agentCode, CreateChatReq req) {
+        if (!getAgentMap().containsKey(agentCode)) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        ResponseEntity<CreateChatResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/appchat/create?access_token={access_token}",
+                req.build(), CreateChatResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+        return resp.getBody().getChatid();
+    }
+
+    /**
+     * 修改群聊会话
+     *
+     * @param agentCode 应用code
+     * @param req
+     */
+    public void updateChat(String agentCode, UpdateChatReq req) {
+        if (!getAgentMap().containsKey(agentCode)) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/appchat/update?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取群聊会话
+     *
+     * @param agentCode 应用code
+     * @param chatId    群聊id
+     * @return
+     */
+    public GetChatResp.Chat getChat(String agentCode, String chatId) {
+        if (!getAgentMap().containsKey(agentCode)) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        ResponseEntity<GetChatResp> resp = restTemplate.getForEntity(baseUrl + "/cgi-bin/appchat/get?access_token={access_token}&chatid={chatid}",
+                GetChatResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)).addAttribute("chatid", chatId));
+        assertOK(resp);
+        return resp.getBody().getChat_info();
+    }
+
+    /**
+     * 应用推送消息
+     * 应用支持推送文本、图片、视频、文件、图文等类型。
+     * <p>
+     * 只允许企业自建应用调用,且应用的可见范围必须是根部门;
+     * chatid所代表的群必须是该应用所创建;
+     * 每企业消息发送量不可超过2万人次/分,不可超过20万人次/小时(若群有100人,每发一次消息算100人次);
+     * 每个成员在群中收到的应用消息不可超过200条/分,1万条/天,超过会被丢弃(接口不会报错);
+     * </p>
+     *
+     * @param agentCode 应用code
+     * @param req
+     */
+    public void sendChat(String agentCode, SendChatReq req) {
+        if (!getAgentMap().containsKey(agentCode)) {
+            throw new RuntimeException("没有找到应用" + agentCode);
+        }
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/appchat/send?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(agentCode)));
+        assertOK(resp);
+    }
+}

+ 79 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/OaSdk.java

@@ -0,0 +1,79 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+
+import java.util.List;
+
+/**
+ * OA数据接口
+ *
+ * @author yingp
+ */
+public class OaSdk extends BaseSdk {
+
+    public final static String CHECKIN_AGENT_CODE = "Checkin";
+
+    public final static String APPROVAL_AGENT_CODE = "Approval";
+
+    public OaSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 获取打卡数据
+     *
+     * @param req
+     */
+    public List<GetCheckinDataResp.CheckinData> getCheckinData(GetCheckinDataReq req) {
+        ResponseEntity<GetCheckinDataResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/checkin/getcheckindata?access_token={access_token}",
+                req.build(),
+                GetCheckinDataResp.class,
+                new ModelMap("access_token", getAccessToken(CHECKIN_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getCheckindata();
+    }
+
+    /**
+     * 获取打卡规则
+     *
+     * @param req
+     */
+    public List<GetCheckinOptionResp.CheckinOption> getCheckinOption(GetCheckinOptionReq req) {
+        ResponseEntity<GetCheckinOptionResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/checkin/getcheckinoption?access_token={access_token}",
+                req.build(),
+                GetCheckinOptionResp.class,
+                new ModelMap("access_token", getAccessToken(CHECKIN_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getInfo();
+    }
+
+    /**
+     * 获取审批模板详情
+     * 企业可通过审批应用或自建应用Secret调用本接口,获取企业微信“审批应用”内指定审批模板的详情
+     * <p>
+     * 1.审批应用的Secret可获取企业自建模板及第三方服务商添加的模板详情;自建应用的Secret可获取企业自建模板的模板详情。
+     * 2.接口调用频率限制为600次/分钟。
+     * </p>
+     *
+     * @param templateId 模板的唯一标识id 可在“获取审批单据详情”、“审批状态变化回调通知”中获得,也可在审批模板的模板编辑页面浏览器Url链接中获得
+     * @return
+     */
+    public GetApprovalTemplateResp getApprovalTemplate(String templateId) {
+        ResponseEntity<GetApprovalTemplateResp> resp = restTemplate.postForEntity(
+                baseUrl + "/cgi-bin/oa/gettemplatedetail?access_token={access_token}",
+                new ModelMap("template_id", templateId),
+                GetApprovalTemplateResp.class,
+                new ModelMap("access_token", getAccessToken(APPROVAL_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody();
+    }
+
+    public void applyApproval(ApplyApprovalReq req) {
+
+    }
+}

+ 186 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/ScheduleSdk.java

@@ -0,0 +1,186 @@
+package com.usoftchina.qywx.sdk;
+
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 日程
+ *
+ * @author yingp
+ */
+public class ScheduleSdk extends BaseSdk {
+
+    public final static String SCHEDULE_AGENT_CODE = "Schedule";
+
+    public ScheduleSdk(QywxProperties properties) {
+        super(properties);
+    }
+
+    /**
+     * 是否启用
+     *
+     * @return
+     */
+    public boolean enabled() {
+        return getAgentMap().containsKey(SCHEDULE_AGENT_CODE);
+    }
+
+    /**
+     * 创建日历
+     * 该接口用于通过应用在企业内创建一个日历。
+     *
+     * @param req
+     * @return 日历ID
+     */
+    public String addCalendar(AddCalendarReq req) {
+        ResponseEntity<AddCalendarResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/calendar/add?access_token={access_token}",
+                req.build(), AddCalendarResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getCal_id();
+    }
+
+    /**
+     * 更新日历
+     * 该接口用于修改指定日历的信息。
+     * <p>更新操作是覆盖式,而不是增量式</p>
+     *
+     * @param req
+     */
+    public void updateCalendar(UpdateCalendarReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/calendar/update?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取日历
+     * 该接口用于获取应用在企业内创建的日历信息。
+     *
+     * @param calendarIdList
+     * @return
+     */
+    public List<GetCalendarListResp.Calendar> getCalendarList(List<String> calendarIdList) {
+        ResponseEntity<GetCalendarListResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/calendar/get?access_token={access_token}",
+                new ModelMap("cal_id_list", calendarIdList), GetCalendarListResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getCalendar_list();
+    }
+
+    /**
+     * 获取日历
+     * 该接口用于获取应用在企业内创建的日历信息
+     *
+     * @param calendarId
+     * @return
+     */
+    public GetCalendarListResp.Calendar getCalendar(String calendarId) {
+        List<GetCalendarListResp.Calendar> calendars = getCalendarList(Arrays.asList(calendarId));
+        return CollectionUtils.isEmpty(calendars) ? null : calendars.get(0);
+    }
+
+    /**
+     * 删除日历
+     * 该接口用于删除指定日历
+     *
+     * @param calendarId
+     */
+    public void deleteCalendar(String calendarId) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/calendar/del?access_token={access_token}",
+                new ModelMap("cal_id", calendarId), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 创建日程
+     * 该接口用于在日历中创建一个日程。
+     *
+     * @param req
+     * @return 日程ID
+     */
+    public String addSchedule(AddScheduleReq req) {
+        ResponseEntity<AddScheduleResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/schedule/add?access_token={access_token}",
+                req.build(), AddScheduleResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getSchedule_id();
+    }
+
+    /**
+     * 更新日程
+     * 该接口用于在日历中更新指定的日程。
+     * <p>更新操作是覆盖式,而不是增量式</p>
+     *
+     * @param req
+     */
+    public void updateSchedule(UpdateScheduleReq req) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/schedule/update?access_token={access_token}",
+                req.build(), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取日程
+     * 该接口用于获取指定的日程详情。
+     *
+     * @param scheduleIdList
+     * @return
+     */
+    public List<GetScheduleListResp.Schedule> getScheduleList(List<String> scheduleIdList) {
+        ResponseEntity<GetScheduleListResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/schedule/get?access_token={access_token}",
+                new ModelMap("schedule_id_list", scheduleIdList), GetScheduleListResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getSchedule_list();
+    }
+
+    /**
+     * 获取日程
+     * 该接口用于获取指定的日程详情。
+     *
+     * @param scheduleId
+     * @return
+     */
+    public GetScheduleListResp.Schedule getSchedule(String scheduleId) {
+        List<GetScheduleListResp.Schedule> schedules = getScheduleList(Arrays.asList(scheduleId));
+        return CollectionUtils.isEmpty(schedules) ? null : schedules.get(0);
+    }
+
+    /**
+     * 取消日程
+     * 该接口用于取消指定的日程。
+     *
+     * @param scheduleId
+     */
+    public void deleteSchedule(String scheduleId) {
+        ResponseEntity<BaseResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/schedule/del?access_token={access_token}",
+                new ModelMap("schedule_id", scheduleId), BaseResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+    }
+
+    /**
+     * 获取日历下的日程列表
+     * 该接口用于获取指定的日历下的日程列表。
+     *
+     * @param req 日历ID
+     * @return
+     */
+    public List<GetScheduleByCalendarResp.Schedule> getScheduleByCalendar(GetScheduleByCalendarReq req) {
+        ResponseEntity<GetScheduleByCalendarResp> resp = restTemplate.postForEntity(baseUrl + "/cgi-bin/oa/schedule/get_by_calendar?access_token={access_token}",
+                req.build(), GetScheduleByCalendarResp.class,
+                new ModelMap("access_token", getAccessToken(SCHEDULE_AGENT_CODE)));
+        assertOK(resp);
+        return resp.getBody().getSchedule_list();
+    }
+}

+ 54 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/config/Agent.java

@@ -0,0 +1,54 @@
+package com.usoftchina.qywx.sdk.config;
+
+/**
+ * @author yingp
+ */
+public class Agent {
+    /**
+     * 编号,统一友好命名。代码里面直接使用
+     */
+    private final String code;
+    /**
+     * agentId
+     */
+    private final Integer id;
+    /**
+     * 私钥
+     */
+    private final String secret;
+    /**
+     * 用于通讯录应用,通讯录应用的secret具有全部api权限,使用自建应用的secret只能调用通讯录读api
+     */
+    private boolean readonly;
+
+    public Agent(String code, Integer id, String secret) {
+        this(code, id, secret, false);
+    }
+
+    public Agent(String code, Integer id, String secret, boolean readonly) {
+        this.code = code;
+        this.id = id;
+        this.secret = secret;
+        this.readonly = readonly;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public Integer getId() {
+        return id;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public boolean isReadonly() {
+        return readonly;
+    }
+
+    public void setReadonly(boolean readonly) {
+        this.readonly = readonly;
+    }
+}

+ 40 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/config/QywxProperties.java

@@ -0,0 +1,40 @@
+package com.usoftchina.qywx.sdk.config;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class QywxProperties {
+    /**
+     * 企业微信ID
+     */
+    private String corpId;
+    /**
+     * <应用CODE, 应用私钥>
+     */
+    private Map<String, Agent> agentMap;
+
+    public String getCorpId() {
+        return corpId;
+    }
+
+    public void setCorpId(String corpId) {
+        this.corpId = corpId;
+    }
+
+    public Map<String, Agent> getAgentMap() {
+        return agentMap;
+    }
+
+    public void setAgents(List<Agent> agents) {
+        if (null == agents) {
+            agentMap = new HashMap<>(0);
+        } else {
+            this.agentMap = agents.stream().collect(Collectors.toMap(Agent::getCode, a -> a));
+        }
+    }
+}

+ 62 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddCalendarReq.java

@@ -0,0 +1,62 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import org.springframework.ui.ModelMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class AddCalendarReq {
+    private String organizer;
+    private String summary;
+    private String color;
+    private String description;
+    private List<String> shares;
+
+    public AddCalendarReq organizer(String organizer) {
+        this.organizer = organizer;
+        return this;
+    }
+
+    public AddCalendarReq summary(String summary) {
+        this.summary = summary;
+        return this;
+    }
+
+    public AddCalendarReq color(String color) {
+        this.color = color;
+        return this;
+    }
+
+    public AddCalendarReq description(String description) {
+        this.description = description;
+        return this;
+    }
+
+    public AddCalendarReq shares(List<String> shares) {
+        this.shares = shares;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        Map<String, Object> calendar = new HashMap<>(5);
+        calendar.put("organizer", organizer);
+        calendar.put("summary", summary);
+        calendar.put("color", color);
+        if (null != description) {
+            calendar.put("description", description);
+        }
+        if (null != shares) {
+            calendar.put("shares",
+                    shares.stream().map(attendee -> new ModelMap("userid", attendee)).collect(Collectors.toList())
+            );
+        }
+        data.put("calendar", calendar);
+        return data;
+    }
+}

+ 16 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddCalendarResp.java

@@ -0,0 +1,16 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class AddCalendarResp extends BaseResp {
+    private String cal_id;
+
+    public String getCal_id() {
+        return cal_id;
+    }
+
+    public void setCal_id(String cal_id) {
+        this.cal_id = cal_id;
+    }
+}

+ 154 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddScheduleReq.java

@@ -0,0 +1,154 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import org.springframework.ui.ModelMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class AddScheduleReq {
+    /**
+     * 组织者userId
+     */
+    private String organizer;
+    /**
+     * 日程开始时间,Unix时间戳
+     */
+    private Long startTime;
+    /**
+     * 日程结束时间,Unix时间戳
+     */
+    private Long endTime;
+    /**
+     * 日程参与者列表。最多支持2000
+     */
+    private List<String> attendees;
+    /**
+     * 日程标题。0 ~ 128 字符。不填会默认显示为“新建事件”
+     */
+    private String summary;
+    /**
+     * 日程描述。0 ~ 512 字符
+     */
+    private String description;
+    /**
+     * 日程开始(start_time)前多少秒提醒,当is_remind为1时有效。例如: 300表示日程开始前5分钟提醒。
+     * 目前仅支持以下数值:
+     * 0 - 事件开始时
+     * 300 - 事件开始前5分钟
+     * 900 - 事件开始前15分钟
+     * 3600 - 事件开始前1小时
+     * 86400 - 事件开始前1
+     */
+    private Integer remindBeforeEventSecs;
+    /**
+     * 重复类型,当is_repeat为1时有效。目前支持如下类型:
+     * 0 - 每日
+     * 1 - 每周
+     * 2 - 每月
+     * 5 - 每年
+     * 7 - 工作日
+     */
+    private RepeatType repeatType;
+    /**
+     * 日程地址。0 ~ 128 字符
+     */
+    private String location;
+    /**
+     * 日程所属日历ID。注意,这个日历必须是属于组织者(organizer)的日历;如果不填,那么插入到组织者的默认日历上
+     */
+    private String calId;
+
+    public AddScheduleReq organizer(String organizer) {
+        this.organizer = organizer;
+        return this;
+    }
+
+    public AddScheduleReq during(long startTime, long endTime) {
+        this.startTime = startTime;
+        this.endTime = endTime;
+        return this;
+    }
+
+    public AddScheduleReq attendees(List<String> attendees) {
+        this.attendees = attendees;
+        return this;
+    }
+
+    public AddScheduleReq summary(String summary) {
+        this.summary = summary;
+        return this;
+    }
+
+    public AddScheduleReq description(String description) {
+        this.description = description;
+        return this;
+    }
+
+    public AddScheduleReq remindBefore(int secs) {
+        this.remindBeforeEventSecs = secs;
+        return this;
+    }
+
+    public AddScheduleReq repeat(RepeatType type) {
+        this.repeatType = type;
+        return this;
+    }
+
+    public AddScheduleReq location(String location) {
+        this.location = location;
+        return this;
+    }
+
+    public AddScheduleReq calId(String calId) {
+        this.calId = calId;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        Map<String, Object> schedule = new HashMap<>(1);
+        schedule.put("organizer", organizer);
+        schedule.put("start_time", startTime);
+        schedule.put("end_time", endTime);
+        if (null != attendees) {
+            schedule.put("attendees",
+                    attendees.stream().map(attendee -> new ModelMap("userid", attendee)).collect(Collectors.toList())
+            );
+        }
+        if (null != summary) {
+            schedule.put("summary", summary);
+        }
+        if (null != description) {
+            schedule.put("description", description);
+        }
+        if (null != remindBeforeEventSecs || null != repeatType) {
+            schedule.put("reminders", new ModelMap("is_remind", null != remindBeforeEventSecs ? 1 : 0)
+                    .addAttribute("remind_before_event_secs", remindBeforeEventSecs)
+                    .addAttribute("is_repeat", null != repeatType ? 1 : 0)
+                    .addAttribute("repeat_type", null == repeatType ? null : repeatType.code));
+        }
+        if (null != location) {
+            schedule.put("location", location);
+        }
+        if (null != calId) {
+            schedule.put("cal_id", calId);
+        }
+        data.put("schedule", schedule);
+        return data;
+    }
+
+    public enum RepeatType {
+        DAY(0), WEEK(1), MONTH(2), YEAR(5), WEEKDAYS(7);
+
+        private final int code;
+
+        RepeatType(int code) {
+            this.code = code;
+        }
+    }
+}

+ 16 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/AddScheduleResp.java

@@ -0,0 +1,16 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class AddScheduleResp extends BaseResp {
+    private String schedule_id;
+
+    public String getSchedule_id() {
+        return schedule_id;
+    }
+
+    public void setSchedule_id(String schedule_id) {
+        this.schedule_id = schedule_id;
+    }
+}

+ 45 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/ApplyApprovalReq.java

@@ -0,0 +1,45 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ * @date 2020/2/12
+ */
+public class ApplyApprovalReq {
+    private String creator_userid;
+    private String template_id;
+    private Integer use_template_approver;
+    private List<Approver> approver;
+    private List<String> notifyer;
+    private Integer notify_type;
+
+    public ApplyApprovalReq approver(List<Approver> approver) {
+        this.approver = approver;
+        return this;
+    }
+
+    public ApplyApprovalReq notifyer(List<String> notifyer) {
+        this.notifyer = notifyer;
+        return this;
+    }
+
+    public static class Approver {
+        private Integer attr;
+        private List<String> userIds;
+
+        public Approver(Integer attr, List<String> userIds) {
+            this.attr = attr;
+            this.userIds = userIds;
+        }
+
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(2);
+            data.put("attr", attr);
+            data.put("userid", userIds);
+            return data;
+        }
+    }
+}

+ 25 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BaseResp.java

@@ -0,0 +1,25 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class BaseResp {
+    protected int errcode;
+    protected String errmsg;
+
+    public int getErrcode() {
+        return errcode;
+    }
+
+    public void setErrcode(int errcode) {
+        this.errcode = errcode;
+    }
+
+    public String getErrmsg() {
+        return errmsg;
+    }
+
+    public void setErrmsg(String errmsg) {
+        this.errmsg = errmsg;
+    }
+}

+ 41 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchGetInvoiceInfoReq.java

@@ -0,0 +1,41 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class BatchGetInvoiceInfoReq {
+
+    private List<Invoice> invoices;
+
+    public BatchGetInvoiceInfoReq(List<Invoice> invoices) {
+        this.invoices = invoices;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        data.put("item_list", invoices.stream().map(Invoice::build).collect(Collectors.toList()));
+        return data;
+    }
+
+    public static class Invoice {
+        private String cardId;
+        private String encryptCode;
+
+        public Invoice(String cardId, String encryptCode) {
+            this.cardId = cardId;
+            this.encryptCode = encryptCode;
+        }
+
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("card_id", cardId);
+            data.put("encrypt_code", encryptCode);
+            return data;
+        }
+    }
+}

+ 434 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchGetInvoiceInfoResp.java

@@ -0,0 +1,434 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class BatchGetInvoiceInfoResp extends BaseResp {
+
+    private List<Invoice> item_list;
+
+    public List<Invoice> getItem_list() {
+        return item_list;
+    }
+
+    public void setItem_list(List<Invoice> item_list) {
+        this.item_list = item_list;
+    }
+
+    public static class Invoice {
+        /**
+         * 发票id
+         */
+        private String card_id;
+        /**
+         * 用户标识
+         */
+        private String openid;
+        /**
+         * 发票类型,如广东增值税普通发票
+         */
+        private String type;
+        /**
+         * 发票的收款方
+         */
+        private String payee;
+        /**
+         * 发票详情
+         */
+        private String detail;
+        /**
+         * 发票的用户信息
+         */
+        private UserInfo user_info;
+
+        public String getCard_id() {
+            return card_id;
+        }
+
+        public void setCard_id(String card_id) {
+            this.card_id = card_id;
+        }
+
+        public String getOpenid() {
+            return openid;
+        }
+
+        public void setOpenid(String openid) {
+            this.openid = openid;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        public String getPayee() {
+            return payee;
+        }
+
+        public void setPayee(String payee) {
+            this.payee = payee;
+        }
+
+        public String getDetail() {
+            return detail;
+        }
+
+        public void setDetail(String detail) {
+            this.detail = detail;
+        }
+
+        public UserInfo getUser_info() {
+            return user_info;
+        }
+
+        public void setUser_info(UserInfo user_info) {
+            this.user_info = user_info;
+        }
+    }
+
+    public static class UserInfo {
+        /**
+         * 发票加税合计金额,以分为单位
+         */
+        private Double fee;
+        /**
+         * 发票的抬头
+         */
+        private String title;
+        /**
+         * 开票时间,为十位时间戳
+         */
+        private Long billing_time;
+        /**
+         * 发票代码
+         */
+        private String billing_no;
+        /**
+         * 发票号码
+         */
+        private String billing_code;
+        /**
+         * 商品信息结构
+         */
+        private List<ProductInfo> info;
+        /**
+         * 不含税金额,以分为单位
+         */
+        private Double fee_without_tax;
+        /**
+         * 税额,以分为单位
+         */
+        private Double tax;
+        /**
+         * 发票详情,一般描述的是发票的使用说明
+         */
+        private String detail;
+        /**
+         * 这张发票对应的PDF_URL
+         */
+        private String pdf_url;
+        /**
+         * 发报销状态INVOICE_REIMBURSE_INIT:发票初始状态,未锁定;INVOICE_REIMBURSE_LOCK:发票已锁定;INVOICE_REIMBURSE_CLOSURE:发票已核销
+         */
+        private String reimburse_status;
+        /**
+         * 开票人,发票右下角处
+         */
+        private String maker;
+        /**
+         * 收款人,发票左下角处
+         */
+        private String cashier;
+        /**
+         * 备注
+         */
+        private String remarks;
+        /**
+         * 销售方开户行及账号
+         */
+        private String seller_bank_account;
+        /**
+         * 销售方地址、电话
+         */
+        private String seller_address_and_phone;
+        /**
+         * 销售方纳税人识别号
+         */
+        private String seller_number;
+        /**
+         * 购买方开户行及账号
+         */
+        private String buyer_bank_account;
+        /**
+         * 购买方地址、电话
+         */
+        private String buyer_address_and_phone;
+        /**
+         * 购买方纳税人识别号
+         */
+        private String buyer_number;
+        /**
+         * 校验码
+         */
+        private String check_code;
+        /**
+         * 其它消费凭证附件对应的URL,如行程单、水单等
+         */
+        private String trip_pdf_url;
+
+        private String order_id;
+
+        public Double getFee() {
+            return fee;
+        }
+
+        public void setFee(Double fee) {
+            this.fee = fee;
+        }
+
+        public String getTitle() {
+            return title;
+        }
+
+        public void setTitle(String title) {
+            this.title = title;
+        }
+
+        public Long getBilling_time() {
+            return billing_time;
+        }
+
+        public void setBilling_time(Long billing_time) {
+            this.billing_time = billing_time;
+        }
+
+        public String getBilling_no() {
+            return billing_no;
+        }
+
+        public void setBilling_no(String billing_no) {
+            this.billing_no = billing_no;
+        }
+
+        public String getBilling_code() {
+            return billing_code;
+        }
+
+        public void setBilling_code(String billing_code) {
+            this.billing_code = billing_code;
+        }
+
+        public List<ProductInfo> getInfo() {
+            return info;
+        }
+
+        public void setInfo(List<ProductInfo> info) {
+            this.info = info;
+        }
+
+        public Double getFee_without_tax() {
+            return fee_without_tax;
+        }
+
+        public void setFee_without_tax(Double fee_without_tax) {
+            this.fee_without_tax = fee_without_tax;
+        }
+
+        public Double getTax() {
+            return tax;
+        }
+
+        public void setTax(Double tax) {
+            this.tax = tax;
+        }
+
+        public String getDetail() {
+            return detail;
+        }
+
+        public void setDetail(String detail) {
+            this.detail = detail;
+        }
+
+        public String getPdf_url() {
+            return pdf_url;
+        }
+
+        public void setPdf_url(String pdf_url) {
+            this.pdf_url = pdf_url;
+        }
+
+        public String getReimburse_status() {
+            return reimburse_status;
+        }
+
+        public void setReimburse_status(String reimburse_status) {
+            this.reimburse_status = reimburse_status;
+        }
+
+        public String getMaker() {
+            return maker;
+        }
+
+        public void setMaker(String maker) {
+            this.maker = maker;
+        }
+
+        public String getCashier() {
+            return cashier;
+        }
+
+        public void setCashier(String cashier) {
+            this.cashier = cashier;
+        }
+
+        public String getRemarks() {
+            return remarks;
+        }
+
+        public void setRemarks(String remarks) {
+            this.remarks = remarks;
+        }
+
+        public String getSeller_bank_account() {
+            return seller_bank_account;
+        }
+
+        public void setSeller_bank_account(String seller_bank_account) {
+            this.seller_bank_account = seller_bank_account;
+        }
+
+        public String getSeller_address_and_phone() {
+            return seller_address_and_phone;
+        }
+
+        public void setSeller_address_and_phone(String seller_address_and_phone) {
+            this.seller_address_and_phone = seller_address_and_phone;
+        }
+
+        public String getSeller_number() {
+            return seller_number;
+        }
+
+        public void setSeller_number(String seller_number) {
+            this.seller_number = seller_number;
+        }
+
+        public String getBuyer_bank_account() {
+            return buyer_bank_account;
+        }
+
+        public void setBuyer_bank_account(String buyer_bank_account) {
+            this.buyer_bank_account = buyer_bank_account;
+        }
+
+        public String getBuyer_address_and_phone() {
+            return buyer_address_and_phone;
+        }
+
+        public void setBuyer_address_and_phone(String buyer_address_and_phone) {
+            this.buyer_address_and_phone = buyer_address_and_phone;
+        }
+
+        public String getBuyer_number() {
+            return buyer_number;
+        }
+
+        public void setBuyer_number(String buyer_number) {
+            this.buyer_number = buyer_number;
+        }
+
+        public String getCheck_code() {
+            return check_code;
+        }
+
+        public void setCheck_code(String check_code) {
+            this.check_code = check_code;
+        }
+
+        public String getTrip_pdf_url() {
+            return trip_pdf_url;
+        }
+
+        public void setTrip_pdf_url(String trip_pdf_url) {
+            this.trip_pdf_url = trip_pdf_url;
+        }
+
+        public String getOrder_id() {
+            return order_id;
+        }
+
+        public void setOrder_id(String order_id) {
+            this.order_id = order_id;
+        }
+    }
+
+    public static class ProductInfo {
+        /**
+         * 项目(商品)名称
+         */
+        private String name;
+        /**
+         * 项目数量
+         */
+        private Double num;
+        /**
+         * 项目单位
+         */
+        private String unit;
+        /**
+         * 金额,以分为单位
+         */
+        private Double fee;
+        /**
+         * 单价,以分为单位
+         */
+        private Double price;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Double getNum() {
+            return num;
+        }
+
+        public void setNum(Double num) {
+            this.num = num;
+        }
+
+        public String getUnit() {
+            return unit;
+        }
+
+        public void setUnit(String unit) {
+            this.unit = unit;
+        }
+
+        public Double getFee() {
+            return fee;
+        }
+
+        public void setFee(Double fee) {
+            this.fee = fee;
+        }
+
+        public Double getPrice() {
+            return price;
+        }
+
+        public void setPrice(Double price) {
+            this.price = price;
+        }
+    }
+}

+ 50 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/BatchUpdateInvoiceStatusReq.java

@@ -0,0 +1,50 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class BatchUpdateInvoiceStatusReq {
+    /**
+     * 用户openid,可用“userid与openid互换接口”获取
+     */
+    private String openId;
+    /**
+     * 发票报销状态 INVOICE_REIMBURSE_INIT:发票初始状态,未锁定;INVOICE_REIMBURSE_LOCK:发票已锁定,无法重复提交报销;INVOICE_REIMBURSE_CLOSURE:发票已核销,从用户卡包中移除
+     */
+    private InvoiceStatus status;
+    private List<Invoice> invoices;
+
+    public BatchUpdateInvoiceStatusReq(String openId, InvoiceStatus status, List<Invoice> invoices) {
+        this.openId = openId;
+        this.status = status;
+        this.invoices = invoices;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(3);
+        data.put("openid", openId);
+        data.put("reimburse_status", status.name());
+        data.put("invoice_list", invoices);
+        return data;
+    }
+
+    public static class Invoice {
+        /**
+         * 发票卡券的card_id
+         */
+        private String cardId;
+        /**
+         * 发票卡券的加密code,和card_id共同构成一张发票卡券的唯一标识
+         */
+        private String encryptCode;
+
+        public Invoice(String cardId, String encryptCode) {
+            this.cardId = cardId;
+            this.encryptCode = encryptCode;
+        }
+    }
+}

+ 61 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateChatReq.java

@@ -0,0 +1,61 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.*;
+
+/**
+ * @author yingp
+ */
+public class CreateChatReq {
+    /**
+     * 群聊名,最多50个utf8字符,超过将截断
+     */
+    private String name;
+    /**
+     * 指定群主的id。如果不指定,系统会随机从userlist中选一人作为群主
+     */
+    private String owner;
+    /**
+     * 群成员id列表。至少2人,至多500
+     */
+    private Set<String> userSet;
+    /**
+     * 群聊的唯一标志,不能与已有的群重复;字符串类型,最长32个字符。只允许字符0-9及字母a-zA-Z。如果不填,系统会随机生成群id
+     */
+    private String chatId;
+
+    public CreateChatReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public CreateChatReq owner(String owner) {
+        this.owner = owner;
+        return this;
+    }
+
+    public CreateChatReq chatId(String chatId) {
+        this.chatId = chatId;
+        return this;
+    }
+
+    public CreateChatReq user(String... userId) {
+        if(null == userSet) {
+            userSet = new HashSet<>(1);
+        }
+        userSet.addAll(Arrays.asList(userId));
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("name", name);
+        if (null != owner) {
+            data.put("owner", owner);
+        }
+        if (null != chatId) {
+            data.put("chatid", chatId);
+        }
+        data.put("userlist", userSet);
+        return data;
+    }
+}

+ 16 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateChatResp.java

@@ -0,0 +1,16 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class CreateChatResp extends BaseResp {
+    private String chatid;
+
+    public String getChatid() {
+        return chatid;
+    }
+
+    public void setChatid(String chatid) {
+        this.chatid = chatid;
+    }
+}

+ 71 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateDepartmentReq.java

@@ -0,0 +1,71 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class CreateDepartmentReq {
+    /**
+     * 部门名称。长度限制为1~32个字符,字符不能包括\:?”<>|
+     */
+    private String name;
+    /**
+     * 英文名称,需要在管理后台开启多语言支持才能生效。长度限制为1~32个字符,字符不能包括\:?”<>|
+     */
+    private String enName;
+    /**
+     * 父部门id,32位整型
+     */
+    private Integer parentId;
+    /**
+     * 在父部门中的次序值。order值大的排序靠前。有效的值范围是[0, 2^32)
+     */
+    private Integer order;
+    /**
+     * 部门id,32位整型,指定时必须大于1。若不填该参数,将自动生成id
+     */
+    private Integer id;
+
+    public CreateDepartmentReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public CreateDepartmentReq enName(String enName) {
+        this.enName = enName;
+        return this;
+    }
+
+    public CreateDepartmentReq parent(Integer parentId) {
+        this.parentId = parentId;
+        return this;
+    }
+
+    public CreateDepartmentReq order(Integer order) {
+        this.order = order;
+        return this;
+    }
+
+    public CreateDepartmentReq id(Integer id) {
+        this.id = id;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(3);
+        data.put("name", name);
+        if (null != enName) {
+            data.put("name_en", enName);
+        }
+        data.put("parentid", parentId);
+        if (null != order) {
+            data.put("order", order);
+        }
+        if (null != id) {
+            data.put("id", id);
+        }
+        return data;
+    }
+}

+ 16 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateDepartmentResp.java

@@ -0,0 +1,16 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class CreateDepartmentResp extends BaseResp {
+    private Integer id;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+}

+ 201 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateUserEvent.java

@@ -0,0 +1,201 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * 新增成员事件
+ *
+ * @author yingp
+ */
+@XmlRootElement(name = "xml")
+public class CreateUserEvent {
+    /**
+     * 企业微信CorpID
+     */
+    private String ToUserName;
+    /**
+     * 此事件该值固定为sys,表示该消息由系统生成
+     */
+    private String FromUserName;
+    /**
+     * 消息创建时间 (整型)
+     */
+    private String CreateTime;
+    /**
+     * 消息的类型,此时固定为event
+     */
+    private String MsgType;
+    /**
+     * 事件的类型,此时固定为change_contact
+     */
+    private String Event;
+    /**
+     * 此时固定为create_user
+     */
+    private String ChangeType;
+    private String UserID;
+    private String Name;
+    private String Department;
+    private String IsLeaderInDept;
+    private String Position;
+    private String Mobile;
+    private String Gender;
+    private String Email;
+    private String Status;
+    private String Avatar;
+    private String Alias;
+    private String Telephone;
+    private String Address;
+
+    public String getToUserName() {
+        return ToUserName;
+    }
+
+    public void setToUserName(String toUserName) {
+        ToUserName = toUserName;
+    }
+
+    public String getFromUserName() {
+        return FromUserName;
+    }
+
+    public void setFromUserName(String fromUserName) {
+        FromUserName = fromUserName;
+    }
+
+    public String getCreateTime() {
+        return CreateTime;
+    }
+
+    public void setCreateTime(String createTime) {
+        CreateTime = createTime;
+    }
+
+    public String getMsgType() {
+        return MsgType;
+    }
+
+    public void setMsgType(String msgType) {
+        MsgType = msgType;
+    }
+
+    public String getEvent() {
+        return Event;
+    }
+
+    public void setEvent(String event) {
+        Event = event;
+    }
+
+    public String getChangeType() {
+        return ChangeType;
+    }
+
+    public void setChangeType(String changeType) {
+        ChangeType = changeType;
+    }
+
+    public String getUserID() {
+        return UserID;
+    }
+
+    public void setUserID(String userID) {
+        UserID = userID;
+    }
+
+    public String getName() {
+        return Name;
+    }
+
+    public void setName(String name) {
+        Name = name;
+    }
+
+    public String getDepartment() {
+        return Department;
+    }
+
+    public void setDepartment(String department) {
+        Department = department;
+    }
+
+    public String getIsLeaderInDept() {
+        return IsLeaderInDept;
+    }
+
+    public void setIsLeaderInDept(String isLeaderInDept) {
+        IsLeaderInDept = isLeaderInDept;
+    }
+
+    public String getPosition() {
+        return Position;
+    }
+
+    public void setPosition(String position) {
+        Position = position;
+    }
+
+    public String getMobile() {
+        return Mobile;
+    }
+
+    public void setMobile(String mobile) {
+        Mobile = mobile;
+    }
+
+    public String getGender() {
+        return Gender;
+    }
+
+    public void setGender(String gender) {
+        Gender = gender;
+    }
+
+    public String getEmail() {
+        return Email;
+    }
+
+    public void setEmail(String email) {
+        Email = email;
+    }
+
+    public String getStatus() {
+        return Status;
+    }
+
+    public void setStatus(String status) {
+        Status = status;
+    }
+
+    public String getAvatar() {
+        return Avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        Avatar = avatar;
+    }
+
+    public String getAlias() {
+        return Alias;
+    }
+
+    public void setAlias(String alias) {
+        Alias = alias;
+    }
+
+    public String getTelephone() {
+        return Telephone;
+    }
+
+    public void setTelephone(String telephone) {
+        Telephone = telephone;
+    }
+
+    public String getAddress() {
+        return Address;
+    }
+
+    public void setAddress(String address) {
+        Address = address;
+    }
+}

+ 221 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/CreateUserReq.java

@@ -0,0 +1,221 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class CreateUserReq {
+    /**
+     * 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节。只能由数字、字母和“_-@.”四种字符组成,且第一个字符必须是数字或字母。
+     */
+    private String userId;
+    /**
+     * 成员名称。长度为1~64个utf8字符
+     */
+    private String name;
+    /**
+     * 成员别名。长度1~32个utf8字符
+     */
+    private String alias;
+    /**
+     * 手机号码。企业内必须唯一,mobile/email二者不能同时为空
+     */
+    private String mobile;
+    /**
+     * 成员所属部门列表,不超过20
+     */
+    private List<Department> department;
+    /**
+     * 职务信息。长度为0~128个字符
+     */
+    private String position;
+    /**
+     * 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示。长度12个汉字内
+     */
+    private String externalPosition;
+    /**
+     * 性别。1表示男性,2表示女性
+     */
+    private Gender gender;
+    /**
+     * 邮箱。长度6~64个字节,且为有效的email格式。企业内必须唯一,mobile/email二者不能同时为空
+     */
+    private String email;
+    /**
+     * 座机。32字节以内,由纯数字或’-‘号组成
+     */
+    private String telephone;
+    /**
+     * 成员头像的mediaid,通过素材管理接口上传图片获得的mediaid
+     */
+    private String avatarMediaId;
+    /**
+     * 启用/禁用成员。1表示启用成员,0表示禁用成员
+     */
+    private Boolean enable;
+    /**
+     * 地址。长度最大128个字符
+     */
+    private String address;
+    /**
+     * 是否邀请该成员使用企业微信(将通过微信服务通知或短信或邮件下发邀请,每天自动下发一次,最多持续3个工作日),默认值为true
+     */
+    private Boolean toInvite;
+
+    public CreateUserReq userId(String userId) {
+        this.userId = userId;
+        return this;
+    }
+
+    public CreateUserReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public CreateUserReq alias(String alias) {
+        this.alias = alias;
+        return this;
+    }
+
+    public CreateUserReq mobile(String mobile) {
+        this.mobile = mobile;
+        return this;
+    }
+
+    public CreateUserReq email(String email) {
+        this.email = email;
+        return this;
+    }
+
+    public CreateUserReq department(List<Department> department) {
+        this.department = department;
+        return this;
+    }
+
+    public CreateUserReq position(String position) {
+        this.position = position;
+        return this;
+    }
+
+    public CreateUserReq externalPosition(String externalPosition) {
+        this.externalPosition = externalPosition;
+        return this;
+    }
+
+    public CreateUserReq gender(Gender gender) {
+        this.gender = gender;
+        return this;
+    }
+
+    public CreateUserReq telephone(String telephone) {
+        this.telephone = telephone;
+        return this;
+    }
+
+    public CreateUserReq avatar(String avatarMediaId) {
+        this.avatarMediaId = avatarMediaId;
+        return this;
+    }
+
+    public CreateUserReq address(String address) {
+        this.address = address;
+        return this;
+    }
+
+    public CreateUserReq toInvite(boolean toInvite) {
+        this.toInvite = toInvite;
+        return this;
+    }
+
+    public CreateUserReq enable(boolean enable) {
+        this.enable = enable;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(8);
+        data.put("userid", userId);
+        data.put("name", name);
+        if (null != alias) {
+            data.put("alias", alias);
+        }
+        if (null != mobile) {
+            data.put("mobile", mobile);
+        }
+        if (null != email) {
+            data.put("email", email);
+        }
+        if (null != department) {
+            List<Integer> idList = new ArrayList<>(department.size());
+            List<Integer> orderList = new ArrayList<>(department.size());
+            List<Integer> leaderList = new ArrayList<>(department.size());
+            department.forEach(dept -> {
+                idList.add(dept.id);
+                orderList.add(dept.order);
+                leaderList.add(dept.leader ? 1 : 0);
+            });
+            data.put("department", idList);
+            data.put("order", orderList);
+            data.put("is_leader_in_dept", leaderList);
+        }
+        if (null != gender) {
+            data.put("gender", gender.code);
+        }
+        if (null != telephone) {
+            data.put("telephone", telephone);
+        }
+        if (null != avatarMediaId) {
+            data.put("avatar_mediaid", avatarMediaId);
+        }
+        if (null != enable) {
+            data.put("enable", enable ? 1 : 0);
+        }
+        if (null != toInvite) {
+            data.put("to_invite", toInvite);
+        }
+        if (null != address) {
+            data.put("address", address);
+        }
+        if (null != position) {
+            data.put("position", position);
+        }
+        if (null != externalPosition) {
+            data.put("external_position", externalPosition);
+        }
+        return data;
+    }
+
+    public enum Gender {
+        MALE("1"), FEMALE("2");
+        private final String code;
+
+        Gender(String code) {
+            this.code = code;
+        }
+    }
+
+    public static class Department {
+        /**
+         * 成员所属部门id
+         */
+        private Integer id;
+        /**
+         * 部门内的排序值,默认为0,成员次序以创建时间从小到大排列,数值越大排序越前面。有效的值范围是[0, 2^32)
+         */
+        private Integer order;
+        /**
+         * 表示在所在的部门内是否为上级。1表示为上级,0表示非上级。在审批等应用里可以用来标识上级审批人
+         */
+        private boolean leader;
+
+        public Department(Integer id, Integer order, boolean leader) {
+            this.id = id;
+            this.order = order;
+            this.leader = leader;
+        }
+    }
+}

+ 25 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetAccessTokenResp.java

@@ -0,0 +1,25 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class GetAccessTokenResp extends BaseResp {
+    private String access_token;
+    private int expires_in;
+
+    public String getAccess_token() {
+        return access_token;
+    }
+
+    public void setAccess_token(String access_token) {
+        this.access_token = access_token;
+    }
+
+    public int getExpires_in() {
+        return expires_in;
+    }
+
+    public void setExpires_in(int expires_in) {
+        this.expires_in = expires_in;
+    }
+}

+ 451 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetApprovalTemplateResp.java

@@ -0,0 +1,451 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ * @date 2020/2/12
+ */
+public class GetApprovalTemplateResp extends BaseResp {
+    /**
+     * 模板名称,若配置了多语言则会包含中英文的模板名称,默认为zh_CN中文
+     */
+    private List<MultilingualText> template_names;
+    /**
+     * 模板控件信息
+     */
+    private TemplateContent template_content;
+
+    public List<MultilingualText> getTemplate_names() {
+        return template_names;
+    }
+
+    public void setTemplate_names(List<MultilingualText> template_names) {
+        this.template_names = template_names;
+    }
+
+    public TemplateContent getTemplate_content() {
+        return template_content;
+    }
+
+    public void setTemplate_content(TemplateContent template_content) {
+        this.template_content = template_content;
+    }
+
+    public static class MultilingualText {
+        private String text;
+        private String lang;
+
+        public String getText() {
+            return text;
+        }
+
+        public void setText(String text) {
+            this.text = text;
+        }
+
+        public String getLang() {
+            return lang;
+        }
+
+        public void setLang(String lang) {
+            this.lang = lang;
+        }
+    }
+
+    public static class TemplateContent {
+        /**
+         * 模板控件数组。模板详情由多个不同类型的控件组成,控件类型详细说明见附录。
+         */
+        private List<Control> controls;
+
+        public List<Control> getControls() {
+            return controls;
+        }
+
+        public void setControls(List<Control> controls) {
+            this.controls = controls;
+        }
+    }
+
+    public static class Control {
+        /**
+         * 模板控件属性,包含了模板内控件的各种属性信息
+         */
+        private ControlProperty property;
+        /**
+         * 模板控件配置,包含了部分控件类型的附加类型、属性,详见附录说明。目前有配置信息的控件类型有:Date-日期/日期+时间;Selector-单选/多选;Contact-成员/部门;Table-明细;Attendance-假勤组件(请假、外出、出差、加班)
+         */
+        private ControlConfig config;
+
+        public ControlProperty getProperty() {
+            return property;
+        }
+
+        public void setProperty(ControlProperty property) {
+            this.property = property;
+        }
+
+        public ControlConfig getConfig() {
+            return config;
+        }
+
+        public void setConfig(ControlConfig config) {
+            this.config = config;
+        }
+    }
+
+    public static class ControlProperty {
+        /**
+         * 控件类型:Text-文本;Textarea-多行文本;Number-数字;Money-金额;Date-日期/日期+时间;Selector-单选/多选;Contact-成员/部门;Tips-说明文字;File-附件;Table-明细;Attendance-假勤控件;Vacation-请假控件
+         */
+        private String control;
+        /**
+         * 控件id
+         */
+        private String id;
+        /**
+         * 控件名称,若配置了多语言则会包含中英文的控件名称,默认为zh_CN中文
+         */
+        private List<MultilingualText> title;
+        /**
+         * 控件说明,向申请者展示的控件填写说明,若配置了多语言则会包含中英文的控件说明,默认为zh_CN中文
+         */
+        private List<MultilingualText> placeholder;
+        /**
+         * 是否必填:1-必填;0-非必填
+         */
+        private Integer require;
+        /**
+         * 是否参与打印:1-不参与打印;0-参与打印
+         */
+        private Integer un_print;
+
+        public String getControl() {
+            return control;
+        }
+
+        public void setControl(String control) {
+            this.control = control;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public List<MultilingualText> getTitle() {
+            return title;
+        }
+
+        public void setTitle(List<MultilingualText> title) {
+            this.title = title;
+        }
+
+        public List<MultilingualText> getPlaceholder() {
+            return placeholder;
+        }
+
+        public void setPlaceholder(List<MultilingualText> placeholder) {
+            this.placeholder = placeholder;
+        }
+
+        public Integer getRequire() {
+            return require;
+        }
+
+        public void setRequire(Integer require) {
+            this.require = require;
+        }
+
+        public Integer getUn_print() {
+            return un_print;
+        }
+
+        public void setUn_print(Integer un_print) {
+            this.un_print = un_print;
+        }
+    }
+
+    public static class ControlConfig {
+        /**
+         * 类型标志,日期/日期+时间控件的config中会包含此参数
+         */
+        private DateControl date;
+        /**
+         * 类型标志,单选/多选控件的config中会包含此参数
+         */
+        private SelectorControl selector;
+        /**
+         * 类型标志,单选/多选控件的config中会包含此参数
+         */
+        private ContactControl contact;
+        /**
+         * 类型标志,明细控件的config中会包含此参数
+         */
+        private TableControl table;
+        /**
+         * 类型标志,假勤控件的config中会包含此参数
+         */
+        private AttendanceControl attendance;
+        /**
+         * 假期类型数组
+         */
+        private VacationControl vacation_list;
+
+        public DateControl getDate() {
+            return date;
+        }
+
+        public void setDate(DateControl date) {
+            this.date = date;
+        }
+
+        public SelectorControl getSelector() {
+            return selector;
+        }
+
+        public void setSelector(SelectorControl selector) {
+            this.selector = selector;
+        }
+
+        public ContactControl getContact() {
+            return contact;
+        }
+
+        public void setContact(ContactControl contact) {
+            this.contact = contact;
+        }
+
+        public TableControl getTable() {
+            return table;
+        }
+
+        public void setTable(TableControl table) {
+            this.table = table;
+        }
+
+        public AttendanceControl getAttendance() {
+            return attendance;
+        }
+
+        public void setAttendance(AttendanceControl attendance) {
+            this.attendance = attendance;
+        }
+
+        public VacationControl getVacation_list() {
+            return vacation_list;
+        }
+
+        public void setVacation_list(VacationControl vacation_list) {
+            this.vacation_list = vacation_list;
+        }
+    }
+
+    public static class DateControl {
+        /**
+         * 时间展示类型:day-日期;hour-日期+时间
+         */
+        private String type;
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+    }
+
+    public static class SelectorControl {
+        /**
+         * 选择类型:single-单选;multi-多选
+         */
+        private String type;
+        /**
+         * 选项,包含单选/多选控件中的所有选项,可能有多个
+         */
+        private List<SelectorOption> options;
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        public List<SelectorOption> getOptions() {
+            return options;
+        }
+
+        public void setOptions(List<SelectorOption> options) {
+            this.options = options;
+        }
+    }
+
+    public static class SelectorOption {
+        /**
+         * 选项key,选项的唯一id,可用于发起审批申请,为单选/多选控件赋值
+         */
+        private String key;
+        /**
+         * 选项值,若配置了多语言则会包含中英文的选项值,默认为zh_CN中文
+         */
+        private List<MultilingualText> value;
+
+        public String getKey() {
+            return key;
+        }
+
+        public void setKey(String key) {
+            this.key = key;
+        }
+
+        public List<MultilingualText> getValue() {
+            return value;
+        }
+
+        public void setValue(List<MultilingualText> value) {
+            this.value = value;
+        }
+    }
+
+    public static class ContactControl {
+        /**
+         * 选择方式:single-单选;multi-多选
+         */
+        private String type;
+        /**
+         * 选择对象:user-成员;department-部门
+         */
+        private String mode;
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        public String getMode() {
+            return mode;
+        }
+
+        public void setMode(String mode) {
+            this.mode = mode;
+        }
+    }
+
+    public static class TableControl {
+        /**
+         * 明细内的子控件,内部结构同controls
+         */
+        private List<Control> children;
+        private List<String> stat_field;
+
+        public List<Control> getChildren() {
+            return children;
+        }
+
+        public void setChildren(List<Control> children) {
+            this.children = children;
+        }
+
+        public List<String> getStat_field() {
+            return stat_field;
+        }
+
+        public void setStat_field(List<String> stat_field) {
+            this.stat_field = stat_field;
+        }
+    }
+
+    public static class AttendanceControl {
+        /**
+         * 假期控件属性
+         */
+        private AttendanceDateRange date_range;
+        /**
+         * 假勤控件类型:1-请假,3-出差,4-外出,5-加班
+         */
+        private Integer type;
+
+        public AttendanceDateRange getDate_range() {
+            return date_range;
+        }
+
+        public void setDate_range(AttendanceDateRange date_range) {
+            this.date_range = date_range;
+        }
+
+        public Integer getType() {
+            return type;
+        }
+
+        public void setType(Integer type) {
+            this.type = type;
+        }
+    }
+
+    public static class AttendanceDateRange {
+        /**
+         * 时间刻度:hour-精确到分钟, halfday—上午/下午
+         */
+        private String type;
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+    }
+
+    public static class VacationControl {
+        /**
+         * 单个假期类型属性
+         */
+        private List<VacationItem> item;
+
+        public List<VacationItem> getItem() {
+            return item;
+        }
+
+        public void setItem(List<VacationItem> item) {
+            this.item = item;
+        }
+    }
+
+    public  static class VacationItem {
+        /**
+         * 假期类型标识id
+         */
+        private Integer id;
+        /**
+         * 假期类型名称,默认zh_CN中文名称
+         */
+        private List<MultilingualText> name;
+
+        public Integer getId() {
+            return id;
+        }
+
+        public void setId(Integer id) {
+            this.id = id;
+        }
+
+        public List<MultilingualText> getName() {
+            return name;
+        }
+
+        public void setName(List<MultilingualText> name) {
+            this.name = name;
+        }
+    }
+}

+ 88 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCalendarListResp.java

@@ -0,0 +1,88 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetCalendarListResp extends BaseResp {
+
+    private List<Calendar> calendar_list;
+
+    public List<Calendar> getCalendar_list() {
+        return calendar_list;
+    }
+
+    public void setCalendar_list(List<Calendar> calendar_list) {
+        this.calendar_list = calendar_list;
+    }
+
+    public static class Calendar {
+        private String cal_id;
+        private String organizer;
+        private String summary;
+        private String color;
+        private String description;
+        private List<Share> shares;
+
+        public String getCal_id() {
+            return cal_id;
+        }
+
+        public void setCal_id(String cal_id) {
+            this.cal_id = cal_id;
+        }
+
+        public String getOrganizer() {
+            return organizer;
+        }
+
+        public void setOrganizer(String organizer) {
+            this.organizer = organizer;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public void setSummary(String summary) {
+            this.summary = summary;
+        }
+
+        public String getColor() {
+            return color;
+        }
+
+        public void setColor(String color) {
+            this.color = color;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public List<Share> getShares() {
+            return shares;
+        }
+
+        public void setShares(List<Share> shares) {
+            this.shares = shares;
+        }
+    }
+
+    public static class Share {
+        private String userid;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+    }
+}

+ 67 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetChatResp.java

@@ -0,0 +1,67 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetChatResp extends BaseResp {
+
+    private Chat chat_info;
+
+    public Chat getChat_info() {
+        return chat_info;
+    }
+
+    public void setChat_info(Chat chat_info) {
+        this.chat_info = chat_info;
+    }
+
+    public static class Chat {
+        private String chatid;
+        /**
+         * 群聊名
+         */
+        private String name;
+        /**
+         * 群主id
+         */
+        private String owner;
+        /**
+         * 群成员id列表
+         */
+        private List<String> userlist;
+
+        public String getChatid() {
+            return chatid;
+        }
+
+        public void setChatid(String chatid) {
+            this.chatid = chatid;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getOwner() {
+            return owner;
+        }
+
+        public void setOwner(String owner) {
+            this.owner = owner;
+        }
+
+        public List<String> getUserlist() {
+            return userlist;
+        }
+
+        public void setUserlist(List<String> userlist) {
+            this.userlist = userlist;
+        }
+    }
+}

+ 55 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinDataReq.java

@@ -0,0 +1,55 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class GetCheckinDataReq {
+
+    private final CheckinType type;
+    private final long startTime;
+    private final long endTime;
+    private final List<String> userList;
+
+    public GetCheckinDataReq(CheckinType type, long startTime, long endTime, List<String> userList) {
+        this.type = type;
+        this.startTime = startTime;
+        this.endTime = endTime;
+        this.userList = userList;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("opencheckindatatype", type.code);
+        data.put("starttime", startTime);
+        data.put("endtime", endTime);
+        data.put("useridlist", userList);
+        return data;
+    }
+
+    /**
+     * 打卡类型
+     */
+    public enum CheckinType {
+        /**
+         * 上下班
+         */
+        COMMUTE(1),
+        /**
+         * 外出
+         */
+        OUT(2),
+        /**
+         * 全部
+         */
+        ALL(3);
+        private final int code;
+
+        CheckinType(int code) {
+            this.code = code;
+        }
+    }
+}

+ 175 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinDataResp.java

@@ -0,0 +1,175 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetCheckinDataResp extends BaseResp{
+
+    private List<CheckinData> checkindata;
+
+    public List<CheckinData> getCheckindata() {
+        return checkindata;
+    }
+
+    public void setCheckindata(List<CheckinData> checkindata) {
+        this.checkindata = checkindata;
+    }
+
+    public static class CheckinData {
+        private String userid;
+        /**
+         * 打卡规则名称
+         */
+        private String groupname;
+        /**
+         * 打卡类型。字符串,目前有:上班打卡,下班打卡,外出打卡
+         */
+        private String checkin_type;
+        /**
+         * 异常类型,字符串,包括:时间异常,地点异常,未打卡,wifi异常,非常用设备。如果有多个异常,以分号间隔
+         */
+        private String exception_type;
+        /**
+         * 打卡时间。Unix时间戳
+         */
+        private Long checkin_time;
+        /**
+         * 打卡地点title
+         */
+        private String location_title;
+        /**
+         * 打卡地点详情
+         */
+        private String location_detail;
+        /**
+         * 打卡wifi名称
+         */
+        private String wifiname;
+        /**
+         * 打卡备注
+         */
+        private String notes;
+        /**
+         * 打卡的MAC地址/bssid
+         */
+        private String wifimac;
+        /**
+         * 打卡的附件media_id,可使用media/get获取附件
+         */
+        private List<String> mediaids;
+        /**
+         * 位置打卡地点纬度,是实际纬度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+         */
+        private Long lat;
+        /**
+         * 位置打卡地点经度,是实际经度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+         */
+        private Long lng;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public String getGroupname() {
+            return groupname;
+        }
+
+        public void setGroupname(String groupname) {
+            this.groupname = groupname;
+        }
+
+        public String getCheckin_type() {
+            return checkin_type;
+        }
+
+        public void setCheckin_type(String checkin_type) {
+            this.checkin_type = checkin_type;
+        }
+
+        public String getException_type() {
+            return exception_type;
+        }
+
+        public void setException_type(String exception_type) {
+            this.exception_type = exception_type;
+        }
+
+        public Long getCheckin_time() {
+            return checkin_time;
+        }
+
+        public void setCheckin_time(Long checkin_time) {
+            this.checkin_time = checkin_time;
+        }
+
+        public String getLocation_title() {
+            return location_title;
+        }
+
+        public void setLocation_title(String location_title) {
+            this.location_title = location_title;
+        }
+
+        public String getLocation_detail() {
+            return location_detail;
+        }
+
+        public void setLocation_detail(String location_detail) {
+            this.location_detail = location_detail;
+        }
+
+        public String getWifiname() {
+            return wifiname;
+        }
+
+        public void setWifiname(String wifiname) {
+            this.wifiname = wifiname;
+        }
+
+        public String getNotes() {
+            return notes;
+        }
+
+        public void setNotes(String notes) {
+            this.notes = notes;
+        }
+
+        public String getWifimac() {
+            return wifimac;
+        }
+
+        public void setWifimac(String wifimac) {
+            this.wifimac = wifimac;
+        }
+
+        public List<String> getMediaids() {
+            return mediaids;
+        }
+
+        public void setMediaids(List<String> mediaids) {
+            this.mediaids = mediaids;
+        }
+
+        public Long getLat() {
+            return lat;
+        }
+
+        public void setLat(Long lat) {
+            this.lat = lat;
+        }
+
+        public Long getLng() {
+            return lng;
+        }
+
+        public void setLng(Long lng) {
+            this.lng = lng;
+        }
+    }
+}

+ 25 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinOptionReq.java

@@ -0,0 +1,25 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class GetCheckinOptionReq {
+    private List<String> userIdList;
+    private Long datetime;
+
+    public GetCheckinOptionReq(long datetime, List<String> userIdList) {
+        this.datetime = datetime;
+        this.userIdList = userIdList;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(2);
+        data.put("datetime", datetime);
+        data.put("useridlist", userIdList);
+        return data;
+    }
+}

+ 439 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetCheckinOptionResp.java

@@ -0,0 +1,439 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetCheckinOptionResp extends BaseResp{
+
+    private List<CheckinOption> info;
+
+    public List<CheckinOption> getInfo() {
+        return info;
+    }
+
+    public void setInfo(List<CheckinOption> info) {
+        this.info = info;
+    }
+
+    public static class CheckinOption {
+        /**
+         * 用户id
+         */
+        private String userid;
+        /**
+         * 打卡规则
+         */
+        private CheckinGroup group;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public CheckinGroup getGroup() {
+            return group;
+        }
+
+        public void setGroup(CheckinGroup group) {
+            this.group = group;
+        }
+    }
+
+    public static class CheckinGroup {
+        /**
+         * 打卡规则类型。1:固定时间上下班;2:按班次上下班;3:自由上下班 。
+         */
+        private Integer grouptype;
+        /**
+         * 打卡规则id
+         */
+        private Integer groupid;
+        /**
+         * 打卡时间
+         */
+        private List<CheckinDate> checkindate;
+        /**
+         * 特殊日期
+         */
+        private List<SpecialDay> spe_workdays;
+        private List<SpecialDay> spe_offdays;
+        /**
+         * 是否同步法定节假日
+         */
+        private Boolean sync_holidays;
+        /**
+         * 打卡规则名称
+         */
+        private String groupname;
+        /**
+         * 是否打卡必须拍照
+         */
+        private Boolean need_photo;
+        /**
+         * WiFi打卡地点信息
+         */
+        private List<WifiMacInfo> wifimac_infos;
+        /**
+         * 是否备注时允许上传本地图片
+         */
+        private Boolean note_can_use_local_pic;
+        /**
+         * 是否非工作日允许打卡
+         */
+        private Boolean allow_checkin_offworkday;
+        /**
+         * 是否允许异常打卡时提交申请
+         */
+        private Boolean allow_apply_offworkday;
+        /**
+         * 位置打卡地点信息
+         */
+        private List<LocInfo> loc_infos;
+
+        public Integer getGrouptype() {
+            return grouptype;
+        }
+
+        public void setGrouptype(Integer grouptype) {
+            this.grouptype = grouptype;
+        }
+
+        public Integer getGroupid() {
+            return groupid;
+        }
+
+        public void setGroupid(Integer groupid) {
+            this.groupid = groupid;
+        }
+
+        public List<CheckinDate> getCheckindate() {
+            return checkindate;
+        }
+
+        public void setCheckindate(List<CheckinDate> checkindate) {
+            this.checkindate = checkindate;
+        }
+
+        public List<SpecialDay> getSpe_workdays() {
+            return spe_workdays;
+        }
+
+        public void setSpe_workdays(List<SpecialDay> spe_workdays) {
+            this.spe_workdays = spe_workdays;
+        }
+
+        public List<SpecialDay> getSpe_offdays() {
+            return spe_offdays;
+        }
+
+        public void setSpe_offdays(List<SpecialDay> spe_offdays) {
+            this.spe_offdays = spe_offdays;
+        }
+
+        public Boolean getSync_holidays() {
+            return sync_holidays;
+        }
+
+        public void setSync_holidays(Boolean sync_holidays) {
+            this.sync_holidays = sync_holidays;
+        }
+
+        public String getGroupname() {
+            return groupname;
+        }
+
+        public void setGroupname(String groupname) {
+            this.groupname = groupname;
+        }
+
+        public Boolean getNeed_photo() {
+            return need_photo;
+        }
+
+        public void setNeed_photo(Boolean need_photo) {
+            this.need_photo = need_photo;
+        }
+
+        public List<WifiMacInfo> getWifimac_infos() {
+            return wifimac_infos;
+        }
+
+        public void setWifimac_infos(List<WifiMacInfo> wifimac_infos) {
+            this.wifimac_infos = wifimac_infos;
+        }
+
+        public Boolean getNote_can_use_local_pic() {
+            return note_can_use_local_pic;
+        }
+
+        public void setNote_can_use_local_pic(Boolean note_can_use_local_pic) {
+            this.note_can_use_local_pic = note_can_use_local_pic;
+        }
+
+        public Boolean getAllow_checkin_offworkday() {
+            return allow_checkin_offworkday;
+        }
+
+        public void setAllow_checkin_offworkday(Boolean allow_checkin_offworkday) {
+            this.allow_checkin_offworkday = allow_checkin_offworkday;
+        }
+
+        public Boolean getAllow_apply_offworkday() {
+            return allow_apply_offworkday;
+        }
+
+        public void setAllow_apply_offworkday(Boolean allow_apply_offworkday) {
+            this.allow_apply_offworkday = allow_apply_offworkday;
+        }
+
+        public List<LocInfo> getLoc_infos() {
+            return loc_infos;
+        }
+
+        public void setLoc_infos(List<LocInfo> loc_infos) {
+            this.loc_infos = loc_infos;
+        }
+    }
+
+    public static class CheckinDate {
+        /**
+         * 工作日。若为固定时间上下班或自由上下班,则16分别表示星期一到星期六,0表示星期日;若为按班次上下班,则表示拉取班次的日期
+         */
+        private List<Integer> workdays;
+        private List<CheckinTime> checkintime;
+        /**
+         * 弹性时间(毫秒)
+         */
+        private Long flex_time;
+        /**
+         * 下班不需要打卡
+         */
+        private Boolean noneed_offwork;
+        /**
+         * 打卡时间限制(毫秒)
+         */
+        private Long limit_aheadtime;
+
+        public List<Integer> getWorkdays() {
+            return workdays;
+        }
+
+        public void setWorkdays(List<Integer> workdays) {
+            this.workdays = workdays;
+        }
+
+        public List<CheckinTime> getCheckintime() {
+            return checkintime;
+        }
+
+        public void setCheckintime(List<CheckinTime> checkintime) {
+            this.checkintime = checkintime;
+        }
+
+        public Long getFlex_time() {
+            return flex_time;
+        }
+
+        public void setFlex_time(Long flex_time) {
+            this.flex_time = flex_time;
+        }
+
+        public Boolean getNoneed_offwork() {
+            return noneed_offwork;
+        }
+
+        public void setNoneed_offwork(Boolean noneed_offwork) {
+            this.noneed_offwork = noneed_offwork;
+        }
+
+        public Long getLimit_aheadtime() {
+            return limit_aheadtime;
+        }
+
+        public void setLimit_aheadtime(Long limit_aheadtime) {
+            this.limit_aheadtime = limit_aheadtime;
+        }
+    }
+
+    public static class CheckinTime {
+        /**
+         * 上班时间,表示为距离当天0点的秒数。
+         */
+        private Integer work_sec;
+        /**
+         * 下班时间,表示为距离当天0点的秒数。
+         */
+        private Integer off_work_sec;
+        /**
+         * 上班提醒时间,表示为距离当天0点的秒数。
+         */
+        private Integer remind_work_sec;
+        /**
+         * 下班提醒时间,表示为距离当天0点的秒数。
+         */
+        private Integer remind_off_work_sec;
+
+        public Integer getWork_sec() {
+            return work_sec;
+        }
+
+        public void setWork_sec(Integer work_sec) {
+            this.work_sec = work_sec;
+        }
+
+        public Integer getOff_work_sec() {
+            return off_work_sec;
+        }
+
+        public void setOff_work_sec(Integer off_work_sec) {
+            this.off_work_sec = off_work_sec;
+        }
+
+        public Integer getRemind_work_sec() {
+            return remind_work_sec;
+        }
+
+        public void setRemind_work_sec(Integer remind_work_sec) {
+            this.remind_work_sec = remind_work_sec;
+        }
+
+        public Integer getRemind_off_work_sec() {
+            return remind_off_work_sec;
+        }
+
+        public void setRemind_off_work_sec(Integer remind_off_work_sec) {
+            this.remind_off_work_sec = remind_off_work_sec;
+        }
+    }
+
+    public static class SpecialDay {
+        /**
+         * 特殊日期具体时间
+         */
+        private Long timestamp;
+        /**
+         * 特殊日期备注
+         */
+        private String notes;
+        private List<CheckinTime> checkintime;
+
+        public Long getTimestamp() {
+            return timestamp;
+        }
+
+        public void setTimestamp(Long timestamp) {
+            this.timestamp = timestamp;
+        }
+
+        public String getNotes() {
+            return notes;
+        }
+
+        public void setNotes(String notes) {
+            this.notes = notes;
+        }
+
+        public List<CheckinTime> getCheckintime() {
+            return checkintime;
+        }
+
+        public void setCheckintime(List<CheckinTime> checkintime) {
+            this.checkintime = checkintime;
+        }
+    }
+
+    public static class WifiMacInfo {
+        /**
+         * WiFi打卡地点名称
+         */
+        private String wifiname;
+        /**
+         * WiFi打卡地点MAC地址/bssid
+         */
+        private String wifimac;
+
+        public String getWifiname() {
+            return wifiname;
+        }
+
+        public void setWifiname(String wifiname) {
+            this.wifiname = wifiname;
+        }
+
+        public String getWifimac() {
+            return wifimac;
+        }
+
+        public void setWifimac(String wifimac) {
+            this.wifimac = wifimac;
+        }
+    }
+
+    public static class LocInfo {
+        /**
+         * 位置打卡地点纬度,是实际纬度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+         */
+        private Long lat;
+        /**
+         * 位置打卡地点经度,是实际经度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+         */
+        private Long lng;
+        /**
+         * 位置打卡地点名称
+         */
+        private String loc_title;
+        /**
+         * 位置打卡地点详情
+         */
+        private String loc_detail;
+        /**
+         * 允许打卡范围(米)
+         */
+        private Integer distance;
+
+        public Long getLat() {
+            return lat;
+        }
+
+        public void setLat(Long lat) {
+            this.lat = lat;
+        }
+
+        public Long getLng() {
+            return lng;
+        }
+
+        public void setLng(Long lng) {
+            this.lng = lng;
+        }
+
+        public String getLoc_title() {
+            return loc_title;
+        }
+
+        public void setLoc_title(String loc_title) {
+            this.loc_title = loc_title;
+        }
+
+        public String getLoc_detail() {
+            return loc_detail;
+        }
+
+        public void setLoc_detail(String loc_detail) {
+            this.loc_detail = loc_detail;
+        }
+
+        public Integer getDistance() {
+            return distance;
+        }
+
+        public void setDistance(Integer distance) {
+            this.distance = distance;
+        }
+    }
+}

+ 72 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetDepartmentListResp.java

@@ -0,0 +1,72 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetDepartmentListResp extends BaseResp {
+    private List<Department> department;
+
+    public List<Department> getDepartment() {
+        return department;
+    }
+
+    public void setDepartment(List<Department> department) {
+        this.department = department;
+    }
+
+    public static class Department {
+        private Integer id;
+        private String name;
+        private String name_en;
+        /**
+         * 父亲部门id。根部门为1
+         */
+        private Integer parentid;
+        /**
+         * 在父部门中的次序值。order值大的排序靠前。值范围是[0, 2^32)
+         */
+        private Integer order;
+
+        public Integer getId() {
+            return id;
+        }
+
+        public void setId(Integer id) {
+            this.id = id;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getName_en() {
+            return name_en;
+        }
+
+        public void setName_en(String name_en) {
+            this.name_en = name_en;
+        }
+
+        public Integer getParentid() {
+            return parentid;
+        }
+
+        public void setParentid(Integer parentid) {
+            this.parentid = parentid;
+        }
+
+        public Integer getOrder() {
+            return order;
+        }
+
+        public void setOrder(Integer order) {
+            this.order = order;
+        }
+    }
+}

+ 435 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetInvoiceInfoResp.java

@@ -0,0 +1,435 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetInvoiceInfoResp extends BaseResp {
+    /**
+     * 发票id
+     */
+    private String card_id;
+    /**
+     * 发票的有效期起始时间
+     */
+    private Long begin_time;
+    /**
+     * 发票的有效期截止时间
+     */
+    private Long end_time;
+    /**
+     * 用户标识
+     */
+    private String openid;
+    /**
+     * 发票类型,如广东增值税普通发票
+     */
+    private String type;
+    /**
+     * 发票的收款方
+     */
+    private String payee;
+    /**
+     * 发票详情
+     */
+    private String detail;
+    /**
+     * 发票的用户信息
+     */
+    private UserInfo user_info;
+
+    public String getCard_id() {
+        return card_id;
+    }
+
+    public void setCard_id(String card_id) {
+        this.card_id = card_id;
+    }
+
+    public Long getBegin_time() {
+        return begin_time;
+    }
+
+    public void setBegin_time(Long begin_time) {
+        this.begin_time = begin_time;
+    }
+
+    public Long getEnd_time() {
+        return end_time;
+    }
+
+    public void setEnd_time(Long end_time) {
+        this.end_time = end_time;
+    }
+
+    public String getOpenid() {
+        return openid;
+    }
+
+    public void setOpenid(String openid) {
+        this.openid = openid;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getPayee() {
+        return payee;
+    }
+
+    public void setPayee(String payee) {
+        this.payee = payee;
+    }
+
+    public String getDetail() {
+        return detail;
+    }
+
+    public void setDetail(String detail) {
+        this.detail = detail;
+    }
+
+    public UserInfo getUser_info() {
+        return user_info;
+    }
+
+    public void setUser_info(UserInfo user_info) {
+        this.user_info = user_info;
+    }
+
+    public static class UserInfo {
+        /**
+         * 发票加税合计金额,以分为单位
+         */
+        private Double fee;
+        /**
+         * 发票的抬头
+         */
+        private String title;
+        /**
+         * 开票时间,为十位时间戳
+         */
+        private Long billing_time;
+        /**
+         * 发票代码
+         */
+        private String billing_no;
+        /**
+         * 发票号码
+         */
+        private String billing_code;
+        /**
+         * 商品信息结构
+         */
+        private List<ProductInfo> info;
+        /**
+         * 不含税金额,以分为单位
+         */
+        private Double fee_without_tax;
+        /**
+         * 税额,以分为单位
+         */
+        private Double tax;
+        /**
+         * 发票详情,一般描述的是发票的使用说明
+         */
+        private String detail;
+        /**
+         * 这张发票对应的PDF_URL
+         */
+        private String pdf_url;
+        /**
+         * 发报销状态INVOICE_REIMBURSE_INIT:发票初始状态,未锁定;INVOICE_REIMBURSE_LOCK:发票已锁定;INVOICE_REIMBURSE_CLOSURE:发票已核销
+         */
+        private String reimburse_status;
+        /**
+         * 开票人,发票右下角处
+         */
+        private String maker;
+        /**
+         * 收款人,发票左下角处
+         */
+        private String cashier;
+        /**
+         * 备注
+         */
+        private String remarks;
+        /**
+         * 销售方开户行及账号
+         */
+        private String seller_bank_account;
+        /**
+         * 销售方地址、电话
+         */
+        private String seller_address_and_phone;
+        /**
+         * 销售方纳税人识别号
+         */
+        private String seller_number;
+        /**
+         * 购买方开户行及账号
+         */
+        private String buyer_bank_account;
+        /**
+         * 购买方地址、电话
+         */
+        private String buyer_address_and_phone;
+        /**
+         * 购买方纳税人识别号
+         */
+        private String buyer_number;
+        /**
+         * 校验码
+         */
+        private String check_code;
+        /**
+         * 其它消费凭证附件对应的URL,如行程单、水单等
+         */
+        private String trip_pdf_url;
+
+        public Double getFee() {
+            return fee;
+        }
+
+        public void setFee(Double fee) {
+            this.fee = fee;
+        }
+
+        public String getTitle() {
+            return title;
+        }
+
+        public void setTitle(String title) {
+            this.title = title;
+        }
+
+        public Long getBilling_time() {
+            return billing_time;
+        }
+
+        public void setBilling_time(Long billing_time) {
+            this.billing_time = billing_time;
+        }
+
+        public String getBilling_no() {
+            return billing_no;
+        }
+
+        public void setBilling_no(String billing_no) {
+            this.billing_no = billing_no;
+        }
+
+        public String getBilling_code() {
+            return billing_code;
+        }
+
+        public void setBilling_code(String billing_code) {
+            this.billing_code = billing_code;
+        }
+
+        public List<ProductInfo> getInfo() {
+            return info;
+        }
+
+        public void setInfo(List<ProductInfo> info) {
+            this.info = info;
+        }
+
+        public Double getFee_without_tax() {
+            return fee_without_tax;
+        }
+
+        public void setFee_without_tax(Double fee_without_tax) {
+            this.fee_without_tax = fee_without_tax;
+        }
+
+        public Double getTax() {
+            return tax;
+        }
+
+        public void setTax(Double tax) {
+            this.tax = tax;
+        }
+
+        public String getDetail() {
+            return detail;
+        }
+
+        public void setDetail(String detail) {
+            this.detail = detail;
+        }
+
+        public String getPdf_url() {
+            return pdf_url;
+        }
+
+        public void setPdf_url(String pdf_url) {
+            this.pdf_url = pdf_url;
+        }
+
+        public String getReimburse_status() {
+            return reimburse_status;
+        }
+
+        public void setReimburse_status(String reimburse_status) {
+            this.reimburse_status = reimburse_status;
+        }
+
+        public String getMaker() {
+            return maker;
+        }
+
+        public void setMaker(String maker) {
+            this.maker = maker;
+        }
+
+        public String getCashier() {
+            return cashier;
+        }
+
+        public void setCashier(String cashier) {
+            this.cashier = cashier;
+        }
+
+        public String getRemarks() {
+            return remarks;
+        }
+
+        public void setRemarks(String remarks) {
+            this.remarks = remarks;
+        }
+
+        public String getSeller_bank_account() {
+            return seller_bank_account;
+        }
+
+        public void setSeller_bank_account(String seller_bank_account) {
+            this.seller_bank_account = seller_bank_account;
+        }
+
+        public String getSeller_address_and_phone() {
+            return seller_address_and_phone;
+        }
+
+        public void setSeller_address_and_phone(String seller_address_and_phone) {
+            this.seller_address_and_phone = seller_address_and_phone;
+        }
+
+        public String getSeller_number() {
+            return seller_number;
+        }
+
+        public void setSeller_number(String seller_number) {
+            this.seller_number = seller_number;
+        }
+
+        public String getBuyer_bank_account() {
+            return buyer_bank_account;
+        }
+
+        public void setBuyer_bank_account(String buyer_bank_account) {
+            this.buyer_bank_account = buyer_bank_account;
+        }
+
+        public String getBuyer_address_and_phone() {
+            return buyer_address_and_phone;
+        }
+
+        public void setBuyer_address_and_phone(String buyer_address_and_phone) {
+            this.buyer_address_and_phone = buyer_address_and_phone;
+        }
+
+        public String getBuyer_number() {
+            return buyer_number;
+        }
+
+        public void setBuyer_number(String buyer_number) {
+            this.buyer_number = buyer_number;
+        }
+
+        public String getCheck_code() {
+            return check_code;
+        }
+
+        public void setCheck_code(String check_code) {
+            this.check_code = check_code;
+        }
+
+        public String getTrip_pdf_url() {
+            return trip_pdf_url;
+        }
+
+        public void setTrip_pdf_url(String trip_pdf_url) {
+            this.trip_pdf_url = trip_pdf_url;
+        }
+    }
+
+    public static class ProductInfo {
+        /**
+         * 项目(商品)名称
+         */
+        private String name;
+        /**
+         * 项目数量
+         */
+        private Double num;
+        /**
+         * 项目单位
+         */
+        private String unit;
+        /**
+         * 金额,以分为单位
+         */
+        private Double fee;
+        /**
+         * 单价,以分为单位
+         */
+        private Double price;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Double getNum() {
+            return num;
+        }
+
+        public void setNum(Double num) {
+            this.num = num;
+        }
+
+        public String getUnit() {
+            return unit;
+        }
+
+        public void setUnit(String unit) {
+            this.unit = unit;
+        }
+
+        public Double getFee() {
+            return fee;
+        }
+
+        public void setFee(Double fee) {
+            this.fee = fee;
+        }
+
+        public Double getPrice() {
+            return price;
+        }
+
+        public void setPrice(Double price) {
+            this.price = price;
+        }
+    }
+}

+ 19 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetJoinQrCodeResp.java

@@ -0,0 +1,19 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class GetJoinQrCodeResp extends BaseResp{
+    /**
+     * 二维码链接,有效期7
+     */
+    private String join_qrcode;
+
+    public String getJoin_qrcode() {
+        return join_qrcode;
+    }
+
+    public void setJoin_qrcode(String join_qrcode) {
+        this.join_qrcode = join_qrcode;
+    }
+}

+ 16 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetOpenIdResp.java

@@ -0,0 +1,16 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class GetOpenIdResp extends BaseResp {
+    private String openid;
+
+    public String getOpenid() {
+        return openid;
+    }
+
+    public void setOpenid(String openid) {
+        this.openid = openid;
+    }
+}

+ 49 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleByCalendarReq.java

@@ -0,0 +1,49 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class GetScheduleByCalendarReq {
+    /**
+     * 日历ID
+     */
+    private String calId;
+    /**
+     * 分页,偏移量, 默认为0
+     */
+    private Integer offset;
+    /**
+     * 分页,预期请求的数据量,默认为500,取值范围 1 ~ 1000
+     */
+    private Integer limit;
+
+    public GetScheduleByCalendarReq calId(String calId) {
+        this.calId = calId;
+        return this;
+    }
+
+    public GetScheduleByCalendarReq offset(int offset) {
+        this.offset = offset;
+        return this;
+    }
+
+    public GetScheduleByCalendarReq limit(int limit) {
+        this.limit = limit;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(3);
+        data.put("cal_id", calId);
+        if (null != offset) {
+            data.put("offset", offset);
+        }
+        if (null != limit) {
+            data.put("limit", limit);
+        }
+        return data;
+    }
+}

+ 190 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleByCalendarResp.java

@@ -0,0 +1,190 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetScheduleByCalendarResp extends BaseResp{
+
+    private List<Schedule> schedule_list;
+
+    public List<Schedule> getSchedule_list() {
+        return schedule_list;
+    }
+
+    public void setSchedule_list(List<Schedule> schedule_list) {
+        this.schedule_list = schedule_list;
+    }
+
+    public static class Schedule {
+        private String schedule_id;
+        private Integer sequence;
+        private String organizer;
+        private List<Attendee> attendees;
+        private String summary;
+        private String description;
+        private Reminders reminders;
+        private String location;
+        private String cal_id;
+        private Long start_time;
+        private Long end_time;
+        private Integer status;
+
+        public String getSchedule_id() {
+            return schedule_id;
+        }
+
+        public void setSchedule_id(String schedule_id) {
+            this.schedule_id = schedule_id;
+        }
+
+        public Integer getSequence() {
+            return sequence;
+        }
+
+        public void setSequence(Integer sequence) {
+            this.sequence = sequence;
+        }
+
+        public String getOrganizer() {
+            return organizer;
+        }
+
+        public void setOrganizer(String organizer) {
+            this.organizer = organizer;
+        }
+
+        public List<Attendee> getAttendees() {
+            return attendees;
+        }
+
+        public void setAttendees(List<Attendee> attendees) {
+            this.attendees = attendees;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public void setSummary(String summary) {
+            this.summary = summary;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public Reminders getReminders() {
+            return reminders;
+        }
+
+        public void setReminders(Reminders reminders) {
+            this.reminders = reminders;
+        }
+
+        public String getLocation() {
+            return location;
+        }
+
+        public void setLocation(String location) {
+            this.location = location;
+        }
+
+        public String getCal_id() {
+            return cal_id;
+        }
+
+        public void setCal_id(String cal_id) {
+            this.cal_id = cal_id;
+        }
+
+        public Long getStart_time() {
+            return start_time;
+        }
+
+        public void setStart_time(Long start_time) {
+            this.start_time = start_time;
+        }
+
+        public Long getEnd_time() {
+            return end_time;
+        }
+
+        public void setEnd_time(Long end_time) {
+            this.end_time = end_time;
+        }
+
+        public Integer getStatus() {
+            return status;
+        }
+
+        public void setStatus(Integer status) {
+            this.status = status;
+        }
+    }
+
+    public static class Attendee {
+        private String userid;
+        private Integer response_status;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public Integer getResponse_status() {
+            return response_status;
+        }
+
+        public void setResponse_status(Integer response_status) {
+            this.response_status = response_status;
+        }
+    }
+
+    public static class Reminders {
+        private Integer is_remind;
+        private Integer is_repeat;
+        private Integer remind_before_event_secs;
+        private Integer repeat_type;
+
+        public Integer getIs_remind() {
+            return is_remind;
+        }
+
+        public void setIs_remind(Integer is_remind) {
+            this.is_remind = is_remind;
+        }
+
+        public Integer getIs_repeat() {
+            return is_repeat;
+        }
+
+        public void setIs_repeat(Integer is_repeat) {
+            this.is_repeat = is_repeat;
+        }
+
+        public Integer getRemind_before_event_secs() {
+            return remind_before_event_secs;
+        }
+
+        public void setRemind_before_event_secs(Integer remind_before_event_secs) {
+            this.remind_before_event_secs = remind_before_event_secs;
+        }
+
+        public Integer getRepeat_type() {
+            return repeat_type;
+        }
+
+        public void setRepeat_type(Integer repeat_type) {
+            this.repeat_type = repeat_type;
+        }
+    }
+}

+ 181 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetScheduleListResp.java

@@ -0,0 +1,181 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetScheduleListResp extends BaseResp {
+
+    private List<Schedule> schedule_list;
+
+    public List<Schedule> getSchedule_list() {
+        return schedule_list;
+    }
+
+    public void setSchedule_list(List<Schedule> schedule_list) {
+        this.schedule_list = schedule_list;
+    }
+
+    public static class Schedule {
+        private String schedule_id;
+        private String organizer;
+        private List<Attendee> attendees;
+        private String summary;
+        private String description;
+        private Reminders reminders;
+        private String location;
+        private String cal_id;
+        private Long start_time;
+        private Long end_time;
+        private Integer status;
+
+        public String getSchedule_id() {
+            return schedule_id;
+        }
+
+        public void setSchedule_id(String schedule_id) {
+            this.schedule_id = schedule_id;
+        }
+
+        public String getOrganizer() {
+            return organizer;
+        }
+
+        public void setOrganizer(String organizer) {
+            this.organizer = organizer;
+        }
+
+        public List<Attendee> getAttendees() {
+            return attendees;
+        }
+
+        public void setAttendees(List<Attendee> attendees) {
+            this.attendees = attendees;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public void setSummary(String summary) {
+            this.summary = summary;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public Reminders getReminders() {
+            return reminders;
+        }
+
+        public void setReminders(Reminders reminders) {
+            this.reminders = reminders;
+        }
+
+        public String getLocation() {
+            return location;
+        }
+
+        public void setLocation(String location) {
+            this.location = location;
+        }
+
+        public String getCal_id() {
+            return cal_id;
+        }
+
+        public void setCal_id(String cal_id) {
+            this.cal_id = cal_id;
+        }
+
+        public Long getStart_time() {
+            return start_time;
+        }
+
+        public void setStart_time(Long start_time) {
+            this.start_time = start_time;
+        }
+
+        public Long getEnd_time() {
+            return end_time;
+        }
+
+        public void setEnd_time(Long end_time) {
+            this.end_time = end_time;
+        }
+
+        public Integer getStatus() {
+            return status;
+        }
+
+        public void setStatus(Integer status) {
+            this.status = status;
+        }
+    }
+
+    public static class Attendee {
+        private String userid;
+        private Integer response_status;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public Integer getResponse_status() {
+            return response_status;
+        }
+
+        public void setResponse_status(Integer response_status) {
+            this.response_status = response_status;
+        }
+    }
+
+    public static class Reminders {
+        private Integer is_remind;
+        private Integer is_repeat;
+        private Integer remind_before_event_secs;
+        private Integer repeat_type;
+
+        public Integer getIs_remind() {
+            return is_remind;
+        }
+
+        public void setIs_remind(Integer is_remind) {
+            this.is_remind = is_remind;
+        }
+
+        public Integer getIs_repeat() {
+            return is_repeat;
+        }
+
+        public void setIs_repeat(Integer is_repeat) {
+            this.is_repeat = is_repeat;
+        }
+
+        public Integer getRemind_before_event_secs() {
+            return remind_before_event_secs;
+        }
+
+        public void setRemind_before_event_secs(Integer remind_before_event_secs) {
+            this.remind_before_event_secs = remind_before_event_secs;
+        }
+
+        public Integer getRepeat_type() {
+            return repeat_type;
+        }
+
+        public void setRepeat_type(Integer repeat_type) {
+            this.repeat_type = repeat_type;
+        }
+    }
+}

+ 52 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetSimpleUserListResp.java

@@ -0,0 +1,52 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetSimpleUserListResp extends BaseResp{
+
+    private List<User> userlist;
+
+    public List<User> getUserlist() {
+        return userlist;
+    }
+
+    public void setUserlist(List<User> userlist) {
+        this.userlist = userlist;
+    }
+
+    public static class User {
+        private String userid;
+        private String name;
+        /**
+         * 成员所属部门列表。列表项为部门ID,32位整型
+         */
+        private List<Integer> department;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public List<Integer> getDepartment() {
+            return department;
+        }
+
+        public void setDepartment(List<Integer> department) {
+            this.department = department;
+        }
+    }
+}

+ 35 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserInfoResp.java

@@ -0,0 +1,35 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+/**
+ * @author yingp
+ */
+public class GetUserInfoResp extends BaseResp {
+    /**
+     * 成员UserID。若需要获得用户详情信息,可调用通讯录接口:读取成员
+     */
+    @JSONField(name = "UserId")
+    private String UserId;
+    /**
+     * 手机设备号(由企业微信在安装时随机生成,删除重装会改变,升级不受影响)
+     */
+    @JSONField(name = "DeviceId")
+    private String DeviceId;
+
+    public String getUserId() {
+        return UserId;
+    }
+
+    public void setUserId(String userId) {
+        UserId = userId;
+    }
+
+    public String getDeviceId() {
+        return DeviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        DeviceId = deviceId;
+    }
+}

+ 263 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserListResp.java

@@ -0,0 +1,263 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetUserListResp extends BaseResp{
+
+    private List<User> userlist;
+
+    public List<User> getUserlist() {
+        return userlist;
+    }
+
+    public void setUserlist(List<User> userlist) {
+        this.userlist = userlist;
+    }
+
+    public static class User {
+        /**
+         * 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节
+         */
+        private String userid;
+        /**
+         * 成员名称,此字段从20191230日起,对新创建第三方应用不再返回,2020630日起,对所有历史第三方应用不再返回,后续第三方仅通讯录应用可获取,第三方页面需要通过通讯录展示组件来展示名字
+         */
+        private String name;
+        /**
+         * 成员所属部门id列表,仅返回该应用有查看权限的部门id
+         */
+        private List<Integer> department;
+        /**
+         * 部门内的排序值,默认为0。数量必须和department一致,数值越大排序越前面。值范围是[0, 2^32)
+         */
+        private List<Integer> order;
+        /**
+         * 职务信息;第三方仅通讯录应用可获取
+         */
+        private String position;
+        /**
+         * 手机号码,第三方仅通讯录应用可获取
+         */
+        private String mobile;
+        /**
+         * 性别。0表示未定义,1表示男性,2表示女性
+         */
+        private String gender;
+        /**
+         * 邮箱,第三方仅通讯录应用可获取
+         */
+        private String email;
+        /**
+         * 表示在所在的部门内是否为上级。;第三方仅通讯录应用可获取
+         */
+        private List<Integer> is_leader_in_dept;
+        /**
+         * 头像url。 第三方仅通讯录应用可获取
+         */
+        private String avatar;
+        /**
+         * 头像缩略图url。第三方仅通讯录应用可获取
+         */
+        private String thumb_avatar;
+        /**
+         * 座机。第三方仅通讯录应用可获取
+         */
+        private String telephone;
+        /**
+         * 成员启用状态。1表示启用的成员,0表示被禁用。注意,服务商调用接口不会返回此字段
+         */
+        private Integer enable;
+        /**
+         * 别名;第三方仅通讯录应用可获取
+         */
+        private String alias;
+        /**
+         * 地址
+         */
+        private String address;
+        /**
+         * 激活状态: 1=已激活,2=已禁用,4=未激活。
+         * 已激活代表已激活企业微信或已关注微工作台(原企业号)。未激活代表既未激活企业微信又未关注微工作台(原企业号)
+         */
+        private Integer status;
+        /**
+         * 员工个人二维码,扫描可添加为外部联系人(注意返回的是一个url,可在浏览器上打开该url以展示二维码);第三方仅通讯录应用可获取
+         */
+        private String qr_code;
+        /**
+         * 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示
+         */
+        private String external_position;
+        /**
+         * 是否隐藏手机号
+         */
+        private Integer hide_mobile;
+        /**
+         * 英文名
+         */
+        private String english_name;
+
+        public String getUserid() {
+            return userid;
+        }
+
+        public void setUserid(String userid) {
+            this.userid = userid;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public List<Integer> getDepartment() {
+            return department;
+        }
+
+        public void setDepartment(List<Integer> department) {
+            this.department = department;
+        }
+
+        public List<Integer> getOrder() {
+            return order;
+        }
+
+        public void setOrder(List<Integer> order) {
+            this.order = order;
+        }
+
+        public String getPosition() {
+            return position;
+        }
+
+        public void setPosition(String position) {
+            this.position = position;
+        }
+
+        public String getMobile() {
+            return mobile;
+        }
+
+        public void setMobile(String mobile) {
+            this.mobile = mobile;
+        }
+
+        public String getGender() {
+            return gender;
+        }
+
+        public void setGender(String gender) {
+            this.gender = gender;
+        }
+
+        public String getEmail() {
+            return email;
+        }
+
+        public void setEmail(String email) {
+            this.email = email;
+        }
+
+        public List<Integer> getIs_leader_in_dept() {
+            return is_leader_in_dept;
+        }
+
+        public void setIs_leader_in_dept(List<Integer> is_leader_in_dept) {
+            this.is_leader_in_dept = is_leader_in_dept;
+        }
+
+        public String getAvatar() {
+            return avatar;
+        }
+
+        public void setAvatar(String avatar) {
+            this.avatar = avatar;
+        }
+
+        public String getThumb_avatar() {
+            return thumb_avatar;
+        }
+
+        public void setThumb_avatar(String thumb_avatar) {
+            this.thumb_avatar = thumb_avatar;
+        }
+
+        public String getTelephone() {
+            return telephone;
+        }
+
+        public void setTelephone(String telephone) {
+            this.telephone = telephone;
+        }
+
+        public Integer getEnable() {
+            return enable;
+        }
+
+        public void setEnable(Integer enable) {
+            this.enable = enable;
+        }
+
+        public String getAlias() {
+            return alias;
+        }
+
+        public void setAlias(String alias) {
+            this.alias = alias;
+        }
+
+        public String getAddress() {
+            return address;
+        }
+
+        public void setAddress(String address) {
+            this.address = address;
+        }
+
+        public Integer getStatus() {
+            return status;
+        }
+
+        public void setStatus(Integer status) {
+            this.status = status;
+        }
+
+        public String getQr_code() {
+            return qr_code;
+        }
+
+        public void setQr_code(String qr_code) {
+            this.qr_code = qr_code;
+        }
+
+        public String getExternal_position() {
+            return external_position;
+        }
+
+        public void setExternal_position(String external_position) {
+            this.external_position = external_position;
+        }
+
+        public Integer getHide_mobile() {
+            return hide_mobile;
+        }
+
+        public void setHide_mobile(Integer hide_mobile) {
+            this.hide_mobile = hide_mobile;
+        }
+
+        public String getEnglish_name() {
+            return english_name;
+        }
+
+        public void setEnglish_name(String english_name) {
+            this.english_name = english_name;
+        }
+    }
+}

+ 226 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/GetUserResp.java

@@ -0,0 +1,226 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class GetUserResp extends BaseResp{
+    /**
+     * 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节
+     */
+    private String userid;
+    /**
+     * 成员名称,此字段从20191230日起,对新创建第三方应用不再返回,2020630日起,对所有历史第三方应用不再返回,后续第三方仅通讯录应用可获取,第三方页面需要通过通讯录展示组件来展示名字
+     */
+    private String name;
+    /**
+     * 成员所属部门id列表,仅返回该应用有查看权限的部门id
+     */
+    private List<Integer> department;
+    /**
+     * 部门内的排序值,默认为0。数量必须和department一致,数值越大排序越前面。值范围是[0, 2^32)
+     */
+    private List<Integer> order;
+    /**
+     * 职务信息;第三方仅通讯录应用可获取
+     */
+    private String position;
+    /**
+     * 手机号码,第三方仅通讯录应用可获取
+     */
+    private String mobile;
+    /**
+     * 性别。0表示未定义,1表示男性,2表示女性
+     */
+    private String gender;
+    /**
+     * 邮箱,第三方仅通讯录应用可获取
+     */
+    private String email;
+    /**
+     * 表示在所在的部门内是否为上级。;第三方仅通讯录应用可获取
+     */
+    private List<Integer> is_leader_in_dept;
+    /**
+     * 头像url。 第三方仅通讯录应用可获取
+     */
+    private String avatar;
+    /**
+     * 头像缩略图url。第三方仅通讯录应用可获取
+     */
+    private String thumb_avatar;
+    /**
+     * 座机。第三方仅通讯录应用可获取
+     */
+    private String telephone;
+    /**
+     * 成员启用状态。1表示启用的成员,0表示被禁用。注意,服务商调用接口不会返回此字段
+     */
+    private Integer enable;
+    /**
+     * 别名;第三方仅通讯录应用可获取
+     */
+    private String alias;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 激活状态: 1=已激活,2=已禁用,4=未激活。
+     * 已激活代表已激活企业微信或已关注微工作台(原企业号)。未激活代表既未激活企业微信又未关注微工作台(原企业号)
+     */
+    private Integer status;
+    /**
+     * 员工个人二维码,扫描可添加为外部联系人(注意返回的是一个url,可在浏览器上打开该url以展示二维码);第三方仅通讯录应用可获取
+     */
+    private String qr_code;
+    /**
+     * 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示
+     */
+    private String external_position;
+
+    public String getUserid() {
+        return userid;
+    }
+
+    public void setUserid(String userid) {
+        this.userid = userid;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<Integer> getDepartment() {
+        return department;
+    }
+
+    public void setDepartment(List<Integer> department) {
+        this.department = department;
+    }
+
+    public List<Integer> getOrder() {
+        return order;
+    }
+
+    public void setOrder(List<Integer> order) {
+        this.order = order;
+    }
+
+    public String getPosition() {
+        return position;
+    }
+
+    public void setPosition(String position) {
+        this.position = position;
+    }
+
+    public String getMobile() {
+        return mobile;
+    }
+
+    public void setMobile(String mobile) {
+        this.mobile = mobile;
+    }
+
+    public String getGender() {
+        return gender;
+    }
+
+    public void setGender(String gender) {
+        this.gender = gender;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public List<Integer> getIs_leader_in_dept() {
+        return is_leader_in_dept;
+    }
+
+    public void setIs_leader_in_dept(List<Integer> is_leader_in_dept) {
+        this.is_leader_in_dept = is_leader_in_dept;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+
+    public String getThumb_avatar() {
+        return thumb_avatar;
+    }
+
+    public void setThumb_avatar(String thumb_avatar) {
+        this.thumb_avatar = thumb_avatar;
+    }
+
+    public String getTelephone() {
+        return telephone;
+    }
+
+    public void setTelephone(String telephone) {
+        this.telephone = telephone;
+    }
+
+    public Integer getEnable() {
+        return enable;
+    }
+
+    public void setEnable(Integer enable) {
+        this.enable = enable;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public void setAddress(String address) {
+        this.address = address;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public String getQr_code() {
+        return qr_code;
+    }
+
+    public void setQr_code(String qr_code) {
+        this.qr_code = qr_code;
+    }
+
+    public String getExternal_position() {
+        return external_position;
+    }
+
+    public void setExternal_position(String external_position) {
+        this.external_position = external_position;
+    }
+}

+ 56 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InviteReq.java

@@ -0,0 +1,56 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.*;
+
+/**
+ * @author yingp
+ */
+public class InviteReq {
+    private Set<String> userSet;
+    private Set<Integer> partySet;
+    private Set<Integer> tagSet;
+
+    public InviteReq user(String... userIds) {
+        if (null == userSet) {
+            userSet = new HashSet<>(userIds.length);
+        }
+        for (String userId : userIds) {
+            userSet.add(userId);
+        }
+        return this;
+    }
+
+    public InviteReq party(Integer... partyIds) {
+        if (null == partySet) {
+            partySet = new HashSet<>(partyIds.length);
+        }
+        for (Integer partyId : partyIds) {
+            partySet.add(partyId);
+        }
+        return this;
+    }
+
+    public InviteReq tag(Integer... tagIds) {
+        if (null == tagSet) {
+            tagSet = new HashSet<>(tagIds.length);
+        }
+        for (Integer tagId : tagIds) {
+            tagSet.add(tagId);
+        }
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        if (null != userSet) {
+            data.put("user", userSet);
+        }
+        if (null != partySet) {
+            data.put("party", partySet);
+        }
+        if (null != tagSet) {
+            data.put("tag", tagSet);
+        }
+        return data;
+    }
+}

+ 36 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InviteResp.java

@@ -0,0 +1,36 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class InviteResp extends BaseResp{
+    private List<String> invaliduser;
+    private List<Integer> invalidparty;
+    private List<Integer> invalidtag;
+
+    public List<String> getInvaliduser() {
+        return invaliduser;
+    }
+
+    public void setInvaliduser(List<String> invaliduser) {
+        this.invaliduser = invaliduser;
+    }
+
+    public List<Integer> getInvalidparty() {
+        return invalidparty;
+    }
+
+    public void setInvalidparty(List<Integer> invalidparty) {
+        this.invalidparty = invalidparty;
+    }
+
+    public List<Integer> getInvalidtag() {
+        return invalidtag;
+    }
+
+    public void setInvalidtag(List<Integer> invalidtag) {
+        this.invalidtag = invalidtag;
+    }
+}

+ 19 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/InvoiceStatus.java

@@ -0,0 +1,19 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public enum InvoiceStatus {
+    /**
+     * 发票初始状态,未锁定
+     */
+    INVOICE_REIMBURSE_INIT,
+    /**
+     * 发票已锁定,无法重复提交报销
+     */
+    INVOICE_REIMBURSE_LOCK,
+    /**
+     * 发票已核销,从用户卡包中移除
+     */
+    INVOICE_REIMBURSE_CLOSURE
+}

+ 382 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendChatReq.java

@@ -0,0 +1,382 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class SendChatReq {
+    /**
+     * 群聊id
+     */
+    private String chatId;
+    /**
+     * 表示是否是保密消息,0表示否,1表示是,默认0
+     */
+    private Boolean safe;
+
+    private String type;
+
+    private AbstractMessageBody body;
+
+    public SendChatReq(String chatId) {
+        this.chatId = chatId;
+    }
+
+    public SendChatReq safe(boolean safe) {
+        this.safe = safe;
+        return this;
+    }
+
+    /**
+     * 文本消息
+     *
+     * @param content 消息内容,最长不超过2048个字节
+     * @return
+     */
+    public SendChatReq text(String content) {
+        this.type = "text";
+        this.body = new Text(content);
+        return this;
+    }
+
+    /**
+     * 图片消息
+     *
+     * @param mediaId 图片媒体文件id,可以调用上传临时素材接口获取
+     * @return
+     */
+    public SendChatReq image(String mediaId) {
+        this.type = "image";
+        this.body = new Image(mediaId);
+        return this;
+    }
+
+    /**
+     * 语音消息
+     *
+     * @param mediaId 语音文件id,可以调用上传临时素材接口获取
+     * @return
+     */
+    public SendChatReq voice(String mediaId) {
+        this.type = "voice";
+        this.body = new Voice(mediaId);
+        return this;
+    }
+
+    /**
+     * 视频消息
+     *
+     * @param mediaId     视频媒体文件id,可以调用上传临时素材接口获取
+     * @param title       视频消息的标题,不超过128个字节,超过会自动截断
+     * @param description 视频消息的描述,不超过512个字节,超过会自动截断
+     * @return
+     */
+    public SendChatReq video(String mediaId, String title, String description) {
+        this.type = "video";
+        this.body = new Video(mediaId, title, description);
+        return this;
+    }
+
+    /**
+     * 文件消息
+     *
+     * @param mediaId 文件id,可以调用上传临时素材接口获取
+     * @return
+     */
+    public SendChatReq file(String mediaId) {
+        this.type = "file";
+        this.body = new File(mediaId);
+        return this;
+    }
+
+    /**
+     * 文本卡片消息
+     * <p>
+     * 卡片消息的展现形式非常灵活,支持使用br标签或者空格来进行换行处理,也支持使用div标签来使用不同的字体颜色,
+     * 目前内置了3种文字颜色:灰色(gray)、高亮(highlight)、默认黑色(normal),将其作为div标签的class属性即可
+     * </p>
+     *
+     * @param title       标题,不超过128个字节,超过会自动截断
+     * @param description 描述,不超过512个字节,超过会自动截断
+     * @param url
+     * @param btnText
+     * @return
+     */
+    public SendChatReq textCard(String title, String description, String url, String btnText) {
+        this.type = "textcard";
+        this.body = new TextCard(title, description, url, btnText);
+        return this;
+    }
+
+    /**
+     * 文本卡片消息
+     * <p>
+     * 卡片消息的展现形式非常灵活,支持使用br标签或者空格来进行换行处理,也支持使用div标签来使用不同的字体颜色,
+     * 目前内置了3种文字颜色:灰色(gray)、高亮(highlight)、默认黑色(normal),将其作为div标签的class属性即可
+     * </p>
+     *
+     * @param title       标题,不超过128个字节,超过会自动截断
+     * @param description 描述,不超过512个字节,超过会自动截断
+     * @param url
+     * @return
+     */
+    public SendChatReq textCard(String title, String description, String url) {
+        return textCard(title, description, url, null);
+    }
+
+    /**
+     * 图文消息
+     *
+     * @param title       标题,不超过128个字节,超过会自动截断
+     * @param description 描述,不超过512个字节,超过会自动截断
+     * @param url         点击后跳转的链接。
+     * @param picUrl      图文消息的图片链接,支持JPG、PNG格式,较好的效果为大图1068*455,小图150*150
+     * @return
+     */
+    public SendChatReq news(String title, String description, String url, String picUrl) {
+        this.type = "news";
+        this.body = new News(title, description, url, picUrl);
+        return this;
+    }
+
+    /**
+     * 图文消息(mpnews)
+     * mpnews类型的图文消息,跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信。
+     * 多次发送mpnews,会被认为是不同的图文,阅读、点赞的统计会被分开计算。
+     *
+     * @param title            标题,不超过128个字节,超过会自动截断
+     * @param thumbMediaId     图文消息缩略图的media_id, 可以通过素材管理接口获得。此处thumb_media_id即上传接口返回的media_id
+     * @param author           图文消息的作者,不超过64个字节
+     * @param contentSourceUrl 图文消息点击“阅读原文”之后的页面链接
+     * @param content          图文消息的内容,支持html标签,不超过666 K个字节
+     * @param digest           图文消息的描述,不超过512个字节,超过会自动截断
+     * @return
+     */
+    public SendChatReq mpNews(String title, String thumbMediaId, String author, String contentSourceUrl, String content, String digest) {
+        this.type = "mpnews";
+        this.body = new MpNews(title, thumbMediaId, author, contentSourceUrl, content, digest);
+        return this;
+    }
+
+    /**
+     * markdown消息
+     * 目前仅支持markdown语法的子集
+     * 微工作台(原企业号)不支持展示markdown消息
+     *
+     * @param content 消息内容,最长不超过2048个字节
+     * @return
+     */
+    public SendChatReq markdown(String content) {
+        this.type = "markdown";
+        this.body = new Markdown(content);
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> message = new HashMap<>(3);
+        message.put("chatid", chatId);
+        if (null != safe) {
+            message.put("safe", safe ? 1 : 0);
+        }
+        if (null != type) {
+            message.put("msgtype", type);
+            message.put(type, body.build());
+        }
+        return message;
+    }
+
+    interface AbstractMessageBody {
+        Map<String, Object> build();
+    }
+
+    class Text implements AbstractMessageBody {
+        private String content;
+
+        public Text(String content) {
+            this.content = content;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("content", content);
+            return data;
+        }
+    }
+
+    class Image implements AbstractMessageBody {
+        private String mediaId;
+
+        public Image(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class Voice implements AbstractMessageBody {
+        private String mediaId;
+
+        public Voice(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class Video implements AbstractMessageBody {
+        private String mediaId;
+        private String title;
+        private String description;
+
+        public Video(String mediaId, String title, String description) {
+            this.mediaId = mediaId;
+            this.title = title;
+            this.description = description;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            data.put("title", title);
+            data.put("description", description);
+            return data;
+        }
+    }
+
+    class File implements AbstractMessageBody {
+        private String mediaId;
+
+        public File(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class TextCard implements AbstractMessageBody {
+        private String title;
+        private String description;
+        private String url;
+        private String btnText;
+
+        public TextCard(String title, String description, String url, String btnText) {
+            this.title = title;
+            this.description = description;
+            this.url = url;
+            this.btnText = btnText;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(4);
+            data.put("title", title);
+            data.put("description", description);
+            data.put("url", url);
+            if (null != btnText) {
+                data.put("btntxt", btnText);
+            }
+            return data;
+        }
+    }
+
+    class News implements AbstractMessageBody {
+        private String title;
+        private String description;
+        private String url;
+        private String picUrl;
+
+        public News(String title, String description, String url, String picUrl) {
+            this.title = title;
+            this.description = description;
+            this.url = url;
+            this.picUrl = picUrl;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            Map<String, Object> article = new HashMap<>(1);
+            article.put("title", title);
+            article.put("description", description);
+            article.put("url", url);
+            if (null != picUrl) {
+                article.put("picurl", picUrl);
+            }
+            data.put("articles", Arrays.asList(article));
+            return data;
+        }
+    }
+
+    class MpNews implements AbstractMessageBody {
+        private String title;
+        private String thumbMediaId;
+        private String author;
+        private String contentSourceUrl;
+        private String content;
+        private String digest;
+
+        public MpNews(String title, String thumbMediaId, String author, String contentSourceUrl, String content, String digest) {
+            this.title = title;
+            this.thumbMediaId = thumbMediaId;
+            this.author = author;
+            this.contentSourceUrl = contentSourceUrl;
+            this.content = content;
+            this.digest = digest;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            Map<String, Object> article = new HashMap<>(1);
+            article.put("title", title);
+            article.put("thumb_media_id", thumbMediaId);
+            if (null != author) {
+                article.put("author", author);
+            }
+            if (null != contentSourceUrl) {
+                article.put("content_source_url", contentSourceUrl);
+            }
+            article.put("content", content);
+            if (null != digest) {
+                article.put("digest", digest);
+            }
+            data.put("articles", Arrays.asList(article));
+            return data;
+        }
+    }
+
+    class Markdown implements AbstractMessageBody {
+        private String content;
+
+        public Markdown(String content) {
+            this.content = content;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("content", content);
+            return data;
+        }
+    }
+}

+ 554 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendMessageReq.java

@@ -0,0 +1,554 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class SendMessageReq {
+
+    private final static int MAX_USER_SIZE = 1000;
+    private final static int MAX_PARTY_SIZE = 100;
+    private final static int MAX_TAG_SIZE = 100;
+
+    private Set<String> userSet;
+    private Set<String> partySet;
+    private Set<String> tagSet;
+    /**
+     * 表示是否是保密消息
+     */
+    private boolean safe;
+    /**
+     * 表示是否开启id转译
+     */
+    private boolean enableIdTrans;
+    /**
+     * 表示是否开启重复消息检查
+     */
+    private boolean enableDuplicateCheck;
+    /**
+     * 表示是否重复消息检查的时间间隔
+     */
+    private Integer duplicateCheckInterval;
+    /**
+     * 消息类型
+     */
+    private String type;
+    /**
+     * 消息内容
+     */
+    private AbstractMessageBody body;
+    /**
+     * 企业应用的id
+     */
+    private Integer agentId;
+
+    public SendMessageReq toUser(String... userIds) {
+        if (null == userSet) {
+            userSet = new HashSet<>(userIds.length);
+        }
+        for (String userId : userIds) {
+            userSet.add(userId);
+        }
+        return this;
+    }
+
+    public SendMessageReq toParty(Integer... partyIds) {
+        if (null == partySet) {
+            partySet = new HashSet<>(partyIds.length);
+        }
+        for (Integer partyId : partyIds) {
+            partySet.add(String.valueOf(partyId));
+        }
+        return this;
+    }
+
+    public SendMessageReq toTag(Integer... tagIds) {
+        if (null == tagSet) {
+            tagSet = new HashSet<>(tagIds.length);
+        }
+        for (Integer tagId : tagIds) {
+            tagSet.add(String.valueOf(tagId));
+        }
+        return this;
+    }
+
+    public SendMessageReq safe(boolean safe) {
+        this.safe = safe;
+        return this;
+    }
+
+    public SendMessageReq enableIdTrans(boolean enableIdTrans) {
+        this.enableIdTrans = enableIdTrans;
+        return this;
+    }
+
+    public SendMessageReq enableDuplicateCheck(boolean enableDuplicateCheck) {
+        this.enableDuplicateCheck = enableDuplicateCheck;
+        return this;
+    }
+
+    public SendMessageReq duplicateCheckInterval(int duplicateCheckInterval) {
+        this.duplicateCheckInterval = duplicateCheckInterval;
+        return this;
+    }
+
+    public SendMessageReq agentId(int agentId) {
+        this.agentId = agentId;
+        return this;
+    }
+
+    public Integer getAgentId() {
+        return agentId;
+    }
+
+    /**
+     * 文本消息
+     *
+     * @param content
+     * @return
+     */
+    public SendMessageReq text(String content) {
+        this.type = "text";
+        this.body = new Text(content);
+        return this;
+    }
+
+    /**
+     * 图片消息
+     *
+     * @param mediaId
+     * @return
+     */
+    public SendMessageReq image(String mediaId) {
+        this.type = "image";
+        this.body = new Image(mediaId);
+        return this;
+    }
+
+    /**
+     * 语音消息
+     *
+     * @param mediaId
+     * @return
+     */
+    public SendMessageReq voice(String mediaId) {
+        this.type = "voice";
+        this.body = new Voice(mediaId);
+        return this;
+    }
+
+    /**
+     * 视频消息
+     *
+     * @param mediaId
+     * @param title
+     * @param description
+     * @return
+     */
+    public SendMessageReq video(String mediaId, String title, String description) {
+        this.type = "video";
+        this.body = new Video(mediaId, title, description);
+        return this;
+    }
+
+    /**
+     * 文件消息
+     *
+     * @param mediaId
+     * @return
+     */
+    public SendMessageReq file(String mediaId) {
+        this.type = "file";
+        this.body = new File(mediaId);
+        return this;
+    }
+
+    /**
+     * 文本卡片消息
+     *
+     * @param title
+     * @param description
+     * @param url
+     * @param btnText
+     * @return
+     */
+    public SendMessageReq textCard(String title, String description, String url, String btnText) {
+        this.type = "textcard";
+        this.body = new TextCard(title, description, url, btnText);
+        return this;
+    }
+
+    /**
+     * 图文消息
+     *
+     * @param title
+     * @param description
+     * @param url
+     * @param picUrl
+     * @return
+     */
+    public SendMessageReq news(String title, String description, String url, String picUrl) {
+        this.type = "news";
+        this.body = new News(title, description, url, picUrl);
+        return this;
+    }
+
+    /**
+     * 存储在企业微信的图文消息
+     *
+     * @param title
+     * @param thumbMediaId
+     * @param author
+     * @param contentSourceUrl
+     * @param content
+     * @param digest
+     * @return
+     */
+    public SendMessageReq mpNews(String title, String thumbMediaId, String author, String contentSourceUrl, String content, String digest) {
+        this.type = "mpnews";
+        this.body = new MpNews(title, thumbMediaId, author, contentSourceUrl, content, digest);
+        return this;
+    }
+
+    /**
+     * markdown消息
+     *
+     * @param content
+     * @return
+     */
+    public SendMessageReq markdown(String content) {
+        this.type = "markdown";
+        this.body = new Markdown(content);
+        return this;
+    }
+
+    /**
+     * 任务卡片消息
+     *
+     * @param title
+     * @param description
+     * @param url
+     * @param taskId
+     * @param btns
+     * @return
+     */
+    public SendMessageReq taskCard(String title, String description, String url, String taskId, List<Btn> btns) {
+        this.type = "taskcard";
+        this.body = new TaskCard(title, description, url, taskId, btns);
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> message = new HashMap<>(3);
+        message.put("agentid", agentId);
+        if (null != userSet) {
+            if (userSet.size() > MAX_USER_SIZE) {
+                throw new RuntimeException(String.format("最多支持%s个用户", MAX_USER_SIZE));
+            }
+            message.put("touser", String.join("|", userSet));
+        }
+        if (null != partySet) {
+            if (partySet.size() > MAX_PARTY_SIZE) {
+                throw new RuntimeException(String.format("最多支持%s个部门", MAX_PARTY_SIZE));
+            }
+            message.put("toparty", String.join("|", partySet));
+        }
+        if (null != tagSet) {
+            if (tagSet.size() > MAX_TAG_SIZE) {
+                throw new RuntimeException(String.format("最多支持%s个标签", MAX_TAG_SIZE));
+            }
+            message.put("totag", String.join("|", tagSet));
+        }
+        if (safe) {
+            message.put("safe", 1);
+        }
+        if (enableIdTrans) {
+            message.put("enable_id_trans", 1);
+        }
+        if (enableDuplicateCheck) {
+            message.put("enable_duplicate_check", 1);
+        }
+        if (null != duplicateCheckInterval) {
+            message.put("duplicate_check_interval", duplicateCheckInterval);
+        }
+        if (null != type) {
+            message.put("msgtype", type);
+            message.put(type, body.build());
+        }
+        return message;
+    }
+
+    interface AbstractMessageBody {
+        Map<String, Object> build();
+    }
+
+    class Text implements AbstractMessageBody {
+        private final String content;
+
+        public Text(String content) {
+            this.content = content;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("content", content);
+            return data;
+        }
+    }
+
+    class Image implements AbstractMessageBody {
+        /**
+         * 图片媒体文件id,可以调用上传临时素材接口获取
+         */
+        private final String mediaId;
+
+        public Image(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class Voice implements AbstractMessageBody {
+        /**
+         * 语音文件id,可以调用上传临时素材接口获取
+         */
+        private final String mediaId;
+
+        public Voice(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class Video implements AbstractMessageBody {
+        /**
+         * 视频媒体文件id,可以调用上传临时素材接口获取
+         */
+        private final String mediaId;
+        private final String title;
+        private final String description;
+
+        public Video(String mediaId, String title, String description) {
+            this.mediaId = mediaId;
+            this.title = title;
+            this.description = description;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            data.put("title", title);
+            data.put("description", description);
+            return data;
+        }
+    }
+
+    class File implements AbstractMessageBody {
+        /**
+         * 文件id,可以调用上传临时素材接口获取
+         */
+        private final String mediaId;
+
+        public File(String mediaId) {
+            this.mediaId = mediaId;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("media_id", mediaId);
+            return data;
+        }
+    }
+
+    class TextCard implements AbstractMessageBody {
+        private final String title;
+        private final String description;
+        private final String url;
+        private final String btnText;
+
+        public TextCard(String title, String description, String url, String btnText) {
+            this.title = title;
+            this.description = description;
+            this.url = url;
+            this.btnText = btnText;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("title", title);
+            data.put("description", description);
+            data.put("url", url);
+            data.put("btntxt", btnText);
+            return data;
+        }
+    }
+
+    class News implements AbstractMessageBody {
+        private final String title;
+        private final String description;
+        private final String url;
+        private final String picUrl;
+
+        public News(String title, String description, String url, String picUrl) {
+            this.title = title;
+            this.description = description;
+            this.url = url;
+            this.picUrl = picUrl;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            Map<String, Object> article = new HashMap<>(4);
+            article.put("title", title);
+            article.put("description", description);
+            article.put("url", url);
+            article.put("picurl", picUrl);
+            data.put("articles", Arrays.asList(article));
+            return data;
+        }
+    }
+
+    class MpNews implements AbstractMessageBody {
+        private final String title;
+        private final String thumbMediaId;
+        private final String author;
+        private final String contentSourceUrl;
+        private final String content;
+        private final String digest;
+
+        public MpNews(String title, String thumbMediaId, String author, String contentSourceUrl, String content, String digest) {
+            this.title = title;
+            this.thumbMediaId = thumbMediaId;
+            this.author = author;
+            this.contentSourceUrl = contentSourceUrl;
+            this.content = content;
+            this.digest = digest;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            Map<String, Object> article = new HashMap<>(4);
+            article.put("title", title);
+            article.put("thumb_media_id", thumbMediaId);
+            article.put("author", author);
+            article.put("content_source_url", contentSourceUrl);
+            article.put("content", content);
+            article.put("digest", digest);
+            data.put("articles", Arrays.asList(article));
+            return data;
+        }
+    }
+
+    class Markdown implements AbstractMessageBody {
+        private final String content;
+
+        public Markdown(String content) {
+            this.content = content;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("content", content);
+            return data;
+        }
+    }
+
+    class TaskCard implements AbstractMessageBody {
+        private final String title;
+        private final String description;
+        private final String url;
+        private final String taskId;
+        private final List<Btn> btns;
+
+        public TaskCard(String title, String description, String url, String taskId, List<Btn> btns) {
+            this.title = title;
+            this.description = description;
+            this.url = url;
+            this.taskId = taskId;
+            this.btns = btns;
+        }
+
+        @Override
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("title", title);
+            data.put("description", description);
+            data.put("url", url);
+            data.put("task_id", taskId);
+            data.put("btn", btns.stream().map(Btn::build).collect(Collectors.toList()));
+            return data;
+        }
+    }
+
+    public static class Btn {
+        /**
+         * 按钮key值,用户点击后,会产生任务卡片回调事件,回调事件会带上该key值,只能由数字、字母和“_-@.”组成,最长支持128字节
+         */
+        private final String key;
+        /**
+         * 按钮名称
+         */
+        private final String name;
+        /**
+         * 点击按钮后显示的名称,默认为“已处理”
+         */
+        private final String replaceName;
+        /**
+         * 按钮字体颜色,可选“red”或者“blue”,默认为“blue”
+         */
+        private String color;
+        /**
+         * 按钮字体是否加粗,默认false
+         */
+        private boolean bold;
+
+        public Btn(String key, String name, String replaceName) {
+            this.key = key;
+            this.name = name;
+            this.replaceName = replaceName;
+        }
+
+        public Btn(String key, String name, String replaceName, String color, boolean bold) {
+            this.key = key;
+            this.name = name;
+            this.replaceName = replaceName;
+            this.color = color;
+            this.bold = bold;
+        }
+
+        public Map<String, Object> build() {
+            Map<String, Object> data = new HashMap<>(1);
+            data.put("key", key);
+            data.put("name", name);
+            data.put("replaceName", replaceName);
+            if (null != color) {
+                data.put("color", color);
+            }
+            if (bold) {
+                data.put("is_bold", true);
+            }
+            return data;
+        }
+    }
+}

+ 62 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/SendMessageResp.java

@@ -0,0 +1,62 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class SendMessageResp extends BaseResp {
+    private String invaliduser;
+    private String invalidparty;
+    private String invalidtag;
+
+    public String getInvaliduser() {
+        return invaliduser;
+    }
+
+    public void setInvaliduser(String invaliduser) {
+        this.invaliduser = invaliduser;
+    }
+
+    public String getInvalidparty() {
+        return invalidparty;
+    }
+
+    public void setInvalidparty(String invalidparty) {
+        this.invalidparty = invalidparty;
+    }
+
+    public String getInvalidtag() {
+        return invalidtag;
+    }
+
+    public void setInvalidtag(String invalidtag) {
+        this.invalidtag = invalidtag;
+    }
+
+    public List<String> invalidUserList() {
+        if (StringUtils.isEmpty(invaliduser)) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(Arrays.asList(invaliduser.split("\\|")));
+    }
+
+    public List<String> invalidPartyList() {
+        if (StringUtils.isEmpty(invalidparty)) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(Arrays.asList(invalidparty.split("\\|")));
+    }
+
+    public List<String> invalidTagList() {
+        if (StringUtils.isEmpty(invalidtag)) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(Arrays.asList(invalidtag.split("\\|")));
+    }
+}

+ 62 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateCalendarReq.java

@@ -0,0 +1,62 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import org.springframework.ui.ModelMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class UpdateCalendarReq {
+    private String calId;
+    private String summary;
+    private String color;
+    private String description;
+    private List<String> shares;
+
+    public UpdateCalendarReq calId(String calId) {
+        this.calId = calId;
+        return this;
+    }
+
+    public UpdateCalendarReq summary(String summary) {
+        this.summary = summary;
+        return this;
+    }
+
+    public UpdateCalendarReq color(String color) {
+        this.color = color;
+        return this;
+    }
+
+    public UpdateCalendarReq description(String description) {
+        this.description = description;
+        return this;
+    }
+
+    public UpdateCalendarReq shares(List<String> shares) {
+        this.shares = shares;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        Map<String, Object> calendar = new HashMap<>(5);
+        calendar.put("cal_id", calId);
+        calendar.put("summary", summary);
+        calendar.put("color", color);
+        if (null != description) {
+            calendar.put("description", description);
+        }
+        if (null != shares) {
+            calendar.put("shares",
+                    shares.stream().map(attendee -> new ModelMap("userid", attendee)).collect(Collectors.toList())
+            );
+        }
+        data.put("calendar", calendar);
+        return data;
+    }
+}

+ 74 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateChatReq.java

@@ -0,0 +1,74 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.*;
+
+/**
+ * @author yingp
+ */
+public class UpdateChatReq {
+    private String chatId;
+    /**
+     * 新的群聊名。若不需更新,请忽略此参数。最多50个utf8字符,超过将截断
+     */
+    private String name;
+    /**
+     * 新群主的id。若不需更新,请忽略此参数
+     */
+    private String owner;
+    /**
+     * 添加成员的id列表
+     */
+    private Set<String> addUserSet;
+    /**
+     * 踢出成员的id列表
+     */
+    private Set<String> delUserSet;
+
+    public UpdateChatReq(String chatId) {
+        this.chatId = chatId;
+    }
+
+    public UpdateChatReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public UpdateChatReq owner(String owner) {
+        this.owner = owner;
+        return this;
+    }
+
+    public UpdateChatReq addUser(String... userId) {
+        if (null == addUserSet) {
+            addUserSet = new HashSet<>(1);
+        }
+        addUserSet.addAll(Arrays.asList(userId));
+        return this;
+    }
+
+    public UpdateChatReq delUser(String... userId) {
+        if (null == delUserSet) {
+            delUserSet = new HashSet<>(1);
+        }
+        delUserSet.addAll(Arrays.asList(userId));
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("chatid", chatId);
+        if (null != name) {
+            data.put("name", name);
+        }
+        if (null != owner) {
+            data.put("owner", owner);
+        }
+        if (null != addUserSet) {
+            data.put("add_user_list", addUserSet);
+        }
+        if (null != delUserSet) {
+            data.put("del_user_list", delUserSet);
+        }
+        return data;
+    }
+}

+ 69 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateDepartmentReq.java

@@ -0,0 +1,69 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class UpdateDepartmentReq {
+    /**
+     * 部门名称。长度限制为1~32个字符,字符不能包括\:?”<>|
+     */
+    private String name;
+    /**
+     * 英文名称,需要在管理后台开启多语言支持才能生效。长度限制为1~32个字符,字符不能包括\:?”<>|
+     */
+    private String enName;
+    /**
+     * 父部门id,32位整型
+     */
+    private Integer parentId;
+    /**
+     * 在父部门中的次序值。order值大的排序靠前。有效的值范围是[0, 2^32)
+     */
+    private Integer order;
+    /**
+     * 部门id
+     */
+    private Integer id;
+
+    public UpdateDepartmentReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public UpdateDepartmentReq enName(String enName) {
+        this.enName = enName;
+        return this;
+    }
+
+    public UpdateDepartmentReq parent(Integer parentId) {
+        this.parentId = parentId;
+        return this;
+    }
+
+    public UpdateDepartmentReq order(Integer order) {
+        this.order = order;
+        return this;
+    }
+
+    public UpdateDepartmentReq id(Integer id) {
+        this.id = id;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(3);
+        data.put("name", name);
+        if (null != enName) {
+            data.put("name_en", enName);
+        }
+        data.put("parentid", parentId);
+        if (null != order) {
+            data.put("order", order);
+        }
+        data.put("id", id);
+        return data;
+    }
+}

+ 159 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateScheduleReq.java

@@ -0,0 +1,159 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import org.springframework.ui.ModelMap;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class UpdateScheduleReq {
+    /**
+     * 组织者userId
+     */
+    private String organizer;
+    /**
+     * 日程ID
+     */
+    private String scheduleId;
+    /**
+     * 日程开始时间,Unix时间戳
+     */
+    private Long startTime;
+    /**
+     * 日程结束时间,Unix时间戳
+     */
+    private Long endTime;
+    /**
+     * 日程参与者列表。最多支持2000
+     */
+    private List<String> attendees;
+    /**
+     * 日程标题。0 ~ 128 字符。不填会默认显示为“新建事件”
+     */
+    private String summary;
+    /**
+     * 日程描述。0 ~ 512 字符
+     */
+    private String description;
+    /**
+     * 日程开始(start_time)前多少秒提醒,当is_remind为1时有效。例如: 300表示日程开始前5分钟提醒。
+     * 目前仅支持以下数值:
+     * 0 - 事件开始时
+     * 300 - 事件开始前5分钟
+     * 900 - 事件开始前15分钟
+     * 3600 - 事件开始前1小时
+     * 86400 - 事件开始前1
+     */
+    private Integer remindBeforeEventSecs;
+    /**
+     * 重复类型,当is_repeat为1时有效。目前支持如下类型:
+     * 0 - 每日
+     * 1 - 每周
+     * 2 - 每月
+     * 5 - 每年
+     * 7 - 工作日
+     */
+    private RepeatType repeatType;
+    /**
+     * 日程地址。0 ~ 128 字符
+     */
+    private String location;
+    /**
+     * 日程所属日历ID。注意,这个日历必须是属于组织者(organizer)的日历;如果不填,那么插入到组织者的默认日历上
+     */
+    private String calId;
+
+    public UpdateScheduleReq organizer(String organizer) {
+        this.organizer = organizer;
+        return this;
+    }
+
+    public UpdateScheduleReq during(long startTime, long endTime) {
+        this.startTime = startTime;
+        this.endTime = endTime;
+        return this;
+    }
+
+    public UpdateScheduleReq attendees(List<String> attendees) {
+        this.attendees = attendees;
+        return this;
+    }
+
+    public UpdateScheduleReq summary(String summary) {
+        this.summary = summary;
+        return this;
+    }
+
+    public UpdateScheduleReq description(String description) {
+        this.description = description;
+        return this;
+    }
+
+    public UpdateScheduleReq remindBefore(int secs) {
+        this.remindBeforeEventSecs = secs;
+        return this;
+    }
+
+    public UpdateScheduleReq repeat(RepeatType type) {
+        this.repeatType = type;
+        return this;
+    }
+
+    public UpdateScheduleReq location(String location) {
+        this.location = location;
+        return this;
+    }
+
+    public UpdateScheduleReq calId(String calId) {
+        this.calId = calId;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(1);
+        Map<String, Object> schedule = new HashMap<>(1);
+        schedule.put("organizer", organizer);
+        schedule.put("schedule_id", scheduleId);
+        schedule.put("start_time", startTime);
+        schedule.put("end_time", endTime);
+        if (null != attendees) {
+            schedule.put("attendees",
+                    attendees.stream().map(attendee -> new ModelMap("userid", attendee)).collect(Collectors.toList())
+            );
+        }
+        if (null != summary) {
+            schedule.put("summary", summary);
+        }
+        if (null != description) {
+            schedule.put("description", description);
+        }
+        if (null != remindBeforeEventSecs || null != repeatType) {
+            schedule.put("reminders", new ModelMap("is_remind", null != remindBeforeEventSecs ? 1 : 0)
+                    .addAttribute("remind_before_event_secs", remindBeforeEventSecs)
+                    .addAttribute("is_repeat", null != repeatType ? 1 : 0)
+                    .addAttribute("repeat_type", null == repeatType ? null : repeatType.code));
+        }
+        if (null != location) {
+            schedule.put("location", location);
+        }
+        if (null != calId) {
+            schedule.put("cal_id", calId);
+        }
+        data.put("schedule", schedule);
+        return data;
+    }
+
+    public enum RepeatType {
+        DAY(0), WEEK(1), MONTH(2), YEAR(5), WEEKDAYS(7);
+
+        private final int code;
+
+        RepeatType(int code) {
+            this.code = code;
+        }
+    }
+}

+ 57 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateTaskCardReq.java

@@ -0,0 +1,57 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.*;
+
+/**
+ * @author yingp
+ */
+public class UpdateTaskCardReq {
+    /**
+     * 企业的成员ID列表(消息接收者,最多支持1000个)。
+     */
+    private Set<String> userIdSet;
+    /**
+     * 应用的agentid
+     */
+    private Long agentId;
+    /**
+     * 发送任务卡片消息时指定的task_id
+     */
+    private String taskId;
+    /**
+     * 设置指定的按钮为选择状态,需要与发送消息时指定的btn:key一致
+     */
+    private String clickedKey;
+
+    public UpdateTaskCardReq userId(String... userId) {
+        if (null == userIdSet) {
+            userIdSet = new HashSet<>(1);
+        }
+        userIdSet.addAll(Arrays.asList(userId));
+        return this;
+    }
+
+    public UpdateTaskCardReq clickedKey(String clickedKey) {
+        this.clickedKey = clickedKey;
+        return this;
+    }
+
+    public UpdateTaskCardReq agentId(long agentId) {
+        this.agentId = agentId;
+        return this;
+    }
+
+    public UpdateTaskCardReq taskId(String taskId) {
+        this.taskId = taskId;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("userids", userIdSet);
+        data.put("agentid", agentId);
+        data.put("task_id", taskId);
+        data.put("clicked_key", clickedKey);
+        return data;
+    }
+}

+ 21 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateTaskCardResp.java

@@ -0,0 +1,21 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class UpdateTaskCardResp extends BaseResp {
+    /**
+     * 不区分大小写,返回的列表都统一转为小写
+     */
+    private List<String> invaliduser;
+
+    public List<String> getInvaliduser() {
+        return invaliduser;
+    }
+
+    public void setInvaliduser(List<String> invaliduser) {
+        this.invaliduser = invaliduser;
+    }
+}

+ 224 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UpdateUserReq.java

@@ -0,0 +1,224 @@
+package com.usoftchina.qywx.sdk.dto;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ */
+public class UpdateUserReq {
+    /**
+     * 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节。只能由数字、字母和“_-@.”四种字符组成,且第一个字符必须是数字或字母。
+     */
+    private String userId;
+    /**
+     * 成员名称。长度为1~64个utf8字符
+     */
+    private String name;
+    /**
+     * 成员别名。长度1~32个utf8字符
+     */
+    private String alias;
+    /**
+     * 手机号码。企业内必须唯一。若成员已激活企业微信,则需成员自行修改(此情况下该参数被忽略,但不会报错)
+     */
+    private String mobile;
+    /**
+     * 成员所属部门列表,不超过20
+     */
+    private List<Department> department;
+    /**
+     * 职务信息。长度为0~128个字符
+     */
+    private String position;
+    /**
+     * 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示。长度12个汉字内
+     */
+    private String externalPosition;
+    /**
+     * 性别。1表示男性,2表示女性
+     */
+    private Gender gender;
+    /**
+     * 邮箱。长度不超过64个字节,且为有效的email格式。企业内必须唯一。若是绑定了腾讯企业邮的企业微信,则需要在腾讯企业邮中修改邮箱(此情况下该参数被忽略,但不会报错)
+     */
+    private String email;
+    /**
+     * 座机。32字节以内,由纯数字或’-‘号组成
+     */
+    private String telephone;
+    /**
+     * 成员头像的mediaid,通过素材管理接口上传图片获得的mediaid
+     */
+    private String avatarMediaId;
+    /**
+     * 启用/禁用成员。1表示启用成员,0表示禁用成员
+     */
+    private Boolean enable;
+    /**
+     * 地址。长度最大128个字符
+     */
+    private String address;
+
+    public UpdateUserReq userId(String userId) {
+        this.userId = userId;
+        return this;
+    }
+
+    public UpdateUserReq name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public UpdateUserReq alias(String alias) {
+        this.alias = alias;
+        return this;
+    }
+
+    public UpdateUserReq mobile(String mobile) {
+        this.mobile = mobile;
+        return this;
+    }
+
+    public UpdateUserReq email(String email) {
+        this.email = email;
+        return this;
+    }
+
+    public UpdateUserReq department(List<Department> department) {
+        this.department = department;
+        return this;
+    }
+
+    public UpdateUserReq position(String position) {
+        this.position = position;
+        return this;
+    }
+
+    public UpdateUserReq externalPosition(String externalPosition) {
+        this.externalPosition = externalPosition;
+        return this;
+    }
+
+    public UpdateUserReq gender(Gender gender) {
+        this.gender = gender;
+        return this;
+    }
+
+    public UpdateUserReq telephone(String telephone) {
+        this.telephone = telephone;
+        return this;
+    }
+
+    public UpdateUserReq avatar(String avatarMediaId) {
+        this.avatarMediaId = avatarMediaId;
+        return this;
+    }
+
+    public UpdateUserReq address(String address) {
+        this.address = address;
+        return this;
+    }
+
+    public UpdateUserReq enable(boolean enable) {
+        this.enable = enable;
+        return this;
+    }
+
+    public Map<String, Object> build() {
+        Map<String, Object> data = new HashMap<>(8);
+        data.put("userid", userId);
+        if (null != name) {
+            data.put("name", name);
+        }
+        if (null != alias) {
+            data.put("alias", alias);
+        }
+        if (null != mobile) {
+            data.put("mobile", mobile);
+        }
+        if (null != email) {
+            data.put("email", email);
+        }
+        if (null != department) {
+            List<Integer> idList = new ArrayList<>(department.size());
+            List<Integer> orderList = new ArrayList<>(department.size());
+            List<Integer> leaderList = new ArrayList<>(department.size());
+            department.forEach(dept -> {
+                idList.add(dept.id);
+                orderList.add(dept.order);
+                leaderList.add(dept.leader ? 1 : 0);
+            });
+            data.put("department", idList);
+            data.put("order", orderList);
+            data.put("is_leader_in_dept", leaderList);
+        }
+        if (null != gender) {
+            data.put("gender", gender.code);
+        }
+        if (null != telephone) {
+            data.put("telephone", telephone);
+        }
+        if (null != avatarMediaId) {
+            data.put("avatar_mediaid", avatarMediaId);
+        }
+        if (null != enable) {
+            data.put("enable", enable ? 1 : 0);
+        }
+        if (null != address) {
+            data.put("address", address);
+        }
+        if (null != position) {
+            data.put("position", position);
+        }
+        if (null != externalPosition) {
+            data.put("external_position", externalPosition);
+        }
+        return data;
+    }
+
+    public enum Gender {
+        MALE("1"), FEMALE("2");
+        private final String code;
+
+        Gender(String code) {
+            this.code = code;
+        }
+
+        public String getCode() {
+            return code;
+        }
+
+        public static Gender of(String code) {
+            for (Gender value : values()) {
+                if (value.code.equals(code)) {
+                    return value;
+                }
+            }
+            return MALE;
+        }
+    }
+
+    public static class Department {
+        /**
+         * 成员所属部门id
+         */
+        private Integer id;
+        /**
+         * 部门内的排序值,默认为0,成员次序以创建时间从小到大排列,数值越大排序越前面。有效的值范围是[0, 2^32)
+         */
+        private Integer order;
+        /**
+         * 表示在所在的部门内是否为上级。1表示为上级,0表示非上级。在审批等应用里可以用来标识上级审批人
+         */
+        private boolean leader;
+
+        public Department(Integer id, Integer order, boolean leader) {
+            this.id = id;
+            this.order = order;
+            this.leader = leader;
+        }
+    }
+}

+ 19 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UploadImageResp.java

@@ -0,0 +1,19 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class UploadImageResp extends BaseResp {
+    /**
+     * 上传后得到的图片URL。永久有效
+     */
+    private String url;
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}

+ 37 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/dto/UploadResp.java

@@ -0,0 +1,37 @@
+package com.usoftchina.qywx.sdk.dto;
+
+/**
+ * @author yingp
+ */
+public class UploadResp extends BaseResp{
+    private String type;
+    /**
+     * 媒体文件上传后获取的唯一标识,3天内有效
+     */
+    private String media_id;
+    private String created_at;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getMedia_id() {
+        return media_id;
+    }
+
+    public void setMedia_id(String media_id) {
+        this.media_id = media_id;
+    }
+
+    public String getCreated_at() {
+        return created_at;
+    }
+
+    public void setCreated_at(String created_at) {
+        this.created_at = created_at;
+    }
+}

+ 20 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/exception/WeiXinAccessException.java

@@ -0,0 +1,20 @@
+package com.usoftchina.qywx.sdk.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * @author yingp
+ */
+public class WeiXinAccessException extends RuntimeException {
+    private final HttpStatus status;
+
+    public WeiXinAccessException(ResponseEntity resp) {
+        super(String.valueOf(resp.getBody()));
+        this.status = resp.getStatusCode();
+    }
+
+    public HttpStatus getStatus() {
+        return status;
+    }
+}

+ 19 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/exception/WeiXinInvokeException.java

@@ -0,0 +1,19 @@
+package com.usoftchina.qywx.sdk.exception;
+
+import com.usoftchina.qywx.sdk.dto.BaseResp;
+
+/**
+ * @author yingp
+ */
+public class WeiXinInvokeException extends RuntimeException {
+    private final int code;
+
+    public WeiXinInvokeException(BaseResp resp) {
+        super(resp.getErrmsg());
+        this.code = resp.getErrcode();
+    }
+
+    public int getCode() {
+        return code;
+    }
+}

+ 174 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/HttpUtils.java

@@ -0,0 +1,174 @@
+package com.usoftchina.qywx.sdk.util;
+
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.support.config.FastJsonConfig;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+
+import javax.net.ssl.*;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ */
+public class HttpUtils {
+
+    public static RestTemplate createTemplate() {
+        RestTemplate restTemplate = new RestTemplate(new HttpsClientRequestFactory());
+
+        List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters().stream()
+                .filter(item -> !(item instanceof MappingJackson2HttpMessageConverter)
+                && !(item instanceof StringHttpMessageConverter))
+                .collect(Collectors.toList());
+        converters.add(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
+
+        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
+        List<MediaType> supportedMediaTypes = new ArrayList<>();
+        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
+        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
+        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
+        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
+        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
+        supportedMediaTypes.add(MediaType.APPLICATION_XML);
+        supportedMediaTypes.add(MediaType.IMAGE_GIF);
+        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
+        supportedMediaTypes.add(MediaType.IMAGE_PNG);
+        supportedMediaTypes.add(MediaType.TEXT_HTML);
+        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
+        supportedMediaTypes.add(MediaType.TEXT_XML);
+        supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
+        fastConverter.setSupportedMediaTypes(supportedMediaTypes);
+        FastJsonConfig fastJsonConfig = new FastJsonConfig();
+        //WriteNullListAsEmpty  :List字段如果为null,输出为[],而非null
+        //WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null
+        //DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)
+        //WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null
+        //WriteMapNullValue:是否输出值为null的字段,默认为false
+        fastJsonConfig.setSerializerFeatures(
+                SerializerFeature.DisableCircularReferenceDetect,
+                SerializerFeature.WriteMapNullValue
+        );
+        fastConverter.setFastJsonConfig(fastJsonConfig);
+        converters.add(fastConverter);
+        restTemplate.setMessageConverters(converters);
+
+        return restTemplate;
+    }
+
+    static class HttpsClientRequestFactory extends SimpleClientHttpRequestFactory {
+        @Override
+        protected void prepareConnection(HttpURLConnection connection, String httpMethod) {
+            try {
+                if (!(connection instanceof HttpsURLConnection)) {
+                    throw new RuntimeException("An instance of HttpsURLConnection is expected");
+                }
+
+                HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
+
+                TrustManager[] trustAllCerts = new TrustManager[]{
+                        new X509TrustManager() {
+                            @Override
+                            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+                                return null;
+                            }
+
+                            @Override
+                            public void checkClientTrusted(X509Certificate[] certs, String authType) {
+                            }
+
+                            @Override
+                            public void checkServerTrusted(X509Certificate[] certs, String authType) {
+                            }
+
+                        }
+                };
+                SSLContext sslContext = SSLContext.getInstance("TLS");
+                sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
+                httpsConnection.setSSLSocketFactory(new MySSLSocketFactory(sslContext.getSocketFactory()));
+
+                httpsConnection.setHostnameVerifier(new HostnameVerifier() {
+                    @Override
+                    public boolean verify(String s, SSLSession sslSession) {
+                        return true;
+                    }
+                });
+
+                super.prepareConnection(httpsConnection, httpMethod);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        static class MySSLSocketFactory extends SSLSocketFactory {
+
+            private final SSLSocketFactory delegate;
+
+            public MySSLSocketFactory(SSLSocketFactory delegate) {
+                this.delegate = delegate;
+            }
+
+            @Override
+            public String[] getDefaultCipherSuites() {
+                return delegate.getDefaultCipherSuites();
+            }
+
+            @Override
+            public String[] getSupportedCipherSuites() {
+                return delegate.getSupportedCipherSuites();
+            }
+
+            @Override
+            public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose) throws IOException {
+                final Socket underlyingSocket = delegate.createSocket(socket, host, port, autoClose);
+                return overrideProtocol(underlyingSocket);
+            }
+
+            @Override
+            public Socket createSocket(final String host, final int port) throws IOException {
+                final Socket underlyingSocket = delegate.createSocket(host, port);
+                return overrideProtocol(underlyingSocket);
+            }
+
+            @Override
+            public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort) throws
+                    IOException {
+                final Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort);
+                return overrideProtocol(underlyingSocket);
+            }
+
+            @Override
+            public Socket createSocket(final InetAddress host, final int port) throws IOException {
+                final Socket underlyingSocket = delegate.createSocket(host, port);
+                return overrideProtocol(underlyingSocket);
+            }
+
+            @Override
+            public Socket createSocket(final InetAddress host, final int port, final InetAddress localAddress, final int localPort) throws
+                    IOException {
+                final Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort);
+                return overrideProtocol(underlyingSocket);
+            }
+
+            private Socket overrideProtocol(final Socket socket) {
+                if (!(socket instanceof SSLSocket)) {
+                    throw new RuntimeException("An instance of SSLSocket is expected");
+                }
+                ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1"});
+                return socket;
+            }
+        }
+    }
+}

+ 11 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/QywxConst.java

@@ -0,0 +1,11 @@
+package com.usoftchina.qywx.sdk.util;
+
+/**
+ * @author yingp
+ */
+public class QywxConst {
+
+    public static final String API_BASE_URL = "https://qyapi.weixin.qq.com";
+
+    public static final String OAUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
+}

+ 35 - 0
qywx-sdk/src/main/java/com/usoftchina/qywx/sdk/util/UrlUtils.java

@@ -0,0 +1,35 @@
+package com.usoftchina.qywx.sdk.util;
+
+import java.net.URLEncoder;
+import java.util.Base64;
+import java.util.regex.Pattern;
+
+/**
+ * @author yingp
+ */
+public class UrlUtils {
+
+    private final static Pattern urlPattern = Pattern.compile("https{0,1}://[^\\x{4e00}-\\x{9fa5}\\n\\r\\s]{3,}");
+
+    /**
+     * 生成获取授权,自动登录后跳转实际界面的oauth链接
+     *
+     * @param corpId       公司ID
+     * @param master       账套
+     * @param agentCode    应用编号
+     * @param agentBaseUrl 应用主页
+     * @param authUrl      授权url
+     * @param redirectUrl  跳转页
+     * @return
+     * @throws Exception
+     */
+    public static String generateOAuthUrl(String corpId, String master, String agentCode, String agentBaseUrl, String authUrl, String redirectUrl) throws Exception {
+        if (!urlPattern.matcher(redirectUrl).matches()) {
+            redirectUrl = agentBaseUrl + redirectUrl;
+        }
+        String base64Url = Base64.getEncoder().encodeToString(redirectUrl.getBytes());
+        String oauthUrl = String.format("%s?agent=%s&url=%s&master=%s", authUrl, agentCode, base64Url, master);
+        return String.format("%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=#wechat_redirect",
+                QywxConst.OAUTH_URL, corpId, URLEncoder.encode(oauthUrl, "UTF-8"));
+    }
+}

+ 90 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/AddrBookSdkTest.java

@@ -0,0 +1,90 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.alibaba.fastjson.JSON;
+import com.usoftchina.qywx.sdk.AddrBookSdk;
+import com.usoftchina.qywx.sdk.dto.*;
+import org.junit.Test;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class AddrBookSdkTest extends BaseTest {
+
+    private static AddrBookSdk sdk;
+
+    static {
+        sdk = new AddrBookSdk(properties);
+    }
+
+    @Test
+    public void getDepartmentList() {
+        List<GetDepartmentListResp.Department> departmentList = sdk.getDepartmentList();
+        if (!CollectionUtils.isEmpty(departmentList)) {
+            System.out.println(JSON.toJSONString(departmentList));
+            departmentList.forEach(department -> {
+                if (department.getName().equals("规划投资部")) {
+                    System.out.println(JSON.toJSONString(department));
+                }
+            });
+        }
+    }
+
+    @Test
+    public void getDepartment() {
+        GetDepartmentListResp.Department department = sdk.getDepartment(1543022854);
+        if (null != department) {
+            System.out.println(JSON.toJSONString(department));
+        }
+    }
+
+    @Test
+    public void getUserList() {
+        List<GetUserListResp.User> userList = sdk.getUserList(1, true);
+        if (!CollectionUtils.isEmpty(userList)) {
+//            System.out.println(JSON.toJSONString(userList));
+            for (GetUserListResp.User user : userList) {
+                if ("张磊".equals(user.getName())) {
+                    System.out.println(JSON.toJSONString(user));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void deleteUser() {
+        sdk.deleteUser("qy01d3ffd8cd39d7002a66ffc62c");
+    }
+
+    @Test
+    public void updateUser() {
+//        sdk.updateUser(new UpdateUserReq().userId("YingPeng").alias("鹏"));
+
+        List<GetUserListResp.User> userList = sdk.getUserList(1, true);
+        if (!CollectionUtils.isEmpty(userList)) {
+            userList.forEach(user -> {
+                if (!user.getUserid().equals("YingPeng")) {
+                    sdk.updateUser(new UpdateUserReq().userId(user.getUserid()).enable(false));
+                }
+            });
+        }
+    }
+
+    @Test
+    public void createUser() {
+        sdk.createUser(new CreateUserReq().toInvite(false).name("测试").mobile("15361512390")
+                .department(Arrays.asList(new CreateUserReq.Department(1, null, false)))
+                .userId("Test15361512390")
+                .position("测试人员")
+                .gender(CreateUserReq.Gender.MALE));
+    }
+
+    @Test
+    public void updateDepartment() {
+        sdk.updateDepartment(new UpdateDepartmentReq().id(1).name("优软科技事业部"));
+    }
+
+}

+ 42 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/BaseTest.java

@@ -0,0 +1,42 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.usoftchina.qywx.sdk.config.Agent;
+import com.usoftchina.qywx.sdk.config.QywxProperties;
+
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+/**
+ * @author yingp
+ * @date 2020/2/9
+ */
+public abstract class BaseTest {
+
+    protected static QywxProperties properties;
+
+    static {
+        properties = new QywxProperties();
+        properties.setCorpId("wwd42c39382ee6298e");
+        List<Agent> agents = new ArrayList<>(3);
+        // 通讯录
+        agents.add(new Agent("AddressBook", null, "uXuJXVLSjMsvvqPeb8tFDAVDZAphz1PoMcMN4KDuV7g"));
+        // 日程
+        agents.add(new Agent("Schedule", null, "hGfE3pCpKAKlzT0KGXeAVsq4W0niqxI1NERl0ztuMKE"));
+        // 打卡
+        agents.add(new Agent("Checkin", 3010011, "jCtXuCvBUQDSSTt3chYDs0qHhYeBPEdXxGV7TX7rmm8"));
+        // 审批
+        agents.add(new Agent("Approval", 3010040, "sSwb62-S-bGftHcCVChnxASz8SnqnvobtIU8Q42kT2w"));
+
+        // UAS系统
+        agents.add(new Agent("Uas", 1000003, "mMojQmQVSeYmh575GhLObRxRFCYKIwvzwOXM8qqaOnY"));
+        // U报表
+        agents.add(new Agent("UasReport", 1000005, "L6lEyc4qYP5lDdErTyyCrWKTAOzRW9sFwfU7ovdHKWs"));
+        // U订阅
+        agents.add(new Agent("UasSubscribe", 1000004, "_hmVlsD4gwT4a2qfNAP1FYEm4yAAIRUcjeuSW2cQ4YE"));
+        // U审批
+        agents.add(new Agent("UasAudit", 1000006, "hdzl2Qhb0oKj28o2PASS53i9jxe3vJI6RdGcPIc_pnc"));
+        properties.setAgents(agents);
+    }
+}

+ 52 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/MessageSdkTest.java

@@ -0,0 +1,52 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.usoftchina.qywx.sdk.dto.CreateChatReq;
+import com.usoftchina.qywx.sdk.dto.SendChatReq;
+import com.usoftchina.qywx.sdk.dto.SendMessageReq;
+import com.usoftchina.qywx.sdk.MessageSdk;
+import com.usoftchina.qywx.sdk.util.UrlUtils;
+import org.junit.Test;
+
+import java.util.*;
+
+/**
+ * @author yingp
+ */
+public class MessageSdkTest extends BaseTest {
+
+    private static MessageSdk sdk;
+
+    static {
+        sdk = new MessageSdk(properties);
+    }
+
+    @Test
+    public void sendTaskCard() throws Exception {
+        SendMessageReq.Btn btn1 = new SendMessageReq.Btn("yes", "批准", "已批准");
+        SendMessageReq.Btn btn2 = new SendMessageReq.Btn("no", "驳回", "已驳回");
+        String oauthUrl = UrlUtils.generateOAuthUrl("wwbb7e27c4decb7872", "N_USOFTSYS", "UasAudit", "http://2788e5c924.qicp.vip/uas",
+                "http://2788e5c924.qicp.vip/qywx/api/authorize", "/jsps/common/flow.jsp?whoami=JProcess!Me&formCondition=jp_nodeId=44382369&gridCondition=null=44382369&_noc=1");
+        sdk.send("UasAudit", new SendMessageReq()
+                .taskCard("工作协助", "吕全明向你发起了工作协助<div class=\"highlight\">2019050003</div>",
+                        oauthUrl, "2019050003", Arrays.asList(btn1, btn2))
+                .toUser("U0308"));
+    }
+
+    @Test
+    public void sendTextCard() throws Exception {
+        String oauthUrl = UrlUtils.generateOAuthUrl("wwd42c39382ee6298e", "N_USOFTSYS", "Uas", "http://2788e5c924.qicp.vip/uas",
+                "http://2788e5c924.qicp.vip/qywx/api/authorize","/jsps/common/flow.jsp?whoami=JProcess!Me&formCondition=jp_nodeId=44382369&gridCondition=null=44382369&_noc=1");
+        sdk.send("UasAudit", new SendMessageReq()
+                .textCard("工作协助", "吕全明向你发起了工作协助<div class=\"highlight\">2019050002</div>", oauthUrl, "详情")
+                .toUser("U0308"));
+    }
+
+    @Test
+    public void createChat() {
+        String chatId = sdk.createChat("Uas", new CreateChatReq().name("微信项目讨论组")
+                .owner("YingPeng")
+                .user("YingPeng", "ZhouYuan"));
+        System.out.println(chatId);
+        sdk.sendChat("Uas", new SendChatReq(chatId).text("Hello World"));
+    }
+}

+ 34 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/OaSdkTest.java

@@ -0,0 +1,34 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.alibaba.fastjson.JSON;
+import com.usoftchina.qywx.sdk.OaSdk;
+import com.usoftchina.qywx.sdk.dto.GetCheckinOptionReq;
+import com.usoftchina.qywx.sdk.dto.GetCheckinOptionResp;
+import org.junit.Test;
+import org.springframework.util.CollectionUtils;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author yingp
+ */
+public class OaSdkTest extends BaseTest {
+
+    private static OaSdk sdk;
+
+    static {
+        sdk = new OaSdk(properties);
+    }
+
+    @Test
+    public void getCheckinOption() {
+        List<GetCheckinOptionResp.CheckinOption> options = sdk.getCheckinOption(new GetCheckinOptionReq(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() / 1000,
+                Arrays.asList("YingPeng")));
+        if (!CollectionUtils.isEmpty(options)) {
+            System.out.println(JSON.toJSONString(options));
+        }
+    }
+}

+ 32 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/ScheduleSdkTest.java

@@ -0,0 +1,32 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.usoftchina.qywx.sdk.ScheduleSdk;
+import com.usoftchina.qywx.sdk.dto.AddScheduleReq;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * @author yingp
+ */
+public class ScheduleSdkTest extends BaseTest {
+
+    private static ScheduleSdk sdk;
+
+    static {
+        sdk = new ScheduleSdk(properties);
+    }
+
+    @Test
+    public void addSchedule() {
+        long now = System.currentTimeMillis();
+        String id = sdk.addSchedule(new AddScheduleReq()
+                .organizer("YingPeng")
+                .summary("测试日程")
+                .description("测试日程详情")
+                .attendees(Arrays.asList("YeZi"))
+                .during(now / 1000 + 360, now / 1000 + 3660)
+                .remindBefore(300));
+        System.out.println(id);
+    }
+}

+ 14 - 0
qywx-sdk/src/test/java/com/usoftchina/qywx/sdk/test/UrlTest.java

@@ -0,0 +1,14 @@
+package com.usoftchina.qywx.sdk.test;
+
+import com.usoftchina.qywx.sdk.util.UrlUtils;
+
+/**
+ * @author yingp
+ */
+public class UrlTest {
+
+    public static void main(String[] args) throws Exception {
+        // https://open.weixin.qq.com/connect/oauth2/authorize?appid=wwbb7e27c4decb7872&redirect_uri=http%3A%2F%2F2788e5c924.qicp.vip%2Fqywx%2Fapi%2Fauthorize%3Fagent%3DUas%26url%3DaHR0cDovLzI3ODhlNWM5MjQucWljcC52aXAvdWFzLw%3D%3D%26master%3D&response_type=code&scope=snsapi_base&state=#wechat_redirect
+        System.out.println(UrlUtils.generateOAuthUrl("wwbb7e27c4decb7872", "", "Uas", "http://2788e5c924.qicp.vip/uas", "http://2788e5c924.qicp.vip/qywx/api/authorize","/"));
+    }
+}

+ 8 - 0
settings.gradle

@@ -0,0 +1,8 @@
+rootProject.name = 'uas-office-integartion'
+include 'qywx-sdk'
+include 'uas-office-server'
+include 'uas-office-core'
+include 'uas-office-qywx'
+include 'dingtalk-sdk'
+include 'uas-office-dingtalk'
+

+ 6 - 0
uas-office-core/build.gradle

@@ -0,0 +1,6 @@
+dependencies {
+    compile 'org.springframework.boot:spring-boot-starter-web'
+    compile 'org.springframework.boot:spring-boot-starter-jdbc'
+    compile 'org.springframework.boot:spring-boot-starter-data-redis'
+    compile "$fastjson"
+}

+ 35 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/config/RedisConfig.java

@@ -0,0 +1,35 @@
+package com.usoftchina.uas.office.config;
+
+import com.usoftchina.uas.office.listener.UasEventListenerAdapter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.listener.ChannelTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public UasEventListenerAdapter uasOperationListener() {
+        return new UasEventListenerAdapter();
+    }
+
+    @Bean
+    public ChannelTopic uasTopic() {
+        return new ChannelTopic("channel:uas");
+    }
+
+    @Bean
+    public RedisMessageListenerContainer redisMessageListenerContainer(LettuceConnectionFactory connectionFactory) {
+        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+        container.setConnectionFactory(connectionFactory);
+        // 订阅来自uas的消息
+        container.addMessageListener(uasOperationListener(), uasTopic());
+        return container;
+    }
+}

+ 24 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/context/ContextHolder.java

@@ -0,0 +1,24 @@
+package com.usoftchina.uas.office.context;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author yingp
+ * @date 2019/5/6
+ */
+@Component
+public class ContextHolder implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) {
+        ContextHolder.applicationContext = applicationContext;
+    }
+
+    public static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+}

+ 38 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/context/MasterHolder.java

@@ -0,0 +1,38 @@
+package com.usoftchina.uas.office.context;
+
+import com.usoftchina.uas.office.entity.Master;
+
+/**
+ * @author yingp
+ * @date 2020/1/13
+ */
+public class MasterHolder {
+
+    private static final ThreadLocal<Master> threadLocalMaster = new InheritableThreadLocal<>();
+
+    /**
+     * 获取当前Master
+     *
+     * @return
+     */
+    public static Master get() {
+        return threadLocalMaster.get();
+    }
+
+    /**
+     * 切换Master
+     *
+     * @param master
+     */
+    public static void set(Master master) {
+        Master old = get();
+        boolean changed = null == old || !old.equals(master);
+        if (changed) {
+            threadLocalMaster.set(master);
+        }
+    }
+
+    public static void clear() {
+        threadLocalMaster.set(null);
+    }
+}

+ 29 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/controller/DataCenterController.java

@@ -0,0 +1,29 @@
+package com.usoftchina.uas.office.controller;
+
+import com.usoftchina.uas.office.dto.Result;
+import com.usoftchina.uas.office.entity.DataCenter;
+import com.usoftchina.uas.office.service.DataCenterService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author yingp
+ * @date 2020/2/15
+ */
+@RequestMapping(path = "/mgm")
+@RestController
+public class DataCenterController {
+    @Autowired
+    private DataCenterService dataCenterService;
+
+    @GetMapping(path = "/dc")
+    public Result getDataCenter() {
+        return Result.success(dataCenterService.find());
+    }
+
+    @PostMapping(path = "/dc")
+    public Result saveDataCenter(DataCenter dataCenter) throws Exception{
+        dataCenterService.save(dataCenter);
+        return Result.success();
+    }
+}

+ 120 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/dto/Result.java

@@ -0,0 +1,120 @@
+package com.usoftchina.uas.office.dto;
+
+import com.alibaba.fastjson.JSON;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * @author Pro1
+ * @date 2017/6/23.
+ */
+public class Result<T> {
+
+    private boolean success;
+
+    private Integer code;
+
+    private String message;
+
+    private T data;
+
+    public boolean isSuccess() {
+        return success;
+    }
+
+    public void setSuccess(boolean success) {
+        this.success = success;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public static Result success() {
+        Result result = new Result();
+        result.setSuccess(true);
+        return result;
+    }
+
+    public static <T> Result success(T data) {
+        Result<T> result = success();
+        result.setData(data);
+        return result;
+    }
+
+    public static Result error() {
+        Result result = new Result();
+        result.setSuccess(false);
+        return result;
+    }
+
+    public static Result error(String message) {
+        Result result = error();
+        result.setMessage(message);
+        return result;
+    }
+
+    public static Result error(int code, String message) {
+        Result result = error();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+
+    public static Result error(int code, String message, Object... args) {
+        Result result = error();
+        result.setCode(code);
+        result.setMessage(String.format(message, args));
+        return result;
+    }
+
+    public static <T> void success(HttpServletResponse response, T data) throws IOException {
+        response.setStatus(HttpStatus.OK.value());
+        response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
+        PrintWriter printWriter = response.getWriter();
+        printWriter.append(JSON.toJSONString(success(data)));
+        printWriter.flush();
+        printWriter.close();
+    }
+
+    public static <T> void error(HttpServletResponse response, int code, String message) throws IOException{
+        response.setStatus(HttpStatus.OK.value());
+        response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
+        PrintWriter printWriter = response.getWriter();
+        printWriter.append(JSON.toJSONString(error(code, message)));
+        printWriter.flush();
+        printWriter.close();
+    }
+
+    public static <T> void error(HttpServletResponse response, String message) throws IOException {
+        error(response, HttpStatus.BAD_REQUEST.value(), message);
+    }
+
+    public static <T> void error(HttpServletResponse response) throws IOException {
+        error(response, HttpStatus.BAD_REQUEST.value(), null);
+    }
+}

+ 64 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/dto/UasEvent.java

@@ -0,0 +1,64 @@
+package com.usoftchina.uas.office.dto;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+public class UasEvent {
+    private String operation;
+    private String caller;
+    private Object key;
+    private String master;
+    private Long timestamp;
+
+    public String getOperation() {
+        return operation;
+    }
+
+    public void setOperation(String operation) {
+        this.operation = operation;
+    }
+
+    public String getCaller() {
+        return caller;
+    }
+
+    public void setCaller(String caller) {
+        this.caller = caller;
+    }
+
+    public Object getKey() {
+        return key;
+    }
+
+    public void setKey(Object key) {
+        this.key = key;
+    }
+
+    public String getMaster() {
+        return master;
+    }
+
+    public void setMaster(String master) {
+        this.master = master;
+    }
+
+    public Long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(Long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    @Override
+    public String toString() {
+        return "UasEvent{" +
+                "operation='" + operation + '\'' +
+                ", caller='" + caller + '\'' +
+                ", key=" + key +
+                ", master='" + master + '\'' +
+                ", timestamp=" + timestamp +
+                '}';
+    }
+}

+ 126 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/entity/DataCenter.java

@@ -0,0 +1,126 @@
+package com.usoftchina.uas.office.entity;
+
+import com.usoftchina.uas.office.jdbc.DataSourceBean;
+import com.usoftchina.uas.office.util.StringUtils;
+
+import java.util.Objects;
+
+/**
+ * @author yingp
+ * @date 2020/1/15
+ */
+public class DataCenter implements DataSourceBean {
+
+    private Integer id;
+    private String url;
+    private String username;
+    private String password;
+    private String driverClassName;
+    /**
+     * uas外网地址
+     */
+    private String erpOuterUrl;
+    /**
+     * 当前对接程序,外网地址
+     */
+    private String outerUrl;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getDriverClassName() {
+        return driverClassName;
+    }
+
+    public void setDriverClassName(String driverClassName) {
+        this.driverClassName = driverClassName;
+    }
+
+    public String getErpOuterUrl() {
+        return erpOuterUrl;
+    }
+
+    public void setErpOuterUrl(String erpOuterUrl) {
+        this.erpOuterUrl = erpOuterUrl;
+    }
+
+    @Override
+    public String url() {
+        return url;
+    }
+
+    @Override
+    public String username() {
+        return username;
+    }
+
+    @Override
+    public String password() {
+        return password;
+    }
+
+    @Override
+    public String driverClassName() {
+        return StringUtils.nvl(driverClassName, "oracle.jdbc.OracleDriver");
+    }
+
+    public String getOuterUrl() {
+        return outerUrl;
+    }
+
+    public void setOuterUrl(String outerUrl) {
+        this.outerUrl = outerUrl;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        DataCenter that = (DataCenter) o;
+        return Objects.equals(id, that.id) &&
+                Objects.equals(url, that.url) &&
+                Objects.equals(username, that.username) &&
+                Objects.equals(password, that.password) &&
+                Objects.equals(driverClassName, that.driverClassName) &&
+                Objects.equals(erpOuterUrl, that.erpOuterUrl) &&
+                Objects.equals(outerUrl, that.outerUrl);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, url, username, password, driverClassName, erpOuterUrl, outerUrl);
+    }
+}

+ 129 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/entity/Master.java

@@ -0,0 +1,129 @@
+package com.usoftchina.uas.office.entity;
+
+import com.usoftchina.uas.office.jdbc.DataSourceBean;
+import com.usoftchina.uas.office.util.StringUtils;
+
+import java.util.Objects;
+
+/**
+ * @author yingp
+ */
+public class Master implements DataSourceBean {
+
+    private int ma_id;
+    private String ma_user;// 数据库用户名
+    private String ms_pwd;// 数据库密码
+    private String ma_name;// 帐套名,与bean名一致
+    private String ma_function;
+    private Short ma_enable;// 是否可使用
+    private String ma_url;
+    private String ma_driver;
+
+    public int getMa_id() {
+        return ma_id;
+    }
+
+    public void setMa_id(int ma_id) {
+        this.ma_id = ma_id;
+    }
+
+    public String getMa_user() {
+        return ma_user;
+    }
+
+    public void setMa_user(String ma_user) {
+        this.ma_user = ma_user;
+    }
+
+    public String getMs_pwd() {
+        return ms_pwd;
+    }
+
+    public void setMs_pwd(String ms_pwd) {
+        this.ms_pwd = ms_pwd;
+    }
+
+    public String getMa_name() {
+        return ma_name;
+    }
+
+    public void setMa_name(String ma_name) {
+        this.ma_name = ma_name;
+    }
+
+    public String getMa_function() {
+        return ma_function;
+    }
+
+    public void setMa_function(String ma_function) {
+        this.ma_function = ma_function;
+    }
+
+    public Short getMa_enable() {
+        return ma_enable;
+    }
+
+    public void setMa_enable(Short ma_enable) {
+        this.ma_enable = ma_enable;
+    }
+
+    public String getMa_url() {
+        return ma_url;
+    }
+
+    public void setMa_url(String ma_url) {
+        this.ma_url = ma_url;
+    }
+
+    public String getMa_driver() {
+        return ma_driver;
+    }
+
+    public void setMa_driver(String ma_driver) {
+        this.ma_driver = ma_driver;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Master master = (Master) o;
+        return ma_id == master.ma_id &&
+                Objects.equals(ma_user, master.ma_user) &&
+                Objects.equals(ms_pwd, master.ms_pwd) &&
+                Objects.equals(ma_name, master.ma_name) &&
+                Objects.equals(ma_function, master.ma_function) &&
+                Objects.equals(ma_enable, master.ma_enable) &&
+                Objects.equals(ma_url, master.ma_url) &&
+                Objects.equals(ma_driver, master.ma_driver);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ma_id, ma_user, ms_pwd, ma_name, ma_function, ma_enable, ma_url, ma_driver);
+    }
+
+    @Override
+    public String url() {
+        return ma_url;
+    }
+
+    @Override
+    public String username() {
+        return ma_user.toUpperCase();
+    }
+
+    @Override
+    public String password() {
+        return ms_pwd;
+    }
+
+    @Override
+    public String driverClassName() {
+        return StringUtils.nvl(ma_driver, "oracle.jdbc.OracleDriver");
+    }
+}

+ 22 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/event/DataCenterEvent.java

@@ -0,0 +1,22 @@
+package com.usoftchina.uas.office.event;
+
+import com.usoftchina.uas.office.entity.DataCenter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @author yingp
+ * @date 2020/1/15
+ */
+public class DataCenterEvent extends ApplicationEvent {
+
+    private final DataCenter dataCenter;
+
+    public DataCenterEvent(Object source, DataCenter dataCenter) {
+        super(source);
+        this.dataCenter = dataCenter;
+    }
+
+    public DataCenter getDataCenter() {
+        return dataCenter;
+    }
+}

+ 22 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/event/MasterEvent.java

@@ -0,0 +1,22 @@
+package com.usoftchina.uas.office.event;
+
+import com.usoftchina.uas.office.entity.Master;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @author yingp
+ * @date 2020/1/15
+ */
+public class MasterEvent extends ApplicationEvent {
+
+    private final Master master;
+
+    public MasterEvent(Object source, Master master) {
+        super(source);
+        this.master = master;
+    }
+
+    public Master getMaster() {
+        return master;
+    }
+}

+ 46 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DataSourceBean.java

@@ -0,0 +1,46 @@
+package com.usoftchina.uas.office.jdbc;
+
+/**
+ * 数据源配置
+ *
+ * @author yingp
+ * @date 2019/5/23
+ */
+public interface DataSourceBean {
+    /**
+     * jdbc url
+     *
+     * @return
+     */
+    String url();
+
+    /**
+     * 用户
+     *
+     * @return
+     */
+    String username();
+
+    /**
+     * 密码
+     *
+     * @return
+     */
+    String password();
+
+    /**
+     * 驱动
+     *
+     * @return
+     */
+    String driverClassName();
+
+    /**
+     * 连接信息
+     *
+     * @return
+     */
+    default String info() {
+        return "datasource{url: " + url() + ", username: " + username() + "}";
+    }
+}

+ 36 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DataSourceHolder.java

@@ -0,0 +1,36 @@
+package com.usoftchina.uas.office.jdbc;
+
+/**
+ * @author yingp
+ * @date 2019/5/23
+ */
+public abstract class DataSourceHolder {
+
+    private static final ThreadLocal<DataSourceBean> threadLocalBean = new InheritableThreadLocal<>();
+
+    /**
+     * 获取当前数据源
+     *
+     * @return
+     */
+    public static DataSourceBean get() {
+        return threadLocalBean.get();
+    }
+
+    /**
+     * 切换到新数据源
+     *
+     * @param bean
+     */
+    public static void set(DataSourceBean bean) {
+        DataSourceBean old = get();
+        boolean changed = null == old || !old.equals(bean);
+        if (changed) {
+            threadLocalBean.set(bean);
+        }
+    }
+
+    public static void clear() {
+        threadLocalBean.set(null);
+    }
+}

+ 172 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSource.java

@@ -0,0 +1,172 @@
+package com.usoftchina.uas.office.jdbc;
+
+import com.usoftchina.uas.office.util.StringUtils;
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.jdbc.datasource.AbstractDataSource;
+import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
+import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
+import org.springframework.util.Assert;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 基于{@AbstractRoutingDataSource},解决不能动态添加新数据源的问题
+ *
+ * @author yingp
+ * @date 2017/7/27
+ */
+public class DynamicDataSource extends AbstractDataSource implements InitializingBean {
+
+    private Map<Object, Object> targetDataSources;
+    private Object defaultTargetDataSource;
+    private boolean lenientFallback = true;
+    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
+    private Map<Object, DataSource> resolvedDataSources;
+    private DataSource resolvedDefaultDataSource;
+
+    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
+        this.targetDataSources = targetDataSources;
+    }
+
+    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
+        this.defaultTargetDataSource = defaultTargetDataSource;
+    }
+
+    public void setLenientFallback(boolean lenientFallback) {
+        this.lenientFallback = lenientFallback;
+    }
+
+    public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
+        this.dataSourceLookup = (DataSourceLookup) (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        if (this.targetDataSources == null) {
+            throw new IllegalArgumentException("Property \'targetDataSources\' is required");
+        } else {
+            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
+            Iterator var1 = this.targetDataSources.entrySet().iterator();
+
+            while (var1.hasNext()) {
+                Map.Entry entry = (Map.Entry) var1.next();
+                Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
+                DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
+                this.resolvedDataSources.put(lookupKey, dataSource);
+            }
+
+            if (this.defaultTargetDataSource != null) {
+                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
+            }
+
+        }
+    }
+
+    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
+        return lookupKey;
+    }
+
+    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
+        if (dataSource instanceof DataSource) {
+            return (DataSource) dataSource;
+        } else if (dataSource instanceof String) {
+            return this.dataSourceLookup.getDataSource((String) dataSource);
+        } else {
+            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
+        }
+    }
+
+    @Override
+    public Connection getConnection() throws SQLException {
+        return this.determineTargetDataSource().getConnection();
+    }
+
+    @Override
+    public Connection getConnection(String username, String password) throws SQLException {
+        return this.determineTargetDataSource().getConnection(username, password);
+    }
+
+    public String getDriverClassName() throws SQLException{
+        HikariDataSource dataSource = unwrap(HikariDataSource.class);
+        return dataSource.getDriverClassName();
+    }
+
+    public String getJdbcUrl() throws SQLException {
+        HikariDataSource dataSource = unwrap(HikariDataSource.class);
+        return dataSource.getJdbcUrl();
+    }
+
+    public String lookupSchema() throws SQLException{
+        HikariDataSource dataSource = unwrap(HikariDataSource.class);
+        return StringUtils.nvl(dataSource.getSchema(), dataSource.getUsername()).toUpperCase();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T unwrap(Class<T> iface) throws SQLException {
+        if (iface.isInstance(this)) {
+            return (T) this;
+        }
+        return determineTargetDataSource().unwrap(iface);
+    }
+
+    @Override
+    public boolean isWrapperFor(Class<?> iface) throws SQLException {
+        return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
+    }
+
+    protected DataSource determineTargetDataSource() {
+        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
+        Object lookupKey = this.determineCurrentLookupKey();
+        DataSource dataSource = (DataSource) this.resolvedDataSources.get(lookupKey);
+        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
+            dataSource = this.resolvedDefaultDataSource;
+        }
+
+        if (dataSource == null) {
+            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
+        } else {
+            return dataSource;
+        }
+    }
+
+    protected Object determineCurrentLookupKey() {
+        return DataSourceHolder.get();
+    }
+
+    public boolean containsDataSource(Object key) {
+        Object lookupKey = this.resolveSpecifiedLookupKey(key);
+        return this.resolvedDataSources.containsKey(lookupKey);
+    }
+
+    public void addDataSource(Object key, Object value) {
+        Object lookupKey = this.resolveSpecifiedLookupKey(key);
+        if (!containsDataSource(lookupKey)) {
+            DataSource dataSource = this.resolveSpecifiedDataSource(value);
+            this.resolvedDataSources.put(lookupKey, dataSource);
+        }
+    }
+
+    public void removeDataSource(Object key) {
+        Object lookupKey = this.resolveSpecifiedLookupKey(key);
+        if (!containsDataSource(lookupKey)) {
+            this.resolvedDataSources.remove(key);
+        }
+    }
+
+    public void removeAllResolved() {
+        Iterator var1 = this.resolvedDataSources.entrySet().iterator();
+        while (var1.hasNext()) {
+            Map.Entry entry = (Map.Entry) var1.next();
+            if (!"defaultDataSource".equals(entry.getKey())) {
+                var1.remove();
+            }
+        }
+    }
+}

+ 81 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSourceConfig.java

@@ -0,0 +1,81 @@
+package com.usoftchina.uas.office.jdbc;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.util.StringUtils;
+
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author yingp
+ * @date 2018/12/10
+ */
+@Configuration
+@ConditionalOnClass({HikariDataSource.class})
+@ConditionalOnMissingBean({DataSource.class})
+@EnableConfigurationProperties({
+        DataSourceProperties.class
+})
+public class DynamicDataSourceConfig {
+
+    @Autowired
+    private DataSourceProperties properties;
+
+    @Primary
+    @Bean(name = "defaultDataSource")
+    @ConfigurationProperties(
+            prefix = "spring.datasource.primary"
+    )
+    public HikariDataSource defaultDataSource() {
+        HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+        if (StringUtils.hasText(properties.getName())) {
+            dataSource.setPoolName(properties.getName());
+        }
+
+        return dataSource;
+    }
+
+    @Bean(name = "dynamicDataSource")
+    public DynamicDataSource dynamicDataSource(@Qualifier("defaultDataSource") DataSource defaultDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>(1);
+        targetDataSources.put("defaultDataSource", defaultDataSource);
+        DynamicDataSource dataSource = new DynamicDataSource();
+        dataSource.setDefaultTargetDataSource(defaultDataSource);
+        dataSource.setTargetDataSources(targetDataSources);
+        return dataSource;
+    }
+
+    @Bean
+    public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) throws Exception {
+        return new DataSourceTransactionManager(dataSource);
+    }
+
+    @Bean
+    public DynamicDataSourceRegister dataSourceRegister(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) {
+        return new DynamicDataSourceRegister(properties, dataSource);
+    }
+
+    @Bean
+    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) {
+        return new JdbcTemplate(dataSource);
+    }
+
+    @Bean
+    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) {
+        return new NamedParameterJdbcTemplate(dataSource);
+    }
+}

+ 46 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/DynamicDataSourceRegister.java

@@ -0,0 +1,46 @@
+package com.usoftchina.uas.office.jdbc;
+
+import com.usoftchina.uas.office.util.StringUtils;
+import com.zaxxer.hikari.HikariDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+
+import java.sql.SQLException;
+
+/**
+ * 数据源动态注册
+ *
+ * @author yingp
+ * @date 2017/7/27
+ */
+public class DynamicDataSourceRegister {
+
+    private DataSourceProperties defaultProperties;
+    private DynamicDataSource dynamicDataSource;
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    public DynamicDataSourceRegister(DataSourceProperties defaultProperties, DynamicDataSource dynamicDataSource) {
+        this.defaultProperties = defaultProperties;
+        this.dynamicDataSource = dynamicDataSource;
+    }
+
+    public void register(DataSourceBean bean) throws SQLException {
+        if (!dynamicDataSource.containsDataSource(bean)) {
+            logger.info(bean.info());
+            HikariDataSource dataSource = DataSourceBuilder.create(defaultProperties.getClassLoader())
+                    .type(HikariDataSource.class)
+                    .driverClassName(StringUtils.nvl(bean.driverClassName(), defaultProperties.determineDriverClassName()))
+                    .url(StringUtils.nvl(bean.url(), dynamicDataSource.getJdbcUrl()))
+                    .username(bean.username())
+                    .password(bean.password())
+                    .build();
+            dynamicDataSource.addDataSource(bean, dataSource);
+        }
+    }
+
+    public void unregister(DataSourceBean bean) {
+        dynamicDataSource.removeDataSource(bean);
+    }
+}

+ 306 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/jdbc/SchemaUtils.java

@@ -0,0 +1,306 @@
+package com.usoftchina.uas.office.jdbc;
+
+import com.usoftchina.uas.office.util.StringUtils;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author yingp
+ * @date 2020/1/15
+ */
+public class SchemaUtils {
+
+    public static TableBuilder newTable(String tableName) {
+        return new TableBuilder(tableName);
+    }
+
+    private static boolean isTableExists(String tableName, JdbcTemplate jdbcTemplate) {
+        int count = jdbcTemplate.queryForObject("select count(1) from user_tables where table_name=?",
+                Integer.class, tableName.toUpperCase());
+        return count > 0;
+    }
+
+    public static ColumnBuilder newColumn(String columnName) {
+        return new ColumnBuilder(columnName);
+    }
+
+    private static boolean isColumnExists(String tableName, String columnName, JdbcTemplate jdbcTemplate) {
+        int count = jdbcTemplate.queryForObject("select count(1) from user_tab_cols where table_name=? and column_name=?",
+                Integer.class, tableName.toUpperCase(), columnName.toUpperCase());
+        return count > 0;
+    }
+
+    public static IndexBuilder newIndex(String indexName, String... columns) {
+        return new IndexBuilder(indexName, columns);
+    }
+
+    private static boolean isIndexExists(String tableName, String indexName, JdbcTemplate jdbcTemplate) {
+        int count = jdbcTemplate.queryForObject("select count(1) from user_indexes where table_name=? and index_name=?",
+                Integer.class, tableName.toUpperCase(), indexName.toUpperCase());
+        return count > 0;
+    }
+
+    public static void create(Table table, JdbcTemplate jdbcTemplate) {
+        TableBuilder builder = newTable(table.name);
+        table.getColumns().forEach(column -> {
+            builder.add(newColumn(column.getName())
+                    .type(column.getType())
+                    .notNull(column.isNotNull())
+                    .primaryKey(column.isPrimaryKey()));
+        });
+        if (null != table.getIndices()) {
+            table.getIndices().forEach(index -> {
+                builder.add(newIndex(index.getName(), index.getColumns())
+                        .unique(index.isUnique()));
+            });
+        }
+        builder.create(jdbcTemplate);
+    }
+
+    public static class Table {
+        private String name;
+        private List<Column> columns;
+        private List<Index> indices;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public List<Column> getColumns() {
+            return columns;
+        }
+
+        public void setColumns(List<Column> columns) {
+            this.columns = columns;
+        }
+
+        public List<Index> getIndices() {
+            return indices;
+        }
+
+        public void setIndices(List<Index> indices) {
+            this.indices = indices;
+        }
+
+        public void create(JdbcTemplate jdbcTemplate) {
+            SchemaUtils.create(this, jdbcTemplate);
+        }
+    }
+
+    private static class TableBuilder {
+        private final String name;
+        private List<ColumnBuilder> columnBuilderList;
+        private List<IndexBuilder> indexBuilderList;
+
+        public TableBuilder(String name) {
+            this.name = name;
+            this.columnBuilderList = new ArrayList<>(1);
+            this.indexBuilderList = new ArrayList<>(0);
+        }
+
+        public TableBuilder add(ColumnBuilder builder) {
+            columnBuilderList.add(builder);
+            return this;
+        }
+
+        public TableBuilder add(IndexBuilder builder) {
+            indexBuilderList.add(builder);
+            return this;
+        }
+
+        public void create(JdbcTemplate jdbcTemplate) {
+            if (isTableExists(name, jdbcTemplate)) {
+                columnBuilderList.forEach(builder -> {
+                    if (isColumnExists(name, builder.name, jdbcTemplate)) {
+                        builder.modify(this, jdbcTemplate);
+                    } else {
+                        builder.add(this, jdbcTemplate);
+                    }
+                });
+            } else {
+                StringBuffer statement = new StringBuffer("create table ");
+                statement.append(name).append(" (");
+                statement.append(columnBuilderList.stream().map(ColumnBuilder::toString).collect(Collectors.joining(",")));
+                statement.append(")");
+                jdbcTemplate.execute(statement.toString());
+            }
+
+            indexBuilderList.forEach(builder -> {
+                if (!isIndexExists(name, builder.name, jdbcTemplate)) {
+                    builder.create(this, jdbcTemplate);
+                }
+            });
+        }
+    }
+
+    private static class Column {
+        private String name;
+        private String type;
+        private boolean notNull;
+        private boolean primaryKey;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        public boolean isNotNull() {
+            return notNull;
+        }
+
+        public void setNotNull(boolean notNull) {
+            this.notNull = notNull;
+        }
+
+        public boolean isPrimaryKey() {
+            return primaryKey;
+        }
+
+        public void setPrimaryKey(boolean primaryKey) {
+            this.primaryKey = primaryKey;
+        }
+    }
+
+    private static class ColumnBuilder {
+        private final String name;
+        private String type;
+        private boolean notNull;
+        private boolean primaryKey;
+
+        public ColumnBuilder(String name) {
+            this.name = name;
+        }
+
+        public ColumnBuilder type(String type) {
+            this.type = type;
+            return this;
+        }
+
+        public ColumnBuilder notNull() {
+            this.notNull = true;
+            return this;
+        }
+
+        public ColumnBuilder notNull(boolean notNull) {
+            this.notNull = notNull;
+            return this;
+        }
+
+        public ColumnBuilder primaryKey() {
+            this.primaryKey = true;
+            return this;
+        }
+
+        public ColumnBuilder primaryKey(boolean primaryKey) {
+            this.primaryKey = primaryKey;
+            return this;
+        }
+
+        private void add(TableBuilder builder, JdbcTemplate jdbcTemplate) {
+            create(builder, jdbcTemplate, false);
+        }
+
+        private void modify(TableBuilder builder, JdbcTemplate jdbcTemplate) {
+            create(builder, jdbcTemplate, true);
+        }
+
+        private void create(TableBuilder builder, JdbcTemplate jdbcTemplate, boolean modify) {
+            StringBuffer statement = new StringBuffer("alter table ");
+            statement.append(builder.name).append(modify ? " modify " : " add ");
+            statement.append(toString());
+            jdbcTemplate.execute(statement.toString());
+        }
+
+        @Override
+        public String toString() {
+            StringBuffer str = new StringBuffer(name).append(" ");
+            str.append(StringUtils.nvl(type, "varchar2(255)"));
+            if (notNull) {
+                str.append(" not null");
+            }
+            if (primaryKey) {
+                str.append(" primary key");
+            }
+            return str.toString();
+        }
+    }
+
+    private static class Index {
+        private String name;
+        private String[] columns;
+        private boolean unique;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String[] getColumns() {
+            return columns;
+        }
+
+        public void setColumns(String[] columns) {
+            this.columns = columns;
+        }
+
+        public boolean isUnique() {
+            return unique;
+        }
+
+        public void setUnique(boolean unique) {
+            this.unique = unique;
+        }
+    }
+
+    private static class IndexBuilder {
+        private final String name;
+        private final String[] columns;
+        private boolean unique;
+
+        public IndexBuilder(String name, String... columns) {
+            this.name = name;
+            this.columns = columns;
+        }
+
+        public IndexBuilder unique() {
+            this.unique = true;
+            return this;
+        }
+
+        public IndexBuilder unique(boolean unique) {
+            this.unique = unique;
+            return this;
+        }
+
+        private void create(TableBuilder builder, JdbcTemplate jdbcTemplate) {
+            StringBuffer statement = new StringBuffer("create ");
+            statement.append(unique ? "unique index " : "index ");
+            statement.append(name).append(" on ");
+            statement.append(builder.name).append(" (");
+            statement.append(StringUtils.arrayToCommaDelimitedString(columns));
+            statement.append(")");
+            jdbcTemplate.execute(statement.toString());
+        }
+    }
+}

+ 27 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListener.java

@@ -0,0 +1,27 @@
+package com.usoftchina.uas.office.listener;
+
+import java.lang.annotation.*;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface UasEventListener {
+    /**
+     * uas单据标志
+     *
+     * @return
+     */
+    String caller();
+
+    /**
+     * uas操作类型
+     *
+     * @return
+     */
+    String[] operation();
+}

+ 28 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerAdapter.java

@@ -0,0 +1,28 @@
+package com.usoftchina.uas.office.listener;
+
+import com.alibaba.fastjson.JSON;
+import com.usoftchina.uas.office.dto.UasEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+@Component
+public class UasEventListenerAdapter implements MessageListener {
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    private UasEventListenerFactory eventListenerFactory;
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        String value = (String) redisTemplate.getValueSerializer().deserialize(message.getBody());
+        UasEvent event = JSON.parseObject(value, UasEvent.class);
+        eventListenerFactory.invokeListeners(event);
+    }
+}

+ 103 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerFactory.java

@@ -0,0 +1,103 @@
+package com.usoftchina.uas.office.listener;
+
+import com.usoftchina.uas.office.context.MasterHolder;
+import com.usoftchina.uas.office.dto.UasEvent;
+import com.usoftchina.uas.office.entity.DataCenter;
+import com.usoftchina.uas.office.entity.Master;
+import com.usoftchina.uas.office.jdbc.DataSourceHolder;
+import com.usoftchina.uas.office.service.DataCenterService;
+import com.usoftchina.uas.office.service.MasterService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+@Component
+public class UasEventListenerFactory {
+    private final Logger logger = LoggerFactory.getLogger(UasEventListenerFactory.class);
+    /**
+     * <caller, <operation, bean+method>>
+     */
+    private Map<String, Map<String, List<BeanMethod>>> beanMethodMap = new HashMap<>();
+
+    @Autowired
+    private DataCenterService dataCenterService;
+    @Autowired
+    private MasterService masterService;
+
+    public void addListener(UasEventListener listener, Object bean, Method method) {
+        Map<String, List<BeanMethod>> map = beanMethodMap.get(listener.caller());
+        if (null == map) {
+            map = new HashMap<>(1);
+            beanMethodMap.put(listener.caller(), map);
+        }
+        if (null != listener.operation() && listener.operation().length > 0) {
+            for (String opera : listener.operation()) {
+                List<BeanMethod> beanMethodList = map.get(opera);
+                if (null == beanMethodList) {
+                    beanMethodList = new ArrayList<>(1);
+                    map.put(opera, beanMethodList);
+                }
+                beanMethodList.add(new BeanMethod(bean, method));
+            }
+        }
+    }
+
+    /**
+     * 查找配置的事件监听,并执行
+     *
+     * @param event
+     * @throws Exception
+     */
+    public void invokeListeners(UasEvent event) {
+        try {
+            DataCenter dataCenter = dataCenterService.find();
+            if (null == dataCenter) {
+                return;
+            }
+            DataSourceHolder.set(dataCenter);
+            Master master = masterService.findByName(event.getMaster());
+            if (null == master) {
+                return;
+            }
+            MasterHolder.set(master);
+            DataSourceHolder.set(master);
+            Map<String, List<BeanMethod>> map = beanMethodMap.get(event.getCaller());
+            if (null != map) {
+                List<BeanMethod> beanMethodList = map.get(event.getOperation());
+                if (null != beanMethodList) {
+                    for (BeanMethod beanMethod : beanMethodList) {
+                        try {
+                            beanMethod.method.invoke(beanMethod.bean, event);
+                        } catch (Exception e) {
+                            logger.error("invoke error on " + event, e);
+                        }
+                    }
+                }
+            }
+        } finally {
+            MasterHolder.clear();
+            DataSourceHolder.clear();
+        }
+    }
+
+    class BeanMethod {
+        private final Object bean;
+        private final Method method;
+
+        public BeanMethod(Object bean, Method method) {
+            this.bean = bean;
+            this.method = method;
+        }
+    }
+}

+ 38 - 0
uas-office-core/src/main/java/com/usoftchina/uas/office/listener/UasEventListenerProcessor.java

@@ -0,0 +1,38 @@
+package com.usoftchina.uas.office.listener;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ReflectionUtils;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author yingp
+ * @date 2020/2/16
+ */
+@Component
+public class UasEventListenerProcessor implements BeanPostProcessor {
+    @Autowired
+    private UasEventListenerFactory eventListenerFactory;
+
+    @Override
+    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+        return bean;
+    }
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
+        if (methods != null) {
+            for (Method method : methods) {
+                if (method.isAnnotationPresent(UasEventListener.class)) {
+                    UasEventListener listener = method.getAnnotation(UasEventListener.class);
+                    eventListenerFactory.addListener(listener, bean, method);
+                }
+            }
+        }
+        return bean;
+    }
+}

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