backend-dev-best-practices.md 15 KB

后端开发最佳实践

1. 数据库表位置规则

1.1 数据库说明

本项目使用两个数据库:

  • TugboatCommon:系统公共库,存放所有机构共享的数据
  • LiandaTugboatMIS:分支机构的业务库,存放各自机构的业务数据

1.2 表位置规则

根据FlinkDataSync项目的配置,以下表位于TugboatCommon库:

  • Sys_User(用户表)
  • Sys_Role(角色表)
  • Sys_Menu(菜单表)
  • Sys_UserRole(用户角色关联表)
  • Sys_Department(部门表)
  • Sys_Title(职务表)
  • Sys_Dictionary(字典表)
  • Sys_DictionaryItem(字典元素表)
  • Sys_Parameter(参数表)
  • Sys_SerialNumber(序号表)
  • Sys_log(日志表)
  • Sys_Attachment(附件表)
  • Sys_Announcement(公告)
  • Sys_Announcement_Sys_Role(公告发布角色)
  • Sys_Announcement_Sys_User(公告发布用户)
  • 以及其他以Sys_开头的系统表

业务表位于LiandaTugboatMIS库:

  • Bus_*(业务相关表)
  • Disp_*(调度相关表)
  • Sal_*(薪酬相关表)
  • Man_*(机务相关表)
  • Tug_*(拖轮相关表)
  • Pro_*(项目相关表)
  • Fin_*(财务相关表)
  • Fabric_*(布品相关表)

1.3 读写分离规则

只有tugboatcommon和liandatugboatmis这两个库同时存在的表才需要读写分离:

  • 写入到tugboatcommon库
  • 从liandatugboatmis库读取数据
  • 其他表都在各自的数据库里读写即可

1.4 数据源配置

@Service
@DataSource(value = RoutingDataSourceConfig.DataSourceType.COMMON)
public class UserService {
    // 使用TugboatCommon库
}

@Service
@DataSource(value = RoutingDataSourceConfig.DataSourceType.MIS)
public class BusinessService {
    // 使用LiandaTugboatMIS库
}

2. 公共字段处理规范

2.1 公共字段定义

以下字段为公共字段,所有表如果存在这些字段,都需要在新增和修改操作中处理:

  • RecordStatus:记录状态(1=启用,0=禁用)
  • CreateUserID:创建人ID
  • CreateTime:创建时间
  • ModifyUserID:修改人ID
  • ModifyTime:修改时间

2.2 新增操作公共字段处理

@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
    User user = new User();
    user.setId(UUID.randomUUID().toString());
    user.setLoginId(userCreateDTO.getLoginId());
    user.setName(userCreateDTO.getName());
    
    // 设置公共字段
    String currentUserId = getCurrentUserId();
    user.setRecordStatus(1); // 新增默认RecordStatus为1(启用)
    user.setCreateUserID(currentUserId); // 创建人为当前登录人
    user.setCreateTime(new Date()); // 创建时间为当前时间
    user.setModifyUserID(currentUserId); // 修改人为当前登录人
    user.setModifyTime(new Date()); // 修改时间为当前时间
    
    user = userRepository.save(user);
    return UserDTO.fromEntity(user);
}

/**
 * 获取当前登录用户ID
 */
private String getCurrentUserId() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername();
    }
    return "admin"; // 默认返回admin
}

2.3 修改操作公共字段处理

@Transactional
public UserDTO updateUser(String userId, UserUpdateDTO userUpdateDTO) {
    User user = userRepository.findById(userId).orElseThrow(() -> 
        new RuntimeException("用户不存在"));
    
    user.setLoginId(userUpdateDTO.getLoginId());
    user.setName(userUpdateDTO.getName());
    
    // 更新公共字段
    String currentUserId = getCurrentUserId();
    user.setModifyUserID(currentUserId); // 修改人为当前登录人
    user.setModifyTime(new Date()); // 修改时间为当前时间
    
    user = userRepository.save(user);
    return UserDTO.fromEntity(user);
}

2.4 前端表单默认值

const addUserForm = reactive({
  loginId: '',
  name: '',
  recordStatus: 1 // 默认设置为1(启用)
})

3. 数据库表名和字段名规则

3.1 表名规则

  • 严格按照数据库结构文档(database_structure.md)中的表名
  • 表名使用PascalCase命名,如:Sys_User、Bus_CustomerCompany
  • 实体类的@Table注解必须与数据库表名完全一致
  • 注意:数据库文档中的表名可能使用下划线分隔,如Sys_User

3.2 字段名规则

  • 严格按照数据库结构文档中的字段名
  • 字段名使用PascalCase命名,如:UserID、LoginID、Name
  • 实体类的@Column注解必须与数据库字段名完全一致
  • 注意:数据库文档中的字段名可能使用下划线分隔,如UserID

