# 后端开发最佳实践 ## 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 数据源配置 ```java @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 新增操作公共字段处理 ```java @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 修改操作公共字段处理 ```java @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 前端表单默认值 ```javascript 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 实体类示例 ```java @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值: ```java // 错误:只传递Value值 SysDictionaryItem item = sysDictionaryItemRepository.findByValue(1); // 正确:同时传递DictionaryCode和Value SysDictionaryItem item = sysDictionaryItemRepository.findByDictionaryCodeAndValue("RecordStatus", 1); ``` ### 4.2 字典Repository示例 ```java @Repository public interface SysDictionaryItemRepository extends JpaRepository { @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 findByDictionaryCode(@Param("dictionaryCode") String dictionaryCode); } ``` ## 5. 重复校验规范 ### 5.1 新增操作重复校验 ```java @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 修改操作重复校验 ```java @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加密密码: ```java 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值: ```java user.setPassword(md5("TIMS123456")); ``` ## 7. 删除操作规范 ### 7.1 删除确认 前端必须显示删除确认对话框: ```javascript 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 级联删除 删除主表数据时,必须先删除关联表数据: ```java @Transactional public void deleteUsers(List userIds) { for (String userId : userIds) { // 先删除关联表数据 sysUserRoleRepository.deleteByUserId(userId); // 再删除主表数据 userRepository.deleteById(userId); } } ``` ## 8. SQL查询规范 ### 8.1 使用JdbcTemplate处理动态SQL 对于需要动态拼接SQL的查询,使用JdbcTemplate: ```java @Autowired @Qualifier("writeJdbcTemplate") private JdbcTemplate writeJdbcTemplate; public List queryUsers(UserQuery query) { StringBuilder sql = new StringBuilder("SELECT * FROM Sys_User WHERE 1=1"); List 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函数处理字符串拼接 ```java // 错误:使用+号拼接字符串 @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: ```java public Page 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抛出业务异常: ```java if (userRepository.findByLoginID(userCreateDTO.getLoginId()) != null) { throw new RuntimeException("账号已存在,请使用其他账号"); } ``` ### 9.2 前端错误处理 ```javascript 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注解: ```java @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): ```java @Transactional(readOnly = true) public Page queryUsers(UserQuery query, Pageable pageable) { return userRepository.findAll(pageable); } ``` ## 11. 分页查询规范 ### 11.1 使用Spring Data JPA分页 ```java public Page queryUsers(UserQuery query, Pageable pageable) { return userRepository.findAll(pageable); } ``` ### 11.2 前端分页参数 ```javascript 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记录日志 ```java 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加密存储