从零搭建智能打卡系统(二):后端架构设计与实现

SailTrack
2026-01-07
点 赞
0
热 度
3
评 论
0
  1. 首页
  2. 学习
  3. 从零搭建智能打卡系统(二):后端架构设计与实现

上一篇文章我们完成了项目概述和技术选型,今天我想详细聊聊后端架构的设计与实现。说实话,后端开发是整个项目的核心,也是我们花费时间最多、踩坑最密集的部分。

从用户认证到考勤逻辑,从数据库设计到API接口,每一个环节都有值得分享的经验和教训。

一、实体类设计:数据库的"骨架"

1.1 用户实体设计

用户实体是整个系统的基础,我们设计了以下字段:

@Data
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 30)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    @Column(length = 50)
    private String realName;
    
    private Long departmentId;
    
    private Long roleId = 3L; // 默认普通员工
    
    private Integer status = 1; // 1-启用,0-禁用
    
    @Column(length = 500)
    private String faceImageUrl; // 人脸照片URL
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

设计思考:

  • 使用@Data注解简化getter/setter

  • 用户名和邮箱添加唯一约束

  • 角色ID默认值为3(普通员工)

  • 状态字段使用Integer而不是Boolean,便于扩展

  • 人脸照片存储URL而不是二进制数据

1.2 考勤记录实体

考勤记录实体比较复杂,包含了很多业务逻辑字段:

@Data
@Entity
@Table(name = "attendance_records")
public class AttendanceRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long userId;
    
    private LocalDate attendanceDate;
    
    private LocalDateTime checkInTime;
    
    private LocalDateTime checkOutTime;
    
    private LocalDateTime expectedCheckOutTime;
    
    private String checkInIp;
    
    private String checkOutIp;
    
    private BigDecimal workHours;
    
    private BigDecimal overtimeHours;
    
    private Integer status; // 1-正常,2-迟到,3-早退,4-缺卡
    
    private Boolean isLate;
    
    private Boolean isEarlyLeave;
    
    private Integer lateMinutes;
    
    private Integer earlyLeaveMinutes;
    
    private String remark;
    
    private Boolean isSupplement = false;
    
    private Integer supplementStatus;
    
    private String supplementReason;
    
    private Long approverId;
    
    private LocalDateTime approvalTime;
    
    private String approvalRemark;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

踩坑经验:

  1. 时间字段类型选择:开始使用Date,后来改为LocalDateTime,更符合Java 8的时间API

  2. 工作时长计算:使用BigDecimal而不是Double,避免精度问题

  3. 状态字段设计:使用Integer而不是枚举,便于数据库查询

1.3 其他实体类

// 请假记录实体
@Data
@Entity
@Table(name = "leave_records")
public class LeaveRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long userId;
    
    private String leaveType; // 事假、病假、年假
    
    private LocalDate startDate;
    
    private LocalDate endDate;
    
    private BigDecimal leaveDays;
    
    private String reason;
    
    private Integer status; // 0-待审批,1-已批准,2-已拒绝
    
    private Long approverId;
    
    private LocalDateTime approvalTime;
    
    private String approvalRemark;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

// 补卡申请实体
@Data
@Entity
@Table(name = "supplement_records")
public class SupplementRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long userId;
    
    private LocalDate targetDate;
    
    private Integer checkType; // 1-签到,2-签退
    
    private LocalDateTime checkTime;
    
    private String reason;
    
    private Integer status; // 0-待审批,1-已批准,2-已拒绝
    
    private Long approverId;
    
    private LocalDateTime approvalTime;
    
    private String approvalRemark;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

二、Repository层:数据访问的"桥梁"

2.1 基础Repository设计

我们使用Spring Data JPA的Repository接口,大大简化了数据访问代码:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
    
    // 根据部门查询用户
    List<User> findByDepartmentId(Long departmentId);
    
    // 根据状态查询用户
    List<User> findByStatus(Integer status);
}

