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 密码),勿提交 |
# 真实环境变量(含密钥 / DSN / SSH),勿提交;仓库只保留 *.example 模板 |
||||
env/*.env |
*/env/*.env |
||||
targets/*.conf |
*/.env |
||||
.env |
|
||||
|
|
||||
# 保留模板 |
|
||||
!env/example.env |
|
||||
!targets/example.conf |
|
||||
|
|
||||
# 运行时日志 |
# 运行时日志 |
||||
console/log/ |
console/log/ |
||||
|
|||||
@ -1,64 +1,73 @@ |
|||||
# yunyan-sas 部署 |
# yunyan-sas 部署 |
||||
|
|
||||
三环境部署:`local`(本机)/ `dev`(开发服务器)/ `prod`(生产服务器),统一入口 `deploy.sh`。 |
**每个服务目录各一个 `deploy.sh`**,服务名取自所在目录,指令为 `<env> [action]`。 |
||||
服务:`yunyan-console`(Go 控制面,:8080)+ `yunyan-admin`(Nuxt 管理前端,:3000)。 |
每个服务自带单服务 compose + 环境模板;依赖服务(PostgreSQL / Redis / NATS) |
||||
|
由目标环境**共享提供**,不在 compose 内搭建,只在 env 里填连接地址。 |
||||
|
|
||||
|
## 服务 |
||||
|
- `console/` — Go 控制面服务(:8080) |
||||
|
- `admin/` — Nuxt 管理前端(:3000,通过 `CONSOLE_BACKEND` 连 console) |
||||
|
|
||||
## 目录结构 |
## 目录结构 |
||||
``` |
``` |
||||
deploy/ |
deploy/ |
||||
├── deploy.sh # 部署入口 |
└── <service>/ # console / admin |
||||
├── docker-compose.yml # 变量驱动 compose(console + admin) |
├── deploy.sh # 本服务部署脚本(服务名 = 目录名) |
||||
|
├── docker-compose.yml # 单服务 compose(变量驱动,无依赖服务) |
||||
├── env/ |
├── env/ |
||||
│ ├── local.env # 本机运行时变量 |
│ ├── local.env.example |
||||
│ ├── dev.env / prod.env # 各环境运行时变量(占位,需填) |
│ ├── dev.env.example |
||||
│ └── example.env # 模板 |
│ ├── prod.env.example |
||||
├── targets/ |
│ └── (复制填写后的 local.env / dev.env / prod.env) |
||||
│ ├── dev.conf / prod.conf # 远程目标 SSH/目录/仓库(占位,需填) |
└── (console 额外: confs/console.yaml、log/) |
||||
│ └── example.conf # 模板 |
|
||||
└── console/confs/console.yaml # console 配置(变量从 .env 注入) |
|
||||
``` |
``` |
||||
|
> 两个服务的 `deploy.sh` 内容相同,靠所在目录名区分服务。 |
||||
## 首次配置(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 公钥已授权。 |
|
||||
|
|
||||
## 用法 |
## 用法 |
||||
|
进入服务目录运行: |
||||
```bash |
```bash |
||||
# 本机 |
cd deploy/console |
||||
./deploy.sh local up # 起服务(访问 http://localhost:3000) |
./deploy.sh local up # 本机起 console |
||||
./deploy.sh local down |
./deploy.sh dev # 一键发布 console 到开发服务器(动作默认 deploy) |
||||
./deploy.sh local logs |
./deploy.sh prod # 一键发布 console 到生产服务器 |
||||
|
./deploy.sh prod logs # 看生产 console 日志 |
||||
|
|
||||
# 开发服务器(一键:构建推送镜像 + 同步配置 + 远程拉起) |
cd deploy/admin |
||||
./deploy.sh dev deploy |
./deploy.sh local up # 本机起 admin |
||||
./deploy.sh dev ps |
./deploy.sh prod # 发布 admin 到生产服务器 |
||||
./deploy.sh dev logs |
|
||||
|
|
||||
# 生产服务器 |
|
||||
./deploy.sh prod deploy |
|
||||
./deploy.sh prod restart |
|
||||
``` |
``` |
||||
|
|
||||
## 操作矩阵 |
参数: |
||||
| 操作 | local | dev / prod | |
- `env`:`local` | `dev` | `prod` |
||||
|------|-------|-----------| |
- `action`: |
||||
| up | 本机 compose up -d | 同步配置 + 远程 pull + up | |
- `local`:up(默认)| down | restart | logs | ps | pull | build |
||||
| down / restart / logs / ps | 本机 | 远程 | |
- `dev` / `prod`:deploy(默认,构建推送+同步+远程拉起)| up(仅远程拉起)| build | down | restart | logs | ps | pull |
||||
| pull | 本机拉镜像 | 远程拉镜像 | |
|
||||
| build | 本机构建推送镜像(TAG 取自 env) | 同(构建始终在本机) | |
## 首次配置(dev / prod) |
||||
| deploy | build + up | build + 同步 + 远程 pull + up | |
配置分两处:**部署目标/仓库凭据写死在各服务的 `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)。 |
依赖服务(DB/Redis/NATS)共享、由环境预先提供。两个应用服务按序: |
||||
2. SSH 到目标机,创建 `REMOTE_DIR`。 |
1. 先 `console`(admin 依赖它) |
||||
3. scp 同步:`docker-compose.yml` + `env/<环境>.env`(→ 远程 `.env`) + `console/confs/console.yaml`。 |
2. 再 `admin`(同一目标机、同一 `DOCKER_NETWORK`,用容器名 `yunyan-console` 连 console) |
||||
4. 远程:确保 `DOCKER_NETWORK` 网络存在(不存在则自动创建)+ `docker login` 私有仓库。 |
|
||||
5. 远程:`docker compose pull && docker compose up -d`。 |
|
||||
|
|
||||
## 注意 |
## 注意 |
||||
- `env/*.env`、`targets/*.conf` 含密钥,已 `.gitignore` 忽略,**不要提交**。 |
- `env/*.env`(真实)含密钥/DSN,已 `.gitignore` 忽略;仓库只保留 `*.example`。 |
||||
- prod 的 `TAG` 建议固定版本号(如 `v1.2.0`)便于回滚;prod 的 `FIELD_ENCRYPT_KEY` / `CONSOLE_TOKEN_KEY` 务必改强随机值。 |
- ⚠️ 部署目标/仓库密码写死在各服务 `deploy.sh`,而它们**会进 git**——`DEPLOY_HOST`、`REGISTRY_PASS` 等会随仓库提交(私有仓库内可接受;公开前务必清理两个 `deploy.sh`)。 |
||||
- redis / nats:在目标机 `DOCKER_NETWORK` 网络内则用容器名(`redis:6379`);否则在 env 填外部 `IP:端口`。 |
- prod 的 `TAG` 用固定版本号便于回滚;`FIELD_ENCRYPT_KEY` / `CONSOLE_TOKEN_KEY` 用强随机值,环境间不复用。 |
||||
- console 首启会在主库自动建表(含 serviceconfig / calltranslate 等新表)。 |
- 镜像构建复用仓库根目录 `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