Browse Source

上传主界面

main
liwei1dao 3 months ago
parent
commit
c44d7145b0
  1. 20
      apps/admin/.dockerignore
  2. 9
      apps/admin/.nuxt/nuxt.lock
  3. BIN
      apps/services/dump.rdb
  4. BIN
      apps/services/home
  5. 25
      apps/services/log/console.log
  6. 5
      apps/services/modules/echomeet/api_summary.go
  7. 32
      apps/services/modules/echomeet/model.go
  8. 16
      apps/services/modules/echomeet/model_cache.go
  9. 2
      apps/services/modules/user/api_v2.go
  10. 6
      apps/services/modules/user/api_v2_getappconfig.go
  11. 69
      deploy/admin/README.md
  12. 79
      deploy/admin/dev-deploy.sh
  13. 18
      deploy/admin/docker-compose.yml
  14. 15
      deploy/admin/env/env.example
  15. 44
      deploy/admin/prod-build.sh
  16. 95
      deploy/admin/prod-deploy.sh
  17. 20
      deploy/console/prod-deploy.sh

20
apps/admin/.dockerignore

@ -0,0 +1,20 @@
# 构建上下文裁剪:避免把本机(尤其 macOS)产物拷进镜像。
# 关键:Dockerfile 先 pnpm install 再 COPY . .,若把本机 node_modules 带进去会覆盖
# 容器内为 linux 装好的依赖,导致 pnpm build 失败/原生模块架构不符。
node_modules
.nuxt
.output
.data
.git
.gitignore
# 本地环境与日志,绝不进镜像
.env
.env.*
*.log
# 杂项
.DS_Store
Dockerfile
.dockerignore
README*.md

9
apps/admin/.nuxt/nuxt.lock

@ -1,9 +0,0 @@
{
"pid": 21165,
"startedAt": 1782613791026,
"command": "dev",
"cwd": "/Users/liwei/work/go/yunyan/yunyan-sas/apps/admin",
"port": 3000,
"hostname": "127.0.0.1",
"url": "http://127.0.0.1:3000"
}

BIN
apps/services/dump.rdb

Binary file not shown.

BIN
apps/services/home

Binary file not shown.

25
apps/services/log/console.log

@ -1,13 +1,12 @@
2026/06/28 10:29:27.276 INFO sys log Init success ! 2026/06/29 10:01:35.232 INFO sys log Init success !
2026/06/28 10:29:27.276 INFO sys event Init success ! 2026/06/29 10:01:35.234 INFO sys event Init success !
2026/06/28 10:29:28.801 INFO console: sys.postgres(主库)初始化成功 2026/06/29 10:01:36.883 INFO console: sys.postgres(主库)初始化成功
2026/06/28 10:29:28.802 INFO console: sys.redis 初始化成功 2026/06/29 10:01:36.888 INFO console: sys.redis 初始化成功
2026/06/28 10:29:28.803 INFO console: sys.nats 初始化成功 2026/06/29 10:01:36.904 INFO console: sys.nats 初始化成功
2026/06/28 10:29:28.803 INFO console: sys.cos 初始化成功 2026/06/29 10:01:36.904 INFO console: sys.cos 初始化成功
2026/06/28 10:29:28.803 INFO 服务[console-d908e1s4kvnl2senkasg] 初始化完成! 2026/06/29 10:01:36.904 INFO 服务[console-d90t3vs4kvnsr2d9sdcg] 初始化完成!
2026/06/28 10:29:28.803 INFO 服务[console-d908e1s4kvnl2senkasg:1.0.0.0] 启动完成! 2026/06/29 10:01:36.904 INFO 服务[console-d90t3vs4kvnsr2d9sdcg:1.0.0.0] 启动完成!
2026/06/28 10:29:33.457 INFO console.stat: 已创建 JetStream 流 STATS(subject=stats.event) 2026/06/29 10:01:40.884 INFO console.stat: 订阅前 JetStream 流 STATS 当前消息数=0 subjects=[stats.event]
2026/06/28 10:29:33.457 INFO console.stat: 订阅前 JetStream 流 STATS 当前消息数=0 subjects=[stats.event] 2026/06/29 10:01:40.888 INFO console.stat: 统计快照消费已启动 subject=stats.event stream=STATS durable=console-stats
2026/06/28 10:29:33.458 INFO console.stat: 统计快照消费已启动 subject=stats.event stream=STATS durable=console-stats 2026/06/29 10:01:40.888 INFO console: HTTP 服务监听 :8080(静态目录 ./consoleweb)
2026/06/28 10:29:33.458 INFO console: HTTP 服务监听 :8080(静态目录 ./consoleweb) 2026/06/29 10:01:40.888 DEBUG 启动模块【console】
2026/06/28 10:29:33.458 DEBUG 启动模块【console】