3.3 实体类示例

@Entity
@Table(name = "Sys_User")
public class User {
    @Id
    @Column(name = "UserID")
    private String id;
    
    @Column(name = "LoginID")
    private String loginId;
    
    @Column(name = "Name")
    private String name;
    
    @Column(name = "Password")
    private String password;
    
    @Column(name = "RecordStatus")
    private Integer recordStatus;
    
    @Column(name = "CreateUserID")
    private String createUserID;
    
    @Column(name = "CreateTime")
    private Date createTime;
    
    @Column(name = "ModifyUserID")
    private String modifyUserID;
    
    @Column(name = "ModifyTime")
    private Date modifyTime;
}

4. 字典数据查询规范

4.1 字典查询规则

获取Sys_DictionaryItem数据时,必须同时传递DictionaryCode和Value值:

// 错误:只传递Value值
SysDictionaryItem item = sysDictionaryItemRepository.findByValue(1);

// 正确:同时传递DictionaryCode和Value
SysDictionaryItem item = sysDictionaryItemRepository.findByDictionaryCodeAndValue("RecordStatus", 1);

4.2 字典Repository示例

@Repository
public interface SysDictionaryItemRepository extends JpaRepository<SysDictionaryItem, String> {
    @Query(value = "SELECT * FROM Sys_DictionaryItem WHERE DictionaryCode = :dictionaryCode AND Value = :value", nativeQuery = true)
    SysDictionaryItem findByDictionaryCodeAndValue(@Param("dictionaryCode") String dictionaryCode, @Param("value") Integer value);
    
    @Query(value = "SELECT * FROM Sys_DictionaryItem WHERE DictionaryCode = :dictionaryCode ORDER BY OrderNo", nativeQuery = true)
    List<SysDictionaryItem> findByDictionaryCode(@Param("dictionaryCode") String dictionaryCode);
}

5. 重复校验规范

5.1 新增操作重复校验

@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
    // 重复校验:检查LoginID是否重复
    if (userRepository.findByLoginID(userCreateDTO.getLoginId()) != null) {
        throw new RuntimeException("账号已存在,请使用其他账号");
    }
    
    // 重复校验:检查Name是否重复
    if (userRepository.findByName(userCreateDTO.getName()) != null) {
        throw new RuntimeException("用户名已存在,请使用其他用户名");
    }
    
    // 创建用户...
}

5.2 修改操作重复校验

@Transactional
public UserDTO updateUser(String userId, UserUpdateDTO userUpdateDTO) {
    User user = userRepository.findById(userId).orElseThrow(() -> 
        new RuntimeException("用户不存在"));
    
    // 重复校验:检查LoginID是否与其他用户重复
    User existingUser = userRepository.findByLoginID(userUpdateDTO.getLoginId());
    if (existingUser != null && !existingUser.getId().equals(userId)) {
        throw new RuntimeException("账号已存在,请使用其他账号");
    }
    
    // 重复校验:检查Name是否与其他用户重复
    existingUser = userRepository.findByName(userUpdateDTO.getName());
    if (existingUser != null && !existingUser.getId().equals(userId)) {
        throw new RuntimeException("用户名已存在,请使用其他用户名");
    }
    
    // 更新用户...
}

6. 密码处理规范

6.1 密码加密

使用MD5加密密码:

private String md5(String password) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(password.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    } catch (Exception e) {
        throw new RuntimeException("密码加密失败", e);
    }
}

6.2 默认密码

新增用户时,默认密码为"TIMS123456"的MD5值:

user.setPassword(md5("TIMS123456"));

7. 删除操作规范

7.1 删除确认

前端必须显示删除确认对话框:

const deleteUser = async (id) => {
  try {
    await ElMessageBox.confirm(
      '确定要删除该用户吗?删除后用户及其角色关联将被永久删除,不可恢复。',
      '删除确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    
    await request.delete(`/user/delete/${id}`)
    ElMessage.success('删除成功')
    getUsers()
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除用户失败:', error)
      ElMessage.error('删除用户失败')
    }
  }
}

7.2 级联删除

删除主表数据时,必须先删除关联表数据:

@Transactional
public void deleteUsers(List<String> userIds) {
    for (String userId : userIds) {
        // 先删除关联表数据
        sysUserRoleRepository.deleteByUserId(userId);
        // 再删除主表数据
        userRepository.deleteById(userId);
    }
}

8. SQL查询规范

8.1 使用JdbcTemplate处理动态SQL

对于需要动态拼接SQL的查询,使用JdbcTemplate:

@Autowired
@Qualifier("writeJdbcTemplate")
private JdbcTemplate writeJdbcTemplate;

