name: "java-code-standard"
| 目录 | 别名 | 说明 |
|---|---|---|
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,数量用 Long 或 Integer@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));
}
}
BaseControllerstartPage()(PageHelper),再查 MapperTableDataInfo(getDataTable(list)),详情返回 AjaxResult@PreAuthorize("@ss.hasPermi('xxx')")@RequestParam(GET)或 @RequestBody(POST)@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);
}
}
I 前缀,实现类用 Impl 后缀@Service 和 @Slf4j@Autowired 字段注入@Transactional(rollbackFor = Exception.class)log.info/warn/error 打印关键参数// ThreadLocal 设置数据源
DynamicDataSourceContextHolder.setDataSourceType("tenant:123");
// 清理
DynamicDataSourceContextHolder.clearDataSourceType();
// 必须与切库同步设置,否则 RedisCache 拿不到租户前缀
RedisTenantContext.setTenantId(tenantId);
// 清理
RedisTenantContext.clear();
定时任务不依赖 @TenantDataScope 切面(线程池异步丢失上下文),须显式切库:
// === 主线程 ===
sopTenantDataSourceAspect.switchTenant(tenantId);
try {
// 业务逻辑
} finally {
sopTenantDataSourceAspect.clear();
}
// === 异步线程 ===
CompletableFuture.runAsync(() -> {
sopTenantDataSourceAspect.switchTenant(tenantId);
try {
// 业务逻辑
} finally {
sopTenantDataSourceAspect.clear();
}
});
switchTenant()内部同时设置DynamicDataSourceContextHolder和RedisTenantContext,无需额外操作。
@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操作业务缓存,除非确实不需要租户隔离。
@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 包下resources/mapper/xxx/ 目录BaseMapper<T>,简单 CRUD 直接调用父类方法@Param 标注public interface CompanyMapper extends BaseMapper<Company> {
List<Company> selectCompanyByIds2(@Param("companyIds") Set<Long> companyIds);
}
// 业务异常
throw new CustomException("租户不存在");
// 运行时异常
throw new RuntimeException("系统异常,请稍后重试");
CustomExceptionRuntimeExceptionlog.info("Token充值成功, companyId:{}, amount:{}", companyId, amount);
log.warn("获取锁失败, companyId:{}", companyId);
log.error("Token扣减异常, companyId:{}, amount:{}", companyId, amount, e);
{} 占位符,不要字符串拼接error 级别必须把异常对象 e 作为最后一个参数常量统一放在 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:";
}
避免魔法字符串散落在各处。
*,IDE 自动整理@Autowired 依赖 → 普通字段 → 常量System.out.println、e.printStackTrace()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(
"是不是做|是做.*的吗|是做.*吗|你们是卖|你们卖|你们是做|你们做|你们是搞"
+ "|是不是卖|有没有卖|是做.*销售|业务范围|你们干什么的|你们是做什么的|你们主要做"
+ "|你们家是|你们公司是做|你们这边是做|你们这是做");
```
tenant_wallet_txn)id),使用自增或雪花算法datetime,对应 Java 的 Date 或 LocalDateTimedecimal(18,2),对应 BigDecimalis_del 字段(0 正常 1 删除)重要:当新增或修改任何前端页面时,必须在回复末尾输出以下清单,供用户在系统中录入菜单和权限。
| 系统 | 文件路径 | 菜单名称 |
|---|---|---|
saas-mgnui |
src/views/xxx/index.vue |
XX管理 |
saas-companyui |
src/views/xxx/index.vue |
XX管理 |
| 系统 | API 文件路径 |
|---|---|
saas-mgnui 或 saas-companyui |
src/api/xxx/xxx.js |
从后端 Controller 的 @PreAuthorize("@ss.hasPermi('xxx')") 注解中提取:
| 权限标识符 | 说明 |
|---|---|
module:page:list |
列表查询 |
module:page:export |
导出 |
module:page:add |
新增 |
module:page:edit |
修改 |
module:page:remove |
删除 |
| 系统 | 菜单名称 | 权限标识 | 组件路径 | 路由地址 |
|---|---|---|---|---|
| saas-mgnui | XX管理 | module:page:list |
xxx/index |
xxx |
src/views/ 的路径,不含 .vue 后缀@PreAuthorize 完全一致提交代码前确认:
BaseController,分页调了 startPage()RedisCache(非 StringRedisTemplate)TenantLock.getLock()(非 RedissonClient 裸调)e.printStackTrace()、System.out.println