# Ether 智慧物业管理平台 - 开发规范 **文档版本**: v1.3 **最后更新**: 2026-02-13 **适用范围**: 所有微服务模块 --- ## 一、代码规范 ### 1.1 基础规范 | 项目 | 规范 | 说明 | | -------------- | ------------------------------ | -------------------- | | **JDK版本** | Java 17+ | 使用LTS版本 | | **编码规范** | Alibaba Java Coding Guidelines | 遵循阿里巴巴编码规范 | | **代码格式化** | 统一使用IDE格式化配置 | 避免格式差异 | | **文件编码** | UTF-8 | 所有源文件 | ### 1.2 命名规范 ```java // 类名: 大驼峰 public class WorkOrderService { } public class WorkOrderController { } // 方法名: 小驼峰 public void createWorkOrder() { } public WorkOrder getById(UUID id) { } // 变量名: 小驼峰 private String orderNo; private LocalDateTime createdAt; // 常量: 全大写下划线 private static final int MAX_RETRY_COUNT = 3; private static final String DEFAULT_STATUS = "CREATED"; // 包名: 全小写 package com.ether.ops.service; package com.ether.ops.controller; ``` ### 1.3 类结构规范 ```java /** * 工单服务 * 负责工单的创建、分配、流转等业务逻辑 * * @author Ether Team * @since 1.0.0 */ @Service @RequiredArgsConstructor @Slf4j public class WorkOrderService { // 1. 常量定义 private static final String ORDER_PREFIX = "WO"; private static final int ORDER_NO_LENGTH = 16; // 2. 依赖注入 private final WorkOrderRepository workOrderRepository; private final WorkOrderFlowRepository flowRepository; private final NotificationService notificationService; // 3. 业务方法 /** * 创建工单 * * @param request 创建请求 * @return 创建的工单 * @throws BusinessException 业务异常 */ @Transactional public WorkOrder create(WorkOrderCreateRequest request) { // 实现逻辑 } // 4. 私有方法 private String generateOrderNo() { // 实现逻辑 } } ``` --- ## 二、数据库规范 ### 2.1 命名规范 | 对象 | 规范 | 示例 | | ---------- | -------------------- | ---------------------------------- | | **表名** | 小写下划线,模块前缀 | `ops_work_order`, `mdm_space_node` | | **字段名** | 小写下划线 | `created_at`, `project_id` | | **索引名** | `idx_表名_字段名` | `idx_work_order_status` | | **外键名** | `fk_表名_关联表` | `fk_work_order_assignee` | ### 2.2 字段规范 ```sql -- 必备字段 CREATE TABLE example_table ( id UUID PRIMARY KEY, -- 主键 project_id UUID NOT NULL, -- 项目ID(多租户) -- 业务字段 status VARCHAR(20) NOT NULL, -- 状态 -- 审计字段 created_at TIMESTAMP NOT NULL DEFAULT NOW(), -- 创建时间 updated_at TIMESTAMP NOT NULL DEFAULT NOW(), -- 更新时间 created_by UUID, -- 创建人 updated_by UUID -- 更新人 ); -- 索引规范 CREATE INDEX idx_example_status ON example_table(status); CREATE INDEX idx_example_project ON example_table(project_id); CREATE INDEX idx_example_created ON example_table(created_at); ``` ### 2.3 数据类型规范 | 数据类型 | 使用场景 | 示例 | | -------------- | ---------------- | ---------------------- | | `UUID` | 主键、外键 | `id UUID PRIMARY KEY` | | `VARCHAR(n)` | 字符串,长度确定 | `code VARCHAR(50)` | | `TEXT` | 长文本 | `description TEXT` | | `NUMERIC(p,s)` | 金额、精确数值 | `amount NUMERIC(12,2)` | | `INTEGER` | 整数 | `sort_order INTEGER` | | `BIGINT` | 大整数、计数 | `view_count BIGINT` | | `BOOLEAN` | 布尔值 | `is_enabled BOOLEAN` | | `TIMESTAMP` | 日期时间 | `created_at TIMESTAMP` | | `DATE` | 日期 | `birth_date DATE` | | `JSONB` | JSON数据 | `attributes JSONB` | --- ## 三、接口规范 ### 3.1 RESTful API 规范 ```java @RestController @RequestMapping("/api/v1/ops/work-orders") @RequiredArgsConstructor @Tag(name = "工单管理", description = "工单相关接口") public class WorkOrderController { private final WorkOrderService workOrderService; // 创建资源: POST @PostMapping @Operation(summary = "创建工单") public Result create(@RequestBody @Valid WorkOrderCreateRequest request) { return Result.success(workOrderService.create(request)); } // 查询单个: GET /{id} @GetMapping("/{id}") @Operation(summary = "获取工单详情") public Result getById(@PathVariable UUID id) { return Result.success(workOrderService.getById(id)); } // 查询列表: GET @GetMapping @Operation(summary = "分页查询工单") public Result> page(WorkOrderQueryRequest request) { return Result.success(workOrderService.page(request)); } // 更新资源: PUT /{id} @PutMapping("/{id}") @Operation(summary = "更新工单") public Result update(@PathVariable UUID id, @RequestBody @Valid WorkOrderUpdateRequest request) { return Result.success(workOrderService.update(id, request)); } // 删除资源: DELETE /{id} @DeleteMapping("/{id}") @Operation(summary = "删除工单") public Result delete(@PathVariable UUID id) { workOrderService.delete(id); return Result.success(); } // 业务操作: POST /{id}/action @PostMapping("/{id}/assign") @Operation(summary = "分配工单") public Result assign(@PathVariable UUID id, @RequestBody @Valid WorkOrderAssignRequest request) { return Result.success(workOrderService.assign(id, request)); } } ``` ### 3.2 接口路径规范 | 操作 | HTTP方法 | 路径示例 | 说明 | | -------- | -------- | ------------------------------------- | ------------ | | 创建 | POST | `/api/v1/ops/work-orders` | 创建资源 | | 查询单个 | GET | `/api/v1/ops/work-orders/{id}` | 根据ID查询 | | 查询列表 | GET | `/api/v1/ops/work-orders` | 分页查询 | | 更新 | PUT | `/api/v1/ops/work-orders/{id}` | 全量更新 | | 删除 | DELETE | `/api/v1/ops/work-orders/{id}` | 删除资源 | | 业务操作 | POST | `/api/v1/ops/work-orders/{id}/assign` | 特定业务操作 | ### 3.3 统一响应格式 ```java @Data public class Result { private Integer code; // 状态码: 200成功, 其他错误 private String message; // 提示信息 private T data; // 数据 private Long timestamp; // 时间戳 // 成功响应 public static Result success(T data) { Result result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); result.setTimestamp(System.currentTimeMillis()); return result; } // 错误响应 public static Result error(String message) { Result result = new Result<>(); result.setCode(500); result.setMessage(message); result.setTimestamp(System.currentTimeMillis()); return result; } } ``` ### 3.4 分页请求/响应 ```java // 分页请求 @Data public class PageRequest { private Integer pageNum = 1; // 页码,从1开始 private Integer pageSize = 10; // 每页大小 private String sortField; // 排序字段 private String sortOrder; // 排序方式: asc/desc } // 分页响应 @Data public class Page { private List list; // 数据列表 private Long total; // 总记录数 private Integer pageNum; // 当前页码 private Integer pageSize; // 每页大小 private Integer totalPages; // 总页数 } ``` --- ## 四、异常处理规范 ### 4.1 异常分类 ```java // 业务异常 public class BusinessException extends RuntimeException { private final Integer code; public BusinessException(String message) { super(message); this.code = 500; } public BusinessException(Integer code, String message) { super(message); this.code = code; } } // 参数异常 public class ValidationException extends BusinessException { public ValidationException(String message) { super(400, message); } } // 未授权异常 public class UnauthorizedException extends BusinessException { public UnauthorizedException(String message) { super(401, message); } } // 无权限异常 public class ForbiddenException extends BusinessException { public ForbiddenException(String message) { super(403, message); } } // 资源不存在异常 public class NotFoundException extends BusinessException { public NotFoundException(String message) { super(404, message); } } ``` ### 4.2 全局异常处理 ```java @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 业务异常 @ExceptionHandler(BusinessException.class) public Result handleBusinessException(BusinessException e) { log.warn("业务异常: {}", e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } // 参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result 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 handleException(Exception e) { log.error("系统异常", e); return Result.error(500, "系统繁忙,请稍后重试"); } } ``` --- ## 五、日志规范 ### 5.1 日志级别使用 | 级别 | 使用场景 | 示例 | | --------- | ---------------------- | ---------------------------- | | **ERROR** | 系统错误,需要立即处理 | 数据库连接失败、关键业务异常 | | **WARN** | 警告,需要注意 | 参数校验失败、业务规则冲突 | | **INFO** | 正常业务日志 | 业务流程记录、关键操作记录 | | **DEBUG** | 调试信息 | 详细参数、执行过程 | | **TRACE** | 最详细跟踪 | 方法进入退出、循环内部 | ### 5.2 日志格式 ```java @Slf4j @Service public class WorkOrderService { public WorkOrder create(WorkOrderCreateRequest request) { // 入口日志 log.info("创建工单开始, title={}", request.getTitle()); try { // 业务逻辑 WorkOrder workOrder = doCreate(request); // 成功日志 log.info("创建工单成功, orderNo={}, id={}", workOrder.getOrderNo(), workOrder.getId()); return workOrder; } catch (Exception e) { // 错误日志 log.error("创建工单失败, title={}", request.getTitle(), e); throw e; } } } ``` --- ## 六、测试规范 ### 6.1 单元测试 ```java @ExtendWith(MockitoExtension.class) class WorkOrderServiceTest { @Mock private WorkOrderRepository workOrderRepository; @InjectMocks private WorkOrderService workOrderService; @Test @DisplayName("创建工单-成功") void create_Success() { // Given WorkOrderCreateRequest request = new WorkOrderCreateRequest(); request.setTitle("测试工单"); request.setDescription("测试描述"); when(workOrderRepository.save(any())).thenAnswer(invocation -> { WorkOrder order = invocation.getArgument(0); order.setId(UUID.randomUUID()); return order; }); // When WorkOrder result = workOrderService.create(request); // Then assertNotNull(result.getId()); assertEquals("测试工单", result.getTitle()); assertEquals(WorkOrderStatus.CREATED, result.getStatus()); verify(workOrderRepository).save(any()); } @Test @DisplayName("创建工单-参数为空") void create_NullTitle_ThrowsException() { // Given WorkOrderCreateRequest request = new WorkOrderCreateRequest(); request.setTitle(null); // When & Then assertThrows(ValidationException.class, () -> { workOrderService.create(request); }); } } ``` ### 6.2 测试命名规范 - 测试类: `被测类名 + Test` - 测试方法: `被测方法名_场景_预期结果` - 使用 `@DisplayName` 添加中文说明 ### 6.3 E2E 测试规范 #### 6.3.1 测试框架架构 E2E 测试采用**组合方案**架构,平衡性能和稳定性: ``` ┌─────────────────────────────────────────────────────────────┐ │ E2E 测试执行流程 │ ├─────────────────────────────────────────────────────────────┤ │ 1. resetPageState() - 轻量级页面状态重置 │ │ ├── 导航到登录页 │ │ ├── 清除 localStorage/sessionStorage │ │ └── 清除 Cookies │ │ │ │ 2. login() - 登录操作 │ │ ├── 调用 resetPageState() │ │ ├── 填写用户名密码 │ │ └── 点击登录按钮 │ │ │ │ 3. waitForAppReady() - 智能等待应用就绪 │ │ ├── 等待侧边栏加载 (可配置) │ │ ├── 等待 Loading 消失 │ │ └── 等待网络空闲 │ │ │ │ 4. 执行测试用例 │ │ ├── 业务操作 │ │ └── 结果断言 │ └─────────────────────────────────────────────────────────────┘ ``` #### 6.3.2 核心方法实现 ```typescript class EtherE2ETestRunner { /** * 轻量级页面状态重置 * 在每个测试用例前调用,确保测试隔离性 */ private async resetPageState() { if (!this.page) throw new Error("Page not initialized"); await this.page.goto(`${this.baseUrl}/login`, { waitUntil: "networkidle2", timeout: 30000, }); await this.page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); const cookies = document.cookie.split(";"); for (const cookie of cookies) { const eqPos = cookie.indexOf("="); const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`; } }); await sleep(500); } /** * 智能等待应用就绪 * 多重等待策略,适应不同网络环境 */ private async waitForAppReady(options?: { requireSider?: boolean }) { if (!this.page) throw new Error("Page not initialized"); const { requireSider = true } = options || {}; try { // 1. 等待侧边栏加载(可配置) if (requireSider) { await this.page.waitForSelector(".ant-layout-sider", { timeout: 20000, }); } // 2. 等待 Loading 状态消失 await this.page.waitForFunction( () => { const loading = document.querySelector(".ant-spin-spinning"); return !loading; }, { timeout: 10000 }, ); // 3. 等待网络空闲 await this.page .waitForNetworkIdle({ idleTime: 300, timeout: 5000, }) .catch(() => {}); } catch (error) { const currentUrl = this.page.url(); throw new Error( `应用未就绪: 当前URL=${currentUrl}, 错误=${error.message}`, ); } } /** * 登录方法 * 组合 resetPageState + 登录操作 + waitForAppReady */ private async login(username: string, password: string) { if (!this.page) throw new Error("Page not initialized"); await this.resetPageState(); await this.page.waitForSelector('input[placeholder*="用户名"]', { timeout: 10000, }); await this.page.type('input[placeholder*="用户名"]', username, { delay: 50, }); await this.page.type('input[placeholder*="密码"]', password, { delay: 50 }); const buttons = await this.page.$$("button"); for (const btn of buttons) { const text = await this.page.evaluate((el) => el.innerText, btn); if (text.includes("登")) { await btn.click(); break; } } await this.waitForAppReady(); } } ``` #### 6.3.3 测试用例编写规范 ```typescript // ✅ 正确示例:使用 waitForAppReady 替代固定等待 await this.runTest("工单管理", "TC-WORK-001: 创建工单-打开弹窗", async () => { if (!this.page) throw new Error("Page not initialized"); // 1. 登录(内部已包含 resetPageState 和 waitForAppReady) await this.login("testuser", "Admin@123"); // 2. 选择项目 const projectSelector = await this.page.$(".project-selector .ant-select"); if (projectSelector) { await projectSelector.click(); await sleep(500); const options = await this.page.$$(".ant-select-dropdown .ant-select-item"); if (options.length > 0) { await options[0].click(); await sleep(1000); } } // 3. 导航到目标页面 await this.clickMenuByText("工单管理"); // 4. 等待页面就绪(不需要侧边栏时设置 requireSider: false) await this.waitForAppReady({ requireSider: false }); // 5. 执行业务操作 const clicked = await this.clickButtonByText("新增"); if (!clicked) { throw new Error("未找到新增工单按钮"); } // 6. 断言结果 await sleep(1000); const modal = await this.page.$(".ant-modal"); if (!modal) { throw new Error("新建工单弹窗未显示"); } }); // ❌ 错误示例:使用固定等待时间 await this.page.waitForSelector(".ant-layout-sider", { timeout: 10000 }); await sleep(2000); // 不推荐:固定等待时间不可靠 ``` #### 6.3.4 测试用例命名规范 | 格式 | 示例 | | -------- | --------------------------------------- | | 功能测试 | `TC-{模块}-{编号}: {功能描述}` | | 权限测试 | `TC-{模块}-{编号}: {功能描述} ({角色})` | | 异常测试 | `TC-{模块}-{编号}: {功能描述}-异常场景` | 示例: - `TC-AUTH-001: 超级管理员登录成功` - `TC-WORK-018: 工单列表查询 (项目成员)` - `TC-USER-004: 创建用户-用户名重复` #### 6.3.5 等待策略选择指南 | 场景 | 推荐策略 | 说明 | | ------------------ | ------------------------------------------ | -------------------------- | | 登录后等待主页加载 | `waitForAppReady()` | 默认等待侧边栏 | | 页面内导航后等待 | `waitForAppReady({ requireSider: false })` | 侧边栏已存在,无需重复等待 | | 等待特定元素 | `waitForSelector()` | 针对特定元素的精确等待 | | 等待 API 响应 | `waitForNetworkIdle()` | 等待网络请求完成 | | 等待动画完成 | `sleep(500)` | 短暂固定等待,仅用于动画 | #### 6.3.6 测试数据管理 ```typescript class EtherE2ETestRunner { // 测试数据存储 private testData = { createdUserId: "", createdRoleId: "", createdProjectId: "", createdWorkOrderId: "", }; // 测试前准备数据 async prepareTestData() { // 确保测试用户存在 // 确保测试项目存在 } // 测试后清理数据 async cleanupTestData() { // 删除创建的测试数据 } } ``` #### 6.3.7 测试报告 测试报告自动保存至 `docs/测试用例/测试报告_{timestamp}.md`,包含: - 执行时间 - 各模块通过率 - 失败用例详情 - 错误堆栈信息 --- ## 七、安全规范 ### 7.1 敏感数据处理 ```java // 密码加密 @Component public class PasswordEncoder { public String encode(String password, String salt) { // MD5(salt + password) return DigestUtils.md5Hex(salt + password); } public String generateSalt() { // 生成8位随机盐值 return RandomStringUtils.randomAlphanumeric(8); } } // 敏感字段脱敏 public class MaskUtils { // 手机号脱敏: 138****8888 public static String maskPhone(String phone) { if (StringUtils.isBlank(phone) || phone.length() != 11) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(7); } // 身份证号脱敏: 110101********1234 public static String maskIdCard(String idCard) { if (StringUtils.isBlank(idCard) || idCard.length() != 18) { return idCard; } return idCard.substring(0, 6) + "********" + idCard.substring(14); } } ``` ### 7.2 接口安全 ```java // JWT Token验证 @Component public class JwtTokenProvider { public String generateToken(User user) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration); return Jwts.builder() .setSubject(user.getId().toString()) .claim("username", user.getUsername()) .claim("projectId", user.getProjectId()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(secretKey) .compact(); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); return true; } catch (Exception e) { return false; } } } ``` --- ## 八、性能规范 ### 8.1 数据库性能 ```java // 1. 批量操作 @Transactional public void batchCreate(List requests) { List orders = requests.stream() .map(this::convertToEntity) .collect(Collectors.toList()); // 批量保存 workOrderRepository.saveAll(orders); } // 2. 分页查询 public Page pageQuery(WorkOrderQueryRequest request) { Pageable pageable = PageRequest.of( request.getPageNum() - 1, request.getPageSize(), Sort.by("createdAt").descending() ); return workOrderRepository.findAll(pageable); } // 3. 缓存使用 @Cacheable(value = "workOrder", key = "#id") public WorkOrder getById(UUID id) { return workOrderRepository.findById(id) .orElseThrow(() -> new NotFoundException("工单不存在")); } @CacheEvict(value = "workOrder", key = "#id") public void update(UUID id, WorkOrderUpdateRequest request) { // 更新逻辑 } ``` ### 8.2 异步处理 ```java // 异步方法 @Async("taskExecutor") public void sendNotificationAsync(WorkOrder workOrder) { notificationService.send(workOrder); } // 线程池配置 @Configuration public class AsyncConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } } ``` ### 8.3 服务稳定性配置 #### 8.3.1 数据库连接池配置 **必须配置 HikariCP 连接池参数**,避免连接泄漏和资源耗尽: ```yaml spring: datasource: hikari: minimum-idle: 2 # 最小空闲连接数 maximum-pool-size: 5 # 最大连接数(开发环境) idle-timeout: 30000 # 空闲超时(毫秒) pool-name: ServiceHikariPool # 连接池名称 max-lifetime: 1800000 # 连接最大生命周期(30分钟) connection-timeout: 30000 # 连接超时(30秒) connection-test-query: SELECT 1 # 连接测试查询 ``` **连接池大小计算公式**:`connections = ((core_count * 2) + effective_spindle_count)` | 环境 | maximum-pool-size | minimum-idle | | -------- | ----------------- | ------------ | | 开发环境 | 5 | 2 | | 测试环境 | 10 | 5 | | 生产环境 | 20 | 10 | #### 8.3.2 健康检查配置 **必须配置 Actuator 健康端点**,支持服务监控: ```yaml management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always ``` #### 8.3.3 网关重试配置 **必须配置网关重试机制**,提高服务容错能力: ```yaml spring: cloud: gateway: routes: - id: ether-auth uri: lb://ether-auth predicates: - Path=/api/v1/auth/** filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE methods: GET,POST backoff: firstBackoff: 100ms maxBackoff: 500ms factor: 2 ``` #### 8.3.4 日志配置 **必须配置文件日志输出**,便于问题排查: ```yaml logging: level: root: INFO com.ether: DEBUG org.springframework.web: INFO org.hibernate.SQL: WARN file: name: logs/ether-service.log pattern: file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" ``` #### 8.3.5 服务发现配置 **必须配置 Nacos 服务发现**,支持服务动态注册: ```yaml spring: cloud: nacos: discovery: enabled: true server-addr: localhost:8848 namespace: dev ``` #### 8.3.6 服务管理脚本 使用项目提供的脚本管理服务: ```bash # 健康检查 ./scripts/health-check.sh # 启动所有服务 ./scripts/service-manager.sh start # 停止所有服务 ./scripts/service-manager.sh stop # 重启所有服务 ./scripts/service-manager.sh restart # 查看服务状态 ./scripts/service-manager.sh status ``` ### 8.4 跨业务数据访问规范 #### 8.4.1 架构原则 Ether 项目采用 **Database-per-Service** 架构,每个微服务拥有独立的数据库: | 数据库 | 对应服务 | 业务范围 | | --------------- | ------------- | -------------------------------- | | `ether_auth` | ether-auth | 用户认证、权限管理 | | `ether_mdm` | ether-mdm | 主数据管理(项目、空间、业主等) | | `ether_ops` | ether-ops | 运营管理(工单、巡检等) | | `ether_finance` | ether-finance | 财务管理(收费、账单等) | **禁止直接跨库查询**,必须通过以下方案实现跨业务数据访问。 #### 8.4.2 方案一:API 组合(推荐) **适用场景**:实时性要求高、数据量小、调用频率低 ```java @Service @RequiredArgsConstructor public class WorkOrderClientServiceImpl implements WorkOrderClientService { private final RestTemplate restTemplate; private static final String WORK_ORDER_SERVICE_URL = "http://ether-ops/api/v1/ops/work-orders"; @Override public UUID createWorkOrderFromInspection( UUID projectId, UUID equipmentId, String pointName, String abnormalDesc, String suggestion, String photos, UUID inspectorId, String inspectorName) { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 传递项目上下文 if (ProjectContextHolder.getProjectId() != null) { headers.set("X-Project-Id", ProjectContextHolder.getProjectId().toString()); } Map workOrderRequest = new HashMap<>(); workOrderRequest.put("orderType", "REPAIR"); workOrderRequest.put("source", "INSPECTION"); workOrderRequest.put("priority", "HIGH"); workOrderRequest.put("title", "巡检异常处理 - " + pointName); workOrderRequest.put("description", buildDescription(pointName, abnormalDesc, suggestion)); workOrderRequest.put("equipmentId", equipmentId); workOrderRequest.put("reporterId", inspectorId); workOrderRequest.put("reporterName", inspectorName); workOrderRequest.put("images", photos); HttpEntity> request = new HttpEntity<>(workOrderRequest, headers); @SuppressWarnings("unchecked") Map response = restTemplate.postForObject( WORK_ORDER_SERVICE_URL, request, Map.class ); if (response != null && response.get("data") != null) { @SuppressWarnings("unchecked") Map data = (Map) response.get("data"); String orderId = (String) data.get("id"); log.info("成功创建巡检异常工单: {}, 巡检点: {}", orderId, pointName); return UUID.fromString(orderId); } } catch (Exception e) { log.error("创建巡检异常工单失败, 巡检点: {}", pointName, e); } return null; } } ``` **配置 RestTemplate**: ```java @Configuration public class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } } ``` #### 8.4.3 方案二:事件驱动 **适用场景**:异步处理、削峰填谷、最终一致性可接受 ```java // 事件发布方 @Service @RequiredArgsConstructor public class InspectionService { private final RabbitTemplate rabbitTemplate; @Value("${ether.mq.exchange.inspection}") private String inspectionExchange; public void reportAbnormality(InspectionResult result) { InspectionAbnormalEvent event = new InspectionAbnormalEvent(); event.setProjectId(result.getProjectId()); event.setEquipmentId(result.getEquipmentId()); event.setAbnormalDesc(result.getAbnormalDesc()); event.setTimestamp(LocalDateTime.now()); rabbitTemplate.convertAndSend( inspectionExchange, "inspection.abnormal", event ); log.info("发布巡检异常事件: {}", event); } } // 事件消费方 @Component @RequiredArgsConstructor @RabbitListener(queues = "ops.inspection.abnormal") public class InspectionAbnormalHandler { private final WorkOrderService workOrderService; @RabbitHandler public void handleAbnormalInspection(InspectionAbnormalEvent event) { log.info("接收巡检异常事件: {}", event); WorkOrderCreateRequest request = new WorkOrderCreateRequest(); request.setOrderType("REPAIR"); request.setSource("INSPECTION"); request.setPriority("HIGH"); request.setTitle("巡检异常处理 - " + event.getPointName()); request.setEquipmentId(event.getEquipmentId()); workOrderService.create(request); } } ``` **RabbitMQ 配置**: ```yaml spring: rabbitmq: host: localhost port: 5672 username: ${MQ_USERNAME:ether} password: ${MQ_PASSWORD:ether123} ether: mq: exchange: inspection: ether.inspection.exchange ``` #### 8.4.4 方案三:数据冗余(慎用) **适用场景**:高频查询、实时性要求极高、数据变更频率低 ```java // 在 MDM 服务中冗余存储用户基本信息 @Entity @Table(name = "mdm_user_snapshot") public class UserSnapshot { @Id private UUID userId; private String username; private String realName; private String phone; @LastModifiedDate private LocalDateTime updatedAt; // 冗余字段,通过事件同步更新 } // 监听用户变更事件,同步更新冗余数据 @Component @RequiredArgsConstructor @RabbitListener(queues = "mdm.user.sync") public class UserSyncHandler { private final UserSnapshotRepository userSnapshotRepository; @RabbitHandler public void handleUserChange(UserChangedEvent event) { UserSnapshot snapshot = new UserSnapshot(); snapshot.setUserId(event.getUserId()); snapshot.setUsername(event.getUsername()); snapshot.setRealName(event.getRealName()); snapshot.setPhone(event.getPhone()); snapshot.setUpdatedAt(LocalDateTime.now()); userSnapshotRepository.save(snapshot); } } ``` **注意**:数据冗余方案会增加数据一致性维护成本,**仅在高频查询场景使用**。 #### 8.4.5 方案选择指南 | 场景 | 推荐方案 | 理由 | | ---------------------- | -------- | ------------------ | | 低频调用、实时性要求高 | API 组合 | 简单可靠、数据实时 | | 异步处理、批量操作 | 事件驱动 | 解耦、削峰填谷 | | 高频查询、数据变更少 | 数据冗余 | 查询高效、减少调用 | | 复杂报表、数据分析 | CQRS | 读写分离、独立优化 | #### 8.4.6 禁止事项 | 禁止行为 | 原因 | 替代方案 | | ---------------------- | ---------------------- | --------------------- | | 直接跨库 JOIN | 破坏服务边界、耦合严重 | API 组合或事件驱动 | | 直接访问其他服务数据库 | 安全风险、难以维护 | 通过服务 API 访问 | | 分布式事务(XA) | 性能差、复杂度高 | 最终一致性 + 补偿机制 | | 循环调用服务 API | 性能问题、死锁风险 | 批量接口或事件驱动 | --- ## 九、代码质量规范 ### 9.1 导入管理 **规则**: 禁止保留未使用的导入语句 **原因**: 未使用的导入会增加代码体积,降低代码可读性 **IDE配置**: 启用 "Optimize imports on save" 或提交前自动优化 ```java // 错误示例 import java.time.LocalDateTime; // 未使用 import java.util.List; // 未使用 import java.util.Map; // 未使用 public class UserService { // ... } // 正确示例 public class UserService { // ... } ``` ### 9.2 变量使用 **规则**: 禁止声明后未使用的局部变量和字段 **例外情况**: - 测试代码中的预期结果验证 - 接口实现要求的必须参数 ```java // 错误示例 public void processOrder(UUID orderId) { WorkOrder order = workOrderRepository.findById(orderId); // 未使用 // 缺少对 order 的处理 } // 正确示例 public void processOrder(UUID orderId) { WorkOrder order = workOrderRepository.findById(orderId); order.setStatus(WorkOrderStatus.PROCESSING); workOrderRepository.save(order); } ``` ### 9.3 泛型类型安全 **规则**: 禁止使用原始类型(Raw Type),必须指定泛型参数 ```java // 错误示例 Map result = new HashMap(); // 原始类型 List items = new ArrayList(); // 原始类型 // 正确示例 Map result = new HashMap<>(); List items = new ArrayList<>(); ``` ### 9.4 类型转换安全 **规则**: 使用泛型方法避免 unchecked cast 警告 ```java // 错误示例 Map result = (Map) objectMapper.readValue(json, Map.class); // 正确示例 Map result = objectMapper.readValue(json, new TypeReference>() {}); ``` ### 9.5 废弃API处理 **规则**: 禁止使用已废弃的API,使用推荐替代方案 ```java // 错误示例 BigDecimal result = amount.divide(100, BigDecimal.ROUND_HALF_UP); // 废弃 // 正确示例 BigDecimal result = amount.divide(100, RoundingMode.HALF_UP); ``` ### 9.6 CSS兼容性 **规则**: 使用浏览器前缀时,同时定义标准属性 ```css /* 错误示例 */ .box { -webkit-line-clamp: 3; } /* 正确示例 */ .box { -webkit-line-clamp: 3; line-clamp: 3; } ``` ### 9.7 IDE警告处理标准 | 警告类型 | 处理方式 | 优先级 | |----------|----------|--------| | 未使用的导入 | 立即删除 | P1 | | 未使用的变量 | 删除或使用 | P1 | | 原始类型 | 添加泛型参数 | P1 | | 类型安全警告 | 使用泛型方法 | P2 | | 废弃API | 替换为推荐方案 | P2 | | CSS兼容性 | 添加标准属性 | P3 | | TODO注释 | 记录到任务清单 | P3 | --- ## 十、文档维护 **维护原则**: - 新增规范时在此文档补充 - 定期审查规范的适用性 - 所有开发人员必须遵守 ## 十一、开发经验总结 ### 11.1 多模块项目构建 | 场景 | 操作 | 命令 | |------|------|------| | 修改子模块代码后 | 重新编译子模块 | `mvn install -pl module-auth -am -DskipTests` | | 整个项目重新构建 | 清理并编译 | `mvn clean compile` | | 启动时确保模块加载 | 先安装再启动 | `mvn install -DskipTests` 后 `mvn spring-boot:run` | **教训**: 子模块代码修改后必须重新 `mvn install`,否则启动类不会加载新代码。 ### 11.2 Spring Security 配置 **问题**: Spring Boot 自动配置与自定义配置冲突 **解决**: 在 `application.yml` 中排除自动配置 ```yaml spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration ``` **教训**: 前后端分离项目必须禁用 CSRF,使用 JWT 认证。 ### 11.3 测试账号管理 | 要求 | 说明 | |------|------| | 密码格式 | BCrypt 加密格式 | | 文档同步 | 修改密码后必须更新 `docs/08-DATABASE/test-users.sql` | | 生成密码 | 使用 `BCryptPasswordEncoder` 生成 | ```java // 生成 BCrypt 密码 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encoded = encoder.encode("Admin@123"); ``` ### 11.4 前端调试技巧 | 问题 | 解决 | |------|------| | 热更新不生效 | 重启前端服务 | | 路由跳转失败 | 检查路由守卫 `isLoggedIn()` 实现 | | 登录状态判断错误 | 优先读取 `localStorage` 而非 store 状态 | ```typescript // 正确示例:isLoggedIn 同时检查 localStorage 和 store const isLoggedIn = () => { return !!localStorage.getItem('token') || !!token.value } ``` ### 11.5 代码优化最佳实践 **调试代码清理规范**: | 场景 | 操作 | |------|------| | 上线前 | 移除所有 `console.log`、`console.error` | | 上线前 | 移除所有 `log.info` 调试日志 | | 生产环境 | 敏感日志改为 `log.debug` | | 上线前 | 移除路由守卫中的调试日志 | **代码示例**: ```java // ❌ 错误:生产环境使用 info 级别 log.info("Authenticated user: {}", username); // ✅ 正确:认证日志使用 debug 级别 log.debug("Authenticated user: {}", username); ``` ```typescript // ❌ 错误:保留调试代码 console.log('准备跳转...') router.push('/').then(() => { console.log('跳转完成', router.currentRoute.value.path) }) // ✅ 正确:简洁的跳转逻辑 router.push('/') ``` ### 11.6 快速测试命令 ```bash # 后端启动 cd ether-pms/pms-starter && mvn spring-boot:run # 前端启动 cd ether-admin && npm run dev # 手动测试登录 curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"Admin@123"}' # 重启前端 lsof -i :5175 | grep LISTEN | awk '{print $2}' | xargs kill -9 npm run dev # 重启后端(修改子模块后) cd ether-pms && mvn install -pl module-auth -am -DskipTests cd ether-pms/pms-starter && mvn spring-boot:run ``` ### 11.7 问题排查清单 当遇到问题时,按以下顺序排查: 1. **后端模块是否加载**? 检查日志中是否有自定义配置类的输出 2. **Security 配置是否生效**? 检查日志 `SecurityFilterChain` 是自定义还是默认 3. **CSRF 是否拦截请求**? 查看日志 `Invalid CSRF token` 4. **前端 token 是否保存**? 检查 `localStorage.getItem('token')` 5. **路由守卫是否正确判断登录状态**? 添加调试日志 **更新记录**: | 版本 | 日期 | 更新内容 | | ---- | ---------- | ------------------------------- | | v1.0 | 2026-02-10 | 初始版本 | | v1.1 | 2026-02-13 | 新增 E2E 测试规范(6.3节) | | v1.2 | 2026-02-13 | 新增服务稳定性配置规范(8.3节) | | v1.3 | 2026-02-13 | 新增跨业务数据访问规范(8.4节) | | v1.4 | 2026-03-19 | 新增开发经验总结(11节) | | v1.5 | 2026-03-20 | 新增代码优化最佳实践(11.5节) |