18 KiB
name, description, agent_created
| name | description | agent_created |
|---|---|---|
| fms-dev | FMS (模块/数据管理平台) 全栈开发技能。本项目后端为 Spring Boot + SqlServer 通用数据接口架构, 前端为 Vue 3 + antdv-next + Pinia。本技能描述项目约定、通用组件用法、API 模式、 数据库规范及常见开发任务的标准操作。当需要新增页面、新增后端查询/保存逻辑、创建数据库迁移脚本、 或修改 FMS 现有功能时使用此技能。 | true |
FMS 开发技能
项目概览
FMS 是一个配置驱动的低代码数据管理平台,核心设计理念是:后端不新增业务接口,一切数据读写走少量通用接口;前端用元数据(模块/字段/按钮/多语言)动态拼出页面。
fms-api/— Spring Boot 3.x + Java 21 + Maven,端口 8088,context-path/apifms-vue/— Vue 3 + Vite + Pinia + antdv-next + stk-table-vue,开发端口 5180- 数据库:SQL Server,多机构独立库,按 JWT 中 orgId 路由
- 连接池:Alibaba Druid,每机构一个
触发场景
- 新增/修改前端页面(数据浏览页、CRUD 页、复杂自定义页)
- 新增/修改后端通用接口调用逻辑
- 创建数据库迁移脚本
- 修改模块管理(s_module / s_module_field 等元数据)
- 新增文件上传/下载功能
- 排查保存或查询问题
- 任何需要理解本项目独特约定的开发任务
核心铁律(必须遵守)
- 不新增业务专用后端接口。所有数据操作复用
DataController通用端点。 - 不新增前端 API 地址。
services/api.js封装的函数是唯一入口。 - 改动 scope 到当前任务,不顺手重构无关代码。
- 改完不要自己跑 build/启动服务,除非用户明确要求。测试验证(
mvnw test/pnpm fmt:check)可以。 - bigint 雪花 ID 必须以字符串返回前端(JavaScript 无法安全表示 64 位整数)。
- 表名/字段名只允许
[A-Za-z_][A-Za-z0-9_]*,后端DbUtils会强校验。 - 迁移脚本必须可重复执行(
IF NOT EXISTS),不修改已有业务表。
后端架构速查
通用数据端点
| 端点 | 用途 | 参数关键字段 |
|---|---|---|
POST /data/loaddata |
条件查询 | view_name, search_condition, order_by, search_columns |
POST /data/page |
分页查询 | view_name, order_by, page_no, page_size, search_condition, search_columns |
POST /data/saveobjt |
跨表事务保存 | [{table, key_field, inserts[], updates[], deletes[]}] |
GET /data/nextid |
雪花 ID | ?count=N(默认1) |
POST /data/loaddatabysql |
自定义 SQL | {sql} — 仅 SELECT/WITH 开头,禁止 DML |
POST /data/describe |
表结构 | {table} |
GET /data/nextcode |
自动编号 | ?moduleId=N&count=N |
保存引擎 (DataSaveService)
saveobjt 在一个手动 JDBC 事务中执行,顺序:delete → update → insert。任一表失败触发 connection.rollback()。
关键行为:
- 主键值
< 0或null→ 自动分配雪花 ID key_field值从 UPDATE SET 中排除(作为 WHERE 条件)- INSERT 只写入
writable=true且在数据中的列 - 要求每条 INSERT/UPDATE/DELETE 影响行数恰好为 1
多机构路由
JwtAuthFilter → OrgContext.setOrgId() → OrgRoutingDataSource
→ OrgDataSourceManager.getConnection() → DruidDataSource[orgId]
→ 对应 SQL Server 实例
机构数据库配置文件:config/dbconfigs/{ORG_ID}.properties(git-ignored)。
异常与响应
| 异常类型 | code | 用途 |
|---|---|---|
AuthenticationException |
401 | 认证失败 |
BusinessException |
1000 | 业务校验失败,message 直接展示 |
SaveObjectException |
2001 | 保存失败,带 {table, action, index} |
SQLException / 其他 |
500 | 统一"系统异常" |
统一响应:ApiResponse<T>(int code, String message, T data),code == 0 表示成功。
后端包结构速查
cn.g3soft.fmsapi
├── config/ JwtAuthFilter, 配置属性
├── controller/ DataController, AuthController, FileController
├── database/ 多机构路由、OrgContext(ThreadLocal)
├── exception/ 异常类 + GlobalExceptionHandler
├── service/ DataService, DataSaveService, AuthService, FileService
└── utils/ DbUtils(核心), JwtUtils, ApiResponse, IdGenerator(雪花)
前端架构速查
HTTP 层
services/http.js:axios 实例,baseURL /api,自动注入 Bearer Token,拦截 code !== 0 转为 Error。认证失效(401/40101)自动跳转登录。
API 服务层
services/api.js 封装全部通用接口调用——这是前端与后端交互的唯一通道:
import { loadDataApi, pageDataApi, saveObjectApi, nextIdApi, describeApi, loginApi } from '@/services/api';
所有 API 返回 { success, code, message, data },其中 success 为前端补充(code === 0)。
国际化
不走 vue-i18n,翻译文本来自数据库 b_i18n 表。app store 的 t(key, fallback) 函数做翻译。多语言键规范:field.{moduleCode}.{fieldCode}、menu.{moduleCode}、power.{moduleCode}.{powerCode}。
权限
stores/permissions.js 从 b_user_module / b_user_power / s_module_power 加载权限。canAccess(moduleId) 判断页面权限,canPower(scopeCode, powerCode) 判断操作权限。特殊账号 g3soft 为超级管理员,跳过所有权限检查。
核心组件
FmsModuleListPage(数据列表页)
最常用的页面组件——大多数 CRUD 页面只需配置一个 data-code 即可运行。
Props 速查:
| Prop | 用途 |
|---|---|
data-code |
数据模块编码(必填),对应 s_module.b_code |
fixed-search-condition |
固定查询条件(不含 WHERE 关键字) |
row-actions |
行操作按钮 [{key, label, onClick}] |
editable |
可编辑模式(行内编辑+新增行) |
row-draggable |
行拖拽排序 |
page-size |
每页条数(默认 20) |
column-extensions |
列自定义(formatter, clickable 等) |
Slots:#actions(工具栏按钮区)、#actions="{ saveChanges, dirtyCount, saving }"(可编辑模式工具栏)
暴露方法:reload(), saveChanges(), addRow(), initialize(), resetColumns()
FmsTree(树形组件)
Prop treeData 接收扁平数组,自动构建树。支持虚拟滚动、拖拽、右键菜单。自动通过 b_i18n 做标题翻译。
路由与页面注册
- 在
router/routes.js添加路由配置,使用静态import()注册组件 meta.moduleId指定权限检查的模块 codemeta.title指定页面标题
{
path: 'my-feature',
name: 'my-feature',
component: () => import('@/views/my-feature/index.vue'),
meta: { title: '我的功能', moduleId: 'my_feature_code' },
}
后端开发模式
模式 1:新增简单查询端点
在现有 Controller(通常是 DataController 或 FileController 同级)中添加方法:
@PostMapping("/my/query")
public ApiResponse<?> myQuery(@RequestBody Map<String, Object> params) throws SQLException {
String viewName = ParamUtils.getRequiredString(params, "view_name");
String condition = ParamUtils.getString(params, "search_condition");
// 业务逻辑 + 调用 dataService.loadData / dbUtils.loadData
return ApiResponse.success(result);
}
模式 2:新增保存端点
封装数据为 [{table, key_field, inserts[], updates[], deletes[]}] 格式,调用 dataSaveService.save(requests)。
List<Map<String, Object>> requests = new ArrayList<>();
Map<String, Object> req = new HashMap<>();
req.put("table", "my_table");
req.put("key_field", "b_id");
req.put("inserts", newList);
dataSaveService.save(requests);
模式 3:自定编号生成
调用 dataSaveService.nextCode(moduleId, count),返回 List<String>。
前端开发模式(按复杂度排序)
模式 A:纯数据列表页(零代码)
适用于只需展示已有数据模块的页面。
<script setup>
import FmsModuleListPage from '@/components/fms-module-list/FmsModuleListPage.vue';
</script>
<template>
<div class="my-page">
<FmsModuleListPage data-code="my_data_code" />
</div>
</template>
<style scoped lang="scss">
.my-page {
width: 100%; height: 100%; overflow: hidden;
border: 1px solid var(--fms-border);
border-radius: 6px;
background: var(--fms-surface);
}
.my-page :deep(.fms-module-list-page) { border: 0; border-radius: 0; }
</style>
模式 B:带行操作和自定义工具栏
<script setup>
import { ref } from 'vue';
import FmsModuleListPage from '@/components/fms-module-list/FmsModuleListPage.vue';
const listRef = ref(null);
const rowActions = [
{ key: 'edit', label: '编辑', onClick: ({ record }) => handleEdit(record) },
{ key: 'delete', label: '删除', danger: true, onClick: ({ record }) => handleDelete(record) },
];
function handleAdd() { /* 打开新增弹窗 */ }
function handleEdit(record) { /* 打开编辑弹窗 */ }
function handleDelete(record) { /* 确认后调用 saveObjectApi 删除 */ }
</script>
<template>
<div class="my-page">
<FmsModuleListPage
ref="listRef"
data-code="my_data_code"
:row-actions="rowActions"
>
<template #actions>
<a-button type="primary" @click="handleAdd">新增</a-button>
</template>
</FmsModuleListPage>
</div>
</template>
模式 C:可编辑表格(行内编辑)
<FmsModuleListPage
ref="listRef"
data-code="my_data_code"
:editable="true"
:row-draggable="true"
>
<template #actions="{ saveChanges, saving }">
<a-button type="primary" :loading="saving" @click="saveChanges">保存</a-button>
</template>
</FmsModuleListPage>
模式 D:完全自定义页面(复杂场景)
直接使用 loadDataApi、saveObjectApi、nextIdApi + 原生 antdv-next 组件。需自行管理数据加载、修改追踪(使用 utils/dataChanges.js 的 diffRows/cloneData)、ID 分配(utils/tempId.js 的 createTempId + allocateTemporaryIds)。
关键工具函数:
cloneData(obj)— JSON 深拷贝,存储原始数据快照diffRows(current, previous)— 对比得到{inserts, updates, deletes}allocateTemporaryIds(rowGroups)— 负号临时 ID 替换为真实雪花 IDtableChange(table, changes)— 生成{table, key_field, ...changes}结构
数据库迁移
脚本命名
fms-api/config/migrations/NNN_description.sql,三位数字递增。
脚本模板
SET XACT_ABORT ON;
BEGIN TRANSACTION;
-- 描述:创建 xxx 表
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'xxx')
BEGIN
CREATE TABLE [xxx] (
[b_id] BIGINT NOT NULL PRIMARY KEY,
[b_name] NVARCHAR(200) NOT NULL,
-- ...
);
END;
COMMIT;
关键规则:
- 使用
XACT_ABORT ON+ 显式BEGIN/COMMIT TRANSACTION - 所有 DDL 用
IF NOT EXISTS包裹 - 不删除或修改已有业务表
- 表名/列名使用小写或驼峰,统一
- 大字段主键用
BIGINT
执行
使用 PowerShell .NET SqlClient(本机 SQL Server 兼容性),参照 fms-api/tools/migration/ 下的工具脚本。
代码约定细节
后端
- Controller 方法声明
throws SQLException,返回ApiResponse<T> - Service 注入
DataSource dataSource(自动多机构路由)+DbUtils dbUtils - 参数提取用
ParamUtils.getRequiredString/getString/getList - ID 生成用
IdGenerator.nextId() - 当前用户 ID 用
OrgContext.getUserId(),机构 ID 用OrgContext.requireOrgId() - 业务失败抛
BusinessException("消息")
前端
- SFC 使用
<script setup>+ Composition API - antdv-next 组件已全局自动导入(通过
@antdv-next/auto-import-resolver),不要在组件中手动 import antdv-next 组件 - 图标使用 Lucide Vue
- ID 处理:收到 bigint 一定是字符串,生成临时 ID 用
createTempId()(返回 "-1", "-2"...) - 错误处理:API 调用失败自动抛 Error,message 可直接展示;捕获后不要吞异常
- 样式使用 scoped SCSS + CSS 变量(
var(--fms-*))
多语言
- 键格式:
field.{moduleCode}.{fieldCode}、power.{moduleCode}.{powerCode}、menu.{moduleCode}、module.{moduleCode} - 模块编码校验:
validateModuleCode(code)— 小写字母开头,2-50 字符 - 字段/操作权限重命名后用
syncModuleItemI18nKeys()同步翻译键
ERP 前端样式规范
本项目是企业级数据管理平台,UI 风格必须服从 数据密度优先、操作效率至上 的 ERP 设计原则。
三条禁令
| 禁止项 | 识别特征 | 替代做法 |
|---|---|---|
| 大卡片 | 圆角 > 6px、带有阴影的大面积色块、Card 组件包裹整行内容、dashboard 风格的数据块 | 表格行、紧凑列表、无边框平铺 |
| 大留白 | padding > 16px、margin > 12px、大面积空白区域、内容区居中且两侧大量留白 | padding ≤ 12px,内容撑满可用宽度,用分隔线而不是留白区分区域 |
| 营销风格 | 渐变色背景、hero banner、大图标 + 文案宣传区、emoji 装饰、"立即体验"风格按钮、彩色卡片阵列 | 纯功能导向:灰白底、蓝色主操作按钮、无装饰性元素 |
布局规则
页面级:
- 内容区
width: 100%; height: 100%,撑满可用空间,不设最大宽度 - 页面仅一层容器包裹,不要嵌套多层无意义的
<div>外套 - 模块页通用外层样式:
.my-page { width: 100%; height: 100%; overflow: hidden; border: 1px solid var(--fms-border); border-radius: 6px; background: var(--fms-surface); }
表单/搜索区:
- 表单网格布局 4 列,
gap: 8px 16px,标签左对齐,宽度 ≤ 100px - 搜索区和内容区之间用 1px 分割线分隔,间距 ≤ 8px
- 表单按钮右对齐,与表单末行同行
- 不要将整个搜索区包在一个 Card 组件里
表格:
- 表头
font-size: 13px,行高32-36px,不对内容行使用大面积背景色 - 斑马纹使用极浅灰色(
#fafafa级别),不用彩色 - 操作列宽度适应内容,不做固定 200px 这样的大宽度
- 分页栏始终在表格正下方,
padding: 8px,背景透明
间距尺度
// 紧凑型 ERP 间距体系
--space-xs: 4px; // 图标与文字间距、表单内紧凑间距
--space-sm: 8px; // 组件内部间距、表单字段间距
--space-md: 12px; // 组件间间距、页面内边距(默认最大值)
--space-lg: 16px; // 仅用于页面级大区块分隔,谨慎使用
| 位置 | 推荐 padding |
|---|---|
| 页面容器 | padding: 0 或 padding: 8px 12px |
| 弹窗 body | padding: 16px 24px |
| 表格单元格 | padding: 6px 10px |
| 表单字段间距 | margin-bottom: 12px |
| 按钮组间距 | gap: 8px |
颜色语义
// ERP 语义色 —— 只用功能色,不用装饰色
--primary: #1677ff; // 主操作按钮、链接、选中态
--success: #52c41a; // 启用状态、已确认、通过
--warning: #faad14; // 待审核、警告
--danger: #ff4d4f; // 删除、禁用、驳回、错误
--inactive: #d9d9d9; // 未启用、禁用态
// 数据状态色(常用于表格行的标签/圆点)
// 正常: #52c41a | 异常: #ff4d4f | 草稿: #faad14 | 归档: #8c8c8c
规则:
- 表格内数据状态的彩色标签使用
variant="light"浅色变体 - 操作按钮组:删除类用
danger,主操作用primary,其余用default - 不要在表格行上使用大面积彩色背景
- 页面背景统一
#f5f5f5(antdv-next 默认),不自定义花哨背景
字体与排版
// 只使用系统字体栈,不引入 Web Font
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
// 字号体系
font-size: 13px; // 表格内容、表单标签
font-size: 14px; // 正文、菜单、按钮
font-size: 16px; // 页面标题
- 不使用图标字体库,统一用 Lucide Vue(已集成)
- 数值列右对齐,文本列左对齐,状态/操作列居中
- 金额列用等宽数字(
font-variant-numeric: tabular-nums)
弹窗与抽屉
- 新增/编辑表单 → Modal 弹窗(
width: 560px-720px,不要默认 520px 太小) - 查看详情 → Drawer 抽屉(
width: 480px-640px) - 弹窗 footer 按钮右对齐:[取消] [确定]
- 弹窗不要嵌套弹窗
消息与反馈
- 保存成功不弹窗,用
message.success("保存成功")顶部提示 - 删除操作必须用
Modal.confirm二次确认 - 错误信息从后端 message 字段获取,直接展示,不做二次包装
- 加载状态用
a-spin包裹内容区或骨架屏,不显示大面积全屏 loading
组件选择对照表
| 场景 | 选这个 | 别选这个 |
|---|---|---|
| 数据展示 | a-table / stk-table-vue |
a-card + a-list |
| 分组信息 | a-descriptions |
a-card 嵌套 |
| 多步骤表单 | a-tabs 分 tab |
Step 组件引导 |
| 多维度筛选 | a-form 行内 + a-select |
Radio Group 平铺 |
| 树状导航 | a-tree |
Menu 手风琴嵌套 |
| 只读信息展示 | a-descriptions + a-tag |
自定义大色块布局 |
常见踩坑点
- 新增表后访问不到:确认当前机构数据库有这张表,检查表名拼写和 schema(默认
dbo)。 - 保存 bigint 报错:确保前端传的是字符串而非数字。
- saveobjt 返回 500:检查
key_field是否存在于表中,inserts 中是否有非 writable 列(如自增/计算列)。 - 分页返回空:
order_by在 page 中是必填的。 - 模块管理保存后权限失效:需重新调用
permissionStore.load()。 - FmsModuleListPage 不显示数据:检查
data-code对应的数据模块是否在s_module中注册且b_canuse=1。 - 拖拽排序失效:确保
s_module_field中有b_xh字段且orderFieldprop 匹配。 - 迁移脚本重复执行报错:确保所有 DDL 用
IF NOT EXISTS包裹。 - SQL 注入检测误杀:
loaddatabysql只允许SELECT/WITH开头,字符串中有--也会拦截——需要时用loaddata+ 参数化。 - 多机构数据隔离:始终通过
OrgContext获取 orgId,不要硬编码机构连接信息。