# Ether 智慧物业管理平台 - 开发规范

**文档版本**: v2.0
**最后更新**: 2026-03-28
**适用范围**: ether-pms 单体应用

---

## 一、代码规范

### 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<WorkOrderVO> create(@RequestBody @Valid WorkOrderCreateRequest request) {
        return Result.success(workOrderService.create(request));
    }

    // 查询单个: GET /{id}
    @GetMapping("/{id}")
    @Operation(summary = "获取工单详情")
    public Result<WorkOrderVO> getById(@PathVariable UUID id) {
        return Result.success(workOrderService.getById(id));
    }

    // 查询列表: GET
    @GetMapping
    @Operation(summary = "分页查询工单")
    public Result<Page<WorkOrderVO>> page(WorkOrderQueryRequest request) {
        return Result.success(workOrderService.page(request));
    }

    // 更新资源: PUT /{id}
    @PutMapping("/{id}")
    @Operation(summary = "更新工单")
    public Result<WorkOrderVO> update(@PathVariable UUID id,
                                       @RequestBody @Valid WorkOrderUpdateRequest request) {
        return Result.success(workOrderService.update(id, request));
    }

    // 删除资源: DELETE /{id}
    @DeleteMapping("/{id}")
    @Operation(summary = "删除工单")
    public Result<Void> delete(@PathVariable UUID id) {
        workOrderService.delete(id);
        return Result.success();
    }

    // 业务操作: POST /{id}/action
    @PostMapping("/{id}/assign")
    @Operation(summary = "分配工单")
    public Result<WorkOrderVO> 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<T> {
    private Integer code;       // 状态码: 200成功, 其他错误
    private String message;     // 提示信息
    private T data;             // 数据
    private Long timestamp;     // 时间戳

    // 成功响应
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        result.setTimestamp(System.currentTimeMillis());
        return result;
    }

    // 错误响应
    public static <T> Result<T> error(String message) {
        Result<T> 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<T> {
    private List<T> 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<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), 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, "系统繁忙，请稍后重试");
    }
}
```

---

## 五、日志规范

### 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<WorkOrderCreateRequest> requests) {
    List<WorkOrder> orders = requests.stream()
            .map(this::convertToEntity)
            .collect(Collectors.toList());

    // 批量保存
    workOrderRepository.saveAll(orders);
}

// 2. 分页查询
public Page<WorkOrder> 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
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.4 健康检查配置

---

## 九、代码质量规范

### 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<String, Object> result = new HashMap<>();
List<WorkOrder> items = new ArrayList<>();
```

### 9.4 类型转换安全

**规则**: 使用泛型方法避免 unchecked cast 警告

```java
// 错误示例
Map<String, Object> result = (Map<String, Object>) objectMapper.readValue(json, Map.class);

// 正确示例
Map<String, Object> result = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
```

### 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节） |
