SKILL.md 13 KB


name: "java-code-standard"

description: "SaaS 多租户 Java 后端代码开发规范。在编写、修改或审查任何 Java 后端代码时(Controller、Service、Mapper、配置类、工具类)必须遵循此规范。"

Java 后端代码开发规范

语言

  • 代码注释、日志信息、异常消息一律使用中文
  • 类名、方法名、变量名使用英文(驼峰命名)

项目结构

前端项目

目录 别名 说明
adminui 总后台 平台管理端,管理所有租户、定价、系统配置等
agentui 代理后台 代理商管理端
saas-mgnui 租户后台 租户自管理后台,管理租户下的公司、用户、课程等
saas-companyui 销售后台 销售/公司端后台,管理销售团队、客户、订单等

后端模块

fs-common      — 公共工具、BaseController、RedisCache、TenantLock、RedisTenantContext、常量等
fs-service     — 业务接口层:Service/ServiceImpl、Mapper、Domain、枚举、工具类
fs-admin       — 接口模块:总后台(adminui 的后端)
fs-saas-admin  — 接口模块:租户后台(saas-mgnui 的后端)
fs-saas-company — 接口模块:销售后台(saas-companyui 的后端)
fs-user-app    — 客户端(包括商城、看课相关接口)
fs-task        — 定时任务模块

前端 ↔ 后端对应关系

