返回 筑基・Web 道途启关

18我的第一个员工管理系统,差点让我丢了工作

博主
大约 20 分钟

18我的第一个员工管理系统,差点让我丢了工作

"王总,为什么工资单上显示发了两份工资?" 财务的电话让我瞬间冷汗直流。我颤抖着打开数据库,看到同一条发薪记录被插入了两次。那一刻我才明白:会写CRUD不等于懂系统开发。

一、"这有什么难的?不就是增删改查"

1.1 我的"天才"设计

老板让我开发员工管理系统,我自信满满:"不就是几张表吗?三天搞定!"

我的"架构":

  • 一个Controller搞定所有EmployeeController 里塞了20个方法
  • Service?不需要:业务逻辑全写在Controller里
  • 直接操作数据库:在Controller里写SQL,多方便啊!
  • image-20260202132722773

java

// 我的"杰作" - 一锅炖Controller
@RestController
public class EmployeeController {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 查询员工
    @GetMapping("/emp/list")
    public List<Map<String, Object>> list() {
        return jdbcTemplate.queryForList("SELECT * FROM emp");
    }
    
    // 添加员工
    @PostMapping("/emp/add")
    public String add(@RequestParam String name,
                      @RequestParam Integer age,
                      @RequestParam Integer gender,
                      @RequestParam Long deptId) {
        // 直接拼接SQL - SQL注入在等着我
        String sql = String.format("INSERT INTO emp(name, age, gender, dept_id) VALUES ('%s', %d, %d, %d)", 
                                  name, age, gender, deptId);
        jdbcTemplate.execute(sql);
        return "success";
    }
    
    // 更新员工
    // 删除员工
    // 查询部门
    // ... 还有15个方法
}

1.2 灾难现场

第一周,系统"顺利"上线。然后问题接踵而至:

  1. 性能问题:员工表到1000条时,列表查询要5秒
  2. 数据错乱:两个人同时修改同一条记录,后提交的覆盖了先提交的
  3. 工资多发:财务点击"发工资"按钮,网络卡顿多点了几下,发了3次工资
  4. 无法维护:新同事接手代码,看了三天没看懂业务逻辑在哪

最严重的是发工资事件:

java

// 我写的发工资逻辑
@PostMapping("/salary/pay")
public String paySalary(@RequestParam Long empId, 
                       @RequestParam BigDecimal amount) {
    // 1. 扣公司账户
    jdbcTemplate.update("UPDATE company_account SET balance = balance - ? WHERE id = 1", amount);
    
    // 网络延迟,财务又点了一次...
    
    // 2. 加员工账户
    jdbcTemplate.update("UPDATE emp_account SET balance = balance + ? WHERE emp_id = ?", amount, empId);
    
    // 3. 记录日志
    jdbcTemplate.update("INSERT INTO salary_log(emp_id, amount) VALUES(?, ?)", empId, amount);
    
    return "发薪成功";
}

问题:如果第2步失败,公司账户钱扣了,员工没收到,日志也没记录。更糟的是,如果财务快速点两次,会发两次工资。

二、分层架构:从"一锅炖"到"精装修"

image-20260202132822118

2.1 导师的"手术刀"

导师看着我的代码,叹了口气:"你这是把厨房、餐厅、卧室都放在一个房间啊。"

他给我画了张图:

text

以前的我:
┌─────────────────────────┐
│   Controller(所有事情)  │
│   • 接收请求            │
│   • 验证参数            │
│   • 写SQL              │
│   • 处理业务逻辑        │
│   • 返回响应            │
└─────────────────────────┘

正确的分层:
┌─────────────────────────┐
│   Controller           │ ← 只负责:接收、验证、返回
├─────────────────────────┤
│   Service              │ ← 业务逻辑的家
├─────────────────────────┤
│   Mapper/Repository    │ ← 数据库操作专家
├─────────────────────────┤
│   Entity               │ ← 数据模型
├─────────────────────────┤
│   DTO                  │ ← 前后端数据传输员
└─────────────────────────┘

2.2 重构之路

第一步:设计数据库

image-20260202133157874

以前我的表设计:

sql

-- 员工表(一堆问题)
CREATE TABLE employee (
    id INT,
    name VARCHAR(20),
    age INT,
    dept VARCHAR(50),  -- 部门名直接存,冗余且易错
    salary DECIMAL(10,0),  -- 工资精度丢失
    join_date DATETIME,
    update_time TIMESTAMP
);

