diff --git a/README.md b/README.md index bc0b3be..fcda0c7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,106 @@ -# server -theurl.io server project. +# Linkyou Server + +Linkyou 的后端服务端仓库,采用 Maven 多模块 + Spring 生态,包含配置中心、身份服务、消息服务、聚合服务及公共模块。 + +## 项目结构 + +```text +server/ +├── pom.xml # 根聚合 POM +├── docker-compose.yaml # 本地完整依赖编排 +├── docker-compose.dev.yaml # 开发环境简化编排 +├── bundle/ # bundle 服务 +├── config/ # 配置中心服务 +├── framework/ # 框架基础能力 +├── identity/ # 身份认证服务 +├── message/ # 消息服务 +└── shared/ # 公共代码模块 +``` + +## 技术栈 + +- Java 25 +- Spring Boot 4.0.6 +- Spring Cloud 2025.1.1 +- Spring Cloud Alibaba 2025.1.0.0 +- Maven 多模块构建 +- PostgreSQL / Redis / MongoDB / RabbitMQ + +> 版本来源:根 `pom.xml` 当前配置。 + +## 模块说明 + +- `config`:统一配置管理服务。 +- `identity`:身份认证与授权相关业务。 +- `message`:消息能力相关业务。 +- `bundle`:聚合编排或对外统一服务入口。 +- `framework`:基础框架和通用能力封装。 +- `shared`:跨模块复用的公共代码。 + +## 本地开发前置条件 + +建议先准备以下环境: + +- JDK 25 +- Maven 3.9+ +- Docker 与 Docker Compose(用于一键启动依赖) + +## 快速开始 + +### 1) 构建全部模块 + +```bash +cd /Users/rong/Code/Github/Linkyou/server +mvn clean install +``` + +### 2) 仅运行测试 + +```bash +cd /Users/rong/Code/Github/Linkyou/server +mvn test +``` + +### 3) 启动本地依赖(Docker Compose) + +```bash +cd /Users/rong/Code/Github/Linkyou/server +docker compose -f docker-compose.yaml up -d +``` + +### 4) 停止本地依赖 + +```bash +cd /Users/rong/Code/Github/Linkyou/server +docker compose -f docker-compose.yaml down +``` + +## 常用模块命令 + +在根目录执行(示例以 `identity` 模块为例): + +```bash +cd /Users/rong/Code/Github/Linkyou/server +mvn -pl identity -am clean package +``` + +## 端口与依赖(基于 compose 文件) + +- 配置中心:`8900` +- 身份服务:`8901` +- 消息服务:`8902` +- 聚合服务:`8903` +- PostgreSQL:`5432` +- Redis:`6379` +- MongoDB:`27017` +- RabbitMQ:`5672`(管理端口见 `docker-compose.yaml`) + +## 配置说明 + +- 示例环境变量可参考 `docker-compose.yaml` 与 `docker-compose.dev.yaml`。 +- 生产环境请务必替换默认账号密码与第三方 OAuth 密钥。 +- 建议通过外部配置中心或安全密钥管理系统注入敏感配置。 + +## 许可证 + +本项目使用仓库根目录 `LICENSE` 中声明的许可证。 diff --git a/bundle/src/main/java/io/theurl/bundle/application/command/BundleUpdateCommand.java b/bundle/src/main/java/io/theurl/bundle/application/command/BundleUpdateCommand.java index 769a778..c3c5050 100644 --- a/bundle/src/main/java/io/theurl/bundle/application/command/BundleUpdateCommand.java +++ b/bundle/src/main/java/io/theurl/bundle/application/command/BundleUpdateCommand.java @@ -1,8 +1,9 @@ package io.theurl.bundle.application.command; import com.neroyun.mediator.Command; +import lombok.Data; -@SuppressWarnings({"LombokGetterMayBeUsed", "LombokSetterMayBeUsed"}) +@Data public class BundleUpdateCommand implements Command { private final String vanity; @@ -13,32 +14,4 @@ public class BundleUpdateCommand implements Command { public BundleUpdateCommand(String vanity) { this.vanity = vanity; } - - public String getVanity() { - return vanity; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getImage() { - return image; - } - - public void setName(String name) { - this.name = name; - } - - public void setDescription(String description) { - this.description = description; - } - - public void setImage(String image) { - this.image = image; - } } diff --git a/bundle/src/main/java/io/theurl/bundle/application/contract/BundleApplicationService.java b/bundle/src/main/java/io/theurl/bundle/application/contract/BundleApplicationService.java index 2aea1f4..9281dbf 100644 --- a/bundle/src/main/java/io/theurl/bundle/application/contract/BundleApplicationService.java +++ b/bundle/src/main/java/io/theurl/bundle/application/contract/BundleApplicationService.java @@ -1,12 +1,15 @@ package io.theurl.bundle.application.contract; -import io.theurl.bundle.application.dto.BundleCreateDto; -import io.theurl.bundle.application.dto.BundleItemEditDto; -import io.theurl.bundle.application.dto.BundleUpdateDto; +import io.theurl.bundle.application.dto.*; import io.theurl.framework.application.ApplicationService; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; +/** + * Application service for managing bundles. Provides methods for creating, updating, and deleting bundles, as well as managing items within bundles. + */ public interface BundleApplicationService extends ApplicationService { /** @@ -61,4 +64,12 @@ public interface BundleApplicationService extends ApplicationService { * @return A CompletableFuture that will complete when the item is removed. */ CompletableFuture removeItemAsync(String vanity, long itemId); + + CompletableFuture> searchAsync(Map criteria, int from, int size); + + CompletableFuture countAsync(Map criteria); + + CompletableFuture> searchItemsAsync(String vanity, Map criteria, int from, int size); + + CompletableFuture countItemsAsync(String vanity, Map criteria); } diff --git a/bundle/src/main/java/io/theurl/bundle/application/dto/BundleItemListDto.java b/bundle/src/main/java/io/theurl/bundle/application/dto/BundleItemListDto.java new file mode 100644 index 0000000..929479f --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/application/dto/BundleItemListDto.java @@ -0,0 +1,16 @@ +package io.theurl.bundle.application.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class BundleItemListDto { + private long id; + private String url; + private String title; + private String description; + private int order; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/bundle/src/main/java/io/theurl/bundle/application/handler/BundleCreateCommandHandler.java b/bundle/src/main/java/io/theurl/bundle/application/handler/BundleCreateCommandHandler.java index 6fecf13..75cbdf4 100644 --- a/bundle/src/main/java/io/theurl/bundle/application/handler/BundleCreateCommandHandler.java +++ b/bundle/src/main/java/io/theurl/bundle/application/handler/BundleCreateCommandHandler.java @@ -28,7 +28,7 @@ public BundleCreateCommandHandler(BundleRepository repository) { @Override public CompletableFuture handleAsync(BundleCreateCommand message, MessageContext context) { - var userId = Long.getLong(Objects.requireNonNull(getRequest()).getUserPrincipal().getName()); + var userId = Long.parseLong(Objects.requireNonNull(getRequest()).getUserPrincipal().getName()); var aggregate = Bundle.create(message.getType(), message.getVanity(), message.getName()); if (message.getDescription() != null) { aggregate.setDescription(message.getDescription()); diff --git a/bundle/src/main/java/io/theurl/bundle/application/implement/BundleApplicationServiceImpl.java b/bundle/src/main/java/io/theurl/bundle/application/implement/BundleApplicationServiceImpl.java index 3a3bf93..da8be99 100644 --- a/bundle/src/main/java/io/theurl/bundle/application/implement/BundleApplicationServiceImpl.java +++ b/bundle/src/main/java/io/theurl/bundle/application/implement/BundleApplicationServiceImpl.java @@ -5,9 +5,11 @@ import io.theurl.bundle.application.command.BundleDeleteCommand; import io.theurl.bundle.application.command.BundleUpdateCommand; import io.theurl.bundle.application.contract.BundleApplicationService; -import io.theurl.bundle.application.dto.BundleCreateDto; -import io.theurl.bundle.application.dto.BundleItemEditDto; -import io.theurl.bundle.application.dto.BundleUpdateDto; +import io.theurl.bundle.application.dto.*; +import io.theurl.bundle.persistence.query.BundleCountQuery; +import io.theurl.bundle.persistence.query.BundleItemCountQuery; +import io.theurl.bundle.persistence.query.BundleItemListQuery; +import io.theurl.bundle.persistence.query.BundleListQuery; import io.theurl.framework.application.BaseApplicationService; import io.theurl.framework.utility.ShortUniqueId; import org.modelmapper.ModelMapper; @@ -17,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; @@ -135,4 +138,41 @@ public CompletableFuture updateItemAsync(String vanity, long itemId, Bundl public CompletableFuture removeItemAsync(String vanity, long itemId) { return null; } + + @Override + public CompletableFuture> searchAsync(Map criteria, int from, int size) { + if (criteria.getOrDefault("owned", false).equals(true)) { + criteria.put("ownerId", currentUserId()); + } + var query = new BundleListQuery(criteria, from, size); + return mediator.executeAsync(query) + .thenApply(models -> { + return models.stream() + .map(model -> mapper.map(model, BundleListDto.class)) + .toList(); + }); + } + + @Override + public CompletableFuture countAsync(Map criteria) { + var query = new BundleCountQuery(criteria); + return mediator.executeAsync(query); + } + + @Override + public CompletableFuture> searchItemsAsync(String vanity, Map criteria, int from, int size) { + var query = new BundleItemListQuery(vanity, criteria, from, size); + return mediator.executeAsync(query) + .thenApply(models -> { + return models.stream() + .map(model -> mapper.map(model, BundleItemListDto.class)) + .toList(); + }); + } + + @Override + public CompletableFuture countItemsAsync(String vanity, Map criteria) { + var query = new BundleItemCountQuery(vanity, criteria); + return mediator.executeAsync(query); + } } diff --git a/bundle/src/main/java/io/theurl/bundle/configure/SecurityConfiguration.java b/bundle/src/main/java/io/theurl/bundle/configure/SecurityConfiguration.java index 85e17e7..166d514 100644 --- a/bundle/src/main/java/io/theurl/bundle/configure/SecurityConfiguration.java +++ b/bundle/src/main/java/io/theurl/bundle/configure/SecurityConfiguration.java @@ -1,8 +1,10 @@ package io.theurl.bundle.configure; +import io.theurl.framework.security.JwtAuthenticationFilter; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; @@ -22,22 +24,25 @@ public class SecurityConfiguration { @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, - io.theurl.framework.security.JwtAuthenticationFilter jwtAuthenticationFilter) { + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) { http.csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))) - .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/error" - ).permitAll() - .anyRequest().authenticated() - ) + .authorizeHttpRequests(auth -> { + + auth.requestMatchers(HttpMethod.GET, "/api/bundle/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/bookmark/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/bookmark/my").authenticated() + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/error" + ).permitAll() + .anyRequest().authenticated(); + }) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); diff --git a/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BookmarkController.java b/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BookmarkController.java new file mode 100644 index 0000000..91b1757 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BookmarkController.java @@ -0,0 +1,179 @@ +package io.theurl.bundle.interfaces.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.theurl.bundle.application.contract.BundleApplicationService; +import io.theurl.bundle.application.dto.*; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Bookmark aggregate HTTP endpoints. + * + *

