基金会官网
基金会官网
业务用途
基金会官网模块是「低代码配置后台 + 公众开放接口」的范例。它为一家公益基金会搭建了完整的官网内容管理后台:站点配置、首页 Banner、关键数据看板、关于我们、团队、合作伙伴、荣誉、党建动态、公益项目、新闻、电子刊物、财务报告、捐赠记录、规章制度、志愿者 / 实习招募、科技成果转化需求、常见问题。
后台完全由 FoundationSeeder 在启动时用声明式 Schema 驱动生成(建表 + 建列表页 + 建菜单 + 建字典 + 灌 mock 数据),运营人员直接在低代码运行时列表页里增删改查内容,无需写一行代码。前台官网(Nuxt)则通过 OpenContentController 提供的公开只读接口拉取内容,支持匿名访问、字段白名单、敏感信息脱敏与提交限流。
这个模块的看点
一是 FoundationSchema.Field 这套声明式字段 DSL(一个方法一种控件类型,几行代码描述一张表);二是 OpenContentController 的公众开放接口安全设计(白名单 + 脱敏 + 限流 + 租户锁定),它是「低代码表如何安全对外暴露」的参考实现。
涉及文件
Seeder
| 文件 | 路径 | 职责 |
|---|---|---|
| 应用 Seeder | backend/src/main/java/com/lowcode/seeder/foundation/FoundationSeeder.java | CommandLineRunner,建应用 + 21 表 + 字典 + 列表页 + 菜单 + mock 数据 + 官网样式切换页,支持增量补丁 |
| 表结构定义 | backend/src/main/java/com/lowcode/seeder/foundation/FoundationSchema.java | 21 个 TableDef + Field DSL + toLcTableMeta / toLcColumns |
| 字典定义 | backend/src/main/java/com/lowcode/seeder/foundation/FoundationDictionaries.java | 16 个字典类型及字典项 |
| 演示数据 | backend/src/main/java/com/lowcode/seeder/foundation/FoundationMockData.java | 各表 mock 数据 |
控制器
| 文件 | 路径 | 基础路径 | 说明 |
|---|---|---|---|
| 后台快速配置 | controller/FoundationAdminController.java | /api/foundation | 官网布局样式切换 / 查询当前布局(需登录) |
| 公众开放接口 | controller/OpenContentController.java | /api/open/foundation | 前台官网无登录读 + 线索提交 + 捐赠聚合 |
数据库表
Seeder 动态创建的 21 张表
表数量说明
FoundationSchema 源码注释写「19 张表」,但实际 all() 返回 21 张表(注释未随 fnd_site_layout、fnd_faq 的新增同步更新)。以实际代码为准。
通用基字段由 SchemaService 自动补齐,下表按业务分组列出。
站点配置(businessGroup = config)
| 表名 | 注释 | 关键字段 |
|---|---|---|
fnd_site_config | 站点全局配置 | logo、slogan、phone、email、address、wechat_qrcode、icp、police_no、copyright_text |
fnd_site_layout | 官网设置 | layout_style、hero_mode、primary_color、accent_color、show_party_section、show_data_visualization、custom_css |
fnd_homepage_banner | 首页Banner | title、subtitle、media_type、media_url、cta_text、sort_order、enabled |
fnd_metric | 关键数据看板 | metric_key、title、value、suffix、icon、sort_order、enabled |
内容(businessGroup = content)
| 表名 | 注释 | 关键字段 |
|---|---|---|
fnd_about | 关于我们 | mission、vision、values_text、history_html、intro_video、intro_image |
fnd_team_member | 团队成员 | name、position、avatar、intro_html、sort_order、enabled |
fnd_partner | 合作伙伴 | name、logo、link、level、sort_order、enabled |
fnd_honor | 荣誉资质 | title、honor_date、image、description、sort_order |
fnd_party_news | 党建动态 | title、cover、content_html、publish_time、status、view_count |
fnd_news | 新闻动态 | title、category、cover、summary、content_html、source、publish_time、status、view_count、is_top |
fnd_publication | 电子刊物 | title、pub_type、cover、file_url、publish_year、publish_quarter、download_count |
fnd_faq | 常见问题 | question、answer_html、category、sort_order、enabled |
项目(businessGroup = project)
| 表名 | 注释 | 关键字段 |
|---|---|---|
fnd_project | 公益项目 | name、cover、intro_html、project_type、status、target_amount、raised_amount、beneficiaries、province、city、longitude、latitude、is_key |
信息公示(businessGroup = disclosure)
| 表名 | 注释 | 关键字段 |
|---|---|---|
fnd_finance_report | 财务报告 | title、report_type、report_year、file_url、publish_time |
fnd_donation | 捐赠记录 | donor_name、donor_type、amount、project_id、channel、donate_time、anonymous |
fnd_regulation | 规章制度 | title、category、file_url、summary、publish_time、sort_order |
招募 / 线索(businessGroup = recruit / leads)
| 表名 | 注释 | publicReadable | 关键字段 |
|---|---|---|---|
fnd_volunteer_post | 志愿者岗位 | 是 | title、city、duration、requirements、description、deadline、status |
fnd_volunteer_apply | 志愿者报名 | 否 | post_id、name、gender、phone、email、city、occupation、self_intro、status、review_remark |
fnd_intern_post | 实习岗位 | 是 | title、department、city、duration、requirements、description、deadline、status |
fnd_intern_apply | 实习申请 | 否 | post_id、name、school、major、grade、phone、email、resume_url、self_intro、status |
fnd_tech_demand | 科技成果转化需求 | 否 | contact_name、company、phone、email、field、demand_desc、attachment、status、follow_remark |
TableDef DSL 字段声明
FoundationSchema.Field 提供了一组静态工厂方法,每个方法对应一种字段类型 + 前端控件,让声明表结构像填表一样简单:
| 方法 | SQL 类型 | htmlType | 含义 |
|---|---|---|---|
Field.of(name, comment, type) | 自定义 | input | 普通输入框 |
Field.req(name, comment, type) | 自定义 | input | 必填输入框(isRequired=Y) |
Field.text(name, comment) | TEXT | textarea | 文本域 |
Field.longtext(name, comment) | LONGTEXT | textarea | 长文本域 |
Field.datetime(name, comment) | DATETIME | datetime | 日期时间 |
Field.date(name, comment) | DATE | datetime | 日期 |
Field.dictSel(name, comment, dictType) | VARCHAR(50) | select | 字典下拉(关联 dictType) |
Field.bool(name, comment) | TINYINT | radio | 布尔单选(默认 0) |
Field.decimal(name, comment) | DECIMAL(14,2) | input | 金额(默认 0) |
Field.intF(name, comment) | INT | input | 整数(默认 0) |
Field.bigintF(name, comment) | BIGINT | input | 长整数(默认 0) |
Field.file(name, comment) | VARCHAR(500) | file | 文件上传 |
Field.img(name, comment) | VARCHAR(500) | file | 图片上传 |
Field.richtext(name, comment) | LONGTEXT | richtext | 富文本编辑器 |
声明示例(公益项目表):
list.add(new TableDef("fnd_project", "公益项目", "project", "project", true, Arrays.asList(
Field.req("name", "项目名称", "VARCHAR(200)"),
Field.img("cover", "封面图"),
Field.richtext("intro_html", "项目介绍"),
Field.dictSel("project_type", "项目类型", "fnd_project_type"),
Field.dictSel("status", "项目状态", "fnd_project_status"),
Field.decimal("target_amount", "目标金额"),
Field.decimal("raised_amount", "已筹金额"),
Field.intF("beneficiaries", "受益人数"),
Field.of("longitude", "经度", "DECIMAL(10,6)"),
Field.bool("is_key", "首页重点")
)));TableDef 构造参数为 (tableName, tableComment, moduleName, businessGroup, publicReadable, fields),其中 businessGroup 决定菜单挂载目录,publicReadable 标记是否对前台官网公开只读。
字典(16 个)
| 字典码 | 名称 |
|---|---|
fnd_news_category | 新闻分类 |
fnd_project_type | 项目类型 |
fnd_project_status | 项目状态 |
fnd_publish_status | 发布状态(draft / published) |
fnd_publication_type | 刊物类型 |
fnd_finance_type | 财务报告类型 |
fnd_regulation_category | 制度分类 |
fnd_partner_level | 合作级别 |
fnd_donor_type | 捐赠人类型 |
fnd_donation_channel | 捐赠渠道 |
fnd_banner_media | Banner 媒体类型 |
fnd_lead_status | 线索状态 |
fnd_apply_status | 申请审核状态 |
fnd_recruit_status | 招募状态 |
fnd_layout_style | 官网布局样式 |
fnd_hero_mode | 首屏样式 |
实现机制
一、FoundationSeeder 流程
FoundationSeeder 是 @Component + CommandLineRunner,@Order(Ordered.LOWEST_PRECEDENCE - 100)(比 ErpAppSeeder 的 -101 靠后一位,先于 ERP 执行)。流程与 ERP Seeder 同构:
启动
├─ 默认租户不存在? -> 跳过本轮重试
├─ lc_application 已有 appCode=foundation_admin?
│ ├─ 是 -> applyIncrementalPatch()(补字典 / 补缺失表 / 补样式切换页)
│ └─ 否 -> 首次初始化:
│ 1. createApplication() APP_CODE=foundation_admin
│ 2. createTables(appId) 21 张表(DDL + 元数据)
│ 3. createDictionaries(appId) 16 个字典
│ 4. createListPages(...) 21 个列表页
│ 5. createMenus(...) 5 目录 + 叶子菜单 + 官网样式切换页
│ 6. seedMockData(...) mock 数据建表同样走 SchemaService.createTable(LcTableMeta)(DDL + 元数据一把梭),物理表已存在时走 ensureMetadataOnly 只补元数据。增量补丁模式保证新增表 / 字典 / 样式切换页重启即补齐。
二、列表页与菜单
每张表生成一个列表页(pageJson 单行单组件 type=list,绑定 tableId),路由 /runtime/foundation_admin/{tableName}。菜单按 businessGroup 挂到 5 个目录:内容管理、项目管理、信息公示、招募管理、站点配置。申请类表(fnd_volunteer_apply / fnd_intern_apply / fnd_tech_demand)归入「招募管理」。
额外地,Seeder 会在「站点配置」目录下创建一个官网样式切换页(pageJson 含 type=layout-switcher 组件),供运营一键切换首页布局。
三、FoundationAdminController 后台快速配置
为了让运营不进表设计器就能切官网样式,FoundationAdminController 提供两个接口:
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/foundation/site-layout/switch | 切换布局样式,body { "layoutStyle": "default" | "industrial" } |
| GET | /api/foundation/site-layout/current | 返回当前 fnd_site_layout 行 |
切换逻辑通过 SchemaService 操作单行表 fnd_site_layout:有行则 updateData,无行则 insertData(兜底插入默认主题色等)。接口需登录(由 SecurityConfig 默认要求 authenticated)。
@PostMapping("/site-layout/switch")
public Result<Map<String, Object>> switchLayout(@RequestBody Map<String, Object> body) {
String style = String.valueOf(body.getOrDefault("layoutStyle", ""));
if (!"default".equals(style) && !"industrial".equals(style)) {
return Result.error("仅支持 default / industrial 两种布局");
}
// 临时切到基金会租户上下文 -> 找 fnd_site_layout 元数据 -> 查现有行 -> update 或 insert
}四、OpenContentController 公众开放接口
这是基金会官网前台(Nuxt,无登录)的数据源。所有接口挂在 /api/open/foundation 下,无需登录。
接口清单
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /{publicCode}/list | 列表查询(分页 + 白名单过滤参数) |
| GET | /{publicCode}/{id} | 单条详情(news / party-news 自动 +1 浏览量) |
| POST | /{publicCode}/leads | 提交线索(仅 leads 类表) |
| GET | /donations/summary | 捐赠聚合(近 12 月趋势 + 渠道分布 + 总额 + 项目地理分布) |
publicCode 白名单
每个 publicCode 在代码里硬编码绑定一张表 + 允许读字段 + 允许查询参数 + 是否可写,与运营在 lc_open_table_api_config 里的配置无关:
| publicCode | 表 | 类型 | 说明 |
|---|---|---|---|
site-config / site-layout | fnd_site_config / fnd_site_layout | 读 | 站点配置 |
banners / metrics | fnd_homepage_banner / fnd_metric | 读 | 首页素材 |
about / team / partners / honors | fnd_about / fnd_team_member / fnd_partner / fnd_honor | 读 | 介绍类 |
party-news / news | fnd_party_news / fnd_news | 读 | 仅返回 status=published |
projects | fnd_project | 读 | 公益项目 |
publications / finance-reports / regulations | 对应 fnd_ 表 | 读 | 刊物 / 财报 / 制度 |
donations | fnd_donation | 读 | 捐赠记录(脱敏) |
volunteer-posts / intern-posts | fnd_volunteer_post / fnd_intern_post | 读 | 招募岗位 |
faqs | fnd_faq | 读 | 常见问题 |
volunteer-applies / intern-applies / tech-demands | fnd_volunteer_apply / fnd_intern_apply / fnd_tech_demand | 写 | 线索提交 |
安全设计(四重防护)
@RestController
@RequestMapping("/api/open/foundation")
public class OpenContentController {
// 1. 字段白名单:每个 publicCode 写死 allowedReadFields / allowedQueryParams / allowedWriteFields
// 2. 脱敏:捐赠表匿名时 donorName -> "爱心人士";实名保留姓 + *
// 3. 限流:POST leads 基于 IP,1 分钟最多 5 次(Redis 计数)
// 4. 租户锁定:强制 TenantContext = foundationTenantId,避免被其它租户同名表干扰
}脱敏是怎么做的
prune() 方法做字段裁剪 + 脱敏。对 fnd_donation:若 anonymous=1,donorName 输出「爱心人士」;否则保留首字 + 最多 3 个 *(如「张***」)。这样既公示捐赠信息又保护隐私。
限流是怎么做的
提交线索时用 StringRedisTemplate 对 key foundation:leads:rate:{publicCode}:{ip} 自增,首次设 1 分钟过期,超过 5 次返回 429 提交过于频繁。Redis 不可用时降级为只记日志、不阻断(避免影响可用性)。
操作步骤
1. 启动后端自动 seeding
mvn spring-boot:run
# 日志:[FoundationSeeder] 初始化完成。访问 /app/foundation_admin 即可进入后台。2. 后台管理内容