public interface AttendanceRecordRepository extends JpaRepository<AttendanceRecord, Long> {
    // 查询用户当天的考勤记录
    Optional<AttendanceRecord> findByUserIdAndAttendanceDate(Long userId, LocalDate date);
    
    // 查询用户某个月的考勤记录
    List<AttendanceRecord> findByUserIdAndAttendanceDateBetween(
        Long userId, LocalDate startDate, LocalDate endDate);
    
    // 统计用户某个月的考勤情况
    @Query("SELECT COUNT(a) FROM AttendanceRecord a WHERE a.userId = :userId " +
           "AND a.attendanceDate BETWEEN :startDate AND :endDate " +
           "AND a.status = :status")
    Long countByUserIdAndDateRangeAndStatus(
        @Param("userId") Long userId,
        @Param("startDate") LocalDate startDate,
        @Param("endDate") LocalDate endDate,
        @Param("status") Integer status);
}

2.2 自定义查询方法

对于一些复杂的查询,我们使用@Query注解:

public interface LeaveRecordRepository extends JpaRepository<LeaveRecord, Long> {
    // 查询用户待审批的请假记录
    List<LeaveRecord> findByUserIdAndStatus(Long userId, Integer status);
    
    // 查询部门所有请假记录(部门经理使用)
    @Query("SELECT l FROM LeaveRecord l WHERE l.userId IN " +
           "(SELECT u.id FROM User u WHERE u.departmentId = :departmentId)")
    List<LeaveRecord> findByDepartmentId(@Param("departmentId") Long departmentId);
    
    // 统计用户某段时间的请假天数
    @Query("SELECT COALESCE(SUM(l.leaveDays), 0) FROM LeaveRecord l " +
           "WHERE l.userId = :userId AND l.status = 1 " +
           "AND l.startDate >= :startDate AND l.endDate <= :endDate")
    BigDecimal sumLeaveDaysByUserIdAndDateRange(
        @Param("userId") Long userId,
        @Param("startDate") LocalDate startDate,
        @Param("endDate") LocalDate endDate);
}

三、Service层:业务逻辑的"大脑"

3.1 用户认证服务

用户认证服务是整个系统的入口,我们实现了注册、登录、验证码等功能:

@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final CaptchaCache captchaCache;
    private final MailService mailService;
    private final JwtUtil jwtUtil;
    
    @Transactional
    public Long register(RegisterRequest request) {
        // 验证用户名唯一性
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new RuntimeException("用户名已存在");
        }
        
        // 验证邮箱唯一性
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new RuntimeException("邮箱已被注册");
        }
        
        // 验证验证码
        if (!captchaCache.verify(request.getEmail(), request.getCaptcha())) {
            throw new RuntimeException("验证码错误或已过期");
        }
        
        // 创建用户
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setEmail(request.getEmail());
        user.setRealName(request.getRealName());
        user.setDepartmentId(request.getDepartmentId());
        user.setRoleId(3L); // 默认普通员工
        user.setStatus(1); // 启用状态
        
        User savedUser = userRepository.save(user);
        return savedUser.getId();
    }
    
    public String login(LoginRequest request) {
        // 查询用户
        User user = userRepository.findByUsername(request.getUsername())
            .orElseThrow(() -> new RuntimeException("用户名或密码错误"));
        
        // 验证密码
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new RuntimeException("用户名或密码错误");
        }
        
        // 验证用户状态
        if (user.getStatus() == 0) {
            throw new RuntimeException("用户已被禁用");
        }
        
        // 生成JWT Token
        return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRoleId());
    }
    
    public void sendCaptcha(String email) {
        // 生成4位随机验证码
        String captcha = String.valueOf((int)(Math.random() * 9000) + 1000);
        
        // 保存到缓存(5分钟过期)
        captchaCache.save(email, captcha);
        
        // 发送邮件
        mailService.sendCaptchaEmail(email, captcha);
    }
}

