Files
rui-docs/standards/coding-standards.md
vifo eba4f07832 docs(standards): 增加 Feign 客户端注册规范章节
按用户提醒 + 本次实际踩坑记录:
- 项目用 rui-common-feign 自定义注册机制
- spring.factories 是 Feign 唯一注册渠道(@EnableFeignClients 包扫描不生效)
- 添加新 FeignClient 必须同步更新 spring.factories
- 漏写会导致运行时 NPE
2026-06-07 17:54:04 +08:00

419 lines
11 KiB
Markdown
Raw Permalink 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.
# 编码规范
## 🎯 通用原则
1. **可读性优先** - 代码是写给人看的,顺便给机器执行
2. **DRY 原则** - Don't Repeat Yourself
3. **单一职责** - 一个类/方法只做一件事
4. **开闭原则** - 对扩展开放,对修改关闭
---
## 📝 Java 编码规范
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 类名 | 大驼峰 | `UserService`, `OrderController` |
| 方法名 | 小驼峰 | `getUserById()`, `createOrder()` |
| 变量名 | 小驼峰 | `userName`, `orderList` |
| 常量 | 全大写下划线 | `MAX_RETRY_COUNT`, `DEFAULT_TIMEOUT` |
| 包名 | 全小写 | `com.rui.service.user` |
### 代码格式
```java
// 正确的类定义
@Service
public class UserServiceImpl implements IUserService {
private final UserMapper userMapper;
private final RedisUtil redisUtil;
// 构造器注入(推荐)
public UserServiceImpl(UserMapper userMapper, RedisUtil redisUtil) {
this.userMapper = userMapper;
this.redisUtil = redisUtil;
}
// 方法注释
/**
* 根据ID获取用户信息
* @param userId 用户ID
* @return 用户信息
*/
@Override
public UserDTO getUserById(Long userId) {
// 先查缓存
UserDTO user = redisUtil.get("user:" + userId);
if (user != null) {
return user;
}
// 再查数据库
User entity = userMapper.selectById(userId);
if (entity == null) {
throw new BizException("用户不存在");
}
// 转换并缓存
user = convertToDTO(entity);
redisUtil.set("user:" + userId, user, 3600);
return user;
}
}
```
### 注释规范
```java
/**
* 用户服务实现类
*
* @author pigeon
* @since 2024-01-01
*/
@Service
public class UserServiceImpl {
/**
* 获取用户详情
*
* @param userId 用户ID,不能为空
* @return 用户详情DTO
* @throws BizException 用户不存在时抛出
*/
public UserDetailDTO getDetail(Long userId) {
// ...
}
}
```
---
## 🌐 TypeScript/Vue 编码规范
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件名 | 大驼峰 | `UserTable.vue`, `OrderForm.vue` |
| 组合式函数 | use前缀 | `useUser()`, `useOrder()` |
| 类型定义 | 大驼峰 | `UserDTO`, `OrderFormData` |
| 常量 | 全大写下划线 | `API_BASE_URL`, `PAGE_SIZE` |
| 变量/函数 | 小驼峰 | `userList`, `getUserList()` |
### 组件规范
```vue
<script setup lang="ts">
/**
* 用户管理组件
*
* @description 展示用户列表,支持增删改查
* @author pigeon
*/
import { ref, onMounted } from 'vue'
import type { UserDTO } from '@/types'
// Props 定义
interface Props {
deptId?: number
}
const props = withDefaults(defineProps<Props>(), {
deptId: undefined
})
// Emits 定义
const emit = defineEmits<{
refresh: []
}>()
// 响应式数据
const userList = ref<UserDTO[]>([])
const loading = ref(false)
// 方法
async function loadUserList() {
loading.value = true
try {
const res = await userApi.getList({ deptId: props.deptId })
userList.value = res.data
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
loadUserList()
})
</script>
```
---
## 🗄️ 数据库规范
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 表名 | 下划线、复数 | `sys_user`, `cashier_order` |
| 字段名 | 下划线 | `user_name`, `created_at` |
| 索引名 | idx_前缀 | `idx_user_name` |
| 外键 | fk_前缀 | `fk_order_user` |
### 必备字段
```sql
-- 所有表必须包含以下字段
CREATE TABLE example_table (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
-- 业务字段
name VARCHAR(100) NOT NULL COMMENT '名称',
-- 审计字段(由框架自动填充)
create_by VARCHAR(64) COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '删除标志(0-正常,1-删除)',
tenant_id BIGINT COMMENT '租户ID',
-- 索引
INDEX idx_name (name)
) COMMENT='示例表';
```
### MyBatis Plus 查询规范
**优先使用 `LambdaQueryWrapper`,避免使用字符串字段名的 `QueryWrapper`。**
```java
// ❌ 错误示例:使用字符串字段名,容易拼写错误,重构时容易遗漏
new QueryWrapper<User>().eq("user_name", username)
.like("phone", phone);
// ✅ 正确示例:使用 LambdaQueryWrapper,类型安全,重构友好
new LambdaQueryWrapper<User>()
.eq(User::getUserName, username)
.like(User::getPhone, phone);
```
**优势:**
- **类型安全**:编译期检查,字段不存在会报错
- **防误写**:避免字符串拼写错误
- **重构友好**:IDE 重构时自动更新引用
- **可读性**:直接看到实体字段,更清晰
---
## 🔒 安全规范
1. **SQL 注入防护** - 使用 MyBatis 参数绑定,禁止字符串拼接 SQL
2. **XSS 防护** - 前端转义输出,后端校验输入
3. **敏感信息** - 密码必须加密存储,日志中禁止输出敏感信息
4. **接口鉴权** - 所有接口必须校验权限(白名单除外)
---
## 🧪 测试规范
### Java 测试
```java
@SpringBootTest
class UserServiceTest {
@Autowired
private IUserService userService;
@Test
@DisplayName("根据ID查询用户-正常情况")
void getUserById_Success() {
// Given
Long userId = 1L;
// When
UserDTO user = userService.getUserById(userId);
// Then
assertThat(user).isNotNull();
assertThat(user.getId()).isEqualTo(userId);
}
@Test
@DisplayName("根据ID查询用户-用户不存在")
void getUserById_NotFound() {
// Given
Long userId = 999L;
// Then
assertThrows(BizException.class, () -> {
userService.getUserById(userId);
});
}
}
```
---
## 📝 Git 提交规范
```
<type>(<scope>): <subject>
<body>
<footer>
```
### Type 类型
| 类型 | 说明 |
|------|------|
| feat | 新功能 |
| fix | 修复 bug |
| docs | 文档更新 |
| style | 代码格式(不影响功能) |
| refactor | 重构 |
| test | 测试相关 |
| chore | 构建/工具相关 |
### 示例
```bash
feat(cashier): 添加订单退款功能
- 支持部分退款
- 支持原路退回
- 添加退款记录表
Closes #123
```
---
> **最后提醒**:编码规范是为了团队协作,请务必遵守!
---
## 🌐 Feign 客户端注册规范
`rui-common-feign` 提供自定义 Feign 注册机制(`CloudFeignAutoConfiguration` +
`CustomFeignClientsRegistrar`),**与 Spring Cloud 默认的包扫描机制不同**。
### 注册渠道
所有 `@FeignClient` 接口**必须**列在 `META-INF/spring.factories` 中:
```properties
# rui-common-{module}/src/main/resources/META-INF/spring.factories
com.rui.common.feign.CloudFeignAutoConfiguration=\
com.rui.{module}.feign.YourFeignClient,\
com.rui.{module}.feign.AnotherFeignClient
```
### 为什么不能只靠包扫描
- 项目使用自定义的 `CustomFeignClientsRegistrar`**只有当 `@CloudEnableFeignClients`
注解存在时才会触发包扫描**
- 项目**零处**使用 `@CloudEnableFeignClients` 注解
- 因此 `spring.factories` 是项目 Feign 客户端的**唯一**注册渠道
### 添加新 FeignClient 步骤
1. 定义 `@FeignClient` 接口(带 `contextId` / `path` / `fallbackFactory`
2. **必须**在 `META-INF/spring.factories` 中追加类名
3. 漏写第 2 步 → Bean 未注册 → 运行时 NPE"no qualifying bean of type ..."
### ❌ 禁止
- 只定义 `@FeignClient` 接口但忘了列 `spring.factories`(最常见的坑)
- 期待 Spring Cloud 默认的包扫描会帮你发现(项目里不会)
---
## 📦 Result 返回规范
`com.rui.common.core.result.Result` 是统一的 API 响应封装。所有 controller 必须遵守:
### 字段语义
| 字段 | 类型 | 用途 |
|---|---|---|
| `error` | int | HTTP 风格状态码(200/400/401/403/404/500/503 等) |
| `code` | String | **业务编码,前端 i18n key**(如 `DATA_NOT_FOUND` |
| `message` | String | 默认中文提示,可由前端 i18n 覆盖 |
| `data` | T | 业务数据 |
### 调用规范
| 场景 | 调用方式 | 备注 |
|---|---|---|
| 成功 | `Result.ok(data)` | data 可为 null,但**列表场景应返回 `emptyList`** |
| 业务校验失败 | `Result.fail(400, "msg")``Result.fail(ResultCode.X, "msg")` | 优先用枚举 |
| 未授权 | `Result.fail(401, "msg")` | 框架层 `GlobalExceptionHandler` 统一处理 |
| 无权限 | `Result.fail(403, "msg")` | 同上 |
| **数据不存在** | **`Result.failNotFound(ResultCode.DATA_NOT_FOUND, key)`** | **推荐写法**:key 放 data 字段便于前端模板替换 |
| 资源不存在(泛指) | `Result.fail(ResultCode.NOT_FOUND)` | 不带 key 的场景 |
| 服务降级 | `Result.fail(503, "服务降级: msg")` | Feign fallback 等场景 |
| 通用失败 | `Result.fail("msg")` | 兜底 |
### ❌ 禁止写法
- **`Result.ok(null)` 表示"未找到"** —— 反直觉(HTTP 200 + null),且与 `fail(404)` 语义冲突
- **message 中拼接 key** —— 如 `"字典不存在: " + dictCode`,应该用 `failNotFound(DATA_NOT_FOUND, dictCode)` 让前端用 i18n 模板 `"字典[${data}]不存在"`
- **数字 code 字符串比较** —— 应该用 `ResultCode` 枚举的 `getCode()` 字符串
### ✅ 正确示例
```java
// 查询接口 - 数据不存在
@GetMapping("/dict/getByCode/{dictCode}")
public Result<Map<String, Object>> getDictByCode(@PathVariable String dictCode) {
SysDictType dict = dictTypeService.findByCode(dictCode);
if (dict == null) {
return Result.failNotFound(ResultCode.DATA_NOT_FOUND, dictCode);
}
return Result.ok(buildDictResult(dict));
}
// 业务校验失败
@PostMapping("/save")
public Result<Void> save(@RequestBody @Valid SysDictDTO dto) {
if (dictService.isCodeExists(dto.getCode())) {
return Result.fail(400, "字典编码已存在: " + dto.getCode());
}
return Result.ok();
}
// 列表查询(即使是空也要返回空集合)
@GetMapping("/list")
public Result<List<SysDict>> list() {
return Result.ok(dictService.list()); // 不要 Result.ok(null)
}
```
### i18n 配合示例
前端拿到 `Result` 后:
```javascript
const i18nMap = {
'DATA_NOT_FOUND': '数据[{0}]不存在', // 占位符 {0} 用 data 字段填充
'AUTH_UNAUTHORIZED': '请先登录',
};
if (result.code === 'DATA_NOT_FOUND') {
showError(i18nMap['DATA_NOT_FOUND'].replace('{0}', result.data));
}
```
### 相关枚举
- `com.rui.common.core.result.ResultCode` —— 业务 code 枚举(404 用 `DATA_NOT_FOUND`401 用 `UNAUTHORIZED` 等)
- 新增业务 code 时在 `ResultCode` 加枚举值,**不要直接 `Result.fail(int, String, String)` 硬编码字符串**