- 包 com.yunhai.formcenter:config / controller(api|oapi) / entity / enums / model / repository / service / common - pom:剥 aviator、加 easyexcel 3.3.2、finalName=form-center、surefire 收 *Test+*IT(CI 不跑单测→本地 mvn test 是门禁) - FormBaseController 平移 FlowBaseController:/api/base/GetUserInfo + GetUserMod,新增可选 target appCode(H4 未上线→空树+warn,不回落本应用树) - CORS 与 yml 分层照搬:jar 内只留本地最小默认,端口/ctx/DB/SSO/redis 由 projman env 注入;notice 的 upload-path 容器锚定教训注释原样抄入,FILE_PATH 本地兜底键保留 - 自验:mvn -o clean package BUILD SUCCESS + 7 单测全绿 + 本地启动 /actuator/health 200(/api/* 302 CAS 墙、/oapi/* 401 验签墙) - docs/T1-report.md:产出说明 + 13 条问题单
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
# Maven
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.properties
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,113 @@
|
||||
# T1 执行说明 · 表单中心工程骨架
|
||||
|
||||
> 任务书:《表单中心任务书-Qoder-20261014.md》v1.1 §T1|执行日期:2026-10-14|执行方:Qoder
|
||||
> 前置事实:H1(projman 建项目)已完成——两仓已由 projman 注入 CI 与(后端)Dockerfile。
|
||||
|
||||
---
|
||||
|
||||
## 1. 做了什么
|
||||
|
||||
### 1.1 仓库与目录
|
||||
|
||||
| 仓 | 本地路径 | 远端 |
|
||||
| --- | --- | --- |
|
||||
| 后端 | `D:\SoftProject\yunhai\form-center\service` | `form-center/form-center-service.git` |
|
||||
| 前端 | `D:\SoftProject\yunhai\form-center\front` | `form-center/form-center-front.git` |
|
||||
|
||||
两仓均为 **clone 既有远端仓**(非本地 init),projman 注入的 `.gitea/workflows/build.yml`、
|
||||
`docker/Dockerfile`、`README.md` 天然在工作树与历史中,未被覆盖、未被删除。
|
||||
|
||||
### 1.2 后端(fork flow-center/service 骨架)
|
||||
|
||||
- **包结构** `com.yunhai.formcenter`:
|
||||
- `config/FormCorsConfiguration`(平移 `FlowCorsConfiguration`,键名换 `form-center.cors.allowed-origins`)
|
||||
- `controller/api/FormBaseController`(见下)
|
||||
- `controller/oapi`(T4 落契约 §9 两个读端点,本任务先声明包)
|
||||
- `entity/BaseEntity`(自增主键 + 审计四件套;`id` 绝不外露,对外身份是 uuid/code——契约 §1)
|
||||
- `common/FormCenterAuditListener`、`enums`、`model`、`repository`、`service`
|
||||
- 空包以 `package-info.java` 声明(javadoc 里写明该包 T2/T4 装什么、受哪条契约约束)
|
||||
- **pom**:照 flow-center 模板(Boot 2.7.18 / JDK8 / `yh-platform-dependencies` BOM /
|
||||
`yh-spring-boot-starter` 聚合 / `yh-appearance-spring-boot-starter` / data-redis / cas-client-core 3.5.1 /
|
||||
mysql-connector-j / lombok / starter-test);**剥 aviator**;**加 `com.alibaba:easyexcel:3.3.2`**;
|
||||
`finalName=form-center`;surefire 收 `*Test.java` + `*IT.java`(CI 不跑单测→本地 `mvn test` 是门禁)。
|
||||
- **FormBaseController**:`GET /api/base/GetUserInfo`(会话用户)、`GET /api/base/GetUserMod`(本应用授权模块树,
|
||||
经 SDK `YschBaseOpenApi.getUserModule`,只做字段改名不组树,与其他子应用同口径),
|
||||
**新增可选 `appCode` 参数**:等于本应用/缺省→本应用树;指向其它应用→**空树 + warn 日志**(H4 未上线,见问题单 P3)。
|
||||
- **application.yml 分层**(jar 内只留本地最小默认,生产值全部由 projman env 注入并被 env 压过):
|
||||
- `server.port: 8086` 仅本地;不写 context-path(生产 `CONTEXT_PATH=/form-center`)
|
||||
- datasource 指向本地 `bsch_formcenter`;`open-in-view: false`、`MySQL5InnoDBDialect`、时区 Asia/Shanghai
|
||||
- **上传路径教训注释原样抄入**(notice/service 的「Linux 容器把 `D:/data/upload` 锚定成
|
||||
`/app/D:/data/upload` 写进可写层,每次部署附件全丢」)+ 本地兜底键 `file.upload-path: D:/data/upload`
|
||||
**未删**,并注明 T3 的读取形态 `${FILE_PATH:${FILE_UPLOAD_PATH:${file.upload-path:/data/upload}}}`
|
||||
- `yh.sso`/`yh.app-auth`(本地占位身份 100021 + 带引号 secret + `/oapi/*` 强制验签 + trust-registry)/
|
||||
`yh.open`(base-host 本地 UserBase;**明确禁止把 `services.userbase` 写进 jar**,沿用 2026-09-18 事故纪律)
|
||||
- `management.health.redis.enabled: false`(Redis 抖动不得把 `/actuator/health` 拉 DOWN)
|
||||
|
||||
### 1.3 前端(仅工程壳)
|
||||
|
||||
`vite ^5` + `vue ^3.4` + TypeScript 最小壳:`index.html` / `src/main.ts` / `src/App.vue`(占位页)/
|
||||
`src/env.d.ts` / `vite.config.ts`(`base: "./"`、dev 端口 8089)/ `tsconfig{,.node}.json` /
|
||||
`.npmrc`(npmmirror)/ `.gitignore` / **`package-lock.json`**(CI 用 `npm ci`,必须有锁文件)。
|
||||
脚本:`build = vite build`(CI 执行项)、`type-check = vue-tsc --noEmit`。
|
||||
A 类 loader / view-ui-plus 1.3.24 / alova / router / layout 属 **T5**,本任务刻意未接。
|
||||
|
||||
### 1.4 环境动作(本机副作用,供知悉)
|
||||
|
||||
- 本机 MySQL 8.0.45(`D:\BaiduNetdiskDownload\mysql-8.0.45-winx64`)此前未运行,为完成「本地能启动」门禁
|
||||
**已拉起**(`--defaults-file=my.ini`,端口 3306),并 **新建库 `bsch_formcenter`**(utf8mb4 / utf8mb4_general_ci)。
|
||||
两者均为本地开发环境常规动作;服务验证完未关,如需释放内存自行停止即可。
|
||||
- 未启动 CAS(8443)/UserBase(8060)/Redis,未触碰 160 共享环境(只做过一次只读可达性探测)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 自验证据
|
||||
|
||||
| # | 门禁 | 命令 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 后端编译+打包+单测 | `mvn -B -o clean package`(离线,本地仓库 `D:\SoftProject\yunhai\.m2`) | **BUILD SUCCESS**,`target/form-center.jar`(finalName 生效);`Tests run: 7, Failures: 0, Errors: 0` |
|
||||
| 2 | 本地启动 | `java -jar target/form-center.jar --server.port=8086` | `Started FormCenterApplication in 9.472 seconds`;HikariPool-1 Start completed;`Initialized JPA EntityManagerFactory` |
|
||||
| 3 | 健康探活 | `curl --noproxy '*' http://localhost:8086/actuator/health` | **HTTP 200** `{"status":"UP"}` |
|
||||
| 4 | 鉴权墙(用户面) | `curl http://localhost:8086/api/base/GetUserInfo` | HTTP 302 → `http://localhost:8443/login?service=...%2Fapi%2Fbase%2FGetUserInfo`(`yh.sso.url-pattern=/api/*` 生效,`/api/appearance` 同墙) |
|
||||
| 5 | 验签墙(对外面) | `curl http://localhost:8086/oapi/form/schema` | HTTP **401**(`yh.app-auth.server.enforce-patterns: [/oapi/*]` 生效) |
|
||||
| 6 | 前端构建 | `npm run build` | **✓ built**,`dist/index.html` + `dist/assets/*`(zip 根含 index.html,满足 projman 前端发布要求) |
|
||||
| 7 | 前端类型检查 | `npm run type-check`(vue-tsc --noEmit) | 无报错 |
|
||||
| 8 | CI | 见 §4(推送后回填) | — |
|
||||
|
||||
单测覆盖(`FormBaseControllerTest`,7 条,全部脱网/脱库可跑):
|
||||
`toFrontendTree` 保层级与字段改名 / `modId` 取数字 custom_id / `children` 恒为数组 / 非数字 customId
|
||||
(`fc:scope:all` 权限锚点,path 为空)回退主键且不炸 / null 与空入参不抛 NPE /
|
||||
`isForeignTarget` 判定(null·空白·本应用·大小写·其它 appCode)/
|
||||
**target appCode 命中时返回空树且不触碰 SDK**(单测里 SDK 静态客户端未装配,误走本应用分支会直接抛异常)。
|
||||
|
||||
未达成项:**「GetUserInfo 返回会话用户」的登录态冒烟**——本地 CAS(8443) 与 UserBase(8060) 未启动,
|
||||
且 GetUserMod 出树还依赖 H2(UserBase 的 form-center 应用行 + 模块行,用户侧待办)。
|
||||
按任务书 §0.1 规则①记缺口继续,取证改由上表 #3/#4/#5 + 单测承担 → 问题单 P1。
|
||||
|
||||
---
|
||||
|
||||
## 3. 问题单(本轮登记,供执行手册 §5 裁决)
|
||||
|
||||
| # | 问题 | 本轮取值(最保守解读) | 影响面 |
|
||||
| --- | --- | --- | --- |
|
||||
| P1 | 任务书 T1 自验含「GetUserInfo 返回会话用户」,本地无 CAS/UserBase 实例,且 H2 应用行未建 | 不停顿;以 health 200 + `/api/*` 302 CAS 墙 + `/oapi/*` 401 墙 + 7 条单测替代取证。一键脚本已核实存在:`D:\c\脚本执行\cas\start-server.bat`、`D:\c\脚本执行\userbase\start-jar.bat`(jar 已构建),需要登录态时在 T2/T5 联调轮补跑 | 仅取证强度,不改设计 |
|
||||
| P2 | target 参数名口径不一:执行手册 §1 写「可选 target **appKey**」,任务书 T1 与 H4 任务书写「**appCode**」 | 取 **appCode**(与 H4 的 UserBase `/oapi/user/modules` 参数、契约 §1 归属列 `app_code` 一致) | 参数名,前端 T8 需按此对齐 |
|
||||
| P3 | SDK 无目标应用形态:`javap` 实测 `YschBaseOpenApi` 只有 `getUserModule(int userId, Integer schId)` | H4 未上线前,`appCode` 指向其它应用一律**空树 + warn**,不回落本应用树(回落会把「要 A 的菜单」伪装成「A 就这些菜单」);H4 上线后消费范式=`YschOpenCall.raw()` 直调(SDK 不动) | 快应用上下文 T8/T9 前置 |
|
||||
| P4 | `appCode` 只加在 GetUserMod:GetUserInfo 无「树」语义,任务书把两端的参数写在一起 | 只 GetUserMod 加参数(T8 原文「模块树经 FormBaseController 带 target appCode」与此一致) | 无 |
|
||||
| P5 | `entity/BaseEntity` 审计四件套含 `create_by/update_by`,契约 §10 DDL 草案九表只列 `create_time/update_time` | 保留基类形态(任务书 §0.4「实体继承 BaseEntity」+ 模板平移纪律);`ddl-auto=update` 会在自动建表时多两列 | 若 DDL 草案为唯一权威,T2 前需裁决:补列 or 精简基类 |
|
||||
| P6 | 端口/身份号未在文档中为 form-center 定死(生产容器端口只从注入的 Dockerfile `EXPOSE 10020` 可见) | 本地 dev:service **8086**、front **8089**(顺延 notice 8101/8077、svc-hall 8087、flow-center 8085/8088);yml 不写生产端口,由 `SERVER_PORT`/`CONTEXT_PATH` env 注入 | 仅本地联调约定 |
|
||||
| P7 | 本地占位 appKey 号段:100018=svc-hall、100019=flow-center、100020=work-leave(实测各仓 yml) | 取 **100021** + `app-secret: "form-center-dev-secret-100021"`(带引号防八进制解析);真实身份需 UserBase 注册后由 projman 注入,仓库内不落真凭据 | 上线前替换项 |
|
||||
| P8 | `trusted-apps` 本地白名单暂无内容可列 | 先列 svc-hall/flow-center 两号(与 flow-center 本地兜底同口径);本应用 `/oapi` 的真实消费方在 T4 出现时补 | T4 |
|
||||
| P9 | maven-compiler-plugin 3.10.1 对「只含 package-info.java 的包」打印 `[WARNING] Error creating missing package info classes`(NoSuchFileException,非致命,BUILD SUCCESS) | 不为此升插件版本(CI 用 `maven:3.6.3-jdk-8`,插件升级有依赖解析风险);T2 起这些包落入真实类即消失 | 观感/日志噪声 |
|
||||
| P10 | 本地 MySQL 是 8.0.45,方案 §7 要求 **MySQL 5.7 兼容 SQL** | 用 `MySQL5InnoDBDialect` + utf8mb4;本地 8.0 跑通**不能**证明 5.7 兼容,T2 起 DDL/查询需按 5.7 复核(160 生产版本仍待核,见手册 §4.3) | T2~T4 |
|
||||
| P11 | 前端壳未接平台约定(A 类 loader / view-ui-plus 1.3.24 / alova / router / layout) | 属 T5 范围,T1 只保 `npm ci + npm run build` 绿;`base: "./"` 已按 notice/flow-center 口径设好,保证 `/form-center/` 挂载可取静态资源 | T5 |
|
||||
| P12 | 上传相关 yml 只落键、未落 `spring.servlet.multipart` 上限 | 属 T3 上传模块范围;**记档**:Spring 默认 `max-file-size=1MB` 会挡住契约 §3 的 ≤50MB 附件,T3 必须显式配 | T3 |
|
||||
| P13 | 首推纪律写的是 `git merge origin/main --allow-unrelated-histories`(适用于本地 init 后合入注入文件) | 实际采用 clone,注入提交已在历史里,merge 为 `Already up to date` 空操作;**未用** `git reset` / `git add -A`,暂存按显式路径 | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 推送与 CI(回填位)
|
||||
|
||||
| 仓 | 提交 | 推送 | CI run | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| form-center-service | 待回填 | 待回填 | 待回填 | 待回填 |
|
||||
| form-center-front | 待回填 | 待回填 | 待回填 | 待回填 |
|
||||
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.yunhai</groupId>
|
||||
<artifactId>form-center</artifactId>
|
||||
<version>0.1</version>
|
||||
<name>form-center</name>
|
||||
<description>表单中心(动态表单:定义与版本/答卷与检索/选项字典/数据范围/导出;对外契约=《表单中心契约-20261014.md》)</description>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-platform-dependencies</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 基础平台全家桶(聚合 starter):CAS 认证 + 应用间签名 + 统一响应 + OpenAPI 客户端 +
|
||||
actuator 健康探活 + 共享 IDHttp(出站自动签名),一行带齐(同 flow-center/svc-hall 约定) -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- 外观偏好同源读端点 GET /api/appearance(三态 proxy/local/off,默认 proxy 经 YschOpenCall
|
||||
代理 UserHome /oapi/appearance,userhome 地址走平台 application-platform.yml 兜底);
|
||||
前端壳的主题通道依赖它,与 flow-center 同款引入 -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-appearance-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- 平台 auth starter(0928 后快照)的 WeChatAuthAutoConfiguration 签名引用 StringRedisTemplate,
|
||||
data-redis 为 optional,接入前提=应用自带(见该 starter pom 注释);缺失时 bean 签名解析直接
|
||||
TypeNotPresentException,应用起不来 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- CAS 客户端(UserHome/Cms/svc-hall/flow-center 同款显式声明) -->
|
||||
<dependency>
|
||||
<groupId>org.jasig.cas.client</groupId>
|
||||
<artifactId>cas-client-core</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 答卷导出(契约 §8 红线:导出受数据范围约束;方案 §5.5 流式写):
|
||||
3.3.x 起 easyexcel 是 easyexcel-core 的聚合壳,版本随本行定死 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>form-center</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>com.yunhai.formcenter.FormCenterApplication</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- 默认 surefire 只收 *Test.java;本工程打真实 MySQL 的集成测试按 *IT 命名,
|
||||
一并纳入 mvn test(CI 不跑单测,本地 mvn test 即门禁——任务书 §0.4) -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/*Test.java</include>
|
||||
<include>**/*IT.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yunhai.formcenter;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 表单中心(app_code=form-center):动态表单的定义与版本、答卷与检索、选项字典、数据范围与导出。
|
||||
* 设计权威=《表单中心建设方案-20261013.md》v1.4,对外承诺=《表单中心契约-20261014.md》v1.0。
|
||||
* 一期边界(方案 §2.3):不做暂存草稿/联动跳题/计算字段/分步向导/专门打印,无 oapi 代提交。
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class FormCenterApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FormCenterApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yunhai.formcenter.common;
|
||||
|
||||
import com.yunhai.formcenter.entity.BaseEntity;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.PreUpdate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审计字段自动填充(flow-center/Cms/svc-hall 同款)。
|
||||
* 账号取 ThreadLocal 中的 CAS 会话用户;定时任务/无会话场景为 null。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FormCenterAuditListener {
|
||||
|
||||
@PrePersist
|
||||
public void prePersist(BaseEntity entity) {
|
||||
String account = currentAccount();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreateBy(account);
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateBy(account);
|
||||
entity.setUpdateTime(now);
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate(BaseEntity entity) {
|
||||
entity.setUpdateBy(currentAccount());
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.yunhai.formcenter.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 本地开发跨端口 CORS(同 flow-center 拍板项#2 口径):本应用 front dev(8089) 直调 /api/* 属跨端口请求,
|
||||
* 需带凭据的 CORS 头(CAS cookie 不隔离端口,免二次登录成立的前提)。
|
||||
* 同主域生产无跨端口问题,配置 allowed-origins 置空即整体关闭。
|
||||
*/
|
||||
@Configuration
|
||||
public class FormCorsConfiguration {
|
||||
|
||||
private final String allowedOrigins;
|
||||
|
||||
public FormCorsConfiguration(@Value("${form-center.cors.allowed-origins:}") String allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<CorsFilter> formCorsFilter() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
if (allowedOrigins != null && !allowedOrigins.trim().isEmpty()) {
|
||||
config.setAllowedOrigins(Arrays.asList(allowedOrigins.split("\\s*,\\s*")));
|
||||
config.setAllowedMethods(Collections.singletonList("*"));
|
||||
config.setAllowedHeaders(Collections.singletonList("*"));
|
||||
config.setAllowCredentials(true);
|
||||
// 只对用户面生效;/oapi/* 是应用签名调用,不经浏览器
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
}
|
||||
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
|
||||
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.yunhai.formcenter.controller.api;
|
||||
|
||||
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 前端壳基础端点(对齐 UserBase BaseController 的 /api/base/* 形态):GetUserInfo / GetUserMod。
|
||||
* <p>GetUserMod 与其他子应用(Notice / svc-hall / 事项库 Cms / flow-center)统一走平台下发:经 SDK
|
||||
* {@code YschBaseOpenApi.getUserModule} 取当前用户在本应用({@code yh.app-auth.app-key})下的
|
||||
* 已授权模块<b>树</b>,层级由平台契约 {@code BoaModule.children} 承载,本侧仅映射为前端 layout
|
||||
* 期望的字段形状({@code page/modId/children})。
|
||||
* <p><b>target appCode(快应用上下文,方案 §6.2 / 任务书 T8)</b>:{@code /app/{appCode}/} 需要的是
|
||||
* <b>那个快应用</b>的模块树,而不是表单中心自己的。它依赖 UserBase {@code /oapi/user/modules} 的
|
||||
* 可选 appCode 参数(跨项目触点 H4),SDK 侧目前只有
|
||||
* {@code getUserModule(int userId, Integer schId)} 一个形态、<b>无 appCode 入参</b>。
|
||||
* H4 上线前本端点对该请求<b>返回空树并打 warn</b>——不静默返回本应用菜单,
|
||||
* 那会把「请求 A 应用的树」伪装成「A 应用就是这些菜单」。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class FormBaseController {
|
||||
|
||||
/** 本应用 app_code(手册 §1 定死:app_code = name = ctx = 目录 key = jump_app)。 */
|
||||
private static final String APP_CODE = "form-center";
|
||||
|
||||
/**
|
||||
* appCode 是否指向「别的应用」(= 需要 H4 的目标应用模块树)。缺省/空白/等于本应用都算本应用。
|
||||
* 包级可见:供同包单测锁定判定,不依赖 SDK 静态客户端。
|
||||
*/
|
||||
static boolean isForeignTarget(String appCode) {
|
||||
String target = appCode == null ? "" : appCode.trim();
|
||||
return !target.isEmpty() && !APP_CODE.equalsIgnoreCase(target);
|
||||
}
|
||||
|
||||
@GetMapping("/api/base/GetUserInfo")
|
||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||
return authUserWithDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户已授权模块树(左侧菜单)。
|
||||
* <p><b>依赖 UserBase 侧数据</b>:本应用的 {@code app_module} 行({@code path} 须逐字对上
|
||||
* front 路由,见 T5 的 router/routes.ts)+ 角色/组授权,二者缺一即返回空菜单。
|
||||
* 这里<b>不保留本地 MENUS 兜底</b>——静默回退会把「平台未配模块/未授权」伪装成正常,
|
||||
* 与其他子应用口径一致,配置缺失应表现为空菜单并被发现。
|
||||
*
|
||||
* @param appCode 可选,目标应用的 app_code。缺省或等于 {@code form-center} 时返回本应用的树;
|
||||
* 指向其它应用时走 H4(UserBase 侧尚未上线)——返回空树并打 warn。
|
||||
*/
|
||||
@GetMapping("/api/base/GetUserMod")
|
||||
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept,
|
||||
@RequestParam(required = false) String appCode) {
|
||||
if (isForeignTarget(appCode)) {
|
||||
log.warn("[GetUserMod] 请求目标应用模块树 appCode={},UserBase /oapi/user/modules 的 "
|
||||
+ "appCode 参数(H4)未上线,SDK 无对应入参,按缺口返回空树", appCode);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (authUserWithDept == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
List<BoaModule> modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schId);
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 用户中心已按 BoaModule.children 下发模块树:本地不再重组层级,仅做前端字段改名映射
|
||||
return toFrontendTree(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* BoaModule 树 → 前端 layout 期望的字段形状(递归,<b>只改名不组树</b>;Notice/svc-hall/Cms 同款口径)。<br>
|
||||
* 层级与兄弟顺序均由用户中心决定(customId 升序),本侧不重排;叶子 children 为空列表。<br>
|
||||
* 包级可见而非 private:供同包单测直接覆盖映射,避免为测试改写 {@code YschBaseOpenApi} 全局静态客户端。
|
||||
*/
|
||||
static List<Map<String, Object>> toFrontendTree(List<BoaModule> modules) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
for (BoaModule m : modules) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", m.getId());
|
||||
node.put("modId", resolveModId(m));
|
||||
node.put("name", m.getName());
|
||||
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
|
||||
node.put("page", m.getPath() != null ? m.getPath() : "");
|
||||
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
|
||||
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
|
||||
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
|
||||
node.put("usable", true);
|
||||
node.put("children", toFrontendTree(m.getChildren()));
|
||||
result.add(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* modId 取值:优先 {@code customId}(数值),以对上 front {@code router/routes.ts} 的
|
||||
* {@code meta.modId} 约定;customId 非数字或缺失时回退主键 id。
|
||||
* 注意:不能直接取主键 id——那会让前端 modId 兜底匹配永久失配(path 匹配仍可用)。
|
||||
* 表单中心的 {@code fc:scope:all}(数据范围权限锚点,契约 §8)是<b>非数字</b> customId,
|
||||
* 且 path 为空、不进导航,故只由后端扫描授权集合消费,不依赖本映射结果。
|
||||
*/
|
||||
private static Object resolveModId(BoaModule m) {
|
||||
String customId = m.getCustomId();
|
||||
if (customId != null && customId.trim().matches("\\d+")) {
|
||||
return Integer.valueOf(customId.trim());
|
||||
}
|
||||
return m.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 对外签名面(/oapi/**,appKey 三件套验签,见 application.yml 的 yh.app-auth.server.enforce-patterns)。
|
||||
* T4 落地契约 §9 的两个读端点:{@code GET /oapi/form/schema}(内联 options + valueSchema)与
|
||||
* {@code GET /oapi/form/answer}。一期<b>无 oapi 代提交</b>(方案 §2.3 边界,答卷身份一律会话推导)。
|
||||
*/
|
||||
package com.yunhai.formcenter.controller.oapi;
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.yunhai.formcenter.entity;
|
||||
|
||||
import com.yunhai.formcenter.common.FormCenterAuditListener;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EntityListeners;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 所有实体的公共基类:自增主键 + 审计四件套(FormCenterAuditListener 自动填充)。
|
||||
* 契约 §1:{@code id} 只做库内定位、**绝不外露**——对外身份是 uuid(数据面)与 code(配置期/入口寻址)。
|
||||
* 多租户键 sch_id 不在基类——各业务实体自带列(Notice/flow-center 先例,语义不同不混入);
|
||||
* 无物理外键、MySQL 5.7 兼容(方案 §7 工程约束)。
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
@EntityListeners(FormCenterAuditListener.class)
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "create_by", updatable = false)
|
||||
private String createBy;
|
||||
|
||||
@Column(name = "create_time", updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_by")
|
||||
private String updateBy;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 表单中心枚举域(T2 起填充):表单生命周期状态、面向人群 audience、限次范围 limitScope、
|
||||
* 控件类型白名单(契约 §2:14 类冻结 / 一期放行 11 类)、答卷状态与操作类型。
|
||||
*/
|
||||
package com.yunhai.formcenter.enums;
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 表单中心出参/入参模型域(T2 起填充):schema 文档与字段模型、值文档、
|
||||
* 列表两段式出参、统计出参、契约 §4 的机读 valueSchema 片段。
|
||||
*/
|
||||
package com.yunhai.formcenter.model;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 表单中心仓储域(T2 起填充):form / form_version / answer / answer_index /
|
||||
* status_log / answer_op_log / fill_quota / form_dict / form_dict_item(契约 §10 九表)。
|
||||
* 检索铁律(方案 §5.5):两段式列表禁 SELECT values、keyset 仅按提交时间序、覆盖索引化。
|
||||
*/
|
||||
package com.yunhai.formcenter.repository;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 表单中心服务域(T2 起填充):定义域(发布/升版/复制/字典/schema 校验器)、
|
||||
* 答卷域(提交幂等/限次/截止/软删留痕)、数据范围解析(契约 §8 矩阵 + fc:scope:all)、
|
||||
* 检索统计与导出、上传模块(FILE_PATH/FILE_WEB_PATH,值只存 {id,fileName,key})。
|
||||
*/
|
||||
package com.yunhai.formcenter.service;
|
||||
@@ -0,0 +1,116 @@
|
||||
server:
|
||||
port: 8086
|
||||
# 本地无 context-path(同 flow-center dev / Notice dev);生产经 CONTEXT_PATH=/form-center 注入,
|
||||
# Caddy 只反代 /form-center/api/* 与 /form-center/oapi/*,页面走前端静态。
|
||||
# 端口/context-path 一律不写生产值:projman 用 SERVER_PORT、CONTEXT_PATH 环境变量注入(env 压过本文件)。
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: form-center
|
||||
datasource:
|
||||
# 本地开发库;生产 DB 连接由 projman 平台供给(SPRING_DATASOURCE_* env 注入,本文件值被压过)
|
||||
url: jdbc:mysql://localhost:3306/bsch_formcenter?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
jpa:
|
||||
hibernate:
|
||||
# 开发期 update 自动建表;生产环境建议 none + 升级脚本(sql/ 目录,见 T2 起)
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
open-in-view: false
|
||||
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
time_zone: Asia/Shanghai
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: Asia/Shanghai
|
||||
|
||||
# ===== 文件上传存储路径 =====
|
||||
# 教训注释自 notice/service application.yml 原样抄入(2026-10 事故):
|
||||
# 仅供本地 Windows 开发兜底;生产读 FILE_PATH(平台统一,共享配置 application-platform.yml 一份
|
||||
# /data/upload,子应用不再各自维护)。
|
||||
# 切勿在生产只依赖本键:历史教训是 Linux 容器把 D:/data/upload
|
||||
# 锚定成 /app/D:/data/upload 写进容器可写层,每次更新部署附件全部丢失(2026-10)。
|
||||
# 读取形态(T3 上传模块实现):${FILE_PATH:${FILE_UPLOAD_PATH:${file.upload-path:/data/upload}}}
|
||||
# —— FILE_PATH 由 projman 注入且持久卷固定挂 /data/upload,本键只在不注入 env 的本地生效,不可删。
|
||||
file:
|
||||
upload-path: D:/data/upload
|
||||
|
||||
# ===== 基础平台 yh 配置(flow-center/Notice/svc-hall 同款三件套)=====
|
||||
yh:
|
||||
auth:
|
||||
enabled: true
|
||||
sso:
|
||||
# 本地全链测试:指向 local-debug 的 CAS(8443 根路径,RSA+AJAX 登录页);生产由平台注入
|
||||
# SSO_SERVER_URL_PREFIX / SSO_CLIENT_HOST_URL 覆盖(projman 按平台设置现算,本文件不要写生产值)
|
||||
server-url-prefix: http://localhost:8443
|
||||
# 本地 dev 指向后端直连(脚本化登录 service=client-host-url+请求路径)
|
||||
client-host-url: http://localhost:8086
|
||||
allow-headers: all
|
||||
allow-origins: all
|
||||
# 用户面与管理面全在 /api/* 下;本应用无匿名面。
|
||||
# 业务方调用走 /oapi/*(appKey 签名,RequireAppSignFilter 强制 401)
|
||||
url-pattern: /api/*
|
||||
app-auth:
|
||||
enabled: true
|
||||
# 本应用自身身份(客户端签名用)——UserBase 注册后分配(进白名单),占位值上线前必须替换,
|
||||
# 生产由 projman 注入 YH_APP_AUTH_APP_KEY / APP_SECRET,禁止明文入库仓库。
|
||||
# 本地占位 appKey=100021(svc-hall=100018 → flow-center=100019 → work-leave=100020 顺延);
|
||||
# 联调时需在 UserBase(及对端 trusted-apps)侧同步此占位对。
|
||||
# 注意:占位 secret 必须带引号且含字母——裸 32 个 0 会被 YAML 按八进制整数解析成 "0",SignUtil 校验直接 500
|
||||
app-key: 100021
|
||||
app-secret: "form-center-dev-secret-100021"
|
||||
# 提供方强制验签(两级模型 §4.1):/oapi/** 无签名三件套即 401(starter 版 RequireAppSignFilter)。
|
||||
# T1 暂无 /oapi 端点,声明保留(T4 对外读接口落地即用,不出现「先上线后端点无门」的空窗)
|
||||
server:
|
||||
enforce-patterns: [/oapi/*]
|
||||
# 中央密钥链(两级模型 §4.2):trusted-apps 先(本地确定性兜底),未命中走 SignKeyResolver
|
||||
# (Redis sign:app:* → Caffeine → UserBase 兜底端点)——任何在 UserBase 注册过的 appKey 自动可验
|
||||
trust-registry: true
|
||||
# 入站验签白名单:只列本地需要直调本应用 /oapi 的对端,联调时按实际补充(号段同各应用本地占位)。
|
||||
# 开 trust-registry 后此表仍保留为主兜底(Redis 停摆时白名单确定性工作)
|
||||
trusted-apps:
|
||||
"100018": "svc-hall-dev-secret-100018"
|
||||
"100019": "flow-center-dev-secret-100019"
|
||||
open:
|
||||
# 身份密钥唯一来源 = yh.app-auth(本文件 app-auth 段),open.* 副本不写;
|
||||
# base-host 是「服务目录三级兜底」的输入(userbase 目标 = base-host + 枚举 ctx /userbase):
|
||||
# 本地指向 local-debug 的 UserBase;生产由平台注入 YH_OPEN_BASE_HOST 覆盖(base.yunhaixingtong.com)。
|
||||
# ⚠️ 绝不能把 services.userbase=http://localhost:8060 之类的本地覆盖写进本文件(同 flow-center 纪律):
|
||||
# yh.open.services.{key} 是目录第 2 级,平台注入的 base-host 只喂第 3 级兜底、压不过它 ——
|
||||
# 写进 jar 会让部署实例去连容器内的 localhost:8060(2026-09-18 事故:Connection refused →
|
||||
# 所有 /api/* 报 code=6 内部服务错误)。本地联调请运行时注入:
|
||||
# YH_OPEN_SERVICES_USERBASE=http://localhost:8060 mvn -o spring-boot:run
|
||||
base-host: http://localhost:8060
|
||||
base-path: ""
|
||||
|
||||
# ===== 表单中心专属配置 =====
|
||||
form-center:
|
||||
# 本地开发跨端口 CORS:本应用 front dev(8089) 直调 /api/* 需带凭据的 CORS 头
|
||||
# (CAS cookie 不隔离端口,免二次登录成立的前提);同主域生产置空即整体关闭
|
||||
cors:
|
||||
allowed-origins: http://localhost:8089
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.yunhai.formcenter: debug
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health
|
||||
endpoint:
|
||||
health:
|
||||
show-details: never
|
||||
health:
|
||||
redis:
|
||||
# 引 data-redis 仅服务于 trust-registry 密钥链;关健康指标防 Redis 停摆拉 DOWN /actuator/health
|
||||
# (组合链 trusted-apps 先行兜底,Redis 不可用时验签仍确定性工作)
|
||||
enabled: false
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.yunhai.formcenter.controller.api;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* GetUserMod 映射与 target appCode 判定单测(本地 {@code mvn test} 即门禁,CI 不跑单测)。<br>
|
||||
* 映射部分锁定三个易错点:modId 取数字 custom_id、children 恒为数组(前端无判空递归)、
|
||||
* path 缺失归空串。appCode 部分锁定「指向别的应用时<b>绝不打 SDK</b>」——
|
||||
* 单测里 SDK 静态客户端未装配,误走本应用分支会直接抛异常而不是返回空树。
|
||||
*/
|
||||
class FormBaseControllerTest {
|
||||
|
||||
@Test
|
||||
void toFrontendTree_shouldKeepServerHierarchyAndRenameFields() {
|
||||
BoaModule parent = module(501, "100", "/admin/forms", 1);
|
||||
BoaModule child = module(502, "101", "/admin/data", 2);
|
||||
parent.setChildren(new ArrayList<>(Collections.singletonList(child)));
|
||||
BoaModule otherRoot = module(503, "105", "/my", 1);
|
||||
|
||||
List<Map<String, Object>> tree = FormBaseController.toFrontendTree(Arrays.asList(parent, otherRoot));
|
||||
|
||||
assertEquals(2, tree.size(), "顶层只应有两个根,子节点不得浮到顶层");
|
||||
Map<String, Object> first = tree.get(0);
|
||||
assertEquals("/admin/forms", first.get("page"), "path → page(前端按 page 匹配菜单高亮)");
|
||||
assertEquals(100, first.get("modId"), "modId 必须取数字 custom_id 而非主键 id");
|
||||
assertEquals(501, first.get("id"), "原始主键仍经 id 字段透出");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) first.get("children");
|
||||
assertEquals(1, children.size(), "层级应原样保留");
|
||||
assertEquals(101, children.get(0).get("modId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toFrontendTree_shouldAlwaysEmitChildrenAsArray() {
|
||||
BoaModule configuredLeaf = module(601, "102", "/admin/dicts", 1);
|
||||
configuredLeaf.setChildren(new ArrayList<>());
|
||||
BoaModule notExpanded = module(602, "103", "/admin/stats", 1); // 上游未展开 → children 为 null
|
||||
|
||||
List<Map<String, Object>> tree = FormBaseController.toFrontendTree(Arrays.asList(configuredLeaf, notExpanded));
|
||||
|
||||
for (Map<String, Object> node : tree) {
|
||||
assertNotNull(node.get("children"), "children 不得为 null,前端要无判空递归");
|
||||
assertTrue(((List<?>) node.get("children")).isEmpty(), "无子节点时应为空列表");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void toFrontendTree_shouldFallBackToPrimaryKeyWhenCustomIdNotNumeric() {
|
||||
// 数据范围权限锚点 fc:scope:all(契约 §8)就是非数字 customId:它 path 为空、不进导航,
|
||||
// 只由后端扫描授权集合消费,这里只保证映射不炸、modId 退回主键。
|
||||
BoaModule scopeAnchor = module(701, "fc:scope:all", null, 1);
|
||||
|
||||
List<Map<String, Object>> tree = FormBaseController.toFrontendTree(Collections.singletonList(scopeAnchor));
|
||||
|
||||
assertEquals(701, tree.get(0).get("modId"), "custom_id 非数字时回退模块主键 id");
|
||||
assertEquals("", tree.get(0).get("page"), "path 缺失应给空串而非 null(前端字符串比较不判空)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toFrontendTree_shouldTolerateEmptyAndNullInput() {
|
||||
assertTrue(FormBaseController.toFrontendTree(Collections.emptyList()).isEmpty());
|
||||
assertTrue(FormBaseController.toFrontendTree(null).isEmpty(), "上游返回 null 不得抛 NPE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isForeignTarget_shouldOnlyTreatOtherAppCodesAsForeign() {
|
||||
assertFalse(FormBaseController.isForeignTarget(null), "缺省=本应用");
|
||||
assertFalse(FormBaseController.isForeignTarget(" "), "空白=缺省");
|
||||
assertFalse(FormBaseController.isForeignTarget("form-center"), "显式等于本应用仍走本应用分支");
|
||||
assertFalse(FormBaseController.isForeignTarget(" Form-Center "), "比对忽略大小写与首尾空白");
|
||||
assertTrue(FormBaseController.isForeignTarget("honor"), "快应用 app_code 才是目标应用分支");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserMod_shouldReturnEmptyTreeForForeignTargetWithoutTouchingSdk() {
|
||||
BoaUser user = new BoaUser();
|
||||
user.setId(10133);
|
||||
|
||||
List<Map<String, Object>> tree = new FormBaseController().getUserMod(user, "honor");
|
||||
|
||||
assertTrue(tree.isEmpty(), "H4(UserBase appCode 参数)未上线,目标应用一律空树");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserMod_shouldReturnEmptyTreeWithoutSessionUser() {
|
||||
assertTrue(new FormBaseController().getUserMod(null, null).isEmpty());
|
||||
}
|
||||
|
||||
private static BoaModule module(Integer id, String customId, String path, Integer level) {
|
||||
BoaModule m = new BoaModule();
|
||||
m.setId(id);
|
||||
m.setCustomId(customId);
|
||||
m.setName("模块" + customId);
|
||||
m.setPath(path);
|
||||
m.setLevel(level);
|
||||
m.setSortNo(level * 10);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user