前端项目 主要后端模块 调用路径特征
adminui fs-admin /admin/*/system/*/course/*(跨租户)
agentui fs-admin / 部分代理专用接口 /admin/*、代理专用路径
saas-mgnui fs-saas-admin /course/*/qw/*/company/*
saas-companyui fs-saas-company /course/*/qw/*/company/*(租户内)

命名规范

类命名

类型 规范 示例
Controller XxxController AdminConsumeRecordController
Service 接口 IXxxService ITokenBalanceService
Service 实现 XxxServiceImpl TokenBalanceServiceImpl
Mapper XxxMapper TenantWalletTxnMapper
实体 Xxx (与表名对应) TenantWalletTxn
VO XxxVO ConsumeRecordExportVO
枚举 XxxEnum ConsumeTypeEnum
工具类 XxxUtils / XxxHelper BillingLabelUtils
配置类 XxxConfig RedisConfig
常量 FsConstants(集中) 或 XxxConstants

方法命名

操作 Controller Service
分页列表 list() selectXxxList()
详情 getInfo() selectXxxById()
新增 add() insertXxx()
修改 edit() updateXxx()
删除 remove() deleteXxxByIds()
导出 export()

字段命名

  • 实体字段与数据库列名对应(下划线 → 驼峰)
  • 布尔字段用 isXxx 前缀
  • 金额用 BigDecimal,数量用 LongInteger

Controller 规范

@RestController
@RequestMapping("/admin/xxx")
public class AdminXxxController extends BaseController {

    @Autowired
    private SomeMapper someMapper;

    @PreAuthorize("@ss.hasPermi('admin:xxx:list')")
    @GetMapping("/list")
    public TableDataInfo list(@RequestParam(required = false) Long tenantId,
                              @RequestParam(required = false) String name) {
        startPage();                               // ← 分页
        List<Some> list = someMapper.selectList(wrapper);
        return getDataTable(list);                 // ← 返回 TableDataInfo
    }

    @GetMapping("/{id}")
    public AjaxResult getInfo(@PathVariable Long id) {
        return AjaxResult.success(someMapper.selectById(id));
    }
}

要点

  1. Controller 必须继承 BaseController
  2. 分页查询必须先调 startPage()(PageHelper),再查 Mapper
  3. 列表返回 TableDataInfogetDataTable(list)),详情返回 AjaxResult
  4. 需要权限控制的加 @PreAuthorize("@ss.hasPermi('xxx')")
  5. 入参用 @RequestParam(GET)或 @RequestBody(POST)
  6. 当 GET 方法的 @RequestParam 参数超过 3 个时,必须封装为请求实体类(XxxQueryReq),包含分页参数、筛选条件等 ```java // 错误:参数过多 ← 反例 public TableDataInfo list(@RequestParam(required = false) String bizType, @RequestParam(required = false) Long companyId, @RequestParam(required = false) String beginTime, @RequestParam(required = false) String endTime) { ... }

// 正确:封装为请求实体 ← 正例 @Data public class ConsumeRecordQueryReq {

   private Integer pageNum;
   private Integer pageSize;
   private String bizType;
   private Long companyId;
   private String beginTime;
   private String endTime;

}

public TableDataInfo list(ConsumeRecordQueryReq req) {

   Long tenantId = SecurityUtils.getTenantId();
   PageDomain pageDomain = TableSupport.buildPageRequest();
   // ...

}

7. 请求实体类放在对应 Controller 模块的 `domain` 包下,命名规范:`XxxQueryReq`(查询)或 `XxxReq`(普通请求)

---

## Service 规范

```java
public interface IXxxService {
    List<Xxx> selectXxxList(Xxx xxx);
    Xxx selectXxxById(Long id);
    int insertXxx(Xxx xxx);
    int updateXxx(Xxx xxx);
    int deleteXxxByIds(Long[] ids);
}

@Slf4j
@Service
public class XxxServiceImpl implements IXxxService {
    @Autowired
    private XxxMapper xxxMapper;

    @Override
    public List<Xxx> selectXxxList(Xxx xxx) {
        return xxxMapper.selectXxxList(xxx);
    }
}

要点

  1. Service 接口用 I 前缀,实现类用 Impl 后缀
  2. 实现类加 @Service@Slf4j
  3. 依赖注入用 @Autowired 字段注入
  4. 事务用 @Transactional(rollbackFor = Exception.class)
  5. 所有日志用 log.info/warn/error 打印关键参数

多租户切库

切数据库

// ThreadLocal 设置数据源
DynamicDataSourceContextHolder.setDataSourceType("tenant:123");

// 清理
DynamicDataSourceContextHolder.clearDataSourceType();

切 Redis 缓存

// 必须与切库同步设置,否则 RedisCache 拿不到租户前缀
RedisTenantContext.setTenantId(tenantId);

// 清理
RedisTenantContext.clear();

定时任务切库模式(fs-ipad-task)

定时任务不依赖 @TenantDataScope 切面(线程池异步丢失上下文),须显式切库:

// === 主线程 ===
sopTenantDataSourceAspect.switchTenant(tenantId);
try {
    // 业务逻辑
} finally {
    sopTenantDataSourceAspect.clear();
}

// === 异步线程 ===
CompletableFuture.runAsync(() -> {
    sopTenantDataSourceAspect.switchTenant(tenantId);
    try {
        // 业务逻辑
    } finally {
        sopTenantDataSourceAspect.clear();
    }
});

switchTenant() 内部同时设置 DynamicDataSourceContextHolderRedisTenantContext,无需额外操作。


Redis 缓存规范

使用 RedisCache(自动加租户前缀)

@Autowired
private RedisCache redisCache;

// 读
String val = redisCache.getCacheObject("company:token:balance:" + companyId);

// 写
redisCache.setCacheObject("company:token:balance:" + companyId, String.valueOf(balance));

RedisCache 使用 TenantKeyRedisSerializer,自动在 key 前加 tenantid:{租户ID}: 前缀。 禁止使用 StringRedisTemplate 操作业务缓存,除非确实不需要租户隔离。

分布式锁(TenantLock)

@Autowired
private TenantLock tenantLock;

RLock lock = tenantLock.getLock(FsConstants.COMPANY_TOKEN_LOCK + companyId);
if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
    try {
        // 临界区
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

TenantLock 自动加租户前缀,实现租户级锁隔离。禁止直接使用 RedissonClient.getLock() 操作租户数据。


Mapper / MyBatis 规范

  • Mapper 接口放在各模块的 mapper 包下
  • XML 放在 resources/mapper/xxx/ 目录
  • 使用 MyBatis-Plus 的 BaseMapper<T>,简单 CRUD 直接调用父类方法
  • 复杂查询在 XML 中写 SQL,方法定义在 Mapper 接口中
  • 多个参数用 @Param 标注
public interface CompanyMapper extends BaseMapper<Company> {
    List<Company> selectCompanyByIds2(@Param("companyIds") Set<Long> companyIds);
}

异常处理

// 业务异常
throw new CustomException("租户不存在");

// 运行时异常
throw new RuntimeException("系统异常,请稍后重试");
  • 可预期的业务异常用 CustomException
  • 不可预期的系统异常用 RuntimeException
  • 不要吞异常:catch 后必须打日志或向上抛

日志规范

log.info("Token充值成功, companyId:{}, amount:{}", companyId, amount);
log.warn("获取锁失败, companyId:{}", companyId);
log.error("Token扣减异常, companyId:{}, amount:{}", companyId, amount, e);
  1. 日志用 {} 占位符,不要字符串拼接
  2. 打印关键业务参数(ID、金额、数量)
  3. error 级别必须把异常对象 e 作为最后一个参数
  4. 不要打印敏感信息(密码、手机号明文)

常量定义

常量统一放在 fs-common/constant/FsConstants.java

public class FsConstants {
    /** Token 余额缓存 Key 前缀 */
    public static final String COMPANY_TOKEN_KEY = "company:token:balance:";
    /** Token 分布式锁 Key 前缀 */
    public static final String COMPANY_TOKEN_LOCK = "company:token:lock:";
}

