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

11 KiB
Raw Permalink Blame History

编码规范

🎯 通用原则

  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

代码格式

// 正确的类定义
@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 重构时自动更新引用
  • 可读性:直接看到实体字段,更清晰

🔒 安全规范

  1. SQL 注入防护 - 使用 MyBatis 参数绑定,禁止字符串拼接 SQL
  2. XSS 防护 - 前端转义输出,后端校验输入
  3. 敏感信息 - 密码必须加密存储,日志中禁止输出敏感信息
  4. 接口鉴权 - 所有接口必须校验权限(白名单除外)

🧪 测试规范

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 步骤

  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() 字符串

正确示例

// 查询接口 - 数据不存在
@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_FOUND401 用 UNAUTHORIZED 等)
  • 新增业务 code 时在 ResultCode 加枚举值,不要直接 Result.fail(int, String, String) 硬编码字符串