进入「广西未来科技发展基金会」应用,左侧菜单可见「内容管理 / 项目管理 / 信息公示 / 招募管理 / 站点配置」。每个菜单项就是一张表的列表页,直接增删改查。
3. 切换官网布局样式
# 切到工业风
curl -X POST http://localhost:52856/api/foundation/site-layout/switch \
-H "Content-Type: application/json" \
-d '{"layoutStyle": "industrial"}'
# 查当前布局
curl http://localhost:52856/api/foundation/site-layout/current4. 前台官网拉取公开内容
# 新闻列表(自动只返回 published)
curl 'http://localhost:52856/api/open/foundation/news/list?pageNum=1&pageSize=10&category=org'
# 新闻详情(自动 +1 浏览量)
curl http://localhost:52856/api/open/foundation/news/1
# 捐赠聚合数据(前台数据可视化用)
curl http://localhost:52856/api/open/foundation/donations/summary5. 公众提交线索
# 志愿者报名(匿名可提交,1 分钟内同 IP 最多 5 次)
curl -X POST http://localhost:52856/api/open/foundation/volunteer-applies/leads \
-H "Content-Type: application/json" \
-d '{
"postId": 1,
"name": "李志愿者",
"phone": "13800000000",
"email": "li@example.com",
"city": "南宁",
"selfIntro": "热心公益,有支教经验"
}'
# 默认 status=pending,后台在招募管理里审核常见问题
后台表为什么没有审批流程?
基金会是内容管理场景,运营直接编辑发布,不需要 ERP 那样的审批闭环。所以 FoundationSeeder 不创建 LcForm、不绑 BPM 定义,BpmBusinessStatusCallback 也因这些表没有 approval_status 列而自动跳过(opt-in 设计)。如需内容审核流,可参照 的方式补建表单 + 流程定义。
公开接口报「资源不存在」?
publicCode 白名单硬编码在 OpenContentController.SPECS 里,不在名单里的 code 返回 404。请确认 publicCode 拼写(如 volunteer-applies 不是 volunteer_apply)。读接口对写专用表(leads 类)也返回 404,反之亦然。
提交线索报 429?
同 IP 1 分钟内提交超过 5 次会触发限流。等 1 分钟或换网络即可。若 Redis 未启用,限流检查会降级(只记日志),不会误拦。
怎么新增一张官网内容表?
在 FoundationSchema.all() 末尾加一个 TableDef(用 Field DSL 声明字段),如需前台可见再到 OpenContentController.buildSpecs() 加一个 publicCode 映射。重启后端,Seeder 增量补丁会自动建表 + 列表页 + 菜单。
