33 changed files with 1163 additions and 350 deletions
@ -0,0 +1,31 @@ |
|||
<template> |
|||
<!-- 全屏「处理中」遮罩:覆盖整页并拦截点击,写操作进行中显示,杜绝重复提交/误点 --> |
|||
<Transition name="fade"> |
|||
<div |
|||
v-if="loading" |
|||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/30 backdrop-blur-[1px]" |
|||
aria-busy="true" |
|||
role="alert" |
|||
> |
|||
<div class="flex flex-col items-center gap-3 rounded-2xl bg-white px-10 py-7 shadow-xl"> |
|||
<span class="loading loading-spinner loading-lg text-primary"></span> |
|||
<span class="text-sm text-slate-600">处理中,请稍候…</span> |
|||
</div> |
|||
</div> |
|||
</Transition> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const { loading } = useLoading() |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.fade-enter-active, |
|||
.fade-leave-active { |
|||
transition: opacity 0.15s ease; |
|||
} |
|||
.fade-enter-from, |
|||
.fade-leave-to { |
|||
opacity: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,16 @@ |
|||
// 全局「处理中」遮罩状态:引用计数(支持并发请求叠加),>0 即显示遮罩。
|
|||
// 与 useToast 同样用 Nuxt useState 做 SSR 安全的全局共享状态。
|
|||
// begin()/end() 成对调用(见 useApi 对写操作的包装),end 取 max(0, n-1) 防计数为负。
|
|||
export function useLoading() { |
|||
const count = useState<number>('app:loading', () => 0) |
|||
const loading = computed(() => count.value > 0) |
|||
|
|||
function begin() { |
|||
count.value++ |
|||
} |
|||
function end() { |
|||
count.value = Math.max(0, count.value - 1) |
|||
} |
|||
|
|||
return { count, loading, begin, end } |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,175 @@ |
|||
package console |
|||
|
|||
/* |
|||
设备管理域(厂家 / 产品 / 版本)只读缓存。 |
|||
|
|||
console 主库(supabase)在海外、直连查询慢;这些是「后台维护、低频变更、高频读取」的引用数据, |
|||
后台多个页面(产品/版本/出货等)每次打开都要全量读,直连 DB 就「卡」。本组件把它们缓存到 |
|||
与服务同区域的 Redis,使后台读请求恒命中 Redis: |
|||
|
|||
- 预热:Start() 异步全量写入 Redis(避免海外慢查询阻塞模块启动) |
|||
- 定时刷新:内部 ticker 每 10 分钟全量刷新(console 服务未装 cron,用 ticker 自驱) |
|||
- 写后失效:增删改 handler 成功后调用对应 refreshXxx() 同步重写缓存,使紧随其后的列表读到最新 |
|||
|
|||
复用 sys/cache(基于 Redis Hash 的全量数据集);DB 回退用 consoleDeviceConn()(=postgres.GetSys())。 |
|||
读方法(GetXxx)缓存优先、未命中或异常时回退查 DB,保证 Redis 异常时功能不受影响。 |
|||
*/ |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"sort" |
|||
"time" |
|||
|
|||
"yunyan/comm" |
|||
"yunyan/lego/core" |
|||
"yunyan/lego/core/cbase" |
|||
"yunyan/lego/sys/log" |
|||
"yunyan/pb" |
|||
"yunyan/sys/cache" |
|||
) |
|||
|
|||
// 兜底 TTL:正常由写后刷新 + 定时刷新覆盖,TTL 仅作异常情况下的过期保护
|
|||
const deviceCacheTTL = time.Hour * 24 |
|||
|
|||
type deviceCacheComp struct { |
|||
cbase.ModuleCompBase |
|||
module *Console |
|||
} |
|||
|
|||
func (this *deviceCacheComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { |
|||
this.ModuleCompBase.Init(service, module, comp, opt) |
|||
this.module = module.(*Console) |
|||
return |
|||
} |
|||
|
|||
func (this *deviceCacheComp) Start() (err error) { |
|||
if err = this.ModuleCompBase.Start(); err != nil { |
|||
return |
|||
} |
|||
go this.Refresh() // 异步预热
|
|||
go this.loopRefresh() // 每 10 分钟兜底全量刷新
|
|||
return |
|||
} |
|||
|
|||
// loopRefresh console 服务未初始化 cron 子系统,这里用 ticker 自驱定时刷新。
|
|||
func (this *deviceCacheComp) loopRefresh() { |
|||
t := time.NewTicker(time.Minute * 10) |
|||
defer t.Stop() |
|||
for range t.C { |
|||
this.Refresh() |
|||
} |
|||
} |
|||
|
|||
// Refresh 全量刷新所有数据集。best-effort:单个数据集失败仅记日志,不影响其它。
|
|||
func (this *deviceCacheComp) Refresh() { |
|||
this.refreshFactorys() |
|||
this.refreshProducts() |
|||
this.refreshProductVersions() |
|||
} |
|||
|
|||
// ---- 厂家 ----
|
|||
|
|||
func (this *deviceCacheComp) refreshFactorys() { |
|||
models, err := dvFactorys(consoleDeviceConn()) |
|||
if err != nil { |
|||
this.module.Error("deviceCache refreshFactorys load", log.Field{Key: "err", Value: err.Error()}) |
|||
return |
|||
} |
|||
items := make(map[string]any, len(models)) |
|||
for _, m := range models { |
|||
items[fmt.Sprintf("%d", m.Id)] = m |
|||
} |
|||
if err = cache.ReplaceAll(context.Background(), comm.Cache_Factory, items, deviceCacheTTL); err != nil { |
|||
this.module.Error("deviceCache refreshFactorys write", log.Field{Key: "err", Value: err.Error()}) |
|||
} |
|||
} |
|||
|
|||
// GetFactorys 读全部厂家:缓存优先,缓存为空或异常时回退查 DB。
|
|||
// 缓存底层是 Redis Hash(HGetAll 无序),统一按厂家 id 升序,保证列表顺序稳定。
|
|||
func (this *deviceCacheComp) GetFactorys() (models []*pb.DBFactory, err error) { |
|||
if list, cerr := cache.GetAll[pb.DBFactory](context.Background(), comm.Cache_Factory); cerr == nil && len(list) > 0 { |
|||
models = list |
|||
} else if models, err = dvFactorys(consoleDeviceConn()); err != nil { |
|||
return |
|||
} |
|||
sort.Slice(models, func(i, j int) bool { return models[i].Id < models[j].Id }) |
|||
return models, nil |
|||
} |
|||
|
|||
// ---- 产品 ----
|
|||
|
|||
func (this *deviceCacheComp) refreshProducts() { |
|||
models, err := dvProducts(consoleDeviceConn()) |
|||
if err != nil { |
|||
this.module.Error("deviceCache refreshProducts load", log.Field{Key: "err", Value: err.Error()}) |
|||
return |
|||
} |
|||
items := make(map[string]any, len(models)) |
|||
for _, m := range models { |
|||
items[fmt.Sprintf("%d", m.Id)] = m |
|||
} |
|||
if err = cache.ReplaceAll(context.Background(), comm.Cache_Product, items, deviceCacheTTL); err != nil { |
|||
this.module.Error("deviceCache refreshProducts write", log.Field{Key: "err", Value: err.Error()}) |
|||
} |
|||
} |
|||
|
|||
// GetProducts 读全部产品:缓存优先,回退 DB。
|
|||
// 缓存底层是 Redis Hash(HGetAll 无序),统一按 厂家id、产品id 升序,保证列表顺序稳定。
|
|||
func (this *deviceCacheComp) GetProducts() (models []*pb.DBProduct, err error) { |
|||
if list, cerr := cache.GetAll[pb.DBProduct](context.Background(), comm.Cache_Product); cerr == nil && len(list) > 0 { |
|||
models = list |
|||
} else if models, err = dvProducts(consoleDeviceConn()); err != nil { |
|||
return |
|||
} |
|||
sort.Slice(models, func(i, j int) bool { |
|||
if models[i].Factoryid != models[j].Factoryid { |
|||
return models[i].Factoryid < models[j].Factoryid |
|||
} |
|||
return models[i].Id < models[j].Id |
|||
}) |
|||
return models, nil |
|||
} |
|||
|
|||
// GetProduct 按 id 读产品:缓存优先,回退 DB。
|
|||
func (this *deviceCacheComp) GetProduct(id uint32) (model *pb.DBProduct, err error) { |
|||
if v, found, cerr := cache.GetOne[pb.DBProduct](context.Background(), comm.Cache_Product, fmt.Sprintf("%d", id)); cerr == nil && found { |
|||
return v, nil |
|||
} |
|||
return dvProduct(consoleDeviceConn(), id) |
|||
} |
|||
|
|||
// ---- 产品版本 ----
|
|||
// 全部版本存一个 Hash(field=版本id),按 productid 在内存过滤。版本数据量小,整表缓存足够。
|
|||
|
|||
func (this *deviceCacheComp) refreshProductVersions() { |
|||
models, err := dvAllProductVersions(consoleDeviceConn()) |
|||
if err != nil { |
|||
this.module.Error("deviceCache refreshProductVersions load", log.Field{Key: "err", Value: err.Error()}) |
|||
return |
|||
} |
|||
items := make(map[string]any, len(models)) |
|||
for _, m := range models { |
|||
items[fmt.Sprintf("%d", m.Id)] = m |
|||
} |
|||
if err = cache.ReplaceAll(context.Background(), comm.Cache_ProductVersion, items, deviceCacheTTL); err != nil { |
|||
this.module.Error("deviceCache refreshProductVersions write", log.Field{Key: "err", Value: err.Error()}) |
|||
} |
|||
} |
|||
|
|||
// GetProductVersions 读某产品的版本:缓存命中则内存过滤 productid,未命中回退 DB(仅查该产品)。
|
|||
// 同样按版本 id 升序,避免 Hash 无序导致版本列表顺序漂移。
|
|||
func (this *deviceCacheComp) GetProductVersions(pid uint32) (models []*pb.DBProductVersion, err error) { |
|||
if list, cerr := cache.GetAll[pb.DBProductVersion](context.Background(), comm.Cache_ProductVersion); cerr == nil && len(list) > 0 { |
|||
models = make([]*pb.DBProductVersion, 0, len(list)) |
|||
for _, v := range list { |
|||
if v.Productid == pid { |
|||
models = append(models, v) |
|||
} |
|||
} |
|||
} else if models, err = dvProductVersions(consoleDeviceConn(), pid); err != nil { |
|||
return |
|||
} |
|||
sort.Slice(models, func(i, j int) bool { return models[i].Id < models[j].Id }) |
|||
return models, nil |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
package console |
|||
|
|||
/* |
|||
账号数据作用域(后端强制隔离)。 |
|||
|
|||
登录时把账号绑定(apps/products/regions,CSV)写进 JWT,tokenValid 解析后放进 gin.Context。 |
|||
每个数据接口用 scopeOf(c) 取出作用域,对查询/结果强制按绑定收敛——无论前端传什么、是否传 X-App-Id, |
|||
代理(3)/运营(4) 都只能拿到自己绑定范围内的数据;超管(1)/管理员(2) 不受限。 |
|||
|
|||
语义(与前端既有行为一致):某维度绑定为空 = 该维度不限制;非空 = 仅限列表内。 |
|||
*/ |
|||
|
|||
import ( |
|||
"strconv" |
|||
"strings" |
|||
|
|||
"yunyan/pb" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
) |
|||
|
|||
type acctScope struct { |
|||
unlimited bool // 超管/管理员:不受任何限制
|
|||
apps []string // 绑定应用名(空=应用维度不限)
|
|||
products []uint32 // 绑定产品 id(空=产品维度不限)
|
|||
regions []string // 绑定区域代码(空=区域维度不限)
|
|||
} |
|||
|
|||
func ctxStr(c *gin.Context, k string) string { |
|||
if v, ok := c.Get(k); ok { |
|||
if s, ok := v.(string); ok { |
|||
return s |
|||
} |
|||
} |
|||
return "" |
|||
} |
|||
|
|||
func splitCSV(s string) []string { |
|||
out := make([]string, 0) |
|||
for _, p := range strings.Split(s, ",") { |
|||
if p = strings.TrimSpace(p); p != "" { |
|||
out = append(out, p) |
|||
} |
|||
} |
|||
return out |
|||
} |
|||
|
|||
func splitCSVU32(s string) []uint32 { |
|||
out := make([]uint32, 0) |
|||
for _, p := range splitCSV(s) { |
|||
if n, err := strconv.ParseUint(p, 10, 32); err == nil { |
|||
out = append(out, uint32(n)) |
|||
} |
|||
} |
|||
return out |
|||
} |
|||
|
|||
func containsStr(list []string, v string) bool { |
|||
for _, x := range list { |
|||
if x == v { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
// scopeOf 从已鉴权的请求上下文解析当前账号的数据作用域。
|
|||
func scopeOf(c *gin.Context) acctScope { |
|||
switch currentIdentity(c) { |
|||
case pb.Identity_Admin, pb.Identity_Manager: |
|||
return acctScope{unlimited: true} // 超管/管理员不受工厂/产品/应用限制
|
|||
default: |
|||
return acctScope{ |
|||
apps: splitCSV(ctxStr(c, "apps")), |
|||
products: splitCSVU32(ctxStr(c, "products")), |
|||
regions: splitCSV(ctxStr(c, "regions")), |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (s acctScope) allowApp(name string) bool { |
|||
return s.unlimited || len(s.apps) == 0 || containsStr(s.apps, name) |
|||
} |
|||
|
|||
func (s acctScope) allowProduct(id uint32) bool { |
|||
if s.unlimited || len(s.products) == 0 { |
|||
return true |
|||
} |
|||
for _, p := range s.products { |
|||
if p == id { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
// filterProducts 把产品列表收敛到绑定范围内。
|
|||
func (s acctScope) filterProducts(in []*pb.DBProduct) []*pb.DBProduct { |
|||
if s.unlimited || len(s.products) == 0 { |
|||
return in |
|||
} |
|||
out := make([]*pb.DBProduct, 0, len(in)) |
|||
for _, p := range in { |
|||
if s.allowProduct(p.Id) { |
|||
out = append(out, p) |
|||
} |
|||
} |
|||
return out |
|||
} |
|||
|
|||
// statsClause 返回账号作用域的统计过滤子句(与前端下钻条件 AND,强制收敛、防越权)。
|
|||
//
|
|||
// 口径:按"最具体的绑定"收敛——有产品绑定就**只按产品**,否则按应用。这样对绑定产品的代理,
|
|||
// 「全部产品」(不传产品) 与「选该产品」(传 product_id) 落到同一过滤 product_id IN(绑定),结果一致。
|
|||
// 注意:登录/用户/订单等"应用级"指标记在 product_id=0,按产品收敛后对代理显示 0(它们不挂产品);
|
|||
// apps 绑定改为只用于"用户查询"页(X-App-Id 校验),不参与仪表盘统计。
|
|||
// 区域再 AND 上(含未归属 region='',让产品级空区域行能通过)。超管/管理员或完全无绑定 → 返回空。
|
|||
func (s acctScope) statsClause() (string, []interface{}) { |
|||
if s.unlimited { |
|||
return "", nil |
|||
} |
|||
conds := make([]string, 0, 2) |
|||
args := make([]interface{}, 0, 2) |
|||
|
|||
if len(s.products) > 0 { |
|||
conds = append(conds, "product_id IN ?") |
|||
args = append(args, s.products) |
|||
} else if len(s.apps) > 0 { |
|||
conds = append(conds, "app_id IN ?") |
|||
args = append(args, s.apps) |
|||
} |
|||
if len(s.regions) > 0 { |
|||
conds = append(conds, "(region IN ? OR region = '')") |
|||
args = append(args, s.regions) |
|||
} |
|||
return strings.Join(conds, " AND "), args |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
#!/usr/bin/env bash |
|||
# 构建(可选推送)服务镜像。被 deploy/<服务>/deploy.sh 的 build/deploy 动作调用,也可单独用。 |
|||
# |
|||
# 用法: [TAG=.. REGISTRY=.. PLATFORM=.. PUSH=0] ./build.sh <service> |
|||
# service : console | admin |
|||
# PUSH=0 : 只构建不推送(本机看效果用;deploy.sh 在 local 环境会自动传 PUSH=0) |
|||
# |
|||
# 镜像名固定 = <REGISTRY>/yunyan-<service>:<TAG>,与各 docker-compose.yml 对齐。 |
|||
set -euo pipefail |
|||
|
|||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|||
SERVICE="${1:-}" |
|||
[ -n "$SERVICE" ] || { echo "用法: ./build.sh <console|admin>"; exit 1; } |
|||
|
|||
REGISTRY="${REGISTRY:-docker-registry.ideapsound.com}" |
|||
TAG="${TAG:-latest}" |
|||
PLATFORM="${PLATFORM:-linux/amd64}" |
|||
PUSH="${PUSH:-1}" |
|||
|
|||
# 服务 → 构建上下文 / Dockerfile(见各 Dockerfile 顶部「构建上下文」注释) |
|||
case "$SERVICE" in |
|||
console) CONTEXT="$REPO_ROOT/apps/services"; DOCKERFILE="$CONTEXT/Dockerfile.console" ;; |
|||
admin) CONTEXT="$REPO_ROOT/apps/admin"; DOCKERFILE="$CONTEXT/Dockerfile" ;; |
|||
*) echo "未知服务: $SERVICE(应为 console|admin)"; exit 1 ;; |
|||
esac |
|||
[ -f "$DOCKERFILE" ] || { echo "✗ 找不到 Dockerfile: $DOCKERFILE"; exit 1; } |
|||
|
|||
IMAGE="$REGISTRY/yunyan-$SERVICE:$TAG" |
|||
|
|||
echo ">>> 构建 $IMAGE (platform=$PLATFORM, context=$CONTEXT)" |
|||
docker build --platform "$PLATFORM" -f "$DOCKERFILE" -t "$IMAGE" "$CONTEXT" |
|||
|
|||
if [ "$PUSH" = 1 ]; then |
|||
echo ">>> 推送 $IMAGE" |
|||
docker push "$IMAGE" |
|||
else |
|||
echo ">>> 跳过推送(PUSH=0),镜像已在本地:$IMAGE" |
|||
fi |
|||
@ -1,11 +1,6 @@ |
|||
# 真实环境变量与部署目标(含密钥 / DSN / SSH 密码),勿提交 |
|||
env/*.env |
|||
targets/*.conf |
|||
.env |
|||
|
|||
# 保留模板 |
|||
!env/example.env |
|||
!targets/example.conf |
|||
# 真实环境变量(含密钥 / DSN / SSH),勿提交;仓库只保留 *.example 模板 |
|||
*/env/*.env |
|||
*/.env |
|||
|
|||
# 运行时日志 |
|||
console/log/ |
|||
|
|||
@ -1,64 +1,73 @@ |
|||
# yunyan-sas 部署 |
|||
|
|||
三环境部署:`local`(本机)/ `dev`(开发服务器)/ `prod`(生产服务器),统一入口 `deploy.sh`。 |
|||
服务:`yunyan-console`(Go 控制面,:8080)+ `yunyan-admin`(Nuxt 管理前端,:3000)。 |
|||
**每个服务目录各一个 `deploy.sh`**,服务名取自所在目录,指令为 `<env> [action]`。 |
|||
每个服务自带单服务 compose + 环境模板;依赖服务(PostgreSQL / Redis / NATS) |
|||
由目标环境**共享提供**,不在 compose 内搭建,只在 env 里填连接地址。 |
|||
|
|||
## 服务 |
|||
- `console/` — Go 控制面服务(:8080) |
|||
- `admin/` — Nuxt 管理前端(:3000,通过 `CONSOLE_BACKEND` 连 console) |
|||
|
|||
## 目录结构 |
|||
``` |
|||
deploy/ |
|||
├── deploy.sh # 部署入口 |
|||
├── docker-compose.yml # 变量驱动 compose(console + admin) |
|||
└── <service>/ # console / admin |
|||
├── deploy.sh # 本服务部署脚本(服务名 = 目录名) |
|||
├── docker-compose.yml # 单服务 compose(变量驱动,无依赖服务) |
|||
├── env/ |
|||
│ ├── local.env # 本机运行时变量 |
|||
│ ├── dev.env / prod.env # 各环境运行时变量(占位,需填) |
|||
│ └── example.env # 模板 |
|||
├── targets/ |
|||
│ ├── dev.conf / prod.conf # 远程目标 SSH/目录/仓库(占位,需填) |
|||
│ └── example.conf # 模板 |
|||
└── console/confs/console.yaml # console 配置(变量从 .env 注入) |
|||
│ ├── local.env.example |
|||
│ ├── dev.env.example |
|||
│ ├── prod.env.example |
|||
│ └── (复制填写后的 local.env / dev.env / prod.env) |
|||
└── (console 额外: confs/console.yaml、log/) |
|||
``` |
|||
|
|||
## 首次配置(dev / prod) |
|||
1. 填 `env/dev.env`、`env/prod.env`:`POSTGRES_DSN` / `REDIS_*` / `NATS_URL` / `DOCKER_NETWORK` / `ADMIN_PORT` / `CONSOLE_*` / `FIELD_ENCRYPT_KEY`。 |
|||
- DSN 密码里的特殊字符要 URL 编码(`&` → `%26`)。 |
|||
2. 填 `targets/dev.conf`、`targets/prod.conf`:`SSH_HOST` / `SSH_PORT` / `SSH_USER` / `SSH_KEY` / `REMOTE_DIR` / `REGISTRY_PASS`。 |
|||
3. 目标服务器需:装 docker + docker compose v2、能访问私有镜像仓库、SSH 公钥已授权。 |
|||
> 两个服务的 `deploy.sh` 内容相同,靠所在目录名区分服务。 |
|||
|
|||
## 用法 |
|||
进入服务目录运行: |
|||
```bash |
|||
# 本机 |
|||
./deploy.sh local up # 起服务(访问 http://localhost:3000) |
|||
./deploy.sh local down |
|||
./deploy.sh local logs |
|||
cd deploy/console |
|||
./deploy.sh local up # 本机起 console |
|||
./deploy.sh dev # 一键发布 console 到开发服务器(动作默认 deploy) |
|||
./deploy.sh prod # 一键发布 console 到生产服务器 |
|||
./deploy.sh prod logs # 看生产 console 日志 |
|||
|
|||
# 开发服务器(一键:构建推送镜像 + 同步配置 + 远程拉起) |
|||
./deploy.sh dev deploy |
|||
./deploy.sh dev ps |
|||
./deploy.sh dev logs |
|||
|
|||
# 生产服务器 |
|||
./deploy.sh prod deploy |
|||
./deploy.sh prod restart |
|||
cd deploy/admin |
|||
./deploy.sh local up # 本机起 admin |
|||
./deploy.sh prod # 发布 admin 到生产服务器 |
|||
``` |
|||
|
|||
## 操作矩阵 |
|||
| 操作 | local | dev / prod | |
|||
|------|-------|-----------| |
|||
| up | 本机 compose up -d | 同步配置 + 远程 pull + up | |
|||
| down / restart / logs / ps | 本机 | 远程 | |
|||
| pull | 本机拉镜像 | 远程拉镜像 | |
|||
| build | 本机构建推送镜像(TAG 取自 env) | 同(构建始终在本机) | |
|||
| deploy | build + up | build + 同步 + 远程 pull + up | |
|||
参数: |
|||
- `env`:`local` | `dev` | `prod` |
|||
- `action`: |
|||
- `local`:up(默认)| down | restart | logs | ps | pull | build |
|||
- `dev` / `prod`:deploy(默认,构建推送+同步+远程拉起)| up(仅远程拉起)| build | down | restart | logs | ps | pull |
|||
|
|||
## 首次配置(dev / prod) |
|||
配置分两处:**部署目标/仓库凭据写死在各服务的 `deploy.sh`,运行时配置放 env 文件**。 |
|||
|
|||
1. 部署目标 + 私有仓库(改各服务 `deploy.sh` 顶部 `deploy_profile`,[console](console/deploy.sh) / [admin](admin/deploy.sh)): |
|||
- 按 `dev` / `prod` 段填 `DEPLOY_HOST`(服务器 IP) 和 `REGISTRY_PASS`(仓库密码) |
|||
- 通用项已给默认:`REGISTRY` / `REGISTRY_USER` / `DEPLOY_PORT` / `DEPLOY_USER` / `DEPLOY_KEY` |
|||
- `DEPLOY_DIR` 自动 = `/opt/yunyan/<服务名>`,无需填 |
|||
- 注:两个 `deploy.sh` 各填各的,不共享 |
|||
2. 运行时配置(每个服务、每个环境一份 env): |
|||
```bash |
|||
cp console/env/dev.env.example console/env/dev.env |
|||
cp admin/env/dev.env.example admin/env/dev.env |
|||
``` |
|||
需要填: |
|||
- 依赖服务连接(console):`POSTGRES_DSN` / `REDIS_ADDR`+`REDIS_PASSWORD` / `NATS_URL` |
|||
- 网络与端口:`DOCKER_NETWORK` / `ADMIN_PORT` / `TAG` |
|||
3. 目标机要求:装 docker + docker compose v2、能访问私有镜像仓库、SSH 公钥已授权、依赖服务在 `DOCKER_NETWORK` 网络内可达。 |
|||
|
|||
## deploy 流程(dev / prod) |
|||
1. 本机 `build.sh` 构建 console + admin 镜像并推送私有仓库(TAG 取自对应 env)。 |
|||
2. SSH 到目标机,创建 `REMOTE_DIR`。 |
|||
3. scp 同步:`docker-compose.yml` + `env/<环境>.env`(→ 远程 `.env`) + `console/confs/console.yaml`。 |
|||
4. 远程:确保 `DOCKER_NETWORK` 网络存在(不存在则自动创建)+ `docker login` 私有仓库。 |
|||
5. 远程:`docker compose pull && docker compose up -d`。 |
|||
## 部署顺序 |
|||
依赖服务(DB/Redis/NATS)共享、由环境预先提供。两个应用服务按序: |
|||
1. 先 `console`(admin 依赖它) |
|||
2. 再 `admin`(同一目标机、同一 `DOCKER_NETWORK`,用容器名 `yunyan-console` 连 console) |
|||
|
|||
## 注意 |
|||
- `env/*.env`、`targets/*.conf` 含密钥,已 `.gitignore` 忽略,**不要提交**。 |
|||
- prod 的 `TAG` 建议固定版本号(如 `v1.2.0`)便于回滚;prod 的 `FIELD_ENCRYPT_KEY` / `CONSOLE_TOKEN_KEY` 务必改强随机值。 |
|||
- redis / nats:在目标机 `DOCKER_NETWORK` 网络内则用容器名(`redis:6379`);否则在 env 填外部 `IP:端口`。 |
|||
- console 首启会在主库自动建表(含 serviceconfig / calltranslate 等新表)。 |
|||
- `env/*.env`(真实)含密钥/DSN,已 `.gitignore` 忽略;仓库只保留 `*.example`。 |
|||
- ⚠️ 部署目标/仓库密码写死在各服务 `deploy.sh`,而它们**会进 git**——`DEPLOY_HOST`、`REGISTRY_PASS` 等会随仓库提交(私有仓库内可接受;公开前务必清理两个 `deploy.sh`)。 |
|||
- prod 的 `TAG` 用固定版本号便于回滚;`FIELD_ENCRYPT_KEY` / `CONSOLE_TOKEN_KEY` 用强随机值,环境间不复用。 |
|||
- 镜像构建复用仓库根目录 `build.sh`(`deploy.sh` 在 deploy/build 动作时自动调用,目标=服务名);当前仓库**尚无 build.sh**,build/deploy 前需先补。 |
|||
|
|||
@ -0,0 +1,184 @@ |
|||
#!/usr/bin/env bash |
|||
# <服务> 部署脚本(每个服务目录各一份,服务名取自所在目录名) |
|||
# |
|||
# 用法: ./deploy.sh <env> [action] (在 deploy/<服务>/ 下运行) |
|||
# env : local | dev | prod |
|||
# action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps |
|||
# local → up(默认) | build | pull | down | restart | logs | ps |
|||
# |
|||
# deploy = 构建推送镜像 + 同步配置 + 远程 pull + up(一键发布,仅 dev/prod) |
|||
# up = 远程:仅同步配置 + 远程 pull + up(用已推送镜像);本机:起容器 |
|||
# |
|||
# 例(在 deploy/console/ 下): |
|||
# ./deploy.sh prod # 一键发布生产 console |
|||
# ./deploy.sh prod logs # 看生产 console 日志 |
|||
# ./deploy.sh local up # 本机起 console |
|||
# ./deploy.sh dev build # 只构建推送 dev 镜像 |
|||
set -euo pipefail |
|||
|
|||
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 本服务部署目录(deploy/<服务>) |
|||
SERVICE="$(basename "$SVC_DIR")" # 服务名 = 目录名(console / admin) |
|||
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根(deploy/<服务>/../..) |
|||
|
|||
blue() { printf "\033[34m%s\033[0m\n" "$*"; } |
|||
green() { printf "\033[32m%s\033[0m\n" "$*"; } |
|||
red() { printf "\033[31m%s\033[0m\n" "$*"; } |
|||
|
|||
# ╔══════════════════════════════════════════════════════════════════════╗ |
|||
# ║ 部署目标 / 私有仓库配置 —— 写死在这里,改这里即可(按环境) ║ |
|||
# ║ console、admin 同机不同目录;DEPLOY_DIR 自动 = /opt/yunyan/<服务名> ║ |
|||
# ║ 注意:本文件纳入 git,下面的密码/IP 会随仓库提交(私有仓库内可接受) ║ |
|||
# ╚══════════════════════════════════════════════════════════════════════╝ |
|||
deploy_profile() { |
|||
DEPLOY_HOST=""; REGISTRY_PASS="" # 占位,local 用不到;dev/prod 在下面填 |
|||
# —— 各环境通用默认 —— |
|||
REGISTRY=docker-registry.ideapsound.com # 私有镜像仓库地址 |
|||
REGISTRY_USER=admin # 仓库账号 |
|||
DEPLOY_PORT=22 # 服务器 SSH 端口 |
|||
DEPLOY_USER=root # 服务器 SSH 用户 |
|||
DEPLOY_KEY=~/.ssh/id_rsa # 本机访问服务器的 SSH 私钥路径 |
|||
case "$ENV_NAME" in |
|||
dev) |
|||
DEPLOY_HOST="" # ← 开发服务器 IP(必填) |
|||
REGISTRY_PASS="" # ← 私有仓库密码(必填) |
|||
;; |
|||
prod) |
|||
DEPLOY_HOST="" # ← 生产服务器 IP(必填) |
|||
REGISTRY_PASS="" # ← 私有仓库密码(必填) |
|||
;; |
|||
local) ;; # 本机不走 SSH,无需填 |
|||
esac |
|||
DEPLOY_DIR="/opt/yunyan/$SERVICE" # 远程部署目录(自动按服务名) |
|||
} |
|||
|
|||
usage() { |
|||
cat >&2 <<EOF |
|||
用法: ./deploy.sh <env> [action] (在 deploy/$SERVICE/ 下运行) |
|||
env : local | dev | prod |
|||
action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps |
|||
local → up(默认) | build | pull | down | restart | logs | ps |
|||
例: |
|||
./deploy.sh prod # 一键发布生产 $SERVICE |
|||
./deploy.sh prod logs # 看生产 $SERVICE 日志 |
|||
./deploy.sh local up # 本机起 $SERVICE |
|||
./deploy.sh dev build # 只构建推送 dev 镜像 |
|||
EOF |
|||
exit 1 |
|||
} |
|||
|
|||
# ── 参数解析与校验 ── |
|||
ENV_NAME="${1:-}"; ACTION="${2:-}" |
|||
[ -n "$ENV_NAME" ] || usage |
|||
[ -f "$SVC_DIR/docker-compose.yml" ] || { red "✗ $SERVICE 目录缺 docker-compose.yml(deploy.sh 须放在服务目录内)"; exit 1; } |
|||
case "$ENV_NAME" in |
|||
local|dev|prod) ;; |
|||
*) red "✗ 未知环境: ${ENV_NAME}(应为 local|dev|prod)"; usage ;; |
|||
esac |
|||
# 默认动作:远程发布默认 deploy,本机默认 up |
|||
[ -n "$ACTION" ] || { [ "$ENV_NAME" = local ] && ACTION=up || ACTION=deploy; } |
|||
|
|||
# 从 ENV_FILE 安全读取单个变量值(去除可能的外层双引号) |
|||
_envget() { |
|||
{ grep -E "^$1=" "$ENV_FILE" || true; } | head -1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' |
|||
} |
|||
|
|||
# 载入 env/<环境>.env —— 现在只放容器运行时配置(DSN / Redis / NATS / 密钥 / 端口 / TAG / 网络)。 |
|||
# 部署目标与仓库凭据已写死在 deploy_profile,不再放 env 文件。脚本只需从这里取 compose 仍要的 TAG/网络。 |
|||
load_env() { |
|||
ENV_FILE="$SVC_DIR/env/$ENV_NAME.env" |
|||
if [ ! -f "$ENV_FILE" ]; then |
|||
red "✗ 缺少 $SERVICE/env/$ENV_NAME.env —— 请先从 $SERVICE/env/$ENV_NAME.env.example 复制并填写" |
|||
exit 1 |
|||
fi |
|||
TAG="$(_envget TAG)"; TAG="${TAG:-latest}" |
|||
DOCKER_NETWORK="$(_envget DOCKER_NETWORK)"; DOCKER_NETWORK="${DOCKER_NETWORK:-1panel-network}" |
|||
} |
|||
|
|||
# 生成 compose 用 .env = 服务运行时 env 文件 + 注入写死的 REGISTRY(镜像地址替换用) |
|||
# 先滤掉源文件里可能残留的 REGISTRY 行,保证唯一、不依赖 compose 的覆盖顺序。 |
|||
gen_env() { |
|||
{ grep -v '^REGISTRY=' "$ENV_FILE" || true; } > "$SVC_DIR/.env" |
|||
printf 'REGISTRY=%s\n' "$REGISTRY" >> "$SVC_DIR/.env" |
|||
} |
|||
|
|||
# 构建并推送本服务镜像(复用仓库根目录 build.sh,目标 = 服务名)。 |
|||
# local 环境只构建不推送(PUSH=0),dev/prod 构建并推送到私有仓库。 |
|||
build_push() { |
|||
local push=1; [ "$ENV_NAME" = local ] && push=0 |
|||
blue ">>> 构建$([ "$push" = 1 ] && echo 推送) $SERVICE 镜像 (TAG=${TAG:-latest}, PUSH=$push)" |
|||
TAG="${TAG:-latest}" REGISTRY="$REGISTRY" PUSH="$push" "$REPO_ROOT/build.sh" "$SERVICE" |
|||
} |
|||
|
|||
ensure_network_local() { |
|||
docker network inspect "$DOCKER_NETWORK" >/dev/null 2>&1 || docker network create "$DOCKER_NETWORK" |
|||
} |
|||
|
|||
# 本机 docker compose(gen_env 落为同目录 .env,既做变量替换又做容器 env_file) |
|||
compose_local() { |
|||
gen_env |
|||
ensure_network_local |
|||
( cd "$SVC_DIR" && docker compose --env-file .env "$@" ) |
|||
} |
|||
|
|||
# ── 远程 SSH 基础 ── |
|||
_ssh() { ssh -p "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "${DEPLOY_USER:-root}@${DEPLOY_HOST}" "$@"; } |
|||
_scp() { scp -P "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; } |
|||
_dest() { echo "${DEPLOY_USER:-root}@${DEPLOY_HOST}:$1"; } |
|||
|
|||
# 远程发布:同步配置 + 远程 login + pull + up |
|||
remote_deploy() { |
|||
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}" |
|||
gen_env |
|||
|
|||
blue ">>> 同步 $SERVICE 配置到 ${DEPLOY_USER:-root}@${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" "$(_dest "$DEPLOY_DIR/.env")" |
|||
# 带 confs/ 的服务(console)一并同步配置并预建 log 目录 |
|||
if [ -d "$SVC_DIR/confs" ]; then |
|||
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'" |
|||
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEPLOY_DIR/confs/")" |
|||
fi |
|||
|
|||
blue ">>> 远程拉起 $SERVICE" |
|||
[ -n "${REGISTRY_PASS:-}" ] && \ |
|||
_ssh "docker login '$REGISTRY' -u '${REGISTRY_USER:-admin}' -p '$REGISTRY_PASS'" |
|||
_ssh "docker network inspect '$DOCKER_NETWORK' >/dev/null 2>&1 || docker network create '$DOCKER_NETWORK'" |
|||
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env pull && docker compose --env-file .env up -d" |
|||
} |
|||
|
|||
# 远程运维(down/restart/logs/ps/pull) |
|||
remote_compose() { |
|||
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}" |
|||
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env $*" |
|||
} |
|||
|
|||
# ── 分发 ── |
|||
deploy_profile # 写死的部署目标 / 仓库凭据 |
|||
load_env # env 文件里的运行时配置(TAG / 网络) |
|||
|
|||
if [ "$ENV_NAME" = local ]; then |
|||
case "$ACTION" in |
|||
up) compose_local up -d ;; |
|||
down) compose_local down ;; |
|||
restart) compose_local restart ;; |
|||
logs) compose_local logs -f --tail=120 ;; |
|||
ps) compose_local ps ;; |
|||
pull) compose_local pull ;; |
|||
build) build_push ;; |
|||
*) red "未知操作: ${ACTION}(local 支持 up|down|restart|logs|ps|pull|build)"; exit 1 ;; |
|||
esac |
|||
else |
|||
case "$ACTION" in |
|||
deploy) build_push; remote_deploy ;; |
|||
up) remote_deploy ;; |
|||
build) build_push ;; |
|||
pull) remote_compose pull ;; |
|||
down) remote_compose down ;; |
|||
restart) remote_compose restart ;; |
|||
ps) remote_compose ps ;; |
|||
logs) remote_compose logs -f --tail=120 ;; |
|||
*) red "未知操作: ${ACTION}(dev/prod 支持 deploy|up|build|pull|down|restart|logs|ps)"; exit 1 ;; |
|||
esac |
|||
fi |
|||
green "✓ $SERVICE $ENV_NAME $ACTION 完成" |
|||
@ -0,0 +1,29 @@ |
|||
# admin 服务部署(单服务) |
|||
# |
|||
# 只部署 yunyan-admin(Nuxt 管理前端)。它通过 CONSOLE_BACKEND 连接同网络的 console 服务。 |
|||
# 依赖服务由目标环境共享提供,不在此 compose。 |
|||
# |
|||
# 不直接 docker compose 调用,统一通过 deploy.sh。 |
|||
|
|||
services: |
|||
yunyan-admin: |
|||
image: ${REGISTRY:-docker-registry.ideapsound.com}/yunyan-admin:${TAG:-latest} |
|||
container_name: yunyan-admin |
|||
restart: unless-stopped |
|||
platform: ${PLATFORM:-linux/amd64} |
|||
|
|||
networks: |
|||
- appnet |
|||
|
|||
environment: |
|||
TZ: Asia/Shanghai |
|||
# server middleware 运行时代理目标(同网络容器名直接解析) |
|||
CONSOLE_BACKEND: ${CONSOLE_BACKEND:-http://yunyan-console:8080} |
|||
|
|||
ports: |
|||
- "${ADMIN_PORT:-3000}:3000" |
|||
|
|||
networks: |
|||
appnet: |
|||
name: ${DOCKER_NETWORK:-1panel-network} |
|||
external: true |
|||
@ -0,0 +1,10 @@ |
|||
# admin 服务 · dev 开发环境(复制为 dev.env 后填写真实值) |
|||
# 仓库地址 REGISTRY 已写死在 deploy.sh,这里不填 |
|||
TAG=dev |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=yunyan-net |
|||
ADMIN_PORT=3000 |
|||
CONSOLE_BACKEND=http://yunyan-console:8080 |
|||
|
|||
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在 |
|||
# deploy.sh 顶部的 deploy_profile(dev 段),不在此文件填。 |
|||
@ -0,0 +1,8 @@ |
|||
# admin 服务 · local 本机环境(复制为 local.env 后使用) |
|||
# 仓库地址 REGISTRY 已写死在 deploy.sh,这里不填 |
|||
TAG=latest |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=1panel-network |
|||
ADMIN_PORT=3000 |
|||
# 连接同网络的 console 服务(容器名);若 console 不在同机/同网络,填其可达地址 |
|||
CONSOLE_BACKEND=http://yunyan-console:8080 |
|||
@ -0,0 +1,10 @@ |
|||
# admin 服务 · prod 生产环境(复制为 prod.env 后填写真实值) |
|||
# 仓库地址 REGISTRY 已写死在 deploy.sh,这里不填 |
|||
TAG=latest # 生产建议固定版本号,如 v1.2.0 |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=yunyan-net |
|||
ADMIN_PORT=3000 |
|||
CONSOLE_BACKEND=http://yunyan-console:8080 |
|||
|
|||
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在 |
|||
# deploy.sh 顶部的 deploy_profile(prod 段),不在此文件填。 |
|||
@ -0,0 +1,184 @@ |
|||
#!/usr/bin/env bash |
|||
# <服务> 部署脚本(每个服务目录各一份,服务名取自所在目录名) |
|||
# |
|||
# 用法: ./deploy.sh <env> [action] (在 deploy/<服务>/ 下运行) |
|||
# env : local | dev | prod |
|||
# action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps |
|||
# local → up(默认) | build | pull | down | restart | logs | ps |
|||
# |
|||
# deploy = 构建推送镜像 + 同步配置 + 远程 pull + up(一键发布,仅 dev/prod) |
|||
# up = 远程:仅同步配置 + 远程 pull + up(用已推送镜像);本机:起容器 |
|||
# |
|||
# 例(在 deploy/console/ 下): |
|||
# ./deploy.sh prod # 一键发布生产 console |
|||
# ./deploy.sh prod logs # 看生产 console 日志 |
|||
# ./deploy.sh local up # 本机起 console |
|||
# ./deploy.sh dev build # 只构建推送 dev 镜像 |
|||
set -euo pipefail |
|||
|
|||
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 本服务部署目录(deploy/<服务>) |
|||
SERVICE="$(basename "$SVC_DIR")" # 服务名 = 目录名(console / admin) |
|||
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根(deploy/<服务>/../..) |
|||
|
|||
blue() { printf "\033[34m%s\033[0m\n" "$*"; } |
|||
green() { printf "\033[32m%s\033[0m\n" "$*"; } |
|||
red() { printf "\033[31m%s\033[0m\n" "$*"; } |
|||
|
|||
# ╔══════════════════════════════════════════════════════════════════════╗ |
|||
# ║ 部署目标 / 私有仓库配置 —— 写死在这里,改这里即可(按环境) ║ |
|||
# ║ console、admin 同机不同目录;DEPLOY_DIR 自动 = /opt/yunyan/<服务名> ║ |
|||
# ║ 注意:本文件纳入 git,下面的密码/IP 会随仓库提交(私有仓库内可接受) ║ |
|||
# ╚══════════════════════════════════════════════════════════════════════╝ |
|||
deploy_profile() { |
|||
DEPLOY_HOST=""; REGISTRY_PASS="" # 占位,local 用不到;dev/prod 在下面填 |
|||
# —— 各环境通用默认 —— |
|||
REGISTRY=docker-registry.ideapsound.com # 私有镜像仓库地址 |
|||
REGISTRY_USER=admin # 仓库账号 |
|||
DEPLOY_PORT=22 # 服务器 SSH 端口 |
|||
DEPLOY_USER=root # 服务器 SSH 用户 |
|||
DEPLOY_KEY=~/.ssh/id_rsa # 本机访问服务器的 SSH 私钥路径 |
|||
case "$ENV_NAME" in |
|||
dev) |
|||
DEPLOY_HOST="" # ← 开发服务器 IP(必填) |
|||
REGISTRY_PASS="" # ← 私有仓库密码(必填) |
|||
;; |
|||
prod) |
|||
DEPLOY_HOST="" # ← 生产服务器 IP(必填) |
|||
REGISTRY_PASS="" # ← 私有仓库密码(必填) |
|||
;; |
|||
local) ;; # 本机不走 SSH,无需填 |
|||
esac |
|||
DEPLOY_DIR="/opt/yunyan/$SERVICE" # 远程部署目录(自动按服务名) |
|||
} |
|||
|
|||
usage() { |
|||
cat >&2 <<EOF |
|||
用法: ./deploy.sh <env> [action] (在 deploy/$SERVICE/ 下运行) |
|||
env : local | dev | prod |
|||
action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps |
|||
local → up(默认) | build | pull | down | restart | logs | ps |
|||
例: |
|||
./deploy.sh prod # 一键发布生产 $SERVICE |
|||
./deploy.sh prod logs # 看生产 $SERVICE 日志 |
|||
./deploy.sh local up # 本机起 $SERVICE |
|||
./deploy.sh dev build # 只构建推送 dev 镜像 |
|||
EOF |
|||
exit 1 |
|||
} |
|||
|
|||
# ── 参数解析与校验 ── |
|||
ENV_NAME="${1:-}"; ACTION="${2:-}" |
|||
[ -n "$ENV_NAME" ] || usage |
|||
[ -f "$SVC_DIR/docker-compose.yml" ] || { red "✗ $SERVICE 目录缺 docker-compose.yml(deploy.sh 须放在服务目录内)"; exit 1; } |
|||
case "$ENV_NAME" in |
|||
local|dev|prod) ;; |
|||
*) red "✗ 未知环境: ${ENV_NAME}(应为 local|dev|prod)"; usage ;; |
|||
esac |
|||
# 默认动作:远程发布默认 deploy,本机默认 up |
|||
[ -n "$ACTION" ] || { [ "$ENV_NAME" = local ] && ACTION=up || ACTION=deploy; } |
|||
|
|||
# 从 ENV_FILE 安全读取单个变量值(去除可能的外层双引号) |
|||
_envget() { |
|||
{ grep -E "^$1=" "$ENV_FILE" || true; } | head -1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' |
|||
} |
|||
|
|||
# 载入 env/<环境>.env —— 现在只放容器运行时配置(DSN / Redis / NATS / 密钥 / 端口 / TAG / 网络)。 |
|||
# 部署目标与仓库凭据已写死在 deploy_profile,不再放 env 文件。脚本只需从这里取 compose 仍要的 TAG/网络。 |
|||
load_env() { |
|||
ENV_FILE="$SVC_DIR/env/$ENV_NAME.env" |
|||
if [ ! -f "$ENV_FILE" ]; then |
|||
red "✗ 缺少 $SERVICE/env/$ENV_NAME.env —— 请先从 $SERVICE/env/$ENV_NAME.env.example 复制并填写" |
|||
exit 1 |
|||
fi |
|||
TAG="$(_envget TAG)"; TAG="${TAG:-latest}" |
|||
DOCKER_NETWORK="$(_envget DOCKER_NETWORK)"; DOCKER_NETWORK="${DOCKER_NETWORK:-1panel-network}" |
|||
} |
|||
|
|||
# 生成 compose 用 .env = 服务运行时 env 文件 + 注入写死的 REGISTRY(镜像地址替换用) |
|||
# 先滤掉源文件里可能残留的 REGISTRY 行,保证唯一、不依赖 compose 的覆盖顺序。 |
|||
gen_env() { |
|||
{ grep -v '^REGISTRY=' "$ENV_FILE" || true; } > "$SVC_DIR/.env" |
|||
printf 'REGISTRY=%s\n' "$REGISTRY" >> "$SVC_DIR/.env" |
|||
} |
|||
|
|||
# 构建并推送本服务镜像(复用仓库根目录 build.sh,目标 = 服务名)。 |
|||
# local 环境只构建不推送(PUSH=0),dev/prod 构建并推送到私有仓库。 |
|||
build_push() { |
|||
local push=1; [ "$ENV_NAME" = local ] && push=0 |
|||
blue ">>> 构建$([ "$push" = 1 ] && echo 推送) $SERVICE 镜像 (TAG=${TAG:-latest}, PUSH=$push)" |
|||
TAG="${TAG:-latest}" REGISTRY="$REGISTRY" PUSH="$push" "$REPO_ROOT/build.sh" "$SERVICE" |
|||
} |
|||
|
|||
ensure_network_local() { |
|||
docker network inspect "$DOCKER_NETWORK" >/dev/null 2>&1 || docker network create "$DOCKER_NETWORK" |
|||
} |
|||
|
|||
# 本机 docker compose(gen_env 落为同目录 .env,既做变量替换又做容器 env_file) |
|||
compose_local() { |
|||
gen_env |
|||
ensure_network_local |
|||
( cd "$SVC_DIR" && docker compose --env-file .env "$@" ) |
|||
} |
|||
|
|||
# ── 远程 SSH 基础 ── |
|||
_ssh() { ssh -p "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "${DEPLOY_USER:-root}@${DEPLOY_HOST}" "$@"; } |
|||
_scp() { scp -P "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; } |
|||
_dest() { echo "${DEPLOY_USER:-root}@${DEPLOY_HOST}:$1"; } |
|||
|
|||
# 远程发布:同步配置 + 远程 login + pull + up |
|||
remote_deploy() { |
|||
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}" |
|||
gen_env |
|||
|
|||
blue ">>> 同步 $SERVICE 配置到 ${DEPLOY_USER:-root}@${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" "$(_dest "$DEPLOY_DIR/.env")" |
|||
# 带 confs/ 的服务(console)一并同步配置并预建 log 目录 |
|||
if [ -d "$SVC_DIR/confs" ]; then |
|||
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'" |
|||
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEPLOY_DIR/confs/")" |
|||
fi |
|||
|
|||
blue ">>> 远程拉起 $SERVICE" |
|||
[ -n "${REGISTRY_PASS:-}" ] && \ |
|||
_ssh "docker login '$REGISTRY' -u '${REGISTRY_USER:-admin}' -p '$REGISTRY_PASS'" |
|||
_ssh "docker network inspect '$DOCKER_NETWORK' >/dev/null 2>&1 || docker network create '$DOCKER_NETWORK'" |
|||
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env pull && docker compose --env-file .env up -d" |
|||
} |
|||
|
|||
# 远程运维(down/restart/logs/ps/pull) |
|||
remote_compose() { |
|||
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}" |
|||
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env $*" |
|||
} |
|||
|
|||
# ── 分发 ── |
|||
deploy_profile # 写死的部署目标 / 仓库凭据 |
|||
load_env # env 文件里的运行时配置(TAG / 网络) |
|||
|
|||
if [ "$ENV_NAME" = local ]; then |
|||
case "$ACTION" in |
|||
up) compose_local up -d ;; |
|||
down) compose_local down ;; |
|||
restart) compose_local restart ;; |
|||
logs) compose_local logs -f --tail=120 ;; |
|||
ps) compose_local ps ;; |
|||
pull) compose_local pull ;; |
|||
build) build_push ;; |
|||
*) red "未知操作: ${ACTION}(local 支持 up|down|restart|logs|ps|pull|build)"; exit 1 ;; |
|||
esac |
|||
else |
|||
case "$ACTION" in |
|||
deploy) build_push; remote_deploy ;; |
|||
up) remote_deploy ;; |
|||
build) build_push ;; |
|||
pull) remote_compose pull ;; |
|||
down) remote_compose down ;; |
|||
restart) remote_compose restart ;; |
|||
ps) remote_compose ps ;; |
|||
logs) remote_compose logs -f --tail=120 ;; |
|||
*) red "未知操作: ${ACTION}(dev/prod 支持 deploy|up|build|pull|down|restart|logs|ps)"; exit 1 ;; |
|||
esac |
|||
fi |
|||
green "✓ $SERVICE $ENV_NAME $ACTION 完成" |
|||
@ -0,0 +1,38 @@ |
|||
# console 服务部署(单服务) |
|||
# |
|||
# 只部署 yunyan-console。依赖服务(PostgreSQL / Redis / NATS)由目标环境共享提供, |
|||
# 不在此 compose 内搭建——在 .env 里用地址/容器名连接它们。 |
|||
# |
|||
# 不直接 docker compose 调用,统一通过 deploy.sh, |
|||
# 它会把 env/<环境>.env 落为同目录 .env(既做变量替换又做容器 env_file)。 |
|||
|
|||
services: |
|||
yunyan-console: |
|||
image: ${REGISTRY:-docker-registry.ideapsound.com}/yunyan-console:${TAG:-latest} |
|||
container_name: yunyan-console |
|||
restart: unless-stopped |
|||
platform: ${PLATFORM:-linux/amd64} |
|||
|
|||
networks: |
|||
- appnet |
|||
|
|||
# 配置与日志:confs/console.yaml 随服务同步,log 持久化到部署目录 |
|||
volumes: |
|||
- ./confs:/app/confs |
|||
- ./log:/app/log |
|||
|
|||
env_file: |
|||
- .env |
|||
|
|||
environment: |
|||
TZ: Asia/Shanghai |
|||
|
|||
expose: |
|||
- "8080" |
|||
|
|||
# 共享外部网络:依赖的 redis/nats/postgres 在此网络(用容器名)或经 .env 填外部地址。 |
|||
# 目标机需预先存在此网络(脚本会自动 docker network create 兜底)。 |
|||
networks: |
|||
appnet: |
|||
name: ${DOCKER_NETWORK:-1panel-network} |
|||
external: true |
|||
@ -0,0 +1,26 @@ |
|||
# console 服务 · dev 开发环境(复制为 dev.env 后填写真实值) |
|||
|
|||
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)── |
|||
TAG=dev |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=yunyan-net |
|||
|
|||
# ── 依赖服务(目标环境共享提供,填地址;密码特殊字符 URL 编码 & → %26)── |
|||
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require |
|||
REDIS_ADDR=redis:6379 |
|||
REDIS_PASSWORD= |
|||
NATS_URL=nats://nats:4222 |
|||
|
|||
# ── COS(可选)── |
|||
COS_SECRET_ID= |
|||
COS_SECRET_KEY= |
|||
|
|||
# ── Console 服务配置 ── |
|||
CONSOLE_TOKEN_KEY=dev-change-me |
|||
CONSOLE_ADMIN_ACCOUNT=admin |
|||
CONSOLE_ADMIN_PASSWORD=change-me |
|||
CONSOLE_SITE_NAME=云言 SaaS 管理平台(开发) |
|||
FIELD_ENCRYPT_KEY=dev-change-me-to-random-key |
|||
|
|||
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在 |
|||
# deploy.sh 顶部的 deploy_profile(dev 段),不在此文件填。 |
|||
@ -0,0 +1,23 @@ |
|||
# console 服务 · local 本机环境(复制为 local.env 后使用) |
|||
|
|||
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)── |
|||
TAG=latest |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=1panel-network |
|||
|
|||
# ── 依赖服务(本机/共享环境提供,仅填连接地址,不在本服务搭建)── |
|||
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require |
|||
REDIS_ADDR=redis:6379 |
|||
REDIS_PASSWORD= |
|||
NATS_URL=nats://nats:4222 |
|||
|
|||
# ── COS(可选)── |
|||
COS_SECRET_ID= |
|||
COS_SECRET_KEY= |
|||
|
|||
# ── Console 服务配置 ── |
|||
CONSOLE_TOKEN_KEY=console-secret-change-me |
|||
CONSOLE_ADMIN_ACCOUNT=admin |
|||
CONSOLE_ADMIN_PASSWORD=change-me |
|||
CONSOLE_SITE_NAME=云言 SaaS 管理平台 |
|||
FIELD_ENCRYPT_KEY=change-me-to-random-key |
|||
@ -0,0 +1,26 @@ |
|||
# console 服务 · prod 生产环境(复制为 prod.env 后填写真实值) |
|||
|
|||
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)── |
|||
TAG=latest # 生产建议固定版本号,如 v1.2.0,便于回滚 |
|||
PLATFORM=linux/amd64 |
|||
DOCKER_NETWORK=yunyan-net |
|||
|
|||
# ── 依赖服务(目标环境共享提供,填地址;密码特殊字符 URL 编码 & → %26)── |
|||
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require |
|||
REDIS_ADDR=redis:6379 |
|||
REDIS_PASSWORD= |
|||
NATS_URL=nats://nats:4222 |
|||
|
|||
# ── COS(可选)── |
|||
COS_SECRET_ID= |
|||
COS_SECRET_KEY= |
|||
|
|||
# ── Console 服务配置(密钥务必强随机)── |
|||
CONSOLE_TOKEN_KEY=prod-change-me-to-strong-secret |
|||
CONSOLE_ADMIN_ACCOUNT=admin |
|||
CONSOLE_ADMIN_PASSWORD=change-me |
|||
CONSOLE_SITE_NAME=云言 SaaS 管理平台 |
|||
FIELD_ENCRYPT_KEY=prod-change-me-to-strong-random-key |
|||
|
|||
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在 |
|||
# deploy.sh 顶部的 deploy_profile(prod 段),不在此文件填。 |
|||
@ -1,136 +0,0 @@ |
|||
#!/usr/bin/env bash |
|||
# yunyan-sas 多环境部署入口 |
|||
# local —— 本机 docker compose |
|||
# dev / prod —— SSH 同步配置到目标机后远程 docker compose |
|||
# |
|||
# 配置来源: |
|||
# env/<环境>.env 运行时变量(镜像/网络/DB/Redis/NATS/Console 等) |
|||
# targets/<环境>.conf 远程目标(SSH 主机/密钥/部署目录/镜像仓库账号) |
|||
set -euo pipefail |
|||
|
|||
ROOT="$(cd "$(dirname "$0")" && pwd)" |
|||
REPO_ROOT="$(cd "$ROOT/.." && pwd)" |
|||
|
|||
ENV="${1:-}" |
|||
ACTION="${2:-}" |
|||
|
|||
green() { printf "\033[32m%s\033[0m\n" "$*"; } |
|||
blue() { printf "\033[34m%s\033[0m\n" "$*"; } |
|||
red() { printf "\033[31m%s\033[0m\n" "$*"; } |
|||
|
|||
usage() { |
|||
cat <<'EOF' |
|||
用法: ./deploy.sh <环境> <操作> |
|||
|
|||
环境: local | dev | prod |
|||
操作: |
|||
up 启动(local 直接起;dev/prod 同步配置后远程起) |
|||
down 停止并移除容器 |
|||
pull 拉取最新镜像 |
|||
restart 重启容器 |
|||
logs 跟随日志 |
|||
ps 查看容器状态 |
|||
build 构建并推送镜像到私有仓库(调用 ../build.sh,TAG 取自 env) |
|||
deploy 一键发布:build + 同步配置 + 远程 pull + up |
|||
|
|||
示例: |
|||
./deploy.sh local up # 本机起服务 |
|||
./deploy.sh dev build # 构建推送 dev 镜像 |
|||
./deploy.sh dev deploy # 一键发布到开发服务器 |
|||
./deploy.sh prod deploy # 一键发布到生产服务器 |
|||
./deploy.sh prod logs # 跟随生产日志 |
|||
EOF |
|||
} |
|||
|
|||
[ -z "$ENV" ] && { usage; exit 1; } |
|||
case "$ENV" in local|dev|prod) ;; *) red "✗ 未知环境: $ENV"; usage; exit 1 ;; esac |
|||
[ -z "$ACTION" ] && { usage; exit 1; } |
|||
|
|||
ENV_FILE="$ROOT/env/$ENV.env" |
|||
[ -f "$ENV_FILE" ] || { red "✗ 缺少环境变量文件: deploy/env/$ENV.env(参考 env/example.env)"; exit 1; } |
|||
|
|||
# 从 env 取某个 key 的值 |
|||
env_val() { grep -E "^$1=" "$ENV_FILE" | head -1 | cut -d= -f2-; } |
|||
TAG="$(env_val TAG)"; TAG="${TAG:-latest}" |
|||
|
|||
# ── 构建并推送镜像(复用根目录 build.sh)── |
|||
do_build() { |
|||
blue ">>> [$ENV] 构建并推送镜像 (TAG=$TAG)" |
|||
TAG="$TAG" "$REPO_ROOT/build.sh" all |
|||
} |
|||
|
|||
# ── 本机 compose ── |
|||
local_compose() { |
|||
cp "$ENV_FILE" "$ROOT/.env" |
|||
( cd "$ROOT" && docker compose --env-file "$ROOT/.env" "$@" ) |
|||
} |
|||
|
|||
local_ensure_network() { |
|||
local net; net="$(env_val DOCKER_NETWORK)"; net="${net:-1panel-network}" |
|||
docker network inspect "$net" >/dev/null 2>&1 || docker network create "$net" |
|||
} |
|||
|
|||
# ── 远程目标 ── |
|||
load_target() { |
|||
local conf="$ROOT/targets/$ENV.conf" |
|||
[ -f "$conf" ] || { red "✗ 缺少部署目标: deploy/targets/$ENV.conf(参考 targets/example.conf)"; exit 1; } |
|||
# shellcheck disable=SC1090 |
|||
source "$conf" |
|||
[ -z "${SSH_HOST:-}" ] && { red "✗ targets/$ENV.conf 未配置 SSH_HOST"; exit 1; } |
|||
[ -z "${REMOTE_DIR:-}" ] && { red "✗ targets/$ENV.conf 未配置 REMOTE_DIR"; exit 1; } |
|||
local port="${SSH_PORT:-22}" |
|||
local key="${SSH_KEY/#\~/$HOME}" |
|||
local common="-i $key -o StrictHostKeyChecking=accept-new" |
|||
SSH="ssh -p $port $common ${SSH_USER}@${SSH_HOST}" |
|||
SCP="scp -P $port $common" |
|||
} |
|||
|
|||
remote_sync() { |
|||
blue ">>> [$ENV] 同步配置到 ${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}" |
|||
$SSH "mkdir -p '$REMOTE_DIR/console/confs' '$REMOTE_DIR/console/log'" |
|||
$SCP "$ROOT/docker-compose.yml" "${SSH_USER}@${SSH_HOST}:$REMOTE_DIR/docker-compose.yml" |
|||
$SCP "$ENV_FILE" "${SSH_USER}@${SSH_HOST}:$REMOTE_DIR/.env" |
|||
$SCP "$ROOT/console/confs/console.yaml" "${SSH_USER}@${SSH_HOST}:$REMOTE_DIR/console/confs/console.yaml" |
|||
} |
|||
|
|||
remote_ensure_network() { |
|||
local net; net="$(env_val DOCKER_NETWORK)"; net="${net:-1panel-network}" |
|||
$SSH "docker network inspect '$net' >/dev/null 2>&1 || docker network create '$net'" |
|||
} |
|||
|
|||
remote_login() { |
|||
[ -n "${REGISTRY_PASS:-}" ] && \ |
|||
$SSH "docker login '${REGISTRY:-docker-registry.ideapsound.com}' -u '${REGISTRY_USER:-admin}' -p '$REGISTRY_PASS'" |
|||
} |
|||
|
|||
remote_compose() { $SSH "cd '$REMOTE_DIR' && docker compose --env-file .env $*"; } |
|||
|
|||
# ================= 分发 ================= |
|||
if [ "$ENV" = "local" ]; then |
|||
case "$ACTION" in |
|||
up) local_ensure_network; local_compose up -d ;; |
|||
down) local_compose down ;; |
|||
pull) local_compose pull ;; |
|||
restart) local_compose restart ;; |
|||
logs) local_compose logs -f --tail=120 ;; |
|||
ps) local_compose ps ;; |
|||
build) do_build ;; |
|||
deploy) do_build; local_ensure_network; local_compose pull; local_compose up -d ;; |
|||
*) red "✗ 未知操作: $ACTION"; usage; exit 1 ;; |
|||
esac |
|||
else |
|||
load_target |
|||
case "$ACTION" in |
|||
up) remote_sync; remote_ensure_network; remote_login; remote_compose pull; remote_compose up -d ;; |
|||
down) remote_compose down ;; |
|||
pull) remote_login; remote_compose pull ;; |
|||
restart) remote_compose restart ;; |
|||
logs) remote_compose logs -f --tail=120 ;; |
|||
ps) remote_compose ps ;; |
|||
build) do_build ;; |
|||
deploy) do_build; remote_sync; remote_ensure_network; remote_login; remote_compose pull; remote_compose up -d ;; |
|||
*) red "✗ 未知操作: $ACTION"; usage; exit 1 ;; |
|||
esac |
|||
fi |
|||
|
|||
green "✓ [$ENV] $ACTION 完成" |
|||
@ -1,63 +0,0 @@ |
|||
# yunyan-sas 多环境部署 compose(变量驱动) |
|||
# |
|||
# 不直接用 docker compose 调用,统一通过 deploy.sh: |
|||
# ./deploy.sh local up # 本机 |
|||
# ./deploy.sh dev deploy # 部署到开发服务器 |
|||
# ./deploy.sh prod deploy # 部署到生产服务器 |
|||
# |
|||
# deploy.sh 会把 env/<环境>.env 落为同目录 .env,compose 同时用它做 |
|||
# ① 变量替换:REGISTRY / TAG / PLATFORM / DOCKER_NETWORK / ADMIN_PORT |
|||
# ② 容器环境注入:env_file 指向同一个 .env(POSTGRES_DSN / REDIS_* / NATS_URL / CONSOLE_* 等) |
|||
|
|||
services: |
|||
# ── console 后台控制面服务 ────────────────────────────────── |
|||
yunyan-console: |
|||
image: ${REGISTRY:-docker-registry.ideapsound.com}/yunyan-console:${TAG:-latest} |
|||
container_name: yunyan-console |
|||
restart: unless-stopped |
|||
platform: ${PLATFORM:-linux/amd64} |
|||
|
|||
networks: |
|||
- appnet |
|||
|
|||
# deploy 目录下 console/ 挂为 /app(WORKDIR),含 confs/ log/ |
|||
volumes: |
|||
- ./console:/app |
|||
|
|||
env_file: |
|||
- .env |
|||
|
|||
environment: |
|||
TZ: Asia/Shanghai |
|||
|
|||
expose: |
|||
- "8080" |
|||
|
|||
# ── admin 管理前端 ───────────────────────────────────────── |
|||
yunyan-admin: |
|||
image: ${REGISTRY:-docker-registry.ideapsound.com}/yunyan-admin:${TAG:-latest} |
|||
container_name: yunyan-admin |
|||
restart: unless-stopped |
|||
platform: ${PLATFORM:-linux/amd64} |
|||
|
|||
networks: |
|||
- appnet |
|||
|
|||
environment: |
|||
TZ: Asia/Shanghai |
|||
# server middleware 运行时代理目标(同网络容器名直接解析) |
|||
CONSOLE_BACKEND: http://yunyan-console:8080 |
|||
|
|||
ports: |
|||
- "${ADMIN_PORT:-3000}:3000" |
|||
|
|||
depends_on: |
|||
- yunyan-console |
|||
|
|||
# 外部网络:各环境用各自的网络名(local 用 1panel-network;dev/prod 在 .env 里配 DOCKER_NETWORK)。 |
|||
# 该网络需在目标机预先存在(docker network create <name>),且 redis/nats 可达 |
|||
# (同网络容器名,或在 .env 里用 REDIS_ADDR/NATS_URL 填外部地址)。 |
|||
networks: |
|||
appnet: |
|||
name: ${DOCKER_NETWORK:-1panel-network} |
|||
external: true |
|||
@ -1,37 +0,0 @@ |
|||
# ===== 环境变量模板 ===== |
|||
# 复制为 dev.env / prod.env 并填写真实值。真实文件不要提交 git(见 .gitignore)。 |
|||
|
|||
# ── 镜像 ── |
|||
REGISTRY=docker-registry.ideapsound.com # 私有镜像仓库地址 |
|||
TAG=latest # 镜像 tag(建议 prod 用版本号,如 v1.2.0) |
|||
PLATFORM=linux/amd64 # 目标机 CPU 架构(amd64 / arm64) |
|||
|
|||
# ── Docker 网络 ── |
|||
# 目标机需预先存在此 external 网络:docker network create <name> |
|||
# redis/nats 若在该网络则下面用容器名;否则用外部 IP:端口 |
|||
DOCKER_NETWORK=yunyan-net |
|||
ADMIN_PORT=3000 # admin 对外端口 |
|||
|
|||
# ── PostgreSQL ── |
|||
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require |
|||
# 注意:密码里的特殊字符要 URL 编码(如 & -> %26) |
|||
|
|||
# ── Redis ── |
|||
REDIS_ADDR=redis:6379 |
|||
REDIS_PASSWORD= |
|||
|
|||
# ── NATS ── |
|||
NATS_URL=nats://nats:4222 |
|||
|
|||
# ── COS(可选)── |
|||
COS_SECRET_ID= |
|||
COS_SECRET_KEY= |
|||
|
|||
# ── Console 服务配置 ── |
|||
CONSOLE_TOKEN_KEY=change-me-to-a-strong-secret |
|||
CONSOLE_ADMIN_ACCOUNT=admin |
|||
CONSOLE_ADMIN_PASSWORD=change-me |
|||
CONSOLE_SITE_NAME=云言 SaaS 管理平台 |
|||
|
|||
# ── 依赖服务配置字段加密密钥(强随机,环境间不要复用)── |
|||
FIELD_ENCRYPT_KEY=change-me-to-a-strong-random-key |
|||
@ -1,17 +0,0 @@ |
|||
# ===== 远程部署目标模板 ===== |
|||
# 复制为 dev.conf / prod.conf 并填真实值。真实文件不要提交 git(含密钥)。 |
|||
# deploy.sh 通过这些信息 SSH 到目标机,同步 compose+配置并执行 docker compose。 |
|||
|
|||
# ── 目标服务器 SSH ── |
|||
SSH_HOST=1.2.3.4 # 目标服务器 IP / 域名 |
|||
SSH_PORT=22 |
|||
SSH_USER=root |
|||
SSH_KEY=~/.ssh/id_rsa # SSH 私钥路径(访问密钥) |
|||
|
|||
# ── 远程部署目录(compose / .env / console 配置同步到此)── |
|||
REMOTE_DIR=/opt/yunyan |
|||
|
|||
# ── 私有镜像仓库(远程机 docker login 用)── |
|||
REGISTRY=docker-registry.ideapsound.com |
|||
REGISTRY_USER=admin |
|||
REGISTRY_PASS=change-me |
|||
Loading…
Reference in new issue