All APIs delegate business logic to {@link BundleApplicationService}; + * the controller only handles request mapping, lightweight parameter shaping, + * and response header composition.

+ */ +@RestController +@RequestMapping("/api/bookmark") +public class BookmarkController { + private final BundleApplicationService service; + + public BookmarkController(BundleApplicationService service) { + this.service = service; + } + + /** + * Retrieves a list of owned bookmarks with optional keyword filtering and pagination. + * + * @param keyword Optional keyword to filter bookmarks. + * @param from Optional starting index for pagination. + * @param size Optional number of bookmarks to retrieve. + * @return A CompletableFuture containing a list of owned bookmarks. + */ + @GetMapping("/my") + @Operation(summary = "Get owned bookmarks", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture> getOwnedAsync(@RequestParam(required = false) String keyword, + @RequestParam(required = false, defaultValue = "0") Integer from, + @RequestParam(required = false, defaultValue = "10") Integer size) { + // Force owned+bookmark filters; optional keyword is merged only when provided. + var criteria = new HashMap<>(Map.of("owned", true, "type", "bookmark")); + if (keyword != null) { + criteria.put("keyword", keyword); + } + return service.searchAsync(criteria, from, size); + } + + /** + * Searches bookmarks across the system with optional keyword filtering and pagination. + * + * @param keyword Optional keyword to filter bookmarks. + * @param from Optional starting index for pagination. + * @param size Optional number of bookmarks to retrieve. + * @return A CompletableFuture containing a list of bookmarks matching the criteria. + */ + @GetMapping("search") + @Operation(summary = "Search bookmarks", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture> searchAsync(@RequestParam(required = false) String keyword, + @RequestParam(required = false, defaultValue = "0") Integer from, + @RequestParam(required = false, defaultValue = "10") Integer size) { + // Shared search endpoint constrained to bookmark type. + var criteria = new HashMap(); + if (keyword != null) { + criteria.put("keyword", keyword); + } + criteria.put("type", "bookmark"); + return service.searchAsync(criteria, from, size); + } + + /** + * Creates a new bookmark with the provided data. The server enforces the resource type to "bookmark" to prevent client-side tampering. + * + * @param data The data for the new bookmark. + * @param response The HTTP response to which headers can be added. + * @return A CompletableFuture representing the asynchronous operation. + */ + @PostMapping + @Operation(summary = "Create a new bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture createAsync(@RequestBody BundleCreateDto data, HttpServletResponse response) { + // Enforce server-side resource type to avoid client-side tampering. + data.setType("bookmark"); + return service.createAsync(data) + // Return created vanity identifier through response header for client navigation. + .thenAccept(result -> response.addHeader("x-vanity", result)); + } + + /** + * Updates an existing bookmark identified by the vanity with the provided data. The server enforces the resource type to "bookmark" to prevent client-side tampering. + * + * @param vanity The vanity identifier of the bookmark to update. + * @param data The data to update the bookmark with. + * @return A CompletableFuture representing the asynchronous operation. + */ + @PutMapping("/{vanity}") + @Operation(summary = "Update an existing bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture updateAsync(@PathVariable String vanity, @RequestBody BundleUpdateDto data) { + return service.updateAsync(vanity, data); + } + + /** + * Deletes the bookmark identified by the vanity. + * + * @param vanity The vanity identifier of the bookmark to delete. + * @return A CompletableFuture representing the asynchronous operation. + */ + @DeleteMapping("/{vanity}") + @Operation(summary = "Delete an existing bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture deleteAsync(@PathVariable String vanity) { + return service.deleteAsync(vanity); + } + + /** + * Retrieves items of a bookmark with optional keyword filtering and pagination. + * Item-level filtering currently supports keyword search across name, description, and URL fields; pagination is delegated to the service layer. + * + * @param vanity The vanity identifier of the bookmark. + * @param keyword The keyword to filter items by. + * @param from The starting index for pagination. + * @param size The number of items to retrieve. + * @return A CompletableFuture representing the asynchronous operation. + */ + @GetMapping("{vanity}/items") + @Operation(summary = "Get items of a bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture> searchItemsAsync(@PathVariable String vanity, + @RequestParam(required = false) String keyword, + @RequestParam(required = false, defaultValue = "0") Integer from, + @RequestParam(required = false, defaultValue = "10") Integer size) { + // Item-level filtering currently supports keyword; pagination is delegated to service. + var criteria = new HashMap(); + if (keyword != null) { + criteria.put("keyword", keyword); + } + return service.searchItemsAsync(vanity, criteria, from, size); + } + + /** + * Appends a new item to the bookmark identified by the vanity with the provided data. + * + * @param vanity The vanity identifier of the bookmark. + * @param data The data of the item to append. + * @return A CompletableFuture representing the asynchronous operation. + */ + @PostMapping("{vanity}/items") + @Operation(summary = "Append items to a bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture appendItemAsync(@PathVariable String vanity, @RequestBody BundleItemEditDto data) { + return service.appendItemAsync(vanity, data); + } + + /** + * Updates an existing item in the bookmark identified by the vanity with the provided data. + * + * @param vanity The vanity identifier of the bookmark. + * @param itemId The identifier of the item to update. + * @param data The data of the item to update. + * @return A CompletableFuture representing the asynchronous operation. + */ + @PutMapping("{vanity}/items/{itemId}") + @Operation(summary = "Update an item in a bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture updateItemAsync(@PathVariable String vanity, @PathVariable long itemId, @RequestBody BundleItemEditDto data) { + return service.updateItemAsync(vanity, itemId, data); + } + + /** + * Removes an existing item from the bookmark identified by the vanity. + * + * @param vanity The vanity identifier of the bookmark. + * @param itemId The identifier of the item to remove. + * @return A CompletableFuture representing the asynchronous operation. + */ + @DeleteMapping("{vanity}/items/{itemId}") + @Operation(summary = "Delete an item from a bookmark", security = @SecurityRequirement(name = "bearerAuth")) + public CompletableFuture removeItemAsync(@PathVariable String vanity, @PathVariable long itemId) { + return service.removeItemAsync(vanity, itemId); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BundleController.java b/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BundleController.java index 7dd7a8f..224d731 100644 --- a/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BundleController.java +++ b/bundle/src/main/java/io/theurl/bundle/interfaces/controller/BundleController.java @@ -5,14 +5,18 @@ import io.theurl.bundle.application.contract.BundleApplicationService; import io.theurl.bundle.application.dto.BundleCreateDto; import io.theurl.bundle.application.dto.BundleItemEditDto; +import io.theurl.bundle.application.dto.BundleListDto; import io.theurl.bundle.application.dto.BundleUpdateDto; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; @RestController -@RequestMapping("/api/bundles") +@RequestMapping("/api/bundle") public class BundleController { private final BundleApplicationService service; @@ -33,7 +37,7 @@ public BundleController(BundleApplicationService service) { */ @PostMapping @Operation(summary = "Create a new bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture create(@RequestBody BundleCreateDto data, HttpServletResponse response) { + public CompletableFuture createAsync(@RequestBody BundleCreateDto data, HttpServletResponse response) { return service.createAsync(data) .thenAccept(vanity -> response.addHeader("x-vanity", vanity)); } @@ -50,7 +54,7 @@ public CompletableFuture create(@RequestBody BundleCreateDto data, HttpSer */ @PutMapping("/{vanity}") @Operation(summary = "Update an existing bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture update(@PathVariable String vanity, @RequestBody BundleUpdateDto data) { + public CompletableFuture updateAsync(@PathVariable String vanity, @RequestBody BundleUpdateDto data) { return service.updateAsync(vanity, data); } @@ -65,7 +69,7 @@ public CompletableFuture update(@PathVariable String vanity, @RequestBody */ @DeleteMapping("/{vanity}") @Operation(summary = "Delete an existing bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture delete(@PathVariable String vanity) { + public CompletableFuture deleteAsync(@PathVariable String vanity) { return service.deleteAsync(vanity); } @@ -79,9 +83,9 @@ public CompletableFuture delete(@PathVariable String vanity) { * @param data The data for the item to be appended. * @return A CompletableFuture representing the asynchronous operation. */ - @PostMapping("/{vanity}/append") + @PostMapping("/{vanity}/items") @Operation(summary = "Append items to an existing bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture append(@PathVariable String vanity, @RequestBody BundleItemEditDto data) { + public CompletableFuture appendItemAsync(@PathVariable String vanity, @RequestBody BundleItemEditDto data) { return service.appendItemAsync(vanity, data); } @@ -95,9 +99,9 @@ public CompletableFuture append(@PathVariable String vanity, @RequestBody * @param itemId The ID of the item to be removed. * @return A CompletableFuture representing the asynchronous operation. */ - @DeleteMapping("/{vanity}/{itemId}") + @DeleteMapping("/{vanity}/items/{itemId}") @Operation(summary = "Remove an item from an existing bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture remove(@PathVariable String vanity, @PathVariable long itemId) { + public CompletableFuture removeItemAsync(@PathVariable String vanity, @PathVariable long itemId) { return service.removeItemAsync(vanity, itemId); } @@ -112,9 +116,35 @@ public CompletableFuture remove(@PathVariable String vanity, @PathVariable * @param data The updated data for the item. * @return A CompletableFuture representing the asynchronous operation. */ - @PutMapping("/{vanity}/{itemId}") + @PutMapping("/{vanity}/items/{itemId}") @Operation(summary = "Update an item in an existing bundle", security = @SecurityRequirement(name = "bearerAuth")) - public CompletableFuture updateItem(@PathVariable String vanity, @PathVariable long itemId, @RequestBody BundleItemEditDto data) { + public CompletableFuture updateItemAsync(@PathVariable String vanity, @PathVariable long itemId, @RequestBody BundleItemEditDto data) { return service.updateItemAsync(vanity, itemId, data); } + + @GetMapping("list") + @Operation(summary = "Search bundles by type and keyword") + public CompletableFuture> search(@RequestParam(required = false) String type, @RequestParam(required = false) String keyword, @RequestParam Integer from, @RequestParam Integer size) { + var criteria = new HashMap(); + if (StringUtils.hasText(type)) { + criteria.put("type", type); + } + if (StringUtils.hasText(keyword)) { + criteria.put("keyword", keyword); + } + return service.searchAsync(criteria, from, size); + } + + @GetMapping("count") + @Operation(summary = "Count bundles by type and keyword") + public CompletableFuture count(@RequestParam(required = false) String type, @RequestParam(required = false) String keyword) { + var criteria = new HashMap(); + if (StringUtils.hasText(type)) { + criteria.put("type", type); + } + if (StringUtils.hasText(keyword)) { + criteria.put("keyword", keyword); + } + return service.countAsync(criteria); + } } diff --git a/bundle/src/main/java/io/theurl/bundle/interfaces/controller/package-info.java b/bundle/src/main/java/io/theurl/bundle/interfaces/package-info.java similarity index 82% rename from bundle/src/main/java/io/theurl/bundle/interfaces/controller/package-info.java rename to bundle/src/main/java/io/theurl/bundle/interfaces/package-info.java index 407f74a..6937838 100644 --- a/bundle/src/main/java/io/theurl/bundle/interfaces/controller/package-info.java +++ b/bundle/src/main/java/io/theurl/bundle/interfaces/package-info.java @@ -1,5 +1,5 @@ @OpenAPIDefinition(info = @Info(title = "Bundle API", version = "1.0", description = "API for managing bundles")) -package io.theurl.bundle.interfaces.controller; +package io.theurl.bundle.interfaces; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Info; diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/entity/Bundle.java b/bundle/src/main/java/io/theurl/bundle/persistence/entity/Bundle.java index 8bd0d5c..0852bc1 100644 --- a/bundle/src/main/java/io/theurl/bundle/persistence/entity/Bundle.java +++ b/bundle/src/main/java/io/theurl/bundle/persistence/entity/Bundle.java @@ -30,7 +30,7 @@ public class Bundle implements Persistable { @Column(name = "name", length = 100, nullable = false) private String name; - @Column(name = "description", columnDefinition = "TEXT") + @Column(name = "description", columnDefinition = "text") private String description; @Column(name = "image") diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/entity/BundleExtend.java b/bundle/src/main/java/io/theurl/bundle/persistence/entity/BundleExtend.java index 758835c..fc13e87 100644 --- a/bundle/src/main/java/io/theurl/bundle/persistence/entity/BundleExtend.java +++ b/bundle/src/main/java/io/theurl/bundle/persistence/entity/BundleExtend.java @@ -14,8 +14,8 @@ public class BundleExtend implements Persistable { @Id private Long id; - @Column(name = "item_count") - private int itemCount; + @Column(name = "items_count") + private int itemsCount; @Column(name = "favorite_count") private int favoriteCount; diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleCountQueryHandler.java b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleCountQueryHandler.java new file mode 100644 index 0000000..ea31a58 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleCountQueryHandler.java @@ -0,0 +1,57 @@ +package io.theurl.bundle.persistence.handler; + +import com.neroyun.mediator.Handler; +import com.neroyun.mediator.MessageContext; +import io.theurl.bundle.persistence.entity.Bundle; +import io.theurl.bundle.persistence.query.BundleCountQuery; +import io.theurl.framework.core.BeanScope; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Component +@Scope(value = BeanScope.REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class BundleCountQueryHandler implements Handler { + + @PersistenceContext + private EntityManager manager; + + @Override + public CompletableFuture handleAsync(BundleCountQuery message, MessageContext context) { + var builder = manager.getCriteriaBuilder(); + var query = builder.createQuery(Long.class); + Root select = query.from(Bundle.class); + query.select(builder.count(select)); + List predicates = new ArrayList<>(List.of(builder.isFalse(select.get("deleted")))); + + message.criteria().forEach((key, value) -> { + switch (key) { + case "ownerId" -> predicates.add(builder.equal(select.get("ownerId"), value)); + case "type" -> predicates.add(builder.equal(select.get("type"), value)); + case "keyword" -> { + if (value instanceof String keyword) { + Predicate orGroup = builder.or( + builder.like(select.get("name"), "%" + keyword + "%"), + builder.like(select.get("description"), "%" + keyword + "%") + ); + predicates.add(orGroup); + } + } + default -> predicates.add(builder.equal(select.get(key), value)); + } + }); + + query.where(builder.and(predicates.toArray(new Predicate[0]))); + var typedQuery = manager.createQuery(query); + var result = typedQuery.getSingleResult(); + return CompletableFuture.completedFuture(result.intValue()); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemCountQueryHandler.java b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemCountQueryHandler.java new file mode 100644 index 0000000..1207f3b --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemCountQueryHandler.java @@ -0,0 +1,63 @@ +package io.theurl.bundle.persistence.handler; + +import com.neroyun.mediator.Handler; +import com.neroyun.mediator.MessageContext; +import io.theurl.bundle.persistence.entity.Bundle; +import io.theurl.bundle.persistence.entity.BundleItem; +import io.theurl.bundle.persistence.model.BundleItemModel; +import io.theurl.bundle.persistence.query.BundleItemCountQuery; +import io.theurl.framework.core.BeanScope; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +@Component +@Scope(value = BeanScope.REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class BundleItemCountQueryHandler implements Handler { + + @PersistenceContext + private EntityManager manager; + + @Override + public CompletableFuture handleAsync(BundleItemCountQuery message, MessageContext context) { + var builder = manager.getCriteriaBuilder(); + var query = builder.createQuery(Long.class); + var select = query.from(BundleItem.class); + query.select(builder.count(select)); + Join join = select.join(Bundle.class); + + List predicates = new ArrayList<>(); + predicates.add(builder.isFalse(join.get("deleted"))); + predicates.add(builder.equal(join.get("vanity"), message.vanity())); + + message.criteria().forEach((k, v) -> { + if (Objects.equals(k, "keyword") && v instanceof String keyword) { + Predicate orGroup = builder.or( + builder.like(select.get("name"), "%" + keyword + "%"), + builder.like(select.get("description"), "%" + keyword + "%"), + builder.like(select.get("url"), "%" + keyword + "%") + ); + predicates.add(orGroup); + } + }); + + query.where(builder.and(predicates.toArray(new Predicate[0]))); + if (!predicates.isEmpty()) { + query.where(builder.or(predicates.toArray(new Predicate[0]))); + } + + var typedQuery = manager.createQuery(query); + var count = typedQuery.getSingleResult(); + + return CompletableFuture.completedFuture(count.intValue()); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemListQueryHandler.java b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemListQueryHandler.java new file mode 100644 index 0000000..fbfe319 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleItemListQueryHandler.java @@ -0,0 +1,73 @@ +package io.theurl.bundle.persistence.handler; + +import com.neroyun.mediator.Handler; +import com.neroyun.mediator.MessageContext; +import io.theurl.bundle.persistence.entity.Bundle; +import io.theurl.bundle.persistence.entity.BundleItem; +import io.theurl.bundle.persistence.model.BundleItemModel; +import io.theurl.bundle.persistence.query.BundleItemListQuery; +import io.theurl.framework.core.BeanScope; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +@Component +@Scope(value = BeanScope.REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class BundleItemListQueryHandler implements Handler> { + + @PersistenceContext + private EntityManager manager; + + private final ModelMapper mapper; + + public BundleItemListQueryHandler(ModelMapper mapper) { + this.mapper = mapper; + } + + @Override + public CompletableFuture> handleAsync(BundleItemListQuery message, MessageContext context) { + var builder = manager.getCriteriaBuilder(); + var query = builder.createQuery(BundleItem.class); + var entity = query.from(BundleItem.class); + + Join join = entity.join(Bundle.class); + + List predicates = new ArrayList<>(); + predicates.add(builder.isFalse(join.get("deleted"))); + predicates.add(builder.equal(join.get("vanity"), message.vanity())); + + message.criteria().forEach((k, v) -> { + if (Objects.equals(k, "keyword") && v instanceof String keyword) { + Predicate orGroup = builder.or( + builder.like(entity.get("name"), "%" + keyword + "%"), + builder.like(entity.get("description"), "%" + keyword + "%"), + builder.like(entity.get("url"), "%" + keyword + "%") + ); + predicates.add(orGroup); + } + }); + + query.where(builder.and(predicates.toArray(new Predicate[0]))); + if (!predicates.isEmpty()) { + query.where(builder.or(predicates.toArray(new Predicate[0]))); + } + + var typedQuery = manager.createQuery(query); + var models = typedQuery.getResultList() + .stream() + .map(item -> mapper.map(item, BundleItemModel.class)) + .toList(); + + return CompletableFuture.completedFuture(models); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleListQueryHandler.java b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleListQueryHandler.java new file mode 100644 index 0000000..316f03c --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/handler/BundleListQueryHandler.java @@ -0,0 +1,70 @@ +package io.theurl.bundle.persistence.handler; + +import com.neroyun.mediator.Handler; +import com.neroyun.mediator.MessageContext; +import io.theurl.bundle.persistence.entity.Bundle; +import io.theurl.bundle.persistence.model.BundleListModel; +import io.theurl.bundle.persistence.query.BundleListQuery; +import io.theurl.framework.core.BeanScope; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.Predicate; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Component +@Scope(value = BeanScope.REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class BundleListQueryHandler implements Handler> { + + @PersistenceContext + private EntityManager manager; + + private final ModelMapper mapper; + + public BundleListQueryHandler(ModelMapper mapper) { + this.mapper = mapper; + } + + @Override + public CompletableFuture> handleAsync(BundleListQuery message, MessageContext context) { + var builder = manager.getCriteriaBuilder(); + var query = builder.createQuery(Bundle.class); + var select = query.from(Bundle.class); + + List predicates = new ArrayList<>(List.of(builder.isFalse(select.get("deleted")))); + + message.criteria().forEach((k, v) -> { + switch (k) { + case "ownerId" -> predicates.add(builder.equal(select.get("ownerId"), v)); + case "type" -> predicates.add(builder.equal(select.get("type"), v)); + case "keyword" -> { + if (v instanceof String keyword) { + Predicate orGroup = builder.or( + builder.like(select.get("name"), "%" + keyword + "%"), + builder.like(select.get("description"), "%" + keyword + "%") + ); + predicates.add(orGroup); + } + } + default -> predicates.add(builder.equal(select.get(k), v)); + } + }); + + query.where(builder.and(predicates.toArray(new Predicate[0]))); + + var typedQuery = manager.createQuery(query); + typedQuery.setFirstResult(message.from()); + typedQuery.setMaxResults(message.size()); + var resultList = typedQuery.getResultList() + .stream() + .map(src -> mapper.map(src, BundleListModel.class)) + .toList(); + return CompletableFuture.completedFuture(resultList); + } +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java index 09f222e..81ea30d 100644 --- a/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java +++ b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleItemModel.java @@ -1,4 +1,14 @@ package io.theurl.bundle.persistence.model; +import lombok.Data; + +@Data public class BundleItemModel { + private long id; + private long bundleId; + private String url; + private String title; + private String description; + private String image; + private int order; } diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleListModel.java b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleListModel.java new file mode 100644 index 0000000..a732379 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/model/BundleListModel.java @@ -0,0 +1,23 @@ +package io.theurl.bundle.persistence.model; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class BundleListModel { + private long id; + private String type; + private String vanity; + private String name; + private String description; + private String image; + private int itemsCount; + private int favoriteCount; + private int commentCount; + private int visitCount; + private Long ownerId; + private String ownerName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/profile/BundleMapProfile.java b/bundle/src/main/java/io/theurl/bundle/persistence/profile/BundleMapProfile.java index 04a5868..efe8ce9 100644 --- a/bundle/src/main/java/io/theurl/bundle/persistence/profile/BundleMapProfile.java +++ b/bundle/src/main/java/io/theurl/bundle/persistence/profile/BundleMapProfile.java @@ -1,6 +1,7 @@ package io.theurl.bundle.persistence.profile; import io.theurl.bundle.persistence.entity.Bundle; +import io.theurl.bundle.persistence.model.BundleListModel; import jakarta.annotation.PostConstruct; import org.modelmapper.ModelMapper; import org.modelmapper.Provider; @@ -70,6 +71,14 @@ public void configure() { // dest.getExtend().setLastVisitedAt(extend.getLastVisitedAt()); // }); }); + + mapper.createTypeMap(io.theurl.bundle.persistence.entity.Bundle.class, BundleListModel.class) + .addMappings(expression -> { + expression.map(src -> src.getExtend().getItemsCount(), BundleListModel::setItemsCount); + expression.map(src -> src.getExtend().getFavoriteCount(), BundleListModel::setFavoriteCount); + expression.map(src -> src.getExtend().getCommentCount(), BundleListModel::setCommentCount); + expression.map(src -> src.getExtend().getVisitCount(), BundleListModel::setVisitCount); + }); } /** diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleCountQuery.java b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleCountQuery.java new file mode 100644 index 0000000..4ae0e40 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleCountQuery.java @@ -0,0 +1,8 @@ +package io.theurl.bundle.persistence.query; + +import com.neroyun.mediator.Query; + +import java.util.Map; + +public record BundleCountQuery(Map criteria) implements Query { +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemCountQuery.java b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemCountQuery.java new file mode 100644 index 0000000..f58e28a --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemCountQuery.java @@ -0,0 +1,8 @@ +package io.theurl.bundle.persistence.query; + +import com.neroyun.mediator.Query; + +import java.util.Map; + +public record BundleItemCountQuery(String vanity, Map criteria) implements Query { +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemListQuery.java b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemListQuery.java new file mode 100644 index 0000000..3755637 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleItemListQuery.java @@ -0,0 +1,11 @@ +package io.theurl.bundle.persistence.query; + +import com.neroyun.mediator.Query; +import io.theurl.bundle.persistence.model.BundleItemModel; + +import java.util.List; +import java.util.Map; + +public record BundleItemListQuery(String vanity, Map criteria, int from, + int size) implements Query> { +} diff --git a/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleListQuery.java b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleListQuery.java new file mode 100644 index 0000000..50b32d2 --- /dev/null +++ b/bundle/src/main/java/io/theurl/bundle/persistence/query/BundleListQuery.java @@ -0,0 +1,25 @@ +package io.theurl.bundle.persistence.query; + +import com.neroyun.mediator.Query; +import io.theurl.bundle.persistence.model.BundleListModel; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public record BundleListQuery(Map criteria, int from, int size) implements Query> { + + public void tryGet(String key, Consumer consumer) { + if (criteria == null || !criteria.containsKey(key)) { + return; + } + + var params = criteria.get(key); + + if (params == null) { + return; + } + + consumer.accept(criteria.get(key)); + } +} diff --git a/bundle/src/main/resources/application.yaml b/bundle/src/main/resources/application.yaml index 962d792..c219742 100644 --- a/bundle/src/main/resources/application.yaml +++ b/bundle/src/main/resources/application.yaml @@ -24,6 +24,7 @@ spring: show-sql: true properties: hibernate: + globally_quoted_identifiers: true multiTenancy: SCHEMA format_sql: true data: diff --git a/framework/src/main/java/io/theurl/framework/utility/MapUtility.java b/framework/src/main/java/io/theurl/framework/utility/MapUtility.java new file mode 100644 index 0000000..f9c2bc3 --- /dev/null +++ b/framework/src/main/java/io/theurl/framework/utility/MapUtility.java @@ -0,0 +1,12 @@ +package io.theurl.framework.utility; + +import java.util.Map; +import java.util.function.Consumer; + +public class MapUtility { + public static void tryGet(Map criteria, K key, Consumer function) { + if (criteria.containsKey(key)) { + function.accept(criteria.get(key)); + } + } +} diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/Authlog.java b/identity/src/main/java/io/theurl/identity/persistence/entity/Authlog.java index 61f5e3f..91d4908 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/Authlog.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/Authlog.java @@ -1,9 +1,6 @@ package io.theurl.identity.persistence.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import org.springframework.data.domain.Persistable; @@ -11,7 +8,10 @@ @Data @Entity -@Table(name = "authlog") +@Table(name = "authlog", indexes = { + @Index(name = "idx_authlog_user_id", columnList = "user_id"), + @Index(name = "idx_authlog_username", columnList = "username") +}) public class Authlog implements Persistable { @Id private Long id; diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/OnetimePassword.java b/identity/src/main/java/io/theurl/identity/persistence/entity/OnetimePassword.java index fdb979c..20d0b5d 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/OnetimePassword.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/OnetimePassword.java @@ -1,9 +1,6 @@ package io.theurl.identity.persistence.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import org.springframework.data.domain.Persistable; @@ -11,7 +8,9 @@ @Data @Entity -@Table(name = "onetime_password") +@Table(name = "onetime_password", indexes = { + @Index(name = "idx_onetime_password_request_id", columnList = "request_id", unique = true) +}) public class OnetimePassword implements Persistable { @Id private Long id; diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/Token.java b/identity/src/main/java/io/theurl/identity/persistence/entity/Token.java index 9559eaa..a4f534e 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/Token.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/Token.java @@ -1,9 +1,6 @@ package io.theurl.identity.persistence.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import org.springframework.data.domain.Persistable; @@ -11,7 +8,10 @@ @Data @Entity -@Table(name = "token") +@Table(name = "token", indexes = { + @Index(name = "idx_token_jti", columnList = "jti", unique = true), + @Index(name = "idx_token_subject", columnList = "subject") +}) public class Token implements Persistable { @Id private Long id; diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/User.java b/identity/src/main/java/io/theurl/identity/persistence/entity/User.java index 99fb606..211726f 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/User.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/User.java @@ -9,10 +9,10 @@ @Data @Entity -@Table(name = "users", indexes = { - @Index(name = "user_idx_username", columnList = "username", unique = true), - @Index(name = "user_idx_email", columnList = "email", unique = true), - @Index(name = "user_idx_phone", columnList = "phone", unique = true) +@Table(name = "user", indexes = { + @Index(name = "idx_user_idx_username", columnList = "username", unique = true), + @Index(name = "idx_user_idx_email", columnList = "email", unique = true), + @Index(name = "idx_user_idx_phone", columnList = "phone", unique = true) }) public class User implements Persistable { static final int USERNAME_LENGTH = 64; diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/UserAuthority.java b/identity/src/main/java/io/theurl/identity/persistence/entity/UserAuthority.java index 2432820..4bf56ed 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/UserAuthority.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/UserAuthority.java @@ -9,7 +9,7 @@ @Data @Entity @Table(name = "user_authority", indexes = { - @Index(name = "user_authority_idx_unique", columnList = "provider,open_id,user_id", unique = true) + @Index(name = "idx_user_authority_unique", columnList = "provider,open_id,user_id", unique = true) }) public class UserAuthority implements Persistable { @Id diff --git a/identity/src/main/java/io/theurl/identity/persistence/entity/UserRole.java b/identity/src/main/java/io/theurl/identity/persistence/entity/UserRole.java index ae0f332..44dcd68 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/entity/UserRole.java +++ b/identity/src/main/java/io/theurl/identity/persistence/entity/UserRole.java @@ -9,7 +9,7 @@ @Data @Entity @Table(name = "user_role", indexes = { - @Index(name = "user_role_idx_unique", columnList = "name,user_id", unique = true) + @Index(name = "idx_user_role_unique", columnList = "name,user_id", unique = true) }) public class UserRole implements Persistable { @Id diff --git a/identity/src/main/resources/application.yaml b/identity/src/main/resources/application.yaml index edf855f..141d6a4 100644 --- a/identity/src/main/resources/application.yaml +++ b/identity/src/main/resources/application.yaml @@ -20,6 +20,7 @@ spring: show-sql: true properties: hibernate: + globally_quoted_identifiers: true multiTenancy: SCHEMA format_sql: true data: diff --git a/message/src/main/resources/application.yaml b/message/src/main/resources/application.yaml index 443e4bc..a4f6a7f 100644 --- a/message/src/main/resources/application.yaml +++ b/message/src/main/resources/application.yaml @@ -22,6 +22,11 @@ spring: ddl-auto: update dialect: ${DB_DIALECT:org.hibernate.dialect.PostgreSQLDialect} show-sql: true + properties: + hibernate: + globally_quoted_identifiers: true + multiTenancy: SCHEMA + format_sql: true data: redis: url: ${REDIS_URL:redis://localhost:6379}