Spring Boot-Based Construction Project Management System: Architecture and Implementation

This system addresses modern construction project oversight needs by replacing error-prone manual and experience-driven processes with a structured, role-aware digital platform. It supports multi-tiered project hierarchies—such as master projects, sub-projects, developer entities, and timeline tracking—while enforcing granular access control and audit-ready operations.

Core Technology Stack

  • Front end: Vue 3 with Element Plus for responsive UI components; Axios for RESTful API communication.
  • Backend: Spring Boot 2.7+, MyBatis Plus (instead of raw MyBatis) for streamlined data access; Spring Security for authentication and RBAC enforcement.
  • Persistence: MySQL 8.0 with InnoDB engine, optimized for referential integrity and concurrent transaction handling.
  • Version Control: Git-based repository hosted on Gitee with feature-branch workflow and CI-ready hooks.

Data base Schema Highlights

Key tables include project, sub_project, developer, user_role, and permission_resource. Relationships follow strict foreign key constraints: a project may contain multiple sub_project entries via project_id, while developer is linked to project through an associative table project_developer. Soft-delete patterns (status = 1/0) are applied unifromly across domain entities.

Authentication & Authorization Flow

Spring Security is configured to delegate credential validation to a custom UserDetailsServiceImpl, which loads user roles and permissions from the database. Passwords are hashed using BCrypt with adaptive strength.

@Configuration
@EnableWebSecurity
public class AuthSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login.html", "/api/auth/login", "/api/public/**").permitAll()
                .requestMatchers("/js/**", "/css/**", "/images/**", "/favicon.ico").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/project/**").hasAnyRole("ADMIN", "PROJECT_MANAGER")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login.html")
                .loginProcessingUrl("/api/auth/login")
                .successHandler(new CustomAuthSuccessHandler())
                .failureUrl("/login.html?error=true")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/api/auth/logout")
                .logoutSuccessUrl("/login.html")
                .invalidateHttpSession(true)
            )
            .headers(headers -> headers.frameOptions().sameOrigin());
        return http.build();
    }
}

Domain Layer Abstraction

The ProjectStageMapper interface replaces direct SQL embedding with type-safe MyBatis Plus wrappers and dynamic query support:

@Mapper
public interface ProjectStageMapper extends BaseMapper<ProjectStage> {

    @Select("SELECT * FROM project_stage WHERE project_id = #{projectId} AND status = 1")
    List<ProjectStage> selectActiveByProjectId(@Param("projectId") String projectId);

    @Select("SELECT COUNT(*) FROM project_stage WHERE status = 1 " +
            "<if test='keyword != null and keyword != ""'>" +
            "AND (stage_name LIKE CONCAT('%', #{keyword}, '%') OR stage_id = #{keyword})" +
            "</if>")
    long countByKeyword(@Param("keyword") String keyword);

    @Select("SELECT * FROM project_stage WHERE status = 1 " +
            "<if test='keyword != null and keyword != ""'>" +
            "AND (stage_name LIKE CONCAT('%', #{keyword}, '%') OR stage_id = #{keyword})" +
            "</if>" +
            "LIMIT #{offset}, #{limit}")
    List<ProjectStage> selectPaged(
        @Param("offset") int offset,
        @Param("limit") int limit,
        @Param("keyword") String keyword
    );

    @Update("UPDATE project_stage SET status = 0 WHERE stage_id = #{id}")
    int softDeleteById(@Param("id") String id);
}

Service Layer with Transactional Boundaries

The ProjectStageService encapsulates business logic—including UUID generation, timestamp injection, and cascade-aware updates—within declarative transactions:

@Service
public class ProjectStageServiceImpl implements ProjectStageService {

    @Autowired
    private ProjectStageMapper stageMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<ProjectStage> createStage(ProjectStage stage) {
        stage.setStageId(UUID.randomUUID().toString().replace("-", ""));
        stage.setStatus(1);
        stage.setCreatedAt(LocalDateTime.now());
        stage.setUpdatedAt(LocalDateTime.now());
        int rows = stageMapper.insert(stage);
        return rows > 0 ? Result.success(stage) : Result.failure("Creation failed");
    }

    @Override
    public PageResult<ProjectStage> searchStages(int page, int size, String keyword) {
        int offset = (page - 1) * size;
        List<ProjectStage> list = stageMapper.selectPaged(offset, size, keyword);
        long total = stageMapper.countByKeyword(keyword);
        return new PageResult<>(list, total, page, size);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<Void> updateStage(ProjectStage stage) {
        stage.setUpdatedAt(LocalDateTime.now());
        int rows = stageMapper.updateById(stage);
        return rows > 0 ? Result.success() : Result.failure("Update failed");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<Void> removeStage(String id) {
        int rows = stageMapper.softDeleteById(id);
        return rows > 0 ? Result.success() : Result.failure("Deletion failed");
    }
}

REST Controller with Consistent Response Contract

The ProjectStageController exposes standardized endpoints using Spring’s @RestController and follows a uniform response envelope (Result<T>):

@RestController
@RequestMapping("/api/stage")
public class ProjectStageController {

    @Autowired
    private ProjectStageService stageService;

    @PostMapping("/search")
    public Result<PageResult<ProjectStage>> search(
            @RequestBody StageQueryParams params) {
        PageResult<ProjectStage> result = stageService.searchStages(
            params.getPage(), params.getSize(), params.getKeyword());
        return Result.success(result);
    }

    @PostMapping
    public Result<ProjectStage> add(@RequestBody ProjectStage stage) {
        return stageService.createStage(stage);
    }

    @GetMapping("/{id}")
    public Result<ProjectStage> get(@PathVariable String id) {
        ProjectStage stage = stageService.findById(id);
        return stage != null ? Result.success(stage) : Result.failure("Not found");
    }

    @PutMapping
    public Result<Void> modify(@RequestBody ProjectStage stage) {
        return stageService.updateStage(stage);
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable String id) {
        return stageService.removeStage(id);
    }
}

Tags: spring-boot mybatis-plus vue MySQL spring-security

Posted on Thu, 07 May 2026 11:38:25 +0000 by elearnindia