Skip to content

Commit db4c166

Browse files
iwangbowenCopilot
andcommitted
feat(cms): 阶段 3 —— 扩展模型三级绑定、字典选项来源与友链分组
对标 ChestnutCMS 三项经消费方核实的能力缺口。 ## C-1 站点级扩展模型绑定 ChestnutCMS 的 XModel 支持站点/栏目/内容三级绑定(SiteExtendModelProperty + EXModelEventListener/EXModelCoreDataHandler/siteExtend.vue 均有真实消费方), 我方此前只有栏目与内容两级,站点级运营元数据(备案信息、客服电话等)只能塞进 settings 无结构 JSONB。 - cms_sites 加 model_id + extend jsonb,与 cms_channels/cms_contents 同构 - 站点编辑页新增「扩展模型」Tab,按所选模型动态渲染字段 - 主题上下文暴露 site.extend - 解绑模型时同步清空 extend,避免残留异构字段 - modelId 走 ensureSiteModelValid 校验,与栏目同口径 ## C-2 模型字段选项支持字典来源 ChestnutCMS 的 XModelUtils.getOptions() 显式分支 OPTIONS_TYPE_DICT 并注入 ISysDictTypeService 解析。我方 options 只能手写 JSON,字典变更要逐个模型同步。 - cms_model_fields 加 option_source('manual'|'dict') + dict_code - 读取时批量解析为 resolvedOptions(一次查询解析全部字典,避免 N+1) - 前端 select/radio/checkbox 统一按 resolvedOptions 渲染 - 顺带修复 listAllCmsModels 不返回 fields 的既有缺陷 ## C-3 友链分组 ChestnutCMS 的 CmsLinkGroup 是独立实体(domain/mapper/service/controller/ <@cms_link_group> 标签齐全),不是字符串字段。分组需要排序与稳定引用标识, 字符串字段两者都表达不了,故建独立表。 - 新建 cms_friend_link_groups(siteId/name/code/sort/status) - cms_friend_links.group_id 外键 onDelete: set null —— 删组不删友链, 组内友链自动转为未分组 - 跨站引用防护:ensureFriendLinkGroupInSite 拒绝挂到别站分组 - 列表页加分组筛选与分组列,新增「分组管理」抽屉 - 主题 footer 按组分块渲染,未分组归入默认块;空组不渲染 ## 修复 - 模型字段写入路径此前遗漏 option_source/dict_code,实跑冒烟才暴露 - 站点更新路径此前遗漏 model_id/extend,同上 - 友链写操作响应补 groupName,与列表结构保持一致 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 1156cd5 commit db4c166

30 files changed

Lines changed: 59273 additions & 70 deletions

docs/cms/content-pipeline.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,22 @@
137137

138138
### 内容模型(自定义字段)
139139

140-
模型定义字段元数据(12 种类型:text/textarea/richtext/number/date/datetime/image/file/select/radio/checkbox/switch),值存入 `cms_contents.extend` JSONB。字段可配置必填、纳入检索(searchable)、列表显示。栏目绑定模型后,该栏目下内容编辑页动态渲染模型字段。
140+
模型定义字段元数据(12 种类型:text/textarea/richtext/number/date/datetime/image/file/select/radio/checkbox/switch),值存入 `extend` JSONB。字段可配置必填、纳入检索(searchable)、列表显示。
141+
142+
模型可绑定到**三级**对象,均通过各自的 `model_id` + `extend` 列承载:
143+
144+
| 绑定级 | 绑定位置 | 值存放 | 用途 |
145+
|---|---|---|---|
146+
| 站点 | `cms_sites.model_id` | `cms_sites.extend` | 站点级运营元数据(备案号、客服电话、App 下载地址等),主题上下文通过 `site.extend` 读取 |
147+
| 栏目 | `cms_channels.model_id` | 栏目下内容的 `extend` | 决定该栏目下内容编辑页动态渲染哪些扩展字段 |
148+
| 内容 | `cms_contents.model_id` | `cms_contents.extend` | 由所属栏目继承,换栏目时跟随目标栏目 |
149+
150+
**选项来源**`select` / `radio` / `checkbox` 三类字段的选项支持两种来源:
151+
152+
- **手动设置**(默认):在模型字段编辑区直接维护 `{label, value}[]`
153+
- **系统字典**:只填字典编码(如 `content_status`),选项在读取模型时实时解析自 `dict_items`(仅取启用项,按 sort 排序)。字典维护一处,所有引用它的模型字段自动同步,无需逐个模型改选项
154+
155+
解析结果通过 `resolvedOptions` 返回给前端,内容编辑页与站点扩展字段区统一按它渲染;`options` 保留手动来源的原始值。字典编码不存在时 `resolvedOptions` 为空数组,不影响其他字段渲染。
141156

142157
### 一文多栏目
143158