导师教我的规范设计:

sql

-- 部门表(先有部门,再有员工)
CREATE TABLE dept (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '部门ID',
    name VARCHAR(50) NOT NULL COMMENT '部门名称',
    leader VARCHAR(50) COMMENT '部门负责人',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标志',
    
    UNIQUE KEY uk_name (name),  -- 部门名唯一
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';

-- 员工表(规范设计)
CREATE TABLE emp (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '员工ID',
    emp_no VARCHAR(20) NOT NULL COMMENT '员工编号(唯一)',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    gender TINYINT NOT NULL DEFAULT 1 COMMENT '性别:1-男,2-女',
    age TINYINT UNSIGNED COMMENT '年龄',
    id_card VARCHAR(18) COMMENT '身份证号',
    phone VARCHAR(20) COMMENT '手机号',
    email VARCHAR(100) COMMENT '邮箱',
    
    -- 关联部门
    dept_id BIGINT UNSIGNED NOT NULL COMMENT '部门ID',
    
    -- 工作信息
    position VARCHAR(50) COMMENT '职位',
    job_level TINYINT COMMENT '职级',
    salary DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '月薪',
    entry_date DATE NOT NULL COMMENT '入职日期',
    
    -- 状态管理
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-在职,2-离职,3-休假',
    
    -- 系统字段
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    create_by BIGINT UNSIGNED COMMENT '创建人',
    update_by BIGINT UNSIGNED COMMENT '更新人',
    is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-否,1-是',
    
    -- 约束和索引
    UNIQUE KEY uk_emp_no (emp_no),
    UNIQUE KEY uk_id_card (id_card),
    UNIQUE KEY uk_phone (phone),
    INDEX idx_dept_id (dept_id),
    INDEX idx_status (status),
    INDEX idx_entry_date (entry_date),
    INDEX idx_create_time (create_time),
    
    -- 外键约束(生产环境谨慎使用)
    FOREIGN KEY (dept_id) REFERENCES dept(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';

第二步:创建实体层

java

// 员工实体类
@Data
@TableName("emp")
public class Employee {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String empNo;      // 员工编号
    private String name;       // 姓名
    private Integer gender;    // 性别
    private Integer age;       // 年龄
    private String idCard;     // 身份证
    private String phone;      // 手机号
    
    private Long deptId;       // 部门ID
    
    private String position;   // 职位
    private Integer jobLevel;  // 职级
    private BigDecimal salary; // 月薪
    private LocalDate entryDate; // 入职日期
    
    private Integer status;    // 状态
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    private Long createBy;
    private Long updateBy;
    
    @TableLogic
    private Integer isDeleted;
}

// 部门实体类
@Data
@TableName("dept")
public class Department {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String name;      // 部门名称
    private String leader;    // 负责人
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    @TableLogic
    private Integer isDeleted;
}

第三步:DTO数据传输对象

为什么需要DTO?导师的比喻:"你不能把整个厨房端给客人,只需要上菜。"

java

// 用于新增员工的DTO
@Data
public class EmployeeAddDTO {
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 50, message = "姓名长度2-50字符")
    private String name;
    
    @NotNull(message = "性别不能为空")
    @Range(min = 1, max = 2, message = "性别值不正确")
    private Integer gender;
    
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 65, message = "年龄不能超过65岁")
    private Integer age;
    
    @NotBlank(message = "身份证号不能为空")
    @Pattern(regexp = "^\\d{17}[0-9Xx]$", message = "身份证格式不正确")
    private String idCard;
    
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @NotNull(message = "部门不能为空")
    private Long deptId;
    
    @NotNull(message = "入职日期不能为空")
    @PastOrPresent(message = "入职日期不能是未来")
    private LocalDate entryDate;
    
    @NotNull(message = "薪资不能为空")
    @DecimalMin(value = "0.00", message = "薪资不能为负数")
    private BigDecimal salary;
}

// 用于查询的DTO
@Data
public class EmployeeQueryDTO {
    private String name;           // 姓名模糊查询
    private Integer gender;        // 性别精确查询
    private Long deptId;           // 部门查询
    private Integer minAge;        // 最小年龄
    private Integer maxAge;        // 最大年龄
    private Integer status = 1;    // 默认查询在职员工
    
    @NotNull(message = "页码不能为空")
    @Min(value = 1, message = "页码最小为1")
    private Integer pageNum = 1;
    
    @NotNull(message = "每页条数不能为空")
    @Range(min = 1, max = 100, message = "每页1-100条")
    private Integer pageSize = 10;
    
    private String sortField = "create_time";  // 排序字段
    private String sortOrder = "desc";         // 排序方式
}

第四步:Mapper持久层

java

// EmployeeMapper.java
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
    // Mybatis-Plus已经提供了基础的CRUD方法
    
    // 分页查询(使用Mybatis-Plus的分页插件)
    IPage<Employee> selectPage(Page<Employee> page, 
                               @Param("query") EmployeeQueryDTO query);
    
    // 复杂的统计查询
    List<Map<String, Object>> selectDeptEmployeeCount();
    
    // 批量更新状态
    int batchUpdateStatus(@Param("ids") List<Long> ids, 
                         @Param("status") Integer status);
}

// EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.tlias.mapper.EmployeeMapper">
    
    <!-- 自定义分页查询 -->
    <select id="selectPage" resultType="com.tlias.entity.Employee">
        SELECT e.*, d.name as dept_name
        FROM emp e
        LEFT JOIN dept d ON e.dept_id = d.id AND d.is_deleted = 0
        <where>
            e.is_deleted = 0
            <if test="query.name != null and query.name != ''">
                AND e.name LIKE CONCAT('%', #{query.name}, '%')
            </if>
            <if test="query.gender != null">
                AND e.gender = #{query.gender}
            </if>
            <if test="query.deptId != null">
                AND e.dept_id = #{query.deptId}
            </if>
            <if test="query.minAge != null">
                AND e.age >= #{query.minAge}
            </if>
            <if test="query.maxAge != null">
                AND e.age &lt;= #{query.maxAge}
            </if>
            <if test="query.status != null">
                AND e.status = #{query.status}
            </if>
        </where>
        ORDER BY 
        <choose>
            <when test="query.sortField != null and query.sortField != ''">
                ${query.sortField} ${query.sortOrder}
            </when>
            <otherwise>
                e.create_time DESC
            </otherwise>
        </choose>
    </select>
    
    <!-- 部门员工统计 -->
    <select id="selectDeptEmployeeCount" resultType="map">
        SELECT 
            d.name as dept_name,
            COUNT(e.id) as employee_count,
            AVG(e.salary) as avg_salary,
            MAX(e.salary) as max_salary,
            MIN(e.salary) as min_salary
        FROM dept d
        LEFT JOIN emp e ON d.id = e.dept_id AND e.is_deleted = 0 AND e.status = 1
        WHERE d.is_deleted = 0
        GROUP BY d.id, d.name
        ORDER BY employee_count DESC
    </select>
    
    <!-- 批量更新 -->
    <update id="batchUpdateStatus">
        UPDATE emp 
        SET status = #{status}, update_time = NOW()
        WHERE id IN 
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        AND is_deleted = 0
    </update>
</mapper>

第五步:Service业务层

image-20260202133623789

java

// EmployeeService接口
public interface EmployeeService {
    
    /**
     * 添加员工(事务控制)
     */
    void addEmployee(EmployeeAddDTO dto);
    
    /**
     * 更新员工信息
     */
    void updateEmployee(Long id, EmployeeUpdateDTO dto);
    
    /**
     * 删除员工(逻辑删除)
     */
    void deleteEmployee(Long id);
    
    /**
     * 批量删除
     */
    void batchDelete(List<Long> ids);
    
    /**
     * 分页查询
     */
    PageResult<EmployeeVO> queryPage(EmployeeQueryDTO query);
    
    /**
     * 获取员工详情
     */
    EmployeeDetailVO getDetail(Long id);
    
    /**
     * 发工资(重点:事务控制)
     */
    void paySalary(PaySalaryDTO dto);
    
    /**
     * 部门员工统计
     */
    List<DeptStatVO> getDeptStatistics();
}

// EmployeeServiceImpl实现类
@Service
@Slf4j
public class EmployeeServiceImpl implements EmployeeService {
    
    @Autowired
    private EmployeeMapper employeeMapper;
    
    @Autowired
    private DepartmentMapper departmentMapper;
    
    @Autowired
    private SalaryRecordMapper salaryRecordMapper;
    
    @Autowired
    private IdGenerator idGenerator;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addEmployee(EmployeeAddDTO dto) {
        log.info("开始添加员工: {}", dto.getName());
        
        // 1. 参数校验(JSR303已经做了基础校验,这里做业务校验)
        // 检查部门是否存在
        Department dept = departmentMapper.selectById(dto.getDeptId());
        if (dept == null || dept.getIsDeleted() == 1) {
            throw new BusinessException("部门不存在");
        }
        
        // 检查员工编号是否重复
        String empNo = idGenerator.generateEmpNo();
        Long count = employeeMapper.selectCount(
            new QueryWrapper<Employee>()
                .eq("emp_no", empNo)
                .eq("is_deleted", 0)
        );
        if (count > 0) {
            throw new BusinessException("员工编号重复,请重试");
        }
        
        // 2. DTO转Entity
        Employee employee = new Employee();
        BeanUtils.copyProperties(dto, employee);
        employee.setEmpNo(empNo);
        employee.setStatus(1); // 默认在职
        
        // 3. 保存到数据库
        int result = employeeMapper.insert(employee);
        if (result != 1) {
            log.error("添加员工失败: {}", dto);
            throw new BusinessException("添加员工失败");
        }
        
        log.info("员工添加成功: {} (ID: {})", employee.getName(), employee.getId());
    }
    
    @Override
    @Transactional(readOnly = true)
    public PageResult<EmployeeVO> queryPage(EmployeeQueryDTO query) {
        log.debug("查询员工列表,条件: {}", query);
        
        // 1. 创建分页对象
        Page<Employee> page = new Page<>(query.getPageNum(), query.getPageSize());
        
        // 2. 查询数据
        IPage<Employee> result = employeeMapper.selectPage(page, query);
        
        // 3. 转换为VO(View Object,用于前端展示)
        List<EmployeeVO> voList = result.getRecords().stream()
            .map(this::convertToVO)
            .collect(Collectors.toList());
        
        // 4. 封装分页结果
        return PageResult.<EmployeeVO>builder()
            .list(voList)
            .total(result.getTotal())
            .pageNum(query.getPageNum())
            .pageSize(query.getPageSize())
            .totalPage(result.getPages())
            .build();
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
    public void paySalary(PaySalaryDTO dto) {
        log.info("开始发工资,员工ID: {}, 金额: {}", dto.getEmpId(), dto.getAmount());
        
        // 1. 查询员工信息(加锁,防止并发)
        Employee employee = employeeMapper.selectForUpdate(dto.getEmpId());
        if (employee == null || employee.getIsDeleted() == 1) {
            throw new BusinessException("员工不存在");
        }
        
        if (employee.getStatus() != 1) {
            throw new BusinessException("员工不在职,无法发薪");
        }
        
        // 2. 生成工资单号(防止重复发薪)
        String salaryNo = "SAL" + System.currentTimeMillis() + employee.getId();
        Long existCount = salaryRecordMapper.selectCount(
            new QueryWrapper<SalaryRecord>()
                .eq("salary_no", salaryNo)
        );
        if (existCount > 0) {
            throw new BusinessException("工资单号重复,请重试");
        }
        
        // 3. 创建工资记录
        SalaryRecord record = SalaryRecord.builder()
            .salaryNo(salaryNo)
            .empId(dto.getEmpId())
            .amount(dto.getAmount())
            .payMonth(dto.getPayMonth())
            .status(1) // 已发放
            .paymentTime(LocalDateTime.now())
            .build();
        
        salaryRecordMapper.insert(record);
        
        // 4. 更新员工账户余额
        employeeMapper.updateSalaryBalance(dto.getEmpId(), dto.getAmount());
        
        // 5. 发送通知(异步,不影响主事务)
        sendSalaryNotification(employee, dto.getAmount());
        
        log.info("工资发放成功,单号: {}", salaryNo);
    }
    
    // 发送通知(异步方法)
    @Async
    public void sendSalaryNotification(Employee employee, BigDecimal amount) {
        try {
            // 发送邮件
            emailService.sendSalaryNotice(employee.getEmail(), amount);
            // 发送短信
            smsService.sendSalaryNotice(employee.getPhone(), amount);
        } catch (Exception e) {
            log.error("发送工资通知失败: {}", e.getMessage());
            // 通知失败不影响主业务
        }
    }
    
    private EmployeeVO convertToVO(Employee employee) {
        EmployeeVO vo = new EmployeeVO();
        BeanUtils.copyProperties(employee, vo);
        
        // 查询部门名称
        Department dept = departmentMapper.selectById(employee.getDeptId());
        if (dept != null) {
            vo.setDeptName(dept.getName());
        }
        
        // 格式化日期
        if (employee.getEntryDate() != null) {
            vo.setEntryDateStr(employee.getEntryDate().format(DateTimeFormatter.ISO_DATE));
        }
        
        return vo;
    }
}

第六步:Controller控制层

java

@RestController
@RequestMapping("/api/employees")
@Slf4j
@Validated
public class EmployeeController {
    
    @Autowired
    private EmployeeService employeeService;
    
    /**
     * 添加员工
     */
    @PostMapping
    public Result<Void> addEmployee(@Valid @RequestBody EmployeeAddDTO dto) {
        log.info("收到添加员工请求: {}", dto);
        
        try {
            employeeService.addEmployee(dto);
            return Result.success("添加成功");
        } catch (BusinessException e) {
            log.warn("业务异常: {}", e.getMessage());
            return Result.error(400, e.getMessage());
        } catch (Exception e) {
            log.error("系统异常: ", e);
            return Result.error(500, "系统异常,请稍后重试");
        }
    }
    
    /**
     * 分页查询员工
     */
    @GetMapping
    public Result<PageResult<EmployeeVO>> queryEmployees(@Valid EmployeeQueryDTO query) {
        log.debug("查询员工列表,参数: {}", query);
        
        try {
            PageResult<EmployeeVO> result = employeeService.queryPage(query);
            return Result.success(result);
        } catch (Exception e) {
            log.error("查询失败: ", e);
            return Result.error(500, "查询失败");
        }
    }
    
    /**
     * 获取员工详情
     */
    @GetMapping("/{id}")
    public Result<EmployeeDetailVO> getEmployee(@PathVariable Long id) {
        log.debug("查询员工详情,ID: {}", id);
        
        if (id == null || id <= 0) {
            return Result.error(400, "ID参数错误");
        }
        
        try {
            EmployeeDetailVO detail = employeeService.getDetail(id);
            if (detail == null) {
                return Result.error(404, "员工不存在");
            }
            return Result.success(detail);
        } catch (Exception e) {
            log.error("查询详情失败: ", e);
            return Result.error(500, "查询失败");
        }
    }
    
    /**
     * 更新员工
     */
    @PutMapping("/{id}")
    public Result<Void> updateEmployee(@PathVariable Long id, 
                                       @Valid @RequestBody EmployeeUpdateDTO dto) {
        log.info("更新员工,ID: {}, 数据: {}", id, dto);
        
        try {
            employeeService.updateEmployee(id, dto);
            return Result.success("更新成功");
        } catch (BusinessException e) {
            return Result.error(400, e.getMessage());
        } catch (Exception e) {
            log.error("更新失败: ", e);
            return Result.error(500, "更新失败");
        }
    }
    
    /**
     * 删除员工
     */
    @DeleteMapping("/{id}")
    public Result<Void> deleteEmployee(@PathVariable Long id) {
        log.info("删除员工,ID: {}", id);
        
        try {
            employeeService.deleteEmployee(id);
            return Result.success("删除成功");
        } catch (BusinessException e) {
            return Result.error(400, e.getMessage());
        } catch (Exception e) {
            log.error("删除失败: ", e);
            return Result.error(500, "删除失败");
        }
    }
    
    /**
     * 批量删除
     */
    @DeleteMapping("/batch")
    public Result<Void> batchDelete(@RequestBody List<Long> ids) {
        log.info("批量删除员工,IDs: {}", ids);
        
        if (ids == null || ids.isEmpty()) {
            return Result.error(400, "请选择要删除的员工");
        }
        
        if (ids.size() > 100) {
            return Result.error(400, "单次最多删除100条记录");
        }
        
        try {
            employeeService.batchDelete(ids);
            return Result.success("批量删除成功");
        } catch (BusinessException e) {
            return Result.error(400, e.getMessage());
        } catch (Exception e) {
            log.error("批量删除失败: ", e);
            return Result.error(500, "批量删除失败");
        }
    }
    
    /**
     * 发工资
     */
    @PostMapping("/pay-salary")
    public Result<Void> paySalary(@Valid @RequestBody PaySalaryDTO dto) {
        log.info("发工资请求: {}", dto);
        
        try {
            employeeService.paySalary(dto);
            return Result.success("发薪成功");
        } catch (BusinessException e) {
            return Result.error(400, e.getMessage());
        } catch (Exception e) {
            log.error("发薪失败: ", e);
            return Result.error(500, "发薪失败");
        }
    }
}

三、统一响应和异常处理

3.1 统一响应格式

java

// Result.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private Integer code;    // 状态码
    private String message;  // 消息
    private T data;          // 数据
    private Long timestamp;  // 时间戳
    
    public static <T> Result<T> success(T data) {
        return Result.<T>builder()
            .code(200)
            .message("success")
            .data(data)
            .timestamp(System.currentTimeMillis())
            .build();
    }
    
    public static <T> Result<T> success(String message) {
        return Result.<T>builder()
            .code(200)
            .message(message)
            .timestamp(System.currentTimeMillis())
            .build();
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return Result.<T>builder()
            .code(code)
            .message(message)
            .timestamp(System.currentTimeMillis())
            .build();
    }
}

// 分页结果
@Data
@Builder
public class PageResult<T> {
    private List<T> list;        // 当前页数据
    private Long total;          // 总记录数
    private Integer pageNum;     // 当前页码
    private Integer pageSize;    // 每页大小
    private Integer totalPage;   // 总页数
}

3.2 全局异常处理

java

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(400, e.getMessage());
    }
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        log.warn("参数校验失败: {}", message);
        return Result.error(400, message);
    }
    
    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常: ", e);
        return Result.error(500, "系统异常,请稍后重试");
    }
}