3.2 考勤服务

考勤服务是系统的核心,实现了打卡、统计、补卡等功能:

@Service
@RequiredArgsConstructor
public class AttendanceService {
    private final AttendanceRecordRepository attendanceRecordRepository;
    private final UserRepository userRepository;
    
    @Transactional
    public AttendanceRecord checkIn(CheckInRequest request, Long userId) {
        LocalDate today = LocalDate.now();
        LocalDateTime now = LocalDateTime.now();
        
        // 检查是否已打卡
        Optional<AttendanceRecord> existingRecord = 
            attendanceRecordRepository.findByUserIdAndAttendanceDate(userId, today);
        
        if (existingRecord.isPresent()) {
            AttendanceRecord record = existingRecord.get();
            if (record.getCheckInTime() != null) {
                throw new RuntimeException("今日已签到");
            }
            
            // 更新签到时间
            record.setCheckInTime(now);
            record.setCheckInIp(request.getIp());
            
            // 计算是否迟到
            calculateLateStatus(record);
            
            return attendanceRecordRepository.save(record);
        } else {
            // 创建新的考勤记录
            AttendanceRecord record = new AttendanceRecord();
            record.setUserId(userId);
            record.setAttendanceDate(today);
            record.setCheckInTime(now);
            record.setCheckInIp(request.getIp());
            record.setStatus(1); // 默认正常
            
            // 计算是否迟到
            calculateLateStatus(record);
            
            // 设置预期签退时间(8小时后)
            record.setExpectedCheckOutTime(now.plusHours(8));
            
            return attendanceRecordRepository.save(record);
        }
    }
    
    @Transactional
    public AttendanceRecord checkOut(CheckInRequest request, Long userId) {
        LocalDate today = LocalDate.now();
        LocalDateTime now = LocalDateTime.now();
        
        // 查询今日考勤记录
        AttendanceRecord record = attendanceRecordRepository
            .findByUserIdAndAttendanceDate(userId, today)
            .orElseThrow(() -> new RuntimeException("请先签到"));
        
        if (record.getCheckOutTime() != null) {
            throw new RuntimeException("今日已签退");
        }
        
        // 更新签退时间
        record.setCheckOutTime(now);
        record.setCheckOutIp(request.getIp());
        
        // 计算工作时长
        calculateWorkHours(record);
        
        // 计算是否早退
        calculateEarlyLeaveStatus(record);
        
        // 计算加班时长
        calculateOvertimeHours(record);
        
        return attendanceRecordRepository.save(record);
    }
    
    private void calculateLateStatus(AttendanceRecord record) {
        LocalDateTime checkInTime = record.getCheckInTime();
        LocalTime lateThreshold = LocalTime.of(10, 0); // 10:00后算迟到
        
        if (checkInTime.toLocalTime().isAfter(lateThreshold)) {
            record.setIsLate(true);
            record.setStatus(2); // 迟到
            
            // 计算迟到分钟数
            long lateMinutes = Duration.between(lateThreshold, checkInTime.toLocalTime()).toMinutes();
            record.setLateMinutes((int) lateMinutes);
        } else {
            record.setIsLate(false);
        }
    }
    
    private void calculateWorkHours(AttendanceRecord record) {
        if (record.getCheckInTime() != null && record.getCheckOutTime() != null) {
            Duration duration = Duration.between(record.getCheckInTime(), record.getCheckOutTime());
            double hours = duration.toMinutes() / 60.0;
            record.setWorkHours(BigDecimal.valueOf(hours).setScale(2, RoundingMode.HALF_UP));
        }
    }
    
    private void calculateOvertimeHours(AttendanceRecord record) {
        if (record.getWorkHours() != null) {
            BigDecimal standardHours = BigDecimal.valueOf(8);
            if (record.getWorkHours().compareTo(standardHours) > 0) {
                BigDecimal overtime = record.getWorkHours().subtract(standardHours);
                record.setOvertimeHours(overtime.setScale(2, RoundingMode.HALF_UP));
            } else {
                record.setOvertimeHours(BigDecimal.ZERO);
            }
        }
    }
    
