From 000993df80d059103eec2c16c995d1410d87aa0f Mon Sep 17 00:00:00 2001 From: chenxing Date: Fri, 21 Aug 2026 09:37:21 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E9=BB=98=E8=AE=A4README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build.yml | 71 +++++++ .gitignore | 20 ++ .../java-backend-doc-maintainer/SKILL.md | 53 +++++ .../references/backend-file-index.template.md | 49 +++++ .../references/backend-knowledge.template.md | 35 ++++ UserhomeService | 1 + docker/Dockerfile | 18 ++ pom.xml | 142 ++++++++++++++ .../yunhai/userhome/UserHomeApplication.java | 12 ++ .../yunhai/userhome/common/BaseEntity.java | 35 ++++ .../com/yunhai/userhome/config/Config.java | 13 ++ .../controller/api/UsedAppController.java | 154 +++++++++++++++ .../controller/api/UserController.java | 88 +++++++++ .../com/yunhai/userhome/entity/UsedApp.java | 64 ++++++ .../userhome/entity/UserCurrentRole.java | 41 ++++ .../com/yunhai/userhome/model/SetType.java | 38 ++++ .../repository/UsedAppRepository.java | 21 ++ .../repository/UserCurrentRoleRepository.java | 11 ++ .../userhome/service/AppRemoteService.java | 24 +++ .../service/IUserCurrentRoleService.java | 17 ++ .../userhome/service/UsedAppService.java | 39 ++++ .../impl/OpenApiAppRemoteServiceImpl.java | 42 ++++ .../service/impl/UsedAppServiceImpl.java | 182 ++++++++++++++++++ .../impl/UserCurrentRoleServiceImpl.java | 26 +++ src/main/resources/application.yml | 62 ++++++ src/main/resources/bootstrap.yml | 45 +++++ src/main/resources/logback-spring.xml | 35 ++++ src/main/resources/sql/userhome_init.sql | 39 ++++ 后端文件速查表.md | 54 ++++++ 后端项目知识库.md | 66 +++++++ 30 files changed, 1497 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 .workbuddy/skills/java-backend-doc-maintainer/SKILL.md create mode 100644 .workbuddy/skills/java-backend-doc-maintainer/references/backend-file-index.template.md create mode 100644 .workbuddy/skills/java-backend-doc-maintainer/references/backend-knowledge.template.md create mode 160000 UserhomeService create mode 100644 docker/Dockerfile create mode 100644 pom.xml create mode 100644 src/main/java/com/yunhai/userhome/UserHomeApplication.java create mode 100644 src/main/java/com/yunhai/userhome/common/BaseEntity.java create mode 100644 src/main/java/com/yunhai/userhome/config/Config.java create mode 100644 src/main/java/com/yunhai/userhome/controller/api/UsedAppController.java create mode 100644 src/main/java/com/yunhai/userhome/controller/api/UserController.java create mode 100644 src/main/java/com/yunhai/userhome/entity/UsedApp.java create mode 100644 src/main/java/com/yunhai/userhome/entity/UserCurrentRole.java create mode 100644 src/main/java/com/yunhai/userhome/model/SetType.java create mode 100644 src/main/java/com/yunhai/userhome/repository/UsedAppRepository.java create mode 100644 src/main/java/com/yunhai/userhome/repository/UserCurrentRoleRepository.java create mode 100644 src/main/java/com/yunhai/userhome/service/AppRemoteService.java create mode 100644 src/main/java/com/yunhai/userhome/service/IUserCurrentRoleService.java create mode 100644 src/main/java/com/yunhai/userhome/service/UsedAppService.java create mode 100644 src/main/java/com/yunhai/userhome/service/impl/OpenApiAppRemoteServiceImpl.java create mode 100644 src/main/java/com/yunhai/userhome/service/impl/UsedAppServiceImpl.java create mode 100644 src/main/java/com/yunhai/userhome/service/impl/UserCurrentRoleServiceImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/bootstrap.yml create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/sql/userhome_init.sql create mode 100644 后端文件速查表.md create mode 100644 后端项目知识库.md diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..b0cb0ab --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,71 @@ +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: + +jobs: + build-push: + runs-on: ubuntu-latest + container: + image: docker:24 + steps: + - name: 安装 git(alpine) + run: apk add --no-cache git + + # 内网 Gitea 无法访问 GitHub 拉 actions/checkout,手动 clone 本仓库 + # GITHUB_TOKEN 由 Gitea Actions 自动下发,用于 clone 私有仓库 + - name: 拉取源码(内网无 GitHub,手动 clone 本仓库) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git clone --depth 1 --branch main "http://x-access-token:${GITHUB_TOKEN}@gitea:3000/${{ github.repository }}.git" /tmp/src + cp -a /tmp/src/. . + rm -rf /tmp/src + + # maven 构建:起独立 maven 容器(入 infra-infra-net 用容器名 nexus 直连 maven-public) + # 通过 docker.sock 调宿主 dockerd,故 -v 路径按宿主解析;用 docker cp 进出源码/产物 + # m2 缓存挂宿主固定目录 /opt/docker/projman-m2,跨构建复用依赖(-U 强制检查远端更新) + # 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 -U -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/userhome-service:${TAG}" -f docker/Dockerfile . + docker push "nexus:8083/docker-hosted/userhome-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\":\"userhome\",\"tag\":\"${TAG}\",\"sha\":\"${GITHUB_SHA}\"}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f08b7a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Maven 构建产物 +target/ +*.class +*.jar +*.war + +# IDE +*.iml +.idea/ +.vscode/ +.settings/ +.project +.classpath + +# 本地配置(不入库) +*.local + +# 日志 +*.log +logs/ diff --git a/.workbuddy/skills/java-backend-doc-maintainer/SKILL.md b/.workbuddy/skills/java-backend-doc-maintainer/SKILL.md new file mode 100644 index 0000000..94cd981 --- /dev/null +++ b/.workbuddy/skills/java-backend-doc-maintainer/SKILL.md @@ -0,0 +1,53 @@ +--- +name: java-backend-doc-maintainer +description: "项目级 skill(由全局 java-backend-doc-maintainer 在用户手动启用本工程时写入)。在本 Spring Boot 工程内:新增/重命名/删除 .java 源文件时自动同步 后端文件速查表.md;发生重大功能变化或用户手动输出 \"更新后端项目知识库\" 时更新 后端项目知识库.md。仅在当前工程生效,打开其他工程不会触发。" +agent_created: true +--- + +# Java 后端文档维护(项目级自动维护) + +> 本 skill 由全局 java-backend-doc-maintainer 在用户于本工程手动触发时写入,仅对当前 Spring Boot 工程生效。生效后:文件增删改自动维护文档,无需每次手动。 + +## 两份文档与触发时机 +| 文档 | 位置 | 触发 | +| --- | --- | --- | +| 后端文件速查表.md | 工程根(pom.xml 同级) | 本工程内**新增 / 重命名 / 删除** .java 时自动同步;或用户手动输出 **"更新后端文件速查表"** 全量重建 | +| 后端项目知识库.md | 同上 | **重大功能变化**时更新;或用户手动输出 **"更新后端项目知识库"** 强制梳理 | + +## 工程根目录 +即本 skill 所在工程的 `pom.xml` 同级目录(已激活,无需再向上查找)。 + +## 一、后端文件速查表.md(文件级索引) +维护触发:**本工程内新增 / 重命名 / 删除** 一个 `.java` 源文件(`src/main/java` 或 `src/test/java`)时立即同步;纯内容改动(不改文件名/路径)不触发。 + +内容结构(保持表格,按层分组): +- 顶部:工程名、基础包、最近更新时间、维护规则。 +- 分组表(每组一个二级标题):Controller / Service / Repository / Entity·Model / DTO·VO·Request / Config / Common(aspect、annotation、util、interceptor、listener)/ 其他(task、event、enums)。 +- 每行字段:`路径(相对工程根)` | `类名` | `职责(一句话)` | `关键关联`(如关联实体、对外接口、调用的服务)。 +- 重命名:更新该行路径与类名,并在表末"变更记录"追加(旧路径 → 新路径,时间)。 +- 删除:移除该行,变更记录追加"删除 路径 时间"。 +- 新增:插入对应分组,变更记录追加"新增 路径 时间"。 +- 若文件缺失,从 `references/backend-file-index.template.md` 复制骨架,先全量扫描现有源码再填,不要留空。 + +## 二、后端项目知识库.md(架构 / 功能 / 约定) +维护触发(满足任一即更新): +1. 用户手动输出 **"更新后端项目知识库"**(最高优先,立即全量梳理)。 +2. 发生重大功能变化:新增业务模块 / 领域、新增一组对外 API、引入新中间件或外部系统对接、核心流程 / 权限模型 / 数据模型重大调整、技术栈版本迁移。判断标准:影响"系统由哪些部分组成、如何协作、有哪些约定"的变化;普通 bug 修复、单文件小改不触发。 + +内容结构见 `references/backend-knowledge.template.md`: +- 项目概览(定位、技术栈版本、运行方式) +- 模块 / 包结构(各层职责) +- 核心领域模型与关系(逻辑外键请注明) +- 关键业务流程 +- 工程约定与模式(异常返回结构、鉴权、CORS、缓存、事务边界等——只记录真实存在的,不臆造) +- 集成点(DB / Redis / MQ / Nacos / SSO-CAS / 外部 API,写用途不写密钥) +- 重要设计决策与原因 +- 待办 / 已知坑 + +若文件缺失,从 `references/backend-knowledge.template.md` 复制骨架再填。 + +## 反幻觉与纪律 +- 维护前用 Glob / Grep / Read 核实文件真实存在、类名真实、职责真实。 +- 不因为"应该长这样"而编造;不确定就标注"待核实"。 +- 两份文件保持可机器读取的表格 / 标题结构,方便后续 AI 速查。 +- 不写入密钥、密码、token;`bootstrap.yml` / `application.yml` 等未经授权不改动,仅给诊断建议。 diff --git a/.workbuddy/skills/java-backend-doc-maintainer/references/backend-file-index.template.md b/.workbuddy/skills/java-backend-doc-maintainer/references/backend-file-index.template.md new file mode 100644 index 0000000..239343a --- /dev/null +++ b/.workbuddy/skills/java-backend-doc-maintainer/references/backend-file-index.template.md @@ -0,0 +1,49 @@ +# 后端文件速查表 + +> 工程:<工程名> | 基础包: | 最近更新: +> 维护规则:本表在**新增 / 重命名 / 删除** .java 源文件时自动同步;纯内容改动不触发。也可手动输出"更新后端文件速查表"全量重建。 + +## Controller(接口层) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//controller/XxxController.java | XxxController | 一句话职责 | 关联实体/接口 | + +## Service +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//service/XxxService.java | XxxService | 一句话职责 | 调用/被调用 | + +## Repository +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//repository/XxxRepository.java | XxxRepository | 数据访问 | 实体 | + +## Entity / Model +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//entity/Xxx.java | Xxx | 实体 | 逻辑外键 | + +## DTO / VO / Request +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//model/XxxDto.java | XxxDto | 入参/出参 | 关联实体 | + +## Config +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//config/XxxConfig.java | XxxConfig | 配置 | Bean/拦截器 | + +## Common(aspect/annotation/util/interceptor/listener) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//common/Xxx.java | Xxx | 公共能力 | 使用点 | + +## 其他(task/event/enums) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java//enums/XxxEnum.java | XxxEnum | 枚举 | 使用点 | + +## 变更记录 +| 时间 | 类型 | 说明 | +| --- | --- | --- | +| YYYY-MM-DD | 初始化 | 建表 | diff --git a/.workbuddy/skills/java-backend-doc-maintainer/references/backend-knowledge.template.md b/.workbuddy/skills/java-backend-doc-maintainer/references/backend-knowledge.template.md new file mode 100644 index 0000000..adedec8 --- /dev/null +++ b/.workbuddy/skills/java-backend-doc-maintainer/references/backend-knowledge.template.md @@ -0,0 +1,35 @@ +# 后端项目知识库 + +> 工程:<工程名> | 技术栈:Spring Boot / JDK / | 最近更新: +> 维护规则:发生重大功能变化、或收到"更新后端项目知识库"指令时更新。 + +## 一、项目概览 +- 定位: +- 技术栈与版本(Spring Boot / JDK / JPA-MyBatis / 构建工具): +- 启动 / 运行方式(端口、profile、入口类): + +## 二、模块与包结构 +(各层 / 各 package 的职责,例如 controller 负责接口、service 负责业务、repository 负责数据访问) + +## 三、核心领域模型与关系 +(实体、关键字段、相互关系;若存在逻辑外键请注明"无物理外键,靠业务字段关联") + +## 四、关键业务流程 +(主要用例 / 请求链路,例如登录、授权、数据集成) + +## 五、工程约定与模式 +(仅记录本项目真实存在的约定,不臆造) +- 异常返回结构(如统一 HTTP 200 + `{code,msg,data}`): +- 鉴权方式(如 CAS SSO / JWT / header 触发式 CORS): +- 缓存(如 Redis 序列化需注册 JavaTimeModule): +- 事务边界: +- 分页查询约定(如 BasePageQuery / BasePageResult): + +## 六、集成点 +(数据库 / Redis / 消息队列 / 配置中心 Nacos / SSO-CAS / 外部 API,写明用途,不写密钥) + +## 七、重要设计决策与原因 +(为何这样分层、关键取舍,例如"去 L1/L2 分层配置、回归单层 + 重启兜底") + +## 八、待办与已知坑 +(未完成的白名单策略、待修项、历史踩坑) diff --git a/UserhomeService b/UserhomeService new file mode 160000 index 0000000..893c4e8 --- /dev/null +++ b/UserhomeService @@ -0,0 +1 @@ +Subproject commit 893c4e83ae8135b919b3873b1b109029e143ea91 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8bdad47 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,18 @@ +# 由 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 部署 env 注入(默认 -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 10000 +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar"] diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..69662a2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.yunhai + UserHome + 0.1 + UserHome + yh-platform 用户首页/应用门户 + + 1.8 + + + + + + com.yunhai.platform + yh-platform-dependencies + 1.0.0-SNAPSHOT + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2021.0.5.0 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-aop + + + + org.projectlombok + lombok + 1.18.36 + true + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + cn.hutool + hutool-all + 5.0.4 + + + org.modelmapper + modelmapper + 3.2.0 + + + + + com.yunhai.platform + yh-auth-spring-boot-starter + + + com.yunhai.platform + yh-app-auth-spring-boot-starter + + + com.yunhai.platform + yh-openapi-spring-boot-starter + + + com.yunhai.platform + yh-api-std + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.8 + + + + org.jasig.cas.client + cas-client-core + 3.5.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + nexus-public + bsch/nexus public + http://nexus.yunhaixingtong.com/repository/maven-public/ + true + true + + + diff --git a/src/main/java/com/yunhai/userhome/UserHomeApplication.java b/src/main/java/com/yunhai/userhome/UserHomeApplication.java new file mode 100644 index 0000000..4eca76f --- /dev/null +++ b/src/main/java/com/yunhai/userhome/UserHomeApplication.java @@ -0,0 +1,12 @@ +package com.yunhai.userhome; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserHomeApplication { + + public static void main(String[] args) { + SpringApplication.run(UserHomeApplication.class, args); + } +} diff --git a/src/main/java/com/yunhai/userhome/common/BaseEntity.java b/src/main/java/com/yunhai/userhome/common/BaseEntity.java new file mode 100644 index 0000000..2332024 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/common/BaseEntity.java @@ -0,0 +1,35 @@ +package com.yunhai.userhome.common; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * JPA 实体基类(与 UserBase/BasePlatform 约定一致):自增主键 + 创建/更新时间。 + */ +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +@MappedSuperclass +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + @Column(name = "id") + private Integer id; + + @Column(name = "create_time") + private LocalDateTime createTime; + + @Column(name = "update_time") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/userhome/config/Config.java b/src/main/java/com/yunhai/userhome/config/Config.java new file mode 100644 index 0000000..62fda55 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/config/Config.java @@ -0,0 +1,13 @@ +package com.yunhai.userhome.config; + +/** + * 路径前缀约定(与 UserBase 保持一致): + * {@code /{platform}} 普通接口(CAS 登录态),{@code /oapi/} 开放接口(yh-app-auth 签名)。 + */ +public enum Config { + Inst; + + // 末尾不要带斜杠:Boot 2.6+ 默认 PathPatternParser 不合并连续斜杠,会导致 404。 + public static final String API_PATH = "/{platform}"; + public static final String OPEN_API_PATH = "/oapi/"; +} diff --git a/src/main/java/com/yunhai/userhome/controller/api/UsedAppController.java b/src/main/java/com/yunhai/userhome/controller/api/UsedAppController.java new file mode 100644 index 0000000..ce800f4 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/controller/api/UsedAppController.java @@ -0,0 +1,154 @@ +package com.yunhai.userhome.controller.api; + +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userhome.config.Config; +import com.yunhai.userhome.entity.UsedApp; +import com.yunhai.userhome.model.SetType; +import com.yunhai.userhome.service.AppRemoteService; +import com.yunhai.userhome.service.UsedAppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +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.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 常用应用接口(CAS 登录态,路径 {@code /{platform}/usedapp/}),对齐旧 UserHome契约 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/usedapp/") +public class UsedAppController { + + @Autowired + private UsedAppService usedAppService; + + @Autowired + private AppRemoteService appRemoteService; + + /** + * 取当前角色生效的常用应用(BoaApp 详情,按 appIds 顺序)。 + * + * @param roleType 角色类型 + * @param schId 学校ID(0 时按用户主学校/主归属组推断) + * @param isSch 是否学校级(超管设置学校常用时 true,isSch 时 uid=schId) + */ + @GetMapping("getUsedApp") + public List getUsedApp(BoaUser authUserWithDept, Integer roleType, Integer schId, + @RequestParam(name = "isSch", defaultValue = "false") Boolean isSch) { + if (schId == null || schId == 0) { + schId = resolveSchId(authUserWithDept); + } + Integer uid = authUserWithDept.getId(); + if (authUserWithDept.getType() != null && authUserWithDept.getType() == 1) { + isSch = false; // 超管设置学校常用时走用户维度 + } + if (isSch) { + uid = schId; + } + return usedAppService.getUsedAppList(schId, uid, roleType, authUserWithDept); + } + + /** + * 覆盖式保存常用应用(≤8 由前端控制)。 + * 参数:appIds(逗号分隔)、type(0系统/1学校/2用户)、roleType、schId;超管强制 type=0。 + */ + @PostMapping("saveUsedApp") + public void saveUsedApp(BoaUser authUserWithDept, @RequestBody Map params) { + String appIds = params.get("appIds") == null ? "" : String.valueOf(params.get("appIds")); + Integer type = params.get("type") == null ? SetType.USER.id() + : Integer.valueOf(String.valueOf(params.get("type"))); + Integer roleType = params.get("roleType") == null ? -1 + : Integer.valueOf(String.valueOf(params.get("roleType"))); + Integer schId = params.get("schId") == null ? 0 + : Integer.valueOf(String.valueOf(params.get("schId"))); + if (schId == 0) { + schId = resolveSchId(authUserWithDept); + } + + UsedApp usedApp = new UsedApp(); + usedApp.setAppIds(appIds); + usedApp.setType(SetType.byId(type)); + usedApp.setRoleType(roleType); + usedApp.setCDate(new Date()); + usedApp.setCId(authUserWithDept.getId()); + usedApp.setSchId(schId); + usedApp.setSetId(usedApp.getType() == SetType.SCHOOL ? schId : authUserWithDept.getId()); + if (authUserWithDept.getType() != null && authUserWithDept.getType() == 1) { + // 超管保存 → 系统级 + usedApp.setType(SetType.BASE); + usedApp.setSetId(authUserWithDept.getId()); + } + usedAppService.save(usedApp); + } + + /** 用户级常用(type=USER, roleType=-1)精简映射(id/name/icon/url)。 */ + @GetMapping("getUsedAppById") + public List> getUsedAppById(BoaUser authUserWithDept) { + return usedAppService.getUsedAppMapList(authUserWithDept, authUserWithDept.getId()); + } + + /** 用户级保存常用(type=USER, roleType=-1)。 */ + @PostMapping("saveUsedAppById") + public void saveUsedAppById(BoaUser authUserWithDept, @RequestBody Map params) { + String appIds = params.get("appIds") == null ? "" : String.valueOf(params.get("appIds")); + UsedApp usedApp = new UsedApp(); + usedApp.setAppIds(appIds); + usedApp.setType(SetType.USER); + usedApp.setRoleType(-1); + usedApp.setCDate(new Date()); + usedApp.setCId(authUserWithDept.getId()); + usedApp.setSetId(authUserWithDept.getId()); + usedAppService.saveUserId(usedApp); + } + + /** 按 appIds(逗号分隔)取应用精简映射(id/name/icon/url),在全集中查找。 */ + @GetMapping("getAppByIds") + public List> getAppByIds(BoaUser authUserWithDept, String appIds) { + List> list = new ArrayList<>(); + if (appIds == null || appIds.trim().isEmpty()) { + return list; + } + List appList = appRemoteService.getUserApps(authUserWithDept).stream() + .filter(t -> t.getApps() != null) + .flatMap(t -> t.getApps().stream()) + .collect(Collectors.toList()); + for (String appId : appIds.split(",")) { + try { + Integer id = Integer.parseInt(appId.trim()); + appList.stream().filter(a -> id.equals(a.getId())).findFirst().ifPresent(app -> { + Map m = new HashMap<>(); + m.put("id", app.getId()); + m.put("name", app.getName()); + m.put("icon", app.getIcon()); + m.put("url", app.getWebAddr()); + list.add(m); + }); + } catch (NumberFormatException ignore) { + // 脏数据容错 + } + } + return list; + } + + /** schId=0 时推断:主学校 → 主归属组(bureau)→ 0。 */ + private Integer resolveSchId(BoaUser user) { + if (user.getSchool() != null) { + return user.getSchool().getId(); + } + if (user.getBureau() != null) { + return user.getBureau().getId(); + } + return 0; + } +} diff --git a/src/main/java/com/yunhai/userhome/controller/api/UserController.java b/src/main/java/com/yunhai/userhome/controller/api/UserController.java new file mode 100644 index 0000000..6072f71 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/controller/api/UserController.java @@ -0,0 +1,88 @@ +package com.yunhai.userhome.controller.api; + +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.platform.utils.model.BoaRoleInfo; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userhome.config.Config; +import com.yunhai.userhome.entity.UserCurrentRole; +import com.yunhai.userhome.service.AppRemoteService; +import com.yunhai.userhome.service.IUserCurrentRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +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; + +/** + * 用户首页接口(CAS 登录态,路径 {@code /{platform}/user/})。 + * 控制器直接 return 对象,由 yh-api-std 包成 {@code {code:0,data}}。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/user/") +public class UserController { + + @Autowired + private AppRemoteService appRemoteService; + + @Autowired + private IUserCurrentRoleService userCurrentRoleService; + + /** 当前登录用户信息(CAS 注入)。 */ + @GetMapping("GetUserInfo") + public BoaUser getUserInfo(BoaUser authUserWithDept) { + return authUserWithDept; + } + + /** + * 当前用户可访问的应用全集(跨仓 UserBase 实时拉取,按应用类型分组)。 + * 前端据此:渲染分类 tab(含「全部」)+ 图标网格;常用区由 getUsedApp 独立提供。 + * + * @param schId 兼容旧前端入参(yh-platform getUserApp 当前按 userId 全局权限过滤,schId 暂不参与) + * @param userId 指定用户(缺省用登录用户) + */ + @GetMapping("GetUserApp") + public List getUserApp(BoaUser authUserWithDept, Integer schId, + @RequestParam(name = "userId", required = false) Integer userId) { + return appRemoteService.getUserApps(authUserWithDept); + } + + /** 按应用ID取单个应用(在已拉取全集中查找,免额外跨仓请求)。 */ + @GetMapping("GetAppInfoById") + public BoaApp getAppInfoById(BoaUser authUserWithDept, @RequestParam(name = "id", required = false) Integer id) { + return appRemoteService.getAppById(authUserWithDept, id).orElse(null); + } + + /** 当前用户角色身份列表(跨仓 UserBase,含 学校/角色/子节点)。 */ + @GetMapping("getUserRole") + public List getUserRole(BoaUser authUserWithDept) { + return YschBaseOpenApi.getUserRoleList(authUserWithDept.getId()); + } + + /** 当前用户激活的角色(本地 user_current_role,可能为空)。 */ + @GetMapping("findUserRole") + public UserCurrentRole findUserRole(BoaUser authUserWithDept) { + return userCurrentRoleService.findById(authUserWithDept.getId()).orElse(null); + } + + /** 保存当前激活角色(校验所选 学校+角色 属于用户角色列表,防越权)。 */ + @PostMapping("savUserRole") + public void savUserRole(BoaUser authUserWithDept, @RequestBody UserCurrentRole roleInfo) { + List roleList = YschBaseOpenApi.getUserRoleList(authUserWithDept.getId()); + boolean exists = roleList.stream().anyMatch(r -> + r.getRoleId() != null && r.getRoleId().equals(roleInfo.getRoleId()) + && r.getSchId() != null && r.getSchId().equals(roleInfo.getSchId())); + if (!exists) { + throw new BizException(ErrorCode.FORBIDDEN, "非法操作,没有权限"); + } + roleInfo.setUserId(authUserWithDept.getId()); + userCurrentRoleService.save(roleInfo); + } +} diff --git a/src/main/java/com/yunhai/userhome/entity/UsedApp.java b/src/main/java/com/yunhai/userhome/entity/UsedApp.java new file mode 100644 index 0000000..8da229a --- /dev/null +++ b/src/main/java/com/yunhai/userhome/entity/UsedApp.java @@ -0,0 +1,64 @@ +package com.yunhai.userhome.entity; + +import com.yunhai.userhome.common.BaseEntity; +import com.yunhai.userhome.model.SetType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Index; +import javax.persistence.Table; +import java.util.Date; + +/** + * 用户「常用应用」设置(本地表)。 + * + *

