# Forge-Hotel 编码习惯与规范 > 本文档记录个人在 Forge 项目中的编码习惯和偏好,所有代码编写(含 AI 辅助生成)必须遵循以下规范。 > > **适用范围**: forge-hotel 模块及后续所有业务模块 > **最后更新**: 2026-08-17 --- ## 一、后端编码习惯 ### 1.1 依赖注入 - **使用 `@Autowired` 字段注入**,不使用 `@RequiredArgsConstructor` 构造器注入 - 每个依赖字段单独加 `@Autowired` 注解 - 示例: ```java @RestController @RequestMapping("/hotel/dish") public class HotelDishController { @Autowired private HotelDishService hotelDishService; } ``` ### 1.2 Controller 查询参数 - **分页查询接口的筛选参数必须用实体类接收**,禁止逐个 `@RequestParam` - 只有 `@PathVariable`(路径参数如 id)和确实无法用实体类表达的参数(如安全校验参数)才单独接收 - 状态更新类接口(如 `/{id}/status`)属于操作类,可保持 `@RequestParam` - 示例: ```java // ✅ 正确 public RespInfo> dishPage(PageQuery pageQuery, HotelDish query) { ... } // ❌ 错误 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 dishDetail(@RequestParam Long id) { ... } @PostMapping("/dish/remove") public RespInfo dishDelete(@RequestParam Long id) { ... } @PutMapping("/dish/updateStatus") public RespInfo dishUpdateStatus(@RequestParam Long id, @RequestParam String status) { ... } // ❌ 错误 - 禁止使用 @PathVariable 和 {id} @GetMapping("/dish/{id}") public RespInfo dishDetail(@PathVariable Long id) { ... } @PostMapping("/dish/remove/{id}") public RespInfo dishDelete(@PathVariable Long id) { ... } ``` - 路径命名规则: - 详情查询:`/xxx/detail`(原 `/{id}`) - 删除:`/xxx/remove`(原 `/remove/{id}` 或 `/{id}`) - 状态更新:`/xxx/updateStatus`(原 `/{id}/status`) - 其他操作:直接用动词方法名,如 `/xxx/accept`、`/xxx/reject`、`/xxx/toggle` - 前端调用时,ID 放在 `params` 中: ```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 boundShortCodes = new ArrayList<>(); for (HotelQrCode e : entities) { if (Objects.equals(e.getTenantId(), tenantId) && e.getRoomId() != null) { boundShortCodes.add(e.getShortCode()); } } // ❌ 错误 - Stream API List 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() { @Override public HotelQrCode get() { return getBaseMapper().selectByShortCodeIgnoreTenant(shortCode); } }); // ❌ 错误 - Lambda 表达式 HotelQrCode qrCode = TenantContextHolder.executeIgnore(() -> getBaseMapper().selectByShortCodeIgnoreTenant(shortCode) ); // ✅ 正确 - QueryWrapper + 字符串列名 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("dish_id", dishId) .eq("tenant_id", tenantId) .orderByAsc("sort_order"); // ❌ 错误 - LambdaQueryWrapper + 方法引用 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(HotelDishSpecGroup::getDishId, dishId) .eq(HotelDishSpecGroup::getTenantId, tenantId) .orderByAsc(HotelDishSpecGroup::getSortOrder); // ✅ 正确 - UpdateWrapper + 字符串列名 UpdateWrapper 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(); ``` - MyBatis-Plus Wrapper 选择: - 使用 `QueryWrapper` / `UpdateWrapper`(字符串列名) - 禁止使用 `LambdaQueryWrapper` / `LambdaUpdateChainWrapper`(方法引用) - 原因:方法引用属于 Java 8+ 特性,不符合传统 JDK 7 风格 ### 1.5 统一返回封装 - 所有接口统一使用 `RespInfo` 封装返回值 - 成功:`RespInfo.success(data)` 或 `RespInfo.success()`(无返回值时) - 失败:`RespInfo.error(msg)` 或抛出 `BusinessException` ### 1.6 实体类规范 - 实体继承 `TenantEntity`(自带 tenantId + 审计字段) - 主键使用 `@TableId(value = "id", type = IdType.ASSIGN_ID)`(雪花算法) - 逻辑删除字段:`@TableLogic(value = "0", delval = "id")`,类型为 `Long`(bigint),删除时写入主键 ID - 使用 Lombok:`@Data` + `@EqualsAndHashCode(callSuper = true)` + `@Serial` - 每个字段必须有 Javadoc 注释 - 示例: ```java @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; } ``` ### 1.5 SQL 编写规范 - **SQL 必须写在 Mapper XML 中**,禁止在 Service 层用 `LambdaQueryWrapper` 构建查询 - 例外:仅 `selectById`、`insert`、`updateById`、`deleteById` 等 MyBatis-Plus 内置方法允许 - 原因:`DataScopeInterceptor` 按 `mapperMethod` 精确匹配改写 SQL - Mapper XML 中的查询必须显式过滤未删除数据:`AND del_flag = 0` - 原因:自定义 XML SQL 不会被 MP 自动补全逻辑删除条件 ### 1.6 操作日志 - 每个 Controller 方法必须加 `@OperationLog` 注解 - 格式:`@OperationLog(module = "模块名", type = OperationType.XXX, desc = "操作描述")` - 操作类型:`QUERY` / `ADD` / `UPDATE` / `DELETE` ### 1.7 包结构 ``` controller/ # REST 控制器 domain/ # 数据库实体(Entity) dto/ # 请求 DTO(新增/修改用) vo/ # 响应 VO(查询返回用) mapper/ # MyBatis Mapper 接口 service/ # 服务接口 └── impl/ # 服务实现 constant/ # 常量 ``` ### 1.8 Service 层规范 - Service 之间**禁止互相注入**(避免循环依赖) - 跨 Service 协调逻辑上提到 Controller 层 - 事务注解:`@Transactional(rollbackFor = Exception.class)` - 校验方法命名:`validateXxx(Xxx entity/dto)` - 存在性校验方法命名:`requireXxx(Long id)`(不存在直接抛异常) --- ## 二、前端编码习惯 ### 2.1 页面组件选择 - **标准 CRUD 页面**:使用 `AiCrudPage` 组件(自动处理搜索栏、工具栏、表格、分页布局) - **分页位置**:右下角(AiCrudPage 内置) - **配置少的简单列表**:不加多余筛选条件,保持界面简洁 - 页面容器样式:`height: 100%` ### 2.2 AiCrudPage 配置规范 - `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', }" ``` - **注意**:detail 和 delete 路径中**不包含 `:id`**,ID 由组件自动作为查询参数传递 - `tableColumns` 和 `editSchema` 必须定义为 `computed`(确保字典异步加载后响应式更新) - 操作列使用 `h('a', { class: 'text-primary cursor-pointer hover:opacity-80', onClick: ... })` 模式 - 刷新方法:`crudRef.value?.refresh()`(不是 reload) - 编辑方法:`crudRef.value?.handleEdit(row)` ### 2.3 字段命名 - 前端字段统一使用 **camelCase** 命名 - 与后端 API 返回字段保持一致 ### 2.4 表格渲染规范 - **空值处理**:空值字段显示空白,**不使用 `-` 作为占位符** - **图片列**: - 列宽设置 `100px` - 用 `div` 包裹 `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 }) ``` ### 2.5 操作按钮样式 使用 UnoCSS 语义化颜色类区分操作类型: | 类名 | 颜色 | 场景 | |------|------|------| | `text-primary` | 蓝 | 编辑、查看、授权 | | `text-warning` | 黄 | 刷新缓存、重置、封禁 | | `text-error` | 红 | 删除、强制下线 | | `text-success` | 绿 | 启用、发布、通过 | | `text-info` | 灰蓝 | 详情、统计 | ### 2.6 字典使用 - **禁止硬编码**下拉选项和状态标签 - 下拉使用 `DictSelect` 组件或 `useDict()` composable - 标签展示使用 `DictTag` 组件 - Schema 中的字典选项必须用 `computed` 包裹 ### 2.7 图片预览 - 采用全屏覆盖层实现(`Teleport to="body"`) - `position: fixed` + 半透明黑色背景 - 右上角 × 关闭按钮,支持点击背景或 ESC 关闭 - 左右箭头切换图片,底部圆点指示器 + 页码计数 - 图片自适应:`max-width: 85vw` + `max-height: 80vh` ### 2.8 时间段选择 - 时间类字段使用**下拉选择**(Select),不使用手动输入框 - 时间选项按业务场景设定间隔(如每 30 分钟一档:09:00, 09:30, 10:00...23:30) ### 2.9 API 文件规范 - 导入路径:`import { request } from '@/utils'`(不是 `@/utils/request`) - 函数命名:`getXxxPage` / `getXxxDetail` / `createXxx` / `updateXxx` / `deleteXxx` - 按模块用注释分隔:`// ==================== 模块名 ====================` - 每个函数加 JSDoc 注释 ### 2.10 API 路径风格(与后端保持一致) - **禁止使用模板字符串拼接 ID**(如 `/hotel/dish/${id}`) - 所有带 ID 的接口,ID 作为查询参数传递 - 示例: ```javascript // ✅ 正确 - ID 放在 params 中 export function getDishDetail(id) { return request.get('/hotel/dish/detail', { params: { 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<版本号>__.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 dishDetail(@RequestParam Long id) { ... } @PostMapping("/dish/remove") public RespInfo dishDelete(@RequestParam Long id) { ... } @PutMapping("/dish/updateStatus") public RespInfo dishUpdateStatus(@RequestParam Long id, @RequestParam String status) { ... } // ❌ 错误 - 禁止使用 @PathVariable @GetMapping("/dish/{id}") public RespInfo dishDetail(@PathVariable Long id) { ... } ``` ### 4.2 权限标识 - 格式:`模块:资源:操作`,如 `hotel:dish:query`、`hotel:businessHours:add` - 按钮权限资源类型 `resource_type = 3` --- ## 五、交互设计习惯 ### 5.1 列表页面 - 配置项少的列表页不加多余筛选(如营业时段不加餐段筛选) - 分页统一在右下角 - 操作列包含:编辑 + 状态切换 + 删除 ### 5.2 批量操作 - 有批量生成的管理页面,应同步提供批量删除、批量下载(ZIP)、单条下载功能 - 保证操作完整性与用户体验一致性 ### 5.3 危险操作 - 删除、解绑等不可逆操作必须弹出**二次确认** - 已绑定的二维码显示"解绑"按钮而非"重新绑定" ### 5.4 扫码绑定 - 绑定成功后,点击"完成"跳转至扫码绑定主界面(无参数状态) - 解绑后返回扫码界面 --- ## 六、多租户习惯 ### 6.1 租户上下文 - 业务数据 `tenant_id` 必须设为 `1`(默认租户),禁止设 `0` - 免登录场景(如扫码):先忽略租户过滤查到实体,再根据实体的 `tenantId` 动态设置租户上下文 - 使用 `TenantContextHolder.getTenantId()` 获取当前租户 ID - 兜底逻辑:`return tenantId == null ? 1L : tenantId;` --- ## 七、其他习惯 ### 7.1 注释风格 - 实体类每个字段必须有 Javadoc 注释 - Controller 每个方法必须有 Javadoc(描述接口用途) - Service 接口方法必须有 Javadoc(含参数说明) - 关键业务逻辑加行内注释说明 ### 7.2 安全红线 - 禁止硬编码密钥、AK/SK、数据库密码 - 禁止在日志中打印手机号、身份证、银行卡 - API Key/Secret 返回前端必须脱敏(保留前4后4,中间 `****`) ### 7.3 构建与验证 - Maven 构建:`mvn compile -pl forge-business/forge-hotel -am -DskipTests` - 前端 Lint:`pnpm lint:fix` - 改完代码必须编译/Lint 验证通过