[
  {
    "path": ".gitignore",
    "content": "# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n*.DS_Store\n.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/\n\n# log\n/log/\n/access_log/\nwork\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM gradle AS builder\nUSER root\nENV GRADLE_OPTS \"-Dorg.gradle.daemon=false\"\nWORKDIR /\nCOPY . .\nRUN gradle bootJar\n\nFROM java:8\nCOPY --from=builder /build/libs/movie-recommend-* app.jar\nEXPOSE 10015\nENTRYPOINT [\"java\", \"-jar\", \"app.jar\"]"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\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"
  },
  {
    "path": "README.md",
    "content": "# movierecommend\n基于Spring Boot的大数据电影推荐系统，采用协同过滤算法实现个性化推荐    \n\n### demo地址：[http://movie.pqdong.com/#/](http://movie.pqdong.com/#/)\n学生机（俗称板砖机），导致资源加载，接口响应比较慢，请耐心多等一会，让子弹多飞一会；    \n待功能开发完毕后会优化此页面加载速度  \n\n### 如何在本地开发\n```\n# 环境依赖\n1. java环境\n2. gradle项目，建议通过Intellij IDEA打开，运行build.gradle下载依赖，具体参考gradle教程\n3. IDEA下载开启 lombok插件\n4. 如果需要正常运行，需要使用mysql数据库和redis，具体配置可根据自己的项目配置在application.yml中\n5. 发送短信和照片上传需要一些token和access_key，可以参考代码`configService.getConfigValue`获取配置和阿里云短信\n```\n\n\n### 架构\n- 项目组织： 前端后端分离，通过Restful接口传递数据\n- 代码组织：基于SpringBoot，采用gradle进行依赖管理\n- 部署方式：采用docker部署，通过nginx实现简单的负载均衡。\n- 大数据处理：采用ElasticSearch进行海量数据的全文检索\n- 推荐算法： 采用Mahout基于用户的协同过滤算法和基于内容的协同过滤算法  \n\n![项目结构图](http://ydschool-online.nos.netease.com/1582746970143Snipaste_2020-02-26_22-19-39.png)\n\n### 技术栈\n* spring boot\n* docker\n* mysql\n* es\n* redis\n* gradle\n\n### 其他说明及文档\n由于一直从事Golang开发，没怎么搞过java，所以决定此毕设使用java来做。其中的一些还代码有待商榷，会一点点完善。  \n其他文档具体可见 /doc目录      \n\n### 数据库中数据来源声明 \n来源：[斗码小院公众号](http://www.csuldw.com/assets/articleImg/2019/code-main-fun.png)。  \n具体可见/doc/databaseSchema.md\n\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n    ext {\n        springBootVersion = '2.2.0.RELEASE'\n    }\n    repositories {\n        maven {\n            url 'http://maven.aliyun.com/nexus/content/groups/public/'\n        }\n        maven {\n            url 'https://plugins.gradle.org/m2/'\n        }\n        maven {\n            name \"elastic\"\n            url \"https://artifacts.elastic.co/maven\"\n        }\n    }\n    dependencies {\n        classpath(\"org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}\")\n        // 代码质量检测sonar\n        classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'\n    }\n}\n\ngroup = 'movie.recommend'\napply plugin: 'java'\napply plugin: 'idea'\napply plugin: 'jacoco'\napply plugin: 'application'\napply plugin: 'maven-publish'\napply plugin: 'org.springframework.boot'\napply plugin: 'io.spring.dependency-management'\napply plugin: 'org.sonarqube'\n\n\njacocoTestReport {\n    reports {\n        xml.enabled false\n        html.enabled true\n    }\n}\n\nversion = '1.8.0'\n\next {\n    springCloudVersion = 'Greenwich.RELEASE'\n}\n\nconfigurations.all {\n    // 去除boot自带的logging\n    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'\n    exclude module: 'commons-logging'\n}\n\nrepositories {\n    maven {\n        url 'http://maven.aliyun.com/nexus/content/groups/public/'\n    }\n    maven {\n        name \"elastic\"\n        url \"https://artifacts.elastic.co/maven\"\n    }\n    mavenCentral()\n}\n\n\ndependencies {\n    // springboot\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n    implementation 'org.springframework.boot:spring-boot-starter-actuator'\n    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'\n    implementation 'org.springframework.boot:spring-boot-starter-data-redis'\n    implementation 'org.springframework.boot:spring-boot-starter-log4j2'\n    implementation 'org.springframework.boot:spring-boot-starter-aop'\n    implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'\n    implementation 'org.elasticsearch.client:x-pack-transport:7.5.2'\n    // springboot-admin\n    implementation 'de.codecentric:spring-boot-admin-starter-client:2.1.5'\n\n    annotationProcessor 'org.projectlombok:lombok:1.18.2'\n    compileOnly 'org.projectlombok:lombok:1.18.2'\n    testAnnotationProcessor 'org.projectlombok:lombok:1.18.2'\n    testCompileOnly 'org.projectlombok:lombok:1.18.2'\n\n    implementation 'org.apache.commons:commons-pool2:2.6.1'\n    implementation 'com.google.guava:guava:26.0-jre'\n    implementation 'org.apache.commons:commons-lang3:3.8'\n    implementation 'com.alibaba:fastjson:1.2.60'\n    implementation 'mysql:mysql-connector-java:5.1.46'\n    implementation 'com.lmax:disruptor:3.3.6'\n    implementation 'com.google.code.gson:gson:2.8.5'\n    implementation 'com.aliyun:aliyun-java-sdk-core:3.5.1'\n    implementation 'com.aliyun:aliyun-java-sdk-dysmsapi:1.1.0'\n    implementation 'com.qiniu:qiniu-java-sdk:7.2.9'\n\n    implementation 'net.sourceforge.javacsv:javacsv:2.0'\n\n    //推荐相关\n    implementation 'org.apache.mahout:mahout-core:0.9'\n    implementation 'org.apache.mahout:mahout-math:0.9'\n    implementation 'org.apache.mahout:mahout-integration:0.9'\n\n    // test\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n}\n\n"
  },
  {
    "path": "doc/ES常用命令记录.md",
    "content": "### ES常用命令记录\n\n```\n# 获取index下数据\n/comment/_search?size=2&pretty\n\n# 查看type mapping\n/comment/_mapping?pretty\n\n# 统计index中数据数量\n/comment/comment/_count\n```"
  },
  {
    "path": "doc/databaseSchema.md",
    "content": "### 数据集简介\n\n本数据集采集于豆瓣电影，电影与演员数据收集于2019年8月上旬，影评数据(用户、评分、评论)收集于2019年9月初，共945万数据，其中包含14万部电影，7万演员，63万用户，416万条电影评分，442万条影评！\n\n### 数据来源声明 \n   \n爬虫项目源码： [AntSpider](https://github.com/csuldw/AntSpider)项目源码。  \n下载来源：[斗码小院公众号](http://www.csuldw.com/assets/articleImg/2019/code-main-fun.png)。  \n\n### Schema\n\n### Movie数据格式\n\n电影数据共140502部，2019年之前的电影有139129，当前未上映的有1373部，包含21个字段，部分字段数据为空，字段说明如下: \n\n- MOVIE_ID: 电影ID\n- NAME: 电影名称\n- ACTORS: 主演\n- COVER: 封面图片地址\n- DIRECTORS: 导演\n- GENRES: 类型\n- OFFICIAL_SITE: 地址\n- REGIONS: 制片国家/地区\n- LANGUAGES: 语言\n- RELEASE_DATE: 上映日期\n- MINS: 片长\n- SCORE: 评分\n- VOTES: 投票数\n- TAGS: 标签\n- STORYLINE: 电影描述\n- YEAR: 年份\n- ACTOR_IDS: 演员与PERSON_ID的对应关系,多个演员采用“\\|”符号分割，格式“演员A:ID\\|演员B:ID”；\n- DIRECTOR_IDS: 导演与PERSON_ID的对应关系,多个导演采用“\\|”符号分割，格式“导演A:ID\\|导演B:ID”；\n\n## Person数据格式\n\nPerson包括演员和导演，共72959个名人数据，包含10个字段，每个PERSON_ID都会对应一个name，不存在PERSON_ID的数据已过滤，各个字段说明如下: \n\n- PERSON_ID: 名人ID\n- NAME: 演员名称\n- SEX: 性别\n- NAME_EN: 更多英文名\n- NAME_ZH: 更多中文名\n- BIRTH: 出生日期\n- BIRTHPLACE: 出生地\n- CONSTELLATORY: 星座\n- PROFESSION: 职业\n- BIOGRAPHY: 简介，存在简介数据的名人只有15135个。\n\n\n\n## User数据格式\n\n639125用户数据，包含4个字段，具体的字段如下：\n\n- USER_ID：用户ID\n- USER_MD: 用户md5，唯一标示，这里设计的及其不好，但是数据集以此为标示，因此我也采用\n- USER_NICKNAME: 用户昵称\n- USER_AVATAR: 用户头像\n- USER_Tags: 用户标签\n\n### 标签表\n- TAG_ID: 标签id\n- TAG_NAME: 标签名称\n\n### Rating数据\n\n600384个用户的4169420条评分数据，涉及电影68471部，评分值为1-5分（1-很差，2-较差，3-还行，4-推荐，5-力荐），共包含5个字段，数据格式如下：\n\n- RATING_ID: 评分ID\n- USER_ID：豆瓣用户ID\n- MOVIE_ID: 电影ID，对应豆瓣的DOUBAN_ID\n- RATING: 评分\n- RATING_TIME: 评分时间\n\n\n### Comment数据格式\n\n评论数据共4428475 条，包含6个字段，各字段说明: \n\n- COMMENT_ID: 评论ID\n- USER_MD：用户ID\n- USERNAME: 用户名称\n- USERAVATAR： 用户头像\n- MOVIE_ID: 电影ID\n- MOVIE_NAME: 电影名称\n- CONTENT: 评论内容\n- VOTES: 评论赞同数\n- COMMENT_TIME: 评论时间\n"
  },
  {
    "path": "doc/功能模块与流程.md",
    "content": "## 功能模块与流程\n\n### 首页  \n1. 首页主要展示热门电影，最新，高分，好评等多种标签电影。通过ES mapping进行查询  \n2. Es mapping组合查询要优于mysql的组合查询\n3. 首页热门推荐：数据产生流程具体下\n4. 任务点：\n- [ ] 电影详情页接口\n- [x] 演员详情页接口\n\n### 热评\n1. 目前从数据库通过movie_id直接load\n2. 任务点\n- [ ] 评论详情页\n- [ ] 电影排名详情页\n\n### 用户\n- [x] userLogin接口\n- [x] userLogout接口\n- [x] userInfo\n- [ ] userEdit\n\n### 数据流转流程\n1. 用户对自身打标签\n2. 用户搜索或者过滤产生行为信息\n3. 用户对电影打分，评价：获取电影信息\n4. 将以上信息通过kafka->Flink聚合，进行推荐算法处理->es\n\n### 难点\nES集群搭建需要比较好的服务器"
  },
  {
    "path": "doc/大数据相关.md",
    "content": "### 大数据相关\n### 1. **java8 stream流式API的使用**，加快了处理常见集合的速度，使得在处理数据时更快，更方便简洁  \n\n```java\npublic Map<String, Object> searchMovies(MovieSearchDto info) {\n        Pair<Integer, Integer> pair = RecommendUtils.getStartAndEnd(info.getPage(), info.getSize());\n        List<MovieEntity> allMovie = info.getTags().stream()\n                .map(t -> getMovies(t, \"tag\", info.getPage() * info.getSize()))\n                .flatMap(Collection::stream)\n                .collect(Collectors.toCollection(LinkedList::new));\n        if (!StringUtils.isEmpty(info.getContent())) {\n            allMovie.addAll(getMovies(info.getContent(), \"name\", info.getPage() * info.getSize()));\n        }\n        List<MovieEntity> movieList = allMovie.subList(pair.getLeft(), pair.getRight() <= allMovie.size() ? pair.getRight() : allMovie.size());\n        Map<String, Object> result = new HashMap<>(2, 1);\n        result.put(\"total\", allMovie.size());\n        result.put(\"movieList\", movieList.stream().peek(m -> {\n            if (StringUtils.isEmpty(m.getCover())) {\n                m.setCover(ServerConstant.DefaultImg);\n            }\n        }).collect(Collectors.toCollection(LinkedList::new)));\n        return result;\n    }\n```\n\n### 2. 评论和观看记录是大数据，因此采用es处理，部署启动es\n\n```\ndocker run -d \\\n--name es \\\n-p 9200:9200 -p 9300:9300 \\\n-e \"discovery.type=single-node\" -e ES_JAVA_OPTS=\"-Xms200m -Xmx200m\" \\\n-e \"xpack.security.enabled=true\" \\\n-v /home/movie/esdata:/usr/share/elasticsearch/data \\\nelasticsearch:7.5.2 \n\n```\n以上方式会报错，查看日志发现是权限问题，因为我们在打数据卷时权限和容器里es，需要的权限是对不上的，所以把容器外数据卷的权限扩大一下，  \n保证容器内对外可写就行。  \n\n最好设置密码：  \n```\n# 进入容器\ndocker exec -it es /bin/sh\ncd bin/\nelasticsearch-setup-passwords interactive\n# 设置成功后\nChanged password for user [apm_system]\nChanged password for user [kibana]\nChanged password for user [logstash_system]\nChanged password for user [beats_system]\nChanged password for user [remote_monitoring_user]\nChanged password for user [elastic]\n```  \n\n### 3. 关于使用es踩了一个大坑\n如上使用了最新版的es，并且使用了xpack来进行权限验证，保证数据安全。但是spring boot 2.2.x版本之后才开始支持 es 7.x版本即以上，  \n一. 因此首先将spring boot的版本升级到 2.2.0\n二. `org.springframework.boot:spring-boot-starter-data-elasticsearch`使用默认的TransportClient是不进行权限校验的，因此链接es失败。  \n关于处理方法，网上一大堆，大部分都是复制粘贴，甚至全是错误的。经过自己摸索，解决了此问题，在此做一下汇总，日后处理。\n```\n1. 首先在maven或者gradle中需要引入xpack依赖包，需要注意在repositories添加https://artifacts.elastic.co/maven源，具体可见build.gradle\n网上说需要去除transport依赖，但是我在用时去除依赖会导致某些类加载不到。因此不用去除。\nimplementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'\nimplementation 'org.elasticsearch.client:x-pack-transport:7.5.2'\n\n2.application中添加es配置\nspring:\n  data:\n      elasticsearch:\n        repositories:\n          enabled: true\n    elasticsearch:\n      ip: es结点ip，这个是用来注入配置的，具体见ElasticSearchConfig\n      rest:\n        uris: ip:port # port结点\n        username: yourname  # es用户名\n        password: yourpassword  # es密码\n\n3. 配置bean，具体见ElasticSearchConfig\n```\n三. 创建CommentEsRepo，否则是不会自动创建index的，同时还要注意在CommentEs中添加es字段的type\n四. 简单的查询可以直接通过CommentEsRepo来实现，复杂的查询需要构造query然后通过ElasticsearchRestTemplate查询即可。  \nElasticsearchRestTemplate是ElasticsearchOperations 接口的另一个实现，其是基于High Level REST Client实现的，所有具有更高的性能  \n\n\n### Comment评论ES index选型\n1. comment中主要包含评论内容，电影id，用户头像，昵称等信息，其中用户头像和昵称是可变的，也就是用户在修改昵称和头像后，在返回给前端时应该是变化后的值  \n2. 因此在对comment index选型时有一下几种方案：  \n\n方案 | 对比\n---|---\nApplication-side joins | 应用层连接，独立了文档，但对应用层负载压力增大\nData denormalization | 非规范化数据，通过冗余来扁平化文档\nNested objects | 嵌套对象，文档不可以单独存在，是隐含在父文档中，需要单独建立文档\nParent/child relationships | 父子文档，文档独立，但是需要在同一个分片中，对内存要求高\n\n3. 考虑到服务器性能，以及业务场景，决定采用Data denormalization的方式，即将用户信息扁平处理到Comment index中，但是这样会带来另一个较为棘手的问题  \n当更新用户信息时需要对comment文档中的相关记录做更新，目前可以通过logstash来同步，  \n但是考虑到配置复杂，且用户信息更新是一个较不频繁的操作，暂时不采用这种方式。  \n所以直接在应用层通过异步线程池批量更新的方式去维护  \n\n\n\n"
  },
  {
    "path": "doc/推荐算法相关.md",
    "content": "### 推荐算法相关\n\n### Apache Mahout\nApache Mahout 是 Apache Software Foundation（ASF） 旗下的一个开源项目，提供一些可扩展的机器学习领域经典算法的实现，  \n旨在帮助开发人员更加方便快捷地创建智能应用程序...具体参考官网[https://www.ibm.com/developerworks/cn/java/j-lo-mahout/](https://www.ibm.com/developerworks/cn/java/j-lo-mahout/)  \n\n### 处理方法\n用户对每一个电影的评分，可以等同于用户对此电影的喜好程度；  \n对电影评分表中的数据采用协同过滤算法生成一个推荐列表，存储在redis中  \n目前考虑到服务器性能，没有做成实时推荐，推荐结果放到了redis中做一天的缓存  \n具体代码可见 /src/java/main/pqdong/movie/recommend/domain/service  \n\n### 基于用户的协同过滤算法\n核心代码：\n\n```java\n\npublic List<Long> userBasedRecommender(long userID,int size) throws TasteException {\n        UserSimilarity similarity  = new EuclideanDistanceSimilarity(dataModel );\n        NearestNUserNeighborhood  neighbor = new NearestNUserNeighborhood(NEIGHBORHOOD_NUM, similarity, dataModel );\n        Recommender recommender = new CachingRecommender(new GenericUserBasedRecommender(dataModel , neighbor, similarity));\n        List<RecommendedItem> recommendations = recommender.recommend(userID, size);\n        return getRecommendedItemIDs(recommendations);\n    }\n\n```\n\n### 基于内容的协同过滤算法\n\n```java\n\npublic List<Long> itemBasedRecommender(long userID,int size) throws TasteException {\n        List<Long> recommendItems = new ArrayList<>();\n        ItemSimilarity itemSimilarity = new PearsonCorrelationSimilarity(dataModel);\n        Recommender recommender = new GenericItemBasedRecommender(dataModel, itemSimilarity);\n        List<RecommendedItem> recommendations = recommender.recommend(userID, size);\n        return getRecommendedItemIDs(recommendations);\n    }\n\n```"
  },
  {
    "path": "doc/数据库建表语句.md",
    "content": "### 数据库建表语句\n\n数据来源：参考/doc/databaseSchema.md文档  \n\n数据库建议使用Mysql 5.6+。  \n项目中使用`@Table`注解，当数据库正确配置并连接时，spring boot会自动创建相关的数据库表，  \n具体可以参考 `/data/entity`和`application.yml`  \n\n1. user表建表语句\n```sql\nCREATE TABLE `user` (\n  `user_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `password` varchar(255) DEFAULT NULL,\n  `user_avatar` varchar(255) DEFAULT NULL,\n  `user_md` varchar(50) DEFAULT NULL,\n  `user_nickname` varchar(50) DEFAULT NULL,\n  `user_tags` varchar(255) DEFAULT NULL,\n  `phone` varchar(255) DEFAULT NULL,\n  `motto` varchar(255) DEFAULT NULL,\n  `sex` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`user_id`),\n  UNIQUE KEY `user_md` (`user_md`,`user_nickname`)\n) ENGINE=InnoDB AUTO_INCREMENT=195086 DEFAULT CHARSET=utf8mb4\n\n```\n\n2. movie表\n```sql\nCREATE TABLE `movie` (\n  `movie_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `actor_ids` varchar(255) DEFAULT NULL,\n  `actors` varchar(255) DEFAULT NULL,\n  `cover` varchar(255) DEFAULT NULL,\n  `director_ids` varchar(255) DEFAULT NULL,\n  `directors` varchar(255) DEFAULT NULL,\n  `genres` varchar(255) DEFAULT NULL,\n  `languages` varchar(255) DEFAULT NULL,\n  `mins` int(11) DEFAULT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `official_site` varchar(255) DEFAULT NULL,\n  `regions` varchar(255) DEFAULT NULL,\n  `release_date` date DEFAULT NULL,\n  `score` float DEFAULT NULL,\n  `storyline` text,\n  `tags` varchar(255) DEFAULT NULL,\n  `votes` int(11) DEFAULT NULL,\n  `year` int(11) DEFAULT NULL,\n  PRIMARY KEY (`movie_id`),\n  KEY `actor` (`actor_ids`(191)),\n  KEY `index1` (`score`),\n  KEY `index2` (`tags`(191))\n) ENGINE=InnoDB AUTO_INCREMENT=34782321 DEFAULT CHARSET=utf8mb4\n```\n\n3. person表\n```sql\nCREATE TABLE `person` (\n  `biography` varchar(255) DEFAULT NULL,\n  `birth` varchar(255) DEFAULT NULL,\n  `constellation` varchar(255) DEFAULT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `profession` varchar(255) DEFAULT NULL,\n  `sex` varchar(255) DEFAULT NULL,\n  `birth_place` varchar(255) DEFAULT NULL,\n  `name_en` varchar(255) DEFAULT NULL,\n  `name_zn` varchar(255) DEFAULT NULL,\n  `person_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `avatar` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`person_id`),\n  KEY `index1` (`name`(191))\n) ENGINE=InnoDB AUTO_INCREMENT=1422443 DEFAULT CHARSET=utf8mb4\n\n```\n\n4. movie_tags\n```sql\nCREATE TABLE `movie_tags` (\n  `movie_tag_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `name` varchar(255) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`movie_tag_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4\n\n```\n\n5. rating\n```sql\nCREATE TABLE `rating` (\n  `rating_id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `movie_id` bigint(20) DEFAULT NULL,\n  `rating` int(11) DEFAULT NULL,\n  `time` date DEFAULT NULL,\n  `user_id` bigint(20) DEFAULT NULL,\n  PRIMARY KEY (`rating_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1970708 DEFAULT CHARSET=utf8mb4\n\n```\n\n6. config\n```sql\nCREATE TABLE `config` (\n  `id` bigint(20) NOT NULL AUTO_INCREMENT,\n  `key` varchar(255) DEFAULT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4\n\n```"
  },
  {
    "path": "doc/服务部署.md",
    "content": "### 服务部署\n\n1. 服务部署与管理  \n\nV1:  \n目前分别为前后端起了两个容器，  \n因为需要暴露端口，以及方便联调，所以docker-compose方式暂时不启用  \n为了解决flink，kafka，es部署对服务器性能的要求，决定将其部署到另外一台服务器    \n\nV2:  \n暴露redis端口到外网不安全，因此还是使用docker-compose  \n\n2. 部署命令  \n```\n# 后端，采用host模式\ndocker run -d --name=movierecommend -p 10015:10015 --network=host -v /home/movie/log:/access_log imagename\n\n# 前端，采用host模式\ndocker run -d --name=moviefront -p 10016:10016 --network=host imagename\n\n# redis容器，采用docker bridge模式，同时iptables禁掉远程访问6379端口，开启aof保存数据\ndocker run -d --name=redis -p 127.0.0.1:6379:6379 -v /home/movie/redisdata:/data redis redis-server --appendonlyyes\n```\n\n3. nginx转发  \n配置了二级域名，并通过nginx转发。\n\n"
  },
  {
    "path": "doc/问题汇总及解决方案.md",
    "content": "## 问题汇总及解决方案\n\n### 1. user表性能分析与数据处理\n1. user表的主键为user_id,但是实际业务中标示用户的唯一字段为user_md（md5序列），因此需要在user_md上创建一个普通索引来加快查询速度。  \n在导入数据的时候，发现有一部分数据nickname重复，在用nickname和password做登录时，因为nickname不唯一，因此会造成登录错误，所以需要对数据进行清洗，  \n清洗方式如下：  \n\n```sql\nUPDATE `user` set user_nickname=CONCAT(user_nickname,user_id) WHERE user_nickname in (SELECT u3.user_nickname FROM (SELECT u2.user_nickname from `user` u2 GROUP BY u2.user_nickname HAVING COUNT(*)>1) AS u3)\n```\n\n对nickname的唯一性维护在程序中实现，不在数据层面做约束，从而降低数据库创建索引的存储和时间消耗。\n\n### 登录问题\n\n用户登录后会以 key: token value:userMd的信息向redis中保存；登录后前端在request的header中带着token,  \n后端通过自定义注解@LoginRequrie来进行登录验证（根据token获取userMd然后查询用户是否存在）\n\n### 部署问题\n服务器宽带太小，直接在服务器上使用docker build构建镜像速度太慢，所以搞了个github的公开仓库，来存储和分发镜像。    \n并使用开发机构建镜像，push到github上来加速构建和部署速度。  \n同时修改了服务器的docker代理，来加速镜像的拉取。  \n\n### 性能问题\n1、发现在更新用户信息时接口响应较慢，排查发现user表没有创建索引(肯定是智障了....)，因此对user表user_md和user_nickname创建唯一索引  \n\n\n### 其他问题\n发现服务器内存使用过高，导致docker服务oom，查看当前进程的内存使用情况发现node服务占用的资源太多，先kill掉。  \n然后将问题定位到前端服务部署方式，在原来的方式中启动了一个node服务，然后使用 `npm run dev`直接启动了前端项目，这样导致占用资源太多。  \n解决方法，直接使用`npm run build`将静态资源打出来，起个http的服务，暴露资源就可以  \n同时启用swap，设置大小为2G  （ps: 因为服务器太弱鸡踩了好多坑，好像有台大的服务器，可惜我没有钱...太惨了...）  \n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n\n  redis:\n    image: redis\n    container_name: redis:4.0-alpine\n    restart: always\n      volumes:\n        - /home/movie/redis:/data\n\n  backend:\n    image: backend:1.0\n    container_name: movierecommend\n    depends_on:\n      - redis\n    volumes:\n      - /home/movie/log:/access_log\n    network_mode: host\n\n  front:\n    image: moviefront:1.0\n    container_name: moviefront\n    depends_on:\n      - backend\n    ports:\n      - \"10016:10016\"\n    network_mode: host"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Feb 27 02:35:42 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.2.1-all.zip\n"
  },
  {
    "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='\"-Xmx64m\"'\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=\"-Xmx64m\"\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 = 'movie-recommend'\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/MovieRecommendApplication.java",
    "content": "package pqdong.movie.recommend;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.scheduling.annotation.EnableAsync;\n\n@EnableAsync\n@SpringBootApplication()\npublic class MovieRecommendApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(MovieRecommendApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/annotation/LoginRequired.java",
    "content": "package pqdong.movie.recommend.annotation;\n\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 pqdong\n * @description\n * @date 2020/03/04\n */\n@Target({ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface LoginRequired {\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/CtrlLogAdviceAop.java",
    "content": "package pqdong.movie.recommend.config;\n\nimport lombok.extern.slf4j.Slf4j;\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.springframework.stereotype.Component;\nimport org.springframework.web.bind.annotation.*;\nimport java.util.Arrays;\n\n@Slf4j\n@Aspect\n@Component\npublic class CtrlLogAdviceAop {\n\n\n    @Around(\"@annotation(requestMapping)\")\n    public Object requestMappingAdvice(ProceedingJoinPoint thisJoinPoint, RequestMapping requestMapping) throws Throwable {\n        String path = requestMapping.value().length == 0 ? \"\" : requestMapping.value()[0];\n        return process(thisJoinPoint, path);\n    }\n\n    @Around(\"@annotation(getMapping)\")\n    public Object getMappingAdvice(ProceedingJoinPoint thisJoinPoint, GetMapping getMapping) throws Throwable {\n        String path = getMapping.value().length == 0 ? \"\" : getMapping.value()[0];\n        return process(thisJoinPoint, path);\n    }\n\n    @Around(\"@annotation(postMapping)\")\n    public Object postMappingAdvice(ProceedingJoinPoint thisJoinPoint, PostMapping postMapping) throws Throwable {\n        String path = postMapping.value().length == 0 ? \"\" : postMapping.value()[0];\n        return process(thisJoinPoint, path);\n    }\n\n    @Around(\"@annotation(putMapping)\")\n    public Object putMappingAdvice(ProceedingJoinPoint thisJoinPoint, PutMapping putMapping) throws Throwable {\n        String path = putMapping.value().length == 0 ? \"\" : putMapping.value()[0];\n        return process(thisJoinPoint, path);\n    }\n\n    @Around(\"@annotation(deleteMapping)\")\n    public Object deleteMappingAdvice(ProceedingJoinPoint thisJoinPoint, DeleteMapping deleteMapping) throws Throwable {\n        String path = deleteMapping.value().length == 0 ? \"\" : deleteMapping.value()[0];\n        return process(thisJoinPoint, path);\n    }\n\n\n    private Object process(ProceedingJoinPoint pjp, String path) throws Throwable {\n        long startTime = System.currentTimeMillis();\n        Object result = pjp.proceed();\n        long endTime = System.currentTimeMillis();\n        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();\n        String logName = methodSignature.getMethod().getName() + \"(\" + path + \")\";\n        log.info(logName + \".time=\" + (endTime - startTime));\n        log.info(logName + \" Args: \" + Arrays.toString(pjp.getArgs()));\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/ElasticSearchConfig.java",
    "content": "package pqdong.movie.recommend.config;\n\nimport org.elasticsearch.client.transport.TransportClient;\nimport org.elasticsearch.common.settings.Settings;\nimport org.elasticsearch.common.transport.TransportAddress;\nimport org.elasticsearch.xpack.client.PreBuiltXPackTransportClient;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport pqdong.movie.recommend.service.ConfigService;\n\nimport javax.annotation.Resource;\nimport java.net.InetSocketAddress;\n\npublic class ElasticSearchConfig {\n\n    @Resource\n    private ConfigService configService;\n\n    @Bean\n    public TransportClient transportClient(@Value(\"${spring.elasticsearch.ip}\") String ip){\n        String password = configService.getConfigValue(\"ESPASSWORD\");\n        try (TransportClient client = new PreBuiltXPackTransportClient(Settings.builder()\n                .put(\"cluster.name\", \"docker-cluster\")\n                .put(\"xpack.security.user\", password)\n                .put(\"timeout\", 10000)\n                .put(\"client.transport.ping_timeout\", 10000)\n                .build())\n                .addTransportAddress(new TransportAddress(new InetSocketAddress(ip, 9300)))) {\n            return client;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/GlobalCorsConfig.java",
    "content": "package pqdong.movie.recommend.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\nimport org.springframework.web.filter.CorsFilter;\n\n@Configuration\npublic class GlobalCorsConfig {\n    @Bean\n    public CorsFilter corsFilter() {\n        CorsConfiguration config = new CorsConfiguration();\n        config.addAllowedOrigin(\"*\");\n        config.setAllowCredentials(true);\n        config.addAllowedMethod(\"*\");\n        config.addAllowedHeader(\"*\");\n//        config.addExposedHeader(\"*\");\n\n        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();\n        configSource.registerCorsConfiguration(\"/**\", config);\n\n        return new CorsFilter(configSource);\n    }\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/MahoutConfig.java",
    "content": "package pqdong.movie.recommend.config;\n\nimport com.mysql.jdbc.jdbc2.optional.MysqlDataSource;\nimport org.apache.mahout.cf.taste.impl.model.file.FileDataModel;\nimport org.apache.mahout.cf.taste.impl.model.jdbc.MySQLJDBCDataModel;\nimport org.apache.mahout.cf.taste.model.DataModel;\nimport org.springframework.beans.factory.annotation.Autowire;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport javax.sql.DataSource;\n\n@Configuration\npublic class MahoutConfig {\n\n    @Autowired\n    private DataSource dataSource;\n\n    @Bean(autowire = Autowire.BY_NAME,value = \"mySQLDataModel\")\n    public DataModel getMySQLJDBCDataModel(){\n        return new MySQLJDBCDataModel(dataSource,\"rating\",\"user_id\",\n                \"movie_id\",\"rating\", \"time\");\n    }\n\n\n//    @Bean(autowire = Autowire.BY_NAME,value = \"fileDataModel\")\n//    public DataModel getDataModel() throws IOException {\n//        URL url=MahoutConfig.class.getClassLoader().getResource(\"/rating.csv\");\n//        return new FileDataModel(new File(Objects.requireNonNull(url).getFile()));\n//    }\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/TaskConfiguration.java",
    "content": "package pqdong.movie.recommend.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\n\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ThreadPoolExecutor;\n\n/**\n * TaskConfiguration\n *\n * @author pqdong\n * @description 异步线程池配置\n * @since 2020/03/31\n */\n\n@Configuration\npublic class TaskConfiguration {\n\n    // 设置下线程池大小，防止oom\n\n    @Bean(\"taskExecutor\")\n    public Executor taskExecutor() {\n        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n        executor.setCorePoolSize(5);\n        executor.setMaxPoolSize(20);\n        executor.setQueueCapacity(50);\n        executor.setKeepAliveSeconds(60);\n        executor.setThreadNamePrefix(\"taskExecutor-\");\n        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());\n        return executor;\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/config/WebConfig.java",
    "content": "\npackage pqdong.movie.recommend.config;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\nimport pqdong.movie.recommend.utils.LoginInterceptor;\n\n/**\n * WebConfig\n *\n * @author pqdong\n * @since 2020/03/09\n */\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    public LoginInterceptor loginInterceptor() {\n        return new LoginInterceptor();\n    }\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(loginInterceptor());\n    }\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/controller/CommentController.java",
    "content": "package pqdong.movie.recommend.controller;\n\nimport org.springframework.web.bind.annotation.*;\nimport pqdong.movie.recommend.annotation.LoginRequired;\nimport pqdong.movie.recommend.data.dto.CommentSearchDto;\nimport pqdong.movie.recommend.data.entity.CommentEs;\nimport pqdong.movie.recommend.domain.util.ResponseMessage;\nimport pqdong.movie.recommend.service.CommentService;\n\nimport javax.annotation.Resource;\n\n@RestController\n@RequestMapping(\"/comment\")\npublic class CommentController {\n\n    @Resource\n    private CommentService commentService;\n\n    /**\n     * @method getCommentList 获取电影标签\n     */\n    @PostMapping(\"/list\")\n    public ResponseMessage getCommentList(@RequestBody CommentSearchDto commentSearchDto) {\n        return ResponseMessage.successMessage(commentService.getCommentList(commentSearchDto));\n    }\n\n    /**\n     * @method submitComment 提交评论\n     */\n    @PostMapping(\"/submit\")\n    @LoginRequired\n    public ResponseMessage submitComment(@RequestBody CommentEs commentEs) {\n        CommentEs comment = commentService.submitComment(commentEs);\n        if (comment == null){\n            return ResponseMessage.failedMessage(\"留言过快或失败，请稍后重试！\");\n        }\n        return ResponseMessage.successMessage(comment);\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/controller/MovieController.java",
    "content": "package pqdong.movie.recommend.controller;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.bind.annotation.*;\nimport pqdong.movie.recommend.annotation.LoginRequired;\nimport pqdong.movie.recommend.data.dto.MovieSearchDto;\nimport pqdong.movie.recommend.data.dto.RatingDto;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.domain.util.ResponseMessage;\nimport pqdong.movie.recommend.service.MovieService;\n\nimport javax.annotation.Resource;\n\n@RestController\n@RequestMapping(\"/movie\")\npublic class MovieController {\n\n    @Resource\n    private MovieService movieService;\n\n    /**\n     * @method getMovieTags 获取电影标签\n     */\n    @GetMapping(\"/tag\")\n    public ResponseMessage get() {\n        return ResponseMessage.successMessage(movieService.getMovieTags());\n    }\n\n    /**\n     * @method allMovie 获取电影列表\n     * @param key 关键字\n     * @param page 当前页数\n     * @param size 每页数据量\n     **/\n    @GetMapping(\"/list\")\n    public ResponseMessage allMovie(\n            @RequestParam(required = false, defaultValue = \"\") String key,\n            @RequestParam(required = false, defaultValue = \"1\") int page,\n            @RequestParam(required = false, defaultValue = \"12\") int size) {\n        return ResponseMessage.successMessage(movieService.getAllMovie(key, page, size));\n    }\n\n    /**\n     * @method getMovie 获取电影详情\n     * @param movieId 电影id\n     **/\n    @GetMapping(\"/info\")\n    public ResponseMessage getMovie(\n            @RequestParam(required = true, defaultValue = \"0\") Long movieId) {\n        return ResponseMessage.successMessage(movieService.getMovie(movieId));\n    }\n\n    /**\n     * @param personName 演员id\n     * @method getPersonMovie 获取演员出演的电影\n     **/\n    @GetMapping(\"/person/attend\")\n    public ResponseMessage getPersonAttendMovie(\n            @RequestParam(required = true, defaultValue = \"0\") String personName) {\n        return ResponseMessage.successMessage(movieService.getPersonAttendMovie(personName));\n    }\n\n    /**\n     * @param info 查找条件\n     * @method getMovieListByTag 根据标签获取电影列表\n     **/\n    @PostMapping(\"/listByTag\")\n    public ResponseMessage getMovieListByTag(@RequestBody(required = true) MovieSearchDto info) {\n        if (info.getTags().isEmpty() && StringUtils.isEmpty(info.getContent())){\n            return ResponseMessage.successMessage(movieService.getAllMovie(\"\", info.getPage(), info.getSize()));\n        }else{\n            return ResponseMessage.successMessage(movieService.searchMovies(info));\n        }\n    }\n\n    /**\n     * @method getHighMovie 获取高分电影\n     **/\n    @GetMapping(\"/high\")\n    public ResponseMessage getHighMovie() {\n        return ResponseMessage.successMessage(movieService.getHighMovie());\n    }\n\n    /**\n     * @param rating 打分\n     * @method updateScore 对电影评分\n     **/\n    @PostMapping(\"/update\")\n    @LoginRequired\n    public ResponseMessage updateScore(@RequestBody(required = true) RatingDto rating) {\n        return ResponseMessage.successMessage(movieService.updateScore(rating));\n    }\n\n    /**\n     * @method getHighMovie 获取高分电影\n     **/\n    @PostMapping(\"/recommend\")\n    public ResponseMessage getRecommendMovie(@RequestBody(required = false) UserEntity user) {\n        return ResponseMessage.successMessage(movieService.getRecommendMovie(user));\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/controller/PersonController.java",
    "content": "package pqdong.movie.recommend.controller;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport pqdong.movie.recommend.domain.util.ResponseMessage;\nimport pqdong.movie.recommend.service.PersonService;\n\nimport javax.annotation.Resource;\n\n@RestController\n@Slf4j\n@RequestMapping(\"/person\")\npublic class PersonController {\n\n    @Resource\n    private PersonService personService;\n\n    /**\n     * @param key  关键字\n     * @param page 当前页数\n     * @param size 每页数据量\n     * @method allPerson 查看所有演员\n     **/\n    @GetMapping(\"/list\")\n    public ResponseMessage allPerson(\n            @RequestParam(required = false, defaultValue = \"\") String key,\n            @RequestParam(required = false, defaultValue = \"1\") int page,\n            @RequestParam(required = false, defaultValue = \"4\") int size) {\n        return ResponseMessage.successMessage(personService.getAllPerson(key, page, size));\n    }\n\n    /**\n     * @param personId 演员id\n     * @method getPerson 获取导演演员详情\n     **/\n    @GetMapping(\"/info\")\n    public ResponseMessage getPerson(\n            @RequestParam(required = true, defaultValue = \"0\") Long personId) {\n        return ResponseMessage.successMessage(personService.getPerson(personId));\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/controller/UserController.java",
    "content": "package pqdong.movie.recommend.controller;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\nimport pqdong.movie.recommend.annotation.LoginRequired;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.data.dto.UserInfo;\nimport pqdong.movie.recommend.domain.util.ResponseMessage;\nimport pqdong.movie.recommend.service.SmsService;\nimport pqdong.movie.recommend.service.UserService;\n\nimport javax.annotation.Resource;\nimport java.util.Map;\n\n/**\n * UserController\n * @description 用户信息相关接口\n * @author pqdong\n * @since 2020/02/27 16:42\n */\n@RestController\n@RequestMapping(\"/user\")\npublic class UserController {\n\n    @Resource\n    private SmsService smsService;\n\n    @Resource\n    private UserService userService;\n\n    /**\n     * @method getUserInfo 获取用户信息\n     */\n    @GetMapping(\"/userInfo\")\n    public ResponseMessage getCourseInfo(@RequestParam(required = true) String token) {\n        return ResponseMessage.successMessage(userService.getUserInfo(token));\n    }\n\n    /**\n     * @method updateUserInfo 修改用户信息\n     */\n    @PostMapping(\"/userInfo\")\n    public ResponseMessage updateUserInfo(@RequestBody(required = true) UserEntity user) {\n        UserEntity userInfo = userService.updateUser(user);\n        if (null == userInfo){\n            return ResponseMessage.failedMessage(\"昵称已经存在，请跟换昵称！\");\n        }\n        return ResponseMessage.successMessage(userService.updateUser(user));\n    }\n\n    /**\n     * @method register 注册用户\n     */\n    @PostMapping(\"/register\")\n    public ResponseMessage register(@RequestBody UserInfo user){\n        String result = userService.register(user);\n        if (result.equals(\"success\")){\n            return ResponseMessage.successMessage(\"success\");\n        } else{\n            return ResponseMessage.failedMessage(result);\n        }\n    }\n\n    /**\n     * @method login 登录接口\n     */\n    @PostMapping(\"/login\")\n    public ResponseMessage userLogin(@RequestBody UserInfo user) {\n        Map<String, Object> info = userService.login(user.getUsername(), user.getPassword());\n        if (info != null){\n            return ResponseMessage.successMessage(info);\n        } else {\n            return ResponseMessage.failedMessage(\"登录失败，请检查用户名或密码！\");\n        }\n\n    }\n\n    /**\n     * @method code 发送短信验证码的接口\n     * @param phone 手机号\n     **/\n    @GetMapping(\"/code\")\n    public ResponseMessage code(@RequestParam String phone) {\n        String code = smsService.sendCode(phone);\n        if (StringUtils.isNotEmpty(code)) {\n            return ResponseMessage.successMessage(\"发送成功\");\n        } else {\n            return ResponseMessage.failedMessage(\"发送失败\");\n        }\n    }\n\n    /**\n     * @method upload 上传用户头像\n     * @param avatar 头像\n     **/\n    @PostMapping(\"/avatar\")\n    @LoginRequired\n    public ResponseMessage upload(@RequestParam(\"userMd\") String userMd, @RequestParam(\"avatar\") MultipartFile avatar) {\n        String url = userService.uploadAvatar(userMd, avatar);\n        if (StringUtils.contains(url,\"http\")) {\n            return ResponseMessage.successMessage(url);\n        } else {\n            return ResponseMessage.failedMessage(url);\n        }\n    }\n\n    /**\n     * @method logout 退出接口\n     **/\n    @PostMapping(\"/logout\")\n    @LoginRequired\n    public ResponseMessage logout() {\n        return ResponseMessage.successMessage(userService.logout());\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/controller/UtilController.java",
    "content": "package pqdong.movie.recommend.controller;\n\n\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport pqdong.movie.recommend.domain.util.ResponseMessage;\nimport pqdong.movie.recommend.service.ElasticSearchService;\n\nimport javax.annotation.Resource;\n\n/**\n * UtilController\n * @description 系统状态请求\n * @author pqdong\n * @since 2020/02/27 16:42\n */\n@RestController\n@RequestMapping(\"/util\")\npublic class UtilController {\n\n    @Resource\n    private ElasticSearchService elasticSearchService;\n\n    @GetMapping(\"/ping/system\")\n    public ResponseMessage pingSystem() {\n        return ResponseMessage.successMessage(\"system health\");\n    }\n\n    @GetMapping(\"/ping/es\")\n    public ResponseMessage pingEs() {\n        return ResponseMessage.successMessage(elasticSearchService.getAllIndex());\n    }\n\n    @GetMapping(\"/backend/comment\")\n    public ResponseMessage backend() {\n        return ResponseMessage.successMessage(elasticSearchService.importCommentToEs());\n    }\n\n    @GetMapping(\"/update/comment\")\n    public ResponseMessage update(@RequestParam(required = false) Long movieId) {\n        return ResponseMessage.successMessage(elasticSearchService.updateCommentToEs(movieId));\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/constant/ServerConstant.java",
    "content": "package pqdong.movie.recommend.data.constant;\n\n/*\n* 系统配置\n* @author pqdong\n* @time 2020/03/28\n */\n\npublic class ServerConstant {\n\n    public static final String DefaultImg = \"https://ydschool-video.nosdn.127.net/1585389729635Snipaste_2020-03-28_18-02-41.png\";\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/constant/UserConstant.java",
    "content": "package pqdong.movie.recommend.data.constant;\n\nimport com.google.common.base.Joiner;\n\npublic class UserConstant {\n\n    public static final String OK = \"OK\";\n\n    public static final String LOGOUT = \"logout\";\n\n    public static final String USER_AVATAR = \"avatar\";\n\n    public static final String PHONE_CODE = \"phone_code\";\n\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/dto/CommentSearchDto.java",
    "content": "package pqdong.movie.recommend.data.dto;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class CommentSearchDto {\n    private Integer page;\n\n    private Integer size;\n\n    private Long movieId;\n\n    private String userMd;\n\n    private String content;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/dto/MovieSearchDto.java",
    "content": "package pqdong.movie.recommend.data.dto;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class MovieSearchDto {\n    private Integer page;\n\n    private Integer size;\n\n    private List<String> tags;\n\n    private String content;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/dto/RatingDto.java",
    "content": "package pqdong.movie.recommend.data.dto;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class RatingDto {\n\n    private Long movieId;\n\n    private Long userId;\n\n    private Float rating;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/dto/UserInfo.java",
    "content": "package pqdong.movie.recommend.data.dto;\n\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport pqdong.movie.recommend.data.entity.MovieTagEntity;\n\nimport java.util.List;\n\n@Data\n// 自动生成无参数构造函数\n@NoArgsConstructor\n// 自动生成全参数构造函数\n@AllArgsConstructor\n@JsonIgnoreProperties(ignoreUnknown = true)\n// 用户详情信息\npublic class UserInfo {\n\n    private long id;\n\n    private String userMd;\n\n    // 用户头像\n    private String userAvatar;\n\n    // 用户标签\n    private String userTags;\n\n    // 用户手机\n    private String phone;\n\n    private String code;\n\n    private String username;\n\n    private String password;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/CommentEs.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport org.springframework.data.annotation.Id;\nimport org.springframework.data.elasticsearch.annotations.DateFormat;\nimport org.springframework.data.elasticsearch.annotations.Document;\nimport org.springframework.data.elasticsearch.annotations.Field;\nimport org.springframework.data.elasticsearch.annotations.FieldType;\n\nimport java.util.Date;\n\n\n/**\n * CommentEs\n * 评论\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Data\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\n@Document(indexName = \"comment\", type = \"comment\")\npublic class CommentEs {\n\n    @Id\n    private Long commentId;\n\n    // 用户id\n    @Field(type= FieldType.Text)\n    private String userMd;\n\n    // 用户名称\n    @Field(type= FieldType.Text)\n    private String userName;\n\n    // 用户头像\n    @Field(type= FieldType.Text)\n    private String userAvatar;\n\n    // 电影id\n    @Field(type= FieldType.Long)\n    private Long movieId;\n\n    // 电影名称\n    @Field(type= FieldType.Text)\n    private String movieName;\n\n    // 电影评论\n    @Field(type= FieldType.Text)\n    private String content;\n\n    // 赞同数\n    @Field(type= FieldType.Long)\n    private Long votes;\n\n    // 评论时间\n    @JsonFormat( pattern=\"yyyy-MM-dd HH:mm:ss\",timezone = \"GMT+8\")\n    @Field(format= DateFormat.custom,pattern = \"yyyy-MM-dd HH:mm:ss\",type= FieldType.Date)\n    private Date commentTime;\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/ConfigEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport javax.persistence.*;\n\n/**\n * ConfigEntity\n * 配置实体\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Data\n@NoArgsConstructor\n@Entity\n@Table(name = \"config\")\npublic class ConfigEntity {\n    @Id\n    @Column(name = \"id\")\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    @Column(name = \"key\")\n    private String key;\n\n    @Column(name = \"value\")\n    private String value;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/MovieEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\n/*\n* 电影\n* @author pqdong\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport javax.persistence.*;\nimport java.util.Date;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@Table(name = \"movie\")\n@Entity\npublic class MovieEntity {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"movie_id\")\n    private Long movieId;\n\n    // 电影名称\n    @Column(name = \"name\")\n    private String name;\n\n    // 主演\n    @Column(name = \"actors\")\n    private String actors;\n\n    // 电影图片封面\n    @Column(name = \"cover\")\n    private String cover;\n\n    // 导演\n    @Column(name = \"directors\")\n    private String directors;\n\n    // 类型\n    @Column(name = \"genres\")\n    private String genres;\n\n    //播放地址\n    @Column(name = \"official_site\")\n    private String officialSite;\n\n    //电影制片地\n    @Column(name = \"regions\")\n    private String regions;\n\n    //电影语言\n    @Column(name = \"languages\")\n    private String languages;\n\n    //片长\n    @Column(name = \"mins\")\n    private Integer mins;\n\n    //评分\n    @Column(name = \"score\")\n    private Float score;\n\n    //投票数\n    @Column(name = \"votes\")\n    private Integer votes;\n\n    //标签\n    @Column(name = \"tags\")\n    private String tags;\n\n    //电影描述\n    @Column(name = \"storyline\")\n    private String storyline;\n\n    //年份\n    @Column(name = \"year\")\n    private Integer year;\n\n    //演员\n    @Column(name = \"actor_ids\")\n    private String actorIds;\n\n    //导演\n    @Column(name = \"director_ids\")\n    private String directorIds;\n\n    // 电影上映时间\n    @Temporal(TemporalType.DATE)\n    @Column(name = \"release_date\")\n    private Date releaseDate;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/MovieTagEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport javax.persistence.*;\n\n@Data\n@NoArgsConstructor\n@Entity\n@Table(name = \"movie_tags\")\npublic class MovieTagEntity {\n    // 电影标签名称\n    @Id\n    @Column(name = \"movie_tag_id\")\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    @Column(name = \"name\")\n    private String name;\n\n    // 电影标签\n    @Column(name = \"value\")\n    private String value;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/PersonEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\nimport javax.persistence.*;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.sql.Timestamp;\n\n/**\n * 导演，演员\n * @author pqdong\n */\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@Entity\n@Table(name = \"person\")\npublic class PersonEntity {\n\n\n    @Id\n    @Column(name = \"person_id\")\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    // 演员名称\n    @Column(name = \"name\")\n    private String name;\n\n    // 演员性别\n    @Column(name = \"sex\")\n    private String sex;\n\n    // 演员英文名称\n    @Column(name = \"name_en\")\n    private String nameEn ;\n\n    // 演员中文名称\n    @Column(name = \"name_zn\")\n    private String nameZn ;\n\n    // 出生日期\n    @Column(name = \"birth\")\n    private String  birth;\n\n    // 出生地\n    @Column(name = \"birth_place\")\n    private String birthPlace;\n\n    // 星座\n    @Column(name = \"constellation\")\n    private String constellation;\n\n    // 职业\n    @Column(name = \"profession\")\n    private String profession;\n\n    // 简介\n    @Column(name = \"biography\")\n    private String biography;\n\n    // 头像\n    @Column(name = \"avatar\")\n    private String avatar;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/RatingEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\n/*\n* 评分\n* @author pqdong\n* @since 2020/04/06\n */\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.data.annotation.CreatedDate;\n\nimport javax.persistence.*;\nimport java.util.Date;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@Builder\n@Table(name = \"rating\")\n@Entity\npublic class RatingEntity {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"rating_id\")\n    private Long ratingId;\n\n    // 用户id\n    @Column(name = \"user_id\")\n    private Long userId;\n\n    // 电影id\n    @Column(name = \"movie_id\")\n    private Long movieId;\n\n    // 评分\n    @Column(name = \"rating\")\n    private Integer rating;\n\n    // 电影上映时间\n    @Temporal(TemporalType.DATE)\n    @Column(name = \"time\")\n    @CreatedDate\n    private Date releaseDate;\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/entity/UserEntity.java",
    "content": "package pqdong.movie.recommend.data.entity;\n\n/*\n* 用户表\n* @author pqdong\n* @since 2020/03/28\n */\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonNaming;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport javax.persistence.*;\nimport java.util.List;\n\n@Data\n@NoArgsConstructor\n@Entity\n@Table(name = \"user\")\npublic class UserEntity {\n\n    @Id\n    @Column(name = \"user_id\")\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    // 用户唯一标签，普通索引，md5值，长字符串为索引并不是很好\n    @Column(name = \"user_md\")\n    private String userMd;\n\n    // 用户昵称\n    @Column(name = \"user_nickname\")\n    private String username;\n\n    // 用户头像\n    @Column(name = \"user_avatar\")\n    private String userAvatar;\n\n    // 用户密码\n    @JsonIgnore\n    @Column(name = \"password\")\n    private String password;\n\n    // 用户标签\n    @Column(name = \"user_tags\")\n    private String userTags;\n\n    // 用户手机号\n    @Column(name = \"phone\")\n    private String phone;\n\n    // 个人宣言\n    @Column(name = \"motto\")\n    private String motto;\n\n    @Column(name = \"sex\")\n    private String sex;\n\n    public List<String> getFormatTag(){\n        return JSONObject.parseArray(this.userTags, String.class);\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/CommentEsRepo.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\nimport org.springframework.data.elasticsearch.repository.ElasticsearchRepository;\nimport org.springframework.stereotype.Repository;\nimport pqdong.movie.recommend.data.entity.CommentEs;\n\nimport java.util.List;\n\n@Repository\npublic interface CommentEsRepo extends ElasticsearchRepository<CommentEs, Long> {\n    List<CommentEs> findByMovieId(Long movieId);\n\n    List<CommentEs> findByUserMd(String userMd);\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/ConfigRepository.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data.repository.query.Param;\nimport pqdong.movie.recommend.data.entity.ConfigEntity;\n\n/**\n * @author pqdong\n * @description\n * @date 2020-03-02\n */\n\n\npublic interface ConfigRepository extends JpaRepository<ConfigEntity, Long> {\n    @Query(\"SELECT e FROM ConfigEntity e WHERE e.key = :keys\")\n    ConfigEntity findConfigByKey(@Param(\"keys\") String keys);\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/MovieRepository.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data.repository.query.Param;\nimport pqdong.movie.recommend.data.entity.MovieEntity;\nimport pqdong.movie.recommend.data.entity.PersonEntity;\n\nimport java.util.List;\n\n/**\n * @author pqdong\n * @description\n * @date 2020-03-02\n */\n\npublic interface MovieRepository extends JpaRepository<MovieEntity, Long> {\n    @Query(nativeQuery = true, value = \"select * from movie where 1=1 limit ?1\")\n    List<MovieEntity> findAllByCountLimit(@Param(\"num\") int num);\n\n    @Query(nativeQuery = true, value = \"SELECT * FROM movie WHERE name LIKE CONCAT('%',?1,'%') limit ?2\")\n    List<MovieEntity> findAllByName(@Param(\"keys\") String keys, @Param(\"total\") int total);\n\n    @Query(nativeQuery = true, value = \"SELECT * FROM movie WHERE tags LIKE CONCAT('%',?1,'%') limit ?2\")\n    List<MovieEntity> findAllByTag(@Param(\"keys\") String keys, @Param(\"total\") int total);\n\n    @Query(nativeQuery = true, value = \"SELECT * FROM movie WHERE 1=1 ORDER BY score DESC limit 12\")\n    List<MovieEntity> findAllByHighScore();\n\n    @Query(nativeQuery = true, value = \"SELECT * FROM movie WHERE actor_ids LIKE CONCAT('%',:keys,'%')\")\n    List<MovieEntity> findAllByPersonName(@Param(\"keys\") String keys);\n\n    @Query(\"SELECT e FROM MovieEntity e WHERE e.movieId = :movieId\")\n    MovieEntity findOneByMovieID(@Param(\"movieId\") Long movieId);\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/PersonRepository.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data.repository.query.Param;\nimport pqdong.movie.recommend.data.entity.PersonEntity;\nimport pqdong.movie.recommend.data.entity.UserEntity;\n\nimport java.util.List;\n\n/**\n * @author pqdong\n * @description\n * @date 2020-03-02\n */\n\npublic interface PersonRepository extends JpaRepository<PersonEntity, Long> {\n    @Query(nativeQuery = true, value = \"select * from person where 1=1 limit ?1\")\n    List<PersonEntity> findAllByCountLimit(@Param(\"num\") int num);\n\n    @Query(\"SELECT e FROM PersonEntity e WHERE e.name like :keys\")\n    List<PersonEntity> findAllByName(@Param(\"keys\") String keys);\n\n    @Query(\"SELECT e FROM PersonEntity e WHERE e.id = :personId\")\n    PersonEntity findOneByPersonID(@Param(\"personId\") long personId);\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/RatingRepository.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data.repository.query.Param;\nimport pqdong.movie.recommend.data.entity.MovieEntity;\nimport pqdong.movie.recommend.data.entity.RatingEntity;\n\nimport java.util.List;\n\n/**\n * @author pqdong\n * @description\n * @date 2020-03-02\n */\n\npublic interface RatingRepository extends JpaRepository<RatingEntity, Long> {\n\n    @Query(\"SELECT e FROM RatingEntity e WHERE e.ratingId = :ratingId\")\n    RatingEntity findOneByRatingID(@Param(\"ratingId\") Long ratingId);\n\n    List<RatingEntity> findAllByUserId(Long userId);\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/data/repository/UserRepository.java",
    "content": "package pqdong.movie.recommend.data.repository;\n\n\nimport org.apache.hadoop.hdfs.server.namenode.BlockPlacementPolicyWithNodeGroup;\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data.repository.query.Param;\nimport pqdong.movie.recommend.data.entity.UserEntity;\n\n/**\n * @author pqdong\n * @description\n * @date 2020-03-03\n */\n\npublic interface UserRepository extends JpaRepository<UserEntity, Long> {\n\n    @Query(\"SELECT e FROM UserEntity e WHERE e.userMd = :userMd\")\n    UserEntity findByUserMd(@Param(\"userMd\") String userMd);\n\n    @Query(\"SELECT e FROM UserEntity e WHERE e.username = :userNickName\")\n    UserEntity findByUserNickName(@Param(\"userNickName\") String userNickName);\n\n    @Query(\"SELECT e FROM UserEntity e WHERE e.id = :userId\")\n    UserEntity findOneByUserID(@Param(\"userId\") Long userId);\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/domain/service/AsyncTask.java",
    "content": "package pqdong.movie.recommend.domain.service;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Random;\n\n/**\n * AsyncTask\n *\n * @author pqdong\n * @description 作为一组领域对象，用以支持测试\n * @since 2020/04/03\n */\n\n@Component\n@Slf4j\npublic class AsyncTask{\n    private static Random random = new Random();\n\n    @Async(\"taskExecutor\")\n    public void doTaskOne() throws Exception {\n        log.info(\"开始做任务一\");\n        long start = System.currentTimeMillis();\n        try {\n            Thread.sleep(random.nextInt(100));\n        }catch (InterruptedException e){\n            Thread.sleep(200);\n        }\n        long end = System.currentTimeMillis();\n        log.info(\"完成任务一，耗时：\" + (end - start) + \"毫秒\");\n    }\n\n    @Async(\"taskExecutor\")\n    public void doTaskTwo() throws Exception {\n        log.info(\"开始做任务二\");\n        long start = System.currentTimeMillis();\n        try {\n            Thread.sleep(random.nextInt(100));\n        }catch (InterruptedException e){\n            Thread.sleep(200);\n        }\n        long end = System.currentTimeMillis();\n        log.info(\"完成任务二，耗时：\" + (end - start) + \"毫秒\");\n    }\n\n    @Async(\"taskExecutor\")\n    public void doTaskThree() throws Exception {\n        log.info(\"开始做任务三\");\n        long start = System.currentTimeMillis();\n        try {\n            Thread.sleep(random.nextInt(100));\n        }catch (InterruptedException e){\n            Thread.sleep(200);\n        }\n        long end = System.currentTimeMillis();\n        log.info(\"完成任务三，耗时：\" + (end - start) + \"毫秒\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/domain/service/MovieRecommender.java",
    "content": "package pqdong.movie.recommend.domain.service;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.mahout.cf.taste.common.TasteException;\nimport org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;\nimport org.apache.mahout.cf.taste.impl.recommender.CachingRecommender;\nimport org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender;\nimport org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;\nimport org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;\nimport org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity;\nimport org.apache.mahout.cf.taste.model.DataModel;\nimport org.apache.mahout.cf.taste.recommender.RecommendedItem;\nimport org.apache.mahout.cf.taste.recommender.Recommender;\nimport org.apache.mahout.cf.taste.similarity.ItemSimilarity;\nimport org.apache.mahout.cf.taste.similarity.UserSimilarity;\nimport org.springframework.stereotype.Component;\n\nimport javax.annotation.Resource;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Component\n@Slf4j\npublic class MovieRecommender {\n    private final static int NEIGHBORHOOD_NUM = 3;\n    @Resource(name = \"mySQLDataModel\")\n    private DataModel dataModel;\n\n    private List<Long> getRecommendedItemIDs(List<RecommendedItem> recommendations){\n        List<Long> recommendItems = new ArrayList<>();\n        for(int i = 0 ; i < recommendations.size() ; i++) {\n            RecommendedItem recommendedItem=recommendations.get(i);\n            recommendItems.add(recommendedItem.getItemID());\n        }\n        return recommendItems;\n    }\n\n    // 基于用户的推荐算法\n    public List<Long> userBasedRecommender(long userID,int size) throws TasteException {\n        UserSimilarity similarity  = new EuclideanDistanceSimilarity(dataModel );\n        NearestNUserNeighborhood  neighbor = new NearestNUserNeighborhood(NEIGHBORHOOD_NUM, similarity, dataModel );\n        Recommender recommender = new CachingRecommender(new GenericUserBasedRecommender(dataModel , neighbor, similarity));\n        List<RecommendedItem> recommendations = recommender.recommend(userID, size);\n        return getRecommendedItemIDs(recommendations);\n    }\n\n    // 基于内容的推荐算法\n    public List<Long> itemBasedRecommender(long userID,int size) throws TasteException {\n        ItemSimilarity itemSimilarity = new PearsonCorrelationSimilarity(dataModel);\n        Recommender recommender = new GenericItemBasedRecommender(dataModel, itemSimilarity);\n        List<RecommendedItem> recommendations = recommender.recommend(userID, size);\n        return getRecommendedItemIDs(recommendations);\n    }\n\n}\n\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/domain/util/ResponseMessage.java",
    "content": "package pqdong.movie.recommend.domain.util;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n\n/**\n * ResponseMessage\n *\n * @author pqdong\n * @since 2020/03/03\n */\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class ResponseMessage<T> {\n\n    private static final int SUCCESS_CODE = 0;\n\n    private static final int FAILED_CODE = 1;\n\n    private static final int PERMISSION_CODE = 2;\n\n    private static final int ILLEGAL_CODE = 3;\n\n    private static final int NEED_LOGIN_CODE = 10;\n\n    private static final int ENCRYPTED_CODE = 1000;\n\n    private int code;\n\n    private String msg;\n\n    private String description;\n\n    private T data;\n\n    public ResponseMessage(int code, String msg) {\n        this.code = code;\n        this.msg = msg;\n    }\n\n    public ResponseMessage(int code, String msg, String description) {\n        this.code = code;\n        this.msg = msg;\n        this.description = description;\n    }\n\n    public static <T> ResponseMessage<T> successMessage(T data) {\n        return new ResponseMessage<>(SUCCESS_CODE, \"success\", null, data);\n    }\n\n    public static ResponseMessage failedMessage(String message) {\n        return new ResponseMessage<>(FAILED_CODE, \"failed\", message, null);\n    }\n\n    public static <T> ResponseMessage<T> failedMessage(String message, T data) {\n        return new ResponseMessage<>(1, \"failed\", message, data);\n    }\n\n    public static <T> ResponseMessage<T> permissionMessage(T data) {\n        return new ResponseMessage<>(PERMISSION_CODE, \"permission denied\", null, data);\n    }\n\n    public static ResponseMessage illegalMessage(String message) {\n        return new ResponseMessage<>(ILLEGAL_CODE, \"illegal\", message, null);\n    }\n\n    public static ResponseMessage needLoginMessage() {\n        return new ResponseMessage<>(NEED_LOGIN_CODE, \"need login\");\n    }\n\n    public static ResponseMessage errorMessage(int code, String msg) {\n        return new ResponseMessage<>(code, \"error\", msg, null);\n    }\n\n    public boolean success() {\n        return this.code == SUCCESS_CODE;\n    }\n\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/exception/MyException.java",
    "content": "package pqdong.movie.recommend.exception;\n\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * MyException\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Slf4j\npublic class MyException extends RuntimeException {\n\n    @Getter\n    private Integer code;\n\n    public MyException(ResultEnum resultEnum) {\n        super(resultEnum.getMsg());\n        this.code = resultEnum.getCode();\n        log.warn(\"exception! {}\", resultEnum.getMsg());\n    }\n\n    public MyException(Integer code,  String msg) {\n        super(msg);\n        this.code = code;\n        log.warn(\"exception! {}\", msg);\n    }\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/exception/ResultEnum.java",
    "content": "package pqdong.movie.recommend.exception;\n\nimport lombok.Getter;\n\n/**\n * @author pqdong\n * @description\n * @date 2020/03/03\n */\npublic enum ResultEnum {\n    UNKNOW_ERROR(500, \"服务器错误\"),\n\n    // 1开头为用户有关的错误\n    NEED_LOGIN(1001, \"未登陆\"),\n\n    // 2开头为第三方接口的错误\n    SEND_NOTE_ERROR(2001, \"发送短信失败\"),\n    QINIU_ERROR(2002, \"七牛云接口出错\"),\n\n    SUCCESS(0, \"成功\");\n\n    @Getter\n    private Integer code;\n\n    @Getter\n    private String msg;\n\n    ResultEnum(Integer code, String msg) {\n        this.code = code;\n        this.msg = msg;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/redis/CacheConfig.java",
    "content": "\npackage pqdong.movie.recommend.redis;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.cache.annotation.CachingConfigurerSupport;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\n\n/**\n * CacheConfig\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Configuration\n@Slf4j\npublic class CacheConfig extends CachingConfigurerSupport {\n\n    @Bean\n    public StringRedisTemplate stringRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {\n        StringRedisTemplate template = new StringRedisTemplate();\n        template.setKeySerializer(new PrefixRedisSerializer());\n        template.setValueSerializer(new StringRedisSerializer());\n        template.setConnectionFactory(redisConnectionFactory);\n        template.setHashKeySerializer(new StringRedisSerializer());\n        template.setHashValueSerializer(new StringRedisSerializer());\n        template.afterPropertiesSet();\n        return template;\n    }\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/redis/PrefixRedisSerializer.java",
    "content": "\npackage pqdong.movie.recommend.redis;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.redis.serializer.RedisSerializer;\nimport org.springframework.util.Assert;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * PrefixRedisSerializer\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Slf4j\npublic class PrefixRedisSerializer implements RedisSerializer<String> {\n\n    private static String PREFIX = \"movie\";\n\n    private final Charset charset;\n\n    public PrefixRedisSerializer() {\n        this(StandardCharsets.UTF_8);\n    }\n\n    public PrefixRedisSerializer(Charset charset) {\n        Assert.notNull(charset, \"Charset must not be null!\");\n        this.charset = charset;\n    }\n\n    public static void setPrefix(String prefix) {\n        PREFIX = prefix;\n    }\n\n    @Override\n    public String deserialize(byte[] bytes) {\n        String saveKey = new String(bytes, charset);\n        int indexOf = saveKey.indexOf(PREFIX);\n        if (indexOf > 0) {\n            log.warn(\"key缺少前缀\");\n        } else {\n            saveKey = saveKey.substring(indexOf + PREFIX.length() + 1);\n        }\n        return saveKey;\n    }\n\n    @Override\n    public byte[] serialize(String string) {\n        String key = PREFIX + \":\" + string;\n        return key.getBytes(charset);\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/redis/RedisApi.java",
    "content": "/**\n * @(#)RedisService.java, 2019-03-12.\n *\n * Copyright 2019 Youdao, Inc. All rights reserved.\n * YOUDAO PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\n */\npackage pqdong.movie.recommend.redis;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.redis.core.*;\nimport org.springframework.data.redis.core.script.DefaultRedisScript;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.Resource;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * RedisService\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Service\n@Slf4j\npublic class RedisApi {\n\n    @Resource(name = \"stringRedisTemplate\")\n    private StringRedisTemplate redis;\n\n    public boolean exist(String key) {\n        try {\n            return Objects.requireNonNull(redis.hasKey(key));\n        } catch (Exception e) {\n            log.warn(\"redis exist error key={}\", key, e);\n            return false;\n        }\n    }\n\n    public void expire(String key, long time, TimeUnit timeUnit) {\n        try {\n            redis.expire(key, time, timeUnit);\n        } catch (Exception e) {\n            log.warn(\"redis expire error key={}\", key, e);\n        }\n    }\n\n    public void delKey(String key) {\n        try {\n            redis.delete(key);\n        } catch (Exception e) {\n            log.warn(\"redis delKey error key={}\", key, e);\n        }\n    }\n\n    public void delKeys(List<String> keys) {\n        try {\n            redis.delete(keys);\n        } catch (Exception e) {\n            log.warn(\"redis delKeys error keys={}\", keys, e);\n        }\n    }\n\n\n    public String getString(String key) {\n        try {\n            return redis.opsForValue().get(key);\n        } catch (Exception e) {\n            log.warn(\"redis getString error key={}\", key, e);\n            return null;\n        }\n    }\n\n    public void delHashKey(String key, String name) {\n        try {\n            HashOperations<String, String, String> opt = redis.opsForHash();\n            opt.delete(key, name);\n        } catch (Exception e) {\n            log.warn(\"redis delHashKey error key={}, name={}\", key, name, e);\n        }\n    }\n\n    public String hget(String key, String name) {\n        try {\n            HashOperations<String, String, String> opt = redis.opsForHash();\n            return opt.get(key, name);\n        } catch (Exception e) {\n            log.warn(\"redis hget error key={}, name={}\", key, name, e);\n            return null;\n        }\n    }\n\n    public Map<String, String> hgetAll(String key) {\n        try {\n            HashOperations<String, String, String> opt = redis.opsForHash();\n            return opt.entries(key);\n        } catch (Exception e) {\n            log.warn(\"redis hgetAll error key={}\", key, e);\n            return new HashMap<>();\n        }\n    }\n\n    public void hdel(String key, String field) {\n        try {\n            HashOperations<String, String, String> opt = redis.opsForHash();\n            opt.delete(key, field);\n        } catch (Exception e) {\n            log.warn(\"redis hdel error key={}, field={}\", key, field, e);\n        }\n    }\n\n    public List<String> hmget(String key, List<String> hashKeys) {\n        try {\n            HashOperations<String, String, String> opt = redis.opsForHash();\n            return opt.multiGet(key, hashKeys);\n        } catch (Exception e) {\n            log.warn(\"redis hmget error key={}, hashKeys={}\", key, hashKeys, e);\n        }\n        return Collections.emptyList();\n    }\n\n    public void hset(String key, String name, String value, long time, TimeUnit unit) {\n        try {\n            redis.executePipelined((RedisCallback) connection -> {\n                HashOperations<String, String, String> opt = redis.opsForHash();\n                connection.openPipeline();\n                opt.put(key, name, value);\n                redis.expire(key, time, unit);\n                connection.closePipeline();\n                return null;\n            });\n        } catch (Exception e) {\n            log.warn(\"redis hset error key={}, name={}, value={}\", key, name, value, e);\n        }\n    }\n\n    public void publish(String key, String message) {\n        try {\n            redis.convertAndSend(key, message);\n        } catch (Exception e) {\n            log.warn(\"redis publish error key={}, message={}\", key, message, e);\n        }\n    }\n\n\n    public void setValue(String key, String value, long time, TimeUnit unit) {\n        try {\n            redis.opsForValue().set(key, value, time, unit);\n        } catch (Exception e) {\n            log.warn(\"redis setValue error key={}, value={}\", key, value, e);\n        }\n    }\n\n    public void setValue(String key, String value, long time) {\n        try {\n            redis.executePipelined((RedisCallback) connection -> {\n                connection.openPipeline();\n                redis.opsForValue().set(key, value);\n                redis.expireAt(key, new Date(time));\n                connection.closePipeline();\n                return null;\n            });\n        } catch (Exception e) {\n            log.warn(\"redis setValue error key={}, value={}\", key, value, e);\n        }\n    }\n\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/redis/RedisKeys.java",
    "content": "package pqdong.movie.recommend.redis;\n\n/**\n * RedisKeys\n *\n * @author pqdong\n * @since 2020/03/04\n */\npublic class RedisKeys {\n\n    // user_token缓存\n    public static final String USER_TOKEN = \"user_token\";\n\n    // 高分电影缓存\n    public static final String HIGH_MOVIE = \"high_movie\";\n\n    // 评论防刷\n    public static final String BRUSH = \"brush\";\n\n    // 推荐电影\n    public static final String RECOMMEND = \"recommend\";\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/CommentService.java",
    "content": "package pqdong.movie.recommend.service;\n\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.dto.CommentSearchDto;\nimport pqdong.movie.recommend.data.entity.CommentEs;\nimport pqdong.movie.recommend.data.repository.CommentEsRepo;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.redis.RedisKeys;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\n\n/**\n * ConfigService\n *\n * @author pqdong\n * @since 2020/04/02\n */\n\n@Service\n@Slf4j\npublic class CommentService {\n\n    @Resource\n    private CommentEsRepo commentEsRepo;\n\n    @Resource\n    private RedisApi redisApi;\n\n    // 获取评论列表\n    public Map<String, Object> getCommentList(CommentSearchDto commentSearchDto) {\n        Pair<Integer, Integer> pair = RecommendUtils.getStartAndEnd(commentSearchDto.getPage(), commentSearchDto.getSize());\n        List<CommentEs> allComment = getComments(commentSearchDto);\n        List<CommentEs> commentList = allComment.subList(pair.getLeft(), pair.getRight() <= allComment.size() ? pair.getRight() : allComment.size());\n        Map<String, Object> result = new HashMap<>(2, 1);\n        result.put(\"total\", commentList.size());\n        result.put(\"commentList\", commentList.stream()\n                .peek(m -> {\n                    if (StringUtils.isEmpty(m.getUserAvatar())) {\n                        m.setUserAvatar(RecommendUtils.getRandomAvatar(m.getUserAvatar()));\n                    }\n                })\n                .collect(Collectors.toCollection(LinkedList::new)));\n        return result;\n    }\n\n    // 根据条件搜索\n    private List<CommentEs> getComments(CommentSearchDto commentSearchDto) {\n        if (commentSearchDto.getMovieId() != null && commentSearchDto.getMovieId() != 0) {\n            return commentEsRepo.findByMovieId(commentSearchDto.getMovieId()).stream()\n                    .sorted(Comparator.comparing(CommentEs::getCommentTime).reversed())\n                    .collect(Collectors.toCollection(LinkedList::new));\n        }else if (!StringUtils.isEmpty(commentSearchDto.getUserMd())) {\n            return commentEsRepo.findByUserMd(commentSearchDto.getUserMd()).stream()\n                    .sorted(Comparator.comparing(CommentEs::getCommentTime).reversed())\n                    .collect(Collectors.toCollection(LinkedList::new));\n        }\n        return new LinkedList<>();\n    }\n\n    // 将评论信息写入es index\n    public CommentEs submitComment(CommentEs commentEs) {\n        // times 用来做防刷爬虫攻击，限制每个用户短时间内的第五次提交\n        Integer times = Integer.valueOf(Optional\n                .ofNullable(redisApi.getString(RecommendUtils.getKey(RedisKeys.BRUSH, commentEs.getUserMd())))\n                .orElse(\"0\"));\n        if (times > 5){\n            return null;\n        } else {\n            times = times + 1;\n            redisApi.setValue(RecommendUtils.getKey(RedisKeys.BRUSH, commentEs.getUserMd()), times.toString(), 3, TimeUnit.MINUTES);\n        }\n        return commentEsRepo.save(commentEs);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/ConfigService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.repository.ConfigRepository;\n\nimport javax.annotation.Resource;\n\n/**\n * ConfigService\n *\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Service\npublic class ConfigService {\n\n    @Resource\n    private ConfigRepository configRepository;\n\n    // 获取七牛云，阿里云短信 access_token等配置信息\n    public String getConfigValue(String key){\n        return configRepository.findConfigByKey(key).getValue();\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/ElasticSearchService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport com.csvreader.CsvReader;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.domain.PageRequest;\nimport org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;\nimport org.springframework.data.elasticsearch.core.query.*;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.entity.CommentEs;\nimport pqdong.movie.recommend.data.entity.MovieEntity;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.data.repository.CommentEsRepo;\nimport pqdong.movie.recommend.data.repository.MovieRepository;\nimport pqdong.movie.recommend.data.repository.UserRepository;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.nio.charset.StandardCharsets;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;\n\n/**\n * ElasticSearchService\n *\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Service\n@Slf4j\npublic class ElasticSearchService {\n\n    @Resource\n    private ElasticsearchRestTemplate elasticsearchTemplate;\n\n    @Resource\n    private MovieRepository movieRepository;\n\n    @Resource\n    private UserRepository userRepository;\n\n    @Resource\n    private CommentEsRepo commentEsRepo;\n\n    public List<CommentEs> getAllIndex() {\n        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder().withQuery(matchAllQuery())\n                .withPageable(PageRequest.of(0, 100));\n        SearchQuery query = builder.build();\n        List<CommentEs> index = elasticsearchTemplate.queryForList(query, CommentEs.class);\n        log.info(\"{}\", index);\n        return index;\n    }\n\n    // 更新所有评论，异步处理\n    @Async(\"taskExecutor\")\n    public void updateAllComment(UserEntity userEntity) {\n        List<CommentEs> commentEs = commentEsRepo.findByUserMd(userEntity.getUserMd());\n        List queries = new ArrayList();\n        int counter = 0;\n        for (CommentEs comment : commentEs) {\n            comment.setUserAvatar(userEntity.getUserAvatar());\n            comment.setUserName(userEntity.getUsername());\n            IndexQuery indexQuery = new IndexQuery();\n            indexQuery.setId(Optional.ofNullable(comment.getCommentId())\n                    .orElse(System.currentTimeMillis())\n                    .toString());\n            try {\n                indexQuery.setSource(new ObjectMapper().writeValueAsString(comment));\n            } catch (JsonProcessingException e) {\n                log.info(\"{}\", e.getMessage());\n                continue;\n            }\n            indexQuery.setIndexName(\"comment\");\n            indexQuery.setType(\"comment\");\n            queries.add(indexQuery);\n            //分批提交修改\n            if (counter != 0 && counter % 1000 == 0) {\n                elasticsearchTemplate.bulkIndex(queries);\n                queries.clear();\n            }\n            counter++;\n        }\n        // 提交不足量修改\n        if (queries.size() > 0) {\n            elasticsearchTemplate.bulkIndex(queries);\n        }\n        if (counter > 0) {\n            elasticsearchTemplate.refresh(\"comment\");\n        }\n        log.info(\"commentEs has update\" + counter);\n    }\n\n    // 用于将csv文件中的数据导入到es表中，在处理用户昵称和电影名称时考虑到速度，不查询数据库，用现有数据代替\n    public long importCommentToEs() {\n        SimpleDateFormat dateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        try {\n            ArrayList<String[]> csvList = new ArrayList<String[]>();\n            CsvReader reader = new CsvReader(\"D:\\\\graduation\\\\data\\\\moviedata\\\\comments.csv\", ',', StandardCharsets.UTF_8);\n            reader.readHeaders(); //跳过表头,不跳可以注释掉\n\n            while (reader.readRecord()) {\n                csvList.add(reader.getValues()); //按行读取，并把每一行的数据添加到list集合\n            }\n            reader.close();\n\n            List queries = new ArrayList();\n            int counter = 0;\n            for (String[] comment : csvList) {\n                IndexQuery indexQuery = new IndexQuery();\n                indexQuery.setId(comment[0]);\n                indexQuery.setSource(new ObjectMapper().writeValueAsString(CommentEs.builder()\n                        .userAvatar(RecommendUtils.getRandomAvatar(comment[1]))\n                        .userMd(comment[1])\n                        .userName(comment[1])\n                        .commentTime(dateFormat.parse(comment[5]))\n                        .movieId(Long.valueOf(comment[2]))\n                        .content(comment[3])\n                        .movieName(comment[2])\n                        .votes(Long.valueOf(comment[4]))\n                        .build()));\n                indexQuery.setIndexName(\"comment\");\n                indexQuery.setType(\"comment\");\n                queries.add(indexQuery);\n                //分批提交修改\n                if (counter != 0 && counter % 1000 == 0) {\n                    elasticsearchTemplate.bulkIndex(queries);\n                    log.info(\"comment to es has update\");\n                    queries.clear();\n                }\n                counter++;\n            }\n            // 提交不足量修改\n            if (queries.size() > 0) {\n                elasticsearchTemplate.bulkIndex(queries);\n                log.info(\"comment to es has update\");\n            }\n            if (counter > 0) {\n                elasticsearchTemplate.refresh(\"comment\");\n                log.info(\"comment to es has refresh\");\n            }\n            log.info(\"commentEs has update\" + counter);\n            return counter;\n\n        } catch (Exception e) {\n            log.info(\"{}\", e.getMessage());\n        }\n        return 0;\n    }\n\n    // 对电影下的评论数据进行数据处理\n    public long updateCommentToEs(Long movieId) {\n        List<MovieEntity> movieEntities = new LinkedList<>();\n        if (movieId != null && movieId != 0) {\n            movieEntities.add(movieRepository.findOneByMovieID(movieId));\n        } else {\n            movieEntities = movieRepository.findAllByCountLimit(50);\n            movieEntities.addAll(movieRepository.findAllByHighScore());\n        }\n        List queries = new ArrayList();\n        int counter = 0;\n        for (MovieEntity movieEntity : movieEntities) {\n            List<CommentEs> commentEs = commentEsRepo.findByMovieId(movieEntity.getMovieId());\n            for (CommentEs comment : commentEs) {\n                UserEntity userEntity = userRepository.findByUserMd(comment.getUserMd());\n                if (userEntity == null) {\n                    if (comment.getContent().length()>5){\n                        comment.setUserName(comment.getContent().substring(0,5));\n                    }else{\n                        comment.setUserName(comment.getContent());\n                    }\n                } else{\n                    comment.setUserName(userEntity.getUsername());\n                }\n                comment.setMovieName(movieEntity.getName());\n                IndexQuery indexQuery = new IndexQuery();\n                indexQuery.setId(Optional.ofNullable(comment.getCommentId())\n                        .orElse(System.currentTimeMillis())\n                        .toString());\n                try {\n                    indexQuery.setSource(new ObjectMapper().writeValueAsString(comment));\n                } catch (JsonProcessingException e) {\n                    log.info(\"{}\", e.getMessage());\n                    continue;\n                }\n                indexQuery.setIndexName(\"comment\");\n                indexQuery.setType(\"comment\");\n                queries.add(indexQuery);\n                //分批提交修改\n                if (counter != 0 && counter % 1000 == 0) {\n                    elasticsearchTemplate.bulkIndex(queries);\n                    queries.clear();\n                }\n                counter++;\n            }\n        }\n        // 提交不足量修改\n        if (queries.size() > 0) {\n            elasticsearchTemplate.bulkIndex(queries);\n        }\n        if (counter > 0) {\n            elasticsearchTemplate.refresh(\"comment\");\n        }\n        log.info(\"commentEs has update\" + counter);\n        return counter;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/MovieService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.apache.mahout.cf.taste.common.TasteException;\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.constant.ServerConstant;\nimport pqdong.movie.recommend.data.dto.MovieSearchDto;\nimport pqdong.movie.recommend.data.dto.RatingDto;\nimport pqdong.movie.recommend.data.entity.MovieEntity;\nimport pqdong.movie.recommend.data.entity.RatingEntity;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.data.repository.MovieRepository;\nimport pqdong.movie.recommend.data.repository.RatingRepository;\nimport pqdong.movie.recommend.data.repository.UserRepository;\nimport pqdong.movie.recommend.domain.service.MovieRecommender;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.redis.RedisKeys;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\n\n/**\n * MovieService\n *\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Service\n@Slf4j\npublic class MovieService {\n\n    @Resource\n    private ConfigService configService;\n\n    @Resource\n    private MovieRepository movieRepository;\n\n    @Resource\n    private RatingRepository ratingRepository;\n\n    @Resource\n    private UserRepository userRepository;\n\n    @Resource\n    private RedisApi redisApi;\n\n    @Resource\n    private MovieRecommender movieRecommender;\n\n    private final static int RECOMMENT_SIZE = 4;\n\n\n    // 获取推荐电影\n    public List<MovieEntity> getRecommendMovie(UserEntity user) {\n        // 用户已经登录\n        List<MovieEntity> recommendMovies = new LinkedList<>();\n        String recommend = \"\";\n        if (user != null) {\n            // load缓存数据\n            recommend = redisApi.getString(RecommendUtils.getKey(RedisKeys.RECOMMEND, user.getUserMd()));\n            if (StringUtils.isEmpty(recommend)) {\n                // 用户打过分\n                if (!ratingRepository.findAllByUserId(user.getId()).isEmpty()){\n                    // 基于用户推荐\n                    try {\n                        List<Long> movieIds = movieRecommender.itemBasedRecommender(user.getId(), RECOMMENT_SIZE);\n                        recommendMovies.addAll(movieRepository.findAllById(movieIds));\n                    } catch (Exception e) {\n                        log.info(\"{}\",e);\n                    }\n                }\n            } else {\n                // 从缓存中直接加载\n                recommendMovies.addAll(JSONObject.parseArray(recommend, MovieEntity.class));\n            }\n        } else{\n            // 用户未登录，随机返回\n            recommendMovies.addAll(movieRepository.findAllByCountLimit(RECOMMENT_SIZE));\n        }\n        // 上述过程异常，或者用户未登录，直接根据标签查询数据库并推荐\n        if (recommendMovies.isEmpty() && user != null){\n            recommendMovies.addAll(movieRepository.findAllByTag(Optional.ofNullable(user.getFormatTag())\n                    .orElse(Collections.singletonList(user.getUserTags())).get(0), RECOMMENT_SIZE));\n        }\n        if (StringUtils.isEmpty(recommend)){\n            redisApi.setValue(RecommendUtils.getKey(RedisKeys.RECOMMEND, user.getUserMd()),JSONObject.toJSONString(recommendMovies),1,TimeUnit.DAYS );\n        }\n        return recommendMovies;\n    }\n\n    // 获取高分电影\n    public Map<String, Object> getHighMovie() {\n        String movieByRedis = redisApi.getString(RedisKeys.HIGH_MOVIE);\n        Map<String, Object> result = new HashMap<>(2, 1);\n        if (StringUtils.isEmpty(movieByRedis)) {\n            List<MovieEntity> allMovies = movieRepository.findAllByHighScore();\n            redisApi.setValue(RedisKeys.HIGH_MOVIE, JSONObject.toJSONString(allMovies), 1, TimeUnit.DAYS);\n            result.put(\"total\", allMovies.size());\n            result.put(\"movieList\", allMovies);\n        } else {\n            List<MovieEntity> allMovies = JSONObject.parseArray(movieByRedis, MovieEntity.class);\n            result.put(\"total\", allMovies.size());\n            result.put(\"movieList\", allMovies);\n        }\n        return result;\n    }\n\n    // 获取电影标签\n    public List<String> getMovieTags() {\n        return JSON.parseArray(configService.getConfigValue(\"MOVIE_TAG\"), String.class);\n    }\n\n    // 获取电影列表\n    public Map<String, Object> getAllMovie(String key, int page, int size) {\n        Pair<Integer, Integer> pair = RecommendUtils.getStartAndEnd(page, size);\n        List<MovieEntity> allMovie = getMovies(key, \"name\", page * size);\n        List<MovieEntity> movieList = allMovie.subList(pair.getLeft(), pair.getRight() <= allMovie.size() ? pair.getRight() : allMovie.size());\n        Map<String, Object> result = new HashMap<>(2, 1);\n        result.put(\"total\", movieList.size());\n        result.put(\"movieList\", movieList.stream().peek(m -> {\n            if (StringUtils.isEmpty(m.getCover())) {\n                m.setCover(ServerConstant.DefaultImg);\n            }\n        }).collect(Collectors.toCollection(LinkedList::new)));\n        return result;\n    }\n\n    // 根据条件搜索电影\n    public Map<String, Object> searchMovies(MovieSearchDto info) {\n        Pair<Integer, Integer> pair = RecommendUtils.getStartAndEnd(info.getPage(), info.getSize());\n        List<MovieEntity> allMovie = info.getTags().stream()\n                .map(t -> getMovies(t, \"tag\", info.getPage() * info.getSize()))\n                .flatMap(Collection::stream)\n                .collect(Collectors.toCollection(LinkedList::new));\n        if (!StringUtils.isEmpty(info.getContent())) {\n            allMovie.addAll(getMovies(info.getContent(), \"name\", info.getPage() * info.getSize()));\n        }\n        List<MovieEntity> movieList = allMovie.subList(pair.getLeft(), pair.getRight() <= allMovie.size() ? pair.getRight() : allMovie.size());\n        Map<String, Object> result = new HashMap<>(2, 1);\n        result.put(\"total\", movieList.size());\n        result.put(\"movieList\", movieList.stream().peek(m -> {\n            if (StringUtils.isEmpty(m.getCover())) {\n                m.setCover(ServerConstant.DefaultImg);\n            }\n        }).collect(Collectors.toCollection(LinkedList::new)));\n        return result;\n    }\n\n    // 根据电影名称等关键字获取电影列表\n    private List<MovieEntity> getMovies(String key, String type, int total) {\n        if (StringUtils.isBlank(key)) {\n            return movieRepository.findAllByCountLimit(total);\n        } else if (type.equals(\"name\")) {\n            return movieRepository.findAllByName(key, total);\n        } else {\n            return movieRepository.findAllByTag(key, total);\n        }\n    }\n\n    // 获取电影详情信息\n    public MovieEntity getMovie(Long movieId) {\n        return movieRepository.findOneByMovieID(movieId);\n    }\n\n    // 获取演员所出演的所有电影\n    public List<MovieEntity> getPersonAttendMovie(String personName) {\n        return movieRepository.findAllByPersonName(personName).stream().peek(g -> {\n            if (StringUtils.isEmpty(g.getCover())) {\n                g.setCover(ServerConstant.DefaultImg);\n            }\n        }).collect(Collectors.toCollection(LinkedList::new));\n    }\n\n    // 对电影打分\n    public MovieEntity updateScore(RatingDto ratingDto) {\n        UserEntity user = userRepository.findOneByUserID(ratingDto.getUserId());\n        MovieEntity movie = movieRepository.findOneByMovieID(ratingDto.getMovieId());\n        if (user == null){\n            return movie;\n        }\n        movie.setScore(ratingDto.getRating());\n        RatingEntity rating = RatingEntity.builder()\n                .movieId(ratingDto.getMovieId())\n                .rating(ratingDto.getRating().intValue())\n                .userId(ratingDto.getUserId())\n                .releaseDate(new Date())\n                .build();\n        ratingRepository.save(rating);\n        return movieRepository.save(movie);\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/PersonService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.constant.ServerConstant;\nimport pqdong.movie.recommend.data.entity.PersonEntity;\nimport pqdong.movie.recommend.data.repository.PersonRepository;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collector;\nimport java.util.stream.Collectors;\n\n/**\n * PersonService 演员相关\n *\n * @author pqdong\n * @since 2020/03/31\n */\n\n@Service\npublic class PersonService {\n\n    @Resource\n    private RedisApi redisApi;\n\n    @Resource\n    private PersonRepository personRepository;\n    /**\n     * @param key  查询关键字\n     * @param page 起始页码\n     * @param size 每页数据量\n     * @return 总数量和数据列表\n     * @method allPerson 获取\n     **/\n    public Map<String, Object> getAllPerson(String key, int page, int size) {\n        Pair<Integer, Integer> pair = RecommendUtils.getStartAndEnd(page, size);\n        List<PersonEntity> allPerson = getPersons(key, page*size);\n        List<PersonEntity> personList = allPerson.subList(pair.getLeft(), pair.getRight() <= allPerson.size() ? pair.getRight() : allPerson.size());\n        Map<String, Object> result = new HashMap<>(2, 1);\n        result.put(\"total\", personList.size());\n        result.put(\"personList\", personList.stream().peek(p -> {\n            if (StringUtils.isEmpty(p.getAvatar())){\n                p.setAvatar(ServerConstant.DefaultImg);\n            }\n        }).collect(Collectors.toCollection(LinkedList::new)));\n        return result;\n    }\n\n\n    // 根据演员名称关键字等获取演员列表\n    private List<PersonEntity> getPersons(String key, int total) {\n        List<PersonEntity> personList;\n        if (StringUtils.isBlank(key)) {\n            personList = personRepository.findAllByCountLimit(total);\n        } else {\n            personList = personRepository.findAllByName(key);\n        }\n        return personList;\n    }\n\n    // 获取导演，演员信息\n    public PersonEntity getPerson(Long personId){\n        return personRepository.findOneByPersonID(personId);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/QiNiuService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport com.qiniu.common.Zone;\nimport com.qiniu.http.Response;\nimport com.qiniu.storage.Configuration;\nimport com.qiniu.storage.UploadManager;\nimport com.qiniu.util.Auth;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.multipart.MultipartFile;\nimport pqdong.movie.recommend.exception.MyException;\nimport pqdong.movie.recommend.exception.ResultEnum;\n\n\n/**\n * QiNiuService\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Service\n@Slf4j\npublic class QiNiuService {\n\n    @Autowired\n    private ConfigService configService;\n    /**\n     * 七牛云\n     **/\n    private Auth auth = Auth.create(\"zfg7aGCs98DbKp_zKHzOAwzd6BoPhLjPkO5ohzEG\", \"l-fs00VcPP2nZRBKZmJj7LeSShi2wKxSMN5RL10w\");\n    Configuration cfg = new Configuration(Zone.huadong());\n    private UploadManager uploadManager = new UploadManager(cfg);\n\n    private String getUpToken() {\n        return auth.uploadToken(configService.getConfigValue(\"BUCKET_NAME\"));\n    }\n\n    public String uploadPicture(MultipartFile picture, String name) {\n        try {\n            Response response = uploadManager.put(picture.getBytes(), name, getUpToken());\n            if (response.isOK() && response.isJson()) {\n                return configService.getConfigValue(\"QINIU_IMAGE_DOMAIN\") + name;\n            }\n        } catch (Exception e) {\n            log.warn(\"upload picture error\", e);\n            throw new MyException(ResultEnum.QINIU_ERROR);\n        }\n        return configService.getConfigValue(\"QINIU_IMAGE_DOMAIN\") + name;\n    }\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/SmsService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport com.aliyuncs.DefaultAcsClient;\nimport com.aliyuncs.IAcsClient;\nimport com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;\nimport com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;\nimport com.aliyuncs.exceptions.ClientException;\nimport com.aliyuncs.http.MethodType;\nimport com.aliyuncs.profile.DefaultProfile;\nimport com.aliyuncs.profile.IClientProfile;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.springframework.stereotype.Service;\nimport pqdong.movie.recommend.data.constant.UserConstant;\nimport pqdong.movie.recommend.exception.MyException;\nimport pqdong.movie.recommend.exception.ResultEnum;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * SmsService\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Service\n@Slf4j\npublic class SmsService {\n\n    @Resource\n    private RedisApi redis;\n\n    @Resource\n    private ConfigService configService;\n\n    /**\n     * 阿里短信\n     **/\n\n    private IAcsClient getAcsClient() throws ClientException {\n        IClientProfile profile = DefaultProfile.getProfile(\"cn-hangzhou\", configService.getConfigValue(\"ACCESS_KEY_ID\"), configService.getConfigValue(\"ACCESS_KEY_SECRET\"));\n        DefaultProfile.addEndpoint(\"cn-hangzhou\", \"cn-hangzhou\", configService.getConfigValue(\"PRODUCT\"), configService.getConfigValue(\"DOMAIN\"));\n        return new DefaultAcsClient(profile);\n    }\n\n    public String sendCode(String phone) {\n        String randomCode = RandomStringUtils.randomNumeric(6);\n        if (sendSms(phone, randomCode)) {\n            // 有效期一天\n            redis.setValue(RecommendUtils.getKey(UserConstant.PHONE_CODE, phone), randomCode, 1, TimeUnit.DAYS);\n            return randomCode;\n        }\n        return null;\n    }\n\n    private boolean sendSms(String phoneNumber, String randomCode) {\n        SendSmsRequest request = new SendSmsRequest();\n        request.setMethod(MethodType.POST);\n        request.setPhoneNumbers(phoneNumber);\n        request.setSignName(\"花瓣电影\");\n        request.setTemplateCode(\"SMS_185231476\");\n        request.setTemplateParam(\"{\\\"code\\\":\\\"\" + randomCode + \"\\\"}\");\n        try {\n            SendSmsResponse sendSmsResponse = getAcsClient().getAcsResponse(request);\n            if (sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals(UserConstant.OK)) {\n                log.info(\"发送短信成功\");\n                return true;\n            }\n            log.error(sendSmsResponse.getCode());\n        } catch (ClientException e) {\n            log.error(\"ClientException异常：\" + e.getMessage());\n            throw new MyException(ResultEnum.SEND_NOTE_ERROR);\n        }\n        log.error(\"发送短信失败\");\n        return false;\n    }\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/service/UserService.java",
    "content": "package pqdong.movie.recommend.service;\n\nimport io.micrometer.core.instrument.util.StringUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.multipart.MultipartFile;\nimport pqdong.movie.recommend.data.constant.UserConstant;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.data.repository.UserRepository;\nimport pqdong.movie.recommend.data.dto.UserInfo;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.redis.RedisKeys;\nimport pqdong.movie.recommend.utils.Md5EncryptionHelper;\nimport pqdong.movie.recommend.utils.RecommendUtils;\n\nimport javax.annotation.Resource;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * userService\n *\n * @author pqdong\n * @since 2020/03/03\n */\n@Slf4j\n@Service\n\npublic class UserService {\n\n    @Resource\n    private UserRepository userRepository;\n\n    @Resource\n    private ElasticSearchService elasticSearchService;\n\n    @Resource\n    private QiNiuService qiNiuService;\n\n    @Resource\n    private RedisApi redisApi;\n\n    public UserEntity updateUser(UserEntity user){\n        // 如果用户名已经存在，不进行更新\n        UserEntity userSearch = userRepository.findByUserNickName(user.getUsername());\n        // nickname唯一\n        if (null != userSearch && !userSearch.getUserMd().equals(user.getUserMd())){\n            return null;\n        }\n        UserEntity userInfo = userRepository.findByUserMd(user.getUserMd());\n        user.setPassword(userInfo.getPassword());\n        UserEntity userEntity =  userRepository.save(user);\n        if (userEntity != null){\n            elasticSearchService.updateAllComment(userEntity);\n        }\n        return userEntity;\n    }\n\n    public String register(UserInfo user){\n        String code = redisApi.getString(RecommendUtils.getKey(UserConstant.PHONE_CODE, user.getPhone()));\n        if (StringUtils.isEmpty(code)){\n            return \"验证码错误\";\n        } else {\n          if (!code.equals(user.getCode())){\n              return \"验证码错误\";\n          }\n          UserEntity userEntity = userRepository.findByUserNickName(user.getUsername());\n          if (userEntity == null){\n              userEntity = new UserEntity();\n              try {\n                  BeanUtils.copyProperties(user,userEntity);\n              } catch (Exception e){\n                  log.error(\"copy properties error\");\n                  return \"注册失败\";\n              }\n              userEntity.setPassword(Md5EncryptionHelper.getMD5WithSalt(user.getPassword()));\n              userEntity.setUserMd(Md5EncryptionHelper.getMD5(Long.toString(System.currentTimeMillis())));\n              userEntity.setUserAvatar(RecommendUtils.getRandomAvatar(user.getUsername()));\n              userRepository.save(userEntity);\n              System.out.println(userEntity.toString());\n              return \"success\";\n          }else{\n              return \"用户已经存在\";\n          }\n        }\n    }\n\n    public UserEntity getUserInfo(String token) {\n        if (StringUtils.isEmpty(token)){\n            return null;\n        }\n        String userMd = redisApi.getString(RecommendUtils.getKey(RedisKeys.USER_TOKEN, token));\n        if (StringUtils.isEmpty(userMd)){\n            return null;\n        }\n        return userRepository.findByUserMd(userMd);\n    }\n\n    // 登录后需要前端设置header\n    public Map<String, Object> login(String userName, String password) {\n        UserEntity user = userRepository.findByUserNickName(userName);\n        if (user == null){\n            return null;\n        }\n        if (user.getPassword().equals(Md5EncryptionHelper.getMD5WithSalt(password))){\n            Map<String, Object> info = new HashMap<>();\n            String token = RecommendUtils.genToken();\n            redisApi.setValue(RecommendUtils.getKey(RedisKeys.USER_TOKEN, token) , user.getUserMd(), 7, TimeUnit.DAYS);\n            info.put(\"token\", token);\n            info.put(\"user\", user);\n            return info;\n        } else {\n            return null;\n        }\n    }\n\n    // 上传并设置用户头像\n    public String uploadAvatar(String userMd, MultipartFile avatar) {\n        String name = RecommendUtils.getKey(UserConstant.USER_AVATAR, userMd);\n        String url = qiNiuService.uploadPicture(avatar, name);\n        UserEntity entity = userRepository.findByUserMd(userMd);\n        if (entity == null) {\n            return \"用户不存在\";\n        }\n        entity.setUserAvatar(url);\n        UserEntity userEntity = userRepository.save(entity);\n        if (userEntity != null){\n            elasticSearchService.updateAllComment(userEntity);\n        }\n        return url;\n    }\n\n    // 退出\n    public boolean logout(){\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/utils/LoginInterceptor.java",
    "content": "package pqdong.movie.recommend.utils;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.handler.HandlerInterceptorAdapter;\nimport pqdong.movie.recommend.annotation.LoginRequired;\nimport pqdong.movie.recommend.data.constant.UserConstant;\nimport pqdong.movie.recommend.data.entity.UserEntity;\nimport pqdong.movie.recommend.data.repository.UserRepository;\nimport pqdong.movie.recommend.exception.MyException;\nimport pqdong.movie.recommend.exception.ResultEnum;\nimport pqdong.movie.recommend.redis.RedisApi;\nimport pqdong.movie.recommend.redis.RedisKeys;\n\nimport javax.annotation.PostConstruct;\nimport javax.annotation.Resource;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\n\n/**\n * LoginInterceptor\n *\n * @author pqdong\n * @since 2020/03/04\n */\n@Slf4j\n@Component\npublic class LoginInterceptor extends HandlerInterceptorAdapter {\n\n    private static LoginInterceptor loginInterceptor;\n\n    @Resource\n    private RedisApi redis;\n\n    @Resource\n    private UserRepository userRepository;\n\n    @PostConstruct\n    public void init() {\n        loginInterceptor = this;\n    }\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\n        if (!(handler instanceof HandlerMethod)) {\n            return true;\n        }\n        Method method = ((HandlerMethod) handler).getMethod();\n        // 判断需要调用需要登陆的接口时是否已经登陆\n        boolean isLoginRequired = isAnnotationPresent(method, LoginRequired.class);\n        if (isLoginRequired) {\n            String uri = request.getRequestURI();\n            String token = RecommendUtils.getToken(request);\n            if (StringUtils.isEmpty(token)){\n                throw new MyException(ResultEnum.NEED_LOGIN);\n            }\n            String userMd = loginInterceptor.redis.getString(RecommendUtils.getKey(RedisKeys.USER_TOKEN, token));\n            if (StringUtils.isEmpty(userMd)){\n                // 没有获取到redis中的信息\n                throw new MyException(ResultEnum.NEED_LOGIN);\n            }\n            UserEntity user = loginInterceptor.userRepository.findByUserMd(userMd);\n            if (user == null) {\n                // token无法获取到用户信息代表未登陆\n                throw new MyException(ResultEnum.NEED_LOGIN);\n            }\n            // 退出时删除缓存\n            if (uri.contains(UserConstant.LOGOUT)) {\n                loginInterceptor.redis.delKey(RecommendUtils.getKey(RedisKeys.USER_TOKEN, token));\n            }\n        }\n        return true;\n    }\n\n    private boolean isAnnotationPresent(Method method, Class<? extends Annotation> annotationClass) {\n        // 查找类注解或者方法注解\n        return method.getDeclaringClass().isAnnotationPresent(annotationClass) || method.isAnnotationPresent(annotationClass);\n    }\n\n}"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/utils/Md5EncryptionHelper.java",
    "content": "package pqdong.movie.recommend.utils;\n\nimport lombok.AccessLevel;\nimport lombok.NoArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\n/**\n * MD5加密工具类\n */\n@Slf4j\n@NoArgsConstructor(access = AccessLevel.PRIVATE)\npublic class Md5EncryptionHelper {\n\n    /**\n     * 获取MD5字符串\n     */\n    public static String getMD5(String content) {\n        try {\n            MessageDigest digest = MessageDigest.getInstance(\"MD5\");\n            digest.update(content.getBytes());\n            return getHashString(digest);\n        } catch (NoSuchAlgorithmException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    private static final String SALT = \"0fpqd5e5a88bebae640a5daaa7c84734\";\n\n    /**\n     * 获取加盐的MD5字符串\n     */\n    public static String getMD5WithSalt(String content) {\n        return getMD5(getMD5(content) + SALT);\n    }\n\n    private static String getHashString(MessageDigest digest) {\n        StringBuilder builder = new StringBuilder();\n        for (byte b : digest.digest()) {\n            builder.append(Integer.toHexString((b >> 4) & 0xf));\n            builder.append(Integer.toHexString(b & 0xf));\n        }\n        return builder.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/pqdong/movie/recommend/utils/RecommendUtils.java",
    "content": "package pqdong.movie.recommend.utils;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.google.common.base.Joiner;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.text.ParseException;\nimport java.text.ParsePosition;\nimport java.text.SimpleDateFormat;\nimport java.util.*;\nimport javax.servlet.http.HttpServletRequest;\nimport lombok.AccessLevel;\nimport lombok.NoArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.tuple.Pair;\nimport pqdong.movie.recommend.data.dto.UserInfo;\n\n/**\n * RecommendUtils\n * 工具类，单例模式\n * @author pqdong\n * @since 2020/03/03\n */\n@Slf4j\n@NoArgsConstructor(access = AccessLevel.PRIVATE)\npublic class RecommendUtils {\n\n    public static final String SPLIT = \":\";\n\n    public static final Joiner JOINER = Joiner.on(SPLIT);\n\n    public static UserInfo getUser(HttpServletRequest request) {\n        try {\n            String userHeader = request.getHeader(\"user\");\n            if (StringUtils.isNotEmpty(userHeader)) {\n                // 中文需要解码\n                String user = URLDecoder.decode(userHeader, \"UTF-8\");\n                if (StringUtils.isNotEmpty(user)) {\n                    return JSONObject.parseObject(user, UserInfo.class);\n                }\n            }\n        } catch (UnsupportedEncodingException e) {\n            log.warn(\"getUser decode error\");\n        }\n        return null;\n    }\n\n    public static String getUserMd(HttpServletRequest request) {\n        UserInfo user = getUser(request);\n        if (user == null) {\n            return null;\n        }\n        return user.getUserMd();\n    }\n\n    /**\n     * 生成token\n     **/\n    public static String genToken() {\n        return RandomStringUtils.randomAlphanumeric(10);\n    }\n    /**\n     * 获取token\n     */\n    public static String getToken(HttpServletRequest request) {\n        try {\n            String tokenHeader = request.getHeader(\"token\");\n            if (StringUtils.isNotEmpty(tokenHeader)) {\n                String token = URLDecoder.decode(tokenHeader, \"UTF-8\");\n                if (StringUtils.isNotEmpty(token)) {\n                    return token;\n                }\n            }\n        } catch (UnsupportedEncodingException e) {\n            log.warn(\"getToken decode error\");\n        }\n        return null;\n    }\n\n    public static String getKey(String... keys) {\n        return JOINER.join(keys);\n    }\n\n    private static Random random = new Random();\n\n    /**\n     * 生成随机过期时间，防止缓存雪崩\n     */\n    public static long genExpireTime(long time, int bound) {\n        return time + random.nextInt(bound);\n    }\n\n    public static long genExpireTime(long time) {\n        return genExpireTime(time, 3);\n    }\n\n\n    /**\n     * 对Map按值排序,reverse为true代表从大到小排序，反之从小到大\n     * Map的value必须实现Comparable，否则无法实现排序\n     **/\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> Map<K, V> sortByValue(Map<K, V> map, final boolean reverse) {\n        List<Map.Entry<K, V>> list = new LinkedList<>(map.entrySet());\n        list.sort((o1, o2) -> {\n            if (reverse) {\n                return ((Comparable<V>) o2.getValue())\n                        .compareTo(o1.getValue());\n            }\n            return ((Comparable<V>) o1.getValue())\n                    .compareTo(o2.getValue());\n        });\n        Map<K, V> result = new LinkedHashMap<>();\n        for (Map.Entry<K, V> entry : list) {\n            result.put(entry.getKey(), entry.getValue());\n        }\n        return result;\n    }\n\n    private static final String[] AVATARURLARRAY = new String[] {\n            \"http://oimagec6.ydstatic.com/image?id=-4541055657611236390&product=bisheng\",\n            \"http://oimageb2.ydstatic.com/image?id=8981431901149412470&product=bisheng\",\n            \"http://oimagea2.ydstatic.com/image?id=-6268572656325873060&product=bisheng\",\n            \"http://oimagea2.ydstatic.com/image?id=-38385107928742692&product=bisheng\",\n            \"http://oimageb4.ydstatic.com/image?id=3484504410139022595&product=bisheng\"\n    };\n\n    /**\n     * 获取随机头像\n     **/\n    public static String getRandomAvatar(String userId) {\n        int h = userId.hashCode();\n        h = h < 0 ? -h : h;\n        return AVATARURLARRAY[h % AVATARURLARRAY.length];\n    }\n\n    public static Pair<Integer, Integer> getStartAndEnd(int page, int size) {\n        int start = (page - 1) * size;\n        int end = start + size;\n        return Pair.of(start, end);\n    }\n\n    public static Date getFormatDate(Date time) throws ParseException {\n        SimpleDateFormat formatter = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        if (time == null){\n            time = new Date();\n        }\n        return formatter.parse(getFormatDateString(time));\n    }\n\n    public static String getFormatDateString(Date time) {\n        SimpleDateFormat formatter = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        return formatter.format(time);\n    }\n\n}"
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "server:\n  port: 10015\n  max-http-header-size: 8192\n  tomcat:\n    accesslog:\n      enabled: true\n      directory: access_log\n      prefix: access_log\n      pattern: '%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\" \"%{X-Real-IP}i\" %D %S \"%U\" %v'\n    basedir: ./\nspring:\n  application:\n    name: movie-recommend\n  datasource:\n    driver-class-name: com.mysql.jdbc.Driver # mysql驱动\n    username: yourname # 你的数据库用户名\n    password: yourpassword # 你的数据库密码，url中添加数据库地址和端口\n    url: jdbc:mysql://yourip:port/movie?useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false\n  redis:\n    timeout: 300s # redis超时时间，建议用docker起一个redis容器即可\n    host: 127.0.0.1\n    port: 6379\n    jedis:\n      pool:\n        max-idle: 8 #最大连接数\n        max-active: 20 # 最大连接数,-1为不限制\n        min-idle: 0 # 最小空闲数量\n        max-wait: 1s # 最大阻塞等待时间，-1为不限制\n  jpa:\n    database : MYSQL\n    show-sql : true\n    hibernate:\n      ddl-auto: update\n      naming:\n        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl\n        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl\n    properties:\n      hibernate:\n        show_sql: true\n        globally_quoted_identifiers: true\n  data:\n    elasticsearch:\n      repositories:\n        enabled: true\n  elasticsearch:\n    ip: yourip\n    rest:\n      uris: yourip:9200\n      username: yourname\n      password: yourpassword\n"
  },
  {
    "path": "src/test/java/pqdong/movie/recommond/BaseTest.java",
    "content": "/**\n * @(#)BaseTest.java, 2018-10-20.\n * <p>\n * Copyright 2018 Youdao, Inc. All rights reserved.\n * YOUDAO PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\n */\npackage pqdong.movie.recommond;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\nimport pqdong.movie.recommend.MovieRecommendApplication;\n\n/**\n * BaseTest\n * @author pqdong\n * @since 2020/04/03\n */\n@RunWith(SpringRunner.class)\n@SpringBootTest(classes = MovieRecommendApplication.class)\npublic class BaseTest{\n\n    @Test\n    public void test() {\n\n    }\n}"
  },
  {
    "path": "src/test/java/pqdong/movie/recommond/UtilsTest.java",
    "content": "package pqdong.movie.recommond;\n\nimport org.junit.Test;\nimport pqdong.movie.recommend.utils.Md5EncryptionHelper;\n\n\n/**\n * UtilsTest\n *\n * @author pqdong\n * @since 2020/03/03\n */\npublic class UtilsTest {\n\n    @Test\n    public void getMD5WithSalt() {\n        System.out.println(Md5EncryptionHelper.getMD5WithSalt(\"123456\"));\n    }\n}"
  },
  {
    "path": "src/test/java/pqdong/movie/recommond/service/AsyncTaskTest.java",
    "content": "package pqdong.movie.recommond.service;\n\nimport org.junit.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport pqdong.movie.recommend.domain.service.AsyncTask;\nimport pqdong.movie.recommond.BaseTest;\n\n\npublic class AsyncTaskTest extends BaseTest {\n    @Autowired\n    private AsyncTask asyncTask;\n\n    @Test\n    public void testAsyncTasks() throws Exception {\n        asyncTask.doTaskOne();\n        asyncTask.doTaskTwo();\n        asyncTask.doTaskThree();\n    }\n}\n"
  },
  {
    "path": "start.sh",
    "content": "#!/bin/sh\n#预设java参数，自定义java参数用容器启动时配置环境变量JAVA_OPTS来实现，相同参数后者会覆盖前者\nJAVA_DEFAULT_OPTS=\"\n     -javaagent:/skywalking-agent/skywalking-agent.jar\n     -Dskywalking.collector.backend_service=${skywalking_service}\n     -Dskywalking.agent.service_name=${HOSTNAME%-*-*}\n     -Xms1g\n     -Xmx3g\n     -XX:+UseG1GC\n     -XX:MaxGCPauseMillis=100\n     -XX:MetaspaceSize=128m\n     -XX:MaxMetaspaceSize=256m\n     -XX:+PrintTenuringDistribution\n     -XX:+PrintGCDetails\n     -XX:+PrintHeapAtGC\n     -XX:+PrintGCDateStamps\n     -XX:+PrintGCTimeStamps\n     -Xloggc:gc-%t.log\n     -Duser.timezone=GMT+08\"\n\n#启动服务\nexec java $JAVA_DEFAULT_OPTS $JAVA_OPTS -jar app.jar $JAVA_ARGS"
  }
]