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.entity.Department;
|
||||||
import com.onixbyte.helix.domain.common.TreeNode;
|
import com.onixbyte.helix.domain.common.TreeNode;
|
||||||
|
import com.onixbyte.helix.domain.web.request.AddDepartmentRequest;
|
||||||
import com.onixbyte.helix.service.DepartmentService;
|
import com.onixbyte.helix.service.DepartmentService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -36,4 +36,9 @@ public class DepartmentController {
|
|||||||
public List<Department> getDepartments() {
|
public List<Department> getDepartments() {
|
||||||
return departmentService.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.domain.web.response.BizExceptionResponse;
|
||||||
import com.onixbyte.helix.exception.BizException;
|
import com.onixbyte.helix.exception.BizException;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
@@ -25,22 +27,6 @@ import java.util.stream.Collectors;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class ExceptionController {
|
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)
|
@ExceptionHandler(BizException.class)
|
||||||
public ResponseEntity<BizExceptionResponse> handleBizException(BizException ex) {
|
public ResponseEntity<BizExceptionResponse> handleBizException(BizException ex) {
|
||||||
return ResponseEntity.status(ex.getStatus())
|
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)
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public BizExceptionResponse handleConstraintViolation(ConstraintViolationException ex) {
|
public BizExceptionResponse handleConstraintViolation(ConstraintViolationException ex) {
|
||||||
@@ -81,4 +48,19 @@ public class ExceptionController {
|
|||||||
errorMessage
|
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
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "user",
|
name = "\"user\"",
|
||||||
uniqueConstraints = {
|
uniqueConstraints = {
|
||||||
@UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}),
|
@UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}),
|
||||||
@UniqueConstraint(name = "uidx_users_email", columnNames = {"email"}),
|
@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.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DepartmentManager {
|
public class DepartmentManager {
|
||||||
|
|
||||||
@@ -27,4 +29,12 @@ public class DepartmentManager {
|
|||||||
public Department selectById(Long id) {
|
public Department selectById(Long id) {
|
||||||
return departmentRepository.findById(id).orElse(null);
|
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;
|
package com.onixbyte.helix.manager;
|
||||||
|
|
||||||
import com.onixbyte.helix.common.regex.Patterns;
|
import com.onixbyte.helix.common.regex.Patterns;
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ package com.onixbyte.helix.repository;
|
|||||||
|
|
||||||
import com.onixbyte.helix.domain.entity.Department;
|
import com.onixbyte.helix.domain.entity.Department;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface DepartmentRepository extends JpaRepository<Department, Long> {
|
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;
|
package com.onixbyte.helix.service;
|
||||||
|
|
||||||
import com.onixbyte.helix.domain.entity.Department;
|
|
||||||
import com.onixbyte.helix.domain.common.TreeNode;
|
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.manager.DepartmentManager;
|
||||||
import com.onixbyte.helix.utils.TreeUtil;
|
import com.onixbyte.helix.utils.TreeUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DepartmentService {
|
public class DepartmentService {
|
||||||
@@ -28,4 +33,22 @@ public class DepartmentService {
|
|||||||
public List<Department> getDepartments() {
|
public List<Department> getDepartments() {
|
||||||
return departmentManager.selectAll(Pageable.unpaged()).getContent();
|
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,
|
INSERT INTO menu (name, parent_id, code, sort, path, is_external_link, is_visible, status,
|
||||||
authority_code, icon, created_at, updated_at)
|
authority_code, icon, created_at, updated_at)
|
||||||
VALUES ('Helix 官网', null, 'helix-official-site', 100, 'https://helix.onixbyte.com', true, true,
|
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