--- name: cross-plugin-fullstack-development description: Full-stack development workflow for Madong plugins. Covers plugin structure, backend CRUD and Swagger, frontend template source, menu import, install copy behavior, i18n, verification, and anti-regression rules for uninstall/reinstall scenarios. globs: - "backend/plugin/**/*.php" - "backend/plugin/**/config/**/*.php" - "backend/plugin/**/resource/**/*.php" - "backend/plugin/**/resource/template/admin/**/*" - "frontend/admin/src/apps/**/*.vue" - "frontend/admin/src/apps/**/*.tsx" - "frontend/admin/src/apps/**/*.ts" - ".windsurf/skills/**/*.md" --- # Madong 插件全流程开发技能 用于开发类似 `article` 这样的完整插件,覆盖从目录结构、后端 CRUD、Swagger、菜单导入、前端模板源、安装复制机制、国际化,到卸载重装后的防回退验证。 ## 适用场景 - 新建一个完整业务插件。 - 为已有插件补齐后台 CRUD、前台模板、菜单和语言文件。 - 修复插件卸载再安装后的页面回退、菜单异常、Swagger 冲突。 - 将一次真实插件开发过程沉淀为可复用工作流。 ## 总原则 1. 先核对项目真实实现,再使用 skill 或模板。 2. 插件前端的权威源目录是 `backend/plugin/{plugin}/resource/template/admin`,不是运行目录。 3. 安装后复制出来的 `frontend/admin/src/apps/{plugin}` 属于运行产物,不作为正式开发目录;若只改这里,卸载重装会回退。 4. 后端 Swagger 路由必须唯一,旧演示接口不可与新 CRUD 路由重复。 5. 插件菜单以安装导入的系统菜单为准,不依赖前端自动扫描插件路由生成侧边栏。 6. 前后端路径、权限码、菜单 component、i18n key 必须一致。 7. 插件 skill 若与真实项目不一致,先更新 skill 再继续生成或修改。 8. 插件开发顺序默认采用 template-first:后端完成后直接开发 `resource/template/admin`,不再单独维护运行目录。 ## 先理解插件安装与复制机制 ### 1. 模板源与运行目录 **模板源目录:** ```text backend/plugin/{plugin}/resource/template/admin/ ``` **安装后目标目录:** ```text frontend/admin/src/apps/{plugin}/ ``` 当前项目会在插件安装时通过 `core\plugin\traits\TemplateTrait::copyTemplateDir('admin')` 把模板源整体复制到前端运行目录。 ### 2. 关键结论 - 改 `frontend/admin/src/apps/{plugin}` 只会影响当前运行结果。 - 改 `backend/plugin/{plugin}/resource/template/admin` 才能影响未来安装结果。 - 只要模板源还是旧版,卸载再安装就会把旧版重新覆盖回来。 ### 3. 正确维护顺序 修改插件前端时遵循: 1. 先完成后端开发与接口联调。 2. 后端完成后,直接在 `backend/plugin/{plugin}/resource/template/admin` 下开发前端模板源。 3. 不要把 `frontend/admin/src/apps/{plugin}` 作为正式开发目录。 4. 完成模板源后,通过插件安装或复制流程生成运行目录并验证结果。 5. 仅当用户明确要求排查运行态问题时,才临时查看运行目录,但不要把它作为主维护对象。 ## 开发前检查 ### 1. 确认插件真实目录结构 以 `article` 为例: ```text backend/plugin/article/ ├── app/ ├── config/ ├── resource/ │ ├── menu/ │ ├── template/admin/ │ └── ... ``` 至少核对: - `app/adminapi/controller` - `app/service/admin` - `app/dao` - `app/model` - `app/validate` 或 `app/adminapi/validate` - `resource/template/admin/api` - `resource/template/admin/views` - `resource/template/admin/lang` - `resource/menu/admin.php` - `config/translation.php` - `config/route.php` ### 2. 确认参考实现 优先寻找以下真实参考: - 同类已完成插件 - 主项目现有 `BasicCrud` 页面 - 主项目已接好 Swagger 的 `Crud` 控制器 - 现有插件菜单定义 - 现有插件语言文件加载方式 ### 3. Windows 开发环境检查 在 Windows 下开发插件时,注意新插件目录创建本身未必会立即触发外层监控重启。 如果新插件接口返回 404,需要核对监控是否覆盖 `backend/plugin` 根目录变更。 ## 插件目录与产物约定 ## 后端 ```text backend/plugin/{plugin}/app/ ├── adminapi/controller/ ├── adminapi/validate/ ├── api/controller/ ├── dao/ ├── model/ ├── schema/request/ ├── schema/response/ └── service/admin/ ``` ## 模板源前端 ```text backend/plugin/{plugin}/resource/template/admin/ ├── api/index.ts ├── lang/ │ ├── zh-cn/zh.json │ └── en/en.json └── views/{plugin_key}/{module}/ ├── index.vue └── schemas/index.tsx ``` ## 安装后前端运行目录 ```text frontend/admin/src/apps/{plugin}/ ├── api/index.ts ├── lang/ │ ├── zh-cn/zh.json │ └── en/en.json └── views/{plugin_key}/{module}/ ├── index.vue └── schemas/index.tsx ``` ## 后端开发流程 ### 1. 数据库迁移 路径: ```text backend/plugin/{plugin}/resource/database/migrations/ ``` 要求: - 主键优先使用雪花 ID:`bigInteger('id')->primary()`。 - 按真实项目风格补齐索引、时间字段、软删除字段。 - 状态字段优先使用 `enabled` 或业务已有枚举字段。 - 不要直接照搬 Laravel 默认 `id()`。 - 不要从旧项目或旧插件盲目继承 `tenant_id`、多租户索引、租户作用域;必须先核对当前项目是否仍为单租户结构。 ### 2. Model / DAO / Service / Validate 要求: - Model 继承插件或项目当前实际使用的 `BaseModel`。 - DAO 继承 `core\base\BaseDao`。 - Service 继承 `core\base\BaseService`。 - Validate 继承 `core\base\BaseValidate`。 - 自定义业务逻辑放在 Service,不要塞进 Controller。 本项目额外要求: - DAO / Service 文件中必须显式 `use core\base\BaseDao;`、`use core\base\BaseService;`,不能只写 `extends BaseDao/BaseService` 而省略导入。 - Service 构造函数使用容器注入 DAO,例如 `__construct(FooDao $dao)`,不要在 Service 内部手动 `new FooDao()`。 - 如果 DAO 重写 `selectList()`,方法签名必须与当前项目 `BaseDao` 保持一致,包括: - `string|array $field = '*'` - `int $page = 0` - `int $limit = 0` - `bool $search = false` - `?array $withoutScopes = null` - `getKeywordFields()` 优先使用 `protected`,与当前项目已落地 DAO 风格保持一致。 - 若页面会通过 `Crud` 的 `format=select` 复用下拉,模型应提供 `getNameAttribute()`,把业务显示字段映射到 `name`,以兼容 `title / name / real_name / id` 标签解析顺序。 - JSON 字段应同时在 Model 中配置 `casts`,并在 Validate 中支持“数组或 JSON 字符串”两种输入形态。 ### 3. Admin 控制器与 CRUD 控制器通常放在: ```text backend/plugin/{plugin}/app/adminapi/controller/ ``` 要求: - 继承 `app\adminapi\controller\Crud`。 - 构造函数注入 Service 和 Validate。 - 标准方法走 `parent::index()`、`parent::show()`、`parent::store()`、`parent::update()`、`parent::destroy()`。 - 路由路径与前端 `BaseService` 的 `baseUrl` 一致。 ### 4. Swagger / OpenAPI 规范 插件最容易踩的坑之一是 Swagger 路由冲突。 必须遵守: - 每个 `OA\Get`、`OA\Post`、`OA\Put`、`OA\Delete` 的 `path` 必须唯一。 - 新 CRUD 控制器不要和旧 demo Controller 使用相同路径。 - 若存在旧演示接口,迁移到 `legacy` 命名空间或改用不冲突路径。 - 详情、删除、批量删除等接口按项目现有 Swagger 属性写法统一。 - `IdRequest`、`BatchDeleteRequest`、`SchemaConstants::X_SCHEMA_REQUEST` 等参数约定要与项目现有控制器保持一致。 ### 5. 路由与权限码 建议保持如下映射: ```text GET /{plugin}/{resource} GET /{plugin}/{resource}/{id} POST /{plugin}/{resource} PUT /{plugin}/{resource}/{id} DELETE /{plugin}/{resource}/{id} ``` 要求: - `addAuth`、`editAuth`、`removeAuth`、`viewAuth` 与后端权限码一致。 - 菜单按钮权限码与控制器 `Permission` 属性一致。 - 不要出现前端权限码、后端权限码、菜单权限码三套不一致。 ## 前端模板源开发流程 ### 1. 后端完成后直接开发模板源 插件前端正式维护目录: ```text backend/plugin/{plugin}/resource/template/admin/ ``` 后端完成后,直接在该目录开发插件前端。 默认不要单独开发 `frontend/admin/src/apps/{plugin}`。 运行目录只作为安装产物和验证结果使用。 ### 2. index.vue 统一使用官方 BasicCrud 标准页面入口应为: - `BasicCrud` - `useCrud` - `crudSchema` 不要继续保留插件 demo 里的: - `el-card` - 手写 `el-table` - `fetchData()` 拉取列表 ### 3. schemas/index.tsx 要求: - 使用 `CrudSchema`。 - 列表接口优先接 `BaseService.list`。 - 标准 CRUD 页面使用 `crudApi.add/edit/remove/view`。 - `rowKey` 默认使用 `id`。 - 表头索引统一使用 `article.common.columns.index` 这类语言键。 - 查询条件使用项目真实后端支持的查询参数格式,例如 `LIKE_name`、`LIKE_title`。 ### 4. API 层 插件模板 API 通常放在: ```text backend/plugin/{plugin}/resource/template/admin/api/index.ts ``` 要求: - 使用 `BaseService` 生成标准 CRUD Service。 - 仍可保留兼容旧页面的 `pluginApi` 包装方法。 - `baseUrl` 必须与后端控制器路径一致。 - 自定义只读接口可继续使用 `request.get()` 封装。 - 如运行目录为调试临时新增了 API 类型、Service 或语言键,最终必须同步回模板源,否则插件重装后会回退。 ### 5. i18n 语言文件 路径: ```text backend/plugin/{plugin}/resource/template/admin/lang/zh-cn/zh.json backend/plugin/{plugin}/resource/template/admin/lang/en/en.json ``` 要求: - 语言文件根对象使用插件名,例如 `article`。 - 如果 `schemas` 中使用了: - `common.columns.index` - `modules.*.search.*` - `modules.*.form.*` - `modules.*.columns.*` 则中英文都必须补齐。 - 不要只改中文不改英文,也不要只改运行目录不改模板源。 ## 插件菜单与路由加载规则 ### 1. 插件菜单来源 插件侧边栏应来自**安装时导入的系统菜单**,不是前端自动扫描插件 `routes/index.ts`。 因此: - 不要依赖 `frontend/admin/src/apps/{plugin}/routes/index.ts` 自动显示菜单。 - 插件安装后应由 `sys_menu` 数据决定导航是否出现。 ### 2. 菜单 component 路径 菜单中的 `component` 应填写相对 `views` 的组件路径,并包含插件 key 目录段。 例如页面文件: ```text frontend/admin/src/apps/article/views/article/content/index.vue ``` 则菜单 component 应使用类似: ```text /article/content/index ``` ### 3. meta.module 安装导入菜单时,`app` 字段应默认使用插件名 `{plugin}`,以便运行时生成: ```text meta.module = {plugin} ``` 这样组件加载器才会到: ```text frontend/admin/src/apps/{plugin}/views... ``` 去查找页面。 ## 防回退规则 这是插件开发最重要的回归规则。 ### 1. 何时会回退 满足以下条件时,卸载再安装会回退: - 你只改了 `frontend/admin/src/apps/{plugin}`。 - 模板源 `backend/plugin/{plugin}/resource/template/admin` 仍是旧版。 - 重装时安装器重新复制旧模板源。 ### 2. 如何避免回退 每次插件前端修改完成后,必须检查: - `index.vue` 是否同步到模板源。 - `schemas/index.tsx` 是否同步到模板源。 - `api/index.ts` 是否同步到模板源。 - `lang/zh-cn/zh.json` 与 `lang/en/en.json` 是否同步到模板源。 ### 3. 最终交付规则 若用户要求“以后重装不要丢”,交付标准不是“当前页面能看”,而是: - 运行目录正常。 - 模板源正常。 - 卸载后重装仍正常。 ## 推荐开发顺序 1. 明确插件名、模块名、路由、权限码。 2. 建迁移、模型、DAO、Service、Validate、Controller。 3. 接好 Swagger 注解,先排除路由冲突。 4. 配置菜单与权限。 5. 在模板源下实现前端 `api + index.vue + schemas + lang`。 6. 安装插件,复制模板到运行目录。 7. 在运行目录联调。 8. 如联调时临时改了运行目录,最终再同步回模板源。 9. 卸载再安装,确认不回退。 ## 验证清单 ### 后端验证 - 启动后无 Swagger `Unable to merge @OA\Get()` 等冲突。 - CRUD 路由与权限码可正常访问。 - 列表接口返回 `items/total`,与 `Crud::formatNormal()` 对齐。 - 删除接口支持单个 `id` 或批量 `ids`,与项目基类能力一致。 ### 前端验证 - 页面使用官方 `BasicCrud` 外观。 - 列表数据结构与 `BaseService` 返回兼容。 - 中英文切换无缺失 key。 - 菜单能正常打开到对应插件页面。 ### 重装验证 - 卸载插件。 - 重新安装插件。 - 确认 `frontend/admin/src/apps/{plugin}` 被重新复制后,页面仍保持最新结构。 - 确认没有回退到旧 `el-table` demo 版本。 ## 常见坑位 ### 1. Swagger 冲突 原因: - 新旧控制器共用了同一路径。 - 旧 `ArticleController` demo 接口未迁移。 处理: - 迁移旧接口到 `legacy` 路径。 - 新 CRUD 控制器使用正式资源路径。 ### 2. 重装后页面回退 原因: - 只改运行目录,没改模板源。 处理: - 以模板源为主,运行目录为辅。 ### 3. 插件菜单显示异常 原因: - 把插件前端 routes 自动扫描混进了静态路由。 - 未安装插件也显示菜单。 处理: - 插件导航仅依赖后端导入菜单。 - 不把 apps 自动扫描结果混进后端菜单模式。 ### 4. 模板目录编辑器报别名错误 现象: - `@/components/crud` - `@/locales` - `@/enums` 原因: - 模板源目录不是主前端 tsconfig 的真实编译入口。 结论: - 这是编辑器静态提示,不等于安装后运行目录会构建失败。 - 但模板源文件内容仍要与运行目录保持一致。 ### 5. 插件第二批模块可打开但接口 500 常见根因: - Service 忘记显式导入 `core\base\BaseService` - DAO 忘记显式导入 `core\base\BaseDao` - Service 构造器手动 `new Dao()`,未按项目真实风格使用容器注入 - DAO 重写 `selectList()` 但方法签名与当前 `BaseDao` 不一致 处理: - 优先对照同插件已可用一期模块的 DAO / Service 写法,不要只凭旧记忆生成。 - 修复顺序优先:`use` 导入 -> 构造注入 -> 方法签名 -> 关联字段 / validate。 ### 6. 旧项目字段迁移后前端下拉不显示名称 常见根因: - 模型没有提供 `name` 访问器 - `Crud::formatSelect()` 只能按 `title / name / real_name / id` 兜底取标签 处理: - 为需要复用 `format=select` 的模型增加 `getNameAttribute()`,将实际显示字段映射到 `name`。 ### 7. JSON 规则字段仅在后端可存,前端表单难以维护 处理建议: - 后端 Validate 接受数组和 JSON 字符串双形态。 - 模型 `casts` 统一转为数组。 - 模板源 `schemas` 中优先使用 textarea 输入 JSON 文本,并同步中英文 placeholder。 ### 8. Windows 下修改插件 Seeder / Install 后重装仍执行旧逻辑 现象: - 已修改 `backend/plugin/{plugin}/resource/database/seeds/*.php` 或 `backend/plugin/{plugin}/Install.php`。 - 重新点击插件安装后,菜单等流程有变化,但 Seeder 结果仍像旧代码。 - 日志看不到新的种子插入 SQL。 常见根因: - Windows 开发态 `config/process.php` 中 `process.monitor.constructor.options.enable_file_monitor` 被关闭。 - 运行中的 Webman 进程未重载,仍持有旧版类定义。 - `SeedTrait::runSeed()` 使用 `require_once`,同一进程内不会重新加载已声明 Seeder 类。 处理: - 确认 `backend/config/process.php` 的 `enable_file_monitor` 在 Windows 开发态也开启。 - 修改插件 `Seeder / Install / Controller / Service / Dao` 后,若不确定是否已重载,先手动重启后端进程再验证。 - 排查“重装插件不生效”时,不要只盯安装接口日志,还要先确认当前进程是否加载了最新代码。 ## 输出契约 当用户要求“完成一个插件开发”时,至少应交付: - 后端 CRUD 可运行。 - Swagger 无冲突。 - 菜单、权限、路由一致。 - 模板源前端完整。 - 运行目录联调通过。 - 卸载再安装不回退。 若只完成运行目录修改,不能判定为插件开发真正完成。