public List<User> queryUsers(UserQuery query) {
    StringBuilder sql = new StringBuilder("SELECT * FROM Sys_User WHERE 1=1");
    List<Object> params = new ArrayList<>();
    
    if (query.getLoginId() != null && !query.getLoginId().isEmpty()) {
        sql.append(" AND LoginID LIKE ?");
        params.add("%" + query.getLoginId() + "%");
    }
    
    if (query.getName() != null && !query.getName().isEmpty()) {
        sql.append(" AND Name LIKE ?");
        params.add("%" + query.getName() + "%");
    }
    
    return writeJdbcTemplate.query(sql.toString(), params.toArray(), new UserRowMapper());
}

8.2 使用CONCAT函数处理字符串拼接

// 错误:使用+号拼接字符串
@Query(value = "SELECT * FROM Sys_User WHERE LoginID LIKE '%' + :loginID + '%'", nativeQuery = true)

// 正确:使用CONCAT函数
@Query(value = "SELECT * FROM Sys_User WHERE LoginID LIKE CONCAT('%', :loginID, '%')", nativeQuery = true)
User findByLoginIDLike(@Param("loginID") String loginID);

8.3 排序处理

使用Pageable处理排序,不要在SQL中写死ORDER BY:

public Page<User> queryUsers(UserQuery query, Pageable pageable) {
    Sort sort = pageable.getSort();
    if (query.getSortBy() != null && query.getSortOrder() != null) {
        sort = Sort.by(
            "asc".equalsIgnoreCase(query.getSortOrder()) ? Sort.Direction.ASC : Sort.Direction.DESC,
            query.getSortBy()
        );
    }
    return userRepository.findAll(pageable.withSort(sort));
}

9. 错误处理规范

9.1 业务异常

使用RuntimeException抛出业务异常:

if (userRepository.findByLoginID(userCreateDTO.getLoginId()) != null) {
    throw new RuntimeException("账号已存在,请使用其他账号");
}

9.2 前端错误处理

try {
  await request.post('/user/create', data)
  ElMessage.success('操作成功')
} catch (error) {
  console.error('操作失败:', error)
  ElMessage.error(error.response?.data?.message || '操作失败,请稍后重试')
}

10. 事务管理规范

10.1 使用@Transactional注解

所有涉及多个数据库操作的方法都必须添加@Transactional注解:

@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
    // 创建用户
    User user = userRepository.save(user);
    
    // 创建用户角色关联
    SysUserRole userRole = new SysUserRole();
    userRole.setUserId(user.getId());
    userRole.setRoleId(userCreateDTO.getRoleId());
    sysUserRoleRepository.save(userRole);
    
    return UserDTO.fromEntity(user);
}

10.2 只读事务

查询方法可以使用@Transactional(readOnly = true):

@Transactional(readOnly = true)
public Page<User> queryUsers(UserQuery query, Pageable pageable) {
    return userRepository.findAll(pageable);
}

11. 分页查询规范

11.1 使用Spring Data JPA分页

public Page<User> queryUsers(UserQuery query, Pageable pageable) {
    return userRepository.findAll(pageable);
}

11.2 前端分页参数

const getUsers = async () => {
  const response = await request.get('/user/list', {
    params: {
      page: currentPage.value,
      pageSize: pageSize.value,
      sortBy: sortMap.value.sortBy,
      sortOrder: sortMap.value.sortOrder
    }
  })
  userList.value = response.data.content
  total.value = response.data.totalElements
}

12. 日志记录规范

12.1 使用SLF4J记录日志

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    public UserDTO createUser(UserCreateDTO userCreateDTO) {
        logger.info("开始创建用户,账号:{}", userCreateDTO.getLoginId());
        try {
            // 创建用户逻辑
            logger.info("用户创建成功,ID:{}", user.getId());
            return UserDTO.fromEntity(user);
        } catch (Exception e) {
            logger.error("用户创建失败", e);
            throw new RuntimeException("用户创建失败", e);
        }
    }
}

13. 注意事项

  1. 不要随意修改数据库结构:数据库结构以database_structure.md文档为准
  2. 不要添加不存在的字段:如果数据库文档没有描述某个字段,就是没有这个字段
  3. 注意表名和字段名的大小写:严格按照数据库文档的大小写
  4. 使用正确的数据源:根据表的位置选择正确的数据源
  5. 处理公共字段:所有新增和修改操作都必须处理公共字段
  6. 添加重复校验:新增和修改操作都必须进行重复校验
  7. 使用事务管理:涉及多个数据库操作的方法必须使用@Transactional
  8. 记录日志:重要操作必须记录日志
  9. 处理异常:使用RuntimeException抛出业务异常,前端统一处理错误提示
  10. 使用MD5加密密码:密码必须使用MD5加密存储