docs/cms/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ graph LR
3636
| 站点管理 | `/cms/sites` | 父子站群、显式继承、域名路由、主题、审核模式、Webhook | [站群与分发](./site-groups-and-distribution) |
3737
| 栏目管理 | `/cms/channels` | 左侧栏目树 + 右侧编辑区,树形栏目(列表/单页/外链),栏目标识 code + 级联 path | [内容管线](./content-pipeline) |
3838
| 内容管理 | `/cms/contents` | 5 态状态机、多形态内容(图文/图集/音视频/外链)、批量操作、导入导出、回收站 | [内容管线](./content-pipeline) |
39-
| 内容模型 | `/cms/models` | 12 种自定义字段类型(EAV via JSONB) | [内容管线](./content-pipeline) |
39+
| 内容模型 | `/cms/models` | 12 种自定义字段类型(EAV via JSONB),选项可绑系统字典,支持站点/栏目/内容三级绑定 | [内容管线](./content-pipeline) |
4040
| 标签管理 | `/cms/tags` | 站点级标签 + 前台聚合页 | [内容管线](./content-pipeline) |
4141
| 碎片管理 | `/cms/fragments` | 模板可引用的后台可编辑区块 | [互动与运营](./interaction) |
42-
| 友情链接 | `/cms/friend-links` | 前台页脚友链 | [互动与运营](./interaction) |
42+
| 友情链接 | `/cms/friend-links` | 前台页脚友链,支持分组管理与按组渲染 | [互动与运营](./interaction) |
4343
| 素材中心 | `/cms/resources` | 文件夹树、完整引用扫描、孤立素材治理任务与报告导出 | [内容管线](./content-pipeline) |
4444
| 站点静态化 | `/cms/sites` | 站点管理操作中提交全站静态化任务 | [渲染与静态化](./static-and-render) |
4545
| 检索管理 | `/cms/search` | 分词测试、词典、热词、死链检测 | [全文检索](./search) |

docs/cms/interaction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@
8686
- 字段支持文本长度、RE2-compatible 规则、邮箱、手机号、URL、数字范围及字段级自定义错误提示;自定义规则由服务端 `re2js` 线性时间引擎编译执行,`(a+)+$` 等表达式不会触发回溯型 ReDoS。浏览器约束仅用于体验,服务端始终重新验证。
8787
- 表单验证码策略支持继承站点、关闭、算术题与 Cloudflare Turnstile。Turnstile 固定调用官方验证地址并通过统一 `http-client` 启用 SSRF 防护、禁止重定向和超时。
8888
- Turnstile Secret 为 write-only:API 仅返回掩码,空串/掩码保留原值,显式 `null` 清除。
89-
- **友情链接**:名称/URL/Logo/排序,前台页脚渲染
89+
- **友情链接**:名称/URL/Logo/排序,可归入**友链分组**`cms_friend_link_groups`:名称 + 稳定标识 code + 排序 + 启停),主题按组分块渲染页脚;未分组友链归入 `code` 为空的默认组。分组删除后组内友链自动转为未分组,不会连带删除
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
CREATE TYPE "public"."cms_field_option_source" AS ENUM('manual', 'dict');--> statement-breakpoint
2+
CREATE TABLE "cms_friend_link_groups" (
3+
"id" serial PRIMARY KEY NOT NULL,
4+
"site_id" integer NOT NULL,
5+
"name" varchar(100) NOT NULL,
6+
"code" varchar(50) NOT NULL,
7+
"status" "status" DEFAULT 'enabled' NOT NULL,
8+
"sort" integer DEFAULT 0 NOT NULL,
9+
"remark" text,
10+
"created_by" integer,
11+
"updated_by" integer,
12+
"created_at" timestamp DEFAULT now() NOT NULL,
13+
"updated_at" timestamp DEFAULT now() NOT NULL
14+
);
15+
--> statement-breakpoint
16+
ALTER TABLE "cms_friend_links" ADD COLUMN "group_id" integer;--> statement-breakpoint
17+
ALTER TABLE "cms_model_fields" ADD COLUMN "option_source" "cms_field_option_source" DEFAULT 'manual' NOT NULL;--> statement-breakpoint
18+
ALTER TABLE "cms_model_fields" ADD COLUMN "dict_code" varchar(64);--> statement-breakpoint
19+
ALTER TABLE "cms_sites" ADD COLUMN "model_id" integer;--> statement-breakpoint
20+
ALTER TABLE "cms_sites" ADD COLUMN "extend" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
21+
ALTER TABLE "cms_friend_link_groups" ADD CONSTRAINT "cms_friend_link_groups_site_id_cms_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."cms_sites"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
22+
ALTER TABLE "cms_friend_link_groups" ADD CONSTRAINT "cms_friend_link_groups_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
23+
ALTER TABLE "cms_friend_link_groups" ADD CONSTRAINT "cms_friend_link_groups_updated_by_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
24+
CREATE UNIQUE INDEX "cms_friend_link_groups_site_code_uq" ON "cms_friend_link_groups" USING btree ("site_id","code");--> statement-breakpoint
25+
CREATE INDEX "cms_friend_link_groups_site_sort_idx" ON "cms_friend_link_groups" USING btree ("site_id","sort","id");--> statement-breakpoint
26+
ALTER TABLE "cms_friend_links" ADD CONSTRAINT "cms_friend_links_group_id_cms_friend_link_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."cms_friend_link_groups"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
27+
ALTER TABLE "cms_sites" ADD CONSTRAINT "cms_sites_model_id_cms_models_id_fk" FOREIGN KEY ("model_id") REFERENCES "public"."cms_models"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
28+
CREATE INDEX "cms_friend_links_site_group_idx" ON "cms_friend_links" USING btree ("site_id","group_id","sort","id");

0 commit comments

Comments
 (0)