Browse Source

【初始化】项目

hmxchen 1 tháng trước cách đây
mục cha
commit
92722bc4a4
100 tập tin đã thay đổi với 6165 bổ sung2 xóa
  1. 131 1
      .gitignore
  2. 12 0
      Dockerfile
  3. 165 1
      README.md
  4. BIN
      doc/swagger.png
  5. 316 0
      mvnw
  6. 188 0
      mvnw.cmd
  7. 143 0
      pom.xml
  8. 67 0
      sql/create_table.sql
  9. 52 0
      sql/post_es_mapping.json
  10. 27 0
      src/main/java/com/yupi/yuso/MainApplication.java
  11. 26 0
      src/main/java/com/yupi/yuso/annotation/AuthCheck.java
  12. 69 0
      src/main/java/com/yupi/yuso/aop/AuthInterceptor.java
  13. 56 0
      src/main/java/com/yupi/yuso/aop/LogInterceptor.java
  14. 35 0
      src/main/java/com/yupi/yuso/common/BaseResponse.java
  15. 21 0
      src/main/java/com/yupi/yuso/common/DeleteRequest.java
  16. 43 0
      src/main/java/com/yupi/yuso/common/ErrorCode.java
  17. 34 0
      src/main/java/com/yupi/yuso/common/PageRequest.java
  18. 52 0
      src/main/java/com/yupi/yuso/common/ResultUtils.java
  19. 28 0
      src/main/java/com/yupi/yuso/config/CorsConfig.java
  20. 53 0
      src/main/java/com/yupi/yuso/config/CosClientConfig.java
  21. 31 0
      src/main/java/com/yupi/yuso/config/JsonConfig.java
  22. 39 0
      src/main/java/com/yupi/yuso/config/Knife4jConfig.java
  23. 31 0
      src/main/java/com/yupi/yuso/config/MyBatisPlusConfig.java
  24. 51 0
      src/main/java/com/yupi/yuso/config/WxOpenConfig.java
  25. 21 0
      src/main/java/com/yupi/yuso/constant/CommonConstant.java
  26. 16 0
      src/main/java/com/yupi/yuso/constant/FileConstant.java
  27. 34 0
      src/main/java/com/yupi/yuso/constant/UserConstant.java
  28. 108 0
      src/main/java/com/yupi/yuso/controller/FileController.java
  29. 249 0
      src/main/java/com/yupi/yuso/controller/PostController.java
  30. 109 0
      src/main/java/com/yupi/yuso/controller/PostFavourController.java
  31. 56 0
      src/main/java/com/yupi/yuso/controller/PostThumbController.java
  32. 311 0
      src/main/java/com/yupi/yuso/controller/UserController.java
  33. 135 0
      src/main/java/com/yupi/yuso/controller/WxMpController.java
  34. 16 0
      src/main/java/com/yupi/yuso/esdao/PostEsDao.java
  35. 36 0
      src/main/java/com/yupi/yuso/exception/BusinessException.java
  36. 31 0
      src/main/java/com/yupi/yuso/exception/GlobalExceptionHandler.java
  37. 45 0
      src/main/java/com/yupi/yuso/exception/ThrowUtils.java
  38. 57 0
      src/main/java/com/yupi/yuso/job/cycle/IncSyncPostToEs.java
  39. 48 0
      src/main/java/com/yupi/yuso/job/once/FullSyncPostToEs.java
  40. 51 0
      src/main/java/com/yupi/yuso/manager/CosManager.java
  41. 35 0
      src/main/java/com/yupi/yuso/mapper/PostFavourMapper.java
  42. 25 0
      src/main/java/com/yupi/yuso/mapper/PostMapper.java
  43. 18 0
      src/main/java/com/yupi/yuso/mapper/PostThumbMapper.java
  44. 18 0
      src/main/java/com/yupi/yuso/mapper/UserMapper.java
  45. 21 0
      src/main/java/com/yupi/yuso/model/dto/file/UploadFileRequest.java
  46. 32 0
      src/main/java/com/yupi/yuso/model/dto/post/PostAddRequest.java
  47. 37 0
      src/main/java/com/yupi/yuso/model/dto/post/PostEditRequest.java
  48. 125 0
      src/main/java/com/yupi/yuso/model/dto/post/PostEsDTO.java
  49. 65 0
      src/main/java/com/yupi/yuso/model/dto/post/PostQueryRequest.java
  50. 37 0
      src/main/java/com/yupi/yuso/model/dto/post/PostUpdateRequest.java
  51. 21 0
      src/main/java/com/yupi/yuso/model/dto/postfavour/PostFavourAddRequest.java
  52. 30 0
      src/main/java/com/yupi/yuso/model/dto/postfavour/PostFavourQueryRequest.java
  53. 21 0
      src/main/java/com/yupi/yuso/model/dto/postthumb/PostThumbAddRequest.java
  54. 36 0
      src/main/java/com/yupi/yuso/model/dto/user/UserAddRequest.java
  55. 20 0
      src/main/java/com/yupi/yuso/model/dto/user/UserLoginRequest.java
  56. 48 0
      src/main/java/com/yupi/yuso/model/dto/user/UserQueryRequest.java
  57. 22 0
      src/main/java/com/yupi/yuso/model/dto/user/UserRegisterRequest.java
  58. 31 0
      src/main/java/com/yupi/yuso/model/dto/user/UserUpdateMyRequest.java
  59. 40 0
      src/main/java/com/yupi/yuso/model/dto/user/UserUpdateRequest.java
  60. 76 0
      src/main/java/com/yupi/yuso/model/entity/Post.java
  61. 49 0
      src/main/java/com/yupi/yuso/model/entity/PostFavour.java
  62. 49 0
      src/main/java/com/yupi/yuso/model/entity/PostThumb.java
  63. 86 0
      src/main/java/com/yupi/yuso/model/entity/User.java
  64. 61 0
      src/main/java/com/yupi/yuso/model/enums/FileUploadBizEnum.java
  65. 63 0
      src/main/java/com/yupi/yuso/model/enums/UserRoleEnum.java
  66. 52 0
      src/main/java/com/yupi/yuso/model/vo/LoginUserVO.java
  67. 118 0
      src/main/java/com/yupi/yuso/model/vo/PostVO.java
  68. 47 0
      src/main/java/com/yupi/yuso/model/vo/UserVO.java
  69. 47 0
      src/main/java/com/yupi/yuso/service/PostFavourService.java
  70. 60 0
      src/main/java/com/yupi/yuso/service/PostService.java
  71. 32 0
      src/main/java/com/yupi/yuso/service/PostThumbService.java
  72. 121 0
      src/main/java/com/yupi/yuso/service/UserService.java
  73. 116 0
      src/main/java/com/yupi/yuso/service/impl/PostFavourServiceImpl.java
  74. 316 0
      src/main/java/com/yupi/yuso/service/impl/PostServiceImpl.java
  75. 105 0
      src/main/java/com/yupi/yuso/service/impl/PostThumbServiceImpl.java
  76. 272 0
      src/main/java/com/yupi/yuso/service/impl/UserServiceImpl.java
  77. 55 0
      src/main/java/com/yupi/yuso/utils/NetUtils.java
  78. 57 0
      src/main/java/com/yupi/yuso/utils/SpringContextUtils.java
  79. 25 0
      src/main/java/com/yupi/yuso/utils/SqlUtils.java
  80. 16 0
      src/main/java/com/yupi/yuso/wxmp/WxMpConstant.java
  81. 61 0
      src/main/java/com/yupi/yuso/wxmp/WxMpMsgRouter.java
  82. 31 0
      src/main/java/com/yupi/yuso/wxmp/handler/EventHandler.java
  83. 30 0
      src/main/java/com/yupi/yuso/wxmp/handler/MessageHandler.java
  84. 31 0
      src/main/java/com/yupi/yuso/wxmp/handler/SubscribeHandler.java
  85. 34 0
      src/main/resources/META-INF/additional-spring-configuration-metadata.json
  86. 28 0
      src/main/resources/application-prod.yml
  87. 24 0
      src/main/resources/application-test.yml
  88. 89 0
      src/main/resources/application.yml
  89. 2 0
      src/main/resources/banner.txt
  90. 27 0
      src/main/resources/mapper/PostFavourMapper.xml
  91. 31 0
      src/main/resources/mapper/PostMapper.xml
  92. 19 0
      src/main/resources/mapper/PostThumbMapper.xml
  93. 24 0
      src/main/resources/mapper/UserMapper.xml
  94. BIN
      src/main/resources/test_excel.xlsx
  95. 25 0
      src/test/java/com/yupi/yuso/MainApplicationTests.java
  96. 83 0
      src/test/java/com/yupi/yuso/esdao/PostEsDaoTest.java
  97. 23 0
      src/test/java/com/yupi/yuso/manager/CosManagerTest.java
  98. 33 0
      src/test/java/com/yupi/yuso/mapper/PostFavourMapperTest.java
  99. 28 0
      src/test/java/com/yupi/yuso/mapper/PostMapperTest.java
  100. 44 0
      src/test/java/com/yupi/yuso/service/PostFavourServiceTest.java

+ 131 - 1
.gitignore

@@ -1,14 +1,144 @@
-# ---> Java
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+### Java template
+# Compiled class file
 *.class
 
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
 # Mobile Tools for Java (J2ME)
 .mtj.tmp/
 
 # Package Files #
 *.jar
 *.war
+*.nar
 *.ear
+*.zip
+*.tar.gz
+*.rar
 
 # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
 hs_err_pid*
 
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+

+ 12 - 0
Dockerfile

@@ -0,0 +1,12 @@
+FROM maven:3.8.1-jdk-8-slim as builder
+
+# Copy local code to the container image.
+WORKDIR /app
+COPY pom.xml .
+COPY src ./src
+
+# Build a release artifact.
+RUN mvn package -DskipTests
+
+# Run the web service on container startup.
+CMD ["java","-jar","/app/target/yuso-backend-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]

+ 165 - 1
README.md

@@ -1,2 +1,166 @@
-# sz-yhsc
+# 鱼皮聚合搜索平台后端
 
+> 作者:[程序员鱼皮](https://github.com/liyupi)
+> 
+> 仅分享于 [编程导航知识星球](https://yupi.icu)
+
+基于 Java SpringBoot 的项目初始模板,整合了常用框架和主流业务的示例代码。
+
+只需 1 分钟即可完成内容网站的后端!!!大家还可以在此基础上快速开发自己的项目。
+
+[toc]
+
+## 模板特点
+
+### 主流框架 & 特性
+
+- Spring Boot 2.7.x(贼新)
+- Spring MVC
+- MyBatis + MyBatis Plus 数据访问(开启分页)
+- Spring Boot 调试工具和项目处理器
+- Spring AOP 切面编程
+- Spring Scheduler 定时任务
+- Spring 事务注解
+
+### 数据存储
+
+- MySQL 数据库
+- Redis 内存数据库
+- Elasticsearch 搜索引擎
+- 腾讯云 COS 对象存储
+
+### 工具类
+
+- Easy Excel 表格处理
+- Hutool 工具库
+- Gson 解析库
+- Apache Commons Lang3 工具类
+- Lombok 注解
+
+### 业务特性
+
+- Spring Session Redis 分布式登录
+- 全局请求响应拦截器(记录日志)
+- 全局异常处理器
+- 自定义错误码
+- 封装通用响应类
+- Swagger + Knife4j 接口文档
+- 自定义权限注解 + 全局校验
+- 全局跨域处理
+- 长整数丢失精度解决
+- 多环境配置
+
+
+## 业务功能
+
+- 提供示例 SQL(用户、帖子、帖子点赞、帖子收藏表)
+- 用户登录、注册、注销、更新、检索、权限管理
+- 帖子创建、删除、编辑、更新、数据库检索、ES 灵活检索
+- 帖子点赞、取消点赞
+- 帖子收藏、取消收藏、检索已收藏帖子
+- 帖子全量同步 ES、增量同步 ES 定时任务
+- 支持微信开放平台登录
+- 支持微信公众号订阅、收发消息、设置菜单
+- 支持分业务的文件上传
+
+### 单元测试
+
+- JUnit5 单元测试
+- 示例单元测试类
+
+### 架构设计
+
+- 合理分层
+
+
+## 快速上手
+
+> 所有需要修改的地方鱼皮都标记了 `todo`,便于大家找到修改的位置~
+
+### MySQL 数据库
+
+1)修改 `application.yml` 的数据库配置为你自己的:
+
+```yml
+spring:
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:3306/my_db
+    username: root
+    password: 123456
+```
+
+2)执行 `sql/create_table.sql` 中的数据库语句,自动创建库表
+
+3)启动项目,访问 `http://localhost:8101/api/doc.html` 即可打开接口文档,不需要写前端就能在线调试接口了~
+
+![](doc/swagger.png)
+
+### Redis 分布式登录
+
+1)修改 `application.yml` 的 Redis 配置为你自己的:
+
+```yml
+spring:
+  redis:
+    database: 1
+    host: localhost
+    port: 6379
+    timeout: 5000
+    password: 123456
+```
+
+2)修改 `application.yml` 中的 session 存储方式:
+
+```yml
+spring:
+  session:
+    store-type: redis
+```
+
+3)移除 `MainApplication` 类开头 `@SpringBootApplication` 注解内的 exclude 参数:
+
+修改前:
+
+```java
+@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
+```
+
+修改后:
+
+
+```java
+@SpringBootApplication
+```
+
+### Elasticsearch 搜索引擎
+
+1)修改 `application.yml` 的 Elasticsearch 配置为你自己的:
+
+```yml
+spring:
+  elasticsearch:
+    uris: http://localhost:9200
+    username: root
+    password: 123456
+```
+
+2)复制 `sql/post_es_mapping.json` 文件中的内容,通过调用 Elasticsearch 的接口或者 Kibana Dev Tools 来创建索引(相当于数据库建表)
+
+```
+PUT post_v1
+{
+ 参数见 sql/post_es_mapping.json 文件
+}
+```
+
+这步不会操作的话需要补充下 Elasticsearch 的知识,或者自行百度一下~
+
+3)开启同步任务,将数据库的帖子同步到 Elasticsearch
+
+找到 job 目录下的 `FullSyncPostToEs` 和 `IncSyncPostToEs` 文件,取消掉 `@Component` 注解的注释,再次执行程序即可触发同步:
+
+```java
+// todo 取消注释开启任务
+//@Component
+```

BIN
doc/swagger.png


+ 316 - 0
mvnw

