返回 筑基・Web 道途启关
18我的第一个员工管理系统,差点让我丢了工作
博主
大约 20 分钟
18我的第一个员工管理系统,差点让我丢了工作
"王总,为什么工资单上显示发了两份工资?" 财务的电话让我瞬间冷汗直流。我颤抖着打开数据库,看到同一条发薪记录被插入了两次。那一刻我才明白:会写CRUD不等于懂系统开发。
一、"这有什么难的?不就是增删改查"
1.1 我的"天才"设计
老板让我开发员工管理系统,我自信满满:"不就是几张表吗?三天搞定!"
我的"架构":
- 一个Controller搞定所有:
EmployeeController里塞了20个方法 - Service?不需要:业务逻辑全写在Controller里
- 直接操作数据库:在Controller里写SQL,多方便啊!

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 灾难现场
第一周,系统"顺利"上线。然后问题接踵而至:
- 性能问题:员工表到1000条时,列表查询要5秒
- 数据错乱:两个人同时修改同一条记录,后提交的覆盖了先提交的
- 工资多发:财务点击"发工资"按钮,网络卡顿多点了几下,发了3次工资
- 无法维护:新同事接手代码,看了三天没看懂业务逻辑在哪
最严重的是发工资事件:
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步失败,公司账户钱扣了,员工没收到,日志也没记录。更糟的是,如果财务快速点两次,会发两次工资。
二、分层架构:从"一锅炖"到"精装修"

2.1 导师的"手术刀"
导师看着我的代码,叹了口气:"你这是把厨房、餐厅、卧室都放在一个房间啊。"
他给我画了张图:
text
以前的我: ┌─────────────────────────┐ │ Controller(所有事情) │ │ • 接收请求 │ │ • 验证参数 │ │ • 写SQL │ │ • 处理业务逻辑 │ │ • 返回响应 │ └─────────────────────────┘ 正确的分层: ┌─────────────────────────┐ │ Controller │ ← 只负责:接收、验证、返回 ├─────────────────────────┤ │ Service │ ← 业务逻辑的家 ├─────────────────────────┤ │ Mapper/Repository │ ← 数据库操作专家 ├─────────────────────────┤ │ Entity │ ← 数据模型 ├─────────────────────────┤ │ DTO │ ← 前后端数据传输员 └─────────────────────────┘
2.2 重构之路
第一步:设计数据库

以前我的表设计:
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 <= #{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业务层

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, "系统异常,请稍后重试");
}
}
四、配置和优化

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 分层架构的价值
- 职责清晰:每层只做自己的事情
- 易于测试:可以单独测试Service层
- 便于维护:新人接手能快速理解
- 代码复用:相同的业务逻辑不用重复写
5.2 事务管理的重要性
- 声明式事务:使用
@Transactional注解 - 隔离级别:根据业务需要选择
- 传播行为:方法间的调用关系
- 回滚规则:什么异常需要回滚
5.3 数据库设计原则
- 规范命名:表名、字段名要见名知意
- 合适类型:根据数据特点选择类型
- 建立索引:提高查询效率
- 外键约束:保证数据完整性
- 审计字段:记录操作痕迹
六、如果重来一次,我会...
- 先设计再编码:花时间做好数据库设计和接口设计
- 严格遵守分层:不让Controller越界
- 重视事务控制:关键操作必须加事务
- 完善日志记录:便于问题排查
- 编写单元测试:保证代码质量
结语:从代码搬运工到系统设计师
那个差点让我丢工作的员工管理系统,最终成为了我最好的老师。它教会我的不仅是技术,更是思考问题的方式:
好的系统不是写出来的,是设计出来的。
现在,当我开始一个新项目时,我会先问自己:
- 这个系统的核心业务是什么?
- 数据应该如何组织?
- 边界在哪里?如何分层?
- 异常情况如何处理?
- 如何保证数据一致性?
这些问题的答案,比会写多少行代码更重要。因为技术是工具,思维才是核心。
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能