diff --git a/sql/t16_app_registry.sql b/sql/t16_app_registry.sql new file mode 100644 index 0000000..b7d05bc --- /dev/null +++ b/sql/t16_app_registry.sql @@ -0,0 +1,39 @@ +-- ============================================================ +-- 表单中心 T16 · app 注册表 DDL(MySQL 5.7 兼容复核版) +-- 范围:《表单中心契约v2》§5.2/§17.2 一笔 = 建表 app_registry +-- 验收:V59(零角色字段)/ V65(uk(app_code) 全库唯一 + 保留字)/ V69(CRUD 与在用禁删) +-- 用途:生产 ddl-auto=none 时的升级脚本;开发期(本机 bsch_formcenter,ddl-auto=update)由 Hibernate +-- 自动追加,本脚本同时是"实表应当长什么样"的评审权威(沿 t2/t3/t10 先例)。 +-- 5.7 复核要点:①无 CTE/WITH、无函数索引、无生成列、无 CHECK 约束;②utf8mb4 显式写 CHARSET/COLLATE; +-- ③InnoDB、无物理外键——form.app_code 对注册表是逻辑引用(§5.2 明令"不校验存在性亦可建 +-- 表单",建 FK 等于把这条改写成强引用,快应用还没注册就先建表的老数据会直接进不来); +-- ④本表无可空列(is_deleted/sch_id 都 NOT NULL,审计列按 P5 实态 DEFAULT NULL)。 +-- **零角色字段(D14/V59 红线)**:全表只有 app_code / name / sch_id / is_deleted 四个业务列 +-- + BaseEntity 四审计列。任何"谁能用这个 app""app 的管理员"之类的列都不许加—— +-- 把授权塞回注册表正是 v2 要出清的东西(§19 作废-1/2 的同一件事)。 +-- 唯一键口径:uk_app_code 是全库唯一且不含 is_deleted(§17.2 原文),所以软删后的 code +-- 仍被占用、不可复用(这层落地差额登记为问题单 P125,服务侧给可诊断文案而不是撞库 500)。 +-- 非幂等:MySQL 5.7 无 ADD/CREATE IF NOT EXISTS 的评审友好写法(写了反而掩盖"表已存在但列不对"的漂移)。 +-- 执行前先 `SHOW TABLES LIKE 'app_registry'` 确认不存在;存量:生产零存量(本表 T16 才建),无需回填。 +-- ============================================================ + +CREATE TABLE `app_registry` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `app_code` VARCHAR(64) NOT NULL COMMENT '全局唯一人读 slug(C10 格式 ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])$;保留字 form-center 不可注册)', + `name` VARCHAR(128) NOT NULL COMMENT '应用显示名(入口链接要贴去 UserBase,名字与那边对照用)', + `sch_id` BIGINT NOT NULL COMMENT '建立者会话校;0=平台级(仅超管可建,D14)', + `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删标记;名下存在 is_deleted=0 的表单行时禁删(§5.2 在用禁删)', + `create_by` VARCHAR(255) DEFAULT NULL, + `create_time` DATETIME DEFAULT NULL, + `update_by` VARCHAR(255) DEFAULT NULL, + `update_time` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_app_code` (`app_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='快应用注册表(契约 v2 §5.2:四业务字段、零角色、无审核;form.app_code 为逻辑引用不建 FK)'; + +-- 自验(执行后逐条核对,V59/V65): +-- SHOW CREATE TABLE app_registry\G -- 应含 UNIQUE KEY `uk_app_code` (`app_code`),且列只有上面这些 +-- SELECT COUNT(*) FROM information_schema.columns +-- WHERE table_schema=DATABASE() AND table_name='app_registry' +-- AND column_name REGEXP 'role|perm|admin|user_id'; -- 必须 0 行(V59 的机械判据) diff --git a/src/main/java/com/yunhai/formcenter/controller/api/AppRegistryController.java b/src/main/java/com/yunhai/formcenter/controller/api/AppRegistryController.java new file mode 100644 index 0000000..b4b54a3 --- /dev/null +++ b/src/main/java/com/yunhai/formcenter/controller/api/AppRegistryController.java @@ -0,0 +1,73 @@ +package com.yunhai.formcenter.controller.api; + +import com.yunhai.formcenter.model.AppModels; +import com.yunhai.formcenter.service.AppRegistryService; +import com.yunhai.formcenter.service.FormAdminGuard; +import com.yunhai.platform.utils.model.BoaUser; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 快应用注册表端点族(契约 v2 C17/§5.2,T16)。 + * + *
+ * GET /api/form/admin/apps → 本校 ∪ 平台级清单(每行附中心生成的 entryUrl)
+ * POST /api/form/admin/apps → 建(appCode + name;超管可 schId 指定域,含 0)
+ * PUT /api/form/admin/apps/{appCode} → 改名
+ * DELETE /api/form/admin/apps/{appCode} → 软删(在用禁删)
+ *
+ *
+ * 守卫与管理员两表同一把:{@link FormAdminGuard#requirePlatformAdmin}(校管理员 ∪ 超管,C4③)—— + * 模块行退役后"应用设置页进不进得来"就是这一个判据。寻址一律按 {@code appCode}(库内 id 不外露,V5), + * 因此改名以外的写操作没有"按 id 改 code"的面。
+ * + *返回形态沿两表那组端点:写操作直接回操作后的单行,前端自己更新那一行即可—— + * 应用数量是个位数,不需要为了省一次刷新而把全量列表塞进每个写响应。
+ */ +@RestController +@RequestMapping("/api/form/admin/apps") +@RequiredArgsConstructor +public class AppRegistryController { + + private final FormAdminGuard adminGuard; + private final AppRegistryService appService; + + /** 清单(超管可用 {@code schId} 显式换域看某校,与两表端点同一条代查口径) */ + @GetMapping + public List四个业务列,零角色列(D14/V59 红线):这张表只回答"有哪些应用、叫什么名字、谁建的、删没删"。 + * "谁能进哪个应用"一律不在这张表里——它由 {@code DataScopeService} 按会话在该应用的表单上现算 ALL/SELF 档。 + * 一旦这里出现 {@code role}/{@code admin}/{@code user_id} 之类的列,就等于把 v2 出清掉的授权层 + * 从后门搬回来({@code AppRegistryIT} 用反射把这条钉成机械断言)。
+ * + *{@code app_code} 全库唯一({@code uk_app_code},不含 is_deleted):软删后 code 仍被占用, + * 不复用(P125)。它是 oapi 游标拉取的域段与 MQ routing key 的组成段,因此 {@code updatable=false}—— + * 改名走 {@code name},code 一旦发布就等于对外契约的一部分(UserBase 那边贴的是含它的入口链接)。
+ */ +@Getter +@Setter +@Entity +@Table(name = "app_registry", + uniqueConstraints = @UniqueConstraint(name = "uk_app_code", columnNames = {"app_code"})) +public class AppRegistry extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** 全局唯一人读 slug(C10 格式;保留字 {@code form-center} 不可注册),发布后不改 */ + @Column(name = "app_code", nullable = false, length = 64, updatable = false) + private String appCode; + + /** 应用显示名 */ + @Column(name = "name", nullable = false, length = 128) + private String name; + + /** 建立者会话校;0=平台级(仅超管可建) */ + @Column(name = "sch_id", nullable = false, updatable = false) + private Long schId; + + /** 软删标记——唯一删除形态;在用(名下有未软删表单)禁删(§5.2) */ + @Column(name = "is_deleted", nullable = false, columnDefinition = "tinyint") + private Integer isDeleted = 0; +} diff --git a/src/main/java/com/yunhai/formcenter/model/AppModels.java b/src/main/java/com/yunhai/formcenter/model/AppModels.java new file mode 100644 index 0000000..da191eb --- /dev/null +++ b/src/main/java/com/yunhai/formcenter/model/AppModels.java @@ -0,0 +1,44 @@ +package com.yunhai.formcenter.model; + +import lombok.Getter; +import lombok.Setter; + +/** + * 快应用注册表的出入参(契约 v2 C17/§5.2,T16)。 + * + *对外身份只有 {@code appCode}(人读 slug,也是入口链接与 MQ routing key 的组成段), + * 库内 {@code id} 一律不外露(契约 §1/V5);因此 PUT/DELETE 都按 code 寻址而不是按 id。
+ */ +public final class AppModels { + + private AppModels() { + } + + /** 注册行:四个业务字段 + 中心生成的入口链接 */ + @Getter + @Setter + public static class AppView { + private String appCode; + private String name; + /** 归属租户域;0=平台级(仅超管可建) */ + private Long schId; + /** 应用入口链接(§5.2"由中心生成"),管理员复制后贴去 UserBase 建应用 */ + private String entryUrl; + } + + /** 建:app_code + name;{@code schId} 只有超管能用它指定目标域(校管理员一律按会话校落) */ + @Getter + @Setter + public static class CreateRequest { + private String appCode; + private String name; + private Long schId; + } + + /** 改:C17 只开改名(code 一旦发布就是对外坐标,不随改) */ + @Getter + @Setter + public static class UpdateRequest { + private String name; + } +} diff --git a/src/main/java/com/yunhai/formcenter/repository/AppRegistryRepository.java b/src/main/java/com/yunhai/formcenter/repository/AppRegistryRepository.java new file mode 100644 index 0000000..7a811d8 --- /dev/null +++ b/src/main/java/com/yunhai/formcenter/repository/AppRegistryRepository.java @@ -0,0 +1,25 @@ +package com.yunhai.formcenter.repository; + +import com.yunhai.formcenter.entity.AppRegistry; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * app 注册表仓库(T16)。查询一律带租户域与软删过滤,唯一例外是 + * {@link #findByAppCode}——它要连软删行一起看,否则"code 曾被注册"会被撞成库内唯一键 500 + * 而不是一句可诊断的拒绝(P125)。 + */ +public interface AppRegistryRepository extends JpaRepository