From 8aeb40f119be75d5199d6a1eaebbbdbe3a6139c8 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 16 Sep 2026 01:22:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SDK=20=E7=B1=BB=E5=9E=8B=E5=8C=96?= =?UTF-8?q?=E9=97=A8=E9=9D=A2=E6=8E=A5=E5=85=A5=EF=BC=88flow/cms=20?= =?UTF-8?q?=E8=B5=B0=E6=9C=8D=E5=8A=A1=E7=9B=AE=E5=BD=95=EF=BC=89+=20?= =?UTF-8?q?=E6=95=99=E5=B8=88=E7=BB=84=E7=A7=91=E5=AE=A4=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20+=20F1-F3=20=E5=AD=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FlowCenterClient/CmsSiteDirectoryImpl:服务目录命名目标(flowcenter/cms)优先, legacy svchall.flow/cms.base-url 回落(迁移期兼容) - SvcModels.FlowSummaryVo extends SDK FlowDto(单一事实源) - 含 2026-09-13/14 存量:bizUrl hash-query(F1)/IDHttp ctx 签名/apply 漏 schId/回填事务 --- .gitea/workflows/build.yml | 86 ------------------- .gitignore | 6 -- README.md | 3 - docker/Dockerfile | 19 ---- .../controller/api/BaseApiController.java | 43 ++++------ .../yunhai/svchall/model/svc/SvcModels.java | 8 +- .../service/impl/CmsSiteDirectoryImpl.java | 23 ++++- .../service/impl/FlowCenterClient.java | 25 ++++-- src/main/resources/application.yml | 9 +- .../impl/CmsSiteDirectoryImplTest.java | 18 ++++ 10 files changed, 84 insertions(+), 156 deletions(-) delete mode 100644 .gitea/workflows/build.yml delete mode 100644 .gitignore delete mode 100644 README.md delete mode 100644 docker/Dockerfile diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml deleted file mode 100644 index baecf1d..0000000 --- a/.gitea/workflows/build.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: build - -# Spring Boot 应用 CI(仿 UserBase ci.yml build 阶段 + projman webhook 建档,不含部署) -# 触发:push 到 main / 手动 Run workflow(可勾选缓存重建) -# 链路:clone 源码 -> maven 容器构建 jar(复用宿主 m2 缓存)-> docker build -f docker/Dockerfile -> push Nexus -> 通知 projman 建档(部署由运维在 DeployTab 手动触发) -# 依赖仓库 Secrets:NEXUS_USER / NEXUS_PASSWORD(docker-hosted 推送)/ DEPLOY_TOKEN(webhook 鉴权) -# 依赖 docker/Dockerfile(由 projman 注入,开发者也可自备——已写则尊重不覆盖) -# maven 依赖走 http://nexus:8081/repository/maven-public/(infra-infra-net 内容器名直连,匿名可拉公司 yh-* starter) - -on: - push: - branches: [ main ] - workflow_dispatch: - inputs: - cache_bust: - description: '构建前清空 maven m2 缓存' - required: false - default: 'false' - -jobs: - build-push: - runs-on: ubuntu-latest - container: - image: docker:24 - options: --network infra-infra-net - steps: - - name: 安装 git / curl(alpine) - run: apk add --no-cache git curl - - # 内网 Gitea 无法访问 GitHub 拉 actions/checkout,手动 clone 本仓库 - - name: 拉取源码(内网无 GitHub,手动 clone 本仓库) - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git clone --depth 1 --branch main "http://x-access-token:${GITHUB_TOKEN}@gitea.yunhaixingtong.com/${{ github.repository }}.git" /tmp/src - cp -a /tmp/src/. . - rm -rf /tmp/src - # 取提交备注首行,脱去换行/双引号/反斜杠(避免破坏 webhook JSON),截断到 200 字符 - COMMIT_MSG=$(git log -1 --pretty=%s | tr '\n' ' ' | sed 's/["\\]/ /g' | cut -c1-200) - echo "COMMIT_MSG=${COMMIT_MSG}" >> "$GITHUB_ENV" - - # 可选:清空 m2 缓存(仅手动勾选 cache_bust 时) - - name: 可选清空 m2 缓存 - if: ${{ inputs.cache_bust == 'true' }} - run: | - docker run --rm -v /opt/docker/projman-m2:/root/.m2 alpine sh -c 'rm -rf /root/.m2/*' - echo "m2 cache busted" - - # maven 构建:起独立 maven 容器(入 infra-infra-net 用容器名 nexus 直连 maven-public) - # 通过 docker.sock 调宿主 dockerd,故 -v 路径按宿主解析;用 docker cp 进出源码/产物 - # m2 缓存挂宿主固定目录 /opt/docker/projman-m2,跨构建复用依赖 - # settings.xml 用 profile 方式追加 nexus maven-public 仓库(匿名可拉,不带凭据) - # maven 实际产物名 = artifactId-version.jar(随 pom 变,-DfinalName 命令行 property 不可靠覆盖不了), - # 故 mvn package 后在容器内 cp 成固定名 app.jar,使 docker cp 与 docker/Dockerfile 的 COPY target/app.jar 绑定 - - name: 构建 jar(maven 容器,复用宿主 m2 缓存) - run: | - set -e - docker rm -f mavenbuild 2>/dev/null || true - docker run -d --name mavenbuild \ - --network infra-infra-net \ - -v /opt/docker/projman-m2:/root/.m2 \ - -w /build maven:3.6.3-jdk-8 sleep 3600 - docker cp "${PWD}/." mavenbuild:/build/ - docker exec mavenbuild sh -c "printf '%s' 'nexus-reponexus-internalhttp://nexus:8081/repository/maven-public/truetruealwaysnexus-repo' > /root/.m2/settings.xml" - docker exec mavenbuild sh -c 'cd /build && mvn -B -DskipTests clean package' - docker exec mavenbuild sh -c 'cd /build/target && cp *.jar app.jar' - mkdir -p "${PWD}/target" - docker cp mavenbuild:/build/target/app.jar "${PWD}/target/app.jar" - docker rm -f mavenbuild - - - name: 构建并推送镜像 - run: | - set -e - TAG=$(echo "$GITHUB_SHA" | cut -c1-8) - echo "${{ secrets.NEXUS_PASSWORD }}" | docker login nexus:8083 -u "${{ secrets.NEXUS_USER }}" --password-stdin - docker build -t "nexus:8083/docker-hosted/svc-hall-service:${TAG}" -f docker/Dockerfile . - docker push "nexus:8083/docker-hosted/svc-hall-service:${TAG}" - echo "TAG=${TAG}" >> "$GITHUB_ENV" - - # 通知 projman:webhook 收到后只建档 Release(status=pending),部署由运维在 DeployTab 手动触发(App.deployMode=manual) - - name: 通知 projman 建档(部署由运维手动触发) - run: | - curl -sS -X POST "http://192.168.10.160:8090/projman/api/deploy/webhook" \ - -H "Content-Type: application/json" \ - -H "X-Deploy-Token: ${{ secrets.DEPLOY_TOKEN }}" \ - -d "{\"app\":\"svc-hall\",\"tag\":\"${TAG}\",\"sha\":\"${GITHUB_SHA}\",\"message\":\"${COMMIT_MSG}\"}" diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0564691..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/target/ -*.log -.idea/ -*.iml -.vscode/ -.DS_Store diff --git a/README.md b/README.md deleted file mode 100644 index 25cb95f..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# svc-hall-service - -managed by projman \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 6192d7b..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# 由 projman 注入的 Spring Boot 运行镜像(单阶段,jar 预构建) -# CI 已先 mvn package,并在容器内把产物 cp 成固定名 target/app.jar -# 构建上下文 = 仓库根:docker build -f docker/Dockerfile . -# 健康检查依赖 curl(projman 部署用 curl 探 /actuator/health)——应用需含 spring-boot-starter-actuator -# 时区 Asia/Shanghai;JAVA_OPTS 由 projman 渲染器注入(OOM 退出参数 + -Xmx=内存上限×75%(设上限时渲染期算死,JDK8 兼容) -# + 用户自定义参数),未注入时容器内兜底 -Xms512m -Xmx1024m -FROM eclipse-temurin:8-jre - -RUN apt-get update \ - && apt-get install -y --no-install-recommends curl tzdata \ - && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ - && echo "Asia/Shanghai" > /etc/timezone \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY target/app.jar /app/app.jar - -EXPOSE 10007 -ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar"] diff --git a/src/main/java/com/yunhai/svchall/controller/api/BaseApiController.java b/src/main/java/com/yunhai/svchall/controller/api/BaseApiController.java index c3bc3cc..c0ffeb5 100644 --- a/src/main/java/com/yunhai/svchall/controller/api/BaseApiController.java +++ b/src/main/java/com/yunhai/svchall/controller/api/BaseApiController.java @@ -15,8 +15,9 @@ import java.util.Map; /** * 平台代理域接口(Notice/CMS BaseApiController 同款契约):GetUserInfo / GetUserMod。 - * GetUserInfo 直接返回 CAS 会话注入的 BoaUser;GetUserMod 经 Tier1 SDK 取当前用户在 - * 本应用(appKey 按 yh.open 身份)下已授权模块并组树,是前端左侧菜单与页头用户信息的数据源。 + * GetUserInfo 直接返回 CAS 会话注入的 BoaUser;GetUserMod 经 Tier1 SDK 取当前用户在本应用 + * (appKey 按 yh.open 身份)下的已授权模块树(层级由用户中心 BoaModule.children 下发), + * 是前端左侧菜单与页头用户信息的数据源。 * 返回裸对象,由统一响应包装为 {code, msg, data}。 */ @RestController @@ -29,7 +30,7 @@ public class BaseApiController { return authUserWithDept; } - /** 当前用户已授权模块列表(左侧菜单树) */ + /** 当前用户已授权模块树(左侧菜单) */ @GetMapping("GetUserMod") public List> getUserMod(BoaUser authUserWithDept) { Integer schId = authUserWithDept.getCurrentRole() != null @@ -38,16 +39,19 @@ public class BaseApiController { if (modules == null || modules.isEmpty()) { return Collections.emptyList(); } - return buildModuleTree(modules); + // 用户中心已按 BoaModule.children 下发模块树:本地不再重组层级,仅做前端字段改名映射 + return toFrontendTree(modules); } - /** 扁平模块列表组树(Notice UserCenterService.buildModuleTree 同构:modId/page/children) */ - private static List> buildModuleTree(List modules) { - Map moduleMap = new LinkedHashMap<>(); - for (BoaModule m : modules) { - moduleMap.put(m.getId(), m); + /** + * BoaModule 树 → 前端 layout 期望的字段形状(递归,只改名不组树;Notice 同款口径)。
+ * 层级与兄弟顺序均由用户中心决定(customId 升序),本侧不重排;叶子 children 为空列表。 + */ + private static List> toFrontendTree(List modules) { + List> result = new ArrayList<>(); + if (modules == null || modules.isEmpty()) { + return result; } - Map> nodeMap = new LinkedHashMap<>(); for (BoaModule m : modules) { Map node = new LinkedHashMap<>(); node.put("id", m.getId()); @@ -59,22 +63,9 @@ public class BaseApiController { node.put("icon", m.getIcon() != null ? m.getIcon() : ""); node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0); node.put("usable", true); - node.put("children", new ArrayList<>()); - nodeMap.put(m.getId(), node); + node.put("children", toFrontendTree(m.getChildren())); + result.add(node); } - List> roots = new ArrayList<>(); - for (BoaModule m : modules) { - Map node = nodeMap.get(m.getId()); - int parentId = m.getParentId() != null ? m.getParentId() : 0; - if (parentId == 0 || !moduleMap.containsKey(parentId)) { - roots.add(node); - } else { - @SuppressWarnings("unchecked") - List> children = - (List>) nodeMap.get(parentId).get("children"); - children.add(node); - } - } - return roots; + return result; } } diff --git a/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java b/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java index f883217..f1d07b1 100644 --- a/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java +++ b/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java @@ -451,11 +451,9 @@ public final class SvcModels { // ===== 流程(管理端回显) ===== + /** 字段单一事实源=SDK 门面 DTO(可直接作 YschFlowOpenApi 泛型出入参) */ @Data - public static class FlowSummaryVo { - private String flowGuid; - private String name; - private Integer version; - private String nodeSummary; + @lombok.EqualsAndHashCode(callSuper = true) + public static class FlowSummaryVo extends com.yunhai.platform.openapi.dto.FlowDto.FlowSummary { } } diff --git a/src/main/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImpl.java b/src/main/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImpl.java index a22b882..858a7b8 100644 --- a/src/main/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImpl.java +++ b/src/main/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImpl.java @@ -23,11 +23,14 @@ import java.util.Map; * {@code GET /oapi/site/list},结果按 schId 缓存 5 分钟(CMS 侧缓存纪律的镜像:站点列表 * 低频变化,失效走 TTL)。解析不到/fail 时 fail-closed(权限校验绝不放行)。 * - *

必须走 YschOpenCall 而非共享 IDHttp:CMS 部署带 context-path(/cms),共享拦截器的 - * basePath 取自 {@code yh.open.base-path}(本应用=/userbase),剥不掉 /cms 前缀,签名 + *

必须走 YschOpenCall 而非共享 IDHttp:CMS 部署带 context-path(本地 {@code /cms}、 + * 内网部署态 {@code /yschcms}),共享拦截器的 + * basePath 取自 {@code yh.open.base-path}(本应用=/userbase),剥不掉这层前缀,签名 * target 与服务端不一致会 401;YschOpenCall 的 per-target base-path 按提供方 context-path - * 剥离,与 {@code AppAuthFilter} 对齐(同 Notice FlowCenterClient 范式)。 - * {@code svchall.cms.base-url} 可配带/不带 context-path 两种形态,构造时拆成 host+basePath。 + * 剥离,与 {@code AppAuthFilter} 对齐(同 Notice FlowCenterClient 范式)。
+ * 目标地址主路径=服务目录命名目标 {@code cms}({@code yh.open.services.cms} 完整 URL, + * CMS 未晋升枚举故按名直调,《子应用配置优化方案》§4.2(5));目录未配置时回落 legacy 键 + * {@code svchall.cms.base-url}(可配带/不带 context-path 两种形态,构造时拆成 host+basePath)。 */ @Slf4j @Component @@ -39,12 +42,21 @@ public class CmsSiteDirectoryImpl implements SiteDirectory { .expireAfterWrite(Duration.ofMinutes(5)) .build(); + /** 服务目录命名目标(cms 未晋升枚举,按名直调);目录未配置时为 null=走 legacy ad-hoc spec */ + private static final String DIRECTORY_KEY = "cms"; + private final YschOpenCall openCall; private final TargetSpec cmsSpec; + private final boolean directoryResolved; public CmsSiteDirectoryImpl(YschOpenCall openCall, @Value("${svchall.cms.base-url}") String cmsBaseUrl) { this.openCall = openCall; + this.directoryResolved = openCall.hasTarget(DIRECTORY_KEY); + if (directoryResolved) { + this.cmsSpec = null; + return; + } String base = trimTrailing(cmsBaseUrl == null ? "" : cmsBaseUrl.trim()); URI uri = URI.create(base); String host = uri.getScheme() + "://" + uri.getRawAuthority(); @@ -105,6 +117,9 @@ public class CmsSiteDirectoryImpl implements SiteDirectory { Map query = schId == null ? Collections.emptyMap() : Collections.singletonMap("schId", String.valueOf(schId)); + if (directoryResolved) { + return openCall.getList(DIRECTORY_KEY, "/oapi/site/list", query, SvcModels.SiteOptionVo.class); + } return openCall.getList(cmsSpec, "/oapi/site/list", query, SvcModels.SiteOptionVo.class); } diff --git a/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java b/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java index 911c77b..ebc1342 100644 --- a/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java +++ b/src/main/java/com/yunhai/svchall/service/impl/FlowCenterClient.java @@ -23,11 +23,11 @@ import java.util.Map; * /oapi/flow/**(flow-center 的 RequireAppSignFilter 验本应用 appKey)。flow-center 未部署时 * enabled=false,方法全部抛"流程中心未接入"。 * - *

必须走 YschOpenCall 而非共享 IDHttp:flow-center 生产部署带 context-path(/flow-center), - * 共享拦截器的 basePath 取自 {@code yh.open.base-path}(本应用=/userbase),剥不掉该前缀,签名 - * target 与服务端不一致会 401;YschOpenCall 的 per-target base-path 按提供方 context-path 剥离 - * (同 {@link CmsSiteDirectoryImpl})。{@code svchall.flow.base-url} 可配带/不带 context-path - * 两种形态,构造时拆成 host+basePath。 + *

主路径走 SDK 类型化门面 {@code YschFlowOpenApi}(服务目录 key=flowcenter: + * yh.call.targets > yh.open.services > 枚举 base-host 兜底,《子应用配置优化方案》§4.2 + * 零专属地址配置);目录未显式配置时回落 legacy 键 {@code svchall.flow.base-url}(ad-hoc + * TargetSpec,可配带/不带 context-path 两种形态,构造时拆成 host+basePath,迁移期兼容)。 + * 两条路径的 per-target base-path 剥离语义一致(同 {@link CmsSiteDirectoryImpl})。 */ @Slf4j @Component @@ -37,6 +37,8 @@ public class FlowCenterClient implements FlowClient { private final ObjectMapper objectMapper; private final boolean enabled; private final TargetSpec flowSpec; + /** 服务目录显式配置了 flowcenter(targets/services)则主路径走门面命名目标 */ + private final boolean directoryResolved; public FlowCenterClient(YschOpenCall openCall, ObjectMapper objectMapper, @Value("${svchall.flow.enabled:false}") boolean enabled, @@ -49,6 +51,11 @@ public class FlowCenterClient implements FlowClient { String host = uri.getScheme() + "://" + uri.getRawAuthority(); String path = uri.getRawPath() == null ? "" : uri.getRawPath(); this.flowSpec = TargetSpec.of(host, path); + this.directoryResolved = openCall.hasTarget( + com.yunhai.platform.openapi.PlatformService.FLOWCENTER.key()); + if (directoryResolved) { + com.yunhai.platform.openapi.client.YschFlowOpenApi.setCall(openCall); + } } @Override @@ -66,6 +73,10 @@ public class FlowCenterClient implements FlowClient { @Override public List listFlows(Long schId) { requireEnabled(); + if (directoryResolved) { + return com.yunhai.platform.openapi.client.YschFlowOpenApi.listFlows( + schId, SvcModels.FlowSummaryVo.class); + } return openCall.getList(flowSpec, "/oapi/flow/list", Collections.singletonMap("schId", String.valueOf(schId)), SvcModels.FlowSummaryVo.class); } @@ -73,6 +84,10 @@ public class FlowCenterClient implements FlowClient { @Override public SvcModels.FlowSummaryVo getFlow(Long schId, String flowGuid) { requireEnabled(); + if (directoryResolved) { + return com.yunhai.platform.openapi.client.YschFlowOpenApi.getFlow( + schId, flowGuid, SvcModels.FlowSummaryVo.class); + } return openCall.get(flowSpec, "/oapi/flow/" + flowGuid, Collections.singletonMap("schId", String.valueOf(schId)), SvcModels.FlowSummaryVo.class); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9cbb754..489b8d1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -78,8 +78,13 @@ svchall: # 服务部门"容器组"类型(UserBase GroupTypeEnum,默认 7=教师组;口径承袭 CMS cms.svc.item.dept-group-type) dept-group-type: 7 cms: - # CMS 基址(站点目录 /oapi/site/list 数据源);本地直连后端,生产由 projman 注入 - base-url: http://localhost:8072/cms + # CMS 基址(站点目录 /oapi/site/list 数据源)。必须指向**后端**且带 context-path: + # 内网 CMS 挂在 base 域下 /yschcms,但 Caddy 只反代 /yschcms/{front,api,portal}, + # 没有 /oapi 路由——用 http://base.yunhaixingtong.com/yschcms 打 /oapi/* 会拿到 Caddy 404, + # 因此走容器直连端口 10005(实测 /yschcms/oapi/site/list 能到应用)。 + # 本机起 CMS(Cms/local-debug/start-cms.bat)时改用 http://localhost:8072/cms; + # 生产由 projman 注入覆盖。 + base-url: http://base.yunhaixingtong.com:10005/yschcms flow: # 流程中心 flow-center 基址;2026-09-12 联调置 true(本地 8085 已部署 flow-center M1) enabled: true diff --git a/src/test/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImplTest.java b/src/test/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImplTest.java index c0caab8..01f8c02 100644 --- a/src/test/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImplTest.java +++ b/src/test/java/com/yunhai/svchall/service/impl/CmsSiteDirectoryImplTest.java @@ -8,6 +8,7 @@ import com.yunhai.svchall.service.SiteDirectory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import java.util.Arrays; import java.util.Collections; @@ -139,4 +140,21 @@ class CmsSiteDirectoryImplTest { assertTrue(brief.contains("ConnectException"), brief); assertTrue(brief.contains("Connection refused: connect"), brief); } + + @Test + @DisplayName("基址拆解:带端口 + context-path 的部署态基址必须原样保留端口,ctx 作 basePath(签名剥离用)") + void baseUrl_splitKeepsPortAndContextPath() { + ArgumentCaptor captor = ArgumentCaptor.forClass(TargetSpec.class); + YschOpenCall call = mock(YschOpenCall.class); + when(call.getList(captor.capture(), eq(PATH), eq(Collections.singletonMap("schId", "3")), eq(VO_TYPE))) + .thenReturn(Collections.emptyList()); + + new CmsSiteDirectoryImpl(call, "http://base.yunhaixingtong.com:10005/yschcms/").listSites(3L); + + TargetSpec spec = captor.getValue(); + // 丢端口会连不上(Caddy 只反代 /yschcms/{front,api,portal},/oapi 只能走容器端口) + assertEquals("http://base.yunhaixingtong.com:10005/yschcms", spec.resolveBaseUrl()); + // ctx 必须进 basePath,否则签名 target 与服务端剥掉 ctx 后不一致 → 401 + assertEquals("/yschcms", spec.getBasePath()); + } }