@@ -0,0 +1,316 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /usr/local/etc/mavenrc ] ; then
+    . /usr/local/etc/mavenrc
+  fi
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`\\unset -f command; \\command -v java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  $MAVEN_DEBUG_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" \
+  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 188 - 0
mvnw.cmd

@@ -0,0 +1,188 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+  %JVM_CONFIG_MAVEN_PROPS% ^
+  %MAVEN_OPTS% ^
+  %MAVEN_DEBUG_OPTS% ^
+  -classpath %WRAPPER_JAR% ^
+  "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%

+ 143 - 0
pom.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.2</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.yupi</groupId>
+    <artifactId>yuso-backend</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>yuso-backend</name>
+    <properties>
+        <java.version>1.8</java.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-freemarker</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.2.2</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.5.2</version>
+        </dependency>
+        <!-- redis -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-data-redis</artifactId>
+        </dependency>
+        <!-- elasticsearch-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.github.binarywang/wx-java-mp-spring-boot-starter -->
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>wx-java-mp-spring-boot-starter</artifactId>
+            <version>4.4.0</version>
+        </dependency>
+        <!-- https://doc.xiaominfo.com/knife4j/documentation/get_start.html-->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>3.0.3</version>
+        </dependency>
+        <!-- https://cloud.tencent.com/document/product/436/10199-->
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+            <version>5.6.89</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.9.1</version>
+        </dependency>
+        <!-- https://github.com/alibaba/easyexcel -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.1.1</version>
+        </dependency>
+        <!-- https://hutool.cn/docs/index.html#/-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 67 - 0
sql/create_table.sql

@@ -0,0 +1,67 @@
+# 建表脚本
+# @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+# @from <a href="https://yupi.icu">编程导航知识星球</a>
+
+-- 创建库
+create database if not exists my_db;
+
+-- 切换库
+use my_db;
+
+-- 用户表
+create table if not exists user
+(
+    id           bigint auto_increment comment 'id' primary key,
+    userAccount  varchar(256)                           not null comment '账号',
+    userPassword varchar(512)                           not null comment '密码',
+    unionId      varchar(256)                           null comment '微信开放平台id',
+    mpOpenId     varchar(256)                           null comment '公众号openId',
+    userName     varchar(256)                           null comment '用户昵称',
+    userAvatar   varchar(1024)                          null comment '用户头像',
+    userProfile  varchar(512)                           null comment '用户简介',
+    userRole     varchar(256) default 'user'            not null comment '用户角色:user/admin/ban',
+    createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
+    updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    isDelete     tinyint      default 0                 not null comment '是否删除',
+    index idx_unionId (unionId)
+) comment '用户' collate = utf8mb4_unicode_ci;
+
+-- 帖子表
+create table if not exists post
+(
+    id         bigint auto_increment comment 'id' primary key,
+    title      varchar(512)                       null comment '标题',
+    content    text                               null comment '内容',
+    tags       varchar(1024)                      null comment '标签列表(json 数组)',
+    thumbNum   int      default 0                 not null comment '点赞数',
+    favourNum  int      default 0                 not null comment '收藏数',
+    userId     bigint                             not null comment '创建用户 id',
+    createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
+    updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    isDelete   tinyint  default 0                 not null comment '是否删除',
+    index idx_userId (userId)
+) comment '帖子' collate = utf8mb4_unicode_ci;
+
+-- 帖子点赞表(硬删除)
+create table if not exists post_thumb
+(
+    id         bigint auto_increment comment 'id' primary key,
+    postId     bigint                             not null comment '帖子 id',
+    userId     bigint                             not null comment '创建用户 id',
+    createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
+    updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    index idx_postId (postId),
+    index idx_userId (userId)
+) comment '帖子点赞';
+
+-- 帖子收藏表(硬删除)
+create table if not exists post_favour
+(
+    id         bigint auto_increment comment 'id' primary key,
+    postId     bigint                             not null comment '帖子 id',
+    userId     bigint                             not null comment '创建用户 id',
+    createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
+    updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    index idx_postId (postId),
+    index idx_userId (userId)
+) comment '帖子收藏';

+ 52 - 0
sql/post_es_mapping.json

@@ -0,0 +1,52 @@
+{
+  "aliases": {
+    "post": {}
+  },
+  "mappings": {
+    "properties": {
+      "title": {
+        "type": "text",
+        "analyzer": "ik_max_word",
+        "search_analyzer": "ik_smart",
+        "fields": {
+          "keyword": {
+            "type": "keyword",
+            "ignore_above": 256
+          }
+        }
+      },
+      "content": {
+        "type": "text",
+        "analyzer": "ik_max_word",
+        "search_analyzer": "ik_smart",
+        "fields": {
+          "keyword": {
+            "type": "keyword",
+            "ignore_above": 256
+          }
+        }
+      },
+      "tags": {
+        "type": "keyword"
+      },
+      "thumbNum": {
+        "type": "long"
+      },
+      "favourNum": {
+        "type": "long"
+      },
+      "userId": {
+        "type": "keyword"
+      },
+      "createTime": {
+        "type": "date"
+      },
+      "updateTime": {
+        "type": "date"
+      },
+      "isDelete": {
+        "type": "keyword"
+      }
+    }
+  }
+}

+ 27 - 0
src/main/java/com/yupi/yuso/MainApplication.java

@@ -0,0 +1,27 @@
+package com.yupi.yuso;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 主类(项目启动入口)
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+// todo 如需开启 Redis,须移除 exclude 中的内容
+@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
+@MapperScan("com.yupi.yuso.mapper")
+@EnableScheduling
+@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
+public class MainApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(MainApplication.class, args);
+    }
+
+}

+ 26 - 0
src/main/java/com/yupi/yuso/annotation/AuthCheck.java

@@ -0,0 +1,26 @@
+package com.yupi.yuso.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 权限校验
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AuthCheck {
+
+    /**
+     * 必须有某个角色
+     *
+     * @return
+     */
+    String mustRole() default "";
+
+}
+

+ 69 - 0
src/main/java/com/yupi/yuso/aop/AuthInterceptor.java

@@ -0,0 +1,69 @@
+package com.yupi.yuso.aop;
+
+import com.yupi.yuso.annotation.AuthCheck;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.enums.UserRoleEnum;
+import com.yupi.yuso.service.UserService;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * 权限校验 AOP
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Aspect
+@Component
+public class AuthInterceptor {
+
+    @Resource
+    private UserService userService;
+
+    /**
+     * 执行拦截
+     *
+     * @param joinPoint
+     * @param authCheck
+     * @return
+     */
+    @Around("@annotation(authCheck)")
+    public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
+        String mustRole = authCheck.mustRole();
+        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
+        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+        // 当前登录用户
+        User loginUser = userService.getLoginUser(request);
+        // 必须有该权限才通过
+        if (StringUtils.isNotBlank(mustRole)) {
+            UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
+            if (mustUserRoleEnum == null) {
+                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
+            }
+            String userRole = loginUser.getUserRole();
+            // 如果被封号,直接拒绝
+            if (UserRoleEnum.BAN.equals(mustUserRoleEnum)) {
+                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
+            }
+            // 必须有管理员权限
+            if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
+                if (!mustRole.equals(userRole)) {
+                    throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
+                }
+            }
+        }
+        // 通过权限校验,放行
+        return joinPoint.proceed();
+    }
+}
+

+ 56 - 0
src/main/java/com/yupi/yuso/aop/LogInterceptor.java

@@ -0,0 +1,56 @@
+package com.yupi.yuso.aop;
+
+import java.util.UUID;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StopWatch;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * 请求响应日志 AOP
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@Aspect
+@Component
+@Slf4j
+public class LogInterceptor {
+
+    /**
+     * 执行拦截
+     */
+    @Around("execution(* com.yupi.yuso.controller.*.*(..))")
+    public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
+        // 计时
+        StopWatch stopWatch = new StopWatch();
+        stopWatch.start();
+        // 获取请求路径
+        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
+        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
+        // 生成请求唯一 id
+        String requestId = UUID.randomUUID().toString();
+        String url = httpServletRequest.getRequestURI();
+        // 获取请求参数
+        Object[] args = point.getArgs();
+        String reqParam = "[" + StringUtils.join(args, ", ") + "]";
+        // 输出请求日志
+        log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
+                httpServletRequest.getRemoteHost(), reqParam);
+        // 执行原方法
+        Object result = point.proceed();
+        // 输出响应日志
+        stopWatch.stop();
+        long totalTimeMillis = stopWatch.getTotalTimeMillis();
+        log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
+        return result;
+    }
+}
+

+ 35 - 0
src/main/java/com/yupi/yuso/common/BaseResponse.java

@@ -0,0 +1,35 @@
+package com.yupi.yuso.common;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 通用返回类
+ *
+ * @param <T>
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class BaseResponse<T> implements Serializable {
+
+    private int code;
+
+    private T data;
+
+    private String message;
+
+    public BaseResponse(int code, T data, String message) {
+        this.code = code;
+        this.data = data;
+        this.message = message;
+    }
+
+    public BaseResponse(int code, T data) {
+        this(code, data, "");
+    }
+
+    public BaseResponse(ErrorCode errorCode) {
+        this(errorCode.getCode(), null, errorCode.getMessage());
+    }
+}

+ 21 - 0
src/main/java/com/yupi/yuso/common/DeleteRequest.java

@@ -0,0 +1,21 @@
+package com.yupi.yuso.common;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 删除请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class DeleteRequest implements Serializable {
+
+    /**
+     * id
+     */
+    private Long id;
+
+    private static final long serialVersionUID = 1L;
+}

+ 43 - 0
src/main/java/com/yupi/yuso/common/ErrorCode.java

@@ -0,0 +1,43 @@
+package com.yupi.yuso.common;
+
+/**
+ * 自定义错误码
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public enum ErrorCode {
+
+    SUCCESS(0, "ok"),
+    PARAMS_ERROR(40000, "请求参数错误"),
+    NOT_LOGIN_ERROR(40100, "未登录"),
+    NO_AUTH_ERROR(40101, "无权限"),
+    NOT_FOUND_ERROR(40400, "请求数据不存在"),
+    FORBIDDEN_ERROR(40300, "禁止访问"),
+    SYSTEM_ERROR(50000, "系统内部异常"),
+    OPERATION_ERROR(50001, "操作失败");
+
+    /**
+     * 状态码
+     */
+    private final int code;
+
+    /**
+     * 信息
+     */
+    private final String message;
+
+    ErrorCode(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+}

+ 34 - 0
src/main/java/com/yupi/yuso/common/PageRequest.java

@@ -0,0 +1,34 @@
+package com.yupi.yuso.common;
+
+import com.yupi.yuso.constant.CommonConstant;
+import lombok.Data;
+
+/**
+ * 分页请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PageRequest {
+
+    /**
+     * 当前页号
+     */
+    private long current = 1;
+
+    /**
+     * 页面大小
+     */
+    private long pageSize = 10;
+
+    /**
+     * 排序字段
+     */
+    private String sortField;
+
+    /**
+     * 排序顺序(默认升序)
+     */
+    private String sortOrder = CommonConstant.SORT_ORDER_ASC;
+}

+ 52 - 0
src/main/java/com/yupi/yuso/common/ResultUtils.java

@@ -0,0 +1,52 @@
+package com.yupi.yuso.common;
+
+/**
+ * 返回工具类
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public class ResultUtils {
+
+    /**
+     * 成功
+     *
+     * @param data
+     * @param <T>
+     * @return
+     */
+    public static <T> BaseResponse<T> success(T data) {
+        return new BaseResponse<>(0, data, "ok");
+    }
+
+    /**
+     * 失败
+     *
+     * @param errorCode
+     * @return
+     */
+    public static BaseResponse error(ErrorCode errorCode) {
+        return new BaseResponse<>(errorCode);
+    }
+
+    /**
+     * 失败
+     *
+     * @param code
+     * @param message
+     * @return
+     */
+    public static BaseResponse error(int code, String message) {
+        return new BaseResponse(code, null, message);
+    }
+
+    /**
+     * 失败
+     *
+     * @param errorCode
+     * @return
+     */
+    public static BaseResponse error(ErrorCode errorCode, String message) {
+        return new BaseResponse(errorCode.getCode(), null, message);
+    }
+}

+ 28 - 0
src/main/java/com/yupi/yuso/config/CorsConfig.java

@@ -0,0 +1,28 @@
+package com.yupi.yuso.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 全局跨域配置
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        // 覆盖所有请求
+        registry.addMapping("/**")
+                // 允许发送 Cookie
+                .allowCredentials(true)
+                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowedHeaders("*")
+                .exposedHeaders("*");
+    }
+}

+ 53 - 0
src/main/java/com/yupi/yuso/config/CosClientConfig.java

@@ -0,0 +1,53 @@
+package com.yupi.yuso.config;
+
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.region.Region;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 腾讯云对象存储客户端
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Configuration
+@ConfigurationProperties(prefix = "cos.client")
+@Data
+public class CosClientConfig {
+
+    /**
+     * accessKey
+     */
+    private String accessKey;
+
+    /**
+     * secretKey
+     */
+    private String secretKey;
+
+    /**
+     * 区域
+     */
+    private String region;
+
+    /**
+     * 桶名
+     */
+    private String bucket;
+
+    @Bean
+    public COSClient cosClient() {
+        // 初始化用户身份信息(secretId, secretKey)
+        COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey);
+        // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
+        ClientConfig clientConfig = new ClientConfig(new Region(region));
+        // 生成cos客户端
+        return new COSClient(cred, clientConfig);
+    }
+}

+ 31 - 0
src/main/java/com/yupi/yuso/config/JsonConfig.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import org.springframework.boot.jackson.JsonComponent;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+/**
+ * Spring MVC Json 配置
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@JsonComponent
+public class JsonConfig {
+
+    /**
+     * 添加 Long 转 json 精度丢失的配置
+     */
+    @Bean
+    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
+        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
+        SimpleModule module = new SimpleModule();
+        module.addSerializer(Long.class, ToStringSerializer.instance);
+        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
+        objectMapper.registerModule(module);
+        return objectMapper;
+    }
+}

+ 39 - 0
src/main/java/com/yupi/yuso/config/Knife4jConfig.java

@@ -0,0 +1,39 @@
+package com.yupi.yuso.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * Knife4j 接口文档配置
+ * https://doc.xiaominfo.com/knife4j/documentation/get_start.html
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Configuration
+@EnableSwagger2
+@Profile({"dev", "test"})
+public class Knife4jConfig {
+
+    @Bean
+    public Docket defaultApi2() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .apiInfo(new ApiInfoBuilder()
+                        .title("接口文档")
+                        .description("yuso-backend")
+                        .version("1.0")
+                        .build())
+                .select()
+                // 指定 Controller 扫描包路径
+                .apis(RequestHandlerSelectors.basePackage("com.yupi.yuso.controller"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+}

+ 31 - 0
src/main/java/com/yupi/yuso/config/MyBatisPlusConfig.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MyBatis Plus 配置
+ *
+ * @author https://github.com/liyupi
+ */
+@Configuration
+@MapperScan("com.yupi.yuso.mapper")
+public class MyBatisPlusConfig {
+
+    /**
+     * 拦截器配置
+     *
+     * @return
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 分页插件
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 51 - 0
src/main/java/com/yupi/yuso/config/WxOpenConfig.java

@@ -0,0 +1,51 @@
+package com.yupi.yuso.config;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信开放平台配置
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Slf4j
+@Configuration
+@ConfigurationProperties(prefix = "wx.open")
+@Data
+public class WxOpenConfig {
+
+    private String appId;
+
+    private String appSecret;
+
+    private WxMpService wxMpService;
+
+    /**
+     * 单例模式(不用 @Bean 是为了防止和公众号的 service 冲突)
+     *
+     * @return
+     */
+    public WxMpService getWxMpService() {
+        if (wxMpService != null) {
+            return wxMpService;
+        }
+        synchronized (this) {
+            if (wxMpService != null) {
+                return wxMpService;
+            }
+            WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
+            config.setAppId(appId);
+            config.setSecret(appSecret);
+            WxMpService service = new WxMpServiceImpl();
+            service.setWxMpConfigStorage(config);
+            wxMpService = service;
+            return wxMpService;
+        }
+    }
+}

+ 21 - 0
src/main/java/com/yupi/yuso/constant/CommonConstant.java

@@ -0,0 +1,21 @@
+package com.yupi.yuso.constant;
+
+/**
+ * 通用常量
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface CommonConstant {
+
+    /**
+     * 升序
+     */
+    String SORT_ORDER_ASC = "ascend";
+
+    /**
+     * 降序
+     */
+    String SORT_ORDER_DESC = " descend";
+    
+}

+ 16 - 0
src/main/java/com/yupi/yuso/constant/FileConstant.java

@@ -0,0 +1,16 @@
+package com.yupi.yuso.constant;
+
+/**
+ * 文件常量
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface FileConstant {
+
+    /**
+     * COS 访问地址
+     * todo 需替换配置
+     */
+    String COS_HOST = "https://yupi.icu";
+}

+ 34 - 0
src/main/java/com/yupi/yuso/constant/UserConstant.java

@@ -0,0 +1,34 @@
+package com.yupi.yuso.constant;
+
+/**
+ * 用户常量
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface UserConstant {
+
+    /**
+     * 用户登录态键
+     */
+    String USER_LOGIN_STATE = "user_login";
+
+    //  region 权限
+
+    /**
+     * 默认角色
+     */
+    String DEFAULT_ROLE = "user";
+
+    /**
+     * 管理员角色
+     */
+    String ADMIN_ROLE = "admin";
+
+    /**
+     * 被封号
+     */
+    String BAN_ROLE = "ban";
+
+    // endregion
+}

+ 108 - 0
src/main/java/com/yupi/yuso/controller/FileController.java

@@ -0,0 +1,108 @@
+package com.yupi.yuso.controller;
+
+import cn.hutool.core.io.FileUtil;
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import com.yupi.yuso.constant.FileConstant;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.manager.CosManager;
+import com.yupi.yuso.model.dto.file.UploadFileRequest;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.enums.FileUploadBizEnum;
+import com.yupi.yuso.service.UserService;
+import java.io.File;
+import java.util.Arrays;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 文件接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestController
+@RequestMapping("/file")
+@Slf4j
+public class FileController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private CosManager cosManager;
+
+    /**
+     * 文件上传
+     *
+     * @param multipartFile
+     * @param uploadFileRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/upload")
+    public BaseResponse<String> uploadFile(@RequestPart("file") MultipartFile multipartFile,
+            UploadFileRequest uploadFileRequest, HttpServletRequest request) {
+        String biz = uploadFileRequest.getBiz();
+        FileUploadBizEnum fileUploadBizEnum = FileUploadBizEnum.getEnumByValue(biz);
+        if (fileUploadBizEnum == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        validFile(multipartFile, fileUploadBizEnum);
+        User loginUser = userService.getLoginUser(request);
+        // 文件目录:根据业务、用户来划分
+        String uuid = RandomStringUtils.randomAlphanumeric(8);
+        String filename = uuid + "-" + multipartFile.getOriginalFilename();
+        String filepath = String.format("/%s/%s/%s", fileUploadBizEnum.getValue(), loginUser.getId(), filename);
+        File file = null;
+        try {
+            // 上传文件
+            file = File.createTempFile(filepath, null);
+            multipartFile.transferTo(file);
+            cosManager.putObject(filepath, file);
+            // 返回可访问地址
+            return ResultUtils.success(FileConstant.COS_HOST + filepath);
+        } catch (Exception e) {
+            log.error("file upload error, filepath = " + filepath, e);
+            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
+        } finally {
+            if (file != null) {
+                // 删除临时文件
+                boolean delete = file.delete();
+                if (!delete) {
+                    log.error("file delete error, filepath = {}", filepath);
+                }
+            }
+        }
+    }
+
+    /**
+     * 校验文件
+     *
+     * @param multipartFile
+     * @param fileUploadBizEnum 业务类型
+     */
+    private void validFile(MultipartFile multipartFile, FileUploadBizEnum fileUploadBizEnum) {
+        // 文件大小
+        long fileSize = multipartFile.getSize();
+        // 文件后缀
+        String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());
+        final long ONE_M = 1024 * 1024L;
+        if (FileUploadBizEnum.USER_AVATAR.equals(fileUploadBizEnum)) {
+            if (fileSize > ONE_M) {
+                throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小不能超过 1M");
+            }
+            if (!Arrays.asList("jpeg", "jpg", "svg", "png", "webp").contains(fileSuffix)) {
+                throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件类型错误");
+            }
+        }
+    }
+}

+ 249 - 0
src/main/java/com/yupi/yuso/controller/PostController.java

@@ -0,0 +1,249 @@
+package com.yupi.yuso.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.google.gson.Gson;
+import com.yupi.yuso.annotation.AuthCheck;
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.DeleteRequest;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import com.yupi.yuso.constant.UserConstant;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.exception.ThrowUtils;
+import com.yupi.yuso.model.dto.post.PostAddRequest;
+import com.yupi.yuso.model.dto.post.PostEditRequest;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import com.yupi.yuso.model.dto.post.PostUpdateRequest;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.vo.PostVO;
+import com.yupi.yuso.service.PostService;
+import com.yupi.yuso.service.UserService;
+import java.util.List;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 帖子接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestController
+@RequestMapping("/post")
+@Slf4j
+public class PostController {
+
+    @Resource
+    private PostService postService;
+
+    @Resource
+    private UserService userService;
+
+    private final static Gson GSON = new Gson();
+
+    // region 增删改查
+
+    /**
+     * 创建
+     *
+     * @param postAddRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/add")
+    public BaseResponse<Long> addPost(@RequestBody PostAddRequest postAddRequest, HttpServletRequest request) {
+        if (postAddRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        Post post = new Post();
+        BeanUtils.copyProperties(postAddRequest, post);
+        List<String> tags = postAddRequest.getTags();
+        if (tags != null) {
+            post.setTags(GSON.toJson(tags));
+        }
+        postService.validPost(post, true);
+        User loginUser = userService.getLoginUser(request);
+        post.setUserId(loginUser.getId());
+        post.setFavourNum(0);
+        post.setThumbNum(0);
+        boolean result = postService.save(post);
+        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
+        long newPostId = post.getId();
+        return ResultUtils.success(newPostId);
+    }
+
+    /**
+     * 删除
+     *
+     * @param deleteRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/delete")
+    public BaseResponse<Boolean> deletePost(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
+        if (deleteRequest == null || deleteRequest.getId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User user = userService.getLoginUser(request);
+        long id = deleteRequest.getId();
+        // 判断是否存在
+        Post oldPost = postService.getById(id);
+        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
+        // 仅本人或管理员可删除
+        if (!oldPost.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
+            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
+        }
+        boolean b = postService.removeById(id);
+        return ResultUtils.success(b);
+    }
+
+    /**
+     * 更新(仅管理员)
+     *
+     * @param postUpdateRequest
+     * @return
+     */
+    @PostMapping("/update")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<Boolean> updatePost(@RequestBody PostUpdateRequest postUpdateRequest) {
+        if (postUpdateRequest == null || postUpdateRequest.getId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        Post post = new Post();
+        BeanUtils.copyProperties(postUpdateRequest, post);
+        List<String> tags = postUpdateRequest.getTags();
+        if (tags != null) {
+            post.setTags(GSON.toJson(tags));
+        }
+        // 参数校验
+        postService.validPost(post, false);
+        long id = postUpdateRequest.getId();
+        // 判断是否存在
+        Post oldPost = postService.getById(id);
+        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
+        boolean result = postService.updateById(post);
+        return ResultUtils.success(result);
+    }
+
+    /**
+     * 根据 id 获取
+     *
+     * @param id
+     * @return
+     */
+    @GetMapping("/get/vo")
+    public BaseResponse<PostVO> getPostVOById(long id, HttpServletRequest request) {
+        if (id <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        Post post = postService.getById(id);
+        if (post == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
+        }
+        return ResultUtils.success(postService.getPostVO(post, request));
+    }
+
+    /**
+     * 分页获取列表(封装类)
+     *
+     * @param postQueryRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/list/page/vo")
+    public BaseResponse<Page<PostVO>> listPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
+            HttpServletRequest request) {
+        long current = postQueryRequest.getCurrent();
+        long size = postQueryRequest.getPageSize();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
+        Page<Post> postPage = postService.page(new Page<>(current, size),
+                postService.getQueryWrapper(postQueryRequest));
+        return ResultUtils.success(postService.getPostVOPage(postPage, request));
+    }
+
+    /**
+     * 分页获取当前用户创建的资源列表
+     *
+     * @param postQueryRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/my/list/page/vo")
+    public BaseResponse<Page<PostVO>> listMyPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
+            HttpServletRequest request) {
+        if (postQueryRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User loginUser = userService.getLoginUser(request);
+        postQueryRequest.setUserId(loginUser.getId());
+        long current = postQueryRequest.getCurrent();
+        long size = postQueryRequest.getPageSize();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
+        Page<Post> postPage = postService.page(new Page<>(current, size),
+                postService.getQueryWrapper(postQueryRequest));
+        return ResultUtils.success(postService.getPostVOPage(postPage, request));
+    }
+
+    // endregion
+
+    /**
+     * 分页搜索(从 ES 查询,封装类)
+     *
+     * @param postQueryRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/search/page/vo")
+    public BaseResponse<Page<PostVO>> searchPostVOByPage(@RequestBody PostQueryRequest postQueryRequest,
+            HttpServletRequest request) {
+        long size = postQueryRequest.getPageSize();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
+        Page<Post> postPage = postService.searchFromEs(postQueryRequest);
+        return ResultUtils.success(postService.getPostVOPage(postPage, request));
+    }
+
+    /**
+     * 编辑(用户)
+     *
+     * @param postEditRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/edit")
+    public BaseResponse<Boolean> editPost(@RequestBody PostEditRequest postEditRequest, HttpServletRequest request) {
+        if (postEditRequest == null || postEditRequest.getId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        Post post = new Post();
+        BeanUtils.copyProperties(postEditRequest, post);
+        List<String> tags = postEditRequest.getTags();
+        if (tags != null) {
+            post.setTags(GSON.toJson(tags));
+        }
+        // 参数校验
+        postService.validPost(post, false);
+        User loginUser = userService.getLoginUser(request);
+        long id = postEditRequest.getId();
+        // 判断是否存在
+        Post oldPost = postService.getById(id);
+        ThrowUtils.throwIf(oldPost == null, ErrorCode.NOT_FOUND_ERROR);
+        // 仅本人或管理员可编辑
+        if (!oldPost.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
+            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
+        }
+        boolean result = postService.updateById(post);
+        return ResultUtils.success(result);
+    }
+
+}

+ 109 - 0
src/main/java/com/yupi/yuso/controller/PostFavourController.java

@@ -0,0 +1,109 @@
+package com.yupi.yuso.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.exception.ThrowUtils;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import com.yupi.yuso.model.dto.postfavour.PostFavourAddRequest;
+import com.yupi.yuso.model.dto.postfavour.PostFavourQueryRequest;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.vo.PostVO;
+import com.yupi.yuso.service.PostFavourService;
+import com.yupi.yuso.service.PostService;
+import com.yupi.yuso.service.UserService;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 帖子收藏接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestController
+@RequestMapping("/post_favour")
+@Slf4j
+public class PostFavourController {
+
+    @Resource
+    private PostFavourService postFavourService;
+
+    @Resource
+    private PostService postService;
+
+    @Resource
+    private UserService userService;
+
+    /**
+     * 收藏 / 取消收藏
+     *
+     * @param postFavourAddRequest
+     * @param request
+     * @return resultNum 收藏变化数
+     */
+    @PostMapping("/")
+    public BaseResponse<Integer> doPostFavour(@RequestBody PostFavourAddRequest postFavourAddRequest,
+            HttpServletRequest request) {
+        if (postFavourAddRequest == null || postFavourAddRequest.getPostId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        // 登录才能操作
+        final User loginUser = userService.getLoginUser(request);
+        long postId = postFavourAddRequest.getPostId();
+        int result = postFavourService.doPostFavour(postId, loginUser);
+        return ResultUtils.success(result);
+    }
+
+    /**
+     * 获取我收藏的帖子列表
+     *
+     * @param postQueryRequest
+     * @param request
+     */
+    @PostMapping("/my/list/page")
+    public BaseResponse<Page<PostVO>> listMyFavourPostByPage(@RequestBody PostQueryRequest postQueryRequest,
+            HttpServletRequest request) {
+        if (postQueryRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User loginUser = userService.getLoginUser(request);
+        long current = postQueryRequest.getCurrent();
+        long size = postQueryRequest.getPageSize();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
+        Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),
+                postService.getQueryWrapper(postQueryRequest), loginUser.getId());
+        return ResultUtils.success(postService.getPostVOPage(postPage, request));
+    }
+
+    /**
+     * 获取用户收藏的帖子列表
+     *
+     * @param postFavourQueryRequest
+     * @param request
+     */
+    @PostMapping("/list/page")
+    public BaseResponse<Page<PostVO>> listFavourPostByPage(@RequestBody PostFavourQueryRequest postFavourQueryRequest,
+            HttpServletRequest request) {
+        if (postFavourQueryRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        long current = postFavourQueryRequest.getCurrent();
+        long size = postFavourQueryRequest.getPageSize();
+        Long userId = postFavourQueryRequest.getUserId();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20 || userId == null, ErrorCode.PARAMS_ERROR);
+        Page<Post> postPage = postFavourService.listFavourPostByPage(new Page<>(current, size),
+                postService.getQueryWrapper(postFavourQueryRequest.getPostQueryRequest()), userId);
+        return ResultUtils.success(postService.getPostVOPage(postPage, request));
+    }
+}

+ 56 - 0
src/main/java/com/yupi/yuso/controller/PostThumbController.java

@@ -0,0 +1,56 @@
+package com.yupi.yuso.controller;
+
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.model.dto.postthumb.PostThumbAddRequest;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.service.PostThumbService;
+import com.yupi.yuso.service.UserService;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 帖子点赞接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestController
+@RequestMapping("/post_thumb")
+@Slf4j
+public class PostThumbController {
+
+    @Resource
+    private PostThumbService postThumbService;
+
+    @Resource
+    private UserService userService;
+
+    /**
+     * 点赞 / 取消点赞
+     *
+     * @param postThumbAddRequest
+     * @param request
+     * @return resultNum 本次点赞变化数
+     */
+    @PostMapping("/")
+    public BaseResponse<Integer> doThumb(@RequestBody PostThumbAddRequest postThumbAddRequest,
+            HttpServletRequest request) {
+        if (postThumbAddRequest == null || postThumbAddRequest.getPostId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        // 登录才能点赞
+        final User loginUser = userService.getLoginUser(request);
+        long postId = postThumbAddRequest.getPostId();
+        int result = postThumbService.doPostThumb(postId, loginUser);
+        return ResultUtils.success(result);
+    }
+
+}

+ 311 - 0
src/main/java/com/yupi/yuso/controller/UserController.java

@@ -0,0 +1,311 @@
+package com.yupi.yuso.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yupi.yuso.annotation.AuthCheck;
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.DeleteRequest;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import com.yupi.yuso.config.WxOpenConfig;
+import com.yupi.yuso.constant.UserConstant;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.exception.ThrowUtils;
+import com.yupi.yuso.model.dto.user.UserAddRequest;
+import com.yupi.yuso.model.dto.user.UserLoginRequest;
+import com.yupi.yuso.model.dto.user.UserQueryRequest;
+import com.yupi.yuso.model.dto.user.UserRegisterRequest;
+import com.yupi.yuso.model.dto.user.UserUpdateMyRequest;
+import com.yupi.yuso.model.dto.user.UserUpdateRequest;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.vo.LoginUserVO;
+import com.yupi.yuso.model.vo.UserVO;
+import com.yupi.yuso.service.UserService;
+import java.util.List;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
+import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
+import me.chanjar.weixin.mp.api.WxMpService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 用户接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestController
+@RequestMapping("/user")
+@Slf4j
+public class UserController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private WxOpenConfig wxOpenConfig;
+
+    // region 登录相关
+
+    /**
+     * 用户注册
+     *
+     * @param userRegisterRequest
+     * @return
+     */
+    @PostMapping("/register")
+    public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
+        if (userRegisterRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        String userAccount = userRegisterRequest.getUserAccount();
+        String userPassword = userRegisterRequest.getUserPassword();
+        String checkPassword = userRegisterRequest.getCheckPassword();
+        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
+            return null;
+        }
+        long result = userService.userRegister(userAccount, userPassword, checkPassword);
+        return ResultUtils.success(result);
+    }
+
+    /**
+     * 用户登录
+     *
+     * @param userLoginRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/login")
+    public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
+        if (userLoginRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        String userAccount = userLoginRequest.getUserAccount();
+        String userPassword = userLoginRequest.getUserPassword();
+        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);
+        return ResultUtils.success(loginUserVO);
+    }
+
+    /**
+     * 用户登录(微信开放平台)
+     */
+    @GetMapping("/login/wx_open")
+    public BaseResponse<LoginUserVO> userLoginByWxOpen(HttpServletRequest request, HttpServletResponse response,
+            @RequestParam("code") String code) {
+        WxOAuth2AccessToken accessToken;
+        try {
+            WxMpService wxService = wxOpenConfig.getWxMpService();
+            accessToken = wxService.getOAuth2Service().getAccessToken(code);
+            WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, code);
+            String unionId = userInfo.getUnionId();
+            String mpOpenId = userInfo.getOpenid();
+            if (StringUtils.isAnyBlank(unionId, mpOpenId)) {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
+            }
+            return ResultUtils.success(userService.userLoginByMpOpen(userInfo, request));
+        } catch (Exception e) {
+            log.error("userLoginByWxOpen error", e);
+            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
+        }
+    }
+
+    /**
+     * 用户注销
+     *
+     * @param request
+     * @return
+     */
+    @PostMapping("/logout")
+    public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
+        if (request == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        boolean result = userService.userLogout(request);
+        return ResultUtils.success(result);
+    }
+
+    /**
+     * 获取当前登录用户
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/get/login")
+    public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {
+        User user = userService.getLoginUser(request);
+        return ResultUtils.success(userService.getLoginUserVO(user));
+    }
+
+    // endregion
+
+    // region 增删改查
+
+    /**
+     * 创建用户
+     *
+     * @param userAddRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/add")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) {
+        if (userAddRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User user = new User();
+        BeanUtils.copyProperties(userAddRequest, user);
+        boolean result = userService.save(user);
+        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
+        return ResultUtils.success(user.getId());
+    }
+
+    /**
+     * 删除用户
+     *
+     * @param deleteRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/delete")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
+        if (deleteRequest == null || deleteRequest.getId() <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        boolean b = userService.removeById(deleteRequest.getId());
+        return ResultUtils.success(b);
+    }
+
+    /**
+     * 更新用户
+     *
+     * @param userUpdateRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/update")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest,
+            HttpServletRequest request) {
+        if (userUpdateRequest == null || userUpdateRequest.getId() == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User user = new User();
+        BeanUtils.copyProperties(userUpdateRequest, user);
+        boolean result = userService.updateById(user);
+        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
+        return ResultUtils.success(true);
+    }
+
+    /**
+     * 根据 id 获取用户(仅管理员)
+     *
+     * @param id
+     * @param request
+     * @return
+     */
+    @GetMapping("/get")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<User> getUserById(long id, HttpServletRequest request) {
+        if (id <= 0) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User user = userService.getById(id);
+        ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);
+        return ResultUtils.success(user);
+    }
+
+    /**
+     * 根据 id 获取包装类
+     *
+     * @param id
+     * @param request
+     * @return
+     */
+    @GetMapping("/get/vo")
+    public BaseResponse<UserVO> getUserVOById(long id, HttpServletRequest request) {
+        BaseResponse<User> response = getUserById(id, request);
+        User user = response.getData();
+        return ResultUtils.success(userService.getUserVO(user));
+    }
+
+    /**
+     * 分页获取用户列表(仅管理员)
+     *
+     * @param userQueryRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/list/page")
+    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
+    public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest,
+            HttpServletRequest request) {
+        long current = userQueryRequest.getCurrent();
+        long size = userQueryRequest.getPageSize();
+        Page<User> userPage = userService.page(new Page<>(current, size),
+                userService.getQueryWrapper(userQueryRequest));
+        return ResultUtils.success(userPage);
+    }
+
+    /**
+     * 分页获取用户封装列表
+     *
+     * @param userQueryRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/list/page/vo")
+    public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest,
+            HttpServletRequest request) {
+        if (userQueryRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        long current = userQueryRequest.getCurrent();
+        long size = userQueryRequest.getPageSize();
+        // 限制爬虫
+        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
+        Page<User> userPage = userService.page(new Page<>(current, size),
+                userService.getQueryWrapper(userQueryRequest));
+        Page<UserVO> userVOPage = new Page<>(current, size, userPage.getTotal());
+        List<UserVO> userVO = userService.getUserVO(userPage.getRecords());
+        userVOPage.setRecords(userVO);
+        return ResultUtils.success(userVOPage);
+    }
+
+    // endregion
+
+    /**
+     * 更新个人信息
+     *
+     * @param userUpdateMyRequest
+     * @param request
+     * @return
+     */
+    @PostMapping("/update/my")
+    public BaseResponse<Boolean> updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest,
+            HttpServletRequest request) {
+        if (userUpdateMyRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        User loginUser = userService.getLoginUser(request);
+        User user = new User();
+        BeanUtils.copyProperties(userUpdateMyRequest, user);
+        user.setId(loginUser.getId());
+        boolean result = userService.updateById(user);
+        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
+        return ResultUtils.success(true);
+    }
+}

+ 135 - 0
src/main/java/com/yupi/yuso/controller/WxMpController.java

@@ -0,0 +1,135 @@
+package com.yupi.yuso.controller;
+
+import com.yupi.yuso.wxmp.WxMpConstant;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
+import me.chanjar.weixin.common.bean.menu.WxMenu;
+import me.chanjar.weixin.common.bean.menu.WxMenuButton;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 微信公众号相关接口
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@RestController
+@RequestMapping("/")
+@Slf4j
+public class WxMpController {
+
+    @Resource
+    private WxMpService wxMpService;
+
+    @Resource
+    private WxMpMessageRouter router;
+
+    @PostMapping("/")
+    public void receiveMessage(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        response.setContentType("text/html;charset=utf-8");
+        response.setStatus(HttpServletResponse.SC_OK);
+        // 校验消息签名,判断是否为公众平台发的消息
+        String signature = request.getParameter("signature");
+        String nonce = request.getParameter("nonce");
+        String timestamp = request.getParameter("timestamp");
+        if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
+            response.getWriter().println("非法请求");
+        }
+        // 加密类型
+        String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ? "raw"
+                : request.getParameter("encrypt_type");
+        // 明文消息
+        if ("raw".equals(encryptType)) {
+            return;
+        }
+        // aes 加密消息
+        if ("aes".equals(encryptType)) {
+            // 解密消息
+            String msgSignature = request.getParameter("msg_signature");
+            WxMpXmlMessage inMessage = WxMpXmlMessage
+                    .fromEncryptedXml(request.getInputStream(), wxMpService.getWxMpConfigStorage(), timestamp,
+                            nonce,
+                            msgSignature);
+            log.info("message content = {}", inMessage.getContent());
+            // 路由消息并处理
+            WxMpXmlOutMessage outMessage = router.route(inMessage);
+            if (outMessage == null) {
+                response.getWriter().write("");
+            } else {
+                response.getWriter().write(outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage()));
+            }
+            return;
+        }
+        response.getWriter().println("不可识别的加密类型");
+    }
+
+    @GetMapping("/")
+    public String check(String timestamp, String nonce, String signature, String echostr) {
+        log.info("check");
+        if (wxMpService.checkSignature(timestamp, nonce, signature)) {
+            return echostr;
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * 设置公众号菜单
+     *
+     * @return
+     * @throws WxErrorException
+     */
+    @GetMapping("/setMenu")
+    public String setMenu() throws WxErrorException {
+        log.info("setMenu");
+        WxMenu wxMenu = new WxMenu();
+        // 菜单一
+        WxMenuButton wxMenuButton1 = new WxMenuButton();
+        wxMenuButton1.setType(MenuButtonType.VIEW);
+        wxMenuButton1.setName("主菜单一");
+        // 子菜单
+        WxMenuButton wxMenuButton1SubButton1 = new WxMenuButton();
+        wxMenuButton1SubButton1.setType(MenuButtonType.VIEW);
+        wxMenuButton1SubButton1.setName("跳转页面");
+        wxMenuButton1SubButton1.setUrl(
+                "https://yupi.icu");
+        wxMenuButton1.setSubButtons(Collections.singletonList(wxMenuButton1SubButton1));
+
+        // 菜单二
+        WxMenuButton wxMenuButton2 = new WxMenuButton();
+        wxMenuButton2.setType(MenuButtonType.CLICK);
+        wxMenuButton2.setName("点击事件");
+        wxMenuButton2.setKey(WxMpConstant.CLICK_MENU_KEY);
+
+        // 菜单三
+        WxMenuButton wxMenuButton3 = new WxMenuButton();
+        wxMenuButton3.setType(MenuButtonType.VIEW);
+        wxMenuButton3.setName("主菜单三");
+        WxMenuButton wxMenuButton3SubButton1 = new WxMenuButton();
+        wxMenuButton3SubButton1.setType(MenuButtonType.VIEW);
+        wxMenuButton3SubButton1.setName("编程学习");
+        wxMenuButton3SubButton1.setUrl("https://yupi.icu");
+        wxMenuButton3.setSubButtons(Collections.singletonList(wxMenuButton3SubButton1));
+
+        // 设置主菜单
+        wxMenu.setButtons(Arrays.asList(wxMenuButton1, wxMenuButton2, wxMenuButton3));
+        wxMpService.getMenuService().menuCreate(wxMenu);
+        return "ok";
+    }
+}

+ 16 - 0
src/main/java/com/yupi/yuso/esdao/PostEsDao.java

@@ -0,0 +1,16 @@
+package com.yupi.yuso.esdao;
+
+import com.yupi.yuso.model.dto.post.PostEsDTO;
+import java.util.List;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+
+/**
+ * 帖子 ES 操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> {
+
+    List<PostEsDTO> findByUserId(Long userId);
+}

+ 36 - 0
src/main/java/com/yupi/yuso/exception/BusinessException.java

@@ -0,0 +1,36 @@
+package com.yupi.yuso.exception;
+
+import com.yupi.yuso.common.ErrorCode;
+
+/**
+ * 自定义异常类
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public class BusinessException extends RuntimeException {
+
+    /**
+     * 错误码
+     */
+    private final int code;
+
+    public BusinessException(int code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public BusinessException(ErrorCode errorCode) {
+        super(errorCode.getMessage());
+        this.code = errorCode.getCode();
+    }
+
+    public BusinessException(ErrorCode errorCode, String message) {
+        super(message);
+        this.code = errorCode.getCode();
+    }
+
+    public int getCode() {
+        return code;
+    }
+}

+ 31 - 0
src/main/java/com/yupi/yuso/exception/GlobalExceptionHandler.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.exception;
+
+import com.yupi.yuso.common.BaseResponse;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.common.ResultUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 全局异常处理器
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BusinessException.class)
+    public BaseResponse<?> businessExceptionHandler(BusinessException e) {
+        log.error("BusinessException", e);
+        return ResultUtils.error(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(RuntimeException.class)
+    public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
+        log.error("RuntimeException", e);
+        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
+    }
+}

+ 45 - 0
src/main/java/com/yupi/yuso/exception/ThrowUtils.java

@@ -0,0 +1,45 @@
+package com.yupi.yuso.exception;
+
+import com.yupi.yuso.common.ErrorCode;
+
+/**
+ * 抛异常工具类
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public class ThrowUtils {
+
+    /**
+     * 条件成立则抛异常
+     *
+     * @param condition
+     * @param runtimeException
+     */
+    public static void throwIf(boolean condition, RuntimeException runtimeException) {
+        if (condition) {
+            throw runtimeException;
+        }
+    }
+
+    /**
+     * 条件成立则抛异常
+     *
+     * @param condition
+     * @param errorCode
+     */
+    public static void throwIf(boolean condition, ErrorCode errorCode) {
+        throwIf(condition, new BusinessException(errorCode));
+    }
+
+    /**
+     * 条件成立则抛异常
+     *
+     * @param condition
+     * @param errorCode
+     * @param message
+     */
+    public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
+        throwIf(condition, new BusinessException(errorCode, message));
+    }
+}

+ 57 - 0
src/main/java/com/yupi/yuso/job/cycle/IncSyncPostToEs.java

@@ -0,0 +1,57 @@
+package com.yupi.yuso.job.cycle;
+
+import com.yupi.yuso.esdao.PostEsDao;
+import com.yupi.yuso.mapper.PostMapper;
+import com.yupi.yuso.model.dto.post.PostEsDTO;
+import com.yupi.yuso.model.entity.Post;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * 增量同步帖子到 es
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+// todo 取消注释开启任务
+//@Component
+@Slf4j
+public class IncSyncPostToEs {
+
+    @Resource
+    private PostMapper postMapper;
+
+    @Resource
+    private PostEsDao postEsDao;
+
+    /**
+     * 每分钟执行一次
+     */
+    @Scheduled(fixedRate = 60 * 1000)
+    public void run() {
+        // 查询近 5 分钟内的数据
+        Date fiveMinutesAgoDate = new Date(new Date().getTime() - 5 * 60 * 1000L);
+        List<Post> postList = postMapper.listPostWithDelete(fiveMinutesAgoDate);
+        if (CollectionUtils.isEmpty(postList)) {
+            log.info("no inc post");
+            return;
+        }
+        List<PostEsDTO> postEsDTOList = postList.stream()
+                .map(PostEsDTO::objToDto)
+                .collect(Collectors.toList());
+        final int pageSize = 500;
+        int total = postEsDTOList.size();
+        log.info("IncSyncPostToEs start, total {}", total);
+        for (int i = 0; i < total; i += pageSize) {
+            int end = Math.min(i + pageSize, total);
+            log.info("sync from {} to {}", i, end);
+            postEsDao.saveAll(postEsDTOList.subList(i, end));
+        }
+        log.info("IncSyncPostToEs end, total {}", total);
+    }
+}

+ 48 - 0
src/main/java/com/yupi/yuso/job/once/FullSyncPostToEs.java

@@ -0,0 +1,48 @@
+package com.yupi.yuso.job.once;
+
+import com.yupi.yuso.esdao.PostEsDao;
+import com.yupi.yuso.model.dto.post.PostEsDTO;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.service.PostService;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.boot.CommandLineRunner;
+
+/**
+ * 全量同步帖子到 es
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+// todo 取消注释开启任务
+//@Component
+@Slf4j
+public class FullSyncPostToEs implements CommandLineRunner {
+
+    @Resource
+    private PostService postService;
+
+    @Resource
+    private PostEsDao postEsDao;
+
+    @Override
+    public void run(String... args) {
+        List<Post> postList = postService.list();
+        if (CollectionUtils.isEmpty(postList)) {
+            return;
+        }
+        List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());
+        final int pageSize = 500;
+        int total = postEsDTOList.size();
+        log.info("FullSyncPostToEs start, total {}", total);
+        for (int i = 0; i < total; i += pageSize) {
+            int end = Math.min(i + pageSize, total);
+            log.info("sync from {} to {}", i, end);
+            postEsDao.saveAll(postEsDTOList.subList(i, end));
+        }
+        log.info("FullSyncPostToEs end, total {}", total);
+    }
+}

+ 51 - 0
src/main/java/com/yupi/yuso/manager/CosManager.java

@@ -0,0 +1,51 @@
+package com.yupi.yuso.manager;
+
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.model.PutObjectRequest;
+import com.qcloud.cos.model.PutObjectResult;
+import com.yupi.yuso.config.CosClientConfig;
+import java.io.File;
+import javax.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+/**
+ * Cos 对象存储操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Component
+public class CosManager {
+
+    @Resource
+    private CosClientConfig cosClientConfig;
+
+    @Resource
+    private COSClient cosClient;
+
+    /**
+     * 上传对象
+     *
+     * @param key 唯一键
+     * @param localFilePath 本地文件路径
+     * @return
+     */
+    public PutObjectResult putObject(String key, String localFilePath) {
+        PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
+                new File(localFilePath));
+        return cosClient.putObject(putObjectRequest);
+    }
+
+    /**
+     * 上传对象
+     *
+     * @param key 唯一键
+     * @param file 文件
+     * @return
+     */
+    public PutObjectResult putObject(String key, File file) {
+        PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
+                file);
+        return cosClient.putObject(putObjectRequest);
+    }
+}

+ 35 - 0
src/main/java/com/yupi/yuso/mapper/PostFavourMapper.java

@@ -0,0 +1,35 @@
+package com.yupi.yuso.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.PostFavour;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 帖子收藏数据库操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostFavourMapper extends BaseMapper<PostFavour> {
+
+    /**
+     * 分页查询收藏帖子列表
+     *
+     * @param page
+     * @param queryWrapper
+     * @param favourUserId
+     * @return
+     */
+    Page<Post> listFavourPostByPage(IPage<Post> page, @Param(Constants.WRAPPER) Wrapper<Post> queryWrapper,
+            long favourUserId);
+
+}
+
+
+
+

+ 25 - 0
src/main/java/com/yupi/yuso/mapper/PostMapper.java

@@ -0,0 +1,25 @@
+package com.yupi.yuso.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yupi.yuso.model.entity.Post;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 帖子数据库操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostMapper extends BaseMapper<Post> {
+
+    /**
+     * 查询帖子列表(包括已被删除的数据)
+     */
+    List<Post> listPostWithDelete(Date minUpdateTime);
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/yupi/yuso/mapper/PostThumbMapper.java

@@ -0,0 +1,18 @@
+package com.yupi.yuso.mapper;
+
+import com.yupi.yuso.model.entity.PostThumb;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * 帖子点赞数据库操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostThumbMapper extends BaseMapper<PostThumb> {
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/yupi/yuso/mapper/UserMapper.java

@@ -0,0 +1,18 @@
+package com.yupi.yuso.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yupi.yuso.model.entity.User;
+
+/**
+ * 用户数据库操作
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface UserMapper extends BaseMapper<User> {
+
+}
+
+
+
+

+ 21 - 0
src/main/java/com/yupi/yuso/model/dto/file/UploadFileRequest.java

@@ -0,0 +1,21 @@
+package com.yupi.yuso.model.dto.file;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 文件上传请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UploadFileRequest implements Serializable {
+
+    /**
+     * 业务
+     */
+    private String biz;
+
+    private static final long serialVersionUID = 1L;
+}

+ 32 - 0
src/main/java/com/yupi/yuso/model/dto/post/PostAddRequest.java

@@ -0,0 +1,32 @@
+package com.yupi.yuso.model.dto.post;
+
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+
+/**
+ * 创建请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostAddRequest implements Serializable {
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tags;
+
+    private static final long serialVersionUID = 1L;
+}

+ 37 - 0
src/main/java/com/yupi/yuso/model/dto/post/PostEditRequest.java

@@ -0,0 +1,37 @@
+package com.yupi.yuso.model.dto.post;
+
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+
+/**
+ * 编辑请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostEditRequest implements Serializable {
+
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tags;
+
+    private static final long serialVersionUID = 1L;
+}

+ 125 - 0
src/main/java/com/yupi/yuso/model/dto/post/PostEsDTO.java

@@ -0,0 +1,125 @@
+package com.yupi.yuso.model.dto.post;
+
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
+import com.yupi.yuso.model.entity.Post;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import lombok.Data;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+/**
+ * 帖子 ES 包装类
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+// todo 取消注释开启 ES(须先配置 ES)
+//@Document(indexName = "post")
+@Data
+public class PostEsDTO implements Serializable {
+
+    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
+
+    /**
+     * id
+     */
+    @Id
+    private Long id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tags;
+
+    /**
+     * 点赞数
+     */
+    private Integer thumbNum;
+
+    /**
+     * 收藏数
+     */
+    private Integer favourNum;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 创建时间
+     */
+    @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
+    private Date updateTime;
+
+    /**
+     * 是否删除
+     */
+    private Integer isDelete;
+
+    private static final long serialVersionUID = 1L;
+
+    private static final Gson GSON = new Gson();
+
+    /**
+     * 对象转包装类
+     *
+     * @param post
+     * @return
+     */
+    public static PostEsDTO objToDto(Post post) {
+        if (post == null) {
+            return null;
+        }
+        PostEsDTO postEsDTO = new PostEsDTO();
+        BeanUtils.copyProperties(post, postEsDTO);
+        String tagsStr = post.getTags();
+        if (StringUtils.isNotBlank(tagsStr)) {
+            postEsDTO.setTags(GSON.fromJson(tagsStr, new TypeToken<List<String>>() {
+            }.getType()));
+        }
+        return postEsDTO;
+    }
+
+    /**
+     * 包装类转对象
+     *
+     * @param postEsDTO
+     * @return
+     */
+    public static Post dtoToObj(PostEsDTO postEsDTO) {
+        if (postEsDTO == null) {
+            return null;
+        }
+        Post post = new Post();
+        BeanUtils.copyProperties(postEsDTO, post);
+        List<String> tagList = postEsDTO.getTags();
+        if (CollectionUtils.isNotEmpty(tagList)) {
+            post.setTags(GSON.toJson(tagList));
+        }
+        return post;
+    }
+}

+ 65 - 0
src/main/java/com/yupi/yuso/model/dto/post/PostQueryRequest.java

@@ -0,0 +1,65 @@
+package com.yupi.yuso.model.dto.post;
+
+import com.yupi.yuso.common.PageRequest;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 查询请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class PostQueryRequest extends PageRequest implements Serializable {
+
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * id
+     */
+    private Long notId;
+
+    /**
+     * 搜索词
+     */
+    private String searchText;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tags;
+
+    /**
+     * 至少有一个标签
+     */
+    private List<String> orTags;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 收藏用户 id
+     */
+    private Long favourUserId;
+
+    private static final long serialVersionUID = 1L;
+}

+ 37 - 0
src/main/java/com/yupi/yuso/model/dto/post/PostUpdateRequest.java

@@ -0,0 +1,37 @@
+package com.yupi.yuso.model.dto.post;
+
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+
+/**
+ * 更新请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostUpdateRequest implements Serializable {
+
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tags;
+
+    private static final long serialVersionUID = 1L;
+}

+ 21 - 0
src/main/java/com/yupi/yuso/model/dto/postfavour/PostFavourAddRequest.java

@@ -0,0 +1,21 @@
+package com.yupi.yuso.model.dto.postfavour;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 帖子收藏 / 取消收藏请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostFavourAddRequest implements Serializable {
+
+    /**
+     * 帖子 id
+     */
+    private Long postId;
+
+    private static final long serialVersionUID = 1L;
+}

+ 30 - 0
src/main/java/com/yupi/yuso/model/dto/postfavour/PostFavourQueryRequest.java

@@ -0,0 +1,30 @@
+package com.yupi.yuso.model.dto.postfavour;
+
+import com.yupi.yuso.common.PageRequest;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 帖子收藏查询请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PostFavourQueryRequest extends PageRequest implements Serializable {
+
+    /**
+     * 帖子查询请求
+     */
+    private PostQueryRequest postQueryRequest;
+
+    /**
+     * 用户 id
+     */
+    private Long userId;
+
+    private static final long serialVersionUID = 1L;
+}

+ 21 - 0
src/main/java/com/yupi/yuso/model/dto/postthumb/PostThumbAddRequest.java

@@ -0,0 +1,21 @@
+package com.yupi.yuso.model.dto.postthumb;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 帖子点赞请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostThumbAddRequest implements Serializable {
+
+    /**
+     * 帖子 id
+     */
+    private Long postId;
+
+    private static final long serialVersionUID = 1L;
+}

+ 36 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserAddRequest.java

@@ -0,0 +1,36 @@
+package com.yupi.yuso.model.dto.user;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 用户创建请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserAddRequest implements Serializable {
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 账号
+     */
+    private String userAccount;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 用户角色: user, admin
+     */
+    private String userRole;
+
+    private static final long serialVersionUID = 1L;
+}

+ 20 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserLoginRequest.java

@@ -0,0 +1,20 @@
+package com.yupi.yuso.model.dto.user;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 用户登录请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserLoginRequest implements Serializable {
+
+    private static final long serialVersionUID = 3191241716373120793L;
+
+    private String userAccount;
+
+    private String userPassword;
+}

+ 48 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserQueryRequest.java

@@ -0,0 +1,48 @@
+package com.yupi.yuso.model.dto.user;
+
+import com.yupi.yuso.common.PageRequest;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 用户查询请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class UserQueryRequest extends PageRequest implements Serializable {
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 开放平台id
+     */
+    private String unionId;
+
+    /**
+     * 公众号openId
+     */
+    private String mpOpenId;
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 简介
+     */
+    private String userProfile;
+
+    /**
+     * 用户角色:user/admin/ban
+     */
+    private String userRole;
+
+    private static final long serialVersionUID = 1L;
+}

+ 22 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserRegisterRequest.java

@@ -0,0 +1,22 @@
+package com.yupi.yuso.model.dto.user;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 用户注册请求体
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserRegisterRequest implements Serializable {
+
+    private static final long serialVersionUID = 3191241716373120793L;
+
+    private String userAccount;
+
+    private String userPassword;
+
+    private String checkPassword;
+}

+ 31 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserUpdateMyRequest.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.model.dto.user;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 用户更新个人信息请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserUpdateMyRequest implements Serializable {
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 简介
+     */
+    private String userProfile;
+
+    private static final long serialVersionUID = 1L;
+}

+ 40 - 0
src/main/java/com/yupi/yuso/model/dto/user/UserUpdateRequest.java

@@ -0,0 +1,40 @@
+package com.yupi.yuso.model.dto.user;
+
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 用户更新请求
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserUpdateRequest implements Serializable {
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 简介
+     */
+    private String userProfile;
+
+    /**
+     * 用户角色:user/admin/ban
+     */
+    private String userRole;
+
+    private static final long serialVersionUID = 1L;
+}

+ 76 - 0
src/main/java/com/yupi/yuso/model/entity/Post.java

@@ -0,0 +1,76 @@
+package com.yupi.yuso.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 帖子
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@TableName(value = "post")
+@Data
+public class Post implements Serializable {
+
+    /**
+     * id
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 标签列表 json
+     */
+    private String tags;
+
+    /**
+     * 点赞数
+     */
+    private Integer thumbNum;
+
+    /**
+     * 收藏数
+     */
+    private Integer favourNum;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 是否删除
+     */
+    @TableLogic
+    private Integer isDelete;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 49 - 0
src/main/java/com/yupi/yuso/model/entity/PostFavour.java

@@ -0,0 +1,49 @@
+package com.yupi.yuso.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 帖子收藏
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@TableName(value = "post_favour")
+@Data
+public class PostFavour implements Serializable {
+
+    /**
+     * id
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 帖子 id
+     */
+    private Long postId;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 49 - 0
src/main/java/com/yupi/yuso/model/entity/PostThumb.java

@@ -0,0 +1,49 @@
+package com.yupi.yuso.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 帖子点赞
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@TableName(value = "post_thumb")
+@Data
+public class PostThumb implements Serializable {
+
+    /**
+     * id
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 帖子 id
+     */
+    private Long postId;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 86 - 0
src/main/java/com/yupi/yuso/model/entity/User.java

@@ -0,0 +1,86 @@
+package com.yupi.yuso.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 用户
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@TableName(value = "user")
+@Data
+public class User implements Serializable {
+
+    /**
+     * id
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * 用户账号
+     */
+    private String userAccount;
+
+    /**
+     * 用户密码
+     */
+    private String userPassword;
+
+    /**
+     * 开放平台id
+     */
+    private String unionId;
+
+    /**
+     * 公众号openId
+     */
+    private String mpOpenId;
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 用户简介
+     */
+    private String userProfile;
+
+    /**
+     * 用户角色:user/admin/ban
+     */
+    private String userRole;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 是否删除
+     */
+    @TableLogic
+    private Integer isDelete;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 61 - 0
src/main/java/com/yupi/yuso/model/enums/FileUploadBizEnum.java

@@ -0,0 +1,61 @@
+package com.yupi.yuso.model.enums;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.ObjectUtils;
+
+/**
+ * 文件上传业务类型枚举
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public enum FileUploadBizEnum {
+
+    USER_AVATAR("用户头像", "user_avatar");
+
+    private final String text;
+
+    private final String value;
+
+    FileUploadBizEnum(String text, String value) {
+        this.text = text;
+        this.value = value;
+    }
+
+    /**
+     * 获取值列表
+     *
+     * @return
+     */
+    public static List<String> getValues() {
+        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
+    }
+
+    /**
+     * 根据 value 获取枚举
+     *
+     * @param value
+     * @return
+     */
+    public static FileUploadBizEnum getEnumByValue(String value) {
+        if (ObjectUtils.isEmpty(value)) {
+            return null;
+        }
+        for (FileUploadBizEnum anEnum : FileUploadBizEnum.values()) {
+            if (anEnum.value.equals(value)) {
+                return anEnum;
+            }
+        }
+        return null;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public String getText() {
+        return text;
+    }
+}

+ 63 - 0
src/main/java/com/yupi/yuso/model/enums/UserRoleEnum.java

@@ -0,0 +1,63 @@
+package com.yupi.yuso.model.enums;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.ObjectUtils;
+
+/**
+ * 用户角色枚举
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public enum UserRoleEnum {
+
+    USER("用户", "user"),
+    ADMIN("管理员", "admin"),
+    BAN("被封号", "ban");
+
+    private final String text;
+
+    private final String value;
+
+    UserRoleEnum(String text, String value) {
+        this.text = text;
+        this.value = value;
+    }
+
+    /**
+     * 获取值列表
+     *
+     * @return
+     */
+    public static List<String> getValues() {
+        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
+    }
+
+    /**
+     * 根据 value 获取枚举
+     *
+     * @param value
+     * @return
+     */
+    public static UserRoleEnum getEnumByValue(String value) {
+        if (ObjectUtils.isEmpty(value)) {
+            return null;
+        }
+        for (UserRoleEnum anEnum : UserRoleEnum.values()) {
+            if (anEnum.value.equals(value)) {
+                return anEnum;
+            }
+        }
+        return null;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public String getText() {
+        return text;
+    }
+}

+ 52 - 0
src/main/java/com/yupi/yuso/model/vo/LoginUserVO.java

@@ -0,0 +1,52 @@
+package com.yupi.yuso.model.vo;
+
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 已登录用户视图(脱敏)
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@Data
+public class LoginUserVO implements Serializable {
+
+    /**
+     * 用户 id
+     */
+    private Long id;
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 用户简介
+     */
+    private String userProfile;
+
+    /**
+     * 用户角色:user/admin/ban
+     */
+    private String userRole;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    private static final long serialVersionUID = 1L;
+}

+ 118 - 0
src/main/java/com/yupi/yuso/model/vo/PostVO.java

@@ -0,0 +1,118 @@
+package com.yupi.yuso.model.vo;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.yupi.yuso.model.entity.Post;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+/**
+ * 帖子视图
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class PostVO implements Serializable {
+
+    private final static Gson GSON = new Gson();
+
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+    /**
+     * 点赞数
+     */
+    private Integer thumbNum;
+
+    /**
+     * 收藏数
+     */
+    private Integer favourNum;
+
+    /**
+     * 创建用户 id
+     */
+    private Long userId;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 标签列表
+     */
+    private List<String> tagList;
+
+    /**
+     * 创建人信息
+     */
+    private UserVO user;
+
+    /**
+     * 是否已点赞
+     */
+    private Boolean hasThumb;
+
+    /**
+     * 是否已收藏
+     */
+    private Boolean hasFavour;
+
+    /**
+     * 包装类转对象
+     *
+     * @param postVO
+     * @return
+     */
+    public static Post voToObj(PostVO postVO) {
+        if (postVO == null) {
+            return null;
+        }
+        Post post = new Post();
+        BeanUtils.copyProperties(postVO, post);
+        List<String> tagList = postVO.getTagList();
+        if (tagList != null) {
+            post.setTags(GSON.toJson(tagList));
+        }
+        return post;
+    }
+
+    /**
+     * 对象转包装类
+     *
+     * @param post
+     * @return
+     */
+    public static PostVO objToVo(Post post) {
+        if (post == null) {
+            return null;
+        }
+        PostVO postVO = new PostVO();
+        BeanUtils.copyProperties(post, postVO);
+        postVO.setTagList(GSON.fromJson(post.getTags(), new TypeToken<List<String>>() {
+        }.getType()));
+        return postVO;
+    }
+}

+ 47 - 0
src/main/java/com/yupi/yuso/model/vo/UserVO.java

@@ -0,0 +1,47 @@
+package com.yupi.yuso.model.vo;
+
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 用户视图(脱敏)
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Data
+public class UserVO implements Serializable {
+
+    /**
+     * id
+     */
+    private Long id;
+
+    /**
+     * 用户昵称
+     */
+    private String userName;
+
+    /**
+     * 用户头像
+     */
+    private String userAvatar;
+
+    /**
+     * 用户简介
+     */
+    private String userProfile;
+
+    /**
+     * 用户角色:user/admin/ban
+     */
+    private String userRole;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    private static final long serialVersionUID = 1L;
+}

+ 47 - 0
src/main/java/com/yupi/yuso/service/PostFavourService.java

@@ -0,0 +1,47 @@
+package com.yupi.yuso.service;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.PostFavour;
+import com.yupi.yuso.model.entity.User;
+
+/**
+ * 帖子收藏服务
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostFavourService extends IService<PostFavour> {
+
+    /**
+     * 帖子收藏
+     *
+     * @param postId
+     * @param loginUser
+     * @return
+     */
+    int doPostFavour(long postId, User loginUser);
+
+    /**
+     * 分页获取用户收藏的帖子列表
+     *
+     * @param page
+     * @param queryWrapper
+     * @param favourUserId
+     * @return
+     */
+    Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper,
+            long favourUserId);
+
+    /**
+     * 帖子收藏(内部服务)
+     *
+     * @param userId
+     * @param postId
+     * @return
+     */
+    int doPostFavourInner(long userId, long postId);
+}

+ 60 - 0
src/main/java/com/yupi/yuso/service/PostService.java

@@ -0,0 +1,60 @@
+package com.yupi.yuso.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.vo.PostVO;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 帖子服务
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostService extends IService<Post> {
+
+    /**
+     * 校验
+     *
+     * @param post
+     * @param add
+     */
+    void validPost(Post post, boolean add);
+
+    /**
+     * 获取查询条件
+     *
+     * @param postQueryRequest
+     * @return
+     */
+    QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest);
+
+    /**
+     * 从 ES 查询
+     *
+     * @param postQueryRequest
+     * @return
+     */
+    Page<Post> searchFromEs(PostQueryRequest postQueryRequest);
+
+    /**
+     * 获取帖子封装
+     *
+     * @param post
+     * @param request
+     * @return
+     */
+    PostVO getPostVO(Post post, HttpServletRequest request);
+
+    /**
+     * 分页获取帖子封装
+     *
+     * @param postPage
+     * @param request
+     * @return
+     */
+    Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request);
+}

+ 32 - 0
src/main/java/com/yupi/yuso/service/PostThumbService.java

@@ -0,0 +1,32 @@
+package com.yupi.yuso.service;
+
+import com.yupi.yuso.model.entity.PostThumb;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yupi.yuso.model.entity.User;
+
+/**
+ * 帖子点赞服务
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface PostThumbService extends IService<PostThumb> {
+
+    /**
+     * 点赞
+     *
+     * @param postId
+     * @param loginUser
+     * @return
+     */
+    int doPostThumb(long postId, User loginUser);
+
+    /**
+     * 帖子点赞(内部服务)
+     *
+     * @param userId
+     * @param postId
+     * @return
+     */
+    int doPostThumbInner(long userId, long postId);
+}

+ 121 - 0
src/main/java/com/yupi/yuso/service/UserService.java

@@ -0,0 +1,121 @@
+package com.yupi.yuso.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yupi.yuso.model.dto.user.UserQueryRequest;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.vo.LoginUserVO;
+import com.yupi.yuso.model.vo.UserVO;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
+
+/**
+ * 用户服务
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public interface UserService extends IService<User> {
+
+    /**
+     * 用户注册
+     *
+     * @param userAccount   用户账户
+     * @param userPassword  用户密码
+     * @param checkPassword 校验密码
+     * @return 新用户 id
+     */
+    long userRegister(String userAccount, String userPassword, String checkPassword);
+
+    /**
+     * 用户登录
+     *
+     * @param userAccount  用户账户
+     * @param userPassword 用户密码
+     * @param request
+     * @return 脱敏后的用户信息
+     */
+    LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);
+
+    /**
+     * 用户登录(微信开放平台)
+     *
+     * @param wxOAuth2UserInfo 从微信获取的用户信息
+     * @param request
+     * @return 脱敏后的用户信息
+     */
+    LoginUserVO userLoginByMpOpen(WxOAuth2UserInfo wxOAuth2UserInfo, HttpServletRequest request);
+
+    /**
+     * 获取当前登录用户
+     *
+     * @param request
+     * @return
+     */
+    User getLoginUser(HttpServletRequest request);
+
+    /**
+     * 获取当前登录用户(允许未登录)
+     *
+     * @param request
+     * @return
+     */
+    User getLoginUserPermitNull(HttpServletRequest request);
+
+    /**
+     * 是否为管理员
+     *
+     * @param request
+     * @return
+     */
+    boolean isAdmin(HttpServletRequest request);
+
+    /**
+     * 是否为管理员
+     *
+     * @param user
+     * @return
+     */
+    boolean isAdmin(User user);
+
+    /**
+     * 用户注销
+     *
+     * @param request
+     * @return
+     */
+    boolean userLogout(HttpServletRequest request);
+
+    /**
+     * 获取脱敏的已登录用户信息
+     *
+     * @return
+     */
+    LoginUserVO getLoginUserVO(User user);
+
+    /**
+     * 获取脱敏的用户信息
+     *
+     * @param user
+     * @return
+     */
+    UserVO getUserVO(User user);
+
+    /**
+     * 获取脱敏的用户信息
+     *
+     * @param userList
+     * @return
+     */
+    List<UserVO> getUserVO(List<User> userList);
+
+    /**
+     * 获取查询条件
+     *
+     * @param userQueryRequest
+     * @return
+     */
+    QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);
+
+}

+ 116 - 0
src/main/java/com/yupi/yuso/service/impl/PostFavourServiceImpl.java

@@ -0,0 +1,116 @@
+package com.yupi.yuso.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.mapper.PostFavourMapper;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.PostFavour;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.service.PostFavourService;
+import com.yupi.yuso.service.PostService;
+import javax.annotation.Resource;
+import org.springframework.aop.framework.AopContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 帖子收藏服务实现
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Service
+public class PostFavourServiceImpl extends ServiceImpl<PostFavourMapper, PostFavour>
+        implements PostFavourService {
+
+    @Resource
+    private PostService postService;
+
+    /**
+     * 帖子收藏
+     *
+     * @param postId
+     * @param loginUser
+     * @return
+     */
+    @Override
+    public int doPostFavour(long postId, User loginUser) {
+        // 判断是否存在
+        Post post = postService.getById(postId);
+        if (post == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
+        }
+        // 是否已帖子收藏
+        long userId = loginUser.getId();
+        // 每个用户串行帖子收藏
+        // 锁必须要包裹住事务方法
+        PostFavourService postFavourService = (PostFavourService) AopContext.currentProxy();
+        synchronized (String.valueOf(userId).intern()) {
+            return postFavourService.doPostFavourInner(userId, postId);
+        }
+    }
+
+    @Override
+    public Page<Post> listFavourPostByPage(IPage<Post> page, Wrapper<Post> queryWrapper, long favourUserId) {
+        if (favourUserId <= 0) {
+            return new Page<>();
+        }
+        return baseMapper.listFavourPostByPage(page, queryWrapper, favourUserId);
+    }
+
+    /**
+     * 封装了事务的方法
+     *
+     * @param userId
+     * @param postId
+     * @return
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int doPostFavourInner(long userId, long postId) {
+        PostFavour postFavour = new PostFavour();
+        postFavour.setUserId(userId);
+        postFavour.setPostId(postId);
+        QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>(postFavour);
+        PostFavour oldPostFavour = this.getOne(postFavourQueryWrapper);
+        boolean result;
+        // 已收藏
+        if (oldPostFavour != null) {
+            result = this.remove(postFavourQueryWrapper);
+            if (result) {
+                // 帖子收藏数 - 1
+                result = postService.update()
+                        .eq("id", postId)
+                        .gt("favourNum", 0)
+                        .setSql("favourNum = favourNum - 1")
+                        .update();
+                return result ? -1 : 0;
+            } else {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR);
+            }
+        } else {
+            // 未帖子收藏
+            result = this.save(postFavour);
+            if (result) {
+                // 帖子收藏数 + 1
+                result = postService.update()
+                        .eq("id", postId)
+                        .setSql("favourNum = favourNum + 1")
+                        .update();
+                return result ? 1 : 0;
+            } else {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR);
+            }
+        }
+    }
+
+}
+
+
+
+

+ 316 - 0
src/main/java/com/yupi/yuso/service/impl/PostServiceImpl.java

@@ -0,0 +1,316 @@
+package com.yupi.yuso.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.google.gson.Gson;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.constant.CommonConstant;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.exception.ThrowUtils;
+import com.yupi.yuso.mapper.PostFavourMapper;
+import com.yupi.yuso.mapper.PostMapper;
+import com.yupi.yuso.mapper.PostThumbMapper;
+import com.yupi.yuso.model.dto.post.PostEsDTO;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.PostFavour;
+import com.yupi.yuso.model.entity.PostThumb;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.vo.PostVO;
+import com.yupi.yuso.model.vo.UserVO;
+import com.yupi.yuso.service.PostService;
+import com.yupi.yuso.service.UserService;
+import com.yupi.yuso.utils.SqlUtils;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.sort.SortBuilder;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
+import org.springframework.data.elasticsearch.core.SearchHit;
+import org.springframework.data.elasticsearch.core.SearchHits;
+import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
+import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
+import org.springframework.stereotype.Service;
+
+/**
+ * 帖子服务实现
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Service
+@Slf4j
+public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {
+
+    private final static Gson GSON = new Gson();
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private PostThumbMapper postThumbMapper;
+
+    @Resource
+    private PostFavourMapper postFavourMapper;
+
+    @Resource
+    private ElasticsearchRestTemplate elasticsearchRestTemplate;
+
+    @Override
+    public void validPost(Post post, boolean add) {
+        if (post == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR);
+        }
+        String title = post.getTitle();
+        String content = post.getContent();
+        String tags = post.getTags();
+        // 创建时,参数不能为空
+        if (add) {
+            ThrowUtils.throwIf(StringUtils.isAnyBlank(title, content, tags), ErrorCode.PARAMS_ERROR);
+        }
+        // 有参数则校验
+        if (StringUtils.isNotBlank(title) && title.length() > 80) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "标题过长");
+        }
+        if (StringUtils.isNotBlank(content) && content.length() > 8192) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "内容过长");
+        }
+    }
+
+    /**
+     * 获取查询包装类
+     *
+     * @param postQueryRequest
+     * @return
+     */
+    @Override
+    public QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest) {
+        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
+        if (postQueryRequest == null) {
+            return queryWrapper;
+        }
+        String searchText = postQueryRequest.getSearchText();
+        String sortField = postQueryRequest.getSortField();
+        String sortOrder = postQueryRequest.getSortOrder();
+        Long id = postQueryRequest.getId();
+        String title = postQueryRequest.getTitle();
+        String content = postQueryRequest.getContent();
+        List<String> tagList = postQueryRequest.getTags();
+        Long userId = postQueryRequest.getUserId();
+        Long notId = postQueryRequest.getNotId();
+        // 拼接查询条件
+        if (StringUtils.isNotBlank(searchText)) {
+            queryWrapper.like("title", title).or().like("content", content);
+        }
+        queryWrapper.like(StringUtils.isNotBlank(title), "title", title);
+        queryWrapper.like(StringUtils.isNotBlank(content), "content", content);
+        if (CollectionUtils.isNotEmpty(tagList)) {
+            for (String tag : tagList) {
+                queryWrapper.like("tags", "\"" + tag + "\"");
+            }
+        }
+        queryWrapper.ne(ObjectUtils.isNotEmpty(notId), "id", notId);
+        queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
+        queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
+        queryWrapper.eq("isDelete", false);
+        queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
+                sortField);
+        return queryWrapper;
+    }
+
+    @Override
+    public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
+        Long id = postQueryRequest.getId();
+        Long notId = postQueryRequest.getNotId();
+        String searchText = postQueryRequest.getSearchText();
+        String title = postQueryRequest.getTitle();
+        String content = postQueryRequest.getContent();
+        List<String> tagList = postQueryRequest.getTags();
+        List<String> orTagList = postQueryRequest.getOrTags();
+        Long userId = postQueryRequest.getUserId();
+        // es 起始页为 0
+        long current = postQueryRequest.getCurrent() - 1;
+        long pageSize = postQueryRequest.getPageSize();
+        String sortField = postQueryRequest.getSortField();
+        String sortOrder = postQueryRequest.getSortOrder();
+        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
+        // 过滤
+        boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
+        if (id != null) {
+            boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
+        }
+        if (notId != null) {
+            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
+        }
+        if (userId != null) {
+            boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
+        }
+        // 必须包含所有标签
+        if (CollectionUtils.isNotEmpty(tagList)) {
+            for (String tag : tagList) {
+                boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
+            }
+        }
+        // 包含任何一个标签即可
+        if (CollectionUtils.isNotEmpty(orTagList)) {
+            BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
+            for (String tag : orTagList) {
+                orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
+            }
+            orTagBoolQueryBuilder.minimumShouldMatch(1);
+            boolQueryBuilder.filter(orTagBoolQueryBuilder);
+        }
+        // 按关键词检索
+        if (StringUtils.isNotBlank(searchText)) {
+            boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
+            boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
+            boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
+            boolQueryBuilder.minimumShouldMatch(1);
+        }
+        // 按标题检索
+        if (StringUtils.isNotBlank(title)) {
+            boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
+            boolQueryBuilder.minimumShouldMatch(1);
+        }
+        // 按内容检索
+        if (StringUtils.isNotBlank(content)) {
+            boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
+            boolQueryBuilder.minimumShouldMatch(1);
+        }
+        // 排序
+        SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
+        if (StringUtils.isNotBlank(sortField)) {
+            sortBuilder = SortBuilders.fieldSort(sortField);
+            sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
+        }
+        // 分页
+        PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
+        // 构造查询
+        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
+                .withPageable(pageRequest).withSorts(sortBuilder).build();
+        SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
+        Page<Post> page = new Page<>();
+        page.setTotal(searchHits.getTotalHits());
+        List<Post> resourceList = new ArrayList<>();
+        // 查出结果后,从 db 获取最新动态数据(比如点赞数)
+        if (searchHits.hasSearchHits()) {
+            List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
+            List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
+                    .collect(Collectors.toList());
+            List<Post> postList = baseMapper.selectBatchIds(postIdList);
+            if (postList != null) {
+                Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
+                postIdList.forEach(postId -> {
+                    if (idPostMap.containsKey(postId)) {
+                        resourceList.add(idPostMap.get(postId).get(0));
+                    } else {
+                        // 从 es 清空 db 已物理删除的数据
+                        String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
+                        log.info("delete post {}", delete);
+                    }
+                });
+            }
+        }
+        page.setRecords(resourceList);
+        return page;
+    }
+
+    @Override
+    public PostVO getPostVO(Post post, HttpServletRequest request) {
+        PostVO postVO = PostVO.objToVo(post);
+        long postId = post.getId();
+        // 1. 关联查询用户信息
+        Long userId = post.getUserId();
+        User user = null;
+        if (userId != null && userId > 0) {
+            user = userService.getById(userId);
+        }
+        UserVO userVO = userService.getUserVO(user);
+        postVO.setUser(userVO);
+        // 2. 已登录,获取用户点赞、收藏状态
+        User loginUser = userService.getLoginUserPermitNull(request);
+        if (loginUser != null) {
+            // 获取点赞
+            QueryWrapper<PostThumb> postThumbQueryWrapper = new QueryWrapper<>();
+            postThumbQueryWrapper.in("postId", postId);
+            postThumbQueryWrapper.eq("userId", loginUser.getId());
+            PostThumb postThumb = postThumbMapper.selectOne(postThumbQueryWrapper);
+            postVO.setHasThumb(postThumb != null);
+            // 获取收藏
+            QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>();
+            postFavourQueryWrapper.in("postId", postId);
+            postFavourQueryWrapper.eq("userId", loginUser.getId());
+            PostFavour postFavour = postFavourMapper.selectOne(postFavourQueryWrapper);
+            postVO.setHasFavour(postFavour != null);
+        }
+        return postVO;
+    }
+
+    @Override
+    public Page<PostVO> getPostVOPage(Page<Post> postPage, HttpServletRequest request) {
+        List<Post> postList = postPage.getRecords();
+        Page<PostVO> postVOPage = new Page<>(postPage.getCurrent(), postPage.getSize(), postPage.getTotal());
+        if (CollectionUtils.isEmpty(postList)) {
+            return postVOPage;
+        }
+        // 1. 关联查询用户信息
+        Set<Long> userIdSet = postList.stream().map(Post::getUserId).collect(Collectors.toSet());
+        Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
+                .collect(Collectors.groupingBy(User::getId));
+        // 2. 已登录,获取用户点赞、收藏状态
+        Map<Long, Boolean> postIdHasThumbMap = new HashMap<>();
+        Map<Long, Boolean> postIdHasFavourMap = new HashMap<>();
+        User loginUser = userService.getLoginUserPermitNull(request);
+        if (loginUser != null) {
+            Set<Long> postIdSet = postList.stream().map(Post::getId).collect(Collectors.toSet());
+            loginUser = userService.getLoginUser(request);
+            // 获取点赞
+            QueryWrapper<PostThumb> postThumbQueryWrapper = new QueryWrapper<>();
+            postThumbQueryWrapper.in("postId", postIdSet);
+            postThumbQueryWrapper.eq("userId", loginUser.getId());
+            List<PostThumb> postPostThumbList = postThumbMapper.selectList(postThumbQueryWrapper);
+            postPostThumbList.forEach(postPostThumb -> postIdHasThumbMap.put(postPostThumb.getPostId(), true));
+            // 获取收藏
+            QueryWrapper<PostFavour> postFavourQueryWrapper = new QueryWrapper<>();
+            postFavourQueryWrapper.in("postId", postIdSet);
+            postFavourQueryWrapper.eq("userId", loginUser.getId());
+            List<PostFavour> postFavourList = postFavourMapper.selectList(postFavourQueryWrapper);
+            postFavourList.forEach(postFavour -> postIdHasFavourMap.put(postFavour.getPostId(), true));
+        }
+        // 填充信息
+        List<PostVO> postVOList = postList.stream().map(post -> {
+            PostVO postVO = PostVO.objToVo(post);
+            Long userId = post.getUserId();
+            User user = null;
+            if (userIdUserListMap.containsKey(userId)) {
+                user = userIdUserListMap.get(userId).get(0);
+            }
+            postVO.setUser(userService.getUserVO(user));
+            postVO.setHasThumb(postIdHasThumbMap.getOrDefault(post.getId(), false));
+            postVO.setHasFavour(postIdHasFavourMap.getOrDefault(post.getId(), false));
+            return postVO;
+        }).collect(Collectors.toList());
+        postVOPage.setRecords(postVOList);
+        return postVOPage;
+    }
+
+}
+
+
+
+

+ 105 - 0
src/main/java/com/yupi/yuso/service/impl/PostThumbServiceImpl.java

@@ -0,0 +1,105 @@
+package com.yupi.yuso.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.mapper.PostThumbMapper;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.PostThumb;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.service.PostService;
+import com.yupi.yuso.service.PostThumbService;
+import javax.annotation.Resource;
+import org.springframework.aop.framework.AopContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 帖子点赞服务实现
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Service
+public class PostThumbServiceImpl extends ServiceImpl<PostThumbMapper, PostThumb>
+        implements PostThumbService {
+
+    @Resource
+    private PostService postService;
+
+    /**
+     * 点赞
+     *
+     * @param postId
+     * @param loginUser
+     * @return
+     */
+    @Override
+    public int doPostThumb(long postId, User loginUser) {
+        // 判断实体是否存在,根据类别获取实体
+        Post post = postService.getById(postId);
+        if (post == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
+        }
+        // 是否已点赞
+        long userId = loginUser.getId();
+        // 每个用户串行点赞
+        // 锁必须要包裹住事务方法
+        PostThumbService postThumbService = (PostThumbService) AopContext.currentProxy();
+        synchronized (String.valueOf(userId).intern()) {
+            return postThumbService.doPostThumbInner(userId, postId);
+        }
+    }
+
+    /**
+     * 封装了事务的方法
+     *
+     * @param userId
+     * @param postId
+     * @return
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int doPostThumbInner(long userId, long postId) {
+        PostThumb postThumb = new PostThumb();
+        postThumb.setUserId(userId);
+        postThumb.setPostId(postId);
+        QueryWrapper<PostThumb> thumbQueryWrapper = new QueryWrapper<>(postThumb);
+        PostThumb oldPostThumb = this.getOne(thumbQueryWrapper);
+        boolean result;
+        // 已点赞
+        if (oldPostThumb != null) {
+            result = this.remove(thumbQueryWrapper);
+            if (result) {
+                // 点赞数 - 1
+                result = postService.update()
+                        .eq("id", postId)
+                        .gt("thumbNum", 0)
+                        .setSql("thumbNum = thumbNum - 1")
+                        .update();
+                return result ? -1 : 0;
+            } else {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR);
+            }
+        } else {
+            // 未点赞
+            result = this.save(postThumb);
+            if (result) {
+                // 点赞数 + 1
+                result = postService.update()
+                        .eq("id", postId)
+                        .setSql("thumbNum = thumbNum + 1")
+                        .update();
+                return result ? 1 : 0;
+            } else {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR);
+            }
+        }
+    }
+
+}
+
+
+
+

+ 272 - 0
src/main/java/com/yupi/yuso/service/impl/UserServiceImpl.java

@@ -0,0 +1,272 @@
+package com.yupi.yuso.service.impl;
+
+import static com.yupi.yuso.constant.UserConstant.USER_LOGIN_STATE;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yupi.yuso.common.ErrorCode;
+import com.yupi.yuso.constant.CommonConstant;
+import com.yupi.yuso.exception.BusinessException;
+import com.yupi.yuso.mapper.UserMapper;
+import com.yupi.yuso.model.dto.user.UserQueryRequest;
+import com.yupi.yuso.model.entity.User;
+import com.yupi.yuso.model.enums.UserRoleEnum;
+import com.yupi.yuso.model.vo.LoginUserVO;
+import com.yupi.yuso.model.vo.UserVO;
+import com.yupi.yuso.service.UserService;
+import com.yupi.yuso.utils.SqlUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+
+/**
+ * 用户服务实现
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Service
+@Slf4j
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
+
+    /**
+     * 盐值,混淆密码
+     */
+    private static final String SALT = "yupi";
+
+    @Override
+    public long userRegister(String userAccount, String userPassword, String checkPassword) {
+        // 1. 校验
+        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
+        }
+        if (userAccount.length() < 4) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
+        }
+        if (userPassword.length() < 8 || checkPassword.length() < 8) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
+        }
+        // 密码和校验密码相同
+        if (!userPassword.equals(checkPassword)) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
+        }
+        synchronized (userAccount.intern()) {
+            // 账户不能重复
+            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("userAccount", userAccount);
+            long count = this.baseMapper.selectCount(queryWrapper);
+            if (count > 0) {
+                throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
+            }
+            // 2. 加密
+            String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
+            // 3. 插入数据
+            User user = new User();
+            user.setUserAccount(userAccount);
+            user.setUserPassword(encryptPassword);
+            boolean saveResult = this.save(user);
+            if (!saveResult) {
+                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
+            }
+            return user.getId();
+        }
+    }
+
+    @Override
+    public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
+        // 1. 校验
+        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
+        }
+        if (userAccount.length() < 4) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
+        }
+        if (userPassword.length() < 8) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
+        }
+        // 2. 加密
+        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
+        // 查询用户是否存在
+        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("userAccount", userAccount);
+        queryWrapper.eq("userPassword", encryptPassword);
+        User user = this.baseMapper.selectOne(queryWrapper);
+        // 用户不存在
+        if (user == null) {
+            log.info("user login failed, userAccount cannot match userPassword");
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
+        }
+        // 3. 记录用户的登录态
+        request.getSession().setAttribute(USER_LOGIN_STATE, user);
+        return this.getLoginUserVO(user);
+    }
+
+    @Override
+    public LoginUserVO userLoginByMpOpen(WxOAuth2UserInfo wxOAuth2UserInfo, HttpServletRequest request) {
+        String unionId = wxOAuth2UserInfo.getUnionId();
+        String mpOpenId = wxOAuth2UserInfo.getOpenid();
+        // 单机锁
+        synchronized (unionId.intern()) {
+            // 查询用户是否已存在
+            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("unionId", unionId);
+            User user = this.getOne(queryWrapper);
+            // 被封号,禁止登录
+            if (user != null && UserRoleEnum.BAN.getValue().equals(user.getUserRole())) {
+                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "该用户已被封,禁止登录");
+            }
+            // 用户不存在则创建
+            if (user == null) {
+                user = new User();
+                user.setUnionId(unionId);
+                user.setMpOpenId(mpOpenId);
+                user.setUserAvatar(wxOAuth2UserInfo.getHeadImgUrl());
+                user.setUserName(wxOAuth2UserInfo.getNickname());
+                boolean result = this.save(user);
+                if (!result) {
+                    throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败");
+                }
+            }
+            // 记录用户的登录态
+            request.getSession().setAttribute(USER_LOGIN_STATE, user);
+            return getLoginUserVO(user);
+        }
+    }
+
+    /**
+     * 获取当前登录用户
+     *
+     * @param request
+     * @return
+     */
+    @Override
+    public User getLoginUser(HttpServletRequest request) {
+        // 先判断是否已登录
+        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
+        User currentUser = (User) userObj;
+        if (currentUser == null || currentUser.getId() == null) {
+            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
+        }
+        // 从数据库查询(追求性能的话可以注释,直接走缓存)
+        long userId = currentUser.getId();
+        currentUser = this.getById(userId);
+        if (currentUser == null) {
+            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
+        }
+        return currentUser;
+    }
+
+    /**
+     * 获取当前登录用户(允许未登录)
+     *
+     * @param request
+     * @return
+     */
+    @Override
+    public User getLoginUserPermitNull(HttpServletRequest request) {
+        // 先判断是否已登录
+        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
+        User currentUser = (User) userObj;
+        if (currentUser == null || currentUser.getId() == null) {
+            return null;
+        }
+        // 从数据库查询(追求性能的话可以注释,直接走缓存)
+        long userId = currentUser.getId();
+        return this.getById(userId);
+    }
+
+    /**
+     * 是否为管理员
+     *
+     * @param request
+     * @return
+     */
+    @Override
+    public boolean isAdmin(HttpServletRequest request) {
+        // 仅管理员可查询
+        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
+        User user = (User) userObj;
+        return isAdmin(user);
+    }
+
+    @Override
+    public boolean isAdmin(User user) {
+        return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
+    }
+
+    /**
+     * 用户注销
+     *
+     * @param request
+     */
+    @Override
+    public boolean userLogout(HttpServletRequest request) {
+        if (request.getSession().getAttribute(USER_LOGIN_STATE) == null) {
+            throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
+        }
+        // 移除登录态
+        request.getSession().removeAttribute(USER_LOGIN_STATE);
+        return true;
+    }
+
+    @Override
+    public LoginUserVO getLoginUserVO(User user) {
+        if (user == null) {
+            return null;
+        }
+        LoginUserVO loginUserVO = new LoginUserVO();
+        BeanUtils.copyProperties(user, loginUserVO);
+        return loginUserVO;
+    }
+
+    @Override
+    public UserVO getUserVO(User user) {
+        if (user == null) {
+            return null;
+        }
+        UserVO userVO = new UserVO();
+        BeanUtils.copyProperties(user, userVO);
+        return userVO;
+    }
+
+    @Override
+    public List<UserVO> getUserVO(List<User> userList) {
+        if (CollectionUtils.isEmpty(userList)) {
+            return new ArrayList<>();
+        }
+        return userList.stream().map(this::getUserVO).collect(Collectors.toList());
+    }
+
+    @Override
+    public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
+        if (userQueryRequest == null) {
+            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
+        }
+        Long id = userQueryRequest.getId();
+        String unionId = userQueryRequest.getUnionId();
+        String mpOpenId = userQueryRequest.getMpOpenId();
+        String userName = userQueryRequest.getUserName();
+        String userProfile = userQueryRequest.getUserProfile();
+        String userRole = userQueryRequest.getUserRole();
+        String sortField = userQueryRequest.getSortField();
+        String sortOrder = userQueryRequest.getSortOrder();
+        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq(id != null, "id", id);
+        queryWrapper.eq(StringUtils.isNotBlank(unionId), "unionId", unionId);
+        queryWrapper.eq(StringUtils.isNotBlank(mpOpenId), "mpOpenId", mpOpenId);
+        queryWrapper.eq(StringUtils.isNotBlank(userRole), "userRole", userRole);
+        queryWrapper.like(StringUtils.isNotBlank(userProfile), "userProfile", userProfile);
+        queryWrapper.like(StringUtils.isNotBlank(userName), "userName", userName);
+        queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
+                sortField);
+        return queryWrapper;
+    }
+}

+ 55 - 0
src/main/java/com/yupi/yuso/utils/NetUtils.java

@@ -0,0 +1,55 @@
+package com.yupi.yuso.utils;
+
+import java.net.InetAddress;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 网络工具类
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public class NetUtils {
+
+    /**
+     * 获取客户端 IP 地址
+     *
+     * @param request
+     * @return
+     */
+    public static String getIpAddress(HttpServletRequest request) {
+        String ip = request.getHeader("x-forwarded-for");
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+            if (ip.equals("127.0.0.1")) {
+                // 根据网卡取本机配置的 IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+                if (inet != null) {
+                    ip = inet.getHostAddress();
+                }
+            }
+        }
+        // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ip != null && ip.length() > 15) {
+            if (ip.indexOf(",") > 0) {
+                ip = ip.substring(0, ip.indexOf(","));
+            }
+        }
+        if (ip == null) {
+            return "127.0.0.1";
+        }
+        return ip;
+    }
+
+}

+ 57 - 0
src/main/java/com/yupi/yuso/utils/SpringContextUtils.java

@@ -0,0 +1,57 @@
+package com.yupi.yuso.utils;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * Spring 上下文获取工具
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Component
+public class SpringContextUtils implements ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
+        SpringContextUtils.applicationContext = applicationContext;
+    }
+
+    /**
+     * 通过名称获取 Bean
+     *
+     * @param beanName
+     * @return
+     */
+    public static Object getBean(String beanName) {
+        return applicationContext.getBean(beanName);
+    }
+
+    /**
+     * 通过 class 获取 Bean
+     *
+     * @param beanClass
+     * @param <T>
+     * @return
+     */
+    public static <T> T getBean(Class<T> beanClass) {
+        return applicationContext.getBean(beanClass);
+    }
+
+    /**
+     * 通过名称和类型获取 Bean
+     *
+     * @param beanName
+     * @param beanClass
+     * @param <T>
+     * @return
+     */
+    public static <T> T getBean(String beanName, Class<T> beanClass) {
+        return applicationContext.getBean(beanName, beanClass);
+    }
+}

+ 25 - 0
src/main/java/com/yupi/yuso/utils/SqlUtils.java

@@ -0,0 +1,25 @@
+package com.yupi.yuso.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * SQL 工具
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+public class SqlUtils {
+
+    /**
+     * 校验排序字段是否合法(防止 SQL 注入)
+     *
+     * @param sortField
+     * @return
+     */
+    public static boolean validSortField(String sortField) {
+        if (StringUtils.isBlank(sortField)) {
+            return false;
+        }
+        return !StringUtils.containsAny(sortField, "=", "(", ")", " ");
+    }
+}

+ 16 - 0
src/main/java/com/yupi/yuso/wxmp/WxMpConstant.java

@@ -0,0 +1,16 @@
+package com.yupi.yuso.wxmp;
+
+/**
+ * 微信公众号相关常量
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+public class WxMpConstant {
+
+    /**
+     * 点击菜单 key
+     */
+    public static final String CLICK_MENU_KEY = "CLICK_MENU_KEY";
+
+}

+ 61 - 0
src/main/java/com/yupi/yuso/wxmp/WxMpMsgRouter.java

@@ -0,0 +1,61 @@
+package com.yupi.yuso.wxmp;
+
+import com.yupi.yuso.wxmp.handler.EventHandler;
+import com.yupi.yuso.wxmp.handler.MessageHandler;
+import com.yupi.yuso.wxmp.handler.SubscribeHandler;
+import javax.annotation.Resource;
+import me.chanjar.weixin.common.api.WxConsts.EventType;
+import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 微信公众号路由
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@Configuration
+public class WxMpMsgRouter {
+
+    @Resource
+    private WxMpService wxMpService;
+
+    @Resource
+    private EventHandler eventHandler;
+
+    @Resource
+    private MessageHandler messageHandler;
+
+    @Resource
+    private SubscribeHandler subscribeHandler;
+
+    @Bean
+    public WxMpMessageRouter getWxMsgRouter() {
+        WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
+        // 消息
+        router.rule()
+                .async(false)
+                .msgType(XmlMsgType.TEXT)
+                .handler(messageHandler)
+                .end();
+        // 关注
+        router.rule()
+                .async(false)
+                .msgType(XmlMsgType.EVENT)
+                .event(EventType.SUBSCRIBE)
+                .handler(subscribeHandler)
+                .end();
+        // 点击按钮
+        router.rule()
+                .async(false)
+                .msgType(XmlMsgType.EVENT)
+                .event(EventType.CLICK)
+                .eventKey(WxMpConstant.CLICK_MENU_KEY)
+                .handler(eventHandler)
+                .end();
+        return router;
+    }
+}

+ 31 - 0
src/main/java/com/yupi/yuso/wxmp/handler/EventHandler.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.wxmp.handler;
+
+import java.util.Map;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 事件处理器
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@Component
+public class EventHandler implements WxMpMessageHandler {
+
+    @Override
+    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService,
+            WxSessionManager wxSessionManager) throws WxErrorException {
+        final String content = "您点击了菜单";
+        // 调用接口,返回验证码
+        return WxMpXmlOutMessage.TEXT().content(content)
+                .fromUser(wxMpXmlMessage.getToUser())
+                .toUser(wxMpXmlMessage.getFromUser())
+                .build();
+    }
+}

+ 30 - 0
src/main/java/com/yupi/yuso/wxmp/handler/MessageHandler.java

@@ -0,0 +1,30 @@
+package com.yupi.yuso.wxmp.handler;
+
+import java.util.Map;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 消息处理器
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@Component
+public class MessageHandler implements WxMpMessageHandler {
+
+    @Override
+    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
+            WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
+        String content = "我是复读机:" + wxMpXmlMessage.getContent();
+        return WxMpXmlOutMessage.TEXT().content(content)
+                .fromUser(wxMpXmlMessage.getToUser())
+                .toUser(wxMpXmlMessage.getFromUser())
+                .build();
+    }
+}

+ 31 - 0
src/main/java/com/yupi/yuso/wxmp/handler/SubscribeHandler.java

@@ -0,0 +1,31 @@
+package com.yupi.yuso.wxmp.handler;
+
+import java.util.Map;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 关注处理器
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ **/
+@Component
+public class SubscribeHandler implements WxMpMessageHandler {
+
+    @Override
+    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
+            WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
+        final String content = "感谢关注";
+        // 调用接口,返回验证码
+        return WxMpXmlOutMessage.TEXT().content(content)
+                .fromUser(wxMpXmlMessage.getToUser())
+                .toUser(wxMpXmlMessage.getFromUser())
+                .build();
+    }
+}

+ 34 - 0
src/main/resources/META-INF/additional-spring-configuration-metadata.json

@@ -0,0 +1,34 @@
+{
+  "properties": [
+    {
+      "name": "cos.client.accessKey",
+      "type": "java.lang.String",
+      "description": "Description for cos.client.accessKey."
+    },
+    {
+      "name": "cos.client.secretKey",
+      "type": "java.lang.String",
+      "description": "Description for cos.client.secretKey."
+    },
+    {
+      "name": "cos.client.region",
+      "type": "java.lang.String",
+      "description": "Description for cos.client.region."
+    },
+    {
+      "name": "cos.client.bucket",
+      "type": "java.lang.String",
+      "description": "Description for cos.client.bucket."
+    },
+    {
+      "name": "wx.open.appId",
+      "type": "java.lang.String",
+      "description": "Description for wx.open.appId."
+    },
+    {
+      "name": "wx.open.appSecret",
+      "type": "java.lang.String",
+      "description": "Description for wx.open.appSecret."
+    }
+  ]
+}

+ 28 - 0
src/main/resources/application-prod.yml

@@ -0,0 +1,28 @@
+server:
+  port: 8102
+spring:
+  # 数据库配置
+  # todo 需替换配置
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:3306/my_db
+    username: root
+    password: 123456
+  # Redis 配置
+  # todo 需替换配置
+  redis:
+    database: 1
+    host: localhost
+    port: 6379
+    timeout: 5000
+    password: 123456
+  # Elasticsearch 配置
+  # todo 需替换配置
+  elasticsearch:
+    uris: http://localhost:9200
+    username: root
+    password: 123456
+mybatis-plus:
+  configuration:
+    # 生产环境关闭日志
+    log-impl: ''

+ 24 - 0
src/main/resources/application-test.yml

@@ -0,0 +1,24 @@
+server:
+  port: 8102
+spring:
+  # 数据库配置
+  # todo 需替换配置
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:3306/my_db
+    username: root
+    password: 123456
+  # Redis 配置
+  # todo 需替换配置
+  redis:
+    database: 1
+    host: localhost
+    port: 6379
+    timeout: 5000
+    password: 123456
+  # Elasticsearch 配置
+  # todo 需替换配置
+  elasticsearch:
+    uris: http://localhost:9200
+    username: root
+    password: 123456

+ 89 - 0
src/main/resources/application.yml

@@ -0,0 +1,89 @@
+spring:
+  application:
+    name: yuso-backend
+  # 默认 dev 环境
+  profiles:
+    active: dev
+  # 支持 swagger3
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
+  # session 配置
+  session:
+    # todo 取消注释开启分布式 session(须先配置 Redis)
+    # store-type: redis
+    # 30 天过期
+    timeout: 2592000
+  # 数据库配置
+  # todo 需替换配置
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:3306/my_db
+    username: root
+    password: 123456
+  # Redis 配置
+  # todo 需替换配置,然后取消注释
+#  redis:
+#    database: 1
+#    host: localhost
+#    port: 6379
+#    timeout: 5000
+#    password: 123456
+  # Elasticsearch 配置
+  # todo 需替换配置,然后取消注释
+#  elasticsearch:
+#    uris: http://localhost:9200
+#    username: root
+#    password: 123456
+  # 文件上传
+  servlet:
+    multipart:
+      # 大小限制
+      max-file-size: 10MB
+server:
+  address: 0.0.0.0
+  port: 8102
+  servlet:
+    context-path: /api
+    # cookie 30 天过期
+    session:
+      cookie:
+        max-age: 2592000
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: false
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+  global-config:
+    db-config:
+      logic-delete-field: isDelete # 全局逻辑删除的实体字段名
+      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
+      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
+# 微信相关
+wx:
+  # 微信公众平台
+  # todo 需替换配置
+  mp:
+    token: xxx
+    aesKey: xxx
+    appId: xxx
+    secret: xxx
+    config-storage:
+      http-client-type: HttpClient
+      key-prefix: wx
+      redis:
+        host: 127.0.0.1
+        port: 6379
+      type: Memory
+  # 微信开放平台
+  # todo 需替换配置
+  open:
+    appId: xxx
+    appSecret: xxx
+# 对象存储
+# todo 需替换配置
+cos:
+  client:
+    accessKey: xxx
+    secretKey: xxx
+    region: xxx
+    bucket: xxx

+ 2 - 0
src/main/resources/banner.txt

@@ -0,0 +1,2 @@
+by 程序员鱼皮:https://github.com/liyupi
+可能是最好的编程学习圈子:https://yupi.icu

+ 27 - 0
src/main/resources/mapper/PostFavourMapper.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.yupi.yuso.mapper.PostFavourMapper">
+
+    <resultMap id="BaseResultMap" type="com.yupi.yuso.model.entity.PostFavour">
+        <id property="id" column="id" jdbcType="BIGINT"/>
+        <result property="postId" column="postId" jdbcType="BIGINT"/>
+        <result property="userId" column="userId" jdbcType="BIGINT"/>
+        <result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
+        <result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,postId,userId,
+        createTime,updateTime
+    </sql>
+
+    <select id="listFavourPostByPage"
+            resultType="com.yupi.yuso.model.entity.Post">
+        select p.*
+        from post p
+                 join (select postId from post_favour where userId = #{favourUserId}) pf
+                      on p.id = pf.postId ${ew.customSqlSegment}
+    </select>
+</mapper>

+ 31 - 0
src/main/resources/mapper/PostMapper.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.yupi.yuso.mapper.PostMapper">
+
+    <resultMap id="BaseResultMap" type="com.yupi.yuso.model.entity.Post">
+        <id property="id" column="id" jdbcType="BIGINT"/>
+        <result property="title" column="title" jdbcType="VARCHAR"/>
+        <result property="content" column="content" jdbcType="VARCHAR"/>
+        <result property="tags" column="tags" jdbcType="VARCHAR"/>
+        <result property="thumbNum" column="thumbNum" jdbcType="BIGINT"/>
+        <result property="favourNum" column="favourNum" jdbcType="BIGINT"/>
+        <result property="userId" column="userId" jdbcType="BIGINT"/>
+        <result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
+        <result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
+        <result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,title,content,tags,
+        thumbNum,favourNum,userId,
+        createTime,updateTime,isDelete
+    </sql>
+
+    <select id="listPostWithDelete" resultType="com.yupi.yuso.model.entity.Post">
+        select *
+        from post
+        where updateTime >= #{minUpdateTime}
+    </select>
+</mapper>

+ 19 - 0
src/main/resources/mapper/PostThumbMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.yupi.yuso.mapper.PostThumbMapper">
+
+    <resultMap id="BaseResultMap" type="com.yupi.yuso.model.entity.PostThumb">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="postId" column="postId" jdbcType="BIGINT"/>
+            <result property="userId" column="userId" jdbcType="BIGINT"/>
+            <result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
+            <result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,postId,
+        userId,createTime,updateTime
+    </sql>
+</mapper>

+ 24 - 0
src/main/resources/mapper/UserMapper.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.yupi.yuso.mapper.UserMapper">
+    <resultMap id="BaseResultMap" type="com.yupi.yuso.model.entity.User">
+        <id property="id" column="id" jdbcType="BIGINT"/>
+        <result property="unionId" column="unionId" jdbcType="VARCHAR"/>
+        <result property="mpOpenId" column="mpOpenId" jdbcType="VARCHAR"/>
+        <result property="userName" column="userName" jdbcType="VARCHAR"/>
+        <result property="userAvatar" column="userAvatar" jdbcType="VARCHAR"/>
+        <result property="userProfile" column="userProfile" jdbcType="VARCHAR"/>
+        <result property="userRole" column="userRole" jdbcType="VARCHAR"/>
+        <result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
+        <result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
+        <result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,unionId,mpOpenId,
+        userName,userAvatar,userProfile,
+        userRole,createTime,updateTime,isDelete
+    </sql>
+</mapper>

BIN
src/main/resources/test_excel.xlsx


+ 25 - 0
src/test/java/com/yupi/yuso/MainApplicationTests.java

@@ -0,0 +1,25 @@
+package com.yupi.yuso;
+
+import com.yupi.yuso.config.WxOpenConfig;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 主类测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+class MainApplicationTests {
+
+    @Resource
+    private WxOpenConfig wxOpenConfig;
+
+    @Test
+    void contextLoads() {
+        System.out.println(wxOpenConfig);
+    }
+
+}

+ 83 - 0
src/test/java/com/yupi/yuso/esdao/PostEsDaoTest.java

@@ -0,0 +1,83 @@
+package com.yupi.yuso.esdao;
+
+import com.yupi.yuso.model.dto.post.PostEsDTO;
+import com.yupi.yuso.model.dto.post.PostQueryRequest;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.service.PostService;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+
+/**
+ * 帖子 ES 操作测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+public class PostEsDaoTest {
+
+    @Resource
+    private PostEsDao postEsDao;
+
+    @Resource
+    private PostService postService;
+
+    @Test
+    void test() {
+        PostQueryRequest postQueryRequest = new PostQueryRequest();
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<Post> page =
+                postService.searchFromEs(postQueryRequest);
+        System.out.println(page);
+    }
+
+    @Test
+    void testSelect() {
+        System.out.println(postEsDao.count());
+        Page<PostEsDTO> PostPage = postEsDao.findAll(
+                PageRequest.of(0, 5, Sort.by("createTime")));
+        List<PostEsDTO> postList = PostPage.getContent();
+        System.out.println(postList);
+    }
+
+    @Test
+    void testAdd() {
+        PostEsDTO postEsDTO = new PostEsDTO();
+        postEsDTO.setId(1L);
+        postEsDTO.setTitle("test");
+        postEsDTO.setContent("test");
+        postEsDTO.setTags(Arrays.asList("java", "python"));
+        postEsDTO.setThumbNum(1);
+        postEsDTO.setFavourNum(1);
+        postEsDTO.setUserId(1L);
+        postEsDTO.setCreateTime(new Date());
+        postEsDTO.setUpdateTime(new Date());
+        postEsDTO.setIsDelete(0);
+        postEsDao.save(postEsDTO);
+        System.out.println(postEsDTO.getId());
+    }
+
+    @Test
+    void testFindById() {
+        Optional<PostEsDTO> postEsDTO = postEsDao.findById(1L);
+        System.out.println(postEsDTO);
+    }
+
+    @Test
+    void testCount() {
+        System.out.println(postEsDao.count());
+    }
+
+    @Test
+    void testFindByCategory() {
+        List<PostEsDTO> postEsDaoTestList = postEsDao.findByUserId(1L);
+        System.out.println(postEsDaoTestList);
+    }
+}

+ 23 - 0
src/test/java/com/yupi/yuso/manager/CosManagerTest.java

@@ -0,0 +1,23 @@
+package com.yupi.yuso.manager;
+
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * Cos 操作测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+class CosManagerTest {
+
+    @Resource
+    private CosManager cosManager;
+
+    @Test
+    void putObject() {
+        cosManager.putObject("test", "test.json");
+    }
+}

+ 33 - 0
src/test/java/com/yupi/yuso/mapper/PostFavourMapperTest.java

@@ -0,0 +1,33 @@
+package com.yupi.yuso.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yupi.yuso.model.entity.Post;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 帖子收藏数据库操作测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+class PostFavourMapperTest {
+
+    @Resource
+    private PostFavourMapper postFavourMapper;
+
+    @Test
+    void listUserFavourPostByPage() {
+        IPage<Post> page = new Page<>(2, 1);
+        QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("id", 1);
+        queryWrapper.like("content", "a");
+        IPage<Post> result = postFavourMapper.listFavourPostByPage(page, queryWrapper, 1);
+        Assertions.assertNotNull(result);
+    }
+}

+ 28 - 0
src/test/java/com/yupi/yuso/mapper/PostMapperTest.java

@@ -0,0 +1,28 @@
+package com.yupi.yuso.mapper;
+
+import com.yupi.yuso.model.entity.Post;
+import java.util.Date;
+import java.util.List;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 帖子数据库操作测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+class PostMapperTest {
+
+    @Resource
+    private PostMapper postMapper;
+
+    @Test
+    void listPostWithDelete() {
+        List<Post> postList = postMapper.listPostWithDelete(new Date());
+        Assertions.assertNotNull(postList);
+    }
+}

+ 44 - 0
src/test/java/com/yupi/yuso/service/PostFavourServiceTest.java

@@ -0,0 +1,44 @@
+package com.yupi.yuso.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yupi.yuso.model.entity.Post;
+import com.yupi.yuso.model.entity.User;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 帖子收藏服务测试
+ *
+ * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
+ * @from <a href="https://yupi.icu">编程导航知识星球</a>
+ */
+@SpringBootTest
+class PostFavourServiceTest {
+
+    @Resource
+    private PostFavourService postFavourService;
+
+    private static final User loginUser = new User();
+
+    @BeforeAll
+    static void setUp() {
+        loginUser.setId(1L);
+    }
+
+    @Test
+    void doPostFavour() {
+        int i = postFavourService.doPostFavour(1L, loginUser);
+        Assertions.assertTrue(i >= 0);
+    }
+
+    @Test
+    void listFavourPostByPage() {
+        QueryWrapper<Post> postQueryWrapper = new QueryWrapper<>();
+        postQueryWrapper.eq("id", 1L);
+        postFavourService.listFavourPostByPage(Page.of(0, 1), postQueryWrapper, loginUser.getId());
+    }
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác