├── system.properties ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── src ├── main │ ├── java │ │ └── com │ │ │ └── uf │ │ │ └── genshinwishes │ │ │ ├── repository │ │ │ ├── ItemRepository.java │ │ │ ├── UserRepository.java │ │ │ ├── BannerRepository.java │ │ │ └── wish │ │ │ │ ├── WishRepository.java │ │ │ │ └── WishSpecification.java │ │ │ ├── dto │ │ │ ├── ItemType.java │ │ │ ├── mihoyo │ │ │ │ ├── MihoyoUserDTO.java │ │ │ │ ├── MihoyoInfoRetDTO.java │ │ │ │ ├── MihoyoWishRetDTO.java │ │ │ │ ├── MihoyoRetDTO.java │ │ │ │ ├── MihoyoWishDataDTO.java │ │ │ │ └── MihoyoWishLogDTO.java │ │ │ ├── CountPerPity.java │ │ │ ├── CountPerItemId.java │ │ │ ├── CountPerBanner.java │ │ │ ├── CountPerRegion.java │ │ │ ├── CountPerDay.java │ │ │ ├── ExclusivityDTO.java │ │ │ ├── CountPerDayDTO.java │ │ │ ├── CountPerRankAndDay.java │ │ │ ├── CountPerUserPerItemId.java │ │ │ ├── LatestEventsCountsDTO.java │ │ │ ├── WishFilterDTO.java │ │ │ ├── WishDTO.java │ │ │ ├── BannerImportStateDTO.java │ │ │ ├── UserDTO.java │ │ │ ├── StatsDTO.java │ │ │ ├── BannerDTO.java │ │ │ ├── mapper │ │ │ │ ├── UserMapper.java │ │ │ │ ├── WishMapper.java │ │ │ │ └── BannerMapper.java │ │ │ └── PublicStatsDTO.java │ │ │ ├── exception │ │ │ ├── ApiError.java │ │ │ ├── RestResponseEntityExceptionHandler.java │ │ │ ├── ErrorType.java │ │ │ └── ExceptionHandlerFilter.java │ │ │ ├── ServletInitializer.java │ │ │ ├── model │ │ │ ├── Image.java │ │ │ ├── Region.java │ │ │ ├── BannerType.java │ │ │ ├── Item.java │ │ │ ├── User.java │ │ │ ├── Banner.java │ │ │ └── Wish.java │ │ │ ├── controller │ │ │ ├── ItemController.java │ │ │ ├── BannerController.java │ │ │ ├── StatsController.java │ │ │ ├── UserController.java │ │ │ ├── PublicController.java │ │ │ ├── ProfileController.java │ │ │ └── WishController.java │ │ │ ├── security │ │ │ ├── CustomUserDetailsService.java │ │ │ ├── CustomOidcUserService.java │ │ │ ├── CustomOauth2UserService.java │ │ │ ├── OAuth2AccessTokenResponseConverterWithDefaults.java │ │ │ ├── UserPrincipal.java │ │ │ └── WebSecurityConfig.java │ │ │ ├── config │ │ │ ├── SerializationSafeRepository.java │ │ │ ├── UserArgumentResolver.java │ │ │ └── CachingConfig.java │ │ │ ├── service │ │ │ ├── BannerService.java │ │ │ ├── CSVHelper.java │ │ │ ├── StatsService.java │ │ │ ├── UserService.java │ │ │ ├── WishService.java │ │ │ └── PublicStatsService.java │ │ │ └── GenshinWishesApplication.java │ └── resources │ │ ├── i18n │ │ └── export.properties │ │ └── application.yml └── test │ └── java │ └── com │ └── uf │ └── genshinwishes │ └── GenshinWishesApplicationTests.java ├── README.md ├── .editorconfig ├── Dockerfile ├── .travis.yml ├── .gitignore ├── pom.xml ├── mvnw.cmd └── mvnw /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=15 -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genshin-wishes/genshin-wishes-api/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/repository/ItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.repository; 2 | 3 | import com.uf.genshinwishes.model.Item; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface ItemRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/ItemType.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @ToString 10 | public enum ItemType { 11 | Weapon, 12 | Character 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/exception/ApiError.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.exception; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class ApiError extends RuntimeException { 9 | 10 | private ErrorType errorType; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.*; 4 | 5 | @Getter 6 | @Setter 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @EqualsAndHashCode 10 | public class MihoyoUserDTO { 11 | private String user_id; 12 | private String nickname; 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Genshin Wishes 2 | 3 | Official repository of [Genshin Wishes](https://genshin-wishes.com) 4 | 5 | Genshin Wishes will no longer be maintained, more information [here](https://tiny.one/ythjv5sm). 6 | 7 | Genshin Impact and miHoYo are trademarks or registered trademarks of miHoYo. Genshin Impact © miHoYo. 8 | For any claim, please contact us 9 | -------------------------------------------------------------------------------- /src/test/java/com/uf/genshinwishes/GenshinWishesApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class GenshinWishesApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerPity.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class CountPerPity { 11 | private Long pity; 12 | private Long count; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/i18n/export.properties: -------------------------------------------------------------------------------- 1 | banner=Banner 2 | index=Index 3 | item=Item 4 | itemType=Item Type 5 | itemRarity=Item Rarity 6 | date=Date 7 | 8 | # Item Types 9 | 10 | Weapon=Weapon 11 | Character=Character 12 | 13 | # Banners 14 | 15 | NOVICE=Novice 16 | PERMANENT=Permanent 17 | CHARACTER_EVENT=Character Event 18 | WEAPON_EVENT=Weapon Event 19 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerItemId.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class CountPerItemId { 11 | private Long itemId; 12 | private Long count; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerBanner.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class CountPerBanner { 11 | private Integer gachaType; 12 | private Long count; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerRegion.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class CountPerRegion { 11 | private String region; 12 | private Long count; 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.ts] 13 | quote_type = single 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerDay.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDate; 8 | 9 | @Getter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CountPerDay { 13 | private LocalDate date; 14 | private Long count; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/ExclusivityDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | public class ExclusivityDTO { 11 | private Long guarantee; 12 | private Long win; 13 | private Long loose; 14 | private Long delta; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoInfoRetDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class MihoyoInfoRetDTO extends MihoyoRetDTO { 13 | private MihoyoUserDTO data; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoWishRetDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class MihoyoWishRetDTO extends MihoyoRetDTO { 13 | private MihoyoWishDataDTO data; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoRetDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public abstract class MihoyoRetDTO { 13 | private String message; 14 | private Integer retcode; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerDayDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDate; 8 | 9 | @Getter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CountPerDayDTO { 13 | private LocalDate date; 14 | private Integer rankType; 15 | private Long count; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoWishDataDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class MihoyoWishDataDTO { 15 | private List list; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerRankAndDay.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDate; 8 | 9 | @Getter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CountPerRankAndDay { 13 | private LocalDate date; 14 | private Integer rankType; 15 | private Long count; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/CountPerUserPerItemId.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class CountPerUserPerItemId { 12 | private User user; 13 | private Long itemId; 14 | private Long count; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.repository; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface UserRepository extends JpaRepository { 7 | 8 | User findByEmail(String email); 9 | 10 | User findByProfileId(String profileId); 11 | 12 | Long countByMihoyoUsernameIsNotNull(); 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### STAGE 1: Build ### 2 | FROM maven:3.6.3-openjdk-15-slim AS build 3 | RUN mkdir -p /workspace 4 | WORKDIR /workspace 5 | COPY pom.xml /workspace 6 | COPY src /workspace/src 7 | RUN mvn -f pom.xml clean package -Dmaven.test.skip=true 8 | 9 | ### STAGE 2: Run ### 10 | FROM openjdk:15-alpine 11 | 12 | RUN apk --no-cache add curl 13 | 14 | COPY --from=build /workspace/target/*.jar app.jar 15 | 16 | EXPOSE 8080 17 | ENTRYPOINT ["java","-jar","app.jar"] -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/LatestEventsCountsDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | import java.util.List; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | public class LatestEventsCountsDTO { 13 | private Long count; 14 | private Long count5; 15 | private Long count4; 16 | private List items; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/WishFilterDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.util.List; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Builder(toBuilder=true) 12 | @ToString 13 | public class WishFilterDTO { 14 | private List items; 15 | private Boolean fr; 16 | private List ranks; 17 | private ItemType itemType; 18 | private List events; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(GenshinWishesApplication.class); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mihoyo/MihoyoWishLogDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mihoyo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class MihoyoWishLogDTO { 13 | private String id; 14 | private String uid; 15 | private String item_id; 16 | private String name; 17 | private Integer gacha_type; 18 | private String time; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/WishDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @Getter 11 | @Setter 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class WishDTO { 15 | private Long index; 16 | private Long pity; 17 | private Integer gachaType; 18 | private Boolean second; 19 | private Long itemId; 20 | private LocalDateTime time; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/BannerImportStateDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import com.uf.genshinwishes.model.BannerType; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class BannerImportStateDTO { 14 | private BannerType bannerType; 15 | private Integer count; 16 | private boolean finished; 17 | private boolean saved; 18 | private String error; 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | os: linux 3 | jdk: 4 | - oraclejdk15 5 | 6 | services: 7 | - docker 8 | 9 | install: true 10 | 11 | before_script: 12 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 13 | - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) 14 | 15 | script: 16 | - docker build -t genshinwishes/genshin-wishes-api:$VERSION . 17 | - docker push genshinwishes/genshin-wishes-api:$VERSION 18 | 19 | before_deploy: 20 | - git tag $VERSION 21 | 22 | deploy: 23 | provider: releases 24 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/Image.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.Id; 11 | 12 | @Entity(name = "upload_file") 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class Image { 18 | @Id 19 | private Long id; 20 | 21 | @Column(nullable = false) 22 | private String url; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class UserDTO { 13 | private String email; 14 | private String lang; 15 | private String region; 16 | private Boolean wholeClock; 17 | private String mihoyoUsername; 18 | private String mihoyoUid; 19 | private String profileId; 20 | private Boolean sharing; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/repository/BannerRepository.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.repository; 2 | 3 | import com.uf.genshinwishes.model.Banner; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface BannerRepository extends JpaRepository { 9 | 10 | @Override 11 | List findAll(); 12 | 13 | List findAllByGachaTypeOrderByStartDesc(Integer gachaType); 14 | 15 | List findAllByOrderByStartDesc(); 16 | 17 | Banner findFirstByGachaTypeOrderByEndDesc(Integer gachaType); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | HELP.md 3 | target/ 4 | lib/ 5 | src/main/resources/application-dev.yml 6 | !.mvn/wrapper/maven-wrapper.jar 7 | !**/src/main/**/target/ 8 | !**/src/test/**/target/ 9 | 10 | ### STS ### 11 | .apt_generated 12 | .classpath 13 | .factorypath 14 | .project 15 | .settings 16 | .springBeans 17 | .sts4-cache 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | 25 | ### NetBeans ### 26 | /nbproject/private/ 27 | /nbbuild/ 28 | /dist/ 29 | /nbdist/ 30 | /.nb-gradle/ 31 | build/ 32 | !**/src/main/**/build/ 33 | !**/src/test/**/build/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/Region.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.Arrays; 6 | 7 | @Getter 8 | public enum Region { 9 | AMERICA("6"), 10 | EUROPE("7"), 11 | ASIA("8"), 12 | 13 | // 14 | ; 15 | 16 | private String prefix; 17 | 18 | Region(String prefix) { 19 | this.prefix = prefix; 20 | } 21 | 22 | public static Region getFromUser(User user) { 23 | return Arrays.stream(Region.values()) 24 | .filter(r -> r.getPrefix().equals(user.getRegion())) 25 | .findFirst().orElse(Region.ASIA); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/StatsDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import com.uf.genshinwishes.model.BannerType; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class StatsDTO { 16 | private BannerType bannerType; 17 | 18 | private Long count; 19 | private Long count4Stars; 20 | private Long count5Stars; 21 | 22 | private List wishes; 23 | 24 | private List countPerDay; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/ItemController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.model.Item; 4 | import com.uf.genshinwishes.repository.ItemRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/items") 12 | public class ItemController { 13 | @Autowired 14 | private ItemRepository itemRepository; 15 | 16 | @GetMapping("") 17 | public Iterable getItems() { 18 | return itemRepository.findAll(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/BannerDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import com.uf.genshinwishes.model.BannerType; 4 | import com.uf.genshinwishes.model.Image; 5 | import com.uf.genshinwishes.model.Item; 6 | import com.uf.genshinwishes.model.Region; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class BannerDTO { 21 | private Long id; 22 | private String version; 23 | private List items; 24 | LocalDateTime start; 25 | LocalDateTime end; 26 | Map startEndByRegion; 27 | private BannerType gachaType; 28 | private Image image; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mapper; 2 | 3 | import com.uf.genshinwishes.dto.UserDTO; 4 | import com.uf.genshinwishes.model.User; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class UserMapper { 9 | public UserDTO toDto(User user) { 10 | if(user == null) return null; 11 | 12 | UserDTO userDTO = new UserDTO(); 13 | 14 | userDTO.setEmail(user.getEmail()); 15 | userDTO.setLang(user.getLang()); 16 | userDTO.setWholeClock(user.getWholeClock()); 17 | userDTO.setMihoyoUid(user.getMihoyoUid()); 18 | userDTO.setRegion(user.getRegion()); 19 | userDTO.setMihoyoUsername(user.getMihoyoUsername()); 20 | userDTO.setProfileId(user.getProfileId()); 21 | userDTO.setSharing(user.getSharing()); 22 | 23 | return userDTO; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/BannerType.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.Arrays; 8 | import java.util.Optional; 9 | import java.util.stream.Stream; 10 | 11 | @Getter 12 | @AllArgsConstructor 13 | @ToString 14 | public enum BannerType { 15 | ALL(-1), 16 | NOVICE(100), 17 | PERMANENT(200), 18 | CHARACTER_EVENT(301), 19 | WEAPON_EVENT(302), 20 | 21 | // 22 | ; 23 | 24 | private Integer type; 25 | 26 | public static Optional from(Integer gachaType) { 27 | return Arrays.stream(BannerType.values()).filter(banner -> banner.getType().equals(gachaType)).findFirst(); 28 | } 29 | 30 | public static Stream getBannersExceptAll() { 31 | return Arrays.stream(BannerType.values()).filter(banner -> banner.getType() > 0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/exception/RestResponseEntityExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.exception; 2 | 3 | import org.springframework.http.HttpHeaders; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.context.request.WebRequest; 8 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 9 | 10 | @ControllerAdvice 11 | public class RestResponseEntityExceptionHandler 12 | extends ResponseEntityExceptionHandler { 13 | 14 | @ExceptionHandler(value 15 | = {ApiError.class}) 16 | protected ResponseEntity handleConflict( 17 | RuntimeException ex, WebRequest request) { 18 | return handleExceptionInternal(ex, ((ApiError) ex).getErrorType(), 19 | new HttpHeaders(), ((ApiError) ex).getErrorType().getStatus(), request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/PublicStatsDTO.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto; 2 | 3 | import com.uf.genshinwishes.model.BannerType; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | public class PublicStatsDTO { 15 | private Long count; 16 | private Long count5Stars; 17 | private Long count4Stars; 18 | 19 | private Float exclusiveRate5Stars; 20 | private Float exclusiveRate4Stars; 21 | 22 | private Map latestEventsCounts; 23 | 24 | private List usersPerRegion; 25 | private List countPerRegion; 26 | 27 | private List countPerBanner; 28 | 29 | private List countPerPity5Stars; 30 | private List countPerPity4Stars; 31 | 32 | private List countPerDay; 33 | private List countPerItemId; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import com.uf.genshinwishes.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class CustomUserDetailsService implements UserDetailsService { 13 | 14 | @Autowired 15 | private UserService userService; 16 | 17 | @Override 18 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 19 | User user = userService.findByEmail(s); 20 | 21 | if (user == null) throw new UsernameNotFoundException("Username not found " + s); 22 | 23 | userService.updateLastLoggingDate(user); 24 | 25 | return UserPrincipal.create(user); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/exception/ErrorType.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.exception; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum ErrorType { 10 | AUTHKEY_INVALID(HttpStatus.INTERNAL_SERVER_ERROR), 11 | MIHOYO_UID_DIFFERENT(HttpStatus.FORBIDDEN), 12 | NO_MIHOYO_LINKED(HttpStatus.FORBIDDEN), 13 | MIHOYO_UNREACHABLE(HttpStatus.GATEWAY_TIMEOUT), 14 | INVALID_LANG(HttpStatus.BAD_REQUEST), 15 | NEW_WISHES_DURING_IMPORT(HttpStatus.BAD_REQUEST), 16 | NO_SUITABLE_ENDPOINT_FOR_GAME_BIZ(HttpStatus.BAD_REQUEST), 17 | NO_REGION_FROM_USER_UID(HttpStatus.INTERNAL_SERVER_ERROR), 18 | INVALID_FILTERS(HttpStatus.BAD_REQUEST), 19 | ALREADY_IMPORTING(HttpStatus.BAD_REQUEST), 20 | PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND), 21 | MISSING_ITEM(HttpStatus.INTERNAL_SERVER_ERROR), 22 | ERROR(HttpStatus.INTERNAL_SERVER_ERROR), 23 | IMPORT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR) 24 | 25 | // 26 | ; 27 | 28 | private HttpStatus status; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/Item.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.hibernate.annotations.NaturalId; 8 | import org.hibernate.annotations.Where; 9 | import org.hibernate.annotations.WhereJoinTable; 10 | 11 | import javax.persistence.*; 12 | import java.io.Serializable; 13 | 14 | @Entity(name = "items") 15 | @Where(clause="published_at is not null") 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class Item implements Serializable { 21 | 22 | @Id 23 | private Long id; 24 | 25 | @NaturalId 26 | @Column(name = "item_id", nullable = false) 27 | private Long itemId; 28 | 29 | @Column(nullable = false) 30 | private String name; 31 | 32 | @Column(nullable = false) 33 | private String itemType; 34 | 35 | @Column(nullable = false) 36 | private Integer rankType; 37 | 38 | @ManyToOne 39 | @JoinTable( 40 | name = "upload_file_morph", 41 | joinColumns = @JoinColumn(name = "related_id"), 42 | inverseJoinColumns = @JoinColumn(name = "upload_file_id")) 43 | @WhereJoinTable(clause = "related_type = 'items'") 44 | private Image image; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/User.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serial; 7 | import java.io.Serializable; 8 | import java.util.Date; 9 | 10 | @Entity(name = "users") 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @EqualsAndHashCode(onlyExplicitlyIncluded = true) 16 | public class User implements Serializable { 17 | @Serial 18 | private static final long serialVersionUID = -51419785561130265L; 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @EqualsAndHashCode.Include 23 | private Long id; 24 | 25 | @Column(nullable = false) 26 | private String email; 27 | 28 | @Column 29 | private String region; 30 | 31 | @Column 32 | private String key; 33 | 34 | @Column() 35 | private String lang; 36 | 37 | @Column() 38 | private Boolean wholeClock; 39 | 40 | @Column(nullable = false) 41 | private Date creationDate; 42 | 43 | @Column() 44 | private Date lastLoggingDate; 45 | 46 | @Column() 47 | private String mihoyoUsername; 48 | 49 | @Column() 50 | private String mihoyoUid; 51 | 52 | @Column() 53 | private String profileId; 54 | 55 | @Column() 56 | private Boolean sharing; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/CustomOidcUserService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import com.uf.genshinwishes.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; 7 | import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; 8 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 9 | import org.springframework.security.oauth2.core.OAuth2Error; 10 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.Map; 14 | 15 | @Service 16 | public class CustomOidcUserService extends OidcUserService { 17 | 18 | @Autowired 19 | private UserService userService; 20 | 21 | @Override 22 | public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { 23 | OidcUser oidcUser = super.loadUser(userRequest); 24 | Map attributes = oidcUser.getAttributes(); 25 | 26 | Object email = attributes.get("email"); 27 | 28 | if (email == null) throw new OAuth2AuthenticationException(new OAuth2Error("email-permission-required")); 29 | 30 | User user = userService.retrieveOrInsertUser((String) email); 31 | 32 | return UserPrincipal.create(user); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/BannerController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.BannerDTO; 4 | import com.uf.genshinwishes.model.BannerType; 5 | import com.uf.genshinwishes.model.User; 6 | import com.uf.genshinwishes.service.BannerService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.Map; 13 | 14 | @RestController 15 | @RequestMapping("/banners") 16 | public class BannerController { 17 | @Autowired 18 | private BannerService bannerService; 19 | 20 | @GetMapping("") 21 | public Iterable getBanners(User user) { 22 | return bannerService.findAllForUser(user); 23 | } 24 | 25 | @GetMapping("/character") 26 | public Iterable getCharacterEvents(User user) { 27 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(user, BannerType.CHARACTER_EVENT.getType()); 28 | } 29 | 30 | @GetMapping("/weapon") 31 | public Iterable getWeaponEvents(User user) { 32 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(user, BannerType.WEAPON_EVENT.getType()); 33 | } 34 | 35 | @GetMapping("/latest") 36 | public Map getLatestEvents(User user) { 37 | return bannerService.getLatestBannerToEventMap(user); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/config/SerializationSafeRepository.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.config; 2 | 3 | import org.springframework.data.redis.core.RedisTemplate; 4 | import org.springframework.data.redis.serializer.SerializationException; 5 | import org.springframework.session.Session; 6 | import org.springframework.session.SessionRepository; 7 | 8 | public class SerializationSafeRepository implements SessionRepository { 9 | private final SessionRepository delegate; 10 | private final RedisTemplate redisTemplate; 11 | private static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:"; 12 | 13 | public SerializationSafeRepository(SessionRepository delegate, 14 | RedisTemplate redisTemplate) { 15 | this.delegate = delegate; 16 | this.redisTemplate = redisTemplate; 17 | } 18 | 19 | @Override 20 | public S createSession() { 21 | return delegate.createSession(); 22 | } 23 | 24 | @Override 25 | public void save(S session) { 26 | delegate.save(session); 27 | } 28 | 29 | @Override 30 | public S findById(String id) { 31 | try { 32 | return delegate.findById(id); 33 | } catch (SerializationException e) { 34 | redisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id); 35 | return null; 36 | } 37 | } 38 | 39 | @Override 40 | public void deleteById(String id) { 41 | delegate.deleteById(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/StatsController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.StatsDTO; 4 | import com.uf.genshinwishes.dto.WishFilterDTO; 5 | import com.uf.genshinwishes.model.BannerType; 6 | import com.uf.genshinwishes.model.User; 7 | import com.uf.genshinwishes.service.StatsService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.Stream; 16 | 17 | @RestController 18 | @RequestMapping("/stats") 19 | public class StatsController { 20 | @Autowired 21 | private StatsService statsService; 22 | 23 | @GetMapping("/{bannerType}") 24 | public StatsDTO getStats(User user, 25 | @PathVariable("bannerType") Optional bannerType, 26 | @RequestParam Optional> characterEvents, 27 | @RequestParam Optional> weaponEvents) { 28 | WishFilterDTO filters = WishFilterDTO.builder().events( 29 | Stream.concat( 30 | characterEvents.orElse(Collections.emptyList()).stream(), 31 | weaponEvents.orElse(Collections.emptyList()).stream() 32 | ).collect(Collectors.toList()) 33 | ).build(); 34 | 35 | return statsService.getStatsFor(user, bannerType.orElse(BannerType.ALL), filters); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/Banner.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.hibernate.annotations.NotFound; 8 | import org.hibernate.annotations.NotFoundAction; 9 | import org.hibernate.annotations.Where; 10 | 11 | import javax.persistence.*; 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | 15 | @Entity(name = "events") 16 | @Where(clause="published_at is not null") 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class Banner { 22 | @Id 23 | private Long id; 24 | 25 | @Column 26 | private String version; 27 | 28 | @ManyToMany 29 | @NotFound(action = NotFoundAction.IGNORE) 30 | @JoinTable( 31 | name = "events__items", 32 | joinColumns = @JoinColumn(name = "event_id"), 33 | inverseJoinColumns = @JoinColumn(name = "item_id")) 34 | private List items; 35 | 36 | @Column(nullable = false) 37 | private LocalDateTime start; 38 | 39 | @Column(nullable = false) 40 | private LocalDateTime end; 41 | 42 | @Column(nullable = false) 43 | private Integer gachaType; 44 | 45 | @Column 46 | private Boolean isStartLocale; 47 | @Column 48 | private Boolean isEndLocale; 49 | 50 | @ManyToOne 51 | @JoinTable( 52 | name = "upload_file_morph", 53 | joinColumns = @JoinColumn(name = "related_id"), 54 | inverseJoinColumns = @JoinColumn(name = "upload_file_id")) 55 | private Image image; 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/exception/ExceptionHandlerFilter.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.exception; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.filter.OncePerRequestFilter; 9 | 10 | import javax.servlet.FilterChain; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | public class ExceptionHandlerFilter extends OncePerRequestFilter { 17 | 18 | Logger logger = LoggerFactory.getLogger(ExceptionHandlerFilter.class); 19 | 20 | @Override 21 | public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 22 | try { 23 | filterChain.doFilter(request, response); 24 | } catch (RuntimeException e) { 25 | 26 | logger.error("Error during request", e); 27 | 28 | response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 29 | response.getWriter().write(convertObjectToJson(null)); 30 | } 31 | } 32 | 33 | public String convertObjectToJson(Object object) throws JsonProcessingException { 34 | if (object == null) { 35 | return null; 36 | } 37 | ObjectMapper mapper = new ObjectMapper(); 38 | return mapper.writeValueAsString(object); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/model/Wish.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.model; 2 | 3 | import com.uf.genshinwishes.dto.mapper.BannerMapper; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import javax.persistence.*; 10 | import java.time.Instant; 11 | import java.time.LocalDateTime; 12 | import java.time.ZoneId; 13 | import java.time.ZonedDateTime; 14 | import java.time.temporal.ChronoUnit; 15 | import java.time.temporal.TemporalUnit; 16 | 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | @Entity(name = "WISHES") 22 | public class Wish { 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | private Long id; 26 | 27 | @Column(nullable = false) 28 | private Long index; 29 | 30 | @Column 31 | private Long pity; 32 | 33 | @ManyToOne(fetch = FetchType.LAZY) 34 | @JoinColumn(name = "user_id", nullable = false) 35 | private User user; 36 | 37 | @Column(nullable = false) 38 | private Integer gachaType; 39 | 40 | @Column 41 | private Boolean second; 42 | 43 | @ManyToOne 44 | @JoinColumn(name = "item_id", referencedColumnName = "item_id") 45 | private Item item; 46 | 47 | @Column(nullable = false) 48 | private LocalDateTime time; 49 | 50 | @Column 51 | private Instant importDate; 52 | 53 | public boolean isBeforeArchive() { 54 | return this.getImportDate() != null 55 | ? this.getImportDate().isBefore(BannerMapper.computeImportArchiveDate()) 56 | : this.getTime().isBefore(BannerMapper.computeArchiveDate(Region.getFromUser(user))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/CustomOauth2UserService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import com.uf.genshinwishes.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; 7 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 8 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 9 | import org.springframework.security.oauth2.core.OAuth2Error; 10 | import org.springframework.security.oauth2.core.user.OAuth2User; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.Map; 14 | 15 | @Service 16 | public class CustomOauth2UserService extends DefaultOAuth2UserService { 17 | 18 | @Autowired 19 | private UserService userService; 20 | 21 | @Override 22 | public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { 23 | OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); 24 | 25 | Map attributes = oAuth2User.getAttributes(); 26 | 27 | Object email = attributes.get("email"); 28 | Object openid = attributes.get("openid"); 29 | 30 | if (email == null && openid == null) 31 | throw new OAuth2AuthenticationException(new OAuth2Error("permission-required")); 32 | 33 | String providerAndId = oAuth2UserRequest.getClientRegistration().getRegistrationId() + "_" + openid; 34 | 35 | User user = userService.retrieveOrInsertUser(email != null ? (String) email : providerAndId); 36 | 37 | return UserPrincipal.create(user); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/config/UserArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.config; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import com.uf.genshinwishes.security.UserPrincipal; 5 | import com.uf.genshinwishes.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.core.MethodParameter; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.bind.support.WebDataBinderFactory; 11 | import org.springframework.web.context.request.NativeWebRequest; 12 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 13 | import org.springframework.web.method.support.ModelAndViewContainer; 14 | 15 | @Component 16 | public class UserArgumentResolver implements HandlerMethodArgumentResolver { 17 | @Autowired 18 | private UserService userService; 19 | 20 | @Override 21 | public boolean supportsParameter(MethodParameter parameter) { 22 | return parameter.getParameterType().equals(User.class); 23 | } 24 | 25 | @Override 26 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 27 | if (SecurityContextHolder.getContext().getAuthentication() == null || !(SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof UserPrincipal)) 28 | return null; 29 | 30 | UserPrincipal user = ((UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); 31 | 32 | return userService.findByEmail(user.getUsername()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.UserDTO; 4 | import com.uf.genshinwishes.dto.mapper.UserMapper; 5 | import com.uf.genshinwishes.model.User; 6 | import com.uf.genshinwishes.service.UserService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | @RestController 11 | @RequestMapping("/user") 12 | public class UserController { 13 | @Autowired 14 | private UserService userService; 15 | @Autowired 16 | private UserMapper userMapper; 17 | 18 | @GetMapping("") 19 | public UserDTO isLoggedIn(User user) { 20 | return userMapper.toDto(user); 21 | } 22 | 23 | @PatchMapping("/share") 24 | public String share(User user) { 25 | String profileId = user.getProfileId(); 26 | 27 | if (profileId == null) { 28 | profileId = userService.initProfileId(user); 29 | } 30 | 31 | userService.share(user, true); 32 | 33 | return profileId; 34 | } 35 | 36 | @PatchMapping("/stopSharing") 37 | public void stopSharing(User user) { 38 | userService.share(user, false); 39 | } 40 | 41 | @DeleteMapping("/delete") 42 | public void deleteUser(User user) { 43 | userService.deleteUser(user); 44 | } 45 | 46 | @PatchMapping("/lang") 47 | public void updateLang(User user, @RequestParam("lang") String lang) { 48 | userService.updateLang(user, lang); 49 | } 50 | 51 | @PatchMapping("/wholeClock") 52 | public void updateWholeClock(User user, @RequestParam("wholeClock") Boolean wholeClock) { 53 | userService.updateWholeClock(user, wholeClock); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/repository/wish/WishRepository.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.repository.wish; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import com.uf.genshinwishes.model.Wish; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.PagingAndSortingRepository; 8 | 9 | import java.time.Instant; 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | public interface WishRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { 15 | 16 | Optional findFirstByUserOrderByTimeDescIdDesc(User user); 17 | 18 | List findFirst100ByUserAndGachaTypeOrderByIdDesc(User user, Integer gachaType); 19 | 20 | void deleteByUser(User user); 21 | 22 | Long countByUserAndGachaType(User user, Integer gachaType); 23 | 24 | @Query(value = """ 25 | select w.* from wishes w 26 | where w.index = ( 27 | select max(w2.index) from wishes w2 28 | join items i on w2.item_id = i.item_id 29 | where w2.index <= :wishIndex and w2.user_id = :userId and w2.gacha_type = :gachaType 30 | and i.rank_type = :rankType 31 | ) and w.user_id = :userId and w.gacha_type = :gachaType 32 | """, nativeQuery = true) 33 | Optional findByUserAndRankTypeAndGachaTypeAndWishIndex(Long userId, Integer rankType, Integer gachaType, Long wishIndex); 34 | 35 | @Query(value = """ 36 | select w.* from wishes w 37 | where w.index = ( 38 | select min(w2.index) from wishes w2 39 | where (w2.import_date is not null and w2.import_date >= :importArchiveDate or w2.time >= :archiveDate) and w2.user_id = :userId and w2.gacha_type = :gachaType 40 | ) and w.user_id = :userId and w.gacha_type = :gachaType 41 | """, nativeQuery = true) 42 | Optional findFirstNonArchived(Long userId, Integer gachaType, Instant importArchiveDate, LocalDateTime archiveDate); 43 | 44 | List findByUserOrderByGachaTypeAscIndexAsc(User user); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mapper/WishMapper.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mapper; 2 | 3 | import com.uf.genshinwishes.dto.WishDTO; 4 | import com.uf.genshinwishes.dto.mihoyo.MihoyoWishLogDTO; 5 | import com.uf.genshinwishes.exception.ApiError; 6 | import com.uf.genshinwishes.exception.ErrorType; 7 | import com.uf.genshinwishes.model.BannerType; 8 | import com.uf.genshinwishes.model.Item; 9 | import com.uf.genshinwishes.model.Wish; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.time.LocalDateTime; 13 | import java.time.format.DateTimeFormatter; 14 | import java.util.List; 15 | 16 | @Component 17 | public class WishMapper { 18 | public WishDTO toDto(Wish wish) { 19 | if (wish == null) return null; 20 | 21 | WishDTO wishDTO = new WishDTO(); 22 | 23 | wishDTO.setTime(wish.getTime()); 24 | wishDTO.setGachaType(wish.getGachaType()); 25 | wishDTO.setSecond(wish.getSecond()); 26 | wishDTO.setIndex(wish.getIndex()); 27 | wishDTO.setPity(wish.getPity()); 28 | 29 | if (wish.getItem() != null) { 30 | wishDTO.setItemId(wish.getItem().getItemId()); 31 | } 32 | 33 | return wishDTO; 34 | } 35 | 36 | public Wish fromMihoyo(MihoyoWishLogDTO mihoyoWish, List items) { 37 | if (mihoyoWish == null) return null; 38 | 39 | Wish wish = new Wish(); 40 | 41 | Item item = items.stream() 42 | .filter(i -> i.getName().equals(mihoyoWish.getName())) 43 | .findFirst() 44 | .orElse(null); 45 | 46 | if (item == null) throw new ApiError(ErrorType.MISSING_ITEM); 47 | 48 | wish.setItem(item); 49 | 50 | Integer gachaType = mihoyoWish.getGacha_type(); 51 | wish.setGachaType(gachaType == 400 ? BannerType.CHARACTER_EVENT.getType() : gachaType); // FIXME dirty fix 52 | wish.setSecond(gachaType == 400); 53 | wish.setTime( 54 | LocalDateTime.from( 55 | DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse( 56 | mihoyoWish.getTime().replace(' ', 'T') 57 | ) 58 | ) 59 | ); 60 | 61 | return wish; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/service/BannerService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.uf.genshinwishes.dto.BannerDTO; 5 | import com.uf.genshinwishes.dto.mapper.BannerMapper; 6 | import com.uf.genshinwishes.model.BannerType; 7 | import com.uf.genshinwishes.model.User; 8 | import com.uf.genshinwishes.repository.BannerRepository; 9 | import lombok.AllArgsConstructor; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | @Service 19 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 20 | public class BannerService { 21 | 22 | private BannerRepository bannerRepository; 23 | private BannerMapper bannerMapper; 24 | 25 | public Map getLatestBannerToEventMap(User user) { 26 | HashMap bannerToEvent = Maps.newHashMap(); 27 | 28 | BannerType.getBannersExceptAll().forEach(banner -> 29 | bannerToEvent.put(banner.getType(), 30 | bannerMapper.toDto(user, bannerRepository.findFirstByGachaTypeOrderByEndDesc(banner.getType())) 31 | )); 32 | 33 | return bannerToEvent; 34 | } 35 | 36 | public List findAll() { 37 | return bannerRepository.findAll().stream().map(bannerMapper::toDto).collect(Collectors.toList()); 38 | } 39 | 40 | public List findAllOrderByStartDateDesc() { 41 | return bannerRepository.findAllByOrderByStartDesc().stream().map(bannerMapper::toDto).collect(Collectors.toList()); 42 | } 43 | 44 | public List findAllForUser(User user) { 45 | return bannerRepository.findAll().stream().map(b -> bannerMapper.toDto(user, b)).collect(Collectors.toList()); 46 | } 47 | 48 | public List findAllByGachaTypeOrderByStartDateDesc(Integer gachaType) { 49 | return bannerRepository.findAllByGachaTypeOrderByStartDesc(gachaType).stream().map(bannerMapper::toDto).collect(Collectors.toList()); 50 | } 51 | 52 | public List findAllByGachaTypeOrderByStartDateDesc(User user, Integer gachaType) { 53 | return bannerRepository.findAllByGachaTypeOrderByStartDesc(gachaType).stream().map(b -> bannerMapper.toDto(user, b)).collect(Collectors.toList()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/PublicController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.BannerDTO; 4 | import com.uf.genshinwishes.dto.PublicStatsDTO; 5 | import com.uf.genshinwishes.model.BannerType; 6 | import com.uf.genshinwishes.service.BannerService; 7 | import com.uf.genshinwishes.service.PublicStatsService; 8 | import com.uf.genshinwishes.service.UserService; 9 | import com.uf.genshinwishes.service.WishService; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | import java.util.Locale; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.stream.Collectors; 18 | 19 | @RestController 20 | @RequestMapping("/public") 21 | public class PublicController { 22 | @Autowired 23 | private UserService userService; 24 | @Autowired 25 | private WishService wishService; 26 | @Autowired 27 | private BannerService bannerService; 28 | @Autowired 29 | private PublicStatsService publicStatsService; 30 | 31 | @GetMapping("/languages") 32 | public Map getLanguages(@RequestParam("locales") List locales) { 33 | return locales.stream() 34 | .map(locale -> Locale.forLanguageTag(locale)) 35 | .collect(Collectors.toMap(Locale::toLanguageTag, locale -> locale.getDisplayName(locale))); 36 | } 37 | 38 | @GetMapping("/banners") 39 | public List getBanners() { 40 | return bannerService.findAll(); 41 | } 42 | 43 | @GetMapping("/banners/character") 44 | public Iterable getCharacterEvents() { 45 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(BannerType.CHARACTER_EVENT.getType()); 46 | } 47 | 48 | @GetMapping("/banners/weapon") 49 | public Iterable getWeaponEvents() { 50 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(BannerType.WEAPON_EVENT.getType()); 51 | } 52 | 53 | @GetMapping("/banners/latest") 54 | public Map getLatestEvents() { 55 | return bannerService.getLatestBannerToEventMap(null); 56 | } 57 | 58 | @GetMapping("/users/count") 59 | public Long getUsersCount() { 60 | return this.userService.getUsersCount(); 61 | } 62 | 63 | @GetMapping("/wishes/count") 64 | public Long getWishesCount() { 65 | return this.wishService.getWishesCount(); 66 | } 67 | 68 | @GetMapping("/stats/{bannerType}") 69 | public PublicStatsDTO getPublicStats(@PathVariable("bannerType") Optional bannerType, 70 | @RequestParam Optional event) { 71 | return this.publicStatsService.getStatsFor(bannerType.orElse(BannerType.ALL), event.orElse(null)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/GenshinWishesApplication.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes; 2 | 3 | import com.uf.genshinwishes.config.SerializationSafeRepository; 4 | import com.uf.genshinwishes.config.UserArgumentResolver; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Primary; 10 | import org.springframework.context.support.ResourceBundleMessageSource; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | import org.springframework.session.SessionRepository; 14 | import org.springframework.session.data.redis.RedisIndexedSessionRepository; 15 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 16 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 17 | import org.springframework.web.servlet.LocaleResolver; 18 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 19 | import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; 20 | 21 | import java.util.List; 22 | import java.util.Locale; 23 | 24 | @SpringBootApplication 25 | @EnableScheduling 26 | @EnableRedisHttpSession 27 | public class GenshinWishesApplication { 28 | @Autowired 29 | private UserArgumentResolver userArgumentResolver; 30 | @Autowired 31 | RedisTemplate redisTemplate; 32 | 33 | public static void main(String[] args) { 34 | SpringApplication.run(GenshinWishesApplication.class, args); 35 | } 36 | 37 | @Primary 38 | @Bean 39 | public SessionRepository primarySessionRepository(RedisIndexedSessionRepository delegate) { 40 | return new SerializationSafeRepository(delegate, redisTemplate); 41 | } 42 | 43 | @Bean 44 | public WebMvcConfigurer corsConfigurer() { 45 | return new WebMvcConfigurer() { 46 | 47 | @Override 48 | public void addArgumentResolvers(List resolvers) { 49 | resolvers.add(userArgumentResolver); 50 | } 51 | }; 52 | } 53 | 54 | @Bean 55 | public LocaleResolver localeResolver() { 56 | AcceptHeaderLocaleResolver slr = new AcceptHeaderLocaleResolver(); 57 | slr.setDefaultLocale(Locale.ENGLISH); 58 | return slr; 59 | } 60 | 61 | @Bean 62 | public ResourceBundleMessageSource messageSource() { 63 | ResourceBundleMessageSource source = new ResourceBundleMessageSource(); 64 | source.setBasenames("i18n/export"); // name of the resource bundle 65 | source.setUseCodeAsDefaultMessage(true); 66 | return source; 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/OAuth2AccessTokenResponseConverterWithDefaults.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 5 | import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; 6 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 7 | import org.springframework.util.Assert; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.*; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | public class OAuth2AccessTokenResponseConverterWithDefaults implements Converter, OAuth2AccessTokenResponse> { 15 | private static final Set TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of( 16 | OAuth2ParameterNames.ACCESS_TOKEN, 17 | OAuth2ParameterNames.TOKEN_TYPE, 18 | OAuth2ParameterNames.EXPIRES_IN, 19 | OAuth2ParameterNames.REFRESH_TOKEN, 20 | OAuth2ParameterNames.SCOPE).collect(Collectors.toSet()); 21 | 22 | private OAuth2AccessToken.TokenType defaultAccessTokenType = OAuth2AccessToken.TokenType.BEARER; 23 | 24 | @Override 25 | public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) { 26 | String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); 27 | 28 | OAuth2AccessToken.TokenType accessTokenType = this.defaultAccessTokenType; 29 | if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase( 30 | tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) { 31 | accessTokenType = OAuth2AccessToken.TokenType.BEARER; 32 | } 33 | 34 | long expiresIn = 0; 35 | if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) { 36 | try { 37 | expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN)); 38 | } catch (NumberFormatException ex) { } 39 | } 40 | 41 | Set scopes = Collections.emptySet(); 42 | if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { 43 | String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE); 44 | scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")).collect(Collectors.toSet()); 45 | } 46 | 47 | Map additionalParameters = new LinkedHashMap<>(); 48 | tokenResponseParameters.entrySet().stream() 49 | .filter(e -> !TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey())) 50 | .forEach(e -> additionalParameters.put(e.getKey(), e.getValue())); 51 | 52 | return OAuth2AccessTokenResponse.withToken(accessToken) 53 | .tokenType(accessTokenType) 54 | .expiresIn(expiresIn) 55 | .scopes(scopes) 56 | .additionalParameters(additionalParameters) 57 | .build(); 58 | } 59 | 60 | public final void setDefaultAccessTokenType(OAuth2AccessToken.TokenType defaultAccessTokenType) { 61 | Assert.notNull(defaultAccessTokenType, "defaultAccessTokenType cannot be null"); 62 | this.defaultAccessTokenType = defaultAccessTokenType; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/dto/mapper/BannerMapper.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.dto.mapper; 2 | 3 | import com.uf.genshinwishes.dto.BannerDTO; 4 | import com.uf.genshinwishes.model.Banner; 5 | import com.uf.genshinwishes.model.BannerType; 6 | import com.uf.genshinwishes.model.Region; 7 | import com.uf.genshinwishes.model.User; 8 | import com.uf.genshinwishes.service.UserService; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.time.Instant; 12 | import java.time.LocalDateTime; 13 | import java.time.ZoneId; 14 | import java.time.ZonedDateTime; 15 | import java.time.temporal.ChronoUnit; 16 | import java.util.Arrays; 17 | import java.util.Map; 18 | import java.util.function.Function; 19 | import java.util.stream.Collectors; 20 | 21 | @Component 22 | public class BannerMapper { 23 | public BannerDTO toDto(Banner banner) { 24 | return this.toDto(null, banner); 25 | } 26 | 27 | public BannerDTO toDto(User user, Banner banner) { 28 | if (banner == null) return null; 29 | 30 | BannerDTO bannerDTO = new BannerDTO(); 31 | 32 | bannerDTO.setId(banner.getId()); 33 | bannerDTO.setVersion(banner.getVersion()); 34 | bannerDTO.setItems(banner.getItems()); 35 | bannerDTO.setGachaType(BannerType.from(banner.getGachaType()).orElse(null)); 36 | bannerDTO.setImage(banner.getImage()); 37 | 38 | if (banner.getStart() != null && banner.getEnd() != null) { 39 | Map startEndByRegion = Arrays.stream(Region.values()) 40 | .collect(Collectors.toMap(Function.identity(), region -> { 41 | LocalDateTime startTime = computeDate(region, banner.getStart(), banner.getIsStartLocale()); 42 | LocalDateTime endTime = computeDate(region, banner.getEnd(), banner.getIsEndLocale()); 43 | 44 | return new LocalDateTime[]{startTime, endTime}; 45 | })); 46 | 47 | bannerDTO.setStartEndByRegion(startEndByRegion); 48 | 49 | if(user != null) { 50 | Region region = Region.getFromUser(user); 51 | 52 | bannerDTO.setStart(startEndByRegion.get(region)[0]); 53 | bannerDTO.setEnd(startEndByRegion.get(region)[1]); 54 | } else { 55 | bannerDTO.setStart(banner.getStart()); 56 | bannerDTO.setEnd(banner.getEnd()); 57 | } 58 | } 59 | 60 | return bannerDTO; 61 | } 62 | 63 | public static LocalDateTime computeDate(Region region, LocalDateTime date, Boolean isLocale) { 64 | return isLocale != null && isLocale ? date : date.minusHours(UserService.getRegionOffset(region)); 65 | } 66 | 67 | public static Instant computeImportArchiveDate() { 68 | return ZonedDateTime.now(ZoneId.of("UTC")).now().minus(6, ChronoUnit.MONTHS).toInstant(); 69 | } 70 | 71 | public static LocalDateTime computeArchiveDate(Region region) { 72 | return BannerMapper.computeDate(region, LocalDateTime.now().minusMonths(6), false); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/service/CSVHelper.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.uf.genshinwishes.model.BannerType; 4 | import com.uf.genshinwishes.model.User; 5 | import com.uf.genshinwishes.model.Wish; 6 | import org.apache.commons.csv.CSVFormat; 7 | import org.apache.commons.csv.CSVPrinter; 8 | import org.apache.commons.csv.QuoteMode; 9 | import org.springframework.context.MessageSource; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Locale; 19 | 20 | public class CSVHelper { 21 | 22 | public static ByteArrayInputStream wishesToCsv(MessageSource messageSource, User user, List wishes) { 23 | final CSVFormat format = CSVFormat.DEFAULT 24 | .withQuoteMode(QuoteMode.MINIMAL) 25 | .withDelimiter(';') 26 | .withHeader( 27 | messageSource.getMessage("banner", null, Locale.ENGLISH), 28 | messageSource.getMessage("index", null, Locale.ENGLISH), 29 | messageSource.getMessage("item", null, Locale.ENGLISH), 30 | messageSource.getMessage("itemType", null, Locale.ENGLISH), 31 | messageSource.getMessage("itemRarity", null, Locale.ENGLISH), 32 | messageSource.getMessage("date", null, Locale.ENGLISH) 33 | ); 34 | 35 | try ( 36 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 37 | final PrintWriter printWriter = new PrintWriter(out) 38 | ) { 39 | printWriter.write('\ufeff'); 40 | 41 | try (CSVPrinter csvPrinter = new CSVPrinter(printWriter, format)) { 42 | for (Wish wish : wishes) { 43 | 44 | List data = Arrays.asList( 45 | messageSource.getMessage( 46 | BannerType.from(wish.getGachaType()).map(BannerType::name).orElse("UNKNOWN"), 47 | null, Locale.ENGLISH), 48 | String.valueOf(wish.getIndex()), 49 | wish.getItem().getName(), 50 | messageSource.getMessage(wish.getItem().getItemType(), null, Locale.ENGLISH), 51 | String.valueOf(wish.getItem().getRankType()), 52 | wish.getTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replace('T', ' ') 53 | ); 54 | 55 | csvPrinter.printRecord(data); 56 | } 57 | 58 | csvPrinter.flush(); 59 | return new ByteArrayInputStream(out.toByteArray()); 60 | } catch (IOException e) { 61 | throw new RuntimeException("failed to import data to CSV file: " + e.getMessage()); 62 | } 63 | } catch (IOException e) { 64 | throw new RuntimeException("failed to import data to CSV file: " + e.getMessage()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/UserPrincipal.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import com.uf.genshinwishes.model.User; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.oauth2.core.oidc.OidcIdToken; 7 | import org.springframework.security.oauth2.core.oidc.OidcUserInfo; 8 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; 9 | import org.springframework.security.oauth2.core.user.OAuth2User; 10 | 11 | import java.io.Serial; 12 | import java.util.Collection; 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | public class UserPrincipal implements OidcUser, OAuth2User, UserDetails { 18 | @Serial 19 | private static final long serialVersionUID = -651392695708225826L; 20 | 21 | private final Long id; 22 | private final String email; 23 | private final String password; 24 | private final Collection authorities; 25 | private Map attributes; 26 | 27 | public UserPrincipal(Long id, String email, String password, Collection authorities) { 28 | this.id = id; 29 | this.email = email; 30 | this.password = password; 31 | this.authorities = authorities; 32 | } 33 | 34 | public static UserPrincipal create(User user) { 35 | List authorities = Collections.emptyList(); 36 | 37 | return new UserPrincipal( 38 | user.getId(), 39 | user.getEmail(), 40 | user.getKey(), 41 | authorities 42 | ); 43 | } 44 | 45 | public Long getId() { 46 | return id; 47 | } 48 | 49 | @Override 50 | public String getPassword() { 51 | return password; 52 | } 53 | 54 | @Override 55 | public String getUsername() { 56 | return email; 57 | } 58 | 59 | @Override 60 | public boolean isAccountNonExpired() { 61 | return true; 62 | } 63 | 64 | @Override 65 | public boolean isAccountNonLocked() { 66 | return true; 67 | } 68 | 69 | @Override 70 | public boolean isCredentialsNonExpired() { 71 | return true; 72 | } 73 | 74 | @Override 75 | public boolean isEnabled() { 76 | return true; 77 | } 78 | 79 | @Override 80 | public Collection getAuthorities() { 81 | return authorities; 82 | } 83 | 84 | @Override 85 | public Map getAttributes() { 86 | return attributes; 87 | } 88 | 89 | @Override 90 | public String getName() { 91 | return String.valueOf(id); 92 | } 93 | 94 | @Override 95 | public Map getClaims() { 96 | return null; 97 | } 98 | 99 | @Override 100 | public OidcUserInfo getUserInfo() { 101 | return null; 102 | } 103 | 104 | @Override 105 | public OidcIdToken getIdToken() { 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/config/CachingConfig.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.config; 2 | 3 | import com.uf.genshinwishes.dto.BannerDTO; 4 | import com.uf.genshinwishes.model.BannerType; 5 | import com.uf.genshinwishes.service.BannerService; 6 | import com.uf.genshinwishes.service.PublicStatsService; 7 | import com.uf.genshinwishes.service.UserService; 8 | import com.uf.genshinwishes.service.WishService; 9 | import lombok.Getter; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.cache.CacheManager; 14 | import org.springframework.cache.annotation.EnableCaching; 15 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Configuration; 18 | import org.springframework.scheduling.annotation.EnableScheduling; 19 | import org.springframework.scheduling.annotation.Scheduled; 20 | 21 | import java.time.Instant; 22 | import java.time.LocalDateTime; 23 | import java.time.ZoneId; 24 | import java.time.temporal.ChronoUnit; 25 | import java.util.List; 26 | 27 | @EnableCaching 28 | @EnableScheduling 29 | @Configuration 30 | public class CachingConfig { 31 | Logger logger = LoggerFactory.getLogger(CachingConfig.class); 32 | 33 | @Autowired 34 | private PublicStatsService publicStatsService; 35 | @Autowired 36 | private BannerService bannerService; 37 | @Autowired 38 | private WishService wishService; 39 | @Autowired 40 | private UserService userService; 41 | 42 | @Getter 43 | private boolean loaded = false; 44 | 45 | @Bean 46 | public CacheManager cacheManager() { 47 | return new ConcurrentMapCacheManager("publicStats", "usersCount", "wishesCount"); 48 | } 49 | 50 | @Scheduled(fixedDelay = 1800000, initialDelay = 0) 51 | public void publicStatsUpdate() { 52 | Instant oldWishes = Instant.now().minus(200, ChronoUnit.DAYS); 53 | 54 | publicStatsService.updateStatsFor(BannerType.ALL, null); 55 | logger.info("Updated ALL"); 56 | publicStatsService.updateStatsFor(BannerType.CHARACTER_EVENT, null); 57 | logger.info("Updated CHARACTER"); 58 | publicStatsService.updateStatsFor(BannerType.WEAPON_EVENT, null); 59 | logger.info("Updated WEAPON"); 60 | 61 | List banners = bannerService.findAllOrderByStartDateDesc(); 62 | 63 | banners.forEach(b -> { 64 | if(loaded && b.getEnd() != null && b.getEnd().isBefore(LocalDateTime.ofInstant(oldWishes, ZoneId.of("UTC")))) { 65 | return; 66 | } 67 | 68 | publicStatsService.updateStatsFor( 69 | b.getGachaType(), 70 | b.getGachaType() == BannerType.CHARACTER_EVENT || b.getGachaType() == BannerType.WEAPON_EVENT 71 | ? b.getId() 72 | : null); 73 | 74 | logger.info("Updated " + b.getGachaType() + " / " + b.getVersion()); 75 | }); 76 | 77 | loaded = true; 78 | } 79 | 80 | @Scheduled(fixedDelay = 300000, initialDelay = 0) 81 | public void countersUpdate() { 82 | this.wishService.updateWishesCount(); 83 | logger.info("Updated wishes"); 84 | this.userService.updateUsersCount(); 85 | logger.info("Updated users"); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/ProfileController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.BannerDTO; 4 | import com.uf.genshinwishes.dto.StatsDTO; 5 | import com.uf.genshinwishes.dto.WishFilterDTO; 6 | import com.uf.genshinwishes.exception.ApiError; 7 | import com.uf.genshinwishes.exception.ErrorType; 8 | import com.uf.genshinwishes.model.BannerType; 9 | import com.uf.genshinwishes.model.User; 10 | import com.uf.genshinwishes.service.BannerService; 11 | import com.uf.genshinwishes.service.StatsService; 12 | import com.uf.genshinwishes.service.UserService; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.Optional; 19 | import java.util.stream.Collectors; 20 | import java.util.stream.Stream; 21 | 22 | @RestController 23 | @RequestMapping("/profile/{profileId}") 24 | public class ProfileController { 25 | @Autowired 26 | private UserService userService; 27 | @Autowired 28 | private BannerService bannerService; 29 | @Autowired 30 | private StatsService statsService; 31 | 32 | @GetMapping("/") 33 | public String getUsername(@PathVariable("profileId") String profileId) { 34 | User user = assertUser(profileId); 35 | 36 | return user.getMihoyoUsername(); 37 | } 38 | 39 | @GetMapping("/banners") 40 | public Iterable getBanners(@PathVariable("profileId") String profileId) { 41 | User user = assertUser(profileId); 42 | 43 | return bannerService.findAllForUser(user); 44 | } 45 | 46 | @GetMapping("/banners/character") 47 | public Iterable getCharacterEvents(@PathVariable("profileId") String profileId) { 48 | User user = assertUser(profileId); 49 | 50 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(user, BannerType.CHARACTER_EVENT.getType()); 51 | } 52 | 53 | @GetMapping("/banners/weapon") 54 | public Iterable getWeaponEvents(@PathVariable("profileId") String profileId) { 55 | User user = assertUser(profileId); 56 | 57 | return bannerService.findAllByGachaTypeOrderByStartDateDesc(user, BannerType.WEAPON_EVENT.getType()); 58 | } 59 | 60 | @GetMapping("/stats/{bannerType}") 61 | public StatsDTO getStats(@PathVariable("profileId") String profileId, 62 | @PathVariable("bannerType") Optional bannerType, 63 | @RequestParam Optional> characterEvents, 64 | @RequestParam Optional> weaponEvents) { 65 | User user = assertUser(profileId); 66 | 67 | WishFilterDTO filters = WishFilterDTO.builder().events( 68 | Stream.concat( 69 | characterEvents.orElse(Collections.emptyList()).stream(), 70 | weaponEvents.orElse(Collections.emptyList()).stream() 71 | ).collect(Collectors.toList()) 72 | ).build(); 73 | 74 | return statsService.getStatsFor(user, bannerType.orElse(BannerType.ALL), filters); 75 | } 76 | 77 | private User assertUser(@PathVariable("profileId") String profileId) { 78 | User user = this.userService.findUserByProfileId(profileId); 79 | 80 | if (user == null || user.getSharing() == null || !user.getSharing()) { 81 | throw new ApiError(ErrorType.PROFILE_NOT_FOUND); 82 | } 83 | 84 | return user; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | client-url: ${CLIENT_URL} 3 | read-timeout: 120000 4 | connect-timeout: 120000 5 | oauth2: 6 | redirectUri: "${app.client-url}/login/oauth2/callback/{registrationId}" 7 | mihoyo: 8 | region-settings: 9 | hk4e_cn: 10 | im-endpoint: "https://api-takumi.mihoyo.com" 11 | wish-endpoint: "https://hk4e-api.mihoyo.com" 12 | hk4e_global: 13 | im-endpoint: "https://api-os-takumi.mihoyo.com" 14 | wish-endpoint: "https://hk4e-api-os.mihoyo.com" 15 | 16 | 17 | 18 | management: 19 | endpoints: 20 | web: 21 | exposure: 22 | include: info,health,prometheus 23 | 24 | spring: 25 | redis: 26 | host: genshin-wishes-redis 27 | port: 6379 28 | session: 29 | store-type: redis 30 | jpa: 31 | properties: 32 | hibernate: 33 | enable_lazy_load_no_trans: true 34 | datasource: 35 | hikari: 36 | maxLifetime: 60000 37 | url: ${JDBC_DATABASE_URL} 38 | username: ${JDBC_DATABASE_USERNAME} 39 | password: ${JDBC_DATABASE_PASSWORD} 40 | security: 41 | oauth2: 42 | client: 43 | registration: 44 | facebook: 45 | client-id: 3643805775683535 46 | client-secret: ${FACEBOOK_SECRET} 47 | redirect-uri: ${app.oauth2.redirectUri} 48 | scope: email 49 | google: 50 | client-id: 431139673562-0676kbn3tpab8j6dml7nbrhvhj6e0432.apps.googleusercontent.com 51 | client-secret: ${GOOGLE_SECRET} 52 | redirect-uri: ${app.oauth2.redirectUri} 53 | baidu: 54 | client-id: xeuziiCgxY0QzcDe6BqCIwhb 55 | client-secret: ${BAIDU_SECRET} 56 | redirect-uri: ${app.oauth2.redirectUri} 57 | authorizationGrantType: authorization_code 58 | clientAuthenticationMethod: post 59 | scope: basic 60 | provider: baidu 61 | discord: 62 | client-id: 777998715255259167 63 | client-secret: ${DISCORD_SECRET} 64 | redirect-uri: ${app.oauth2.redirectUri} 65 | authorizationGrantType: authorization_code 66 | scope: 67 | - identify 68 | - email 69 | provider: Discord 70 | provider: 71 | facebook: 72 | authorizationUri: https://www.facebook.com/v2.8/dialog/oauth?auth_type=rerequest 73 | baidu: 74 | authorizationUri: https://openapi.baidu.com/oauth/2.0/authorize 75 | tokenUri: https://openapi.baidu.com/oauth/2.0/token 76 | userInfoUri: https://openapi.baidu.com/rest/2.0/passport/users/getInfo 77 | usernameAttribute: openid 78 | userInfoAuthenticationMethod: form 79 | Discord: 80 | authorizationUri: https://discord.com/api/v8/oauth2/authorize 81 | tokenUri: https://discord.com/api/v8/oauth2/token 82 | userInfoUri: https://discord.com/api/v8/users/@me 83 | usernameAttribute: username 84 | 85 | logging: 86 | level: 87 | root: ERROR 88 | org.springframework.web: ERROR 89 | com.uf: INFO 90 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/service/StatsService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.uf.genshinwishes.dto.*; 4 | import com.uf.genshinwishes.dto.mapper.WishMapper; 5 | import com.uf.genshinwishes.model.BannerType; 6 | import com.uf.genshinwishes.model.User; 7 | import com.uf.genshinwishes.model.Wish; 8 | import com.uf.genshinwishes.repository.wish.WishRepository; 9 | import com.uf.genshinwishes.repository.wish.WishSpecification; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.persistence.EntityManager; 16 | import javax.persistence.criteria.CriteriaBuilder; 17 | import javax.persistence.criteria.CriteriaQuery; 18 | import javax.persistence.criteria.Expression; 19 | import javax.persistence.criteria.Root; 20 | import java.time.LocalDate; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.Date; 24 | import java.util.List; 25 | import java.util.stream.Collectors; 26 | 27 | @Service 28 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 29 | public class StatsService { 30 | 31 | private EntityManager em; 32 | private BannerService bannerService; 33 | private WishRepository wishRepository; 34 | private WishMapper wishMapper; 35 | 36 | public StatsDTO getStatsFor(User user, BannerType bannerType, WishFilterDTO filters) { 37 | List banners = bannerService.findAll(); 38 | filters.setRanks(Arrays.asList(4, 5)); 39 | WishSpecification specification = WishSpecification.builder().user(user).bannerType(bannerType).banners(banners).filters(filters).build(); 40 | 41 | StatsDTO stats = new StatsDTO(); 42 | 43 | stats.setBannerType(bannerType); 44 | 45 | WishSpecification allRanksSpecifications = specification.toBuilder().filters(specification.getFilters().toBuilder().ranks(null).build()).build(); 46 | stats.setCount(wishRepository.count(allRanksSpecifications)); 47 | 48 | WishSpecification fourStarsSpecifications = specification.toBuilder() 49 | .filters(specification.getFilters().toBuilder().ranks(Collections.singletonList(4)).build()) 50 | .build(); 51 | stats.setCount4Stars(wishRepository.count(fourStarsSpecifications)); 52 | 53 | WishSpecification fiveStarsSpecifications = specification.toBuilder() 54 | .filters(specification.getFilters().toBuilder().ranks(Collections.singletonList(5)).build()) 55 | .build(); 56 | stats.setCount5Stars(wishRepository.count(fiveStarsSpecifications)); 57 | 58 | 59 | List wishes = wishRepository.findAll(specification.toBuilder().fetchBanner(true).build(), Sort.by(Sort.Order.asc("gachaType"), Sort.Order.asc("index"))); 60 | List wishDTOs = wishes.stream() 61 | .map(wishMapper::toDto).collect(Collectors.toList()); 62 | 63 | stats.setWishes(wishDTOs); 64 | 65 | stats.setCountPerDay(getCountPerDay(allRanksSpecifications, bannerType == BannerType.ALL)); 66 | 67 | return stats; 68 | } 69 | 70 | public List getCountPerDay(WishSpecification specification, boolean perBanner) { 71 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 72 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerRankAndDay.class); 73 | Root root = query.from(Wish.class); 74 | 75 | Expression dateTrunc = criteriaBuilder.function("DATE_TRUNC", Date.class, criteriaBuilder.literal("WEEK"), root.get("time")).as(LocalDate.class); 76 | 77 | query.where(specification.toPredicate(root, query, criteriaBuilder)); 78 | 79 | query.groupBy(dateTrunc, root.get("item").get("rankType")); 80 | 81 | return em.createQuery(query.multiselect(dateTrunc, root.get("item").get("rankType"), criteriaBuilder.count(root))).getResultList(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.uf.genshinwishes.exception.ApiError; 4 | import com.uf.genshinwishes.exception.ErrorType; 5 | import com.uf.genshinwishes.model.Region; 6 | import com.uf.genshinwishes.model.User; 7 | import com.uf.genshinwishes.repository.UserRepository; 8 | import lombok.AllArgsConstructor; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.cache.annotation.CachePut; 13 | import org.springframework.cache.annotation.Cacheable; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.math.BigInteger; 17 | import java.time.Instant; 18 | import java.util.Date; 19 | import java.util.Locale; 20 | import java.util.UUID; 21 | 22 | @Service 23 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 24 | public class UserService { 25 | private static final Logger logger = LoggerFactory.getLogger(UserService.class); 26 | 27 | private UserRepository userRepository; 28 | private WishService wishService; 29 | 30 | public User findByEmail(String email) { 31 | return this.userRepository.findByEmail(email); 32 | } 33 | 34 | public User insertUser(String email) { 35 | User user = new User(); 36 | user.setEmail(email); 37 | user.setCreationDate(new Date()); 38 | user.setLastLoggingDate(new Date()); 39 | user.setKey(UUID.randomUUID().toString()); 40 | 41 | userRepository.save(user); 42 | 43 | return user; 44 | } 45 | 46 | public User retrieveOrInsertUser(String email) { 47 | User user = this.findByEmail(email); 48 | 49 | if (user == null) { 50 | user = this.insertUser(email); 51 | } else if (user.getKey() == null) { 52 | this.createKey(user); 53 | } else { 54 | this.updateLastLoggingDate(user); 55 | } 56 | 57 | return user; 58 | } 59 | 60 | public void createKey(User user) { 61 | user.setLastLoggingDate(new Date()); 62 | user.setKey(UUID.randomUUID().toString()); 63 | 64 | userRepository.save(user); 65 | } 66 | 67 | public void deleteUser(User user) { 68 | userRepository.delete(user); 69 | } 70 | 71 | public void updateLang(User user, String lang) { 72 | if (lang == null || "".equals(Locale.forLanguageTag(lang).toString())) 73 | throw new ApiError(ErrorType.INVALID_LANG); 74 | 75 | user.setLang(lang); 76 | 77 | userRepository.save(user); 78 | } 79 | 80 | public void updateWholeClock(User user, Boolean wholeClock) { 81 | user.setWholeClock(wholeClock); 82 | 83 | userRepository.save(user); 84 | } 85 | 86 | public void updateLastLoggingDate(User user) { 87 | user.setLastLoggingDate(new Date()); 88 | 89 | userRepository.save(user); 90 | } 91 | 92 | public String initProfileId(User user) { 93 | String profileId = BigInteger.valueOf(Long.parseLong(Instant.now().toEpochMilli() + "" + user.getId())).toString(36); 94 | user.setProfileId(profileId); 95 | 96 | userRepository.save(user); 97 | 98 | return profileId; 99 | } 100 | 101 | public User findUserByProfileId(String profileId) { 102 | return userRepository.findByProfileId(profileId); 103 | } 104 | 105 | @Cacheable("usersCount") 106 | public Long getUsersCount() { 107 | return null; 108 | } 109 | 110 | @CachePut("usersCount") 111 | public Long updateUsersCount() { 112 | return userRepository.countByMihoyoUsernameIsNotNull(); 113 | } 114 | 115 | public void share(User user, boolean share) { 116 | user.setSharing(share); 117 | 118 | userRepository.save(user); 119 | } 120 | 121 | public Long count() { 122 | return this.userRepository.count(); 123 | } 124 | 125 | public static int getRegionOffset(Region region) { 126 | switch (region) { 127 | case AMERICA: 128 | return 5; 129 | case EUROPE: 130 | return -1; 131 | default: 132 | case ASIA: 133 | return -8; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/service/WishService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Multimap; 5 | import com.google.common.collect.MultimapBuilder; 6 | import com.uf.genshinwishes.dto.BannerDTO; 7 | import com.uf.genshinwishes.dto.WishDTO; 8 | import com.uf.genshinwishes.dto.WishFilterDTO; 9 | import com.uf.genshinwishes.dto.mapper.BannerMapper; 10 | import com.uf.genshinwishes.dto.mapper.WishMapper; 11 | import com.uf.genshinwishes.dto.mihoyo.MihoyoWishLogDTO; 12 | import com.uf.genshinwishes.exception.ApiError; 13 | import com.uf.genshinwishes.exception.ErrorType; 14 | import com.uf.genshinwishes.model.*; 15 | import com.uf.genshinwishes.repository.ItemRepository; 16 | import com.uf.genshinwishes.repository.wish.WishRepository; 17 | import com.uf.genshinwishes.repository.wish.WishSpecification; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.cache.annotation.CachePut; 22 | import org.springframework.cache.annotation.Cacheable; 23 | import org.springframework.data.domain.PageRequest; 24 | import org.springframework.data.domain.Sort; 25 | import org.springframework.stereotype.Service; 26 | import org.springframework.transaction.annotation.Transactional; 27 | 28 | import java.time.Instant; 29 | import java.time.LocalDateTime; 30 | import java.util.*; 31 | import java.util.concurrent.CompletableFuture; 32 | import java.util.concurrent.CompletionException; 33 | import java.util.concurrent.ExecutorService; 34 | import java.util.concurrent.Executors; 35 | import java.util.concurrent.atomic.AtomicReference; 36 | import java.util.stream.Collectors; 37 | 38 | @Service 39 | public class WishService { 40 | Logger logger = LoggerFactory.getLogger(WishService.class); 41 | 42 | @Autowired 43 | private WishRepository wishRepository; 44 | @Autowired 45 | private BannerService bannerService; 46 | @Autowired 47 | private WishMapper wishMapper; 48 | 49 | public Map> getBanners(User user) { 50 | Multimap wishesByBanner = MultimapBuilder.hashKeys(BannerType.values().length).arrayListValues().build(); 51 | 52 | Arrays.stream(BannerType.values()) 53 | .forEach(type -> { 54 | List wishes = wishRepository.findFirst100ByUserAndGachaTypeOrderByIdDesc(user, type.getType()); 55 | 56 | wishesByBanner.putAll(type, wishes.stream().map(wishMapper::toDto).collect(Collectors.toList())); 57 | }); 58 | 59 | return wishesByBanner.asMap(); 60 | } 61 | 62 | @Transactional 63 | public void deleteAllUserWishes(User user) { 64 | wishRepository.deleteByUser(user); 65 | } 66 | 67 | public List findByUserAndBannerType(User user, BannerType bannerType, Integer page, WishFilterDTO filters) { 68 | List banners = bannerService.findAllForUser(user); 69 | List wishes = this.wishRepository.findAll( 70 | WishSpecification.builder().user(user).bannerType(bannerType).banners(banners).filters(filters).build(), 71 | PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "time", "id")) 72 | ).getContent(); 73 | 74 | return wishes.stream().map(wishMapper::toDto).collect(Collectors.toList()); 75 | } 76 | 77 | public Long countAllByUserAndGachaType(User user, BannerType bannerType, WishFilterDTO filters) { 78 | List banners = bannerService.findAllForUser(user); 79 | return this.wishRepository.count(WishSpecification.builder().user(user).bannerType(bannerType).banners(banners).filters(filters).build()); 80 | } 81 | 82 | public Map countAllByUser(User user) { 83 | return Arrays.stream(BannerType.values()) 84 | .collect(Collectors.toMap( 85 | (banner) -> banner, 86 | (banner) -> this.wishRepository.countByUserAndGachaType(user, banner.getType()) 87 | )); 88 | } 89 | 90 | public List findByUser(User user) { 91 | return this.wishRepository.findByUserOrderByGachaTypeAscIndexAsc(user); 92 | } 93 | 94 | @Cacheable("wishesCount") 95 | public Long getWishesCount() { 96 | return null; 97 | } 98 | 99 | @CachePut("wishesCount") 100 | public Long updateWishesCount() { 101 | return wishRepository.count(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/controller/WishController.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.controller; 2 | 3 | import com.uf.genshinwishes.dto.ItemType; 4 | import com.uf.genshinwishes.dto.WishDTO; 5 | import com.uf.genshinwishes.dto.WishFilterDTO; 6 | import com.uf.genshinwishes.model.BannerType; 7 | import com.uf.genshinwishes.model.User; 8 | import com.uf.genshinwishes.model.Wish; 9 | import com.uf.genshinwishes.service.CSVHelper; 10 | import com.uf.genshinwishes.service.WishService; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.context.MessageSource; 15 | import org.springframework.core.io.InputStreamResource; 16 | import org.springframework.http.HttpHeaders; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.web.bind.annotation.*; 20 | 21 | import java.util.*; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.Stream; 24 | 25 | @RestController 26 | @RequestMapping("/wishes") 27 | public class WishController { 28 | private final Logger logger = LoggerFactory.getLogger(WishController.class); 29 | 30 | @Autowired 31 | private WishService wishService; 32 | @Autowired 33 | private MessageSource messageSource; 34 | 35 | @GetMapping("/{bannerType}") 36 | public List getWishes(User user, 37 | @PathVariable("bannerType") BannerType bannerType, 38 | @RequestParam("page") Integer page, 39 | @RequestParam Optional> items, 40 | @RequestParam Optional fr, 41 | @RequestParam Optional> ranks, 42 | @RequestParam Optional itemType, 43 | @RequestParam Optional> characterEvents, 44 | @RequestParam Optional> weaponEvents) { 45 | return wishService.findByUserAndBannerType(user, bannerType, page, new WishFilterDTO( 46 | items.orElse(null), 47 | fr.orElse(null), 48 | ranks.orElse(null), 49 | itemType.orElse(null), 50 | Stream.concat( 51 | characterEvents.orElse(Collections.emptyList()).stream(), 52 | weaponEvents.orElse(Collections.emptyList()).stream() 53 | ).collect(Collectors.toList()) 54 | )); 55 | } 56 | 57 | @GetMapping("/{bannerType}/count") 58 | public Long countWishesByBanner(User user, @PathVariable("bannerType") BannerType bannerType, 59 | @RequestParam Optional> items, 60 | @RequestParam Optional fr, 61 | @RequestParam Optional> ranks, 62 | @RequestParam Optional itemType, 63 | @RequestParam Optional> characterEvents, 64 | @RequestParam Optional> weaponEvents 65 | ) { 66 | WishFilterDTO filters = new WishFilterDTO( 67 | items.orElse(null), 68 | fr.orElse(null), 69 | ranks.orElse(null), 70 | itemType.orElse(null), 71 | Stream.concat( 72 | characterEvents.orElse(Collections.emptyList()).stream(), 73 | weaponEvents.orElse(Collections.emptyList()).stream() 74 | ).collect(Collectors.toList()) 75 | ); 76 | 77 | return wishService.countAllByUserAndGachaType(user, bannerType, filters); 78 | } 79 | 80 | @GetMapping("/count") 81 | public Map countWishes(User user) { 82 | return wishService.countAllByUser(user); 83 | } 84 | 85 | @GetMapping("/export") 86 | public ResponseEntity exportWishes(User user) { 87 | String filename = user.getMihoyoUid() + "_wishes.csv"; 88 | 89 | List userWishes = wishService.findByUser(user); 90 | 91 | InputStreamResource file = new InputStreamResource(CSVHelper.wishesToCsv(messageSource, user, userWishes)); 92 | 93 | return ResponseEntity.ok() 94 | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename) 95 | .contentType(MediaType.parseMediaType("application/csv")) 96 | .body(file); 97 | } 98 | 99 | @GetMapping("/banners") 100 | public Map> getBanners(User user) { 101 | return wishService.getBanners(user); 102 | } 103 | 104 | @DeleteMapping("/deleteAll") 105 | public void deleteAll(User user) { 106 | wishService.deleteAllUserWishes(user); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/repository/wish/WishSpecification.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.repository.wish; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.uf.genshinwishes.dto.BannerDTO; 5 | import com.uf.genshinwishes.dto.WishFilterDTO; 6 | import com.uf.genshinwishes.exception.ApiError; 7 | import com.uf.genshinwishes.exception.ErrorType; 8 | import com.uf.genshinwishes.model.*; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.NoArgsConstructor; 12 | import org.springframework.data.jpa.domain.Specification; 13 | 14 | import javax.persistence.criteria.*; 15 | import java.time.LocalDateTime; 16 | import java.util.Arrays; 17 | import java.util.Date; 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | 21 | @Builder(toBuilder = true) 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public class WishSpecification implements Specification { 25 | 26 | private User user; 27 | private BannerType bannerType; 28 | private List banners; 29 | private WishFilterDTO filters; 30 | private Boolean fetchBanner; 31 | private Boolean ignoreFirstPity; 32 | 33 | public WishFilterDTO getFilters() { 34 | return filters; 35 | } 36 | 37 | @Override 38 | public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder builder) { 39 | if (bannerType == null || banners == null) throw new ApiError(ErrorType.INVALID_FILTERS); 40 | 41 | List predicates = Lists.newArrayList(); 42 | 43 | // Mandatory 44 | if (!BannerType.ALL.equals(bannerType)) 45 | predicates.add(builder.equal(root.get("gachaType"), bannerType.getType())); 46 | 47 | // Optional 48 | if (user != null) { 49 | predicates.add(builder.equal(root.get("user"), user)); 50 | } 51 | 52 | if (filters != null) { 53 | predicates.add(getItemPredicate(root)); 54 | predicates.add(getRankPredicate(root)); 55 | predicates.add(getItemTypePredicate(root, builder)); 56 | predicates.add(getDatePredicate(root, builder)); 57 | } 58 | 59 | if (ignoreFirstPity != null && ignoreFirstPity) { 60 | predicates.add(getIgnoreFirstPityPredicate(root, builder)); 61 | } 62 | 63 | return builder.and(predicates.stream().filter(predicate -> predicate != null).toArray(Predicate[]::new)); 64 | } 65 | 66 | private Predicate getItemPredicate(Root root) { 67 | if (filters.getItems() != null) { 68 | return root.get("item").get("itemId").in( 69 | filters.getItems() 70 | ); 71 | } 72 | return null; 73 | } 74 | 75 | private Predicate getRankPredicate(Root root) { 76 | if (filters.getRanks() != null) { 77 | return root.get("item").get("rankType").in( 78 | filters.getRanks() 79 | ); 80 | } 81 | return null; 82 | } 83 | 84 | private Predicate getItemTypePredicate(Root root, CriteriaBuilder builder) { 85 | if (filters.getItemType() != null) { 86 | return builder.equal( 87 | root.get("item").get("itemType"), 88 | filters.getItemType().name() 89 | ); 90 | } 91 | return null; 92 | } 93 | 94 | private Predicate getDatePredicate(Root root, CriteriaBuilder builder) { 95 | if (filters.getEvents() != null && !filters.getEvents().isEmpty()) { 96 | List orTime = filters.getEvents().stream().map(event -> { 97 | BannerDTO banner = getBanner(event); 98 | 99 | List timePredicates = Arrays.stream(Region.values()).map(region -> { 100 | LocalDateTime start = banner.getStartEndByRegion().get(region)[0]; 101 | LocalDateTime end = banner.getStartEndByRegion().get(region)[1]; 102 | 103 | return builder.and(builder.equal(root.get("user").get("region"), region.getPrefix()), builder.between( 104 | builder.function("DATE_TRUNC", Date.class, builder.literal("MINUTE"), root.get("time")).as(LocalDateTime.class), 105 | builder.literal(start), 106 | builder.literal(end))); 107 | }).collect(Collectors.toList()); 108 | 109 | return builder.or(timePredicates.toArray(new Predicate[]{})); 110 | }).collect(Collectors.toList()); 111 | 112 | return builder.or(orTime.toArray(new Predicate[]{})); 113 | } 114 | 115 | return null; 116 | } 117 | 118 | private Predicate getIgnoreFirstPityPredicate(Root root, CriteriaBuilder builder) { 119 | return builder.or( 120 | root.get("pity").isNull(), 121 | builder.not(builder.equal(root.get("index"), root.get("pity"))), 122 | builder.equal(root.get("gachaType"), BannerType.NOVICE.getType()) 123 | ); 124 | } 125 | 126 | private BannerDTO getBanner(Long event) { 127 | return banners.stream().filter(b -> b.getId().equals(event)).findFirst().orElseThrow(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present 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 | 17 | import java.net.*; 18 | import java.io.*; 19 | import java.nio.channels.*; 20 | import java.util.Properties; 21 | 22 | public class MavenWrapperDownloader { 23 | 24 | private static final String WRAPPER_VERSION = "0.5.6"; 25 | /** 26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 27 | */ 28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 30 | 31 | /** 32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 33 | * use instead of the default one. 34 | */ 35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 36 | ".mvn/wrapper/maven-wrapper.properties"; 37 | 38 | /** 39 | * Path where the maven-wrapper.jar will be saved to. 40 | */ 41 | private static final String MAVEN_WRAPPER_JAR_PATH = 42 | ".mvn/wrapper/maven-wrapper.jar"; 43 | 44 | /** 45 | * Name of the property which should be used to override the default download url for the wrapper. 46 | */ 47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 48 | 49 | public static void main(String args[]) { 50 | System.out.println("- Downloader started"); 51 | File baseDirectory = new File(args[0]); 52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 53 | 54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 55 | // wrapperUrl parameter. 56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 57 | String url = DEFAULT_DOWNLOAD_URL; 58 | if (mavenWrapperPropertyFile.exists()) { 59 | FileInputStream mavenWrapperPropertyFileInputStream = null; 60 | try { 61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 62 | Properties mavenWrapperProperties = new Properties(); 63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 65 | } catch (IOException e) { 66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 67 | } finally { 68 | try { 69 | if (mavenWrapperPropertyFileInputStream != null) { 70 | mavenWrapperPropertyFileInputStream.close(); 71 | } 72 | } catch (IOException e) { 73 | // Ignore ... 74 | } 75 | } 76 | } 77 | System.out.println("- Downloading from: " + url); 78 | 79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 80 | if (!outputFile.getParentFile().exists()) { 81 | if (!outputFile.getParentFile().mkdirs()) { 82 | System.out.println( 83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 84 | } 85 | } 86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 87 | try { 88 | downloadFileFromURL(url, outputFile); 89 | System.out.println("Done"); 90 | System.exit(0); 91 | } catch (Throwable e) { 92 | System.out.println("- Error downloading"); 93 | e.printStackTrace(); 94 | System.exit(1); 95 | } 96 | } 97 | 98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 100 | String username = System.getenv("MVNW_USERNAME"); 101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 102 | Authenticator.setDefault(new Authenticator() { 103 | @Override 104 | protected PasswordAuthentication getPasswordAuthentication() { 105 | return new PasswordAuthentication(username, password); 106 | } 107 | }); 108 | } 109 | URL website = new URL(urlString); 110 | ReadableByteChannel rbc; 111 | rbc = Channels.newChannel(website.openStream()); 112 | FileOutputStream fos = new FileOutputStream(destination); 113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 114 | fos.close(); 115 | rbc.close(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.4.4 9 | 10 | 11 | com.uf 12 | genshin-wishes 13 | 1.16.0 14 | jar 15 | genshin-wishes 16 | 17 | 18 | 19 | 15 20 | 3.6.28 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.apache.commons 27 | commons-csv 28 | ${apachecsv.version} 29 | 30 | 31 | org.mockito 32 | mockito-core 33 | ${mockito.version} 34 | 35 | 36 | com.google.guava 37 | guava 38 | 30.0-jre 39 | 40 | 41 | org.modelmapper 42 | modelmapper 43 | 2.3.8 44 | 45 | 46 | org.projectlombok 47 | lombok 48 | true 49 | 50 | 51 | io.springfox 52 | springfox-boot-starter 53 | 3.0.0 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-data-jpa 58 | 59 | 60 | org.springframework 61 | spring-context 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-cache 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-oauth2-client 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-security 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-starter-actuator 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-web 82 | 83 | 84 | org.springframework.session 85 | spring-session-data-redis 86 | 87 | 88 | io.lettuce 89 | lettuce-core 90 | 91 | 92 | io.micrometer 93 | micrometer-registry-prometheus 94 | 95 | 96 | org.postgresql 97 | postgresql 98 | 42.2.18 99 | runtime 100 | 101 | 102 | org.springframework.boot 103 | spring-boot-starter-tomcat 104 | provided 105 | 106 | 107 | org.springframework.boot 108 | spring-boot-starter-test 109 | test 110 | 111 | 112 | org.springframework.security 113 | spring-security-test 114 | test 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.springframework.session 122 | spring-session-bom 123 | 2020.0.3 124 | pom 125 | import 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.springframework.boot 134 | spring-boot-maven-plugin 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/main/java/com/uf/genshinwishes/security/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.security; 2 | 3 | import lombok.NonNull; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.converter.FormHttpMessageConverter; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; 15 | import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; 16 | import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; 17 | import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; 18 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 19 | import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; 20 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 21 | import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; 22 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 23 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 24 | import org.springframework.web.bind.annotation.RequestMethod; 25 | import org.springframework.web.client.RestTemplate; 26 | import org.springframework.web.cors.CorsConfiguration; 27 | import org.springframework.web.cors.CorsConfigurationSource; 28 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 29 | 30 | import javax.servlet.http.HttpServletResponse; 31 | import java.util.Arrays; 32 | import java.util.LinkedHashMap; 33 | import java.util.Map; 34 | import java.util.stream.Collectors; 35 | 36 | @Configuration 37 | @EnableWebSecurity 38 | @RequiredArgsConstructor(onConstructor = @__(@Autowired)) 39 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 40 | 41 | @Value("${app.client-url}") 42 | private String clientUrl; 43 | 44 | @NonNull 45 | private CustomOidcUserService customOidcUserService; 46 | @NonNull 47 | private CustomOauth2UserService customOauth2UserService; 48 | @NonNull 49 | private CustomUserDetailsService customUserDetailsService; 50 | 51 | @Override 52 | protected void configure(HttpSecurity http) throws Exception { 53 | http 54 | .cors() 55 | .and() 56 | .csrf() 57 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 58 | .and() 59 | .authorizeRequests() 60 | .antMatchers("/oauth2/**", 61 | "/login/**", 62 | "/user", 63 | "/items/**", 64 | "/actuator/**", 65 | "/public/**", 66 | "/profile/**").permitAll() 67 | .anyRequest().authenticated() 68 | .and() 69 | .oauth2Login() 70 | .tokenEndpoint() 71 | .accessTokenResponseClient(authorizationCodeTokenResponseClient()) 72 | .and() 73 | .userInfoEndpoint() 74 | .oidcUserService(customOidcUserService) 75 | .userService(customOauth2UserService) 76 | .and() 77 | .successHandler((httpServletRequest, httpServletResponse, authentication) -> { 78 | httpServletResponse.getWriter().write(""); 79 | }) 80 | .and() 81 | .rememberMe() 82 | .rememberMeServices(getRememberMeServices()) 83 | .and() 84 | .exceptionHandling() 85 | .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), new AntPathRequestMatcher("/**")) 86 | .and() 87 | .logout(logout -> logout 88 | .permitAll() 89 | .logoutSuccessHandler((request, response, authentication) -> { 90 | response.setStatus(HttpServletResponse.SC_OK); 91 | } 92 | )); 93 | } 94 | 95 | @Bean 96 | public TokenBasedRememberMeServices getRememberMeServices() { 97 | TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("rememberme", customUserDetailsService); 98 | rememberMeServices.setAlwaysRemember(true); 99 | return rememberMeServices; 100 | } 101 | 102 | @Bean 103 | CorsConfigurationSource corsConfigurationSource() { 104 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 105 | CorsConfiguration corsConfiguration = new CorsConfiguration(); 106 | corsConfiguration.setAllowedOrigins(Arrays.asList(clientUrl)); 107 | corsConfiguration.setAllowedMethods( 108 | Arrays.stream(RequestMethod.values()) 109 | .map(RequestMethod::toString) 110 | .collect(Collectors.toList()) 111 | ); 112 | source.registerCorsConfiguration("/**", corsConfiguration); 113 | return source; 114 | } 115 | 116 | private OAuth2AccessTokenResponseClient authorizationCodeTokenResponseClient() { 117 | OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = 118 | new OAuth2AccessTokenResponseHttpMessageConverter(); 119 | tokenResponseHttpMessageConverter.setTokenResponseConverter(new OAuth2AccessTokenResponseConverterWithDefaults()); 120 | 121 | RestTemplate restTemplate = new RestTemplate(Arrays.asList( 122 | new FormHttpMessageConverter(), tokenResponseHttpMessageConverter)); 123 | restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); 124 | 125 | DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); 126 | tokenResponseClient.setRestOperations(restTemplate); 127 | 128 | return tokenResponseClient; 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /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 Maven 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 keystroke 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.6/maven-wrapper-0.5.6.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.6/maven-wrapper-0.5.6.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/main/java/com/uf/genshinwishes/service/PublicStatsService.java: -------------------------------------------------------------------------------- 1 | package com.uf.genshinwishes.service; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.uf.genshinwishes.dto.*; 5 | import com.uf.genshinwishes.model.BannerType; 6 | import com.uf.genshinwishes.model.User; 7 | import com.uf.genshinwishes.model.Wish; 8 | import com.uf.genshinwishes.repository.wish.WishRepository; 9 | import com.uf.genshinwishes.repository.wish.WishSpecification; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.cache.annotation.CachePut; 13 | import org.springframework.cache.annotation.Cacheable; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Isolation; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | import javax.persistence.EntityManager; 19 | import javax.persistence.criteria.*; 20 | import java.time.LocalDate; 21 | import java.util.*; 22 | 23 | // TODO refactor (not dry at all) 24 | 25 | @Service 26 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 27 | public class PublicStatsService { 28 | 29 | private EntityManager em; 30 | private BannerService bannerService; 31 | private WishRepository wishRepository; 32 | 33 | @Cacheable(value = "publicStats") 34 | public PublicStatsDTO getStatsFor(BannerType bannerType, Long event) { 35 | return null; 36 | } 37 | 38 | @CachePut("publicStats") 39 | @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) 40 | public PublicStatsDTO updateStatsFor(BannerType bannerType, Long event) { 41 | WishFilterDTO filters = WishFilterDTO.builder().events( 42 | event != null ? Arrays.asList(event) : Collections.emptyList() 43 | ).build(); 44 | 45 | List banners = bannerService.findAll(); 46 | filters.setRanks(Arrays.asList(4, 5)); 47 | WishSpecification fourFiveSpecifications = WishSpecification.builder() 48 | .bannerType(bannerType) 49 | .banners(banners) 50 | .filters(filters) 51 | .ignoreFirstPity(true) 52 | .build(); 53 | 54 | PublicStatsDTO stats = new PublicStatsDTO(); 55 | 56 | WishSpecification allRanksSpecifications = fourFiveSpecifications.toBuilder().filters(fourFiveSpecifications.getFilters().toBuilder().ranks(null).build()).build(); 57 | stats.setCount(wishRepository.count(allRanksSpecifications)); 58 | 59 | WishSpecification fourStarsSpecifications = fourFiveSpecifications.toBuilder() 60 | .filters(fourFiveSpecifications.getFilters().toBuilder().ranks(Collections.singletonList(4)).build()) 61 | .build(); 62 | stats.setCount4Stars(wishRepository.count(fourStarsSpecifications)); 63 | 64 | WishSpecification fiveStarsSpecifications = fourFiveSpecifications.toBuilder() 65 | .filters(fourFiveSpecifications.getFilters().toBuilder().ranks(Collections.singletonList(5)).build()) 66 | .build(); 67 | stats.setCount5Stars(wishRepository.count(fiveStarsSpecifications)); 68 | 69 | if (bannerType == BannerType.ALL) { 70 | Map latestBannerToEventMap = this.bannerService.getLatestBannerToEventMap(null); 71 | Map latestEventsCounts = Maps.newHashMap(); 72 | 73 | Arrays.asList(BannerType.CHARACTER_EVENT, BannerType.WEAPON_EVENT).stream().forEach(banner -> { 74 | LatestEventsCountsDTO latestEventsCountsDTO = new LatestEventsCountsDTO(); 75 | 76 | WishFilterDTO filter = WishFilterDTO.builder() 77 | .ranks(Arrays.asList(4, 5)) 78 | .events(Arrays.asList(latestBannerToEventMap.get(banner.getType()).getId())) 79 | .build(); 80 | WishSpecification eventSpecification = WishSpecification.builder() 81 | .banners(banners) 82 | .bannerType(banner) 83 | .filters(filter) 84 | .ignoreFirstPity(true) 85 | .build(); 86 | 87 | latestEventsCountsDTO.setCount(wishRepository.count(eventSpecification.toBuilder() 88 | .filters(filter.toBuilder().ranks(null).build()) 89 | .build())); 90 | latestEventsCountsDTO.setItems(this.getCountPerItemId(eventSpecification)); 91 | 92 | latestEventsCounts.put(banner, latestEventsCountsDTO); 93 | }); 94 | 95 | stats.setLatestEventsCounts(latestEventsCounts); 96 | stats.setCountPerBanner(getCountPerBanner(allRanksSpecifications)); 97 | } else { 98 | stats.setCountPerPity5Stars(getCountPerPity(fiveStarsSpecifications)); 99 | stats.setCountPerPity4Stars(getCountPerPity(fourStarsSpecifications)); 100 | 101 | // TODO not ready yet 102 | // if (bannerType == BannerType.CHARACTER_EVENT || bannerType == BannerType.WEAPON_EVENT) { 103 | // stats.setExclusiveRate5Stars(getExclusiveCount(bannerType, 5, fiveStarsSpecifications)); 104 | // stats.setExclusiveRate4Stars(getExclusiveCount(bannerType, 4, fourStarsSpecifications)); 105 | // } 106 | } 107 | 108 | stats.setUsersPerRegion(getUserPerRegion(allRanksSpecifications)); 109 | stats.setCountPerItemId(getCountPerItemId(fourFiveSpecifications)); 110 | stats.setCountPerDay(getCountPerDay(allRanksSpecifications)); 111 | stats.setCountPerRegion(getCountPerRegion(allRanksSpecifications)); 112 | 113 | return stats; 114 | } 115 | 116 | public List getCountPerDay(WishSpecification specification) { 117 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 118 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerDay.class); 119 | Root root = query.from(Wish.class); 120 | 121 | Expression dateTrunc = criteriaBuilder.function("DATE_TRUNC", Date.class, criteriaBuilder.literal("DAY"), root.get("time")).as(LocalDate.class); 122 | 123 | query.where(specification.toPredicate(root, query, criteriaBuilder)); 124 | 125 | query.groupBy(dateTrunc); 126 | 127 | return em.createQuery(query.multiselect(dateTrunc, criteriaBuilder.count(root))).getResultList(); 128 | } 129 | 130 | public List getCountPerItemId(WishSpecification specification) { 131 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 132 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerItemId.class); 133 | Root root = query.from(Wish.class); 134 | 135 | query.where(specification.toPredicate(root, query, criteriaBuilder)); 136 | 137 | query.groupBy(root.get("item").get("itemId")); 138 | 139 | query.orderBy(criteriaBuilder.desc(criteriaBuilder.count(root))); 140 | 141 | return em.createQuery(query.multiselect(root.get("item").get("itemId"), criteriaBuilder.count(root))).getResultList(); 142 | } 143 | 144 | public List getCountPerPity(WishSpecification specification) { 145 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 146 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerPity.class); 147 | Root root = query.from(Wish.class); 148 | 149 | query.where(criteriaBuilder.and(root.get("pity").isNotNull(), 150 | specification.toPredicate(root, query, criteriaBuilder))); 151 | 152 | query.groupBy(root.get("pity")); 153 | 154 | return em.createQuery(query.multiselect(root.get("pity"), criteriaBuilder.count(root))).getResultList(); 155 | } 156 | 157 | public List getCountPerBanner(WishSpecification specification) { 158 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 159 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerBanner.class); 160 | Root root = query.from(Wish.class); 161 | 162 | query.where(specification.toPredicate(root, query, criteriaBuilder)); 163 | 164 | query.groupBy(root.get("gachaType")); 165 | 166 | return em.createQuery(query.multiselect(root.get("gachaType"), criteriaBuilder.count(root))).getResultList(); 167 | } 168 | 169 | public List getCountPerRegion(WishSpecification specification) { 170 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 171 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerRegion.class); 172 | Root root = query.from(Wish.class); 173 | 174 | query.where(specification.toPredicate(root, query, criteriaBuilder)); 175 | 176 | query.groupBy(root.get("user").get("region")); 177 | 178 | return em.createQuery(query.multiselect(root.get("user").get("region"), criteriaBuilder.count(root))).getResultList(); 179 | } 180 | 181 | public List getUserPerRegion(WishSpecification specification) { 182 | CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); 183 | CriteriaQuery query = criteriaBuilder.createQuery(CountPerRegion.class); 184 | Root root = query.from(User.class); 185 | 186 | Subquery subQuery = query.subquery(Wish.class); 187 | Root subRoot = subQuery.from(Wish.class); 188 | subQuery.where(criteriaBuilder.and(criteriaBuilder.equal(subRoot.get("user"), root)), 189 | specification.toPredicate(subRoot, query, criteriaBuilder)); 190 | 191 | Expression region = root.get("region"); 192 | 193 | query.where(criteriaBuilder.and(root.get("region").isNotNull(), 194 | criteriaBuilder.exists(subQuery.select(subRoot)))); 195 | 196 | query.groupBy(region); 197 | 198 | return em.createQuery(query.multiselect(region, criteriaBuilder.count(root))).getResultList(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /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 | # Maven 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.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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 | --------------------------------------------------------------------------------