    public Map<String, Object> getMonthlyStats(Long userId, String month) {
        // 解析月份
        YearMonth yearMonth = YearMonth.parse(month);
        LocalDate startDate = yearMonth.atDay(1);
        LocalDate endDate = yearMonth.atEndOfMonth();
        
        // 查询该月的考勤记录
        List<AttendanceRecord> records = attendanceRecordRepository
            .findByUserIdAndAttendanceDateBetween(userId, startDate, endDate);
        
        // 统计计算
        long workDays = records.stream()
            .filter(r -> r.getCheckInTime() != null && r.getCheckOutTime() != null)
            .count();
        
        long lateCount = records.stream()
            .filter(r -> Boolean.TRUE.equals(r.getIsLate()))
            .count();
        
        long earlyLeaveCount = records.stream()
            .filter(r -> Boolean.TRUE.equals(r.getIsEarlyLeave()))
            .count();
        
        BigDecimal totalWorkHours = records.stream()
            .map(AttendanceRecord::getWorkHours)
            .filter(Objects::nonNull)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        BigDecimal totalOvertimeHours = records.stream()
            .map(AttendanceRecord::getOvertimeHours)
            .filter(Objects::nonNull)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 计算平均工时
        BigDecimal avgWorkHours = workDays > 0 
            ? totalWorkHours.divide(BigDecimal.valueOf(workDays), 2, RoundingMode.HALF_UP)
            : BigDecimal.ZERO;
        
        // 组装返回结果
        Map<String, Object> stats = new HashMap<>();
        stats.put("workDays", workDays);
        stats.put("lateCount", lateCount);
        stats.put("earlyLeaveCount", earlyLeaveCount);
        stats.put("totalWorkHours", totalWorkHours);
        stats.put("totalOvertimeHours", totalOvertimeHours);
        stats.put("avgWorkHours", avgWorkHours);
        stats.put("attendanceRate", workDays * 100.0 / endDate.getDayOfMonth());
        
        return stats;
    }
}

四、Controller层:API的"门面"

4.1 统一响应格式

我们设计了统一的响应格式,便于前端处理:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(RuntimeException.class)
    public Map<String, Object> handleRuntimeException(RuntimeException e) {
        Map<String, Object> response = new HashMap<>();
        response.put("ok", false);
        response.put("code", 400);
        response.put("message", e.getMessage());
        return response;
    }
    
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleException(Exception e) {
        Map<String, Object> response = new HashMap<>();
        response.put("ok", false);
        response.put("code", 500);
        response.put("message", "服务器内部错误");
        // 生产环境应该记录日志而不是返回详细错误信息
        return response;
    }
}

// 成功响应的工具方法
public class ResponseUtil {
    public static Map<String, Object> success(Object data) {
        Map<String, Object> response = new HashMap<>();
        response.put("ok", true);
        response.put("code", 200);
        response.put("data", data);
        response.put("message", "成功");
        return response;
    }
    
    public static Map<String, Object> success(String message) {
        Map<String, Object> response = new HashMap<>();
        response.put("ok", true);
        response.put("code", 200);
        response.put("message", message);
        return response;
    }
}

4.2 认证控制器

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;
    
    @PostMapping("/send-captcha")
    public Map<String, Object> sendCaptcha(@RequestParam String email) {
        authService.sendCaptcha(email);
        return ResponseUtil.success("验证码发送成功");
    }
    
    @PostMapping("/register")
    public Map<String, Object> register(@Valid @RequestBody RegisterRequest request) {
        Long userId = authService.register(request);
        return ResponseUtil.success(Map.of("userId", userId));
    }
    
    @PostMapping("/login")
    public Map<String, Object> login(@Valid @RequestBody LoginRequest request) {
        String token = authService.login(request);
        return ResponseUtil.success(Map.of("token", token));
    }
}

