Compare commits

37 Commits
Author SHA1 Message Date
ops 22ca6e0836 fix(ci): 构建容器名唯一化 + npm 走国内镜像源(避免并发互踩 exit 137) 2026-10-21 06:28:48 +00:00
ops 14b65e2a4a ci: apk 源统一走 aliyun(规避 dl-cdn 时段性被拦卡 60s) 2026-10-21 01:36:47 +00:00
admin 0f16e370ea ci: deploy with correct Nacos env (SPRING_CLOUD_NACOS_CONFIG_NAMESPACE + top-level username/password); NACOS_NAMESPACE env name is not resolved by SCA 2.2.4 bootstrap, causing empty tenant + 403 (server-url-prefix fell back to bundled, /cas missing) 2026-08-10 12:34:00 +08:00
admin fd031fa6b0 fix: remove CasSsoConfiguration (conflicts with yh-auth CasSsoAutoConfiguration bean casSingleSignOutFilter; deleted locally in unpushed commits, aligning Gitea main) 2026-08-10 12:14:43 +08:00
admin 20383f4d2c ci: docker rm -f mavenbuild before docker run to clear leftover container from failed runs (fixes name conflict) 2026-08-10 12:06:47 +08:00
admin 1b021e2748 ci: use settings.xml profile (not mirror) to add nexus:8081 maven-public as repo; clear only yh-auth cache (keep BOM); verified resolves yh-auth -7 with CasSsoAutoConfiguration 2026-08-10 12:04:57 +08:00
admin e69e634138 ci: mavenbuild join infra-infra-net + mirror HTTP repo to nexus:8081 maven-public (bypass unreachable Caddy path); clear stale yh.platform cache at correct path /root/.m2/repository/ — fixes serverName crash from 08-05 stale yh-auth 2026-08-10 11:56:47 +08:00
admin acea548670 ci: clear cached yh.platform SNAPSHOT in maven container before build so re-deployed yh-auth (-7, with CasSsoAutoConfiguration) is pulled; fixes serverName crash from stale snapshot 2026-08-10 11:40:31 +08:00
admin 8164e84b46 ci: add -U to maven (force refresh re-deployed yh-auth SNAPSHOT); deploy with NACOS env (prod ns); tag :latest on success for correct rollback anchor 2026-08-10 11:21:17 +08:00
admin b083aa2cb2 revert run-app.sh to clean (yh-auth re-deployed to Nexus now provides yh.sso->serverName mapping; no hardcoded -D needed) 2026-08-10 11:18:53 +08:00
admin 6eea4ddfb8 diag: inspect running container env + embedded run-app.sh (-D patch?) 2026-08-10 10:49:18 +08:00
admin 391a837b80 ci: add read-only diag workflow for userbase-service (160 docker.sock) 2026-08-10 10:37:38 +08:00
admin acf41f6564 ci: add deploy stage (pull+rollout+rollback) on 160 act_runner; add deploy dispatch input 2026-08-10 10:10:32 +08:00
admin ab1f34d56b fix: inject yh.sso CAS properties via -D to defeat yh-auth relaxed-binding null (serverName or service must be set) 2026-08-10 10:10:30 +08:00
admin 655385630b fix: drop duplicate docker.sock volume (act_runner mounts it by default) 2026-08-10 09:35:32 +08:00
admin e21bd27813 fix: drop duplicate docker.sock volume (act_runner mounts it by default) 2026-08-10 09:29:20 +08:00
admin 82ccbda9c8 fix: drop duplicate docker.sock volume (act_runner mounts it by default) 2026-08-10 09:22:11 +08:00
admin 176592fbff add backend CI workflow 2026-08-10 09:17:41 +08:00
chenxing 3a3b0e7e79 chore(deploy): remove _debug.txt on success; gitignore build output 2026-08-04 23:32:29 +08:00
chenxing 68d9e3520a fix(deploy): deterministic mv landing + robust base64 host diagnostics 2026-08-04 23:26:50 +08:00
chenxing 4249c7f3cd debug(deploy): truncate _debug.txt per run for unambiguous diagnostics 2026-08-04 23:21:42 +08:00
chenxing c2e7b8f12a fix(deploy): correct dist nesting + capture host-readable diagnostics 2026-08-04 23:06:47 +08:00
chenxing dabb053b19 fix(deploy): use Docker archive API to copy dist (no volume-source discovery) 2026-08-04 22:58:33 +08:00
chenxing f0bd70ff6e fix(deploy): discover workspace source via container inspect (not mountinfo) 2026-08-04 22:56:08 +08:00
chenxing 1619e41c20 debug(deploy): write helper diagnostics to host _debug.txt 2026-08-04 22:53:41 +08:00
chenxing 1aef90eff5 fix(ci): quote run value containing colon (YAML parse error) 2026-08-04 22:50:25 +08:00
chenxing aa5cc483ec ci: allow manual workflow_dispatch (manual re-deploy) 2026-08-04 22:49:49 +08:00
chenxing caec1d864c fix(deploy): copy from /src (helper bind) not host workspace path 2026-08-04 22:46:27 +08:00
chenxing 5e368f6c4e ci: deploy front via docker.sock helper container (act_runner ignores custom volumes) 2026-08-04 22:45:44 +08:00
chenxing 31b5d183da ci: declare /data/www/userbase in job container.volumes (options honors only first --volume) 2026-08-04 22:29:54 +08:00
chenxing af9655c548 ci: mount /data/www/userbase via runner options (valid_volumes precise bind) 2026-08-04 22:26:39 +08:00
chenxing fffdd7e4c5 ci: document valid_volumes bind requirement (re-trigger with fixed runner config) 2026-08-04 22:15:47 +08:00
chenxing aa0724dc49 fix(build): drop node-sass (no Node20 prebuilt); CI strips it from lockfile before npm ci 2026-08-04 22:10:13 +08:00
chenxing 6c4d800c26 fix(ci): replace GitHub actions/checkout with internal Gitea clone (no github.com egress) 2026-08-04 21:51:55 +08:00
chenxing a94140fdb8 fix(ci): mount /data/www/userbase explicitly in job container (runner options 2nd volume dropped) 2026-08-04 21:42:12 +08:00
ab e8eaee0e77 ci: add deploy-front workflow (static) + example docker-push workflow 2026-08-04 21:31:34 +08:00
ab e02dcc23da init: userbase front/service/deploy source (CI builds front via Gitea Actions) 2026-08-04 21:24:50 +08:00
436 changed files with 20908 additions and 9406 deletions
-9
View File
@@ -1,9 +0,0 @@
# 开发环境 (dev):仅 vite dev 加载(--mode dev)。值可自由指本地或线上地址。
# build 产物不读此文件(build 默认 production mode,只认 .env / .env.production,两者均不存在),
# 线上环境差异全部由 projman 的 env-config-{目标}.json 承担。
VITE_BASE_HOST=http://base.bestsch.com
VITE_SSO_HOST=http://sso.yunhaixingtong.com
VITE_SERVICE_HOST=http://localhost:8060
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
VITE_IS_MOCK=false
+144
View File
@@ -0,0 +1,144 @@
name: ci-userbase-service
# 后端标准化 CI:maven 构建 jar -> docker build -> 推 Nexus docker-hosted
# 触发:push 到 main(仅 service 或本文件变更)/ 手动 Run workflow
# 依赖仓库 Actions Secrets:NEXUS_USER / NEXUS_PASSWORD(ci-docker 账号,仅 docker-hosted 推送权)
on:
push:
branches: [main]
paths:
- 'service/**'
- '.gitea/workflows/ci.yml'
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: |
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
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 <path> 里的路径会被宿主按【宿主路径】解析;
# 而 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 mirror 把
# pom 的 HTTP 仓库重定向到 http://nexus:8081/repository/maven-public/(已验证 -7 可达)。
# 先清理上次失败残留的同名容器(set -e 下末尾的 rm 不会执行)
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
BUILD_CONTAINER="mavenbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
docker run -d --name "$BUILD_CONTAINER" \
--network infra-infra-net \
-v /opt/docker/ubs-m2:/root/.m2 \
-w /build maven:3.6.3-jdk-8 sleep 3600
docker cp "${PWD}/." "$BUILD_CONTAINER":/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 "$BUILD_CONTAINER" sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
# 仅清旧 yh-auth 缓存(含 08-05 local install 的 maven-metadata-local.xml,避免本地安装优先于远端 -7);
# BOM 等其它 yh.platform 模块保留缓存(Nexus 上它们仍是 08-05 的 -1,缓存与远端一致)。
docker exec "$BUILD_CONTAINER" rm -rf /root/.m2/repository/com/yunhai/platform/yh-auth-spring-boot-starter
docker exec "$BUILD_CONTAINER" sh -c 'cd /build/service && mvn -U -B -DskipTests clean package'
mkdir -p "${PWD}/service/target"
docker cp "$BUILD_CONTAINER":/build/service/target/userbase.jar "${PWD}/service/target/userbase.jar"
docker rm -f "$BUILD_CONTAINER"
- 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 service/docker/Dockerfile service
- 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
# 注意(SCA 2.2.4 坑): 命名空间必须用标准属性名 SPRING_CLOUD_NACOS_CONFIG_NAMESPACE,
# 用 NACOS_NAMESPACE 这个 env 名不会被 bootstrap 的 ${NACOS_NAMESPACE:dev} 解析到(会落 public);
# 凭据必须放顶层 SPRING_CLOUD_NACOS_USERNAME/PASSWORD, 放在 spring.cloud.nacos.config.* 下不生效(403)。
docker run -d --name userbase-service --restart unless-stopped --network infra-infra-net -p 8060:8060 \
-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 \
"$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
docker run -d --name userbase-service --restart unless-stopped --network infra-infra-net -p 8060:8060 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
+55
View File
@@ -0,0 +1,55 @@
name: deploy-userbase-front
on:
push:
branches: [main]
paths:
- 'front/**'
- '.gitea/workflows/deploy-front.yml'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
# node:20 自带 npm 与 git;通过已配置的 registry-mirrors 拉取
# /data/www/userbase 与 docker.sock 由 act_runner 全局 options 统一挂载(见 act-runner config)
container:
image: node:20
# 注意:本环境 act_runner v0.6.1 既不生效 container.options 的额外 --volume,
# 也不生效 workflow 的 container.volumes(自定义卷被静默忽略),
# 但它会默认把宿主 docker.sock 挂入每个 job 容器。
# 因此部署改用 scripts/deploy-via-docker.js:通过该 docker.sock 启动一次性
# helper 容器,把构建产物拷到宿主 /data/www/userbase/front。
steps:
# 内网 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: Use npm mirror (npmmirror)
working-directory: front
run: npm config set registry https://registry.npmmirror.com
# node-sass(6.x) 无 Node20 预编译二进制,源码编译必失败;项目实际用 dart-sass(sass)。
# 本地 lockfile 暂不便修改,这里在 runner 内把它从 lockfile 剔除后再 npm ci(不改仓库文件)。
- name: Strip node-sass from lockfile (CI-only)
run: |
node -e "const fs=require('fs');const p='front/package-lock.json';const d=JSON.parse(fs.readFileSync(p,'utf8'));const pk=d.packages||{};for(const k of Object.keys(pk)){if(k==='node_modules/node-sass'||k.startsWith('node_modules/node-sass/'))delete pk[k];}if(d.dependencies&&d.dependencies.node-sass)delete d.dependencies.node-sass;fs.writeFileSync(p,JSON.stringify(d,null,2));"
echo "node-sass stripped from lockfile"
- name: Install dependencies
working-directory: front
run: npm ci --no-audit --no-fund
- name: Build (prod -> dist/prod)
working-directory: front
run: npm run build:prod
- name: Deploy to static dir (/data/www/userbase/front)
# act_runner 不挂载自定义卷,改用宿主 docker.sock 启动一次性 helper 容器拷贝产物。
run: node scripts/deploy-via-docker.js
- name: Done
run: 'echo "deploy step finished; inspect host /data/www/userbase/_debug.txt for the full report"'
-99
View File
@@ -1,99 +0,0 @@
name: deploy-userbase-front
# 前端部署(方案 B:不打包镜像,直接部署静态文件 + 软链接版本控制)
# 触发:push 到 main(已启用仓库 Actions,push 自动部署)/ 手动 Run workflow
# 核心:vite build --mode prod -> dist/prod
# -> 拷贝到 /data/www/userbase/releases/<version>/
# -> 原子切换 /data/www/userbase/front 软链接指向新版本
# -> 保留最近 5 个版本,更早自动清理(秒级回滚)
# 不再构建/推送 nginx 镜像;Caddy 已改为 file_server(root=/srv/userbase=/data/www/userbase)。
# 依赖:160 宿主的 docker daemon(runner 已挂 docker.sock),且 runner valid_volumes 已放行 /data/www/**。无需 NEXUS secret。
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
# docker:24 自带 docker CLI,复用宿主 docker daemon(act_runner v0.6.1 已挂载 docker.sock)。
# 切勿再声明 volumes: docker.sock(会报 Duplicate mount point)。
container:
image: docker:24
steps:
- name: Install git (alpine)
run: |
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
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
# 构建 + 部署:在 node:20 容器里完成(源码经 docker cp 传入,并挂载宿主 /data/www/userbase)
- name: Build & Deploy (symlink versioning)
run: |
set -e
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
BUILD_CONTAINER="frontbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
# 版本号:short sha + 时间戳(可改为 git tag 优先,见下)
VERSION="$(echo ${GITHUB_SHA} | cut -c1-7)-$(date +%Y%m%d-%H%M)"
echo "Deploying version: $VERSION"
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
docker run -d --name "$BUILD_CONTAINER" \
-e VERSION="$VERSION" \
-v /data/www/userbase:/data/www/userbase \
-w /build node:20 sleep 3600
# 源码拷入构建容器(绕过 act 工作区非宿主路径的坑,与 service 的 mavenbuild 一致)
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
docker exec "$BUILD_CONTAINER" sh -c '
set -e
cd /build
# 去掉 Windows 专用 sass 二进制路径(Linux 容器内该路径无效,否则 build 报错)
# .npmrc 被 gitignore、CI 克隆下来没有它,故仅在存在时才删(缺失不报错)
if [ -f .npmrc ]; then sed -i "/SASS_BINARY_PATH/d" .npmrc; fi
# 国内镜像源:内网直连 registry.npmjs.org 跨境极慢(实测 npm install 12 分 46 秒 / 754 包)
npm config set registry https://registry.npmmirror.com
npm ci
npm run build:prod
'
# 部署:首次把真实目录 front 迁入 releases/initial 并建软链;之后均为软链替换
docker exec "$BUILD_CONTAINER" sh -c '
set -e
cd /build
# ---- 首次初始化:front 当前是真实目录,无法直接 ln -sfn 覆盖 ----
if [ ! -L /data/www/userbase/front ]; then
mkdir -p /data/www/userbase/releases
mv /data/www/userbase/front /data/www/userbase/releases/initial
# 相对软链:Caddy 容器内 /data/www/userbase 挂在 /srv/userbase,
# 绝对路径在容器内解析不到,必须用相对路径(相对 front 所在目录)
ln -sfn releases/initial /data/www/userbase/front
echo "INIT: front -> releases/initial (保留上线前状态用于回滚)"
fi
mkdir -p /data/www/userbase/releases/$VERSION
cp -r /build/dist/prod/. /data/www/userbase/releases/$VERSION/
# 原子切换:front 此时已是软链接,ln -sfn 直接替换指向(必须用相对路径)
ln -sfn releases/$VERSION /data/www/userbase/front
echo "current symlink ->"; ls -la /data/www/userbase/front
# 清理旧版本,保留最近 5 个(initial 也会被计入,最多保留 5 个含 initial)
cd /data/www/userbase/releases
ls -1t | tail -n +6 | while read d; do echo "rm old: $d"; rm -rf "$d"; done
echo "remaining versions:"; ls -1t
'
docker rm -f "$BUILD_CONTAINER"
echo "DEPLOY OK: $VERSION"
+27
View File
@@ -0,0 +1,27 @@
name: diag-userbase
on:
workflow_dispatch:
inputs:
dummy:
description: 'noop'
required: false
default: 'x'
jobs:
diag:
runs-on: ubuntu-latest
container:
image: docker:24
steps:
- name: inspect running container config + embedded run-app.sh
run: |
echo "===== container env (NACOS?) ====="
docker inspect userbase-service --format '{{json .Config.Env}}' 2>&1 || echo "NO CONTAINER"
echo
echo "===== -D patch present in RUNNING container's run-app.sh? ====="
docker exec userbase-service grep -E 'Dyh.sso|client-host-url|server-url-prefix' /app/run-app.sh 2>&1 || echo "NO -D patch found in running container"
echo
echo "===== running image id ====="
docker inspect userbase-service --format '{{.Image}}' 2>&1 || true
echo
echo "===== all userbase-service images (id / tag / age) ====="
docker images --filter reference='*/userbase-service*' --format '{{.ID}} {{.Tag}} {{.CreatedSince}}' 2>&1
+47
View File
@@ -0,0 +1,47 @@
name: example-docker-push
# 示例:演示 act_runner 的 docker.sock 能力(构建并推送镜像到 Nexus docker-hosted)
# 需要仓库 Settings -> Actions Secrets 配置:
# NEXUS_USER = admin
# NEXUS_PASSWORD = <nexus admin password>
# 手动触发(Actions 页面 -> Run workflow),不会自动跑。
on:
workflow_dispatch:
inputs:
tag:
description: 'image tag to push'
required: false
default: 'demo'
jobs:
docker-build-push:
runs-on: ubuntu-latest
# docker:24 自带 docker CLI,复用宿主 docker daemon
# docker.sock 显式挂入 job 容器(act_runner 会校验 valid_volumes 白名单)
container:
image: docker:24
volumes:
- /var/run/docker.sock:/var/run/docker.sock
steps:
# docker:24 基于 alpine,默认无 git;先装 git 再手动 clone 本仓库
- name: Install git (alpine)
run: |
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
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: Login to Nexus docker registry
run: echo "${{ secrets.NEXUS_PASSWORD }}" | docker login nexus:8083 -u ${{ secrets.NEXUS_USER }}
- name: Build image (front nginx)
run: docker build -t nexus:8083/docker-hosted/userbase-front:${{ inputs.tag }} -f front/docker/Dockerfile front
- name: Push image
run: docker push nexus:8083/docker-hosted/userbase-front:${{ inputs.tag }}
+26 -24
View File
@@ -1,30 +1,32 @@
# WorkBuddy local state # ---- dependencies ----
node_modules/
# ---- build output (CI builds, do not commit) ----
front/dist/
front/dist_ra/
# ---- java build ----
service/target/
# ---- local tooling / IDE ----
.local-context/
.workbuddy/ .workbuddy/
# Logs # ---- env secrets (keep *.example) ----
logs .env
!.env.example
# ---- logs ----
*.log *.log
npm-debug.log* deploy/*_run*.log
yarn-debug.log* deploy/verify_run*.log
yarn-error.log* deploy/push_run.log
pnpm-debug.log*
lerna-debug.log*
node_modules # ---- OS ----
dist Thumbs.db
dist-ssr
*.local
yarn.lock
.npmrc
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store .DS_Store
*.suo
*.ntvs* # ---- IDE ----
*.njsproj .idea/
*.sln # ---- vite temp cache ----
*.sw?
*.timestamp-*.mjs *.timestamp-*.mjs
-26
View File
@@ -1,26 +0,0 @@
// 纠正当前 api:gen 产物中命名空间的大小写错误
// 映射依据:后端 service 下真实的 *Controller 类名 -> PascalCase 命名空间
const fs = require('fs');
const map = {
Approle: 'AppRole',
Appsettings: 'AppSettings',
Groupauthz: 'GroupAuthz',
Pushsubscription: 'PushSubscription',
Signkeyinternal: 'SignKeyInternal',
Sysgroup: 'SysGroup',
Sysuser: 'SysUser',
};
const targets = ['src/api/apiDefinitions.ts', 'src/api/globals.d.ts'];
for (const f of targets) {
let s = fs.readFileSync(f, 'utf8');
for (const [wrong, correct] of Object.entries(map)) {
// 带引号 key:'Sysgroup. -> 'SysGroup.
s = s.split(`'${wrong}.`).join(`'${correct}.`);
// 裸引用(globals.d.ts 里的 interface/属性声明):Sysgroup: -> SysGroup: / Sysgroup. -> SysGroup.
s = s.split(`${wrong}:`).join(`${correct}:`);
s = s.split(`${wrong}.`).join(`${correct}.`);
}
fs.writeFileSync(f, s);
console.log('fixed', f);
}
console.log('done');
-65
View File
@@ -1,65 +0,0 @@
import type { GeneratorConfig } from "@alova/wormhole";
export default {
generator: [
{
input: "http://localhost:8060/v3/api-docs",
// 文档平台类型
platform: "swagger",
// 生成代码的输出目录
output: "src/api",
// 响应数据的媒体类型
responseMediaType: "application/json",
// 请求体数据的媒体类型
bodyMediaType: "application/json",
// 生成的 API 版本
version: 1,
// 生成代码类型
type: "typescript",
// 全局 API 名称
global: "Apis",
splitImplementation: true,
implementation: "inline",
// API 处理函数
handleApi: apiDescriptor => {
// 过滤掉已废弃的 API
if (apiDescriptor.deprecated) {
return undefined;
}
// 优先使用后端显式声明的 operationId(稳定、可读、无重名后缀),
// 仅在 operationId 缺失时回退到 summary。切勿用 summary 覆盖 operationId,
// 否则中文/长文本 summary 会变成方法名导致运行时查不到。
const newOpId = apiDescriptor.operationId ?? apiDescriptor.summary?.trim();
const url = apiDescriptor.url.replace(/^\/{platform}\//, "/api/");
// springdoc 默认把 Controller 类名转为 kebab tag(如 base-controller),
// 而业务代码统一用 Pascal 无 Controller 的命名空间(如 Base)调用。
// 这里剥掉 -controller 后缀并转为 PascalCase,使 wormhole 生成的
// API 路径(Base.getUserInfo 等)与 Apis.Base.xxx 调用风格一致。
// 由于转换结果是合法 JS 标识符,wormhole 会原样保留、不做 camelCase 改写。
// springdoc v3 的 tag 有两种形态,都要能正确转为 PascalCase 且无 Controller 后缀:
// - Pascal+Controller:SysGroupController -> SysGroup
// - kebab+controller:sys-group-controller -> SysGroup
// 关键:剥掉尾部 Controller 后,要在 camelCase 边界补空格再逐段首字母大写,
// 否则 SysGroup -> Sysgroup(后面全被 toLowerCase 压成小写)会让运行时查不到。
const toNamespace = (tag: string): string => {
const stripped = tag.replace(/controller$/i, "");
const withSpaces = stripped.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
const parts = withSpaces.split(/[^a-zA-Z0-9]+/).filter(Boolean);
return parts
.map((p: string) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
.join("");
};
if (Array.isArray(apiDescriptor.tags)) {
apiDescriptor.tags = apiDescriptor.tags.map(toNamespace);
}
return {
...apiDescriptor,
operationId: newOpId, // 关键:改掉它
url,
};
},
},
],
} as GeneratorConfig;
+20
View File
@@ -0,0 +1,20 @@
# 本地构建推送 / 远端部署 用的环境变量示例
# 用法:复制为 .env 或直接 export 后执行脚本
# ---- ACR 阿里云镜像仓库 ----
ACR_REGISTRY=registry.cn-hangzhou.aliyuncs.com
ACR_NAMESPACE=damaomi_docker
ACR_REPO=yunhai
ACR_USERNAME=your-acr-username
ACR_PASSWORD=your-acr-password
# ---- 镜像 tag ----
APP_VERSION=userbase-v1.0
FRONT_VERSION=userbase-front-v1.0
# ---- 部署到远端 ----
DEPLOY_HOST=192.168.10.157
DEPLOY_PORT=22
DEPLOY_USER=yunhai
REMOTE_DIR=/opt/docker/userbase
SSHPASS=your-ssh-password
+6
View File
@@ -0,0 +1,6 @@
docker run --rm --network infra-infra-net --entrypoint java registry.cn-hangzhou.aliyuncs.com/damaomi_docker/yunhai:userbase-v1.0 -jar /app/userbase.jar --spring.profiles.active=prod --debug > /tmp/ub_debug.log 2>&1
echo "EXIT=$?"
echo "=== conditions / binding matches ==="
grep -nE "YhAuthAutoConfiguration|CasSsoConfiguration|Failed to bind|Binding to|YhSsoProperties|Exception starting filter|serverUrlPrefix|NoSuchBean" /tmp/ub_debug.log | head -60
echo "=== tail (conditions report) ==="
tail -50 /tmp/ub_debug.log
+1
View File
@@ -0,0 +1 @@
EOF
+17
View File
@@ -0,0 +1,17 @@
set -e
echo "== wait for health (up to 180s) =="
for i in $(seq 1 36); do
st=$(docker inspect -f '{{.State.Health.Status}}' userbase-app 2>/dev/null || echo unknown)
echo " [$i] health=$st"
if [ "$st" = "healthy" ]; then break; fi
sleep 5
done
echo "== container status =="
docker ps --filter name=userbase --format "table {{.Names}}\t{{.Status}}"
echo "== nacos reachability (host) =="
curl -s -m 5 -o /dev/null -w "host nacos http=%{http_code}\n" "http://nacos.yunhaixingtong.com:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant=" || echo "host cannot reach nacos"
echo "== nacos reachability (container) =="
docker exec userbase-app sh -c "curl -s -m 5 -o /dev/null -w 'container nacos http=%{http_code}\n' 'http://nacos.yunhaixingtong.com:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant='" || echo "container cannot reach nacos"
echo "== app logs: nacos / config / errors (tail) =="
docker logs --tail 200 userbase-app 2>&1 | grep -iE "Loading nacos data|userbase.yaml|locate Nacos|NacosConfig|get config from nacos|ERROR|Exception|connect.*refused|UnknownHost|403|401|拒绝|超时|Started " | tail -40 || true
echo "== done =="
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# 本地构建 UserBase 前后端镜像并推送到阿里云 ACR
# 用法: ./build-push.sh [app_tag] [front_tag]
# 前置环境变量: ACR_NAMESPACE, ACR_USERNAME, ACR_PASSWORD
# 前置产物: service/target/userbase.jar、front/dist/prod/
set -euo pipefail
APP_VERSION="${1:-${APP_VERSION:-userbase-v1.0}}"
FRONT_VERSION="${2:-${FRONT_VERSION:-userbase-front-v1.0}}"
REGISTRY="${ACR_REGISTRY:-registry.cn-hangzhou.aliyuncs.com}"
NAMESPACE="${ACR_NAMESPACE:?请设置 ACR_NAMESPACE}"
REPO="${ACR_REPO:-yunhai}"
APP_IMG="$REGISTRY/$NAMESPACE/$REPO:$APP_VERSION"
FRONT_IMG="$REGISTRY/$NAMESPACE/$REPO:$FRONT_VERSION"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
[ -f "$ROOT/service/target/userbase.jar" ] || { echo "缺少 service/target/userbase.jar,请先 mvn package"; exit 1; }
[ -f "$ROOT/front/dist/prod/index.html" ] || { echo "缺少 front/dist/prod,请先 npm run build:prod"; exit 1; }
echo "== login $REGISTRY =="
echo "${ACR_PASSWORD:?请设置 ACR_PASSWORD}" | docker login --username="${ACR_USERNAME:?请设置 ACR_USERNAME}" --password-stdin "$REGISTRY"
echo "== build $APP_IMG =="
docker build -f "$ROOT/service/docker/Dockerfile" -t "$APP_IMG" "$ROOT/service"
echo "== build $FRONT_IMG =="
docker build -f "$ROOT/front/docker/Dockerfile" -t "$FRONT_IMG" "$ROOT/front"
echo "== push =="
docker push "$APP_IMG"
docker push "$FRONT_IMG"
echo "PUSHED $APP_IMG"
echo "PUSHED $FRONT_IMG"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -e
MC=mysql8
echo "MYSQL_CONTAINER=$MC"
docker exec userbase-app cat /app/config/application-prod.yml > /tmp/cfg.yml
PW=$(awk '/username: root/{f=1} f&&/^ *password:/{sub(/^ *password: */,""); gsub(/\r/,""); print; exit}' /tmp/cfg.yml)
echo "PW_LEN=${#PW}"
printf '[client]\nuser=root\npassword=%s\n' "$PW" > /tmp/my.cnf
docker cp /tmp/my.cnf "$MC":/tmp/my.cnf
echo "== check sys_user.type column (via TCP 127.0.0.1) =="
docker exec "$MC" sh -c "mysql --defaults-extra-file=/tmp/my.cnf -h 127.0.0.1 -N -e \"SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='bsch_userbase' AND TABLE_NAME='sys_user' AND COLUMN_NAME='type';\""
echo "QUERY_EXIT=$?"
echo "== also confirm DB reachable & table exists =="
docker exec "$MC" sh -c "mysql --defaults-extra-file=/tmp/my.cnf -h 127.0.0.1 -N -e \"SHOW TABLES FROM bsch_userbase LIKE 'sys_user';\""
docker exec "$MC" rm -f /tmp/my.cnf
rm -f /tmp/my.cnf /tmp/cfg.yml
echo "CLEANED"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# 一键部署:上传编排文件 -> SSH 到服务器拉取镜像并启动
# 用法: ./deploy.sh [app_tag] [front_tag]
# 前置环境变量: DEPLOY_HOST, SSHPASS(可选 ACR_NAMESPACE / DEPLOY_USER / REMOTE_DIR)
# 注:服务器需已 docker login registry.cn-hangzhou.aliyuncs.com
set -euo pipefail
APP_VERSION="${1:-${APP_VERSION:-userbase-v1.0}}"
FRONT_VERSION="${2:-${FRONT_VERSION:-userbase-front-v1.0}}"
HOST="${DEPLOY_HOST:?请设置 DEPLOY_HOST}"
PORT="${DEPLOY_PORT:-22}"
USER="${DEPLOY_USER:-yunhai}"
REMOTE_DIR="${REMOTE_DIR:-/opt/docker/userbase}"
NAMESPACE="${ACR_NAMESPACE:-damaomi_docker}"
REGISTRY="${ACR_REGISTRY:-registry.cn-hangzhou.aliyuncs.com}"
PY="${PYTHON_BIN:-python3}"
cd "$(dirname "$0")"
echo "== 1/2 上传 compose 到 $HOST:$REMOTE_DIR =="
"$PY" upload.py "$HOST" "$PORT" "$USER" \
docker-compose.userbase.yml "$REMOTE_DIR/docker-compose.userbase.yml"
echo "== 2/2 远端拉取镜像并启动 =="
"$PY" remote_run.py "$HOST" "$PORT" "$USER" <<EOF
set -e
mkdir -p "$REMOTE_DIR/upload-data"
cd "$REMOTE_DIR"
cat > .env <<ENV
ACR_REGISTRY=$REGISTRY
ACR_NAMESPACE=$NAMESPACE
APP_VERSION=$APP_VERSION
FRONT_VERSION=$FRONT_VERSION
ENV
docker compose -f docker-compose.userbase.yml pull
docker compose -f docker-compose.userbase.yml up -d
docker image prune -f
docker ps --filter name=userbase --format "table {{.Names}}\t{{.Status}}"
echo DEPLOY_OK
EOF
+23
View File
@@ -0,0 +1,23 @@
set -u
CP=/opt/docker/infra/Caddyfile
echo "===== 1. Caddyfile /userbase blocks (line numbers) ====="
grep -n "userbase" "$CP"
echo
echo "===== 2. Full /userbase route region (lines 40-80) ====="
sed -n '40,80p' "$CP"
echo
echo "===== 3. Caddy mounts in container ====="
docker inspect caddy --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
echo
echo "===== 4. Running caddy admin config: does it contain 'userbase'? ====="
docker exec caddy curl -s localhost:2019/config/ | grep -i "userbase" | head -n 20
echo "--- end grep ---"
echo
echo "===== 5. Caddy container status ====="
docker ps --filter name=caddy --format '{{.Names}} {{.Status}}'
echo
echo "===== 6. Direct test via 127.0.0.1 with Host header ====="
for p in /userbase/ /userbase/index.html /userbase/static/js/ /userbase/api/x; do
code=$(curl -s -o /tmp/ub_body -w "%{http_code}" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1$p")
echo "$p -> $code | $(head -c 80 /tmp/ub_body | tr -d '\n')"
done
+24
View File
@@ -0,0 +1,24 @@
set -u
echo "===== 1. In-container view of /etc/caddy/Caddyfile: userbase count ====="
docker exec caddy grep -c "userbase" /etc/caddy/Caddyfile
echo
echo "===== 2. Container start command ====="
docker inspect caddy --format 'CMD={{.Config.Cmd}} ENTRY={{.Config.Entrypoint}}'
echo
echo "===== 3. Admin API reachable? top-level keys ====="
docker exec caddy curl -s localhost:2019/
echo
echo "===== 4. Running HTTP servers + route matchers in config ====="
docker exec caddy curl -s localhost:2019/config/ > /tmp/running.json
echo "bytes: $(wc -c < /tmp/running.json)"
echo "occurrences:"
for s in "userbase" "Welcome to Base Platform" "handle_path" "reverse_proxy" "www.yunhaixingtong.com"; do
echo " $s : $(grep -o "$s" /tmp/running.json | wc -l)"
done
echo
echo "===== 5. Do a manual POST /load from inside container and show result ====="
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1
echo "exit=$?"
sleep 1
echo "--- after reload, recount ---"
docker exec caddy curl -s localhost:2019/config/ | grep -c "userbase"
+74
View File
@@ -0,0 +1,74 @@
# UserBase 编排:后端(Spring Boot 8060) + 前端(nginx 80)
# 两者都挂在 infra-infra-net,由 caddy 按服务名反代到 www.yunhaixingtong.com/userbase
# 启动:cd /opt/docker/userbase && docker compose -f docker-compose.userbase.yml up -d
services:
userbase-app:
image: ${ACR_REGISTRY:-registry.cn-hangzhou.aliyuncs.com}/${ACR_NAMESPACE:-damaomi_docker}/yunhai:${APP_VERSION:-userbase-v1.0}
container_name: userbase-app
restart: unless-stopped
networks:
- infra-infra-net
# SSO 回调后容器内要做 CAS serviceValidate 回调 sso.yunhaixingtong.com,
# 该虚拟主机由宿主机 caddy 按 vhost 路由,但容器内无 DNS 记录会 UnknownHostException。
# host-gateway 指向宿主机网关(caddy 监听 :80/:443 全接口),无需硬编码 IP;
# 旧版 Docker(<20.10)不支持 host-gateway,可改为宿主机桥接网关 IP,如 172.17.0.1。
extra_hosts:
- "sso.yunhaixingtong.com:host-gateway"
environment:
TZ: Asia/Shanghai
# 配置中心:由 bootstrap.yml 中 spring.cloud.nacos.config 在 bootstrap 阶段自动拉取
# 容器内通过 infra-infra-net 解析内网别名 nacos:8848(宿主机 shell 无法直接解析),
# 公网域名 nacos.yunhaixingtong.com 仅用于控制台管理,生产容器走内网。
NACOS_ADDR: nacos:8848
# Nacos 已开启鉴权,config 客户端需携带账号(与旧 run-app.sh 的 nacos/nacos 一致)
SPRING_CLOUD_NACOS_USERNAME: nacos
SPRING_CLOUD_NACOS_PASSWORD: nacos
SPRING_CLOUD_NACOS_CONFIG_USERNAME: nacos
SPRING_CLOUD_NACOS_CONFIG_PASSWORD: nacos
JAVA_OPTS: "-Xms512m -Xmx1024m"
volumes:
- ./upload-data:/data/upload
expose:
- "8060"
healthcheck:
# 只要能返回 HTTP 状态码就认为进程已起(业务接口普遍需要鉴权,不能用 -f)
test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:8060/userbase/ | grep -qE '^[234]'"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
deploy:
resources:
limits:
cpus: '1.5'
memory: 1536M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
userbase-front:
image: ${ACR_REGISTRY:-registry.cn-hangzhou.aliyuncs.com}/${ACR_NAMESPACE:-damaomi_docker}/yunhai:${FRONT_VERSION:-userbase-front-v1.0}
container_name: userbase-front
restart: unless-stopped
networks:
- infra-infra-net
environment:
TZ: Asia/Shanghai
expose:
- "80"
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
infra-infra-net:
external: true
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
echo "== all containers (name + image) =="
docker ps -a --format '{{.Names}}\t{{.Image}}'
echo "== which containers are on infra-infra-net =="
docker network inspect infra-infra-net --format '{{range .Containers}}{{.Name}} ({{.IPv4Address}}){{"\n"}}{{end}}' 2>/dev/null || echo "no such network"
echo "== resolve mysql8 from userbase-app =="
docker exec userbase-app getent hosts mysql8 2>/dev/null || echo "getent failed"
+15
View File
@@ -0,0 +1,15 @@
set -u
echo "===== A. Reload caddy from inside the container (reads mounted /etc/caddy/Caddyfile) ====="
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
echo "reload exit: $?"
sleep 2
echo
echo "===== B. Verify running config now contains userbase ====="
docker exec caddy curl -s localhost:2019/config/ | grep -c "userbase"
echo "(count above; 0 means still missing)"
echo
echo "===== C. Test routes via 127.0.0.1 with Host header ====="
for p in /userbase/ /userbase/index.html /userbase/static/js/ /userbase/api/x /userbase/login; do
code=$(curl -s -o /tmp/ub_body -w "%{http_code}" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1$p")
echo "$p -> $code | $(head -c 90 /tmp/ub_body | tr -d '\n')"
done
+19
View File
@@ -0,0 +1,19 @@
set -u
echo "===== host file (has userbase) ====="
grep -c "userbase" /opt/docker/infra/Caddyfile
echo "===== container file BEFORE restart (expect 0) ====="
docker exec caddy grep -c "userbase" /etc/caddy/Caddyfile
echo
echo "===== restart caddy to re-bind the edited file ====="
docker restart caddy
echo "restart exit: $?"
sleep 6
echo
echo "===== container file AFTER restart (expect >0) ====="
docker exec caddy grep -c "userbase" /etc/caddy/Caddyfile
echo
echo "===== test routes ====="
for p in /userbase/ /userbase/index.html /userbase/static/js/ /userbase/api/x /userbase/login; do
code=$(curl -s -o /tmp/ub_body -w "%{http_code}" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1$p")
echo "$p -> $code | $(head -c 100 /tmp/ub_body | tr -d '\n')"
done
+33
View File
@@ -0,0 +1,33 @@
set -u
CP=/opt/docker/infra/Caddyfile
echo "===== before: line 70 ====="
sed -n '70p' "$CP"
echo "===== apply wildcard matcher (inode will change; restart re-binds) ====="
sed -i 's#handle_path /userbase/ {#handle_path /userbase* {#' "$CP"
echo "after:"; sed -n '70p' "$CP"
echo
echo "===== restart caddy to re-bind edited file ====="
docker restart caddy
echo "restart exit: $?"
sleep 6
echo "===== container sees fix? ====="
docker exec caddy grep -n "handle_path /userbase" /etc/caddy/Caddyfile
echo
echo "===== extract a real JS asset from SPA and test it ====="
html=$(curl -s -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/)
echo "$html" | grep -oE 'src="[^"]+\.js"' | head -3
asset=$(echo "$html" | grep -oE '/[A-Za-z0-9_./-]+\.js' | head -1)
echo "first asset path: $asset"
if [ -n "$asset" ]; then
code=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1/userbase${asset}")
echo "/userbase${asset} -> $code"
fi
echo
echo "===== route tests ====="
for p in /userbase/ /userbase/index.html "/userbase${asset:-/static/js/}"; do
code=$(curl -s -o /tmp/b -w "%{http_code}" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1$p")
echo "$p -> $code | $(head -c 70 /tmp/b|tr -d '\n')"
done
echo
echo "===== backend test ====="
curl -s -o /dev/null -w "/userbase/api/x -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/api/x
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# 探测内网 nacos:8848 是否可达、public 空间下 userbase.yaml 是否存在、是否需要鉴权
echo "== resolve nacos =="
getent hosts nacos || echo "cannot resolve nacos"
echo "== fetch userbase.yaml (public, no auth) =="
curl -s -m 8 -o /tmp/nc.txt -w "http=%{http_code}\n" "http://nacos:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant="
echo "--- body (first 200 chars) ---"
head -c 200 /tmp/nc.txt; echo
if grep -q "unknown user" /tmp/nc.txt || grep -q "403" /tmp/nc.txt || [ "$(head -c 1 /tmp/nc.txt)" = "" ]; then
echo "== needs auth, try login nacos/nacos =="
TOKEN=$(curl -s -m 8 -X POST "http://nacos:8848/nacos/v1/auth/login?username=nacos&password=nacos" | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
echo "token_len=${#TOKEN}"
if [ -n "$TOKEN" ]; then
curl -s -m 8 -o /tmp/nc2.txt -w "http=%{http_code}\n" "http://nacos:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant=&accessToken=$TOKEN"
echo "--- authed body (first 200 chars) ---"
head -c 200 /tmp/nc2.txt; echo
fi
fi
echo "== done =="
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
echo "== containers on host =="
docker ps --format '{{.Names}}\t{{.Networks}}' | head -40
echo "== infra-infra-net members =="
docker network inspect infra-infra-net --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null
echo "== resolve nacos from inside userbase-app =="
docker exec userbase-app sh -c "getent hosts nacos || echo 'no nacos in container DNS'"
echo "== curl nacos:8848 from inside userbase-app (no auth) =="
docker exec userbase-app sh -c "curl -s -m 8 -o /tmp/nc.txt -w 'http=%{http_code}\n' 'http://nacos:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant=' ; head -c 200 /tmp/nc.txt; echo" || echo "curl failed"
echo "== done =="
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# 重新部署 userbase-app:拉取新镜像、重建、等待健康,并核对是否真正从 Nacos 拉到了配置
echo "== login ACR =="
echo '59qtJRqlT7' | docker login --username=15121037482 --password-stdin registry.cn-hangzhou.aliyuncs.com
cd /opt/docker/userbase
echo "== pull userbase-app =="
docker compose -f docker-compose.userbase.yml pull userbase-app
echo "== up -d userbase-app =="
docker compose -f docker-compose.userbase.yml up -d userbase-app
echo "== wait for health (up to 150s) =="
for i in $(seq 1 30); do
st=$(docker inspect -f '{{.State.Health.Status}}' userbase-app 2>/dev/null || echo unknown)
echo " [$i] health=$st"
if [ "$st" = "healthy" ]; then break; fi
sleep 5
done
echo "== nacos reachability (host) =="
curl -s -m 5 -o /dev/null -w "host nacos http=%{http_code}\n" "http://nacos.yunhaixingtong.com:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant=" || echo "host cannot reach nacos"
echo "== nacos reachability (container) =="
docker exec userbase-app sh -c "curl -s -m 5 -o /dev/null -w 'container nacos http=%{http_code}\n' 'http://nacos.yunhaixingtong.com:8848/nacos/v1/cs/configs?dataId=userbase.yaml&group=USERBASE&tenant='" || echo "container cannot reach nacos"
echo "== app logs: nacos / config / errors =="
docker logs --tail 150 userbase-app 2>&1 | grep -iE "Loading nacos data|userbase.yaml|locate Nacos|NacosConfig|get config from nacos|ERROR|Exception|connect.*refused|UnknownHost|403|401|拒绝|超时" | tail -50 || true
echo "== done =="
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# 通过 SSH 在远端以 sudo 执行脚本(从 stdin 读取)
# 用法: python3 remote_run.py <host> <port> <user> < script.sh
# 依赖环境变量 SSHPASS
import os, sys, paramiko
def main():
host = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 22
user = sys.argv[3] if len(sys.argv) > 3 else "root"
pw = os.environ.get("SSHPASS")
if not pw:
sys.stderr.write("ERROR: 请先设置环境变量 SSHPASS\n")
sys.exit(1)
script = sys.stdin.read()
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect(host, port, user, password=pw, timeout=30)
stdin, stdout, stderr = c.exec_command("sudo -S bash -s", timeout=1800)
stdin.write(pw + "\n")
stdin.write(script)
stdin.flush()
stdin.channel.shutdown_write()
sys.stdout.write(stdout.read().decode(errors="replace"))
err = stderr.read().decode(errors="replace")
if err.strip():
sys.stderr.write("STDERR:\n" + err)
code = stdout.channel.recv_exit_status()
c.close()
sys.exit(code)
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# 线上冒烟:经 Caddy 访问 UserBase,确认 Nacos 配置生效、前后端可达
echo "== SPA 首页 =="
curl -s -m 8 -o /dev/null -w "/userbase/ -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/
echo "== 前端静态资源 =="
curl -s -m 8 -o /dev/null -w "/userbase/index.html -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/index.html
echo "== 后端 API 可达性(CAS 重定向即代表到达后端) =="
curl -s -m 8 -o /dev/null -w "/userbase/api/x -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/api/x
echo "== 容器健康 =="
docker inspect -f 'userbase-app health={{.State.Health.Status}}' userbase-app
echo "== 确认应用真正连的是 nacos 下发的 mysql8 数据源(日志里看 datasource url) =="
docker logs --tail 200 userbase-app 2>&1 | grep -iE "jdbc:mysql://mysql8|HikariPool|Started UserBaseApplication" | tail -5 || true
echo "== done =="
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# 通过 SFTP 上传文件到远端(参数: host port user local remote [local remote ...])
# 依赖环境变量 SSHPASS
import os, sys, paramiko
def main():
host = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 22
user = sys.argv[3] if len(sys.argv) > 3 else "root"
pw = os.environ.get("SSHPASS")
if not pw:
sys.stderr.write("ERROR: 请先设置环境变量 SSHPASS\n")
sys.exit(1)
t = paramiko.Transport((host, port))
t.connect(username=user, password=pw)
sftp = paramiko.SFTPClient.from_transport(t)
args = sys.argv[4:]
for i in range(0, len(args), 2):
local, remote = args[i], args[i + 1]
cur = ""
for p in [p for p in os.path.dirname(remote).split("/") if p]:
cur += "/" + p
try:
sftp.stat(cur)
except IOError:
try:
sftp.mkdir(cur)
except IOError:
pass
sftp.put(local, remote)
print(f"PUT {local} -> {remote}")
t.close()
if __name__ == "__main__":
main()
+104
View File
@@ -0,0 +1,104 @@
# ============================================================
# 请在 Nacos 控制台新增此配置(public 命名空间)
# Data ID : userbase.yaml
# Group : USERBASE
# Format : YAML
# 说明:
# - 本文件即应用自身配置。Nacos 解析规则为 ${spring.application.name}.${file-extension},
# 因 bootstrap.yml 中 name=userbase、file-extension=yaml,故 Data ID 必须是 userbase.yaml。
# - public 空间下的 common.properties 会作为共享配置优先加载(低优先级),
# 本文件(高优先级)中的同名字段会覆盖 common.properties。
# - 若 common.properties 已包含 datasource / redis 等公共项,下面这些行可按需删减;
# 保留也无害(应用配置优先级更高,仍以本文件为准)。
# - 生产启动时 profile=prod,Nacos 还会额外尝试 userbase-prod.yaml(可选,不存在也无妨)。
# ============================================================
server:
port: 8060
servlet:
# 对外经 caddy 暴露为 http://www.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
password: 'wv&WcDYAmLtqTIGM'
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
password: 'CVf^x$sW*b6CGHxh'
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
password: 'JdU29I#Q2E4yXoUl'
listener:
direct:
auto-startup: false
simple:
auto-startup: false
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
yh:
sso:
server-url-prefix: http://sso.yunhaixingtong.com
client-host-url: http://www.yunhaixingtong.com/userbase
allow-headers: all
allow-origins: all
url-pattern: /api/*
mobi-api-pattern: /mobiapi/*
app-auth:
enabled: true
# 本应用自身身份(客户端签名用,若 UserBase 需调用其他带签名服务)
app-key: 100616
app-secret: 1febb9502d2744b6a235d3e4cfde4a2d
# 受信应用(服务端验签用):appKey -> appSecret
trusted-apps:
100616: 1febb9502d2744b6a235d3e4cfde4a2d
ADMIN_TAG_ID: 290
# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data
FILE_PATH: /data/upload
FILE_WEB_PATH: http://www.yunhaixingtong.com/userbase/upload/
ZIYUAN_WEB_PATH_HTTP: http://base.bestsch.com/ziyuanku/file/
UPLOAD_WEB_PATH_HTTP: http://www.yunhaixingtong.com/userbase/upload/file/
ORC_SERVICE_URL: http://oa.bestsch.com/OrcService/api/Ocr/Upload
logging:
level:
root: INFO
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
echo "== app startup DDL/schema hints =="
docker logs userbase-app 2>&1 | grep -iE "alter table|update schema|create table|ddl|column|sys_user" | head -30 || echo "(no ddl lines captured)"
echo "== datasource config from container =="
docker exec userbase-app cat /app/config/application-prod.yml 2>/dev/null | grep -iE "url:|username:|password:|jdbc" | sed -E 's/(password:).*/\1 ****/' || echo "(no config)"
echo "== mysql client available? =="
which mysql || echo "NO_MYSQL_CLI"
+19
View File
@@ -0,0 +1,19 @@
set -u
echo "===== 1. Main app JS chunk from SPA ====="
html=$(curl -s -H "Host: www.yunhaixingtong.com" http://127.0.0.1/userbase/)
appjs=$(echo "$html" | grep -oE '/static/js/[A-Za-z0-9_./-]+\.js' | head -1)
echo "app js: $appjs"
curl -s -o /dev/null -w " /userbase${appjs} -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" "http://127.0.0.1/userbase${appjs}"
echo
echo "===== 2. Ensure other sites not regressed (sso / b) ====="
curl -s -o /dev/null -w " sso host / -> %{http_code}\n" -H "Host: sso.yunhaixingtong.com" http://127.0.0.1/
curl -s -o /dev/null -w " b host / -> %{http_code}\n" -H "Host: www.yunhaixingtong.com" http://127.0.0.1/b/
echo
echo "===== 3. Caddy listening ports on host ====="
ss -ltnp 2>/dev/null | grep -E ':80 |:443 ' | head
echo
echo "===== 4. Containers status ====="
docker ps --format '{{.Names}} {{.Status}}' | grep -E 'caddy|userbase'
echo
echo "===== 5. userbase-front health (serve index directly) ====="
docker exec userbase-front wget -qO- http://localhost/ 2>/dev/null | head -c 60; echo
+7
View File
@@ -0,0 +1,7 @@
# 正式环境 - base
VITE_BASE_HOST=https://base.bestsch.com
VITE_SSO_HOST=https://sso.bestsch.com
VITE_SERVICE_HOST=https://base.bestsch.com/LearnCenter
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
VITE_IS_MOCK=false
+7
View File
@@ -0,0 +1,7 @@
# 开发环境 (dev)
VITE_BASE_HOST=http://base.bestsch.com
VITE_SSO_HOST=http://sso.bestsch.com
VITE_SERVICE_HOST=http://localhost:8060
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
VITE_IS_MOCK=false
+7
View File
@@ -0,0 +1,7 @@
# 正式环境 - ld
VITE_BASE_HOST=https://ld.bestsch.com
VITE_SSO_HOST=https://sso.bestsch.com
VITE_SERVICE_HOST=https://ld.bestsch.com/LearnCenter
VITE_ZIYUANKU_HOST=https://ld.bestsch.com/BschZyk
VITE_ZHUANXIANG_HOST=https://ld.bestsch.com/BschZncpd
VITE_IS_MOCK=false
+7
View File
@@ -0,0 +1,7 @@
# 正式环境 - 云海星通(部署在 www.yunhaixingtong.com/userbase)
VITE_BASE_HOST=http://www.yunhaixingtong.com
VITE_SSO_HOST=http://sso.yunhaixingtong.com
VITE_SERVICE_HOST=http://www.yunhaixingtong.com/userbase
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
VITE_IS_MOCK=false
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
yarn.lock
.npmrc
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+41
View File
@@ -0,0 +1,41 @@
import type { GeneratorConfig } from "@alova/wormhole";
export default {
generator: [
{
input: "http://localhost:8060/v2/api-docs",
// 文档平台类型
platform: "swagger",
// 生成代码的输出目录
output: "src/api",
// 响应数据的媒体类型
responseMediaType: "application/json",
// 请求体数据的媒体类型
bodyMediaType: "application/json",
// 生成的 API 版本
version: 1,
// 生成代码类型
type: "typescript",
// 全局 API 名称
global: "Apis",
splitImplementation: true,
implementation: "inline",
// API 处理函数
handleApi: apiDescriptor => {
// 过滤掉已废弃的 API
if (apiDescriptor.deprecated) {
return undefined;
}
const newOpId = apiDescriptor.summary?.trim() || apiDescriptor.operationId;
const url = apiDescriptor.url.replace(/^\/{platform}\//, "/api/");
return {
...apiDescriptor,
operationId: newOpId, // 关键:改掉它
url: apiDescriptor.url.replace(/^\/{platform}\//, "/api/"),
};
},
},
],
} as GeneratorConfig;
+6
View File
@@ -0,0 +1,6 @@
//铃铛debug
window.conf = {
baseHost: "http://basedebug.bestsch.com",
ssoHost: "http://ssodebug.bestsch.com",
serviceHost: "http://basedebug.bestsch.com/QualityEvaluate",
};
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
<script src="./libs/jquery.min.js"></script>
<script src="./libs/lodash.min.js"></script>
<script src="./libs/ztree/jquery.ztree.all.min.js"></script>
<script src=" https://base.bestsch.com/BschHome/base/js/bschload.min.js?v=2025011001"></script>
<%- injectScript %>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
View File
+3 -3
View File
@@ -6,10 +6,10 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --mode dev", "dev": "vite --mode dev",
"build": "vite build", "build": "vite build --mode base",
"build:prod": "vite build --mode prod",
"build:ld": "vite build --mode ld",
"build:base": "vite build --mode base", "build:base": "vite build --mode base",
"build:ld": "vite build --mode ld",
"build:prod": "vite build --mode prod",
"preview": "vite preview", "preview": "vite preview",
"api:gen": "alova gen -f" "api:gen": "alova gen -f"
}, },

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 933 B

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+8 -39
View File
@@ -10,7 +10,6 @@ import Apis from '@/api/index'
import { alovaInstance } from "@/utils/alova/alova-iview"; import { alovaInstance } from "@/utils/alova/alova-iview";
import useCurrentInstance from "@/utils/hook/useCurrentInstance"; import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { UserRoleEnum } from "./types"; import { UserRoleEnum } from "./types";
import { isRouteAllowed } from "@/router";
const store = useDefaultStore(); const store = useDefaultStore();
const router = useRouter(); const router = useRouter();
const { $bus } = useCurrentInstance(); const { $bus } = useCurrentInstance();
@@ -54,9 +53,10 @@ const layoutInit = (info: any, modList: any[], appList: Array<any>, baseHost: st
}; };
/** /**
* 角色判定(降级兜底):GetUserScope 失败时按 GetUserInfo 的 user.type 推断—— * 角色判定
* type=1 → SuperAdmin,其余 → School。同时写入 currentRole 供下游 * 现基于 GetUserInfo 返回的 user.type 判定角色:
* Utils.getCurrentRoleInfo 与请求头注入读取(保持兼容;正常路径以 GetUserScope 为准)。 * type=1 → SuperAdmin,type=2 → School(不再有 Teacher 角色)
* 同时写入 currentRole 供下游 Utils.getCurrentRoleInfo 读取(保持兼容)
*/ */
const setCurrentRole = (user: any) => { const setCurrentRole = (user: any) => {
const userId = user?.id ?? null; const userId = user?.id ?? null;
@@ -87,43 +87,12 @@ onMounted(async () => {
try { try {
var user: any = await alovaInstance.Request(Apis.Base.getUserInfo()); var user: any = await alovaInstance.Request(Apis.Base.getUserInfo());
currentUser.value = user; // 系统字典 & 模块列表(模块当前写死,后续接入接口)
// 系统字典
await store.fetchSysDict(); await store.fetchSysDict();
// 角色判定:优先 GetUserScope(后端单点判定四级访问级别,前端不重复实现规则),
// 失败时降级走 setCurrentRole 的 type 推断
setCurrentRole(user);
try {
const scope: any = await alovaInstance.Request(Apis.Base.getUserScope());
if (scope && scope.level != null) {
store.setUserRole(scope.level);
store.setUserScope({
bureauGroupId: scope.bureauGroupId ?? null,
schoolGroupIds: scope.schoolGroupIds ?? null,
});
}
} catch (e) {
console.warn("获取访问范围失败,按用户类型降级判定角色", e);
}
// NONE 级:整体「暂无权限」,不加载菜单(接口层已被后端 NONE 拦截器 403 双挡)
store.setRoleLoaded(true);
if (store.userRole === UserRoleEnum.None) {
isNoAuthority.value = true;
layoutInit(user, [], [], baseHost, ssoHost);
isLogin.value = true;
return;
}
// 首次导航早于角色落定被守卫放行:此处复核当前路由(直接敲 URL 场景),越权重定向 /no-auth
if (!isRouteAllowed(router.currentRoute.value, store.userRole)) {
router.replace("/no-auth");
}
// 模块列表(按当前级别过滤菜单权限矩阵)
await store.fetchModules(); await store.fetchModules();
currentUser.value = user;
setCurrentRole(user);
layoutInit(user, store.modules, store.modules, baseHost, ssoHost); layoutInit(user, store.modules, store.modules, baseHost, ssoHost);
// 登录后直接跳转到左侧菜单第一个可用入口 // 登录后直接跳转到左侧菜单第一个可用入口
@@ -2,12 +2,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/** /**
* 用户中心接口文档 - version 0.0.1 * 用户中心接口文档 - version 0.0.1-SNAPSHOT
* *
* UserBase 用户中心 OpenAPI 文档(springdoc-openapi) * Demo project for Spring Boot
* *
* OpenAPI version: 3.0.1 * OpenAPI version: 3.0.0
* *
* Contact:
* *
* NOTE: This file is auto generated by the alova's vscode plugin. * NOTE: This file is auto generated by the alova's vscode plugin.
* *
@@ -16,104 +17,67 @@
* **Do not edit the file manually.** * **Do not edit the file manually.**
*/ */
export default { export default {
'SysUser.enable': ['POST', '/api/sysuser/Enable/{id}'],
'SysUser.edit': ['POST', '/api/sysuser/Edit/{id}'],
'SysUser.disable': ['POST', '/api/sysuser/Disable/{id}'],
'SysUser.deleteFromGroup': ['POST', '/api/sysuser/DeleteFromGroup/{userId}/{groupId}'],
'SysUser.changeSortNo': ['POST', '/api/sysuser/ChangeSortNo/{userId}'],
'SysUser.changeGroups': ['POST', '/api/sysuser/ChangeGroups'],
'SysUser.batchImport': ['POST', '/api/sysuser/BatchImport'],
'SysUser.batchDelete': ['POST', '/api/sysuser/BatchDelete'],
'SysUser.batchChangeStatus': ['POST', '/api/sysuser/BatchChangeStatus'],
'SysUser.add': ['POST', '/api/sysuser/Add'],
'SysGroup.sortChildren': ['POST', '/api/sysgroup/SortChildren'],
'SysGroup.move': ['POST', '/api/sysgroup/Move'],
'SysGroup.sysGroupEdit': ['POST', '/api/sysgroup/Edit/{id}'],
'SysGroup.sysGroupRemove': ['POST', '/api/sysgroup/Delete/{id}'],
'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
'PushSubscription.test': ['POST', '/api/push/Test'],
'PushSubscription.retry': ['POST', '/api/push/Retry/{id}'],
'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
'GroupAuthz.setGroupModule': ['POST', '/api/groupauthz/SetGroupModule'],
'GroupAuthz.saveModulePerms': ['POST', '/api/groupauthz/SaveModulePerms'],
'GroupAuthz.assignApps': ['POST', '/api/groupauthz/AssignApps'],
'Base.uploadFile': ['POST', '/api/base/UploadFile'],
'AppRole.saveUserRoles': ['POST', '/api/approle/SaveUserRoles'],
'AppRole.saveRoleUsers': ['POST', '/api/approle/SaveRoleUsers'],
'AppRole.saveRoleModules': ['POST', '/api/approle/SaveRoleModules'],
'AppRole.roleEdit': ['POST', '/api/approle/RoleEdit/{id}'],
'AppRole.roleDelete': ['POST', '/api/approle/RoleDelete/{id}'],
'AppRole.roleAdd': ['POST', '/api/approle/RoleAdd'],
'AppRole.removeRoleUser': ['POST', '/api/approle/RemoveRoleUser'],
'AppSettings.savePushSetting': ['POST', '/api/app/SavePushSetting/{id}'],
'AppSettings.resetAppSecret': ['POST', '/api/app/ResetAppSecret/{id}'],
'AppSettings.refreshSignKeys': ['POST', '/api/app/RefreshSignKeys'],
'AppSettings.editType': ['POST', '/api/app/EditType/{id}'],
'AppSettings.editModule': ['POST', '/api/app/EditModule/{id}'],
'AppSettings.editApp': ['POST', '/api/app/EditApp/{id}'],
'AppSettings.deleteType': ['POST', '/api/app/DeleteType/{id}'],
'AppSettings.deleteModule': ['POST', '/api/app/DeleteModule/{id}'],
'AppSettings.deleteApp': ['POST', '/api/app/DeleteApp/{id}'],
'AppSettings.appTypeSort': ['POST', '/api/app/AppTypeSort'],
'AppSettings.appSort': ['POST', '/api/app/AppSort'],
'AppSettings.addType': ['POST', '/api/app/AddType'],
'AppSettings.addModule': ['POST', '/api/app/AddModule'],
'AppSettings.addApp': ['POST', '/api/app/AddApp'], 'AppSettings.addApp': ['POST', '/api/app/AddApp'],
'User.moveToGroup': ['POST', '/oapi/user/moveToGroup'], 'AppSettings.addModule': ['POST', '/api/app/AddModule'],
'User.edit_1': ['POST', '/oapi/user/edit/{id}'], 'AppSettings.addType': ['POST', '/api/app/AddType'],
'User.delete_': ['POST', '/oapi/user/delete/{id}/{groupId}'], 'AppSettings.appAuthzList': ['GET', '/api/app/AppAuthzList'],
'User.batchAdd_1': ['POST', '/oapi/user/batchAdd'], 'AppSettings.appList': ['GET', '/api/app/AppList'],
'User.add_1': ['POST', '/oapi/user/add'], 'AppSettings.appSort': ['POST', '/api/app/AppSort'],
'Group.edit_2': ['POST', '/oapi/group/edit/{id}'], 'AppSettings.appSortList': ['GET', '/api/app/AppSortList'],
'Group.delete_1': ['POST', '/oapi/group/delete/{id}'], 'AppSettings.appTypeList': ['GET', '/api/app/AppTypeList'],
'Group.batchAdd_2': ['POST', '/oapi/group/batchAdd'], 'AppSettings.appTypeSort': ['POST', '/api/app/AppTypeSort'],
'Group.add_2': ['POST', '/oapi/group/add'], 'AppSettings.deleteApp': ['POST', '/api/app/DeleteApp/{id}'],
'SysUser.list': ['GET', '/api/sysuser/List'], 'AppSettings.deleteModule': ['POST', '/api/app/DeleteModule/{id}'],
'SysUser.getUserGroups': ['GET', '/api/sysuser/Groups/{userId}'], 'AppSettings.deleteType': ['POST', '/api/app/DeleteType/{id}'],
'SysUser.userIdentity': ['GET', '/api/sysuser/UserIdentity'], 'AppSettings.editApp': ['POST', '/api/app/EditApp/{id}'],
'SysUser.export_': ['GET', '/api/sysuser/Export'], 'AppSettings.editModule': ['POST', '/api/app/EditModule/{id}'],
'SysUser.downloadTemplate': ['GET', '/api/sysuser/DownloadTemplate'], 'AppSettings.editType': ['POST', '/api/app/EditType/{id}'],
'SysGroup.sysGroupList': ['GET', '/api/sysgroup/List'], 'AppSettings.moduleList': ['GET', '/api/app/ModuleList'],
'SysGroup.getTree': ['GET', '/api/sysgroup/GetTree'], 'AppSettings.resetAppSecret': ['POST', '/api/app/ResetAppSecret/{id}'],
'SysGroup.getSchoolTree': ['GET', '/api/sysgroup/GetSchoolTree'], 'AppRole.removeRoleUser': ['POST', '/api/approle/RemoveRoleUser'],
'PushSubscription.recordList': ['GET', '/api/push/RecordList'], 'AppRole.roleAdd': ['POST', '/api/approle/RoleAdd'],
'PushSubscription.download': ['GET', '/api/push/Download/{id}'], 'AppRole.roleDelete': ['POST', '/api/approle/RoleDelete/{id}'],
'SysOperationLog.operationLogList': ['GET', '/api/oplog/List'], 'AppRole.roleEdit': ['POST', '/api/approle/RoleEdit/{id}'],
'SysOperationLog.operationLogDownload': ['GET', '/api/oplog/Download/{id}'], 'AppRole.roleList': ['GET', '/api/approle/RoleList'],
'SysOapiLog.oapiLogList': ['GET', '/api/oapilog/List'], 'AppRole.roleModuleTree': ['GET', '/api/approle/RoleModuleTree'],
'SysOapiLog.oapiLogDownload': ['GET', '/api/oapilog/Download/{id}'], 'AppRole.roleUsers': ['GET', '/api/approle/RoleUsers'],
'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'], 'AppRole.saveRoleModules': ['POST', '/api/approle/SaveRoleModules'],
'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'], 'AppRole.saveRoleUsers': ['POST', '/api/approle/SaveRoleUsers'],
'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'], 'AppRole.saveUserRoles': ['POST', '/api/approle/SaveUserRoles'],
'GroupAuthz.searchUsers': ['GET', '/api/groupauthz/SearchUsers'], 'Base.getSysDict': ['GET', '/api/base/GetSysDict'],
'GroupAuthz.hasModulePerm': ['GET', '/api/groupauthz/HasModulePerm'], 'Base.getUserInfo': ['GET', '/api/base/GetUserInfo'],
'GroupAuthz.groupUsers': ['GET', '/api/groupauthz/GroupUsers'], 'Base.manualHashCacheDemo': ['GET', '/api/base/ManualHashCacheDemo'],
'Base.RedirectTo': ['GET', '/api/base/RedirectTo/{id}'],
'Base.uploadFile': ['POST', '/api/base/UploadFile'],
'GroupAuthz.assignApps': ['POST', '/api/groupauthz/AssignApps'],
'GroupAuthz.getSchoolAppId': ['GET', '/api/groupauthz/GetSchoolAppId'],
'GroupAuthz.groupAppList': ['GET', '/api/groupauthz/GroupAppList'],
'GroupAuthz.groupModuleTree': ['GET', '/api/groupauthz/GroupModuleTree'], 'GroupAuthz.groupModuleTree': ['GET', '/api/groupauthz/GroupModuleTree'],
'GroupAuthz.groupModuleTreeByCategory': ['GET', '/api/groupauthz/GroupModuleTreeByCategory'], 'GroupAuthz.groupModuleTreeByCategory': ['GET', '/api/groupauthz/GroupModuleTreeByCategory'],
'GroupAuthz.groupAppList': ['GET', '/api/groupauthz/GroupAppList'], 'GroupAuthz.groupUsers': ['GET', '/api/groupauthz/GroupUsers'],
'GroupAuthz.getSchoolAppId': ['GET', '/api/groupauthz/GetSchoolAppId'], 'GroupAuthz.hasModulePerm': ['GET', '/api/groupauthz/HasModulePerm'],
'Base.RedirectTo': ['GET', '/api/base/RedirectTo/{id}'], 'GroupAuthz.saveModulePerms': ['POST', '/api/groupauthz/SaveModulePerms'],
'Base.manualHashCacheDemo': ['GET', '/api/base/ManualHashCacheDemo'], 'GroupAuthz.searchUsers': ['GET', '/api/groupauthz/SearchUsers'],
'Base.getUserInfo': ['GET', '/api/base/GetUserInfo'], 'GroupAuthz.setGroupModule': ['POST', '/api/groupauthz/SetGroupModule'],
'Base.getUserScope': ['GET', '/api/base/GetUserScope'], 'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
'Base.getSysDict': ['GET', '/api/base/GetSysDict'], 'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'],
'AppSettings.schoolList': ['GET', '/api/app/SchoolList'], 'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'],
'AppRole.roleUsers': ['GET', '/api/approle/RoleUsers'], 'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'],
'AppRole.roleModuleTree': ['GET', '/api/approle/RoleModuleTree'], 'OperationLog.list': ['GET', '/api/oplog/List'],
'AppRole.roleList': ['GET', '/api/approle/RoleList'], 'OperationLog.download': ['GET', '/api/oplog/Download/{id}'],
'AppSettings.moduleList': ['GET', '/api/app/ModuleList'], 'SysGroup.add': ['POST', '/api/sysgroup/Add'],
'AppSettings.getPushSetting': ['GET', '/api/app/GetPushSetting/{id}'], 'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
'AppSettings.appTypeList': ['GET', '/api/app/AppTypeList'], 'SysGroup.delete': ['POST', '/api/sysgroup/Delete/{id}'],
'AppSettings.appSortList': ['GET', '/api/app/AppSortList'], 'SysGroup.edit': ['POST', '/api/sysgroup/Edit/{id}'],
'AppSettings.appList': ['GET', '/api/app/AppList'], 'SysGroup.getSchoolTree': ['GET', '/api/sysgroup/GetSchoolTree'],
'AppSettings.appAuthzList': ['GET', '/api/app/AppAuthzList'], 'SysGroup.getTree': ['GET', '/api/sysgroup/GetTree'],
'User.modules': ['GET', '/oapi/user/modules'], 'SysGroup.list': ['GET', '/api/sysgroup/List'],
'User.byPhone': ['GET', '/oapi/user/byPhone'], 'SysGroup.move': ['POST', '/api/sysgroup/Move'],
'User.byLoginName': ['GET', '/oapi/user/byLoginName'], 'SysGroup.sortChildren': ['POST', '/api/sysgroup/SortChildren'],
'User.byId': ['GET', '/oapi/user/byId'], 'SysUser.add': ['POST', '/api/sysuser/Add'],
'SignKeyInternal.signKey': ['GET', '/oapi/internal/sign-key/{appId}'], 'SysUser.delete': ['POST', '/api/sysuser/Delete/{id}'],
'Group.tree': ['GET', '/oapi/group/tree'], 'SysUser.disable': ['POST', '/api/sysuser/Disable/{id}'],
'Group.list_1': ['GET', '/oapi/group/list'], 'SysUser.edit': ['POST', '/api/sysuser/Edit/{id}'],
'Group.byId_1': ['GET', '/oapi/group/byId'] 'SysUser.enable': ['POST', '/api/sysuser/Enable/{id}'],
'SysUser.list': ['GET', '/api/sysuser/List']
}; };
@@ -1,12 +1,13 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/** /**
* 用户中心接口文档 - version 0.0.1 * 用户中心接口文档 - version 0.0.1-SNAPSHOT
* *
* UserBase 用户中心 OpenAPI 文档(springdoc-openapi) * Demo project for Spring Boot
* *
* OpenAPI version: 3.0.1 * OpenAPI version: 3.0.0
* *
* Contact:
* *
* NOTE: This file is auto generated by the alova's vscode plugin. * NOTE: This file is auto generated by the alova's vscode plugin.
* *
+2949
View File
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,7 @@ let baseConfig: AxiosRequestConfig = {
headers: { headers: {
// "Content-Type": "application/x-www-form-urlencoded", // "Content-Type": "application/x-www-form-urlencoded",
"Content-Type": "application/json;charset=urf-8", "Content-Type": "application/json;charset=urf-8",
yschapirequest: "1", bschapirequest: "1",
"Access-Control-Allow-Headers": "Set-Cookie", "Access-Control-Allow-Headers": "Set-Cookie",
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
+40
View File
@@ -0,0 +1,40 @@
import { BaseConf } from "@/types/Basics";
/**
* 通过 Vite 环境变量管理配置,不再依赖 window.conf / config.js
* 环境变量定义在项目根目录的 .env.dev / .env.base / .env.ld 中
*/
const env = import.meta.env;
let conf: BaseConf = {
baseHost: env.VITE_BASE_HOST,
ssoHost: env.VITE_SSO_HOST,
serviceHost: env.VITE_SERVICE_HOST,
relayConf: {
ziyuankuHost: env.VITE_ZIYUANKU_HOST,
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
},
isMock: env.VITE_IS_MOCK === "true",
isDebug: env.DEV,
};
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
var ishttps = "https:" == document.location.protocol ? true : false;
if (ishttps) {
Object.keys(conf).forEach(element => {
if (typeof conf[element] === "string" && conf[element].indexOf("http://") > -1) {
conf[element] = conf[element].replace("http://", "https://");
}
});
// relayConf 内部也需要处理
if (conf.relayConf) {
Object.keys(conf.relayConf).forEach(key => {
const val = (conf.relayConf as any)[key];
if (typeof val === "string" && val.indexOf("http://") > -1) {
(conf.relayConf as any)[key] = val.replace("http://", "https://");
}
});
}
}
export default conf;
+68
View File
@@ -0,0 +1,68 @@
import type { IModule } from "@/store";
/**
* 左侧菜单数据(系统管理功能分组)
* ----------------------------------------------------------------
* 当前直接 Promise.resolve 返回写死数据;后续接入真实接口时,
* 只需把 `return Promise.resolve(menu)` 替换为
* `return alovaInstance.Request(Apis.xxx.getMenu())`
* 即可无缝对接,调用方(store.fetchModules)无需任何改动。
*
* 分组结构:
* 1. 组,用户管理 —— 顶层独立选项卡(单页)
* 2. 应用管理 —— 顶层独立选项卡(= 应用列表主页 /app-manage/list);
* 页内按钮跳转「应用类别管理」子页(/app-manage/category,不在菜单里,
* 由 routes.ts 的 meta.parentId=1102 关联,面包屑「应用管理>应用类别管理」)
* 3. 权限管理 —— 父目录:应用开通 / 组授权 / 权限一览
* 4. 日志管理 —— 父目录:接口日志 / 系统日志
*
* 每个节点带唯一 id;子节点的 pid 由 layout.vue 的 flaten() 扁平化时
* 依据父节点 id 自动推导,用于「高亮自身 + 面包屑展示父级」联动。
* 菜单项不再渲染 icon。
*/
export function getMenuList(): Promise<IModule[]> {
const menu: IModule[] = [
// 1. 组,用户管理(顶层独立选项卡)
{
id: 1,
modId: 1101,
name: "组,用户管理",
page: "/group-user",
},
// 2. 应用管理(顶层独立选项卡,= 应用列表主页;页内按钮跳转应用类别管理子页)
{
id: 2,
modId: 1102,
name: "应用管理",
page: "/app-manage/list",
},
// 3. 权限管理(父目录)
{
id: 4,
modId: 1108,
name: "权限管理",
page: "",
childState: true,
children: [
{ id: 41, modId: 1103, name: "应用开通", page: "/app-open" },
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth" },
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list" },
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view" },
],
},
// 4. 日志管理(父目录)
{
id: 5,
modId: 1109,
name: "日志管理",
page: "",
childState: true,
children: [
{ id: 51, modId: 1106, name: "接口日志", page: "/api-log" },
{ id: 52, modId: 1107, name: "系统日志", page: "/sys-log" },
{ id: 53, modId: 1111, name: "操作日志", page: "/oplog" },
],
},
];
return Promise.resolve(menu);
}

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 914 B

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 380 B

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 695 B

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 919 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 793 B

After

Width:  |  Height:  |  Size: 793 B

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

Before

Width:  |  Height:  |  Size: 740 B

After

Width:  |  Height:  |  Size: 740 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 795 B

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1014 B

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

Some files were not shown because too many files have changed in this diff Show More