对齐旧 UserHome used_app 结构:一行存一组常用(appIds 逗号分隔), + * 按 {@code setType(0系统/1学校/2用户) + setId + roleType + schId} 四维唯一覆盖保存。 + * 应用元数据不落本地,跨仓来自 UserBase。

+ */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "used_app", + indexes = { + @Index(name = "idx_used_app_set", columnList = "set_id,set_type,role_type,sch_id") + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class UsedApp extends BaseEntity { + + /** 常用应用ID,逗号分隔字符串(按展示顺序)。 */ + @Column(name = "app_ids", length = 500) + private String appIds; + + /** 设置级别:0=系统 1=学校 2=用户。 */ + @Enumerated(EnumType.ORDINAL) + @Column(name = "set_type") + private SetType type; + + /** 归属维度ID:系统=超管用户ID、学校=schId、用户=用户ID。 */ + @Column(name = "set_id") + private Integer setId; + + /** 学校ID(逻辑外键 → UserBase sys_group.id,schId=0 表示非学校群组)。 */ + @Column(name = "sch_id") + private Integer schId; + + /** 角色类型(-1 表示用户级不区分角色)。 */ + @Column(name = "role_type") + private Integer roleType; + + /** 操作人ID(逻辑外键 → UserBase 用户)。 */ + @Column(name = "c_id") + private Integer cId; + + /** 操作时间。 */ + @Column(name = "c_date") + private Date cDate; +} diff --git a/src/main/java/com/yunhai/userhome/entity/UserCurrentRole.java b/src/main/java/com/yunhai/userhome/entity/UserCurrentRole.java new file mode 100644 index 0000000..5250dc4 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/entity/UserCurrentRole.java @@ -0,0 +1,41 @@ +package com.yunhai.userhome.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +/** + * 用户当前激活角色(会话态,本地表 user_current_role)。 + * 对应旧 UserHome 同名单表:一用户一行,记录当前角色 + 学校。 + */ +@Data +@Entity +@Table( + name = "user_current_role", + uniqueConstraints = @UniqueConstraint(columnNames = {"userId"}), + indexes = {@Index(columnList = "userId")} +) +public class UserCurrentRole { + + @Id + @JsonIgnore + private Integer userId; + + private Integer roleId; + + private String roleName; + + private Integer schId; + + private String schName; + + private Integer childId; + + private String childName; +} diff --git a/src/main/java/com/yunhai/userhome/model/SetType.java b/src/main/java/com/yunhai/userhome/model/SetType.java new file mode 100644 index 0000000..b822054 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/model/SetType.java @@ -0,0 +1,38 @@ +package com.yunhai.userhome.model; + +/** + * 常用应用设置级别(对齐旧 UserHome 约定): + * 0=系统(超管默认)、1=学校(按学校+角色)、2=用户(按用户+角色)。 + */ +public enum SetType { + + BASE(0, "系统"), + SCHOOL(1, "学校"), + USER(2, "用户"); + + private final int mId; + private final String mName; + + SetType(int id, String name) { + this.mId = id; + this.mName = name; + } + + public int id() { + return mId; + } + + public static SetType byId(int id) { + for (SetType t : values()) { + if (t.mId == id) { + return t; + } + } + return null; + } + + @Override + public String toString() { + return mName; + } +} diff --git a/src/main/java/com/yunhai/userhome/repository/UsedAppRepository.java b/src/main/java/com/yunhai/userhome/repository/UsedAppRepository.java new file mode 100644 index 0000000..aa96eb9 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/repository/UsedAppRepository.java @@ -0,0 +1,21 @@ +package com.yunhai.userhome.repository; + +import com.yunhai.userhome.entity.UsedApp; +import com.yunhai.userhome.model.SetType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; +import java.util.Optional; + +public interface UsedAppRepository extends JpaRepository, JpaSpecificationExecutor { + + /** 按「归属+级别+角色+学校」四维查唯一记录(覆盖保存前判重)。 */ + Optional findFirstBySetIdAndTypeAndRoleTypeAndSchId(Integer setId, SetType type, Integer roleType, Integer schId); + + /** 按「归属+级别+角色」查(用户级保存,schId 不参与)。 */ + Optional findFirstBySetIdAndTypeAndRoleType(Integer setId, SetType type, Integer roleType); + + /** 兼容旧查询(供 Specification 之外的简单场景)。 */ + List findBySetIdAndTypeAndRoleTypeAndSchId(Integer setId, SetType type, Integer roleType, Integer schId); +} diff --git a/src/main/java/com/yunhai/userhome/repository/UserCurrentRoleRepository.java b/src/main/java/com/yunhai/userhome/repository/UserCurrentRoleRepository.java new file mode 100644 index 0000000..57fe257 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/repository/UserCurrentRoleRepository.java @@ -0,0 +1,11 @@ +package com.yunhai.userhome.repository; + +import com.yunhai.userhome.entity.UserCurrentRole; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserCurrentRoleRepository extends JpaRepository { + + Optional findByUserId(Integer userId); +} diff --git a/src/main/java/com/yunhai/userhome/service/AppRemoteService.java b/src/main/java/com/yunhai/userhome/service/AppRemoteService.java new file mode 100644 index 0000000..e9b4278 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/AppRemoteService.java @@ -0,0 +1,24 @@ +package com.yunhai.userhome.service; + +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.platform.utils.model.BoaUser; + +import java.util.List; +import java.util.Optional; + +/** + * 远程「用户可访问应用」数据服务(跨仓 UserBase)。 + * + *

实现 {@link com.yunhai.userhome.service.impl.OpenApiAppRemoteServiceImpl} + * 经 {@code YschBaseOpenApi.getUserApp} 实时拉取(自动带 yh-app-auth 签名), + * 数据权威在 UserBase;本模块不冗余应用元数据。

+ */ +public interface AppRemoteService { + + /** 取当前用户可访问的应用全集(按应用类型分组,BoaAppType.id/name/apps)。 */ + List getUserApps(BoaUser user); + + /** 按应用ID取单个应用(在已拉取的全集中查找,避免额外跨仓请求)。 */ + Optional getAppById(BoaUser user, Integer appId); +} diff --git a/src/main/java/com/yunhai/userhome/service/IUserCurrentRoleService.java b/src/main/java/com/yunhai/userhome/service/IUserCurrentRoleService.java new file mode 100644 index 0000000..3561658 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/IUserCurrentRoleService.java @@ -0,0 +1,17 @@ +package com.yunhai.userhome.service; + +import com.yunhai.userhome.entity.UserCurrentRole; + +import java.util.Optional; + +/** + * 用户当前激活角色(user_current_role 表读写)。 + */ +public interface IUserCurrentRoleService { + + /** 取用户当前角色(可能不存在)。 */ + Optional findById(Integer userId); + + /** 保存/覆盖用户当前角色。 */ + void save(UserCurrentRole role); +} diff --git a/src/main/java/com/yunhai/userhome/service/UsedAppService.java b/src/main/java/com/yunhai/userhome/service/UsedAppService.java new file mode 100644 index 0000000..790867d --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/UsedAppService.java @@ -0,0 +1,39 @@ +package com.yunhai.userhome.service; + +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userhome.entity.UsedApp; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 本地「常用应用」服务(used_app 表读写,对齐旧 UserHome 四维结构)。 + */ +public interface UsedAppService { + + /** 覆盖式保存(同 setId+type+roleType+schId 唯一,存在则更新)。 */ + void save(UsedApp usedApp); + + /** + * 按「学校+用户+角色」取生效的常用设置(多级:用户 > 学校 > 系统;schId==uid 直接取学校级)。 + * + * @param schId 学校ID(0 时按用户主学校推断后由调用方处理) + * @param uid 用户ID(isSch 时为 schId) + * @param roleType 角色类型(用户级 -1) + */ + Optional findById(Integer schId, Integer uid, Integer roleType); + + /** 取常用应用详情列表(BoaApp,按 appIds 顺序从可访问全集过滤)。 */ + List getUsedAppList(Integer schId, Integer uid, Integer roleType, BoaUser user); + + /** 用户级保存(type=USER, roleType=-1)。 */ + void saveUserId(UsedApp usedApp); + + /** 用户级常用(type=USER, roleType=-1)。 */ + Optional findByUserId(Integer uid); + + /** 常用应用精简映射(id/name/icon/url),用于 getUsedAppById / getAppByIds。 */ + List> getUsedAppMapList(BoaUser user, Integer uid); +} diff --git a/src/main/java/com/yunhai/userhome/service/impl/OpenApiAppRemoteServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/OpenApiAppRemoteServiceImpl.java new file mode 100644 index 0000000..9ea21c4 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/impl/OpenApiAppRemoteServiceImpl.java @@ -0,0 +1,42 @@ +package com.yunhai.userhome.service.impl; + +import com.yunhai.platform.openapi.client.YschBaseOpenApi; +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userhome.service.AppRemoteService; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * {@link AppRemoteService} 真实实现:经 yh-platform {@code YschBaseOpenApi} 调 UserBase + * {@code GET /oapi/app/list?userId=&clientType=}(自动带 app-auth 签名)。 + */ +@Service +public class OpenApiAppRemoteServiceImpl implements AppRemoteService { + + @Override + public List getUserApps(BoaUser user) { + if (user == null || user.getId() == null) { + return Collections.emptyList(); + } + // PC 浏览器端;UserBase 侧已按 userId 权限过滤(组开通 ∪ 角色模块所属应用,超管全量) + return YschBaseOpenApi.getUserApp(user.getId(), ClientEnum.PC_BROWSER); + } + + @Override + public Optional getAppById(BoaUser user, Integer appId) { + if (appId == null) { + return Optional.empty(); + } + return getUserApps(user).stream() + .filter(t -> t.getApps() != null) + .flatMap(t -> t.getApps().stream()) + .filter(a -> appId.equals(a.getId())) + .findFirst(); + } +} diff --git a/src/main/java/com/yunhai/userhome/service/impl/UsedAppServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/UsedAppServiceImpl.java new file mode 100644 index 0000000..b8255d5 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/impl/UsedAppServiceImpl.java @@ -0,0 +1,182 @@ +package com.yunhai.userhome.service.impl; + +import com.yunhai.platform.utils.model.BoaApp; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userhome.entity.UsedApp; +import com.yunhai.userhome.model.SetType; +import com.yunhai.userhome.repository.UsedAppRepository; +import com.yunhai.userhome.service.AppRemoteService; +import com.yunhai.userhome.service.UsedAppService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class UsedAppServiceImpl implements UsedAppService { + + @Autowired + private UsedAppRepository usedAppRepository; + + @Autowired + private AppRemoteService appRemoteService; + + @Override + public void save(UsedApp usedApp) { + Optional model = usedAppRepository + .findFirstBySetIdAndTypeAndRoleTypeAndSchId(usedApp.getSetId(), usedApp.getType(), + usedApp.getRoleType(), usedApp.getSchId()); + if (model.isPresent()) { + usedApp.setId(model.get().getId()); + } + usedAppRepository.save(usedApp); + } + + @Override + public Optional findById(Integer schId, Integer uid, Integer roleType) { + List list = usedAppRepository.findAll(select(schId, uid, roleType)); + Optional baseMod = list.stream() + .filter(x -> x.getType() == SetType.BASE) + .sorted(Comparator.comparing(UsedApp::getCDate).reversed()) + .findFirst(); + Optional schMod = list.stream() + .filter(x -> x.getType() == SetType.SCHOOL) + .sorted(Comparator.comparing(UsedApp::getCDate).reversed()) + .findFirst(); + Optional userMod = list.stream() + .filter(x -> x.getType() == SetType.USER) + .sorted(Comparator.comparing(UsedApp::getCDate).reversed()) + .findFirst(); + if (schId != null && schId.equals(uid)) { + return schMod; + } + // 优先级:用户 > 学校 > 系统 + if (userMod.isPresent()) { + return userMod; + } + if (schMod.isPresent()) { + return schMod; + } + return baseMod; + } + + @Override + public List getUsedAppList(Integer schId, Integer uid, Integer roleType, BoaUser user) { + List appIdList = new ArrayList<>(); + Optional data = findById(schId, uid, roleType); + if (data.isPresent() && data.get().getAppIds() != null) { + String usedStr = data.get().getAppIds(); + if (!usedStr.trim().isEmpty()) { + appIdList = Arrays.asList(usedStr.split(",")); + } + } + if (appIdList.isEmpty()) { + return new ArrayList<>(); + } + List appList = flattenApps(appRemoteService.getUserApps(user)); + Map appMap = appList.stream() + .collect(Collectors.toMap(BoaApp::getId, a -> a, (a, b) -> a)); + List result = new ArrayList<>(); + for (String appId : appIdList) { + try { + BoaApp app = appMap.get(Integer.parseInt(appId.trim())); + if (app != null) { + result.add(app); + } + } catch (NumberFormatException ignore) { + // 脏数据容错 + } + } + return result; + } + + @Override + public void saveUserId(UsedApp usedApp) { + Optional model = usedAppRepository + .findFirstBySetIdAndTypeAndRoleType(usedApp.getSetId(), SetType.USER, -1); + if (model.isPresent()) { + usedApp.setId(model.get().getId()); + } + usedAppRepository.save(usedApp); + } + + @Override + public Optional findByUserId(Integer uid) { + return usedAppRepository.findFirstBySetIdAndTypeAndRoleType(uid, SetType.USER, -1); + } + + @Override + public List> getUsedAppMapList(BoaUser user, Integer uid) { + List> list = new ArrayList<>(); + Optional data = findByUserId(uid); + if (!data.isPresent() || data.get().getAppIds() == null) { + return list; + } + List appIds = Arrays.asList(data.get().getAppIds().split(",")); + List appList = flattenApps(appRemoteService.getUserApps(user)); + Map appMap = appList.stream() + .collect(Collectors.toMap(BoaApp::getId, a -> a, (a, b) -> a)); + for (String appId : appIds) { + try { + BoaApp app = appMap.get(Integer.parseInt(appId.trim())); + if (app != null) { + Map m = new HashMap<>(); + m.put("id", app.getId()); + m.put("name", app.getName()); + m.put("icon", app.getIcon()); + m.put("url", app.getWebAddr()); + list.add(m); + } + } catch (NumberFormatException ignore) { + // 脏数据容错 + } + } + return list; + } + + /** BoaAppType 分组平铺为 BoaApp 列表。 */ + private List flattenApps(List types) { + List apps = new ArrayList<>(); + if (types == null) { + return apps; + } + for (BoaAppType type : types) { + if (type.getApps() != null) { + apps.addAll(type.getApps()); + } + } + return apps; + } + + /** 一级常用生效范围:BASE(按角色) OR SCHOOL(setId=schId+角色) OR USER(setId=uid+角色+schId)。 */ + private Specification select(Integer schId, Integer uid, Integer roleType) { + return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { + Predicate basePre = cb.and( + cb.equal(root.get("type"), SetType.BASE), + cb.equal(root.get("roleType"), roleType)); + Predicate schPer = cb.and( + cb.equal(root.get("setId"), schId), + cb.equal(root.get("type"), SetType.SCHOOL), + cb.equal(root.get("roleType"), roleType)); + Predicate userPer = cb.and( + cb.equal(root.get("setId"), uid), + cb.equal(root.get("type"), SetType.USER), + cb.equal(root.get("roleType"), roleType), + cb.equal(root.get("schId"), schId)); + return cb.or(basePre, schPer, userPer); + }; + } +} diff --git a/src/main/java/com/yunhai/userhome/service/impl/UserCurrentRoleServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/UserCurrentRoleServiceImpl.java new file mode 100644 index 0000000..f22f8b5 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/impl/UserCurrentRoleServiceImpl.java @@ -0,0 +1,26 @@ +package com.yunhai.userhome.service.impl; + +import com.yunhai.userhome.entity.UserCurrentRole; +import com.yunhai.userhome.repository.UserCurrentRoleRepository; +import com.yunhai.userhome.service.IUserCurrentRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserCurrentRoleServiceImpl implements IUserCurrentRoleService { + + @Autowired + private UserCurrentRoleRepository repository; + + @Override + public Optional findById(Integer userId) { + return repository.findByUserId(userId); + } + + @Override + public void save(UserCurrentRole role) { + repository.save(role); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ca21770 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,62 @@ +server: + port: 8070 + servlet: + context-path: / + +spring: + application: + name: userhome + # 关闭 Spring Cloud 对 nacos 配置中心的强制 spring.config.import 校验: + # 本项目用 spring-cloud-starter-bootstrap + bootstrap.yml 的老机制加载 nacos(enabled 控制开关), + # 不依赖 2.4+ 的 configdata import 机制——否则 classpath 含 nacos-config 会强制要求声明 spring.config.import, + # 且该 import 在本地 bootstrap.yml 尚未加载时因 server-addr 为空而硬连失败。 + # 本地 enabled=false 纯走 application.yml;CI 注入 NACOS_CONFIG_ENABLED=true 走 bootstrap.yml 老路径拉配置。 + cloud: + nacos: + config: + import-check: + enabled: false + datasource: + url: jdbc:mysql://localhost:3306/bsch_userhome?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: false + open-in-view: false + properties: + hibernate: + jdbc: + time_zone: Asia/Shanghai + redis: + host: localhost + port: 6379 + database: 0 + +# ===== 基础平台 yh 配置(遵循 yh-* spring-boot-starter 约定,具体 key 以对应 starter 版本为准)===== +yh: + auth: + # CAS 单点登录(浏览器 SSO 重定向 / ticket 校验) + cas: + server-url-prefix: https://sso.yunhaixingtong.com/cas + service: http://localhost:8070 + app-auth: + # 应用间签名鉴权(UserHome 作为调用方访问 UserBase 跨仓接口时使用) + enabled: true + # UserHome 在 UserBase 注册得到的 appKey/appSecret(应用注册后填入) + app-key: 100000 + app-secret: change-me-in-phase0 + open: + # yh-openapi 开放接口客户端(前缀必须是 yh.open,OpenApiProperties 绑定;base-host + base-path 拼出 + # 完整跨仓基址,openapi 客户端再拼 /oapi/** 端点;app-key/app-secret 同时驱动应用间签名) + enabled: true + base-host: http://localhost:8060 + base-path: /userbase + app-key: 100000 + app-secret: change-me-in-phase0 + +logging: + level: + com.yunhai.userhome: debug diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..1142646 --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -0,0 +1,45 @@ +spring: + application: + name: userhome + # 本地默认 local(不连 nacos);CI 注入 SPRING_PROFILES_ACTIVE=prod + profiles: + active: ${SPRING_PROFILES_ACTIVE:local} + cloud: + nacos: + config: + # ⭐ 本地默认关闭 nacos(profile=local 时完全不连,无 403 噪音,纯走 application.yml) + # CI 注入 NACOS_CONFIG_ENABLED=true 才启用,拉 prod namespace 配置覆盖 application.yml + enabled: ${NACOS_CONFIG_ENABLED:false} + # Nacos 配置中心地址;CI 注入 NACOS_ADDR=nacos:8848(容器内网别名) + server-addr: ${NACOS_ADDR:nacos.yunhaixingtong.com:8848} + # 默认指向 prod namespace UUID,避免本地误开时连到空的 dev 命名空间 + # CI 用 SPRING_CLOUD_NACOS_CONFIG_NAMESPACE 覆盖(注意:NACOS_NAMESPACE 这个 env 名不生效,必须用 SPRING_CLOUD_NACOS_CONFIG_NAMESPACE) + namespace: ${SPRING_CLOUD_NACOS_CONFIG_NAMESPACE:6196f0de-a28d-4894-8f10-e2982b57c6bc} + # 主配置 group:userhome.yaml 在 USERHOME group(与下方 shared-configs 的 DEFAULT_GROUP 不同组,各自指定) + group: USERHOME + # 配置文件格式;最终主配置 dataId = ${spring.application.name}.${file-extension} = userhome.yaml + file-extension: yaml + encode: UTF-8 + # nacos 不可达时不强制退出,回退到本地 application.yml 兜底 + fail-fast: false + # 只读账号(仅读取配置权限);CI 注入 SPRING_CLOUD_NACOS_USERNAME/PASSWORD + username: ${SPRING_CLOUD_NACOS_USERNAME:yh-reader} + password: ${SPRING_CLOUD_NACOS_PASSWORD:123456} + + # ============================================================ + # 共享配置加载顺序(决定优先级!:排在数组越靠后 = 优先级越高 = 覆盖前面的同 key) + # 低 1) public 命名空间 application-default.yaml(跨环境公共) + # 中 2) prod 命名空间 application-infra.yaml(仅生产:mysql/redis 等中间件连接) + # 高 3) prod 命名空间 userhome.yaml(应用级主配置,覆盖前两个) + # 最高 4) 启动命令 -D 参数 / 环境变量(覆盖一切) + # ============================================================ + shared-configs: + # ① public 命名空间 application-default.yaml + - data-id: application-default.yaml + group: DEFAULT_GROUP + namespace: "" + refresh: false + # ② prod 命名空间 application-infra.yaml + - data-id: application-infra.yaml + group: DEFAULT_GROUP + refresh: false diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..a301781 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,35 @@ + + + + + + + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + + /app/logs/app.log + + /app/logs/app.%d{yyyy-MM-dd}.log + 30 + 5GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + + + + + + + + + + + diff --git a/src/main/resources/sql/userhome_init.sql b/src/main/resources/sql/userhome_init.sql new file mode 100644 index 0000000..e112e50 --- /dev/null +++ b/src/main/resources/sql/userhome_init.sql @@ -0,0 +1,39 @@ +-- ============================================================ +-- UserHome 初始化脚本(used_app 常用应用表 + user_current_role 当前角色表) +-- 说明:JPA ddl-auto=update 会自动建表;本脚本用于生产环境显式初始化 / 回滚参考。 +-- 2026-08-20:used_app 结构升级对齐旧 UserHome(app_ids 一行 + set_type/set_id/sch_id/role_type 四维唯一)。 +-- 旧表若已有数据(user_id/app_id/sort_no 结构),请先备份后 DROP 重建,避免残留列。 +-- ============================================================ + +-- 常用应用设置表:一行存一组常用(appIds 逗号分隔),按 set_type+set_id+role_type+sch_id 覆盖保存 +CREATE TABLE IF NOT EXISTS `used_app` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', + `app_ids` VARCHAR(500) NULL COMMENT '常用应用ID,逗号分隔(按展示顺序)', + `set_type` INT NULL COMMENT '设置级别:0=系统 1=学校 2=用户', + `set_id` INT NULL COMMENT '归属维度ID:系统=超管用户ID、学校=schId、用户=用户ID', + `sch_id` INT NULL COMMENT '学校ID(逻辑外键 → UserBase sys_group.id,0=非学校群组)', + `role_type` INT NULL COMMENT '角色类型(-1 表示用户级不区分角色)', + `c_id` INT NULL COMMENT '操作人ID(逻辑外键 → UserBase 用户)', + `c_date` DATETIME NULL COMMENT '操作时间', + `create_time` DATETIME NULL COMMENT '创建时间', + `update_time` DATETIME NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_used_app_set` (`set_id`, `set_type`, `role_type`, `sch_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT = '用户「常用应用」设置(本地表,不冗余应用元数据)'; + +-- 用户当前激活角色表(一用户一行) +CREATE TABLE IF NOT EXISTS `user_current_role` ( + `user_id` INT NOT NULL COMMENT '用户ID(逻辑外键 → UserBase 用户)', + `role_id` INT NULL COMMENT '角色ID', + `role_name` VARCHAR(64) NULL COMMENT '角色名称', + `sch_id` INT NULL COMMENT '学校ID(逻辑外键 → UserBase sys_group.id)', + `sch_name` VARCHAR(128) NULL COMMENT '学校名称', + `child_id` INT NULL COMMENT '子节点ID(班级/学段等)', + `child_name` VARCHAR(128) NULL COMMENT '子节点名称', + PRIMARY KEY (`user_id`), + KEY `idx_user_current_role_user` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT = '用户当前激活角色(会话态)'; diff --git a/后端文件速查表.md b/后端文件速查表.md new file mode 100644 index 0000000..2813795 --- /dev/null +++ b/后端文件速查表.md @@ -0,0 +1,54 @@ +# 后端文件速查表 + +> 工程:UserHome(yh-platform 用户首页/应用门户) | 基础包:com.yunhai.userhome | 最近更新:2026-08-20 +> 维护规则:本表在**新增 / 重命名 / 删除** .java 源文件时自动同步;纯内容改动不触发。也可手动输出"更新后端文件速查表"全量重建。 + +## Controller(接口层) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/controller/api/UserController.java | UserController | 用户首页接口(CAS 登录态 `/{platform}/user/`):登录信息、可访问应用分组全集、按 id 取应用、角色列表/当前角色保存 | AppRemoteService、IUserCurrentRoleService、YschBaseOpenApi | +| src/main/java/com/yunhai/userhome/controller/api/UsedAppController.java | UsedAppController | 常用应用接口(CAS 登录态 `/{platform}/usedapp/`):常用详情(按角色/学校)、覆盖式保存(appIds+type+roleType+schId)、用户级常用、按 ids 取应用 | UsedAppService、AppRemoteService | + +## Service +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/service/AppRemoteService.java | AppRemoteService | 远程「用户可访问应用」数据服务接口(跨仓 UserBase 分组全集) | 实现 OpenApiAppRemoteServiceImpl;产出 BoaAppType | +| src/main/java/com/yunhai/userhome/service/impl/OpenApiAppRemoteServiceImpl.java | OpenApiAppRemoteServiceImpl | AppRemoteService 真实实现:经 YschBaseOpenApi.getUserApp 实时拉取(PC_BROWSER,自动带 app-auth 签名) | YschBaseOpenApi、ClientEnum | +| src/main/java/com/yunhai/userhome/service/UsedAppService.java | UsedAppService | 本地「常用应用」服务接口(used_app 表读写,四维结构) | 实现 UsedAppServiceImpl | +| src/main/java/com/yunhai/userhome/service/impl/UsedAppServiceImpl.java | UsedAppServiceImpl | 常用应用读写:覆盖式保存(setId+type+roleType+schId 唯一)、多级生效(用户>学校>系统)、详情从跨仓全集过滤 | UsedAppRepository、AppRemoteService | +| src/main/java/com/yunhai/userhome/service/IUserCurrentRoleService.java | IUserCurrentRoleService | 用户当前激活角色服务接口(user_current_role 表) | 实现 UserCurrentRoleServiceImpl | +| src/main/java/com/yunhai/userhome/service/impl/UserCurrentRoleServiceImpl.java | UserCurrentRoleServiceImpl | 当前角色读写实现 | UserCurrentRoleRepository | + +## Repository +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/repository/UsedAppRepository.java | UsedAppRepository | used_app 数据访问(Spring Data JPA + Specification):四维唯一判重、多级生效查询 | 实体 UsedApp | +| src/main/java/com/yunhai/userhome/repository/UserCurrentRoleRepository.java | UserCurrentRoleRepository | user_current_role 数据访问:按 userId 查当前角色 | 实体 UserCurrentRole | + +## Entity / Model +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/entity/UsedApp.java | UsedApp | 常用应用设置实体(app_ids 逗号分隔 + set_type/set_id/sch_id/role_type/c_id/c_date),本地表不冗余应用元数据 | 继承 BaseEntity;SetType | +| src/main/java/com/yunhai/userhome/entity/UserCurrentRole.java | UserCurrentRole | 用户当前激活角色实体(user_id 主键 + role_id/role_name/sch_id/sch_name/child_id/child_name) | — | +| src/main/java/com/yunhai/userhome/model/SetType.java | SetType | 常用应用设置级别枚举:0=系统 1=学校 2=用户 | 被 UsedApp 引用 | + +## Config +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/config/Config.java | Config | 路径前缀枚举:`/{platform}` 普通接口(CAS 登录态)、`/oapi/` 开放接口(yh-app-auth 签名) | 被 Controller 使用;注 Boot 2.6+ 路径勿带尾斜杠 | + +## Common(aspect/annotation/util/interceptor/listener) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/common/BaseEntity.java | BaseEntity | JPA 实体基类(自增主键 id + create_time + update_time),与 UserBase/BasePlatform 约定一致 | 被 UsedApp 继承 | + +## 其他(task/event/enums) +| 路径(相对工程根) | 类名 | 职责 | 关键关联 | +| --- | --- | --- | --- | +| src/main/java/com/yunhai/userhome/UserHomeApplication.java | UserHomeApplication | Spring Boot 启动入口类 | — | + +## 变更记录 +| 时间 | 类型 | 说明 | +| --- | --- | --- | +| 2026-08-19 | 初始化 | 建表(随"启用后端文档维护"触发,扫描现有 12 个 .java 生成) | +| 2026-08-20 | 重构 | 跨仓接口就绪:删 PlaceholderAppRemoteServiceImpl / UserAppVo,新增 OpenApiAppRemoteServiceImpl(真实 YschBaseOpenApi 调用);used_app 升级四维结构;新增 UserCurrentRole 一列实体/Repo/Service;UserController 增 getUserRole/findUserRole/savUserRole;UsedAppController 增 getUsedAppById/saveUsedAppById/getAppByIds,getUsedApp 返回 BoaApp 详情;新增 model/SetType | diff --git a/后端项目知识库.md b/后端项目知识库.md new file mode 100644 index 0000000..cf281cd --- /dev/null +++ b/后端项目知识库.md @@ -0,0 +1,66 @@ +# 后端项目知识库 + +> 工程:UserHome | 技术栈:Spring Boot 2.7.18 / JDK 1.8 / Spring Data JPA(Hibernate) + MySQL 8.0.33 / Redis | 最近更新:2026-08-20 +> 维护规则:发生重大功能变化、或收到"更新后端项目知识库"指令时更新。 + +## 一、项目概览 +- 定位:yh-platform 的**用户首页 / 应用门户**模块。负责用户首页应用聚合展示(分类 tab + 图标网格 + 搜索)、用户「常用应用」设置(≤8)、角色身份切换。应用元数据不落本地库,跨仓来自 UserBase。 +- 技术栈与版本:Spring Boot 2.7.18(parent `spring-boot-starter-parent`);JDK 1.8;持久化 Spring Data JPA + Hibernate,MySQL 8.0.33;缓存 Redis(`spring-boot-starter-data-redis`);构建 Maven;统一依赖经 `yh-platform-dependencies` BOM 与 `spring-cloud-alibaba-dependencies 2021.0.5.0`(Nacos,适配 Boot 2.7)。 +- 启动 / 运行方式:入口 `UserHomeApplication`;端口 **8070**,context-path `/`。默认 profile=`local`(不连 Nacos,纯走 application.yml);CI 注入 `SPRING_PROFILES_ACTIVE=prod` 启用 Nacos 配置中心覆盖。CAS 登录态用户以 `BoaUser` 注入 Controller。 + +## 二、模块与包结构 +- `controller.api`:接口层(`UserController`、`UsedAppController`),直接 return 对象,由 `yh-api-std` 包成 `{code:0,data}`。 +- `service` / `service.impl`:业务层。远程应用(`AppRemoteService`,跨仓 UserBase)+ 本地常用应用(`UsedAppService`)+ 当前角色(`IUserCurrentRoleService`)。 +- `repository`:数据访问(`UsedAppRepository`、`UserCurrentRoleRepository`,JPA)。 +- `entity`:JPA 实体(`UsedApp`、`UserCurrentRole`)。 +- `model`:枚举(`SetType`)。 +- `config`:路径前缀约定(`Config` 枚举)。 +- `common`:实体基类(`BaseEntity`)。 + +## 三、核心领域模型与关系 +- `UsedApp`(表 `used_app`):用户常用应用设置,**一行存一组**(`appIds` 逗号分隔),`set_type(0系统/1学校/2用户) + setId + roleType + schId` 四维唯一,覆盖式保存。**不冗余应用元数据**。 + - 逻辑外键:`set_id/c_id → UserBase 用户`、`sch_id → UserBase sys_group.id`、appId → `UserBase application_info.id`。**无物理外键,靠业务字段关联**。 + - 唯一索引 `idx_used_app_set(set_id,set_type,role_type,sch_id)`。 +- `UserCurrentRole`(表 `user_current_role`):用户当前激活角色(会话态),`userId` 主键 + `roleId/roleName/schId/schName/childId/childName`,对齐旧 UserHome 同名表。 +- `SetType`:常用设置级别 0=系统(超管)/1=学校/2=用户。 +- `BaseEntity`(抽象 `@MappedSuperclass`):自增 `id` + `create_time` + `update_time`(与 UserBase/BasePlatform 约定一致)。 +- 关系边界:**UserHome 只存「常用设置」与「当前角色」,应用元数据跨仓来自 UserBase**(`YschBaseOpenApi.getUserApp`)。 + +## 四、关键业务流程 +1. **获取用户应用**:`GET /{platform}/user/GetUserApp` → `AppRemoteService.getUserApps`(经 `YschBaseOpenApi.getUserApp(userId, PC_BROWSER)` 实时拉取)→ 返回 `List`(id/name/apps 分组)→ 前端渲染分类 tab(含「全部」)+ 图标网格 + 搜索。 +2. **常用应用展示**:`GET /{platform}/usedapp/getUsedApp?roleType=&schId=&isSch=` → 本地 `used_app` 按「用户 > 学校 > 系统」多级取生效记录 → appIds 拆分 → 从跨仓全集过滤 → 返回 `List`(详情,保持顺序)。 +3. **保存常用应用**:`POST /{platform}/usedapp/saveUsedApp`(body:appIds 逗号分隔 / type 0|1|2 / roleType / schId)→ 超管强制 type=0(系统级)→ `UsedAppService.save`:四维判重 upsert 覆盖保存。 +4. **角色身份**:`getUserRole`(跨仓角色列表)、`findUserRole`(本地当前角色)、`savUserRole`(校验所选 学校+角色 属于角色列表后 upsert 本地表)。 +5. **其它**:`GetUserInfo`(登录 BoaUser)、`GetAppInfoById?id=`(在全集中查找,免额外跨仓请求)、`getUsedAppById/saveUsedAppById`(用户级常用,roleType=-1)、`getAppByIds`(按 ids 取应用精简映射)。 + +## 五、工程约定与模式 +- 异常返回结构:Controller 直接 return 对象,`yh-api-std` 统一包成 `{code:0,data}`(HTTP 200);业务异常抛 `BizException(ErrorCode.FORBIDDEN/PARAM_ERROR, msg)`。 +- 鉴权方式:CAS SSO(浏览器登录态,BoaUser 注入,作用于 `/{platform}` 普通接口);跨仓调用 UserBase 走 `yh-app-auth` 应用间签名(`yh.open` 配置 appKey/appSecret,`OpenApiHttpClient` 自动附加 `X-App-Key/X-Timestamp/X-Signature`)。 +- 路径前缀约定:`Config` 枚举定义 `/{platform}` 普通、`/oapi/` 开放(与 UserBase 一致)。**注意 Boot 2.6+ 默认 PathPatternParser 不合并连续斜杠,路径末尾不要带斜杠,否则 404**。 +- 缓存:已引入 Redis(host localhost:6379 db0),当前未实际用于业务。 +- 事务边界:`UsedAppServiceImpl.save` 单表 upsert,未标 `@Transactional`(单条 save 自带事务)。 + +## 六、集成点 +- **MySQL(库 `bsch_userhome`)**:`used_app` + `user_current_role` 表;JPA `ddl-auto=update` 自动建表。凭据见 `application.yml`。 +- **Redis(localhost:6379 db0)**:已配置,用途待核实。 +- **Nacos 配置中心**:仅 `prod` profile 启用(`NACOS_CONFIG_ENABLED=true`),拉 `userhome.yaml`(group USERHOME)覆盖本地;本地 `local` 默认不连。详见 `bootstrap.yml`。 +- **CAS SSO**:`https://sso.yunhaixingtong.com/cas`。 +- **UserBase 跨仓开放接口**:经 `yh-openapi-spring-boot-starter` 自动装配 `YschBaseOpenApi` → `GET {yh.open.base-host}{base-path}/oapi/app/list?userId=&clientType=` 返回 `List`;`/oapi/user/roleList` 返回 `List`。 +- **yh-platform starters**:`yh-auth-spring-boot-starter`(CAS)、`yh-app-auth-spring-boot-starter`(签名)、`yh-openapi-spring-boot-starter`(跨仓调用)、`yh-api-std`(统一响应)。 + +## 七、重要设计决策与原因 +- **本地只存「常用设置 + 当前角色」**,不冗余应用元数据 —— 元数据跨仓来自 UserBase,避免双写不一致。 +- **一行存一组(appIds 逗号分隔)**而非逐应用一行 —— 对齐旧 UserHome 四维结构(系统/学校/用户 × 角色),支持超管设系统级、学校设角色级常用;`sortNo` 即逗号顺序。 +- **多级生效(用户 > 学校 > 系统)** —— 个人未设置时回退学校/系统默认,`schId==uid` 直接取学校级(对齐旧逻辑)。 +- **覆盖式保存(四维唯一 upsert)** —— 幂等,前端顺序即最终序。 +- **跨仓调用仅替换实现** —— `AppRemoteService` 接口稳定,`OpenApiAppRemoteServiceImpl` 一行调用 `YschBaseOpenApi.getUserApp`,前端与 Controller 无需感知底层。 +- **本地默认关闭 Nacos(profile=local)** —— CI 注入 prod 才连,避免本地 403 噪音;Nacos 不可达 `fail-fast=false` 回退本地配置兜底。 + +## 八、待办与已知坑 +- **appKey/appSecret 待填**:`yh.open` / `yh.app-auth` 的 app-key/app-secret 当前为占位(`100000` / `change-me-in-phase0`),需在 UserBase 完成 UserHome 应用注册后填入真实值,否则跨仓请求签名失败 401。 +- **`yh.open.base-host` 指向**:本地默认 `http://localhost:8060`(UserBase),生产经 Nacos `userhome.yaml` 覆盖。 +- **used_app 旧结构残留**:升级前若已有 `user_id/app_id/sort_no` 结构的旧表,`ddl-auto=update` 只加列不删列,建议按 `sql/userhome_init.sql` 备份后重建。 +- **Redis 已引入但未实际使用**,待核实是否计划启用。 +- **Nacos env 名陷阱**:`NACOS_NAMESPACE` 不生效,必须用 `SPRING_CLOUD_NACOS_CONFIG_NAMESPACE` 覆盖 namespace。 +- **路径前缀尾斜杠**:Boot 2.6+ PathPatternParser 不合并连续斜杠,勿带尾斜杠(防 404)。 +- **生产建表**:`ddl-auto=update` 自动建表;`sql/userhome_init.sql` 用于显式初始化 / 回滚参考,二者需保持字段一致。