四、配置和优化

image-20260202134134257

4.1 MyBatis配置

yaml

# application.yml
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL
    default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
  global-config:
    db-config:
      logic-delete-field: is_deleted  # 逻辑删除字段
      logic-delete-value: 1           # 逻辑删除值
      logic-not-delete-value: 0       # 逻辑未删除值
      id-type: auto                   # 主键策略
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.tlias.entity

4.2 分页插件配置

java

@Configuration
public class MybatisPlusConfig {
    
    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 分页插件
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
        paginationInterceptor.setMaxLimit(100L);  // 单页最大100条
        paginationInterceptor.setOverflow(true);  // 超过总页数时返回第一页
        interceptor.addInnerInterceptor(paginationInterceptor);
        
        // 乐观锁插件
        OptimisticLockerInnerInterceptor optimisticLockerInterceptor = new OptimisticLockerInnerInterceptor();
        interceptor.addInnerInterceptor(optimisticLockerInterceptor);
        
        return interceptor;
    }
}

五、我学到的教训

5.1 分层架构的价值

  1. 职责清晰:每层只做自己的事情
  2. 易于测试:可以单独测试Service层
  3. 便于维护:新人接手能快速理解
  4. 代码复用:相同的业务逻辑不用重复写

5.2 事务管理的重要性

  • 声明式事务:使用@Transactional注解
  • 隔离级别:根据业务需要选择
  • 传播行为:方法间的调用关系
  • 回滚规则:什么异常需要回滚

