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);
}
}