feat: add department creation endpoint and request validation
This commit is contained in:
@@ -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<Department> getDepartments() {
|
||||
return departmentService.getDepartments();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Department addDepartment(@Validated @RequestBody AddDepartmentRequest request) {
|
||||
return departmentService.addDepartment(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<BizExceptionResponse> handleBizException(BizException ex) {
|
||||
return ResponseEntity.status(ex.getStatus())
|
||||
@@ -50,25 +36,6 @@ public class ExceptionController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles bean validation constraint violation exceptions.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}),
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Department, Long> {
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -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<Department> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
'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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user