Files
workspace/code/fms/.workbuddy/skills/fms-dev/SKILL.md
T
2026-08-02 22:05:44 +08:00

18 KiB
Raw Blame History

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 /api
  • fms-vue/ — Vue 3 + Vite + Pinia + antdv-next + stk-table-vue,开发端口 5180
  • 数据库:SQL Server,多机构独立库,按 JWT 中 orgId 路由
  • 连接池:Alibaba Druid,每机构一个

触发场景

  • 新增/修改前端页面(数据浏览页、CRUD 页、复杂自定义页)
  • 新增/修改后端通用接口调用逻辑
  • 创建数据库迁移脚本
  • 修改模块管理(s_module / s_module_field 等元数据)
  • 新增文件上传/下载功能
  • 排查保存或查询问题
  • 任何需要理解本项目独特约定的开发任务

核心铁律(必须遵守)

  1. 不新增业务专用后端接口。所有数据操作复用 DataController 通用端点。
  2. 不新增前端 API 地址。services/api.js 封装的函数是唯一入口。
  3. 改动 scope 到当前任务,不顺手重构无关代码。
  4. 改完不要自己跑 build/启动服务,除非用户明确要求。测试验证(mvnw test/pnpm fmt:check)可以。
  5. bigint 雪花 ID 必须以字符串返回前端(JavaScript 无法安全表示 64 位整数)。
  6. 表名/字段名只允许 [A-Za-z_][A-Za-z0-9_]*,后端 DbUtils 会强校验。
  7. 迁移脚本必须可重复执行(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 做标题翻译。

路由与页面注册

  1. 在 router/routes.js 添加路由配置,使用静态 import() 注册组件
  2. meta.moduleId 指定权限检查的模块 code
  3. meta.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 替换为真实雪花 ID
  • tableChange(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 自定义大色块布局

常见踩坑点

  1. 新增表后访问不到:确认当前机构数据库有这张表,检查表名拼写和 schema(默认 dbo)。
  2. 保存 bigint 报错:确保前端传的是字符串而非数字。
  3. saveobjt 返回 500:检查 key_field 是否存在于表中,inserts 中是否有非 writable 列(如自增/计算列)。
  4. 分页返回空:order_by 在 page 中是必填的。
  5. 模块管理保存后权限失效:需重新调用 permissionStore.load()。
  6. FmsModuleListPage 不显示数据:检查 data-code 对应的数据模块是否在 s_module 中注册且 b_canuse=1。
  7. 拖拽排序失效:确保 s_module_field 中有 b_xh 字段且 orderField prop 匹配。
  8. 迁移脚本重复执行报错:确保所有 DDL 用 IF NOT EXISTS 包裹。
  9. SQL 注入检测误杀:loaddatabysql 只允许 SELECT/WITH 开头,字符串中有 -- 也会拦截——需要时用 loaddata + 参数化。
  10. 多机构数据隔离:始终通过 OrgContext 获取 orgId,不要硬编码机构连接信息。