@@ -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' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /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}\"}"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Maven 构建产物
|
||||
target/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
|
||||
# IDE
|
||||
*.iml
|
||||
.idea/
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# 本地配置(不入库)
|
||||
*.local
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
logs/
|
||||
@@ -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` 等未经授权不改动,仅给诊断建议。
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# 后端文件速查表
|
||||
|
||||
> 工程:<工程名> | 基础包:<base package> | 最近更新:<YYYY-MM-DD>
|
||||
> 维护规则:本表在**新增 / 重命名 / 删除** .java 源文件时自动同步;纯内容改动不触发。也可手动输出"更新后端文件速查表"全量重建。
|
||||
|
||||
## Controller(接口层)
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/controller/XxxController.java | XxxController | 一句话职责 | 关联实体/接口 |
|
||||
|
||||
## Service
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/service/XxxService.java | XxxService | 一句话职责 | 调用/被调用 |
|
||||
|
||||
## Repository
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/repository/XxxRepository.java | XxxRepository | 数据访问 | 实体 |
|
||||
|
||||
## Entity / Model
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/entity/Xxx.java | Xxx | 实体 | 逻辑外键 |
|
||||
|
||||
## DTO / VO / Request
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/model/XxxDto.java | XxxDto | 入参/出参 | 关联实体 |
|
||||
|
||||
## Config
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/config/XxxConfig.java | XxxConfig | 配置 | Bean/拦截器 |
|
||||
|
||||
## Common(aspect/annotation/util/interceptor/listener)
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/common/Xxx.java | Xxx | 公共能力 | 使用点 |
|
||||
|
||||
## 其他(task/event/enums)
|
||||
| 路径(相对工程根) | 类名 | 职责 | 关键关联 |
|
||||
| --- | --- | --- | --- |
|
||||
| src/main/java/<base>/enums/XxxEnum.java | XxxEnum | 枚举 | 使用点 |
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| YYYY-MM-DD | 初始化 | 建表 |
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# 后端项目知识库
|
||||
|
||||
> 工程:<工程名> | 技术栈:Spring Boot <x.y.z> / JDK <n> / <ORM> | 最近更新:<YYYY-MM-DD>
|
||||
> 维护规则:发生重大功能变化、或收到"更新后端项目知识库"指令时更新。
|
||||
|
||||
## 一、项目概览
|
||||
- 定位:
|
||||
- 技术栈与版本(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 分层配置、回归单层 + 重启兜底")
|
||||
|
||||
## 八、待办与已知坑
|
||||
(未完成的白名单策略、待修项、历史踩坑)
|
||||
Submodule
+1
Submodule UserhomeService added at 893c4e83ae
@@ -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"]
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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>UserHome</artifactId>
|
||||
<version>0.1</version>
|
||||
<name>UserHome</name>
|
||||
<description>yh-platform 用户首页/应用门户</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>
|
||||
<!-- Spring Cloud Alibaba(Nacos 配置中心,对齐 Boot 2.7) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||
<version>2021.0.5.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.36</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.0.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.modelmapper</groupId>
|
||||
<artifactId>modelmapper</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 基础平台架包:CAS 认证 + 应用间签名 + 统一响应(经 BOM 传递 yh-openapi-client 等) -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-auth-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-app-auth-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-openapi-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-api-std</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Nacos 配置中心(替代 Spring Cloud Config)。Spring Cloud Alibaba 2021.0.5 适配 Spring Boot 2.7 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<!-- Boot 2.4+ 默认不再处理 bootstrap.yml,需显式引入以继续从 Nacos 拉取配置 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
<version>3.1.8</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jasig.cas.client</groupId>
|
||||
<artifactId>cas-client-core</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>nexus-public</id>
|
||||
<name>bsch/nexus public</name>
|
||||
<url>http://nexus.yunhaixingtong.com/repository/maven-public/</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>true</enabled></snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/";
|
||||
}
|
||||
@@ -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<BoaApp> 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<String, Object> 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<Map<String, Object>> getUsedAppById(BoaUser authUserWithDept) {
|
||||
return usedAppService.getUsedAppMapList(authUserWithDept, authUserWithDept.getId());
|
||||
}
|
||||
|
||||
/** 用户级保存常用(type=USER, roleType=-1)。 */
|
||||
@PostMapping("saveUsedAppById")
|
||||
public void saveUsedAppById(BoaUser authUserWithDept, @RequestBody Map<String, Object> 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<Map<String, Object>> getAppByIds(BoaUser authUserWithDept, String appIds) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
if (appIds == null || appIds.trim().isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
List<BoaApp> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<BoaAppType> 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<BoaRoleInfo> 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<BoaRoleInfo> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 用户「常用应用」设置(本地表)。
|
||||
*
|
||||
* <p>对齐旧 UserHome used_app 结构:<b>一行存一组常用</b>(appIds 逗号分隔),
|
||||
* 按 {@code setType(0系统/1学校/2用户) + setId + roleType + schId} 四维唯一覆盖保存。
|
||||
* 应用元数据不落本地,跨仓来自 UserBase。</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<UsedApp, Integer>, JpaSpecificationExecutor<UsedApp> {
|
||||
|
||||
/** 按「归属+级别+角色+学校」四维查唯一记录(覆盖保存前判重)。 */
|
||||
Optional<UsedApp> findFirstBySetIdAndTypeAndRoleTypeAndSchId(Integer setId, SetType type, Integer roleType, Integer schId);
|
||||
|
||||
/** 按「归属+级别+角色」查(用户级保存,schId 不参与)。 */
|
||||
Optional<UsedApp> findFirstBySetIdAndTypeAndRoleType(Integer setId, SetType type, Integer roleType);
|
||||
|
||||
/** 兼容旧查询(供 Specification 之外的简单场景)。 */
|
||||
List<UsedApp> findBySetIdAndTypeAndRoleTypeAndSchId(Integer setId, SetType type, Integer roleType, Integer schId);
|
||||
}
|
||||
@@ -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<UserCurrentRole, Integer> {
|
||||
|
||||
Optional<UserCurrentRole> findByUserId(Integer userId);
|
||||
}
|
||||
@@ -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)。
|
||||
*
|
||||
* <p>实现 {@link com.yunhai.userhome.service.impl.OpenApiAppRemoteServiceImpl}
|
||||
* 经 {@code YschBaseOpenApi.getUserApp} 实时拉取(自动带 yh-app-auth 签名),
|
||||
* 数据权威在 UserBase;本模块不冗余应用元数据。</p>
|
||||
*/
|
||||
public interface AppRemoteService {
|
||||
|
||||
/** 取当前用户可访问的应用全集(按应用类型分组,BoaAppType.id/name/apps)。 */
|
||||
List<BoaAppType> getUserApps(BoaUser user);
|
||||
|
||||
/** 按应用ID取单个应用(在已拉取的全集中查找,避免额外跨仓请求)。 */
|
||||
Optional<BoaApp> getAppById(BoaUser user, Integer appId);
|
||||
}
|
||||
@@ -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<UserCurrentRole> findById(Integer userId);
|
||||
|
||||
/** 保存/覆盖用户当前角色。 */
|
||||
void save(UserCurrentRole role);
|
||||
}
|
||||
@@ -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<UsedApp> findById(Integer schId, Integer uid, Integer roleType);
|
||||
|
||||
/** 取常用应用详情列表(BoaApp,按 appIds 顺序从可访问全集过滤)。 */
|
||||
List<BoaApp> getUsedAppList(Integer schId, Integer uid, Integer roleType, BoaUser user);
|
||||
|
||||
/** 用户级保存(type=USER, roleType=-1)。 */
|
||||
void saveUserId(UsedApp usedApp);
|
||||
|
||||
/** 用户级常用(type=USER, roleType=-1)。 */
|
||||
Optional<UsedApp> findByUserId(Integer uid);
|
||||
|
||||
/** 常用应用精简映射(id/name/icon/url),用于 getUsedAppById / getAppByIds。 */
|
||||
List<Map<String, Object>> getUsedAppMapList(BoaUser user, Integer uid);
|
||||
}
|
||||
@@ -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<BoaAppType> 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<BoaApp> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<UsedApp> 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<UsedApp> findById(Integer schId, Integer uid, Integer roleType) {
|
||||
List<UsedApp> list = usedAppRepository.findAll(select(schId, uid, roleType));
|
||||
Optional<UsedApp> baseMod = list.stream()
|
||||
.filter(x -> x.getType() == SetType.BASE)
|
||||
.sorted(Comparator.comparing(UsedApp::getCDate).reversed())
|
||||
.findFirst();
|
||||
Optional<UsedApp> schMod = list.stream()
|
||||
.filter(x -> x.getType() == SetType.SCHOOL)
|
||||
.sorted(Comparator.comparing(UsedApp::getCDate).reversed())
|
||||
.findFirst();
|
||||
Optional<UsedApp> 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<BoaApp> getUsedAppList(Integer schId, Integer uid, Integer roleType, BoaUser user) {
|
||||
List<String> appIdList = new ArrayList<>();
|
||||
Optional<UsedApp> 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<BoaApp> appList = flattenApps(appRemoteService.getUserApps(user));
|
||||
Map<Integer, BoaApp> appMap = appList.stream()
|
||||
.collect(Collectors.toMap(BoaApp::getId, a -> a, (a, b) -> a));
|
||||
List<BoaApp> 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<UsedApp> model = usedAppRepository
|
||||
.findFirstBySetIdAndTypeAndRoleType(usedApp.getSetId(), SetType.USER, -1);
|
||||
if (model.isPresent()) {
|
||||
usedApp.setId(model.get().getId());
|
||||
}
|
||||
usedAppRepository.save(usedApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UsedApp> findByUserId(Integer uid) {
|
||||
return usedAppRepository.findFirstBySetIdAndTypeAndRoleType(uid, SetType.USER, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getUsedAppMapList(BoaUser user, Integer uid) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Optional<UsedApp> data = findByUserId(uid);
|
||||
if (!data.isPresent() || data.get().getAppIds() == null) {
|
||||
return list;
|
||||
}
|
||||
List<String> appIds = Arrays.asList(data.get().getAppIds().split(","));
|
||||
List<BoaApp> appList = flattenApps(appRemoteService.getUserApps(user));
|
||||
Map<Integer, BoaApp> 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<String, Object> 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<BoaApp> flattenApps(List<BoaAppType> types) {
|
||||
List<BoaApp> 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<UsedApp> select(Integer schId, Integer uid, Integer roleType) {
|
||||
return (Root<UsedApp> 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<UserCurrentRole> findById(Integer userId) {
|
||||
return repository.findByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(UserCurrentRole role) {
|
||||
repository.save(role);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 引入 Spring Boot 默认彩色日志变量 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
|
||||
<!-- 控制台:所有环境都有,且带颜色 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件日志:只有非 local 环境才启用 -->
|
||||
<springProfile name="!local">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/app/logs/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>5GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</springProfile>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<springProfile name="!local">
|
||||
<appender-ref ref="FILE" />
|
||||
</springProfile>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -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 = '用户当前激活角色(会话态)';
|
||||
+54
@@ -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 |
|
||||
+66
@@ -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<BoaAppType>`(id/name/apps 分组)→ 前端渲染分类 tab(含「全部」)+ 图标网格 + 搜索。
|
||||
2. **常用应用展示**:`GET /{platform}/usedapp/getUsedApp?roleType=&schId=&isSch=` → 本地 `used_app` 按「用户 > 学校 > 系统」多级取生效记录 → appIds 拆分 → 从跨仓全集过滤 → 返回 `List<BoaApp>`(详情,保持顺序)。
|
||||
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<BoaAppType>`;`/oapi/user/roleList` 返回 `List<BoaRoleInfo>`。
|
||||
- **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` 用于显式初始化 / 回滚参考,二者需保持字段一致。
|
||||
Reference in New Issue
Block a user