33 KiB
33 KiB
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 -DskipTests 后 mvn 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.log、console.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 问题排查清单
当遇到问题时,按以下顺序排查:
- 后端模块是否加载? 检查日志中是否有自定义配置类的输出
- Security 配置是否生效? 检查日志
SecurityFilterChain是自定义还是默认 - CSRF 是否拦截请求? 查看日志
Invalid CSRF token - 前端 token 是否保存? 检查
localStorage.getItem('token') - 路由守卫是否正确判断登录状态? 添加调试日志
更新记录:
| 版本 | 日期 | 更新内容 |
|---|---|---|
| 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节) |