eba4f07832
按用户提醒 + 本次实际踩坑记录: - 项目用 rui-common-feign 自定义注册机制 - spring.factories 是 Feign 唯一注册渠道(@EnableFeignClients 包扫描不生效) - 添加新 FeignClient 必须同步更新 spring.factories - 漏写会导致运行时 NPE
419 lines
11 KiB
Markdown
419 lines
11 KiB
Markdown
# 编码规范
|
||
|
||
## 🎯 通用原则
|
||
|
||
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)` 硬编码字符串** |