eba4f07832
按用户提醒 + 本次实际踩坑记录: - 项目用 rui-common-feign 自定义注册机制 - spring.factories 是 Feign 唯一注册渠道(@EnableFeignClients 包扫描不生效) - 添加新 FeignClient 必须同步更新 spring.factories - 漏写会导致运行时 NPE
11 KiB
11 KiB
编码规范
🎯 通用原则
- 可读性优先 - 代码是写给人看的,顺便给机器执行
- DRY 原则 - Don't Repeat Yourself
- 单一职责 - 一个类/方法只做一件事
- 开闭原则 - 对扩展开放,对修改关闭
📝 Java 编码规范
命名规范
| 类型 | 规范 | 示例 |
|---|---|---|
| 类名 | 大驼峰 | UserService, OrderController |
| 方法名 | 小驼峰 | getUserById(), createOrder() |
| 变量名 | 小驼峰 | userName, orderList |
| 常量 | 全大写下划线 | MAX_RETRY_COUNT, DEFAULT_TIMEOUT |
| 包名 | 全小写 | com.rui.service.user |
代码格式
// 正确的类定义
@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;
}
}
注释规范
/**
* 用户服务实现类
*
* @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() |
组件规范
<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 |
必备字段
-- 所有表必须包含以下字段
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。
// ❌ 错误示例:使用字符串字段名,容易拼写错误,重构时容易遗漏
new QueryWrapper<User>().eq("user_name", username)
.like("phone", phone);
// ✅ 正确示例:使用 LambdaQueryWrapper,类型安全,重构友好
new LambdaQueryWrapper<User>()
.eq(User::getUserName, username)
.like(User::getPhone, phone);
优势:
- 类型安全:编译期检查,字段不存在会报错
- 防误写:避免字符串拼写错误
- 重构友好:IDE 重构时自动更新引用
- 可读性:直接看到实体字段,更清晰
🔒 安全规范
- SQL 注入防护 - 使用 MyBatis 参数绑定,禁止字符串拼接 SQL
- XSS 防护 - 前端转义输出,后端校验输入
- 敏感信息 - 密码必须加密存储,日志中禁止输出敏感信息
- 接口鉴权 - 所有接口必须校验权限(白名单除外)
🧪 测试规范
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 | 构建/工具相关 |
示例
feat(cashier): 添加订单退款功能
- 支持部分退款
- 支持原路退回
- 添加退款记录表
Closes #123
最后提醒:编码规范是为了团队协作,请务必遵守!
🌐 Feign 客户端注册规范
rui-common-feign 提供自定义 Feign 注册机制(CloudFeignAutoConfiguration +
CustomFeignClientsRegistrar),与 Spring Cloud 默认的包扫描机制不同。
注册渠道
所有 @FeignClient 接口必须列在 META-INF/spring.factories 中:
# 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 步骤
- 定义
@FeignClient接口(带contextId/path/fallbackFactory) - 必须在
META-INF/spring.factories中追加类名 - 漏写第 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()字符串
✅ 正确示例
// 查询接口 - 数据不存在
@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 后:
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)硬编码字符串