避免魔法字符串散落在各处。


代码风格

  1. 缩进:4 空格
  2. 行宽:不超过 120 字符
  3. 空行:方法之间空一行,逻辑块之间空一行
  4. 注释:复杂逻辑加行内注释,方法加 Javadoc(至少一句话说明用途)
  5. import:不用通配符 *,IDE 自动整理
  6. 字段顺序@Autowired 依赖 → 普通字段 → 常量
  7. 不要:未使用的 import、未使用的变量、System.out.printlne.printStackTrace()
  8. 正则表达式可读性Pattern.compile()禁止使用 Unicode 转义字符(\uXXXX)表示中文字符,直接用中文字面量。转义序列对阅读和维护不友好。 ```java // 错误:Unicode 转义,完全不可读 ← 反例 private static final Pattern P = Pattern.compile( "\u662f\u4e0d\u662f\u505a|\u662f\u505a.*\u7684\u5417");

// 正确:直接写中文,一目了然 ← 正例 private static final Pattern P = Pattern.compile(

   "是不是做|是做.*的吗|是做.*吗|你们是卖|你们卖|你们是做|你们做|你们是搞"
   + "|是不是卖|有没有卖|是做.*销售|业务范围|你们干什么的|你们是做什么的|你们主要做"
   + "|你们家是|你们公司是做|你们这边是做|你们这是做");

```


数据库规范

  1. 表名、列名:小写 + 下划线(tenant_wallet_txn
  2. 必须有主键(id),使用自增或雪花算法
  3. 时间字段用 datetime,对应 Java 的 DateLocalDateTime
  4. 金额字段用 decimal(18,2),对应 BigDecimal
  5. 软删除用 is_del 字段(0 正常 1 删除)

前端新增/修改页面规范

重要:当新增或修改任何前端页面时,必须在回复末尾输出以下清单,供用户在系统中录入菜单和权限。

输出格式

1. 页面文件路径

系统 文件路径 菜单名称
saas-mgnui src/views/xxx/index.vue XX管理
saas-companyui src/views/xxx/index.vue XX管理

2. API 文件路径

系统 API 文件路径
saas-mgnuisaas-companyui src/api/xxx/xxx.js

3. 权限标识符

从后端 Controller 的 @PreAuthorize("@ss.hasPermi('xxx')") 注解中提取:

权限标识符 说明
module:page:list 列表查询
module:page:export 导出
module:page:add 新增
module:page:edit 修改
module:page:remove 删除

4. 菜单录入信息(系统管理→菜单管理)

系统 菜单名称 权限标识 组件路径 路由地址
saas-mgnui XX管理 module:page:list xxx/index xxx

规则

  1. 路由不需要修改前端 router 文件,系统通过后端菜单表动态生成路由
  2. 菜单组件路径格式:相对于 src/views/ 的路径,不含 .vue 后缀
  3. 权限标识符必须与后端 Controller 的 @PreAuthorize 完全一致
  4. 如果页面涉及多个系统(如同时需要租户后台和销售后台),必须分别列出各系统的文件路径和菜单信息

检查清单

提交代码前确认:

  • Controller 继承了 BaseController,分页调了 startPage()
  • 多租户场景:切库 + 切缓存 + 锁都要有租户前缀
  • Redis 操作使用 RedisCache(非 StringRedisTemplate
  • 分布式锁使用 TenantLock.getLock()(非 RedissonClient 裸调)
  • 日志中文、占位符、关键参数完整
  • 无硬编码魔法值(用常量或枚举)
  • e.printStackTrace()System.out.println
  • 无未使用的 import