4.3 考勤控制器

@RestController
@RequestMapping("/api/attendance")
@RequiredArgsConstructor
public class AttendanceController {
    private final AttendanceService attendanceService;
    
    @PostMapping("/check")
    public Map<String, Object> checkIn(@Valid @RequestBody CheckInRequest request,
                                      @RequestAttribute Long userId) {
        AttendanceRecord record;
        
        if (request.getType() == 1) {
            // 签到
            record = attendanceService.checkIn(request, userId);
        } else if (request.getType() == 2) {
            // 签退
            record = attendanceService.checkOut(request, userId);
        } else {
            throw new RuntimeException("无效的打卡类型");
        }
        
        return ResponseUtil.success(record);
    }
    
    @GetMapping("/today")
    public Map<String, Object> getTodayAttendance(@RequestAttribute Long userId) {
        LocalDate today = LocalDate.now();
        Optional<AttendanceRecord> record = attendanceService
            .getAttendanceRecord(userId, today);
        
        return ResponseUtil.success(record.orElse(null));
    }
    
    @GetMapping("/monthly-stats")
    public Map<String, Object> getMonthlyStats(@RequestAttribute Long userId,
                                              @RequestParam String month) {
        Map<String, Object> stats = attendanceService.getMonthlyStats(userId, month);
        return ResponseUtil.success(stats);
    }
    
    @GetMapping("/recent-records")
    public Map<String, Object> getRecentRecords(@RequestAttribute Long userId,
                                               @RequestParam(defaultValue = "10") int limit) {
        List<AttendanceRecord> records = attendanceService
            .getRecentRecords(userId, limit);
        return ResponseUtil.success(records);
    }
}

五、安全配置:系统的"防护墙"

5.1 JWT工具类

@Component
public class JwtUtil {
    private final String SECRET_KEY = "sailTrack2025SecretKeyForJWTSigning";
    private final long EXPIRATION_TIME = 3600000; // 1小时
    
    public String generateToken(Long userId, String username, Long roleId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", username);
        claims.put("roleId", roleId);
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            .compact();
    }
    
    public Claims extractClaims(String token) {
        return Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody();
    }
    