5
apps/services/modules/echomeet/api_summary.go

@ -39,8 +39,9 @@ func (this *apiComp) Summary(session comm.IUserSession, req *pb.EchomeetSummaryR
templateid = model.Templateid templateid = model.Templateid
} }
// templateid 是某一语种的主键,按主键查到的还是原语种模板。 // templateid 是某一语种的主键,按主键查到的还是原语种模板。
// 切换语言时需要通过 tid 取对应语言版本,因此先反查 tid。 // 仅公共模板(id<起点)有多语言版本,切换语言时通过 tid 反查对应版本;
if tid == "" && templateid != 0 { // 私有模板(id>=起点)单条、无多语言版本,保持按主键解析,不反查 tid。
if tid == "" && templateid != 0 && templateid < templatePrivateIdStart {
if base, berr := this.module.cache.GetTemplateForId(templateid); berr == nil && base.Tid != "" { if base, berr := this.module.cache.GetTemplateForId(templateid); berr == nil && base.Tid != "" {
tid = base.Tid tid = base.Tid
} }

32
apps/services/modules/echomeet/model.go

@ -14,6 +14,12 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
// templatePrivateIdStart 模板 id 分段阈值(唯一的归属判定依据):
// - id < 此值 → 公共模板,存公共数据库(Postgres),可有多语言版本;
// - id >= 此值 → 用户私有模板,存业务服务数据库(MySQL),单条、无多语言版本。
// 由建表时对私有模板表设置自增起点保证两段 id 不重叠(见 modelComp.Init)。
const templatePrivateIdStart = 10000
// 代理模型 // 代理模型
type modelComp struct { type modelComp struct {
cbase.ModuleCompBase cbase.ModuleCompBase
@ -24,7 +30,11 @@ type modelComp struct {
func (this *modelComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { func (this *modelComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
this.ModuleCompBase.Init(service, module, comp, opt) this.ModuleCompBase.Init(service, module, comp, opt)
this.module = module.(*Echomeet) this.module = module.(*Echomeet)
err = mysql.CreateTable(comm.TableEchomeetTemplate, &pb.DBEchoMeetTemplate{}) if err = mysql.CreateTable(comm.TableEchomeetTemplate, &pb.DBEchoMeetTemplate{}); err == nil {
// 私有模板 id 从分段起点(10000)开始递增,确保与公共模板(公共库,id<10000)id 段不重叠,
// 从而 gettemplateforid 能仅凭 id 把请求路由到正确的库(仅空表生效)。
mysql.AutoIncrementStart(comm.TableEchomeetTemplate, "id", templatePrivateIdStart)
}
err = mysql.CreateTable(comm.TableEchomeetRecord, &pb.DBEchoMeetRecord{}) err = mysql.CreateTable(comm.TableEchomeetRecord, &pb.DBEchoMeetRecord{})
return return
} }
@ -96,23 +106,27 @@ func (this *modelComp) gettemplates(uid string, language string) (models []*pb.D
} }
/* /*
根据id读取模版 gettemplateforid 按主键读取模板,仅凭 id 段判定归属并路由到对应库:
id 小于 10000 从设备数据库读取 - id < templatePrivateIdStart:公共模板 → 公共数据库(Postgres)
id 大于等于 10000 从管理员数据库读取 - id >= templatePrivateIdStart:用户私有模板 → 业务服务数据库(MySQL)
私有模板单条、无多语言版本,按主键直接取即可。
*/ */
// gettemplateforid 按主键查找模板(用于用户自定义模板)
func (this *modelComp) gettemplateforid(id uint64) (template *pb.DBEchoMeetTemplate, err error) { func (this *modelComp) gettemplateforid(id uint64) (template *pb.DBEchoMeetTemplate, err error) {
template = &pb.DBEchoMeetTemplate{} template = &pb.DBEchoMeetTemplate{}
err = postgres.FindOne(comm.TableEchomeetTemplate, template, "id=?", id) if id >= templatePrivateIdStart {
if err == nil { err = mysql.FindOne(comm.TableEchomeetTemplate, template, "id=?", id)
return return
} }
err = mysql.FindOne(comm.TableEchomeetTemplate, template, "id=?", id) err = postgres.FindOne(comm.TableEchomeetTemplate, template, "id=?", id)
return return
} }
// resolvetemplate tid 非空则按 tid+language 查找,否则按 templateid 主键查找 // resolvetemplate 私有模板(id>=起点)单条无多语言,按主键直取;
// 公共模板 tid 非空按 tid+language 取对应语言版本,否则按主键。
func (this *modelComp) resolvetemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) { func (this *modelComp) resolvetemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) {
if templateid >= templatePrivateIdStart {
return this.gettemplateforid(templateid)
}
if tid != "" { if tid != "" {
return this.gettemplatefortid(tid, language) return this.gettemplatefortid(tid, language)
} }

16
apps/services/modules/echomeet/model_cache.go

@ -126,10 +126,13 @@ func (this *modelCacheComp) GetCommTemplates(language string) (models []*pb.DBEc
return return
} }
// GetTemplateForId 按 id 读取模板:先查 public 缓存命中即返回,否则回退 DB(覆盖用户私有模板) // GetTemplateForId 按 id 读取模板:公共模板(id<起点)先查 public 缓存,未命中回退 DB;
// 私有模板(id>=起点)不在公共缓存内,直接查 DB(避免与公共模板 id 误命中)
func (this *modelCacheComp) GetTemplateForId(id uint64) (template *pb.DBEchoMeetTemplate, err error) { func (this *modelCacheComp) GetTemplateForId(id uint64) (template *pb.DBEchoMeetTemplate, err error) {
if v, found, cerr := cache.GetOne[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate, fmt.Sprintf("%d", id)); cerr == nil && found { if id < templatePrivateIdStart {
return v, nil if v, found, cerr := cache.GetOne[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate, fmt.Sprintf("%d", id)); cerr == nil && found {
return v, nil
}
} }
return this.module.model.gettemplateforid(id) return this.module.model.gettemplateforid(id)
} }
@ -161,8 +164,13 @@ func (this *modelCacheComp) GetTemplateForTid(tid string, language string) (temp
return this.module.model.gettemplatefortid(tid, language) return this.module.model.gettemplatefortid(tid, language)
} }
// ResolveTemplate tid 非空则按 tid+language 查找,否则按主键查找(与 model.resolvetemplate 行为一致) // ResolveTemplate 解析模板(与 model.resolvetemplate 行为一致):
// - 私有模板(templateid>=起点):单条、无多语言版本,按主键直取,忽略 tid/language;
// - 公共模板:tid 非空按 tid+language 取对应语言版本,否则按主键。
func (this *modelCacheComp) ResolveTemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) { func (this *modelCacheComp) ResolveTemplate(tid string, templateid uint64, language string) (template *pb.DBEchoMeetTemplate, err error) {
if templateid >= templatePrivateIdStart {
return this.GetTemplateForId(templateid)
}
if tid != "" { if tid != "" {
return this.GetTemplateForTid(tid, language) return this.GetTemplateForTid(tid, language)
} }

2
apps/services/modules/user/api_v2.go

@ -20,7 +20,7 @@ func (this *apiV2Comp) Init(service core.IService, module core.IModule, comp cor
this.module = module.(*User) this.module = module.(*User)
this.options = options.(*Options) this.options = options.(*Options)
this.Version = "v2" this.Version = "v2"
this.EncryptMsgs = []string{"GetAppConfigV2"} this.EncryptMsgs = []string{"GetAppConfig"}
return return
} }

6
apps/services/modules/user/api_v2_getappconfig.go

@ -16,11 +16,11 @@ import (
// @Security BearerAuth // @Security BearerAuth
// @Param user body pb.UserGetAppConfigReq true "获取应用配置" // @Param user body pb.UserGetAppConfigReq true "获取应用配置"
// @Success 200 {object} comm.HttpResult{data=pb.UserGetAppConfigResp} "成功返回(整体响应体经 AES-CBC 加密,响应头 X-Encrypted:1)" // @Success 200 {object} comm.HttpResult{data=pb.UserGetAppConfigResp} "成功返回(整体响应体经 AES-CBC 加密,响应头 X-Encrypted:1)"
// @Router /api/home/user_getappconfigv2_v2 [post] // @Router /api/home/user_getappconfig_v2 [post]
// //
// 注意:本方法注册在 apiV2Comp(Version=v2)上,反射路由 = user_<方法名小写>_<Version>, // 注意:本方法注册在 apiV2Comp(Version=v2)上,反射路由 = user_<方法名小写>_<Version>,
// 即 user_getappconfigv2_v2;在 EncryptMsgs 中声明后,网关会对整个响应体做 AES-CBC 加密。 // 即 user_getappconfig_v2;在 EncryptMsgs 中声明后,网关会对整个响应体做 AES-CBC 加密。
func (this *apiV2Comp) GetAppConfigV2(session comm.IUserSession, req *pb.UserGetAppConfigV2Req) (resp *pb.UserGetAppConfigResp, errdata *pb.ErrorData) { func (this *apiV2Comp) GetAppConfig(session comm.IUserSession, req *pb.UserGetAppConfigV2Req) (resp *pb.UserGetAppConfigResp, errdata *pb.ErrorData) {
var ( var (
ipdata *ipinfo.IPData ipdata *ipinfo.IPData
region pb.Region region pb.Region

69
deploy/admin/README.md

@ -0,0 +1,69 @@
# admin 管理前端部署
把 Nuxt 4 SSR 管理后台 **admin** 打成镜像 `yunyan-admin`,推送到私有仓库
`docker-registry.ideapsound.com`,再到目标机拉取重启。它本身不连数据库,只通过
`CONSOLE_BACKEND` 把 `/web/api`、`/console/api` 代理到同网络的 **console** 后端(容器名 `console-a11`)。
> 约定(与 console 一致):仓库**只提交模板**(`*.example`)。部署脚本会把**模板 + `docker-compose.yml`** 同步到服务器,
> 但**绝不上传/覆盖**你在服务器上填好的真实 `.env`(首次需登录服务器照模板复制并填值)。admin 没有 yaml 配置,全靠 `.env`。
## 文件
| 文件 | 说明 |
|---|---|
| `../../apps/admin/Dockerfile` | 镜像构建(Nuxt SSR:pnpm build → node server),构建上下文 = `apps/admin/` |
| `docker-compose.yml` | 运行镜像 `yunyan-admin`,`.env` 注入容器,只 `expose 3000`(经反代对外) |
| `env/env.example` | 运行时环境**模板**(只一份,不分环境);真实 `.env` 被 gitignore |
| `dev-deploy.sh` | **开发**:构建 dev 镜像 → 推送 → 远端拉取重启(固定 tag `dev-latest`) |
| `prod-build.sh` | **生产①**:构建带版本号镜像并推送(`:版本` + `:latest`),不部署 |
| `prod-deploy.sh` | **生产②**:部署指定版本到生产机(单机,无区域),支持回滚 |
## 用法
### 本地(local)
本机直接起容器(用本目录的 `.env`,自行从 `env/env.example` 复制填写):
```bash
cd deploy/admin
cp env/env.example .env # 填真实值(至少 CONSOLE_BACKEND)
docker compose up -d # 本机起 yunyan-admin
```
### 服务器准备(dev / prod,一次性)
脚本每次会同步 `docker-compose.yml` + `env/env.example` 模板到目标机。**首次**需登录服务器把模板复制成真实 `.env` 并填值
(`cp env.example .env`,dev-deploy 缺真实文件时也会打印该命令):
```
<部署目录>/ # dev / prod 均默认 /home/work/yunyan-admin
├── docker-compose.yml # 从本目录拷贝
└── .env # 照 env.example 填(CONSOLE_BACKEND / TAG / DOCKER_NETWORK)
```
### 部署
```bash
cd deploy/admin
./dev-deploy.sh # 开发:构建 dev-latest → 推送 → 远端拉取重启
./prod-build.sh 0.1.1 # 生产①:构建并推送 yunyan-admin:0.1.1 (+latest)
./prod-deploy.sh 0.1.1 # 生产②:部署 0.1.1 到生产机
./prod-deploy.sh rollback # 生产回滚到上一版本(与当前版本来回切换)
```
> 改了服务器上的 `.env` 后,必须经 `up`(脚本里的 `docker compose up -d`)**重建容器**才生效,`restart` 不会重读环境变量。
> 生产版本/回滚记录存服务器上的 `.deploy_tag` / `.deploy_tag.prev`,不动你维护的 `.env`。
## 需要填写 / 提供的东西
- **部署目标与凭据**:写死在各脚本顶部。`dev-deploy.sh` 的 `DEV_HOST`、`prod-deploy.sh` 的 `DEPLOY_HOST`;
仓库账号 `admin` / 密码已写死;`DEPLOY_KEY` 默认 `/Users/liwei/Documents/key/yunyan-us.pem`(prod)。
- ⚠️ 本目录脚本纳入 git,**公开仓库前务必清空** `*_HOST` 与 `REGISTRY_PASS`。
- **后端地址**:`.env` 的 `CONSOLE_BACKEND`,须指向同网络的 console 容器,默认 `http://console-a11:8080`。
## 端口与反代
admin HTTP `3000`(容器内,只 `expose` 不 publish)。须由 **1panel 反代 / NPM** 把后台域名
(如 `console.yunyanservice.xyz`)转发到 `yunyan-admin:3000`。admin 再在内部把 `/web/api`、`/console/api`
代理到 `console-a11:8080`,所以**单域名**即可跑通整套(浏览器 → 反代 → yunyan-admin → console-a11)。
admin 与 console 须在同一 `DOCKER_NETWORK`(默认 `1panel-network`)。

79
deploy/admin/dev-deploy.sh

@ -0,0 +1,79 @@
#!/usr/bin/env bash
# admin 开发环境:构建开发镜像 → 推送 → 远端拉取重启。
#
# Nuxt 管理前端打成镜像 yunyan-admin,推到私有仓库后远端拉取重启。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env 由你自行维护
# (照着仓库里的 env/env.example 在服务器上放好真实 .env),本脚本不上传任何真实配置。
#
# 用法(在 deploy/admin/ 下):
# ./dev-deploy.sh # 构建并发布 yunyan-admin:dev-latest
#
# 开发版镜像始终使用固定 tag dev-latest,每次构建覆盖同一 tag。
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/admin
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根
BUILD_CONTEXT="$REPO_ROOT/apps/admin" # Nuxt 构建上下文
DOCKERFILE="$BUILD_CONTEXT/Dockerfile"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 开发部署目标 / 私有仓库凭据 —— 写死在此(本文件纳入 git,私有仓库内可接受) ║
# ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║
# ╚══════════════════════════════════════════════════════════════════════╝
IMAGE_NAME="yunyan-admin" # 业务镜像名
REGISTRY="docker-registry.ideapsound.com" # 私有镜像仓库
REGISTRY_USER="admin" # 仓库账号
REGISTRY_PASS="li13451234" # 仓库密码
DEV_HOST="124.222.232.218" # ← 开发服务器 IP(必填)
DEV_PORT=22
DEV_USER=root
DEV_KEY=/Users/liwei/Documents/key/earphone.pem # 本机访问开发服务器的 SSH 私钥
DEV_DIR="/home/work/yunyan-admin" # 远端部署目录(其中 docker-compose.yml/.env 由你自己维护)
PLATFORM="${PLATFORM:-linux/amd64}"
TAG="dev-latest" # 开发版固定 tag,每次构建覆盖同一 dev-latest
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST(开发服务器 IP)}"
# ── 远程 SSH ──
_ssh() { ssh -p "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEV_USER@$DEV_HOST" "$@"; }
_scp() { scp -P "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEV_USER@$DEV_HOST:$1"; }
# 1) 构建并推送镜像
blue ">>> 构建镜像 $FULL_IMAGE (context=$BUILD_CONTEXT)"
echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$FULL_IMAGE" "$BUILD_CONTEXT"
docker push "$FULL_IMAGE"
# 2) 同步「模板 + compose」到服务器(只传模板,绝不覆盖你维护的真实 .env)
blue ">>> 同步模板与 compose → $DEV_USER@$DEV_HOST:$DEV_DIR"
_ssh "mkdir -p '$DEV_DIR'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEV_DIR/docker-compose.yml")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEV_DIR/env.example")"
# 3) 远端拉取并重启(用你在服务器维护的真实 .env)
blue ">>> 远端拉取并重启"
_ssh "echo '$REGISTRY_PASS' | docker login '$REGISTRY' -u '$REGISTRY_USER' --password-stdin >/dev/null"
_ssh "docker network inspect 1panel-network >/dev/null 2>&1 || docker network create 1panel-network"
if ! _ssh "test -f '$DEV_DIR/.env'"; then
red "⚠ 服务器缺少真实 .env —— 模板已同步,请登录服务器按模板填好后重跑本脚本:"
echo " ssh -i $DEV_KEY $DEV_USER@$DEV_HOST"
echo " cd $DEV_DIR && cp env.example .env # 编辑 .env 填真实值,然后重跑 ./dev-deploy.sh"
exit 0
fi
# 自动适配 compose 版本 + 去掉 --env-file(老版本/ v1 不认);.env 同目录会自动加载。
_ssh "cd '$DEV_DIR' \
&& if docker compose version >/dev/null 2>&1; then DC='docker compose'; else DC='docker-compose'; fi \
&& REGISTRY='$REGISTRY' TAG='$TAG' \$DC pull \
&& REGISTRY='$REGISTRY' TAG='$TAG' \$DC up -d \
&& docker image prune -f"
green "✓ admin dev 已发布: $FULL_IMAGE → $DEV_HOST:$DEV_DIR"

18
deploy/admin/docker-compose.yml

@ -3,7 +3,7 @@
# 只部署 yunyan-admin(Nuxt 管理前端)。它通过 CONSOLE_BACKEND 连接同网络的 console 服务。 # 只部署 yunyan-admin(Nuxt 管理前端)。它通过 CONSOLE_BACKEND 连接同网络的 console 服务。
# 依赖服务由目标环境共享提供,不在此 compose。 # 依赖服务由目标环境共享提供,不在此 compose。
# #
# 不直接 docker compose 调用,统一通过 deploy.sh。 # 不直接 docker compose 调用,统一通过 dev-deploy.sh / prod-deploy.sh(与 console 一致)。
services: services:
yunyan-admin: yunyan-admin:
@ -15,14 +15,22 @@ services:
networks: networks:
- appnet - appnet
env_file:
- .env
environment: environment:
TZ: Asia/Shanghai TZ: Asia/Shanghai
# server middleware 运行时代理目标(同网络容器名直接解析) # server middleware 运行时代理目标(同网络容器名直接解析);
CONSOLE_BACKEND: ${CONSOLE_BACKEND:-http://yunyan-console:8080} # console 容器名为 console-a11,不是 yunyan-console。
CONSOLE_BACKEND: ${CONSOLE_BACKEND:-http://console-a11:8080}
ports: # 与 console 一致:只 expose 不 publish,对外经 1panel 反代/NPM 路由到本容器:3000。
- "${ADMIN_PORT:-3000}:3000" # 若需本机直连调试,把下面两行换成 ports: ["${ADMIN_PORT:-3000}:3000"]。
expose:
- "3000"
# 共享外部网络:与 console 同网络(用容器名互访)。
# 目标机需预先存在此网络(脚本会自动 docker network create 兜底)。
networks: networks:
appnet: appnet:
name: ${DOCKER_NETWORK:-1panel-network} name: ${DOCKER_NETWORK:-1panel-network}

15
deploy/admin/env/env.example

@ -0,0 +1,15 @@
# admin(Nuxt 管理前端)运行时环境模板(只一份,不分 local/dev/prod)。
# 用法:在目标机上复制为 .env 并填真实值;本地起则在本目录复制为 .env。
# 真实 .env 已 gitignore,勿提交。
# ── 镜像(仓库地址 REGISTRY 已写死在脚本里,这里不填)──
TAG=latest # 生产建议固定版本号(如 0.1.1)便于回滚;dev 用 dev-latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network # compose 外部网络名,须与目标机一致(与 console 同网络)
# ── 后端代理目标 ──
# admin 的 server middleware 运行时把 /web/api、/console/api 代理到这里。
# 同 DOCKER_NETWORK 内用容器名直接解析;console 容器名为 console-a11(不是 yunyan-console)。
CONSOLE_BACKEND=http://console-a11:8080
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)写死在 dev-deploy.sh / prod-deploy.sh 顶部,不在此填。

44
deploy/admin/prod-build.sh

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# admin 生产环境:仅「构建带版本号的镜像并推送」,不部署。
#
# Nuxt 管理前端打成镜像 yunyan-admin,同时推送 :<version> 与 :latest 两个 tag。
# 部署用 ./prod-deploy.sh <version>。
#
# 用法(在 deploy/admin/ 下):
# ./prod-build.sh <version> # version 必须形如 X.Y.Z,如 0.1.1
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)"
BUILD_CONTEXT="$REPO_ROOT/apps/admin"
DOCKERFILE="$BUILD_CONTEXT/Dockerfile"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ── 私有仓库凭据(写死,公开仓库前清空 REGISTRY_PASS)──
IMAGE_NAME="yunyan-admin"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
PLATFORM="${PLATFORM:-linux/amd64}"
VERSION="${1:-}"
[ -n "$VERSION" ] || { red "用法: ./prod-build.sh <version>(如 0.1.1)"; exit 1; }
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
red "✗ version 必须形如 0.1.1,当前: $VERSION"; exit 1
fi
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
IMG_VER="$REGISTRY/$IMAGE_NAME:$VERSION"
IMG_LATEST="$REGISTRY/$IMAGE_NAME:latest"
blue ">>> 构建生产镜像 $IMG_VER (+latest) context=$BUILD_CONTEXT"
echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$IMG_VER" -t "$IMG_LATEST" "$BUILD_CONTEXT"
docker push "$IMG_VER"
docker push "$IMG_LATEST"
green "✓ 已推送 $IMG_VER 与 $IMG_LATEST"
echo " 部署:./prod-deploy.sh $VERSION"

95
deploy/admin/prod-deploy.sh

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# admin 生产环境:部署指定镜像版本到生产机,支持回滚。admin 为单机 Nuxt 管理前端,无区域之分。
#
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「远端拉取 + 重启」。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env 由你自行维护
# (照着仓库里的 env/env.example 在服务器上放好真实 .env),本脚本不上传任何真实配置。
#
# 用法(在 deploy/admin/ 下):
# ./prod-deploy.sh <tag> # 部署指定版本,如 ./prod-deploy.sh 0.1.1
# ./prod-deploy.sh rollback # 回滚到上一次部署版本(与当前版本来回切换)
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 生产部署目标 / 私有仓库凭据 —— 写死在此(本文件纳入 git)。公开仓库前清空 HOST/PASS。 ║
# ╚══════════════════════════════════════════════════════════════════════╝
IMAGE_NAME="yunyan-admin"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致)
DEPLOY_HOST="47.253.88.66" # ← 生产服务器 IP(必填)
DEPLOY_PORT=22
DEPLOY_USER=root
DEPLOY_KEY=/Users/liwei/Documents/key/yunyan-us.pem
DEPLOY_DIR="/home/work/yunyan-admin" # 远端部署目录(其中 docker-compose.yml/.env 由你自己维护)
# ── 参数 ──
ARG1="${1:-}"
[ -n "$ARG1" ] || { red "用法: ./prod-deploy.sh <tag|rollback>(如 0.1.1)"; exit 1; }
if [ "$ARG1" = "rollback" ]; then MODE="rollback"; NEW_TAG=""; else MODE="deploy"; NEW_TAG="$ARG1"; fi
: "${DEPLOY_HOST:?请在 prod-deploy.sh 顶部填 DEPLOY_HOST(生产服务器 IP)}"
_ssh() { ssh -p "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" "$@"; }
_scp() { scp -P "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEPLOY_USER@$DEPLOY_HOST:$1"; }
# 同步「模板 + compose」(只传模板,绝不覆盖你维护的真实 .env)
blue ">>> 同步模板与 compose → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh "mkdir -p '$DEPLOY_DIR'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEPLOY_DIR/env.example")"
# 远端拉取并重启(不上传真实配置):tag 历史存服务器上独立的 .deploy_tag,不动你维护的 .env。
blue ">>> 远端部署($MODE${NEW_TAG:+ $NEW_TAG}) → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh bash -s -- "$DEPLOY_DIR" "$MODE" "$NEW_TAG" "$REGISTRY/$IMAGE_NAME" "$REGISTRY" "$REGISTRY_USER" "$REGISTRY_PASS" "$DOCKER_NETWORK" <<'REMOTE'
set -e
DIR="$1"; MODE="$2"; NEW_TAG="$3"; IMG_PREFIX="$4"; REG="$5"; REG_USER="$6"; REG_PASS="$7"; NET="$8"
[ -f "$DIR/.env" ] || { echo "✗ 缺少 $DIR/.env(请照 env.example 在服务器填好真实 .env)"; exit 1; }
[ -f "$DIR/docker-compose.yml" ] || { echo "✗ 缺少 $DIR/docker-compose.yml"; exit 1; }
STATE="$DIR/.deploy_tag" # 当前部署 tag(独立文件,不动用户的 .env)
PREVF="$DIR/.deploy_tag.prev" # 上一次部署 tag
CUR_TAG="$([ -f "$STATE" ] && cat "$STATE" || true)"
CUR_PREV="$([ -f "$PREVF" ] && cat "$PREVF" || true)"
if [ "$MODE" = "rollback" ]; then
[ -n "$CUR_PREV" ] || { echo "没有上一次版本记录(.deploy_tag.prev),无法回滚"; exit 1; }
FINAL_TAG="$CUR_PREV"; FINAL_PREV="$CUR_TAG" # 与当前版本来回切换
else
FINAL_TAG="$NEW_TAG"
if [ -n "$CUR_TAG" ] && [ "$CUR_TAG" != "$NEW_TAG" ]; then FINAL_PREV="$CUR_TAG"; else FINAL_PREV="$CUR_PREV"; fi
fi
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
docker pull "$IMG_PREFIX:$FINAL_TAG"
# 自动适配 compose 版本:优先 v2 插件(docker compose),回退 v1(docker-compose)
if docker compose version >/dev/null 2>&1; then DC="docker compose"; else DC="docker-compose"; fi
# .env 与 compose 同目录会被自动加载,无需 --env-file(老版本/ v1 不认该 flag);
# REGISTRY/TAG 用 inline 环境变量覆盖(优先级高于 .env),不改写用户的 .env
cd "$DIR"
# 先正常 up;若因历史遗留的同名容器(非本 compose 项目创建,compose 不会自动替换)冲突,
# 强制删掉该容器后重试一次。容器名与 docker-compose.yml 的 container_name 一致。
REGISTRY="$REG" TAG="$FINAL_TAG" $DC up -d || {
echo ">>> up 失败,尝试清理同名遗留容器(yunyan-admin)后重试"
docker rm -f yunyan-admin 2>/dev/null || true
REGISTRY="$REG" TAG="$FINAL_TAG" $DC up -d
}
docker image prune -f
echo "$FINAL_TAG" > "$STATE"
[ -n "$FINAL_PREV" ] && echo "$FINAL_PREV" > "$PREVF" || true
echo "已部署版本 TAG=$FINAL_TAG (上一次=${FINAL_PREV:-无})"
REMOTE
green "✓ admin $MODE 完成"

20
deploy/console/prod-deploy.sh

@ -25,11 +25,11 @@ REGISTRY_USER="admin"
REGISTRY_PASS="li13451234" REGISTRY_PASS="li13451234"
DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致) DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致)
DEPLOY_HOST="" # ← 生产服务器 IP(必填) DEPLOY_HOST="47.253.88.66" # ← 生产服务器 IP(必填)
DEPLOY_PORT=22 DEPLOY_PORT=22
DEPLOY_USER=root DEPLOY_USER=root
DEPLOY_KEY=~/.ssh/id_rsa DEPLOY_KEY=/Users/liwei/Documents/key/yunyan-us.pem
DEPLOY_DIR="/opt/yunyan/console" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护) DEPLOY_DIR="/home/work/console-a11" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护)
# ── 参数 ── # ── 参数 ──
ARG1="${1:-}" ARG1="${1:-}"
@ -75,8 +75,18 @@ fi
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET" docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
docker pull "$IMG_PREFIX:$FINAL_TAG" docker pull "$IMG_PREFIX:$FINAL_TAG"
# REGISTRY/TAG 通过 inline 环境变量传给 compose(优先级高于 --env-file),不改写用户的 .env # 自动适配 compose 版本:优先 v2 插件(docker compose),回退 v1(docker-compose)
cd "$DIR" && REGISTRY="$REG" TAG="$FINAL_TAG" docker compose --env-file .env up -d if docker compose version >/dev/null 2>&1; then DC="docker compose"; else DC="docker-compose"; fi
# .env 与 compose 同目录会被自动加载,无需 --env-file(老版本/ v1 不认该 flag);
# REGISTRY/TAG 用 inline 环境变量覆盖(优先级高于 .env),不改写用户的 .env
cd "$DIR"
# 先正常 up;若因历史遗留的同名容器(非本 compose 项目创建,compose 不会自动替换)冲突,
# 强制删掉该容器后重试一次。容器名与 docker-compose.yml 的 container_name 一致。
REGISTRY="$REG" TAG="$FINAL_TAG" $DC up -d || {
echo ">>> up 失败,尝试清理同名遗留容器(console-a11)后重试"
docker rm -f console-a11 2>/dev/null || true
REGISTRY="$REG" TAG="$FINAL_TAG" $DC up -d
}
docker image prune -f docker image prune -f
echo "$FINAL_TAG" > "$STATE" echo "$FINAL_TAG" > "$STATE"

Loading…
Cancel
Save