diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index 34c5a5e..24117e5 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -2,11 +2,11 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.common.TreeNode; +import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; import com.onixbyte.helix.service.DepartmentService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -36,4 +36,9 @@ public class DepartmentController { public List getDepartments() { return departmentService.getDepartments(); } + + @PostMapping + public Department addDepartment(@Validated @RequestBody AddDepartmentRequest request) { + return departmentService.addDepartment(request); + } } diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java index f8a6677..dbec193 100644 --- a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -3,8 +3,10 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.response.BizExceptionResponse; import com.onixbyte.helix.exception.BizException; import jakarta.validation.ConstraintViolationException; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -25,22 +27,6 @@ import java.util.stream.Collectors; @RestControllerAdvice public class ExceptionController { - /** - * Handles business logic exceptions thrown throughout the application. - *

- * This method intercepts {@link BizException} instances and converts them into appropriate HTTP - * responses. The HTTP status code is determined by the exception's status property, whilst the - * error message is extracted from the exception and included in the response body. - *

- * The response includes a timestamp indicating when the error occurred and the specific error - * message describing the business logic violation. - * - * @param ex the business exception that was thrown - * @return a {@link ResponseEntity} containing the error response with appropriate HTTP status - * and {@link BizExceptionResponse} body - * @see BizException - * @see BizExceptionResponse - */ @ExceptionHandler(BizException.class) public ResponseEntity handleBizException(BizException ex) { return ResponseEntity.status(ex.getStatus()) @@ -50,25 +36,6 @@ public class ExceptionController { ); } - /** - * Handles bean validation constraint violation exceptions. - *

- * This method processes {@link ConstraintViolationException} instances that occur when bean - * validation constraints are violated during request processing. It extracts all constraint - * violations, formats them into a readable error message, and returns a standardised - * error response. - *

- * The error message includes the property path and violation message for each constraint that - * was violated, separated by commas for multiple violations. The response is automatically - * assigned a {@code 400 Bad Request} status. - * - * @param ex the constraint violation exception containing validation errors - * @return a {@link BizExceptionResponse} containing the formatted validation - * error messages and timestamp - * @see ConstraintViolationException - * @see BizExceptionResponse - * @see jakarta.validation.constraints - */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public BizExceptionResponse handleConstraintViolation(ConstraintViolationException ex) { @@ -81,4 +48,19 @@ public class ExceptionController { errorMessage ); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public BizExceptionResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + var errorMessage = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining("; ")); + + return new BizExceptionResponse( + LocalDateTime.now(), + errorMessage + ); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index dae750e..3523650 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -22,7 +22,7 @@ import java.util.Objects; */ @Entity @Table( - name = "user", + name = "\"user\"", uniqueConstraints = { @UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}), @UniqueConstraint(name = "uidx_users_email", columnNames = {"email"}), diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java new file mode 100644 index 0000000..7b31ee9 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.enumeration.Status; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AddDepartmentRequest( + @NotNull(message = "Name of the department should not be null") + @NotBlank(message = "Name of the department should not be null") + String name, + Long parentId, + Integer sort, + Status status +) { +} diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index 1c88c20..db9524e 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -8,6 +8,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component public class DepartmentManager { @@ -27,4 +29,12 @@ public class DepartmentManager { public Department selectById(Long id) { return departmentRepository.findById(id).orElse(null); } + + public Integer getNextSort(Long parentId) { + return Optional.ofNullable(departmentRepository.findMaxSort(parentId)).orElse(0) + 1; + } + + public Department save(Department department) { + return departmentRepository.save(department); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 80dcbc1..4c986b3 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package com.onixbyte.helix.manager; import com.onixbyte.helix.common.regex.Patterns; diff --git a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java index 70ceeab..79a1849 100644 --- a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java @@ -2,8 +2,17 @@ package com.onixbyte.helix.repository; import com.onixbyte.helix.domain.entity.Department; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface DepartmentRepository extends JpaRepository { + + @Query(""" + select max(d.sort) + from Department d + where (:parentId is null and d.parentId is null) + or d.parentId = :parentId + """) + Integer findMaxSort(Long parentId); } diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 0cae30a..41e5a8c 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -1,14 +1,19 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.common.TreeNode; +import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.manager.DepartmentManager; import com.onixbyte.helix.utils.TreeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Service public class DepartmentService { @@ -28,4 +33,22 @@ public class DepartmentService { public List getDepartments() { return departmentManager.selectAll(Pageable.unpaged()).getContent(); } + + @Transactional(rollbackFor = Throwable.class) + public Department addDepartment(AddDepartmentRequest request) { + var createdAt = LocalDateTime.now(); + + var parentId = request.parentId(); + var sort = Optional.ofNullable(request.sort()) + .orElseGet(() -> departmentManager.getNextSort(parentId)); + + return departmentManager.save(Department.builder() + .name(request.name()) + .parentId(parentId) + .sort(sort) + .status(Optional.ofNullable(request.status()).orElse(Status.ACTIVE)) + .createdAt(createdAt) + .updatedAt(createdAt) + .build()); + } } diff --git a/src/main/resources/db/migrations/V1__init_db.sql b/src/main/resources/db/migrations/V1__init_db.sql index 5073696..61213ee 100644 --- a/src/main/resources/db/migrations/V1__init_db.sql +++ b/src/main/resources/db/migrations/V1__init_db.sql @@ -409,4 +409,21 @@ FROM system_menu; INSERT INTO menu (name, parent_id, code, sort, path, is_external_link, is_visible, status, authority_code, icon, created_at, updated_at) VALUES ('Helix 官网', null, 'helix-official-site', 100, 'https://helix.onixbyte.com', true, true, - 'ACTIVE'::STATUS, null, null, NOW(), NOW()); \ No newline at end of file + 'ACTIVE'::STATUS, null, null, NOW(), NOW()); + +DO +$$ + DECLARE + v_user_id BIGINT; + v_now TIMESTAMP; + BEGIN + SELECT id INTO v_user_id FROM "user" WHERE username = 'helix'; + v_now := CURRENT_TIMESTAMP; + + RAISE NOTICE 'User ID: %, Timestamp: %', v_user_id, v_now; + + -- Default password is '123456' + INSERT INTO "user_credential" + VALUES (v_user_id, 'LOCAL'::credential_provider, '$2a$10$1LoatLVvHL3LFK0pnNXcM.eiPK6.UdA9cl9IDwanWHBAAILn1xe0K', v_now, v_now); + END; +$$; \ No newline at end of file