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

33 KiB
Raw Blame History

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

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


一、代码规范

1.1 基础规范

项目 规范 说明
JDK版本 Java 17+ 使用LTS版本
编码规范 Alibaba Java Coding Guidelines 遵循阿里巴巴编码规范
代码格式化 统一使用IDE格式化配置 避免格式差异
文件编码 UTF-8 所有源文件

1.2 命名规范

// 类名: 大驼峰
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 类结构规范

/**
 * 工单服务
 * 负责工单的创建、分配、流转等业务逻辑
 *
 * @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 字段规范

-- 必备字段
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 规范

@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 统一响应格式

@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 分页请求/响应

// 分页请求
@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 异常分类

// 业务异常
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 全局异常处理

@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 日志格式

@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 单元测试

@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 核心方法实现

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 测试用例编写规范

// ✅ 正确示例:使用 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 测试数据管理

class EtherE2ETestRunner {
  // 测试数据存储
  private testData = {
    createdUserId: "",
    createdRoleId: "",
    createdProjectId: "",
    createdWorkOrderId: "",
  };

  // 测试前准备数据
  async prepareTestData() {
    // 确保测试用户存在
    // 确保测试项目存在
  }

  // 测试后清理数据
  async cleanupTestData() {
    // 删除创建的测试数据
  }
}

6.3.7 测试报告

测试报告自动保存至 docs/测试用例/测试报告_{timestamp}.md,包含:

  • 执行时间
  • 各模块通过率
  • 失败用例详情
  • 错误堆栈信息

七、安全规范

7.1 敏感数据处理

// 密码加密
@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 接口安全

// 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 数据库性能

// 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 异步处理

// 异步方法
@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 连接池参数,避免连接泄漏和资源耗尽:

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 健康端点,支持服务监控:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always

8.3.3 日志配置

必须配置文件日志输出,便于问题排查:

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" 或提交前自动优化

// 错误示例
import java.time.LocalDateTime;  // 未使用
import java.util.List;           // 未使用
import java.util.Map;            // 未使用

public class UserService {
    // ...
}

// 正确示例
public class UserService {
    // ...
}

9.2 变量使用

规则: 禁止声明后未使用的局部变量和字段

例外情况:

  • 测试代码中的预期结果验证
  • 接口实现要求的必须参数
// 错误示例
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必须指定泛型参数

// 错误示例
Map result = new HashMap();  // 原始类型
List items = new ArrayList();  // 原始类型

// 正确示例
Map<String, Object> result = new HashMap<>();
List<WorkOrder> items = new ArrayList<>();

9.4 类型转换安全

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

// 错误示例
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使用推荐替代方案

// 错误示例
BigDecimal result = amount.divide(100, BigDecimal.ROUND_HALF_UP);  // 废弃

// 正确示例
BigDecimal result = amount.divide(100, RoundingMode.HALF_UP);

9.6 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 -DskipTestsmvn spring-boot:run

教训: 子模块代码修改后必须重新 mvn install,否则启动类不会加载新代码。

11.2 Spring Security 配置

问题: Spring Boot 自动配置与自定义配置冲突

解决: 在 application.yml 中排除自动配置

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration

教训: 前后端分离项目必须禁用 CSRF使用 JWT 认证。

11.3 测试账号管理

要求 说明
密码格式 BCrypt 加密格式
文档同步 修改密码后必须更新 docs/08-DATABASE/test-users.sql
生成密码 使用 BCryptPasswordEncoder 生成
// 生成 BCrypt 密码
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("Admin@123");

11.4 前端调试技巧

问题 解决
热更新不生效 重启前端服务
路由跳转失败 检查路由守卫 isLoggedIn() 实现
登录状态判断错误 优先读取 localStorage 而非 store 状态
// 正确示例isLoggedIn 同时检查 localStorage 和 store
const isLoggedIn = () => {
  return !!localStorage.getItem('token') || !!token.value
}

11.5 代码优化最佳实践

调试代码清理规范:

场景 操作
上线前 移除所有 console.logconsole.error
上线前 移除所有 log.info 调试日志
生产环境 敏感日志改为 log.debug
上线前 移除路由守卫中的调试日志

代码示例:

// ❌ 错误:生产环境使用 info 级别
log.info("Authenticated user: {}", username);

// ✅ 正确:认证日志使用 debug 级别
log.debug("Authenticated user: {}", username);
// ❌ 错误:保留调试代码
console.log('准备跳转...')
router.push('/').then(() => {
  console.log('跳转完成', router.currentRoute.value.path)
})

// ✅ 正确:简洁的跳转逻辑
router.push('/')

11.6 快速测试命令

# 后端启动
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节)