    public boolean validateToken(String token) {
        try {
            extractClaims(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    public Long extractUserId(String token) {
        Claims claims = extractClaims(token);
        return claims.get("userId", Long.class);
    }
    
    public Long extractRoleId(String token) {
        Claims claims = extractClaims(token);
        return claims.get("roleId", Long.class);
    }
}

5.2 JWT拦截器

@Component
public class JwtInterceptor implements HandlerInterceptor {
    
    private final JwtUtil jwtUtil;
    
    public JwtInterceptor(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 放行公开接口
        String path = request.getRequestURI();
        if (path.startsWith("/api/auth/") || path.startsWith("/api/test/")) {
            return true;
        }
        
        // 获取Token
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new RuntimeException("未提供认证Token");
        }
        
        String token = authHeader.substring(7);
        
        // 验证Token
        if (!jwtUtil.validateToken(token)) {
            throw new RuntimeException("Token无效或已过期");
        }
        
        // 提取用户信息并设置到请求属性中
        Long userId = jwtUtil.extractUserId(token);
        Long roleId = jwtUtil.extractRoleId(token);
        
        request.setAttribute("userId", userId);
        request.setAttribute("roleId", roleId);
        
        return true;
    }
}

5.3 拦截器配置

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    private final JwtInterceptor jwtInterceptor;
    
    public WebConfig(JwtInterceptor jwtInterceptor) {
        this.jwtInterceptor = jwtInterceptor;
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/auth/**", "/api/test/**");
    }
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setAllowedOrigins(Arrays.asList(
            "http://127.0.0.1:5502",
            "http://localhost:3000"
        ));
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

六、测试与验证

6.1 单元测试

@SpringBootTest
class AuthServiceTest {
    
    @Autowired
    private AuthService authService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testRegister() {
        RegisterRequest request = new RegisterRequest();
        request.setUsername("testuser");
        request.setPassword("password123");
        request.setEmail("test@example.com");
        request.setCaptcha("1234");
        request.setRealName("测试用户");
        request.setDepartmentId(1L);
        
        // 模拟验证码验证
        // ...
        
        Long userId = authService.register(request);
        assertNotNull(userId);
        
        Optional<User> user = userRepository.findById(userId);
        assertTrue(user.isPresent());
        assertEquals("testuser", user.get().getUsername());
    }
    
    @Test
    void testLogin() {
        LoginRequest request = new LoginRequest();
        request.setUsername("testuser");
        request.setPassword("password123");
        
        String token = authService.login(request);
        assertNotNull(token);
        assertTrue(token.length() > 0);
    }
}

6.2 接口测试

使用Postman或curl测试接口:

# 发送验证码
curl -X POST "http://localhost:8080/api/auth/send-captcha?email=test@example.com"

# 注册用户
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "password123",
    "email": "test@example.com",
    "captcha": "1234",
    "realName": "测试用户",
    "departmentId": 1
  }'

# 用户登录
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "password123"
  }'

# 使用Token访问受保护接口
curl -X GET http://localhost:8080/api/attendance/today \
  -H "Authorization: Bearer <your_token>"

七、总结与反思

7.1 技术收获

通过后端开发,我学到了:

第一分层架构的重要性。清晰的Controller-Service-Repository分层让代码结构清晰,便于维护和测试。

第二异常处理的统一性。全局异常处理器让错误处理更加规范,前端可以统一处理错误响应。

第三JWT认证的实践。理解了无状态认证的原理和实现方式。

第四事务管理的关键性。使用@Transactional注解确保数据一致性。

7.2 踩坑经验

  1. 时间处理问题:Java 8的时间API比旧的Date类好用很多,但需要注意时区问题。

  2. BigDecimal精度问题:金融计算一定要用BigDecimal,避免浮点数精度问题。

  3. JWT密钥安全:生产环境一定要使用安全的密钥,不能硬编码在代码中。

  4. 跨域配置:前后端分离项目一定要配置CORS,否则前端无法访问API。

7.3 性能优化思考

虽然当前版本功能完整,但还有优化空间:

  1. 缓存优化:验证码缓存可以使用Redis替代内存缓存。

  2. 数据库索引:为常用查询字段添加索引,提高查询性能。

  3. 连接池配置:优化数据库连接池参数。

  4. 异步处理:邮件发送等耗时操作可以异步执行。

八、下一步计划

后端核心功能已经完成,但还有几个重要模块需要实现:

  1. 人脸识别服务:集成阿里云人脸识别API

  2. 邮件服务优化:支持HTML模板邮件

  3. 文件上传功能:支持人脸照片上传到OSS

  4. 数据导出功能:导出考勤报表为Excel

在下一篇文章中,我将介绍前端界面的开发过程,包括:

  • Vue.js 3.x项目搭建

  • Element Plus组件使用

  • 前后端API对接

  • 数据可视化图表实现

说实话,前端开发也有很多值得分享的经验,特别是Vue 3的组合式API和响应式系统,让代码组织更加灵活。


技术如人生,后端开发就像搭建房子的地基,虽然不直接面对用户,但决定了整个系统的稳定性和扩展性。每一个实体类、每一个Service方法、每一个API接口,都需要仔细设计和测试。

如果你也在学习Spring Boot开发,希望这篇文章能给你一些启发。代码虽然重要,但架构设计和开发思想更重要。


让我们忠于理想,让我们面对显示

SailTrack

entp 辩论家

站长

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

文章目录

欢迎来到SailTrack的站点,为您导航全站动态

21 文章数
8 分类数
1 评论数
11标签数