commit f182fe44ce52de77906c84108e91659e701d0387 Author: chenxing Date: Fri Aug 21 10:46:47 2026 +0800 本地工作区快照:恢复 .git 仓库(合并远端 main 前基线) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c6b15c7 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,156 @@ +name: ci-userbase-service + +# 后端标准化 CI:maven 构建 jar -> docker build -> 推 Nexus docker-hosted +# 触发:push 到 main(本仓库为 UserBaseService 独立仓,代码在根目录)/ 手动 Run workflow +# 依赖仓库 Actions Secrets:NEXUS_USER / NEXUS_PASSWORD(ci-docker 账号,仅 docker-hosted 推送权) +on: + push: + branches: [main] + workflow_dispatch: + inputs: + tag: + description: 'image tag (default = short sha)' + required: false + default: 'auto' + deploy: + description: 'deploy to 160 after build (auto / skip)' + required: false + default: 'auto' + +jobs: + build-and-push: + runs-on: ubuntu-latest + # docker:24 自带 docker CLI,复用宿主 docker daemon。 + # 注意:act_runner v0.6.1 默认已把宿主 docker.sock 挂入每个 job 容器, + # 切勿在此再声明 volumes: docker.sock(会与默认挂载重复,报 + # "Duplicate mount point" 导致容器创建失败、全部 step cancelled)。 + container: + image: docker:24 + steps: + # docker:24 基于 alpine,默认无 git;先装 git 再手动 clone 本仓库 + - name: Install git (alpine) + run: apk add --no-cache git + + # 内网 Gitea 无法访问 github.com 拉取 actions/checkout,改为直接 clone 本仓库 + - name: Checkout (manual, internal Gitea) + run: | + git clone --depth 1 --branch main http://192.168.10.160:3000/${GITHUB_REPOSITORY}.git /tmp/src + cp -a /tmp/src/. . + rm -rf /tmp/src + + - name: Compute image tag + id: meta + run: | + if [ "${{ inputs.tag }}" = "auto" ] || [ -z "${{ inputs.tag }}" ]; then + echo "tag=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT + else + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + fi + + - name: Build jar with Maven + run: | + # 注意:job 容器通过 docker.sock 调的是【宿主 docker daemon】, + # 因此 docker run -v 里的路径会被宿主按【宿主路径】解析; + # 而 act 工作区是宿主上的 docker volume,容器内 $PWD 对宿主不可见, + # 直接 -v $PWD:/build 会绑成空目录导致找不到 pom.xml。 + # 故改为:起 maven 容器 -> docker cp 源码入容器 -> exec 构建 -> cp 产物回工作区。 + # mavenbuild 加入 infra-infra-net,用容器名 nexus 直连 Nexus(8081 maven-public)。 + # 之前用 --add-host=nexus.yunhaixingtong.com:host-gateway 经 Caddy:80 反代,但该路径在 + # CI 容器内不可达,maven 回退到 /opt/docker/ubs-m2 里 08-05 的旧 yh-auth 缓存(缺 + # CasSsoAutoConfiguration),镜像启动报 serverName 崩溃。现改用 settings.xml profile 把 + # pom 的 HTTP 仓库重定向到 http://nexus:8081/repository/maven-public/(已验证 -7 可达)。 + # 先清理上次失败残留的同名容器(set -e 下末尾的 rm 不会执行) + docker rm -f mavenbuild 2>/dev/null || true + docker run -d --name mavenbuild \ + --network infra-infra-net \ + -v /opt/docker/ubs-m2:/root/.m2 \ + -w /build maven:3.6.3-jdk-8 sleep 3600 + docker cp "${PWD}/." mavenbuild:/build/ + # 写 settings.xml:新增 profile 把 nexus:8081 maven-public 作为额外仓库(infra-infra-net 内容器名直连,已验证 -7 可达)。 + # 不用 mirror(external:http:*) 是因为它对 BOM import 解析未生效;profile 方式更可靠。 + # pom 原有的 nexus.yunhaixingtong.com(Caddy:80) 在 CI 容器内不可解析 -> 快速失败,maven 转而用本仓库。 + docker exec mavenbuild sh -c "printf '%s' 'nexus-reponexus-internalhttp://nexus:8081/repository/maven-public/truetruealwaysnexus-repo' > /root/.m2/settings.xml" + # 仅清旧 yh-auth 缓存(含 08-05 local install 的 maven-metadata-local.xml,避免本地安装优先于远端 -7); + # BOM 等其它 yh.platform 模块保留缓存(Nexus 上它们仍是 08-05 的 -1,缓存与远端一致)。 + docker exec mavenbuild rm -rf /root/.m2/repository/com/yunhai/platform/yh-auth-spring-boot-starter + docker exec mavenbuild sh -c 'cd /build && mvn -U -B -DskipTests clean package' + mkdir -p "${PWD}/target" + docker cp mavenbuild:/build/target/userbase.jar "${PWD}/target/userbase.jar" + docker rm -f mavenbuild + + - name: Login to Nexus docker registry + run: echo "${{ secrets.NEXUS_PASSWORD }}" | docker login nexus:8083 -u ${{ secrets.NEXUS_USER }} --password-stdin + + - name: Build image + run: docker build -t nexus:8083/docker-hosted/userbase-service:${{ steps.meta.outputs.tag }} -f docker/Dockerfile . + + - name: Push image + run: docker push nexus:8083/docker-hosted/userbase-service:${{ steps.meta.outputs.tag }} + + - name: Report build + run: echo "pushed nexus:8083/docker-hosted/userbase-service:${{ steps.meta.outputs.tag }}" + + # —— 部署阶段(仅在 push 或手动 dispatch deploy=auto 时执行)—— + # 运行在 160 的 act_runner 上,复用宿主 docker daemon(docker.sock 由 act_runner 自动挂载), + # 因此 docker pull / run 直接作用于 160 宿主,等价于在 160 上执行部署。 + - name: Deploy (pull + rollout with rollback) + if: github.event_name == 'push' || inputs.deploy == 'auto' + run: | + set +e + IMG=nexus:8083/docker-hosted/userbase-service:${{ steps.meta.outputs.tag }} + echo "deploying $IMG" + echo "${{ secrets.NEXUS_PASSWORD }}" | docker login nexus:8083 -u ${{ secrets.NEXUS_USER }} --password-stdin + docker pull "$IMG" + # 回滚锚:保留当前 userbase-service:latest(已知良好镜像),再额外打一个时间点标签 + RB=userbase-service:rb-$(date +%s) + docker tag userbase-service:latest "$RB" 2>/dev/null || true + docker stop userbase-service 2>/dev/null; docker rm -f userbase-service 2>/dev/null + # bootstrap.yml 默认 nacos.config.enabled=false(本地不连);CI 必须注入 NACOS_CONFIG_ENABLED=true 才会拉配置 + # SPRING_PROFILES_ACTIVE=prod 激活生产 profile(留给将来拆 application-prod.yml 用) + # namespace/凭据仍用顶层 SPRING_CLOUD_NACOS_* env(bootstrap 的 ${...} 占位符才能解析到) + docker run -d --name userbase-service --restart unless-stopped --network infra-infra-net -p 8060:8060 \ + --add-host sso.yunhaixingtong.com:192.168.10.160 \ + --memory=1.5g --memory-swap=1.5g \ + -v /data/logs/userbase:/app/logs \ + -e JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m" \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e NACOS_CONFIG_ENABLED=true \ + -e NACOS_ADDR=nacos:8848 \ + -e SPRING_CLOUD_NACOS_CONFIG_NAMESPACE=6196f0de-a28d-4894-8f10-e2982b57c6bc \ + -e SPRING_CLOUD_NACOS_USERNAME=yh-reader \ + -e SPRING_CLOUD_NACOS_PASSWORD=123456 \ + -e REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} \ + -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \ + -e RABBITMQ_PASSWORD=${{ secrets.RABBITMQ_PASSWORD }} \ + "$IMG" + # 健康检查:Spring 起来后 /userbase/ 返回 404(根路径无映射控制器),000 表示未起来 + OK=0 + for i in $(seq 1 30); do + sleep 5 + CODE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8060/userbase/ 2>/dev/null || true) + if [ "$CODE" != "000" ]; then OK=1; break; fi + done + if [ "$OK" = "0" ]; then + echo "!! UNHEALTHY -> rollback to userbase-service:latest" + docker stop userbase-service 2>/dev/null; docker rm -f userbase-service 2>/dev/null + # 回滚也必须注入同样的 env,否则 bootstrap 默认 enabled=false 会连不上 nacos 拉不到线上配置 + docker run -d --name userbase-service --restart unless-stopped --network infra-infra-net -p 8060:8060 \ + --add-host sso.yunhaixingtong.com:192.168.10.160 \ + --memory=1.5g --memory-swap=1.5g \ + -v /data/logs/userbase:/app/logs \ + -e JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m" \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e NACOS_CONFIG_ENABLED=true \ + -e NACOS_ADDR=nacos:8848 \ + -e SPRING_CLOUD_NACOS_CONFIG_NAMESPACE=6196f0de-a28d-4894-8f10-e2982b57c6bc \ + -e SPRING_CLOUD_NACOS_USERNAME=yh-reader \ + -e SPRING_CLOUD_NACOS_PASSWORD=123456 \ + -e REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} \ + -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \ + -e RABBITMQ_PASSWORD=${{ secrets.RABBITMQ_PASSWORD }} \ + userbase-service:latest + echo "rolled back"; exit 7 + fi + echo "DEPLOY OK: $IMG" + docker tag "$IMG" userbase-service:latest + docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep userbase || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..970d9d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# ---- java build ---- +target/ + +# ---- IDE ---- +.idea/ +*.iml + +# ---- local tooling ---- +.workbuddy/ + +# ---- env secrets (keep *.example) ---- +.env +!.env.example + +# ---- logs ---- +*.log +deploy/*_run*.log +deploy/verify_run*.log +deploy/push_run.log + +# ---- OS ---- +Thumbs.db +.DS_Store + +# ---- scanner artifacts ---- +defender_chk.txt diff --git a/.mvnlog/mvn-gs.xml b/.mvnlog/mvn-gs.xml new file mode 100644 index 0000000..85f34a6 --- /dev/null +++ b/.mvnlog/mvn-gs.xml @@ -0,0 +1,3 @@ + + + diff --git a/.mvnlog/mvn-secure.xml b/.mvnlog/mvn-secure.xml new file mode 100644 index 0000000..4c31414 --- /dev/null +++ b/.mvnlog/mvn-secure.xml @@ -0,0 +1,29 @@ + + + + + + nexus-releases + admin + @4Jo8iDj10uSHvg + + + nexus-snapshots + admin + @4Jo8iDj10uSHvg + + + + + + nexus-public + Nexus Public Group + http://nexus.yunhaixingtong.com/repository/maven-public/ + * + true + + + + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..19225e5 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,20 @@ +# UserBase 后端镜像(基于已构建好的 target/userbase.jar) +# 构建上下文 = service 目录: docker build -f docker/Dockerfile -t . +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/userbase.jar /app/userbase.jar +# 镜像内 application-prod.yml 仅作「nacos 不可达时的兜底默认」; +# 正常启动由 run-app.sh 从 nacos 拉取线上配置(dataId=userbase-prod.yml)后覆盖。 +COPY docker/application-prod.yml /app/config/application-prod.yml +COPY docker/run-app.sh /app/run-app.sh +RUN chmod +x /app/run-app.sh && mkdir -p /data/upload + +EXPOSE 8060 +ENTRYPOINT ["/app/run-app.sh"] diff --git a/docker/application-prod.yml b/docker/application-prod.yml new file mode 100644 index 0000000..80bb6d3 --- /dev/null +++ b/docker/application-prod.yml @@ -0,0 +1,88 @@ +# UserBase 生产配置(线上以 nacos 的 userbase-prod.yml 为准,本文件是 nacos 不可达时的兜底) +server: + port: 8060 + servlet: + # 对外经 caddy 暴露为 http://base.yunhaixingtong.com/userbase + context-path: /userbase + +spring: + mvc: + date-format: yyyy-MM-dd HH:mm:ss + servlet: + multipart: + max-file-size: 50MB + max-request-size: 200MB + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + + redis: + host: redis-server + port: 6379 + database: 0 + # 生产由 nacos application-infra.yaml 注入;此处仅作 nacos 不可达时的兜底, + # 密码不写死,运行时由容器环境变量 REDIS_PASSWORD 注入(值来自 Gitea Secrets,不入镜像) + password: ${REDIS_PASSWORD} + timeout: 2000ms + data: + redis: + repositories: + enabled: false + +# datasource: +# url: jdbc:mysql://mysql8:3306/bsch_userbase?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true +# username: root +# # 生产由 nacos application-infra.yaml 注入;兜底密码运行时由 DB_PASSWORD 环境变量注入(来自 Gitea Secrets) +# password: ${DB_PASSWORD} +# driver-class-name: com.mysql.cj.jdbc.Driver +# min-idle: 10 +# max-active: 30 + + jpa: + database: MySQL + show-sql: false + hibernate: + ddl-auto: update + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate.order_by.default_null_ordering: last + hibernate: + dialect: org.hibernate.dialect.MySQL5InnoDBDialect + open-in-view: true + + rabbitmq: + host: rabbitmq + port: 5672 + username: admin + # 生产由 nacos application-infra.yaml 注入;兜底密码运行时由 RABBITMQ_PASSWORD 环境变量注入(来自 Gitea Secrets) + password: ${RABBITMQ_PASSWORD} + listener: + direct: + auto-startup: false + simple: + auto-startup: false + +# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth) +yh: + sso: + server-url-prefix: http://sso.yunhaixingtong.com/cas/ + client-host-url: http://base.yunhaixingtong.com/userbase + allow-headers: all + allow-origins: all + url-pattern: /api/* + mobi-api-pattern: /mobiapi/* + app-auth: + enabled: true + +ADMIN_TAG_ID: 290 + +# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data +FILE_PATH: /data/upload +FILE_WEB_PATH: http://base.yunhaixingtong.com/userbase/upload/ +UPLOAD_WEB_PATH_HTTP: http://base.yunhaixingtong.com/userbase/upload/file/ + +logging: + level: + root: INFO + file: + path: /app/logs diff --git a/docker/run-app.sh b/docker/run-app.sh new file mode 100644 index 0000000..163abd4 --- /dev/null +++ b/docker/run-app.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# UserBase 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml) +# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到 +# 镜像内烤好的 application-prod.yml 兜底默认。 +set -e + +echo "[run-app] starting UserBase (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..." + +exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/userbase.jar \ + --spring.profiles.active=prod diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c11623 --- /dev/null +++ b/pom.xml @@ -0,0 +1,260 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.yunhai + yunhaibase + 0.0.1 + UserBase + 用户中心 + + 1.8 + 5.8.36 + 3.1.2 + + + + + + + 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-web + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.security + spring-security-crypto + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.8 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-amqp + + + + + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework + spring-context-support + + + + + com.github.ben-manes.caffeine + caffeine + + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + org.redisson + redisson + 3.13.6 + + + + com.alibaba + fastjson + 1.2.80 + + + + com.mysql + mysql-connector-j + runtime + + + + org.projectlombok + lombok + true + + + + + cn.hutool + hutool-core + ${cn.hutool.version} + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + org.apache.commons + commons-lang3 + + + cn.afterturn + easypoi-base + 4.1.0 + + + + + com.yunhai.platform + yh-auth-spring-boot-starter + + + com.yunhai.platform + yh-app-auth-spring-boot-starter + + + + com.yunhai.platform + yh-api-std + + + + + + + + userbase + + + org.apache.maven.plugins + maven-compiler-plugin + + + false + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.yunhai.userbase.UserBaseApplication + + + + + repackage + + + + + + + + + + + nexus-public + bsch/nexus public + http://nexus.yunhaixingtong.com/repository/maven-public/ + true + true + + + + + + ci + + C:/ub-build2/userbase + + + + + diff --git a/scripts/deploy-via-docker.js b/scripts/deploy-via-docker.js new file mode 100644 index 0000000..af308de --- /dev/null +++ b/scripts/deploy-via-docker.js @@ -0,0 +1,268 @@ +#!/usr/bin/env node +// Deploy front/dist/prod to host /data/www/userbase/front using the Docker daemon +// that act_runner already mounts into the job container (/var/run/docker.sock). +// +// Why this script exists: +// act_runner (v0.6.1) ignores both `container.options` extra --volume and +// workflow `container.volumes` in this setup, so the host static dir can't be +// bind-mounted directly into the job container. But the host docker.sock IS +// mounted by default, so we use the Docker HTTP API to: +// 1. tar the built assets straight out of THIS container's filesystem +// (GET /containers/{jobId}/archive?path=...), and +// 2. upload that tar into a throwaway helper container that has +// /data/www/userbase mounted RW (PUT /containers/{helperId}/archive), +// then `mv` the extracted folder so files land at the correct top level. +// +// Diagnostics: every step is collected and written to /data/www/userbase/_debug.txt +// on the HOST via `docker exec` into the helper (the job container itself cannot +// write there). The Gitea logs API is unavailable here, so this file is our only +// reliable window into what happened. +// +// No docker CLI and no network egress required — we speak the Docker HTTP API +// over the unix socket directly with Node's http module. + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const SOCKET = '/var/run/docker.sock'; +const HOST_STATIC = '/data/www/userbase'; +const HELPER_IMAGE = 'node:20'; // already cached on host (used by the job) +const STAGING = HOST_STATIC + '/_staging'; + +// --- low level docker http helpers ------------------------------------------- + +function dockerJson(method, p, bodyObj) { + return new Promise((resolve, reject) => { + const data = bodyObj ? JSON.stringify(bodyObj) : null; + const headers = {}; + if (data) { + headers['Content-Type'] = 'application/json'; + headers['Content-Length'] = Buffer.byteLength(data); + } + const req = http.request( + { socketPath: SOCKET, path: p, method, headers }, + (res) => { + let buf = ''; + res.on('data', (c) => (buf += c)); + res.on('end', () => resolve({ status: res.statusCode, body: buf })); + } + ); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +function getArchive(id, p) { + return new Promise((resolve, reject) => { + const req = http.request( + { + socketPath: SOCKET, + path: `/containers/${id}/archive?path=${encodeURIComponent(p)}`, + method: 'GET', + }, + (res) => resolve(res) + ); + req.on('error', reject); + req.end(); + }); +} + +function putArchive(id, p, tarStream) { + return new Promise((resolve, reject) => { + const req = http.request( + { + socketPath: SOCKET, + path: `/containers/${id}/archive?path=${encodeURIComponent(p)}`, + method: 'PUT', + headers: { 'Content-Type': 'application/x-tar' }, + }, + (res) => { + let buf = ''; + res.on('data', (c) => (buf += c)); + res.on('end', () => resolve({ status: res.statusCode, body: buf })); + } + ); + req.on('error', reject); + tarStream.pipe(req); + }); +} + +// Run a command inside the helper and return its stdout/stderr (Detach:false so +// the call resolves only after the command finishes — reliable for sequencing). +async function runExec(id, cmd) { + const created = await dockerJson('POST', `/containers/${id}/exec`, { + Cmd: ['sh', '-c', cmd], + AttachStdout: true, + AttachStderr: true, + }); + if (created.status >= 400) { + throw new Error('exec create failed: ' + created.status + ' ' + created.body); + } + const execId = JSON.parse(created.body).Id; + const started = await dockerJson('POST', `/exec/${execId}/start`, { + Detach: false, + Tty: false, + }); + return started; // { status, body(stdout+stderr) } +} + +// --- helper container + diagnostics ------------------------------------------ + +function getContainerId() { + const cg = fs.readFileSync('/proc/self/cgroup', 'utf8'); + const m = cg.match(/[0-9a-f]{64}/); + if (!m) { + const h = fs.readFileSync('/etc/hostname', 'utf8').trim(); + return h; + } + return m[0]; +} + +let HELPER_ID = null; +let DEBUG_BUF = []; + +function dbg(line) { + const ts = new Date().toISOString(); + DEBUG_BUF.push(`[${ts}] ${line}`); + console.log(line); // also emit to job log (best-effort) +} + +// Write the accumulated debug buffer to the HOST file via a base64 pipe. Passing +// the payload inline (single-quoted) avoids env-var/newline fragility that broke +// the earlier diagnostics channel. Uses Detach:false so we know it completed. +async function flushDebug() { + if (!HELPER_ID) { + console.error('flushDebug: no helper, cannot write host debug'); + return; + } + const content = DEBUG_BUF.join('\n') + '\n'; + const b64 = Buffer.from(content, 'utf8').toString('base64'); + const cmd = `echo ${b64} | base64 -d > ${HOST_STATIC}/_debug.txt`; + try { + const r = await runExec(HELPER_ID, cmd); + if (r.status >= 400) console.error('flushDebug exec failed:', r.status, r.body); + else dbg('debug written to host (' + content.length + ' bytes)'); + } catch (e) { + console.error('flushDebug error:', e.message); + } +} + +// --- main -------------------------------------------------------------------- + +function resolveDistPath() { + const candidates = [ + path.join(process.cwd(), 'front', 'dist', 'prod'), + '/workspace/admin/userbase/front/dist/prod', + ]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + return null; +} + +async function main() { + dbg('START script version=2026-08-04c'); + dbg('cwd=' + process.cwd()); + + // 1) helper container with the host static dir mounted RW; keep it alive. + const createBody = { + Image: HELPER_IMAGE, + HostConfig: { NetworkMode: 'none', Binds: [`${HOST_STATIC}:${HOST_STATIC}`] }, + Cmd: ['sh', '-c', `mkdir -p ${STAGING} && exec tail -f /dev/null`], + }; + const created = await dockerJson('POST', '/containers/create', createBody); + if (created.status >= 400) { + throw new Error('create helper failed: ' + created.status + ' ' + created.body); + } + HELPER_ID = JSON.parse(created.body).Id; + dbg('helper created id=' + HELPER_ID); + + const started = await dockerJson('POST', `/containers/${HELPER_ID}/start`); + if (started.status >= 400) { + throw new Error('start helper failed: ' + started.status + ' ' + started.body); + } + dbg('helper started'); + // clean any stale staging from a previous failed run + await runExec(HELPER_ID, `rm -rf ${STAGING} && mkdir -p ${STAGING}`); + + // 2) locate the built assets inside THIS container. + const jobId = getContainerId(); + dbg('job container id=' + jobId); + + const distDir = resolveDistPath(); + if (!distDir) { + dbg('dist dir NOT found; cwd listing:'); + try { + dbg(fs.readdirSync(process.cwd()).join(' ')); + const front = path.join(process.cwd(), 'front'); + if (fs.existsSync(front)) dbg('front listing: ' + fs.readdirSync(front).join(' ')); + } catch (e) { + dbg('ls err: ' + e.message); + } + throw new Error('built dist/prod not found under any candidate path'); + } + dbg('dist dir=' + distDir); + dbg('dist listing: ' + fs.readdirSync(distDir).join(' ')); + + // 3) tar of the built assets, fetched from THIS container's own filesystem. + // We fetch WITHOUT a trailing slash so the tar contains a top-level "prod/" + // folder; we then mv it into place (deterministic, no reliance on the + // trailing-slash "contents only" quirk which this moby build ignores). + const tar = await getArchive(jobId, distDir); + if (tar.statusCode !== 200) { + let msg = ''; + tar.on('data', (c) => (msg += c)); + await new Promise((r) => tar.on('end', r)); + throw new Error('get archive failed (status ' + tar.statusCode + '): ' + msg); + } + dbg('fetched dist tar (content-type=' + tar.headers['content-type'] + ')'); + + // 4) upload the tar into the staging dir of the helper's mounted host dir. + const put = await putArchive(HELPER_ID, STAGING, tar); + dbg('put archive status=' + put.status + (put.status >= 400 ? ' ' + put.body : '')); + if (put.status >= 400) { + throw new Error('put archive failed: ' + put.status + ' ' + put.body); + } + + // 5) move staging/prod -> HOST_STATIC/front (deterministic top-level landing). + const mv = await runExec( + HELPER_ID, + `rm -rf ${HOST_STATIC}/front && mv ${STAGING}/prod ${HOST_STATIC}/front && rm -rf ${STAGING}` + ); + dbg('mv status=' + mv.status + ' out=' + mv.body.replace(/\n/g, ' | ')); + + // 6) verify on host via exec into helper. + const verify = await runExec( + HELPER_ID, + `echo "count=$(find ${HOST_STATIC}/front -type f | wc -l)"; ` + + `echo "has_stray_prod=$(test -d ${HOST_STATIC}/front/prod && echo YES || echo NO)"; ` + + `ls -la ${HOST_STATIC}/front | head -20` + ); + dbg('verify output: ' + verify.body.replace(/\n/g, ' | ')); + + dbg('DEPLOY OK -> ' + HOST_STATIC + '/front (top-level, no prod/ nesting)'); +} + +main() + .then(async () => { + // On success, keep diagnostics only briefly, then remove the debug file so + // it is not left web-served at /userbase/_debug.txt. Failures keep it for + // post-mortem (see .catch below). + await flushDebug(); + try { + if (HELPER_ID) await runExec(HELPER_ID, `rm -f ${HOST_STATIC}/_debug.txt`); + } catch (e) { + console.error('cleanup debug file failed:', e.message); + } + if (HELPER_ID) await dockerJson('DELETE', `/containers/${HELPER_ID}?force=true`); + process.exit(0); + }) + .catch(async (err) => { + dbg('DEPLOY FAILED: ' + err.stack); + await flushDebug(); // keep debug file on failure for post-mortem + if (HELPER_ID) await dockerJson('DELETE', `/containers/${HELPER_ID}?force=true`); + console.error('DEPLOY FAILED:', err.message); + process.exit(1); + }); diff --git a/src/main/java/com/yunhai/userbase/UserBaseApplication.java b/src/main/java/com/yunhai/userbase/UserBaseApplication.java new file mode 100644 index 0000000..34cc402 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/UserBaseApplication.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase; + +import com.alibaba.fastjson.parser.ParserConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +@EnableAsync +@EnableScheduling +public class UserBaseApplication { + + public static void main(String[] args) { + // 开启安全模式 + ParserConfig.getGlobalInstance().setSafeMode(true); + SpringApplication.run(UserBaseApplication.class, args); + } + +} diff --git a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java new file mode 100644 index 0000000..c204f7b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java @@ -0,0 +1,279 @@ +package com.yunhai.userbase.aspect; + +import com.yunhai.platform.apistd.ApiResponse; +import com.yunhai.platform.apistd.BizException; +import com.yunhai.platform.appauth.AppAuthFilter; +import com.yunhai.platform.utils.exception.DException; +import com.yunhai.userbase.common.annotation.OapiLog; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.entity.SysOapiLog; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import com.yunhai.userbase.service.ISysOapiLogService; +import com.yunhai.userbase.utils.GzipUtil; +import com.yunhai.userbase.utils.IpUtils; +import com.yunhai.userbase.utils.OperationLogResolver; +import com.yunhai.userbase.utils.SensitiveDataMasker; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * 开放接口(/oapi/**)调用日志切面。 + * 拦截 {@code controller.oapi} 包下所有方法,规则: + * - POST 写操作:全量自动记录(无需注解),级别/类型由 OperationLogResolver 关键词判定; + * - GET 查询:默认不记录(高频调用,防刷爆表),满足任一条件则记录:方法标注 {@code @OapiLog}、或配置 oapi.log-get=true; + * - 排除 {@code SignKeyInternalController}(/oapi/internal,响应含 appSecret 明文,绝不落库); + * - 请求参数与响应均经 {@link SensitiveDataMasker} 脱敏后入库; + * - finally 中异步落库(有界池 oapiLogExecutor),失败不影响主业务。 + * + * @author chenx + * @date 2026/8/20 + */ +@Slf4j +@Aspect +@Component +public class OapiLogAspect { + + /** 内部密钥端点控制器,排除(响应含 appSecret 明文)。 */ + private static final String INTERNAL_CONTROLLER = "com.yunhai.userbase.controller.oapi.SignKeyInternalController"; + + /** GET 默认不记录,开启本配置后全量记录。 */ + @Value("${oapi.log-get:false}") + private boolean logGet; + + @Autowired + private ISysOapiLogService sysOapiLogService; + + @Autowired + private ApplicationInfoRepository applicationInfoRepository; + + @Around("execution(* com.yunhai.userbase.controller.oapi..*.*(..))") + public Object around(ProceedingJoinPoint point) throws Throwable { + if (INTERNAL_CONTROLLER.equals(point.getSignature().getDeclaringTypeName())) { + return point.proceed(); + } + + HttpServletRequest request = getRequest(); + String httpMethod = request != null ? request.getMethod() : ""; + boolean isGet = "GET".equalsIgnoreCase(httpMethod); + + OapiLog oapiLog = findAnnotation(point); + // 决策:POST 全记;GET 仅当注解强制或配置开启才记 + if (isGet && oapiLog == null && !logGet) { + return point.proceed(); + } + + long start = System.currentTimeMillis(); + SysOapiLog entity = buildEntity(point, request, httpMethod, oapiLog); + + boolean saveResponse = oapiLog != null ? oapiLog.saveResponse() : true; + int maxResponseSize = oapiLog != null ? oapiLog.maxResponseSize() : 10240; + + Object result = null; + try { + result = point.proceed(); + entity.setStatusCode(200); + fillResponse(entity, result, saveResponse, maxResponseSize); + } catch (Throwable e) { + entity.setStatusCode(500); + entity.setErrorMsg(truncate(e.getMessage(), 2000)); + // 业务异常(DException/BizException)同样构造实际返回的响应体落库,便于详情下载 + fillErrorResponse(entity, e, saveResponse, maxResponseSize); + throw e; + } finally { + entity.setCostTime(System.currentTimeMillis() - start); + sysOapiLogService.saveAsync(entity); + } + return result; + } + + private OapiLog findAnnotation(ProceedingJoinPoint point) { + try { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + return method.getAnnotation(OapiLog.class); + } catch (Exception e) { + return null; + } + } + + private SysOapiLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request, + String httpMethod, OapiLog oapiLog) { + SysOapiLog entity = new SysOapiLog(); + + // 调用方应用:验签 attribute → appKey → application_info 解析应用名(失败记 appKey 原值,不阻塞) + resolveCallerApp(request, entity); + + String requestUri = ""; + if (request != null) { + entity.setRequestIp(IpUtils.getIpAddr(request)); + requestUri = request.getRequestURI(); + entity.setUserAgent(request.getHeader("User-Agent")); + entity.setRequestParams(maskRequestParams(point, request)); + } else { + entity.setRequestParams(maskRequestParams(point, null)); + } + entity.setRequestUri(requestUri); + entity.setRequestMethod(httpMethod); + + String methodName = point.getSignature().getName(); + entity.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, requestUri, methodName).getCode()); + entity.setOperationType(OperationLogResolver.resolveOperationType(httpMethod, requestUri, methodName)); + entity.setClassMethod(point.getTarget().getClass().getName() + "." + methodName); + if (oapiLog != null && StringUtils.isNotBlank(oapiLog.value())) { + entity.setOperationDesc(oapiLog.value()); + } + return entity; + } + + /** + * 从 request attribute 解析调用方 appKey,并映射应用名。 + * 无 attribute / 解析失败:appKey 记 0、应用名记 unknown,仅 warn,不抛异常。 + */ + private void resolveCallerApp(HttpServletRequest request, SysOapiLog entity) { + entity.setCallerAppKey(0); + entity.setCallerAppName("unknown"); + if (request == null) { + return; + } + Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR); + if (attr == null) { + log.warn("[oapi-log] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}", + request.getRequestURI()); + return; + } + String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr); + if (StringUtils.isBlank(appKeyStr)) { + log.warn("[oapi-log] AppAuth attr is blank, uri={}", request.getRequestURI()); + return; + } + Integer appKey; + try { + appKey = Integer.parseInt(appKeyStr.trim()); + } catch (NumberFormatException e) { + log.warn("[oapi-log] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI()); + return; + } + entity.setCallerAppKey(appKey); + ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey); + entity.setCallerAppName(app != null && StringUtils.isNotBlank(app.getName()) + ? app.getName() : String.valueOf(appKey)); + } + + /** + * 构建请求参数:GET 优先 queryString;其余序列化方法入参(过滤 Servlet 三大件),统一过脱敏。 + */ + private String maskRequestParams(ProceedingJoinPoint point, HttpServletRequest request) { + try { + if (request != null && "GET".equalsIgnoreCase(request.getMethod())) { + String qs = request.getQueryString(); + if (StringUtils.isNotBlank(qs)) { + return SensitiveDataMasker.maskJson(qs); + } + } + Object[] args = point.getArgs(); + if (args == null || args.length == 0) { + return ""; + } + List filtered = new ArrayList(); + for (Object arg : args) { + if (arg == null) { + continue; + } + if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse + || arg instanceof HttpSession) { + continue; + } + filtered.add(arg); + } + if (filtered.isEmpty()) { + return ""; + } + return SensitiveDataMasker.maskObject(filtered); + } catch (Exception e) { + return ""; + } + } + + /** + * 写入响应结果(脱敏 → 截断 → GZIP)。 + */ + private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse, int maxResponseSize) { + if (!saveResponse || result == null) { + return; + } + writeResponseData(entity, result, maxResponseSize); + } + + /** + * 异常路径:与 {@code GlobalExceptionHandler} 保持一致的响应体(DException/BizException → ApiResponse), + * 使日志下载内容等于调用方实际收到的 body;其余异常不构造(响应不可预期)。 + */ + private void fillErrorResponse(SysOapiLog entity, Throwable e, boolean saveResponse, int maxResponseSize) { + if (!saveResponse) { + return; + } + ApiResponse> response = toApiResponse(e); + if (response == null) { + return; + } + writeResponseData(entity, response, maxResponseSize); + } + + private ApiResponse> toApiResponse(Throwable e) { + if (e instanceof DException) { + DException de = (DException) e; + return ApiResponse.fail(de.getCode(), de.getMessage(), de.getData()); + } + if (e instanceof BizException) { + BizException be = (BizException) e; + return ApiResponse.fail(be.getErrorCode(), be.getMessage()); + } + return null; + } + + private void writeResponseData(SysOapiLog entity, Object payload, int maxResponseSize) { + if (payload == null) { + return; + } + try { + String resp = SensitiveDataMasker.maskObject(payload); + if (resp == null) { + return; + } + if (resp.length() > maxResponseSize) { + resp = resp.substring(0, maxResponseSize) + "...[截断]"; + } + entity.setResponseData(GzipUtil.compress(resp)); + } catch (Exception e) { + log.warn("开放接口日志响应序列化失败, uri={}", entity.getRequestUri(), e); + } + } + + private HttpServletRequest getRequest() { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attrs != null ? attrs.getRequest() : null; + } + + private String truncate(String text, int maxLen) { + if (text == null) { + return null; + } + return text.length() > maxLen ? text.substring(0, maxLen) : text; + } +} diff --git a/src/main/java/com/yunhai/userbase/aspect/OperationLogAspect.java b/src/main/java/com/yunhai/userbase/aspect/OperationLogAspect.java new file mode 100644 index 0000000..99afc9b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/aspect/OperationLogAspect.java @@ -0,0 +1,205 @@ +package com.yunhai.userbase.aspect; + +import com.alibaba.fastjson.JSON; +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.context.UserContext; +import com.yunhai.userbase.entity.SysOperationLog; +import com.yunhai.userbase.service.ISysOperationLogService; +import com.yunhai.userbase.utils.GzipUtil; +import com.yunhai.userbase.utils.IpUtils; +import com.yunhai.userbase.utils.OperationLogResolver; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * 操作日志切面。 + * 拦截所有 Controller 方法,按如下规则自动采集并落库: + * - GET 请求:自动记录为「一般(查询)」,无需任何注解; + * - 非 GET 请求:仅当方法标注 @OperationLog 时才记录(沿用原注解式行为); + * - 排除日志查看接口自身(SysOperationLogController),避免递归与刷屏。 + * 响应结果 GZIP 压缩 + 截断后,在 finally 中异步落库,失败不影响主业务。 + * + * @author chenx + * @date 2026/7/28 + */ +@Slf4j +@Aspect +@Component +public class OperationLogAspect { + + /** 不记录日志查询/下载接口自身,避免递归与刷屏。 */ + private static final String LOG_CONTROLLER = "com.yunhai.userbase.controller.api.SysOperationLogController"; + + /** GET 自动记录时使用的默认响应保存上限(10KB)。 */ + private static final int DEFAULT_MAX_RESPONSE_SIZE = 10240; + + @Autowired + private ISysOperationLogService sysOperationLogService; + + @Around("execution(* com.yunhai.userbase.controller..*.*(..))" + + " && !execution(* com.yunhai.userbase.controller.oapi..*.*(..))") + public Object around(ProceedingJoinPoint point) throws Throwable { + // 排除日志查看接口自身 + if (LOG_CONTROLLER.equals(point.getSignature().getDeclaringTypeName())) { + return point.proceed(); + } + + HttpServletRequest request = getRequest(); + String httpMethod = request != null ? request.getMethod() : ""; + boolean isGet = "GET".equalsIgnoreCase(httpMethod); + + // 解析方法上的 @OperationLog 注解(可能为 null) + OperationLog operationLog = findAnnotation(point); + + // 决策:GET 自动记录;非 GET 仅在有注解时记录 + if (!isGet && operationLog == null) { + return point.proceed(); + } + + long start = System.currentTimeMillis(); + SysOperationLog entity = buildEntity(point, request, httpMethod); + + // GET 未标注注解时使用默认参数;标注了则优先用注解配置 + boolean saveResponse = operationLog != null ? operationLog.saveResponse() : true; + int maxResponseSize = operationLog != null ? operationLog.maxResponseSize() : DEFAULT_MAX_RESPONSE_SIZE; + + Object result = null; + try { + result = point.proceed(); + entity.setStatusCode(200); + fillResponse(entity, result, saveResponse, maxResponseSize); + } catch (Throwable e) { + entity.setStatusCode(500); + entity.setErrorMsg(truncate(e.getMessage(), 2000)); + throw e; + } finally { + entity.setCostTime(System.currentTimeMillis() - start); + sysOperationLogService.saveAsync(entity); + } + return result; + } + + /** + * 从连接点解析方法上的 @OperationLog 注解。 + */ + private OperationLog findAnnotation(ProceedingJoinPoint point) { + try { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + return method.getAnnotation(OperationLog.class); + } catch (Exception e) { + return null; + } + } + + /** + * 组装日志实体(基础字段 + 级别/类型判定)。 + */ + private SysOperationLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request, String httpMethod) { + SysOperationLog entity = new SysOperationLog(); + + Integer userId = UserContext.getCurrentUser(); + entity.setUserId(userId != null ? userId.longValue() : 0L); + String username = request != null ? request.getRemoteUser() : null; + entity.setUsername(StringUtils.isNotBlank(username) ? username : "anonymous"); + + String requestUri = ""; + if (request != null) { + entity.setRequestIp(IpUtils.getIpAddr(request)); + requestUri = request.getRequestURI(); + entity.setUserAgent(request.getHeader("User-Agent")); + entity.setRequestParams(buildRequestParams(point, request)); + } else { + entity.setRequestParams(buildRequestParams(point, null)); + } + entity.setRequestUri(requestUri); + entity.setRequestMethod(httpMethod); + + String methodName = point.getSignature().getName(); + entity.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, requestUri, methodName).getCode()); + entity.setOperationType(OperationLogResolver.resolveOperationType(httpMethod, requestUri, methodName)); + entity.setClassMethod(point.getTarget().getClass().getName() + "." + methodName); + return entity; + } + + /** + * 构建请求参数。 + * GET 优先使用 queryString;其余序列化方法入参,过滤掉请求/响应等不可序列化对象。 + */ + private String buildRequestParams(ProceedingJoinPoint point, HttpServletRequest request) { + try { + if (request != null && "GET".equalsIgnoreCase(request.getMethod())) { + String qs = request.getQueryString(); + if (StringUtils.isNotBlank(qs)) { + return qs; + } + } + Object[] args = point.getArgs(); + if (args == null || args.length == 0) { + return ""; + } + List filtered = new ArrayList(); + for (Object arg : args) { + if (arg == null) { + continue; + } + if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse + || arg instanceof HttpSession) { + continue; + } + filtered.add(arg); + } + if (filtered.isEmpty()) { + return ""; + } + return JSON.toJSONString(filtered); + } catch (Exception e) { + return ""; + } + } + + /** + * 写入响应结果(GZIP 压缩 + 截断)。 + */ + private void fillResponse(SysOperationLog entity, Object result, boolean saveResponse, int maxResponseSize) { + if (!saveResponse || result == null) { + return; + } + try { + String resp = JSON.toJSONString(result); + if (resp.length() > maxResponseSize) { + resp = resp.substring(0, maxResponseSize) + "...[截断]"; + } + entity.setResponseData(GzipUtil.compress(resp)); + } catch (Exception e) { + log.warn("操作日志响应结果序列化失败, uri={}", entity.getRequestUri(), e); + } + } + + private HttpServletRequest getRequest() { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attrs != null ? attrs.getRequest() : null; + } + + private String truncate(String text, int maxLen) { + if (text == null) { + return null; + } + return text.length() > maxLen ? text.substring(0, maxLen) : text; + } +} diff --git a/src/main/java/com/yunhai/userbase/common/GlobalExceptionHandler.java b/src/main/java/com/yunhai/userbase/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..2e740f9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/GlobalExceptionHandler.java @@ -0,0 +1,86 @@ +package com.yunhai.userbase.common; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 全局异常处理器:仅处理「参数校验」类异常,把 Spring 默认抛出的冗长消息收敛成「一句话」返回, + * 避免前端收到整段 Validation failed ... 堆栈文本。 + * 返回 HTTP 200 + {@code {code:4, msg:..., data:null}} 结构(而非 HTTP 400), + * 与前端 alova errorHandle 的约定一致:status!=200 会被当作网络错误,code=4 才走「参数错误」分支。 + * 注意:本处理器【不】处理业务异常(如 {@code DException}),业务异常交由项目框架统一处理 + * (返回 code=5、msg=异常消息),此处若拦截会覆盖框架行为。 + * + * @author chenx + */ +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class GlobalExceptionHandler { + + /** 参数校验失败的业务码,前端按「参数错误」处理 */ + private static final int CODE_PARAM_ERROR = 4; + + /** @RequestBody + @Validated/@Valid 失败:提取每个字段的 defaultMessage。 */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Map handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + return paramError(fieldErrorsMessage(ex.getBindingResult().getFieldErrors())); + } + + /** 普通 Bean 参数(如 SortChildren/Move 的 @Validated 请求体对象)校验失败。 */ + @ExceptionHandler(BindException.class) + public Map handleBind(BindException ex) { + return paramError(fieldErrorsMessage(ex.getBindingResult().getFieldErrors())); + } + + /** 方法级约束(@PathVariable/@RequestParam 上的 @NotNull 等)校验失败。 */ + @ExceptionHandler(ConstraintViolationException.class) + public Map handleConstraintViolation(ConstraintViolationException ex) { + String msg = ex.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(";")); + return paramError(msg); + } + + /** 缺少必需的请求参数(@RequestParam 必填项未传,如 oapi 调用方漏传 groupId)。 */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public Map handleMissingParam(MissingServletRequestParameterException ex) { + return paramError("缺少必需的请求参数: " + ex.getParameterName()); + } + + /** 请求参数类型不匹配(如 Integer 参数收到非数字字符串)。 */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public Map handleTypeMismatch(MethodArgumentTypeMismatchException ex) { + return paramError("参数类型不匹配: " + ex.getName()); + } + + private String fieldErrorsMessage(java.util.List fieldErrors) { + if (fieldErrors == null || fieldErrors.isEmpty()) { + return "参数校验失败"; + } + return fieldErrors.stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(";")); + } + + /** HTTP 200 + code/msg/data 结构体,前端根据 code 提示 msg */ + private Map paramError(String msg) { + Map body = new LinkedHashMap<>(); + body.put("code", CODE_PARAM_ERROR); + body.put("msg", msg); + body.put("data", null); + return body; + } +} diff --git a/src/main/java/com/yunhai/userbase/common/annotation/OapiLog.java b/src/main/java/com/yunhai/userbase/common/annotation/OapiLog.java new file mode 100644 index 0000000..f34290c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/annotation/OapiLog.java @@ -0,0 +1,40 @@ +package com.yunhai.userbase.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 开放接口日志注解。 + * 标注在 {@code controller.oapi} 方法上: + * + * POST 写操作无需标注,OapiLogAspect 自动全量记录; + * GET 默认不记录(高频查询),标注本注解可强制记录(或开启配置 oapi.log-get=true); + * value 提供中文操作描述,落入 operation_desc 字段。 + * + * + * @author chenx + * @date 2026/8/20 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OapiLog { + + /** + * 操作描述,可选。 + */ + String value() default ""; + + /** + * 是否保存响应结果(GZIP 压缩后入库)。 + */ + boolean saveResponse() default true; + + /** + * 响应结果序列化后的最大字符数,超过则截断。默认 10KB。 + */ + int maxResponseSize() default 10240; +} diff --git a/src/main/java/com/yunhai/userbase/common/annotation/OperationLog.java b/src/main/java/com/yunhai/userbase/common/annotation/OperationLog.java new file mode 100644 index 0000000..db7b8d4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/annotation/OperationLog.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 操作日志注解。 + * 标注在 Controller 方法上,AOP 拦截后自动记录操作日志并异步落库。 + * + * @author chenx + * @date 2026/7/28 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLog { + + /** + * 操作描述,可选。 + */ + String value() default ""; + + /** + * 是否保存响应结果(GZIP 压缩后入库)。 + */ + boolean saveResponse() default true; + + /** + * 响应结果序列化后的最大字符数,超过则截断。默认 10KB。 + */ + int maxResponseSize() default 10240; +} diff --git a/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java b/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java new file mode 100644 index 0000000..0450bf8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.common.context; + +/** + * 开放接口(oapi)调用方来源上下文:在 oapi 请求链路中保存"触发本次数据变更的订阅方 appKey", + * 供业务层发布 {@code DataSyncEvent} 时写入事件来源,推送分发器据此回推时排除来源应用自身 + * (避免应用 A 自己调 oapi 写接口后又收到自己的回声)。 + * 生命周期:由 {@code OapiCallerAppInterceptor} 在 {@code preHandle} 写入、 + * {@code afterCompletion} 强制清除(防线程池复用泄漏)。管理后台 /api/* 写接口不走 oapi 拦截器, + * 此处为 null,表示"来源非订阅方",推送不对其做来源排除(全量投递)。 + * 设计与 {@link UserContext} 同套路(ThreadLocal + remove)。 + */ +public class CallerAppContext { + + private static final ThreadLocal CALLER_APP_KEY = new ThreadLocal<>(); + + public static void setAppKey(Integer appKey) { + CALLER_APP_KEY.set(appKey); + } + + public static Integer getAppKey() { + return CALLER_APP_KEY.get(); + } + + public static void clear() { + CALLER_APP_KEY.remove(); + } +} diff --git a/src/main/java/com/yunhai/userbase/common/context/UserContext.java b/src/main/java/com/yunhai/userbase/common/context/UserContext.java new file mode 100644 index 0000000..1cca890 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/context/UserContext.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.common.context; + +public class UserContext { + + private static final ThreadLocal CURRENT_USER = new ThreadLocal<>(); + + public static void setCurrentUser(Integer userId) { + CURRENT_USER.set(userId); + } + + public static Integer getCurrentUser() { + return CURRENT_USER.get(); + } + + public static void clear() { + CURRENT_USER.remove(); + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/common/converter/ClientEnumConverter.java b/src/main/java/com/yunhai/userbase/common/converter/ClientEnumConverter.java new file mode 100644 index 0000000..1899280 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/converter/ClientEnumConverter.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.common.converter; + +import com.yunhai.platform.utils.enums.ClientEnum; + +import javax.persistence.AttributeConverter; + +/** + * ClientEnum 专用 JPA 转换器(int <-> 枚举)。 + * 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。 + * + * @author chenx + */ +public class ClientEnumConverter extends CommonIntEnumConverter { + + public ClientEnumConverter() { + super(ClientEnum.class); + } +} diff --git a/src/main/java/com/yunhai/userbase/common/converter/CommonIntEnumConverter.java b/src/main/java/com/yunhai/userbase/common/converter/CommonIntEnumConverter.java new file mode 100644 index 0000000..47be82a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/common/converter/CommonIntEnumConverter.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.common.converter; + +import com.yunhai.platform.utils.enums.IEnum; + +import javax.persistence.AttributeConverter; + +/** + * 通用「int <-> 实现 IEnum<Integer> 的枚举」JPA 转换器抽象基类。 + * + * 绑定机制(通用做法):为每个枚举写一个极薄的子类,在构造器里传入枚举 class 即可完成绑定, + * 然后实体字段用 {@code @Convert(converter = XxxEnumConverter.class)} 引用该子类。例如: + * + * public class GroupTypeEnumConverter extends CommonIntEnumConverter<GroupTypeEnum> { + * public GroupTypeEnumConverter() { super(GroupTypeEnum.class); } + * } + * + * 这样同一套逻辑可服务任意枚举;又因为每个枚举拥有独立的转换器实例, + * 彼此之间不会串类型(标准 JPA 的 @Convert 是按 converter 类创建共享单例的, + * 若让多个不同枚举共用同一个泛型实例,则无法区分各自的枚举 class,故采用「每枚举一子类」的绑定方式)。 + *
+ * 绑定机制(通用做法):为每个枚举写一个极薄的子类,在构造器里传入枚举 class 即可完成绑定, + * 然后实体字段用 {@code @Convert(converter = XxxEnumConverter.class)} 引用该子类。例如: + *
+ * public class GroupTypeEnumConverter extends CommonIntEnumConverter<GroupTypeEnum> { + * public GroupTypeEnumConverter() { super(GroupTypeEnum.class); } + * } + *
Spring Boot 的 {@code spring.jackson.date-format} 只作用于 {@link java.util.Date}, + * 对 {@code LocalDateTime}/{@code LocalDate} 无效;且 {@code spring.jackson.local-date-time-format} + * 并非 {@code JacksonProperties} 的合法绑定项(会被静默忽略)。本配置把 Nacos 中已配置的 + * {@code spring.jackson.date-format} 同时应用到 JSR-310 时间类型,做到全局统一。 + * + *
仅用 {@code serializerByType}/{@code deserializerByType} 覆盖 JavaTimeModule 默认序列化器, + * 不 new 整个 ObjectMapper,因此不会禁用 Spring Boot 的 Jackson 自动配置; + * 字段上的 {@code @JsonFormat} 优先级更高,不受影响。 + */ +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer( + @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}") String dateFormat) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return (Jackson2ObjectMapperBuilder builder) -> { + builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter)); + builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter)); + builder.serializerByType(LocalDate.class, new LocalDateSerializer(dateFormatter)); + builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(dateFormatter)); + }; + } +} diff --git a/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java b/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java new file mode 100644 index 0000000..c439d9d --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MyWebAppConfigurer implements WebMvcConfigurer { + + @Value("${FILE_PATH}") + public String FILE_PATH; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + String IMG_FILE_WEB_PATH = "/upload/image/"; + String File_FILE_WEB_PATH = "/upload/file/"; + String ICON_FILE_WEB_PATH = "/upload/icon/"; + String WORD_FILE_WEB_PATH = "/upload/word/"; + String HTML_FILE_WEB_PATH = "/upload/html/"; + registry.addResourceHandler(IMG_FILE_WEB_PATH + "**").addResourceLocations("file:" + FILE_PATH + "/image/"); + registry.addResourceHandler(File_FILE_WEB_PATH + "**").addResourceLocations("file:" + FILE_PATH + "/file/"); + registry.addResourceHandler(ICON_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/icon/"); + registry.addResourceHandler(WORD_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/word/"); + registry.addResourceHandler(HTML_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/html/"); + + } + +} diff --git a/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java b/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java new file mode 100644 index 0000000..ed6f41b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.AppAuthFilter; +import com.yunhai.userbase.common.context.CallerAppContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * oapi 调用方来源注入拦截器:在 oapi 请求处理前,从 yh-app-auth 验签过滤器写入的 + * {@link AppAuthFilter#APP_AUTH_ATTR}(调用方 appKey)解析出整型 appKey, + * 存入 {@link CallerAppContext};请求结束(无论成功/异常)在 {@code afterCompletion} 强制清除, + * 防止线程池复用导致 appKey 串号、进而误把其他订阅方排除。 + * 配合 {@code PushDispatcher} 的来源排除逻辑,实现"应用自己触发的变更不回推给自己"。 + * 管理后台 /api/* 不走此拦截器 → CallerAppContext 为 null → 推送对该来源不做排除(全量投递)。 + */ +@Component +public class OapiCallerAppInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) { + Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR); + if (attr == null) { + return true; + } + String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr); + if (StringUtils.isBlank(appKeyStr)) { + return true; + } + try { + CallerAppContext.setAppKey(Integer.valueOf(appKeyStr.trim())); + } catch (NumberFormatException ignored) { + // 非法 appKey 格式:不设置来源,等同"非订阅方来源",推送全量投递 + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, Exception ex) { + CallerAppContext.clear(); + } +} diff --git a/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java b/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java new file mode 100644 index 0000000..2779fac --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/OpenApiAuthEnforceConfiguration.java @@ -0,0 +1,93 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.AppAuthFilter; +import com.yunhai.platform.appauth.AppAuthProperties; +import com.yunhai.platform.auth.UserDetailsProvider; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import com.yunhai.userbase.service.impl.UserCenterService; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 强制开放接口必须带应用签名 + UserBase 自身 CAS 认证兜底的 UserDetailsProvider。 + * + * yh-app-auth 的 {@link AppAuthFilter} 设计为「无签名头则放行」(以兼容浏览器 CAS 流), + * 但 UserBase 作为用户中心的开放接口({/open/api/**})与内部密钥兜底端点({/oapi/internal/**}) + * 绝不允许匿名访问。本过滤器在 {@code AppAuthFilter} 之后运行,校验其写入的请求属性: + * 已验签(属性存在)则放行,否则返回 401。 + * 顺序须晚于 {@code AppAuthFilter}(HIGHEST_PRECEDENCE+10),故置为 +11。 + * + * + * 额外注册 {@code UserDetailsProvider}(@Bean @Primary,名=userDetailsProvider): + * 覆盖 yh-auth-spring-boot-starter 新版默认的 NoOpUserDetailsProvider(调用直接抛 + * UnsupportedOperationException「未配置 UserDetailsProvider」)。必须「显式 @Bean + * + @Primary」 + * 而不能只靠 @Component 扫描:starter 新版 AutoConfiguration 运行顺序可能早于 UserBase 组件扫描, + * 导致其 @ConditionalOnMissingBean 抢先注册 NoOp,PathBasedAuthInterceptor 注入后首次请求即抛错。 + */ +@Configuration +@EnableConfigurationProperties({ SignKeyInternalProperties.class, AppAuthProperties.class }) +public class OpenApiAuthEnforceConfiguration { + + @Bean + public FilterRegistrationBean requireAppSignFilter() { + FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new RequireAppSignFilter()); + bean.addUrlPatterns("/open/api/*", "/oapi/*"); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 11); + return bean; + } + + /** + * 内部开放接口(/oapi/internal/*)来源 IP 白名单: + * 在验签过滤器之前先执行(HIGHEST_PRECEDENCE + 9),先卡掉公网来源再进行后续处理。 + * 配置项 yh.sign-key-internal.ips / enabled 可由 Nacos 下发。 + */ + @Bean + public FilterRegistrationBean signKeyInternalIpFilter( + SignKeyInternalProperties properties) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new SignKeyInternalIpFilter(properties)); + bean.addUrlPatterns("/oapi/internal/*"); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 9); + return bean; + } + + /** + * UserBase 本地 CAS 登录用户提供者:按登录名/手机号查 UserBase 自身库返回 BoaUser。 + * 加 {@code @Primary} 稳定覆盖 yh-auth 默认 NoOp 实现;bean 名固定=userDetailsProvider, + * 与 starter 默认同名,即使开启 allow-bean-definition-overriding=false 也能优先匹配。 + */ + @Bean(name = "userDetailsProvider") + @Primary + public UserDetailsProvider userDetailsProvider(UserCenterService userCenterService) { + return new UserBaseUserDetailsProvider(userCenterService); + } + + public static class RequireAppSignFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + if (req.getAttribute(AppAuthFilter.APP_AUTH_ATTR) == null) { + res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "missing app signature"); + return; + } + chain.doFilter(request, response); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/PasswordEncoderConfig.java b/src/main/java/com/yunhai/userbase/config/PasswordEncoderConfig.java new file mode 100644 index 0000000..83ca123 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/PasswordEncoderConfig.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * 密码编码器配置:统一使用 BCrypt。 + * 用户中心存库与后续 CAS 登录校验共用同一编码器,保证加密/校验规则一致。 + * + * @author chenx + * @date 2026/7/23 + * @description BCrypt 密码编码器 Bean + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * BCrypt 密码编码器。 + * strength 使用默认 10,兼顾安全与性能;CAS 侧用同一 encoder 的 matches 校验即可。 + * + * @return PasswordEncoder + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/yunhai/userbase/config/PushMqConfig.java b/src/main/java/com/yunhai/userbase/config/PushMqConfig.java new file mode 100644 index 0000000..2be5b9a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/PushMqConfig.java @@ -0,0 +1,102 @@ +package com.yunhai.userbase.config; + +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.yunhai.userbase.model.push.PushTaskMessage; + +import java.util.HashMap; +import java.util.Map; + +/** + * 推送订阅 MQ 配置。 + * 拓扑: + * + * push.topic (topic, durable) + * └─ push.main (durable, concurrency=1, x-dead-letter-exchange=push.dlx) ← 串行消费,可观测 + * push.dlx (fanout, durable) + * └─ push.dlq (durable) ← 死信归档(重试耗尽) + * + * 连接信息(host/port/user/pwd/vhost)由 Nacos {@code application-infra.yaml} 的 {@code spring.rabbitmq.*} + * 公共配置注入,本类不写死(也不改 bootstrap.yml / application.yml)。 + * 可靠性:消费端手动 ack,失败由 {@code PushConsumer} 应用层自管重试(共 3 次),耗尽进死信队列。 + * + * @author chenx + * @date 2026/8/14 + */ +@Configuration +public class PushMqConfig { + + public static final String EXCHANGE_TOPIC = "push.topic"; + public static final String EXCHANGE_DLX = "push.dlx"; + public static final String QUEUE_MAIN = "push.main"; + public static final String QUEUE_DLQ = "push.dlq"; + + /** 统一 JSON 消息转换器(固定反序列化为 PushTaskMessage,绕过 trusted-packages 版本差异)。 */ + @Bean + public MessageConverter pushMessageConverter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setClassMapper(new PushTaskClassMapper(PushTaskMessage.class)); + return converter; + } + + @Bean + public TopicExchange pushTopicExchange() { + return new TopicExchange(EXCHANGE_TOPIC, true, false); + } + + @Bean + public FanoutExchange pushDlxExchange() { + return new FanoutExchange(EXCHANGE_DLX, true, false); + } + + /** 主队列:单消费者串行 + 死信路由到 push.dlx。 */ + @Bean + public Queue pushMainQueue() { + Map args = new HashMap<>(); + args.put("x-dead-letter-exchange", EXCHANGE_DLX); + return new Queue(QUEUE_MAIN, true, false, false, args); + } + + /** 死信队列:归档重试耗尽的任务。 */ + @Bean + public Queue pushDlqQueue() { + return new Queue(QUEUE_DLQ, true, false, false); + } + + @Bean + public Binding pushMainBinding() { + return BindingBuilder.bind(pushMainQueue()).to(pushTopicExchange()).with("#"); + } + + @Bean + public Binding pushDlqBinding() { + return BindingBuilder.bind(pushDlqQueue()).to(pushDlxExchange()); + } + + @Bean + public RabbitTemplate pushRabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(pushMessageConverter()); + return template; + } + + /** 消费者容器:手动 ack + 单并发 + prefetch=1,保证串行可观测。 */ + @Bean(name = "pushListenerContainerFactory") + public SimpleRabbitListenerContainerFactory pushListenerContainerFactory(ConnectionFactory connectionFactory) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setMessageConverter(pushMessageConverter()); + factory.setAcknowledgeMode(org.springframework.amqp.core.AcknowledgeMode.MANUAL); + factory.setConcurrentConsumers(1); + factory.setMaxConcurrentConsumers(1); + factory.setPrefetchCount(1); + return factory; + } +} diff --git a/src/main/java/com/yunhai/userbase/config/PushTaskClassMapper.java b/src/main/java/com/yunhai/userbase/config/PushTaskClassMapper.java new file mode 100644 index 0000000..fce04f8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/PushTaskClassMapper.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.config; + +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.ClassMapper; + +/** + * 推送消息固定反序列化为 {@link PushTaskMessage},绕过 Jackson2JsonMessageConverter 的 + * trusted-packages 安全限制(不同 spring-amqp 版本该方法签名/默认值不一致,编译期不可靠)。 + * 生产者端序列化仍由 Jackson2JsonMessageConverter 完成(写入 __TypeId__),消费者端忽略 __TypeId__ + * 直接按本映射器返回 PushTaskMessage,类型单一且可控。 + * + * @author chenx + * @date 2026/8/14 + */ +public class PushTaskClassMapper implements ClassMapper { + + private final Class> payloadClass; + + public PushTaskClassMapper(Class> payloadClass) { + this.payloadClass = payloadClass; + } + + @Override + public Class> toClass(MessageProperties properties) { + return payloadClass; + } + + @Override + public void fromClass(Class> clazz, MessageProperties properties) { + properties.setHeader("__TypeId__", clazz.getName()); + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java new file mode 100644 index 0000000..2359640 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java @@ -0,0 +1,124 @@ +package com.yunhai.userbase.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +/** + * 内部开放接口(/oapi/internal/*)来源 IP 白名单过滤器。 + * 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用; + * 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。 + * 支持 X-Forwarded-For / X-Real-IP 等常见代理头解析,取最左侧真实来源 IP 做校验。 + */ +public class SignKeyInternalIpFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(SignKeyInternalIpFilter.class); + + private final SignKeyInternalProperties properties; + + public SignKeyInternalIpFilter(SignKeyInternalProperties properties) { + this.properties = properties; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!properties.isEnabled()) { + filterChain.doFilter(request, response); + return; + } + String sourceIp = resolveSourceIp(request); + List effectiveIps = properties.getEffectiveIps(); + if (CollectionUtils.isEmpty(effectiveIps) || matchAny(sourceIp, effectiveIps)) { + filterChain.doFilter(request, response); + return; + } + log.warn("[SignKeyInternal] 拒绝非白名单来源访问内部接口: uri={}, sourceIp={}, whitelist={}", + request.getRequestURI(), sourceIp, effectiveIps); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"msg\":\"内部接口不允许从该来源访问\",\"data\":null}"); + response.getWriter().flush(); + } + + private static String resolveSourceIp(HttpServletRequest request) { + String[] headers = new String[] { + "X-Forwarded-For", + "X-Real-IP", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + for (String header : headers) { + String raw = request.getHeader(header); + if (raw == null || raw.isEmpty() || "unknown".equalsIgnoreCase(raw)) { + continue; + } + // X-Forwarded-For 可能是 多个IP逗号分隔,取最左侧 + String first = raw.split(",")[0].trim(); + if (!first.isEmpty()) { + return first; + } + } + return request.getRemoteAddr(); + } + + private static boolean matchAny(String ip, List cidrs) { + for (String cidr : cidrs) { + if (matchCidr(ip, cidr)) { + return true; + } + } + return false; + } + + private static boolean matchCidr(String ip, String cidr) { + if (ip == null || cidr == null) { + return false; + } + try { + int prefixLen; + String network; + if (cidr.contains("/")) { + String[] parts = cidr.split("/"); + network = parts[0].trim(); + prefixLen = Integer.parseInt(parts[1].trim()); + } else { + network = cidr.trim(); + // 判断地址族决定默认前缀长度 + InetAddress na = InetAddress.getByName(network); + prefixLen = (na instanceof Inet6Address) ? 128 : 32; + } + InetAddress ipAddr = InetAddress.getByName(ip); + InetAddress netAddr = InetAddress.getByName(network); + if (ipAddr.getClass() != netAddr.getClass()) { + return false; + } + byte[] ipBytes = ipAddr.getAddress(); + byte[] netBytes = netAddr.getAddress(); + int bits = prefixLen; + for (int i = 0; i < ipBytes.length && bits > 0; i++) { + int mask = bits >= 8 ? 0xFF : (0xFF << (8 - bits)); + if ((ipBytes[i] & mask) != (netBytes[i] & mask)) { + return false; + } + bits -= 8; + } + return true; + } catch (UnknownHostException | NumberFormatException ignored) { + return false; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java new file mode 100644 index 0000000..f5eb76b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java @@ -0,0 +1,55 @@ +package com.yunhai.userbase.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 内部开放接口(/oapi/internal/*)访问白名单 IP 配置。 + * 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。 + * + * 可在 Nacos 上配置,例如: + * yh: + * sign-key-internal: + * ips: + * - 10.0.0.0/8 + * - 172.16.0.0/12 + * - 192.168.0.0/16 + * - 127.0.0.1/32 + * - 172.20.10.5/32 # 某台固定机器 + * + * 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。 + */ +@ConfigurationProperties(prefix = "yh.sign-key-internal") +public class SignKeyInternalProperties { + + /** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */ + private List ips = new ArrayList<>(); + + /** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */ + private boolean enabled = true; + + public List getIps() { return ips; } + + public void setIps(List ips) { this.ips = ips; } + + public boolean isEnabled() { return enabled; } + + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + /** 未配置 ips 时返回默认私有段 + localhost 规则。 */ + public List getEffectiveIps() { + if (ips != null && !ips.isEmpty()) { + return ips; + } + return Arrays.asList( + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "127.0.0.1/32", + "0:0:0:0:0:0:0:1/128" + ); + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java new file mode 100644 index 0000000..3018a12 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.userbase.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; + +/** + * 接口文档(springdoc-openapi,替代旧 Springfox)。 + * 提供统一文档元信息;并通过全局定制器隐藏由切面注入的 {@code BoaUser authUserWithDept} 参数, + * 避免其作为请求体出现在文档中(该参数不参与 HTTP 入参,由 {@code BoaUserArgumentResolver} 解析)。 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI userBaseOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("用户中心接口文档") + .description("UserBase 用户中心 OpenAPI 文档(springdoc-openapi)") + .version("0.0.1")); + } + + /** 隐藏切面注入的登录用户参数,保持文档整洁。 */ + @Bean + public GlobalOperationCustomizer hideAuthUserParam() { + return (operation, handlerMethod) -> { + if (operation.getParameters() != null) { + operation.getParameters().removeIf(p -> "authUserWithDept".equals(p.getName())); + } + return operation; + }; + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java new file mode 100644 index 0000000..3f07052 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java @@ -0,0 +1,98 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.AppAuthProperties; +import com.yunhai.platform.appauth.AppSecretProvider; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * UserBase 作为「应用密钥权威源」的验签密钥提供方。 + * + * 解析优先级: + * 1) 本库 application_info —— 覆盖全部「已颁发」应用,新增/重置密钥后即时生效; + * 2) 配置式受信应用 {@code yh.app-auth.trusted-apps} —— 平台自身默认密钥(如 应用中心 100616) + * 兜底,使其即便尚未落库也可验签,避免「默认 appKey 未建行」导致验签失败。 + * + * 带 60s 本地 TTL 缓存,避免每次验签直打库(密钥变更最长 60s 后生效)。 + * + * 这样 UserBase 作为颁发方不依赖 Redis 预热、也不局限于 trustedApps 写死的少量密钥, + * 能够验签所有它已颁发的应用。 + */ +public class UserBaseAppSecretProvider implements AppSecretProvider { + + private final ApplicationInfoRepository repository; + private final Map trustedApps; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private static final long TTL_MS = 60_000L; + + public UserBaseAppSecretProvider(ApplicationInfoRepository repository, AppAuthProperties properties) { + this.repository = repository; + this.trustedApps = properties.getTrustedApps(); + } + + @Override + public String getSecret(String appKey) { + if (appKey == null) { + return null; + } + long now = System.currentTimeMillis(); + CacheEntry hit = cache.get(appKey); + if (hit != null && now - hit.ts < TTL_MS) { + return hit.secret; + } + String secret = null; + try { + Integer id = parse(appKey); + if (id != null) { + ApplicationInfo info = repository.findByAppKey(id); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + secret = info.getAppSecret(); + } + } + } catch (Exception ignored) { + // 查询异常不阻断,继续走配置兜底 + } + if (secret == null) { + secret = trustedApps.get(appKey); + } + cache.put(appKey, new CacheEntry(now, secret)); + return secret; + } + + /** + * 主动失效:立刻清除本地缓存中指定 appKey 的条目,密钥重置后调用 → 下一次 getSecret 直查 DB 拿新值, + * 不再受最长 60s TTL 生效窗口限制。 + */ + public void invalidate(String appKey) { + if (appKey == null) { + return; + } + cache.remove(appKey); + } + + /** 主动失效:立刻清空全部本地缓存(refreshAll / Redis 全量对账时调用)。 */ + public void invalidateAll() { + cache.clear(); + } + + private static Integer parse(String appKey) { + try { + return Integer.parseInt(appKey.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + private static final class CacheEntry { + final long ts; + final String secret; + + CacheEntry(long ts, String secret) { + this.ts = ts; + this.secret = secret; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java new file mode 100644 index 0000000..1c553ac --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java @@ -0,0 +1,71 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.LocalAppIdentityProvider; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * UserBase 自身作为「出站签名调用方」的身份提供方。 + * 约定: + * 1) appKey 固定占用 100000(基石应用段),由 application_bootstrap.sql 初始化脚本幂等插入; + * 2) appSecret 从 application_info 表按 appKey=100000 读取,带 60s 本地缓存(密钥重设后 60s 内生效)。 + * 显式覆盖 yh-app-auth-spring-boot-starter 默认的 PropertyLocalAppIdentityProvider(yml 不再需要 + * 配置 yh.app-auth.app-key / app-secret)。 + */ +public class UserBaseLocalAppIdentityProvider implements LocalAppIdentityProvider { + + private static final String USER_CENTER_APP_KEY = "100000"; + private static final int USER_CENTER_APP_KEY_INT = 100000; + private static final long TTL_MS = 60_000L; + + private final ApplicationInfoRepository repository; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + public UserBaseLocalAppIdentityProvider(ApplicationInfoRepository repository) { + this.repository = repository; + } + + @Override + public String getAppKey() { + return USER_CENTER_APP_KEY; + } + + @Override + public String getAppSecret() { + long now = System.currentTimeMillis(); + CacheEntry hit = cache.get(USER_CENTER_APP_KEY); + if (hit != null && now - hit.ts < TTL_MS) { + return hit.secret; + } + String secret = null; + try { + ApplicationInfo info = repository.findByAppKey(USER_CENTER_APP_KEY_INT); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + secret = info.getAppSecret(); + } + } catch (Exception ignored) { + } + cache.put(USER_CENTER_APP_KEY, new CacheEntry(now, secret)); + return secret; + } + + /** + * 主动失效:立刻清除 UserBase 自身(appKey=100000)的本地缓存,密钥重置后调用 → + * 下一次 getAppSecret 直查 DB 拿新值,不再受最长 60s TTL 生效窗口限制。 + */ + public void invalidate() { + cache.remove(USER_CENTER_APP_KEY); + } + + private static final class CacheEntry { + final long ts; + final String secret; + + CacheEntry(long ts, String secret) { + this.ts = ts; + this.secret = secret; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java new file mode 100644 index 0000000..ccfec7b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java @@ -0,0 +1,41 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.auth.UserDetailsProvider; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.service.impl.UserCenterService; +import com.yunhai.userbase.service.impl.UserCenterService; + +/** + * 平台 {@link UserDetailsProvider} 的 UserBase 本地实现:CAS 会话流解析出登录名/手机号后, + * 由本类查 UserBase 自身库,组装成基础平台统一的 {@link BoaUser}。 + * + * 本类不再加 {@code @Component},改为由 {@link OpenApiAuthEnforceConfiguration} + * 显式以 {@code @Bean @Primary} 注册(bean 名固定为 {@code userDetailsProvider}), + * 原因:yh-auth-spring-boot-starter 新版 AutoConfiguration 处理顺序可能早于 UserBase 组件扫描, + * 导致其默认注册的 {@code NoOpUserDetailsProvider}(直接抛 UnsupportedOperationException) + * 抢先被 PathBasedAuthInterceptor 注入。显式 @Bean+@Primary 可稳定覆盖默认实现。 + */ +public class UserBaseUserDetailsProvider implements UserDetailsProvider { + + private final UserCenterService userCenterService; + + public UserBaseUserDetailsProvider(UserCenterService userCenterService) { + this.userCenterService = userCenterService; + } + + @Override + public BoaUser loadByLoginName(String loginName) { + if (loginName == null) { + return null; + } + return userCenterService.getUserInfoWithDeptByAccount(loginName.trim()); + } + + @Override + public BoaUser loadByPhone(String phone) { + if (phone == null) { + return null; + } + return userCenterService.getUserInfoWithDeptByPhone(phone.trim()); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java b/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java new file mode 100644 index 0000000..f25da78 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java @@ -0,0 +1,135 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; +import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; +import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; +import com.yunhai.userbase.model.approle.AppRoleUserVo; +import com.yunhai.userbase.model.approle.SysAppRoleQuery; +import com.yunhai.userbase.model.approle.SysAppRoleRequest; +import com.yunhai.userbase.model.approle.SysAppRoleVo; +import com.yunhai.userbase.model.approle.UserAppRoleBindRequest; +import com.yunhai.userbase.service.IAppRoleService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.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; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色管理(角色 CRUD + 角色模块授权 + 用户角色绑定)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/approle/") +public class AppRoleController { + + @Autowired + private IAppRoleService appRoleService; + + /** + * 角色分页列表 :name 模糊,appId/allAppFlag/disabled 精确;普通角色回填所属应用名称 + */ + @GetMapping("/RoleList") + public BasePageResult roleList(SysAppRoleQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "id", direction = Sort.Direction.DESC) Sort sort) { + return PageUtils.toResult(appRoleService.list(query, PageUtils.toPageable(pageQuery, sort))); + } + + /** + * 新增角色:普通角色(allAppFlag=false,appId 必填,同应用下名称唯一); + * 全应用角色(allAppFlag=true,appId 为空,全应用角色之间名称唯一)。 + */ + @OperationLog("新增角色") + @PostMapping("RoleAdd") + public void roleAdd(@RequestBody @Validated(ValidationGroups.Create.class) SysAppRoleRequest request) { + appRoleService.add(request); + } + + /** + * 编辑角色:仅可改 name/remark/disabled;allAppFlag 与 appId 不允许修改。 + */ + @OperationLog("编辑角色") + @PostMapping("RoleEdit/{id}") + public void roleEdit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysAppRoleRequest request) { + appRoleService.edit(id, request); + } + + /** + * 删除角色:级联清理该角色的模块授权与用户绑定记录。 + */ + @OperationLog("删除角色") + @PostMapping("RoleDelete/{id}") + public void roleDelete(@PathVariable Integer id) { + appRoleService.delete(id); + } + + /** + * 角色模块授权回显:返回该角色可授权的应用及模块树(已授权 checked=true)。 + * 普通角色仅本应用;全应用角色返回全部应用(可跨应用授权)。 + */ + @GetMapping("/RoleModuleTree") + public List roleModuleTree(@RequestParam Integer roleId) { + return appRoleService.findRoleModuleTree(roleId); + } + + /** + * 角色模块授权保存:全量覆盖式更新该角色的模块授权记录。 + */ + @OperationLog("保存角色模块授权") + @PostMapping("SaveRoleModules") + public void saveRoleModules(@RequestBody @Validated AppRoleModuleAssignRequest request) { + appRoleService.saveRoleModules(request); + } + + /** + * 查询角色已绑定的用户列表。 + */ + @GetMapping("/RoleUsers") + public List roleUsers(@RequestParam Integer roleId) { + return appRoleService.findRoleUsers(roleId); + } + + /** + * 角色绑定用户保存(角色维度,全量覆盖)。 + */ + @OperationLog("保存角色用户绑定") + @PostMapping("SaveRoleUsers") + public void saveRoleUsers(@RequestBody @Validated AppRoleUserBindRequest request) { + appRoleService.saveRoleUsers(request); + } + + /** + * 用户绑定角色保存(用户维度,全量覆盖)。 + */ + @OperationLog("保存用户角色绑定") + @PostMapping("SaveUserRoles") + public void saveUserRoles(@RequestBody @Validated UserAppRoleBindRequest request) { + appRoleService.saveUserRoles(request); + } + + /** + * 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。 + * 前端「用户管理 - 删除」调用此接口。 + */ + @OperationLog("移除角色用户绑定") + @PostMapping("RemoveRoleUser") + public void removeRoleUser(@RequestParam Integer roleId, @RequestParam Integer userId) { + appRoleService.removeRoleUser(roleId, userId); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java b/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java new file mode 100644 index 0000000..cc3e9d6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java @@ -0,0 +1,178 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.appauthz.AppAuthzCategoryVo; +import com.yunhai.userbase.model.appcategory.AppCategoryQuery; +import com.yunhai.userbase.model.appcategory.AppCategoryRequest; +import com.yunhai.userbase.model.appcategory.AppCategoryVo; +import com.yunhai.userbase.model.application.ApplicationInfoQuery; +import com.yunhai.userbase.model.application.ApplicationInfoRequest; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.model.application.PushSettingRequest; +import com.yunhai.userbase.model.application.PushSettingVo; +import com.yunhai.userbase.model.appmodule.AppModuleQuery; +import com.yunhai.userbase.model.appmodule.AppModuleRequest; +import com.yunhai.userbase.model.appmodule.AppModuleVo; +import com.yunhai.userbase.service.IAppCategoryService; +import com.yunhai.userbase.service.IApplicationInfoService; +import com.yunhai.userbase.service.IAppModuleService; +import com.yunhai.userbase.service.impl.SignKeyCacheService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = Config.API_PATH + "/app/") +public class AppSettingsController { + + @Autowired + private IAppCategoryService appCategoryService; + + @Autowired + private IApplicationInfoService applicationInfoService; + + @Autowired + private IAppModuleService appModuleService; + + @Autowired + private SignKeyCacheService signKeyCacheService; + + + + @OperationLog("新增应用类型") + @PostMapping("AddType") + public void addType(@RequestBody @Validated(ValidationGroups.Create.class) AppCategoryRequest request) { + appCategoryService.add(request); + } + + @OperationLog("编辑应用类型") + @PostMapping("EditType/{id}") + public void editType(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) AppCategoryRequest request) { + appCategoryService.edit(id, request); + } + + @OperationLog("删除应用类型") + @PostMapping("DeleteType/{id}") + public void deleteType(@PathVariable Integer id) { + appCategoryService.delete(id); + } + + @GetMapping("/AppTypeList") + public List appTypeList(AppCategoryQuery query) { + return appCategoryService.list(query); + } + + @OperationLog("应用类型排序") + @PostMapping("/AppTypeSort") + public void appTypeSort(@RequestBody List ids) { + appCategoryService.sort(ids); + } + + /** + * 应用授权用:获取所有应用类型及其类型下的应用(每个应用含 id/name/icon)。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无应用的类型 app 为空数组。 + */ + @GetMapping("/AppAuthzList") + public List appAuthzList() { + return appCategoryService.listWithApps(); + } + + @OperationLog("新增应用") + @PostMapping("AddApp") + public void addApp(@RequestBody @Validated(ValidationGroups.Create.class) ApplicationInfoRequest request) { + applicationInfoService.add(request); + } + + @OperationLog("编辑应用") + @PostMapping("EditApp/{id}") + public void editApp(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) ApplicationInfoRequest request) { + applicationInfoService.edit(id, request); + } + + @OperationLog("删除应用") + @PostMapping("DeleteApp/{id}") + public void deleteApp(@PathVariable Integer id) { + applicationInfoService.delete(id); + } + + @OperationLog("重置应用密钥") + @PostMapping("ResetAppSecret/{id}") + public String resetAppSecret(@PathVariable Integer id) { + applicationInfoService.resetAppSecret(id); + ApplicationInfo info= applicationInfoService.getById(id); + return info.getAppSecret(); + } + + @OperationLog("全量刷新应用签名密钥到Redis") + @PostMapping("RefreshSignKeys") + public Map refreshSignKeys() { + int count = signKeyCacheService.refreshAll(); + Map result = new LinkedHashMap<>(); + result.put("refreshed", count); + return result; + } + + @GetMapping("/AppList") + public BasePageResult appList(ApplicationInfoQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "sortNo", direction = Sort.Direction.ASC) Sort sort) { + return PageUtils.toResult(applicationInfoService.list(query, PageUtils.toPageable(pageQuery, sort))); + } + + @OperationLog("应用排序") + @PostMapping("/AppSort") + public void appSort(@RequestBody List ids) { + applicationInfoService.sort(ids); + } + + @GetMapping("/AppSortList") + public List appSortList(@RequestParam Integer categoryId) { + return applicationInfoService.findListByCategoryId(categoryId); + } + + @OperationLog("新增模块") + @PostMapping("AddModule") + public void addModule(@RequestBody @Validated(ValidationGroups.Create.class) AppModuleRequest request) { + appModuleService.add(request); + } + + @OperationLog("编辑模块") + @PostMapping("EditModule/{id}") + public void editModule(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) AppModuleRequest request) { + appModuleService.edit(id, request); + } + + @OperationLog("删除模块") + @PostMapping("DeleteModule/{id}") + public void deleteModule(@PathVariable Integer id) { + appModuleService.delete(id); + } + + @GetMapping("/ModuleList") + public List moduleList(AppModuleQuery query) { + return appModuleService.list(query); + } + + @OperationLog("保存应用推送设置") + @PostMapping("SavePushSetting/{id}") + public void savePushSetting(@PathVariable Integer id, @RequestBody PushSettingRequest request) { + applicationInfoService.savePushSetting(id, request); + } + + @GetMapping("GetPushSetting/{id}") + public PushSettingVo getPushSetting(@PathVariable Integer id) { + return applicationInfoService.getPushSetting(id); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/BaseController.java b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java new file mode 100644 index 0000000..ee159c5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java @@ -0,0 +1,142 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.enums.IEnum; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.enums.*; +import com.yunhai.userbase.utils.*; +import com.yunhai.platform.utils.exception.DException; +import org.springframework.web.multipart.MultipartFile; +import lombok.var; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.*; +import java.util.function.Supplier; + +@RestController +@RequestMapping(value = Config.API_PATH +"/base/") +public class BaseController { + + + @Autowired + private RedisUtil redisUtil; + + /** + * 一般hashMap初始化大小 + */ + public static final int initialCapacity = 6; + + /** + * 通用上传允许的子目录(与 MyWebAppConfigurer.addResourceHandlers 的资源映射一一对应)。 + * 注:html 因 FileUtil 的危险后缀黑名单被禁止通过此接口上传,故未列入,避免存储可被浏览器执行的 HTML 引发 XSS。 + */ + private static final Set ALLOWED_UPLOAD_TYPES = new HashSet<>(Arrays.asList("image", "file", "icon", "word")); + + @GetMapping("GetUserInfo") + public Object getUserInfo(BoaUser authUserWithDept){ + return authUserWithDept; + } + + //返回系统枚举2 + @GetMapping("GetSysDict") + public Map getSysDict(BoaUser authUserWithDept){ + Map resultMap = new HashMap<>(7); + resultMap.put("grouptype", IEnum.toSimpleMapList(GroupTypeEnum.class, "k", "v")); + resultMap.put("gender", IEnum.toSimpleMapList(GenderEnum.class, "k", "v")); + resultMap.put("client", IEnum.toSimpleMapList(ClientEnum.class, "k", "v")); + resultMap.put("integ", IEnum.toSimpleMapList(IntegEnum.class, "k", "v")); + resultMap.put("status", IEnum.toSimpleMapList(StatusEnum.class, "k", "v")); + resultMap.put("sysrole", IEnum.toSimpleMapList(SysRoleEnum.class, "k", "v")); + resultMap.put("mode", IEnum.toSimpleMapList(SysModeEnum.class, "k", "v")); + return resultMap; + } + + /** + * 手动管理 Redis Hash 缓存的示例(@Cacheable 注解无法满足的场景)。 + * 适用场景:需要把一个对象的多个字段按 Hash 结构存储, + * 可单独读写/更新某个字段,而不像 @Cacheable 那样把整个对象序列化成一个大 value。 + * 访问路径:GET {Config.API_PATH}/base/ManualHashCacheDemo。 + * 注:手动缓存依赖 Redis;Redis 不可用时 RedisUtil 已内部捕获异常,操作失败但不影响接口本身。 + * + * @return 本次演示各步骤的结果快照 + */ + @GetMapping("ManualHashCacheDemo") + public Map manualHashCacheDemo() { + Map result = new HashMap<>(8); + String hashKey = "demo:user:1001"; + // 1) 写入整张 hash(带 TTL):适合一次性缓存一个对象的多个属性 + Map profile = new HashMap<>(4); + profile.put("name", "yunhai"); + profile.put("age", 18); + profile.put("role", "admin"); + redisUtil.hmset(hashKey, profile, 300); + // 2) 只更新其中一个字段,无需整体覆盖 + redisUtil.hset(hashKey, "role", "superadmin"); + // 3) 读取单个字段 + result.put("roleAfterUpdate", redisUtil.hget(hashKey, "role")); + // 4) 读取整张 hash + result.put("wholeHash", redisUtil.hmget(hashKey)); + // 5) 判断字段是否存在 + result.put("hasAgeField", redisUtil.hHasKey(hashKey, "age")); + // 6) Cache-Aside(复用下方私有方法):字段不存在时回源并写回 Hash + Object nickname = getHashFieldWithFallback(hashKey, "nickname", () -> "yunhai-nick"); + result.put("nickname", nickname); + // 7) 删除某个字段 + redisUtil.hdel(hashKey, "age"); + result.put("afterDelAge", redisUtil.hmget(hashKey)); + return result; + } + + + + @GetMapping("RedirectTo/{id}") + public RedirectView RedirectTo(BoaUser authUserWithDept, @PathVariable(name = "id") String id) { + String url= OtherUtil.hexToString(id); + return new RedirectView(url,true); + } + + /** + * 通用文件上传接口(一个接口覆盖所有类型,无需按类型拆分)。 + * type 决定存储子目录,与 MyWebAppConfigurer.addResourceHandlers 的 /upload/{type}/** 映射一一对应, + * 上传成功后可直接用返回的 URL 访问,无需再改资源映射配置。 + * 访问路径:POST {Config.API_PATH}/base/UploadFile(multipart/form-data)。 + * 支持 type:image / file / icon / word(html 被 FileUtil 黑名单禁止上传)。 + * + * @param file 上传文件(必填,单文件上限 50MB,见 application.yml 的 multipart 配置) + * @param type 存储子目录类型,默认 image + * @return 文件对外可访问的 Web URL + */ + @PostMapping("UploadFile") + public String uploadFile(@RequestParam("file") MultipartFile file, + @RequestParam(value = "type", defaultValue = "image") String type) { + if (!ALLOWED_UPLOAD_TYPES.contains(type)) { + throw new DException("不支持的上传类型: " + type + ",可选: image/file/icon/word"); + } + // FileUtil.upload 内部已处理空文件校验、危险后缀黑名单、目录创建与访问 URL 拼接 + return FileUtil.upload(file, type); + } + + /** + * 私有方法(可复用):演示 Hash 结构的 Cache-Aside(缓存旁路)模式。 + * 先从 Redis Hash 读字段;读不到则回源(此处用 fallback 模拟数据库查询),再把结果写回 Hash。 + * + * @param hashKey hash 键 + * @param field 字段名 + * @param fallback 回源加载器(真实场景替换为查数据库/远程调用) + * @return 字段值 + */ + private Object getHashFieldWithFallback(String hashKey, String field, Supplier fallback) { + if (redisUtil.hHasKey(hashKey, field)) { + return redisUtil.hget(hashKey, field); + } + Object value = fallback.get(); + if (value != null) { + redisUtil.hset(hashKey, field, value); + } + return value; + } + +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java b/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java new file mode 100644 index 0000000..3297b28 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java @@ -0,0 +1,179 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.groupauthz.GroupAppAssignRequest; +import com.yunhai.userbase.model.groupauthz.GroupAppVo; +import com.yunhai.userbase.model.groupauthz.GroupAuthzAppVo; +import com.yunhai.userbase.model.groupauthz.GroupAuthzCategoryVo; +import com.yunhai.userbase.model.groupauthz.GroupModuleAuthzItem; +import com.yunhai.userbase.model.groupauthz.GroupModulePermRequest; +import com.yunhai.userbase.model.groupauthz.SchoolAppAuthzRequest; +import com.yunhai.userbase.model.groupauthz.UserGroupNodeVo; +import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo; +import com.yunhai.userbase.model.groupauthz.UserPermissionUserVo; +import com.yunhai.userbase.model.groupauthz.UserRoleVo; +import com.yunhai.userbase.service.IGroupAuthzService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import javax.validation.Valid; +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; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用权限管理(应用开通 + 组授权 + 权限判定)。 + * 左侧组树复用 /sysgroup/GetTree;本控制器提供右侧数据加载与保存接口。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/groupauthz/") +public class GroupAuthzController { + + @Autowired + private IGroupAuthzService groupAuthzService; + + /** + * 功能1回显:点击左侧组后,返回系统全量应用列表,已开通的 checked=true。 + */ + @GetMapping("/GroupAppList") + public List groupAppList(@RequestParam Integer groupId) { + return groupAuthzService.listGroupApps(groupId); + } + + /** + * 功能1保存:全量覆盖式更新该组的应用开通记录。 + */ + @OperationLog("保存组应用开通") + @PostMapping("AssignApps") + public void assignApps(@RequestBody @Validated GroupAppAssignRequest request) { + groupAuthzService.assignAppsToGroup(request); + } + + /** + * 功能2数据加载:返回该组已开通的应用及其模块权限树(含勾选与子组继承回显)。 + */ + @GetMapping("/GroupModuleTree") + public List groupModuleTree(@RequestParam Integer groupId) { + return groupAuthzService.findGroupAppsWithModules(groupId); + } + + /** + * 功能2数据加载(按应用分类分组):供组授权页面按分类面板展示, + * 返回全部分类及该组已开通的应用模块权限树。 + */ + @GetMapping("/GroupModuleTreeByCategory") + public List groupModuleTreeByCategory(@RequestParam Integer groupId) { + return groupAuthzService.findGroupAppsWithModulesByCategory(groupId); + } + + /** + * 功能2保存:全量覆盖式更新该组的模块权限(含子组继承开关)。 + */ + @OperationLog("保存组模块权限") + @PostMapping("SaveModulePerms") + public void saveModulePerms(@RequestBody @Validated GroupModulePermRequest request) { + groupAuthzService.saveModulePermissions(request); + } + + /** + * 权限判定:判断用户对某模块是否有权限(供登录/拦截器/联调验证使用)。 + */ + @GetMapping("/HasModulePerm") + public boolean hasModulePerm(@RequestParam Integer userId, @RequestParam Integer moduleId) { + return groupAuthzService.hasModulePermission(userId, moduleId); + } + + /** + * 权限一览:根据组ID获取该组下的直接用户列表(不含子组,不分页)。 + */ + @GetMapping("/GroupUsers") + public List groupUsers(@RequestParam Integer groupId) { + return groupAuthzService.findGroupUsers(groupId); + } + + /** + * 权限一览:全局搜索用户(按账号/姓名/身份证模糊、手机号精确匹配),不分页。 + * 返回用户及其主归属部门/组织路径,供左侧用户搜索形态展示。 + */ + @GetMapping("/SearchUsers") + public List searchUsers(@RequestParam String keyword) { + return groupAuthzService.searchUsers(keyword); + } + + /** + * 权限一览:根据用户ID聚合该用户的全部已授权应用模块树。 + * 只返回有授权模块的应用,并按应用类型分组;无授权的应用类型和应用不显示。 + * 角色授权并集:结果 = 组授权模块 ∪ 启用角色授权模块(角色模块不要求组先开通应用)。 + */ + @GetMapping("/UserPermissionTree") + public List userPermissionTree(@RequestParam Integer userId, + @RequestParam(required = false) Integer groupId) { + return groupAuthzService.findUserPermissionTree(groupId, userId); + } + + /** + * 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。 + * 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。 + */ + @GetMapping("/UserRoles") + public List userRoles(@RequestParam Integer userId) { + return groupAuthzService.findUserRoles(userId); + } + + /** + * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。 + * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组)。 + */ + @GetMapping("/UserGroupTree") + public List userGroupTree(@RequestParam Integer userId) { + return groupAuthzService.findUserGroupTree(userId); + } + + // ===================== 新增:树节点式增量授权(左侧树 + 右侧 appAuthzList 勾选交互) ===================== + + /** + * 功能3回显:点击左侧树节点后,返回该组「已开通的应用ID列表」,供右侧 AppSettings.appAuthzList 回显勾选状态。 + * 交互:左树节点 → 右 appAuthzList(全量应用)→ 本接口拿到已选 id → 前端把对应多选框置为勾选。 + * 实现:应用开通不继承,直接查 sys_group_app_rel(group_id=nodeId) 返回 app_id 列表(详见 service.findAuthorizedAppIds)。 + * 与 GroupAppList(返回全量应用+checked)互为补充:本接口更轻量,只给 id 集合。 + */ + @GetMapping("/GetSchoolAppId") + public List getSchoolAppId(@RequestParam Integer nodeId) { + return groupAuthzService.findAuthorizedAppIds(nodeId); + } + + /** + * 功能3保存(应用维):点击应用多选框触发,对该组做应用开通/取消。 + * 入参(请求体,JSON):{@link SchoolAppAuthzRequest} —— schid=组ID(nodeId);appid=单个应用ID(一次只传一个,逐次点击);mode=操作模式。 + * mode 语义(SysModeEnum):0=追加(开通应用) / 2=删除(取消开通);1=覆盖 不适用,会被拒绝。 + * 实现:应用开通不继承,仅操作本组;删除时同步清理该组在该应用下模块的权限残留(详见 service.setSchoolApps)。 + * 注意:appid 为单个 Integer,后端不再做多值/逗号解析;每次点击提交一次请求。 + */ + @OperationLog("设置组应用开通") + @PostMapping("SetSchoolApp") + public void setSchoolApp(@RequestBody @Valid SchoolAppAuthzRequest request) { + groupAuthzService.setSchoolApps(request); + } + + /** + * 功能4保存(模块维):前端「组授权」面板逐条勾选/取消时提交,对 (组,应用,模块) 做增量授权/取消。 + * 入参:请求体为数组,元素见 {@link GroupModuleAuthzItem}:{groupId, appId, moduleId, mode, isAll}。 + * mode:0=追加 / 2=删除(1=覆盖不适用)。 + * isAll:是否对子组继承,映射 sys_group_module_perm.inherit_flag(1=子孙组自动继承 / 0=仅本组)。 + * moduleId=0:表示「该应用下全部模块」,后端展开为应用下每个真实模块各写/删一条。 + * 实现:逐条处理、顺序即执行顺序;追加前要求该组已开通该应用;越权写别的应用模块会被拒绝(详见 service.setGroupModulePerms)。 + * 与现有 SaveModulePerms(全量覆盖)共存:本接口是单条增量,二者操作同一张表、互不冲突。 + */ + @OperationLog("设置组模块权限") + @PostMapping("SetGroupModule") + public void setGroupModule(@RequestBody @Valid List items) { + groupAuthzService.setGroupModulePerms(items); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java b/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java new file mode 100644 index 0000000..4c3c669 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java @@ -0,0 +1,62 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.identity.IdentityCurrentVo; +import com.yunhai.userbase.model.identity.IdentityOptionVo; +import com.yunhai.userbase.model.identity.IdentitySwitchRequest; +import com.yunhai.userbase.service.impl.IdentityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +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.RestController; + +import java.util.List; + +/** + * 用户身份(平台身份角色 × 学校)管理接口。 + * 走 CAS 会话路线({@code /{platform}/identity/}),当前用户从 CAS 会话注入的 + * {@link BoaUser#getId()} 取,不接受外部传入 userId(防越权)。 + * 三个端点对应「切换身份」业务: + * ① {@code GET current} 当前激活身份(无切换记录则兜底主身份); + * ② {@code GET available} 全部可选身份清单; + * ③ {@code POST switch} 切换到指定 (roleId, schoolId),服务端校验属主后写入当前激活状态。 + * + * @author chenx + * @date 2026/8/18 + */ +@RestController +@RequestMapping(Config.API_PATH + "/identity/") +public class IdentityController { + + @Autowired + private IdentityService identityService; + + /** + * 当前激活身份。 + */ + @GetMapping("current") + public IdentityCurrentVo current(BoaUser authUserWithDept) { + return identityService.getCurrent(authUserWithDept.getId()); + } + + /** + * 全部可选身份(正常态),供「切换身份」页渲染。 + */ + @GetMapping("available") + public List available(BoaUser authUserWithDept) { + return identityService.listAvailable(authUserWithDept.getId()); + } + + /** + * 切换身份:校验属主后在 sys_user_current_role 写当前激活状态。 + */ + @PostMapping("switch") + public IdentityCurrentVo switchIdentity(BoaUser authUserWithDept, + @RequestBody @Validated IdentitySwitchRequest request) { + return identityService.switchIdentity(authUserWithDept.getId(), request); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java b/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java new file mode 100644 index 0000000..b16850d --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java @@ -0,0 +1,97 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.push.PushRecordListVo; +import com.yunhai.userbase.model.push.PushRecordQuery; +import com.yunhai.userbase.model.push.PushTestRequest; +import com.yunhai.userbase.service.push.PushDispatcher; +import com.yunhai.userbase.service.push.PushRecordService; +import com.yunhai.userbase.utils.PageUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 推送订阅控制器:测试推送 + 推送日志记录 / 重试。 + * + * @author chenx + * @date 2026/8/14 + */ +@RestController +@Slf4j +@RequestMapping(value = Config.API_PATH + "/push/") +public class PushSubscriptionController { + + @Autowired + private PushDispatcher pushDispatcher; + @Autowired + private PushRecordService pushRecordService; + + /** 测试推送:手动触发一次推送(投递 MQ),并返回请求预览(URL/头/体,密钥不外露)。 */ + @PostMapping("Test") + public Map test(@RequestBody PushTestRequest request) { + pushDispatcher.dispatchManual(request.getAppId(), request.getInterfacePath(), request.getTargetIds()); + Map preview = pushDispatcher.previewManual( + request.getAppId(), request.getInterfacePath(), request.getTargetIds()); + Map result = new LinkedHashMap<>(); + result.put("sent", true); + result.put("message", "已投递推送队列,可在「推送日志」查看实际响应与耗时"); + result.put("preview", preview); + return result; + } + + /** 推送日志列表(不含 payload,避免全量推送数据量过大;payload 经 Download 接口单独获取)。 */ + @GetMapping("RecordList") + public BasePageResult recordList(PushRecordQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "createdAt", direction = Sort.Direction.DESC) Sort sort) { + return PageUtils.toResult(pushRecordService.query(query, PageUtils.toPageable(pageQuery, sort))); + } + + /** 手动重试一条失败的推送记录。 */ + @PostMapping("Retry/{id}") + public void retry(@PathVariable Long id) { + pushRecordService.retry(id); + } + + /** + * 下载某条推送记录的请求体原文(payload 文本文件)。 + * 采用与项目 DownloadFileUtil 一致的 octet-stream + attachment 写法,确保浏览器稳定触发下载。 + * 若该记录未记录 payload,返回一段说明文本文件而非 404,便于排查。 + */ + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = pushRecordService.getPayload(id); + if (data == null) { + data = "该条推送记录未记录请求体(payload 为空),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("payload-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("推送日志请求体下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java b/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java new file mode 100644 index 0000000..82fbeed --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java @@ -0,0 +1,102 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupQuery; +import com.yunhai.userbase.model.sysgroup.SysGroupRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupSortRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupVo; +import com.yunhai.userbase.service.ISysGroupService; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组管理(组织架构树) + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sysgroup/") +public class SysGroupController { + + @Autowired + private ISysGroupService sysGroupService; + + @OperationLog("新增组") + @Operation(operationId = "sysGroupAdd") + @PostMapping("Add") + public void add(@RequestBody @Validated(ValidationGroups.Create.class) SysGroupRequest request) { + sysGroupService.add(request); + } + + @OperationLog("编辑组") + @Operation(operationId = "sysGroupEdit") + @PostMapping("Edit/{id}") + public void edit(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) SysGroupRequest request) { + sysGroupService.edit(id, request); + } + + @OperationLog("删除组") + @Operation(operationId = "sysGroupRemove") + @PostMapping("Delete/{id}") + public void delete(@PathVariable Integer id) { + sysGroupService.delete(id); + } + + @Operation(operationId = "sysGroupList") + @GetMapping("/List") + public List list(SysGroupQuery query) { + return sysGroupService.list(query); + } + + /** + * 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 + */ + @GetMapping("GetTree") + public List getTree() { + return sysGroupService.getTree(); + } + + /** + * 获取「学校组」剪枝树:末端叶子一定是学校组;学校组下的组不显示; + * 子树中无学校组的分支整体剪掉;根节点始终保留。 + */ + @GetMapping("GetSchoolTree") + public List getSchoolTree() { + return sysGroupService.getSchoolTree(); + } + + /** + * 批量新增子组:请求体 { pid, type, items:[{name},...] }(JSON)。 + */ + @OperationLog("批量新增子组") + @PostMapping("BatchAdd") + public void batchAdd(@RequestBody @Validated SysGroupBatchRequest request) { + sysGroupService.batchAdd(request); + } + + /** + * 子组排序:传入 pid 与有序 ids(必须是 pid 的直接子节点),按传入顺序落库。 + */ + @OperationLog("子组排序") + @PostMapping("SortChildren") + public void sortChildren(@Validated SysGroupSortRequest request) { + sysGroupService.sortChildren(request); + } + + /** + * 移动组:将 moveId 移动到 pid 下(不支持移动到自身或自身后代)。 + */ + @OperationLog("移动组") + @PostMapping("Move") + public void move(@Validated SysGroupMoveRequest request) { + sysGroupService.move(request); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java b/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java new file mode 100644 index 0000000..46ffd0f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.oapilog.SysOapiLogListVo; +import com.yunhai.userbase.model.oapilog.SysOapiLogQuery; +import com.yunhai.userbase.service.ISysOapiLogService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 开放接口日志查询与下载接口。 + * 本控制器位于 {@code controller.api} 包,不会被 OapiLogAspect(切 oapi 包)拦截。 + * + * @author chenx + * @date 2026/8/20 + */ +@Slf4j +@RestController +@RequestMapping(value = Config.API_PATH + "/oapilog/") +public class SysOapiLogController { + + @Autowired + private ISysOapiLogService sysOapiLogService; + + /** + * 分页查询开放接口日志列表(不含响应结果)。 + */ + @Operation(summary = "开放接口日志分页查询", operationId = "oapiLogList") + @GetMapping("/List") + public BasePageResult list(SysOapiLogQuery query, BasePageQuery pageQuery) { + return sysOapiLogService.pageQuery(query, pageQuery); + } + + /** + * 下载某条日志的响应结果(GZIP 解压后原文,文本文件)。 + * 与 {@code SysOperationLogController.Download} 一致的 octet-stream + attachment 写法。 + * 若该日志未记录响应数据,返回一段说明文本文件而非 404,便于排查。 + */ + @Operation(summary = "下载开放接口日志响应原文", operationId = "oapiLogDownload") + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = sysOapiLogService.getResponseData(id); + if (data == null) { + data = "该条日志未记录响应数据(saveResponse=false、或接口响应为空、或响应序列化失败),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("response-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("开放接口日志响应结果下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java b/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java new file mode 100644 index 0000000..e980c23 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.oplog.SysOperationLogListVo; +import com.yunhai.userbase.model.oplog.SysOperationLogQuery; +import com.yunhai.userbase.service.ISysOperationLogService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 操作日志查询与下载接口。 + * 本控制器自身不标注 @OperationLog,避免记录日志查看行为。 + * + * @author chenx + * @date 2026/7/28 + */ +@Slf4j +@RestController +@RequestMapping(value = Config.API_PATH + "/oplog/") +public class SysOperationLogController { + + @Autowired + private ISysOperationLogService sysOperationLogService; + + /** + * 分页查询操作日志列表(不含响应结果)。 + */ + @Operation(summary = "操作日志分页查询", operationId = "operationLogList") + @GetMapping("/List") + public BasePageResult list(SysOperationLogQuery query, BasePageQuery pageQuery) { + return sysOperationLogService.pageQuery(query, pageQuery); + } + + /** + * 下载某条日志的响应结果(GZIP 解压后原文, 文本文件)。 + * 采用与项目 DownloadFileUtil 一致的 octet-stream + attachment 写法,确保浏览器稳定触发下载。 + * 若该日志未记录响应数据,返回一段说明文本文件而非 404,便于排查。 + */ + @Operation(summary = "下载日志响应原文", operationId = "operationLogDownload") + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = sysOperationLogService.getResponseData(id); + if (data == null) { + data = "该条日志未记录响应数据(saveResponse=false、或接口响应为空、或响应序列化失败),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("response-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("操作日志响应结果下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java b/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java new file mode 100644 index 0000000..84c23bc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java @@ -0,0 +1,147 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.exception.DException; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.sysuser.BatchChangeStatusRequest; +import com.yunhai.userbase.model.sysuser.BatchDeleteRequest; +import com.yunhai.userbase.model.sysuser.BatchImportResult; +import com.yunhai.userbase.model.sysuser.ChangeGroupsRequest; +import com.yunhai.userbase.model.sysuser.ChangeSortNoRequest; +import com.yunhai.userbase.model.sysuser.SysUserQuery; +import com.yunhai.userbase.model.sysuser.SysUserRequest; +import com.yunhai.userbase.model.sysuser.SysUserVo; +import com.yunhai.userbase.service.ISysUserService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 组用户管理 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sysuser/") +public class SysUserController { + + @Autowired + private ISysUserService sysUserService; + + @OperationLog("新增用户") + @PostMapping("Add") + public Integer add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) { + List data=new ArrayList<>(); + + return sysUserService.add(request); + } + + @OperationLog("编辑用户") + @PostMapping("Edit/{id}") + public void edit(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) { + sysUserService.edit(id, request); + } + + /** + * 将用户从指定组中移除;若该用户无其它组关联则级联删除用户。 + */ + @OperationLog("从组中删除用户") + @PostMapping("DeleteFromGroup/{userId}/{groupId}") + public void deleteFromGroup(@PathVariable Integer userId, @PathVariable Integer groupId) { + sysUserService.deleteFromGroup(userId, groupId); + } + + /** + * 批量从指定组中移除用户;无其它组关联者级联删除用户。 + */ + @OperationLog("批量从组中删除用户") + @PostMapping("BatchDelete") + public void batchDelete(@RequestBody @Validated BatchDeleteRequest request) { + sysUserService.batchDeleteFromGroup(request.getUserIds(), request.getGroupId()); + } + + /** 禁用用户(disabled=true) */ + @OperationLog("禁用用户") + @PostMapping("Disable/{id}") + public void disable(@PathVariable Integer id) { + sysUserService.changeStatus(id, true); + } + + /** 启用用户(disabled=false) */ + @OperationLog("启用用户") + @PostMapping("Enable/{id}") + public void enable(@PathVariable Integer id) { + sysUserService.changeStatus(id, false); + } + + /** 批量设置用户状态 */ + @OperationLog("批量设置用户状态") + @PostMapping("BatchChangeStatus") + public void batchChangeStatus(@RequestBody @Validated BatchChangeStatusRequest request) { + sysUserService.batchChangeStatus(request.getUserIds(), request.getDisabled()); + } + + @GetMapping("/List") + public BasePageResult list(SysUserQuery query, BasePageQuery pageQuery) { + // 排序字段在关联表,无法走 Pageable.sort,统一由 Service.Specification 控制 + // (根节点按 id 升序,具体组按组内 sort_no 升序)。此处不传 Sort,避免覆盖 Spec 的 orderBy。 + return PageUtils.toResult(sysUserService.list(query, PageUtils.toPageable(pageQuery, null))); + } + + /** 调整用户在某组内的排序号(sort_no):1~999,可空表示清除 */ + @OperationLog("设置用户组内排序号") + @PostMapping("ChangeSortNo/{userId}") + public void changeSortNo(@PathVariable Integer userId, + @RequestBody @Validated ChangeSortNoRequest request) { + sysUserService.changeSortNo(userId, request.getGroupId(), request.getSortNo()); + } + + @OperationLog("调整组") + @PostMapping("ChangeGroups") + public void changeGroups(@RequestBody @Validated ChangeGroupsRequest request) { + sysUserService.changeGroups(request.getUserIds(), request.getGroupIds()); + } + + @GetMapping("Groups/{userId}") + public List getUserGroups(@PathVariable Integer userId) { + return sysUserService.findGroupIdsByUserId(userId); + } + + @OperationLog("批量导入用户") + @PostMapping("BatchImport") + public BatchImportResult batchImport(@RequestParam("file") MultipartFile file) { + return sysUserService.batchImport(file); + } + + /** + * 下载用户批量导入模板(真实 .xlsx):平铺当前组及子孙组,仅填充组名(路径)与组id;其余列留空供填写。 + */ + @GetMapping("DownloadTemplate") + public void downloadTemplate(@RequestParam(value = "groupId", required = false) Integer groupId, + HttpServletResponse response) throws IOException { + sysUserService.downloadImportTemplate(groupId, response); + } + + /** + * 导出组下所有用户(含子组)为 Excel;与导入模板同构,末尾追加用户id列。 + * 前端直接跳转此地址触发浏览器下载。 + */ + @GetMapping("Export") + public void export(@RequestParam(value = "groupId", required = false) Integer groupId, + @RequestParam(value = "withSub", defaultValue = "1") Integer withSub, + @RequestParam(value = "name", required = false) String name, + HttpServletResponse response) throws IOException { + sysUserService.exportUsers(groupId, withSub, name, response); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java new file mode 100644 index 0000000..efc3ebb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.enums.IEnum; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.service.IAppCategoryService; +import com.yunhai.userbase.service.IApplicationInfoService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +/** + * 应用域开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/app/...})。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求 + * (URL pattern {@code /oapi/*} 已覆盖本控制器),未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "app/") +public class AppController { + + @Autowired + private IAppCategoryService appCategoryService; + + @Autowired + private IApplicationInfoService applicationInfoService; + + /** + * 按主键 ID 查询应用信息(带缓存)。 + * 与客户端契约 {@code GET /oapi/app/{appId}} 对应;查不到返回 null(HTTP 200 + data:null 信封)。 + * 返回 {@link ApplicationInfoVo} 并清空 appSecret,与 list 接口一致不透出签名密钥。 + * + * @param appId 应用主键 ID(必填) + * @return 应用信息视图;不存在时返回 null + */ + @GetMapping("{appId}") + public ApplicationInfoVo byId(@PathVariable Integer appId) { + ApplicationInfo info = applicationInfoService.getById(appId); + if (info == null) { + return null; + } + ApplicationInfoVo vo = new ApplicationInfoVo(); + BeanUtils.copyProperties(info, vo); + // 不透出签名密钥(与 list 契约一致) + vo.setAppSecret(null); + return vo; + } + + /** + * 查询用户在指定客户端类型下有权访问的应用列表(按应用类型分组)。 + * 权限过滤复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}(组开通 ∪ 启用角色模块所属应用), + * 超级管理员返回全部;不透出 appSecret。 + * clientType 传枚举 key(2 的幂值,如 1=PC 2=移动App 4=系统模块…);非法/缺失按不限制端处理。 + * + * @param userId 用户ID(必填) + * @param clientType 客户端类型 key(可选) + * @return 应用类型分组列表;无权限时各类型 apps 为空数组 + */ + @GetMapping("list") + public List list(@RequestParam("userId") Integer userId, + @RequestParam(value = "clientType", required = false) Integer clientType) { + ClientEnum clientEnum = (clientType == null) ? null + : IEnum.getByKey(ClientEnum.class, clientType); + return appCategoryService.listBoaAppType(userId, clientEnum); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java new file mode 100644 index 0000000..62108c9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java @@ -0,0 +1,116 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.utils.model.BoaGroup; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupRequest; +import com.yunhai.userbase.service.ISysGroupService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.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; + +/** + * 组织架构开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/group/...})。 + * + * 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient} + * 硬编码的契约严格一致: + * {@code GET /oapi/group/tree}、{@code /byId?groupId=&withSub=}、{@code /list?ids=&withSub=}。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求, + * 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "group/") +public class GroupController { + + @Autowired + private ISysGroupService sysGroupService; + + /** + * 获取整个平台的组树(从根节点开始,children 递归嵌套全部后代)。 + */ + @GetMapping("tree") + public BoaGroup tree() { + return sysGroupService.getGroups(); + } + + /** + * 获取单个组;withSub=true 时递归挂上全部子孙(整棵子树)。 + * + * @param groupId 组ID(必填) + * @param withSub 是否展开子节点,缺省 false + * @return 组节点;不存在返回 null + */ + @GetMapping("byId") + public BoaGroup byId(@RequestParam("groupId") Integer groupId, + @RequestParam(value = "withSub", required = false) Boolean withSub) { + return sysGroupService.getGroupById(groupId, withSub); + } + + /** + * 批量获取组;ids 逗号/空格分隔,按入参顺序返回,不存在的跳过。 + * + * @param ids 组ID列表(逗号/空格分隔) + * @param withSub 是否展开子节点,缺省 false + * @return 组节点列表(按入参顺序;不存在的跳过) + */ + @GetMapping("list") + public List list(@RequestParam("ids") String ids, + @RequestParam(value = "withSub", required = false) Boolean withSub) { + return sysGroupService.getGroupList(ids, withSub); + } + + /** + * 新增组:返回创建后的组节点(含 id)。 + * 层级类型约束、根节点保护等复用后台逻辑;鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @PostMapping("add") + public BoaGroup add(@RequestBody @Validated(ValidationGroups.Create.class) SysGroupRequest request) { + return sysGroupService.add(request); + } + + /** + * 编辑组(不含移动,移动请走 group/move 后台接口):返回更新后的组节点(含 id)。 + */ + @PostMapping("edit/{id}") + public BoaGroup edit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysGroupRequest request) { + return sysGroupService.edit(id, request); + } + + /** + * 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。 + * 新增 {@code schoolId} 边界:服务端强制 id 属于该校子树且不等于 schoolId,避免删学校根组/跨校删组。 + */ + @PostMapping("delete/{id}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer schoolId) { + sysGroupService.delete(id, schoolId); + } + + /** + * 批量新增子组:请求体 { pid, type, items:[{name},...] },返回创建后的组节点列表(均含 id)。 + */ + @PostMapping("batchAdd") + public List batchAdd(@RequestBody @Validated SysGroupBatchRequest request) { + return sysGroupService.batchAdd(request); + } + + /** + * 移动组(学校限定):仅允许在同一学校(组)子树内移动——moveId、pid 都必须落在 schoolId 子树内。 + * 语义复用后台 Move(环检测/类型约束/重排),额外叠加学校边界。鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @PostMapping("move/{schoolId}") + public void move(@PathVariable Integer schoolId, + @RequestBody @Validated SysGroupMoveRequest request) { + sysGroupService.move(request, schoolId); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java b/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java new file mode 100644 index 0000000..b435789 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java @@ -0,0 +1,70 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.appauth.AppAuthProperties; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 验签兜底端点:供其他业务服务在「共享 Redis 宕机且本地缓存也未命中」时直连获取某应用的密钥。 + * 直接从 MySQL 读(绕过可能同宕的共享 Redis),是密钥的权威源;写入侧仍由 + * {@code SignKeyCacheService} 同步到 Redis。返回形状与 BasePlatform 读侧解析契约一致: + * {@code {"appKey":"100616","secret":"...","enabled":true}}。 + * 解析优先级: + * 1) 本库 application_info(覆盖全部「已颁发」应用的实时密钥); + * 2) 配置式受信应用 {@code yh.app-auth.trusted-apps}(平台自身默认密钥,如 应用中心 100616, + * 即便尚未落库也可解析,避免「默认 appKey 未建行」时此处返回 404 导致验签链路失败); + * 两者皆无 → 404。 + * 注意:此端点返回的是应用密钥,必须仅内网可达(由部署网络隔离保障),切勿暴露到公网。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "internal") +public class SignKeyInternalController { + + private final ApplicationInfoRepository applicationInfoRepository; + private final AppAuthProperties appAuthProperties; + + public SignKeyInternalController(ApplicationInfoRepository applicationInfoRepository, + AppAuthProperties appAuthProperties) { + this.applicationInfoRepository = applicationInfoRepository; + this.appAuthProperties = appAuthProperties; + } + + @GetMapping("sign-key/{appId}") + public ResponseEntity> signKey(@PathVariable String appId) { + Integer key; + try { + key = Integer.parseInt(appId.trim()); + } catch (NumberFormatException e) { + return ResponseEntity.notFound().build(); + } + ApplicationInfo info = applicationInfoRepository.findByAppKey(key); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + return ok(info.getAppKey() == null ? appId : String.valueOf(info.getAppKey()), + info.getAppSecret()); + } + // 库内无此应用密钥:回退到配置式受信应用(平台自身默认密钥 100616 等), + // 避免「应用中心默认 appKey 未落库」时兜底查询返回 404。 + String cfgSecret = appAuthProperties.getTrustedApps().get(appId); + if (cfgSecret != null && !cfgSecret.trim().isEmpty()) { + return ok(appId, cfgSecret); + } + return ResponseEntity.notFound().build(); + } + + private static ResponseEntity> ok(String appKey, String secret) { + Map body = new LinkedHashMap<>(); + body.put("appKey", appKey); + body.put("secret", secret); + body.put("enabled", true); + return ResponseEntity.ok(body); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java new file mode 100644 index 0000000..9623605 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java @@ -0,0 +1,233 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.appauth.AppAuthFilter; +import com.yunhai.platform.utils.exception.DException; +import com.yunhai.platform.utils.model.BoaGroup; +import com.yunhai.platform.utils.model.BoaModule; +import com.yunhai.platform.utils.model.BoaRoleInfo; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem; +import com.yunhai.userbase.model.sysuser.SysUserMoveRequest; +import com.yunhai.userbase.model.sysuser.SysUserPasswordRequest; +import com.yunhai.userbase.model.sysuser.SysUserRequest; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import com.yunhai.userbase.service.IGroupAuthzService; +import com.yunhai.userbase.service.ISysUserService; +import com.yunhai.userbase.service.impl.UserCenterService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/user/...})。 + * + * 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient} + * 硬编码的契约严格一致: + * {@code GET /oapi/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、 + * {@code /oapi/user/modules?userId=&schId=}。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求, + * 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "user/") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + + @Autowired + private UserCenterService userCenterService; + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private IGroupAuthzService groupAuthzService; + + @Autowired + private ApplicationInfoRepository applicationInfoRepository; + + @GetMapping("byId") + public BoaUser byId(@RequestParam("id") Integer id) { + return userCenterService.getUserInfoWithDeptById(id); + } + + @GetMapping("byLoginName") + public BoaUser byLoginName(@RequestParam("loginName") String loginName) { + if (loginName == null || loginName.trim().isEmpty()) { + return null; + } + return userCenterService.getUserInfoWithDeptByAccount(loginName); + } + + @GetMapping("byPhone") + public BoaUser byPhone(@RequestParam("phone") String phone) { + if (phone == null || phone.trim().isEmpty()) { + return null; + } + return userCenterService.getUserInfoWithDeptByPhone(phone); + } + + /** + * 查询用户在调用方应用下、指定学校(上下文)里的已授权模块列表。 + * + * 与管理端「权限一览」返回全应用聚合不同,本方法严格按调用方应用维度过滤: + * ① 通过 {@link AppAuthFilter#APP_AUTH_ATTR} 拿到验签时的 appKey(RequireAppSignFilter + * 已保证非空); + * ② 查 application_info 映射到具体 appId; + * ③ 调 Service 专用方法:所有 SQL/集合构建均限定在该 appId 范围内(数据库层面过滤)。 + * + * 边界:调用方 appKey 查不到对应应用 → 直接返回空列表(安全优先,禁止意外返回其他应用的模块)。 + * + * @param request 用于取 AppAuthFilter 设置的 appKey + * @param userId 用户ID(必填) + * @param schId 学校ID/组ID(可选;用于限定「学校上下文」:非空时仅取该学校子树内的真实归属组参与判定, + * 为空时取用户全部真实归属组,与权限一览一致) + * @return 该应用下用户已授权模块(扁平化;无权限时返回空列表) + */ + @GetMapping("modules") + public List modules(HttpServletRequest request, + @RequestParam("userId") Integer userId, + @RequestParam(value = "schId", required = false) Integer schId) { + if (userId == null) { + return Collections.emptyList(); + } + Integer appId = resolveCallerAppId(request); + if (appId == null) { + return Collections.emptyList(); + } + return groupAuthzService.findUserModulesForApp(appId, schId, userId); + } + + /** + * 从请求 attribute 中解析调用方 appKey,并映射为 application_info 的主键 id。 + * 映射失败返回 null(Controller 层统一返回空列表;异常只打 warn 日志,不给前端 500)。 + */ + private Integer resolveCallerAppId(HttpServletRequest request) { + Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR); + if (attr == null) { + log.warn("[oapi:user:modules] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}", + request.getRequestURI()); + return null; + } + String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr); + if (appKeyStr.trim().isEmpty()) { + log.warn("[oapi:user:modules] AppAuth attr is blank, uri={}", request.getRequestURI()); + return null; + } + Integer appKey; + try { + appKey = Integer.parseInt(appKeyStr.trim()); + } catch (NumberFormatException e) { + log.warn("[oapi:user:modules] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI()); + return null; + } + ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey); + if (app == null) { + log.warn("[oapi:user:modules] appKey={} not found in application_info, uri={}", appKey, + request.getRequestURI()); + return null; + } + return app.getId(); + } + + + /** + * 新增用户:走完整校验(账号唯一/手机号/身份证/密码强度),BCrypt 加密并写入组关联; + * 返回创建后的用户(含 id 与组织链)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。 + */ + @PostMapping("add") + public BoaUser add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) { + Integer id = sysUserService.add(request); + return userCenterService.getUserInfoWithDeptById(id); + } + + /** + * 编辑用户:密码为空时不修改密码;返回更新后的用户(含 id 与组织链)。 + */ + @PostMapping("edit/{id}") + public BoaUser edit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) { + sysUserService.edit(id, request); + return userCenterService.getUserInfoWithDeptById(id); + } + + /** + * 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联; + * 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。 + * 这样多校用户只会被退出指定学校,而不会被整体误删。 + * 新增 {@code schoolId} 边界:服务端强制 groupId 属于该校子树且不等于 schoolId,避免跨校/整校移除。 + */ + @PostMapping("delete/{id}/{groupId}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer groupId, @PathVariable Integer schoolId) { + sysUserService.deleteFromGroup(id, groupId, schoolId); + } + + /** + * 批量删除用户-组关系:请求体为 {@code SysUserGroupDeleteItem} 数组,每条携带自身 (userId, groupId, schoolId); + * 服务端逐条校验学校边界后解绑,语义与单条 delete 一致。 + */ + @PostMapping("deleteUsers") + public void deleteUsers(@RequestBody List items) { + sysUserService.deleteUsers(items); + } + + /** + * 批量新增用户:逐条走完整校验(含密码强度,每条需携带 password + confirmPassword); + * 返回创建后的用户列表(顺序与入参一致,均含 id 与组织链)。 + */ + @PostMapping("batchAdd") + public List batchAdd(@RequestBody List requests) { + List ids = sysUserService.batchAdd(requests); + List result = new ArrayList<>(); + for (Integer id : ids) { + result.add(userCenterService.getUserInfoWithDeptById(id)); + } + return result; + } + + /** + * 移动用户到指定组:仅允许在同一学校(组)范围内移动——用户当前必须已在该学校子树内, + * 且目标组也必须在该学校子树内,否则拒绝(禁止跨校移动)。 + * 语义同后台「调整组」——覆盖式把用户组归属替换为目标组(目标设为主组)。返回移动后的用户(含新的组织链)。 + */ + @PostMapping("moveToGroup") + public BoaUser moveToGroup(@RequestBody @Validated SysUserMoveRequest request) { + sysUserService.moveUserWithinSchool(request.getUserId(), request.getGroupId(), request.getSchoolId()); + return userCenterService.getUserInfoWithDeptById(request.getUserId()); + } + + /** + * 查询用户的业务角色身份列表(角色 + 主归属学校组上下文)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @GetMapping("roles") + public List roles(@RequestParam("userId") Integer userId) { + return sysUserService.findUserRoles(userId); + } + + /** + * 修改用户密码:先校验旧密码,再写入新密码(BCrypt)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。 + */ + @PostMapping("password") + public void password(@RequestBody @Validated SysUserPasswordRequest request) { + sysUserService.changePassword(request.getUserId(), request.getOldPassword(), request.getNewPassword()); + } +} diff --git a/src/main/java/com/yunhai/userbase/entity/AppCategory.java b/src/main/java/com/yunhai/userbase/entity/AppCategory.java new file mode 100644 index 0000000..15dccc3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/AppCategory.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/15 15:53 + * @description + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "app_category") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class AppCategory extends BaseEntity { + //类别名称 + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "sort_no") + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/entity/AppModule.java b/src/main/java/com/yunhai/userbase/entity/AppModule.java new file mode 100644 index 0000000..6ae9897 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/AppModule.java @@ -0,0 +1,81 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块(树形,逻辑外键,最深5层) + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "app_module", + indexes = { + @Index(name = "idx_app_module_app", columnList = "app_id"), + @Index(name = "idx_app_module_parent", columnList = "parent_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_module_custom", columnNames = {"app_id", "custom_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class AppModule extends BaseEntity { + + /** + * 所属应用ID(逻辑外键 → ApplicationInfo.id),创建后不可改。 + */ + @NotNull + @Column(name = "app_id", nullable = false, updatable = false) + private Integer appId; + + /** + * 父模块ID(逻辑外键 → AppModule.id);根模块为 null。 + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 模块名称。 + */ + @NotBlank + @Column(name = "name", nullable = false) + private String name; + + /** + * 自定义ID(应用内唯一,业务编码,如 "user-mgmt")。 + */ + @NotBlank + @Column(name = "custom_id", nullable = false, length = 64) + private String customId; + + /** + * 模块路径(前端路由/资源路径,如 "/system/user")。 + */ + @Column(name = "path", length = 255) + private String path; + + /** + * 层级:根=1,子=父+1,上限5(由 Service 校验)。 + */ + @NotNull + @Column(name = "level", nullable = false) + private Integer level; + + /** + * 排序号(沿用 AppCategory 约定)。 + */ + @Column(name = "sort_no") + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java new file mode 100644 index 0000000..6d17c06 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java @@ -0,0 +1,146 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.ListIntegerJsonConverter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 15:53 + * @description + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "application_info") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ApplicationInfo extends BaseEntity { + + /** + * 应用名称(页面*必填) + */ + private String name; + + /** + * 应用类型ID + */ + private Integer categoryId; + + /** + * 排序号,仅在同一个应用类型内生效;默认为 null(新增时不赋值)。 + */ + @Column(name = "sort_no") + private Integer sortNo; + + /** + * 应用描述 + */ + @Column(name = "app_desc", length = 500) + private String desc; + + /** + * appKey 系统生成唯一标识,整型,从100000起自增。 + */ + @NotNull + @Column(name = "app_key", unique = true, nullable = false) + private Integer appKey; + + /** + * appSecret 密钥,支持重设 + */ + @NotBlank + @Column(name = "app_secret", nullable = false, length = 128) + private String appSecret; + + //应用前端类型 + @Convert(converter = ListIntegerJsonConverter.class) +// @Column(name = "clients", columnDefinition = "json") + @Column(name = "clients", columnDefinition = "TEXT") + private List clients; + + /** + * PC应用访问地址 + */ + @Column(name = "pc_url") + private String pcUrl; + + /** + * PC应用图标 图片URL + */ + @Column(name = "pc_icon") + private String pcIcon; + + /** + * App访问地址(移动端主地址) + */ + @Column(name = "app_url") + private String appUrl; + + /** + * App应用图标 图片URL + */ + @Column(name = "app_icon") + private String appIcon; + + /** + * 公众号应用地址 + */ + @Column(name = "official_account_url") + private String officialAccountUrl; + + /** + * 钉钉应用地址 + */ + @Column(name = "dingtalk_url") + private String dingtalkUrl; + + // ===================== 推送订阅配置(文档《弘育基础平台数据同步接口 2》) ===================== + + /** + * 推送外部接口地址(不含固定后缀),如 http://host/notice/api/base/ 。 + * 完整调用地址 = pushBaseUrl + 接口路径(如 group/AddGroup)。 + */ + @Column(name = "push_base_url") + private String pushBaseUrl; + + /** + * 推送总开关:0关 1开(默认 0,自动推送默认不触发)。 + */ + @Column(name = "push_enabled", nullable = false) + private Integer pushEnabled = 0; + + /** + * 启用的接口路径,逗号分隔(如 group/AddGroup,user/AddUser);空=不推送任何接口。 + */ + @Column(name = "push_event_types") + private String pushEventTypes; + + /** + * 关联学校组 id,逗号分隔;空=全局(监听所有学校变化)。 + */ + @Column(name = "push_school_ids") + private String pushSchoolIds; + + /** + * 推送角色 key,逗号分隔;空=全部角色。 + */ + @Column(name = "push_role_ids") + private String pushRoleIds; + + /** + * 推送签名密钥;空则复用 appSecret。 + */ + @Column(name = "push_secret") + private String pushSecret; + +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/entity/BaseEntity.java b/src/main/java/com/yunhai/userbase/entity/BaseEntity.java new file mode 100644 index 0000000..4ac3dcf --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/BaseEntity.java @@ -0,0 +1,63 @@ +package com.yunhai.userbase.entity; + + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +import com.yunhai.userbase.common.listener.UserAuditListener; +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/14 10:46 + * @description + */ + +@MappedSuperclass +@Getter +@Setter +@EntityListeners(UserAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = ValidationGroups.Update.class) + private Integer id; + + /** + * 创建人ID + */ + @Column(name = "create_user", nullable = false, updatable = false) + private Integer createUser; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新人ID + */ + @Column(name = "update_user") + private Integer updateUser; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(name = "update_time") + private LocalDateTime updateTime; + + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysAppRole.java b/src/main/java/com/yunhai/userbase/entity/SysAppRole.java new file mode 100644 index 0000000..c8be052 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysAppRole.java @@ -0,0 +1,68 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色。 + * 普通角色:allAppFlag=false 且 appId 必填; + * 全应用角色:allAppFlag=true 且 appId 为空; + * 注意:这里的“业务角色”不等同于 {@code sys_user.roles} 对应的系统身份枚举。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_app_role", indexes = { + @Index(name = "idx_app_role_app", columnList = "app_id"), + @Index(name = "idx_app_role_all_app", columnList = "all_app_flag"), + @Index(name = "idx_app_role_name", columnList = "name") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysAppRole extends BaseEntity { + + /** + * 角色名称。 + */ + @NotBlank + @Column(name = "name", nullable = false, length = 100) + private String name; + + /** + * 所属应用ID(逻辑外键 → ApplicationInfo.id)。 + * 普通角色必填;全应用角色为空。 + */ + @Column(name = "app_id") + private Integer appId; + + /** + * 是否全应用角色。 + * true=可跨应用勾选模块;false=仅能授权所属应用下的模块。 + */ + @NotNull + @Column(name = "all_app_flag", nullable = false) + private Boolean allAppFlag = Boolean.FALSE; + + /** + * 角色备注。 + */ + @Column(name = "remark", length = 500) + private String remark; + + /** + * 是否禁用。 + * 禁用后角色关系可保留,但权限计算时应忽略该角色。 + */ + @NotNull + @Column(name = "disabled", nullable = false) + private Boolean disabled = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java b/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java new file mode 100644 index 0000000..fda1c4b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 业务角色-模块权限关系表(逻辑外键,无物理 FK)。 + * 存在记录即代表该角色显式拥有该模块。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_app_role_module_rel", + indexes = { + @Index(name = "idx_app_role_module_role", columnList = "role_id"), + @Index(name = "idx_app_role_module_module", columnList = "module_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_role_module", columnNames = {"role_id", "module_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysAppRoleModuleRel extends BaseEntity { + + /** + * 角色ID(逻辑外键 → SysAppRole.id)。 + */ + @NotNull + @Column(name = "role_id", nullable = false) + private Integer roleId; + + /** + * 模块ID(逻辑外键 → AppModule.id)。 + */ + @NotNull + @Column(name = "module_id", nullable = false) + private Integer moduleId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroup.java b/src/main/java/com/yunhai/userbase/entity/SysGroup.java new file mode 100644 index 0000000..fcfb5b7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroup.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.GroupTypeEnumConverter; +import com.yunhai.userbase.enums.GroupTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/13 22:48 + * @description 系统节点(组)表 + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroup extends BaseEntity { + + private String name; + + @Column(name = "parent_id", nullable = false) + private Integer parentId; + + @Column(name = "type") + @Convert(converter = GroupTypeEnumConverter.class) + private GroupTypeEnum type; + + private Integer sort; + + + // 一对多反向关联中间表 + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "groupId", insertable = false, updatable = false) + private List userGroupRelList; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java b/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java new file mode 100644 index 0000000..3a99ffb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用开通表(逻辑外键,无物理 FK)。 + * 记录某个组显式开通了哪些应用;仅当本表存在记录时,「组授权」界面右侧才显示该应用。 + * 开通不继承:子组要使用应用必须自己开通。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_group_app_rel", + indexes = { + @Index(name = "idx_group_app_rel_app", columnList = "app_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_group_app", columnNames = {"group_id", "app_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroupAppRel extends BaseEntity { + + /** + * 组ID(逻辑外键 → SysGroup.id)。 + */ + @NotNull + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** + * 应用ID(逻辑外键 → ApplicationInfo.id)。 + */ + @NotNull + @Column(name = "app_id", nullable = false) + private Integer appId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java b/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java new file mode 100644 index 0000000..c59c22a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java @@ -0,0 +1,56 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组模块权限表(逻辑外键,无物理 FK)。 + * 存在记录即代表该组勾选了该模块;inheritFlag=true 时当前组及其所有子孙组自动继承该模块权限。 + * 取消勾选时物理删除记录。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_group_module_perm", + indexes = { + @Index(name = "idx_group_module_perm_module", columnList = "module_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_group_module", columnNames = {"group_id", "module_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroupModulePerm extends BaseEntity { + + /** + * 组ID(逻辑外键 → SysGroup.id)。 + */ + @NotNull + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** + * 模块ID(逻辑外键 → AppModule.id)。 + */ + @NotNull + @Column(name = "module_id", nullable = false) + private Integer moduleId; + + /** + * 是否子组继承(false=仅本组生效,true=子孙组自动继承),对应 UI「子组继承」勾选框。 + */ + @NotNull + @Column(name = "inherit_flag", nullable = false) + private Boolean inheritFlag = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java b/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java new file mode 100644 index 0000000..5ab18d6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java @@ -0,0 +1,148 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +/** + * 开放接口调用日志实体。 + * 记录 yh-app-auth 签名调用方(YschBaseOpenApi 门面背后的 /oapi/** 端点)的操作, + * 身份主体为「调用方应用」(callerAppKey/callerAppName),区别于管理端 sys_operation_log 的登录用户维度。 + * 请求参数与响应均已脱敏后入库(敏感字段见 SensitiveDataMasker)。 + * + * @author chenx + * @date 2026/8/20 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_oapi_log", indexes = { + @Index(name = "idx_create_time", columnList = "create_time"), + @Index(name = "idx_appkey_time", columnList = "caller_app_key, create_time"), + @Index(name = "idx_uri_time", columnList = "request_uri, create_time"), + @Index(name = "idx_level_time", columnList = "log_level, create_time"), + @Index(name = "idx_ip_time", columnList = "request_ip, create_time") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysOapiLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询) + */ + @Column(name = "log_level", nullable = false, columnDefinition = "TINYINT") + private Integer logLevel; + + /** + * 调用方应用 appKey(验签后写入 request attribute 的 AppAuthFilter.APP_AUTH_ATTR) + */ + @Column(name = "caller_app_key", nullable = false) + private Integer callerAppKey; + + /** + * 调用方应用名(application_info 解析;解析失败记 appKey 原值) + */ + @Column(name = "caller_app_name", length = 128) + private String callerAppName; + + /** + * 请求IP + */ + @Column(name = "request_ip", nullable = false, length = 64) + private String requestIp; + + /** + * 请求路径 + */ + @Column(name = "request_uri", nullable = false, length = 512) + private String requestUri; + + /** + * 请求方法 GET/POST/PUT/DELETE + */ + @Column(name = "request_method", nullable = false, length = 16) + private String requestMethod; + + /** + * 操作类型 DELETE/CREATE/UPDATE/QUERY + */ + @Column(name = "operation_type", nullable = false, length = 32) + private String operationType; + + /** + * 类名.方法名 + */ + @Column(name = "class_method", nullable = false, length = 512) + private String classMethod; + + /** + * 操作描述(@OapiLog 注解 value 提供,可选) + */ + @Column(name = "operation_desc", length = 255) + private String operationDesc; + + /** + * 请求参数JSON(脱敏后) + */ + @Column(name = "request_params", columnDefinition = "TEXT") + private String requestParams; + + /** + * 响应结果(GZIP 压缩,脱敏后) + */ + @Column(name = "response_data", columnDefinition = "MEDIUMBLOB") + private byte[] responseData; + + /** + * HTTP状态码 + */ + @Column(name = "status_code", nullable = false) + private Integer statusCode; + + /** + * 耗时(ms) + */ + @Column(name = "cost_time", nullable = false) + private Long costTime; + + /** + * 调用方UA + */ + @Column(name = "user_agent", length = 512) + private String userAgent; + + /** + * 错误信息 + */ + @Column(name = "error_msg", columnDefinition = "TEXT") + private String errorMsg; + + /** + * 调用时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") + private LocalDateTime createTime; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java b/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java new file mode 100644 index 0000000..ade5499 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java @@ -0,0 +1,140 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +/** + * 系统操作日志实体。 + * 记录用户的关键操作(增删改查),便于审计与问题排查。 + * + * @author chenx + * @date 2026/7/28 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_operation_log", indexes = { + @Index(name = "idx_create_time", columnList = "create_time"), + @Index(name = "idx_user_time", columnList = "user_id, create_time"), + @Index(name = "idx_uri_time", columnList = "request_uri, create_time"), + @Index(name = "idx_level_time", columnList = "log_level, create_time"), + @Index(name = "idx_ip_time", columnList = "request_ip, create_time") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysOperationLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询) + */ + @Column(name = "log_level", nullable = false, columnDefinition = "TINYINT") + private Integer logLevel; + + /** + * 操作用户ID + */ + @Column(name = "user_id", nullable = false) + private Long userId; + + /** + * 操作用户名 + */ + @Column(name = "username", nullable = false, length = 64) + private String username; + + /** + * 请求IP + */ + @Column(name = "request_ip", nullable = false, length = 64) + private String requestIp; + + /** + * 请求路径 + */ + @Column(name = "request_uri", nullable = false, length = 512) + private String requestUri; + + /** + * 请求方法 GET/POST/PUT/DELETE + */ + @Column(name = "request_method", nullable = false, length = 16) + private String requestMethod; + + /** + * 操作类型 DELETE/CREATE/UPDATE/QUERY + */ + @Column(name = "operation_type", nullable = false, length = 32) + private String operationType; + + /** + * 类名.方法名 + */ + @Column(name = "class_method", nullable = false, length = 512) + private String classMethod; + + /** + * 请求参数JSON + */ + @Column(name = "request_params", columnDefinition = "TEXT") + private String requestParams; + + /** + * 响应结果(GZIP压缩) + */ + @Column(name = "response_data", columnDefinition = "MEDIUMBLOB") + private byte[] responseData; + + /** + * HTTP状态码 + */ + @Column(name = "status_code", nullable = false) + private Integer statusCode; + + /** + * 耗时(ms) + */ + @Column(name = "cost_time", nullable = false) + private Long costTime; + + /** + * 浏览器UA + */ + @Column(name = "user_agent", length = 512) + private String userAgent; + + /** + * 错误信息 + */ + @Column(name = "error_msg", columnDefinition = "TEXT") + private String errorMsg; + + /** + * 操作时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") + private LocalDateTime createTime; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUser.java b/src/main/java/com/yunhai/userbase/entity/SysUser.java new file mode 100644 index 0000000..6a1c6ed --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUser.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yunhai.userbase.common.converter.ListIntegerJsonConverter; +import com.yunhai.userbase.common.converter.StatusEnumConverter; +import com.yunhai.userbase.common.converter.UserTypeEnumConverter; +import com.yunhai.userbase.enums.StatusEnum; +import com.yunhai.userbase.enums.UserTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDate; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/13 23:30 + * @description 系统用户表 + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_user") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUser extends BaseEntity { + + @Column(name = "username", nullable = false, unique = true) + private String account; + private String password; + //出生日期 + private LocalDate birthday; + + @JsonProperty("name") + private String realName; + + private String phone; + + private String idcard; + + private String email; + + private Integer gender; + + //头像 + private String profilePicture; + + /** + * 角色列表,存储为 MySQL JSON 类型,读取时自动映射为 List + * 取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key,例如 [1,2,3] + */ + @Convert(converter = ListIntegerJsonConverter.class) +// @Column(name = "roles", columnDefinition = "json") + @Column(name = "roles",columnDefinition = "TEXT") + private List roles; + + // 一对多:关联用户分组中间表,实现多对多业务 + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "userId", insertable = false, updatable = false) + private List userGroupRelList; + + // 是否禁用:存储为 StatusEnum(0=正常/未禁用,1=禁用),与 GroupTypeEnum 同机制;默认启用 + @Convert(converter = StatusEnumConverter.class) + private StatusEnum disabled; + //是否过期 + private Boolean expired=false; + + /** + * 用户类型:0=普通用户,1=超级管理员,2=学校管理员(见 UserTypeEnum)。 + * 由应用层根据用户账号/角色推导:内置超级管理员账号固定为 1,其余账号含 ADMIN(7) 角色则为 2,默认 0。 + */ + @Convert(converter = UserTypeEnumConverter.class) + private UserTypeEnum type; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java new file mode 100644 index 0000000..631a609 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 用户-业务角色关系表(逻辑外键,无物理 FK)。 + * 一个用户可绑定多个业务角色;一个角色也可绑定多个用户。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_user_app_role_rel", + indexes = { + @Index(name = "idx_user_app_role_user", columnList = "user_id"), + @Index(name = "idx_user_app_role_role", columnList = "role_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_user_app_role", columnNames = {"user_id", "role_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserAppRoleRel extends BaseEntity { + + /** + * 用户ID(逻辑外键 → SysUser.id)。 + */ + @NotNull + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** + * 角色ID(逻辑外键 → SysAppRole.id)。 + */ + @NotNull + @Column(name = "role_id", nullable = false) + private Integer roleId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java b/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java new file mode 100644 index 0000000..7935603 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java @@ -0,0 +1,60 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/15 15:05 + * @description 用户当前角色 + */ +@Data +@NoArgsConstructor +@Entity +@Table( + name = "sys_user_current_role", + uniqueConstraints = @UniqueConstraint(columnNames = {"userId"}), + indexes = {@Index(columnList = "userId")} +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserCurrentRole { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = ValidationGroups.Update.class) + private Integer id; + + /** + * 用户ID,一对一唯一 + */ + @Column(nullable = false) + private Integer userId; + + /** + * 用户切换选中的学校ID(根级group,数值即学校组 sys_group.id;区域/学段/根直属=0或null) + */ + private Integer schoolId; + /** + * 用户切换后生效角色ID(SysRoleEnum.key) + */ + private Integer roleId; + + /** + * 最近一次切换时间(服务端 now() 写入) + */ + @Column(name = "switch_time") + private LocalDateTime switchTime; + + /** + * 学校名冗余(避免每次 join sys_group 取 name) + */ + @Column(name = "school_name", length = 100) + private String schoolName; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java new file mode 100644 index 0000000..de68291 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java @@ -0,0 +1,54 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/15 15:03 + * @description 组用户多对多关联表 + */ + +@Data +@NoArgsConstructor +@Entity +@Table(name = "sys_user_group_rel") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserGroupRel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = Update.class) + private Integer id; + /** 用户ID */ + private Integer userId; + /** 分组ID */ + private Integer groupId; + /** 是否主归属 1=本职主学校/部门 0=兼职兼任 */ + private Integer isMain; + + /** 组内排序号:越小越靠前;null 表示未设置,列表按 user_id 兜底升序 */ + @Column(name = "sort_no") + private Integer sortNo; + + private LocalDateTime createTime; + + // 关联用户,仅查询,不维护外键 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", referencedColumnName = "id", insertable = false, updatable = false) + private SysUser user; + + // 关联分组,仅查询,不维护外键 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "groupId", referencedColumnName = "id", insertable = false, updatable = false) + private SysGroup group; + + + public @interface Update { + } +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java new file mode 100644 index 0000000..07cb6a6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java @@ -0,0 +1,86 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.StatusEnumConverter; +import com.yunhai.userbase.enums.StatusEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户-SysRoleEnum角色-学校 三元关联(平台身份角色的权威源)。 + * 区别于 {@link SysUserAppRoleRel}(业务权限角色):本表表达"用户在哪个学校下激活哪个平台身份角色" + * (如"刘文娟在 A 学校是老师"),角色取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key。 + * 不继承 BaseEntity:高频读写关系表,自定义字段更轻量,与 {@link SysUserGroupRel} 风格一致。 + */ +@Data +@NoArgsConstructor +@Entity +@Table( + name = "sys_user_role_rel", + uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "roleId", "schoolId"}), + indexes = { + @Index(columnList = "userId"), + @Index(columnList = "schoolId"), + @Index(columnList = "roleId") + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserRoleRel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = Update.class) + private Integer id; + + /** + * 用户ID + */ + @Column(nullable = false) + private Integer userId; + + /** + * 角色ID(SysRoleEnum.key,1-7) + */ + @Column(nullable = false) + private Integer roleId; + + /** + * 学校组ID(=type=SCHOOL_GROUP 的 sys_group.id);区域/学段/根直属用户填 0 + */ + @Column(nullable = false) + private Integer schoolId; + + /** + * 是否主身份标记:每 userId 最多 1 条 isMain=1(应用层校验,不靠 uk) + */ + @Column(nullable = false) + private Integer isMain = 0; + + /** + * 排序号:越小越靠前 + */ + @Column(name = "sort_no", nullable = false) + private Integer sortNo = 0; + + /** + * 状态:0=禁用 1=正常(StatusEnum) + */ + @Convert(converter = StatusEnumConverter.class) + @Column(nullable = false) + private StatusEnum status = StatusEnum.NORMAL; + + @Column(name = "create_time", nullable = false, updatable = false) + private LocalDateTime createTime; + + @Column(name = "update_time", nullable = false) + private LocalDateTime updateTime; + + public @interface Update { + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/BizType.java b/src/main/java/com/yunhai/userbase/enums/BizType.java new file mode 100644 index 0000000..625634c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/BizType.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/1/5 11:15 + * @description 虚拟节点类型 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum BizType { + + ROOT (1), + TEXTBOOK (2), + GRADE (3), + KNOWLEDGE (4), + REALLY (5), + ; + + private Integer key; + + public static BizType getByKey(Integer key){ + BizType[] enums = BizType.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/BookType.java b/src/main/java/com/yunhai/userbase/enums/BookType.java new file mode 100644 index 0000000..c4b8ad9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/BookType.java @@ -0,0 +1,50 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/1/16 9:32 + * @description 资源类型 + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum BookType { + /** 教材 */ + TextBook (1,"教材"), + /** 教辅 */ + TeachingTutoring (2,"教辅"), + /** 知识点 */ + KnowledgePoint (3,"知识点"), + ; + + private Integer key; + private String value; + + public static BookType getByKey(Integer key){ + BookType[] enums = BookType.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static String getValue(Integer key) { + for (BookType bookType : BookType.values()) { + if (bookType.getKey().equals(key)) { + return bookType.getValue(); + } + } + return null; + } + + +} diff --git a/src/main/java/com/yunhai/userbase/enums/GenderEnum.java b/src/main/java/com/yunhai/userbase/enums/GenderEnum.java new file mode 100644 index 0000000..f456f66 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/GenderEnum.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 性别 + */ +@Getter +@AllArgsConstructor +public enum GenderEnum implements IEnum { + + FEMALE(0, "女"), + MALE(1, "男"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java b/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java new file mode 100644 index 0000000..2c7fd11 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 分组类型 + */ +@Getter +@AllArgsConstructor +public enum GroupTypeEnum implements IEnum { + + NORMAL_GROUP(1, "普通组"), + SCHOOL_GROUP(2, "学校组"), + GRADE_GROUP(3, "年级组"), + CLASS_GROUP(4, "班级组"), + AREA_GROUP(5, "区域组"), + STAGE_GROUP(6, "学段组"), + TEACHER_GROUP(7, "教师组"), + STUDENT_GROUP(8, "学生组"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/IntegEnum.java b/src/main/java/com/yunhai/userbase/enums/IntegEnum.java new file mode 100644 index 0000000..bb13031 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/IntegEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 集成方式 + */ +@Getter +@AllArgsConstructor +public enum IntegEnum implements IEnum { + + INNER(0, "内置"), + WEB(1, "Web"), + APP_JUMP(3, "App跳转"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/LogLevel.java b/src/main/java/com/yunhai/userbase/enums/LogLevel.java new file mode 100644 index 0000000..606d3e8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/LogLevel.java @@ -0,0 +1,41 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志级别。 + * 1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)。 + * + * @author chenx + * @date 2026/7/28 + */ +@Getter +@AllArgsConstructor +public enum LogLevel { + + IMPORTANT(1, "重要(删除)"), + SECONDARY(2, "次要(新增/编辑)"), + GENERAL(3, "一般(查询)"); + + private final int code; + private final String desc; + + /** + * 根据 code 反查级别。 + * + * @param code 级别编码 + * @return 对应级别,未知编码默认次要 + */ + public static LogLevel fromCode(Integer code) { + if (code == null) { + return SECONDARY; + } + for (LogLevel level : values()) { + if (level.code == code) { + return level; + } + } + return SECONDARY; + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java b/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java new file mode 100644 index 0000000..a0d2c4f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/1/5 13:53 + * @description 资源来源 + */ +@AllArgsConstructor +@Getter +public enum ResourceSourceEnum { + ZIYUAN("ZIYUAN","资源库"), + LOCAL("LOCAL","平台资源库"), + ; + + private final String enKey; + private final String cnKey; + +} diff --git a/src/main/java/com/yunhai/userbase/enums/RootNode.java b/src/main/java/com/yunhai/userbase/enums/RootNode.java new file mode 100644 index 0000000..dac0df3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/RootNode.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2025/12/15 13:29 + * @description 根目录树类型 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum RootNode { + + ZIYUANKU (1,"ZIYUAN","资源"), + TUTORING (2,"TUTORING","辅导"), + QUESTION (3,"QUESTION","题库"), + SHIJUAN (4,"SHIJUAN","试卷"), + ZHUANXIANG (5,"ZHUANXIANG","专项"), + ; + + private Integer key; + private String enKey; + private String value; + + + public static RootNode getByKey(Integer key){ + RootNode[] enums = RootNode.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static RootNode fromKey(Integer key) { + for (RootNode n : values()) if (n.key.equals(key)) return n; + throw new IllegalArgumentException("非法 RootNode key: " + key); + } + + +} diff --git a/src/main/java/com/yunhai/userbase/enums/SchoolStage.java b/src/main/java/com/yunhai/userbase/enums/SchoolStage.java new file mode 100644 index 0000000..512d41b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SchoolStage.java @@ -0,0 +1,66 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2025/12/15 15:55 + * @description 学段 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum SchoolStage { + + PRIMARY (2,"PRIMARY","小学"), + MIDDLE (3,"MIDDLE","初中"), + HIGH (4,"HIGH","高中"), + ; + + private Integer key; + private String enKey; + private String value; + + + public static SchoolStage getByKey(Integer key){ + SchoolStage[] enums = SchoolStage.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static String getName(Integer key) { + for (SchoolStage stage : SchoolStage.values()) { + if (stage.getKey().equals(key)) { + return stage.getValue(); + } + } + return null; + } + + + public static SchoolStage getByValue(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return Arrays.stream(values()) + .filter(item -> item.getValue().equals(value)) + .findAny() + .orElse(null); + } + + + public static SchoolStage fromKey(Integer key) { + for (SchoolStage n : values()) if (n.key.equals(key)) return n; + throw new IllegalArgumentException("非法 SchoolStage key: " + key); + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/StatusEnum.java b/src/main/java/com/yunhai/userbase/enums/StatusEnum.java new file mode 100644 index 0000000..fe5b7b3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/StatusEnum.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 通用启用禁用状态 + */ +@Getter +@AllArgsConstructor +public enum StatusEnum implements IEnum { + + NORMAL(0, "正常"), + DISABLED(1, "禁用"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java b/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java new file mode 100644 index 0000000..fa19a8a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/25 + * @description 操作模式枚举(应用授权等场景:追加/覆盖/删除) + */ +@Getter +@AllArgsConstructor +public enum SysModeEnum implements IEnum { + + APPEND(0, "追加"), + OVERWRITE(1, "覆盖"), + DELETE(2, "删除"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } + +} diff --git a/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java b/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java new file mode 100644 index 0000000..0bb3c67 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/07/13 + * @description 用户角色枚举 + */ +@Getter +@AllArgsConstructor +public enum SysRoleEnum implements IEnum{ + + TEACHER(1, "老师"), + SCHOOL_LEADER(2, "校领导"), + STUDENT(3, "学生"), + PARENT(4, "家长"), + BUREAU_LEADER(5, "局领导"), + WORK_ACCOUNT(6, "工作号"), + ADMIN(7, "管理员"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } + +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/SystemGrade.java b/src/main/java/com/yunhai/userbase/enums/SystemGrade.java new file mode 100644 index 0000000..7bc5ac3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SystemGrade.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * @ Description : java类作用描述 + * @Author : cls + * @ CreateDate : 2020/2/21 18:39 + * @ Company : bestsch + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +public enum SystemGrade { + + one(1,"一年级",2),two(2,"二年级",2), + three(3,"三年级",2),four(4,"四年级",2), + five(5,"五年级",2),six(6,"六年级",2), + seven(7,"七年级",3),eight(8,"八年级",3), + nine(9,"九年级",3),ten(10,"高一",4), + eleven(11,"高二",4),twelve(12,"高三",4); + + private Integer gradeId; + private String gradeName; + private Integer stage; + + public static String getName(Integer gradeId) { + for (SystemGrade grade : SystemGrade.values()) { + if (grade.getGradeId().equals(gradeId)) { + return grade.gradeName; + } + } + return null; + } + +} diff --git a/src/main/java/com/yunhai/userbase/enums/UserType.java b/src/main/java/com/yunhai/userbase/enums/UserType.java new file mode 100644 index 0000000..73c9e56 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/UserType.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +/** + * @program: UserProfile + * @description: 用户类别 + * @author: chenx + * @create: 2023-05-26 10:46 + **/ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum UserType { + + TEACHER (1,"教师","Teacher"), + STUDENT (2,"学生","Student"), + ; + + private Integer key; + private String value; + private String code; + +} diff --git a/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java b/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java new file mode 100644 index 0000000..beb2653 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java @@ -0,0 +1,36 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/08/03 + * @description 用户类型枚举 + * - NORMAL(0) 普通用户:默认类型 + * - SUPER_ADMIN(1) 超级管理员:系统内置种子账号(yunhaisch / yunhaisch2 / yunhaisch3),由初始化脚本写入 + * - SCHOOL_ADMIN(2) 学校管理员:拥有管理身份(角色含 ADMIN(7))的普通账号(非超级管理员) + */ +@Getter +@AllArgsConstructor +public enum UserTypeEnum implements IEnum { + + NORMAL(0, "普通用户"), + SUPER_ADMIN(1, "超级管理员"), + SCHOOL_ADMIN(2, "学校管理员"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java b/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java new file mode 100644 index 0000000..ac0aabf --- /dev/null +++ b/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.event; + +import com.yunhai.userbase.entity.ApplicationInfo; +import org.springframework.context.ApplicationEvent; + +/** + * 应用密钥/元数据变更事件:在 ApplicationInfoService 的 save/add/edit/delete/resetSecret 等事务内发布, + * 由 SignKeyCacheService 的 @TransactionalEventListener(phase = AFTER_COMMIT 订阅, + * 保证 DB 真正提交成功后再写共享 Redis / 清本地缓存,避免事务回滚导致的脏数据。 + */ +public class ApplicationInfoCacheChangedEvent extends ApplicationEvent { + + public enum Type { + UPSERT, DELETE + } + + private final Type type; + private final Integer appKey; + + /** + * 删除事件:appKey 必填;upsert 事件:可以传 entity 非空。 + */ + private final ApplicationInfo upsertEntity; + + public ApplicationInfoCacheChangedEvent(Object source, Type type, Integer appKey, ApplicationInfo upsertEntity) { + super(source); + this.type = type; + this.appKey = appKey; + this.upsertEntity = upsertEntity; + } + + public static ApplicationInfoCacheChangedEvent upsert(Object source, ApplicationInfo entity) { + return new ApplicationInfoCacheChangedEvent(source, Type.UPSERT, entity.getAppKey(), entity); + } + + public static ApplicationInfoCacheChangedEvent delete(Object source, Integer appKey) { + return new ApplicationInfoCacheChangedEvent(source, Type.DELETE, appKey, null); + } + + public Type getType() { return type; } + + public Integer getAppKey() { return appKey; } + + public ApplicationInfo getUpsertEntity() { return upsertEntity; } +} diff --git a/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java new file mode 100644 index 0000000..267a2aa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.event; + +import com.yunhai.userbase.model.push.PushInterfaceType; +import lombok.Data; + +import java.util.List; + +/** + * 数据同步事件:组 / 用户变更后由 {@code SysGroupService}/{@code SysUserService} 在事务内 publish, + * {@code PushDispatcher} 用 {@code @TransactionalEventListener(AFTER_COMMIT)} 消费(事务提交后才推送, + * 推送失败不影响主业务)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class DataSyncEvent { + + /** 接口类型,如 group/AddGroup */ + private PushInterfaceType type; + /** 组事件携带的受影响组 id 列表 */ + private List groupIds; + /** 用户事件携带的受影响用户 id 列表 */ + private List userIds; + /** 组维度的关联事件(如删除用户=解除组关系)携带的组 id */ + private Integer groupId; + /** 触发本次变更的 oapi 调用方 appKey;非 oapi 来源(如管理后台)为 null。 + * 推送分发器据此回推时排除来源应用自身,避免收到自己的回声。 */ + private Integer sourceAppKey; +} diff --git a/src/main/java/com/yunhai/userbase/model/BaseDict.java b/src/main/java/com/yunhai/userbase/model/BaseDict.java new file mode 100644 index 0000000..5873a5e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BaseDict.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model; + +/** + * @program: QualityEvaluate + * @description: 系统字典类 + * @author: chenx + * @create: 2023-05-15 17:17 + **/ +public class BaseDict { + public Integer k; + public String v; + + + public int getK() { + return this.k; + } + + public void setK(int k) { + this.k = k; + } + + public String getV() { + return this.v; + } + + public void setV(String v) { + this.v = v; + } + + public BaseDict(Integer k, String v) { + this.k = k; + this.v = v; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/BasePageQuery.java b/src/main/java/com/yunhai/userbase/model/BasePageQuery.java new file mode 100644 index 0000000..ca04263 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BasePageQuery.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.model; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/21 21:41 + * @description + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/model/BasePageResult.java b/src/main/java/com/yunhai/userbase/model/BasePageResult.java new file mode 100644 index 0000000..7e60ed1 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BasePageResult.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + * 由 Controller 层负责装配(见 {@link com.yunhai.userbase.utils.PageUtils#toResult}), + * Service 层仍返回 Spring Data 的 {@link org.springframework.data.domain.Page},保持与传输协议解耦。 + * + * @author chenx + * @date 2026/7/21 21:40 + * @description 统一分页响应外壳 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起,与 {@link BasePageQuery#getPageIndex()} 约定一致) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/BaseTree.java b/src/main/java/com/yunhai/userbase/model/BaseTree.java new file mode 100644 index 0000000..f8f93cc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BaseTree.java @@ -0,0 +1,19 @@ +package com.yunhai.userbase.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BaseTree { + public Integer id; + public String name; + public Integer pid ; + public Integer type; + public List children; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java b/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java new file mode 100644 index 0000000..edc2d24 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.Excel; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author chenx + * @date 2026/1/19 19:45 + * @description 教材导入 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ExcelKnowledge { + + @Excel(name = "第一层", width = 20) + private String first; + + @Excel(name = "第二层", width = 20) + private String second; + + @Excel(name = "第三层", width =20) + private String third; + + @Excel(name = "第四层", width = 20) + private String fourth; + + @Excel(name = "第五层", width = 20) + private String five; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java b/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java new file mode 100644 index 0000000..68256ad --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.model.Excel; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +/** + * @program: ResearchLearn + * @description: 导入研学活动填报实体类 + * @author: chenx + * @create: 2023-07-21 14:12 + **/ + +@Data +public class ExcelResearchFill { + + @Excel(name = "年级") + private String gradeName; + + @Excel(name = "班级") + private String className; + + @Excel(name = "姓名") + private String stuName; + + @Excel(name = "研学/活动名称") + private String researchName; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Page.java b/src/main/java/com/yunhai/userbase/model/Page.java new file mode 100644 index 0000000..e58b366 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Page.java @@ -0,0 +1,72 @@ +package com.yunhai.userbase.model; + +import org.apache.commons.lang3.StringUtils; + +public class Page { + private int current; + private int pageSize; + private long count; + + public Page() { + } + + public int getCurrent() { + return this.current; + } + + public void setCurrent(int current) { + this.current = current; + } + + public int getPageSize() { + return this.pageSize == 0 ? 10 : this.pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public void setPageSize(String pageSize) { + if (!StringUtils.isEmpty(pageSize) && StringUtils.isNumeric(pageSize)) { + this.pageSize = Integer.valueOf(pageSize); + } + + } + + public long getCount() { + return this.count; + } + + public void setCount(long count) { + this.count = count; + } + + + + public int getPageCount() { + return this.getPageSize() != 0 ? (int)Math.ceil((double)this.count / (double)this.getPageSize()) : 0; + } + + public int getStartIndex() { + return (this.getCurrent() - 1) * this.getPageSize(); + } + + public void setCurrent(String current) { + if (!StringUtils.isEmpty(current) && StringUtils.isNumeric(current)) { + this.current = Integer.valueOf(current); + } + + } + + public void setCount(String count) { + if (!StringUtils.isEmpty(count) && StringUtils.isNumeric(count)) { + this.count = Long.valueOf(count); + } + + } + + public int getEndIndex() { + return this.getStartIndex() + this.getPageSize(); + } + +} diff --git a/src/main/java/com/yunhai/userbase/model/StartEndDate.java b/src/main/java/com/yunhai/userbase/model/StartEndDate.java new file mode 100644 index 0000000..94c1ea7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/StartEndDate.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.model; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StartEndDate { + private Date startDate; + private Date endDate; +} diff --git a/src/main/java/com/yunhai/userbase/model/Tree.java b/src/main/java/com/yunhai/userbase/model/Tree.java new file mode 100644 index 0000000..2523235 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Tree.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Tree { + private String label; + private String value; + private Integer type = 0; + private List children; +} diff --git a/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java new file mode 100644 index 0000000..15248e8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java @@ -0,0 +1,37 @@ +package com.yunhai.userbase.model.appauthz; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 应用授权查询的扁平投影行(含 categoryId 供分组)。 + * 仅取授权列表所需字段,避免加载 appSecret 等无关大字段。 + * + * @author chenx + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppAuthzAppRow { + + /** + * 应用类型ID(分组键)。 + */ + private Integer categoryId; + + /** + * 应用ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 应用图标(pcIcon)。 + */ + private String icon; +} diff --git a/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java new file mode 100644 index 0000000..7021c9b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java @@ -0,0 +1,48 @@ +package com.yunhai.userbase.model.appauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 应用授权视图:应用类型 + 该类型下的应用列表。 + * + * @author chenx + */ +@Data +public class AppAuthzCategoryVo { + + /** + * 应用类型ID。 + */ + private Integer id; + + /** + * 应用类型名称。 + */ + private String name; + + /** + * 该类型下的应用列表。 + */ + private List app = new ArrayList<>(); + + /** + * 授权用的应用精简项(id/name/icon)。 + */ + @Data + public static class AppItem { + + /** + * 应用ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + } +} diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java new file mode 100644 index 0000000..e0d659d --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java @@ -0,0 +1,13 @@ +package com.yunhai.userbase.model.appcategory; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryQuery { + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java new file mode 100644 index 0000000..be296a3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.appcategory; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "类别名称不能为空") + private String name; + + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java new file mode 100644 index 0000000..65a2060 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.appcategory; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryVo { + private Integer id; + private String name; + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java new file mode 100644 index 0000000..a78b759 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 应用信息查询条件。 + * 用于列表查询时封装前端传入的筛选参数。 + * + * @author chenx + */ +@Data +public class ApplicationInfoQuery { + /** + * 应用名称,用于模糊匹配。 + */ + private String name; + + /** + * 应用类别ID,用于精确筛选。 + */ + private Integer categoryId; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java new file mode 100644 index 0000000..99abc9a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java @@ -0,0 +1,76 @@ +package com.yunhai.userbase.model.application; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 应用信息新增/修改入参。 + * 用于接收前端提交的应用数据,并按分组做参数校验。 + * 说明:appKey/appSecret 为系统生成字段,不在此入参中暴露。 + * + * @author chenx + */ +@Data +public class ApplicationInfoRequest { + /** + * 主键ID,编辑时必填。 + */ + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** + * 应用名称,新增与编辑均必填。 + */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "应用名称不能为空") + private String name; + + /** + * 应用类别ID,新增与编辑均必填。 + */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "应用类别不能为空") + private Integer categoryId; + + /** + * 应用描述。 + */ + private String desc; + + /** + * 应用前端类型列表。 + */ + private List clients; + + /** + * PC应用访问地址。 + */ + private String pcUrl; + + /** + * PC应用图标地址。 + */ + private String pcIcon; + + /** + * App访问地址(移动端主地址)。 + */ + private String appUrl; + + /** + * App应用图标地址。 + */ + private String appIcon; + + /** + * 公众号应用地址。 + */ + private String officialAccountUrl; + + /** + * 钉钉应用地址。 + */ + private String dingtalkUrl; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java new file mode 100644 index 0000000..ddc0a93 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model.application; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 应用信息返回视图。 + * 用于接口返回给前端的应用数据。 + * + * @author chenx + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApplicationInfoSortVo { + /** + * 主键ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 排序号,仅在同一个应用类型内有效;未排序时为 null。 + */ + private Integer sortNo; + +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java new file mode 100644 index 0000000..5b669ef --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java @@ -0,0 +1,84 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +import java.util.List; + +/** + * 应用信息返回视图。 + * 用于接口返回给前端的应用数据。 + * + * @author chenx + */ +@Data +public class ApplicationInfoVo { + /** + * 主键ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 应用类别ID。 + */ + private Integer categoryId; + + /** + * 排序号,仅在同一个应用类型内有效;未排序时为 null。 + */ + private Integer sortNo; + + /** + * 应用描述。 + */ + private String desc; + + /** + * 系统生成的唯一标识(appKey),整型,从100000起自增。 + */ + private Integer appKey; + + /** + * 密钥(appSecret),与数据库 app_secret 字段一致,字符串类型。 + */ + private String appSecret; + + /** + * 应用前端类型列表。 + */ + private List clients; + + /** + * PC应用访问地址。 + */ + private String pcUrl; + + /** + * PC应用图标地址。 + */ + private String pcIcon; + + /** + * App访问地址。 + */ + private String appUrl; + + /** + * App应用图标地址。 + */ + private String appIcon; + + /** + * 公众号应用地址。 + */ + private String officialAccountUrl; + + /** + * 钉钉应用地址。 + */ + private String dingtalkUrl; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java b/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java new file mode 100644 index 0000000..a6d58a4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 保存应用推送设置请求(应用列表「推送设置」按钮提交)。 + * 所有字段均为可选更新:传 null 表示不修改该项。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushSettingRequest { + + /** 推送外部接口地址(不含固定后缀) */ + private String pushBaseUrl; + /** 推送总开关:0关 1开 */ + private Integer pushEnabled; + /** 启用的接口路径,逗号分隔 */ + private String pushEventTypes; + /** 关联学校组 id,逗号分隔;空=全局 */ + private String pushSchoolIds; + /** 推送角色 key,逗号分隔;空=全部角色 */ + private String pushRoleIds; + /** + * 推送签名密钥;传 null 表示不修改(保留现有值), + * 传空串 "" 表示清空(后续复用 appSecret)。 + */ + private String pushSecret; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java b/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java new file mode 100644 index 0000000..38ce68f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 应用推送设置回显 VO(GetPushSetting 返回,仅含推送相关字段,不含 appSecret 等无关大字段)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushSettingVo { + + private Integer id; + private Integer appKey; + private String name; + private String pushBaseUrl; + private Integer pushEnabled; + private String pushEventTypes; + private String pushSchoolIds; + private String pushRoleIds; + /** 推送签名密钥(可展示供复制;空=复用 appSecret) */ + private String pushSecret; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java new file mode 100644 index 0000000..e16dc7c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model.appmodule; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块查询条件。 + */ +@Data +public class AppModuleQuery { + /** 所属应用ID(按应用过滤模块树)。 */ + private Integer appId; + /** 模块名称(模糊匹配)。 */ + private String name; + /** 父模块ID(传入则以该模块为根,递归返回其完整子树;不传则顶层只显示 level-1 根模块)。 */ + private Integer parentId; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java new file mode 100644 index 0000000..ff49dfd --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java @@ -0,0 +1,36 @@ +package com.yunhai.userbase.model.appmodule; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块请求体。 + */ +@Data +public class AppModuleRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** 所属应用ID(创建时必填,创建后不可改)。 */ + @NotNull(groups = ValidationGroups.Create.class, message = "所属应用不能为空") + private Integer appId; + + /** 父模块ID;根模块传 null。 */ + private Integer parentId; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "模块名称不能为空") + private String name; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "自定义ID不能为空") + private String customId; + + /** 模块路径(前端路由/资源路径)。 */ + private String path; + + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java new file mode 100644 index 0000000..85641b0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.appmodule; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块视图(树形,children 递归存放子模块)。 + */ +@Data +public class AppModuleVo { + private Integer id; + private Integer appId; + private Integer parentId; + private String name; + private String customId; + private String path; + private Integer level; + private Integer sortNo; + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java new file mode 100644 index 0000000..0d41d16 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 角色授权页应用节点视图:应用信息 + 其下模块权限树。 + * 普通角色只返回其所属应用;全应用角色返回系统全部应用。 + * + * @author chenx + */ +@Data +public class AppRoleAuthzAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC 应用图标地址。 */ + private String pcIcon; + + /** 排序号。 */ + private Integer sortNo; + + /** 该应用下的模块权限树(含勾选回显)。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java new file mode 100644 index 0000000..1ece8da --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 角色授权页模块节点视图(树形):模块信息 + 该角色勾选状态。 + * + * @author chenx + */ +@Data +public class AppRoleAuthzModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 该角色是否已勾选该模块(sys_app_role_module_rel 存在记录即勾选)。 */ + private Boolean checked = Boolean.FALSE; + + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java new file mode 100644 index 0000000..25949f8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 角色模块授权保存入参(全量覆盖)。 + * moduleIds 为该角色本次勾选的模块ID全量列表;传空列表即清空该角色全部模块授权。 + * + * @author chenx + */ +@Data +public class AppRoleModuleAssignRequest { + + /** + * 角色ID。 + */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** + * 勾选的模块ID全量列表。 + */ + private List moduleIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java new file mode 100644 index 0000000..8a4b952 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 角色绑定用户保存入参(全量覆盖,角色维度)。 + * userIds 为该角色本次绑定的用户ID全量列表;传空列表即解绑该角色全部用户。 + * + * @author chenx + */ +@Data +public class AppRoleUserBindRequest { + + /** + * 角色ID。 + */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** + * 绑定的用户ID全量列表。 + */ + private List userIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java new file mode 100644 index 0000000..d6e66a9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 角色已绑定用户列表项视图。 + * + * @author chenx + */ +@Data +public class AppRoleUserVo { + + /** 用户ID。 */ + private Integer id; + + /** 账号。 */ + private String account; + + /** 真实姓名。 */ + private String realName; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java new file mode 100644 index 0000000..6f20bc3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 业务角色列表查询条件。 + * + * @author chenx + */ +@Data +public class SysAppRoleQuery { + + /** 角色名称(模糊匹配)。 */ + private String name; + + /** 所属应用ID(精确匹配,仅命中普通角色)。 */ + private Integer appId; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 是否禁用。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java new file mode 100644 index 0000000..7140af5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java @@ -0,0 +1,50 @@ +package com.yunhai.userbase.model.approle; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 业务角色新增/修改入参。 + * 普通角色:allAppFlag=false 且 appId 必填;全应用角色:allAppFlag=true 且 appId 必须为空(服务层校验)。 + * + * @author chenx + */ +@Data +public class SysAppRoleRequest { + + /** + * 主键ID,编辑时必填。 + */ + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** + * 角色名称,新增与编辑均必填。 + */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "角色名称不能为空") + private String name; + + /** + * 所属应用ID;普通角色必填,全应用角色必须为空(appId 与 allAppFlag 的匹配关系在服务层校验)。 + */ + private Integer appId; + + /** + * 是否全应用角色,新增与编辑均必填。 + */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "角色类型不能为空") + private Boolean allAppFlag; + + /** + * 角色备注。 + */ + private String remark; + + /** + * 是否禁用;空值按不禁用处理。 + */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java new file mode 100644 index 0000000..b4826cb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 业务角色列表项视图。 + * + * @author chenx + */ +@Data +public class SysAppRoleVo { + + /** 角色ID。 */ + private Integer id; + + /** 角色名称。 */ + private String name; + + /** 所属应用ID;全应用角色为 null。 */ + private Integer appId; + + /** 所属应用名称;全应用角色为 null。 */ + private String appName; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 角色备注。 */ + private String remark; + + /** 是否禁用。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java b/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java new file mode 100644 index 0000000..18e038e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 用户绑定角色保存入参(全量覆盖,用户维度)。 + * roleIds 为该用户本次绑定的角色ID全量列表;传空列表即解绑该用户全部角色。 + * + * @author chenx + */ +@Data +public class UserAppRoleBindRequest { + + /** + * 用户ID。 + */ + @NotNull(message = "用户ID不能为空") + private Integer userId; + + /** + * 绑定的角色ID全量列表。 + */ + private List roleIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java new file mode 100644 index 0000000..7b63405 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 应用开通保存请求(功能1,全量覆盖式)。 + * 前端传递组ID与勾选的应用ID列表;appIds 为空或空列表代表取消该组全部应用开通。 + */ +@Data +public class GroupAppAssignRequest { + + /** 组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 勾选的应用ID列表(全量);为空代表全部取消开通。 */ + private List appIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java new file mode 100644 index 0000000..6a8b799 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/23 + * @description 应用开通视图(功能1):系统全量应用 + 该组是否已开通。 + */ +@Data +public class GroupAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用类别ID。 */ + private Integer categoryId; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC应用图标地址。 */ + private String pcIcon; + + /** 该组是否已开通(用于复选框回显)。 */ + private Boolean checked = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java new file mode 100644 index 0000000..c90d124 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权应用视图(功能2):该组已开通的应用 + 其下模块权限树。 + */ +@Data +public class GroupAuthzAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC应用图标地址。 */ + private String pcIcon; + + /** 应用类型ID;用于按应用分类分组展示组授权面板。 */ + private Integer categoryId; + + private Integer sortNo; + + /** 该应用下的模块权限树;默认空列表,避免 JSON 序列化出现 null。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java new file mode 100644 index 0000000..515af61 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 组授权分类视图:按应用类型分组后,每个分类下挂载已开通的应用及其模块权限树。 + * + * @author chenx + * @date 2026/7/23 + */ +@Data +public class GroupAuthzCategoryVo { + + /** 应用类型ID。 */ + private Integer id; + + /** 应用类型名称。 */ + private String name; + + /** 该分类下已开通的应用列表(含模块权限树)。 */ + private List apps = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java new file mode 100644 index 0000000..3bc34a0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权模块节点视图(功能2,树形):模块信息 + 该组勾选状态与子组继承开关。 + */ +@Data +public class GroupAuthzModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 该组是否已勾选该模块(sys_group_module_perm 存在记录即勾选)。 */ + private Boolean checked = Boolean.FALSE; + + /** 是否子组继承(仅 checked=true 时有意义)。 */ + private Boolean inheritFlag = Boolean.FALSE; + + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java new file mode 100644 index 0000000..ef3b2c4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 组模块授权单项(增量式:对某个组的「某应用下的某模块」做追加/删除)。 + * 与 GroupModulePermRequest(全量覆盖式 SaveModulePerms)互为补充:本类用于单条/批量增量操作, + * 由前端「组授权」面板逐条勾选或取消时提交。 + * + * 字段语义(务必与前端对齐): + * - groupId:目标组ID + * - appId:应用ID(被授权的模块必须属于该应用,越权写别的应用的模块会被拒绝) + * - moduleId:模块ID;传 0 表示「该应用下的全部模块」(前端勾选应用根/整应用时用) + * - mode:操作模式,取值见 SysModeEnum —— 0=追加(APPEND,授予授权) / 2=删除(DELETE,取消授权);1=覆盖 不适用于单条增量,会被拒绝 + * - isAll:是否对子组继承,直接映射现有 sys_group_module_perm.inherit_flag 列 —— 1=子孙组自动继承 / 0=仅本组生效 + * + * @author chenx + */ +@Data +public class GroupModuleAuthzItem { + + /** 目标组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 应用ID(模块必须属于该应用)。 */ + @NotNull(message = "应用ID不能为空") + private Integer appId; + + /** + * 模块ID;传 0 表示「该应用下全部模块」。 + * 注意:后端不会向 sys_group_module_perm 写入 module_id=0 的伪记录, + * 而是把 0 展开为应用下每个真实模块各写一条权限记录。 + */ + @NotNull(message = "模块ID不能为空(整应用传 0)") + private Integer moduleId; + + /** 操作模式:0=追加 / 2=删除(1=覆盖不适用)。 */ + @NotNull(message = "操作模式不能为空(0=追加/2=删除)") + private Integer mode; + + /** 是否对子组继承:1=子孙组继承 / 0=仅本组。映射 inherit_flag。 */ + @NotNull(message = "isAll不能为空(0=仅本组/1=子组继承)") + private Integer isAll; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java new file mode 100644 index 0000000..33a4202 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权保存请求(功能2,全量覆盖式)。 + * perms 为该组当前勾选的模块权限全量列表:有旧记录则更新 inheritFlag,无则新增, + * 不在列表中的旧记录物理删除(即取消勾选)。 + */ +@Data +public class GroupModulePermRequest { + + /** 组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 勾选的模块权限全量列表;为空代表全部取消勾选。 */ + @Valid + private List perms; + + /** 单个模块权限项。 */ + @Data + public static class ModulePermItem { + + /** 模块ID。 */ + @NotNull(message = "模块ID不能为空") + private Integer moduleId; + + /** 是否子组继承;不传默认 false。 */ + private Boolean inheritFlag; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java new file mode 100644 index 0000000..af3d91c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 学校组应用授权增量请求(对应前端点击单个应用多选框勾选/取消时提交)。 + * 与 {@link GroupAppAssignRequest}(全量覆盖式 AssignApps)互为补充:本类用于「单条」增量开通或取消应用, + * 前端一次只传一个应用ID(逐次点击触发),因此 appid 为单个 Integer,而非集合/逗号串。 + * + * 字段语义(务必与前端对齐): + * - schid:目标学校组ID(即左侧树点击的 nodeId) + * - appid:单个应用ID(每次只传一个) + * - mode:操作模式,取值见 SysModeEnum —— 0=追加(APPEND,开通应用) / 2=删除(DELETE,取消开通);1=覆盖 不适用于单条增量,会被拒绝 + * + * 设计说明:每次请求只携带一个 appid,控制器/服务均不再做多值解析;逐次点击即多次提交。 + * + * @author chenx + */ +@Data +public class SchoolAppAuthzRequest { + + /** 目标学校组ID(左侧树节点 nodeId)。 */ + @NotNull(message = "组ID不能为空") + private Integer schid; + + /** + * 单个应用ID。 + * 前端逐次点击应用多选框触发,每次只传一个应用ID,故为 Integer 而非集合/字符串。 + */ + @NotNull(message = "应用ID不能为空") + private Integer appid; + + /** 操作模式:0=追加 / 2=删除(1=覆盖不适用)。 */ + @NotNull(message = "操作模式不能为空(0=追加/2=删除)") + private Integer mode; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java new file mode 100644 index 0000000..5212e4f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户所属部门树节点(左侧上方「所属部门」区域)。 + * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组), + * 不包含无关的兄弟/子孙节点。 + * + * @author chenx + */ +@Data +public class UserGroupNodeVo { + + /** 组ID。 */ + private Integer id; + + /** 组名称。 */ + private String name; + + /** 是否用户直接归属的组(叶子高亮用);祖先组为 false。 */ + private Boolean own; + + /** 子节点(仅含祖先链上的节点)。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java new file mode 100644 index 0000000..23a1394 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权应用节点。 + * 父节点为 {@link UserPermissionCategoryVo},子节点为 {@link UserPermissionModuleVo}。 + */ +@Data +public class UserPermissionAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC 应用图标地址。 */ + private String pcIcon; + + /** 排序号。 */ + private Integer sortNo; + + /** 该应用下已授权的模块树。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java new file mode 100644 index 0000000..360e92f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java @@ -0,0 +1,26 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权应用分类节点。 + * 一级子节点为 {@link UserPermissionAppVo}。 + */ +@Data +public class UserPermissionCategoryVo { + + /** 分类ID。 */ + private Integer id; + + /** 分类名称。 */ + private String name; + + /** 排序号。 */ + private Integer sortNo; + + /** 该分类下已授权的应用列表。 */ + private List apps = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java new file mode 100644 index 0000000..50edcc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权模块节点(递归树)。 + * 仅包含有权限的模块,无权限的模块及其子节点均不显示。 + */ +@Data +public class UserPermissionModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 子模块(递归)。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java new file mode 100644 index 0000000..4ec8fb0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * 权限一览:用户列表项(组下用户或全局搜索用户)。 + */ +@Data +public class UserPermissionUserVo { + + /** 用户ID。 */ + private Integer id; + + /** 账号。 */ + private String account; + + /** 真实姓名。 */ + private String realName; + + /** 是否主归属:1=本职主学校/部门,0=兼职兼任。 */ + private Integer isMain; + + /** 用户所属部门/组织路径,如 /上海市仙霞高级中学/教师/行政。 */ + private String groupPath; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java new file mode 100644 index 0000000..7e5adc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * 权限一览:用户所属业务角色列表项(中间下方「所属角色」区域)。 + * + * @author chenx + */ +@Data +public class UserRoleVo { + + /** 角色ID。 */ + private Integer id; + + /** 角色名称。 */ + private String name; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 所属应用ID;全应用角色为 null。 */ + private Integer appId; + + /** 所属应用名称;全应用角色为 null。 */ + private String appName; + + /** 是否禁用(禁用角色仍展示,但不参与权限计算)。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java b/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java new file mode 100644 index 0000000..60adcaa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.identity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 当前激活身份(来自 {@code sys_user_current_role};无切换记录时兜底主身份)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityCurrentVo { + + /** 当前激活角色ID(SysRoleEnum.key) */ + private Integer roleId; + + /** 当前激活角色名 */ + private String roleName; + + /** 当前激活学校ID(=学校组 sys_group.id;无学校时为 0) */ + private Integer schoolId; + + /** 当前激活学校名 */ + private String schoolName; + + /** 是否兜底(无主动切换记录时为 true,提示前端可引导用户显式切换) */ + private Boolean fallback; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java b/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java new file mode 100644 index 0000000..c06e657 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.identity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户可选身份(一条 = 某学校下的某平台角色)。 + * 数据源:{@code sys_user_role_rel}(平台身份角色权威源)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityOptionVo { + + /** 角色ID(SysRoleEnum.key,1-7) */ + private Integer roleId; + + /** 角色名(老师/校领导/学生…,查枚举快取) */ + private String roleName; + + /** 学校ID(=学校组 sys_group.id;区域/学段/直属无学校时为 0) */ + private Integer schoolId; + + /** 学校名(冗余,免 join sys_group) */ + private String schoolName; + + /** 是否主身份(isMain=1) */ + private Boolean isMain; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java b/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java new file mode 100644 index 0000000..f7f88d7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.identity; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 切换身份入参:指定目标学校与该学校下的角色。 + * 服务端校验 (userId, roleId, schoolId) 在 {@code sys_user_role_rel} 中存在且正常态, + * 不存在则拒绝(防止越权切换到非授权身份)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +public class IdentitySwitchRequest { + + /** 目标角色ID(SysRoleEnum.key) */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** 目标学校ID(=学校组 sys_group.id;无学校维度传 0) */ + @NotNull(message = "学校ID不能为空") + private Integer schoolId; +} diff --git a/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java new file mode 100644 index 0000000..37a5acd --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java @@ -0,0 +1,82 @@ +package com.yunhai.userbase.model.oapilog; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 开放接口日志列表项视图。 + * 不含 response_data(响应结果),列表不展示,需通过下载接口单独获取。 + * + * @author chenx + * @date 2026/8/20 + */ +@Data +public class SysOapiLogListVo { + + /** 主键 */ + private Long id; + /** 日志级别 1-重要 2-次要 3-一般 */ + private Integer logLevel; + /** 调用方应用 appKey */ + private Integer callerAppKey; + /** 调用方应用名 */ + private String callerAppName; + /** 请求IP */ + private String requestIp; + /** 请求路径 */ + private String requestUri; + /** 请求方法 GET/POST/PUT/DELETE */ + private String requestMethod; + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + /** 类名.方法名 */ + private String classMethod; + /** 操作描述 */ + private String operationDesc; + /** 请求参数JSON(脱敏后) */ + private String requestParams; + /** 响应结果(GZIP 解压后原文,脱敏后;随列表带出,详情弹窗直接展示) */ + private String responseData; + /** HTTP 状态码 */ + private Integer statusCode; + /** 耗时(ms) */ + private Long costTime; + /** 调用方UA */ + private String userAgent; + /** 错误信息 */ + private String errorMsg; + /** 调用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 全参构造,顺序需与 {@code SysOapiLog} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public SysOapiLogListVo(Long id, Integer logLevel, Integer callerAppKey, String callerAppName, + String requestIp, String requestUri, String requestMethod, + String operationType, String classMethod, String operationDesc, + String requestParams, Integer statusCode, Long costTime, + String userAgent, String errorMsg, LocalDateTime createTime) { + this.id = id; + this.logLevel = logLevel; + this.callerAppKey = callerAppKey; + this.callerAppName = callerAppName; + this.requestIp = requestIp; + this.requestUri = requestUri; + this.requestMethod = requestMethod; + this.operationType = operationType; + this.classMethod = classMethod; + this.operationDesc = operationDesc; + this.requestParams = requestParams; + this.statusCode = statusCode; + this.costTime = costTime; + this.userAgent = userAgent; + this.errorMsg = errorMsg; + this.createTime = createTime; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java new file mode 100644 index 0000000..cfbad9b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java @@ -0,0 +1,59 @@ +package com.yunhai.userbase.model.oapilog; + +import lombok.Data; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 开放接口日志查询条件。 + * 覆盖调用方应用、URI、级别、IP、时间等高频查询场景,与表索引前缀对齐。 + * + * @author chenx + * @date 2026/8/20 + */ +@Data +public class SysOapiLogQuery { + + /** 日志级别(1-重要 2-次要 3-一般) */ + private Integer logLevel; + + /** 调用方应用 appKey(等值) */ + private Integer callerAppKey; + + /** 调用方应用名(模糊) */ + private String callerAppName; + + /** 请求路径(模糊) */ + private String requestUri; + + /** 请求IP(模糊) */ + private String requestIp; + + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + + /** HTTP 状态码 */ + private Integer statusCode; + + /** + * 成功/失败筛选。 + * true=仅 HTTP 200(成功),false=非 200(失败)。 + */ + private Boolean success; + + /** + * 关键字,同时匹配请求路径(requestUri)与应用名(callerAppName)。 + * 前端单输入框搜索这两个字段。 + */ + private String keyword; + + /** 调用起始时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + /** 调用结束时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java new file mode 100644 index 0000000..c731bfc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.model.oplog; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 操作日志列表项视图。 + * 不含 response_data(响应结果),列表不展示,需通过下载接口单独获取。 + * + * @author chenx + * @date 2026/7/28 + */ +@Data +public class SysOperationLogListVo { + + /** 主键 */ + private Long id; + /** 日志级别 1-重要 2-次要 3-一般 */ + private Integer logLevel; + /** 操作用户ID */ + private Long userId; + /** 操作用户名 */ + private String username; + /** 请求IP */ + private String requestIp; + /** 请求路径 */ + private String requestUri; + /** 请求方法 GET/POST/PUT/DELETE */ + private String requestMethod; + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + /** 类名.方法名 */ + private String classMethod; + /** 请求参数JSON */ + private String requestParams; + /** HTTP 状态码 */ + private Integer statusCode; + /** 耗时(ms) */ + private Long costTime; + /** 浏览器UA */ + private String userAgent; + /** 错误信息 */ + private String errorMsg; + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 全参构造,顺序需与 {@code SysOperationLog} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public SysOperationLogListVo(Long id, Integer logLevel, Long userId, String username, + String requestIp, String requestUri, String requestMethod, + String operationType, String classMethod, String requestParams, + Integer statusCode, Long costTime, String userAgent, + String errorMsg, LocalDateTime createTime) { + this.id = id; + this.logLevel = logLevel; + this.userId = userId; + this.username = username; + this.requestIp = requestIp; + this.requestUri = requestUri; + this.requestMethod = requestMethod; + this.operationType = operationType; + this.classMethod = classMethod; + this.requestParams = requestParams; + this.statusCode = statusCode; + this.costTime = costTime; + this.userAgent = userAgent; + this.errorMsg = errorMsg; + this.createTime = createTime; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java new file mode 100644 index 0000000..ca67306 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java @@ -0,0 +1,59 @@ +package com.yunhai.userbase.model.oplog; + +import lombok.Data; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 操作日志查询条件。 + * 覆盖时间、用户、URI、级别、IP 等高频查询场景,与表索引前缀对齐。 + * + * @author chenx + * @date 2026/7/28 + */ +@Data +public class SysOperationLogQuery { + + /** 日志级别(1-重要 2-次要 3-一般) */ + private Integer logLevel; + + /** 操作用户ID */ + private Long userId; + + /** 操作用户名(模糊) */ + private String username; + + /** 请求路径(模糊) */ + private String requestUri; + + /** 请求IP(模糊) */ + private String requestIp; + + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + + /** HTTP 状态码 */ + private Integer statusCode; + + /** + * 成功/失败筛选。 + * true=仅 HTTP 200(成功),false=非 200(失败)。 + */ + private Boolean success; + + /** + * 关键字,同时匹配接口名称(requestUri)与用户名(username)。 + * 前端单输入框搜索这两个字段。 + */ + private String keyword; + + /** 操作起始时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + /** 操作结束时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java b/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java new file mode 100644 index 0000000..c0f146b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.other; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @program: UserProfile + * @description: 接口日志操作应用详情 + * @author: chenx + * @create: 2024-07-05 12:34 + **/ + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OperationDetailsModel { + + private Integer id;//应用id + private String name;//应用名称 + private Long time;//requestTime + private String url; + private String errorMsg; + + @JsonIgnore + private Integer indicatorId; + @JsonIgnore + private String resultMsg; + @JsonIgnore + private Double computeScore;//计算后的分数 + +} diff --git a/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java b/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java new file mode 100644 index 0000000..a41d539 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.other; + +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/25 21:28 + * @description 学校学制年级实体 + */ + +@Data +public class SchLenGradeModel { + private Integer gradeId; + private String gradeName; + private Integer stage; + + public SchLenGradeModel(Integer gradeId, String gradeName, Integer stage) { + this.gradeId = gradeId; + this.gradeName = gradeName; + this.stage = stage; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java b/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java new file mode 100644 index 0000000..fc11539 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java @@ -0,0 +1,19 @@ +package com.yunhai.userbase.model.other; + +import com.yunhai.userbase.enums.SchoolStage; +import lombok.Data; + +import java.util.List; + +/** + * @author chenx + * @date 2025/12/25 17:44 + * @description 学校学段学制年级实体 + */ + +@Data +public class SchLenLevelModel { + private SchoolStage stage; + private List grades; +} + diff --git a/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java b/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java new file mode 100644 index 0000000..a158910 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java @@ -0,0 +1,26 @@ +package com.yunhai.userbase.model.other; + +import com.yunhai.userbase.enums.UserType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @program: UserProfile + * @description: 用户信息 + * @author: chenx + * @create: 2024-07-09 17:30 + **/ + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserInfoModel { + + private Integer userId; + private String userName; + private Integer schId; + private Integer groupId; + private String photo; + private UserType userType; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java b/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java new file mode 100644 index 0000000..61b45f7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.model.push; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文档约定的 8 个订阅接口:path(固定后缀,拼在订阅方 base URL 后)↔ 友好名 label。 + * + * @author chenx + * @date 2026/8/14 + */ +@Getter +@AllArgsConstructor +public enum PushInterfaceType { + + SYNC_GROUP("group/SyncGroup", "组全量同步"), + SYNC_USER("user/SyncUser", "用户全量同步"), + ADD_GROUP("group/AddGroup", "添加组"), + UPDATE_GROUP("group/UpdateGroup", "更新组"), + DELETE_GROUP("group/DeleteGroup", "删除组"), + ADD_USER("user/AddUser", "添加用户"), + UPDATE_USER("user/UpdateUser", "更新用户"), + DELETE_USER("user/DeleteUser", "删除用户"); + + private final String path; + private final String label; + + public static PushInterfaceType getByPath(String path) { + if (path == null) { + return null; + } + for (PushInterfaceType type : values()) { + if (type.path.equals(path)) { + return type; + } + } + return null; + } + + /** 是否为组相关接口。 */ + public boolean isGroup() { + return path.startsWith("group/"); + } + + /** 是否为用户相关接口。 */ + public boolean isUser() { + return path.startsWith("user/"); + } +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java new file mode 100644 index 0000000..9017d6a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 推送订阅记录实体(订阅方维度,落库用于日志 / 重试 / 排查)。 + * 与 DDL(push_subscription_ddl.sql §2)的 push_record 表一一对应。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +@NoArgsConstructor +@Entity +@Table(name = "push_record") +public class PushRecordEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "subscriber_app_key", nullable = false) + private Integer subscriberAppKey; + + @Column(name = "app_name") + private String appName; + + @Column(name = "interface_path", nullable = false) + private String interfacePath; + + @Column(name = "interface_label") + private String interfaceLabel; + + @Column(name = "request_url", nullable = false) + private String requestUrl; + + @Column(name = "payload", columnDefinition = "MEDIUMTEXT") + private String payload; + + @Column(name = "sign_t") + private String signT; + + @Column(name = "http_status") + private Integer httpStatus; + + @Column(name = "resp_code") + private Integer respCode; + + @Column(name = "resp_msg") + private String respMsg; + + /** 是否成功:0失败 1成功 */ + @Column(name = "success", nullable = false) + private Integer success = 0; + + @Column(name = "attempt_no", nullable = false) + private Integer attemptNo = 1; + + @Column(name = "cost_ms") + private Integer costMs; + + /** 触发类型:manual / auto / retry */ + @Column(name = "trigger_type") + private String triggerType; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java new file mode 100644 index 0000000..41e13db --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.model.push; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 推送记录列表项视图。 + * 不含 payload(请求体原文),列表不展示,需通过下载接口单独获取,避免全量推送时数据量过大。 + * + * @author chenx + * @date 2026/8/17 + */ +@Data +public class PushRecordListVo { + + /** 主键 */ + private Long id; + /** 订阅方 appKey */ + private Integer subscriberAppKey; + /** 应用名称 */ + private String appName; + /** 接口路径,如 group/SyncGroup */ + private String interfacePath; + /** 接口中文标签 */ + private String interfaceLabel; + /** 请求地址 */ + private String requestUrl; + /** 签名时间 t */ + private String signT; + /** HTTP 状态码 */ + private Integer httpStatus; + /** 业务码 */ + private Integer respCode; + /** 业务消息 */ + private String respMsg; + /** 是否成功:0失败 1成功 */ + private Integer success; + /** 重试次数 */ + private Integer attemptNo; + /** 耗时(ms) */ + private Integer costMs; + /** 触发类型:manual / auto / retry */ + private String triggerType; + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 全参构造,顺序需与 {@code PushRecordEntity} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public PushRecordListVo(Long id, Integer subscriberAppKey, String appName, String interfacePath, + String interfaceLabel, String requestUrl, String signT, Integer httpStatus, + Integer respCode, String respMsg, Integer success, Integer attemptNo, + Integer costMs, String triggerType, LocalDateTime createdAt) { + this.id = id; + this.subscriberAppKey = subscriberAppKey; + this.appName = appName; + this.interfacePath = interfacePath; + this.interfaceLabel = interfaceLabel; + this.requestUrl = requestUrl; + this.signT = signT; + this.httpStatus = httpStatus; + this.respCode = respCode; + this.respMsg = respMsg; + this.success = success; + this.attemptNo = attemptNo; + this.costMs = costMs; + this.triggerType = triggerType; + this.createdAt = createdAt; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java new file mode 100644 index 0000000..ffa1240 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; + +/** + * 推送记录查询条件(前端日志页搜索用)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushRecordQuery { + + /** 订阅方 appKey */ + private Integer appKey; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 是否成功:0失败 1成功 */ + private Integer success; + /** 起始时间 yyyy-MM-dd(含当天 00:00:00) */ + private String startTime; + /** 结束时间 yyyy-MM-dd(含当天 23:59:59) */ + private String endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java b/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java new file mode 100644 index 0000000..744d641 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.push; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * MQ 推送任务消息。 + * 由 {@code PushDispatcher}(生产方)投递,{@code PushConsumer}(消费方)取出后 POST 给订阅方。 + * 签名(t + sign)在投递时由基础平台计算一次;自动重试保持原 t/sign,手动重试重新生成。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PushTaskMessage implements Serializable { + + /** 订阅方 appKey */ + private Integer appKey; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 接口友好名 */ + private String interfaceLabel; + /** 业务数据原始 JSON 字符串(签名与发送均基于该原始字符串字节) */ + private String body; + /** 签名时间戳(字符串) */ + private String t; + /** HMAC-SHA256(业务JSON + t, secret) hex 小写 */ + private String sign; + /** 第几次尝试:1=首次 */ + private int attemptNo; + /** 触发类型:manual / auto / retry */ + private String triggerType; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java b/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java new file mode 100644 index 0000000..4859229 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; + +import java.util.List; + +/** + * 测试推送请求(测试推送页调用)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushTestRequest { + + /** 订阅方应用 id */ + private Integer appId; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 目标 id 列表(可选):不传则按接口类型取全量/全部 */ + private List targetIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java b/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java new file mode 100644 index 0000000..06cd21a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.push.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 文档「组」契约 DTO(group/SyncGroup、AddGroup、UpdateGroup 共用)。 + * 字段严格对齐《弘育基础平台数据同步接口 2》: + * + * SyncGroup:children 递归树(可空) + * AddGroup:pid 必填(上级组 id,0 为顶级) + * UpdateGroup:仅 id 必填,其余可选 + * + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushGroupDTO { + + private Integer id; + private String name; + private String code; + /** 组类别 1-8,与 GroupTypeEnum 1:1 零映射 */ + private Integer type; + /** 上级组 id(AddGroup 必填) */ + private Integer pid; + private Integer no; + /** 子组列表(SyncGroup 用) */ + private List children; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java b/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java new file mode 100644 index 0000000..697ca4b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java @@ -0,0 +1,53 @@ +package com.yunhai.userbase.model.push.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 文档「用户」契约 DTO(user/SyncUser、AddUser、UpdateUser 共用)。 + * 字段严格对齐《弘育基础平台数据同步接口 2》: + * + * status:0正常 / 2禁用(由 userbase StatusEnum 映射) + * groups:用户所属组,含 groupId / no / schoolId(schoolId = 组所属学校范围 id) + * roleList:用户身份,含 roleId / schId(schId = 用户主组所属学校范围 id) + * + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushUserDTO { + + private Integer id; + private String account; + private String name; + private String idcard; + private String phone; + private String email; + /** 性别 0女 1男 */ + private Integer gender; + private String birthday; + /** 用户状态 0正常 2禁用 */ + private Integer status; + private String profilePicture; + private List groups; + private List roleList; + + /** 用户所属组引用。 */ + @Data + public static class PushUserGroupRef { + private Integer groupId; + private Integer no; + /** 组所属学校范围 id(沿 parentId 向上最近的 SCHOOL_GROUP) */ + private Integer schoolId; + } + + /** 用户身份引用。 */ + @Data + public static class PushUserRoleRef { + private Integer roleId; + /** 所属学校 id(userbase 无角色-学校关联,统一取用户主组所属学校) */ + private Integer schId; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java new file mode 100644 index 0000000..a8c41f8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java @@ -0,0 +1,20 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** +* @Description: 学年学期 +* @Author: chenx +* @Date: 2023/9/5 +*/ + +@Data +public class AcademicYear { + public Integer id; + public String name; + public Integer year; + public Integer semester; + public String startDate; + public String endDate; + public String intervalDate;//日期区间 +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java new file mode 100644 index 0000000..23f6b1f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +import java.util.Date; + +/** +* @Description: 班级管理学年学期序列化 +* @Author: chenx +* @Date: 2023/9/5 +*/ + +@Data +public class AcademicYearSeq { + private int id; + private String name; + private Date startDate; + private Date endDate; + private Date progressionDate; + private int year; + private Date firstSemesterStartDate; + private Date firstSemesterEndDate; + private Date secondSemesterStartDate; + private Date secondSemesterEndDate; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java b/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java new file mode 100644 index 0000000..029444e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/30 19:31 + * @description 学校列表 + */ + +@Data +public class BaseSchool { + private String SchSerID; + private int Type; + private String PID; + private int ID; + private String Name; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java b/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java new file mode 100644 index 0000000..5e87ee4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** +* @Description: 班级管理接口序列化 :GetClaArrStulist +* @Author: chenx +* @Date: 2023/6/8 +*/ + +@Data +public class GradeClassStu { + private Integer gradeId; + private String gradeName; + private Integer classId; + private String className; + private Integer stuId; + private String stuName; + private String CardID;//学号 + private Integer StuSex;//性别 + private String StuPhoto; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java b/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java new file mode 100644 index 0000000..bdfe307 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class SchClass { + + public String SchSerID; + public int SchID; + public String GradeName; + public String ClassName; + public String ClassID; + public int GradeID; + public int CID; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java b/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java new file mode 100644 index 0000000..d8913a7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java @@ -0,0 +1,12 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class SchClassModel { + public Integer classID; + public Integer gradeID; + public String gradeName; + public String className; + +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java b/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java new file mode 100644 index 0000000..fd8d7d8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.serialize; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/25 15:56 + * @description 学校学制年级信息 + */ + +@Data +public class SchLenLevel { + private Integer code; + @JSONField(name = "OwnerId") + private Integer ownerId; + private String sx; + @JSONField(name = "Id") + private Integer id; + private Integer levell; + @JSONField(name = "Name") + private String name; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java b/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java new file mode 100644 index 0000000..7ee4969 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.serialize; + +/** + * @program: ResearchLearn + * @description: 全部教师信息 + * @author: chenx + * @create: 2024-06-07 10:16 + **/ + +import lombok.Data; + +@Data +public class SchTeaInfo { + private String SchSerID; + private String Teapolitical; + private String TeaWorkStart; + private String Account; + private String role; + private Integer TeaSex; + private String IsLeadingMiddle; + private String TeaOriginPlace; + private String Photo; + private String TeaIDCard; + private String Name; + private Integer GroupID; + private String SchName; + private String TeaTeachStart; + private String TeaSerID; + private Integer UserID; + private String TeaNation; + private String Tel; + private String TeaWorkType; + private Integer TeaNo; + private String TeaBirth; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java new file mode 100644 index 0000000..e5a32b4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** + * @program: QualityEvaluate + * @description: 获取班级列表的学生信息序列化 + * @author: chenx + * @create: 2023-08-21 10:27 + **/ + +@Data +public class StuInfo { + private String SchSerID; + private String Account; + private String StuStatus; + private String invitecode; + private int StuSex; + private String ClassID; + private String CardID; + private String ClassStuID; + private String StuPhoto; + private long GroupID; + private String StuName; + private long UserStuSerID; + private boolean Graduated; + private String StuIDCard; + private String StuCurrentTel; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java new file mode 100644 index 0000000..4c65fe6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +import java.util.Date; + +/** + * @program: QualityEvaluate + * @description: 获取单个学生信息序列化 + * @author: chenx + * @create: 2023-09-06 14:46 + **/ + +@Data +public class StuInfo2 { + private int status_code; + private String idCard; + private int sex; + private String photo; + private int IsGraduation; + private Date birth; + private String ClaName; + private String stuName; + private String SchName; + private String studentId; + private long classId; + private String GradeName; + private long UserStuSerID; + private String ClassName; + private int IsDelete; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java b/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java new file mode 100644 index 0000000..1eb1237 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java @@ -0,0 +1,13 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class TeaFirstClass { + public String gradeName; + public int gradeId ; + public String alias; + public String className ; + public String name ; + public int id ; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java new file mode 100644 index 0000000..7102524 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java @@ -0,0 +1,48 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 系统组批量新增请求:在指定父组下,按 items 列表逐个创建子组。 + * 请求体示例: + * { + * "pid": 11285, + * "type": 4, + * "items": [ + * { "name": "高一1班" }, + * { "name": "高一2班" } + * ] + * } + */ +@Data +public class SysGroupBatchRequest { + + /** 父组ID(待生成子组的挂载点,必须存在)。 */ + @NotNull(message = "父组ID不能为空") + private Integer pid; + + /** 组类型(GroupTypeEnum.key,如 4=班级组)。 */ + @NotNull(message = "组类型不能为空") + private Integer type; + + /** 待新增的子组列表,每一项仅需提供 name。 */ + @NotEmpty(message = "批量新增的子组列表不能为空") + @Valid + private List items; + + /** 批量新增的单项。 */ + @Data + public static class BatchItem { + /** 子组名称。 */ + @javax.validation.constraints.NotBlank(message = "组名称不能为空") + private String name; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java new file mode 100644 index 0000000..3643b72 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 移动组请求:将 moveId 节点移动到 pid 节点下。 + * 不支持移动到自身或自身后代下(环检测)。 + */ +@Data +public class SysGroupMoveRequest { + + /** 待移动节点ID。 */ + @NotNull(message = "待移动节点ID不能为空") + private Integer moveId; + + /** 目标父节点ID(移动后的挂载点,必须存在)。 */ + @NotNull(message = "目标父节点ID不能为空") + private Integer pid; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java new file mode 100644 index 0000000..ece4fc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组查询条件 + */ +@Data +public class SysGroupQuery { + private String name; + + private Integer parentId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java new file mode 100644 index 0000000..405668c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.model.sysgroup; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotBlank; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组新增/编辑请求 + */ +@Data +public class SysGroupRequest { + // ID 通过路径参数传递,body 无需携带,故不在校验组内强制 + private Integer id; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "组名称不能为空") + private String name; + + // 新增时必填;编辑不通过此字段移动节点(移动请走 Move 接口),故编辑时可选 + @NotNull(groups = ValidationGroups.Create.class, message = "上级组ID不能为空") + private Integer parentId; + + // 新增时必填;编辑时可选(仅当需要变更类型时传入) + @NotNull(groups = ValidationGroups.Create.class, message = "组类型不能为空") + private Integer type; + + private Integer sort; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java new file mode 100644 index 0000000..d01ecd1 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 子组排序请求:将 pid 的直接子节点按给定 ids 顺序重新排列并落库。 + * ids 必须全部是 pid 下一级的直接子节点,否则报错。 + */ +@Data +public class SysGroupSortRequest { + + /** 父组ID。 */ + @NotNull(message = "父组ID不能为空") + private Integer pid; + + /** 期望的、pid 直接子节点有序 ID 列表(按传入顺序生效)。 */ + @NotNull(message = "子节点ID列表不能为空") + private List ids; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java new file mode 100644 index 0000000..8aadbe5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组返回对象(树形:children 递归存放子组)。 + */ +@Data +public class SysGroupVo { + private Integer id; + private String name; + private Integer type; + private Integer sort; + /** 子组(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java new file mode 100644 index 0000000..28905f7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量设置用户状态请求 + */ +@Data +public class BatchChangeStatusRequest { + + /** 待设置用户ID列表 */ + @NotEmpty(message = "待设置用户不能为空") + private List userIds; + + /** 是否禁用:true=禁用,false=启用 */ + @NotNull(message = "状态不能为空") + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java new file mode 100644 index 0000000..5f1b4cb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量从当前组删除用户请求 + */ +@Data +public class BatchDeleteRequest { + + /** 待删除用户ID列表 */ + @NotEmpty(message = "待删除用户不能为空") + private List userIds; + + /** 当前组ID */ + @NotNull(message = "组ID不能为空") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java new file mode 100644 index 0000000..f479d3a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/11 + * @description 用户批量导入结果 + */ +@Data +public class BatchImportResult { + + /** 总行数(含表头) */ + private int total; + + /** 成功条数 */ + private int success; + + /** 失败条数 */ + private int fail; + + /** 失败明细:行号 + 原因 */ + private List fails = new ArrayList<>(); + + @Data + public static class FailItem { + private int row; + private String reason; + + public FailItem(int row, String reason) { + this.row = row; + this.reason = reason; + } + } + + public void addSuccess() { + this.success++; + } + + public void addFail(int row, String reason) { + this.fail++; + this.fails.add(new FailItem(row, reason)); + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java new file mode 100644 index 0000000..5c15b38 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/11 + * @description 批量调整用户所属组请求 + */ +@Data +public class ChangeGroupsRequest { + + /** 待调整用户ID列表 */ + @NotEmpty(message = "待调整用户不能为空") + private List userIds; + + /** 目标组ID列表(覆盖式) */ + @NotEmpty(message = "请至少保留一个组") + private List groupIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java new file mode 100644 index 0000000..0631049 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/8/12 + * @description 调整用户在某组内的排序号 + */ +@Data +public class ChangeSortNoRequest { + + /** 所属分组(排序号按组维度存储,必须明确是哪个组) */ + @NotNull(message = "所属分组不能为空") + private Integer groupId; + + /** + * 组内排序号:1~999 的正整数;越小越靠前。 + * 允许为 null —— 表示清除排序号,列表回退到 user_id 升序兜底。 + * 注意:@Min/@Max 对 null 不生效,null 由业务层解释为「清除」。 + */ + @Min(1) + @Max(999) + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java b/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java new file mode 100644 index 0000000..675e952 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java @@ -0,0 +1,53 @@ +package com.yunhai.userbase.model.sysuser; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; + +/** + * 组用户导出 Excel 实体:与导入模板 {@code IMPORT_COLUMN_DEFS} 同构(前 12 列顺序一致), + * 末尾追加「用户id」列。导出 = 读取数据库数据填充 list,复用 EasyPoi 的 {@code @Excel} 注解生成 Excel。 + */ +@Data +public class ExcelSysUser implements Serializable { + + @Excel(name = "组名\n(不必填,供与id对应参考用)", orderNum = "0", width = 36) + private String groupName; + + @Excel(name = "组id(必须)", orderNum = "1", width = 14) + private String groupId; + + @Excel(name = "用户名(必须)", orderNum = "2", width = 20) + private String account; + + @Excel(name = "密码\n(可选,\n默认Vzuk4Dp3)", orderNum = "3", width = 18) + private String password; + + @Excel(name = "姓名(必须)", orderNum = "4", width = 16) + private String realName; + + @Excel(name = "身份证号(可选)", orderNum = "5", width = 22) + private String idcard; + + @Excel(name = "手机号(可选)", orderNum = "6", width = 16) + private String phone; + + @Excel(name = "email(可选)", orderNum = "7", width = 24) + private String email; + + @Excel(name = "用户性质\n(可选,默认普通用户\n组管理员2,\n普通用户0,\n导入不接受别的代码)", orderNum = "8", width = 26) + private Integer nature; + + @Excel(name = "用户身份\n(1教师 2教职工 3学生 4家长)", orderNum = "9", width = 24) + private Integer identity; + + @Excel(name = "性别(可选)", orderNum = "10", width = 12) + private Integer gender; + + @Excel(name = "生日(yyyy-MM-dd)(可选)", orderNum = "11", width = 22) + private String birthday; + + @Excel(name = "用户id", orderNum = "12", width = 14) + private Integer userId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java new file mode 100644 index 0000000..7645bfb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java @@ -0,0 +1,40 @@ +package com.yunhai.userbase.model.sysuser; + +/** + * 批量删除用户-组关系单项(与 BasePlatform 开放接口模型 {@code SysUserGroupDeleteItem} 字段对齐)。 + * 每条记录携带自身所属学校范围 {@code schoolId},供 {@code SysUserService.deleteUsers} 逐条校验: + * groupId 必须属于 schoolId 子树且不能等于 schoolId(禁止跨校解绑 / 整校移除)。 + */ +public class SysUserGroupDeleteItem { + + /** 待解绑用户 ID */ + private Integer userId; + /** 待解绑的组 ID(必须属于 schoolId 子树,且不能等于 schoolId) */ + private Integer groupId; + /** 学校组 ID:限定该条解绑的学校范围 */ + private Integer schoolId; + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public Integer getGroupId() { + return groupId; + } + + public void setGroupId(Integer groupId) { + this.groupId = groupId; + } + + public Integer getSchoolId() { + return schoolId; + } + + public void setSchoolId(Integer schoolId) { + this.schoolId = schoolId; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java new file mode 100644 index 0000000..abad1a7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @description 开放接口:移动用户到指定组请求(语义同后台「调整组」——覆盖式把用户组归属替换为目标组,目标设为主组)。 + */ +@Data +public class SysUserMoveRequest { + + /** 待移动用户ID。 */ + @NotNull(message = "用户ID不能为空") + private Integer userId; + + /** 目标组ID(移动后的主归属组,必须存在)。 */ + @NotNull(message = "目标组ID不能为空") + private Integer groupId; + + /** 学校组ID:限定移动范围,仅允许将用户在同一学校(组)范围内移动。 */ + @NotNull(message = "学校ID不能为空") + private Integer schoolId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java new file mode 100644 index 0000000..78454c2 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 修改用户密码请求(oapi 开放接口专用)。 + * + * @author chenx + */ +@Data +public class SysUserPasswordRequest { + + /** + * 用户ID(必填)。 + */ + @NotNull + private Integer userId; + + /** + * 旧密码(必填,须与库中 BCrypt 密文匹配)。 + */ + @NotBlank + private String oldPassword; + + /** + * 新密码(必填,走弱密码规则校验后 BCrypt 加密写入)。 + */ + @NotBlank + private String newPassword; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java new file mode 100644 index 0000000..2f35855 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户列表查询条件 + */ +@Data +public class SysUserQuery { + + /** 分组ID(列表以某个组节点为上下文查询其下用户) */ + private Integer groupId; + + /** + * 是否包含子组用户:0=仅查询该节点下的用户;1=查询该节点及其所有子孙组下的用户。 + * 默认 0。 + */ + private Integer withSub = 0; + + /** 关键字:同时匹配账号、姓名、身份证(模糊)与手机号(精确) */ + private String name; + + /** 启用状态过滤(可选);对应实体 disabled 字段(StatusEnum key:0=正常/未禁用,1=禁用) */ + private Integer disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java new file mode 100644 index 0000000..ccdf606 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.model.sysuser; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户新增/编辑请求 + */ +@Data +public class SysUserRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** 所属分组ID,新增时必填(用户创建后写入组用户关联表) */ + @NotNull(groups = ValidationGroups.Create.class, message = "所属分组不能为空") + private Integer groupId; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "账号不能为空") + private String account; + + @NotBlank(groups = ValidationGroups.Create.class, message = "密码不能为空") + private String password; + + /** 确认密码:与 password 一致性由 Service 校验(编辑时若不修改密码可不传) */ + private String confirmPassword; + + private String realName; + private String phone; + private LocalDate birthday; + private String idcard; + private String email; + private Integer gender; + private String profilePicture; + private List roles; + /** 启用状态:对应 StatusEnum 的 key(0=正常/未禁用,1=禁用);不传则默认启用 */ + private Integer disabled; + private Boolean expired; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java new file mode 100644 index 0000000..3dee203 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户返回对象 + */ +@Data +public class SysUserVo { + private Integer id; + private String account; + private LocalDate birthday; + private String realName; + private String phone; + private String idcard; + private String email; + private Integer gender; + private String profilePicture; + private List roles; + /** 启用状态:StatusEnum 的 key(0=正常/未禁用,1=禁用) */ + private Integer disabled; + /** 用户类型:UserTypeEnum 的 key(0=普通用户,1=超级管理员,2=学校管理员) */ + private Integer type; + /** 当前组维度的排序号(组内 sort_no);根节点/未选组时为 null */ + private Integer sortNo; + private Boolean expired; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java b/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java new file mode 100644 index 0000000..1401768 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.AppCategory; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:29 + * @description + */ +public interface AppCategoryRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT MAX(c.sortNo) FROM AppCategory c") + Integer findMaxSortNo(); + + List findAllByOrderBySortNoAsc(); + + List findByNameContainingOrderBySortNoAsc(String name); + + boolean existsByName(String name); + + boolean existsByNameAndIdNot(String name, Integer id); + + @Modifying + @Query("UPDATE AppCategory c SET c.sortNo = :sortNo WHERE c.id = :id") + void updateSortNoById(@Param("id") Integer id, @Param("sortNo") Integer sortNo); +} diff --git a/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java b/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java new file mode 100644 index 0000000..e1306d9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java @@ -0,0 +1,46 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.AppModule; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块仓储。 + */ +public interface AppModuleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 动态过滤:appId/parentId/name 均可空; + * 排序:sortNo 正序(空视为 999),再按 id 正序。 + */ + @Query("SELECT m FROM AppModule m " + + "WHERE (:appId IS NULL OR m.appId = :appId) " + + "AND (:parentId IS NULL OR m.parentId = :parentId) " + + "AND (:name IS NULL OR m.name LIKE CONCAT('%', :name, '%')) " + + "ORDER BY CASE WHEN m.sortNo IS NULL THEN 999 ELSE m.sortNo END ASC, m.id ASC") + List findModules(@Param("appId") Integer appId, + @Param("parentId") Integer parentId, + @Param("name") String name); + + boolean existsByAppIdAndCustomId(Integer appId, String customId); + + boolean existsByAppIdAndCustomIdAndIdNot(Integer appId, String customId, Integer id); + + long countByParentId(Integer parentId); + + /** + * 按应用ID集合查询全部模块(组授权按已开通应用批量加载模块树用)。 + */ + List findByAppIdIn(List appIds); + + /** + * 按应用ID级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理整棵子树)。 + */ + void deleteByAppId(Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java new file mode 100644 index 0000000..a34acb3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java @@ -0,0 +1,80 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.appauthz.AppAuthzAppRow; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:29 + * @description + */ +public interface ApplicationInfoRepository extends JpaRepository, JpaSpecificationExecutor { + @Query("SELECT COUNT(a.id) FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000") + Integer countByCategoryId(@Param("categoryId") Integer id); + + boolean existsByName(String name); + + boolean existsByNameAndIdNot(String name, Integer id); + + /** + * 查询当前最大的 appKey 值。 + * 用于新增应用时生成从 100000 起自增的整型 appKey。 + * + * @return 当前最大 appKey,无记录时返回 null + */ + @Query("SELECT MAX(a.appKey) FROM ApplicationInfo a") + Integer findMaxAppKey(); + + /** + * 按整型 appKey 查单个应用(验签兜底端点直连读取用)。 + * appKey 由新增应用自增生成,业务上唯一。 + * + * @param appKey 应用标识(整数) + * @return 匹配的应用实体,无则 null + */ + ApplicationInfo findByAppKey(Integer appKey); + + /** 查询所有开启推送的应用(自动推送遍历订阅方用)。 */ + List findByPushEnabled(Integer pushEnabled); + + /** + * 按 id 更新排序号(同一应用类型内排序使用)。 + * + * @param id 应用ID + * @param sortNo 新的排序号 + */ + @Modifying + @Query("UPDATE ApplicationInfo a SET a.sortNo = :sortNo WHERE a.id = :id") + void updateSortNoById(@Param("id") Integer id, @Param("sortNo") Integer sortNo); + + /** + * 按应用类型查询排序所需的精简视图(仅 id/name/sortNo),不分页。 + * 使用构造器表达式做 DTO 投影,避免加载整个实体(含 appSecret、各类 URL 等无关大字段)。 + * 排除用户中心自身的基石应用(appKey=100000)。 + * + * @param categoryId 应用类型ID + * @return 该类型下全部应用的排序视图,按 sortNo 升序(未排序排末尾),id 升序兜底 + */ + @Query("SELECT new com.yunhai.userbase.model.application.ApplicationInfoSortVo(a.id, a.name, a.sortNo) " + + "FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000 ORDER BY a.sortNo ASC, a.id ASC") + List findSortVoByCategoryId(@Param("categoryId") Integer categoryId); + + /** + * 应用授权用:查询全部应用的扁平投影(含 categoryId 供按类型分组),仅取 id/name/pcIcon。 + * 按 categoryId、sortNo、id 升序,便于分组后同类型内保持排序。 + * 排除用户中心自身的基石应用(appKey=100000)。 + * + * @return 全部应用的授权投影行 + */ + @Query("SELECT new com.yunhai.userbase.model.appauthz.AppAuthzAppRow(a.categoryId, a.id, a.name, a.pcIcon) " + + "FROM ApplicationInfo a WHERE a.appKey > 100000 ORDER BY a.categoryId ASC, a.sortNo ASC, a.id ASC") + List findAllAuthzRows(); +} diff --git a/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java b/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java new file mode 100644 index 0000000..65a4eaa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.model.push.PushRecordEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +/** + * 推送记录仓储。 + * + * @author chenx + * @date 2026/8/14 + */ +@Repository +public interface PushRecordRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java new file mode 100644 index 0000000..d94a60c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysAppRoleModuleRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 业务角色-模块权限关系仓储。 + */ +public interface SysAppRoleModuleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByRoleId(Integer roleId); + + /** + * 权限计算用:批量查询一组角色的全部模块授权记录。 + */ + List findByRoleIdIn(List roleIds); + + /** + * 按角色ID批量物理删除模块授权记录(bulk delete 直接发 SQL)。 + * 用于「角色模块授权」全量覆盖式保存的先删后插,避免 Hibernate flush 先 insert 后 delete 触发唯一键冲突。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.roleId = :roleId") + void deleteByRoleIdBulk(@Param("roleId") Integer roleId); + + /** + * 按角色ID集合批量物理删除模块授权记录(删除应用时级联清理该应用下角色)。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.roleId IN :roleIds") + void deleteByRoleIdInBulk(@Param("roleIds") List roleIds); + + /** + * 按模块ID集合批量物理删除模块授权记录(删除模块/应用时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.moduleId IN :moduleIds") + void deleteByModuleIdInBulk(@Param("moduleIds") List moduleIds); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java b/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java new file mode 100644 index 0000000..4c30a65 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java @@ -0,0 +1,40 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysAppRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色仓储。 + */ +public interface SysAppRoleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 普通角色重名校验:同一应用下角色名称唯一(新增用)。 + */ + boolean existsByNameAndAppId(String name, Integer appId); + + /** + * 普通角色重名校验:同一应用下角色名称唯一,排除自身(编辑用)。 + */ + boolean existsByNameAndAppIdAndIdNot(String name, Integer appId, Integer id); + + /** + * 全应用角色重名校验:全应用角色之间名称唯一(新增用)。 + */ + boolean existsByNameAndAllAppFlagTrue(String name); + + /** + * 全应用角色重名校验:全应用角色之间名称唯一,排除自身(编辑用)。 + */ + boolean existsByNameAndAllAppFlagTrueAndIdNot(String name, Integer id); + + /** + * 查询某应用下的全部普通角色(删除应用时级联清理用)。 + */ + List findByAppId(Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java new file mode 100644 index 0000000..8ec1200 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroupAppRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用开通仓储。 + */ +public interface SysGroupAppRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByGroupId(Integer groupId); + + List findByGroupIdIn(List groupIds); + + boolean existsByGroupIdAndAppId(Integer groupId, Integer appId); + + long countByAppId(Integer appId); + + /** + * 按组ID批量物理删除开通记录(bulk delete 直接发 SQL)。 + * 用于「应用开通」全量覆盖式保存的先删后插,避免 Hibernate flush 先 insert 后 delete 触发唯一键冲突。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按应用ID批量物理删除开通记录(删除应用时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.appId = :appId") + void deleteByAppIdBulk(@Param("appId") Integer appId); + + /** + * 按 (组ID + 应用ID) 物理删除单条开通记录(增量取消开通用)。 + * 用于 SetSchoolApp 的「删除」模式:取消某个组下某个应用的开通。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.groupId = :groupId AND r.appId = :appId") + void deleteByGroupIdAndAppIdBulk(@Param("groupId") Integer groupId, @Param("appId") Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java new file mode 100644 index 0000000..c63ad22 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java @@ -0,0 +1,60 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroupModulePerm; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组模块权限仓储。 + */ +public interface SysGroupModulePermRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByGroupId(Integer groupId); + + /** + * 增量 upsert 用:查询某组对某模块是否已有权限记录。 + */ + SysGroupModulePerm findByGroupIdAndModuleId(Integer groupId, Integer moduleId); + + /** + * 权限判定用:查询目标模块在指定组ID集合(祖先链)上的全部权限记录。 + */ + List findByModuleIdAndGroupIdIn(Integer moduleId, List groupIds); + + /** + * 按组ID批量物理删除权限记录(删除组时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 权限一览用:批量查询一组模块在指定组集合上的权限记录。 + */ + List findByModuleIdInAndGroupIdIn(List moduleIds, List groupIds); + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.moduleId IN :moduleIds") + void deleteByModuleIdInBulk(@Param("moduleIds") List moduleIds); + + /** + * 删除某组在指定模块集合上的权限记录(组取消开通应用时清理该应用下模块的残留权限)。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId AND p.moduleId IN :moduleIds") + void deleteByGroupIdAndModuleIdInBulk(@Param("groupId") Integer groupId, @Param("moduleIds") List moduleIds); + + /** + * 按 (组ID + 模块ID) 物理删除单条权限记录(增量取消模块授权用)。 + * 用于 SetGroupModule 的「删除」模式。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId AND p.moduleId = :moduleId") + void deleteByGroupIdAndModuleIdBulk(@Param("groupId") Integer groupId, @Param("moduleId") Integer moduleId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java new file mode 100644 index 0000000..a9c2f02 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroup; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组仓储 + */ +public interface SysGroupRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT MAX(g.sort) FROM SysGroup g") + Integer findMaxSort(); + + @Query("SELECT MAX(g.sort) FROM SysGroup g WHERE g.parentId = :parentId") + Integer findMaxSortByParentId(Integer parentId); + + List findAllByOrderBySortAsc(); + + List findByNameContainingOrderBySortAsc(String name); + + List findByParentIdOrderBySortAsc(Integer parentId); + + long countByParentId(Integer parentId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java b/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java new file mode 100644 index 0000000..7bcf205 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysOapiLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDateTime; + +/** + * 开放接口调用日志仓储。 + * + * @author chenx + * @date 2026/8/20 + */ +public interface SysOapiLogRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按日志级别 + 创建时间批量删除(单条 DELETE 语句,不先加载实体)。 + * 用于定时清理:一般日志保留 12 个月,重要/次要日志保留 36 个月。 + * + * @param logLevel 日志级别编码 + * @param createTime 截止时间(早于该时间的记录被删除) + * @return 删除条数 + */ + long deleteByLogLevelAndCreateTimeBefore(Integer logLevel, LocalDateTime createTime); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java b/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java new file mode 100644 index 0000000..d8faaac --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysOperationLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDateTime; + +/** + * 系统操作日志仓储。 + * + * @author chenx + * @date 2026/7/28 + */ +public interface SysOperationLogRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按日志级别 + 创建时间批量删除(单条 DELETE 语句,不先加载实体)。 + * 用于定时清理:一般日志保留 6 个月,重要/次要日志保留 12 个月。 + * + * @param logLevel 日志级别编码 + * @param createTime 截止时间(早于该时间的记录被删除) + * @return 删除条数 + */ + long deleteByLogLevelAndCreateTimeBefore(Integer logLevel, LocalDateTime createTime); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java new file mode 100644 index 0000000..82b7231 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java @@ -0,0 +1,51 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserAppRoleRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 用户-业务角色关系仓储。 + */ +public interface SysUserAppRoleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByUserId(Integer userId); + + List findByRoleId(Integer roleId); + + /** + * 按角色ID批量物理删除绑定记录(bulk delete 直接发 SQL)。 + * 用于「角色绑定用户」全量覆盖式保存的先删后插,以及删除角色时级联清理。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId") + void deleteByRoleIdBulk(@Param("roleId") Integer roleId); + + /** + * 按角色ID集合批量物理删除绑定记录(删除应用时级联清理该应用下角色)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId IN :roleIds") + void deleteByRoleIdInBulk(@Param("roleIds") List roleIds); + + /** + * 按用户ID批量物理删除绑定记录(「用户绑定角色」全量覆盖保存 + 删除用户时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.userId = :userId") + void deleteByUserIdBulk(@Param("userId") Integer userId); + + /** + * 按角色ID + 用户ID 删除单条绑定记录(移除单个用户的角色关联,避免全量重提)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId AND r.userId = :userId") + void deleteByRoleIdAndUserId(@Param("roleId") Integer roleId, @Param("userId") Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java new file mode 100644 index 0000000..bd79400 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java @@ -0,0 +1,17 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserCurrentRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Optional; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户当前激活身份仓储(一对一:user_id 唯一) + */ +public interface SysUserCurrentRoleRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByUserId(Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java new file mode 100644 index 0000000..0a53aff --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserGroupRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 组用户关联仓储(用于删除组/用户时校验关联数据) + */ +public interface SysUserGroupRelRepository extends JpaRepository, JpaSpecificationExecutor { + + long countByGroupId(Integer groupId); + + long countByUserId(Integer userId); + + List findByUserId(Integer userId); + + List findByUserIdIn(List userIds); + + List findByUserIdInAndGroupId(List userIds, Integer groupId); + + Optional findByUserIdAndGroupId(Integer userId, Integer groupId); + + List findByGroupId(Integer groupId); + + void deleteByUserId(Integer userId); + + void deleteByUserIdAndGroupId(Integer userId, Integer groupId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java new file mode 100644 index 0000000..015d584 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java @@ -0,0 +1,86 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户仓储 + */ +public interface SysUserRepository extends JpaRepository, JpaSpecificationExecutor { + + long countByAccount(String account); + + long countByAccountAndIdNot(String account, Integer id); + + /** 按登录账号(username)查用户,供用户中心组装 BoaUser。 */ + SysUser findByAccount(String account); + + /** 按手机号查用户,供用户中心组装 BoaUser。 */ + SysUser findByPhone(String phone); + + /** + * 组上下文列表专用分页(原生 SQL)。 + * INNER JOIN 关联表后按 user_id 聚合(GROUP BY u.id),以「组内最小 sort_no」为排序键 + * (NULLS LAST + sort_no 升序 + user_id 兜底升序)。GROUP BY 天然去重,规避 MySQL 的 + * DISTINCT+ORDER BY 限制与 Hibernate 不支持 ORDER BY 标量子查询的限制。 + * 入参 name / disabled 为可空:为空(NULL)时该过滤条件整体跳过。 + */ + @Query(value = + "SELECT u.id AS userId, MIN(r.sort_no) AS minSortNo " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled) " + + "GROUP BY u.id " + + "ORDER BY CASE WHEN MIN(r.sort_no) IS NULL THEN 1 ELSE 0 END, MIN(r.sort_no) ASC, u.id ASC", + countQuery = + "SELECT COUNT(DISTINCT u.id) " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled)", + nativeQuery = true) + Page findGroupUserPage(@Param("groupIds") List groupIds, + @Param("name") String name, + @Param("disabled") Integer disabled, + Pageable pageable); + + /** + * 组上下文导出专用(原生 SQL,非分页):INNER JOIN 关联表后按 user_id 聚合, + * 仅返回「属于指定组范围」的用户(无组归属的用户天然被排除);超管(type=1)豁免 + * (type 为 NULL 视为非超管,一并保留)。入参 name 为可空:为空时跳过关键字过滤。 + * 排序按 user_id 升序,保证导出结果稳定。 + */ + @Query(value = + "SELECT u.id " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (u.type IS NULL OR u.type != 1) " + + "GROUP BY u.id " + + "ORDER BY u.id ASC", + nativeQuery = true) + List findGroupUserIds(@Param("groupIds") List groupIds, + @Param("name") String name); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java new file mode 100644 index 0000000..44b7af7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserRoleRel; +import com.yunhai.userbase.enums.StatusEnum; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户-SysRoleEnum角色-学校 关联仓储(平台身份角色权威源) + */ +public interface SysUserRoleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + /** 用户全部可选身份(含禁用态由调用方过滤) */ + List findByUserId(Integer userId); + + /** 用户某状态下的全部可选身份 */ + List findByUserIdAndStatus(Integer userId, StatusEnum status); + + /** 用户主身份(isMain=1),最多 1 条 */ + Optional findByUserIdAndIsMain(Integer userId, Integer isMain); + + /** 用户在某学校下、某状态下的全部角色关联 */ + List findByUserIdAndSchoolIdAndStatus(Integer userId, Integer schoolId, StatusEnum status); + + /** 用户所有正常态的 roleId 去重列表(用于 BoaUser.roleIds) */ + @Query("SELECT DISTINCT r.roleId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status") + List findDistinctRoleIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status); + + /** 用户所有正常态的 schoolId 去重列表(用于 BoaUser.schools,schoolId>0 的过滤在调用方做) */ + @Query("SELECT DISTINCT r.schoolId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status") + List findDistinctSchoolIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status); +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java b/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java new file mode 100644 index 0000000..13772b4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java @@ -0,0 +1,52 @@ +package com.yunhai.userbase.service; + +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.userbase.entity.AppCategory; +import com.yunhai.userbase.model.appauthz.AppAuthzCategoryVo; +import com.yunhai.userbase.model.appcategory.AppCategoryQuery; +import com.yunhai.userbase.model.appcategory.AppCategoryRequest; +import com.yunhai.userbase.model.appcategory.AppCategoryVo; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:30 + * @description + */ +public interface IAppCategoryService { + + void add(AppCategoryRequest request); + + void edit(Integer id, AppCategoryRequest request); + + void delete(Integer id); + + List list(AppCategoryQuery query); + + void sort(List ids); + + /** + * 应用授权用:获取所有应用类型及其类型下的应用(id/name/icon)。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无应用的类型 app 为空数组。 + * + * @return 应用类型 + 应用列表的授权视图 + */ + List listWithApps(); + + /** + * 开放接口专用:按用户 + 客户端类型查询其有权访问的应用列表(按应用类型分组)。 + * 规则: + * ① 权限过滤:仅返回该用户有权访问的应用(复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}); + * ② 客户端过滤:仅返回支持该客户端类型的应用({@code app.clients} 位掩码与 clientType 有交集;clients 为空视为不限端); + * ③ 不透出 appSecret。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无有权应用的类型 apps 为空数组。 + * + * @param userId 用户ID + * @param clientType 客户端类型(null=不按端过滤) + * @return 应用类型 + 有权应用列表 + */ + List listBoaAppType(Integer userId, ClientEnum clientType); + +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppModuleService.java b/src/main/java/com/yunhai/userbase/service/IAppModuleService.java new file mode 100644 index 0000000..aa0d1b7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppModuleService.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.model.appmodule.AppModuleQuery; +import com.yunhai.userbase.model.appmodule.AppModuleRequest; +import com.yunhai.userbase.model.appmodule.AppModuleVo; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块服务。 + */ +public interface IAppModuleService { + + void add(AppModuleRequest request); + + void edit(Integer id, AppModuleRequest request); + + void delete(Integer id); + + List list(AppModuleQuery query); +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppRoleService.java b/src/main/java/com/yunhai/userbase/service/IAppRoleService.java new file mode 100644 index 0000000..681240c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppRoleService.java @@ -0,0 +1,105 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; +import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; +import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; +import com.yunhai.userbase.model.approle.AppRoleUserVo; +import com.yunhai.userbase.model.approle.SysAppRoleQuery; +import com.yunhai.userbase.model.approle.SysAppRoleRequest; +import com.yunhai.userbase.model.approle.SysAppRoleVo; +import com.yunhai.userbase.model.approle.UserAppRoleBindRequest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色服务(角色 CRUD + 角色模块授权 + 用户角色绑定)。 + */ +public interface IAppRoleService { + + /** + * 分页查询角色列表(name 模糊、appId/allAppFlag/disabled 精确)。 + * 普通角色回填所属应用名称 appName。 + * + * @param query 查询条件 + * @param pageable 分页参数 + * @return 分页角色列表 + */ + Page list(SysAppRoleQuery query, Pageable pageable); + + /** + * 新增角色。 + * 规则:普通角色 allAppFlag=false 且 appId 必填(应用须存在),同应用下名称唯一; + * 全应用角色 allAppFlag=true 且 appId 必须为空,全应用角色之间名称唯一。 + * + * @param request 角色入参 + */ + void add(SysAppRoleRequest request); + + /** + * 编辑角色(可改 name/remark/disabled)。 + * 角色类型 allAppFlag 与所属应用 appId 不允许修改(修改会导致已授权模块越界),需删除后重建。 + * 重名校验规则与新增一致(排除自身)。 + * + * @param id 角色ID + * @param request 角色入参 + */ + void edit(Integer id, SysAppRoleRequest request); + + /** + * 删除角色,并级联清理其模块授权记录与用户绑定记录(逻辑外键,无物理 FK)。 + * + * @param id 角色ID + */ + void delete(Integer id); + + /** + * 角色模块授权页回显:返回该角色可授权的应用及其模块树,已授权模块 checked=true。 + * 普通角色仅返回其所属应用;全应用角色返回系统全部应用(可跨应用授权)。 + * + * @param roleId 角色ID + * @return 应用列表(含模块权限树) + */ + List findRoleModuleTree(Integer roleId); + + /** + * 角色模块授权保存:全量覆盖式更新 sys_app_role_module_rel(bulk 先删后插)。 + * 校验:模块必须存在;普通角色只能授权其所属应用下的模块,越权写其他应用的模块会被拒绝。 + * + * @param request 角色ID + 勾选的模块ID全量列表 + */ + void saveRoleModules(AppRoleModuleAssignRequest request); + + /** + * 查询角色已绑定的用户列表。 + * + * @param roleId 角色ID + * @return 用户列表 + */ + List findRoleUsers(Integer roleId); + + /** + * 角色绑定用户保存(角色维度,全量覆盖):bulk 先删该角色全部绑定再批量插入。 + * + * @param request 角色ID + 用户ID全量列表 + */ + void saveRoleUsers(AppRoleUserBindRequest request); + + /** + * 用户绑定角色保存(用户维度,全量覆盖):bulk 先删该用户全部绑定再批量插入。 + * + * @param request 用户ID + 角色ID全量列表 + */ + void saveUserRoles(UserAppRoleBindRequest request); + + /** + * 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。 + * + * @param roleId 角色ID + * @param userId 用户ID + */ + void removeRoleUser(Integer roleId, Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java new file mode 100644 index 0000000..08d31c0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java @@ -0,0 +1,104 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.application.ApplicationInfoQuery; +import com.yunhai.userbase.model.application.ApplicationInfoRequest; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.model.application.PushSettingRequest; +import com.yunhai.userbase.model.application.PushSettingVo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 应用信息业务接口(缓存由 Spring Cache 注解统一管控)。 + */ +public interface IApplicationInfoService { + + /** + * 根据 id 查询应用信息(带缓存,防击穿)。 + */ + ApplicationInfo getById(Integer id); + + /** + * 保存/更新应用信息(写后同步更新缓存)。 + */ + ApplicationInfo save(ApplicationInfo info); + + /** + * 根据 id 删除应用信息(同步清除缓存)。 + */ + void deleteById(Integer id); + + /** + * 新增应用信息。 + * 由入参构造实体并落库;appKey 为整型,从 100000 起按数据库最大值自增,appSecret 为 32 位 UUID。 + * + * @param request 应用新增入参 + */ + void add(ApplicationInfoRequest request); + + /** + * 编辑应用信息。 + * 按 id 查找后拷贝入参字段并保存,appKey/appSecret 不随编辑变更。 + * + * @param id 应用ID + * @param request 应用编辑入参 + */ + void edit(Integer id, ApplicationInfoRequest request); + + /** + * 根据 id 删除应用信息。 + * + * @param id 应用ID + */ + void delete(Integer id); + + /** + * 重设应用密钥(appSecret)。 + * 按 id 查找应用并生成新的 32 位 UUID 密钥后保存。 + * + * @param id 应用ID + */ + void resetAppSecret(Integer id); + + /** + * 根据条件分页查询应用列表。 + * + * @param query 查询条件(name 关键词按名称模糊或 appKey 精确匹配,categoryId 按类别精确匹配) + * @param pageable 分页参数(page 从 0 起,size 每页条数) + * @return 分页后的应用视图(含总条数 totalElements) + */ + Page list(ApplicationInfoQuery query, Pageable pageable); + + List findListByCategoryId(Integer categoryId); + + /** + * 同一应用类型内排序。 + * 入参为该类型下应用的完整有序 id 列表,按列表顺序写入 sortNo(1 起)。 + * 接口会额外校验:所有 id 必须存在且同属一个应用类型,避免跨类型串号导致排序错乱。 + * + * @param ids 同一应用类型下的应用ID有序列表 + */ + void sort(List ids); + + /** + * 保存应用推送设置(外部接口地址 / 总开关 / 启用的接口 / 学校范围 / 角色范围 / 推送密钥)。 + * 仅更新推送相关字段,写后同步更新 appInfo 缓存。 + * + * @param id 应用ID + * @param request 推送设置(各字段可选,null 表示不修改) + */ + ApplicationInfo savePushSetting(Integer id, PushSettingRequest request); + + /** + * 获取应用推送设置回显 VO。 + * + * @param id 应用ID + * @return 推送设置 VO(仅含推送相关字段) + */ + PushSettingVo getPushSetting(Integer id); +} diff --git a/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java new file mode 100644 index 0000000..95ef9c6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java @@ -0,0 +1,190 @@ +package com.yunhai.userbase.service; + +import com.yunhai.platform.utils.model.BoaModule; +import com.yunhai.userbase.model.groupauthz.*; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用权限服务(应用开通 + 组授权 + 权限判定)。 + */ +public interface IGroupAuthzService { + + /** + * 功能1回显:查询系统全量应用列表,并标记该组已开通的应用(checked)。 + * + * @param groupId 组ID + * @return 全量应用列表(含开通勾选状态) + */ + List listGroupApps(Integer groupId); + + /** + * 功能1保存:全量覆盖式更新组的应用开通记录(先删旧记录再批量插入)。 + * 被取消开通的应用,同步清理该组在其模块上的权限记录,避免残留脏数据。 + * + * @param request 组ID + 勾选的应用ID全量列表 + */ + void assignAppsToGroup(GroupAppAssignRequest request); + + /** + * 功能2数据加载:查询该组已开通的应用及其模块树,并回显勾选状态与子组继承开关。 + * 仅返回 sys_group_app_rel 中有记录的应用;未开通的应用不显示。 + * + * @param groupId 组ID + * @return 已开通应用列表(含模块权限树) + */ + List findGroupAppsWithModules(Integer groupId); + + /** + * 功能2数据加载(按应用分类分组版):与 {@link #findGroupAppsWithModules(Integer)} 数据一致, + * 但按应用类型分类聚合,便于组授权页面按分类面板展示。 + * 返回全部分类(含空数组),保证前端分类筛选下拉完整。 + * + * @param groupId 组ID + * @return 按应用分类分组的已开通应用列表 + */ + List findGroupAppsWithModulesByCategory(Integer groupId); + + /** + * 功能2保存:全量覆盖式更新组的模块权限。 + * 有旧记录则更新 inheritFlag,无则新增,不在传入列表中的旧记录物理删除。 + * + * @param request 组ID + 勾选的模块权限全量列表(moduleId + inheritFlag) + */ + void saveModulePermissions(GroupModulePermRequest request); + + /** + * 权限判定(供登录/拦截器使用):判断用户对某模块是否有权限。 + * 规则:用户任一所属组,沿祖先链回溯——自身显式勾选即通过;祖先勾选且 inheritFlag=true 即通过。 + * 前置校验:该所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 该组已开通的应用)。 + * 角色授权并集:用户经「启用状态」业务角色获得该模块授权也通过,且不要求组已开通模块所属应用;禁用角色不参与判定。 + * + * @param userId 用户ID + * @param moduleId 模块ID + * @return true=有权限 + */ + boolean hasModulePermission(Integer userId, Integer moduleId); + + /** + * 权限一览:查询指定组下的直接用户列表(不含子组,不分页)。 + * + * @param groupId 组ID + * @return 用户列表 + */ + List findGroupUsers(Integer groupId); + + /** + * 权限一览:全局搜索用户(按账号/姓名/身份证模糊、手机号精确匹配),不分页。 + * 返回用户及其主归属部门/组织路径。 + * + * @param keyword 搜索关键字 + * @return 用户列表 + */ + List searchUsers(String keyword); + + /** + * 权限一览:根据用户ID聚合该用户的全部已授权应用模块树。 + * 只返回有授权模块的应用,并按应用类型分组;无授权的应用类型和应用不显示。 + * 权限判定复用 {@link #hasModulePermission(Integer, Integer)} 的祖先链继承逻辑。 + * 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块; + * 最终应用范围 = 组开通应用 ∪ 角色模块所属应用(角色模块不要求组必须先开通应用)。 + * + * @param groupId 组ID(上下文,可选;目前仅用于校验用户存在性,实际聚合以 userId 为准) + * @param userId 用户ID + * @return 按应用类型分组的已授权应用模块树 + */ + List findUserPermissionTree(Integer groupId, Integer userId); + + /** + * 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。 + * 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。 + * + * @param userId 用户ID + * @return 用户所属角色列表 + */ + List findUserRoles(Integer userId); + + /** + * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。
+ * push.topic (topic, durable) + * └─ push.main (durable, concurrency=1, x-dead-letter-exchange=push.dlx) ← 串行消费,可观测 + * push.dlx (fanout, durable) + * └─ push.dlq (durable) ← 死信归档(重试耗尽) + *
+ * 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用; + * 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。
+ * 支持 X-Forwarded-For / X-Real-IP 等常见代理头解析,取最左侧真实来源 IP 做校验。 + */ +public class SignKeyInternalIpFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(SignKeyInternalIpFilter.class); + + private final SignKeyInternalProperties properties; + + public SignKeyInternalIpFilter(SignKeyInternalProperties properties) { + this.properties = properties; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!properties.isEnabled()) { + filterChain.doFilter(request, response); + return; + } + String sourceIp = resolveSourceIp(request); + List effectiveIps = properties.getEffectiveIps(); + if (CollectionUtils.isEmpty(effectiveIps) || matchAny(sourceIp, effectiveIps)) { + filterChain.doFilter(request, response); + return; + } + log.warn("[SignKeyInternal] 拒绝非白名单来源访问内部接口: uri={}, sourceIp={}, whitelist={}", + request.getRequestURI(), sourceIp, effectiveIps); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"msg\":\"内部接口不允许从该来源访问\",\"data\":null}"); + response.getWriter().flush(); + } + + private static String resolveSourceIp(HttpServletRequest request) { + String[] headers = new String[] { + "X-Forwarded-For", + "X-Real-IP", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + for (String header : headers) { + String raw = request.getHeader(header); + if (raw == null || raw.isEmpty() || "unknown".equalsIgnoreCase(raw)) { + continue; + } + // X-Forwarded-For 可能是 多个IP逗号分隔,取最左侧 + String first = raw.split(",")[0].trim(); + if (!first.isEmpty()) { + return first; + } + } + return request.getRemoteAddr(); + } + + private static boolean matchAny(String ip, List cidrs) { + for (String cidr : cidrs) { + if (matchCidr(ip, cidr)) { + return true; + } + } + return false; + } + + private static boolean matchCidr(String ip, String cidr) { + if (ip == null || cidr == null) { + return false; + } + try { + int prefixLen; + String network; + if (cidr.contains("/")) { + String[] parts = cidr.split("/"); + network = parts[0].trim(); + prefixLen = Integer.parseInt(parts[1].trim()); + } else { + network = cidr.trim(); + // 判断地址族决定默认前缀长度 + InetAddress na = InetAddress.getByName(network); + prefixLen = (na instanceof Inet6Address) ? 128 : 32; + } + InetAddress ipAddr = InetAddress.getByName(ip); + InetAddress netAddr = InetAddress.getByName(network); + if (ipAddr.getClass() != netAddr.getClass()) { + return false; + } + byte[] ipBytes = ipAddr.getAddress(); + byte[] netBytes = netAddr.getAddress(); + int bits = prefixLen; + for (int i = 0; i < ipBytes.length && bits > 0; i++) { + int mask = bits >= 8 ? 0xFF : (0xFF << (8 - bits)); + if ((ipBytes[i] & mask) != (netBytes[i] & mask)) { + return false; + } + bits -= 8; + } + return true; + } catch (UnknownHostException | NumberFormatException ignored) { + return false; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java new file mode 100644 index 0000000..f5eb76b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java @@ -0,0 +1,55 @@ +package com.yunhai.userbase.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 内部开放接口(/oapi/internal/*)访问白名单 IP 配置。 + * 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。 + * + * 可在 Nacos 上配置,例如: + * yh: + * sign-key-internal: + * ips: + * - 10.0.0.0/8 + * - 172.16.0.0/12 + * - 192.168.0.0/16 + * - 127.0.0.1/32 + * - 172.20.10.5/32 # 某台固定机器 + * + * 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。 + */ +@ConfigurationProperties(prefix = "yh.sign-key-internal") +public class SignKeyInternalProperties { + + /** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */ + private List ips = new ArrayList<>(); + + /** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */ + private boolean enabled = true; + + public List getIps() { return ips; } + + public void setIps(List ips) { this.ips = ips; } + + public boolean isEnabled() { return enabled; } + + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + /** 未配置 ips 时返回默认私有段 + localhost 规则。 */ + public List getEffectiveIps() { + if (ips != null && !ips.isEmpty()) { + return ips; + } + return Arrays.asList( + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "127.0.0.1/32", + "0:0:0:0:0:0:0:1/128" + ); + } +} diff --git a/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java new file mode 100644 index 0000000..3018a12 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/SwaggerConfig.java @@ -0,0 +1,37 @@ +package com.yunhai.userbase.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; + +/** + * 接口文档(springdoc-openapi,替代旧 Springfox)。 + * 提供统一文档元信息;并通过全局定制器隐藏由切面注入的 {@code BoaUser authUserWithDept} 参数, + * 避免其作为请求体出现在文档中(该参数不参与 HTTP 入参,由 {@code BoaUserArgumentResolver} 解析)。 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI userBaseOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("用户中心接口文档") + .description("UserBase 用户中心 OpenAPI 文档(springdoc-openapi)") + .version("0.0.1")); + } + + /** 隐藏切面注入的登录用户参数,保持文档整洁。 */ + @Bean + public GlobalOperationCustomizer hideAuthUserParam() { + return (operation, handlerMethod) -> { + if (operation.getParameters() != null) { + operation.getParameters().removeIf(p -> "authUserWithDept".equals(p.getName())); + } + return operation; + }; + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java new file mode 100644 index 0000000..3f07052 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseAppSecretProvider.java @@ -0,0 +1,98 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.AppAuthProperties; +import com.yunhai.platform.appauth.AppSecretProvider; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * UserBase 作为「应用密钥权威源」的验签密钥提供方。 + * + * 解析优先级: + * 1) 本库 application_info —— 覆盖全部「已颁发」应用,新增/重置密钥后即时生效; + * 2) 配置式受信应用 {@code yh.app-auth.trusted-apps} —— 平台自身默认密钥(如 应用中心 100616) + * 兜底,使其即便尚未落库也可验签,避免「默认 appKey 未建行」导致验签失败。 + * + * 带 60s 本地 TTL 缓存,避免每次验签直打库(密钥变更最长 60s 后生效)。 + * + * 这样 UserBase 作为颁发方不依赖 Redis 预热、也不局限于 trustedApps 写死的少量密钥, + * 能够验签所有它已颁发的应用。 + */ +public class UserBaseAppSecretProvider implements AppSecretProvider { + + private final ApplicationInfoRepository repository; + private final Map trustedApps; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private static final long TTL_MS = 60_000L; + + public UserBaseAppSecretProvider(ApplicationInfoRepository repository, AppAuthProperties properties) { + this.repository = repository; + this.trustedApps = properties.getTrustedApps(); + } + + @Override + public String getSecret(String appKey) { + if (appKey == null) { + return null; + } + long now = System.currentTimeMillis(); + CacheEntry hit = cache.get(appKey); + if (hit != null && now - hit.ts < TTL_MS) { + return hit.secret; + } + String secret = null; + try { + Integer id = parse(appKey); + if (id != null) { + ApplicationInfo info = repository.findByAppKey(id); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + secret = info.getAppSecret(); + } + } + } catch (Exception ignored) { + // 查询异常不阻断,继续走配置兜底 + } + if (secret == null) { + secret = trustedApps.get(appKey); + } + cache.put(appKey, new CacheEntry(now, secret)); + return secret; + } + + /** + * 主动失效:立刻清除本地缓存中指定 appKey 的条目,密钥重置后调用 → 下一次 getSecret 直查 DB 拿新值, + * 不再受最长 60s TTL 生效窗口限制。 + */ + public void invalidate(String appKey) { + if (appKey == null) { + return; + } + cache.remove(appKey); + } + + /** 主动失效:立刻清空全部本地缓存(refreshAll / Redis 全量对账时调用)。 */ + public void invalidateAll() { + cache.clear(); + } + + private static Integer parse(String appKey) { + try { + return Integer.parseInt(appKey.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + private static final class CacheEntry { + final long ts; + final String secret; + + CacheEntry(long ts, String secret) { + this.ts = ts; + this.secret = secret; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java new file mode 100644 index 0000000..1c553ac --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java @@ -0,0 +1,71 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.appauth.LocalAppIdentityProvider; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * UserBase 自身作为「出站签名调用方」的身份提供方。 + * 约定: + * 1) appKey 固定占用 100000(基石应用段),由 application_bootstrap.sql 初始化脚本幂等插入; + * 2) appSecret 从 application_info 表按 appKey=100000 读取,带 60s 本地缓存(密钥重设后 60s 内生效)。 + * 显式覆盖 yh-app-auth-spring-boot-starter 默认的 PropertyLocalAppIdentityProvider(yml 不再需要 + * 配置 yh.app-auth.app-key / app-secret)。 + */ +public class UserBaseLocalAppIdentityProvider implements LocalAppIdentityProvider { + + private static final String USER_CENTER_APP_KEY = "100000"; + private static final int USER_CENTER_APP_KEY_INT = 100000; + private static final long TTL_MS = 60_000L; + + private final ApplicationInfoRepository repository; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + public UserBaseLocalAppIdentityProvider(ApplicationInfoRepository repository) { + this.repository = repository; + } + + @Override + public String getAppKey() { + return USER_CENTER_APP_KEY; + } + + @Override + public String getAppSecret() { + long now = System.currentTimeMillis(); + CacheEntry hit = cache.get(USER_CENTER_APP_KEY); + if (hit != null && now - hit.ts < TTL_MS) { + return hit.secret; + } + String secret = null; + try { + ApplicationInfo info = repository.findByAppKey(USER_CENTER_APP_KEY_INT); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + secret = info.getAppSecret(); + } + } catch (Exception ignored) { + } + cache.put(USER_CENTER_APP_KEY, new CacheEntry(now, secret)); + return secret; + } + + /** + * 主动失效:立刻清除 UserBase 自身(appKey=100000)的本地缓存,密钥重置后调用 → + * 下一次 getAppSecret 直查 DB 拿新值,不再受最长 60s TTL 生效窗口限制。 + */ + public void invalidate() { + cache.remove(USER_CENTER_APP_KEY); + } + + private static final class CacheEntry { + final long ts; + final String secret; + + CacheEntry(long ts, String secret) { + this.ts = ts; + this.secret = secret; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java new file mode 100644 index 0000000..ccfec7b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/config/UserBaseUserDetailsProvider.java @@ -0,0 +1,41 @@ +package com.yunhai.userbase.config; + +import com.yunhai.platform.auth.UserDetailsProvider; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.service.impl.UserCenterService; +import com.yunhai.userbase.service.impl.UserCenterService; + +/** + * 平台 {@link UserDetailsProvider} 的 UserBase 本地实现:CAS 会话流解析出登录名/手机号后, + * 由本类查 UserBase 自身库,组装成基础平台统一的 {@link BoaUser}。 + * + * 本类不再加 {@code @Component},改为由 {@link OpenApiAuthEnforceConfiguration} + * 显式以 {@code @Bean @Primary} 注册(bean 名固定为 {@code userDetailsProvider}), + * 原因:yh-auth-spring-boot-starter 新版 AutoConfiguration 处理顺序可能早于 UserBase 组件扫描, + * 导致其默认注册的 {@code NoOpUserDetailsProvider}(直接抛 UnsupportedOperationException) + * 抢先被 PathBasedAuthInterceptor 注入。显式 @Bean+@Primary 可稳定覆盖默认实现。 + */ +public class UserBaseUserDetailsProvider implements UserDetailsProvider { + + private final UserCenterService userCenterService; + + public UserBaseUserDetailsProvider(UserCenterService userCenterService) { + this.userCenterService = userCenterService; + } + + @Override + public BoaUser loadByLoginName(String loginName) { + if (loginName == null) { + return null; + } + return userCenterService.getUserInfoWithDeptByAccount(loginName.trim()); + } + + @Override + public BoaUser loadByPhone(String phone) { + if (phone == null) { + return null; + } + return userCenterService.getUserInfoWithDeptByPhone(phone.trim()); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java b/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java new file mode 100644 index 0000000..f25da78 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/AppRoleController.java @@ -0,0 +1,135 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; +import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; +import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; +import com.yunhai.userbase.model.approle.AppRoleUserVo; +import com.yunhai.userbase.model.approle.SysAppRoleQuery; +import com.yunhai.userbase.model.approle.SysAppRoleRequest; +import com.yunhai.userbase.model.approle.SysAppRoleVo; +import com.yunhai.userbase.model.approle.UserAppRoleBindRequest; +import com.yunhai.userbase.service.IAppRoleService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.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; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色管理(角色 CRUD + 角色模块授权 + 用户角色绑定)。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/approle/") +public class AppRoleController { + + @Autowired + private IAppRoleService appRoleService; + + /** + * 角色分页列表 :name 模糊,appId/allAppFlag/disabled 精确;普通角色回填所属应用名称 + */ + @GetMapping("/RoleList") + public BasePageResult roleList(SysAppRoleQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "id", direction = Sort.Direction.DESC) Sort sort) { + return PageUtils.toResult(appRoleService.list(query, PageUtils.toPageable(pageQuery, sort))); + } + + /** + * 新增角色:普通角色(allAppFlag=false,appId 必填,同应用下名称唯一); + * 全应用角色(allAppFlag=true,appId 为空,全应用角色之间名称唯一)。 + */ + @OperationLog("新增角色") + @PostMapping("RoleAdd") + public void roleAdd(@RequestBody @Validated(ValidationGroups.Create.class) SysAppRoleRequest request) { + appRoleService.add(request); + } + + /** + * 编辑角色:仅可改 name/remark/disabled;allAppFlag 与 appId 不允许修改。 + */ + @OperationLog("编辑角色") + @PostMapping("RoleEdit/{id}") + public void roleEdit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysAppRoleRequest request) { + appRoleService.edit(id, request); + } + + /** + * 删除角色:级联清理该角色的模块授权与用户绑定记录。 + */ + @OperationLog("删除角色") + @PostMapping("RoleDelete/{id}") + public void roleDelete(@PathVariable Integer id) { + appRoleService.delete(id); + } + + /** + * 角色模块授权回显:返回该角色可授权的应用及模块树(已授权 checked=true)。 + * 普通角色仅本应用;全应用角色返回全部应用(可跨应用授权)。 + */ + @GetMapping("/RoleModuleTree") + public List roleModuleTree(@RequestParam Integer roleId) { + return appRoleService.findRoleModuleTree(roleId); + } + + /** + * 角色模块授权保存:全量覆盖式更新该角色的模块授权记录。 + */ + @OperationLog("保存角色模块授权") + @PostMapping("SaveRoleModules") + public void saveRoleModules(@RequestBody @Validated AppRoleModuleAssignRequest request) { + appRoleService.saveRoleModules(request); + } + + /** + * 查询角色已绑定的用户列表。 + */ + @GetMapping("/RoleUsers") + public List roleUsers(@RequestParam Integer roleId) { + return appRoleService.findRoleUsers(roleId); + } + + /** + * 角色绑定用户保存(角色维度,全量覆盖)。 + */ + @OperationLog("保存角色用户绑定") + @PostMapping("SaveRoleUsers") + public void saveRoleUsers(@RequestBody @Validated AppRoleUserBindRequest request) { + appRoleService.saveRoleUsers(request); + } + + /** + * 用户绑定角色保存(用户维度,全量覆盖)。 + */ + @OperationLog("保存用户角色绑定") + @PostMapping("SaveUserRoles") + public void saveUserRoles(@RequestBody @Validated UserAppRoleBindRequest request) { + appRoleService.saveUserRoles(request); + } + + /** + * 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。 + * 前端「用户管理 - 删除」调用此接口。 + */ + @OperationLog("移除角色用户绑定") + @PostMapping("RemoveRoleUser") + public void removeRoleUser(@RequestParam Integer roleId, @RequestParam Integer userId) { + appRoleService.removeRoleUser(roleId, userId); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java b/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java new file mode 100644 index 0000000..cc3e9d6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/AppSettingsController.java @@ -0,0 +1,178 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.appauthz.AppAuthzCategoryVo; +import com.yunhai.userbase.model.appcategory.AppCategoryQuery; +import com.yunhai.userbase.model.appcategory.AppCategoryRequest; +import com.yunhai.userbase.model.appcategory.AppCategoryVo; +import com.yunhai.userbase.model.application.ApplicationInfoQuery; +import com.yunhai.userbase.model.application.ApplicationInfoRequest; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.model.application.PushSettingRequest; +import com.yunhai.userbase.model.application.PushSettingVo; +import com.yunhai.userbase.model.appmodule.AppModuleQuery; +import com.yunhai.userbase.model.appmodule.AppModuleRequest; +import com.yunhai.userbase.model.appmodule.AppModuleVo; +import com.yunhai.userbase.service.IAppCategoryService; +import com.yunhai.userbase.service.IApplicationInfoService; +import com.yunhai.userbase.service.IAppModuleService; +import com.yunhai.userbase.service.impl.SignKeyCacheService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = Config.API_PATH + "/app/") +public class AppSettingsController { + + @Autowired + private IAppCategoryService appCategoryService; + + @Autowired + private IApplicationInfoService applicationInfoService; + + @Autowired + private IAppModuleService appModuleService; + + @Autowired + private SignKeyCacheService signKeyCacheService; + + + + @OperationLog("新增应用类型") + @PostMapping("AddType") + public void addType(@RequestBody @Validated(ValidationGroups.Create.class) AppCategoryRequest request) { + appCategoryService.add(request); + } + + @OperationLog("编辑应用类型") + @PostMapping("EditType/{id}") + public void editType(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) AppCategoryRequest request) { + appCategoryService.edit(id, request); + } + + @OperationLog("删除应用类型") + @PostMapping("DeleteType/{id}") + public void deleteType(@PathVariable Integer id) { + appCategoryService.delete(id); + } + + @GetMapping("/AppTypeList") + public List appTypeList(AppCategoryQuery query) { + return appCategoryService.list(query); + } + + @OperationLog("应用类型排序") + @PostMapping("/AppTypeSort") + public void appTypeSort(@RequestBody List ids) { + appCategoryService.sort(ids); + } + + /** + * 应用授权用:获取所有应用类型及其类型下的应用(每个应用含 id/name/icon)。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无应用的类型 app 为空数组。 + */ + @GetMapping("/AppAuthzList") + public List appAuthzList() { + return appCategoryService.listWithApps(); + } + + @OperationLog("新增应用") + @PostMapping("AddApp") + public void addApp(@RequestBody @Validated(ValidationGroups.Create.class) ApplicationInfoRequest request) { + applicationInfoService.add(request); + } + + @OperationLog("编辑应用") + @PostMapping("EditApp/{id}") + public void editApp(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) ApplicationInfoRequest request) { + applicationInfoService.edit(id, request); + } + + @OperationLog("删除应用") + @PostMapping("DeleteApp/{id}") + public void deleteApp(@PathVariable Integer id) { + applicationInfoService.delete(id); + } + + @OperationLog("重置应用密钥") + @PostMapping("ResetAppSecret/{id}") + public String resetAppSecret(@PathVariable Integer id) { + applicationInfoService.resetAppSecret(id); + ApplicationInfo info= applicationInfoService.getById(id); + return info.getAppSecret(); + } + + @OperationLog("全量刷新应用签名密钥到Redis") + @PostMapping("RefreshSignKeys") + public Map refreshSignKeys() { + int count = signKeyCacheService.refreshAll(); + Map result = new LinkedHashMap<>(); + result.put("refreshed", count); + return result; + } + + @GetMapping("/AppList") + public BasePageResult appList(ApplicationInfoQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "sortNo", direction = Sort.Direction.ASC) Sort sort) { + return PageUtils.toResult(applicationInfoService.list(query, PageUtils.toPageable(pageQuery, sort))); + } + + @OperationLog("应用排序") + @PostMapping("/AppSort") + public void appSort(@RequestBody List ids) { + applicationInfoService.sort(ids); + } + + @GetMapping("/AppSortList") + public List appSortList(@RequestParam Integer categoryId) { + return applicationInfoService.findListByCategoryId(categoryId); + } + + @OperationLog("新增模块") + @PostMapping("AddModule") + public void addModule(@RequestBody @Validated(ValidationGroups.Create.class) AppModuleRequest request) { + appModuleService.add(request); + } + + @OperationLog("编辑模块") + @PostMapping("EditModule/{id}") + public void editModule(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) AppModuleRequest request) { + appModuleService.edit(id, request); + } + + @OperationLog("删除模块") + @PostMapping("DeleteModule/{id}") + public void deleteModule(@PathVariable Integer id) { + appModuleService.delete(id); + } + + @GetMapping("/ModuleList") + public List moduleList(AppModuleQuery query) { + return appModuleService.list(query); + } + + @OperationLog("保存应用推送设置") + @PostMapping("SavePushSetting/{id}") + public void savePushSetting(@PathVariable Integer id, @RequestBody PushSettingRequest request) { + applicationInfoService.savePushSetting(id, request); + } + + @GetMapping("GetPushSetting/{id}") + public PushSettingVo getPushSetting(@PathVariable Integer id) { + return applicationInfoService.getPushSetting(id); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/BaseController.java b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java new file mode 100644 index 0000000..ee159c5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java @@ -0,0 +1,142 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.enums.IEnum; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.enums.*; +import com.yunhai.userbase.utils.*; +import com.yunhai.platform.utils.exception.DException; +import org.springframework.web.multipart.MultipartFile; +import lombok.var; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.*; +import java.util.function.Supplier; + +@RestController +@RequestMapping(value = Config.API_PATH +"/base/") +public class BaseController { + + + @Autowired + private RedisUtil redisUtil; + + /** + * 一般hashMap初始化大小 + */ + public static final int initialCapacity = 6; + + /** + * 通用上传允许的子目录(与 MyWebAppConfigurer.addResourceHandlers 的资源映射一一对应)。 + * 注:html 因 FileUtil 的危险后缀黑名单被禁止通过此接口上传,故未列入,避免存储可被浏览器执行的 HTML 引发 XSS。 + */ + private static final Set ALLOWED_UPLOAD_TYPES = new HashSet<>(Arrays.asList("image", "file", "icon", "word")); + + @GetMapping("GetUserInfo") + public Object getUserInfo(BoaUser authUserWithDept){ + return authUserWithDept; + } + + //返回系统枚举2 + @GetMapping("GetSysDict") + public Map getSysDict(BoaUser authUserWithDept){ + Map resultMap = new HashMap<>(7); + resultMap.put("grouptype", IEnum.toSimpleMapList(GroupTypeEnum.class, "k", "v")); + resultMap.put("gender", IEnum.toSimpleMapList(GenderEnum.class, "k", "v")); + resultMap.put("client", IEnum.toSimpleMapList(ClientEnum.class, "k", "v")); + resultMap.put("integ", IEnum.toSimpleMapList(IntegEnum.class, "k", "v")); + resultMap.put("status", IEnum.toSimpleMapList(StatusEnum.class, "k", "v")); + resultMap.put("sysrole", IEnum.toSimpleMapList(SysRoleEnum.class, "k", "v")); + resultMap.put("mode", IEnum.toSimpleMapList(SysModeEnum.class, "k", "v")); + return resultMap; + } + + /** + * 手动管理 Redis Hash 缓存的示例(@Cacheable 注解无法满足的场景)。 + * 适用场景:需要把一个对象的多个字段按 Hash 结构存储, + * 可单独读写/更新某个字段,而不像 @Cacheable 那样把整个对象序列化成一个大 value。 + * 访问路径:GET {Config.API_PATH}/base/ManualHashCacheDemo。 + * 注:手动缓存依赖 Redis;Redis 不可用时 RedisUtil 已内部捕获异常,操作失败但不影响接口本身。 + * + * @return 本次演示各步骤的结果快照 + */ + @GetMapping("ManualHashCacheDemo") + public Map manualHashCacheDemo() { + Map result = new HashMap<>(8); + String hashKey = "demo:user:1001"; + // 1) 写入整张 hash(带 TTL):适合一次性缓存一个对象的多个属性 + Map profile = new HashMap<>(4); + profile.put("name", "yunhai"); + profile.put("age", 18); + profile.put("role", "admin"); + redisUtil.hmset(hashKey, profile, 300); + // 2) 只更新其中一个字段,无需整体覆盖 + redisUtil.hset(hashKey, "role", "superadmin"); + // 3) 读取单个字段 + result.put("roleAfterUpdate", redisUtil.hget(hashKey, "role")); + // 4) 读取整张 hash + result.put("wholeHash", redisUtil.hmget(hashKey)); + // 5) 判断字段是否存在 + result.put("hasAgeField", redisUtil.hHasKey(hashKey, "age")); + // 6) Cache-Aside(复用下方私有方法):字段不存在时回源并写回 Hash + Object nickname = getHashFieldWithFallback(hashKey, "nickname", () -> "yunhai-nick"); + result.put("nickname", nickname); + // 7) 删除某个字段 + redisUtil.hdel(hashKey, "age"); + result.put("afterDelAge", redisUtil.hmget(hashKey)); + return result; + } + + + + @GetMapping("RedirectTo/{id}") + public RedirectView RedirectTo(BoaUser authUserWithDept, @PathVariable(name = "id") String id) { + String url= OtherUtil.hexToString(id); + return new RedirectView(url,true); + } + + /** + * 通用文件上传接口(一个接口覆盖所有类型,无需按类型拆分)。 + * type 决定存储子目录,与 MyWebAppConfigurer.addResourceHandlers 的 /upload/{type}/** 映射一一对应, + * 上传成功后可直接用返回的 URL 访问,无需再改资源映射配置。 + * 访问路径:POST {Config.API_PATH}/base/UploadFile(multipart/form-data)。 + * 支持 type:image / file / icon / word(html 被 FileUtil 黑名单禁止上传)。 + * + * @param file 上传文件(必填,单文件上限 50MB,见 application.yml 的 multipart 配置) + * @param type 存储子目录类型,默认 image + * @return 文件对外可访问的 Web URL + */ + @PostMapping("UploadFile") + public String uploadFile(@RequestParam("file") MultipartFile file, + @RequestParam(value = "type", defaultValue = "image") String type) { + if (!ALLOWED_UPLOAD_TYPES.contains(type)) { + throw new DException("不支持的上传类型: " + type + ",可选: image/file/icon/word"); + } + // FileUtil.upload 内部已处理空文件校验、危险后缀黑名单、目录创建与访问 URL 拼接 + return FileUtil.upload(file, type); + } + + /** + * 私有方法(可复用):演示 Hash 结构的 Cache-Aside(缓存旁路)模式。 + * 先从 Redis Hash 读字段;读不到则回源(此处用 fallback 模拟数据库查询),再把结果写回 Hash。 + * + * @param hashKey hash 键 + * @param field 字段名 + * @param fallback 回源加载器(真实场景替换为查数据库/远程调用) + * @return 字段值 + */ + private Object getHashFieldWithFallback(String hashKey, String field, Supplier fallback) { + if (redisUtil.hHasKey(hashKey, field)) { + return redisUtil.hget(hashKey, field); + } + Object value = fallback.get(); + if (value != null) { + redisUtil.hset(hashKey, field, value); + } + return value; + } + +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java b/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java new file mode 100644 index 0000000..3297b28 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/GroupAuthzController.java @@ -0,0 +1,179 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.groupauthz.GroupAppAssignRequest; +import com.yunhai.userbase.model.groupauthz.GroupAppVo; +import com.yunhai.userbase.model.groupauthz.GroupAuthzAppVo; +import com.yunhai.userbase.model.groupauthz.GroupAuthzCategoryVo; +import com.yunhai.userbase.model.groupauthz.GroupModuleAuthzItem; +import com.yunhai.userbase.model.groupauthz.GroupModulePermRequest; +import com.yunhai.userbase.model.groupauthz.SchoolAppAuthzRequest; +import com.yunhai.userbase.model.groupauthz.UserGroupNodeVo; +import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo; +import com.yunhai.userbase.model.groupauthz.UserPermissionUserVo; +import com.yunhai.userbase.model.groupauthz.UserRoleVo; +import com.yunhai.userbase.service.IGroupAuthzService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import javax.validation.Valid; +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; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用权限管理(应用开通 + 组授权 + 权限判定)。 + * 左侧组树复用 /sysgroup/GetTree;本控制器提供右侧数据加载与保存接口。 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/groupauthz/") +public class GroupAuthzController { + + @Autowired + private IGroupAuthzService groupAuthzService; + + /** + * 功能1回显:点击左侧组后,返回系统全量应用列表,已开通的 checked=true。 + */ + @GetMapping("/GroupAppList") + public List groupAppList(@RequestParam Integer groupId) { + return groupAuthzService.listGroupApps(groupId); + } + + /** + * 功能1保存:全量覆盖式更新该组的应用开通记录。 + */ + @OperationLog("保存组应用开通") + @PostMapping("AssignApps") + public void assignApps(@RequestBody @Validated GroupAppAssignRequest request) { + groupAuthzService.assignAppsToGroup(request); + } + + /** + * 功能2数据加载:返回该组已开通的应用及其模块权限树(含勾选与子组继承回显)。 + */ + @GetMapping("/GroupModuleTree") + public List groupModuleTree(@RequestParam Integer groupId) { + return groupAuthzService.findGroupAppsWithModules(groupId); + } + + /** + * 功能2数据加载(按应用分类分组):供组授权页面按分类面板展示, + * 返回全部分类及该组已开通的应用模块权限树。 + */ + @GetMapping("/GroupModuleTreeByCategory") + public List groupModuleTreeByCategory(@RequestParam Integer groupId) { + return groupAuthzService.findGroupAppsWithModulesByCategory(groupId); + } + + /** + * 功能2保存:全量覆盖式更新该组的模块权限(含子组继承开关)。 + */ + @OperationLog("保存组模块权限") + @PostMapping("SaveModulePerms") + public void saveModulePerms(@RequestBody @Validated GroupModulePermRequest request) { + groupAuthzService.saveModulePermissions(request); + } + + /** + * 权限判定:判断用户对某模块是否有权限(供登录/拦截器/联调验证使用)。 + */ + @GetMapping("/HasModulePerm") + public boolean hasModulePerm(@RequestParam Integer userId, @RequestParam Integer moduleId) { + return groupAuthzService.hasModulePermission(userId, moduleId); + } + + /** + * 权限一览:根据组ID获取该组下的直接用户列表(不含子组,不分页)。 + */ + @GetMapping("/GroupUsers") + public List groupUsers(@RequestParam Integer groupId) { + return groupAuthzService.findGroupUsers(groupId); + } + + /** + * 权限一览:全局搜索用户(按账号/姓名/身份证模糊、手机号精确匹配),不分页。 + * 返回用户及其主归属部门/组织路径,供左侧用户搜索形态展示。 + */ + @GetMapping("/SearchUsers") + public List searchUsers(@RequestParam String keyword) { + return groupAuthzService.searchUsers(keyword); + } + + /** + * 权限一览:根据用户ID聚合该用户的全部已授权应用模块树。 + * 只返回有授权模块的应用,并按应用类型分组;无授权的应用类型和应用不显示。 + * 角色授权并集:结果 = 组授权模块 ∪ 启用角色授权模块(角色模块不要求组先开通应用)。 + */ + @GetMapping("/UserPermissionTree") + public List userPermissionTree(@RequestParam Integer userId, + @RequestParam(required = false) Integer groupId) { + return groupAuthzService.findUserPermissionTree(groupId, userId); + } + + /** + * 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。 + * 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。 + */ + @GetMapping("/UserRoles") + public List userRoles(@RequestParam Integer userId) { + return groupAuthzService.findUserRoles(userId); + } + + /** + * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。 + * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组)。 + */ + @GetMapping("/UserGroupTree") + public List userGroupTree(@RequestParam Integer userId) { + return groupAuthzService.findUserGroupTree(userId); + } + + // ===================== 新增:树节点式增量授权(左侧树 + 右侧 appAuthzList 勾选交互) ===================== + + /** + * 功能3回显:点击左侧树节点后,返回该组「已开通的应用ID列表」,供右侧 AppSettings.appAuthzList 回显勾选状态。 + * 交互:左树节点 → 右 appAuthzList(全量应用)→ 本接口拿到已选 id → 前端把对应多选框置为勾选。 + * 实现:应用开通不继承,直接查 sys_group_app_rel(group_id=nodeId) 返回 app_id 列表(详见 service.findAuthorizedAppIds)。 + * 与 GroupAppList(返回全量应用+checked)互为补充:本接口更轻量,只给 id 集合。 + */ + @GetMapping("/GetSchoolAppId") + public List getSchoolAppId(@RequestParam Integer nodeId) { + return groupAuthzService.findAuthorizedAppIds(nodeId); + } + + /** + * 功能3保存(应用维):点击应用多选框触发,对该组做应用开通/取消。 + * 入参(请求体,JSON):{@link SchoolAppAuthzRequest} —— schid=组ID(nodeId);appid=单个应用ID(一次只传一个,逐次点击);mode=操作模式。 + * mode 语义(SysModeEnum):0=追加(开通应用) / 2=删除(取消开通);1=覆盖 不适用,会被拒绝。 + * 实现:应用开通不继承,仅操作本组;删除时同步清理该组在该应用下模块的权限残留(详见 service.setSchoolApps)。 + * 注意:appid 为单个 Integer,后端不再做多值/逗号解析;每次点击提交一次请求。 + */ + @OperationLog("设置组应用开通") + @PostMapping("SetSchoolApp") + public void setSchoolApp(@RequestBody @Valid SchoolAppAuthzRequest request) { + groupAuthzService.setSchoolApps(request); + } + + /** + * 功能4保存(模块维):前端「组授权」面板逐条勾选/取消时提交,对 (组,应用,模块) 做增量授权/取消。 + * 入参:请求体为数组,元素见 {@link GroupModuleAuthzItem}:{groupId, appId, moduleId, mode, isAll}。 + * mode:0=追加 / 2=删除(1=覆盖不适用)。 + * isAll:是否对子组继承,映射 sys_group_module_perm.inherit_flag(1=子孙组自动继承 / 0=仅本组)。 + * moduleId=0:表示「该应用下全部模块」,后端展开为应用下每个真实模块各写/删一条。 + * 实现:逐条处理、顺序即执行顺序;追加前要求该组已开通该应用;越权写别的应用模块会被拒绝(详见 service.setGroupModulePerms)。 + * 与现有 SaveModulePerms(全量覆盖)共存:本接口是单条增量,二者操作同一张表、互不冲突。 + */ + @OperationLog("设置组模块权限") + @PostMapping("SetGroupModule") + public void setGroupModule(@RequestBody @Valid List items) { + groupAuthzService.setGroupModulePerms(items); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java b/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java new file mode 100644 index 0000000..4c3c669 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/IdentityController.java @@ -0,0 +1,62 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.identity.IdentityCurrentVo; +import com.yunhai.userbase.model.identity.IdentityOptionVo; +import com.yunhai.userbase.model.identity.IdentitySwitchRequest; +import com.yunhai.userbase.service.impl.IdentityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +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.RestController; + +import java.util.List; + +/** + * 用户身份(平台身份角色 × 学校)管理接口。 + * 走 CAS 会话路线({@code /{platform}/identity/}),当前用户从 CAS 会话注入的 + * {@link BoaUser#getId()} 取,不接受外部传入 userId(防越权)。 + * 三个端点对应「切换身份」业务: + * ① {@code GET current} 当前激活身份(无切换记录则兜底主身份); + * ② {@code GET available} 全部可选身份清单; + * ③ {@code POST switch} 切换到指定 (roleId, schoolId),服务端校验属主后写入当前激活状态。 + * + * @author chenx + * @date 2026/8/18 + */ +@RestController +@RequestMapping(Config.API_PATH + "/identity/") +public class IdentityController { + + @Autowired + private IdentityService identityService; + + /** + * 当前激活身份。 + */ + @GetMapping("current") + public IdentityCurrentVo current(BoaUser authUserWithDept) { + return identityService.getCurrent(authUserWithDept.getId()); + } + + /** + * 全部可选身份(正常态),供「切换身份」页渲染。 + */ + @GetMapping("available") + public List available(BoaUser authUserWithDept) { + return identityService.listAvailable(authUserWithDept.getId()); + } + + /** + * 切换身份:校验属主后在 sys_user_current_role 写当前激活状态。 + */ + @PostMapping("switch") + public IdentityCurrentVo switchIdentity(BoaUser authUserWithDept, + @RequestBody @Validated IdentitySwitchRequest request) { + return identityService.switchIdentity(authUserWithDept.getId(), request); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java b/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java new file mode 100644 index 0000000..b16850d --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/PushSubscriptionController.java @@ -0,0 +1,97 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.push.PushRecordListVo; +import com.yunhai.userbase.model.push.PushRecordQuery; +import com.yunhai.userbase.model.push.PushTestRequest; +import com.yunhai.userbase.service.push.PushDispatcher; +import com.yunhai.userbase.service.push.PushRecordService; +import com.yunhai.userbase.utils.PageUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 推送订阅控制器:测试推送 + 推送日志记录 / 重试。 + * + * @author chenx + * @date 2026/8/14 + */ +@RestController +@Slf4j +@RequestMapping(value = Config.API_PATH + "/push/") +public class PushSubscriptionController { + + @Autowired + private PushDispatcher pushDispatcher; + @Autowired + private PushRecordService pushRecordService; + + /** 测试推送:手动触发一次推送(投递 MQ),并返回请求预览(URL/头/体,密钥不外露)。 */ + @PostMapping("Test") + public Map test(@RequestBody PushTestRequest request) { + pushDispatcher.dispatchManual(request.getAppId(), request.getInterfacePath(), request.getTargetIds()); + Map preview = pushDispatcher.previewManual( + request.getAppId(), request.getInterfacePath(), request.getTargetIds()); + Map result = new LinkedHashMap<>(); + result.put("sent", true); + result.put("message", "已投递推送队列,可在「推送日志」查看实际响应与耗时"); + result.put("preview", preview); + return result; + } + + /** 推送日志列表(不含 payload,避免全量推送数据量过大;payload 经 Download 接口单独获取)。 */ + @GetMapping("RecordList") + public BasePageResult recordList(PushRecordQuery query, BasePageQuery pageQuery, + @SortDefault(sort = "createdAt", direction = Sort.Direction.DESC) Sort sort) { + return PageUtils.toResult(pushRecordService.query(query, PageUtils.toPageable(pageQuery, sort))); + } + + /** 手动重试一条失败的推送记录。 */ + @PostMapping("Retry/{id}") + public void retry(@PathVariable Long id) { + pushRecordService.retry(id); + } + + /** + * 下载某条推送记录的请求体原文(payload 文本文件)。 + * 采用与项目 DownloadFileUtil 一致的 octet-stream + attachment 写法,确保浏览器稳定触发下载。 + * 若该记录未记录 payload,返回一段说明文本文件而非 404,便于排查。 + */ + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = pushRecordService.getPayload(id); + if (data == null) { + data = "该条推送记录未记录请求体(payload 为空),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("payload-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("推送日志请求体下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java b/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java new file mode 100644 index 0000000..82fbeed --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysGroupController.java @@ -0,0 +1,102 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupQuery; +import com.yunhai.userbase.model.sysgroup.SysGroupRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupSortRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupVo; +import com.yunhai.userbase.service.ISysGroupService; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组管理(组织架构树) + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sysgroup/") +public class SysGroupController { + + @Autowired + private ISysGroupService sysGroupService; + + @OperationLog("新增组") + @Operation(operationId = "sysGroupAdd") + @PostMapping("Add") + public void add(@RequestBody @Validated(ValidationGroups.Create.class) SysGroupRequest request) { + sysGroupService.add(request); + } + + @OperationLog("编辑组") + @Operation(operationId = "sysGroupEdit") + @PostMapping("Edit/{id}") + public void edit(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) SysGroupRequest request) { + sysGroupService.edit(id, request); + } + + @OperationLog("删除组") + @Operation(operationId = "sysGroupRemove") + @PostMapping("Delete/{id}") + public void delete(@PathVariable Integer id) { + sysGroupService.delete(id); + } + + @Operation(operationId = "sysGroupList") + @GetMapping("/List") + public List list(SysGroupQuery query) { + return sysGroupService.list(query); + } + + /** + * 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 + */ + @GetMapping("GetTree") + public List getTree() { + return sysGroupService.getTree(); + } + + /** + * 获取「学校组」剪枝树:末端叶子一定是学校组;学校组下的组不显示; + * 子树中无学校组的分支整体剪掉;根节点始终保留。 + */ + @GetMapping("GetSchoolTree") + public List getSchoolTree() { + return sysGroupService.getSchoolTree(); + } + + /** + * 批量新增子组:请求体 { pid, type, items:[{name},...] }(JSON)。 + */ + @OperationLog("批量新增子组") + @PostMapping("BatchAdd") + public void batchAdd(@RequestBody @Validated SysGroupBatchRequest request) { + sysGroupService.batchAdd(request); + } + + /** + * 子组排序:传入 pid 与有序 ids(必须是 pid 的直接子节点),按传入顺序落库。 + */ + @OperationLog("子组排序") + @PostMapping("SortChildren") + public void sortChildren(@Validated SysGroupSortRequest request) { + sysGroupService.sortChildren(request); + } + + /** + * 移动组:将 moveId 移动到 pid 下(不支持移动到自身或自身后代)。 + */ + @OperationLog("移动组") + @PostMapping("Move") + public void move(@Validated SysGroupMoveRequest request) { + sysGroupService.move(request); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java b/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java new file mode 100644 index 0000000..46ffd0f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysOapiLogController.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.oapilog.SysOapiLogListVo; +import com.yunhai.userbase.model.oapilog.SysOapiLogQuery; +import com.yunhai.userbase.service.ISysOapiLogService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 开放接口日志查询与下载接口。 + * 本控制器位于 {@code controller.api} 包,不会被 OapiLogAspect(切 oapi 包)拦截。 + * + * @author chenx + * @date 2026/8/20 + */ +@Slf4j +@RestController +@RequestMapping(value = Config.API_PATH + "/oapilog/") +public class SysOapiLogController { + + @Autowired + private ISysOapiLogService sysOapiLogService; + + /** + * 分页查询开放接口日志列表(不含响应结果)。 + */ + @Operation(summary = "开放接口日志分页查询", operationId = "oapiLogList") + @GetMapping("/List") + public BasePageResult list(SysOapiLogQuery query, BasePageQuery pageQuery) { + return sysOapiLogService.pageQuery(query, pageQuery); + } + + /** + * 下载某条日志的响应结果(GZIP 解压后原文,文本文件)。 + * 与 {@code SysOperationLogController.Download} 一致的 octet-stream + attachment 写法。 + * 若该日志未记录响应数据,返回一段说明文本文件而非 404,便于排查。 + */ + @Operation(summary = "下载开放接口日志响应原文", operationId = "oapiLogDownload") + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = sysOapiLogService.getResponseData(id); + if (data == null) { + data = "该条日志未记录响应数据(saveResponse=false、或接口响应为空、或响应序列化失败),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("response-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("开放接口日志响应结果下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java b/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java new file mode 100644 index 0000000..e980c23 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysOperationLogController.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.oplog.SysOperationLogListVo; +import com.yunhai.userbase.model.oplog.SysOperationLogQuery; +import com.yunhai.userbase.service.ISysOperationLogService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 操作日志查询与下载接口。 + * 本控制器自身不标注 @OperationLog,避免记录日志查看行为。 + * + * @author chenx + * @date 2026/7/28 + */ +@Slf4j +@RestController +@RequestMapping(value = Config.API_PATH + "/oplog/") +public class SysOperationLogController { + + @Autowired + private ISysOperationLogService sysOperationLogService; + + /** + * 分页查询操作日志列表(不含响应结果)。 + */ + @Operation(summary = "操作日志分页查询", operationId = "operationLogList") + @GetMapping("/List") + public BasePageResult list(SysOperationLogQuery query, BasePageQuery pageQuery) { + return sysOperationLogService.pageQuery(query, pageQuery); + } + + /** + * 下载某条日志的响应结果(GZIP 解压后原文, 文本文件)。 + * 采用与项目 DownloadFileUtil 一致的 octet-stream + attachment 写法,确保浏览器稳定触发下载。 + * 若该日志未记录响应数据,返回一段说明文本文件而非 404,便于排查。 + */ + @Operation(summary = "下载日志响应原文", operationId = "operationLogDownload") + @GetMapping("/Download/{id}") + public ResponseEntity download(@PathVariable Long id) { + String data = sysOperationLogService.getResponseData(id); + if (data == null) { + data = "该条日志未记录响应数据(saveResponse=false、或接口响应为空、或响应序列化失败),无法下载原始内容。"; + } + try { + ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + new String(("response-" + id + ".txt").getBytes("gbk"), "iso8859-1")); + headers.add("Pragma", "no-cache"); + headers.add("Expires", "0"); + return ResponseEntity.ok().headers(headers) + .contentType(MediaType.parseMediaType("application/octet-stream")) + .body(new InputStreamResource(bis)); + } catch (Exception e) { + log.error("操作日志响应结果下载失败, id={}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java b/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java new file mode 100644 index 0000000..84c23bc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/api/SysUserController.java @@ -0,0 +1,147 @@ +package com.yunhai.userbase.controller.api; + +import com.yunhai.platform.utils.exception.DException; +import com.yunhai.platform.utils.exception.ErrorCode; +import com.yunhai.userbase.common.annotation.OperationLog; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.BasePageQuery; +import com.yunhai.userbase.model.BasePageResult; +import com.yunhai.userbase.model.sysuser.BatchChangeStatusRequest; +import com.yunhai.userbase.model.sysuser.BatchDeleteRequest; +import com.yunhai.userbase.model.sysuser.BatchImportResult; +import com.yunhai.userbase.model.sysuser.ChangeGroupsRequest; +import com.yunhai.userbase.model.sysuser.ChangeSortNoRequest; +import com.yunhai.userbase.model.sysuser.SysUserQuery; +import com.yunhai.userbase.model.sysuser.SysUserRequest; +import com.yunhai.userbase.model.sysuser.SysUserVo; +import com.yunhai.userbase.service.ISysUserService; +import com.yunhai.userbase.utils.PageUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 组用户管理 + */ +@RestController +@RequestMapping(value = Config.API_PATH + "/sysuser/") +public class SysUserController { + + @Autowired + private ISysUserService sysUserService; + + @OperationLog("新增用户") + @PostMapping("Add") + public Integer add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) { + List data=new ArrayList<>(); + + return sysUserService.add(request); + } + + @OperationLog("编辑用户") + @PostMapping("Edit/{id}") + public void edit(@PathVariable Integer id, @RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) { + sysUserService.edit(id, request); + } + + /** + * 将用户从指定组中移除;若该用户无其它组关联则级联删除用户。 + */ + @OperationLog("从组中删除用户") + @PostMapping("DeleteFromGroup/{userId}/{groupId}") + public void deleteFromGroup(@PathVariable Integer userId, @PathVariable Integer groupId) { + sysUserService.deleteFromGroup(userId, groupId); + } + + /** + * 批量从指定组中移除用户;无其它组关联者级联删除用户。 + */ + @OperationLog("批量从组中删除用户") + @PostMapping("BatchDelete") + public void batchDelete(@RequestBody @Validated BatchDeleteRequest request) { + sysUserService.batchDeleteFromGroup(request.getUserIds(), request.getGroupId()); + } + + /** 禁用用户(disabled=true) */ + @OperationLog("禁用用户") + @PostMapping("Disable/{id}") + public void disable(@PathVariable Integer id) { + sysUserService.changeStatus(id, true); + } + + /** 启用用户(disabled=false) */ + @OperationLog("启用用户") + @PostMapping("Enable/{id}") + public void enable(@PathVariable Integer id) { + sysUserService.changeStatus(id, false); + } + + /** 批量设置用户状态 */ + @OperationLog("批量设置用户状态") + @PostMapping("BatchChangeStatus") + public void batchChangeStatus(@RequestBody @Validated BatchChangeStatusRequest request) { + sysUserService.batchChangeStatus(request.getUserIds(), request.getDisabled()); + } + + @GetMapping("/List") + public BasePageResult list(SysUserQuery query, BasePageQuery pageQuery) { + // 排序字段在关联表,无法走 Pageable.sort,统一由 Service.Specification 控制 + // (根节点按 id 升序,具体组按组内 sort_no 升序)。此处不传 Sort,避免覆盖 Spec 的 orderBy。 + return PageUtils.toResult(sysUserService.list(query, PageUtils.toPageable(pageQuery, null))); + } + + /** 调整用户在某组内的排序号(sort_no):1~999,可空表示清除 */ + @OperationLog("设置用户组内排序号") + @PostMapping("ChangeSortNo/{userId}") + public void changeSortNo(@PathVariable Integer userId, + @RequestBody @Validated ChangeSortNoRequest request) { + sysUserService.changeSortNo(userId, request.getGroupId(), request.getSortNo()); + } + + @OperationLog("调整组") + @PostMapping("ChangeGroups") + public void changeGroups(@RequestBody @Validated ChangeGroupsRequest request) { + sysUserService.changeGroups(request.getUserIds(), request.getGroupIds()); + } + + @GetMapping("Groups/{userId}") + public List getUserGroups(@PathVariable Integer userId) { + return sysUserService.findGroupIdsByUserId(userId); + } + + @OperationLog("批量导入用户") + @PostMapping("BatchImport") + public BatchImportResult batchImport(@RequestParam("file") MultipartFile file) { + return sysUserService.batchImport(file); + } + + /** + * 下载用户批量导入模板(真实 .xlsx):平铺当前组及子孙组,仅填充组名(路径)与组id;其余列留空供填写。 + */ + @GetMapping("DownloadTemplate") + public void downloadTemplate(@RequestParam(value = "groupId", required = false) Integer groupId, + HttpServletResponse response) throws IOException { + sysUserService.downloadImportTemplate(groupId, response); + } + + /** + * 导出组下所有用户(含子组)为 Excel;与导入模板同构,末尾追加用户id列。 + * 前端直接跳转此地址触发浏览器下载。 + */ + @GetMapping("Export") + public void export(@RequestParam(value = "groupId", required = false) Integer groupId, + @RequestParam(value = "withSub", defaultValue = "1") Integer withSub, + @RequestParam(value = "name", required = false) String name, + HttpServletResponse response) throws IOException { + sysUserService.exportUsers(groupId, withSub, name, response); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java new file mode 100644 index 0000000..efc3ebb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.enums.IEnum; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.service.IAppCategoryService; +import com.yunhai.userbase.service.IApplicationInfoService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +/** + * 应用域开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/app/...})。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求 + * (URL pattern {@code /oapi/*} 已覆盖本控制器),未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "app/") +public class AppController { + + @Autowired + private IAppCategoryService appCategoryService; + + @Autowired + private IApplicationInfoService applicationInfoService; + + /** + * 按主键 ID 查询应用信息(带缓存)。 + * 与客户端契约 {@code GET /oapi/app/{appId}} 对应;查不到返回 null(HTTP 200 + data:null 信封)。 + * 返回 {@link ApplicationInfoVo} 并清空 appSecret,与 list 接口一致不透出签名密钥。 + * + * @param appId 应用主键 ID(必填) + * @return 应用信息视图;不存在时返回 null + */ + @GetMapping("{appId}") + public ApplicationInfoVo byId(@PathVariable Integer appId) { + ApplicationInfo info = applicationInfoService.getById(appId); + if (info == null) { + return null; + } + ApplicationInfoVo vo = new ApplicationInfoVo(); + BeanUtils.copyProperties(info, vo); + // 不透出签名密钥(与 list 契约一致) + vo.setAppSecret(null); + return vo; + } + + /** + * 查询用户在指定客户端类型下有权访问的应用列表(按应用类型分组)。 + * 权限过滤复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}(组开通 ∪ 启用角色模块所属应用), + * 超级管理员返回全部;不透出 appSecret。 + * clientType 传枚举 key(2 的幂值,如 1=PC 2=移动App 4=系统模块…);非法/缺失按不限制端处理。 + * + * @param userId 用户ID(必填) + * @param clientType 客户端类型 key(可选) + * @return 应用类型分组列表;无权限时各类型 apps 为空数组 + */ + @GetMapping("list") + public List list(@RequestParam("userId") Integer userId, + @RequestParam(value = "clientType", required = false) Integer clientType) { + ClientEnum clientEnum = (clientType == null) ? null + : IEnum.getByKey(ClientEnum.class, clientType); + return appCategoryService.listBoaAppType(userId, clientEnum); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java new file mode 100644 index 0000000..62108c9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java @@ -0,0 +1,116 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.utils.model.BoaGroup; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest; +import com.yunhai.userbase.model.sysgroup.SysGroupRequest; +import com.yunhai.userbase.service.ISysGroupService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.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; + +/** + * 组织架构开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/group/...})。 + * + * 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient} + * 硬编码的契约严格一致: + * {@code GET /oapi/group/tree}、{@code /byId?groupId=&withSub=}、{@code /list?ids=&withSub=}。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求, + * 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "group/") +public class GroupController { + + @Autowired + private ISysGroupService sysGroupService; + + /** + * 获取整个平台的组树(从根节点开始,children 递归嵌套全部后代)。 + */ + @GetMapping("tree") + public BoaGroup tree() { + return sysGroupService.getGroups(); + } + + /** + * 获取单个组;withSub=true 时递归挂上全部子孙(整棵子树)。 + * + * @param groupId 组ID(必填) + * @param withSub 是否展开子节点,缺省 false + * @return 组节点;不存在返回 null + */ + @GetMapping("byId") + public BoaGroup byId(@RequestParam("groupId") Integer groupId, + @RequestParam(value = "withSub", required = false) Boolean withSub) { + return sysGroupService.getGroupById(groupId, withSub); + } + + /** + * 批量获取组;ids 逗号/空格分隔,按入参顺序返回,不存在的跳过。 + * + * @param ids 组ID列表(逗号/空格分隔) + * @param withSub 是否展开子节点,缺省 false + * @return 组节点列表(按入参顺序;不存在的跳过) + */ + @GetMapping("list") + public List list(@RequestParam("ids") String ids, + @RequestParam(value = "withSub", required = false) Boolean withSub) { + return sysGroupService.getGroupList(ids, withSub); + } + + /** + * 新增组:返回创建后的组节点(含 id)。 + * 层级类型约束、根节点保护等复用后台逻辑;鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @PostMapping("add") + public BoaGroup add(@RequestBody @Validated(ValidationGroups.Create.class) SysGroupRequest request) { + return sysGroupService.add(request); + } + + /** + * 编辑组(不含移动,移动请走 group/move 后台接口):返回更新后的组节点(含 id)。 + */ + @PostMapping("edit/{id}") + public BoaGroup edit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysGroupRequest request) { + return sysGroupService.edit(id, request); + } + + /** + * 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。 + * 新增 {@code schoolId} 边界:服务端强制 id 属于该校子树且不等于 schoolId,避免删学校根组/跨校删组。 + */ + @PostMapping("delete/{id}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer schoolId) { + sysGroupService.delete(id, schoolId); + } + + /** + * 批量新增子组:请求体 { pid, type, items:[{name},...] },返回创建后的组节点列表(均含 id)。 + */ + @PostMapping("batchAdd") + public List batchAdd(@RequestBody @Validated SysGroupBatchRequest request) { + return sysGroupService.batchAdd(request); + } + + /** + * 移动组(学校限定):仅允许在同一学校(组)子树内移动——moveId、pid 都必须落在 schoolId 子树内。 + * 语义复用后台 Move(环检测/类型约束/重排),额外叠加学校边界。鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @PostMapping("move/{schoolId}") + public void move(@PathVariable Integer schoolId, + @RequestBody @Validated SysGroupMoveRequest request) { + sysGroupService.move(request, schoolId); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java b/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java new file mode 100644 index 0000000..b435789 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/SignKeyInternalController.java @@ -0,0 +1,70 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.appauth.AppAuthProperties; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 验签兜底端点:供其他业务服务在「共享 Redis 宕机且本地缓存也未命中」时直连获取某应用的密钥。 + * 直接从 MySQL 读(绕过可能同宕的共享 Redis),是密钥的权威源;写入侧仍由 + * {@code SignKeyCacheService} 同步到 Redis。返回形状与 BasePlatform 读侧解析契约一致: + * {@code {"appKey":"100616","secret":"...","enabled":true}}。 + * 解析优先级: + * 1) 本库 application_info(覆盖全部「已颁发」应用的实时密钥); + * 2) 配置式受信应用 {@code yh.app-auth.trusted-apps}(平台自身默认密钥,如 应用中心 100616, + * 即便尚未落库也可解析,避免「默认 appKey 未建行」时此处返回 404 导致验签链路失败); + * 两者皆无 → 404。 + * 注意:此端点返回的是应用密钥,必须仅内网可达(由部署网络隔离保障),切勿暴露到公网。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "internal") +public class SignKeyInternalController { + + private final ApplicationInfoRepository applicationInfoRepository; + private final AppAuthProperties appAuthProperties; + + public SignKeyInternalController(ApplicationInfoRepository applicationInfoRepository, + AppAuthProperties appAuthProperties) { + this.applicationInfoRepository = applicationInfoRepository; + this.appAuthProperties = appAuthProperties; + } + + @GetMapping("sign-key/{appId}") + public ResponseEntity> signKey(@PathVariable String appId) { + Integer key; + try { + key = Integer.parseInt(appId.trim()); + } catch (NumberFormatException e) { + return ResponseEntity.notFound().build(); + } + ApplicationInfo info = applicationInfoRepository.findByAppKey(key); + if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) { + return ok(info.getAppKey() == null ? appId : String.valueOf(info.getAppKey()), + info.getAppSecret()); + } + // 库内无此应用密钥:回退到配置式受信应用(平台自身默认密钥 100616 等), + // 避免「应用中心默认 appKey 未落库」时兜底查询返回 404。 + String cfgSecret = appAuthProperties.getTrustedApps().get(appId); + if (cfgSecret != null && !cfgSecret.trim().isEmpty()) { + return ok(appId, cfgSecret); + } + return ResponseEntity.notFound().build(); + } + + private static ResponseEntity> ok(String appKey, String secret) { + Map body = new LinkedHashMap<>(); + body.put("appKey", appKey); + body.put("secret", secret); + body.put("enabled", true); + return ResponseEntity.ok(body); + } +} diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java new file mode 100644 index 0000000..9623605 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java @@ -0,0 +1,233 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.platform.appauth.AppAuthFilter; +import com.yunhai.platform.utils.exception.DException; +import com.yunhai.platform.utils.model.BoaGroup; +import com.yunhai.platform.utils.model.BoaModule; +import com.yunhai.platform.utils.model.BoaRoleInfo; +import com.yunhai.platform.utils.model.BoaUser; +import com.yunhai.userbase.common.validation.ValidationGroups; +import com.yunhai.userbase.config.Config; +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem; +import com.yunhai.userbase.model.sysuser.SysUserMoveRequest; +import com.yunhai.userbase.model.sysuser.SysUserPasswordRequest; +import com.yunhai.userbase.model.sysuser.SysUserRequest; +import com.yunhai.userbase.repository.ApplicationInfoRepository; +import com.yunhai.userbase.service.IGroupAuthzService; +import com.yunhai.userbase.service.ISysUserService; +import com.yunhai.userbase.service.impl.UserCenterService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/user/...})。 + * + * 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient} + * 硬编码的契约严格一致: + * {@code GET /oapi/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、 + * {@code /oapi/user/modules?userId=&schId=}。 + * 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求, + * 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 + */ +@RestController +@RequestMapping(Config.OPEN_API_PATH + "user/") +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + + @Autowired + private UserCenterService userCenterService; + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private IGroupAuthzService groupAuthzService; + + @Autowired + private ApplicationInfoRepository applicationInfoRepository; + + @GetMapping("byId") + public BoaUser byId(@RequestParam("id") Integer id) { + return userCenterService.getUserInfoWithDeptById(id); + } + + @GetMapping("byLoginName") + public BoaUser byLoginName(@RequestParam("loginName") String loginName) { + if (loginName == null || loginName.trim().isEmpty()) { + return null; + } + return userCenterService.getUserInfoWithDeptByAccount(loginName); + } + + @GetMapping("byPhone") + public BoaUser byPhone(@RequestParam("phone") String phone) { + if (phone == null || phone.trim().isEmpty()) { + return null; + } + return userCenterService.getUserInfoWithDeptByPhone(phone); + } + + /** + * 查询用户在调用方应用下、指定学校(上下文)里的已授权模块列表。 + * + * 与管理端「权限一览」返回全应用聚合不同,本方法严格按调用方应用维度过滤: + * ① 通过 {@link AppAuthFilter#APP_AUTH_ATTR} 拿到验签时的 appKey(RequireAppSignFilter + * 已保证非空); + * ② 查 application_info 映射到具体 appId; + * ③ 调 Service 专用方法:所有 SQL/集合构建均限定在该 appId 范围内(数据库层面过滤)。 + * + * 边界:调用方 appKey 查不到对应应用 → 直接返回空列表(安全优先,禁止意外返回其他应用的模块)。 + * + * @param request 用于取 AppAuthFilter 设置的 appKey + * @param userId 用户ID(必填) + * @param schId 学校ID/组ID(可选;用于限定「学校上下文」:非空时仅取该学校子树内的真实归属组参与判定, + * 为空时取用户全部真实归属组,与权限一览一致) + * @return 该应用下用户已授权模块(扁平化;无权限时返回空列表) + */ + @GetMapping("modules") + public List modules(HttpServletRequest request, + @RequestParam("userId") Integer userId, + @RequestParam(value = "schId", required = false) Integer schId) { + if (userId == null) { + return Collections.emptyList(); + } + Integer appId = resolveCallerAppId(request); + if (appId == null) { + return Collections.emptyList(); + } + return groupAuthzService.findUserModulesForApp(appId, schId, userId); + } + + /** + * 从请求 attribute 中解析调用方 appKey,并映射为 application_info 的主键 id。 + * 映射失败返回 null(Controller 层统一返回空列表;异常只打 warn 日志,不给前端 500)。 + */ + private Integer resolveCallerAppId(HttpServletRequest request) { + Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR); + if (attr == null) { + log.warn("[oapi:user:modules] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}", + request.getRequestURI()); + return null; + } + String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr); + if (appKeyStr.trim().isEmpty()) { + log.warn("[oapi:user:modules] AppAuth attr is blank, uri={}", request.getRequestURI()); + return null; + } + Integer appKey; + try { + appKey = Integer.parseInt(appKeyStr.trim()); + } catch (NumberFormatException e) { + log.warn("[oapi:user:modules] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI()); + return null; + } + ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey); + if (app == null) { + log.warn("[oapi:user:modules] appKey={} not found in application_info, uri={}", appKey, + request.getRequestURI()); + return null; + } + return app.getId(); + } + + + /** + * 新增用户:走完整校验(账号唯一/手机号/身份证/密码强度),BCrypt 加密并写入组关联; + * 返回创建后的用户(含 id 与组织链)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。 + */ + @PostMapping("add") + public BoaUser add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) { + Integer id = sysUserService.add(request); + return userCenterService.getUserInfoWithDeptById(id); + } + + /** + * 编辑用户:密码为空时不修改密码;返回更新后的用户(含 id 与组织链)。 + */ + @PostMapping("edit/{id}") + public BoaUser edit(@PathVariable Integer id, + @RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) { + sysUserService.edit(id, request); + return userCenterService.getUserInfoWithDeptById(id); + } + + /** + * 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联; + * 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。 + * 这样多校用户只会被退出指定学校,而不会被整体误删。 + * 新增 {@code schoolId} 边界:服务端强制 groupId 属于该校子树且不等于 schoolId,避免跨校/整校移除。 + */ + @PostMapping("delete/{id}/{groupId}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer groupId, @PathVariable Integer schoolId) { + sysUserService.deleteFromGroup(id, groupId, schoolId); + } + + /** + * 批量删除用户-组关系:请求体为 {@code SysUserGroupDeleteItem} 数组,每条携带自身 (userId, groupId, schoolId); + * 服务端逐条校验学校边界后解绑,语义与单条 delete 一致。 + */ + @PostMapping("deleteUsers") + public void deleteUsers(@RequestBody List items) { + sysUserService.deleteUsers(items); + } + + /** + * 批量新增用户:逐条走完整校验(含密码强度,每条需携带 password + confirmPassword); + * 返回创建后的用户列表(顺序与入参一致,均含 id 与组织链)。 + */ + @PostMapping("batchAdd") + public List batchAdd(@RequestBody List requests) { + List ids = sysUserService.batchAdd(requests); + List result = new ArrayList<>(); + for (Integer id : ids) { + result.add(userCenterService.getUserInfoWithDeptById(id)); + } + return result; + } + + /** + * 移动用户到指定组:仅允许在同一学校(组)范围内移动——用户当前必须已在该学校子树内, + * 且目标组也必须在该学校子树内,否则拒绝(禁止跨校移动)。 + * 语义同后台「调整组」——覆盖式把用户组归属替换为目标组(目标设为主组)。返回移动后的用户(含新的组织链)。 + */ + @PostMapping("moveToGroup") + public BoaUser moveToGroup(@RequestBody @Validated SysUserMoveRequest request) { + sysUserService.moveUserWithinSchool(request.getUserId(), request.getGroupId(), request.getSchoolId()); + return userCenterService.getUserInfoWithDeptById(request.getUserId()); + } + + /** + * 查询用户的业务角色身份列表(角色 + 主归属学校组上下文)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。 + */ + @GetMapping("roles") + public List roles(@RequestParam("userId") Integer userId) { + return sysUserService.findUserRoles(userId); + } + + /** + * 修改用户密码:先校验旧密码,再写入新密码(BCrypt)。 + * 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。 + */ + @PostMapping("password") + public void password(@RequestBody @Validated SysUserPasswordRequest request) { + sysUserService.changePassword(request.getUserId(), request.getOldPassword(), request.getNewPassword()); + } +} diff --git a/src/main/java/com/yunhai/userbase/entity/AppCategory.java b/src/main/java/com/yunhai/userbase/entity/AppCategory.java new file mode 100644 index 0000000..15dccc3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/AppCategory.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/15 15:53 + * @description + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "app_category") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class AppCategory extends BaseEntity { + //类别名称 + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "sort_no") + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/entity/AppModule.java b/src/main/java/com/yunhai/userbase/entity/AppModule.java new file mode 100644 index 0000000..6ae9897 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/AppModule.java @@ -0,0 +1,81 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块(树形,逻辑外键,最深5层) + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "app_module", + indexes = { + @Index(name = "idx_app_module_app", columnList = "app_id"), + @Index(name = "idx_app_module_parent", columnList = "parent_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_module_custom", columnNames = {"app_id", "custom_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class AppModule extends BaseEntity { + + /** + * 所属应用ID(逻辑外键 → ApplicationInfo.id),创建后不可改。 + */ + @NotNull + @Column(name = "app_id", nullable = false, updatable = false) + private Integer appId; + + /** + * 父模块ID(逻辑外键 → AppModule.id);根模块为 null。 + */ + @Column(name = "parent_id") + private Integer parentId; + + /** + * 模块名称。 + */ + @NotBlank + @Column(name = "name", nullable = false) + private String name; + + /** + * 自定义ID(应用内唯一,业务编码,如 "user-mgmt")。 + */ + @NotBlank + @Column(name = "custom_id", nullable = false, length = 64) + private String customId; + + /** + * 模块路径(前端路由/资源路径,如 "/system/user")。 + */ + @Column(name = "path", length = 255) + private String path; + + /** + * 层级:根=1,子=父+1,上限5(由 Service 校验)。 + */ + @NotNull + @Column(name = "level", nullable = false) + private Integer level; + + /** + * 排序号(沿用 AppCategory 约定)。 + */ + @Column(name = "sort_no") + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java new file mode 100644 index 0000000..6d17c06 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/ApplicationInfo.java @@ -0,0 +1,146 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.ListIntegerJsonConverter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 15:53 + * @description + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "application_info") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class ApplicationInfo extends BaseEntity { + + /** + * 应用名称(页面*必填) + */ + private String name; + + /** + * 应用类型ID + */ + private Integer categoryId; + + /** + * 排序号,仅在同一个应用类型内生效;默认为 null(新增时不赋值)。 + */ + @Column(name = "sort_no") + private Integer sortNo; + + /** + * 应用描述 + */ + @Column(name = "app_desc", length = 500) + private String desc; + + /** + * appKey 系统生成唯一标识,整型,从100000起自增。 + */ + @NotNull + @Column(name = "app_key", unique = true, nullable = false) + private Integer appKey; + + /** + * appSecret 密钥,支持重设 + */ + @NotBlank + @Column(name = "app_secret", nullable = false, length = 128) + private String appSecret; + + //应用前端类型 + @Convert(converter = ListIntegerJsonConverter.class) +// @Column(name = "clients", columnDefinition = "json") + @Column(name = "clients", columnDefinition = "TEXT") + private List clients; + + /** + * PC应用访问地址 + */ + @Column(name = "pc_url") + private String pcUrl; + + /** + * PC应用图标 图片URL + */ + @Column(name = "pc_icon") + private String pcIcon; + + /** + * App访问地址(移动端主地址) + */ + @Column(name = "app_url") + private String appUrl; + + /** + * App应用图标 图片URL + */ + @Column(name = "app_icon") + private String appIcon; + + /** + * 公众号应用地址 + */ + @Column(name = "official_account_url") + private String officialAccountUrl; + + /** + * 钉钉应用地址 + */ + @Column(name = "dingtalk_url") + private String dingtalkUrl; + + // ===================== 推送订阅配置(文档《弘育基础平台数据同步接口 2》) ===================== + + /** + * 推送外部接口地址(不含固定后缀),如 http://host/notice/api/base/ 。 + * 完整调用地址 = pushBaseUrl + 接口路径(如 group/AddGroup)。 + */ + @Column(name = "push_base_url") + private String pushBaseUrl; + + /** + * 推送总开关:0关 1开(默认 0,自动推送默认不触发)。 + */ + @Column(name = "push_enabled", nullable = false) + private Integer pushEnabled = 0; + + /** + * 启用的接口路径,逗号分隔(如 group/AddGroup,user/AddUser);空=不推送任何接口。 + */ + @Column(name = "push_event_types") + private String pushEventTypes; + + /** + * 关联学校组 id,逗号分隔;空=全局(监听所有学校变化)。 + */ + @Column(name = "push_school_ids") + private String pushSchoolIds; + + /** + * 推送角色 key,逗号分隔;空=全部角色。 + */ + @Column(name = "push_role_ids") + private String pushRoleIds; + + /** + * 推送签名密钥;空则复用 appSecret。 + */ + @Column(name = "push_secret") + private String pushSecret; + +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/entity/BaseEntity.java b/src/main/java/com/yunhai/userbase/entity/BaseEntity.java new file mode 100644 index 0000000..4ac3dcf --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/BaseEntity.java @@ -0,0 +1,63 @@ +package com.yunhai.userbase.entity; + + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +import com.yunhai.userbase.common.listener.UserAuditListener; +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/14 10:46 + * @description + */ + +@MappedSuperclass +@Getter +@Setter +@EntityListeners(UserAuditListener.class) +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = ValidationGroups.Update.class) + private Integer id; + + /** + * 创建人ID + */ + @Column(name = "create_user", nullable = false, updatable = false) + private Integer createUser; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新人ID + */ + @Column(name = "update_user") + private Integer updateUser; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(name = "update_time") + private LocalDateTime updateTime; + + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysAppRole.java b/src/main/java/com/yunhai/userbase/entity/SysAppRole.java new file mode 100644 index 0000000..c8be052 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysAppRole.java @@ -0,0 +1,68 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色。 + * 普通角色:allAppFlag=false 且 appId 必填; + * 全应用角色:allAppFlag=true 且 appId 为空; + * 注意:这里的“业务角色”不等同于 {@code sys_user.roles} 对应的系统身份枚举。 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_app_role", indexes = { + @Index(name = "idx_app_role_app", columnList = "app_id"), + @Index(name = "idx_app_role_all_app", columnList = "all_app_flag"), + @Index(name = "idx_app_role_name", columnList = "name") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysAppRole extends BaseEntity { + + /** + * 角色名称。 + */ + @NotBlank + @Column(name = "name", nullable = false, length = 100) + private String name; + + /** + * 所属应用ID(逻辑外键 → ApplicationInfo.id)。 + * 普通角色必填;全应用角色为空。 + */ + @Column(name = "app_id") + private Integer appId; + + /** + * 是否全应用角色。 + * true=可跨应用勾选模块;false=仅能授权所属应用下的模块。 + */ + @NotNull + @Column(name = "all_app_flag", nullable = false) + private Boolean allAppFlag = Boolean.FALSE; + + /** + * 角色备注。 + */ + @Column(name = "remark", length = 500) + private String remark; + + /** + * 是否禁用。 + * 禁用后角色关系可保留,但权限计算时应忽略该角色。 + */ + @NotNull + @Column(name = "disabled", nullable = false) + private Boolean disabled = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java b/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java new file mode 100644 index 0000000..fda1c4b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysAppRoleModuleRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 业务角色-模块权限关系表(逻辑外键,无物理 FK)。 + * 存在记录即代表该角色显式拥有该模块。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_app_role_module_rel", + indexes = { + @Index(name = "idx_app_role_module_role", columnList = "role_id"), + @Index(name = "idx_app_role_module_module", columnList = "module_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_role_module", columnNames = {"role_id", "module_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysAppRoleModuleRel extends BaseEntity { + + /** + * 角色ID(逻辑外键 → SysAppRole.id)。 + */ + @NotNull + @Column(name = "role_id", nullable = false) + private Integer roleId; + + /** + * 模块ID(逻辑外键 → AppModule.id)。 + */ + @NotNull + @Column(name = "module_id", nullable = false) + private Integer moduleId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroup.java b/src/main/java/com/yunhai/userbase/entity/SysGroup.java new file mode 100644 index 0000000..fcfb5b7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroup.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.GroupTypeEnumConverter; +import com.yunhai.userbase.enums.GroupTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/13 22:48 + * @description 系统节点(组)表 + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_group") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroup extends BaseEntity { + + private String name; + + @Column(name = "parent_id", nullable = false) + private Integer parentId; + + @Column(name = "type") + @Convert(converter = GroupTypeEnumConverter.class) + private GroupTypeEnum type; + + private Integer sort; + + + // 一对多反向关联中间表 + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "groupId", insertable = false, updatable = false) + private List userGroupRelList; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java b/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java new file mode 100644 index 0000000..3a99ffb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroupAppRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用开通表(逻辑外键,无物理 FK)。 + * 记录某个组显式开通了哪些应用;仅当本表存在记录时,「组授权」界面右侧才显示该应用。 + * 开通不继承:子组要使用应用必须自己开通。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_group_app_rel", + indexes = { + @Index(name = "idx_group_app_rel_app", columnList = "app_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_group_app", columnNames = {"group_id", "app_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroupAppRel extends BaseEntity { + + /** + * 组ID(逻辑外键 → SysGroup.id)。 + */ + @NotNull + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** + * 应用ID(逻辑外键 → ApplicationInfo.id)。 + */ + @NotNull + @Column(name = "app_id", nullable = false) + private Integer appId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java b/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java new file mode 100644 index 0000000..c59c22a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysGroupModulePerm.java @@ -0,0 +1,56 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组模块权限表(逻辑外键,无物理 FK)。 + * 存在记录即代表该组勾选了该模块;inheritFlag=true 时当前组及其所有子孙组自动继承该模块权限。 + * 取消勾选时物理删除记录。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_group_module_perm", + indexes = { + @Index(name = "idx_group_module_perm_module", columnList = "module_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_group_module", columnNames = {"group_id", "module_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysGroupModulePerm extends BaseEntity { + + /** + * 组ID(逻辑外键 → SysGroup.id)。 + */ + @NotNull + @Column(name = "group_id", nullable = false) + private Integer groupId; + + /** + * 模块ID(逻辑外键 → AppModule.id)。 + */ + @NotNull + @Column(name = "module_id", nullable = false) + private Integer moduleId; + + /** + * 是否子组继承(false=仅本组生效,true=子孙组自动继承),对应 UI「子组继承」勾选框。 + */ + @NotNull + @Column(name = "inherit_flag", nullable = false) + private Boolean inheritFlag = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java b/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java new file mode 100644 index 0000000..5ab18d6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysOapiLog.java @@ -0,0 +1,148 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +/** + * 开放接口调用日志实体。 + * 记录 yh-app-auth 签名调用方(YschBaseOpenApi 门面背后的 /oapi/** 端点)的操作, + * 身份主体为「调用方应用」(callerAppKey/callerAppName),区别于管理端 sys_operation_log 的登录用户维度。 + * 请求参数与响应均已脱敏后入库(敏感字段见 SensitiveDataMasker)。 + * + * @author chenx + * @date 2026/8/20 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_oapi_log", indexes = { + @Index(name = "idx_create_time", columnList = "create_time"), + @Index(name = "idx_appkey_time", columnList = "caller_app_key, create_time"), + @Index(name = "idx_uri_time", columnList = "request_uri, create_time"), + @Index(name = "idx_level_time", columnList = "log_level, create_time"), + @Index(name = "idx_ip_time", columnList = "request_ip, create_time") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysOapiLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询) + */ + @Column(name = "log_level", nullable = false, columnDefinition = "TINYINT") + private Integer logLevel; + + /** + * 调用方应用 appKey(验签后写入 request attribute 的 AppAuthFilter.APP_AUTH_ATTR) + */ + @Column(name = "caller_app_key", nullable = false) + private Integer callerAppKey; + + /** + * 调用方应用名(application_info 解析;解析失败记 appKey 原值) + */ + @Column(name = "caller_app_name", length = 128) + private String callerAppName; + + /** + * 请求IP + */ + @Column(name = "request_ip", nullable = false, length = 64) + private String requestIp; + + /** + * 请求路径 + */ + @Column(name = "request_uri", nullable = false, length = 512) + private String requestUri; + + /** + * 请求方法 GET/POST/PUT/DELETE + */ + @Column(name = "request_method", nullable = false, length = 16) + private String requestMethod; + + /** + * 操作类型 DELETE/CREATE/UPDATE/QUERY + */ + @Column(name = "operation_type", nullable = false, length = 32) + private String operationType; + + /** + * 类名.方法名 + */ + @Column(name = "class_method", nullable = false, length = 512) + private String classMethod; + + /** + * 操作描述(@OapiLog 注解 value 提供,可选) + */ + @Column(name = "operation_desc", length = 255) + private String operationDesc; + + /** + * 请求参数JSON(脱敏后) + */ + @Column(name = "request_params", columnDefinition = "TEXT") + private String requestParams; + + /** + * 响应结果(GZIP 压缩,脱敏后) + */ + @Column(name = "response_data", columnDefinition = "MEDIUMBLOB") + private byte[] responseData; + + /** + * HTTP状态码 + */ + @Column(name = "status_code", nullable = false) + private Integer statusCode; + + /** + * 耗时(ms) + */ + @Column(name = "cost_time", nullable = false) + private Long costTime; + + /** + * 调用方UA + */ + @Column(name = "user_agent", length = 512) + private String userAgent; + + /** + * 错误信息 + */ + @Column(name = "error_msg", columnDefinition = "TEXT") + private String errorMsg; + + /** + * 调用时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") + private LocalDateTime createTime; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java b/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java new file mode 100644 index 0000000..ade5499 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysOperationLog.java @@ -0,0 +1,140 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +/** + * 系统操作日志实体。 + * 记录用户的关键操作(增删改查),便于审计与问题排查。 + * + * @author chenx + * @date 2026/7/28 + */ +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_operation_log", indexes = { + @Index(name = "idx_create_time", columnList = "create_time"), + @Index(name = "idx_user_time", columnList = "user_id, create_time"), + @Index(name = "idx_uri_time", columnList = "request_uri, create_time"), + @Index(name = "idx_level_time", columnList = "log_level, create_time"), + @Index(name = "idx_ip_time", columnList = "request_ip, create_time") +}) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysOperationLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @EqualsAndHashCode.Include + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询) + */ + @Column(name = "log_level", nullable = false, columnDefinition = "TINYINT") + private Integer logLevel; + + /** + * 操作用户ID + */ + @Column(name = "user_id", nullable = false) + private Long userId; + + /** + * 操作用户名 + */ + @Column(name = "username", nullable = false, length = 64) + private String username; + + /** + * 请求IP + */ + @Column(name = "request_ip", nullable = false, length = 64) + private String requestIp; + + /** + * 请求路径 + */ + @Column(name = "request_uri", nullable = false, length = 512) + private String requestUri; + + /** + * 请求方法 GET/POST/PUT/DELETE + */ + @Column(name = "request_method", nullable = false, length = 16) + private String requestMethod; + + /** + * 操作类型 DELETE/CREATE/UPDATE/QUERY + */ + @Column(name = "operation_type", nullable = false, length = 32) + private String operationType; + + /** + * 类名.方法名 + */ + @Column(name = "class_method", nullable = false, length = 512) + private String classMethod; + + /** + * 请求参数JSON + */ + @Column(name = "request_params", columnDefinition = "TEXT") + private String requestParams; + + /** + * 响应结果(GZIP压缩) + */ + @Column(name = "response_data", columnDefinition = "MEDIUMBLOB") + private byte[] responseData; + + /** + * HTTP状态码 + */ + @Column(name = "status_code", nullable = false) + private Integer statusCode; + + /** + * 耗时(ms) + */ + @Column(name = "cost_time", nullable = false) + private Long costTime; + + /** + * 浏览器UA + */ + @Column(name = "user_agent", length = 512) + private String userAgent; + + /** + * 错误信息 + */ + @Column(name = "error_msg", columnDefinition = "TEXT") + private String errorMsg; + + /** + * 操作时间 + */ + @CreationTimestamp + @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") + private LocalDateTime createTime; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUser.java b/src/main/java/com/yunhai/userbase/entity/SysUser.java new file mode 100644 index 0000000..6a1c6ed --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUser.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yunhai.userbase.common.converter.ListIntegerJsonConverter; +import com.yunhai.userbase.common.converter.StatusEnumConverter; +import com.yunhai.userbase.common.converter.UserTypeEnumConverter; +import com.yunhai.userbase.enums.StatusEnum; +import com.yunhai.userbase.enums.UserTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDate; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/13 23:30 + * @description 系统用户表 + */ + +@NoArgsConstructor +@Data +@Entity +@Table(name = "sys_user") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUser extends BaseEntity { + + @Column(name = "username", nullable = false, unique = true) + private String account; + private String password; + //出生日期 + private LocalDate birthday; + + @JsonProperty("name") + private String realName; + + private String phone; + + private String idcard; + + private String email; + + private Integer gender; + + //头像 + private String profilePicture; + + /** + * 角色列表,存储为 MySQL JSON 类型,读取时自动映射为 List + * 取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key,例如 [1,2,3] + */ + @Convert(converter = ListIntegerJsonConverter.class) +// @Column(name = "roles", columnDefinition = "json") + @Column(name = "roles",columnDefinition = "TEXT") + private List roles; + + // 一对多:关联用户分组中间表,实现多对多业务 + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "userId", insertable = false, updatable = false) + private List userGroupRelList; + + // 是否禁用:存储为 StatusEnum(0=正常/未禁用,1=禁用),与 GroupTypeEnum 同机制;默认启用 + @Convert(converter = StatusEnumConverter.class) + private StatusEnum disabled; + //是否过期 + private Boolean expired=false; + + /** + * 用户类型:0=普通用户,1=超级管理员,2=学校管理员(见 UserTypeEnum)。 + * 由应用层根据用户账号/角色推导:内置超级管理员账号固定为 1,其余账号含 ADMIN(7) 角色则为 2,默认 0。 + */ + @Convert(converter = UserTypeEnumConverter.class) + private UserTypeEnum type; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java new file mode 100644 index 0000000..631a609 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserAppRoleRel.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/27 + * @description 用户-业务角色关系表(逻辑外键,无物理 FK)。 + * 一个用户可绑定多个业务角色;一个角色也可绑定多个用户。 + */ +@NoArgsConstructor +@Data +@Entity +@Table( + name = "sys_user_app_role_rel", + indexes = { + @Index(name = "idx_user_app_role_user", columnList = "user_id"), + @Index(name = "idx_user_app_role_role", columnList = "role_id") + }, + uniqueConstraints = { + @UniqueConstraint(name = "uk_user_app_role", columnNames = {"user_id", "role_id"}) + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserAppRoleRel extends BaseEntity { + + /** + * 用户ID(逻辑外键 → SysUser.id)。 + */ + @NotNull + @Column(name = "user_id", nullable = false) + private Integer userId; + + /** + * 角色ID(逻辑外键 → SysAppRole.id)。 + */ + @NotNull + @Column(name = "role_id", nullable = false) + private Integer roleId; +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java b/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java new file mode 100644 index 0000000..7935603 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserCurrentRole.java @@ -0,0 +1,60 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/15 15:05 + * @description 用户当前角色 + */ +@Data +@NoArgsConstructor +@Entity +@Table( + name = "sys_user_current_role", + uniqueConstraints = @UniqueConstraint(columnNames = {"userId"}), + indexes = {@Index(columnList = "userId")} +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserCurrentRole { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = ValidationGroups.Update.class) + private Integer id; + + /** + * 用户ID,一对一唯一 + */ + @Column(nullable = false) + private Integer userId; + + /** + * 用户切换选中的学校ID(根级group,数值即学校组 sys_group.id;区域/学段/根直属=0或null) + */ + private Integer schoolId; + /** + * 用户切换后生效角色ID(SysRoleEnum.key) + */ + private Integer roleId; + + /** + * 最近一次切换时间(服务端 now() 写入) + */ + @Column(name = "switch_time") + private LocalDateTime switchTime; + + /** + * 学校名冗余(避免每次 join sys_group 取 name) + */ + @Column(name = "school_name", length = 100) + private String schoolName; + +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java new file mode 100644 index 0000000..de68291 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserGroupRel.java @@ -0,0 +1,54 @@ +package com.yunhai.userbase.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/7/15 15:03 + * @description 组用户多对多关联表 + */ + +@Data +@NoArgsConstructor +@Entity +@Table(name = "sys_user_group_rel") +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserGroupRel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = Update.class) + private Integer id; + /** 用户ID */ + private Integer userId; + /** 分组ID */ + private Integer groupId; + /** 是否主归属 1=本职主学校/部门 0=兼职兼任 */ + private Integer isMain; + + /** 组内排序号:越小越靠前;null 表示未设置,列表按 user_id 兜底升序 */ + @Column(name = "sort_no") + private Integer sortNo; + + private LocalDateTime createTime; + + // 关联用户,仅查询,不维护外键 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", referencedColumnName = "id", insertable = false, updatable = false) + private SysUser user; + + // 关联分组,仅查询,不维护外键 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "groupId", referencedColumnName = "id", insertable = false, updatable = false) + private SysGroup group; + + + public @interface Update { + } +} diff --git a/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java b/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java new file mode 100644 index 0000000..07cb6a6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/entity/SysUserRoleRel.java @@ -0,0 +1,86 @@ +package com.yunhai.userbase.entity; + +import com.yunhai.userbase.common.converter.StatusEnumConverter; +import com.yunhai.userbase.enums.StatusEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户-SysRoleEnum角色-学校 三元关联(平台身份角色的权威源)。 + * 区别于 {@link SysUserAppRoleRel}(业务权限角色):本表表达"用户在哪个学校下激活哪个平台身份角色" + * (如"刘文娟在 A 学校是老师"),角色取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key。 + * 不继承 BaseEntity:高频读写关系表,自定义字段更轻量,与 {@link SysUserGroupRel} 风格一致。 + */ +@Data +@NoArgsConstructor +@Entity +@Table( + name = "sys_user_role_rel", + uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "roleId", "schoolId"}), + indexes = { + @Index(columnList = "userId"), + @Index(columnList = "schoolId"), + @Index(columnList = "roleId") + } +) +@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) +public class SysUserRoleRel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NotNull(groups = Update.class) + private Integer id; + + /** + * 用户ID + */ + @Column(nullable = false) + private Integer userId; + + /** + * 角色ID(SysRoleEnum.key,1-7) + */ + @Column(nullable = false) + private Integer roleId; + + /** + * 学校组ID(=type=SCHOOL_GROUP 的 sys_group.id);区域/学段/根直属用户填 0 + */ + @Column(nullable = false) + private Integer schoolId; + + /** + * 是否主身份标记:每 userId 最多 1 条 isMain=1(应用层校验,不靠 uk) + */ + @Column(nullable = false) + private Integer isMain = 0; + + /** + * 排序号:越小越靠前 + */ + @Column(name = "sort_no", nullable = false) + private Integer sortNo = 0; + + /** + * 状态:0=禁用 1=正常(StatusEnum) + */ + @Convert(converter = StatusEnumConverter.class) + @Column(nullable = false) + private StatusEnum status = StatusEnum.NORMAL; + + @Column(name = "create_time", nullable = false, updatable = false) + private LocalDateTime createTime; + + @Column(name = "update_time", nullable = false) + private LocalDateTime updateTime; + + public @interface Update { + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/BizType.java b/src/main/java/com/yunhai/userbase/enums/BizType.java new file mode 100644 index 0000000..625634c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/BizType.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/1/5 11:15 + * @description 虚拟节点类型 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum BizType { + + ROOT (1), + TEXTBOOK (2), + GRADE (3), + KNOWLEDGE (4), + REALLY (5), + ; + + private Integer key; + + public static BizType getByKey(Integer key){ + BizType[] enums = BizType.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/BookType.java b/src/main/java/com/yunhai/userbase/enums/BookType.java new file mode 100644 index 0000000..c4b8ad9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/BookType.java @@ -0,0 +1,50 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/1/16 9:32 + * @description 资源类型 + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum BookType { + /** 教材 */ + TextBook (1,"教材"), + /** 教辅 */ + TeachingTutoring (2,"教辅"), + /** 知识点 */ + KnowledgePoint (3,"知识点"), + ; + + private Integer key; + private String value; + + public static BookType getByKey(Integer key){ + BookType[] enums = BookType.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static String getValue(Integer key) { + for (BookType bookType : BookType.values()) { + if (bookType.getKey().equals(key)) { + return bookType.getValue(); + } + } + return null; + } + + +} diff --git a/src/main/java/com/yunhai/userbase/enums/GenderEnum.java b/src/main/java/com/yunhai/userbase/enums/GenderEnum.java new file mode 100644 index 0000000..f456f66 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/GenderEnum.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 性别 + */ +@Getter +@AllArgsConstructor +public enum GenderEnum implements IEnum { + + FEMALE(0, "女"), + MALE(1, "男"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java b/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java new file mode 100644 index 0000000..2c7fd11 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/GroupTypeEnum.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 分组类型 + */ +@Getter +@AllArgsConstructor +public enum GroupTypeEnum implements IEnum { + + NORMAL_GROUP(1, "普通组"), + SCHOOL_GROUP(2, "学校组"), + GRADE_GROUP(3, "年级组"), + CLASS_GROUP(4, "班级组"), + AREA_GROUP(5, "区域组"), + STAGE_GROUP(6, "学段组"), + TEACHER_GROUP(7, "教师组"), + STUDENT_GROUP(8, "学生组"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/IntegEnum.java b/src/main/java/com/yunhai/userbase/enums/IntegEnum.java new file mode 100644 index 0000000..bb13031 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/IntegEnum.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 集成方式 + */ +@Getter +@AllArgsConstructor +public enum IntegEnum implements IEnum { + + INNER(0, "内置"), + WEB(1, "Web"), + APP_JUMP(3, "App跳转"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/LogLevel.java b/src/main/java/com/yunhai/userbase/enums/LogLevel.java new file mode 100644 index 0000000..606d3e8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/LogLevel.java @@ -0,0 +1,41 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志级别。 + * 1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)。 + * + * @author chenx + * @date 2026/7/28 + */ +@Getter +@AllArgsConstructor +public enum LogLevel { + + IMPORTANT(1, "重要(删除)"), + SECONDARY(2, "次要(新增/编辑)"), + GENERAL(3, "一般(查询)"); + + private final int code; + private final String desc; + + /** + * 根据 code 反查级别。 + * + * @param code 级别编码 + * @return 对应级别,未知编码默认次要 + */ + public static LogLevel fromCode(Integer code) { + if (code == null) { + return SECONDARY; + } + for (LogLevel level : values()) { + if (level.code == code) { + return level; + } + } + return SECONDARY; + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java b/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java new file mode 100644 index 0000000..a0d2c4f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/ResourceSourceEnum.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/1/5 13:53 + * @description 资源来源 + */ +@AllArgsConstructor +@Getter +public enum ResourceSourceEnum { + ZIYUAN("ZIYUAN","资源库"), + LOCAL("LOCAL","平台资源库"), + ; + + private final String enKey; + private final String cnKey; + +} diff --git a/src/main/java/com/yunhai/userbase/enums/RootNode.java b/src/main/java/com/yunhai/userbase/enums/RootNode.java new file mode 100644 index 0000000..dac0df3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/RootNode.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2025/12/15 13:29 + * @description 根目录树类型 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum RootNode { + + ZIYUANKU (1,"ZIYUAN","资源"), + TUTORING (2,"TUTORING","辅导"), + QUESTION (3,"QUESTION","题库"), + SHIJUAN (4,"SHIJUAN","试卷"), + ZHUANXIANG (5,"ZHUANXIANG","专项"), + ; + + private Integer key; + private String enKey; + private String value; + + + public static RootNode getByKey(Integer key){ + RootNode[] enums = RootNode.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static RootNode fromKey(Integer key) { + for (RootNode n : values()) if (n.key.equals(key)) return n; + throw new IllegalArgumentException("非法 RootNode key: " + key); + } + + +} diff --git a/src/main/java/com/yunhai/userbase/enums/SchoolStage.java b/src/main/java/com/yunhai/userbase/enums/SchoolStage.java new file mode 100644 index 0000000..512d41b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SchoolStage.java @@ -0,0 +1,66 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2025/12/15 15:55 + * @description 学段 + */ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum SchoolStage { + + PRIMARY (2,"PRIMARY","小学"), + MIDDLE (3,"MIDDLE","初中"), + HIGH (4,"HIGH","高中"), + ; + + private Integer key; + private String enKey; + private String value; + + + public static SchoolStage getByKey(Integer key){ + SchoolStage[] enums = SchoolStage.values(); + Optional opt = Arrays.stream(enums).filter(item -> item.getKey().equals(key)).findAny(); + if(opt.isPresent()){ + return opt.get(); + }else { + return null; + } + } + + public static String getName(Integer key) { + for (SchoolStage stage : SchoolStage.values()) { + if (stage.getKey().equals(key)) { + return stage.getValue(); + } + } + return null; + } + + + public static SchoolStage getByValue(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return Arrays.stream(values()) + .filter(item -> item.getValue().equals(value)) + .findAny() + .orElse(null); + } + + + public static SchoolStage fromKey(Integer key) { + for (SchoolStage n : values()) if (n.key.equals(key)) return n; + throw new IllegalArgumentException("非法 SchoolStage key: " + key); + } +} diff --git a/src/main/java/com/yunhai/userbase/enums/StatusEnum.java b/src/main/java/com/yunhai/userbase/enums/StatusEnum.java new file mode 100644 index 0000000..fe5b7b3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/StatusEnum.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/13 + * @description 通用启用禁用状态 + */ +@Getter +@AllArgsConstructor +public enum StatusEnum implements IEnum { + + NORMAL(0, "正常"), + DISABLED(1, "禁用"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java b/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java new file mode 100644 index 0000000..fa19a8a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SysModeEnum.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/07/25 + * @description 操作模式枚举(应用授权等场景:追加/覆盖/删除) + */ +@Getter +@AllArgsConstructor +public enum SysModeEnum implements IEnum { + + APPEND(0, "追加"), + OVERWRITE(1, "覆盖"), + DELETE(2, "删除"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } + +} diff --git a/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java b/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java new file mode 100644 index 0000000..0bb3c67 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SysRoleEnum.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/07/13 + * @description 用户角色枚举 + */ +@Getter +@AllArgsConstructor +public enum SysRoleEnum implements IEnum{ + + TEACHER(1, "老师"), + SCHOOL_LEADER(2, "校领导"), + STUDENT(3, "学生"), + PARENT(4, "家长"), + BUREAU_LEADER(5, "局领导"), + WORK_ACCOUNT(6, "工作号"), + ADMIN(7, "管理员"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } + +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/enums/SystemGrade.java b/src/main/java/com/yunhai/userbase/enums/SystemGrade.java new file mode 100644 index 0000000..7bc5ac3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/SystemGrade.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * @ Description : java类作用描述 + * @Author : cls + * @ CreateDate : 2020/2/21 18:39 + * @ Company : bestsch + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +public enum SystemGrade { + + one(1,"一年级",2),two(2,"二年级",2), + three(3,"三年级",2),four(4,"四年级",2), + five(5,"五年级",2),six(6,"六年级",2), + seven(7,"七年级",3),eight(8,"八年级",3), + nine(9,"九年级",3),ten(10,"高一",4), + eleven(11,"高二",4),twelve(12,"高三",4); + + private Integer gradeId; + private String gradeName; + private Integer stage; + + public static String getName(Integer gradeId) { + for (SystemGrade grade : SystemGrade.values()) { + if (grade.getGradeId().equals(gradeId)) { + return grade.gradeName; + } + } + return null; + } + +} diff --git a/src/main/java/com/yunhai/userbase/enums/UserType.java b/src/main/java/com/yunhai/userbase/enums/UserType.java new file mode 100644 index 0000000..73c9e56 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/UserType.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +/** + * @program: UserProfile + * @description: 用户类别 + * @author: chenx + * @create: 2023-05-26 10:46 + **/ + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum UserType { + + TEACHER (1,"教师","Teacher"), + STUDENT (2,"学生","Student"), + ; + + private Integer key; + private String value; + private String code; + +} diff --git a/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java b/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java new file mode 100644 index 0000000..beb2653 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/enums/UserTypeEnum.java @@ -0,0 +1,36 @@ +package com.yunhai.userbase.enums; + +import com.yunhai.platform.utils.enums.IEnum; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author chenx + * @date 2026/08/03 + * @description 用户类型枚举 + * - NORMAL(0) 普通用户:默认类型 + * - SUPER_ADMIN(1) 超级管理员:系统内置种子账号(yunhaisch / yunhaisch2 / yunhaisch3),由初始化脚本写入 + * - SCHOOL_ADMIN(2) 学校管理员:拥有管理身份(角色含 ADMIN(7))的普通账号(非超级管理员) + */ +@Getter +@AllArgsConstructor +public enum UserTypeEnum implements IEnum { + + NORMAL(0, "普通用户"), + SUPER_ADMIN(1, "超级管理员"), + SCHOOL_ADMIN(2, "学校管理员"); + + private Integer key; + private String value; + + @Override + public Integer getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java b/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java new file mode 100644 index 0000000..ac0aabf --- /dev/null +++ b/src/main/java/com/yunhai/userbase/event/ApplicationInfoCacheChangedEvent.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.event; + +import com.yunhai.userbase.entity.ApplicationInfo; +import org.springframework.context.ApplicationEvent; + +/** + * 应用密钥/元数据变更事件:在 ApplicationInfoService 的 save/add/edit/delete/resetSecret 等事务内发布, + * 由 SignKeyCacheService 的 @TransactionalEventListener(phase = AFTER_COMMIT 订阅, + * 保证 DB 真正提交成功后再写共享 Redis / 清本地缓存,避免事务回滚导致的脏数据。 + */ +public class ApplicationInfoCacheChangedEvent extends ApplicationEvent { + + public enum Type { + UPSERT, DELETE + } + + private final Type type; + private final Integer appKey; + + /** + * 删除事件:appKey 必填;upsert 事件:可以传 entity 非空。 + */ + private final ApplicationInfo upsertEntity; + + public ApplicationInfoCacheChangedEvent(Object source, Type type, Integer appKey, ApplicationInfo upsertEntity) { + super(source); + this.type = type; + this.appKey = appKey; + this.upsertEntity = upsertEntity; + } + + public static ApplicationInfoCacheChangedEvent upsert(Object source, ApplicationInfo entity) { + return new ApplicationInfoCacheChangedEvent(source, Type.UPSERT, entity.getAppKey(), entity); + } + + public static ApplicationInfoCacheChangedEvent delete(Object source, Integer appKey) { + return new ApplicationInfoCacheChangedEvent(source, Type.DELETE, appKey, null); + } + + public Type getType() { return type; } + + public Integer getAppKey() { return appKey; } + + public ApplicationInfo getUpsertEntity() { return upsertEntity; } +} diff --git a/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java new file mode 100644 index 0000000..267a2aa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.event; + +import com.yunhai.userbase.model.push.PushInterfaceType; +import lombok.Data; + +import java.util.List; + +/** + * 数据同步事件:组 / 用户变更后由 {@code SysGroupService}/{@code SysUserService} 在事务内 publish, + * {@code PushDispatcher} 用 {@code @TransactionalEventListener(AFTER_COMMIT)} 消费(事务提交后才推送, + * 推送失败不影响主业务)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class DataSyncEvent { + + /** 接口类型,如 group/AddGroup */ + private PushInterfaceType type; + /** 组事件携带的受影响组 id 列表 */ + private List groupIds; + /** 用户事件携带的受影响用户 id 列表 */ + private List userIds; + /** 组维度的关联事件(如删除用户=解除组关系)携带的组 id */ + private Integer groupId; + /** 触发本次变更的 oapi 调用方 appKey;非 oapi 来源(如管理后台)为 null。 + * 推送分发器据此回推时排除来源应用自身,避免收到自己的回声。 */ + private Integer sourceAppKey; +} diff --git a/src/main/java/com/yunhai/userbase/model/BaseDict.java b/src/main/java/com/yunhai/userbase/model/BaseDict.java new file mode 100644 index 0000000..5873a5e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BaseDict.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model; + +/** + * @program: QualityEvaluate + * @description: 系统字典类 + * @author: chenx + * @create: 2023-05-15 17:17 + **/ +public class BaseDict { + public Integer k; + public String v; + + + public int getK() { + return this.k; + } + + public void setK(int k) { + this.k = k; + } + + public String getV() { + return this.v; + } + + public void setV(String v) { + this.v = v; + } + + public BaseDict(Integer k, String v) { + this.k = k; + this.v = v; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/BasePageQuery.java b/src/main/java/com/yunhai/userbase/model/BasePageQuery.java new file mode 100644 index 0000000..ca04263 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BasePageQuery.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.model; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/21 21:41 + * @description + */ +@Data +public class BasePageQuery { + private int pageIndex = 1; + private int pageSize = 10; + /** 通用模糊搜索关键词 */ + private String keyword; +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/model/BasePageResult.java b/src/main/java/com/yunhai/userbase/model/BasePageResult.java new file mode 100644 index 0000000..7e60ed1 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BasePageResult.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + * 由 Controller 层负责装配(见 {@link com.yunhai.userbase.utils.PageUtils#toResult}), + * Service 层仍返回 Spring Data 的 {@link org.springframework.data.domain.Page},保持与传输协议解耦。 + * + * @author chenx + * @date 2026/7/21 21:40 + * @description 统一分页响应外壳 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起,与 {@link BasePageQuery#getPageIndex()} 约定一致) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) { + this.content = content; + this.total = total; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.totalPages = totalPages; + this.hasNext = hasNext; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/BaseTree.java b/src/main/java/com/yunhai/userbase/model/BaseTree.java new file mode 100644 index 0000000..f8f93cc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/BaseTree.java @@ -0,0 +1,19 @@ +package com.yunhai.userbase.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class BaseTree { + public Integer id; + public String name; + public Integer pid ; + public Integer type; + public List children; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java b/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java new file mode 100644 index 0000000..edc2d24 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Excel/ExcelKnowledge.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.Excel; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author chenx + * @date 2026/1/19 19:45 + * @description 教材导入 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ExcelKnowledge { + + @Excel(name = "第一层", width = 20) + private String first; + + @Excel(name = "第二层", width = 20) + private String second; + + @Excel(name = "第三层", width =20) + private String third; + + @Excel(name = "第四层", width = 20) + private String fourth; + + @Excel(name = "第五层", width = 20) + private String five; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java b/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java new file mode 100644 index 0000000..68256ad --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Excel/ExcelResearchFill.java @@ -0,0 +1,28 @@ +package com.yunhai.userbase.model.Excel; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +/** + * @program: ResearchLearn + * @description: 导入研学活动填报实体类 + * @author: chenx + * @create: 2023-07-21 14:12 + **/ + +@Data +public class ExcelResearchFill { + + @Excel(name = "年级") + private String gradeName; + + @Excel(name = "班级") + private String className; + + @Excel(name = "姓名") + private String stuName; + + @Excel(name = "研学/活动名称") + private String researchName; + +} diff --git a/src/main/java/com/yunhai/userbase/model/Page.java b/src/main/java/com/yunhai/userbase/model/Page.java new file mode 100644 index 0000000..e58b366 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Page.java @@ -0,0 +1,72 @@ +package com.yunhai.userbase.model; + +import org.apache.commons.lang3.StringUtils; + +public class Page { + private int current; + private int pageSize; + private long count; + + public Page() { + } + + public int getCurrent() { + return this.current; + } + + public void setCurrent(int current) { + this.current = current; + } + + public int getPageSize() { + return this.pageSize == 0 ? 10 : this.pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public void setPageSize(String pageSize) { + if (!StringUtils.isEmpty(pageSize) && StringUtils.isNumeric(pageSize)) { + this.pageSize = Integer.valueOf(pageSize); + } + + } + + public long getCount() { + return this.count; + } + + public void setCount(long count) { + this.count = count; + } + + + + public int getPageCount() { + return this.getPageSize() != 0 ? (int)Math.ceil((double)this.count / (double)this.getPageSize()) : 0; + } + + public int getStartIndex() { + return (this.getCurrent() - 1) * this.getPageSize(); + } + + public void setCurrent(String current) { + if (!StringUtils.isEmpty(current) && StringUtils.isNumeric(current)) { + this.current = Integer.valueOf(current); + } + + } + + public void setCount(String count) { + if (!StringUtils.isEmpty(count) && StringUtils.isNumeric(count)) { + this.count = Long.valueOf(count); + } + + } + + public int getEndIndex() { + return this.getStartIndex() + this.getPageSize(); + } + +} diff --git a/src/main/java/com/yunhai/userbase/model/StartEndDate.java b/src/main/java/com/yunhai/userbase/model/StartEndDate.java new file mode 100644 index 0000000..94c1ea7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/StartEndDate.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.model; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StartEndDate { + private Date startDate; + private Date endDate; +} diff --git a/src/main/java/com/yunhai/userbase/model/Tree.java b/src/main/java/com/yunhai/userbase/model/Tree.java new file mode 100644 index 0000000..2523235 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/Tree.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Tree { + private String label; + private String value; + private Integer type = 0; + private List children; +} diff --git a/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java new file mode 100644 index 0000000..15248e8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzAppRow.java @@ -0,0 +1,37 @@ +package com.yunhai.userbase.model.appauthz; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 应用授权查询的扁平投影行(含 categoryId 供分组)。 + * 仅取授权列表所需字段,避免加载 appSecret 等无关大字段。 + * + * @author chenx + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppAuthzAppRow { + + /** + * 应用类型ID(分组键)。 + */ + private Integer categoryId; + + /** + * 应用ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 应用图标(pcIcon)。 + */ + private String icon; +} diff --git a/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java new file mode 100644 index 0000000..7021c9b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appauthz/AppAuthzCategoryVo.java @@ -0,0 +1,48 @@ +package com.yunhai.userbase.model.appauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 应用授权视图:应用类型 + 该类型下的应用列表。 + * + * @author chenx + */ +@Data +public class AppAuthzCategoryVo { + + /** + * 应用类型ID。 + */ + private Integer id; + + /** + * 应用类型名称。 + */ + private String name; + + /** + * 该类型下的应用列表。 + */ + private List app = new ArrayList<>(); + + /** + * 授权用的应用精简项(id/name/icon)。 + */ + @Data + public static class AppItem { + + /** + * 应用ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + } +} diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java new file mode 100644 index 0000000..e0d659d --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryQuery.java @@ -0,0 +1,13 @@ +package com.yunhai.userbase.model.appcategory; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryQuery { + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java new file mode 100644 index 0000000..be296a3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.appcategory; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "类别名称不能为空") + private String name; + + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java new file mode 100644 index 0000000..65a2060 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appcategory/AppCategoryVo.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.appcategory; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/15 22:35 + * @description + */ +@Data +public class AppCategoryVo { + private Integer id; + private String name; + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java new file mode 100644 index 0000000..a78b759 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoQuery.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 应用信息查询条件。 + * 用于列表查询时封装前端传入的筛选参数。 + * + * @author chenx + */ +@Data +public class ApplicationInfoQuery { + /** + * 应用名称,用于模糊匹配。 + */ + private String name; + + /** + * 应用类别ID,用于精确筛选。 + */ + private Integer categoryId; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java new file mode 100644 index 0000000..99abc9a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoRequest.java @@ -0,0 +1,76 @@ +package com.yunhai.userbase.model.application; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 应用信息新增/修改入参。 + * 用于接收前端提交的应用数据,并按分组做参数校验。 + * 说明:appKey/appSecret 为系统生成字段,不在此入参中暴露。 + * + * @author chenx + */ +@Data +public class ApplicationInfoRequest { + /** + * 主键ID,编辑时必填。 + */ + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** + * 应用名称,新增与编辑均必填。 + */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "应用名称不能为空") + private String name; + + /** + * 应用类别ID,新增与编辑均必填。 + */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "应用类别不能为空") + private Integer categoryId; + + /** + * 应用描述。 + */ + private String desc; + + /** + * 应用前端类型列表。 + */ + private List clients; + + /** + * PC应用访问地址。 + */ + private String pcUrl; + + /** + * PC应用图标地址。 + */ + private String pcIcon; + + /** + * App访问地址(移动端主地址)。 + */ + private String appUrl; + + /** + * App应用图标地址。 + */ + private String appIcon; + + /** + * 公众号应用地址。 + */ + private String officialAccountUrl; + + /** + * 钉钉应用地址。 + */ + private String dingtalkUrl; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java new file mode 100644 index 0000000..ddc0a93 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoSortVo.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model.application; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 应用信息返回视图。 + * 用于接口返回给前端的应用数据。 + * + * @author chenx + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApplicationInfoSortVo { + /** + * 主键ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 排序号,仅在同一个应用类型内有效;未排序时为 null。 + */ + private Integer sortNo; + +} diff --git a/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java new file mode 100644 index 0000000..5b669ef --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/ApplicationInfoVo.java @@ -0,0 +1,84 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +import java.util.List; + +/** + * 应用信息返回视图。 + * 用于接口返回给前端的应用数据。 + * + * @author chenx + */ +@Data +public class ApplicationInfoVo { + /** + * 主键ID。 + */ + private Integer id; + + /** + * 应用名称。 + */ + private String name; + + /** + * 应用类别ID。 + */ + private Integer categoryId; + + /** + * 排序号,仅在同一个应用类型内有效;未排序时为 null。 + */ + private Integer sortNo; + + /** + * 应用描述。 + */ + private String desc; + + /** + * 系统生成的唯一标识(appKey),整型,从100000起自增。 + */ + private Integer appKey; + + /** + * 密钥(appSecret),与数据库 app_secret 字段一致,字符串类型。 + */ + private String appSecret; + + /** + * 应用前端类型列表。 + */ + private List clients; + + /** + * PC应用访问地址。 + */ + private String pcUrl; + + /** + * PC应用图标地址。 + */ + private String pcIcon; + + /** + * App访问地址。 + */ + private String appUrl; + + /** + * App应用图标地址。 + */ + private String appIcon; + + /** + * 公众号应用地址。 + */ + private String officialAccountUrl; + + /** + * 钉钉应用地址。 + */ + private String dingtalkUrl; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java b/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java new file mode 100644 index 0000000..a6d58a4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/PushSettingRequest.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 保存应用推送设置请求(应用列表「推送设置」按钮提交)。 + * 所有字段均为可选更新:传 null 表示不修改该项。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushSettingRequest { + + /** 推送外部接口地址(不含固定后缀) */ + private String pushBaseUrl; + /** 推送总开关:0关 1开 */ + private Integer pushEnabled; + /** 启用的接口路径,逗号分隔 */ + private String pushEventTypes; + /** 关联学校组 id,逗号分隔;空=全局 */ + private String pushSchoolIds; + /** 推送角色 key,逗号分隔;空=全部角色 */ + private String pushRoleIds; + /** + * 推送签名密钥;传 null 表示不修改(保留现有值), + * 传空串 "" 表示清空(后续复用 appSecret)。 + */ + private String pushSecret; +} diff --git a/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java b/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java new file mode 100644 index 0000000..38ce68f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/application/PushSettingVo.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.application; + +import lombok.Data; + +/** + * 应用推送设置回显 VO(GetPushSetting 返回,仅含推送相关字段,不含 appSecret 等无关大字段)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushSettingVo { + + private Integer id; + private Integer appKey; + private String name; + private String pushBaseUrl; + private Integer pushEnabled; + private String pushEventTypes; + private String pushSchoolIds; + private String pushRoleIds; + /** 推送签名密钥(可展示供复制;空=复用 appSecret) */ + private String pushSecret; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java new file mode 100644 index 0000000..e16dc7c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleQuery.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model.appmodule; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块查询条件。 + */ +@Data +public class AppModuleQuery { + /** 所属应用ID(按应用过滤模块树)。 */ + private Integer appId; + /** 模块名称(模糊匹配)。 */ + private String name; + /** 父模块ID(传入则以该模块为根,递归返回其完整子树;不传则顶层只显示 level-1 根模块)。 */ + private Integer parentId; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java new file mode 100644 index 0000000..ff49dfd --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleRequest.java @@ -0,0 +1,36 @@ +package com.yunhai.userbase.model.appmodule; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块请求体。 + */ +@Data +public class AppModuleRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** 所属应用ID(创建时必填,创建后不可改)。 */ + @NotNull(groups = ValidationGroups.Create.class, message = "所属应用不能为空") + private Integer appId; + + /** 父模块ID;根模块传 null。 */ + private Integer parentId; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "模块名称不能为空") + private String name; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "自定义ID不能为空") + private String customId; + + /** 模块路径(前端路由/资源路径)。 */ + private String path; + + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java new file mode 100644 index 0000000..85641b0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/appmodule/AppModuleVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.appmodule; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块视图(树形,children 递归存放子模块)。 + */ +@Data +public class AppModuleVo { + private Integer id; + private Integer appId; + private Integer parentId; + private String name; + private String customId; + private String path; + private Integer level; + private Integer sortNo; + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java new file mode 100644 index 0000000..0d41d16 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 角色授权页应用节点视图:应用信息 + 其下模块权限树。 + * 普通角色只返回其所属应用;全应用角色返回系统全部应用。 + * + * @author chenx + */ +@Data +public class AppRoleAuthzAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC 应用图标地址。 */ + private String pcIcon; + + /** 排序号。 */ + private Integer sortNo; + + /** 该应用下的模块权限树(含勾选回显)。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java new file mode 100644 index 0000000..1ece8da --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzModuleVo.java @@ -0,0 +1,42 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 角色授权页模块节点视图(树形):模块信息 + 该角色勾选状态。 + * + * @author chenx + */ +@Data +public class AppRoleAuthzModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 该角色是否已勾选该模块(sys_app_role_module_rel 存在记录即勾选)。 */ + private Boolean checked = Boolean.FALSE; + + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java new file mode 100644 index 0000000..25949f8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleModuleAssignRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 角色模块授权保存入参(全量覆盖)。 + * moduleIds 为该角色本次勾选的模块ID全量列表;传空列表即清空该角色全部模块授权。 + * + * @author chenx + */ +@Data +public class AppRoleModuleAssignRequest { + + /** + * 角色ID。 + */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** + * 勾选的模块ID全量列表。 + */ + private List moduleIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java new file mode 100644 index 0000000..8a4b952 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserBindRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 角色绑定用户保存入参(全量覆盖,角色维度)。 + * userIds 为该角色本次绑定的用户ID全量列表;传空列表即解绑该角色全部用户。 + * + * @author chenx + */ +@Data +public class AppRoleUserBindRequest { + + /** + * 角色ID。 + */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** + * 绑定的用户ID全量列表。 + */ + private List userIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java new file mode 100644 index 0000000..d6e66a9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleUserVo.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 角色已绑定用户列表项视图。 + * + * @author chenx + */ +@Data +public class AppRoleUserVo { + + /** 用户ID。 */ + private Integer id; + + /** 账号。 */ + private String account; + + /** 真实姓名。 */ + private String realName; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java new file mode 100644 index 0000000..6f20bc3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleQuery.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 业务角色列表查询条件。 + * + * @author chenx + */ +@Data +public class SysAppRoleQuery { + + /** 角色名称(模糊匹配)。 */ + private String name; + + /** 所属应用ID(精确匹配,仅命中普通角色)。 */ + private Integer appId; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 是否禁用。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java new file mode 100644 index 0000000..7140af5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleRequest.java @@ -0,0 +1,50 @@ +package com.yunhai.userbase.model.approle; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 业务角色新增/修改入参。 + * 普通角色:allAppFlag=false 且 appId 必填;全应用角色:allAppFlag=true 且 appId 必须为空(服务层校验)。 + * + * @author chenx + */ +@Data +public class SysAppRoleRequest { + + /** + * 主键ID,编辑时必填。 + */ + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** + * 角色名称,新增与编辑均必填。 + */ + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "角色名称不能为空") + private String name; + + /** + * 所属应用ID;普通角色必填,全应用角色必须为空(appId 与 allAppFlag 的匹配关系在服务层校验)。 + */ + private Integer appId; + + /** + * 是否全应用角色,新增与编辑均必填。 + */ + @NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "角色类型不能为空") + private Boolean allAppFlag; + + /** + * 角色备注。 + */ + private String remark; + + /** + * 是否禁用;空值按不禁用处理。 + */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java new file mode 100644 index 0000000..b4826cb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/SysAppRoleVo.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +/** + * 业务角色列表项视图。 + * + * @author chenx + */ +@Data +public class SysAppRoleVo { + + /** 角色ID。 */ + private Integer id; + + /** 角色名称。 */ + private String name; + + /** 所属应用ID;全应用角色为 null。 */ + private Integer appId; + + /** 所属应用名称;全应用角色为 null。 */ + private String appName; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 角色备注。 */ + private String remark; + + /** 是否禁用。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java b/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java new file mode 100644 index 0000000..18e038e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/approle/UserAppRoleBindRequest.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.approle; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 用户绑定角色保存入参(全量覆盖,用户维度)。 + * roleIds 为该用户本次绑定的角色ID全量列表;传空列表即解绑该用户全部角色。 + * + * @author chenx + */ +@Data +public class UserAppRoleBindRequest { + + /** + * 用户ID。 + */ + @NotNull(message = "用户ID不能为空") + private Integer userId; + + /** + * 绑定的角色ID全量列表。 + */ + private List roleIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java new file mode 100644 index 0000000..7b63405 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppAssignRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 应用开通保存请求(功能1,全量覆盖式)。 + * 前端传递组ID与勾选的应用ID列表;appIds 为空或空列表代表取消该组全部应用开通。 + */ +@Data +public class GroupAppAssignRequest { + + /** 组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 勾选的应用ID列表(全量);为空代表全部取消开通。 */ + private List appIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java new file mode 100644 index 0000000..6a8b799 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAppVo.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/23 + * @description 应用开通视图(功能1):系统全量应用 + 该组是否已开通。 + */ +@Data +public class GroupAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用类别ID。 */ + private Integer categoryId; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC应用图标地址。 */ + private String pcIcon; + + /** 该组是否已开通(用于复选框回显)。 */ + private Boolean checked = Boolean.FALSE; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java new file mode 100644 index 0000000..c90d124 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权应用视图(功能2):该组已开通的应用 + 其下模块权限树。 + */ +@Data +public class GroupAuthzAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC应用图标地址。 */ + private String pcIcon; + + /** 应用类型ID;用于按应用分类分组展示组授权面板。 */ + private Integer categoryId; + + private Integer sortNo; + + /** 该应用下的模块权限树;默认空列表,避免 JSON 序列化出现 null。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java new file mode 100644 index 0000000..515af61 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzCategoryVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 组授权分类视图:按应用类型分组后,每个分类下挂载已开通的应用及其模块权限树。 + * + * @author chenx + * @date 2026/7/23 + */ +@Data +public class GroupAuthzCategoryVo { + + /** 应用类型ID。 */ + private Integer id; + + /** 应用类型名称。 */ + private String name; + + /** 该分类下已开通的应用列表(含模块权限树)。 */ + private List apps = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java new file mode 100644 index 0000000..3bc34a0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzModuleVo.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权模块节点视图(功能2,树形):模块信息 + 该组勾选状态与子组继承开关。 + */ +@Data +public class GroupAuthzModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 该组是否已勾选该模块(sys_group_module_perm 存在记录即勾选)。 */ + private Boolean checked = Boolean.FALSE; + + /** 是否子组继承(仅 checked=true 时有意义)。 */ + private Boolean inheritFlag = Boolean.FALSE; + + /** 子模块(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java new file mode 100644 index 0000000..ef3b2c4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 组模块授权单项(增量式:对某个组的「某应用下的某模块」做追加/删除)。 + * 与 GroupModulePermRequest(全量覆盖式 SaveModulePerms)互为补充:本类用于单条/批量增量操作, + * 由前端「组授权」面板逐条勾选或取消时提交。 + * + * 字段语义(务必与前端对齐): + * - groupId:目标组ID + * - appId:应用ID(被授权的模块必须属于该应用,越权写别的应用的模块会被拒绝) + * - moduleId:模块ID;传 0 表示「该应用下的全部模块」(前端勾选应用根/整应用时用) + * - mode:操作模式,取值见 SysModeEnum —— 0=追加(APPEND,授予授权) / 2=删除(DELETE,取消授权);1=覆盖 不适用于单条增量,会被拒绝 + * - isAll:是否对子组继承,直接映射现有 sys_group_module_perm.inherit_flag 列 —— 1=子孙组自动继承 / 0=仅本组生效 + * + * @author chenx + */ +@Data +public class GroupModuleAuthzItem { + + /** 目标组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 应用ID(模块必须属于该应用)。 */ + @NotNull(message = "应用ID不能为空") + private Integer appId; + + /** + * 模块ID;传 0 表示「该应用下全部模块」。 + * 注意:后端不会向 sys_group_module_perm 写入 module_id=0 的伪记录, + * 而是把 0 展开为应用下每个真实模块各写一条权限记录。 + */ + @NotNull(message = "模块ID不能为空(整应用传 0)") + private Integer moduleId; + + /** 操作模式:0=追加 / 2=删除(1=覆盖不适用)。 */ + @NotNull(message = "操作模式不能为空(0=追加/2=删除)") + private Integer mode; + + /** 是否对子组继承:1=子孙组继承 / 0=仅本组。映射 inherit_flag。 */ + @NotNull(message = "isAll不能为空(0=仅本组/1=子组继承)") + private Integer isAll; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java new file mode 100644 index 0000000..33a4202 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModulePermRequest.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组授权保存请求(功能2,全量覆盖式)。 + * perms 为该组当前勾选的模块权限全量列表:有旧记录则更新 inheritFlag,无则新增, + * 不在列表中的旧记录物理删除(即取消勾选)。 + */ +@Data +public class GroupModulePermRequest { + + /** 组ID。 */ + @NotNull(message = "组ID不能为空") + private Integer groupId; + + /** 勾选的模块权限全量列表;为空代表全部取消勾选。 */ + @Valid + private List perms; + + /** 单个模块权限项。 */ + @Data + public static class ModulePermItem { + + /** 模块ID。 */ + @NotNull(message = "模块ID不能为空") + private Integer moduleId; + + /** 是否子组继承;不传默认 false。 */ + private Boolean inheritFlag; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java b/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java new file mode 100644 index 0000000..af3d91c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/SchoolAppAuthzRequest.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 学校组应用授权增量请求(对应前端点击单个应用多选框勾选/取消时提交)。 + * 与 {@link GroupAppAssignRequest}(全量覆盖式 AssignApps)互为补充:本类用于「单条」增量开通或取消应用, + * 前端一次只传一个应用ID(逐次点击触发),因此 appid 为单个 Integer,而非集合/逗号串。 + * + * 字段语义(务必与前端对齐): + * - schid:目标学校组ID(即左侧树点击的 nodeId) + * - appid:单个应用ID(每次只传一个) + * - mode:操作模式,取值见 SysModeEnum —— 0=追加(APPEND,开通应用) / 2=删除(DELETE,取消开通);1=覆盖 不适用于单条增量,会被拒绝 + * + * 设计说明:每次请求只携带一个 appid,控制器/服务均不再做多值解析;逐次点击即多次提交。 + * + * @author chenx + */ +@Data +public class SchoolAppAuthzRequest { + + /** 目标学校组ID(左侧树节点 nodeId)。 */ + @NotNull(message = "组ID不能为空") + private Integer schid; + + /** + * 单个应用ID。 + * 前端逐次点击应用多选框触发,每次只传一个应用ID,故为 Integer 而非集合/字符串。 + */ + @NotNull(message = "应用ID不能为空") + private Integer appid; + + /** 操作模式:0=追加 / 2=删除(1=覆盖不适用)。 */ + @NotNull(message = "操作模式不能为空(0=追加/2=删除)") + private Integer mode; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java new file mode 100644 index 0000000..5212e4f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserGroupNodeVo.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户所属部门树节点(左侧上方「所属部门」区域)。 + * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组), + * 不包含无关的兄弟/子孙节点。 + * + * @author chenx + */ +@Data +public class UserGroupNodeVo { + + /** 组ID。 */ + private Integer id; + + /** 组名称。 */ + private String name; + + /** 是否用户直接归属的组(叶子高亮用);祖先组为 false。 */ + private Boolean own; + + /** 子节点(仅含祖先链上的节点)。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java new file mode 100644 index 0000000..23a1394 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionAppVo.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权应用节点。 + * 父节点为 {@link UserPermissionCategoryVo},子节点为 {@link UserPermissionModuleVo}。 + */ +@Data +public class UserPermissionAppVo { + + /** 应用ID。 */ + private Integer id; + + /** 应用名称。 */ + private String name; + + /** 应用唯一标识 appKey。 */ + private Integer appKey; + + /** PC 应用图标地址。 */ + private String pcIcon; + + /** 排序号。 */ + private Integer sortNo; + + /** 该应用下已授权的模块树。 */ + private List modules = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java new file mode 100644 index 0000000..360e92f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionCategoryVo.java @@ -0,0 +1,26 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权应用分类节点。 + * 一级子节点为 {@link UserPermissionAppVo}。 + */ +@Data +public class UserPermissionCategoryVo { + + /** 分类ID。 */ + private Integer id; + + /** 分类名称。 */ + private String name; + + /** 排序号。 */ + private Integer sortNo; + + /** 该分类下已授权的应用列表。 */ + private List apps = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java new file mode 100644 index 0000000..50edcc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionModuleVo.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 权限一览:用户已授权模块节点(递归树)。 + * 仅包含有权限的模块,无权限的模块及其子节点均不显示。 + */ +@Data +public class UserPermissionModuleVo { + + /** 模块ID。 */ + private Integer id; + + /** 父模块ID;根模块为 null。 */ + private Integer parentId; + + /** 模块名称。 */ + private String name; + + /** 自定义ID(应用内唯一业务编码)。 */ + private String customId; + + /** 模块路径。 */ + private String path; + + /** 层级(根=1)。 */ + private Integer level; + + /** 排序号。 */ + private Integer sortNo; + + /** 子模块(递归)。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java new file mode 100644 index 0000000..4ec8fb0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * 权限一览:用户列表项(组下用户或全局搜索用户)。 + */ +@Data +public class UserPermissionUserVo { + + /** 用户ID。 */ + private Integer id; + + /** 账号。 */ + private String account; + + /** 真实姓名。 */ + private String realName; + + /** 是否主归属:1=本职主学校/部门,0=兼职兼任。 */ + private Integer isMain; + + /** 用户所属部门/组织路径,如 /上海市仙霞高级中学/教师/行政。 */ + private String groupPath; +} diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java new file mode 100644 index 0000000..7e5adc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserRoleVo.java @@ -0,0 +1,30 @@ +package com.yunhai.userbase.model.groupauthz; + +import lombok.Data; + +/** + * 权限一览:用户所属业务角色列表项(中间下方「所属角色」区域)。 + * + * @author chenx + */ +@Data +public class UserRoleVo { + + /** 角色ID。 */ + private Integer id; + + /** 角色名称。 */ + private String name; + + /** 是否全应用角色。 */ + private Boolean allAppFlag; + + /** 所属应用ID;全应用角色为 null。 */ + private Integer appId; + + /** 所属应用名称;全应用角色为 null。 */ + private String appName; + + /** 是否禁用(禁用角色仍展示,但不参与权限计算)。 */ + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java b/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java new file mode 100644 index 0000000..60adcaa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentityCurrentVo.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.identity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 当前激活身份(来自 {@code sys_user_current_role};无切换记录时兜底主身份)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityCurrentVo { + + /** 当前激活角色ID(SysRoleEnum.key) */ + private Integer roleId; + + /** 当前激活角色名 */ + private String roleName; + + /** 当前激活学校ID(=学校组 sys_group.id;无学校时为 0) */ + private Integer schoolId; + + /** 当前激活学校名 */ + private String schoolName; + + /** 是否兜底(无主动切换记录时为 true,提示前端可引导用户显式切换) */ + private Boolean fallback; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java b/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java new file mode 100644 index 0000000..c06e657 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentityOptionVo.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.identity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户可选身份(一条 = 某学校下的某平台角色)。 + * 数据源:{@code sys_user_role_rel}(平台身份角色权威源)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityOptionVo { + + /** 角色ID(SysRoleEnum.key,1-7) */ + private Integer roleId; + + /** 角色名(老师/校领导/学生…,查枚举快取) */ + private String roleName; + + /** 学校ID(=学校组 sys_group.id;区域/学段/直属无学校时为 0) */ + private Integer schoolId; + + /** 学校名(冗余,免 join sys_group) */ + private String schoolName; + + /** 是否主身份(isMain=1) */ + private Boolean isMain; +} diff --git a/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java b/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java new file mode 100644 index 0000000..f7f88d7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/identity/IdentitySwitchRequest.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.identity; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 切换身份入参:指定目标学校与该学校下的角色。 + * 服务端校验 (userId, roleId, schoolId) 在 {@code sys_user_role_rel} 中存在且正常态, + * 不存在则拒绝(防止越权切换到非授权身份)。 + * + * @author chenx + * @date 2026/8/18 + */ +@Data +public class IdentitySwitchRequest { + + /** 目标角色ID(SysRoleEnum.key) */ + @NotNull(message = "角色ID不能为空") + private Integer roleId; + + /** 目标学校ID(=学校组 sys_group.id;无学校维度传 0) */ + @NotNull(message = "学校ID不能为空") + private Integer schoolId; +} diff --git a/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java new file mode 100644 index 0000000..37a5acd --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogListVo.java @@ -0,0 +1,82 @@ +package com.yunhai.userbase.model.oapilog; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 开放接口日志列表项视图。 + * 不含 response_data(响应结果),列表不展示,需通过下载接口单独获取。 + * + * @author chenx + * @date 2026/8/20 + */ +@Data +public class SysOapiLogListVo { + + /** 主键 */ + private Long id; + /** 日志级别 1-重要 2-次要 3-一般 */ + private Integer logLevel; + /** 调用方应用 appKey */ + private Integer callerAppKey; + /** 调用方应用名 */ + private String callerAppName; + /** 请求IP */ + private String requestIp; + /** 请求路径 */ + private String requestUri; + /** 请求方法 GET/POST/PUT/DELETE */ + private String requestMethod; + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + /** 类名.方法名 */ + private String classMethod; + /** 操作描述 */ + private String operationDesc; + /** 请求参数JSON(脱敏后) */ + private String requestParams; + /** 响应结果(GZIP 解压后原文,脱敏后;随列表带出,详情弹窗直接展示) */ + private String responseData; + /** HTTP 状态码 */ + private Integer statusCode; + /** 耗时(ms) */ + private Long costTime; + /** 调用方UA */ + private String userAgent; + /** 错误信息 */ + private String errorMsg; + /** 调用时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 全参构造,顺序需与 {@code SysOapiLog} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public SysOapiLogListVo(Long id, Integer logLevel, Integer callerAppKey, String callerAppName, + String requestIp, String requestUri, String requestMethod, + String operationType, String classMethod, String operationDesc, + String requestParams, Integer statusCode, Long costTime, + String userAgent, String errorMsg, LocalDateTime createTime) { + this.id = id; + this.logLevel = logLevel; + this.callerAppKey = callerAppKey; + this.callerAppName = callerAppName; + this.requestIp = requestIp; + this.requestUri = requestUri; + this.requestMethod = requestMethod; + this.operationType = operationType; + this.classMethod = classMethod; + this.operationDesc = operationDesc; + this.requestParams = requestParams; + this.statusCode = statusCode; + this.costTime = costTime; + this.userAgent = userAgent; + this.errorMsg = errorMsg; + this.createTime = createTime; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java new file mode 100644 index 0000000..cfbad9b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oapilog/SysOapiLogQuery.java @@ -0,0 +1,59 @@ +package com.yunhai.userbase.model.oapilog; + +import lombok.Data; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 开放接口日志查询条件。 + * 覆盖调用方应用、URI、级别、IP、时间等高频查询场景,与表索引前缀对齐。 + * + * @author chenx + * @date 2026/8/20 + */ +@Data +public class SysOapiLogQuery { + + /** 日志级别(1-重要 2-次要 3-一般) */ + private Integer logLevel; + + /** 调用方应用 appKey(等值) */ + private Integer callerAppKey; + + /** 调用方应用名(模糊) */ + private String callerAppName; + + /** 请求路径(模糊) */ + private String requestUri; + + /** 请求IP(模糊) */ + private String requestIp; + + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + + /** HTTP 状态码 */ + private Integer statusCode; + + /** + * 成功/失败筛选。 + * true=仅 HTTP 200(成功),false=非 200(失败)。 + */ + private Boolean success; + + /** + * 关键字,同时匹配请求路径(requestUri)与应用名(callerAppName)。 + * 前端单输入框搜索这两个字段。 + */ + private String keyword; + + /** 调用起始时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + /** 调用结束时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java new file mode 100644 index 0000000..c731bfc --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogListVo.java @@ -0,0 +1,77 @@ +package com.yunhai.userbase.model.oplog; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 操作日志列表项视图。 + * 不含 response_data(响应结果),列表不展示,需通过下载接口单独获取。 + * + * @author chenx + * @date 2026/7/28 + */ +@Data +public class SysOperationLogListVo { + + /** 主键 */ + private Long id; + /** 日志级别 1-重要 2-次要 3-一般 */ + private Integer logLevel; + /** 操作用户ID */ + private Long userId; + /** 操作用户名 */ + private String username; + /** 请求IP */ + private String requestIp; + /** 请求路径 */ + private String requestUri; + /** 请求方法 GET/POST/PUT/DELETE */ + private String requestMethod; + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + /** 类名.方法名 */ + private String classMethod; + /** 请求参数JSON */ + private String requestParams; + /** HTTP 状态码 */ + private Integer statusCode; + /** 耗时(ms) */ + private Long costTime; + /** 浏览器UA */ + private String userAgent; + /** 错误信息 */ + private String errorMsg; + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 全参构造,顺序需与 {@code SysOperationLog} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public SysOperationLogListVo(Long id, Integer logLevel, Long userId, String username, + String requestIp, String requestUri, String requestMethod, + String operationType, String classMethod, String requestParams, + Integer statusCode, Long costTime, String userAgent, + String errorMsg, LocalDateTime createTime) { + this.id = id; + this.logLevel = logLevel; + this.userId = userId; + this.username = username; + this.requestIp = requestIp; + this.requestUri = requestUri; + this.requestMethod = requestMethod; + this.operationType = operationType; + this.classMethod = classMethod; + this.requestParams = requestParams; + this.statusCode = statusCode; + this.costTime = costTime; + this.userAgent = userAgent; + this.errorMsg = errorMsg; + this.createTime = createTime; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java new file mode 100644 index 0000000..ca67306 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/oplog/SysOperationLogQuery.java @@ -0,0 +1,59 @@ +package com.yunhai.userbase.model.oplog; + +import lombok.Data; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 操作日志查询条件。 + * 覆盖时间、用户、URI、级别、IP 等高频查询场景,与表索引前缀对齐。 + * + * @author chenx + * @date 2026/7/28 + */ +@Data +public class SysOperationLogQuery { + + /** 日志级别(1-重要 2-次要 3-一般) */ + private Integer logLevel; + + /** 操作用户ID */ + private Long userId; + + /** 操作用户名(模糊) */ + private String username; + + /** 请求路径(模糊) */ + private String requestUri; + + /** 请求IP(模糊) */ + private String requestIp; + + /** 操作类型 DELETE/CREATE/UPDATE/QUERY */ + private String operationType; + + /** HTTP 状态码 */ + private Integer statusCode; + + /** + * 成功/失败筛选。 + * true=仅 HTTP 200(成功),false=非 200(失败)。 + */ + private Boolean success; + + /** + * 关键字,同时匹配接口名称(requestUri)与用户名(username)。 + * 前端单输入框搜索这两个字段。 + */ + private String keyword; + + /** 操作起始时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + /** 操作结束时间(含) */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java b/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java new file mode 100644 index 0000000..c0f146b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/OperationDetailsModel.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.other; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @program: UserProfile + * @description: 接口日志操作应用详情 + * @author: chenx + * @create: 2024-07-05 12:34 + **/ + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OperationDetailsModel { + + private Integer id;//应用id + private String name;//应用名称 + private Long time;//requestTime + private String url; + private String errorMsg; + + @JsonIgnore + private Integer indicatorId; + @JsonIgnore + private String resultMsg; + @JsonIgnore + private Double computeScore;//计算后的分数 + +} diff --git a/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java b/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java new file mode 100644 index 0000000..a41d539 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/SchLenGradeModel.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.other; + +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/25 21:28 + * @description 学校学制年级实体 + */ + +@Data +public class SchLenGradeModel { + private Integer gradeId; + private String gradeName; + private Integer stage; + + public SchLenGradeModel(Integer gradeId, String gradeName, Integer stage) { + this.gradeId = gradeId; + this.gradeName = gradeName; + this.stage = stage; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java b/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java new file mode 100644 index 0000000..fc11539 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/SchLenLevelModel.java @@ -0,0 +1,19 @@ +package com.yunhai.userbase.model.other; + +import com.yunhai.userbase.enums.SchoolStage; +import lombok.Data; + +import java.util.List; + +/** + * @author chenx + * @date 2025/12/25 17:44 + * @description 学校学段学制年级实体 + */ + +@Data +public class SchLenLevelModel { + private SchoolStage stage; + private List grades; +} + diff --git a/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java b/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java new file mode 100644 index 0000000..a158910 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/other/UserInfoModel.java @@ -0,0 +1,26 @@ +package com.yunhai.userbase.model.other; + +import com.yunhai.userbase.enums.UserType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @program: UserProfile + * @description: 用户信息 + * @author: chenx + * @create: 2024-07-09 17:30 + **/ + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserInfoModel { + + private Integer userId; + private String userName; + private Integer schId; + private Integer groupId; + private String photo; + private UserType userType; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java b/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java new file mode 100644 index 0000000..61b45f7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushInterfaceType.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.model.push; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文档约定的 8 个订阅接口:path(固定后缀,拼在订阅方 base URL 后)↔ 友好名 label。 + * + * @author chenx + * @date 2026/8/14 + */ +@Getter +@AllArgsConstructor +public enum PushInterfaceType { + + SYNC_GROUP("group/SyncGroup", "组全量同步"), + SYNC_USER("user/SyncUser", "用户全量同步"), + ADD_GROUP("group/AddGroup", "添加组"), + UPDATE_GROUP("group/UpdateGroup", "更新组"), + DELETE_GROUP("group/DeleteGroup", "删除组"), + ADD_USER("user/AddUser", "添加用户"), + UPDATE_USER("user/UpdateUser", "更新用户"), + DELETE_USER("user/DeleteUser", "删除用户"); + + private final String path; + private final String label; + + public static PushInterfaceType getByPath(String path) { + if (path == null) { + return null; + } + for (PushInterfaceType type : values()) { + if (type.path.equals(path)) { + return type; + } + } + return null; + } + + /** 是否为组相关接口。 */ + public boolean isGroup() { + return path.startsWith("group/"); + } + + /** 是否为用户相关接口。 */ + public boolean isUser() { + return path.startsWith("user/"); + } +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java new file mode 100644 index 0000000..9017d6a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordEntity.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 推送订阅记录实体(订阅方维度,落库用于日志 / 重试 / 排查)。 + * 与 DDL(push_subscription_ddl.sql §2)的 push_record 表一一对应。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +@NoArgsConstructor +@Entity +@Table(name = "push_record") +public class PushRecordEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "subscriber_app_key", nullable = false) + private Integer subscriberAppKey; + + @Column(name = "app_name") + private String appName; + + @Column(name = "interface_path", nullable = false) + private String interfacePath; + + @Column(name = "interface_label") + private String interfaceLabel; + + @Column(name = "request_url", nullable = false) + private String requestUrl; + + @Column(name = "payload", columnDefinition = "MEDIUMTEXT") + private String payload; + + @Column(name = "sign_t") + private String signT; + + @Column(name = "http_status") + private Integer httpStatus; + + @Column(name = "resp_code") + private Integer respCode; + + @Column(name = "resp_msg") + private String respMsg; + + /** 是否成功:0失败 1成功 */ + @Column(name = "success", nullable = false) + private Integer success = 0; + + @Column(name = "attempt_no", nullable = false) + private Integer attemptNo = 1; + + @Column(name = "cost_ms") + private Integer costMs; + + /** 触发类型:manual / auto / retry */ + @Column(name = "trigger_type") + private String triggerType; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java new file mode 100644 index 0000000..41e13db --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordListVo.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.model.push; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 推送记录列表项视图。 + * 不含 payload(请求体原文),列表不展示,需通过下载接口单独获取,避免全量推送时数据量过大。 + * + * @author chenx + * @date 2026/8/17 + */ +@Data +public class PushRecordListVo { + + /** 主键 */ + private Long id; + /** 订阅方 appKey */ + private Integer subscriberAppKey; + /** 应用名称 */ + private String appName; + /** 接口路径,如 group/SyncGroup */ + private String interfacePath; + /** 接口中文标签 */ + private String interfaceLabel; + /** 请求地址 */ + private String requestUrl; + /** 签名时间 t */ + private String signT; + /** HTTP 状态码 */ + private Integer httpStatus; + /** 业务码 */ + private Integer respCode; + /** 业务消息 */ + private String respMsg; + /** 是否成功:0失败 1成功 */ + private Integer success; + /** 重试次数 */ + private Integer attemptNo; + /** 耗时(ms) */ + private Integer costMs; + /** 触发类型:manual / auto / retry */ + private String triggerType; + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 全参构造,顺序需与 {@code PushRecordEntity} 的列表投影字段一致。 + * 用于 JPA Criteria {@code cb.construct} 投影查询。 + */ + public PushRecordListVo(Long id, Integer subscriberAppKey, String appName, String interfacePath, + String interfaceLabel, String requestUrl, String signT, Integer httpStatus, + Integer respCode, String respMsg, Integer success, Integer attemptNo, + Integer costMs, String triggerType, LocalDateTime createdAt) { + this.id = id; + this.subscriberAppKey = subscriberAppKey; + this.appName = appName; + this.interfacePath = interfacePath; + this.interfaceLabel = interfaceLabel; + this.requestUrl = requestUrl; + this.signT = signT; + this.httpStatus = httpStatus; + this.respCode = respCode; + this.respMsg = respMsg; + this.success = success; + this.attemptNo = attemptNo; + this.costMs = costMs; + this.triggerType = triggerType; + this.createdAt = createdAt; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java b/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java new file mode 100644 index 0000000..ffa1240 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushRecordQuery.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; + +/** + * 推送记录查询条件(前端日志页搜索用)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushRecordQuery { + + /** 订阅方 appKey */ + private Integer appKey; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 是否成功:0失败 1成功 */ + private Integer success; + /** 起始时间 yyyy-MM-dd(含当天 00:00:00) */ + private String startTime; + /** 结束时间 yyyy-MM-dd(含当天 23:59:59) */ + private String endTime; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java b/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java new file mode 100644 index 0000000..744d641 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushTaskMessage.java @@ -0,0 +1,38 @@ +package com.yunhai.userbase.model.push; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * MQ 推送任务消息。 + * 由 {@code PushDispatcher}(生产方)投递,{@code PushConsumer}(消费方)取出后 POST 给订阅方。 + * 签名(t + sign)在投递时由基础平台计算一次;自动重试保持原 t/sign,手动重试重新生成。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PushTaskMessage implements Serializable { + + /** 订阅方 appKey */ + private Integer appKey; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 接口友好名 */ + private String interfaceLabel; + /** 业务数据原始 JSON 字符串(签名与发送均基于该原始字符串字节) */ + private String body; + /** 签名时间戳(字符串) */ + private String t; + /** HMAC-SHA256(业务JSON + t, secret) hex 小写 */ + private String sign; + /** 第几次尝试:1=首次 */ + private int attemptNo; + /** 触发类型:manual / auto / retry */ + private String triggerType; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java b/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java new file mode 100644 index 0000000..4859229 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/PushTestRequest.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.push; + +import lombok.Data; + +import java.util.List; + +/** + * 测试推送请求(测试推送页调用)。 + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushTestRequest { + + /** 订阅方应用 id */ + private Integer appId; + /** 接口路径,如 group/AddGroup */ + private String interfacePath; + /** 目标 id 列表(可选):不传则按接口类型取全量/全部 */ + private List targetIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java b/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java new file mode 100644 index 0000000..06cd21a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/dto/PushGroupDTO.java @@ -0,0 +1,32 @@ +package com.yunhai.userbase.model.push.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 文档「组」契约 DTO(group/SyncGroup、AddGroup、UpdateGroup 共用)。 + * 字段严格对齐《弘育基础平台数据同步接口 2》: + * + * SyncGroup:children 递归树(可空) + * AddGroup:pid 必填(上级组 id,0 为顶级) + * UpdateGroup:仅 id 必填,其余可选 + * + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushGroupDTO { + + private Integer id; + private String name; + private String code; + /** 组类别 1-8,与 GroupTypeEnum 1:1 零映射 */ + private Integer type; + /** 上级组 id(AddGroup 必填) */ + private Integer pid; + private Integer no; + /** 子组列表(SyncGroup 用) */ + private List children; +} diff --git a/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java b/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java new file mode 100644 index 0000000..697ca4b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/push/dto/PushUserDTO.java @@ -0,0 +1,53 @@ +package com.yunhai.userbase.model.push.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 文档「用户」契约 DTO(user/SyncUser、AddUser、UpdateUser 共用)。 + * 字段严格对齐《弘育基础平台数据同步接口 2》: + * + * status:0正常 / 2禁用(由 userbase StatusEnum 映射) + * groups:用户所属组,含 groupId / no / schoolId(schoolId = 组所属学校范围 id) + * roleList:用户身份,含 roleId / schId(schId = 用户主组所属学校范围 id) + * + * + * @author chenx + * @date 2026/8/14 + */ +@Data +public class PushUserDTO { + + private Integer id; + private String account; + private String name; + private String idcard; + private String phone; + private String email; + /** 性别 0女 1男 */ + private Integer gender; + private String birthday; + /** 用户状态 0正常 2禁用 */ + private Integer status; + private String profilePicture; + private List groups; + private List roleList; + + /** 用户所属组引用。 */ + @Data + public static class PushUserGroupRef { + private Integer groupId; + private Integer no; + /** 组所属学校范围 id(沿 parentId 向上最近的 SCHOOL_GROUP) */ + private Integer schoolId; + } + + /** 用户身份引用。 */ + @Data + public static class PushUserRoleRef { + private Integer roleId; + /** 所属学校 id(userbase 无角色-学校关联,统一取用户主组所属学校) */ + private Integer schId; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java new file mode 100644 index 0000000..a8c41f8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYear.java @@ -0,0 +1,20 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** +* @Description: 学年学期 +* @Author: chenx +* @Date: 2023/9/5 +*/ + +@Data +public class AcademicYear { + public Integer id; + public String name; + public Integer year; + public Integer semester; + public String startDate; + public String endDate; + public String intervalDate;//日期区间 +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java new file mode 100644 index 0000000..23f6b1f --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/AcademicYearSeq.java @@ -0,0 +1,25 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +import java.util.Date; + +/** +* @Description: 班级管理学年学期序列化 +* @Author: chenx +* @Date: 2023/9/5 +*/ + +@Data +public class AcademicYearSeq { + private int id; + private String name; + private Date startDate; + private Date endDate; + private Date progressionDate; + private int year; + private Date firstSemesterStartDate; + private Date firstSemesterEndDate; + private Date secondSemesterStartDate; + private Date secondSemesterEndDate; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java b/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java new file mode 100644 index 0000000..029444e --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/BaseSchool.java @@ -0,0 +1,18 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/30 19:31 + * @description 学校列表 + */ + +@Data +public class BaseSchool { + private String SchSerID; + private int Type; + private String PID; + private int ID; + private String Name; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java b/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java new file mode 100644 index 0000000..5e87ee4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/GradeClassStu.java @@ -0,0 +1,22 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** +* @Description: 班级管理接口序列化 :GetClaArrStulist +* @Author: chenx +* @Date: 2023/6/8 +*/ + +@Data +public class GradeClassStu { + private Integer gradeId; + private String gradeName; + private Integer classId; + private String className; + private Integer stuId; + private String stuName; + private String CardID;//学号 + private Integer StuSex;//性别 + private String StuPhoto; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java b/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java new file mode 100644 index 0000000..bdfe307 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchClass.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class SchClass { + + public String SchSerID; + public int SchID; + public String GradeName; + public String ClassName; + public String ClassID; + public int GradeID; + public int CID; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java b/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java new file mode 100644 index 0000000..d8913a7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchClassModel.java @@ -0,0 +1,12 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class SchClassModel { + public Integer classID; + public Integer gradeID; + public String gradeName; + public String className; + +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java b/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java new file mode 100644 index 0000000..fd8d7d8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchLenLevel.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.serialize; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +/** + * @author chenx + * @date 2025/12/25 15:56 + * @description 学校学制年级信息 + */ + +@Data +public class SchLenLevel { + private Integer code; + @JSONField(name = "OwnerId") + private Integer ownerId; + private String sx; + @JSONField(name = "Id") + private Integer id; + private Integer levell; + @JSONField(name = "Name") + private String name; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java b/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java new file mode 100644 index 0000000..7ee4969 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/SchTeaInfo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.serialize; + +/** + * @program: ResearchLearn + * @description: 全部教师信息 + * @author: chenx + * @create: 2024-06-07 10:16 + **/ + +import lombok.Data; + +@Data +public class SchTeaInfo { + private String SchSerID; + private String Teapolitical; + private String TeaWorkStart; + private String Account; + private String role; + private Integer TeaSex; + private String IsLeadingMiddle; + private String TeaOriginPlace; + private String Photo; + private String TeaIDCard; + private String Name; + private Integer GroupID; + private String SchName; + private String TeaTeachStart; + private String TeaSerID; + private Integer UserID; + private String TeaNation; + private String Tel; + private String TeaWorkType; + private Integer TeaNo; + private String TeaBirth; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java new file mode 100644 index 0000000..e5a32b4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +/** + * @program: QualityEvaluate + * @description: 获取班级列表的学生信息序列化 + * @author: chenx + * @create: 2023-08-21 10:27 + **/ + +@Data +public class StuInfo { + private String SchSerID; + private String Account; + private String StuStatus; + private String invitecode; + private int StuSex; + private String ClassID; + private String CardID; + private String ClassStuID; + private String StuPhoto; + private long GroupID; + private String StuName; + private long UserStuSerID; + private boolean Graduated; + private String StuIDCard; + private String StuCurrentTel; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java new file mode 100644 index 0000000..4c65fe6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/StuInfo2.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +import java.util.Date; + +/** + * @program: QualityEvaluate + * @description: 获取单个学生信息序列化 + * @author: chenx + * @create: 2023-09-06 14:46 + **/ + +@Data +public class StuInfo2 { + private int status_code; + private String idCard; + private int sex; + private String photo; + private int IsGraduation; + private Date birth; + private String ClaName; + private String stuName; + private String SchName; + private String studentId; + private long classId; + private String GradeName; + private long UserStuSerID; + private String ClassName; + private int IsDelete; +} diff --git a/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java b/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java new file mode 100644 index 0000000..1eb1237 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/serialize/TeaFirstClass.java @@ -0,0 +1,13 @@ +package com.yunhai.userbase.model.serialize; + +import lombok.Data; + +@Data +public class TeaFirstClass { + public String gradeName; + public int gradeId ; + public String alias; + public String className ; + public String name ; + public int id ; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java new file mode 100644 index 0000000..7102524 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupBatchRequest.java @@ -0,0 +1,48 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 系统组批量新增请求:在指定父组下,按 items 列表逐个创建子组。 + * 请求体示例: + * { + * "pid": 11285, + * "type": 4, + * "items": [ + * { "name": "高一1班" }, + * { "name": "高一2班" } + * ] + * } + */ +@Data +public class SysGroupBatchRequest { + + /** 父组ID(待生成子组的挂载点,必须存在)。 */ + @NotNull(message = "父组ID不能为空") + private Integer pid; + + /** 组类型(GroupTypeEnum.key,如 4=班级组)。 */ + @NotNull(message = "组类型不能为空") + private Integer type; + + /** 待新增的子组列表,每一项仅需提供 name。 */ + @NotEmpty(message = "批量新增的子组列表不能为空") + @Valid + private List items; + + /** 批量新增的单项。 */ + @Data + public static class BatchItem { + /** 子组名称。 */ + @javax.validation.constraints.NotBlank(message = "组名称不能为空") + private String name; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java new file mode 100644 index 0000000..3643b72 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupMoveRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/7/22 + * @description 移动组请求:将 moveId 节点移动到 pid 节点下。 + * 不支持移动到自身或自身后代下(环检测)。 + */ +@Data +public class SysGroupMoveRequest { + + /** 待移动节点ID。 */ + @NotNull(message = "待移动节点ID不能为空") + private Integer moveId; + + /** 目标父节点ID(移动后的挂载点,必须存在)。 */ + @NotNull(message = "目标父节点ID不能为空") + private Integer pid; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java new file mode 100644 index 0000000..ece4fc8 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupQuery.java @@ -0,0 +1,15 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组查询条件 + */ +@Data +public class SysGroupQuery { + private String name; + + private Integer parentId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java new file mode 100644 index 0000000..405668c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupRequest.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.model.sysgroup; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotBlank; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组新增/编辑请求 + */ +@Data +public class SysGroupRequest { + // ID 通过路径参数传递,body 无需携带,故不在校验组内强制 + private Integer id; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "组名称不能为空") + private String name; + + // 新增时必填;编辑不通过此字段移动节点(移动请走 Move 接口),故编辑时可选 + @NotNull(groups = ValidationGroups.Create.class, message = "上级组ID不能为空") + private Integer parentId; + + // 新增时必填;编辑时可选(仅当需要变更类型时传入) + @NotNull(groups = ValidationGroups.Create.class, message = "组类型不能为空") + private Integer type; + + private Integer sort; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java new file mode 100644 index 0000000..d01ecd1 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupSortRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 子组排序请求:将 pid 的直接子节点按给定 ids 顺序重新排列并落库。 + * ids 必须全部是 pid 下一级的直接子节点,否则报错。 + */ +@Data +public class SysGroupSortRequest { + + /** 父组ID。 */ + @NotNull(message = "父组ID不能为空") + private Integer pid; + + /** 期望的、pid 直接子节点有序 ID 列表(按传入顺序生效)。 */ + @NotNull(message = "子节点ID列表不能为空") + private List ids; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java new file mode 100644 index 0000000..8aadbe5 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysgroup/SysGroupVo.java @@ -0,0 +1,21 @@ +package com.yunhai.userbase.model.sysgroup; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组返回对象(树形:children 递归存放子组)。 + */ +@Data +public class SysGroupVo { + private Integer id; + private String name; + private Integer type; + private Integer sort; + /** 子组(递归);默认空列表,避免 JSON 序列化出现 null。 */ + private List children = new ArrayList<>(); +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java new file mode 100644 index 0000000..28905f7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量设置用户状态请求 + */ +@Data +public class BatchChangeStatusRequest { + + /** 待设置用户ID列表 */ + @NotEmpty(message = "待设置用户不能为空") + private List userIds; + + /** 是否禁用:true=禁用,false=启用 */ + @NotNull(message = "状态不能为空") + private Boolean disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java new file mode 100644 index 0000000..5f1b4cb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/12 + * @description 批量从当前组删除用户请求 + */ +@Data +public class BatchDeleteRequest { + + /** 待删除用户ID列表 */ + @NotEmpty(message = "待删除用户不能为空") + private List userIds; + + /** 当前组ID */ + @NotNull(message = "组ID不能为空") + private Integer groupId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java new file mode 100644 index 0000000..f479d3a --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchImportResult.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/11 + * @description 用户批量导入结果 + */ +@Data +public class BatchImportResult { + + /** 总行数(含表头) */ + private int total; + + /** 成功条数 */ + private int success; + + /** 失败条数 */ + private int fail; + + /** 失败明细:行号 + 原因 */ + private List fails = new ArrayList<>(); + + @Data + public static class FailItem { + private int row; + private String reason; + + public FailItem(int row, String reason) { + this.row = row; + this.reason = reason; + } + } + + public void addSuccess() { + this.success++; + } + + public void addFail(int row, String reason) { + this.fail++; + this.fails.add(new FailItem(row, reason)); + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java new file mode 100644 index 0000000..5c15b38 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeGroupsRequest.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * @author chenx + * @date 2026/8/11 + * @description 批量调整用户所属组请求 + */ +@Data +public class ChangeGroupsRequest { + + /** 待调整用户ID列表 */ + @NotEmpty(message = "待调整用户不能为空") + private List userIds; + + /** 目标组ID列表(覆盖式) */ + @NotEmpty(message = "请至少保留一个组") + private List groupIds; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java new file mode 100644 index 0000000..0631049 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java @@ -0,0 +1,29 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +/** + * @author chenx + * @date 2026/8/12 + * @description 调整用户在某组内的排序号 + */ +@Data +public class ChangeSortNoRequest { + + /** 所属分组(排序号按组维度存储,必须明确是哪个组) */ + @NotNull(message = "所属分组不能为空") + private Integer groupId; + + /** + * 组内排序号:1~999 的正整数;越小越靠前。 + * 允许为 null —— 表示清除排序号,列表回退到 user_id 升序兜底。 + * 注意:@Min/@Max 对 null 不生效,null 由业务层解释为「清除」。 + */ + @Min(1) + @Max(999) + private Integer sortNo; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java b/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java new file mode 100644 index 0000000..675e952 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/ExcelSysUser.java @@ -0,0 +1,53 @@ +package com.yunhai.userbase.model.sysuser; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; + +/** + * 组用户导出 Excel 实体:与导入模板 {@code IMPORT_COLUMN_DEFS} 同构(前 12 列顺序一致), + * 末尾追加「用户id」列。导出 = 读取数据库数据填充 list,复用 EasyPoi 的 {@code @Excel} 注解生成 Excel。 + */ +@Data +public class ExcelSysUser implements Serializable { + + @Excel(name = "组名\n(不必填,供与id对应参考用)", orderNum = "0", width = 36) + private String groupName; + + @Excel(name = "组id(必须)", orderNum = "1", width = 14) + private String groupId; + + @Excel(name = "用户名(必须)", orderNum = "2", width = 20) + private String account; + + @Excel(name = "密码\n(可选,\n默认Vzuk4Dp3)", orderNum = "3", width = 18) + private String password; + + @Excel(name = "姓名(必须)", orderNum = "4", width = 16) + private String realName; + + @Excel(name = "身份证号(可选)", orderNum = "5", width = 22) + private String idcard; + + @Excel(name = "手机号(可选)", orderNum = "6", width = 16) + private String phone; + + @Excel(name = "email(可选)", orderNum = "7", width = 24) + private String email; + + @Excel(name = "用户性质\n(可选,默认普通用户\n组管理员2,\n普通用户0,\n导入不接受别的代码)", orderNum = "8", width = 26) + private Integer nature; + + @Excel(name = "用户身份\n(1教师 2教职工 3学生 4家长)", orderNum = "9", width = 24) + private Integer identity; + + @Excel(name = "性别(可选)", orderNum = "10", width = 12) + private Integer gender; + + @Excel(name = "生日(yyyy-MM-dd)(可选)", orderNum = "11", width = 22) + private String birthday; + + @Excel(name = "用户id", orderNum = "12", width = 14) + private Integer userId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java new file mode 100644 index 0000000..7645bfb --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java @@ -0,0 +1,40 @@ +package com.yunhai.userbase.model.sysuser; + +/** + * 批量删除用户-组关系单项(与 BasePlatform 开放接口模型 {@code SysUserGroupDeleteItem} 字段对齐)。 + * 每条记录携带自身所属学校范围 {@code schoolId},供 {@code SysUserService.deleteUsers} 逐条校验: + * groupId 必须属于 schoolId 子树且不能等于 schoolId(禁止跨校解绑 / 整校移除)。 + */ +public class SysUserGroupDeleteItem { + + /** 待解绑用户 ID */ + private Integer userId; + /** 待解绑的组 ID(必须属于 schoolId 子树,且不能等于 schoolId) */ + private Integer groupId; + /** 学校组 ID:限定该条解绑的学校范围 */ + private Integer schoolId; + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public Integer getGroupId() { + return groupId; + } + + public void setGroupId(Integer groupId) { + this.groupId = groupId; + } + + public Integer getSchoolId() { + return schoolId; + } + + public void setSchoolId(Integer schoolId) { + this.schoolId = schoolId; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java new file mode 100644 index 0000000..abad1a7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserMoveRequest.java @@ -0,0 +1,24 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @description 开放接口:移动用户到指定组请求(语义同后台「调整组」——覆盖式把用户组归属替换为目标组,目标设为主组)。 + */ +@Data +public class SysUserMoveRequest { + + /** 待移动用户ID。 */ + @NotNull(message = "用户ID不能为空") + private Integer userId; + + /** 目标组ID(移动后的主归属组,必须存在)。 */ + @NotNull(message = "目标组ID不能为空") + private Integer groupId; + + /** 学校组ID:限定移动范围,仅允许将用户在同一学校(组)范围内移动。 */ + @NotNull(message = "学校ID不能为空") + private Integer schoolId; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java new file mode 100644 index 0000000..78454c2 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserPasswordRequest.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 修改用户密码请求(oapi 开放接口专用)。 + * + * @author chenx + */ +@Data +public class SysUserPasswordRequest { + + /** + * 用户ID(必填)。 + */ + @NotNull + private Integer userId; + + /** + * 旧密码(必填,须与库中 BCrypt 密文匹配)。 + */ + @NotBlank + private String oldPassword; + + /** + * 新密码(必填,走弱密码规则校验后 BCrypt 加密写入)。 + */ + @NotBlank + private String newPassword; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java new file mode 100644 index 0000000..2f35855 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserQuery.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户列表查询条件 + */ +@Data +public class SysUserQuery { + + /** 分组ID(列表以某个组节点为上下文查询其下用户) */ + private Integer groupId; + + /** + * 是否包含子组用户:0=仅查询该节点下的用户;1=查询该节点及其所有子孙组下的用户。 + * 默认 0。 + */ + private Integer withSub = 0; + + /** 关键字:同时匹配账号、姓名、身份证(模糊)与手机号(精确) */ + private String name; + + /** 启用状态过滤(可选);对应实体 disabled 字段(StatusEnum key:0=正常/未禁用,1=禁用) */ + private Integer disabled; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java new file mode 100644 index 0000000..ccdf606 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserRequest.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.model.sysuser; + +import com.yunhai.userbase.common.validation.ValidationGroups; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户新增/编辑请求 + */ +@Data +public class SysUserRequest { + @NotNull(groups = ValidationGroups.Update.class, message = "编辑时ID不能为空") + private Integer id; + + /** 所属分组ID,新增时必填(用户创建后写入组用户关联表) */ + @NotNull(groups = ValidationGroups.Create.class, message = "所属分组不能为空") + private Integer groupId; + + @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "账号不能为空") + private String account; + + @NotBlank(groups = ValidationGroups.Create.class, message = "密码不能为空") + private String password; + + /** 确认密码:与 password 一致性由 Service 校验(编辑时若不修改密码可不传) */ + private String confirmPassword; + + private String realName; + private String phone; + private LocalDate birthday; + private String idcard; + private String email; + private Integer gender; + private String profilePicture; + private List roles; + /** 启用状态:对应 StatusEnum 的 key(0=正常/未禁用,1=禁用);不传则默认启用 */ + private Integer disabled; + private Boolean expired; +} diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java new file mode 100644 index 0000000..3dee203 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserVo.java @@ -0,0 +1,35 @@ +package com.yunhai.userbase.model.sysuser; + +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户返回对象 + */ +@Data +public class SysUserVo { + private Integer id; + private String account; + private LocalDate birthday; + private String realName; + private String phone; + private String idcard; + private String email; + private Integer gender; + private String profilePicture; + private List roles; + /** 启用状态:StatusEnum 的 key(0=正常/未禁用,1=禁用) */ + private Integer disabled; + /** 用户类型:UserTypeEnum 的 key(0=普通用户,1=超级管理员,2=学校管理员) */ + private Integer type; + /** 当前组维度的排序号(组内 sort_no);根节点/未选组时为 null */ + private Integer sortNo; + private Boolean expired; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java b/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java new file mode 100644 index 0000000..1401768 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/AppCategoryRepository.java @@ -0,0 +1,33 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.AppCategory; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:29 + * @description + */ +public interface AppCategoryRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT MAX(c.sortNo) FROM AppCategory c") + Integer findMaxSortNo(); + + List findAllByOrderBySortNoAsc(); + + List findByNameContainingOrderBySortNoAsc(String name); + + boolean existsByName(String name); + + boolean existsByNameAndIdNot(String name, Integer id); + + @Modifying + @Query("UPDATE AppCategory c SET c.sortNo = :sortNo WHERE c.id = :id") + void updateSortNoById(@Param("id") Integer id, @Param("sortNo") Integer sortNo); +} diff --git a/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java b/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java new file mode 100644 index 0000000..e1306d9 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/AppModuleRepository.java @@ -0,0 +1,46 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.AppModule; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块仓储。 + */ +public interface AppModuleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 动态过滤:appId/parentId/name 均可空; + * 排序:sortNo 正序(空视为 999),再按 id 正序。 + */ + @Query("SELECT m FROM AppModule m " + + "WHERE (:appId IS NULL OR m.appId = :appId) " + + "AND (:parentId IS NULL OR m.parentId = :parentId) " + + "AND (:name IS NULL OR m.name LIKE CONCAT('%', :name, '%')) " + + "ORDER BY CASE WHEN m.sortNo IS NULL THEN 999 ELSE m.sortNo END ASC, m.id ASC") + List findModules(@Param("appId") Integer appId, + @Param("parentId") Integer parentId, + @Param("name") String name); + + boolean existsByAppIdAndCustomId(Integer appId, String customId); + + boolean existsByAppIdAndCustomIdAndIdNot(Integer appId, String customId, Integer id); + + long countByParentId(Integer parentId); + + /** + * 按应用ID集合查询全部模块(组授权按已开通应用批量加载模块树用)。 + */ + List findByAppIdIn(List appIds); + + /** + * 按应用ID级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理整棵子树)。 + */ + void deleteByAppId(Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java new file mode 100644 index 0000000..a34acb3 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java @@ -0,0 +1,80 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.appauthz.AppAuthzAppRow; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:29 + * @description + */ +public interface ApplicationInfoRepository extends JpaRepository, JpaSpecificationExecutor { + @Query("SELECT COUNT(a.id) FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000") + Integer countByCategoryId(@Param("categoryId") Integer id); + + boolean existsByName(String name); + + boolean existsByNameAndIdNot(String name, Integer id); + + /** + * 查询当前最大的 appKey 值。 + * 用于新增应用时生成从 100000 起自增的整型 appKey。 + * + * @return 当前最大 appKey,无记录时返回 null + */ + @Query("SELECT MAX(a.appKey) FROM ApplicationInfo a") + Integer findMaxAppKey(); + + /** + * 按整型 appKey 查单个应用(验签兜底端点直连读取用)。 + * appKey 由新增应用自增生成,业务上唯一。 + * + * @param appKey 应用标识(整数) + * @return 匹配的应用实体,无则 null + */ + ApplicationInfo findByAppKey(Integer appKey); + + /** 查询所有开启推送的应用(自动推送遍历订阅方用)。 */ + List findByPushEnabled(Integer pushEnabled); + + /** + * 按 id 更新排序号(同一应用类型内排序使用)。 + * + * @param id 应用ID + * @param sortNo 新的排序号 + */ + @Modifying + @Query("UPDATE ApplicationInfo a SET a.sortNo = :sortNo WHERE a.id = :id") + void updateSortNoById(@Param("id") Integer id, @Param("sortNo") Integer sortNo); + + /** + * 按应用类型查询排序所需的精简视图(仅 id/name/sortNo),不分页。 + * 使用构造器表达式做 DTO 投影,避免加载整个实体(含 appSecret、各类 URL 等无关大字段)。 + * 排除用户中心自身的基石应用(appKey=100000)。 + * + * @param categoryId 应用类型ID + * @return 该类型下全部应用的排序视图,按 sortNo 升序(未排序排末尾),id 升序兜底 + */ + @Query("SELECT new com.yunhai.userbase.model.application.ApplicationInfoSortVo(a.id, a.name, a.sortNo) " + + "FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000 ORDER BY a.sortNo ASC, a.id ASC") + List findSortVoByCategoryId(@Param("categoryId") Integer categoryId); + + /** + * 应用授权用:查询全部应用的扁平投影(含 categoryId 供按类型分组),仅取 id/name/pcIcon。 + * 按 categoryId、sortNo、id 升序,便于分组后同类型内保持排序。 + * 排除用户中心自身的基石应用(appKey=100000)。 + * + * @return 全部应用的授权投影行 + */ + @Query("SELECT new com.yunhai.userbase.model.appauthz.AppAuthzAppRow(a.categoryId, a.id, a.name, a.pcIcon) " + + "FROM ApplicationInfo a WHERE a.appKey > 100000 ORDER BY a.categoryId ASC, a.sortNo ASC, a.id ASC") + List findAllAuthzRows(); +} diff --git a/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java b/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java new file mode 100644 index 0000000..65a4eaa --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/PushRecordRepository.java @@ -0,0 +1,16 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.model.push.PushRecordEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +/** + * 推送记录仓储。 + * + * @author chenx + * @date 2026/8/14 + */ +@Repository +public interface PushRecordRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java new file mode 100644 index 0000000..d94a60c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysAppRoleModuleRelRepository.java @@ -0,0 +1,47 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysAppRoleModuleRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 业务角色-模块权限关系仓储。 + */ +public interface SysAppRoleModuleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByRoleId(Integer roleId); + + /** + * 权限计算用:批量查询一组角色的全部模块授权记录。 + */ + List findByRoleIdIn(List roleIds); + + /** + * 按角色ID批量物理删除模块授权记录(bulk delete 直接发 SQL)。 + * 用于「角色模块授权」全量覆盖式保存的先删后插,避免 Hibernate flush 先 insert 后 delete 触发唯一键冲突。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.roleId = :roleId") + void deleteByRoleIdBulk(@Param("roleId") Integer roleId); + + /** + * 按角色ID集合批量物理删除模块授权记录(删除应用时级联清理该应用下角色)。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.roleId IN :roleIds") + void deleteByRoleIdInBulk(@Param("roleIds") List roleIds); + + /** + * 按模块ID集合批量物理删除模块授权记录(删除模块/应用时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysAppRoleModuleRel r WHERE r.moduleId IN :moduleIds") + void deleteByModuleIdInBulk(@Param("moduleIds") List moduleIds); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java b/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java new file mode 100644 index 0000000..4c30a65 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysAppRoleRepository.java @@ -0,0 +1,40 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysAppRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色仓储。 + */ +public interface SysAppRoleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 普通角色重名校验:同一应用下角色名称唯一(新增用)。 + */ + boolean existsByNameAndAppId(String name, Integer appId); + + /** + * 普通角色重名校验:同一应用下角色名称唯一,排除自身(编辑用)。 + */ + boolean existsByNameAndAppIdAndIdNot(String name, Integer appId, Integer id); + + /** + * 全应用角色重名校验:全应用角色之间名称唯一(新增用)。 + */ + boolean existsByNameAndAllAppFlagTrue(String name); + + /** + * 全应用角色重名校验:全应用角色之间名称唯一,排除自身(编辑用)。 + */ + boolean existsByNameAndAllAppFlagTrueAndIdNot(String name, Integer id); + + /** + * 查询某应用下的全部普通角色(删除应用时级联清理用)。 + */ + List findByAppId(Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java new file mode 100644 index 0000000..8ec1200 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java @@ -0,0 +1,49 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroupAppRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用开通仓储。 + */ +public interface SysGroupAppRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByGroupId(Integer groupId); + + List findByGroupIdIn(List groupIds); + + boolean existsByGroupIdAndAppId(Integer groupId, Integer appId); + + long countByAppId(Integer appId); + + /** + * 按组ID批量物理删除开通记录(bulk delete 直接发 SQL)。 + * 用于「应用开通」全量覆盖式保存的先删后插,避免 Hibernate flush 先 insert 后 delete 触发唯一键冲突。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 按应用ID批量物理删除开通记录(删除应用时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.appId = :appId") + void deleteByAppIdBulk(@Param("appId") Integer appId); + + /** + * 按 (组ID + 应用ID) 物理删除单条开通记录(增量取消开通用)。 + * 用于 SetSchoolApp 的「删除」模式:取消某个组下某个应用的开通。 + */ + @Modifying + @Query("DELETE FROM SysGroupAppRel r WHERE r.groupId = :groupId AND r.appId = :appId") + void deleteByGroupIdAndAppIdBulk(@Param("groupId") Integer groupId, @Param("appId") Integer appId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java new file mode 100644 index 0000000..c63ad22 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupModulePermRepository.java @@ -0,0 +1,60 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroupModulePerm; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组模块权限仓储。 + */ +public interface SysGroupModulePermRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByGroupId(Integer groupId); + + /** + * 增量 upsert 用:查询某组对某模块是否已有权限记录。 + */ + SysGroupModulePerm findByGroupIdAndModuleId(Integer groupId, Integer moduleId); + + /** + * 权限判定用:查询目标模块在指定组ID集合(祖先链)上的全部权限记录。 + */ + List findByModuleIdAndGroupIdIn(Integer moduleId, List groupIds); + + /** + * 按组ID批量物理删除权限记录(删除组时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId") + void deleteByGroupIdBulk(@Param("groupId") Integer groupId); + + /** + * 权限一览用:批量查询一组模块在指定组集合上的权限记录。 + */ + List findByModuleIdInAndGroupIdIn(List moduleIds, List groupIds); + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.moduleId IN :moduleIds") + void deleteByModuleIdInBulk(@Param("moduleIds") List moduleIds); + + /** + * 删除某组在指定模块集合上的权限记录(组取消开通应用时清理该应用下模块的残留权限)。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId AND p.moduleId IN :moduleIds") + void deleteByGroupIdAndModuleIdInBulk(@Param("groupId") Integer groupId, @Param("moduleIds") List moduleIds); + + /** + * 按 (组ID + 模块ID) 物理删除单条权限记录(增量取消模块授权用)。 + * 用于 SetGroupModule 的「删除」模式。 + */ + @Modifying + @Query("DELETE FROM SysGroupModulePerm p WHERE p.groupId = :groupId AND p.moduleId = :moduleId") + void deleteByGroupIdAndModuleIdBulk(@Param("groupId") Integer groupId, @Param("moduleId") Integer moduleId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java new file mode 100644 index 0000000..a9c2f02 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupRepository.java @@ -0,0 +1,31 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysGroup; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 系统组仓储 + */ +public interface SysGroupRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT MAX(g.sort) FROM SysGroup g") + Integer findMaxSort(); + + @Query("SELECT MAX(g.sort) FROM SysGroup g WHERE g.parentId = :parentId") + Integer findMaxSortByParentId(Integer parentId); + + List findAllByOrderBySortAsc(); + + List findByNameContainingOrderBySortAsc(String name); + + List findByParentIdOrderBySortAsc(Integer parentId); + + long countByParentId(Integer parentId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java b/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java new file mode 100644 index 0000000..7bcf205 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysOapiLogRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysOapiLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDateTime; + +/** + * 开放接口调用日志仓储。 + * + * @author chenx + * @date 2026/8/20 + */ +public interface SysOapiLogRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按日志级别 + 创建时间批量删除(单条 DELETE 语句,不先加载实体)。 + * 用于定时清理:一般日志保留 12 个月,重要/次要日志保留 36 个月。 + * + * @param logLevel 日志级别编码 + * @param createTime 截止时间(早于该时间的记录被删除) + * @return 删除条数 + */ + long deleteByLogLevelAndCreateTimeBefore(Integer logLevel, LocalDateTime createTime); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java b/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java new file mode 100644 index 0000000..d8faaac --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysOperationLogRepository.java @@ -0,0 +1,27 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysOperationLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDateTime; + +/** + * 系统操作日志仓储。 + * + * @author chenx + * @date 2026/7/28 + */ +public interface SysOperationLogRepository extends JpaRepository, + JpaSpecificationExecutor { + + /** + * 按日志级别 + 创建时间批量删除(单条 DELETE 语句,不先加载实体)。 + * 用于定时清理:一般日志保留 6 个月,重要/次要日志保留 12 个月。 + * + * @param logLevel 日志级别编码 + * @param createTime 截止时间(早于该时间的记录被删除) + * @return 删除条数 + */ + long deleteByLogLevelAndCreateTimeBefore(Integer logLevel, LocalDateTime createTime); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java new file mode 100644 index 0000000..82b7231 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserAppRoleRelRepository.java @@ -0,0 +1,51 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserAppRoleRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 用户-业务角色关系仓储。 + */ +public interface SysUserAppRoleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByUserId(Integer userId); + + List findByRoleId(Integer roleId); + + /** + * 按角色ID批量物理删除绑定记录(bulk delete 直接发 SQL)。 + * 用于「角色绑定用户」全量覆盖式保存的先删后插,以及删除角色时级联清理。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId") + void deleteByRoleIdBulk(@Param("roleId") Integer roleId); + + /** + * 按角色ID集合批量物理删除绑定记录(删除应用时级联清理该应用下角色)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId IN :roleIds") + void deleteByRoleIdInBulk(@Param("roleIds") List roleIds); + + /** + * 按用户ID批量物理删除绑定记录(「用户绑定角色」全量覆盖保存 + 删除用户时级联清理)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.userId = :userId") + void deleteByUserIdBulk(@Param("userId") Integer userId); + + /** + * 按角色ID + 用户ID 删除单条绑定记录(移除单个用户的角色关联,避免全量重提)。 + */ + @Modifying + @Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId AND r.userId = :userId") + void deleteByRoleIdAndUserId(@Param("roleId") Integer roleId, @Param("userId") Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java new file mode 100644 index 0000000..bd79400 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserCurrentRoleRepository.java @@ -0,0 +1,17 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserCurrentRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Optional; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户当前激活身份仓储(一对一:user_id 唯一) + */ +public interface SysUserCurrentRoleRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByUserId(Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java new file mode 100644 index 0000000..0a53aff --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java @@ -0,0 +1,34 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserGroupRel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/7/20 16:00 + * @description 组用户关联仓储(用于删除组/用户时校验关联数据) + */ +public interface SysUserGroupRelRepository extends JpaRepository, JpaSpecificationExecutor { + + long countByGroupId(Integer groupId); + + long countByUserId(Integer userId); + + List findByUserId(Integer userId); + + List findByUserIdIn(List userIds); + + List findByUserIdInAndGroupId(List userIds, Integer groupId); + + Optional findByUserIdAndGroupId(Integer userId, Integer groupId); + + List findByGroupId(Integer groupId); + + void deleteByUserId(Integer userId); + + void deleteByUserIdAndGroupId(Integer userId, Integer groupId); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java new file mode 100644 index 0000000..015d584 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java @@ -0,0 +1,86 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/20 16:14 + * @description 系统用户仓储 + */ +public interface SysUserRepository extends JpaRepository, JpaSpecificationExecutor { + + long countByAccount(String account); + + long countByAccountAndIdNot(String account, Integer id); + + /** 按登录账号(username)查用户,供用户中心组装 BoaUser。 */ + SysUser findByAccount(String account); + + /** 按手机号查用户,供用户中心组装 BoaUser。 */ + SysUser findByPhone(String phone); + + /** + * 组上下文列表专用分页(原生 SQL)。 + * INNER JOIN 关联表后按 user_id 聚合(GROUP BY u.id),以「组内最小 sort_no」为排序键 + * (NULLS LAST + sort_no 升序 + user_id 兜底升序)。GROUP BY 天然去重,规避 MySQL 的 + * DISTINCT+ORDER BY 限制与 Hibernate 不支持 ORDER BY 标量子查询的限制。 + * 入参 name / disabled 为可空:为空(NULL)时该过滤条件整体跳过。 + */ + @Query(value = + "SELECT u.id AS userId, MIN(r.sort_no) AS minSortNo " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled) " + + "GROUP BY u.id " + + "ORDER BY CASE WHEN MIN(r.sort_no) IS NULL THEN 1 ELSE 0 END, MIN(r.sort_no) ASC, u.id ASC", + countQuery = + "SELECT COUNT(DISTINCT u.id) " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled)", + nativeQuery = true) + Page findGroupUserPage(@Param("groupIds") List groupIds, + @Param("name") String name, + @Param("disabled") Integer disabled, + Pageable pageable); + + /** + * 组上下文导出专用(原生 SQL,非分页):INNER JOIN 关联表后按 user_id 聚合, + * 仅返回「属于指定组范围」的用户(无组归属的用户天然被排除);超管(type=1)豁免 + * (type 为 NULL 视为非超管,一并保留)。入参 name 为可空:为空时跳过关键字过滤。 + * 排序按 user_id 升序,保证导出结果稳定。 + */ + @Query(value = + "SELECT u.id " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.username LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (u.type IS NULL OR u.type != 1) " + + "GROUP BY u.id " + + "ORDER BY u.id ASC", + nativeQuery = true) + List findGroupUserIds(@Param("groupIds") List groupIds, + @Param("name") String name); +} diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java new file mode 100644 index 0000000..44b7af7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java @@ -0,0 +1,39 @@ +package com.yunhai.userbase.repository; + +import com.yunhai.userbase.entity.SysUserRoleRel; +import com.yunhai.userbase.enums.StatusEnum; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * @author chenx + * @date 2026/8/18 + * @description 用户-SysRoleEnum角色-学校 关联仓储(平台身份角色权威源) + */ +public interface SysUserRoleRelRepository extends JpaRepository, JpaSpecificationExecutor { + + /** 用户全部可选身份(含禁用态由调用方过滤) */ + List findByUserId(Integer userId); + + /** 用户某状态下的全部可选身份 */ + List findByUserIdAndStatus(Integer userId, StatusEnum status); + + /** 用户主身份(isMain=1),最多 1 条 */ + Optional findByUserIdAndIsMain(Integer userId, Integer isMain); + + /** 用户在某学校下、某状态下的全部角色关联 */ + List findByUserIdAndSchoolIdAndStatus(Integer userId, Integer schoolId, StatusEnum status); + + /** 用户所有正常态的 roleId 去重列表(用于 BoaUser.roleIds) */ + @Query("SELECT DISTINCT r.roleId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status") + List findDistinctRoleIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status); + + /** 用户所有正常态的 schoolId 去重列表(用于 BoaUser.schools,schoolId>0 的过滤在调用方做) */ + @Query("SELECT DISTINCT r.schoolId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status") + List findDistinctSchoolIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status); +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java b/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java new file mode 100644 index 0000000..13772b4 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppCategoryService.java @@ -0,0 +1,52 @@ +package com.yunhai.userbase.service; + +import com.yunhai.platform.utils.enums.ClientEnum; +import com.yunhai.platform.utils.model.BoaAppType; +import com.yunhai.userbase.entity.AppCategory; +import com.yunhai.userbase.model.appauthz.AppAuthzCategoryVo; +import com.yunhai.userbase.model.appcategory.AppCategoryQuery; +import com.yunhai.userbase.model.appcategory.AppCategoryRequest; +import com.yunhai.userbase.model.appcategory.AppCategoryVo; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/15 21:30 + * @description + */ +public interface IAppCategoryService { + + void add(AppCategoryRequest request); + + void edit(Integer id, AppCategoryRequest request); + + void delete(Integer id); + + List list(AppCategoryQuery query); + + void sort(List ids); + + /** + * 应用授权用:获取所有应用类型及其类型下的应用(id/name/icon)。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无应用的类型 app 为空数组。 + * + * @return 应用类型 + 应用列表的授权视图 + */ + List listWithApps(); + + /** + * 开放接口专用:按用户 + 客户端类型查询其有权访问的应用列表(按应用类型分组)。 + * 规则: + * ① 权限过滤:仅返回该用户有权访问的应用(复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}); + * ② 客户端过滤:仅返回支持该客户端类型的应用({@code app.clients} 位掩码与 clientType 有交集;clients 为空视为不限端); + * ③ 不透出 appSecret。 + * 类型按 sortNo 升序,类型内应用按 sortNo、id 升序;无有权应用的类型 apps 为空数组。 + * + * @param userId 用户ID + * @param clientType 客户端类型(null=不按端过滤) + * @return 应用类型 + 有权应用列表 + */ + List listBoaAppType(Integer userId, ClientEnum clientType); + +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppModuleService.java b/src/main/java/com/yunhai/userbase/service/IAppModuleService.java new file mode 100644 index 0000000..aa0d1b7 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppModuleService.java @@ -0,0 +1,23 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.model.appmodule.AppModuleQuery; +import com.yunhai.userbase.model.appmodule.AppModuleRequest; +import com.yunhai.userbase.model.appmodule.AppModuleVo; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/22 + * @description 应用模块服务。 + */ +public interface IAppModuleService { + + void add(AppModuleRequest request); + + void edit(Integer id, AppModuleRequest request); + + void delete(Integer id); + + List list(AppModuleQuery query); +} diff --git a/src/main/java/com/yunhai/userbase/service/IAppRoleService.java b/src/main/java/com/yunhai/userbase/service/IAppRoleService.java new file mode 100644 index 0000000..681240c --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IAppRoleService.java @@ -0,0 +1,105 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; +import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; +import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; +import com.yunhai.userbase.model.approle.AppRoleUserVo; +import com.yunhai.userbase.model.approle.SysAppRoleQuery; +import com.yunhai.userbase.model.approle.SysAppRoleRequest; +import com.yunhai.userbase.model.approle.SysAppRoleVo; +import com.yunhai.userbase.model.approle.UserAppRoleBindRequest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/27 + * @description 应用业务角色服务(角色 CRUD + 角色模块授权 + 用户角色绑定)。 + */ +public interface IAppRoleService { + + /** + * 分页查询角色列表(name 模糊、appId/allAppFlag/disabled 精确)。 + * 普通角色回填所属应用名称 appName。 + * + * @param query 查询条件 + * @param pageable 分页参数 + * @return 分页角色列表 + */ + Page list(SysAppRoleQuery query, Pageable pageable); + + /** + * 新增角色。 + * 规则:普通角色 allAppFlag=false 且 appId 必填(应用须存在),同应用下名称唯一; + * 全应用角色 allAppFlag=true 且 appId 必须为空,全应用角色之间名称唯一。 + * + * @param request 角色入参 + */ + void add(SysAppRoleRequest request); + + /** + * 编辑角色(可改 name/remark/disabled)。 + * 角色类型 allAppFlag 与所属应用 appId 不允许修改(修改会导致已授权模块越界),需删除后重建。 + * 重名校验规则与新增一致(排除自身)。 + * + * @param id 角色ID + * @param request 角色入参 + */ + void edit(Integer id, SysAppRoleRequest request); + + /** + * 删除角色,并级联清理其模块授权记录与用户绑定记录(逻辑外键,无物理 FK)。 + * + * @param id 角色ID + */ + void delete(Integer id); + + /** + * 角色模块授权页回显:返回该角色可授权的应用及其模块树,已授权模块 checked=true。 + * 普通角色仅返回其所属应用;全应用角色返回系统全部应用(可跨应用授权)。 + * + * @param roleId 角色ID + * @return 应用列表(含模块权限树) + */ + List findRoleModuleTree(Integer roleId); + + /** + * 角色模块授权保存:全量覆盖式更新 sys_app_role_module_rel(bulk 先删后插)。 + * 校验:模块必须存在;普通角色只能授权其所属应用下的模块,越权写其他应用的模块会被拒绝。 + * + * @param request 角色ID + 勾选的模块ID全量列表 + */ + void saveRoleModules(AppRoleModuleAssignRequest request); + + /** + * 查询角色已绑定的用户列表。 + * + * @param roleId 角色ID + * @return 用户列表 + */ + List findRoleUsers(Integer roleId); + + /** + * 角色绑定用户保存(角色维度,全量覆盖):bulk 先删该角色全部绑定再批量插入。 + * + * @param request 角色ID + 用户ID全量列表 + */ + void saveRoleUsers(AppRoleUserBindRequest request); + + /** + * 用户绑定角色保存(用户维度,全量覆盖):bulk 先删该用户全部绑定再批量插入。 + * + * @param request 用户ID + 角色ID全量列表 + */ + void saveUserRoles(UserAppRoleBindRequest request); + + /** + * 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。 + * + * @param roleId 角色ID + * @param userId 用户ID + */ + void removeRoleUser(Integer roleId, Integer userId); +} diff --git a/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java new file mode 100644 index 0000000..08d31c0 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java @@ -0,0 +1,104 @@ +package com.yunhai.userbase.service; + +import com.yunhai.userbase.entity.ApplicationInfo; +import com.yunhai.userbase.model.application.ApplicationInfoQuery; +import com.yunhai.userbase.model.application.ApplicationInfoRequest; +import com.yunhai.userbase.model.application.ApplicationInfoSortVo; +import com.yunhai.userbase.model.application.ApplicationInfoVo; +import com.yunhai.userbase.model.application.PushSettingRequest; +import com.yunhai.userbase.model.application.PushSettingVo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * 应用信息业务接口(缓存由 Spring Cache 注解统一管控)。 + */ +public interface IApplicationInfoService { + + /** + * 根据 id 查询应用信息(带缓存,防击穿)。 + */ + ApplicationInfo getById(Integer id); + + /** + * 保存/更新应用信息(写后同步更新缓存)。 + */ + ApplicationInfo save(ApplicationInfo info); + + /** + * 根据 id 删除应用信息(同步清除缓存)。 + */ + void deleteById(Integer id); + + /** + * 新增应用信息。 + * 由入参构造实体并落库;appKey 为整型,从 100000 起按数据库最大值自增,appSecret 为 32 位 UUID。 + * + * @param request 应用新增入参 + */ + void add(ApplicationInfoRequest request); + + /** + * 编辑应用信息。 + * 按 id 查找后拷贝入参字段并保存,appKey/appSecret 不随编辑变更。 + * + * @param id 应用ID + * @param request 应用编辑入参 + */ + void edit(Integer id, ApplicationInfoRequest request); + + /** + * 根据 id 删除应用信息。 + * + * @param id 应用ID + */ + void delete(Integer id); + + /** + * 重设应用密钥(appSecret)。 + * 按 id 查找应用并生成新的 32 位 UUID 密钥后保存。 + * + * @param id 应用ID + */ + void resetAppSecret(Integer id); + + /** + * 根据条件分页查询应用列表。 + * + * @param query 查询条件(name 关键词按名称模糊或 appKey 精确匹配,categoryId 按类别精确匹配) + * @param pageable 分页参数(page 从 0 起,size 每页条数) + * @return 分页后的应用视图(含总条数 totalElements) + */ + Page list(ApplicationInfoQuery query, Pageable pageable); + + List findListByCategoryId(Integer categoryId); + + /** + * 同一应用类型内排序。 + * 入参为该类型下应用的完整有序 id 列表,按列表顺序写入 sortNo(1 起)。 + * 接口会额外校验:所有 id 必须存在且同属一个应用类型,避免跨类型串号导致排序错乱。 + * + * @param ids 同一应用类型下的应用ID有序列表 + */ + void sort(List ids); + + /** + * 保存应用推送设置(外部接口地址 / 总开关 / 启用的接口 / 学校范围 / 角色范围 / 推送密钥)。 + * 仅更新推送相关字段,写后同步更新 appInfo 缓存。 + * + * @param id 应用ID + * @param request 推送设置(各字段可选,null 表示不修改) + */ + ApplicationInfo savePushSetting(Integer id, PushSettingRequest request); + + /** + * 获取应用推送设置回显 VO。 + * + * @param id 应用ID + * @return 推送设置 VO(仅含推送相关字段) + */ + PushSettingVo getPushSetting(Integer id); +} diff --git a/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java new file mode 100644 index 0000000..95ef9c6 --- /dev/null +++ b/src/main/java/com/yunhai/userbase/service/IGroupAuthzService.java @@ -0,0 +1,190 @@ +package com.yunhai.userbase.service; + +import com.yunhai.platform.utils.model.BoaModule; +import com.yunhai.userbase.model.groupauthz.*; + +import java.util.List; + +/** + * @author chenx + * @date 2026/7/23 + * @description 组应用权限服务(应用开通 + 组授权 + 权限判定)。 + */ +public interface IGroupAuthzService { + + /** + * 功能1回显:查询系统全量应用列表,并标记该组已开通的应用(checked)。 + * + * @param groupId 组ID + * @return 全量应用列表(含开通勾选状态) + */ + List listGroupApps(Integer groupId); + + /** + * 功能1保存:全量覆盖式更新组的应用开通记录(先删旧记录再批量插入)。 + * 被取消开通的应用,同步清理该组在其模块上的权限记录,避免残留脏数据。 + * + * @param request 组ID + 勾选的应用ID全量列表 + */ + void assignAppsToGroup(GroupAppAssignRequest request); + + /** + * 功能2数据加载:查询该组已开通的应用及其模块树,并回显勾选状态与子组继承开关。 + * 仅返回 sys_group_app_rel 中有记录的应用;未开通的应用不显示。 + * + * @param groupId 组ID + * @return 已开通应用列表(含模块权限树) + */ + List findGroupAppsWithModules(Integer groupId); + + /** + * 功能2数据加载(按应用分类分组版):与 {@link #findGroupAppsWithModules(Integer)} 数据一致, + * 但按应用类型分类聚合,便于组授权页面按分类面板展示。 + * 返回全部分类(含空数组),保证前端分类筛选下拉完整。 + * + * @param groupId 组ID + * @return 按应用分类分组的已开通应用列表 + */ + List findGroupAppsWithModulesByCategory(Integer groupId); + + /** + * 功能2保存:全量覆盖式更新组的模块权限。 + * 有旧记录则更新 inheritFlag,无则新增,不在传入列表中的旧记录物理删除。 + * + * @param request 组ID + 勾选的模块权限全量列表(moduleId + inheritFlag) + */ + void saveModulePermissions(GroupModulePermRequest request); + + /** + * 权限判定(供登录/拦截器使用):判断用户对某模块是否有权限。 + * 规则:用户任一所属组,沿祖先链回溯——自身显式勾选即通过;祖先勾选且 inheritFlag=true 即通过。 + * 前置校验:该所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 该组已开通的应用)。 + * 角色授权并集:用户经「启用状态」业务角色获得该模块授权也通过,且不要求组已开通模块所属应用;禁用角色不参与判定。 + * + * @param userId 用户ID + * @param moduleId 模块ID + * @return true=有权限 + */ + boolean hasModulePermission(Integer userId, Integer moduleId); + + /** + * 权限一览:查询指定组下的直接用户列表(不含子组,不分页)。 + * + * @param groupId 组ID + * @return 用户列表 + */ + List findGroupUsers(Integer groupId); + + /** + * 权限一览:全局搜索用户(按账号/姓名/身份证模糊、手机号精确匹配),不分页。 + * 返回用户及其主归属部门/组织路径。 + * + * @param keyword 搜索关键字 + * @return 用户列表 + */ + List searchUsers(String keyword); + + /** + * 权限一览:根据用户ID聚合该用户的全部已授权应用模块树。 + * 只返回有授权模块的应用,并按应用类型分组;无授权的应用类型和应用不显示。 + * 权限判定复用 {@link #hasModulePermission(Integer, Integer)} 的祖先链继承逻辑。 + * 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块; + * 最终应用范围 = 组开通应用 ∪ 角色模块所属应用(角色模块不要求组必须先开通应用)。 + * + * @param groupId 组ID(上下文,可选;目前仅用于校验用户存在性,实际聚合以 userId 为准) + * @param userId 用户ID + * @return 按应用类型分组的已授权应用模块树 + */ + List findUserPermissionTree(Integer groupId, Integer userId); + + /** + * 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。 + * 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。 + * + * @param userId 用户ID + * @return 用户所属角色列表 + */ + List findUserRoles(Integer userId); + + /** + * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。
+ * 可在 Nacos 上配置,例如:
+ * yh: + * sign-key-internal: + * ips: + * - 10.0.0.0/8 + * - 172.16.0.0/12 + * - 192.168.0.0/16 + * - 127.0.0.1/32 + * - 172.20.10.5/32 # 某台固定机器 + *
区别于 {@link SysUserAppRoleRel}(业务权限角色):本表表达"用户在哪个学校下激活哪个平台身份角色" + * (如"刘文娟在 A 学校是老师"),角色取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key。
不继承 BaseEntity:高频读写关系表,自定义字段更轻量,与 {@link SysUserGroupRel} 风格一致。