本文档记录个人在 Forge 项目中的编码习惯和偏好,所有代码编写(含 AI 辅助生成)必须遵循以下规范。
适用范围: forge-hotel 模块及后续所有业务模块 最后更新: 2026-08-17
@Autowired 字段注入,不使用 @RequiredArgsConstructor 构造器注入@Autowired 注解java
@RestController
@RequestMapping("/hotel/dish")
public class HotelDishController {
@Autowired
private HotelDishService hotelDishService;
}
@RequestParam@PathVariable(路径参数如 id)和确实无法用实体类表达的参数(如安全校验参数)才单独接收/{id}/status)属于操作类,可保持 @RequestParam// ❌ 错误 public RespInfo> dishPage(PageQuery pageQuery,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String name) { ... }
- Service 接口和 Mapper 方法签名同步使用实体类参数
- Mapper XML 中用 `@Param("query")` 标注,引用时用 `#{query.fieldName}`
### 1.3 URL 路径风格(传统 JDK 7 风格)
- **禁止使用 `{id}` 路径占位符**,统一使用方法名 + `@RequestParam` 方式传递 ID
- 所有带 ID 参数的接口,ID 作为查询参数而非路径参数
- 示例:
```java
// ✅ 正确 - 使用 @RequestParam
@GetMapping("/dish/detail")
public RespInfo<HotelDishVO> dishDetail(@RequestParam Long id) { ... }
@PostMapping("/dish/remove")
public RespInfo<Void> dishDelete(@RequestParam Long id) { ... }
@PutMapping("/dish/updateStatus")
public RespInfo<Void> dishUpdateStatus(@RequestParam Long id, @RequestParam String status) { ... }
// ❌ 错误 - 禁止使用 @PathVariable 和 {id}
@GetMapping("/dish/{id}")
public RespInfo<HotelDishVO> dishDetail(@PathVariable Long id) { ... }
@PostMapping("/dish/remove/{id}")
public RespInfo<Void> dishDelete(@PathVariable Long id) { ... }
/xxx/detail(原 /{id})/xxx/remove(原 /remove/{id} 或 /{id})/xxx/updateStatus(原 /{id}/status)/xxx/accept、/xxx/reject、/xxx/toggleparams 中:
```javascript
// ✅ 正确
request.get('/hotel/dish/detail', { params: { id } })
request.post('/hotel/dish/remove', null, { params: { id } })
request.put('/hotel/dish/updateStatus', null, { params: { id, status } })// ❌ 错误
request.get(/hotel/dish/${id})
request.post(/hotel/dish/remove/${id})
### 1.4 Java 版本限制(JDK 7 及以下风格)
- **禁止使用 Java 8+ 特性**,包括:
- Lambda 表达式(`() -> {}`)
- 方法引用(`ClassName::methodName`)
- Stream API(`.stream().filter().map().collect()`)
- 增强 switch 表达式(`case "X" -> value`)
- 替代方案:
- Lambda → 匿名内部类
- Stream → 传统 for 循环 + if 判断
- 方法引用 → 直接调用或使用字符串列名
- 增强 switch → 传统 switch + break
- 示例:
```java
// ✅ 正确 - 传统 switch
for (Long id : dto.getIds()) {
switch (dto.getOperation()) {
case "ON_SALE":
dishUpdateStatus(id, "ON_SALE");
break;
case "DELETE":
dishDelete(id);
break;
default:
throw new BusinessException("不支持的操作类型: " + dto.getOperation());
}
}
// ❌ 错误 - 增强 switch
for (Long id : dto.getIds()) {
switch (dto.getOperation()) {
case "ON_SALE" -> dishUpdateStatus(id, "ON_SALE");
case "DELETE" -> dishDelete(id);
default -> throw new BusinessException("不支持的操作类型: " + dto.getOperation());
}
}
// ✅ 正确 - 传统 for 循环过滤
List<String> boundShortCodes = new ArrayList<>();
for (HotelQrCode e : entities) {
if (Objects.equals(e.getTenantId(), tenantId) && e.getRoomId() != null) {
boundShortCodes.add(e.getShortCode());
}
}
// ❌ 错误 - Stream API
List<String> boundShortCodes = entities.stream()
.filter(e -> Objects.equals(e.getTenantId(), tenantId))
.filter(e -> e.getRoomId() != null)
.map(HotelQrCode::getShortCode)
.toList();
// ✅ 正确 - 匿名内部类
HotelQrCode qrCode = TenantContextHolder.executeIgnore(new java.util.function.Supplier<HotelQrCode>() {
@Override
public HotelQrCode get() {
return getBaseMapper().selectByShortCodeIgnoreTenant(shortCode);
}
});
// ❌ 错误 - Lambda 表达式
HotelQrCode qrCode = TenantContextHolder.executeIgnore(() ->
getBaseMapper().selectByShortCodeIgnoreTenant(shortCode)
);
// ✅ 正确 - QueryWrapper + 字符串列名
QueryWrapper<HotelDishSpecGroup> wrapper = new QueryWrapper<>();
wrapper.eq("dish_id", dishId)
.eq("tenant_id", tenantId)
.orderByAsc("sort_order");
// ❌ 错误 - LambdaQueryWrapper + 方法引用
LambdaQueryWrapper<HotelDishSpecGroup> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HotelDishSpecGroup::getDishId, dishId)
.eq(HotelDishSpecGroup::getTenantId, tenantId)
.orderByAsc(HotelDishSpecGroup::getSortOrder);
// ✅ 正确 - UpdateWrapper + 字符串列名
UpdateWrapper<HotelQrCode> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", qrCodeId)
.set("room_id", null)
.set("room_no", null);
getBaseMapper().update(null, updateWrapper);
// ❌ 错误 - LambdaUpdateChainWrapper + 方法引用
new LambdaUpdateChainWrapper<>(getBaseMapper())
.eq(HotelQrCode::getId, qrCodeId)
.set(HotelQrCode::getRoomId, null)
.set(HotelQrCode::getRoomNo, null)
.update();
QueryWrapper / UpdateWrapper(字符串列名)LambdaQueryWrapper / LambdaUpdateChainWrapper(方法引用)RespInfo 封装返回值RespInfo.success(data) 或 RespInfo.success()(无返回值时)RespInfo.error(msg) 或抛出 BusinessExceptionTenantEntity(自带 tenantId + 审计字段)@TableId(value = "id", type = IdType.ASSIGN_ID)(雪花算法)@TableLogic(value = "0", delval = "id"),类型为 Long(bigint),删除时写入主键 ID@Data + @EqualsAndHashCode(callSuper = true) + @Serial示例:
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("hotel_business_hours")
public class HotelBusinessHours extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID(雪花算法) */
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/** 状态: ENABLED-启用, DISABLED-禁用 */
private String status;
/** 删除标记(0=未删除,删除时写入主键ID) */
@TableLogic(value = "0", delval = "id")
private Long delFlag;
}
LambdaQueryWrapper 构建查询selectById、insert、updateById、deleteById 等 MyBatis-Plus 内置方法允许DataScopeInterceptor 按 mapperMethod 精确匹配改写 SQLAND del_flag = 0@OperationLog 注解@OperationLog(module = "模块名", type = OperationType.XXX, desc = "操作描述")QUERY / ADD / UPDATE / DELETEcontroller/ # REST 控制器
domain/ # 数据库实体(Entity)
dto/ # 请求 DTO(新增/修改用)
vo/ # 响应 VO(查询返回用)
mapper/ # MyBatis Mapper 接口
service/ # 服务接口
└── impl/ # 服务实现
constant/ # 常量
@Transactional(rollbackFor = Exception.class)validateXxx(Xxx entity/dto)requireXxx(Long id)(不存在直接抛异常)AiCrudPage 组件(自动处理搜索栏、工具栏、表格、分页布局)height: 100%api-config 格式:
javascript
:api-config="{
list: 'get@/hotel/xxx/page',
detail: 'get@/hotel/xxx/detail',
add: 'post@/hotel/xxx',
update: 'put@/hotel/xxx',
delete: 'post@/hotel/xxx/remove',
}"
:id,ID 由组件自动作为查询参数传递tableColumns 和 editSchema 必须定义为 computed(确保字典异步加载后响应式更新)h('a', { class: 'text-primary cursor-pointer hover:opacity-80', onClick: ... }) 模式crudRef.value?.refresh()(不是 reload)crudRef.value?.handleEdit(row)- 作为占位符100pxdiv 包裹 AuthImage 组件,设置 overflow: hidden 防止图片溢出NAvatar 的 src 属性NTag 组件,配合颜色映射
javascript
const statusColorMap = { ENABLED: 'success', DISABLED: 'default' }
render: row => h(NTag, { type: statusColorMap[row.status] || 'default', size: 'small' },
{ default: () => statusLabelMap[row.status] || row.status })
使用 UnoCSS 语义化颜色类区分操作类型:
| 类名 | 颜色 | 场景 |
|---|---|---|
text-primary |
蓝 | 编辑、查看、授权 |
text-warning |
黄 | 刷新缓存、重置、封禁 |
text-error |
红 | 删除、强制下线 |
text-success |
绿 | 启用、发布、通过 |
text-info |
灰蓝 | 详情、统计 |
DictSelect 组件或 useDict() composableDictTag 组件computed 包裹Teleport to="body")position: fixed + 半透明黑色背景max-width: 85vw + max-height: 80vhimport { request } from '@/utils'(不是 @/utils/request)getXxxPage / getXxxDetail / createXxx / updateXxx / deleteXxx// ==================== 模块名 ====================/hotel/dish/${id})export function deleteDish(id) { return request.post('/hotel/dish/remove', null, { params: { id } }) }
export function updateDishStatus(id, status) { return request.put('/hotel/dish/updateStatus', null, { params: { id, status } }) }
// ❌ 错误 - 禁止使用模板字符串拼接 ID
export function getDishDetail(id) {
return request.get(/hotel/dish/${id})
}
export function deleteDish(id) {
return request.post(/hotel/dish/remove/${id})
}
- 路径映射规则(与后端 Controller 对应):
- 详情:`/xxx/:id` → `/xxx/detail` + `{ params: { id } }`
- 删除:`/xxx/remove/:id` → `/xxx/remove` + `{ params: { id } }`
- 状态更新:`/xxx/:id/status` → `/xxx/updateStatus` + `{ params: { id, status } }`
- 其他操作:`/xxx/:id/action` → `/xxx/action` + `{ params: { id } }`
---
## 三、数据库编码习惯
### 3.1 表结构规范
- 所有业务表必须包含:`id`, `tenant_id`, `create_by`, `create_time`, `create_dept`, `update_by`, `update_time`
- 字符集 `utf8mb4`,引擎 `InnoDB`
- 主键用 `bigint`(雪花算法),不用自增
- 金额字段用 `bigint`,单位**分**
- 时间字段用 `datetime`(对应 Java `LocalDateTime`)
- 状态字段用 `varchar`,值为大写英文(如 `ENABLED` / `DISABLED`、`ON_SALE` / `OFF_SHELF`)
### 3.2 逻辑删除规范
- 业务表默认使用逻辑删除
- `del_flag` 字段类型 `bigint NOT NULL DEFAULT 0`
- 删除时写入当前行主键 ID(不是固定值 `1`)
- 实体必须显式声明 `@TableLogic(value = "0", delval = "id")`
### 3.3 Flyway 迁移脚本
- 命名格式:`V<版本号>__<lower_snake_case_description>.sql`
- 版本号必须大于 `1.0.0`,单调递增
- 脚本必须具备**防重复保护**:
- 建表:`CREATE TABLE IF NOT EXISTS`
- 插入:`INSERT ... SELECT ... WHERE NOT EXISTS`
- 新增列/索引前查 `information_schema`
- `INSERT` 必须显式写列名,禁止依赖表字段顺序
- 业务内置数据 `tenant_id` 必须为 `1`,禁止写 `0`
- 菜单资源 SQL 使用动态 `parent_id`(子查询),避免硬编码 ID
- 字典数据格式:`sys_dict_type` + `sys_dict_data`,`dict_value` 与后端枚举值保持一致
### 3.4 字典规范
- 字典类型命名:小写下划线,业务级用 `hotel_` 前缀,系统级用 `sys_` 前缀
- `dict_value` 必须与后端枚举/状态值严格一致
- `dict_label` 只负责展示文案
- `list_class` 负责标签样式(success/primary/warning/info/default)
- 每个字典数据单独一行 INSERT + WHERE NOT EXISTS 防重复
---
## 四、API 设计习惯
### 4.1 RESTful 风格(传统 JDK 7 风格)
| 操作 | HTTP 方法 | 路径 | 说明 |
|------|-----------|------|------|
| 分页查询 | GET | `/xxx/page` | 筛选参数用实体类接收 |
| 查询全部 | GET | `/xxx/all` 或 `/xxx/list` | 不分页列表 |
| 查询详情 | GET | `/xxx/detail` | **ID 作为 `@RequestParam`**,不是路径参数 |
| 新增 | POST | `/xxx` | RequestBody |
| 修改 | PUT | `/xxx` | RequestBody |
| 删除 | POST | `/xxx/remove` | **ID 作为 `@RequestParam`**,不是路径参数 |
| 状态切换 | POST | `/xxx/toggle` | **ID 作为 `@RequestParam`** |
| 状态更新 | PUT | `/xxx/updateStatus` | `@RequestParam Long id, @RequestParam String status` |
| 批量操作 | POST | `/xxx/batch` | RequestBody |
- **禁止使用 `{id}` 路径占位符**
- 所有带 ID 的接口统一使用方法名 + `@RequestParam` 方式
- 示例:
```java
// ✅ 正确
@GetMapping("/dish/detail")
public RespInfo<HotelDishVO> dishDetail(@RequestParam Long id) { ... }
@PostMapping("/dish/remove")
public RespInfo<Void> dishDelete(@RequestParam Long id) { ... }
@PutMapping("/dish/updateStatus")
public RespInfo<Void> dishUpdateStatus(@RequestParam Long id, @RequestParam String status) { ... }
// ❌ 错误 - 禁止使用 @PathVariable
@GetMapping("/dish/{id}")
public RespInfo<HotelDishVO> dishDetail(@PathVariable Long id) { ... }
模块:资源:操作,如 hotel:dish:query、hotel:businessHours:addresource_type = 3tenant_id 必须设为 1(默认租户),禁止设 0tenantId 动态设置租户上下文TenantContextHolder.getTenantId() 获取当前租户 IDreturn tenantId == null ? 1L : tenantId;****)mvn compile -pl forge-business/forge-hotel -am -DskipTestspnpm lint:fix