5.3 数据库设计原则

  1. 规范命名:表名、字段名要见名知意
  2. 合适类型:根据数据特点选择类型
  3. 建立索引:提高查询效率
  4. 外键约束:保证数据完整性
  5. 审计字段:记录操作痕迹

六、如果重来一次,我会...

  1. 先设计再编码:花时间做好数据库设计和接口设计
  2. 严格遵守分层:不让Controller越界
  3. 重视事务控制:关键操作必须加事务
  4. 完善日志记录:便于问题排查
  5. 编写单元测试:保证代码质量

结语:从代码搬运工到系统设计师

那个差点让我丢工作的员工管理系统,最终成为了我最好的老师。它教会我的不仅是技术,更是思考问题的方式:

好的系统不是写出来的,是设计出来的。

现在,当我开始一个新项目时,我会先问自己:

  • 这个系统的核心业务是什么?
  • 数据应该如何组织?
  • 边界在哪里?如何分层?
  • 异常情况如何处理?
  • 如何保证数据一致性?

这些问题的答案,比会写多少行代码更重要。因为技术是工具,思维才是核心。

知识点测试

读完文章了?来测试一下你对知识点的掌握程度吧!

评论区

使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。

如果评论系统无法加载,请确保:

  • 您的网络可以访问 GitHub
  • giscus GitHub App 已安装到仓库
  • 仓库已启用 Discussions 功能