[
  {
    "path": ".gitignore",
    "content": ".gradle\n/build/\n!gradle/wrapper/gradle-wrapper.jar\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n/out/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"{}\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n   Copyright 2018 xuexiangjys\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "# XUpdateService\n\n使用Spring Boot简易搭建，Gradle构建，为XUpdate提供的更新服务。\n\n## 内容\n\n* 使用spring boot快速搭建，并使用Gradle进行构建【区别Maven】。\n\n* 使用阿里的druid数据库连接池和tk.mybatis进行数据库的连接。\n\n* 使用MySql作为数据库。\n\n* 提供了应用版本后台管理所需的API服务，使用Html和ajax简单实现了应用版本管理界面。\n\n* 使用了AOP对api请求进行动态日志记录。\n\n* 实现通用的文件上传（包括多文件上传）和下载功能。\n\n* 增加了请求流量以及请求权限的控制。\n\n* 支持浏览器跨域请求。\n\n## 搭建方法\n\n1. clone项目到本地\n\n```\ngit clone https://github.com/xuexiangjys/XUpdateService.git\n```\n\n2.使用IntelliJ IDEA 导入该项目。\n\n3.进行本地数据库的配置。\n\n因为使用的是MySql数据库，如果你电脑上没有安装MySql的话，请先[点击安装](https://www.mysql.com/)。你可以安装`MySQL Community Server`和`MySQL Workbench`,建议下载的MySql版本是5.7。\n\n* MySql安装完成后，请执行根目录下`sql`文件夹下的脚本，创建数据库表和内容。\n\n* 配置`src/main/resources/application.yml`文件，包括服务端口、数据库配置、mybatis配置、文件上传配置等。\n\n* 如果你需要使用mybatis的自动生成代码脚本`generator`，请配置`src/main/resources/db-mysql.properties`文件，然后执行`./gradlew mybatisGenerate`或者在Gradle的Task列表中选择`mybatisGenerate`双击即可。\n\n4.直接运行`XUpdateServiceApplication`即可运行服务。\n\n## 程序打包\n\n1.执行`./gradlew bootJar`或点击Gradle任务栏点击`Tasks` -> `Build` -> `bootJar`.\n\n2.打包后的是jar文件，打包路径：`build/libs/` 下，如下图：\n\n![](https://github.com/xuexiangjys/XUpdateService/blob/master/img/2.png)\n\n3.最后执行jar包即可.\n\n```\njava -jar build/libs/xxxxx.jar\n```\n\n4.目前最新的已打包好的jar在项目的`Package`下[xupdateservice-1.0.0.jar](./package/xupdateservice-1.0.0.jar), 运行前请保证你的数据库连接正常。\n\n## 版本更新管理后台\n\n由于使用Java编写web管理后台不是很好看，因此我特地去学习了最近比较火的Vue.js编写了一个简洁优美的管理后台供大家参考。\n\n项目地址: https://github.com/xuexiangjys/xupdate-management\n\n### 项目预览\n\n![](https://github.com/xuexiangjys/xupdate-management/blob/master/art/2.png)\n\n----------------\n\n## API构成\n\n### 管理接口\n\n#### 1、注册APK的版本信息\n\n* 请求类型: post\n* url : /update/addVersionInfo\n* 参数 :\n\n```\n{\n    \"updateStatus\":2,\n    \"modifyContent\":\"1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。\",\n    \"appKey\":\"test\",\n    \"versionName\":\"1.0.3\",\n    \"versionCode\":4\n}\n```\n\n* 响应 :\n\n```\n{\n    \"Msg\":\"\",\n    \"Code\":0,\n    \"Data\":{\n        \"versionId\":12,\n        \"updateStatus\":2,\n        \"modifyContent\":\"1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。\",\n        \"appKey\":\"test\",\n        \"versionName\":\"1.0.3\",\n        \"versionCode\":4\n    }\n}\n```\n\n#### 2、上传APK\n\n* 请求类型: post【multipart/form-data】\n* url : /update/uploadApk\n* 参数 :\n\n```\nfile=[文件]\nversionId=12\n```\n* 响应 :\n\n```\n{\n    \"Code\":0,\n    \"Msg\":\"\",\n    \"Data\":true\n}\n```\n\n#### 3、添加版本信息\n\n* 请求类型: post【multipart/form-data】\n* url : /update/addAppVersion\n* 参数 :\n\n```\nfile=[文件]\n\nappVersionInfo= {\n    \"updateStatus\":2,\n    \"modifyContent\":\"1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。\",\n    \"appKey\":\"test\",\n    \"versionName\":\"1.0.3\",\n    \"versionCode\":4\n}\n\n```\n\n* 响应 :\n\n```\n{\n    \"Code\":0,\n    \"Msg\":\"\",\n    \"Data\":true\n}\n```\n\n### 版本更新接口\n\n#### 1、版本信息检查\n\n* 请求类型: post\n* url : /update/checkVersion\n* 参数 :\n\n```\nversionCode=1,\nappKey=com.xuexiang.xupdatedemo \n```\n\n* 响应 :\n\n```\n{\n    \"Msg\":\"\",\n    \"Code\":0,\n    \"Data\":{\n        \"apkMd5\":\"E4B79A36EFB9F17DF7E3BB161F9BCFD8\",\n        \"versionId\":11,\n        \"updateStatus\":1,\n        \"downloadUrl\":\"xupdate_demo_1.0.2.apk\",\n        \"modifyContent\":\"1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。\",\n        \"appKey\":\"com.xuexiang.xupdatedemo\",\n        \"apkSize\":1649,\n        \"uploadTime\":\"2018-07-30 09:47:25\",\n        \"versionName\":\"1.23.4\",\n        \"versionCode\":34\n    }\n}\n```\n\n#### 2、最新版本下载\n\n* 请求类型: get\n* url : /update/apk/{fileName:.+}\n* 响应 : 文件流\n"
  },
  {
    "path": "build.gradle",
    "content": "plugins {\n    id 'org.springframework.boot' version '2.4.1'\n    id 'io.spring.dependency-management' version '1.0.10.RELEASE'\n    id 'java'\n}\n\ngroup = 'com.xuexiang'\nversion = '1.0.0'\nsourceCompatibility = 1.8\n\nrepositories {\n    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }\n}\n\nconfigurations {\n    mybatisGenerator\n}\n\ndependencies {\n    //web服务\n    implementation('org.springframework.boot:spring-boot-starter-web')\n    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'\n    testImplementation('org.springframework.boot:spring-boot-starter-test')\n\n    //物理分页\n    implementation 'com.github.pagehelper:pagehelper-spring-boot-starter:1.3.0'\n\n    //数据库连接\n    implementation('org.springframework.boot:spring-boot-starter-jdbc')\n    implementation 'mysql:mysql-connector-java:8.0.22'\n    implementation 'com.alibaba:fastjson:1.2.75'\n    implementation 'com.alibaba:druid:1.2.4'\n    implementation 'com.alibaba:druid-spring-boot-starter:1.2.4'\n\n    // token令牌\n    implementation 'io.jsonwebtoken:jjwt:0.9.1'\n\n    //AOP\n    implementation 'org.aspectj:aspectjweaver:1.9.6'\n    implementation('org.assertj:assertj-core')\n    implementation 'org.apache.commons:commons-lang3:3.4'\n    implementation 'org.apache.commons:commons-collections4:4.1'\n\n    //mybatis自动生成代码\n    mybatisGenerator 'org.mybatis.generator:mybatis-generator-core:1.3.7'\n    mybatisGenerator 'tk.mybatis:mapper:4.1.5'\n    implementation 'tk.mybatis:mapper-spring-boot-starter:2.1.5'\n\n    //网页加载\n    implementation('org.springframework.boot:spring-boot-starter-thymeleaf')\n}\n\njar {\n    enabled = true\n}\n\napply from: './generator.gradle'\n"
  },
  {
    "path": "db/generatorConfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE generatorConfiguration\n        PUBLIC \"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN\"\n        \"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd\">\n<generatorConfiguration>\n    <!-- 数据库驱动:选择你的本地硬盘上面的数据库驱动包-->\n    <classPathEntry location=\"${classPath}\"/>\n\n    <context id=\"Mysql\" targetRuntime=\"MyBatis3Simple\" defaultModelType=\"flat\">\n        <plugin type=\"tk.mybatis.mapper.generator.MapperPlugin\">\n            <property name=\"mappers\" value=\"tk.mybatis.mapper.common.Mapper\"/>\n            <!-- caseSensitive默认false，当数据库表名区分大小写时，可以将该属性设置为true -->\n            <property name=\"caseSensitive\" value=\"true\"/>\n        </plugin>\n        <commentGenerator>\n            <property name=\"suppressAllComments\" value=\"true\"/>\n        </commentGenerator>\n        <jdbcConnection driverClass=\"${driverClass}\"\n                        connectionURL=\"${connectionURL}\"\n                        userId=\"${userId}\"\n                        password=\"${password}\">\n        </jdbcConnection>\n        <javaModelGenerator targetPackage=\"${modelPackage}\" targetProject=\"${src_main_java}\"/>\n        <sqlMapGenerator targetPackage=\"${sqlMapperPackage}\" targetProject=\"${src_main_resources}\"/>\n        <javaClientGenerator targetPackage=\"${mapperPackage}\" targetProject=\"${src_main_java}\" type=\"XMLMAPPER\"/>\n        <!-- sql占位符，表示所有的表 -->\n        <table tableName=\"%\"/>\n\n    </context>\n</generatorConfiguration>"
  },
  {
    "path": "generator.gradle",
    "content": "def getDbProperties = {\n    def properties = new Properties()\n    file(\"src/main/resources/db-mysql.properties\").withInputStream { inputStream ->\n        properties.load(inputStream)\n    }\n    properties;\n}\ntask mybatisGenerate {\n    def properties = getDbProperties()\n    ant.properties['targetProject'] = projectDir.path\n    ant.properties['classPath'] = properties.getProperty(\"classPath\")\n    ant.properties['driverClass'] = properties.getProperty(\"jdbc.driverClassName\")\n    ant.properties['connectionURL'] = properties.getProperty(\"jdbc.url\")\n    ant.properties['userId'] = properties.getProperty(\"jdbc.user\")\n    ant.properties['password'] = properties.getProperty(\"jdbc.pass\")\n    ant.properties['src_main_java'] = sourceSets.main.java.srcDirs[0].path\n    ant.properties['src_main_resources'] = sourceSets.main.resources.srcDirs[0].path\n    ant.properties['modelPackage'] = this.modelPackage\n    ant.properties['mapperPackage'] = this.mapperPackage\n    ant.properties['sqlMapperPackage'] = this.sqlMapperPackage\n    ant.taskdef(\n            name: 'mbgenerator',\n            classname: 'org.mybatis.generator.ant.GeneratorAntTask',\n            classpath: configurations.mybatisGenerator.asPath\n    )\n    ant.mbgenerator(overwrite: true,\n            configfile: 'db/generatorConfig.xml', verbose: true) {\n        propertyset {\n            propertyref(name: 'targetProject')\n            propertyref(name: 'classPath')\n            propertyref(name: 'driverClass')\n            propertyref(name: 'connectionURL')\n            propertyref(name: 'userId')\n            propertyref(name: 'password')\n            propertyref(name: 'src_main_java')\n            propertyref(name: 'src_main_resources')\n            propertyref(name: 'modelPackage')\n            propertyref(name: 'mapperPackage')\n            propertyref(name: 'sqlMapperPackage')\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "\n\nmodelPackage=com.xuexiang.xupdateservice.model\n#生成的mapper接口类所在包\nmapperPackage=com.xuexiang.xupdateservice.mapper\n#生成的mapper xml文件所在包，默认存储在resources目录下\nsqlMapperPackage=mybatis_mapper"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave ( ) {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'xupdateservice'\n"
  },
  {
    "path": "sql/xupdate.sql",
    "content": "-- MySQL dump 10.13  Distrib 5.7.17, for Win64 (x86_64)\n--\n-- Host: localhost    Database: xupdate\n-- ------------------------------------------------------\n-- Server version\t5.7.25-log\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `account`\n--\n\nDROP TABLE IF EXISTS `account`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `account` (\n  `account_id` int(11) NOT NULL AUTO_INCREMENT,\n  `login_name` varchar(45) NOT NULL DEFAULT 'admin',\n  `password` varchar(45) NOT NULL DEFAULT '123456',\n  `nick` varchar(45) NOT NULL DEFAULT 'admin',\n  `authority` varchar(45) NOT NULL DEFAULT 'admin',\n  `avatar` varchar(200) DEFAULT NULL,\n  `phone` char(11) DEFAULT NULL,\n  `address` varchar(60) DEFAULT NULL,\n  `register_time` datetime DEFAULT NULL,\n  PRIMARY KEY (`account_id`),\n  UNIQUE KEY `account_id_UNIQUE` (`account_id`),\n  UNIQUE KEY `login_name_UNIQUE` (`login_name`)\n) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `account`\n--\n\nLOCK TABLES `account` WRITE;\n/*!40000 ALTER TABLE `account` DISABLE KEYS */;\nINSERT INTO `account` VALUES (1,'admin','123456','admin','admin','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','13513957542','南京市江宁区','2018-05-06 00:00:00'),(2,'xuexiang','123456','薛翔','admin','https://raw.githubusercontent.com/xuexiangjys/Resource/master/img/avatar/avatar_github.jpg','13913845875','南京市江宁区','2018-12-11 00:00:00');\n/*!40000 ALTER TABLE `account` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `app_version_info`\n--\n\nDROP TABLE IF EXISTS `app_version_info`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `app_version_info` (\n  `version_id` int(11) NOT NULL AUTO_INCREMENT,\n  `update_status` int(11) NOT NULL,\n  `version_code` int(11) NOT NULL,\n  `version_name` varchar(45) NOT NULL,\n  `upload_time` varchar(45) DEFAULT NULL,\n  `modify_content` longtext,\n  `download_url` longtext,\n  `apk_size` int(11) DEFAULT NULL,\n  `apk_md5` longtext,\n  `app_key` varchar(45) NOT NULL,\n  PRIMARY KEY (`version_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `app_version_info`\n--\n\nLOCK TABLES `app_version_info` WRITE;\n/*!40000 ALTER TABLE `app_version_info` DISABLE KEYS */;\nINSERT INTO `app_version_info` VALUES (10,2,24,'1.0.4','2018-07-30 09:36:39','1、优化api接口。\\\\r\\\\n2、添加使用demo演示。\\\\r\\\\n3、新增自定义更新服务API接口。\\\\r\\\\n4、优化更新提示界面。','xupdate_demo_1.0.2.apk',1697,'E4B79A36EFB9F17DF7E3BB161F9BCFD8','test3'),(11,1,34,'1.23.4','2018-07-30 09:47:25','1、优化api接口。\\\\r\\\\n2、添加使用demo演示。\\\\r\\\\n3、新增自定义更新服务API接口。\\\\r\\\\n4、优化更新提示界面。','xupdate_demo_1.0.2.apk',1649,'E4B79A36EFB9F17DF7E3BB161F9BCFD8','com.xuexiang.xupdatedemo'),(12,1,4,'1.0.3','2018-07-30 10:52:53','1、优化api接口。\\\\r\\\\n2、添加使用demo演示。\\\\r\\\\n3、新增自定义更新服务API接口。\\\\r\\\\n4、优化更新提示界面。','xupdate_demo_1.0.2.apk',1649,'E4B79A36EFB9F17DF7E3BB161F9BCFD8','com.xuexiang.xupdatedemo'),(13,1,23,'1.2.34',NULL,'........',NULL,NULL,NULL,'test1');\n/*!40000 ALTER TABLE `app_version_info` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2019-04-23  9:19:39\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/XUpdateServiceApplication.java",
    "content": "package com.xuexiang.xupdateservice;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport tk.mybatis.spring.annotation.MapperScan;\n\n@SpringBootApplication\n@MapperScan(\"com.xuexiang.xupdateservice.mapper\")\npublic class XUpdateServiceApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(XUpdateServiceApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/request/ApiRequest.java",
    "content": "package com.xuexiang.xupdateservice.api.request;\n\n/**\n * 基础请求包装类\n *\n * @author xuexiang\n * @since 2018/7/16 下午7:00\n */\npublic class ApiRequest<T> {\n\n    public T request;\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/request/PageQuery.java",
    "content": "package com.xuexiang.xupdateservice.api.request;\n\n/**\n * @author xuexiang\n * @since 2018/8/15 上午12:26\n */\npublic class PageQuery {\n\n    /**\n     *  第几页数\n     */\n    public int pageNum;\n    /**\n     * 每页的数量\n     */\n    public int pageSize;\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/response/ApiResult.java",
    "content": "/*\n * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *       http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.xuexiang.xupdateservice.api.response;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.xuexiang.xupdateservice.exception.ApiException;\n\n/**\n * 提供的默认的标注返回api\n *\n * @author xuexiang\n * @since 2018/5/22 下午4:22\n */\npublic class ApiResult<T> {\n\n    @JsonProperty(value = \"code\")\n    private int Code = 0;\n    @JsonProperty(value = \"msg\")\n    private String Msg = \"\";\n    @JsonProperty(value = \"data\")\n    private T Data;\n\n    @JsonIgnore\n    public int getCode() {\n        return Code;\n    }\n\n    @JsonIgnore\n    public ApiResult<T> setCode(int code) {\n        Code = code;\n        return this;\n    }\n\n    @JsonIgnore\n    public String getMsg() {\n        return Msg;\n    }\n\n    @JsonIgnore\n    public ApiResult<T> setMsg(String msg) {\n        Msg = msg;\n        return this;\n    }\n\n    @JsonIgnore\n    public T getData() {\n        return Data;\n    }\n\n    @JsonIgnore\n    public ApiResult<T> setData(T data) {\n        Data = data;\n        return this;\n    }\n\n    @JsonIgnore\n    public ApiResult setError(int code, String msg) {\n        Code = code;\n        Msg = msg;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"ApiResult{\" +\n                \"Code='\" + Code + '\\'' +\n                \", Msg='\" + Msg + '\\'' +\n                \", Data=\" + Data +\n                '}';\n    }\n\n\n\n    /**\n     * 获取出错返回\n     *\n     * @param ex\n     * @return\n     */\n    public static ApiResult error(ApiException ex) {\n        ApiResult apiResult = new ApiResult();\n        apiResult.setError(ex.getCode(), ex.getMessage());\n        return apiResult;\n    }\n\n    /**\n     * 获取出错返回\n     *\n     * @param code\n     * @param msg\n     * @return\n     */\n    public static ApiResult error(int code, String msg) {\n        ApiResult apiResult = new ApiResult();\n        apiResult.setError(code, msg);\n        return apiResult;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/response/LoginInfo.java",
    "content": "package com.xuexiang.xupdateservice.api.response;\n\nimport com.xuexiang.xupdateservice.model.Account;\n\n/**\n * @author xuexiang\n * @since 2018/8/6 下午6:14\n */\npublic class LoginInfo {\n\n    private Account account;\n\n    private String token;\n\n    public Account getUser() {\n        return account;\n    }\n\n    public LoginInfo setUser(Account account) {\n        this.account = account;\n        return this;\n    }\n\n    public String getToken() {\n        return token;\n    }\n\n    public LoginInfo setToken(String token) {\n        this.token = token;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/response/PageData.java",
    "content": "package com.xuexiang.xupdateservice.api.response;\n\nimport java.util.List;\n\n/**\n * 分页请求的响应数据\n *\n * @author xuexiang\n * @since 2019/4/25 下午11:52\n */\npublic class PageData<T> {\n\n    private List<T> array;\n\n    private long total;\n\n    private int pageNum;\n\n    private int pageSize;\n\n    public PageData() {\n    }\n\n    public PageData(int pageNum, int pageSize) {\n        this.pageNum = pageNum;\n        this.pageSize = pageSize;\n    }\n\n    public List<T> getArray() {\n        return array;\n    }\n\n    public PageData<T> setArray(List<T> array) {\n        this.array = array;\n        return this;\n    }\n\n    public long getTotal() {\n        return total;\n    }\n\n    public PageData<T> setTotal(long total) {\n        this.total = total;\n        return this;\n    }\n\n    public int getPageNum() {\n        return pageNum;\n    }\n\n    public PageData<T> setPageNum(int pageNum) {\n        this.pageNum = pageNum;\n        return this;\n    }\n\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    public PageData<T> setPageSize(int pageSize) {\n        this.pageSize = pageSize;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/api/response/UploadFileResponse.java",
    "content": "package com.xuexiang.xupdateservice.api.response;\n\n\n/**\n * 文件上传返回结果\n *\n * @author xuexiang\n * @since 2018/7/18 下午3:55\n */\npublic class UploadFileResponse {\n    private String fileName;\n    private String fileDownloadUri;\n    private String fileType;\n    private long size;\n\n    public UploadFileResponse(String fileName, String fileDownloadUri, String fileType, long size) {\n        this.fileName = fileName;\n        this.fileDownloadUri = fileDownloadUri;\n        this.fileType = fileType;\n        this.size = size;\n    }\n\n    public String getFileName() {\n        return fileName;\n    }\n\n    public void setFileName(String fileName) {\n        this.fileName = fileName;\n    }\n\n    public String getFileDownloadUri() {\n        return fileDownloadUri;\n    }\n\n    public void setFileDownloadUri(String fileDownloadUri) {\n        this.fileDownloadUri = fileDownloadUri;\n    }\n\n    public String getFileType() {\n        return fileType;\n    }\n\n    public void setFileType(String fileType) {\n        this.fileType = fileType;\n    }\n\n    public long getSize() {\n        return size;\n    }\n\n    public void setSize(long size) {\n        this.size = size;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/annotation/CurrentAccount.java",
    "content": "package com.xuexiang.xupdateservice.component.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 在Controller的方法参数中使用此注解，该方法在映射时会注入当前登录的Account对象\n *\n * @author xuexiang\n * @since 2018/8/6 下午4:36\n */\n@Target(ElementType.PARAMETER)          // 可用在方法的参数上\n@Retention(RetentionPolicy.RUNTIME)     // 运行时有效\npublic @interface CurrentAccount {\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/annotation/LimitedRequest.java",
    "content": "package com.xuexiang.xupdateservice.component.annotation;\n\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\n\nimport java.lang.annotation.*;\n\n/**\n * 受控的请求【控制一段时间内请求的次数】\n *\n * @author xuexiang\n * @since 2018/8/7 下午1:59\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\n@Documented\n//最高优先级\n@Order(Ordered.HIGHEST_PRECEDENCE)\npublic @interface LimitedRequest {\n\n    int DEFAULT_COUNT = 3;\n\n    int DEFAULT_INTERVAL = 30 * 1000;\n\n    /**\n     * 允许访问的次数，默认值3\n     */\n    int count() default DEFAULT_COUNT;\n\n    /**\n     *\n     * 时间段，单位为毫秒，默认值30s\n     */\n    long interval() default DEFAULT_INTERVAL;\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/annotation/LoginRequired.java",
    "content": "package com.xuexiang.xupdateservice.component.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 在需要登录验证的Controller的方法上使用此注解\n *\n * @author xuexiang\n * @since 2018/8/6 下午4:37\n */\n@Target({ElementType.METHOD})// 可用在方法名上\n@Retention(RetentionPolicy.RUNTIME)// 运行时有效\npublic @interface LoginRequired {\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/annotation/QuickRequest.java",
    "content": "package com.xuexiang.xupdateservice.component.annotation;\n\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * @author xuexiang\n * @since 2018/8/7 下午3:39\n */\n@Target({ElementType.METHOD})// 可用在方法名上\n@Retention(RetentionPolicy.RUNTIME)// 运行时有效\n//最高优先级\n@Order(Ordered.HIGHEST_PRECEDENCE)\npublic @interface QuickRequest {\n    /**\n     * 默认请求间隔为5s\n     */\n    int DEFAULT_INTERVAL = 5 * 1000;\n    /**\n     *\n     * 请求之间的间隔，默认值5s\n     */\n    long interval() default DEFAULT_INTERVAL;\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/aspect/LimitedRequestAspect.java",
    "content": "package com.xuexiang.xupdateservice.component.aspect;\n\nimport com.xuexiang.xupdateservice.component.annotation.LimitedRequest;\nimport com.xuexiang.xupdateservice.exception.ApiException;\nimport com.xuexiang.xupdateservice.utils.AspectJUtils;\nimport com.xuexiang.xupdateservice.utils.IpUtils;\nimport com.xuexiang.xupdateservice.utils.QuickRequestUtils;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.aspectj.lang.annotation.Before;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\n\nimport static com.xuexiang.xupdateservice.exception.ApiException.ERROR.REQUEST_BEYOND_LIMIT;\n\n/**\n * 请求次数控制拦截器（对{@link LimitedRequest}进行拦截）\n *\n * @author xuexiang\n * @since 2018/8/7 下午2:06\n */\n@Aspect\n@Component\npublic class LimitedRequestAspect {\n\n    @Before(\"@within(org.springframework.web.bind.annotation.RestController) && @annotation(limit)\")\n    public void requestLimit(final JoinPoint joinPoint, LimitedRequest limit) throws Exception {\n        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();\n        String ip = IpUtils.getRealIp(request);\n        String url = request.getRequestURL().toString();\n        String methodName = AspectJUtils.getMethodName(joinPoint);\n        String key = \"requestLimit_\".concat(url).concat(ip).concat(methodName);\n\n        if (QuickRequestUtils.isQuickRequest(key, limit)) {\n            throw new ApiException(\"请求过于频繁！\", REQUEST_BEYOND_LIMIT);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/aspect/RestControllerAspect.java",
    "content": "package com.xuexiang.xupdateservice.component.aspect;\n\nimport com.xuexiang.xupdateservice.utils.AspectJUtils;\nimport com.xuexiang.xupdateservice.utils.IpUtils;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.aspectj.lang.reflect.MethodSignature;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.lang.reflect.Method;\n\n/**\n * 请求api日志记录（对{@link RestController}进行拦截）\n *\n * @author xuexiang\n * @since 2018/7/17 上午10:42\n */\n@Aspect\n@Component\npublic class RestControllerAspect {\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n\n    /**\n     * 环绕通知\n     * @param joinPoint 连接点\n     * @return 切入点返回值\n     * @throws Throwable 异常信息\n     */\n    @Around(\"@within(org.springframework.web.bind.annotation.RestController) || @annotation(org.springframework.web.bind.annotation.RestController)\")\n    public Object apiLog(ProceedingJoinPoint joinPoint) throws Throwable {\n        MethodSignature signature = (MethodSignature) joinPoint.getSignature();\n        Method method = signature.getMethod();\n\n        boolean logFlag = this.needToLog(method);\n        if (!logFlag) {\n            return joinPoint.proceed();\n        }\n        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();\n\n        String userAgent = request.getHeader(\"user-agent\");\n        String ip = IpUtils.getRealIp(request);\n        String methodName = AspectJUtils.getMethodName(joinPoint);\n        String params = AspectJUtils.getMethodParams(joinPoint);\n\n        logger.info(\"\\n\\r\" +\n                \"---------->|开始请求方法:{} \\n\\r\" +\n                \"           |请求参数:{} \\n\\r\" +\n                \"           |IP:{} \\n\\r\" +\n                \"           |userAgent:{}\", methodName, params, ip, userAgent);\n        long start = System.currentTimeMillis();\n        Object result = joinPoint.proceed();\n        long end = System.currentTimeMillis();\n        String deleteSensitiveContent =  AspectJUtils.deleteSensitiveContent(result);\n        logger.info(\"\\n\\r\" +\n                \"<----------|结束请求方法:{}\\n\\r\" +\n                \"           |返回结果{} \\n\\r\" +\n                \"           |耗时:{}毫秒 \", methodName, deleteSensitiveContent, end - start);\n        return result;\n    }\n\n    /**\n     * 判断是否需要记录日志\n     */\n    private boolean needToLog(Method method) {\n        return method.getAnnotation(GetMapping.class) == null; //不打印Get请求\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/interceptor/CorsInterceptor.java",
    "content": "package com.xuexiang.xupdateservice.component.interceptor;\n\nimport org.springframework.web.servlet.HandlerInterceptor;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 跨域支持拦截器\n *\n * @author xuexiang\n * @since 2019/4/21 上午1:46\n */\npublic class CorsInterceptor implements HandlerInterceptor {\n    @Override\n    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {\n        System.out.println(\"----------【跨域支持拦截器】-----------\");\n\n        httpServletResponse.setHeader(\"Access-Control-Allow-Origin\",\"*\");\n        httpServletResponse.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n        httpServletResponse.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, PATCH, DELETE, PUT\");\n        httpServletResponse.setHeader(\"Access-Control-Max-Age\", \"3600\");\n        httpServletResponse.setHeader(\"Access-Control-Allow-Headers\", \"Origin, X-Requested-With, Content-Type, Accept\");\n        return true;\n    }\n\n    @Override\n    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {\n\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/interceptor/QuickRequestInterceptor.java",
    "content": "package com.xuexiang.xupdateservice.component.interceptor;\n\nimport com.xuexiang.xupdateservice.component.annotation.LoginRequired;\nimport com.xuexiang.xupdateservice.component.annotation.QuickRequest;\nimport com.xuexiang.xupdateservice.exception.ApiException;\nimport com.xuexiang.xupdateservice.utils.IpUtils;\nimport com.xuexiang.xupdateservice.utils.QuickRequestUtils;\nimport com.xuexiang.xupdateservice.utils.TokenUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.lang.reflect.Method;\n\nimport static com.xuexiang.xupdateservice.exception.ApiException.ERROR.REQUEST_BEYOND_LIMIT;\n\n/**\n * 快速请求拦截器【根据请求传入的时间戳来判断】\n *\n * @author xuexiang\n * @since 2018/8/7 下午3:36\n */\npublic class QuickRequestInterceptor implements HandlerInterceptor {\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse httpServletResponse, Object handler) throws Exception {\n        System.out.println(\"----------【快速请求拦截器】-----------\");\n\n        // 如果不是映射到方法直接通过\n        if (!(handler instanceof HandlerMethod)) {\n            return true;\n        }\n        HandlerMethod handlerMethod = (HandlerMethod) handler;\n        Method method = handlerMethod.getMethod();\n        // 判断接口是否需要登录\n        QuickRequest quickRequest = method.getAnnotation(QuickRequest.class);\n\n        if (quickRequest == null) { //没有 @QuickRequest 注解，无需要校验\n            return true;\n        }\n\n        // 获取客户端请求携带的时间戳\n        String timeStamp = request.getHeader(\"X-TimeStamp\");\n\n        if (StringUtils.isEmpty(timeStamp)) {\n            timeStamp = request.getParameter(\"timeStamp\");\n            if (StringUtils.isEmpty(timeStamp)) { //如果没有携带时间戳，也无需校验\n                return true;\n            }\n        }\n\n        String identity = IpUtils.getRealIp(request); //身份默认使用请求的ip地址\n\n        //如果注释有需要登录验证，就使用token作为身份\n        LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);\n        if (loginRequired != null) { //没有 @LoginRequired 注解，无需认证\n            String accessToken = TokenUtils.parseToken(request);\n            if (!StringUtils.isEmpty(accessToken)) {\n                identity = accessToken;\n            }\n        }\n\n        String url = request.getRequestURL().toString();\n        String methodName = method.getName();\n        String key = \"QuickRequest_\".concat(url).concat(methodName).concat(identity);\n\n        if (QuickRequestUtils.isQuickRequest(key, quickRequest, timeStamp)) {\n            throw new ApiException(\"请求过于频繁,请稍后再试！\", REQUEST_BEYOND_LIMIT);\n        }\n        return true;\n    }\n\n    @Override\n    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {\n\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/token/AuthenticationInterceptor.java",
    "content": "package com.xuexiang.xupdateservice.component.token;\n\nimport com.xuexiang.xupdateservice.component.annotation.LoginRequired;\nimport com.xuexiang.xupdateservice.config.Constants;\nimport com.xuexiang.xupdateservice.exception.ApiException;\nimport com.xuexiang.xupdateservice.model.Account;\nimport com.xuexiang.xupdateservice.service.AccountService;\nimport com.xuexiang.xupdateservice.utils.TokenUtils;\nimport io.jsonwebtoken.Claims;\nimport io.jsonwebtoken.ExpiredJwtException;\nimport io.jsonwebtoken.SignatureException;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.lang.reflect.Method;\n\nimport static com.xuexiang.xupdateservice.exception.ApiException.ERROR.*;\n\n/**\n * 用户认证拦截器\n *\n * @author xuexiang\n * @since 2018/8/6 下午4:40\n */\npublic class AuthenticationInterceptor implements HandlerInterceptor {\n\n    @Autowired\n    private AccountService accountService;\n\n    // 在业务处理器处理请求之前被调用\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        System.out.println(\"----------【用户认证拦截器】-----------\");\n\n        // 如果不是映射到方法直接通过\n        if (!(handler instanceof HandlerMethod)) {\n            return true;\n        }\n\n        HandlerMethod handlerMethod = (HandlerMethod) handler;\n        Method method = handlerMethod.getMethod();\n        // 判断接口是否需要登录\n        LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);\n\n        if (loginRequired == null) { //没有 @LoginRequired 注解，无需认证\n            return true;\n        }\n\n        // 判断是否存在令牌信息，如果存在，则允许登录\n        String accessToken = TokenUtils.parseToken(request);\n\n        if (StringUtils.isEmpty(accessToken)) {\n            throw new ApiException(\"未携带token，请先进行登录\", TOKEN_MISSING);\n        }\n\n        // 从Redis 中查看 token 是否过期\n        Claims claims;\n        try {\n            claims = TokenUtils.parseJWT(accessToken);\n        } catch (ExpiredJwtException e) {\n            throw new ApiException(\"token失效，请重新登录\", TOKEN_INVALID);\n        } catch (SignatureException se) {\n            throw new ApiException(\"token令牌错误\", AUTH_ERROR);\n        }\n\n        String loginName = claims.getId();\n        Account account = accountService.checkAccount(loginName);\n\n        if (account == null) {\n            throw new ApiException(\"用户不存在，请重新登录\", TOKEN_INVALID);\n        }\n        // 当前登录用户@CurrentAccount\n        request.setAttribute(Constants.CURRENT_ACCOUNT, account);\n        return true;\n    }\n\n    // 请求处理之后进行调用，但是在视图被渲染之前（Controller方法调用之后）\n    @Override\n    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {\n\n    }\n\n    // 在整个请求结束之后被调用，也就是在DispatcherServlet 渲染了对应的视图之后执行（主要是用于进行资源清理工作）\n    @Override\n    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/component/token/CurrentAccountMethodArgumentResolver.java",
    "content": "package com.xuexiang.xupdateservice.component.token;\n\nimport com.xuexiang.xupdateservice.component.annotation.CurrentAccount;\nimport com.xuexiang.xupdateservice.config.Constants;\nimport com.xuexiang.xupdateservice.model.Account;\nimport org.springframework.core.MethodParameter;\nimport org.springframework.web.bind.support.WebDataBinderFactory;\nimport org.springframework.web.context.request.NativeWebRequest;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.method.support.HandlerMethodArgumentResolver;\nimport org.springframework.web.method.support.ModelAndViewContainer;\nimport org.springframework.web.multipart.support.MissingServletRequestPartException;\n\n/**\n * 已登录账户自定义参数解析器\n *\n * @author xuexiang\n * @since 2018/8/6 下午5:09\n */\npublic class CurrentAccountMethodArgumentResolver implements HandlerMethodArgumentResolver {\n\n    @Override\n    public boolean supportsParameter(MethodParameter parameter) {\n        return parameter.getParameterType().isAssignableFrom(Account.class)//判断是否能转成Account类型\n                && parameter.hasParameterAnnotation(CurrentAccount.class);//是否有CurrentAccount注解\n    }\n\n    @Override\n    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {\n        Account account = (Account) nativeWebRequest.getAttribute(Constants.CURRENT_ACCOUNT, RequestAttributes.SCOPE_REQUEST);\n        if (account != null) {\n            return account;\n        }\n        throw new MissingServletRequestPartException(Constants.CURRENT_ACCOUNT);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/config/Constants.java",
    "content": "package com.xuexiang.xupdateservice.config;\n\n/**\n * @author xuexiang\n * @since 2018/8/6 下午5:07\n */\npublic class Constants {\n\n    /**\n     * 当前用户参数名\n     */\n    public final static String CURRENT_ACCOUNT = \"CurrentAccount\";\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/config/FileStorageProperties.java",
    "content": "package com.xuexiang.xupdateservice.config;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.stereotype.Component;\n\n/**\n * @author xuexiang\n * @since 2018/7/18 下午2:15\n */\n@Component\n@ConfigurationProperties(prefix = \"upload\")\npublic class FileStorageProperties {\n\n    private String fileDirectory;\n    private boolean keepName;\n\n    public boolean isKeepName() {\n        return keepName;\n    }\n\n    public void setKeepName(boolean keepName) {\n        this.keepName = keepName;\n    }\n\n    public String getFileDirectory() {\n        return fileDirectory;\n    }\n\n    public void setFileDirectory(String fileDirectory) {\n        this.fileDirectory = fileDirectory;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/config/WebAppConfigurer.java",
    "content": "package com.xuexiang.xupdateservice.config;\n\nimport com.xuexiang.xupdateservice.component.interceptor.QuickRequestInterceptor;\nimport com.xuexiang.xupdateservice.component.token.AuthenticationInterceptor;\nimport com.xuexiang.xupdateservice.component.token.CurrentAccountMethodArgumentResolver;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.method.support.HandlerMethodArgumentResolver;\nimport org.springframework.web.servlet.config.annotation.*;\n\nimport java.util.List;\n\n/**\n * 配置URLInterceptor拦截器，以及拦截路径\n *\n * @author xuexiang\n * @since 2018/8/6 下午5:50\n */\n@EnableWebMvc\n@Configuration\npublic class WebAppConfigurer extends WebMvcConfigurerAdapter {\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(authenticationInterceptor());\n        registry.addInterceptor(quickRequestInterceptor());\n        super.addInterceptors(registry);\n    }\n\n    @Override\n    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {\n        argumentResolvers.add(currentAccountMethodArgumentResolver());\n        super.addArgumentResolvers(argumentResolvers);\n    }\n\n    @Override\n    public void addCorsMappings(CorsRegistry registry) {\n        registry.addMapping(\"/**\")\n                .allowedOriginPatterns(\"*\")\n                .allowCredentials(true)\n                .allowedHeaders(\"Origin\", \"X-Requested-With\", \"Content-Type\", \"Accept\", \"X-Token\", \"X-TimeStamp\")\n                .allowedMethods(\"GET\", \"POST\", \"PATCH\", \"DELETE\", \"PUT\")\n                .maxAge(3600);\n        super.addCorsMappings(registry);\n    }\n\n    @Bean\n    public CurrentAccountMethodArgumentResolver currentAccountMethodArgumentResolver() {\n        return new CurrentAccountMethodArgumentResolver();\n    }\n\n    @Bean\n    public AuthenticationInterceptor authenticationInterceptor() {\n        return new AuthenticationInterceptor();\n    }\n\n    @Bean\n    public QuickRequestInterceptor quickRequestInterceptor() {\n        return new QuickRequestInterceptor();\n    }\n\n    /**\n     * 资源列表\n     */\n    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {\n            \"classpath:/META-INF/resources/\", \"classpath:/resources/\",\n            \"classpath:/static/\", \"classpath:/public/\"};\n\n    @Override\n    public void addResourceHandlers(ResourceHandlerRegistry registry) {\n        if (!registry.hasMappingForPattern(\"/webjars/**\")) {\n            registry.addResourceHandler(\"/webjars/**\").addResourceLocations(\n                    \"classpath:/META-INF/resources/webjars/\");\n        }\n        if (!registry.hasMappingForPattern(\"/**\")) {\n            registry.addResourceHandler(\"/**\").addResourceLocations(\n                    CLASSPATH_RESOURCE_LOCATIONS);\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/controller/AccountController.java",
    "content": "package com.xuexiang.xupdateservice.controller;\n\nimport com.xuexiang.xupdateservice.api.request.PageQuery;\nimport com.xuexiang.xupdateservice.api.response.ApiResult;\nimport com.xuexiang.xupdateservice.api.response.LoginInfo;\nimport com.xuexiang.xupdateservice.component.annotation.CurrentAccount;\nimport com.xuexiang.xupdateservice.component.annotation.LoginRequired;\nimport com.xuexiang.xupdateservice.exception.ApiException;\nimport com.xuexiang.xupdateservice.model.Account;\nimport com.xuexiang.xupdateservice.service.AccountService;\nimport com.xuexiang.xupdateservice.utils.TokenUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.Date;\n\nimport static com.xuexiang.xupdateservice.exception.ApiException.ERROR.COMMON_BUSINESS_ERROR;\n\n/**\n * 账户管理服务api\n *\n * @author xuexiang\n * @since 2019/4/21 上午12:24\n */\n@RestController\n@RequestMapping(value = \"/account\")\npublic class AccountController {\n\n    @Autowired\n    private AccountService accountService;\n\n    @ResponseBody\n    @RequestMapping(value = \"/accountPageQuery\", method = RequestMethod.POST)\n    public ApiResult pageQueryAccounts(@RequestBody PageQuery pageQuery) throws Exception {\n        return new ApiResult<>().setData(accountService.getAllAccount(pageQuery.pageNum, pageQuery.pageSize));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/accounts\", method = RequestMethod.GET)\n    public ApiResult getAllAccount() throws Exception {\n        return new ApiResult<>().setData(accountService.getAllAccount());\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/login\", method = RequestMethod.POST)\n    public ApiResult login(@RequestBody Account account) throws Exception {\n        if (accountService.checkAccount(account.getLoginName()) == null) {\n            throw new ApiException(\"账号不存在！\", COMMON_BUSINESS_ERROR);\n        }\n        Account user = accountService.loginAccount(account.getLoginName(), account.getPassword());\n        if (user != null) {\n            ApiResult<LoginInfo> apiResult = new ApiResult<>();\n            apiResult.setData(new LoginInfo()\n                    .setUser(user)\n                    .setToken(TokenUtils.createJwtToken(user.getLoginName())));\n            return apiResult;\n        } else {\n            throw new ApiException(\"用户名或密码错误！\", COMMON_BUSINESS_ERROR);\n        }\n    }\n\n    @LoginRequired\n    @ResponseBody\n    @RequestMapping(value = \"/info\", method = RequestMethod.GET)\n    public ApiResult getCurrentAccount(@CurrentAccount Account account) throws Exception {\n        return new ApiResult<Account>().setData(account);\n    }\n\n    @LoginRequired\n    @ResponseBody\n    @RequestMapping(value = \"/logout\", method = RequestMethod.POST)\n    public ApiResult logout() throws Exception {\n        //清理用户数据\n        return new ApiResult<Boolean>().setData(true);\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/checkExist\", method = RequestMethod.POST)\n    public ApiResult checkAccountExist(String loginName) throws Exception {\n        return new ApiResult<Boolean>().setData(accountService.checkAccount(loginName) != null);\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/register\", method = RequestMethod.POST)\n    public ApiResult register(@RequestBody Account account) throws Exception {\n        if (accountService.checkAccount(account.getLoginName()) != null) {\n            throw new ApiException(\"账号已存在！\", COMMON_BUSINESS_ERROR);\n        }\n        account.setRegisterTime(new Date());\n        return new ApiResult<Boolean>().setData(accountService.registerAccount(account));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/delete\")\n    public ApiResult deleteAccount(@RequestBody Account account) throws Exception {\n        return new ApiResult<Boolean>().setData(accountService.deleteAccount(account.getAccountId()));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/updateInfo\")\n    public ApiResult updateAccountInfo(@RequestBody Account account) throws Exception {\n        return new ApiResult<Boolean>().setData(accountService.updateAccount(account));\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/controller/HomeController.java",
    "content": "package com.xuexiang.xupdateservice.controller;\n\n\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.GetMapping;\n\n@Controller\npublic class HomeController {\n\n    @GetMapping(value = {\"/\", \"/index\"})\n    public String index() {\n        return \"index\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/controller/UpdateController.java",
    "content": "package com.xuexiang.xupdateservice.controller;\n\nimport com.xuexiang.xupdateservice.api.request.PageQuery;\nimport com.xuexiang.xupdateservice.api.response.ApiResult;\nimport com.xuexiang.xupdateservice.model.AppVersionInfo;\nimport com.xuexiang.xupdateservice.service.FileStorageService;\nimport com.xuexiang.xupdateservice.service.UpdateService;\nimport com.xuexiang.xupdateservice.utils.DateUtils;\nimport com.xuexiang.xupdateservice.utils.FileUtils;\nimport com.xuexiang.xupdateservice.utils.Md5Utils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * 版本更新api\n *\n * @author xuexiang\n * @since 2018/7/23 下午6:21\n */\n@RestController\n@RequestMapping(value = \"/update\")\npublic class UpdateController {\n    private static final int SERVICE_ERROR_CODE = 5000;\n\n    private static final Logger logger = LoggerFactory.getLogger(UpdateController.class);\n\n    @Autowired\n    private UpdateService updateService;\n\n    @Autowired\n    private FileStorageService fileService;\n\n    @ResponseBody\n    @RequestMapping(value = \"/checkVersion\", method = RequestMethod.POST)\n    public ApiResult doCheckVersion(int versionCode, String appKey) {\n        return new ApiResult<AppVersionInfo>().setData(updateService.getLatestAppVersionInfo(versionCode, appKey));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/versionPageQuery\", method = RequestMethod.POST)\n    public ApiResult pageQueryVersions(@RequestBody PageQuery pageQuery) {\n        return new ApiResult<>().setData(updateService.getAllAppVersionInfo(pageQuery.pageNum, pageQuery.pageSize));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/versions\", method = RequestMethod.GET)\n    public ApiResult getAllVersions() {\n        return new ApiResult<>().setData(updateService.getAllAppVersionInfo());\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/newVersion\", method = RequestMethod.POST)\n    public ApiResult register(@RequestBody AppVersionInfo appVersionInfo) throws Exception {\n        return addNewAppVersion(appVersionInfo);\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/delete\")\n    public ApiResult deleteAppVersionInfo(@RequestBody AppVersionInfo appVersionInfo) throws Exception {\n        return new ApiResult<Boolean>().setData(updateService.deleteAppVersionInfo(appVersionInfo.getVersionId()));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/updateInfo\")\n    public ApiResult updateAppVersionInfo(@RequestBody AppVersionInfo appVersionInfo) throws Exception {\n        return new ApiResult<Boolean>().setData(updateService.updateAppVersionInfo(appVersionInfo));\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/addVersionInfo\", method = RequestMethod.POST)\n    public ApiResult addAppVersionInfo(AppVersionInfo appVersionInfo) {\n        return addNewAppVersion(appVersionInfo);\n    }\n\n    /**\n     * 添加新版本\n     * @param appVersionInfo\n     * @return\n     */\n    private ApiResult addNewAppVersion(AppVersionInfo appVersionInfo) {\n        ApiResult<AppVersionInfo> result = new ApiResult<>();\n        if (updateService.getAppVersionInfo(appVersionInfo.getVersionCode(), appVersionInfo.getAppKey()) != null) {\n            return getOnErrorApiResult(result, \"该版本信息已存在！\");\n        } else {\n            if (updateService.addAppVersionInfo(appVersionInfo)) {\n                return result.setData(updateService.getAppVersionInfo(appVersionInfo.getVersionCode(), appVersionInfo.getAppKey()));\n            } else {\n                return getOnErrorApiResult(result, \"版本信息添加失败！\");\n            }\n        }\n    }\n\n    /**\n     * 上传apk文件\n     *\n     * @param file      apk文件\n     * @param versionId apk的版本id\n     * @return\n     */\n    @PostMapping(\"/uploadApk\")\n    public ApiResult uploadApkFile(MultipartFile file, int versionId) {\n        ApiResult<Boolean> result = new ApiResult<>();\n        try {\n            String fileName = fileService.storeFile(file);\n            if (!StringUtils.isEmpty(fileName)) {  //更新apk信息\n                AppVersionInfo appVersionInfo = new AppVersionInfo();\n                appVersionInfo.setVersionId(versionId);\n                updateVersionInfo(fileName, appVersionInfo);\n\n                result.setData(updateService.updateAppVersionInfo(appVersionInfo));\n            } else {\n                result.setCode(SERVICE_ERROR_CODE)\n                        .setMsg(\"APK上传失败\")\n                        .setData(false);\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n            result.setCode(SERVICE_ERROR_CODE)\n                    .setMsg(e.getMessage())\n                    .setData(false);\n        }\n        return result;\n    }\n\n    private void updateVersionInfo(String fileName, AppVersionInfo appVersionInfo) throws Exception {\n        File apkFile = fileService.loadFileAsResource(fileName).getFile();\n        appVersionInfo.setApkMd5(Md5Utils.getFileMD5(apkFile));\n        appVersionInfo.setApkSize(FileUtils.getApkFileSize(apkFile));\n        appVersionInfo.setUploadTime(DateUtils.getNowString(DateUtils.yyyyMMddHHmmss.get()));\n        appVersionInfo.setDownloadUrl(fileName);\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/addAppVersion\", method = RequestMethod.POST)\n    public ApiResult addAppVersion(MultipartFile file, AppVersionInfo appVersionInfo) {\n        ApiResult<String> apiResult = new ApiResult<>();\n        if (updateService.getAppVersionInfo(appVersionInfo.getVersionCode(), appVersionInfo.getAppKey()) != null) {\n            return getOnErrorApiResult(apiResult, \"该版本信息已存在！\");\n        } else {\n            boolean result = updateService.addAppVersionInfo(appVersionInfo);\n            if (result) {\n                AppVersionInfo newVersion = updateService.getAppVersionInfo(appVersionInfo.getVersionCode(), appVersionInfo.getAppKey());\n                try {\n                    String fileName = fileService.storeFile(file);\n                    if (!StringUtils.isEmpty(fileName)) {  //更新apk信息\n                        updateVersionInfo(fileName, newVersion);\n\n                        if (updateService.updateAppVersionInfo(newVersion)) {\n                            return apiResult.setData(\"版本信息添加成功!\" );\n                        } else {\n                            return getOnErrorApiResult(apiResult, \"Apk信息添加失败!\");\n                        }\n                    } else {\n                        return getOnErrorApiResult(apiResult, \"APK上传失败:\");\n                    }\n                } catch (Exception e) {\n                    e.printStackTrace();\n                    return getOnErrorApiResult(apiResult, \"APK上传失败:\" + e.getMessage());\n                }\n            } else {\n                return getOnErrorApiResult(apiResult, \"版本信息添加失败！\");\n            }\n        }\n    }\n\n    private <T> ApiResult<T> getOnErrorApiResult(ApiResult<T> apiResult, String errorMsg) {\n        return apiResult.setCode(SERVICE_ERROR_CODE).setMsg(errorMsg);\n    }\n\n\n    @GetMapping(\"/apk/{fileName:.+}\")\n    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) throws Exception {\n        // Load file as Resource\n        Resource resource = fileService.loadFileAsResource(fileName);\n\n        // Try to determine file's content type\n        String contentType = null;\n        try {\n            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());\n        } catch (IOException ex) {\n            logger.info(\"Could not determine file type.\");\n        }\n\n        // Fallback to the default content type if type could not be determined\n        if (contentType == null) {\n            contentType = \"application/octet-stream\";\n        }\n        return ResponseEntity.ok()\n                .contentType(MediaType.parseMediaType(contentType))\n                .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=\\\"\" + resource.getFilename() + \"\\\"\")\n                .body(resource);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/exception/ApiException.java",
    "content": "package com.xuexiang.xupdateservice.exception;\n\n/**\n * @author xuexiang\n * @since 2018/8/6 下午3:11\n */\npublic class ApiException extends Exception {\n\n    /**\n     * 错误的code码\n     */\n    private int mCode;\n\n    public ApiException(String message, int code) {\n        super(message);\n        mCode = code;\n    }\n\n    public ApiException(Throwable e, int code) {\n        super(e);\n        mCode = code;\n    }\n\n    public int getCode() {\n        return mCode;\n    }\n\n    /**\n     * 约定异常\n     */\n    public static class ERROR {\n        /**\n         * Token失效，需要重新获取token的code码\n         */\n        public static final int TOKEN_INVALID = 100;\n        /**\n         * 缺少Token\n         */\n        public static final int TOKEN_MISSING = TOKEN_INVALID + 1;\n        /**\n         * 认证失败\n         */\n        public static final int AUTH_ERROR = TOKEN_MISSING + 1;\n\n\n        /**\n         * 未知错误\n         */\n        public static final int UNKNOWN = 5000;\n\n        /**\n         * 一般性业务错误\n         */\n        public static final int COMMON_BUSINESS_ERROR = UNKNOWN + 1;\n\n        /**\n         * 文件存储失败\n         */\n        public static final int FILE_STORE_ERROR = COMMON_BUSINESS_ERROR + 1;\n\n        /**\n         * 请求超出限制\n         */\n        public static final int REQUEST_BEYOND_LIMIT = FILE_STORE_ERROR + 1;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/exception/ApiExceptionHandler.java",
    "content": "package com.xuexiang.xupdateservice.exception;\n\nimport com.xuexiang.xupdateservice.api.response.ApiResult;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * @author xuexiang\n * @since 2018/8/6 下午3:07\n */\n@RestControllerAdvice\npublic class ApiExceptionHandler {\n\n    private static final Logger logger = LoggerFactory.getLogger(ApiExceptionHandler.class);\n\n    /**\n     * 拦截捕捉自定义异常\n     *\n     * @param ex\n     * @return\n     */\n    @ExceptionHandler(value = Exception.class)\n    @ResponseBody\n    public ApiResult apiErrorHandler(HttpServletRequest req, Exception ex) {\n        if (ex instanceof ApiException) {\n            logger.error(\"请求:{}, 发生业务异常:{}\", req.getRequestURL(), ex.getMessage(), ex);\n            return ApiResult.error((ApiException) ex);\n        } else {\n            logger.error(\"请求:{}, 发生系统异常:{}\", req.getRequestURL(), ex.getMessage(), ex);\n            return ApiResult.error(-1, ex.getMessage()); //系统异常错误\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/exception/FileNotFoundException.java",
    "content": "package com.xuexiang.xupdateservice.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.bind.annotation.ResponseStatus;\n\n@ResponseStatus(HttpStatus.NOT_FOUND)\npublic class FileNotFoundException extends RuntimeException {\n\n    public FileNotFoundException(String message) {\n        super(message);\n    }\n\n    public FileNotFoundException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/exception/FileStorageException.java",
    "content": "package com.xuexiang.xupdateservice.exception;\n\npublic class FileStorageException extends RuntimeException {\n    public FileStorageException(String message) {\n        super(message);\n    }\n\n    public FileStorageException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/mapper/AccountMapper.java",
    "content": "package com.xuexiang.xupdateservice.mapper;\n\nimport com.xuexiang.xupdateservice.model.Account;\nimport tk.mybatis.mapper.common.Mapper;\n\npublic interface AccountMapper extends Mapper<Account> {\n}"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/mapper/AppVersionInfoMapper.java",
    "content": "package com.xuexiang.xupdateservice.mapper;\n\nimport com.xuexiang.xupdateservice.model.AppVersionInfo;\nimport tk.mybatis.mapper.common.Mapper;\n\npublic interface AppVersionInfoMapper extends Mapper<AppVersionInfo> {\n}"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/model/Account.java",
    "content": "package com.xuexiang.xupdateservice.model;\n\nimport java.util.Date;\nimport javax.persistence.*;\n\n@Table(name = \"account\")\npublic class Account {\n    @Id\n    @Column(name = \"account_id\")\n    private Integer accountId;\n\n    @Column(name = \"login_name\")\n    private String loginName;\n\n    private String password;\n\n    private String nick;\n\n    private String authority;\n\n    private String avatar;\n\n    private String phone;\n\n    private String address;\n\n    @Column(name = \"register_time\")\n    private Date registerTime;\n\n    /**\n     * @return account_id\n     */\n    public Integer getAccountId() {\n        return accountId;\n    }\n\n    /**\n     * @param accountId\n     */\n    public void setAccountId(Integer accountId) {\n        this.accountId = accountId;\n    }\n\n    /**\n     * @return login_name\n     */\n    public String getLoginName() {\n        return loginName;\n    }\n\n    /**\n     * @param loginName\n     */\n    public void setLoginName(String loginName) {\n        this.loginName = loginName;\n    }\n\n    /**\n     * @return password\n     */\n    public String getPassword() {\n        return password;\n    }\n\n    /**\n     * @param password\n     */\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    /**\n     * @return nick\n     */\n    public String getNick() {\n        return nick;\n    }\n\n    /**\n     * @param nick\n     */\n    public void setNick(String nick) {\n        this.nick = nick;\n    }\n\n    /**\n     * @return authority\n     */\n    public String getAuthority() {\n        return authority;\n    }\n\n    /**\n     * @param authority\n     */\n    public void setAuthority(String authority) {\n        this.authority = authority;\n    }\n\n    /**\n     * @return avatar\n     */\n    public String getAvatar() {\n        return avatar;\n    }\n\n    /**\n     * @param avatar\n     */\n    public void setAvatar(String avatar) {\n        this.avatar = avatar;\n    }\n\n    /**\n     * @return phone\n     */\n    public String getPhone() {\n        return phone;\n    }\n\n    /**\n     * @param phone\n     */\n    public void setPhone(String phone) {\n        this.phone = phone;\n    }\n\n    /**\n     * @return address\n     */\n    public String getAddress() {\n        return address;\n    }\n\n    /**\n     * @param address\n     */\n    public void setAddress(String address) {\n        this.address = address;\n    }\n\n    /**\n     * @return register_time\n     */\n    public Date getRegisterTime() {\n        return registerTime;\n    }\n\n    /**\n     * @param registerTime\n     */\n    public void setRegisterTime(Date registerTime) {\n        this.registerTime = registerTime;\n    }\n}"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/model/AppVersionInfo.java",
    "content": "package com.xuexiang.xupdateservice.model;\n\nimport javax.persistence.*;\n\n@Table(name = \"app_version_info\")\npublic class AppVersionInfo {\n    @Id\n    @Column(name = \"version_id\")\n    private Integer versionId;\n\n    @Column(name = \"update_status\")\n    private Integer updateStatus;\n\n    @Column(name = \"version_code\")\n    private Integer versionCode;\n\n    @Column(name = \"version_name\")\n    private String versionName;\n\n    @Column(name = \"upload_time\")\n    private String uploadTime;\n\n    @Column(name = \"apk_size\")\n    private Integer apkSize;\n\n    @Column(name = \"app_key\")\n    private String appKey;\n\n    @Column(name = \"modify_content\")\n    private String modifyContent;\n\n    @Column(name = \"download_url\")\n    private String downloadUrl;\n\n    @Column(name = \"apk_md5\")\n    private String apkMd5;\n\n    /**\n     * @return version_id\n     */\n    public Integer getVersionId() {\n        return versionId;\n    }\n\n    /**\n     * @param versionId\n     */\n    public void setVersionId(Integer versionId) {\n        this.versionId = versionId;\n    }\n\n    /**\n     * @return update_status\n     */\n    public Integer getUpdateStatus() {\n        return updateStatus;\n    }\n\n    /**\n     * @param updateStatus\n     */\n    public void setUpdateStatus(Integer updateStatus) {\n        this.updateStatus = updateStatus;\n    }\n\n    /**\n     * @return version_code\n     */\n    public Integer getVersionCode() {\n        return versionCode;\n    }\n\n    /**\n     * @param versionCode\n     */\n    public void setVersionCode(Integer versionCode) {\n        this.versionCode = versionCode;\n    }\n\n    /**\n     * @return version_name\n     */\n    public String getVersionName() {\n        return versionName;\n    }\n\n    /**\n     * @param versionName\n     */\n    public void setVersionName(String versionName) {\n        this.versionName = versionName;\n    }\n\n    /**\n     * @return upload_time\n     */\n    public String getUploadTime() {\n        return uploadTime;\n    }\n\n    /**\n     * @param uploadTime\n     */\n    public void setUploadTime(String uploadTime) {\n        this.uploadTime = uploadTime;\n    }\n\n    /**\n     * @return apk_size\n     */\n    public Integer getApkSize() {\n        return apkSize;\n    }\n\n    /**\n     * @param apkSize\n     */\n    public void setApkSize(Integer apkSize) {\n        this.apkSize = apkSize;\n    }\n\n    /**\n     * @return app_key\n     */\n    public String getAppKey() {\n        return appKey;\n    }\n\n    /**\n     * @param appKey\n     */\n    public void setAppKey(String appKey) {\n        this.appKey = appKey;\n    }\n\n    /**\n     * @return modify_content\n     */\n    public String getModifyContent() {\n        return modifyContent;\n    }\n\n    /**\n     * @param modifyContent\n     */\n    public void setModifyContent(String modifyContent) {\n        this.modifyContent = modifyContent;\n    }\n\n    /**\n     * @return download_url\n     */\n    public String getDownloadUrl() {\n        return downloadUrl;\n    }\n\n    /**\n     * @param downloadUrl\n     */\n    public void setDownloadUrl(String downloadUrl) {\n        this.downloadUrl = downloadUrl;\n    }\n\n    /**\n     * @return apk_md5\n     */\n    public String getApkMd5() {\n        return apkMd5;\n    }\n\n    /**\n     * @param apkMd5\n     */\n    public void setApkMd5(String apkMd5) {\n        this.apkMd5 = apkMd5;\n    }\n}"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/AccountService.java",
    "content": "package com.xuexiang.xupdateservice.service;\n\nimport com.xuexiang.xupdateservice.api.response.PageData;\nimport com.xuexiang.xupdateservice.model.Account;\n\nimport java.util.List;\n\n/**\n * 账户管理服务\n *\n * @author xuexiang\n * @since 2019/4/20 下午11:55\n */\npublic interface AccountService {\n\n    /**\n     * 分页查询查询所有账户信息\n     *\n     * @param pageNum 开始的页数\n     * @param pageSize 每页的数量\n     * @return\n     */\n    PageData<Account> getAllAccount(int pageNum, int pageSize);\n\n    /**\n     * 查询所有账户信息\n     *\n     * @return\n     */\n    List<Account> getAllAccount();\n\n\n    /**\n     * 更新账户信息\n     *\n     * @param account\n     * @return\n     */\n    boolean updateAccount(Account account);\n\n    /**\n     * 删除账户信息\n     *\n     * @param accountId\n     * @return\n     */\n    boolean deleteAccount(int accountId);\n\n    /**\n     * 注册账户\n     *\n     * @param account\n     * @return\n     */\n    boolean registerAccount(Account account);\n\n    /**\n     * 检测账户是否存在\n     * @param loginName\n     * @return\n     */\n    Account checkAccount(String loginName);\n\n    /**\n     * 登陆账户\n     * @param loginName\n     * @param password\n     * @return\n     */\n    Account loginAccount(String loginName, String password);\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/FileStorageService.java",
    "content": "package com.xuexiang.xupdateservice.service;\n\nimport org.springframework.core.io.Resource;\nimport org.springframework.web.multipart.MultipartFile;\n\n/**\n * 文件存储服务\n *\n * @author xuexiang\n * @since 2018/7/18 下午3:36\n */\npublic interface FileStorageService {\n\n\n    /**\n     * 存储文件\n     *\n     * @param file\n     * @return 存储文件的文件名\n     */\n    String storeFile(MultipartFile file) throws Exception;\n\n\n    /**\n     * 读取文件\n     * @param fileName\n     * @return\n     */\n    Resource loadFileAsResource(String fileName) throws Exception;\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/UpdateService.java",
    "content": "package com.xuexiang.xupdateservice.service;\n\nimport com.xuexiang.xupdateservice.api.response.PageData;\nimport com.xuexiang.xupdateservice.model.AppVersionInfo;\n\nimport java.util.List;\n\n/**\n * @author xuexiang\n * @since 2018/7/26 上午11:04\n */\npublic interface UpdateService {\n\n    /**\n     * 无版本更新\n     */\n    int NO_NEW_VERSION = 0; // 0:无版本更新\n    /**\n     * 有版本更新，不需要强制升级\n     */\n    int HAVE_NEW_VERSION = 1; // 1:有版本更新，不需要强制升级\n    /**\n     * 有版本更新，需要强制升级\n     */\n    int HAVE_NEW_VERSION_FORCED_UPLOAD = 2; // 2:有版本更新，需要强制升级\n\n    /**\n     * 获取最新的apk版本信息\n     *\n     * @param versionCode\n     * @param appKey\n     * @return\n     */\n    AppVersionInfo getLatestAppVersionInfo(int versionCode, String appKey);\n\n    /**\n     * 根据appKey获取唯一apk的所有版本信息\n     *\n     * @param appKey\n     * @return\n     */\n    List<AppVersionInfo> getAllAppVersionInfo(String appKey);\n\n    /**\n     * 获取所有应用的版本信息\n     *\n     * @return\n     */\n    List<AppVersionInfo> getAllAppVersionInfo();\n\n    /**\n     * 分页查询所有应用的版本信息\n     *\n     * @param pageNum\n     * @param pageSize\n     * @return\n     */\n    PageData<AppVersionInfo> getAllAppVersionInfo(int pageNum, int pageSize);\n\n    /**\n     * 根据appKey和versionCode获取唯一的版本信息\n     *\n     * @param appKey\n     * @return\n     */\n    AppVersionInfo getAppVersionInfo(int versionCode, String appKey);\n\n    /**\n     * 添加app版本信息\n     *\n     * @param appVersionInfo\n     * @return\n     */\n    boolean addAppVersionInfo(AppVersionInfo appVersionInfo);\n\n    /**\n     * 更新app版本信息\n     *\n     * @param appVersionInfo\n     * @return\n     */\n    boolean updateAppVersionInfo(AppVersionInfo appVersionInfo);\n\n\n    /**\n     * 删除版本信息\n     *\n     * @param versionId\n     * @return\n     */\n    boolean deleteAppVersionInfo(int versionId);\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/impl/AccountServiceImpl.java",
    "content": "package com.xuexiang.xupdateservice.service.impl;\n\nimport com.github.pagehelper.Page;\nimport com.github.pagehelper.PageHelper;\nimport com.xuexiang.xupdateservice.api.response.PageData;\nimport com.xuexiang.xupdateservice.mapper.AccountMapper;\nimport com.xuexiang.xupdateservice.model.Account;\nimport com.xuexiang.xupdateservice.service.AccountService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport tk.mybatis.mapper.entity.Condition;\nimport java.util.List;\n\n/**\n *\n *\n * @author xuexiang\n * @since 2019/4/21 上午12:11\n */\n@Service(value = \"accountService\")\npublic class AccountServiceImpl implements AccountService {\n\n    @Autowired\n    private AccountMapper accountMapper;\n\n    @Override\n    public PageData<Account> getAllAccount(int pageNum, int pageSize) {\n        PageData<Account> pageData = new PageData<>();\n        Page<Account> page = PageHelper.startPage(pageNum, pageSize);\n        pageData.setArray(accountMapper.selectAll());\n        pageData.setPageNum(page.getPageNum())\n                .setPageSize(page.getPageSize())\n                .setTotal(page.getTotal());\n        return pageData;\n    }\n\n    @Override\n    public List<Account> getAllAccount() {\n        return accountMapper.selectAll();\n    }\n\n    @Override\n    public boolean updateAccount(Account account) {\n        return accountMapper.updateByPrimaryKeySelective(account) > 0;\n    }\n\n    @Override\n    public boolean deleteAccount(int accountId) {\n        return accountMapper.deleteByPrimaryKey(accountId) > 0;\n    }\n\n    @Override\n    public boolean registerAccount(Account account) {\n        return accountMapper.insert(account) > 0;\n    }\n\n    @Override\n    public Account checkAccount(String loginName) {\n        Condition condition = new Condition(Account.class);\n        condition.createCriteria().andEqualTo(\"loginName\", loginName);\n        return selectFirstAccountByCondition(condition);\n    }\n\n    @Override\n    public Account loginAccount(String loginName, String password) {\n        Condition condition = new Condition(Account.class);\n        condition.createCriteria().andEqualTo(\"loginName\", loginName)\n                .andEqualTo(\"password\", password);\n        return selectFirstAccountByCondition(condition);\n    }\n\n    private Account selectFirstAccountByCondition(Condition condition) {\n        List<Account> list = accountMapper.selectByExample(condition);\n        if (list != null && list.size() > 0) {\n            return list.get(0);\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/impl/FileStorageServiceImpl.java",
    "content": "package com.xuexiang.xupdateservice.service.impl;\n\nimport com.xuexiang.xupdateservice.config.FileStorageProperties;\nimport com.xuexiang.xupdateservice.exception.FileNotFoundException;\nimport com.xuexiang.xupdateservice.exception.FileStorageException;\nimport com.xuexiang.xupdateservice.service.FileStorageService;\nimport com.xuexiang.xupdateservice.utils.FileUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.UrlResource;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport java.io.IOException;\nimport java.net.MalformedURLException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\n@Service(value = \"fileService\")\npublic class FileStorageServiceImpl implements FileStorageService {\n\n    private final Path fileStorageLocation;\n\n    private final FileStorageProperties fileStorageProperties;\n\n    @Autowired\n    public FileStorageServiceImpl(FileStorageProperties fileStorageProperties) {\n        this.fileStorageProperties = fileStorageProperties;\n        this.fileStorageLocation = Paths.get(fileStorageProperties.getFileDirectory())\n                .toAbsolutePath().normalize();\n        try {\n            Files.createDirectories(this.fileStorageLocation);\n        } catch (Exception ex) {\n            throw new FileStorageException(\"Could not create the directory where the uploaded files will be stored.\", ex);\n        }\n    }\n\n    @Override\n    public String storeFile(MultipartFile file) throws Exception {\n        // Normalize file name\n        String fileName = StringUtils.cleanPath(file.getOriginalFilename());\n\n        if (!fileStorageProperties.isKeepName()){ //不保持文件名\n            fileName = FileUtils.randomFileName(fileName);\n        }\n\n        try {\n            // Check if the file's name contains invalid characters\n            if(fileName.contains(\"..\")) {\n                throw new FileStorageException(\"Sorry! Filename contains invalid path sequence \" + fileName);\n            }\n\n            // Copy file to the target location (Replacing existing file with the same name)\n            Path targetLocation = this.fileStorageLocation.resolve(fileName);\n\n            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);\n\n            return fileName;\n        } catch (IOException ex) {\n            throw new FileStorageException(\"Could not store file \" + fileName + \". Please try again!\", ex);\n        }\n    }\n\n    @Override\n    public Resource loadFileAsResource(String fileName) throws Exception {\n        try {\n            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();\n            Resource resource = new UrlResource(filePath.toUri());\n            if(resource.exists()) {\n                return resource;\n            } else {\n                throw new FileNotFoundException(\"File not found \" + fileName);\n            }\n        } catch (MalformedURLException ex) {\n            throw new FileNotFoundException(\"File not found \" + fileName, ex);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/service/impl/UpdateServiceImpl.java",
    "content": "package com.xuexiang.xupdateservice.service.impl;\n\nimport com.github.pagehelper.Page;\nimport com.github.pagehelper.PageHelper;\nimport com.xuexiang.xupdateservice.api.response.PageData;\nimport com.xuexiang.xupdateservice.mapper.AppVersionInfoMapper;\nimport com.xuexiang.xupdateservice.model.AppVersionInfo;\nimport com.xuexiang.xupdateservice.service.UpdateService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport tk.mybatis.mapper.entity.Condition;\n\nimport java.util.List;\n\n/**\n * 版本更新服务\n *\n * @author xuexiang\n * @since 2018/7/26 上午11:58\n */\n@Service(value = \"updateService\")\npublic class UpdateServiceImpl implements UpdateService {\n\n    @Autowired\n    private AppVersionInfoMapper appVersionInfoMapper;//这里会报错，但是并不会影响\n\n    @Override\n    public AppVersionInfo getLatestAppVersionInfo(int versionCode, String appKey) {\n        List<AppVersionInfo> appInfos = getAllAppVersionInfo(appKey);\n\n        if (appInfos.size() > 0) {\n            AppVersionInfo appVersionInfo = appInfos.get(0); //获取到最新的版本\n            if (appVersionInfo.getVersionCode() > versionCode) { //最新版本大，需要更新\n                return appVersionInfo;\n            } else {\n                AppVersionInfo appInfo = new AppVersionInfo();\n                appInfo.setUpdateStatus(UpdateService.NO_NEW_VERSION);\n                return appInfo;\n            }\n        } else {\n            AppVersionInfo appInfo = new AppVersionInfo();\n            appInfo.setUpdateStatus(UpdateService.NO_NEW_VERSION);\n            return appInfo;\n        }\n    }\n\n    @Override\n    public List<AppVersionInfo> getAllAppVersionInfo(String appKey) {\n        Condition condition = new Condition(AppVersionInfo.class);\n        condition.createCriteria().andEqualTo(\"appKey\", appKey);\n        condition.setOrderByClause(\"version_code desc\"); //根据版本号降序\n        return appVersionInfoMapper.selectByExample(condition);\n    }\n\n    @Override\n    public List<AppVersionInfo> getAllAppVersionInfo() {\n        return appVersionInfoMapper.selectAll();\n    }\n\n    /*\n     * 这个方法中用到了我们开头配置依赖的分页插件PageHelper\n     * 很简单，只需要在service层传入参数，然后将参数传递给一个插件的一个静态方法即可；\n     * pageNum 开始页数\n     * pageSize 每页显示的数据条数\n     * */\n    @Override\n    public PageData<AppVersionInfo> getAllAppVersionInfo(int pageNum, int pageSize) {\n        //将参数传给这个方法就可以实现物理分页了，非常简单。\n        PageData<AppVersionInfo> pageData = new PageData<>();\n        Page<AppVersionInfo> page = PageHelper.startPage(pageNum, pageSize);\n        pageData.setArray(appVersionInfoMapper.selectAll());\n        pageData.setPageNum(page.getPageNum())\n                .setPageSize(page.getPageSize())\n                .setTotal(page.getTotal());\n        return pageData;\n    }\n\n    @Override\n    public AppVersionInfo getAppVersionInfo(int versionCode, String appKey) {\n        Condition condition = new Condition(AppVersionInfo.class);\n        condition.createCriteria().andEqualTo(\"appKey\", appKey)\n                .andEqualTo(\"versionCode\", versionCode);\n        condition.setOrderByClause(\"version_code desc\"); //根据版本号降序\n        List<AppVersionInfo> list = appVersionInfoMapper.selectByExample(condition);\n        if (list != null && list.size() > 0) {\n            return list.get(0);\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public boolean addAppVersionInfo(AppVersionInfo appVersionInfo) {\n        return appVersionInfoMapper.insert(appVersionInfo) > 0;\n    }\n\n    @Override\n    public boolean updateAppVersionInfo(AppVersionInfo appVersionInfo) {\n        return appVersionInfoMapper.updateByPrimaryKeySelective(appVersionInfo) > 0;\n    }\n\n    @Override\n    public boolean deleteAppVersionInfo(int versionId) {\n        return appVersionInfoMapper.deleteByPrimaryKey(versionId) > 0;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/AspectJUtils.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.assertj.core.util.Lists;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.List;\n\n/**\n * AspectJ 工具类\n *\n * @author xuexiang\n * @since 2018/7/17 上午10:34\n */\npublic final class AspectJUtils {\n\n    private AspectJUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 获取方法名\n     *\n     * @param joinPoint\n     * @return\n     */\n    public static String getMethodName(JoinPoint joinPoint) {\n        String methodName = joinPoint.getSignature().toShortString();\n        String shortMethodNameSuffix = \"(..)\";\n        if (methodName.endsWith(shortMethodNameSuffix)) {\n            methodName = methodName.substring(0, methodName.length() - shortMethodNameSuffix.length());\n        }\n        return methodName;\n    }\n\n    /**\n     * 获取需要记录日志方法的参数，敏感参数用*代替\n     *\n     * @param joinPoint 切点\n     * @return 去除敏感参数后的Json字符串\n     */\n    public static String getMethodParams(ProceedingJoinPoint joinPoint) {\n        Object[] arguments = joinPoint.getArgs();\n        StringBuilder sb = new StringBuilder();\n        if (arguments == null || arguments.length <= 0) {\n            return sb.toString();\n        }\n        for (Object arg : arguments) {\n            //移除敏感内容\n            String paramStr;\n            if (arg instanceof HttpServletResponse) {\n                paramStr = HttpServletResponse.class.getSimpleName();\n            } else if (arg instanceof HttpServletRequest) {\n                paramStr = HttpServletRequest.class.getSimpleName();\n            } else if (arg instanceof MultipartFile) {\n                long size = ((MultipartFile) arg).getSize();\n                paramStr = MultipartFile.class.getSimpleName() + \" size:\" + size;\n            } else {\n                paramStr = deleteSensitiveContent(arg);\n            }\n            sb.append(paramStr).append(\",\");\n        }\n        return sb.deleteCharAt(sb.length() - 1).toString();\n    }\n\n    /**\n     * 删除参数中的敏感内容\n     *\n     * @param obj 参数对象\n     * @return 去除敏感内容后的参数对象\n     */\n    public static String deleteSensitiveContent(Object obj) {\n        JSONObject jsonObject = new JSONObject();\n        if (obj == null || obj instanceof Exception) {\n            return jsonObject.toJSONString();\n        }\n        String param = JSON.toJSONString(obj);\n        try {\n            jsonObject = JSONObject.parseObject(param);\n        } catch (Exception e) {\n            return String.valueOf(obj);\n        }\n        List<String> sensitiveFieldList = getSensitiveFieldList();\n        for (String sensitiveField : sensitiveFieldList) {\n            if (jsonObject.containsKey(sensitiveField)) {\n                jsonObject.put(sensitiveField, \"******\");\n            }\n        }\n        return jsonObject.toJSONString();\n    }\n\n    /**\n     * 敏感字段列表（当然这里你可以更改为可配置的）\n     */\n    private static List<String> getSensitiveFieldList() {\n        List<String> sensitiveFieldList = Lists.newArrayList();\n        sensitiveFieldList.add(\"pwd\");\n        sensitiveFieldList.add(\"password\");\n        return sensitiveFieldList;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/DateUtils.java",
    "content": "/*\n * Copyright (C) 2018 nl-xx(xx@aecg.com.cn)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *       http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.xuexiang.xupdateservice.utils;\n\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport static org.apache.commons.lang3.StringUtils.EMPTY;\n\n/**\n * 日期工具\n *\n * @author xuexiang\n * @since 2018/7/26 下午4:07\n */\npublic final class DateUtils {\n\n    /**\n     * yyyy-MM-dd\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMdd = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyy-MM-dd\"));\n    /**\n     * yyyyMMdd\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMddNoSep = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyyMMdd\"));\n    /**\n     * HH:mm:ss\n     */\n    public static final ThreadLocal<DateFormat> HHmmss = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"HH:mm:ss\"));\n    /**\n     * HH:mm\n     */\n    public static final ThreadLocal<DateFormat> HHmm = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"HH:mm\"));\n    /**\n     * yyyy-MM-dd HH:mm:ss\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMddHHmmss = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\"));\n    /**\n     * yyyyMMddHHmmss\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMddHHmmssNoSep = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyyMMddHHmmss\"));\n    /**\n     * yyyy-MM-dd HH:mm\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMddHHmm = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyy-MM-dd HH:mm\"));\n    /**\n     * yyyy-MM-dd HH:mm:ss:SSS\n     */\n    public static final ThreadLocal<DateFormat> yyyyMMddHHmmssSSS = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss:SSS\"));\n\n\n    //============================时间类型转化================================//\n\n    /**\n     * Don't let anyone instantiate this class.\n     */\n    private DateUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n\n    /**\n     * 将时间戳转为时间字符串\n     * <p>格式为 format</p>\n     *\n     * @param millis 毫秒时间戳\n     * @param format 时间格式\n     * @return 时间字符串\n     */\n    public static String millis2String(final long millis, final DateFormat format) {\n        return date2String(new Date(millis), format);\n    }\n\n    /**\n     * 将 Date 类型转为时间字符串\n     * <p>格式为 format</p>\n     *\n     * @param date   Date 类型时间\n     * @param format 时间格式\n     * @return 时间字符串\n     */\n    public static String date2String(final Date date, final DateFormat format) {\n        if (format != null) {\n            return format.format(date);\n        } else {\n            return EMPTY;\n        }\n    }\n\n    /**\n     * 将时间字符串转为时间戳\n     * <p>time 格式为 format</p>\n     *\n     * @param time   时间字符串\n     * @param format 时间格式\n     * @return 毫秒时间戳\n     */\n    public static long string2Millis(final String time, final DateFormat format) {\n        try {\n            if (format != null) {\n                return format.parse(time).getTime();\n            }\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return -1;\n    }\n\n    /**\n     * 将时间字符串转为 Date 类型\n     * <p>time 格式为 format</p>\n     *\n     * @param time   时间字符串\n     * @param format 时间格式\n     * @return Date 类型\n     */\n    public static Date string2Date(final String time, final DateFormat format) {\n        try {\n            if (format != null) {\n                return format.parse(time);\n            }\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * 将 Date 类型转为时间戳\n     *\n     * @param date Date 类型时间\n     * @return 毫秒时间戳\n     */\n    public static long date2Millis(final Date date) {\n        return date != null ? date.getTime() : -1;\n    }\n\n    /**\n     * 将时间戳转为 Date 类型\n     *\n     * @param millis 毫秒时间戳\n     * @return Date 类型时间\n     */\n    public static Date millis2Date(final long millis) {\n        return new Date(millis);\n    }\n\n\n    /**\n     * 生日期获取年龄\n     *\n     * @param birthDay 出生日期字符串\n     * @param format   日期格式\n     * @return 计算出的年龄\n     * @throws IllegalArgumentException\n     */\n    public static int getAgeByBirthDay(final String birthDay, final DateFormat format) throws IllegalArgumentException {\n        return getAgeByBirthDay(DateUtils.string2Date(birthDay, format));\n    }\n\n    /**\n     * 根据出生日期获取年龄\n     *\n     * @param birthDay 出生日期\n     * @return 计算出的年龄\n     * @throws IllegalArgumentException\n     */\n    public static int getAgeByBirthDay(Date birthDay) throws IllegalArgumentException {\n        Calendar cal = Calendar.getInstance();\n\n        if (cal.before(birthDay)) {\n            throw new IllegalArgumentException(\"The birthDay is before Now.It's unbelievable!\");\n        }\n        int yearNow = cal.get(Calendar.YEAR);\n        int monthNow = cal.get(Calendar.MONTH);\n        int dayNow = cal.get(Calendar.DAY_OF_MONTH);\n\n        cal.setTime(birthDay);\n        int yearBirth = cal.get(Calendar.YEAR);\n        int monthBirth = cal.get(Calendar.MONTH);\n        int dayBirth = cal.get(Calendar.DAY_OF_MONTH);\n\n        int age = yearNow - yearBirth;\n\n        if (monthNow <= monthBirth) {\n            if (monthNow == monthBirth) {\n                if (dayNow < dayBirth) {\n                    age--;\n                }\n            } else {\n                age--;\n            }\n        }\n        return age;\n    }\n\n    //==============================时间获取===============================//\n\n    /**\n     * 获取当前毫秒时间戳\n     *\n     * @return 毫秒时间戳\n     */\n    public static long getNowMills() {\n        return System.currentTimeMillis();\n    }\n\n    /**\n     * 获取当前时间字符串\n     * <p>格式为 format</p>\n     *\n     * @param format 时间格式\n     * @return 时间字符串\n     */\n    public static String getNowString(final DateFormat format) {\n        return millis2String(System.currentTimeMillis(), format);\n    }\n\n    /**\n     * 获取当前 Date\n     *\n     * @return Date 类型时间\n     */\n    public static Date getNowDate() {\n        return new Date();\n    }\n\n    /**\n     * 获取星期索引\n     * <p>注意：周日的 Index 才是 1，周六为 7</p>\n     * <p>time 格式为 format</p>\n     *\n     * @param time   时间字符串\n     * @param format 时间格式\n     * @return 1...7\n     * @see Calendar#SUNDAY\n     * @see Calendar#MONDAY\n     * @see Calendar#TUESDAY\n     * @see Calendar#WEDNESDAY\n     * @see Calendar#THURSDAY\n     * @see Calendar#FRIDAY\n     * @see Calendar#SATURDAY\n     */\n    public static int getWeekIndex(final String time, final DateFormat format) {\n        return getWeekIndex(string2Date(time, format));\n    }\n\n    /**\n     * 得到年\n     *\n     * @param date Date对象\n     * @return 年\n     */\n    public static int getYear(final Date date) {\n        Calendar c = Calendar.getInstance();\n        c.setTime(date);\n        return c.get(Calendar.YEAR);\n    }\n\n    /**\n     * 得到月\n     *\n     * @param date Date对象\n     * @return 月\n     */\n    public static int getMonth(final Date date) {\n        Calendar c = Calendar.getInstance();\n        c.setTime(date);\n        return c.get(Calendar.MONTH) + 1;\n    }\n\n    /**\n     * 得到日\n     *\n     * @param date Date对象\n     * @return 日\n     */\n    public static int getDay(final Date date) {\n        Calendar c = Calendar.getInstance();\n        c.setTime(date);\n        return c.get(Calendar.DAY_OF_MONTH);\n    }\n\n    /**\n     * 获取星期索引\n     * <p>注意：周日的 Index 才是 1，周六为 7</p>\n     *\n     * @param date Date 类型时间\n     * @return 1...7\n     * @see Calendar#SUNDAY\n     * @see Calendar#MONDAY\n     * @see Calendar#TUESDAY\n     * @see Calendar#WEDNESDAY\n     * @see Calendar#THURSDAY\n     * @see Calendar#FRIDAY\n     * @see Calendar#SATURDAY\n     */\n    public static int getWeekIndex(final Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        return cal.get(Calendar.DAY_OF_WEEK);\n    }\n\n    /**\n     * 获取星期索引\n     * <p>注意：周日的 Index 才是 1，周六为 7</p>\n     *\n     * @param millis 毫秒时间戳\n     * @return 1...7\n     * @see Calendar#SUNDAY\n     * @see Calendar#MONDAY\n     * @see Calendar#TUESDAY\n     * @see Calendar#WEDNESDAY\n     * @see Calendar#THURSDAY\n     * @see Calendar#FRIDAY\n     * @see Calendar#SATURDAY\n     */\n    public static int getWeekIndex(final long millis) {\n        return getWeekIndex(millis2Date(millis));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/FileUtils.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport org.springframework.util.StringUtils;\n\nimport java.io.File;\n\n/**\n * @author xuexiang\n * @since 2018/7/18 下午4:30\n */\npublic final class FileUtils {\n\n    private FileUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 生成随机的文件名\n     *\n     * @return\n     */\n    public static String randomFileName(String filePath) {\n        String fileExtension = getFileExtension(filePath);\n        return RandomGUID.getRandomGUID() + \".\" + fileExtension;\n    }\n\n\n    /**\n     * 获取全路径中的文件拓展名\n     *\n     * @param filePath 文件路径\n     * @return 文件拓展名\n     */\n    public static String getFileExtension(final String filePath) {\n        if (StringUtils.isEmpty(filePath)) return filePath;\n        int lastPoi = filePath.lastIndexOf('.');\n        int lastSep = filePath.lastIndexOf(File.separator);\n        if (lastPoi == -1 || lastSep >= lastPoi) return \"\";\n        return filePath.substring(lastPoi + 1);\n    }\n\n    /**\n     * 获取apk的大小【单位：KB】\n     *\n     * @param file 文件\n     * @return 文件长度\n     */\n    public static int getApkFileSize(final File file) {\n        long fileLength = getFileLength(file);\n        return (int) (fileLength / 1024);\n    }\n\n    /**\n     * 获取文件长度\n     *\n     * @param file 文件\n     * @return 文件长度\n     */\n    private static long getFileLength(final File file) {\n        if (!isFile(file)) return -1;\n        return file.length();\n    }\n\n    /**\n     * 判断是否是文件\n     *\n     * @param file 文件\n     * @return {@code true}: 是<br>{@code false}: 否\n     */\n    private static boolean isFile(final File file) {\n        return file != null && file.exists() && file.isFile();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/IpUtils.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.net.InetAddress;\nimport java.net.NetworkInterface;\nimport java.net.UnknownHostException;\nimport java.util.Enumeration;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * IP操作工具类\n *\n * @author xuexiang\n * @since 2018/7/17 上午10:34\n */\npublic final class IpUtils {\n\n    private IpUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    private static final Logger logger = LoggerFactory.getLogger(IpUtils.class);\n\n    private static final String IP_PATTERN = \"^(?:(?:[01]?\\\\d{1,2}|2[0-4]\\\\d|25[0-5])\\\\.){3}(?:[01]?\\\\d{1,2}|2[0-4]\\\\d|25[0-5])\\\\b\";\n\n    private static final String UNKNOWN = \"unknown\";\n\n    private static final String LOCAL_IP = \"127.0.0.1\";\n\n    /**\n     * 获取请求中的ip地址：过了多级反向代理，获取的ip不是唯一的，二是包含中间代理层ip\n     * @param request\n     * @return 可能有多个，例如：192.168.1.110， 192.168.1.120\n     */\n    public static String getIpAddr(HttpServletRequest request) {\n        String ip = LOCAL_IP;\n        if (request != null) {\n            ip = request.getHeader(\"x-forwarded-for\");\n            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n                ip = request.getHeader(\"Proxy-Client-IP\");\n            }\n\n            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n                ip = request.getHeader(\"WL-Proxy-Client-IP\");\n            }\n\n            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {\n                ip = request.getRemoteAddr();\n            }\n        }\n        return ip;\n\n    }\n\n    /**\n     * 获取客户端请求中的真实的ip地址\n     *  获取客户端的IP地址的方法是：request.getRemoteAddr()，这种方法在大部分情况下都是有效的。\n     *  但是在通过了Apache，Squid等反向代理软件就不能获取到客户端的真实IP地址。而且，如果通过了多级反向代理的话，X-Forwarded-For的值并不止一个，\n     *  而是一串ip值，例如：192.168.1.110， 192.168.1.120， 192.168.1.130， 192.168.1.100。其中第一个192.168.1.110才是用户真实的ip\n     * @param request\n     * @return\n     */\n    public static String getRealIp(HttpServletRequest request) {\n        String ip = LOCAL_IP;\n        if (request == null) {\n            return ip;\n        }\n        ip = request.getHeader(\"x-forwarded-for\");\n        ip = getIp(request,ip);\n        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {\n            ip = request.getRemoteAddr();\n            if (LOCAL_IP.equals(ip) || \"0:0:0:0:0:0:0:1\".equals(ip)) {\n                //根据网卡取本机配置的IP\n                InetAddress inet = null;\n                try {\n                    inet = InetAddress.getLocalHost();\n                    ip = inet.getHostAddress();\n                } catch (UnknownHostException e) {\n                    logger.error(\"getRealIp occurs error, caused by: \", e);\n                }\n            }\n        }\n        String ch = \",\";\n        if (ip != null && ip.contains(ch)) {\n                ip = ip.substring(0, ip.indexOf(ch));\n        }\n        return ip;\n    }\n\n    /**\n     * 通过各种方式获取IP\n     * @param request\n     * @param ip\n     * @return\n     */\n    private static String getIp(HttpServletRequest request, String ip){\n        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"Proxy-Client-IP\");\n        }\n        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"WL-Proxy-Client-IP\");\n        }\n        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"HTTP_CLIENT_IP\");\n        }\n        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"HTTP_X_FORWARDED_FOR\");\n        }\n        return ip;\n    }\n\n    /**\n     * 获取服务器IP\n     */\n    public static String getServiceIp() {\n        Enumeration<NetworkInterface> netInterfaces = null;\n        String ipsStr = \"\";\n        try {\n            netInterfaces = NetworkInterface.getNetworkInterfaces();\n            while (netInterfaces.hasMoreElements()) {\n                NetworkInterface ni = netInterfaces.nextElement();\n                Enumeration<InetAddress> ips = ni.getInetAddresses();\n                Pattern pattern = Pattern.compile(IP_PATTERN);\n                while (ips.hasMoreElements()) {\n                    String ip = ips.nextElement().getHostAddress();\n                    Matcher matcher = pattern.matcher(ip);\n                    if (matcher.matches() && !LOCAL_IP.equals(ip)) {\n                        ipsStr = ip;\n                    }\n                }\n            }\n        } catch (Exception e) {\n            logger.error(\"getServiceIp occurs error, caused by: \", e);\n        }\n        return ipsStr;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/Md5Utils.java",
    "content": "/*\n * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *       http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.xuexiang.xupdateservice.utils;\n\nimport org.apache.commons.lang3.StringUtils;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.nio.MappedByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\n/**\n * MD5加密工具类\n *\n * @author xuexiang\n * @since 2018/7/2 下午3:14\n */\npublic final class Md5Utils {\n\n    private Md5Utils() {\n        throw new UnsupportedOperationException(\"cannot be instantiated\");\n    }\n\n    public static String getFileMD5(File file) {\n        if (!file.exists()) {\n            return \"\";\n        }\n        FileInputStream in = null;\n        try {\n            in = new FileInputStream(file);\n            FileChannel channel = in.getChannel();\n            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());\n            MessageDigest md = MessageDigest.getInstance(\"MD5\");\n            md.update(buffer);\n            return bytes2Hex(md.digest());\n        } catch (NoSuchAlgorithmException | IOException e) {\n            e.printStackTrace();\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (IOException ignored) {\n                }\n            }\n        }\n        return \"\";\n    }\n\n    /**\n     * 验证文件是否有效【通过MD5值比较】\n     *\n     * @param md5  如果md5值为空，则不进行校验，直接为true\n     * @param file 需要校验的文件\n     * @return 文件是否有效\n     */\n    public static boolean isFileValid(String md5, File file) {\n        return StringUtils.isEmpty(md5) || md5.equals(Md5Utils.getFileMD5(file));\n    }\n\n    /**\n     * 一个byte转为2个hex字符\n     *\n     * @param src byte数组\n     * @return 16进制大写字符串\n     */\n    private static String bytes2Hex(byte[] src) {\n        char[] res = new char[src.length << 1];\n        final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};\n        for (int i = 0, j = 0; i < src.length; i++) {\n            res[j++] = hexDigits[src[i] >>> 4 & 0x0f];\n            res[j++] = hexDigits[src[i] & 0x0f];\n        }\n        return new String(res);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/QuickRequestUtils.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport com.xuexiang.xupdateservice.component.annotation.LimitedRequest;\nimport com.xuexiang.xupdateservice.component.annotation.QuickRequest;\nimport java.util.Map;\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 快速请求工具类\n *\n * @author xuexiang\n * @since 2018/8/7 下午2:27\n */\npublic final class QuickRequestUtils {\n\n    private QuickRequestUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n    /**\n     * 存放请求的次数\n     */\n    private static Map<String, Integer> sRequestCount = new ConcurrentHashMap<>();\n\n    /**\n     * 存放请求的时间戳\n     */\n    private static Map<String, Long> sRequestTimeStamp = new ConcurrentHashMap<>();\n\n    /**\n     * 是否是快速请求\n     *\n     * @param key\n     * @param quickRequest\n     * @param requestTimeStamp 请求携带的时间戳\n     * @return\n     */\n    public static boolean isQuickRequest(String key, QuickRequest quickRequest, String requestTimeStamp) {\n        long now = Long.valueOf(requestTimeStamp);\n        long lastRequestTimeStamp = sRequestTimeStamp.getOrDefault(key, 0L); //获取最后一次请求的时间戳\n        long timeD = now - lastRequestTimeStamp; //计算出间隔\n        if (0 < timeD && timeD < quickRequest.interval()) {\n            return true;\n        } else {\n            sRequestTimeStamp.put(key, now);\n            return false;\n        }\n    }\n\n\n    /**\n     * 是否是快速请求\n     *\n     * @param key\n     * @param limit\n     * @return\n     */\n    public static boolean isQuickRequest(String key, LimitedRequest limit) {\n        int count = sRequestCount.getOrDefault(key, 0) + 1;\n        sRequestCount.put(key, count);\n\n        if (count > 0) {\n            Timer timer = new Timer();\n            TimerTask task = new TimerTask() { //创建一个新的计时器任务。\n                @Override\n                public void run() {\n                    int number = sRequestCount.getOrDefault(key, 0) - 1;\n                    if (number > 0) {\n                        sRequestCount.put(key, number);\n                    } else {\n                        sRequestCount.remove(key);\n                    }\n                }\n            };\n            timer.schedule(task, limit.interval());  //安排在指定延迟后执行指定的任务。task : 所要安排的任务。: 执行任务前的延迟时间，单位是毫秒。\n        }\n        return count > limit.count();\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/RandomGUID.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.Random;\n\n/**\n * \t生成唯一的GUID\n *\n * @author XUE\n * @since 2019/4/22 13:30\n */\npublic final class RandomGUID {\n\n\tpublic String valueBeforeMD5 = \"\";\n\tpublic String valueAfterMD5 = \"\";\n\tprivate static Random myRand;\n\tprivate static SecureRandom mySecureRand;\n\n\tprivate static String s_id;\n\tprivate static final int PAD_BELOW = 0x10;\n\tprivate static final int TWO_BYTES = 0xFF;\n\n\tstatic {\n\t\tmySecureRand = new SecureRandom();\n\t\tlong secureInitializer = mySecureRand.nextLong();\n\t\tmyRand = new Random(secureInitializer);\n\t\tnew Thread(new Runnable() {\n\t\t\tpublic void run() {\n\t\t\t\ttry {\n\t\t\t\t\ts_id = InetAddress.getLocalHost().toString();\n\t\t\t\t} catch (UnknownHostException e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\t\t}).start();\n\t}\n\n\t/**\n\t * 获取随机生成的GUID\n\t * @return\n\t */\n\tpublic static String getRandomGUID() {\n\t\treturn new RandomGUID().toString();\n\t}\n\n\tpublic RandomGUID() {\n\t\tgetRandomGUID(false);\n\t}\n\tpublic RandomGUID(boolean secure) {\n\t\tgetRandomGUID(secure);\n\t}\n\n\t/*\n\t * Method to generate the random GUID\n\t */\n\tprivate void getRandomGUID(boolean secure) {\n\t\tMessageDigest md5 = null;\n\t\tStringBuffer sbValueBeforeMD5 = new StringBuffer(128);\n\n\t\ttry {\n\t\t\tmd5 = MessageDigest.getInstance(\"MD5\");\n\t\t} catch (NoSuchAlgorithmException e) {\n\n\t\t}\n\t\ttry {\n\t\t\tlong time = System.currentTimeMillis();\n\t\t\tlong rand = 0;\n\n\t\t\tif (secure) {\n\t\t\t\trand = mySecureRand.nextLong();\n\t\t\t} else {\n\t\t\t\trand = myRand.nextLong();\n\t\t\t}\n\t\t\tsbValueBeforeMD5.append(s_id);\n\t\t\tsbValueBeforeMD5.append(\":\");\n\t\t\tsbValueBeforeMD5.append(time);\n\t\t\tsbValueBeforeMD5.append(\":\");\n\t\t\tsbValueBeforeMD5.append(rand);\n\n\t\t\tvalueBeforeMD5 = sbValueBeforeMD5.toString();\n\t\t\tmd5.update(valueBeforeMD5.getBytes());\n\n\t\t\tbyte[] array = md5.digest();\n\t\t\tStringBuffer sb = new StringBuffer(32);\n\t\t\tfor (byte b1 : array) {\n\t\t\t\tint b = b1 & TWO_BYTES;\n\t\t\t\tif (b < PAD_BELOW)\n\t\t\t\t\tsb.append('0');\n\t\t\t\tsb.append(Integer.toHexString(b));\n\t\t\t}\n\n\t\t\tvalueAfterMD5 = sb.toString();\n\n\t\t} catch (Exception e) {\n\n\t\t}\n\t}\n\n\tpublic String toString() {\n\t\tString raw = valueAfterMD5.toUpperCase();\n\t\tStringBuffer sb = new StringBuffer(64);\n\t\tsb.append(raw, 0, 8);\n\t\tsb.append(\"-\");\n\t\tsb.append(raw, 8, 12);\n\t\tsb.append(\"-\");\n\t\tsb.append(raw, 12, 16);\n\t\tsb.append(\"-\");\n\t\tsb.append(raw, 16, 20);\n\t\tsb.append(\"-\");\n\t\tsb.append(raw.substring(20));\n\n\t\treturn sb.toString();\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/com/xuexiang/xupdateservice/utils/TokenUtils.java",
    "content": "package com.xuexiang.xupdateservice.utils;\n\nimport io.jsonwebtoken.Claims;\nimport io.jsonwebtoken.JwtBuilder;\nimport io.jsonwebtoken.Jwts;\nimport io.jsonwebtoken.SignatureAlgorithm;\nimport org.apache.commons.lang3.StringUtils;\n\nimport javax.crypto.spec.SecretKeySpec;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.xml.bind.DatatypeConverter;\nimport java.security.Key;\nimport java.util.Date;\n\n/**\n * token生成工具\n * @author xuexiang\n * @since 2018/8/6 下午4:27\n */\npublic final class TokenUtils {\n\n    /**\n     * 签名秘钥\n     */\n    public static final String SECRET = \"xuexiangjys\";\n\n    /**\n     * 生成token\n     *\n     * @param id 一般传入userName\n     * @return\n     */\n    public static String createJwtToken(String id) {\n        String issuer = \"www.github.com\";\n        String subject = \"xuexiangjys@163.com\";\n        long ttlMillis = 60 * 60 * 1000; //有效期一小时\n        return createJwtToken(id, issuer, subject, ttlMillis);\n    }\n\n    /**\n     * 生成Token\n     *\n     * @param id        编号\n     * @param issuer    该JWT的签发者，是否使用是可选的\n     * @param subject   该JWT所面向的用户，是否使用是可选的；\n     * @param ttlMillis 签发时间 （有效时间，过期会报错）\n     * @return token String\n     */\n    public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) {\n\n        // 签名算法 ，将对token进行签名\n        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;\n\n        // 生成签发时间\n        long nowMillis = System.currentTimeMillis();\n        Date now = new Date(nowMillis);\n\n        // 通过秘钥签名JWT\n        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);\n        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());\n\n        // Let's set the JWT Claims\n        JwtBuilder builder = Jwts.builder().setId(id)\n                .setIssuedAt(now)\n                .setSubject(subject)\n                .setIssuer(issuer)\n                .signWith(signatureAlgorithm, signingKey);\n\n        // if it has been specified, let's add the expiration\n        if (ttlMillis >= 0) {\n            long expMillis = nowMillis + ttlMillis;\n            Date exp = new Date(expMillis);\n            builder.setExpiration(exp);\n        }\n\n        // Builds the JWT and serializes it to a compact, URL-safe string\n        return builder.compact();\n\n    }\n\n    // Sample method to validate and read the JWT\n    public static Claims parseJWT(String jwt) {\n        // This line will throw an exception if it is not a signed JWS (as expected)\n        Claims claims = Jwts.parser()\n                .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))\n                .parseClaimsJws(jwt).getBody();\n        return claims;\n    }\n\n\n    /**\n     * 从HttpServletRequest中解析出token\n     *\n     * @param request\n     * @return\n     */\n    public static String parseToken(HttpServletRequest request) {\n        String accessToken = request.getHeader(\"X-Token\");\n        if (StringUtils.isEmpty(accessToken)) {\n            accessToken = request.getParameter(\"token\");\n        }\n        return accessToken;\n    }\n\n    public static void main(String[] args) {\n        System.out.println(TokenUtils.createJwtToken(\"11111\"));\n    }\n\n}\n"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "server:\n  port: 1111\n\nspring:\n    datasource:\n        name: XUpdateService\n        url: jdbc:mysql://localhost:3306/xupdate?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true\n        username: root\n        password: 123456\n        # 使用druid数据源\n        type: com.alibaba.druid.pool.DruidDataSource\n        driver-class-name: com.mysql.cj.jdbc.Driver\n        filters: stat\n        maxActive: 20\n        initialSize: 1\n        maxWait: 60000\n        minIdle: 1\n        timeBetweenEvictionRunsMillis: 60000\n        minEvictableIdleTimeMillis: 300000\n        validationQuery: select 'x'\n        testWhileIdle: true\n        testOnBorrow: false\n        testOnReturn: false\n        poolPreparedStatements: true\n        maxOpenPreparedStatements: 20\n    http:\n        multipart:\n            max-file-size: 100MB\n            max-request-size: 1000MB\n    servlet:\n        multipart:\n            enabled: true\n            file-size-threshold: 2KB\n            max-file-size: 10MB\n            max-request-size: 100MB\n    mvc:\n        view:\n          prefix: classpath:/templates/\n          suffix: .html\n\nmybatis:\n  mapper-locations: classpath:mapping/*.xml\n  type-aliases-package: com.xuexiang.xupdateservice.model\n\n#pagehelper\npagehelper:\n    helperDialect: mysql\n    reasonable: true\n    supportMethodsArguments: true\n    params: count=countSql\n\n\nupload:\n  file-directory: ./uploads\n  keep-name: true\n"
  },
  {
    "path": "src/main/resources/db-mysql.properties",
    "content": "#Mybatis Generator configuration\n##这边请换成你电脑上的绝对路径\n## Windows 配置\n## classPath=C:/Users/XUE/Documents/GitHub/XUpdateService/libs/mysql-connector-java-5.1.35.jar\n## Mac 配置\nclassPath=/Users/xuexiang/Documents/MyGitHub/XUpdateService/libs/mysql-connector-java-5.1.35.jar\njdbc.driverClassName=com.mysql.jdbc.Driver\njdbc.url=jdbc:mysql://localhost:3306/xupdate?useUnicode=true&characterEncoding=UTF-8\njdbc.user=root\njdbc.pass=123456"
  },
  {
    "path": "src/main/resources/mybatis_mapper/AccountMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.xuexiang.xupdateservice.mapper.AccountMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.xuexiang.xupdateservice.model.Account\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"account_id\" jdbcType=\"INTEGER\" property=\"accountId\" />\n    <result column=\"login_name\" jdbcType=\"VARCHAR\" property=\"loginName\" />\n    <result column=\"password\" jdbcType=\"VARCHAR\" property=\"password\" />\n    <result column=\"nick\" jdbcType=\"VARCHAR\" property=\"nick\" />\n    <result column=\"authority\" jdbcType=\"VARCHAR\" property=\"authority\" />\n    <result column=\"avatar\" jdbcType=\"VARCHAR\" property=\"avatar\" />\n    <result column=\"phone\" jdbcType=\"CHAR\" property=\"phone\" />\n    <result column=\"address\" jdbcType=\"VARCHAR\" property=\"address\" />\n    <result column=\"register_time\" jdbcType=\"TIMESTAMP\" property=\"registerTime\" />\n  </resultMap>\n</mapper>"
  },
  {
    "path": "src/main/resources/mybatis_mapper/AppVersionInfoMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.xuexiang.xupdateservice.mapper.AppVersionInfoMapper\">\n  <resultMap id=\"BaseResultMap\" type=\"com.xuexiang.xupdateservice.model.AppVersionInfo\">\n    <!--\n      WARNING - @mbg.generated\n    -->\n    <id column=\"version_id\" jdbcType=\"INTEGER\" property=\"versionId\" />\n    <result column=\"update_status\" jdbcType=\"INTEGER\" property=\"updateStatus\" />\n    <result column=\"version_code\" jdbcType=\"INTEGER\" property=\"versionCode\" />\n    <result column=\"version_name\" jdbcType=\"VARCHAR\" property=\"versionName\" />\n    <result column=\"upload_time\" jdbcType=\"VARCHAR\" property=\"uploadTime\" />\n    <result column=\"apk_size\" jdbcType=\"INTEGER\" property=\"apkSize\" />\n    <result column=\"app_key\" jdbcType=\"VARCHAR\" property=\"appKey\" />\n    <result column=\"modify_content\" jdbcType=\"LONGVARCHAR\" property=\"modifyContent\" />\n    <result column=\"download_url\" jdbcType=\"LONGVARCHAR\" property=\"downloadUrl\" />\n    <result column=\"apk_md5\" jdbcType=\"LONGVARCHAR\" property=\"apkMd5\" />\n  </resultMap>\n</mapper>"
  },
  {
    "path": "src/main/resources/static/css/main.css",
    "content": "* {\n    -webkit-box-sizing: border-box;\n    -moz-box-sizing: border-box;\n    box-sizing: border-box;\n}\n\nbody {\n    margin: 0;\n    padding: 0;\n    font-weight: 400;\n    font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n    font-size: 1rem;\n    line-height: 1.58;\n    color: #333;\n    background-color: #f4f4f4;\n}\n\nbody:before {\n    height: 50%;\n    width: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background: #128ff2;\n    content: \"\";\n    z-index: 0;\n}\n\n.clearfix:after {\n    display: block;\n    content: \"\";\n    clear: both;\n}\n\n\nh1, h2, h3, h4, h5, h6 {\n    margin-top: 20px;\n    margin-bottom: 20px;\n}\n\nh1 {\n    font-size: 1.7em;\n}\n\na {\n    color: #128ff2;\n}\n\nbutton {\n    box-shadow: none;\n    border: 1px solid transparent;\n    font-size: 14px;\n    outline: none;\n    line-height: 100%;\n    white-space: nowrap;\n    vertical-align: middle;\n    padding: 0.6rem 1rem;\n    border-radius: 2px;\n    transition: all 0.2s ease-in-out;\n    cursor: pointer;\n    min-height: 38px;\n}\n\ninput {\n    font-size: 1rem;\n}\n\ninput[type=\"file\"] {\n    border: 1px solid #128ff2;\n    padding: 6px;\n    max-width: 100%;\n}\n\n.file-input {\n    width: 100%;\n}\n\n\n.version-input {\n    width: 100%;\n}\n\n.submit-btn {\n    background-color: #128ff2;\n    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);\n    color: #fff;\n\n    display: inline-block;\n    margin-top: 15px;\n    min-width: 100px;\n}\n\n.reset-btn {\n    background-color: gray;\n    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);\n    color: #fff;\n\n    display: inline-block;\n    margin-top: 15px;\n    min-width: 100px;\n}\n\n.update-container {\n      max-width: 750px;\n      margin-left: auto;\n      margin-right: auto;\n      background-color: #fff;\n      box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);\n      margin-top: 60px;\n      min-height: 400px;\n      position: relative;\n      padding: 20px;\n}\n\n.update-header {\n    border-bottom: 1px solid #ececec;\n}\n\n.upload-header h2 {\n    font-weight: 500;\n}\n\n.apk-upload {\n    padding-bottom: 20px;\n    margin-bottom: 20px;\n    border-bottom: 1px solid #e8e8e8;\n}\n\n.update-response {\n    overflow-x: hidden;\n    word-break: break-all;\n}\n"
  },
  {
    "path": "src/main/resources/static/js/upload.js",
    "content": "var versionId = 0; //保存的临时变量\n\nfunction onAddInfo() {\n    var form = document.getElementById('infoForm');\n    var fd = new FormData(form);\n\n    $.ajax({\n        url: \"/update/addVersionInfo\",\n        type: \"POST\",\n        data: fd,\n        dataType: \"json\",\n        processData: false,  // 告诉jQuery不要去处理发送的数据\n        contentType: false,   // 告诉jQuery不要去设置Content-Type请求头\n        success: function (data) {\n            if (data[\"Code\"] === 0) {\n                $('#result').html(\"版本信息保存成功！\");\n                versionId = data['Data']['versionId'];\n            } else {\n                $('#result').html(\"版本信息保存失败:\" + data['Msg']);\n            }\n        },\n        error: function (error) {\n            $('#result').html(\"请求错误：\" + JSON.stringify(error));\n        }\n    });\n    return false;\n}\n\nfunction onUpload() {\n    if (versionId === 0) {\n        alert('请先填写应用版本信息!')\n        return false;\n    }\n\n    var fd = new FormData();\n    var files = document.querySelector('input[type=file]').files;\n    fd.append(\"versionId\", versionId);\n    fd.append(\"file\", files[0]);\n\n    $.ajax({\n        url: \"/update/uploadApk\",\n        type: \"POST\",\n        data: fd,\n        dataType: \"json\",\n        processData: false,  // 告诉jQuery不要去处理发送的数据\n        contentType: false,   // 告诉jQuery不要去设置Content-Type请求头\n\n        success: function (data) {\n            if (data[\"Code\"] === 0) {\n                $('#result').html(\"APK文件上传成功！\");\n                versionId = data['Data']['versionId'];\n            } else {\n                $('#result').html(\"APK文件上传失败:\" + data['Msg']);\n            }\n        },\n        error: function (error) {\n            $('#result').html(\"请求错误：\" + error.toString());\n        }\n    });\n    return false;\n}"
  },
  {
    "path": "src/main/resources/templates/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <title>XUpdate Service</title>\n    <link rel=\"stylesheet\" href=\"../static/css/main.css\"/>\n</head>\n<body>\n<noscript>\n    <h2>Sorry! Your browser doesn't support Javascript</h2>\n</noscript>\n<div class=\"update-container\">\n    <div class=\"update-header\">\n        <h2>APP版本管理</h2>\n    </div>\n\n    <div class=\"app-version-info\">\n        <form id=\"infoForm\" name=\"infoForm\" target=\"nm_iframe\">\n            <label>应用唯一号(App Key): </label><br>\n            <input id=\"appKey\" type=\"text\" name=\"appKey\" class=\"version-input\" required/><br>\n            <label>版本名: [例如：1.0.24]</label><br>\n            <input id=\"versionName\" type=\"text\" name=\"versionName\" class=\"version-input\" required/><br>\n            <label>版本号: [例如：24]</label><br>\n            <input id=\"versionCode\" type=\"number\" name=\"versionCode\" class=\"version-input\" required/><br>\n            <label>是否强制更新: </label>\n            <input id=\"updateStatus1\" type=\"radio\" value=\"1\" name=\"updateStatus\">否</input>\n            <input id=\"updateStatus2\" type=\"radio\" value=\"2\" name=\"updateStatus\">是</input><br>\n            <label>更新内容: </label><br>\n            <textarea id=\"modifyContent\" name=\"modifyContent\" rows=\"5\" cols=\"100\" class=\"version-input\"\n                      placeholder=\"请输入更新日志...\"></textarea>\n            <div>\n                <button type=\"submit\" class=\"submit-btn\" onclick=\"onAddInfo()\">保存</button>\n                <button type=\"reset\" class=\"reset-btn\">重置</button>\n            </div>\n        </form>\n        <br>\n        <form id=\"uploadForm\" name=\"uploadForm\" target=\"nm_iframe\">\n            <label>APK文件: </label><br>\n            <input id=\"file\" type=\"file\" name=\"file\" class=\"file-input\" required/>\n            <button type=\"submit\" class=\"submit-btn\" onclick=\"onUpload()\">保存</button>\n        </form><br>\n\n        <iframe id=\"id_iframe\" name=\"nm_iframe\" style=\"display:none;\"></iframe>\n        <div class=\"update-response\">\n            <div id=\"result\"></div>\n        </div>\n    </div>\n</div>\n<script src=\"https://code.jquery.com/jquery-3.1.1.min.js\"></script>\n<script src=\"../static/js/upload.js\" ></script>\n</body>\n</html>\n"
  },
  {
    "path": "src/test/java/com/xuexiang/xupdateservice/XUpdateServiceApplicationTests.java",
    "content": "package com.xuexiang.xupdateservice;\n\nimport com.xuexiang.xupdateservice.mapper.AppVersionInfoMapper;\nimport com.xuexiang.xupdateservice.model.AppVersionInfo;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\nimport tk.mybatis.mapper.entity.Condition;\n\nimport java.util.List;\n\n@RunWith(SpringRunner.class)\n@SpringBootTest\npublic class XUpdateServiceApplicationTests {\n\n\t@Autowired\n\tprivate AppVersionInfoMapper appVersionInfoMapper;//这里会报错，但是并不会影响\n\n\t@Test\n\tpublic void contextLoads() {\n\n\t}\n\n\t@Test\n\tpublic void SelectByConditionMapper(){\n\t\tCondition condition = new Condition(AppVersionInfo.class);\n\t\tcondition.createCriteria().andEqualTo(\"appKey\", \"test\");\n\t\tcondition.setOrderByClause(\"version_code desc\");\n\t\tList<AppVersionInfo> list = appVersionInfoMapper.selectByExample(condition);\n\t\tSystem.out.println(list.size());\n\t}\n\n}\n"
  }
]