├── .dockerignore ├── src ├── main │ ├── java │ │ └── net │ │ │ └── jaggerwang │ │ │ └── sbip │ │ │ ├── adapter │ │ │ ├── cli │ │ │ │ └── .gitkeep │ │ │ ├── gui │ │ │ │ └── .gitkeep │ │ │ ├── api │ │ │ │ ├── Application.java │ │ │ │ ├── controller │ │ │ │ │ ├── IndexController.java │ │ │ │ │ ├── dto │ │ │ │ │ │ ├── MetricDTO.java │ │ │ │ │ │ ├── RoleDTO.java │ │ │ │ │ │ ├── RootDTO.java │ │ │ │ │ │ ├── PostStatDTO.java │ │ │ │ │ │ ├── FileDTO.java │ │ │ │ │ │ ├── UserDTO.java │ │ │ │ │ │ ├── UserStatDTO.java │ │ │ │ │ │ └── PostDTO.java │ │ │ │ │ ├── MetricController.java │ │ │ │ │ ├── RestExceptionHandler.java │ │ │ │ │ ├── AuthController.java │ │ │ │ │ ├── FileController.java │ │ │ │ │ ├── PostController.java │ │ │ │ │ ├── UserController.java │ │ │ │ │ └── AbstractController.java │ │ │ │ ├── config │ │ │ │ │ ├── CommonConfig.java │ │ │ │ │ ├── WebMvcConfig.java │ │ │ │ │ ├── JpaConfig.java │ │ │ │ │ ├── RedisConfig.java │ │ │ │ │ ├── SwaggerConfig.java │ │ │ │ │ └── SecurityConfig.java │ │ │ │ └── security │ │ │ │ │ ├── LoggedUser.java │ │ │ │ │ └── UserDetailsServiceImpl.java │ │ │ ├── dao │ │ │ │ ├── jpa │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── FileRepository.java │ │ │ │ │ │ ├── PostRepository.java │ │ │ │ │ │ ├── UserRoleRepository.java │ │ │ │ │ │ ├── RoleRepository.java │ │ │ │ │ │ ├── PostStatRepository.java │ │ │ │ │ │ ├── UserStatRepository.java │ │ │ │ │ │ ├── UserRepository.java │ │ │ │ │ │ ├── PostLikeRepository.java │ │ │ │ │ │ └── UserFollowRepository.java │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── PostLike.java │ │ │ │ │ │ ├── UserRole.java │ │ │ │ │ │ ├── UserFollow.java │ │ │ │ │ │ ├── Role.java │ │ │ │ │ │ ├── PostStat.java │ │ │ │ │ │ ├── User.java │ │ │ │ │ │ ├── File.java │ │ │ │ │ │ ├── UserStat.java │ │ │ │ │ │ └── Post.java │ │ │ │ │ └── converter │ │ │ │ │ │ ├── FileMetaConverter.java │ │ │ │ │ │ └── PostImageIdsConverter.java │ │ │ │ ├── PostStatDaoImpl.java │ │ │ │ ├── UserStatDaoImpl.java │ │ │ │ ├── MetricDaoImpl.java │ │ │ │ ├── FileDaoImpl.java │ │ │ │ ├── RoleDaoImpl.java │ │ │ │ ├── UserDaoImpl.java │ │ │ │ └── PostDaoImpl.java │ │ │ └── service │ │ │ │ └── LocalStorageServiceImpl.java │ │ │ ├── entity │ │ │ ├── MetricBO.java │ │ │ ├── RoleBO.java │ │ │ ├── PostStatBO.java │ │ │ ├── UserBO.java │ │ │ ├── UserStatBO.java │ │ │ ├── PostBO.java │ │ │ └── FileBO.java │ │ │ ├── usecase │ │ │ ├── exception │ │ │ │ ├── UsecaseException.java │ │ │ │ ├── NotFoundException.java │ │ │ │ ├── UnauthorizedException.java │ │ │ │ └── UnauthenticatedException.java │ │ │ ├── port │ │ │ │ ├── dao │ │ │ │ │ ├── MetricDao.java │ │ │ │ │ ├── FileDao.java │ │ │ │ │ ├── PostStatDao.java │ │ │ │ │ ├── UserStatDao.java │ │ │ │ │ ├── RoleDao.java │ │ │ │ │ ├── UserDao.java │ │ │ │ │ └── PostDao.java │ │ │ │ └── service │ │ │ │ │ └── StorageService.java │ │ │ ├── MetricUsecase.java │ │ │ ├── StatUsecase.java │ │ │ ├── FileUsecase.java │ │ │ ├── PostUsecase.java │ │ │ └── UserUsecase.java │ │ │ └── util │ │ │ ├── generator │ │ │ ├── IdGenerator.java │ │ │ └── RandomGenerator.java │ │ │ └── encoder │ │ │ ├── PasswordEncoder.java │ │ │ └── DigestEncoder.java │ └── resources │ │ ├── application.yml │ │ └── db │ │ └── migration │ │ └── mysql │ │ └── V1__Initial_create_tables.sql └── test │ ├── resources │ ├── db │ │ ├── clean-db-test.sql │ │ ├── init-db-test.sql │ │ └── migration │ │ │ └── h2 │ │ │ └── V1__Initial_create_tables.sql │ └── application-test.yml │ └── java │ └── net │ └── jaggerwang │ └── sbip │ ├── adapter │ ├── dao │ │ └── UserDaoTests.java │ └── api │ │ └── RestApiTests.java │ └── usecase │ └── UserUsecaseTests.java ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── docker-compose.repository-test.yml ├── docker-compose.usecase-test.yml ├── Dockerfile ├── .gitignore ├── settings.xml ├── sources.list ├── docker-compose.api-test.yml ├── docker-compose.yml ├── pom.xml ├── README.md ├── mvnw.cmd └── mvnw /.dockerignore: -------------------------------------------------------------------------------- 1 | .mvn/ 2 | .vscode/ 3 | target/ -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/cli/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/gui/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/db/clean-db-test.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM `user`; -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/spring-boot-in-practice/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/db/init-db-test.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `user` (`id`, `username`, `password`) VALUES (1, 'jaggerwang', '$2a$10$UOCgLxghU78h4UvlZcjvIup9YrETv6tGmRjPPpMTQ.EjSRUsJzJJS'); -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/MetricBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import lombok.*; 4 | 5 | /** 6 | * @author Jagger Wang 7 | */ 8 | @Data 9 | @Builder 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class MetricBO { 13 | private Long registerCount; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/exception/UsecaseException.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.exception; 2 | 3 | /** 4 | * @author Jagger Wang 5 | */ 6 | public class UsecaseException extends RuntimeException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | public UsecaseException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.exception; 2 | 3 | /** 4 | * @author Jagger Wang 5 | */ 6 | public class NotFoundException extends UsecaseException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | public NotFoundException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/exception/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.exception; 2 | 3 | 4 | /** 5 | * @author Jagger Wang 6 | */ 7 | public class UnauthorizedException extends UsecaseException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | public UnauthorizedException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/exception/UnauthenticatedException.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.exception; 2 | 3 | 4 | /** 5 | * @author Jagger Wang 6 | */ 7 | public class UnauthenticatedException extends UsecaseException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | public UnauthenticatedException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.repository-test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | test: 4 | image: maven:3-jdk-11 5 | command: bash -c "cd /app && cp sources.list /etc/apt/ && cp settings.xml /root/.m2/ && mvn -Dtest.dao.enabled=true test" 6 | environment: 7 | TZ: Asia/Shanghai 8 | SBIP_DEBUG: 'false' 9 | SBIP_LOGGING_LEVEL_REQUEST: INFO 10 | volumes: 11 | - ~/.m2:/root/.m2 12 | - ./:/app 13 | -------------------------------------------------------------------------------- /docker-compose.usecase-test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | test: 4 | image: maven:3-jdk-11 5 | command: bash -c "cd /app && cp sources.list /etc/apt/ && cp settings.xml /root/.m2/ && mvn -Dtest.usecase.enabled=true test" 6 | environment: 7 | TZ: Asia/Shanghai 8 | SBIP_DEBUG: 'false' 9 | SBIP_LOGGING_LEVEL_REQUEST: INFO 10 | volumes: 11 | - ~/.m2:/root/.m2 12 | - ./:/app 13 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: ${SBIP_SPRING_DATASOURCE_URL:jdbc:mysql://localhost/sbip_test} 4 | username: ${SBIP_SPRING_DATASOURCE_USERNAME:root} 5 | password: ${SBIP_SPRING_DATASOURCE_PASSWORD:} 6 | redis: 7 | host: ${SBIP_SPRING_REDIS_HOST:localhost} 8 | port: ${SBIP_SPRING_REDIS_PORT:6379} 9 | password: ${SBIP_SPRING_REDIS_PASSWORD:} 10 | database: 1 -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/util/generator/IdGenerator.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.util.generator; 2 | 3 | import java.util.UUID; 4 | import org.bson.types.ObjectId; 5 | 6 | /** 7 | * @author Jagger Wang 8 | */ 9 | public class IdGenerator { 10 | public String objectId() { 11 | return new ObjectId().toHexString(); 12 | } 13 | 14 | public String uuid() { 15 | return UUID.randomUUID().toString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/RoleBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class RoleBO { 15 | private Long id; 16 | 17 | private String name; 18 | 19 | private LocalDateTime createdAt; 20 | 21 | private LocalDateTime updatedAt; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/Application.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author Jagger Wang 8 | */ 9 | @SpringBootApplication(scanBasePackages = "net.jaggerwang.sbip") 10 | public class Application { 11 | public static void main(String[] args) { 12 | SpringApplication.run(Application.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3-jdk-11 AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY sources.list /etc/apt/ 6 | COPY settings.xml /root/.m2/ 7 | COPY pom.xml . 8 | RUN mvn dependency:go-offline 9 | 10 | COPY . . 11 | RUN mvn package 12 | 13 | FROM openjdk:11-jre 14 | COPY --from=build /app/target/spring-boot-in-practice-1.0.0-SNAPSHOT.jar /app.jar 15 | 16 | VOLUME [ "/data" ] 17 | EXPOSE 8080 18 | RUN mkdir -p /data/log /data/tmp /data/upload 19 | CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | ### MacOS ### 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/PostStatBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class PostStatBO { 15 | private Long id; 16 | 17 | private Long postId; 18 | 19 | @Builder.Default 20 | private Long likeCount = 0L; 21 | 22 | private LocalDateTime createdAt; 23 | 24 | private LocalDateTime updatedAt; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/MetricDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import net.jaggerwang.sbip.entity.MetricBO; 4 | 5 | /** 6 | * @author Jagger Wang 7 | */ 8 | public interface MetricDao { 9 | /** 10 | * 更新某个指标的值 11 | * @param name 指标名 12 | * @param amount 增量,可以为负数 13 | * @return 更新后的值 14 | */ 15 | Long increment(String name, Long amount); 16 | 17 | /** 18 | * 查询所有指标的当前值 19 | * @return 所有指标及其当前值 20 | */ 21 | MetricBO get(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/FileRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.File; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | @Repository 12 | public interface FileRepository extends JpaRepository, QuerydslPredicateExecutor { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.Post; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | @Repository 12 | public interface PostRepository extends JpaRepository, QuerydslPredicateExecutor { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/service/StorageService.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.service; 2 | 3 | import java.io.IOException; 4 | 5 | import net.jaggerwang.sbip.entity.FileBO; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | public interface StorageService { 11 | /** 12 | * 保存内容到文件 13 | * @param path 存放目录 14 | * @param content 存储内容 15 | * @param meta 元信息 16 | * @return 文件路径 17 | * @throws IOException 18 | */ 19 | String store(String path, byte[] content, FileBO.Meta meta) throws IOException; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/util/encoder/PasswordEncoder.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.util.encoder; 2 | 3 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 4 | 5 | /** 6 | * @author Jagger Wang 7 | */ 8 | public class PasswordEncoder { 9 | public String encode(String password) { 10 | return new BCryptPasswordEncoder().encode(password); 11 | } 12 | 13 | public Boolean matches(String rawPassword, String encodedPassword) { 14 | return new BCryptPasswordEncoder().matches(rawPassword, encodedPassword); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/UserRoleRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.UserRole; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | @Repository 12 | public interface UserRoleRepository extends JpaRepository, QuerydslPredicateExecutor { 13 | } 14 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | aliyun 9 | 阿里云公共仓库 10 | central,jcenter,google,spring 11 | https://maven.aliyun.com/repository/public 12 | 13 | 14 | -------------------------------------------------------------------------------- /sources.list: -------------------------------------------------------------------------------- 1 | deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib 2 | deb-src http://mirrors.aliyun.com/debian/ stretch main non-free contrib 3 | deb http://mirrors.aliyun.com/debian-security stretch/updates main 4 | deb-src http://mirrors.aliyun.com/debian-security stretch/updates main 5 | deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib 6 | deb-src http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib 7 | deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib 8 | deb-src http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 6 | import org.springframework.stereotype.Repository; 7 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.Role; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Repository 13 | public interface RoleRepository extends JpaRepository, QuerydslPredicateExecutor { 14 | Optional findByName(String name); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/UserBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class UserBO { 15 | private Long id; 16 | 17 | private String username; 18 | 19 | private String password; 20 | 21 | private String mobile; 22 | 23 | private String email; 24 | 25 | private Long avatarId; 26 | 27 | private String intro; 28 | 29 | private LocalDateTime createdAt; 30 | 31 | private LocalDateTime updatedAt; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/util/encoder/DigestEncoder.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.util.encoder; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.util.Arrays; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | public class DigestEncoder { 11 | public String sha512(String content, String salt) { 12 | try { 13 | var md = MessageDigest.getInstance("SHA-512"); 14 | return Arrays.toString(md.digest((content + salt).getBytes())); 15 | } catch (NoSuchAlgorithmException e) { 16 | throw new RuntimeException(e); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/PostStatRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 6 | import org.springframework.stereotype.Repository; 7 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.PostStat; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Repository 13 | public interface PostStatRepository extends JpaRepository, QuerydslPredicateExecutor { 14 | Optional findByPostId(Long postId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/UserStatRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 6 | import org.springframework.stereotype.Repository; 7 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.UserStat; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Repository 13 | public interface UserStatRepository extends JpaRepository, QuerydslPredicateExecutor { 14 | Optional findByUserId(Long userId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.web.bind.annotation.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @RestController 11 | @RequestMapping("/") 12 | public class IndexController extends AbstractController { 13 | @GetMapping("/") 14 | public RootDTO index() { 15 | var authentication = SecurityContextHolder.getContext().getAuthentication(); 16 | return new RootDTO().addDataEntry("authentication", authentication); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/MetricUsecase.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import net.jaggerwang.sbip.entity.MetricBO; 4 | import net.jaggerwang.sbip.usecase.port.dao.MetricDao; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Component 11 | public class MetricUsecase { 12 | private final MetricDao metricDAO; 13 | 14 | public MetricUsecase(MetricDao metricDAO) { 15 | this.metricDAO = metricDAO; 16 | } 17 | 18 | public Long increment(String name, Long amount) { 19 | return metricDAO.increment(name, amount); 20 | } 21 | 22 | public MetricBO info() { 23 | return metricDAO.get(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/CommonConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Configuration(proxyBeanMethods = false) 13 | public class CommonConfig { 14 | @Bean 15 | public ObjectMapper objectMapper() { 16 | return new ObjectMapper().registerModule(new JavaTimeModule()) 17 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/UserStatBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class UserStatBO { 15 | private Long id; 16 | 17 | private Long userId; 18 | 19 | @Builder.Default 20 | private Long postCount = 0L; 21 | 22 | @Builder.Default 23 | private Long likeCount = 0L; 24 | 25 | @Builder.Default 26 | private Long followingCount = 0L; 27 | 28 | @Builder.Default 29 | private Long followerCount = 0L; 30 | 31 | private LocalDateTime createdAt; 32 | 33 | private LocalDateTime updatedAt; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/FileDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import net.jaggerwang.sbip.entity.FileBO; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | public interface FileDao { 12 | /** 13 | * 保存文件 14 | * @param fileBO 要保存的文件 15 | * @return 已保存的文件 16 | */ 17 | FileBO save(FileBO fileBO); 18 | 19 | /** 20 | * 查找指定 ID 的文件 21 | * @param id 文件 ID 22 | * @return 文件 23 | */ 24 | Optional findById(Long id); 25 | 26 | /** 27 | * 批量查找指定 ID 列表里的文件 28 | * @param ids 文件 ID 列表 29 | * @return 文件列表 30 | */ 31 | List findAllById(List ids); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/PostStatDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.Optional; 4 | 5 | import net.jaggerwang.sbip.entity.PostStatBO; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | public interface PostStatDao { 11 | /** 12 | * 保存帖子统计 13 | * @param postStatBO 要保存的帖子统计 14 | * @return 已保存的帖子统计 15 | */ 16 | PostStatBO save(PostStatBO postStatBO); 17 | 18 | /** 19 | * 查找指定 ID 的帖子统计 20 | * @param id 帖子统计 ID 21 | * @return 帖子统计 22 | */ 23 | Optional findById(Long id); 24 | 25 | /** 26 | * 查找指定帖子的统计 27 | * @param postId 帖子 ID 28 | * @return 帖子统计 29 | */ 30 | Optional findByPostId(Long postId); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/UserStatDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.Optional; 4 | 5 | import net.jaggerwang.sbip.entity.UserStatBO; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | public interface UserStatDao { 11 | /** 12 | * 保存用户统计 13 | * @param userStatBO 要保存的用户统计 14 | * @return 已保存的用户统计 15 | */ 16 | UserStatBO save(UserStatBO userStatBO); 17 | 18 | /** 19 | * 查找指定 ID 的用户统计 20 | * @param id 用户统计 ID 21 | * @return 用户统计 22 | */ 23 | Optional findById(Long id); 24 | 25 | /** 26 | * 查找指定用户的统计 27 | * @param userId 用户 ID 28 | * @return 用户统计 29 | */ 30 | Optional findByUserId(Long userId); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/security/LoggedUser.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.security; 2 | 3 | import java.util.Collection; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.User; 6 | 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | public class LoggedUser extends User { 12 | private static final long serialVersionUID = 1L; 13 | 14 | private final Long id; 15 | 16 | public LoggedUser(Long id, String username, String password, 17 | Collection authorities) { 18 | super(username, password, authorities); 19 | 20 | this.id = id; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.User; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 8 | import org.springframework.stereotype.Repository; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Repository 14 | public interface UserRepository extends JpaRepository, QuerydslPredicateExecutor { 15 | Optional findByUsername(String username); 16 | 17 | Optional findByMobile(String mobile); 18 | 19 | Optional findByEmail(String email); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/MetricDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import net.jaggerwang.sbip.entity.MetricBO; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class MetricDTO { 17 | private Long registerCount; 18 | 19 | public static MetricDTO fromBO(MetricBO metricBO) { 20 | return MetricDTO.builder().registerCount(metricBO.getRegisterCount()).build(); 21 | } 22 | 23 | public MetricBO toBO() { 24 | return MetricBO.builder().registerCount(registerCount).build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/PostBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import lombok.*; 7 | 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class PostBO { 17 | public enum Type { 18 | // 文本 19 | TEXT, 20 | // 图形 21 | IMAGE, 22 | // 视频 23 | VIDEO 24 | } 25 | 26 | private Long id; 27 | 28 | private Long userId; 29 | 30 | private Type type; 31 | 32 | private String text; 33 | 34 | private List imageIds; 35 | 36 | private Long videoId; 37 | 38 | private LocalDateTime createdAt; 39 | 40 | private LocalDateTime updatedAt; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/PostLikeRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.PostLike; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Repository 13 | public interface PostLikeRepository 14 | extends JpaRepository, QuerydslPredicateExecutor { 15 | @Transactional 16 | void deleteByUserIdAndPostId(Long userId, Long postId); 17 | 18 | Boolean existsByUserIdAndPostId(Long userId, Long postId); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | @Configuration 12 | public class WebMvcConfig implements WebMvcConfigurer { 13 | @Value("${file.upload-dir}") 14 | private String uploadDir; 15 | 16 | @Override 17 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 18 | registry.addResourceHandler("/files/**") 19 | .addResourceLocations(String.format("file:%s/", uploadDir)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/repository/UserFollowRepository.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.UserFollow; 8 | 9 | /** 10 | * @author Jagger Wang 11 | */ 12 | @Repository 13 | public interface UserFollowRepository 14 | extends JpaRepository, QuerydslPredicateExecutor { 15 | @Transactional 16 | void deleteByFollowerIdAndFollowingId(Long followerId, Long followingId); 17 | 18 | Boolean existsByFollowerIdAndFollowingId(Long followerId, Long followingId); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/MetricController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 9 | import net.jaggerwang.sbip.adapter.api.controller.dto.MetricDTO; 10 | 11 | /** 12 | * @author Jagger Wang 13 | */ 14 | @RestController 15 | @RequestMapping("/metric") 16 | @Api(tags = "Metric Apis") 17 | public class MetricController extends AbstractController { 18 | @GetMapping("/info") 19 | @ApiOperation("Get metric info") 20 | public RootDTO info() { 21 | var metric = metricUsecase.info(); 22 | 23 | return new RootDTO().addDataEntry("metric", MetricDTO.fromBO(metric)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import javax.persistence.EntityManager; 4 | import javax.persistence.PersistenceContext; 5 | 6 | import com.querydsl.jpa.impl.JPAQueryFactory; 7 | import org.springframework.boot.autoconfigure.domain.EntityScan; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 11 | 12 | /** 13 | * @author Jagger Wang 14 | */ 15 | @Configuration(proxyBeanMethods = false) 16 | @EntityScan("net.jaggerwang.sbip.adapter.dao.jpa.entity") 17 | @EnableJpaRepositories("net.jaggerwang.sbip.adapter.dao.jpa.repository") 18 | public class JpaConfig { 19 | @PersistenceContext 20 | private EntityManager entityManager; 21 | 22 | @Bean 23 | public JPAQueryFactory jpaQueryFactory() { 24 | return new JPAQueryFactory(entityManager); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.api-test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | test: 4 | image: maven:3-jdk-11 5 | command: bash -c "cd /app && cp sources.list /etc/apt/ && cp settings.xml /root/.m2/ && mvn -Dtest.api.enabled=true test" 6 | environment: 7 | TZ: Asia/Shanghai 8 | SBIP_DEBUG: 'false' 9 | SBIP_LOGGING_LEVEL_REQUEST: INFO 10 | SBIP_SPRING_DATASOURCE_URL: jdbc:mysql://mysql/sbip 11 | SBIP_SPRING_DATASOURCE_USERNAME: sbip 12 | SBIP_SPRING_DATASOURCE_PASSWORD: 123456 13 | SBIP_SPRING_REDIS_HOST: redis 14 | SBIP_SPRING_REDIS_PORT: 6379 15 | SBIP_SPRING_REDIS_PASSWORD: 16 | volumes: 17 | - ~/.m2:/root/.m2 18 | - ./:/app 19 | depends_on: 20 | - mysql 21 | - redis 22 | mysql: 23 | image: mysql:8.0 24 | environment: 25 | TZ: Asia/Shanghai 26 | MYSQL_ROOT_PASSWORD: 123456 27 | MYSQL_DATABASE: sbip 28 | MYSQL_USER: sbip 29 | MYSQL_PASSWORD: 123456 30 | redis: 31 | image: redis:5.0 32 | environment: 33 | TZ: Asia/Shanghai 34 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/RoleDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import net.jaggerwang.sbip.entity.RoleBO; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | /** 12 | * @author Jagger Wang 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class RoleDTO { 19 | private Long id; 20 | 21 | private String name; 22 | 23 | private LocalDateTime createdAt; 24 | 25 | private LocalDateTime updatedAt; 26 | 27 | public static RoleDTO fromBO(RoleBO roleBO) { 28 | return RoleDTO.builder().id(roleBO.getId()).name(roleBO.getName()) 29 | .createdAt(roleBO.getCreatedAt()).updatedAt(roleBO.getUpdatedAt()).build(); 30 | } 31 | 32 | public RoleBO toBO() { 33 | return RoleBO.builder().id(id).name(name).createdAt(createdAt).updatedAt(updatedAt) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/RootDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import lombok.Data; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | @Data 12 | public class RootDTO { 13 | private String code; 14 | 15 | private String message; 16 | 17 | private Map data; 18 | 19 | public RootDTO(String code, String message, Map data) { 20 | this.code = code; 21 | this.message = message; 22 | this.data = data; 23 | } 24 | 25 | public RootDTO(String code, String message) { 26 | this(code, message, new HashMap<>(16)); 27 | } 28 | 29 | public RootDTO(Map data) { 30 | this("ok", "", data); 31 | } 32 | 33 | public RootDTO() { 34 | this("ok", "", new HashMap<>(16)); 35 | } 36 | 37 | public RootDTO addDataEntry(String key, Object value) { 38 | data.put(key, value); 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/RoleDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import net.jaggerwang.sbip.entity.RoleBO; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | public interface RoleDao { 12 | /** 13 | * 保存角色 14 | * @param roleBO 要保存的角色 15 | * @return 已保存的角色 16 | */ 17 | RoleBO save(RoleBO roleBO); 18 | 19 | /** 20 | * 查找指定 ID 的角色 21 | * @param id 角色 ID 22 | * @return 角色 23 | */ 24 | Optional findById(Long id); 25 | 26 | /** 27 | * 查找指定名字的角色 28 | * @param name 角色名 29 | * @return 角色 30 | */ 31 | Optional findByName(String name); 32 | 33 | /** 34 | * 查找指定用户(ID)的角色列表 35 | * @param userId 用户 ID 36 | * @return 角色列表 37 | */ 38 | List rolesOfUser(Long userId); 39 | 40 | /** 41 | * 查找指定用户(用户名)的角色列表 42 | * @param username 用户名 43 | * @return 角色列表 44 | */ 45 | List rolesOfUser(String username); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/entity/FileBO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.entity; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.*; 6 | 7 | /** 8 | * @author Jagger Wang 9 | */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class FileBO { 15 | public enum Region { 16 | // 本地存储 17 | LOCAL 18 | } 19 | 20 | public enum ThumbType { 21 | // 小 22 | SMALL, 23 | // 中 24 | MIDDLE, 25 | // 大 26 | LARGE, 27 | // 超大 28 | HUGE 29 | } 30 | 31 | @Data 32 | @Builder 33 | @NoArgsConstructor 34 | @AllArgsConstructor 35 | public static class Meta { 36 | private String name; 37 | 38 | private Long size; 39 | 40 | private String type; 41 | } 42 | 43 | private Long id; 44 | 45 | private Long userId; 46 | 47 | private Region region; 48 | 49 | private String bucket; 50 | 51 | private String path; 52 | 53 | private Meta meta; 54 | 55 | private LocalDateTime createdAt; 56 | 57 | private LocalDateTime updatedAt; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/PostStatDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import net.jaggerwang.sbip.entity.PostStatBO; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class PostStatDTO { 18 | private Long id; 19 | 20 | private Long postId; 21 | 22 | private Long likeCount; 23 | 24 | private LocalDateTime createdAt; 25 | 26 | private LocalDateTime updatedAt; 27 | 28 | public static PostStatDTO fromBO(PostStatBO postStatBO) { 29 | return PostStatDTO.builder().id(postStatBO.getId()).postId(postStatBO.getPostId()) 30 | .likeCount(postStatBO.getLikeCount()).createdAt(postStatBO.getCreatedAt()) 31 | .updatedAt(postStatBO.getUpdatedAt()).build(); 32 | } 33 | 34 | public PostStatBO toBO() { 35 | return PostStatBO.builder().id(id).postId(postId).likeCount(likeCount) 36 | .createdAt(createdAt).updatedAt(updatedAt).build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import java.io.Serializable; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.serializer.RedisSerializer; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Configuration(proxyBeanMethods = false) 14 | public class RedisConfig { 15 | @Bean 16 | public RedisTemplate redisTemplate( 17 | LettuceConnectionFactory connectionFactory) { 18 | var redisTemplate = new RedisTemplate(); 19 | 20 | redisTemplate.setConnectionFactory(connectionFactory); 21 | 22 | redisTemplate.setKeySerializer(RedisSerializer.string()); 23 | redisTemplate.setValueSerializer(RedisSerializer.json()); 24 | redisTemplate.setHashKeySerializer(RedisSerializer.string()); 25 | redisTemplate.setHashValueSerializer(RedisSerializer.json()); 26 | 27 | return redisTemplate; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/PostStatDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.Optional; 4 | import org.springframework.stereotype.Component; 5 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.PostStatRepository; 6 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.PostStat; 7 | import net.jaggerwang.sbip.entity.PostStatBO; 8 | import net.jaggerwang.sbip.usecase.port.dao.PostStatDao; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Component 14 | public class PostStatDaoImpl implements PostStatDao { 15 | private PostStatRepository postStatRepository; 16 | 17 | public PostStatDaoImpl(PostStatRepository postStatRepository) { 18 | this.postStatRepository = postStatRepository; 19 | } 20 | 21 | @Override 22 | public PostStatBO save(PostStatBO postStatBO) { 23 | return postStatRepository.save(PostStat.fromBO(postStatBO)).toBO(); 24 | } 25 | 26 | @Override 27 | public Optional findById(Long id) { 28 | return postStatRepository.findById(id).map(postStat -> postStat.toBO()); 29 | } 30 | 31 | @Override 32 | public Optional findByPostId(Long postId) { 33 | return postStatRepository.findByPostId(postId).map(postStat -> postStat.toBO()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/UserStatDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.Optional; 4 | import org.springframework.stereotype.Component; 5 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.UserStatRepository; 6 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.UserStat; 7 | import net.jaggerwang.sbip.entity.UserStatBO; 8 | import net.jaggerwang.sbip.usecase.port.dao.UserStatDao; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Component 14 | public class UserStatDaoImpl implements UserStatDao { 15 | private UserStatRepository userStatRepository; 16 | 17 | public UserStatDaoImpl(UserStatRepository userStatRepository) { 18 | this.userStatRepository = userStatRepository; 19 | } 20 | 21 | @Override 22 | public UserStatBO save(UserStatBO userStatBO) { 23 | return userStatRepository.save(UserStat.fromBO(userStatBO)).toBO(); 24 | } 25 | 26 | @Override 27 | public Optional findById(Long id) { 28 | return userStatRepository.findById(id).map(userStat -> userStat.toBO()); 29 | } 30 | 31 | @Override 32 | public Optional findByUserId(Long userId) { 33 | return userStatRepository.findByUserId(userId).map(userStat -> userStat.toBO()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/service/LocalStorageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.service; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | 7 | import net.jaggerwang.sbip.util.generator.IdGenerator; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Component; 10 | import net.jaggerwang.sbip.entity.FileBO.Meta; 11 | import net.jaggerwang.sbip.usecase.port.service.StorageService; 12 | 13 | /** 14 | * @author Jagger Wang 15 | */ 16 | @Component 17 | public class LocalStorageServiceImpl implements StorageService { 18 | @Value("${file.upload-dir}") 19 | private String uploadDir; 20 | 21 | @Override 22 | public String store(String path, byte[] content, Meta meta) throws IOException { 23 | var saveDir = Paths.get(uploadDir, path); 24 | if (Files.notExists(saveDir)) { 25 | Files.createDirectories(saveDir); 26 | } 27 | 28 | var filename = 29 | new IdGenerator().objectId() + meta.getName().substring( 30 | meta.getName().lastIndexOf('.')); 31 | Files.write(saveDir.resolve(filename), content); 32 | 33 | return Paths.get(path, filename).toString(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/MetricDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.io.Serializable; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.stereotype.Component; 7 | import net.jaggerwang.sbip.entity.MetricBO; 8 | import net.jaggerwang.sbip.usecase.port.dao.MetricDao; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Component 14 | public class MetricDaoImpl implements MetricDao { 15 | private static final String KEY = "sbip:metric"; 16 | 17 | private RedisTemplate redisTemplate; 18 | private ObjectMapper objectMapper; 19 | 20 | public MetricDaoImpl(RedisTemplate redisTemplate, 21 | ObjectMapper objectMapper) { 22 | this.redisTemplate = redisTemplate; 23 | this.objectMapper = objectMapper; 24 | } 25 | 26 | @Override 27 | public Long increment(String hashKey, Long delta) { 28 | return redisTemplate.opsForHash().increment(KEY, hashKey, delta); 29 | } 30 | 31 | @Override 32 | public MetricBO get() { 33 | var map = redisTemplate.opsForHash().entries(KEY); 34 | return objectMapper.convertValue(map, MetricBO.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/PostLike.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | /** 17 | * @author Jagger Wang 18 | */ 19 | @Entity 20 | @Table(name = "post_like") 21 | @Data 22 | @Builder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class PostLike { 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | @Column(name = "user_id") 31 | private Long userId; 32 | 33 | @Column(name = "post_id") 34 | private Long postId; 35 | 36 | @Column(name = "created_at") 37 | private LocalDateTime createdAt; 38 | 39 | @Column(name = "updated_at") 40 | private LocalDateTime updatedAt; 41 | 42 | @PrePersist 43 | public void prePersist() { 44 | if (createdAt == null) { 45 | createdAt = LocalDateTime.now(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/UserRole.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | /** 17 | * @author Jagger Wang 18 | */ 19 | @Entity 20 | @Table(name = "user_role") 21 | @Data 22 | @Builder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class UserRole { 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | @Column(name = "user_id") 31 | private Long userId; 32 | 33 | @Column(name = "role_id") 34 | private Long roleId; 35 | 36 | @Column(name = "created_at") 37 | private LocalDateTime createdAt; 38 | 39 | @Column(name = "updated_at") 40 | private LocalDateTime updatedAt; 41 | 42 | @PrePersist 43 | public void prePersist() { 44 | if (createdAt == null) { 45 | createdAt = LocalDateTime.now(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | server: 4 | image: spring-boot-in-practice 5 | build: . 6 | environment: 7 | TZ: Asia/Shanghai 8 | SBIP_DEBUG: 'false' 9 | SBIP_LOGGING_FILE_PATH: /data/log 10 | SBIP_LOGGING_LEVEL_REQUEST: INFO 11 | SBIP_SPRING_SERVLET_MULTIPART_LOCATION: /data/tmp 12 | SBIP_SPRING_DATASOURCE_URL: jdbc:mysql://mysql/sbip 13 | SBIP_SPRING_DATASOURCE_USERNAME: sbip 14 | SBIP_SPRING_DATASOURCE_PASSWORD: 123456 15 | SBIP_SPRING_REDIS_HOST: redis 16 | SBIP_SPRING_REDIS_PORT: 6379 17 | SBIP_SPRING_REDIS_PASSWORD: 18 | SBIP_FILE_UPLOAD_DIR: /data/upload 19 | SBIP_FILE_BASE_URL: http://localhost:8080/files 20 | ports: 21 | - 8080:8080 22 | volumes: 23 | - ~/data/spring-boot-in-practice/server:/data 24 | depends_on: 25 | - mysql 26 | - redis 27 | mysql: 28 | image: mysql:8.0 29 | environment: 30 | TZ: Asia/Shanghai 31 | MYSQL_ROOT_PASSWORD: 123456 32 | MYSQL_DATABASE: sbip 33 | MYSQL_USER: sbip 34 | MYSQL_PASSWORD: 123456 35 | volumes: 36 | - ~/data/spring-boot-in-practice/mysql:/var/lib/mysql 37 | redis: 38 | image: redis:5.0 39 | environment: 40 | TZ: Asia/Shanghai 41 | volumes: 42 | - ~/data/spring-boot-in-practice/redis:/data 43 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/UserFollow.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | /** 17 | * @author Jagger Wang 18 | */ 19 | @Entity 20 | @Table(name = "user_follow") 21 | @Data 22 | @Builder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class UserFollow { 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | @Column(name = "following_id") 31 | private Long followingId; 32 | 33 | @Column(name = "follower_id") 34 | private Long followerId; 35 | 36 | @Column(name = "created_at") 37 | private LocalDateTime createdAt; 38 | 39 | @Column(name = "updated_at") 40 | private LocalDateTime updatedAt; 41 | 42 | @PrePersist 43 | public void prePersist() { 44 | if (createdAt == null) { 45 | createdAt = LocalDateTime.now(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/converter/FileMetaConverter.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.converter; 2 | 3 | import javax.persistence.AttributeConverter; 4 | import javax.persistence.Converter; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import net.jaggerwang.sbip.entity.FileBO; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * @author Jagger Wang 14 | */ 15 | @Component 16 | @Converter 17 | public class FileMetaConverter implements AttributeConverter { 18 | @Autowired 19 | private ObjectMapper objectMapper; 20 | 21 | @Override 22 | public String convertToDatabaseColumn(FileBO.Meta attribute) { 23 | if (attribute == null) { 24 | return null; 25 | } 26 | 27 | try { 28 | return objectMapper.writeValueAsString(attribute); 29 | } catch (JsonProcessingException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | @Override 35 | public FileBO.Meta convertToEntityAttribute(String dbData) { 36 | try { 37 | return objectMapper.readValue(dbData, FileBO.Meta.class); 38 | } catch (JsonProcessingException e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | debug: ${SBIP_DEBUG:false} 2 | 3 | logging: 4 | file: 5 | path: ${SBIP_LOGGING_FILE_PATH:/tmp} 6 | 7 | server: 8 | address: 0.0.0.0 9 | port: 8080 10 | 11 | spring: 12 | application: 13 | name: spring-boot-in-practice 14 | servlet: 15 | multipart: 16 | file-size-threshold: 10MB 17 | location: ${SBIP_SPRING_SERVLET_MULTIPART_LOCATION:/tmp} 18 | max-file-size: 100MB 19 | max-request-size: 100MB 20 | datasource: 21 | url: ${SBIP_SPRING_DATASOURCE_URL:jdbc:mysql://localhost/sbip} 22 | username: ${SBIP_SPRING_DATASOURCE_USERNAME:root} 23 | password: ${SBIP_SPRING_DATASOURCE_PASSWORD:} 24 | flyway: 25 | enabled: true 26 | locations: classpath:db/migration/{vendor} 27 | clean-disabled: true 28 | baseline-on-migrate: true 29 | redis: 30 | host: ${SBIP_SPRING_REDIS_HOST:localhost} 31 | port: ${SBIP_SPRING_REDIS_PORT:6379} 32 | password: ${SBIP_SPRING_REDIS_PASSWORD:} 33 | timeout: 1s 34 | lettuce: 35 | pool: 36 | max-wait: 1s 37 | session: 38 | timeout: 7d 39 | store-type: redis 40 | redis: 41 | namespace: sbip:session 42 | jpa: 43 | open-in-view: false 44 | 45 | management: 46 | endpoints: 47 | web: 48 | exposure: 49 | include: "*" 50 | health: 51 | probes: 52 | enabled: true 53 | 54 | file: 55 | upload-dir: ${SBIP_FILE_UPLOAD_DIR:/tmp} 56 | base-url: ${SBIP_FILE_BASE_URL:http://localhost:8080/files} -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/FileDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Map; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import net.jaggerwang.sbip.entity.FileBO; 10 | 11 | /** 12 | * @author Jagger Wang 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class FileDTO { 19 | private Long id; 20 | 21 | private Long userId; 22 | 23 | private FileBO.Region region; 24 | 25 | private String bucket; 26 | 27 | private String path; 28 | 29 | private FileBO.Meta meta; 30 | 31 | private LocalDateTime createdAt; 32 | 33 | private LocalDateTime updatedAt; 34 | 35 | private String url; 36 | 37 | private Map thumbs; 38 | 39 | public static FileDTO fromBO(FileBO fileBO) { 40 | return FileDTO.builder().id(fileBO.getId()).userId(fileBO.getUserId()).region(fileBO.getRegion()) 41 | .bucket(fileBO.getBucket()).path(fileBO.getPath()).meta(fileBO.getMeta()) 42 | .createdAt(fileBO.getCreatedAt()).updatedAt(fileBO.getUpdatedAt()).build(); 43 | } 44 | 45 | public FileBO toBO() { 46 | return FileBO.builder().id(id).userId(userId).region(region).bucket(bucket).path(path).meta(meta) 47 | .createdAt(createdAt).updatedAt(updatedAt).build(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/StatUsecase.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import java.time.LocalDateTime; 4 | import net.jaggerwang.sbip.entity.PostStatBO; 5 | import net.jaggerwang.sbip.entity.UserStatBO; 6 | import net.jaggerwang.sbip.usecase.port.dao.PostStatDao; 7 | import net.jaggerwang.sbip.usecase.port.dao.UserStatDao; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Component 14 | public class StatUsecase { 15 | private final UserStatDao userStatDAO; 16 | private final PostStatDao postStatDAO; 17 | 18 | public StatUsecase(UserStatDao userStatDAO, 19 | PostStatDao postStatDAO) { 20 | this.userStatDAO = userStatDAO; 21 | this.postStatDAO = postStatDAO; 22 | } 23 | 24 | public UserStatBO userStatInfoByUserId(Long userId) { 25 | var userStatBO = userStatDAO.findByUserId(userId); 26 | if (userStatBO.isEmpty()) { 27 | return UserStatBO.builder().id(0L).userId(userId).createdAt(LocalDateTime.now()) 28 | .build(); 29 | } 30 | 31 | return userStatBO.get(); 32 | } 33 | 34 | public PostStatBO postStatInfoByPostId(Long postId) { 35 | var postStatBO = postStatDAO.findByPostId(postId); 36 | if (postStatBO.isEmpty()) { 37 | return PostStatBO.builder().id(0L).postId(postId).createdAt(LocalDateTime.now()) 38 | .build(); 39 | } 40 | 41 | return postStatBO.get(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/converter/PostImageIdsConverter.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.converter; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import javax.persistence.AttributeConverter; 6 | import javax.persistence.Converter; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * @author Jagger Wang 14 | */ 15 | @Component 16 | @Converter 17 | public class PostImageIdsConverter implements AttributeConverter, String> { 18 | @Autowired 19 | private ObjectMapper objectMapper; 20 | 21 | @Override 22 | public String convertToDatabaseColumn(List attribute) { 23 | if (attribute == null) { 24 | return null; 25 | } 26 | 27 | try { 28 | return objectMapper.writeValueAsString(attribute); 29 | } catch (JsonProcessingException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | @Override 35 | public List convertToEntityAttribute(String dbData) { 36 | if (dbData == null) { 37 | return List.of(); 38 | } 39 | 40 | try { 41 | var meta = objectMapper.readValue(dbData, Long[].class); 42 | return Arrays.asList(meta); 43 | } catch (JsonProcessingException e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/Role.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | import net.jaggerwang.sbip.entity.RoleBO; 16 | 17 | /** 18 | * @author Jagger Wang 19 | */ 20 | @Entity 21 | @Data 22 | @Builder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class Role { 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | private String name; 31 | 32 | @Column(name = "created_at") 33 | private LocalDateTime createdAt; 34 | 35 | @Column(name = "updated_at") 36 | private LocalDateTime updatedAt; 37 | 38 | public static Role fromBO(RoleBO roleBO) { 39 | return Role.builder().id(roleBO.getId()).name(roleBO.getName()).createdAt(roleBO.getCreatedAt()) 40 | .updatedAt(roleBO.getUpdatedAt()).build(); 41 | } 42 | 43 | public RoleBO toBO() { 44 | return RoleBO.builder().id(id).name(name).createdAt(createdAt).updatedAt(updatedAt).build(); 45 | } 46 | 47 | @PrePersist 48 | public void prePersist() { 49 | if (createdAt == null) { 50 | createdAt = LocalDateTime.now(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import net.jaggerwang.sbip.entity.UserBO; 10 | 11 | /** 12 | * @author Jagger Wang 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class UserDTO { 19 | private Long id; 20 | 21 | private String username; 22 | 23 | private String password; 24 | 25 | private String mobile; 26 | 27 | private String email; 28 | 29 | private Long avatarId; 30 | 31 | private String intro; 32 | 33 | private LocalDateTime createdAt; 34 | 35 | private LocalDateTime updatedAt; 36 | 37 | private FileDTO avatar; 38 | 39 | private UserStatDTO stat; 40 | 41 | private Boolean following; 42 | 43 | public static UserDTO fromBO(UserBO userBO) { 44 | return UserDTO.builder().id(userBO.getId()).username(userBO.getUsername()) 45 | .mobile(userBO.getMobile()).email(userBO.getEmail()) 46 | .avatarId(userBO.getAvatarId()).intro(userBO.getIntro()) 47 | .createdAt(userBO.getCreatedAt()).updatedAt(userBO.getUpdatedAt()).build(); 48 | } 49 | 50 | public UserBO toBO() { 51 | return UserBO.builder().id(id).username(username).password(password).mobile(mobile) 52 | .email(email).avatarId(avatarId).intro(intro).createdAt(createdAt) 53 | .updatedAt(updatedAt).build(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/UserStatDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import net.jaggerwang.sbip.entity.UserStatBO; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Data 14 | @Builder 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class UserStatDTO { 18 | private Long id; 19 | 20 | private Long userId; 21 | 22 | private Long postCount; 23 | 24 | private Long likeCount; 25 | 26 | private Long followingCount; 27 | 28 | private Long followerCount; 29 | 30 | private LocalDateTime createdAt; 31 | 32 | private LocalDateTime updatedAt; 33 | 34 | public static UserStatDTO fromBO(UserStatBO userStatBO) { 35 | return UserStatDTO.builder().id(userStatBO.getId()).userId(userStatBO.getUserId()) 36 | .postCount(userStatBO.getPostCount()).likeCount(userStatBO.getLikeCount()) 37 | .followingCount(userStatBO.getFollowingCount()) 38 | .followerCount(userStatBO.getFollowerCount()) 39 | .createdAt(userStatBO.getCreatedAt()).updatedAt(userStatBO.getUpdatedAt()) 40 | .build(); 41 | } 42 | 43 | public UserStatBO toBO() { 44 | return UserStatBO.builder().id(id).userId(userId).postCount(postCount) 45 | .likeCount(likeCount).followingCount(followingCount).followerCount(followerCount) 46 | .createdAt(createdAt).updatedAt(updatedAt).build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.ApiInfoBuilder; 6 | import springfox.documentation.builders.PathSelectors; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.service.Contact; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 12 | 13 | /** 14 | * @author Jagger Wang 15 | */ 16 | @Configuration 17 | @EnableSwagger2 18 | public class SwaggerConfig { 19 | @Bean 20 | public Docket docket() { 21 | var apiInfo = new ApiInfoBuilder() 22 | .title("Spring Boot in Practice Api Documentation") 23 | .description("https://github.com/jaggerwang/spring-boot-in-practice") 24 | .version("1.0") 25 | .contact(new Contact("Jagger Wang", "https://blog.jaggerwang.net/", "jaggerwang@gmail.com")) 26 | .license("MIT") 27 | .licenseUrl("https://opensource.org/licenses/MIT") 28 | .build(); 29 | 30 | return new Docket(DocumentationType.SWAGGER_2) 31 | .select() 32 | .apis(RequestHandlerSelectors.basePackage("net.jaggerwang.sbip.adapter.api.controller")) 33 | .paths(PathSelectors.any()) 34 | .build() 35 | .apiInfo(apiInfo); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/dto/PostDTO.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller.dto; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import net.jaggerwang.sbip.entity.PostBO; 11 | 12 | /** 13 | * @author Jagger Wang 14 | */ 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class PostDTO { 20 | private Long id; 21 | 22 | private Long userId; 23 | 24 | private PostBO.Type type; 25 | 26 | private String text; 27 | 28 | @Builder.Default 29 | private List imageIds = List.of(); 30 | 31 | private Long videoId; 32 | 33 | private LocalDateTime createdAt; 34 | 35 | private LocalDateTime updatedAt; 36 | 37 | private UserDTO user; 38 | 39 | @Builder.Default 40 | private List images = List.of(); 41 | 42 | private FileDTO video; 43 | 44 | private PostStatDTO stat; 45 | 46 | private Boolean liked; 47 | 48 | public static PostDTO fromBO(PostBO postBO) { 49 | return PostDTO.builder().id(postBO.getId()).userId(postBO.getUserId()) 50 | .type(postBO.getType()).text(postBO.getText()) 51 | .imageIds(postBO.getImageIds()).videoId(postBO.getVideoId()) 52 | .createdAt(postBO.getCreatedAt()).updatedAt(postBO.getUpdatedAt()).build(); 53 | } 54 | 55 | public PostBO toBO() { 56 | return PostBO.builder().id(id).userId(userId).type(type).text(text).imageIds(imageIds) 57 | .videoId(videoId).createdAt(createdAt).updatedAt(updatedAt).build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/FileDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.IntStream; 8 | 9 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.File; 10 | import net.jaggerwang.sbip.entity.FileBO; 11 | import org.springframework.stereotype.Component; 12 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.FileRepository; 13 | import net.jaggerwang.sbip.usecase.port.dao.FileDao; 14 | 15 | /** 16 | * @author Jagger Wang 17 | */ 18 | @Component 19 | public class FileDaoImpl implements FileDao { 20 | private FileRepository fileRepository; 21 | 22 | public FileDaoImpl(FileRepository fileRepository) { 23 | this.fileRepository = fileRepository; 24 | } 25 | 26 | @Override 27 | public FileBO save(FileBO fileBO) { 28 | return fileRepository.save(File.fromBO(fileBO)).toBO(); 29 | } 30 | 31 | @Override 32 | public Optional findById(Long id) { 33 | return fileRepository.findById(id).map(file -> file.toBO()); 34 | } 35 | 36 | @Override 37 | public List findAllById(List ids) { 38 | var files = fileRepository.findAllById(ids).stream() 39 | .collect(Collectors.toMap(file -> file.getId(), file -> file.toBO())); 40 | 41 | var fileBOs = new FileBO[ids.size()]; 42 | IntStream.range(0, ids.size()).forEach(i -> { 43 | var id = ids.get(i); 44 | if (files.containsKey(id)) { 45 | fileBOs[i] = files.get(id); 46 | } 47 | }); 48 | 49 | return Arrays.asList(fileBOs); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/FileUsecase.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | 8 | import net.jaggerwang.sbip.entity.FileBO; 9 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 10 | import net.jaggerwang.sbip.usecase.port.dao.FileDao; 11 | import net.jaggerwang.sbip.usecase.port.service.StorageService; 12 | import org.springframework.stereotype.Component; 13 | 14 | /** 15 | * @author Jagger Wang 16 | */ 17 | @Component 18 | public class FileUsecase { 19 | private final FileDao fileDAO; 20 | private final StorageService storageService; 21 | 22 | public FileUsecase(FileDao fileDAO, StorageService storageService) { 23 | this.fileDAO = fileDAO; 24 | this.storageService = storageService; 25 | } 26 | 27 | public FileBO upload(String path, byte[] content, FileBO fileBO) { 28 | String savedPath; 29 | try { 30 | savedPath = storageService.store(path, content, fileBO.getMeta()); 31 | } catch (IOException e) { 32 | throw new UsecaseException("存储文件出错"); 33 | } 34 | 35 | var file = FileBO.builder().userId(fileBO.getUserId()) 36 | .region(fileBO.getRegion()).bucket(fileBO.getBucket()).path(savedPath) 37 | .meta(fileBO.getMeta()).build(); 38 | return fileDAO.save(file); 39 | } 40 | 41 | public Optional info(Long id) { 42 | return fileDAO.findById(id); 43 | } 44 | 45 | public List infos(List ids, Boolean keepNull) { 46 | var fileBOs = fileDAO.findAllById(ids); 47 | 48 | if (!keepNull) { 49 | fileBOs.removeIf(Objects::isNull); 50 | } 51 | 52 | return fileBOs; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/util/generator/RandomGenerator.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.util.generator; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.Optional; 5 | 6 | /** 7 | * @author Jagger Wang 8 | */ 9 | public class RandomGenerator { 10 | static final String UPPER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 11 | static final String LOWER_LETTERS = "abcdefghijklmnopqrstuvwxyz"; 12 | static final String NUMBERS = "0123456789"; 13 | static final String UPPER_HEX_CHARS = "0123456789ABCDEF"; 14 | static final String LOWER_HEX_CHARS = "0123456789abcdef"; 15 | 16 | static final SecureRandom RANDOM = new SecureRandom(); 17 | 18 | public String randomString(int len, String chars) { 19 | var sb = new StringBuilder(len); 20 | for (int i = 0; i < len; i++) { 21 | sb.append(chars.charAt(RANDOM.nextInt(chars.length()))); 22 | } 23 | return sb.toString(); 24 | } 25 | 26 | public String letterString(int len, Boolean upper) { 27 | var chars = UPPER_LETTERS + LOWER_LETTERS; 28 | if (upper != null) { 29 | chars = upper ? UPPER_LETTERS : LOWER_LETTERS; 30 | } 31 | return randomString(len, chars); 32 | } 33 | 34 | public String letterNumberString(int len, Boolean upper) { 35 | var chars = UPPER_LETTERS + LOWER_LETTERS; 36 | if (upper != null) { 37 | chars = upper ? UPPER_LETTERS : LOWER_LETTERS; 38 | } 39 | chars += NUMBERS; 40 | return randomString(len, chars); 41 | } 42 | 43 | public String numberString(int len) { 44 | return randomString(len, NUMBERS); 45 | } 46 | 47 | public String hexString(int len, Boolean upper) { 48 | var chars = upper ? UPPER_HEX_CHARS : LOWER_HEX_CHARS; 49 | return randomString(len, chars); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/PostStat.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | import net.jaggerwang.sbip.entity.PostStatBO; 16 | 17 | /** 18 | * @author Jagger Wang 19 | */ 20 | @Entity 21 | @Table(name = "post_stat") 22 | @Data 23 | @Builder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class PostStat { 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Long id; 30 | 31 | @Column(name = "post_id") 32 | private Long postId; 33 | 34 | @Column(name = "like_count") 35 | private Long likeCount; 36 | 37 | @Column(name = "created_at") 38 | private LocalDateTime createdAt; 39 | 40 | @Column(name = "updated_at") 41 | private LocalDateTime updatedAt; 42 | 43 | public static PostStat fromBO(PostStatBO postStatBO) { 44 | return PostStat.builder().id(postStatBO.getId()).postId(postStatBO.getPostId()) 45 | .likeCount(postStatBO.getLikeCount()).createdAt(postStatBO.getCreatedAt()) 46 | .updatedAt(postStatBO.getUpdatedAt()).build(); 47 | } 48 | 49 | public PostStatBO toBO() { 50 | return PostStatBO.builder().id(id).postId(postId).likeCount(likeCount) 51 | .createdAt(createdAt).updatedAt(updatedAt).build(); 52 | } 53 | 54 | @PrePersist 55 | public void prePersist() { 56 | if (createdAt == null) { 57 | createdAt = LocalDateTime.now(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/net/jaggerwang/sbip/adapter/dao/UserDaoTests.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import com.querydsl.jpa.impl.JPAQueryFactory; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.UserFollowRepository; 12 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.UserRepository; 13 | import net.jaggerwang.sbip.adapter.api.config.CommonConfig; 14 | import net.jaggerwang.sbip.adapter.api.config.JpaConfig; 15 | import net.jaggerwang.sbip.entity.UserBO; 16 | import net.jaggerwang.sbip.usecase.port.dao.UserDao; 17 | 18 | @DataJpaTest 19 | @ContextConfiguration(classes = {CommonConfig.class, JpaConfig.class}) 20 | @EnabledIfSystemProperty(named = "test.dao.enabled", matches = "true") 21 | public class UserDaoTests { 22 | @Autowired 23 | private JPAQueryFactory jpaQueryFactory; 24 | 25 | @Autowired 26 | private UserRepository userRepository; 27 | 28 | @Autowired 29 | private UserFollowRepository userFollowRepository; 30 | 31 | private UserDao userDAO; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | userDAO = 36 | new UserDaoImpl(jpaQueryFactory, userRepository, userFollowRepository); 37 | } 38 | 39 | @Test 40 | void save() { 41 | var userBO = UserBO.builder().username("jaggerwang").password("123456").build(); 42 | var savedUser = userDAO.save(userBO); 43 | assertThat(savedUser).hasFieldOrPropertyWithValue("username", userBO.getUsername()) 44 | .hasFieldOrPropertyWithValue("password", userBO.getPassword()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/security/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.security; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.stream.Collectors; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Service; 12 | import net.jaggerwang.sbip.entity.UserBO; 13 | import net.jaggerwang.sbip.usecase.UserUsecase; 14 | 15 | /** 16 | * @author Jagger Wang 17 | */ 18 | @Service 19 | public class UserDetailsServiceImpl implements UserDetailsService { 20 | private UserUsecase userUsecase; 21 | 22 | public UserDetailsServiceImpl(UserUsecase userUsecase) { 23 | this.userUsecase = userUsecase; 24 | } 25 | 26 | @Override 27 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 28 | Optional userBO; 29 | if (username.matches("[0-9]+")) { 30 | userBO = userUsecase.infoByMobile(username); 31 | } else if (username.matches("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+")) { 32 | userBO = userUsecase.infoByEmail(username); 33 | } else { 34 | userBO = userUsecase.infoByUsername(username); 35 | } 36 | if (userBO.isEmpty()) { 37 | throw new UsernameNotFoundException("用户未找到"); 38 | } 39 | 40 | List authorities = userUsecase.roles(username).stream() 41 | .map(v -> new SimpleGrantedAuthority("ROLE_" + v.getName())) 42 | .collect(Collectors.toList()); 43 | 44 | return new LoggedUser(userBO.get().getId(), userBO.get().getUsername(), 45 | userBO.get().getPassword(), authorities); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/net/jaggerwang/sbip/usecase/UserUsecaseTests.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import net.jaggerwang.sbip.usecase.port.dao.RoleDao; 4 | import net.jaggerwang.sbip.util.encoder.PasswordEncoder; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import static org.assertj.core.api.Assertions.*; 10 | import static org.mockito.BDDMockito.*; 11 | import java.util.Optional; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | import org.springframework.test.context.junit.jupiter.SpringExtension; 14 | import net.jaggerwang.sbip.entity.UserBO; 15 | import net.jaggerwang.sbip.usecase.port.dao.UserDao; 16 | 17 | @ExtendWith(SpringExtension.class) 18 | @EnabledIfSystemProperty(named = "test.usecase.enabled", matches = "true") 19 | public class UserUsecaseTests { 20 | private UserUsecase userUsecase; 21 | 22 | @MockBean 23 | private UserDao userDAO; 24 | 25 | @MockBean 26 | private RoleDao roleDAO; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | userUsecase = new UserUsecase(userDAO, roleDAO); 31 | } 32 | 33 | @Test 34 | void register() { 35 | var passwordEncoder = new PasswordEncoder(); 36 | 37 | var userBO = UserBO.builder().username("jaggerwang").password("123456").build(); 38 | given(userDAO.findByUsername(userBO.getUsername())).willReturn(Optional.empty()); 39 | 40 | var savedUser = UserBO.builder().username(userBO.getUsername()) 41 | .password(passwordEncoder.encode(userBO.getPassword())).build(); 42 | given(userDAO.save(any(UserBO.class))).willReturn(savedUser); 43 | 44 | var registeredUser = userUsecase.register(userBO); 45 | assertThat(registeredUser).hasFieldOrPropertyWithValue("username", userBO.getUsername()) 46 | .hasFieldOrProperty("password"); 47 | assertThat(registeredUser.getPassword()).isNotEqualTo(userBO.getPassword()); 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/User.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | import net.jaggerwang.sbip.entity.UserBO; 16 | 17 | /** 18 | * @author Jagger Wang 19 | */ 20 | @Entity 21 | @Data 22 | @Builder 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public class User { 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | private String username; 31 | 32 | private String password; 33 | 34 | private String mobile; 35 | 36 | private String email; 37 | 38 | @Column(name = "avatar_id") 39 | private Long avatarId; 40 | 41 | private String intro; 42 | 43 | @Column(name = "created_at") 44 | private LocalDateTime createdAt; 45 | 46 | @Column(name = "updated_at") 47 | private LocalDateTime updatedAt; 48 | 49 | public static User fromBO(UserBO userBO) { 50 | return User.builder().id(userBO.getId()).username(userBO.getUsername()) 51 | .password(userBO.getPassword()).mobile(userBO.getMobile()).email(userBO.getEmail()) 52 | .avatarId(userBO.getAvatarId()).intro(userBO.getIntro()).createdAt(userBO.getCreatedAt()) 53 | .updatedAt(userBO.getUpdatedAt()).build(); 54 | } 55 | 56 | public UserBO toBO() { 57 | return UserBO.builder().id(id).username(username).password(password).mobile(mobile).email(email) 58 | .avatarId(avatarId).intro(intro).createdAt(createdAt).updatedAt(updatedAt).build(); 59 | } 60 | 61 | @PrePersist 62 | public void prePersist() { 63 | if (intro == null) { 64 | intro = ""; 65 | } 66 | if (createdAt == null) { 67 | createdAt = LocalDateTime.now(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.security.access.AccessDeniedException; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 7 | import net.jaggerwang.sbip.usecase.exception.UnauthenticatedException; 8 | import net.jaggerwang.sbip.usecase.exception.UnauthorizedException; 9 | import net.jaggerwang.sbip.usecase.exception.NotFoundException; 10 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | 14 | /** 15 | * @author Jagger Wang 16 | */ 17 | @RestControllerAdvice 18 | public class RestExceptionHandler { 19 | @ExceptionHandler(Throwable.class) 20 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 21 | public RootDTO handle(Throwable throwable) { 22 | throwable.printStackTrace(); 23 | return new RootDTO("fail", throwable.toString()); 24 | } 25 | 26 | @ExceptionHandler(UsecaseException.class) 27 | @ResponseStatus(HttpStatus.OK) 28 | public RootDTO handle(UsecaseException exception) { 29 | return new RootDTO("fail", exception.getMessage()); 30 | } 31 | 32 | @ExceptionHandler(NotFoundException.class) 33 | @ResponseStatus(HttpStatus.NOT_FOUND) 34 | public RootDTO handle(NotFoundException exception) { 35 | return new RootDTO("not_found", exception.getMessage()); 36 | } 37 | 38 | @ExceptionHandler(UnauthenticatedException.class) 39 | @ResponseStatus(HttpStatus.UNAUTHORIZED) 40 | public RootDTO handle(UnauthenticatedException exception) { 41 | return new RootDTO("unauthenticated", exception.getMessage()); 42 | } 43 | 44 | @ExceptionHandler({UnauthorizedException.class, AccessDeniedException.class}) 45 | @ResponseStatus(HttpStatus.FORBIDDEN) 46 | public RootDTO handle(UnauthorizedException exception) { 47 | return new RootDTO("unauthorized", exception.getMessage()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/PostUsecase.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import net.jaggerwang.sbip.entity.PostBO; 7 | import net.jaggerwang.sbip.usecase.port.dao.PostDao; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * @author Jagger Wang 12 | */ 13 | @Component 14 | public class PostUsecase { 15 | private final PostDao postDAO; 16 | 17 | public PostUsecase(PostDao postDAO) { 18 | this.postDAO = postDAO; 19 | } 20 | 21 | public PostBO publish(PostBO postBO) { 22 | var post = PostBO.builder().userId(postBO.getUserId()).type(postBO.getType()) 23 | .text(postBO.getText()).imageIds(postBO.getImageIds()) 24 | .videoId(postBO.getVideoId()).build(); 25 | return postDAO.save(post); 26 | } 27 | 28 | public void delete(Long id) { 29 | postDAO.delete(id); 30 | } 31 | 32 | public Optional info(Long id) { 33 | return postDAO.findById(id); 34 | } 35 | 36 | public List published(Long userId, Long limit, Long offset) { 37 | return postDAO.published(userId, limit, offset); 38 | } 39 | 40 | public Long publishedCount(Long userId) { 41 | return postDAO.publishedCount(userId); 42 | } 43 | 44 | public void like(Long userId, Long postId) { 45 | postDAO.like(userId, postId); 46 | } 47 | 48 | public void unlike(Long userId, Long postId) { 49 | postDAO.unlike(userId, postId); 50 | } 51 | 52 | public Boolean isLiked(Long userId, Long postId) { 53 | return postDAO.isLiked(userId, postId); 54 | } 55 | 56 | public List liked(Long userId, Long limit, Long offset) { 57 | return postDAO.liked(userId, limit, offset); 58 | } 59 | 60 | public Long likedCount(Long userId) { 61 | return postDAO.likedCount(userId); 62 | } 63 | 64 | public List following(Long userId, Long limit, Long beforeId, Long afterId) { 65 | return postDAO.following(userId, limit, beforeId, afterId); 66 | } 67 | 68 | public Long followingCount(Long userId) { 69 | return postDAO.followingCount(userId); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/File.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Convert; 6 | import javax.persistence.Entity; 7 | import javax.persistence.EnumType; 8 | import javax.persistence.Enumerated; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.PrePersist; 13 | 14 | import lombok.AllArgsConstructor; 15 | import lombok.Builder; 16 | import lombok.Data; 17 | import lombok.NoArgsConstructor; 18 | import net.jaggerwang.sbip.adapter.dao.jpa.converter.FileMetaConverter; 19 | import net.jaggerwang.sbip.entity.FileBO; 20 | 21 | /** 22 | * @author Jagger Wang 23 | */ 24 | @Entity 25 | @Data 26 | @Builder 27 | @NoArgsConstructor 28 | @AllArgsConstructor 29 | public class File { 30 | @Id 31 | @GeneratedValue(strategy = GenerationType.IDENTITY) 32 | private Long id; 33 | 34 | @Column(name = "user_id") 35 | private Long userId; 36 | 37 | @Enumerated(EnumType.STRING) 38 | private FileBO.Region region; 39 | 40 | private String bucket; 41 | 42 | private String path; 43 | 44 | @Convert(converter = FileMetaConverter.class) 45 | private FileBO.Meta meta; 46 | 47 | @Column(name = "created_at") 48 | private LocalDateTime createdAt; 49 | 50 | @Column(name = "updated_at") 51 | private LocalDateTime updatedAt; 52 | 53 | public static File fromBO(FileBO fileBO) { 54 | return File.builder().id(fileBO.getId()).userId(fileBO.getUserId()).region(fileBO.getRegion()) 55 | .bucket(fileBO.getBucket()).path(fileBO.getPath()).meta(fileBO.getMeta()) 56 | .createdAt(fileBO.getCreatedAt()).updatedAt(fileBO.getUpdatedAt()).build(); 57 | } 58 | 59 | public FileBO toBO() { 60 | return FileBO.builder().id(id).userId(userId).region(region).bucket(bucket).path(path).meta(meta) 61 | .createdAt(createdAt).updatedAt(updatedAt).build(); 62 | } 63 | 64 | @PrePersist 65 | public void prePersist() { 66 | if (createdAt == null) { 67 | createdAt = LocalDateTime.now(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/UserStat.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | import net.jaggerwang.sbip.entity.UserStatBO; 16 | 17 | /** 18 | * @author Jagger Wang 19 | */ 20 | @Entity 21 | @Table(name = "user_stat") 22 | @Data 23 | @Builder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class UserStat { 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Long id; 30 | 31 | @Column(name = "user_id") 32 | private Long userId; 33 | 34 | @Column(name = "post_count") 35 | private Long postCount; 36 | 37 | @Column(name = "like_count") 38 | private Long likeCount; 39 | 40 | @Column(name = "following_count") 41 | private Long followingCount; 42 | 43 | @Column(name = "follower_count") 44 | private Long followerCount; 45 | 46 | @Column(name = "created_at") 47 | private LocalDateTime createdAt; 48 | 49 | @Column(name = "updated_at") 50 | private LocalDateTime updatedAt; 51 | 52 | public static UserStat fromBO(UserStatBO userStatBO) { 53 | return UserStat.builder().id(userStatBO.getId()).userId(userStatBO.getUserId()) 54 | .postCount(userStatBO.getPostCount()).likeCount(userStatBO.getLikeCount()) 55 | .followingCount(userStatBO.getFollowingCount()) 56 | .followerCount(userStatBO.getFollowerCount()).createdAt(userStatBO.getCreatedAt()) 57 | .updatedAt(userStatBO.getUpdatedAt()).build(); 58 | } 59 | 60 | public UserStatBO toBO() { 61 | return UserStatBO.builder().id(id).userId(userId).postCount(postCount) 62 | .likeCount(likeCount).followingCount(followingCount).followerCount(followerCount) 63 | .createdAt(createdAt).updatedAt(updatedAt).build(); 64 | } 65 | 66 | @PrePersist 67 | public void prePersist() { 68 | if (createdAt == null) { 69 | createdAt = LocalDateTime.now(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 12 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 13 | 14 | /** 15 | * @author Jagger Wang 16 | */ 17 | @Configuration 18 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 19 | @Bean 20 | public PasswordEncoder passwordEncoder() { 21 | return new BCryptPasswordEncoder(); 22 | } 23 | 24 | @Bean("authenticationManager") 25 | @Override 26 | public AuthenticationManager authenticationManagerBean() throws Exception { 27 | return super.authenticationManagerBean(); 28 | } 29 | 30 | @Override 31 | protected void configure(HttpSecurity http) throws Exception { 32 | http 33 | .csrf(csrf -> csrf.disable()) 34 | // This config will disable form login. 35 | .exceptionHandling(exceptionHandling -> exceptionHandling 36 | .defaultAuthenticationEntryPointFor( 37 | new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), 38 | new AntPathRequestMatcher("/**")) 39 | ) 40 | .authorizeRequests(authorizeRequests -> authorizeRequests 41 | .antMatchers("/favicon.ico", "/csrf", "/vendor/**", "/webjars/**", 42 | "/actuator/**", "/v2/api-docs", "/swagger-ui.html", 43 | "/swagger-resources/**", "/", "/login", "/logout", 44 | "/auth/**", "/user/register", "/files/**").permitAll() 45 | .anyRequest().authenticated() 46 | ) 47 | .formLogin(formLogin -> {}); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/jpa/entity/Post.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao.jpa.entity; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import javax.persistence.Column; 6 | import javax.persistence.Convert; 7 | import javax.persistence.Entity; 8 | import javax.persistence.EnumType; 9 | import javax.persistence.Enumerated; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.PrePersist; 14 | 15 | import lombok.AllArgsConstructor; 16 | import lombok.Builder; 17 | import lombok.Data; 18 | import lombok.NoArgsConstructor; 19 | import net.jaggerwang.sbip.adapter.dao.jpa.converter.PostImageIdsConverter; 20 | import net.jaggerwang.sbip.entity.PostBO; 21 | 22 | /** 23 | * @author Jagger Wang 24 | */ 25 | @Entity 26 | @Data 27 | @Builder 28 | @NoArgsConstructor 29 | @AllArgsConstructor 30 | public class Post { 31 | @Id 32 | @GeneratedValue(strategy = GenerationType.IDENTITY) 33 | private Long id; 34 | 35 | @Column(name = "user_id") 36 | private Long userId; 37 | 38 | @Enumerated(EnumType.STRING) 39 | private PostBO.Type type; 40 | 41 | private String text; 42 | 43 | @Column(name = "image_ids") 44 | @Convert(converter = PostImageIdsConverter.class) 45 | private List imageIds; 46 | 47 | @Column(name = "video_id") 48 | private Long videoId; 49 | 50 | @Column(name = "created_at") 51 | private LocalDateTime createdAt; 52 | 53 | @Column(name = "updated_at") 54 | private LocalDateTime updatedAt; 55 | 56 | public static Post fromBO(PostBO postBO) { 57 | return Post.builder().id(postBO.getId()).userId(postBO.getUserId()).type(postBO.getType()) 58 | .text(postBO.getText()).imageIds(postBO.getImageIds()).videoId(postBO.getVideoId()) 59 | .createdAt(postBO.getCreatedAt()).updatedAt(postBO.getUpdatedAt()).build(); 60 | } 61 | 62 | public PostBO toBO() { 63 | return PostBO.builder().id(id).userId(userId).type(type).text(text).imageIds(imageIds).videoId(videoId) 64 | .createdAt(createdAt).updatedAt(updatedAt).build(); 65 | } 66 | 67 | @PrePersist 68 | public void prePersist() { 69 | if (text == null) { 70 | text = ""; 71 | } 72 | if (createdAt == null) { 73 | createdAt = LocalDateTime.now(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/RoleDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.stream.Collectors; 6 | import com.querydsl.jpa.impl.JPAQueryFactory; 7 | import org.springframework.stereotype.Component; 8 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.RoleRepository; 9 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QRole; 10 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QUser; 11 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QUserRole; 12 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.Role; 13 | import net.jaggerwang.sbip.entity.RoleBO; 14 | import net.jaggerwang.sbip.usecase.port.dao.RoleDao; 15 | 16 | /** 17 | * @author Jagger Wang 18 | */ 19 | @Component 20 | public class RoleDaoImpl implements RoleDao { 21 | private JPAQueryFactory jpaQueryFactory; 22 | private RoleRepository roleRepository; 23 | 24 | public RoleDaoImpl(JPAQueryFactory jpaQueryFactory, RoleRepository roleRepository) { 25 | this.jpaQueryFactory = jpaQueryFactory; 26 | this.roleRepository = roleRepository; 27 | } 28 | 29 | @Override 30 | public RoleBO save(RoleBO roleBO) { 31 | return roleRepository.save(Role.fromBO(roleBO)).toBO(); 32 | } 33 | 34 | @Override 35 | public Optional findById(Long id) { 36 | return roleRepository.findById(id).map(role -> role.toBO()); 37 | } 38 | 39 | @Override 40 | public Optional findByName(String name) { 41 | return roleRepository.findByName(name).map(role -> role.toBO()); 42 | } 43 | 44 | @Override 45 | public List rolesOfUser(Long userId) { 46 | var query = jpaQueryFactory.selectFrom(QRole.role) 47 | .join(QUserRole.userRole).on(QRole.role.id.eq(QUserRole.userRole.roleId)) 48 | .where(QUserRole.userRole.userId.eq(userId)); 49 | return query.fetch().stream().map(role -> role.toBO()).collect(Collectors.toList()); 50 | } 51 | 52 | @Override 53 | public List rolesOfUser(String username) { 54 | var query = jpaQueryFactory.selectFrom(QRole.role) 55 | .join(QUserRole.userRole).on(QRole.role.id.eq(QUserRole.userRole.roleId)) 56 | .join(QUser.user).on(QUser.user.id.eq(QUserRole.userRole.userId)) 57 | .where(QUser.user.username.eq(username)); 58 | return query.fetch().stream().map(role -> role.toBO()).collect(Collectors.toList()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 6 | import net.jaggerwang.sbip.adapter.api.controller.dto.UserDTO; 7 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 8 | import org.springframework.util.StringUtils; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | /** 12 | * @author Jagger Wang 13 | */ 14 | @RestController 15 | @RequestMapping("/auth") 16 | @Api(tags = "User Apis") 17 | public class AuthController extends AbstractController { 18 | @PostMapping("/login") 19 | @ApiOperation("Login") 20 | public RootDTO login(@RequestBody UserDTO userDto) { 21 | String username = null; 22 | if (userDto.getUsername() != null) { 23 | username = userDto.getUsername(); 24 | } else if (userDto.getMobile() != null) { 25 | username = userDto.getMobile(); 26 | } else if (userDto.getEmail() != null) { 27 | username = userDto.getEmail(); 28 | } 29 | if (StringUtils.isEmpty(username)) { 30 | throw new UsecaseException("用户名、手机或邮箱不能都为空"); 31 | } 32 | var password = userDto.getPassword(); 33 | if (StringUtils.isEmpty(password)) { 34 | throw new UsecaseException("密码不能为空"); 35 | } 36 | 37 | var loggedUser = loginUser(username, password); 38 | 39 | var userBO = userUsecase.info(loggedUser.getId()); 40 | return new RootDTO().addDataEntry("user", UserDTO.fromBO(userBO.get())); 41 | } 42 | 43 | @GetMapping("/logout") 44 | @ApiOperation("Logout") 45 | public RootDTO logout() { 46 | var loggedUser = logoutUser(); 47 | if (loggedUser == null) { 48 | return new RootDTO().addDataEntry("user", null); 49 | } 50 | 51 | var userBO = userUsecase.info(loggedUser.getId()); 52 | return new RootDTO().addDataEntry("user", UserDTO.fromBO(userBO.get())); 53 | } 54 | 55 | @GetMapping("/logged") 56 | @ApiOperation("Current user") 57 | public RootDTO logged() { 58 | if (loggedUserId() == null) { 59 | return new RootDTO().addDataEntry("user", null); 60 | } 61 | 62 | var userBO = userUsecase.info(loggedUserId()); 63 | return new RootDTO().addDataEntry("user", userBO.map(this::fullUserDto).get()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/UserDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import net.jaggerwang.sbip.entity.UserBO; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | public interface UserDao { 12 | /** 13 | * 保存用户 14 | * @param userBO 要保存的用户 15 | * @return 已保存的用户 16 | */ 17 | UserBO save(UserBO userBO); 18 | 19 | /** 20 | * 查找指定 ID 的用户 21 | * @param id 用户 ID 22 | * @return 用户 23 | */ 24 | Optional findById(Long id); 25 | 26 | /** 27 | * 查找指定用户名的用户 28 | * @param username 用户名 29 | * @return 用户 30 | */ 31 | Optional findByUsername(String username); 32 | 33 | /** 34 | * 查找指定手机号的用户 35 | * @param mobile 用户名 36 | * @return 用户 37 | */ 38 | Optional findByMobile(String mobile); 39 | 40 | /** 41 | * 查找指定邮箱的用户 42 | * @param email 用户名 43 | * @return 用户 44 | */ 45 | Optional findByEmail(String email); 46 | 47 | /** 48 | * 关注用户 49 | * @param followerId 用户 ID 50 | * @param followingId 要关注用户 ID 51 | */ 52 | void follow(Long followerId, Long followingId); 53 | 54 | /** 55 | * 取消关注用户 56 | * @param followerId 用户 ID 57 | * @param followingId 已关注用户 ID 58 | */ 59 | void unfollow(Long followerId, Long followingId); 60 | 61 | /** 62 | * 查找某个用户关注的用户 63 | * @param followerId 用户 ID 64 | * @param limit 返回数量 65 | * @param offset 起始位置 66 | * @return 用户列表 67 | */ 68 | List following(Long followerId, Long limit, Long offset); 69 | 70 | /** 71 | * 统计某个用户关注的用户数量 72 | * @param followerId 用户 ID 73 | * @return 用户数量 74 | */ 75 | Long followingCount(Long followerId); 76 | 77 | /** 78 | * 查找某个用户的关注者 79 | * @param followingId 用户 ID 80 | * @param limit 返回数量 81 | * @param offset 起始位置 82 | * @return 用户列表 83 | */ 84 | List follower(Long followingId, Long limit, Long offset); 85 | 86 | /** 87 | * 统计某个用户的关注者数量 88 | * @param followingId 用户 ID 89 | * @return 用户数量 90 | */ 91 | Long followerCount(Long followingId); 92 | 93 | /** 94 | * 是否关注了某个用户 95 | * @param followerId 用户 ID 96 | * @param followingId 被关注用户 ID 97 | * @return 是否关注 98 | */ 99 | Boolean isFollowing(Long followerId, Long followingId); 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/port/dao/PostDao.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase.port.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import net.jaggerwang.sbip.entity.PostBO; 7 | 8 | /** 9 | * @author Jagger Wang 10 | */ 11 | public interface PostDao { 12 | /** 13 | * 保存帖子 14 | * @param postBO 要保存的帖子对象 15 | * @return 已保存的帖子对象 16 | */ 17 | PostBO save(PostBO postBO); 18 | 19 | /** 20 | * 删除帖子 21 | * @param id 帖子 ID 22 | */ 23 | void delete(Long id); 24 | 25 | /** 26 | * 查找指定 ID 的帖子 27 | * @param id 帖子 ID 28 | * @return 帖子 29 | */ 30 | Optional findById(Long id); 31 | 32 | /** 33 | * 查找某个用户发布的帖子 34 | * @param userId 用户 ID,若为 Null 则查找所有用户 35 | * @param limit 返回数量 36 | * @param offset 起始位置 37 | * @return 帖子列表 38 | */ 39 | List published(Long userId, Long limit, Long offset); 40 | 41 | /** 42 | * 统计某个用户发布的帖子数量 43 | * @param userId 用户 ID,若为 Null 则查找所有用户 44 | * @return 帖子数量 45 | */ 46 | Long publishedCount(Long userId); 47 | 48 | /** 49 | * 收藏某个帖子 50 | * @param userId 用户 ID 51 | * @param postId 帖子 ID 52 | */ 53 | void like(Long userId, Long postId); 54 | 55 | /** 56 | * 取消收藏某个帖子 57 | * @param userId 用户 ID 58 | * @param postId 帖子 ID 59 | */ 60 | void unlike(Long userId, Long postId); 61 | 62 | /** 63 | * 查找某个用户收藏的帖子 64 | * @param userId 用户 ID 65 | * @param limit 返回数量 66 | * @param offset 起始位置 67 | * @return 帖子列表 68 | */ 69 | List liked(Long userId, Long limit, Long offset); 70 | 71 | /** 72 | * 统计某个用户收藏的帖子数量 73 | * @param userId 用户 ID 74 | * @return 帖子数量 75 | */ 76 | Long likedCount(Long userId); 77 | 78 | /** 79 | * 查找某个用户关注的人所发布的帖子,按帖子 ID 倒序,可筛选某个帖子之前和之后的 80 | * @param userId 用户 ID 81 | * @param limit 返回数量 82 | * @param beforeId 小于帖子 ID 83 | * @param afterId 大于帖子 ID 84 | * @return 帖子列表 85 | */ 86 | List following(Long userId, Long limit, Long beforeId, Long afterId); 87 | 88 | /** 89 | * 统计某个用户关注的人所发布的帖子数量 90 | * @param userId 用户 ID 91 | * @return 帖子数量 92 | */ 93 | Long followingCount(Long userId); 94 | 95 | /** 96 | * 是否收藏过某个帖子 97 | * @param userId 用户 ID 98 | * @param postId 帖子 ID 99 | * @return 是否收藏 100 | */ 101 | Boolean isLiked(Long userId, Long postId); 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/FileController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import net.jaggerwang.sbip.entity.FileBO; 6 | import net.jaggerwang.sbip.usecase.exception.NotFoundException; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.multipart.MultipartFile; 13 | 14 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | 21 | /** 22 | * @author Jagger Wang 23 | */ 24 | @RestController 25 | @RequestMapping("/file") 26 | @Api(tags = "File Apis") 27 | public class FileController extends AbstractController { 28 | @PostMapping("/upload") 29 | @ApiOperation("Upload file") 30 | public RootDTO upload(@RequestParam(defaultValue = "LOCAL") FileBO.Region region, 31 | @RequestParam(defaultValue = "") String bucket, 32 | @RequestParam(defaultValue = "") String path, 33 | @RequestParam("file") List files) throws IOException { 34 | var fileBOs = new ArrayList(); 35 | for (var file : files) { 36 | var content = file.getBytes(); 37 | 38 | var meta = FileBO.Meta.builder() 39 | .name(file.getOriginalFilename()) 40 | .size(file.getSize()) 41 | .type(file.getContentType()) 42 | .build(); 43 | var fileBO = FileBO.builder() 44 | .userId(loggedUserId()) 45 | .region(region) 46 | .bucket(bucket) 47 | .meta(meta) 48 | .build(); 49 | 50 | fileBO = fileUsecase.upload(path, content, fileBO); 51 | 52 | fileBOs.add(fileBO); 53 | } 54 | 55 | return new RootDTO().addDataEntry("files", 56 | fileBOs.stream().map(this::fullFileDto).collect(Collectors.toList())); 57 | } 58 | 59 | @GetMapping("/info") 60 | @ApiOperation("Get file info") 61 | public RootDTO info(@RequestParam Long id) { 62 | var fileBO = fileUsecase.info(id); 63 | if (fileBO.isEmpty()) { 64 | throw new NotFoundException("文件未找到"); 65 | } 66 | 67 | return new RootDTO().addDataEntry("file", fullFileDto(fileBO.get())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/net/jaggerwang/sbip/adapter/api/RestApiTests.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.security.test.context.support.WithUserDetails; 11 | import net.jaggerwang.sbip.entity.UserBO; 12 | import org.springframework.test.context.ActiveProfiles; 13 | import org.springframework.test.context.jdbc.Sql; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | 16 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 18 | 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | @ActiveProfiles("test") 22 | @Sql({"/db/init-db-test.sql"}) 23 | @Sql(scripts = {"/db/clean-db-test.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) 24 | @EnabledIfSystemProperty(named = "test.api.enabled", matches = "true") 25 | public class RestApiTests { 26 | @Autowired 27 | private MockMvc mvc; 28 | 29 | @Autowired 30 | private ObjectMapper objectMapper; 31 | 32 | @Test 33 | void login() throws Exception { 34 | var userBO = UserBO.builder().username("jaggerwang").password("123456").build(); 35 | mvc.perform(post("/auth/login").contentType(MediaType.APPLICATION_JSON) 36 | .content(objectMapper.writeValueAsString(userBO))).andExpect(status().isOk()) 37 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 38 | .andExpect(jsonPath("$.code").value("ok")) 39 | .andExpect(jsonPath("$.data.user.username").value(userBO.getUsername())); 40 | } 41 | 42 | @WithUserDetails("jaggerwang") 43 | @Test 44 | void logout() throws Exception { 45 | mvc.perform(get("/auth/logout")).andExpect(status().isOk()) 46 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 47 | .andExpect(jsonPath("$.code").value("ok")) 48 | .andExpect(jsonPath("$.data.user.username").value("jaggerwang")); 49 | } 50 | 51 | @WithUserDetails("jaggerwang") 52 | @Test 53 | void logged() throws Exception { 54 | mvc.perform(get("/auth/logged")).andExpect(status().isOk()) 55 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 56 | .andExpect(jsonPath("$.code").value("ok")) 57 | .andExpect(jsonPath("$.data.user.username").value("jaggerwang")); 58 | } 59 | 60 | @Test 61 | void register() throws Exception { 62 | var userBO = UserBO.builder().username("jagger001").password("123456").build(); 63 | mvc.perform(post("/user/register").contentType(MediaType.APPLICATION_JSON) 64 | .content(objectMapper.writeValueAsString(userBO))).andExpect(status().isOk()) 65 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 66 | .andExpect(jsonPath("$.code").value("ok")) 67 | .andExpect(jsonPath("$.data.user.username").value(userBO.getUsername())); 68 | } 69 | 70 | @WithUserDetails("jaggerwang") 71 | @Test 72 | void info() throws Exception { 73 | mvc.perform(get("/user/info").param("id", "1")).andExpect(status().isOk()) 74 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 75 | .andExpect(jsonPath("$.code").value("ok")) 76 | .andExpect(jsonPath("$.data.user.id").value(1)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/PostController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import java.util.Map; 4 | import java.util.Objects; 5 | import java.util.stream.Collectors; 6 | 7 | import io.swagger.annotations.Api; 8 | import io.swagger.annotations.ApiOperation; 9 | import net.jaggerwang.sbip.usecase.exception.NotFoundException; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 17 | import net.jaggerwang.sbip.adapter.api.controller.dto.PostDTO; 18 | import net.jaggerwang.sbip.entity.PostBO; 19 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 20 | 21 | /** 22 | * @author Jagger Wang 23 | */ 24 | @RestController 25 | @RequestMapping("/post") 26 | @Api(tags = "Post Apis") 27 | public class PostController extends AbstractController { 28 | @PostMapping("/publish") 29 | @ApiOperation("Publish post") 30 | public RootDTO publish(@RequestBody PostBO postInput) { 31 | postInput.setUserId(loggedUserId()); 32 | var postBO = postUsecase.publish(postInput); 33 | 34 | return new RootDTO().addDataEntry("post", PostDTO.fromBO(postBO)); 35 | } 36 | 37 | @PostMapping("/delete") 38 | @ApiOperation("Delete post") 39 | public RootDTO delete(@RequestBody Map input) { 40 | var id = objectMapper.convertValue(input.get("id"), Long.class); 41 | 42 | var postBO = postUsecase.info(id); 43 | if (postBO.isEmpty()) { 44 | throw new NotFoundException("动态未找到"); 45 | } 46 | if (!Objects.equals(postBO.get().getUserId(), loggedUserId())) { 47 | throw new UsecaseException("无权删除"); 48 | } 49 | 50 | postUsecase.delete(id); 51 | 52 | return new RootDTO(); 53 | } 54 | 55 | @GetMapping("/info") 56 | @ApiOperation("Get post info") 57 | public RootDTO info(@RequestParam Long id) { 58 | var postBO = postUsecase.info(id); 59 | if (postBO.isEmpty()) { 60 | throw new NotFoundException("动态未找到"); 61 | } 62 | 63 | return new RootDTO().addDataEntry("post", fullPostDto(postBO.get())); 64 | } 65 | 66 | @GetMapping("/published") 67 | @ApiOperation("Posts published by some user") 68 | public RootDTO published(@RequestParam(required = false) Long userId, 69 | @RequestParam(defaultValue = "10") Long limit, 70 | @RequestParam(defaultValue = "0") Long offset) { 71 | var postBOs = postUsecase.published(userId, limit, offset); 72 | 73 | return new RootDTO().addDataEntry("posts", 74 | postBOs.stream().map(this::fullPostDto).collect(Collectors.toList())); 75 | } 76 | 77 | @PostMapping("/like") 78 | @ApiOperation("Like post") 79 | public RootDTO like(@RequestBody Map input) { 80 | var postId = objectMapper.convertValue(input.get("postId"), Long.class); 81 | 82 | postUsecase.like(loggedUserId(), postId); 83 | 84 | return new RootDTO(); 85 | } 86 | 87 | @PostMapping("/unlike") 88 | @ApiOperation("Unlike post") 89 | public RootDTO unlike(@RequestBody Map input) { 90 | var postId = objectMapper.convertValue(input.get("postId"), Long.class); 91 | 92 | postUsecase.unlike(loggedUserId(), postId); 93 | 94 | return new RootDTO(); 95 | } 96 | 97 | @GetMapping("/liked") 98 | @ApiOperation("Posts liked by some user") 99 | public RootDTO liked(@RequestParam(required = false) Long userId, 100 | @RequestParam(defaultValue = "10") Long limit, 101 | @RequestParam(defaultValue = "0") Long offset) { 102 | var postBOs = postUsecase.liked(userId, limit, offset); 103 | 104 | return new RootDTO().addDataEntry("posts", 105 | postBOs.stream().map(this::fullPostDto).collect(Collectors.toList())); 106 | } 107 | 108 | @GetMapping("/following") 109 | @ApiOperation("Posts published by users which are followed by current user") 110 | public RootDTO following(@RequestParam(defaultValue = "10") Long limit, 111 | @RequestParam(required = false) Long beforeId, 112 | @RequestParam(required = false) Long afterId) { 113 | var postBOs = postUsecase.following(loggedUserId(), limit, beforeId, afterId); 114 | 115 | return new RootDTO().addDataEntry("posts", 116 | postBOs.stream().map(this::fullPostDto).collect(Collectors.toList())); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/UserDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.stream.Collectors; 6 | 7 | import com.querydsl.jpa.impl.JPAQuery; 8 | import com.querydsl.jpa.impl.JPAQueryFactory; 9 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.User; 10 | import org.springframework.stereotype.Component; 11 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.UserRepository; 12 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QUser; 13 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QUserFollow; 14 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.UserFollow; 15 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.UserFollowRepository; 16 | import net.jaggerwang.sbip.entity.UserBO; 17 | import net.jaggerwang.sbip.usecase.port.dao.UserDao; 18 | 19 | /** 20 | * @author Jagger Wang 21 | */ 22 | @Component 23 | public class UserDaoImpl implements UserDao { 24 | private JPAQueryFactory jpaQueryFactory; 25 | private UserRepository userRepository; 26 | private UserFollowRepository userFollowRepository; 27 | 28 | public UserDaoImpl(JPAQueryFactory jpaQueryFactory, UserRepository userRepository, 29 | UserFollowRepository userFollowRepository) { 30 | this.jpaQueryFactory = jpaQueryFactory; 31 | this.userRepository = userRepository; 32 | this.userFollowRepository = userFollowRepository; 33 | } 34 | 35 | @Override 36 | public UserBO save(UserBO userBO) { 37 | return userRepository.save(User.fromBO(userBO)).toBO(); 38 | } 39 | 40 | @Override 41 | public Optional findById(Long id) { 42 | return userRepository.findById(id).map(user -> user.toBO()); 43 | } 44 | 45 | @Override 46 | public Optional findByUsername(String username) { 47 | return userRepository.findByUsername(username).map(user -> user.toBO()); 48 | } 49 | 50 | @Override 51 | public Optional findByEmail(String email) { 52 | return userRepository.findByEmail(email).map(user -> user.toBO()); 53 | } 54 | 55 | @Override 56 | public Optional findByMobile(String mobile) { 57 | return userRepository.findByMobile(mobile).map(user -> user.toBO()); 58 | } 59 | 60 | @Override 61 | public void follow(Long followerId, Long followingId) { 62 | userFollowRepository.save( 63 | UserFollow.builder().followerId(followerId).followingId(followingId).build()); 64 | } 65 | 66 | @Override 67 | public void unfollow(Long followerId, Long followingId) { 68 | userFollowRepository.deleteByFollowerIdAndFollowingId(followerId, followingId); 69 | } 70 | 71 | private JPAQuery followingQuery(Long followerId) { 72 | var query = jpaQueryFactory.selectFrom(QUser.user) 73 | .join(QUserFollow.userFollow).on( 74 | QUser.user.id.eq(QUserFollow.userFollow.followingId)); 75 | if (followerId != null) { 76 | query.where(QUserFollow.userFollow.followerId.eq(followerId)); 77 | } 78 | return query; 79 | } 80 | 81 | @Override 82 | public List following(Long followerId, Long limit, Long offset) { 83 | var query = followingQuery(followerId); 84 | query.orderBy(QUserFollow.userFollow.id.desc()); 85 | if (limit != null) { 86 | query.limit(limit); 87 | } 88 | if (offset != null) { 89 | query.offset(offset); 90 | } 91 | 92 | return query.fetch().stream().map(user -> user.toBO()).collect(Collectors.toList()); 93 | } 94 | 95 | @Override 96 | public Long followingCount(Long followerId) { 97 | return followingQuery(followerId).fetchCount(); 98 | } 99 | 100 | private JPAQuery followerQuery(Long followingId) { 101 | var query = jpaQueryFactory.selectFrom(QUser.user) 102 | .join(QUserFollow.userFollow).on( 103 | QUser.user.id.eq(QUserFollow.userFollow.followerId)); 104 | if (followingId != null) { 105 | query.where(QUserFollow.userFollow.followingId.eq(followingId)); 106 | } 107 | return query; 108 | } 109 | 110 | @Override 111 | public List follower(Long followingId, Long limit, Long offset) { 112 | var query = followerQuery(followingId); 113 | query.orderBy(QUserFollow.userFollow.id.desc()); 114 | if (limit != null) { 115 | query.limit(limit); 116 | } 117 | if (offset != null) { 118 | query.offset(offset); 119 | } 120 | 121 | return query.fetch().stream().map(user -> user.toBO()).collect(Collectors.toList()); 122 | } 123 | 124 | @Override 125 | public Long followerCount(Long followingId) { 126 | return followerQuery(followingId).fetchCount(); 127 | } 128 | 129 | @Override 130 | public Boolean isFollowing(Long followerId, Long followingId) { 131 | return userFollowRepository.existsByFollowerIdAndFollowingId(followerId, followingId); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/dao/PostDaoImpl.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.dao; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.stream.Collectors; 6 | 7 | import com.querydsl.jpa.impl.JPAQuery; 8 | import com.querydsl.jpa.impl.JPAQueryFactory; 9 | import org.springframework.stereotype.Component; 10 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.PostRepository; 11 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.Post; 12 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.PostLike; 13 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QPost; 14 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QPostLike; 15 | import net.jaggerwang.sbip.adapter.dao.jpa.entity.QUserFollow; 16 | import net.jaggerwang.sbip.adapter.dao.jpa.repository.PostLikeRepository; 17 | import net.jaggerwang.sbip.entity.PostBO; 18 | import net.jaggerwang.sbip.usecase.port.dao.PostDao; 19 | 20 | /** 21 | * @author Jagger Wang 22 | */ 23 | @Component 24 | public class PostDaoImpl implements PostDao { 25 | private JPAQueryFactory jpaQueryFactory; 26 | private PostRepository postRepository; 27 | private PostLikeRepository postLikeRepository; 28 | 29 | public PostDaoImpl(JPAQueryFactory jpaQueryFactory, PostRepository postRepository, 30 | PostLikeRepository postLikeRepository) { 31 | this.jpaQueryFactory = jpaQueryFactory; 32 | this.postRepository = postRepository; 33 | this.postLikeRepository = postLikeRepository; 34 | } 35 | 36 | @Override 37 | public PostBO save(PostBO postBO) { 38 | return postRepository.save(Post.fromBO(postBO)).toBO(); 39 | } 40 | 41 | @Override 42 | public void delete(Long id) { 43 | postRepository.deleteById(id); 44 | } 45 | 46 | @Override 47 | public Optional findById(Long id) { 48 | return postRepository.findById(id).map(post -> post.toBO()); 49 | } 50 | 51 | private JPAQuery publishedQuery(Long userId) { 52 | var query = jpaQueryFactory.selectFrom(QPost.post); 53 | if (userId != null) { 54 | query.where(QPost.post.userId.eq(userId)); 55 | } 56 | return query; 57 | } 58 | 59 | @Override 60 | public List published(Long userId, Long limit, Long offset) { 61 | var query = publishedQuery(userId); 62 | query.orderBy(QPost.post.id.desc()); 63 | if (limit != null) { 64 | query.limit(limit); 65 | } 66 | if (offset != null) { 67 | query.offset(offset); 68 | } 69 | 70 | return query.fetch().stream().map(post -> post.toBO()).collect(Collectors.toList()); 71 | } 72 | 73 | @Override 74 | public Long publishedCount(Long userId) { 75 | return publishedQuery(userId).fetchCount(); 76 | } 77 | 78 | @Override 79 | public void like(Long userId, Long postId) { 80 | postLikeRepository.save(PostLike.builder().userId(userId).postId(postId).build()); 81 | } 82 | 83 | @Override 84 | public void unlike(Long userId, Long postId) { 85 | postLikeRepository.deleteByUserIdAndPostId(userId, postId); 86 | } 87 | 88 | private JPAQuery likedQuery(Long userId) { 89 | var query = jpaQueryFactory.selectFrom(QPost.post) 90 | .join(QPostLike.postLike).on(QPost.post.id.eq(QPostLike.postLike.postId)); 91 | if (userId != null) { 92 | query.where(QPostLike.postLike.userId.eq(userId)); 93 | } 94 | return query; 95 | } 96 | 97 | @Override 98 | public List liked(Long userId, Long limit, Long offset) { 99 | var query = likedQuery(userId); 100 | query.orderBy(QPostLike.postLike.id.desc()); 101 | if (limit != null) { 102 | query.limit(limit); 103 | } 104 | if (offset != null) { 105 | query.offset(offset); 106 | } 107 | 108 | return query.fetch().stream().map(post -> post.toBO()).collect(Collectors.toList()); 109 | } 110 | 111 | @Override 112 | public Long likedCount(Long userId) { 113 | return likedQuery(userId).fetchCount(); 114 | } 115 | 116 | private JPAQuery followingQuery(Long userId) { 117 | return jpaQueryFactory.selectFrom(QPost.post) 118 | .join(QUserFollow.userFollow).on( 119 | QPost.post.userId.eq(QUserFollow.userFollow.followingId)) 120 | .where(QUserFollow.userFollow.followerId.eq(userId)); 121 | } 122 | 123 | @Override 124 | public List following(Long userId, Long limit, Long beforeId, Long afterId) { 125 | var query = followingQuery(userId); 126 | query.orderBy(QPost.post.id.desc()); 127 | if (limit != null) { 128 | query.limit(limit); 129 | } 130 | if (beforeId != null) { 131 | query.where(QPost.post.id.lt(beforeId)); 132 | } 133 | if (afterId != null) { 134 | query.where(QPost.post.id.gt(afterId)); 135 | } 136 | 137 | return query.fetch().stream().map(post -> post.toBO()).collect(Collectors.toList()); 138 | } 139 | 140 | @Override 141 | public Long followingCount(Long userId) { 142 | return followingQuery(userId).fetchCount(); 143 | } 144 | 145 | @Override 146 | public Boolean isLiked(Long userId, Long postId) { 147 | return postLikeRepository.existsByUserIdAndPostId(userId, postId); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.5.2 9 | 10 | 11 | 12 | net.jaggerwang 13 | spring-boot-in-practice 14 | 1.0.0-SNAPSHOT 15 | spring-boot-in-practice 16 | Spring boot in practice 17 | 18 | 19 | 11 20 | 30.1.1-jre 21 | 3.0.0 22 | 4.4.0 23 | 1.3 24 | 1.1.3 25 | 26 | 27 | 28 | 29 | com.google.guava 30 | guava 31 | ${guava.version} 32 | 33 | 34 | 35 | io.springfox 36 | springfox-swagger2 37 | ${swagger.version} 38 | 39 | 40 | io.springfox 41 | springfox-swagger-ui 42 | ${swagger.version} 43 | 44 | 45 | 46 | org.projectlombok 47 | lombok 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-web 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-security 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-data-jpa 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-data-redis 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-actuator 69 | true 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-devtools 74 | runtime 75 | true 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-configuration-processor 80 | true 81 | 82 | 83 | 84 | org.springframework.session 85 | spring-session-data-redis 86 | 87 | 88 | 89 | org.apache.commons 90 | commons-pool2 91 | 92 | 93 | 94 | mysql 95 | mysql-connector-java 96 | runtime 97 | 98 | 99 | 100 | org.flywaydb 101 | flyway-core 102 | 103 | 104 | 105 | com.querydsl 106 | querydsl-jpa 107 | ${querydsl.version} 108 | 109 | 110 | 111 | org.mongodb 112 | bson 113 | 114 | 115 | 116 | org.springframework.boot 117 | spring-boot-starter-test 118 | test 119 | 120 | 121 | 122 | org.springframework.security 123 | spring-security-test 124 | test 125 | 126 | 127 | 128 | com.h2database 129 | h2 130 | test 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | com.github.johnpoth 139 | jshell-maven-plugin 140 | ${jshell-plugin.version} 141 | 142 | 143 | 144 | 145 | 146 | 147 | org.springframework.boot 148 | spring-boot-maven-plugin 149 | 150 | 151 | 152 | com.mysema.maven 153 | apt-maven-plugin 154 | ${apt-plugin.version} 155 | 156 | 157 | 158 | process 159 | 160 | 161 | target/generated-sources/java 162 | com.querydsl.apt.jpa.JPAAnnotationProcessor 163 | 164 | 165 | 166 | 167 | 168 | com.querydsl 169 | querydsl-apt 170 | ${querydsl.version} 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import java.util.Map; 4 | import java.util.stream.Collectors; 5 | 6 | import io.swagger.annotations.Api; 7 | import io.swagger.annotations.ApiOperation; 8 | import net.jaggerwang.sbip.usecase.exception.NotFoundException; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import net.jaggerwang.sbip.adapter.api.controller.dto.RootDTO; 16 | import net.jaggerwang.sbip.adapter.api.controller.dto.UserDTO; 17 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 18 | 19 | /** 20 | * @author Jagger Wang 21 | */ 22 | @RestController 23 | @RequestMapping("/user") 24 | @Api(tags = "User Apis") 25 | public class UserController extends AbstractController { 26 | @PostMapping("/register") 27 | @ApiOperation("Register user") 28 | public RootDTO register(@RequestBody UserDTO userDto) { 29 | var userBO = userUsecase.register(userDto.toBO()); 30 | 31 | loginUser(userDto.getUsername(), userDto.getPassword()); 32 | 33 | metricUsecase.increment("registerCount", 1L); 34 | 35 | return new RootDTO().addDataEntry("user", UserDTO.fromBO(userBO)); 36 | } 37 | 38 | @PostMapping("/modify") 39 | @ApiOperation("Modify info of current user") 40 | public RootDTO modify(@RequestBody Map input) { 41 | var userDto = objectMapper.convertValue(input.get("user"), UserDTO.class); 42 | var code = objectMapper.convertValue(input.get("code"), String.class); 43 | 44 | if ((userDto.getMobile() != null 45 | && !userUsecase.checkMobileVerifyCode("modify", userDto.getMobile(), code)) 46 | || userDto.getEmail() != null 47 | && !userUsecase.checkEmailVerifyCode("modify", userDto.getEmail(), code)) { 48 | throw new UsecaseException("验证码错误"); 49 | } 50 | 51 | var userBO = userUsecase.modify(loggedUserId(), userDto.toBO()); 52 | 53 | return new RootDTO().addDataEntry("user", UserDTO.fromBO(userBO)); 54 | } 55 | 56 | @GetMapping("/info") 57 | @ApiOperation("Get user info") 58 | public RootDTO info(@RequestParam Long id) { 59 | var userBO = userUsecase.info(id); 60 | if (userBO.isEmpty()) { 61 | throw new NotFoundException("用户未找到"); 62 | } 63 | 64 | return new RootDTO().addDataEntry("user", fullUserDto(userBO.get())); 65 | } 66 | 67 | @PostMapping("/follow") 68 | @ApiOperation("Follow user") 69 | public RootDTO follow(@RequestBody Map input) { 70 | var userId = objectMapper.convertValue(input.get("userId"), Long.class); 71 | 72 | userUsecase.follow(loggedUserId(), userId); 73 | 74 | return new RootDTO(); 75 | } 76 | 77 | @PostMapping("/unfollow") 78 | @ApiOperation("Unfollow user") 79 | public RootDTO unfollow(@RequestBody Map input) { 80 | var userId = objectMapper.convertValue(input.get("userId"), Long.class); 81 | 82 | userUsecase.unfollow(loggedUserId(), userId); 83 | 84 | return new RootDTO(); 85 | } 86 | 87 | @GetMapping("/following") 88 | @ApiOperation("Users followed by some user") 89 | public RootDTO following(@RequestParam(required = false) Long userId, 90 | @RequestParam(defaultValue = "20") Long limit, 91 | @RequestParam(defaultValue = "0") Long offset) { 92 | var userBOs = userUsecase.following(userId, limit, offset); 93 | 94 | return new RootDTO().addDataEntry("users", 95 | userBOs.stream().map(this::fullUserDto).collect(Collectors.toList())); 96 | } 97 | 98 | @GetMapping("/follower") 99 | @ApiOperation("Users following some user") 100 | public RootDTO follower(@RequestParam(required = false) Long userId, 101 | @RequestParam(defaultValue = "20") Long limit, 102 | @RequestParam(defaultValue = "0") Long offset) { 103 | var userBOs = userUsecase.follower(userId, limit, offset); 104 | 105 | return new RootDTO().addDataEntry("users", 106 | userBOs.stream().map(this::fullUserDto).collect(Collectors.toList())); 107 | } 108 | 109 | @PostMapping("/sendMobileVerifyCode") 110 | @ApiOperation("Send verify code by mobile") 111 | public RootDTO sendMobileVerifyCode(@RequestBody Map input) { 112 | var type = objectMapper.convertValue(input.get("type"), String.class); 113 | var mobile = objectMapper.convertValue(input.get("mobile"), String.class); 114 | 115 | var verifyCode = userUsecase.sendMobileVerifyCode(type, mobile); 116 | 117 | return new RootDTO().addDataEntry("verifyCode", verifyCode); 118 | } 119 | 120 | @PostMapping("/sendEmailVerifyCode") 121 | @ApiOperation("Send verify code by email") 122 | public RootDTO sendEmailVerifyCode(@RequestBody Map input) { 123 | var type = objectMapper.convertValue(input.get("type"), String.class); 124 | var email = objectMapper.convertValue(input.get("email"), String.class); 125 | 126 | var verifyCode = userUsecase.sendEmailVerifyCode(type, email); 127 | 128 | return new RootDTO().addDataEntry("verifyCode", verifyCode); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/adapter/api/controller/AbstractController.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.adapter.api.controller; 2 | 3 | import java.nio.file.Paths; 4 | import java.util.HashMap; 5 | import java.util.Optional; 6 | import java.util.stream.Collectors; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import net.jaggerwang.sbip.adapter.api.controller.dto.*; 9 | import net.jaggerwang.sbip.entity.FileBO; 10 | import net.jaggerwang.sbip.entity.PostBO; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.security.authentication.AnonymousAuthenticationToken; 14 | import org.springframework.security.authentication.AuthenticationManager; 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 16 | import org.springframework.security.core.context.SecurityContextHolder; 17 | import net.jaggerwang.sbip.adapter.api.security.LoggedUser; 18 | import net.jaggerwang.sbip.usecase.FileUsecase; 19 | import net.jaggerwang.sbip.usecase.MetricUsecase; 20 | import net.jaggerwang.sbip.usecase.PostUsecase; 21 | import net.jaggerwang.sbip.usecase.StatUsecase; 22 | import net.jaggerwang.sbip.usecase.UserUsecase; 23 | import net.jaggerwang.sbip.entity.UserBO; 24 | 25 | /** 26 | * @author Jagger Wang 27 | */ 28 | abstract public class AbstractController { 29 | @Value("${file.base-url}") 30 | protected String baseUrl; 31 | 32 | @Autowired 33 | protected ObjectMapper objectMapper; 34 | 35 | @Autowired 36 | protected AuthenticationManager authenticationManager; 37 | 38 | @Autowired 39 | protected FileUsecase fileUsecase; 40 | 41 | @Autowired 42 | protected MetricUsecase metricUsecase; 43 | 44 | @Autowired 45 | protected PostUsecase postUsecase; 46 | 47 | @Autowired 48 | protected StatUsecase statUsecase; 49 | 50 | @Autowired 51 | protected UserUsecase userUsecase; 52 | 53 | protected LoggedUser loginUser(String username, String password) { 54 | var auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( 55 | username, password)); 56 | var securityContext = SecurityContextHolder.getContext(); 57 | securityContext.setAuthentication(auth); 58 | return (LoggedUser) auth.getPrincipal(); 59 | } 60 | 61 | protected LoggedUser logoutUser() { 62 | var loggedUser = loggedUser(); 63 | SecurityContextHolder.getContext().setAuthentication(null); 64 | return loggedUser; 65 | } 66 | 67 | protected LoggedUser loggedUser() { 68 | var auth = SecurityContextHolder.getContext().getAuthentication(); 69 | if (auth == null || auth instanceof AnonymousAuthenticationToken || 70 | !auth.isAuthenticated()) { 71 | return null; 72 | } 73 | return (LoggedUser) auth.getPrincipal(); 74 | } 75 | 76 | protected Long loggedUserId() { 77 | var loggedUser = loggedUser(); 78 | return loggedUser != null ? loggedUser.getId() : null; 79 | } 80 | 81 | protected UserDTO fullUserDto(UserBO userBO) { 82 | var userDto = UserDTO.fromBO(userBO); 83 | 84 | if (userDto.getAvatarId() != null) { 85 | var avatar = fileUsecase.info(userDto.getAvatarId()); 86 | avatar.ifPresent(file -> userDto.setAvatar(fullFileDto(file))); 87 | } 88 | 89 | userDto.setStat(UserStatDTO.fromBO(statUsecase.userStatInfoByUserId(userDto.getId()))); 90 | 91 | if (loggedUserId() != null) { 92 | userDto.setFollowing(userUsecase.isFollowing(loggedUserId(), userDto.getId())); 93 | } 94 | 95 | return userDto; 96 | } 97 | 98 | protected PostDTO fullPostDto(PostBO postBO) { 99 | var postDto = PostDTO.fromBO(postBO); 100 | 101 | var userBO = userUsecase.info(postDto.getUserId()); 102 | userBO.ifPresent(user -> postDto.setUser(fullUserDto(user))); 103 | 104 | if (postDto.getImageIds().size() > 0) { 105 | postDto.setImages(fileUsecase.infos(postDto.getImageIds(), false).stream() 106 | .map(this::fullFileDto).collect(Collectors.toList())); 107 | } 108 | 109 | if (postDto.getVideoId() != null) { 110 | var video = fileUsecase.info(postDto.getVideoId()); 111 | video.ifPresent(file -> postDto.setVideo(fullFileDto(file))); 112 | } 113 | 114 | postDto.setStat(PostStatDTO.fromBO(statUsecase.postStatInfoByPostId(postDto.getId()))); 115 | 116 | if (loggedUserId() != null) { 117 | postDto.setLiked(postUsecase.isLiked(loggedUserId(), postDto.getId())); 118 | } 119 | 120 | return postDto; 121 | } 122 | 123 | protected FileDTO fullFileDto(FileBO fileBO) { 124 | var fileDto = FileDTO.fromBO(fileBO); 125 | 126 | var url = ""; 127 | if (fileDto.getRegion() == FileBO.Region.LOCAL) { 128 | url = baseUrl + Paths.get("/", fileDto.getBucket(), fileDto.getPath()).toString(); 129 | } 130 | fileDto.setUrl(url); 131 | 132 | if (fileDto.getMeta().getType().startsWith("image/")) { 133 | var thumbs = new HashMap(8); 134 | thumbs.put(FileBO.ThumbType.SMALL, 135 | String.format("%s?process=%s", url, "thumb-small")); 136 | thumbs.put(FileBO.ThumbType.MIDDLE, 137 | String.format("%s?process=%s", url, "thumb-middle")); 138 | thumbs.put(FileBO.ThumbType.LARGE, 139 | String.format("%s?process=%s", url, "thumb-large")); 140 | thumbs.put(FileBO.ThumbType.HUGE, 141 | String.format("%s?process=%s", url, "thumb-huge")); 142 | fileDto.setThumbs(thumbs); 143 | } 144 | 145 | return fileDto; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/net/jaggerwang/sbip/usecase/UserUsecase.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.sbip.usecase; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import net.jaggerwang.sbip.entity.RoleBO; 8 | import net.jaggerwang.sbip.entity.UserBO; 9 | import net.jaggerwang.sbip.usecase.exception.NotFoundException; 10 | import net.jaggerwang.sbip.usecase.exception.UsecaseException; 11 | import net.jaggerwang.sbip.usecase.port.dao.RoleDao; 12 | import net.jaggerwang.sbip.usecase.port.dao.UserDao; 13 | import net.jaggerwang.sbip.util.encoder.PasswordEncoder; 14 | import net.jaggerwang.sbip.util.generator.RandomGenerator; 15 | import org.springframework.stereotype.Component; 16 | 17 | /** 18 | * @author Jagger Wang 19 | */ 20 | @Component 21 | public class UserUsecase { 22 | private final static HashMap MOBILE_VERIFY_CODES = new HashMap<>(); 23 | private final static HashMap EMAIL_VERIFY_CODES = new HashMap<>(); 24 | 25 | private final UserDao userDAO; 26 | private final RoleDao roleDAO; 27 | 28 | public UserUsecase(UserDao userDAO, RoleDao roleDAO) { 29 | this.userDAO = userDAO; 30 | this.roleDAO = roleDAO; 31 | } 32 | 33 | public UserBO register(UserBO userBO) { 34 | if (userDAO.findByUsername(userBO.getUsername()).isPresent()) { 35 | throw new UsecaseException("用户名重复"); 36 | } 37 | 38 | var user = UserBO.builder().username(userBO.getUsername()) 39 | .password(encodePassword(userBO.getPassword())).build(); 40 | return userDAO.save(user); 41 | } 42 | 43 | public String encodePassword(String password) { 44 | return new PasswordEncoder().encode(password); 45 | } 46 | 47 | public UserBO modify(Long id, UserBO userBO) { 48 | var user = userDAO.findById(id).orElse(null); 49 | if (user == null) { 50 | throw new NotFoundException("用户未找到"); 51 | } 52 | 53 | if (userBO.getUsername() != null) { 54 | if (userDAO.findByUsername(userBO.getUsername()).isPresent()) { 55 | throw new UsecaseException("用户名重复"); 56 | } 57 | user.setUsername(userBO.getUsername()); 58 | } 59 | if (userBO.getPassword() != null) { 60 | user.setPassword(encodePassword(userBO.getPassword())); 61 | } 62 | if (userBO.getMobile() != null) { 63 | if (userDAO.findByMobile(userBO.getMobile()).isPresent()) { 64 | throw new UsecaseException("手机重复"); 65 | } 66 | user.setMobile(userBO.getMobile()); 67 | } 68 | if (userBO.getEmail() != null) { 69 | if (userDAO.findByEmail(userBO.getEmail()).isPresent()) { 70 | throw new UsecaseException("邮箱重复"); 71 | } 72 | user.setEmail(userBO.getEmail()); 73 | } 74 | if (userBO.getAvatarId() != null) { 75 | user.setAvatarId(userBO.getAvatarId()); 76 | } 77 | if (userBO.getIntro() != null) { 78 | user.setIntro(userBO.getIntro()); 79 | } 80 | 81 | return userDAO.save(user); 82 | } 83 | 84 | public String sendMobileVerifyCode(String type, String mobile) { 85 | var key = String.format("%s_%s", type, mobile); 86 | if (MOBILE_VERIFY_CODES.get(key) == null) { 87 | var code = new RandomGenerator().numberString(6); 88 | MOBILE_VERIFY_CODES.put(key, code); 89 | } 90 | return MOBILE_VERIFY_CODES.get(key); 91 | } 92 | 93 | public Boolean checkMobileVerifyCode(String type, String mobile, String code) { 94 | var key = String.format("%s_%s", type, mobile); 95 | if (code != null && code.equals(MOBILE_VERIFY_CODES.get(key))) { 96 | MOBILE_VERIFY_CODES.remove(key); 97 | return true; 98 | } else { 99 | return false; 100 | } 101 | } 102 | 103 | public String sendEmailVerifyCode(String type, String email) { 104 | var key = String.format("%s_%s", type, email); 105 | if (EMAIL_VERIFY_CODES.get(key) == null) { 106 | var code = new RandomGenerator().numberString(6); 107 | EMAIL_VERIFY_CODES.put(key, code); 108 | } 109 | return EMAIL_VERIFY_CODES.get(key); 110 | } 111 | 112 | public Boolean checkEmailVerifyCode(String type, String email, String code) { 113 | var key = String.format("%s_%s", type, email); 114 | if (code != null && code.equals(EMAIL_VERIFY_CODES.get(key))) { 115 | EMAIL_VERIFY_CODES.remove(key); 116 | return true; 117 | } else { 118 | return false; 119 | } 120 | } 121 | 122 | public Optional info(Long id) { 123 | return userDAO.findById(id); 124 | } 125 | 126 | public Optional infoByUsername(String username) { 127 | return userDAO.findByUsername(username); 128 | } 129 | 130 | public Optional infoByMobile(String mobile) { 131 | return userDAO.findByMobile(mobile); 132 | } 133 | 134 | public Optional infoByEmail(String email) { 135 | return userDAO.findByEmail(email); 136 | } 137 | 138 | public List roles(String username) { 139 | return roleDAO.rolesOfUser(username); 140 | } 141 | 142 | public void follow(Long followerId, Long followingId) { 143 | userDAO.follow(followerId, followingId); 144 | } 145 | 146 | public void unfollow(Long followerId, Long followingId) { 147 | userDAO.unfollow(followerId, followingId); 148 | } 149 | 150 | public Boolean isFollowing(Long followerId, Long followingId) { 151 | return userDAO.isFollowing(followerId, followingId); 152 | } 153 | 154 | public List following(Long followerId, Long limit, Long offset) { 155 | return userDAO.following(followerId, limit, offset); 156 | } 157 | 158 | public Long followingCount(Long followerId) { 159 | return userDAO.followingCount(followerId); 160 | } 161 | 162 | public List follower(Long followingId, Long limit, Long offset) { 163 | return userDAO.follower(followingId, limit, offset); 164 | } 165 | 166 | public Long followerCount(Long followingId) { 167 | return userDAO.followerCount(followingId); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot in Practice 2 | 3 | This project can be used as a starter for spring boot api service development, it is also a reference implementation of [Clean Architecture](https://blog.jaggerwang.net/clean-architecture-in-practice/). This api service can be used as the backend api service for this flutter app [Flutter in Practice](https://github.com/jaggerwang/flutter-in-practice). There is also an article [Spring Boot API 服务开发指南](https://blog.jaggerwang.net/spring-boot-api-service-develop-tour/) for learning this project. 4 | 5 | ## Branches 6 | 7 | 1. `mybatis` Using MyBatis to implement DAO(Data Access Object) 8 | 1. `mybatis-plus` Using MyBatis-Plus to implement DAO(Data Access Object) 9 | 10 | ## Dependent frameworks and packages 11 | 12 | 1. [Spring Boot](https://spring.io/projects/spring-boot) Web framework and server 13 | 1. [Spring Data JPA](https://spring.io/projects/spring-data-jpa) Access database 14 | 1. [Querydsl JPA](https://github.com/querydsl/querydsl/tree/master/querydsl-jpa) Type safe dynamic sql builder 15 | 1. [MyBatis](https://mybatis.org/) SQL Mapping Framework 16 | 1. [MyBatis-Plus](https://baomidou.com/) An powerful enhanced toolkit of MyBatis for simplify development 17 | 1. [Spring Data Redis](https://spring.io/projects/spring-data-redis) Cache data 18 | 1. [Spring Security](https://spring.io/projects/spring-security) Authenticate and authrorize 19 | 1. [Spring Session](https://spring.io/projects/spring-session) Manage session 20 | 1. [Flyway](https://flywaydb.org/) Database migration 21 | 1. [Swagger](https://swagger.io/) Api documentation 22 | 23 | ## APIs 24 | 25 | ### Rest 26 | 27 | | Path | Method | Description | 28 | | ------------- | ------------- | ------------- | 29 | | /auth/login | POST | Login | 30 | | /auth/logout | GET | Logout | 31 | | /auth/logged | GET | Get logged user | 32 | | /user/register | POST | Register | 33 | | /user/modify | POST | Modify logged user | 34 | | /user/info | GET | Get user info | 35 | | /user/follow | POST | Follow user | 36 | | /user/unfollow | POST | Unfollow user | 37 | | /user/following | GET | Following users of someone | 38 | | /user/follower | GET | Fans of some user | 39 | | /user/sendMobileVerifyCode | POST | Send mobile verify code | 40 | | /post/publish | POST | Publish post | 41 | | /post/delete | POST | Delete post | 42 | | /post/info | GET | Get post info | 43 | | /post/published | GET | Get published posts of some user | 44 | | /post/like | POST | Like post | 45 | | /post/unlike | POST | Unlike post | 46 | | /post/liked | GET | Liked posts of some user | 47 | | /post/following | GET | Posts of following users of someone | 48 | | /file/upload | POST | Upload file | 49 | | /file/info | GET | Get file meta info | 50 | 51 | This project uses [Swagger](https://swagger.io/) to auto generate api documentation. After started the api service, you can browse all apis at `http://localhost:8080/swagger-ui.html`. 52 | 53 | ## How to run 54 | 55 | This project need java v11+. 56 | 57 | ### By local environment 58 | 59 | #### Clone repository 60 | 61 | ```bash 62 | git clone https://github.com/jaggerwang/spring-boot-in-practice.git && cd spring-boot-in-practice 63 | ``` 64 | 65 | #### Prepare mysql and redis service 66 | 67 | Install mysql and redis server, and start them. After mysql started, create a database for this project, and a user to access this database. 68 | 69 | ```sql 70 | CREATE DATABASE `sbip`; 71 | CREATE USER 'sbip'@'%' IDENTIFIED BY '123456'; 72 | GRANT ALL PRIVILEGES ON `sbip`.* TO 'sbip'@'%'; 73 | ``` 74 | 75 | #### Configure application 76 | 77 | Change configs in `src/main/resources/application.yml` as your need, especially mysql, redis and path related configs. You can also change configs by environment variables, you need add `SBIP_` prefix to each config you want to change. You should make sure the directories you configured is existing. 78 | 79 | #### Start server 80 | 81 | ```bash 82 | ./mvnw spring-boot:run 83 | ``` 84 | 85 | The running main class is `net.jaggerwang.sbip.adapter.api.Application`. When first start server, it will auto create tables, we use flyway to migrate database changes. 86 | 87 | After started, the api service's endpoint is `http://localhost:8080/`. 88 | 89 | ### By docker compose 90 | 91 | You need install [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) at first. 92 | 93 | #### Configure compose 94 | 95 | Change the content of `docker-compose.yml` as your need, especially the host path of mounted volumes. 96 | 97 | #### Start all services 98 | 99 | ```bash 100 | docker-compose up -d 101 | ``` 102 | 103 | It will start server, mysql and redis services. If you need to stop and remove all services, you can execute command `docker-compose down`. The container port `8080` is mapping to the same port on local host, so the endpoint of api service is same as previous. 104 | 105 | When first start mysql, it will auto create a database `sbip` and a user `sbip` with password `123456` to access this database. The password of `root` user is also `123456`. 106 | 107 | ## How to test 108 | 109 | By default it will not run any tests when run maven `test` or `package` task. You can specify corresponding system property to enable each test. 110 | 111 | ### By local environment 112 | 113 | #### Test repositories 114 | 115 | ```bash 116 | ./mvnw -Dtest.dao.enabled=true test 117 | ``` 118 | 119 | #### Test usecases 120 | 121 | ```bash 122 | ./mvnw -Dtest.usecase.enabled=true test 123 | ``` 124 | 125 | Usecase tests are unit tests, it not dependent on outside mysql or redis service. 126 | 127 | Repository tests are integration tests, but it use an embedded H2 database, so there is no need to start a mysql service. 128 | 129 | #### Test apis 130 | 131 | ```bash 132 | ./mvnw -Dtest.api.enabled=true test 133 | ``` 134 | 135 | Api tests are integration tests, it do need mysql and redis service. You can config the services for testing in `application-test.yml`. 136 | 137 | > Be careful, api integration tests will init and clean data in database, do not connect to the real using database. 138 | 139 | ### By docker compose 140 | 141 | To reduce the work of preparing test environment, especilly api integration tests, we can use docker container to run tests. You can use the following commands to run corresponding tests in docker container. 142 | 143 | ```bash 144 | docker-compose -p spring-boot-in-practice-usecase-test -f docker-compose.usecase-test.yml up 145 | docker-compose -p spring-boot-in-practice-repository-test -f docker-compose.repository-test.yml up 146 | docker-compose -p spring-boot-in-practice-api-test -f docker-compose.api-test.yml up 147 | ``` 148 | 149 | After running tests, you can run the following commands to get the corresponding test result. The result of `0` means success, others means failure. 150 | 151 | ```bash 152 | docker inspect spring-boot-in-practice-usecase-test_server_1 --format='{{.State.ExitCode}}' 153 | docker inspect spring-boot-in-practice-repository-test_server_1 --format='{{.State.ExitCode}}' 154 | docker inspect spring-boot-in-practice-api-test_server_1 --format='{{.State.ExitCode}}' 155 | ``` 156 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /src/test/resources/db/migration/h2/V1__Initial_create_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `file` 2 | ( 3 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 4 | `user_id` bigint(20) NOT NULL COMMENT '上传者 ID', 5 | `region` varchar(20) NOT NULL COMMENT '区域', 6 | `bucket` varchar(20) NOT NULL COMMENT '桶', 7 | `path` varchar(100) NOT NULL COMMENT '路径', 8 | `meta` json NOT NULL COMMENT '元信息', 9 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 10 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 11 | PRIMARY KEY (`id`) 12 | ); 13 | COMMENT ON TABLE `file` IS '文件'; 14 | 15 | CREATE TABLE `post` 16 | ( 17 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 18 | `user_id` bigint(20) NOT NULL COMMENT '发布者 ID', 19 | `type` varchar(20) NOT NULL COMMENT '类型', 20 | `text` varchar(100) NOT NULL DEFAULT '' COMMENT '文本内容', 21 | `image_ids` json DEFAULT NULL COMMENT '图片文件 ID 列表', 22 | `video_id` bigint(20) DEFAULT NULL COMMENT '视频文件 ID', 23 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 24 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 25 | PRIMARY KEY (`id`) 26 | ); 27 | COMMENT ON TABLE `post` IS '动态'; 28 | 29 | CREATE TABLE `post_like` 30 | ( 31 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 32 | `post_id` bigint(20) NOT NULL COMMENT '动态 ID', 33 | `user_id` bigint(20) NOT NULL COMMENT '点赞用户 ID', 34 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 35 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 36 | PRIMARY KEY (`id`) 37 | ); 38 | COMMENT ON TABLE `post_like` IS '动态点赞'; 39 | 40 | CREATE TABLE `post_stat` 41 | ( 42 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 43 | `post_id` bigint(20) NOT NULL COMMENT '动态 ID', 44 | `like_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '被喜欢次数', 45 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 46 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 47 | PRIMARY KEY (`id`) 48 | ); 49 | COMMENT ON TABLE `post_stat` IS '动态统计'; 50 | 51 | CREATE TABLE `role` 52 | ( 53 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 54 | `name` varchar(20) NOT NULL COMMENT '角色名', 55 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 56 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 57 | PRIMARY KEY (`id`) 58 | ); 59 | COMMENT ON TABLE `role` IS '角色'; 60 | 61 | CREATE TABLE `user` 62 | ( 63 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 64 | `username` varchar(20) NOT NULL COMMENT '用户名', 65 | `password` varchar(64) NOT NULL COMMENT '已加密的密码', 66 | `mobile` char(11) DEFAULT NULL COMMENT '手机号', 67 | `email` varchar(50) DEFAULT NULL COMMENT '邮箱', 68 | `avatar_id` bigint(20) DEFAULT NULL COMMENT '头像文件 ID', 69 | `intro` varchar(100) NOT NULL DEFAULT '' COMMENT '自我介绍', 70 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 71 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 72 | PRIMARY KEY (`id`) 73 | ); 74 | COMMENT ON TABLE `user` IS '用户'; 75 | 76 | CREATE TABLE `user_follow` 77 | ( 78 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 79 | `following_id` bigint(20) NOT NULL COMMENT '被关注用户 ID', 80 | `follower_id` bigint(20) NOT NULL COMMENT '粉丝用户 ID', 81 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 82 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 83 | PRIMARY KEY (`id`) 84 | ); 85 | COMMENT ON TABLE `user_follow` IS '用户关注'; 86 | 87 | CREATE TABLE `user_role` 88 | ( 89 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 90 | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', 91 | `role_id` bigint(20) NOT NULL COMMENT '角色 ID', 92 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 93 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 94 | PRIMARY KEY (`id`) 95 | ); 96 | COMMENT ON TABLE `user_role` IS '用户角色'; 97 | 98 | CREATE TABLE `user_stat` 99 | ( 100 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 101 | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', 102 | `post_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '发布动态数', 103 | `like_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '喜欢动态数', 104 | `following_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '关注人数', 105 | `follower_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '粉丝人数', 106 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 107 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 108 | PRIMARY KEY (`id`) 109 | ); 110 | COMMENT ON TABLE `user_stat` IS '用户统计'; 111 | 112 | 113 | CREATE INDEX `file_idx_user_id` ON `file` (`user_id`); 114 | 115 | CREATE INDEX `post_idx_video_id` ON `post` (`video_id`); 116 | CREATE INDEX `post_idx_user_id` ON `post` (`user_id`); 117 | 118 | CREATE UNIQUE INDEX `post_like_idx_user_id_post_id` ON `post_like` (`user_id`,`post_id`); 119 | CREATE INDEX `post_like_idx_user_id_created_at` ON `post_like` (`user_id`,`created_at`); 120 | CREATE INDEX `post_like_idx_post_id_created_at` ON `post_like` (`post_id`,`created_at`); 121 | 122 | CREATE UNIQUE INDEX `post_stat_idx_post_id` ON `post_stat` (`post_id`); 123 | 124 | CREATE UNIQUE INDEX `name_idx_name` ON `role` (`name`); 125 | 126 | CREATE UNIQUE INDEX `user_idx_username` ON `user` (`username`); 127 | CREATE UNIQUE INDEX `user_idx_mobile` ON `user` (`mobile`); 128 | CREATE UNIQUE INDEX `user_idx_email` ON `user` (`email`); 129 | 130 | CREATE UNIQUE INDEX `user_follow_idx_follower_id_following_id` ON `user_follow` (`follower_id`,`following_id`); 131 | CREATE INDEX `user_follow_idx_following_id_created_at` ON `user_follow` (`following_id`,`created_at`); 132 | CREATE INDEX `user_follow_idx_follower_id_created_at` ON `user_follow` (`follower_id`,`created_at`); 133 | 134 | CREATE UNIQUE INDEX `user_role_idx_user_id_role_id` ON `user_role` (`user_id`,`role_id`); 135 | CREATE INDEX `user_role_idx_role_id_created_at` ON `user_role` (`role_id`,`created_at`); 136 | CREATE INDEX `user_role_idx_user_id_created_at` ON `user_role` (`user_id`,`created_at`); 137 | 138 | CREATE UNIQUE INDEX `user_stat_idx_user_id` ON `user_stat` (`user_id`); 139 | 140 | 141 | ALTER TABLE `file` ADD CONSTRAINT `file_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 142 | 143 | ALTER TABLE `post` ADD CONSTRAINT `post_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 144 | ALTER TABLE `post` ADD CONSTRAINT `post_fk_video_id` FOREIGN KEY (`video_id`) REFERENCES `file` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 145 | 146 | ALTER TABLE `post_like` ADD CONSTRAINT `post_like_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 147 | ALTER TABLE `post_like` ADD CONSTRAINT `post_like_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 148 | 149 | ALTER TABLE `post_stat` ADD CONSTRAINT `post_stat_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 150 | 151 | ALTER TABLE `user_follow` ADD CONSTRAINT `user_follow_fk_following_id` FOREIGN KEY (`following_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 152 | ALTER TABLE `user_follow` ADD CONSTRAINT `user_follow_fk_follower_id` FOREIGN KEY (`follower_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 153 | 154 | ALTER TABLE `user_role` ADD CONSTRAINT `user_role_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 155 | ALTER TABLE `user_role` ADD CONSTRAINT `user_role_fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 156 | 157 | ALTER TABLE `user_stat` ADD CONSTRAINT `user_stat_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; -------------------------------------------------------------------------------- /src/main/resources/db/migration/mysql/V1__Initial_create_tables.sql: -------------------------------------------------------------------------------- 1 | SET NAMES utf8mb4; 2 | SET FOREIGN_KEY_CHECKS = 0; 3 | 4 | DROP TABLE IF EXISTS `file`; 5 | CREATE TABLE `file` 6 | ( 7 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 8 | `user_id` bigint(20) NOT NULL COMMENT '上传者 ID', 9 | `region` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '区域', 10 | `bucket` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '桶', 11 | `path` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '路径', 12 | `meta` json NOT NULL COMMENT '元信息', 13 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 14 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 15 | PRIMARY KEY (`id`), 16 | KEY `idx_user_id` (`user_id`), 17 | CONSTRAINT `file_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文件'; 19 | 20 | DROP TABLE IF EXISTS `post`; 21 | CREATE TABLE `post` 22 | ( 23 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 24 | `user_id` bigint(20) NOT NULL COMMENT '发布者 ID', 25 | `type` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '类型', 26 | `text` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文本内容', 27 | `image_ids` json DEFAULT NULL COMMENT '图片文件 ID 列表', 28 | `video_id` bigint(20) DEFAULT NULL COMMENT '视频文件 ID', 29 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 30 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 31 | PRIMARY KEY (`id`), 32 | KEY `idx_video_id` (`video_id`), 33 | KEY `idx_user_id` (`user_id`), 34 | CONSTRAINT `post_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 35 | CONSTRAINT `post_fk_video_id` FOREIGN KEY (`video_id`) REFERENCES `file` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 36 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='动态'; 37 | 38 | DROP TABLE IF EXISTS `post_like`; 39 | CREATE TABLE `post_like` 40 | ( 41 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 42 | `post_id` bigint(20) NOT NULL COMMENT '动态 ID', 43 | `user_id` bigint(20) NOT NULL COMMENT '点赞用户 ID', 44 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 45 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 46 | PRIMARY KEY (`id`), 47 | UNIQUE KEY `idx_user_id_post_id` (`user_id`,`post_id`), 48 | KEY `idx_user_id_created_at` (`user_id`,`created_at`), 49 | KEY `idx_post_id_created_at` (`post_id`,`created_at`), 50 | CONSTRAINT `post_like_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 51 | CONSTRAINT `post_like_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='动态点赞'; 53 | 54 | DROP TABLE IF EXISTS `post_stat`; 55 | CREATE TABLE `post_stat` 56 | ( 57 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 58 | `post_id` bigint(20) NOT NULL COMMENT '动态 ID', 59 | `like_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '被喜欢次数', 60 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 61 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 62 | PRIMARY KEY (`id`), 63 | UNIQUE KEY `idx_post_id` (`post_id`), 64 | CONSTRAINT `post_stat_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 65 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='动态统计'; 66 | 67 | DROP TABLE IF EXISTS `role`; 68 | CREATE TABLE `role` 69 | ( 70 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 71 | `name` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名', 72 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 73 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 74 | PRIMARY KEY (`id`), 75 | UNIQUE KEY `idx_name` (`name`) 76 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色'; 77 | 78 | DROP TABLE IF EXISTS `user`; 79 | CREATE TABLE `user` 80 | ( 81 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 82 | `username` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', 83 | `password` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT '已加密的密码', 84 | `mobile` char(11) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号', 85 | `email` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '邮箱', 86 | `avatar_id` bigint(20) DEFAULT NULL COMMENT '头像文件 ID', 87 | `intro` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '自我介绍', 88 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 89 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 90 | PRIMARY KEY (`id`), 91 | UNIQUE KEY `idx_username` (`username`), 92 | UNIQUE KEY `idx_mobile` (`mobile`), 93 | UNIQUE KEY `idx_email` (`email`) 94 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户'; 95 | 96 | DROP TABLE IF EXISTS `user_follow`; 97 | CREATE TABLE `user_follow` 98 | ( 99 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 100 | `following_id` bigint(20) NOT NULL COMMENT '被关注用户 ID', 101 | `follower_id` bigint(20) NOT NULL COMMENT '粉丝用户 ID', 102 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 103 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 104 | PRIMARY KEY (`id`), 105 | UNIQUE KEY `idx_follower_id_following_id` (`follower_id`,`following_id`), 106 | KEY `idx_following_id_created_at` (`following_id`,`created_at`), 107 | KEY `idx_follower_id_created_at` (`follower_id`,`created_at`), 108 | CONSTRAINT `user_follow_fk_follower_id` FOREIGN KEY (`follower_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 109 | CONSTRAINT `user_follow_fk_following_id` FOREIGN KEY (`following_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 110 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户关注'; 111 | 112 | DROP TABLE IF EXISTS `user_role`; 113 | CREATE TABLE `user_role` 114 | ( 115 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 116 | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', 117 | `role_id` bigint(20) NOT NULL COMMENT '角色 ID', 118 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 119 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 120 | PRIMARY KEY (`id`), 121 | UNIQUE KEY `idx_user_id_role_id` (`user_id`,`role_id`) USING BTREE, 122 | KEY `idx_role_id_created_at` (`role_id`,`created_at`) USING BTREE, 123 | KEY `idx_user_id_created_at` (`user_id`,`created_at`) USING BTREE, 124 | CONSTRAINT `user_role_fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 125 | CONSTRAINT `user_role_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 126 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户角色'; 127 | 128 | DROP TABLE IF EXISTS `user_stat`; 129 | CREATE TABLE `user_stat` 130 | ( 131 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', 132 | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', 133 | `post_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '发布动态数', 134 | `like_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '喜欢动态数', 135 | `following_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '关注人数', 136 | `follower_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '粉丝人数', 137 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 138 | `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 139 | PRIMARY KEY (`id`), 140 | UNIQUE KEY `idx_user_id` (`user_id`), 141 | CONSTRAINT `user_stat_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 142 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户统计'; 143 | 144 | SET FOREIGN_KEY_CHECKS = 1; -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | --------------------------------------------------------------------------------