ether-docs/02-DESIGN/standards/DEVELOPMENT_STANDARDS.md

1155 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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