Browse Source

上传基础开发版

main
liwei1dao 4 months ago
parent
commit
ec7d512172
  1. 63
      apps/services/Dockerfile.voitrans
  2. 54
      apps/services/entrypoint.voitrans.sh
  3. 7
      apps/services/modules/user/api_v2_getappconfig.go
  4. 2
      apps/services/modules/user/module.go
  5. 2
      apps/services/swagger/docs.go
  6. 2
      apps/services/swagger/swagger.json
  7. 2
      apps/services/swagger/swagger.yaml
  8. BIN
      apps/services/sys/.DS_Store
  9. BIN
      apps/services/sys/ipinfo/ip2region_v4.xdb
  10. BIN
      apps/services/sys/ipinfo/ip2region_v6.xdb
  11. 14
      apps/services/sys/ipinfo/sys_test.go
  12. 2
      deploy/.gitignore
  13. 66
      deploy/voitrans/README.md
  14. 51
      deploy/voitrans/confs/api.yaml
  15. 31
      deploy/voitrans/confs/gateway.yaml
  16. 206
      deploy/voitrans/confs/home.yaml
  17. 59
      deploy/voitrans/confs/mcp.yaml
  18. 27
      deploy/voitrans/confs/timer.yaml
  19. 79
      deploy/voitrans/dev-deploy.sh
  20. 57
      deploy/voitrans/docker-compose.yml
  21. 45
      deploy/voitrans/env/dev.env.example
  22. 35
      deploy/voitrans/env/japan.env.example
  23. 36
      deploy/voitrans/env/shanghai.env.example
  24. 35
      deploy/voitrans/env/singapore.env.example
  25. 44
      deploy/voitrans/prod-build.sh
  26. 105
      deploy/voitrans/prod-deploy.sh

63
apps/services/Dockerfile.voitrans

@ -0,0 +1,63 @@
# voitrans 业务单镜像:gateway + home + api + mcp + timer 五个集群(RPCX/ETCD)服务打进一个镜像
# 构建上下文:apps/services/
# 用 shell 脚本(entrypoint)管理多进程:任一服务退出即整个容器退出,交给 docker restart 策略拉起。
# 运行期配置 confs/ 由宿主机 deploy/voitrans/ 目录挂入当 /app(见 docker-compose.yml)。
# ============ Stage 1: Builder ============
FROM golang:1.25-alpine AS builder
ENV GOPROXY=https://goproxy.cn,direct \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
WORKDIR /src
# 预热依赖缓存
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
# 编译 5 个服务(统一 .a11 后缀,与镜像名 voitrans-a11 对齐)
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
mkdir -p /out && \
go build -trimpath -ldflags "-s -w" -o /out/gateway.a11 ./services/gateway/ && \
go build -trimpath -ldflags "-s -w" -o /out/home.a11 ./services/home/ && \
go build -trimpath -ldflags "-s -w" -o /out/api.a11 ./services/api/ && \
go build -trimpath -ldflags "-s -w" -o /out/mcp.a11 ./services/mcp/ && \
go build -trimpath -ldflags "-s -w" -o /out/timer.a11 ./services/timer/
# ============ Stage 2: Runtime ============
FROM alpine:3.20
# nc 用于 compose 健康检查探测端口
RUN apk add --no-cache tzdata ca-certificates && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 二进制与启动脚本放 /opt/voitrans,避免与宿主机挂载的 /app 冲突
COPY --from=builder /out/gateway.a11 /opt/voitrans/a11/gateway.a11
COPY --from=builder /out/home.a11 /opt/voitrans/a11/home.a11
COPY --from=builder /out/api.a11 /opt/voitrans/a11/api.a11
COPY --from=builder /out/mcp.a11 /opt/voitrans/a11/mcp.a11
COPY --from=builder /out/timer.a11 /opt/voitrans/a11/timer.a11
COPY entrypoint.voitrans.sh /opt/voitrans/entrypoint.sh
RUN chmod +x /opt/voitrans/entrypoint.sh /opt/voitrans/a11/*.a11 && \
mkdir -p /app
# 工作目录:宿主机把 deploy/voitrans 目录挂为 /app,内含 confs/ log/ 及各服务运行期资源
WORKDIR /app
# 端口声明(容器内部,对外经反代/compose 暴露)
# gateway: 7001 RPCX / 7100 HTTP
# home: 7002 RPCX
# timer: 7003 RPCX
# mcp: 7004 RPCX / 7300 HTTP(MCP)
# api: 7005 RPCX / 8080 Console
EXPOSE 7001 7002 7003 7004 7005 7100 7300 8080
ENTRYPOINT ["/opt/voitrans/entrypoint.sh"]

54
apps/services/entrypoint.voitrans.sh

@ -0,0 +1,54 @@
#!/bin/sh
# voitrans 容器入口:在一个容器内拉起 5 个集群服务,shell 监控进程。
#
# 运行时布局(宿主机挂 deploy/voitrans 目录为 /app,二进制在镜像内 /opt/voitrans):
# /app/ ← 宿主机 deploy/voitrans 目录
# ├── confs/*.yaml ← 各服务配置(gateway/home/api/mcp/timer)
# ├── log/ ← 日志输出
# └── ... ← 其他运行期资源(密钥/词库等,如有)
# /opt/voitrans/a11/*.a11 ← 镜像内二进制
#
# cwd=/app,所以配置里的相对路径 ./confs/... ./log/... 均直接生效。
# 任一服务进程退出 → 杀掉其余 → 容器退出码 1 → docker 按 restart 策略整体拉起。
set -e
cd /app
A11=/opt/voitrans/a11
echo "[entrypoint] starting voitrans services..."
# gateway 是 HTTP 入口 + 服务发现枢纽,先起;home/api/timer 随后;mcp 依赖 gateway 发现,最后起。
${A11}/gateway.a11 -conf ./confs/gateway.yaml & PID_GW=$!
${A11}/home.a11 -conf ./confs/home.yaml & PID_HOME=$!
${A11}/api.a11 -conf ./confs/api.yaml & PID_API=$!
${A11}/timer.a11 -conf ./confs/timer.yaml & PID_TIMER=$!
# mcp 依赖 gateway 的服务发现,稍等再起
sleep 3
${A11}/mcp.a11 -conf ./confs/mcp.yaml & PID_MCP=$!
echo "[entrypoint] gateway=$PID_GW home=$PID_HOME api=$PID_API timer=$PID_TIMER mcp=$PID_MCP"
ALL_PIDS="$PID_GW $PID_HOME $PID_API $PID_TIMER $PID_MCP"
shutdown() {
echo "[entrypoint] received shutdown signal, stopping services..."
kill -TERM $ALL_PIDS 2>/dev/null || true
wait
echo "[entrypoint] all services stopped, exiting"
exit 0
}
trap shutdown TERM INT
# 守护:任一进程消失即整体退出(交给 docker restart)
while :; do
for pid in $ALL_PIDS; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "[entrypoint] FATAL: pid $pid exited, shutting down container"
kill -TERM $ALL_PIDS 2>/dev/null || true
wait
exit 1
fi
done
sleep 2
done

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

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

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

@ -23,6 +23,7 @@ type User struct {
modules.ModuleBase
service base.IRPCXService //rpcx服务接口 主要client->server
api *apiComp
apiv2 *apiV2Comp
model *modelUserComp
configmodel *modelConfigComp
cache *modelCacheComp
@ -57,6 +58,7 @@ func (this *User) Start() (err error) {
func (this *User) OnInstallComp() {
this.ModuleBase.OnInstallComp()
this.api = this.RegisterComp(new(apiComp)).(*apiComp)
this.apiv2 = this.RegisterComp(new(apiV2Comp)).(*apiV2Comp)
this.model = this.RegisterComp(new(modelUserComp)).(*modelUserComp)
this.cache = this.RegisterComp(new(modelCacheComp)).(*modelCacheComp)
this.configmodel = this.RegisterComp(new(modelConfigComp)).(*modelConfigComp)

2
apps/services/swagger/docs.go

@ -2094,7 +2094,7 @@ const docTemplate = `{
}
}
},
"/api/home/user_getappconfig_v2": {
"/api/home/user_getappconfigv2_v2": {
"post": {
"security": [
{

2
apps/services/swagger/swagger.json

@ -2086,7 +2086,7 @@
}
}
},
"/api/home/user_getappconfig_v2": {
"/api/home/user_getappconfigv2_v2": {
"post": {
"security": [
{

2
apps/services/swagger/swagger.yaml

@ -4256,7 +4256,7 @@ paths:
summary: 获取应用配置
tags:
- User
/api/home/user_getappconfig_v2:
/api/home/user_getappconfigv2_v2:
post:
consumes:
- application/json

BIN
apps/services/sys/.DS_Store

Binary file not shown.

BIN
apps/services/sys/ipinfo/ip2region_v4.xdb

Binary file not shown.

BIN
apps/services/sys/ipinfo/ip2region_v6.xdb

Binary file not shown.

14
apps/services/sys/ipinfo/sys_test.go

@ -1,9 +1,9 @@
package ipinfo_test
import (
"yunyan/sys/ipinfo"
"fmt"
"testing"
"yunyan/sys/ipinfo"
)
func TestSys(t *testing.T) {
@ -17,12 +17,12 @@ func TestSys(t *testing.T) {
defer sys.Close()
cases := []string{
"36.152.160.156", // CN
"8.8.8.8", // US
"1.1.1.1", // AU
"203.129.64.1",
"103.110.204.1",
"240e:3b7:3272:d8d0:db09:c067:8d59:539e", // CN v6
"43.153.96.16", // CN
// "8.8.8.8", // US
// "1.1.1.1", // AU
// "203.129.64.1",
// "103.110.204.1",
// "240e:3b7:3272:d8d0:db09:c067:8d59:539e", // CN v6
}
for _, ip := range cases {
info, err := sys.GetIPInfo(ip)

2
deploy/.gitignore

@ -1,6 +1,8 @@
# 真实环境变量(含密钥 / DSN / SSH),勿提交;仓库只保留 *.example 模板
*/env/*.env
*/.env
*/.env.infra
# 运行时日志
console/log/
voitrans/log/

66
deploy/voitrans/README.md

@ -0,0 +1,66 @@
# voitrans 业务服务部署
把 **gateway + home + api + mcp + timer** 五个集群(RPCX/ETCD)服务打成**一个镜像** `voitrans-a11`,
推送到私有仓库 `docker-registry.ideapsound.com`,再到目标机拉取重启。容器内用 shell(entrypoint) 管理 5 个进程,
任一进程退出即整体重启。
## 文件
| 文件 | 说明 |
|---|---|
| `../../apps/services/Dockerfile.voitrans` | 镜像构建(编译 5 个二进制 + entrypoint),构建上下文 = `apps/services/` |
| `../../apps/services/entrypoint.voitrans.sh` | 容器入口,拉起并守护 5 个服务进程 |
| `docker-compose.yml` | 运行镜像,把本目录挂为容器 `/app`,`.env` 注入容器 |
| `confs/*.yaml` | 5 个服务的运行配置(含真实密钥,随仓库提交);基础设施端点用 `${VAR}` 由 `.env` 注入 |
| `env/<env>.env.example` | 运行时配置模板(真实 `.env` 被 gitignore,需复制后填写) |
| `dev-deploy.sh` | **开发**:构建 dev 镜像 → 推送 → 更新开发环境(一条命令) |
| `prod-build.sh` | **生产①**:构建带版本号镜像并推送(`:版本` + `:latest`),不部署 |
| `prod-deploy.sh` | **生产②**:选区域 + 版本 tag 部署,支持回滚 |
## 用法
首次:复制并填写 env(真实文件已 gitignore)
```bash
cd deploy/voitrans
cp env/dev.env.example env/dev.env
cp env/shanghai.env.example env/shanghai.env # 生产区域同理 japan / singapore
# 编辑 env/*.env,填 DSN / Redis / ETCD / NATS / 统计应用名等(CHANGE_ME 处)
```
开发环境(一键发布)
```bash
./dev-deploy.sh # 构建并发布 dev-latest
./dev-deploy.sh dev_meet-latest # 指定 tag
```
生产环境(构建与部署分离)
```bash
./prod-build.sh 0.1.1 # 构建并推送 voitrans-a11:0.1.1 (+latest)
./prod-deploy.sh shanghai 0.1.1 # 部署 0.1.1 到上海
./prod-deploy.sh japan 0.1.1 # 部署到日本
./prod-deploy.sh singapore 0.1.1 # 部署到新加坡
./prod-deploy.sh shanghai rollback # 上海回滚到上一版本(与当前版本来回切换)
```
## 需要填写 / 提供的东西
- **部署目标与凭据**:写死在各脚本顶部。
- `dev-deploy.sh`:`DEV_HOST`(开发服务器 IP)。
- `prod-deploy.sh`:`region_profile()` 里各区域的 `DEPLOY_HOST`。
- 仓库账号 `admin` / 密码已写死;`DEPLOY_KEY` 默认 `~/.ssh/id_rsa`。
- ⚠️ 本目录脚本纳入 git,**公开仓库前务必清空** `*_HOST` 与 `REGISTRY_PASS`。
- **基础设施端点**:填 `env/<env>.env` 的 `ETCD_SERVERS / DB_ADMIN_DSN / DB_DEVICE_DSN / REDIS_* / NATS_URL`,
以及统计上报 `ANALYZE_APP_NAME`(须与 console 注册表应用名一致)/ `ANALYZE_REGION`、集群隔离 `CLUSTER_TAG`。
- **confs 引用的附属文件**(按需放入本目录,会随 `confs/`/工作目录挂入容器 `/app`):
- `home.yaml`:`./smart-bluetooth-447104-2b269474bd72.json`(Google)、firebase/google 登录 json、
`./apiclient_key.pem`(微信支付)、`confs/ip2region_v4.xdb` 与 `v6.xdb`(IP 库)、
`wordfilter/*.txt`(敏感词,不需要则删掉 `home.yaml` 的 `wordfilter` 段)。
- 缺这些文件会导致对应服务启动失败(容器反复重启),用不到的集成请删掉对应配置段。
## 端口(容器内,对外经反代/NPM 暴露)
`gateway` 7001 RPCX / 7100 HTTP · `home` 7002 · `timer` 7003 · `mcp` 7004 / 7300 HTTP · `api` 7005 / 8080 Console。
依赖的 `etcd / mysql / redis / nats` 须在 `DOCKER_NETWORK`(默认 `1panel-network`)网络内,用容器名连接。

51
deploy/voitrans/confs/api.yaml

@ -0,0 +1,51 @@
tag: "${CLUSTER_TAG:-voitrans-a11}" #服务集群标签,按 .env 的 CLUSTER_TAG 隔离不同部署
type: "api"
version: 1.0
exte: #RPCX 相关配置
Port: 7005
Addr: 127.0.0.1:7005
ETCDServers: ["${ETCD_SERVERS:-etcd:2379}"]
UpdateInterval: 5
sys:
log:
FileName: "./log/api.log"
Loglevel: 5
MaxAgeTime: 7
IsDebug: ${LOG_DEBUG:-false}
mysql: #业务库
MySQLDsn: ${DB_ADMIN_DSN:-root:li13451234@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local}
postgres: #公共库
Dsn: ${DB_DEVICE_DSN:-postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable}
ReadDsn: ${DB_DEVICE_READ_DSN:-}
MaxPoolSize: 5
redis:
Addr: ["${REDIS_ADDR:-redis:6379}"]
Password: ${REDIS_PASSWORD:-li13451234}
DB: ${REDIS_DB:-1}
KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans}
cos:
SecretID: AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA
SecretKey: OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc
AppId: "1253517901"
Region: ap-guangzhou
BucketName: "deepsound-1253517901"
BucketURL: "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com"
translate: #Azure Translator,按需填写
Key: "${AZURE_TRANSLATE_KEY:-}"
Region: "eastasia"
Endpoint: "https://api.cognitive.microsofttranslator.com"
email:
EmailType: 1
FromEmail: liwei@yunqiinnovation.com
FromName: DeepSound
Password: eNdXWrjSQbdYtwbt
modules:
api:
TokenKey: "${GATEWAY_TOKEN_KEY:-asd123adqeqwe}"
AdninAccount: ${API_ADMIN_ACCOUNT:-admin}
AdninPassword: "${API_ADMIN_PASSWORD:-123}"
ConsolePort: 8080
SiteName: "${API_SITE_NAME:-Voitrans}" #后台站点名称(不同环境可区分)
CurrencyType: ${API_CURRENCY_TYPE:-0} #0=人民币(元) 1=美元(USD)

31
deploy/voitrans/confs/gateway.yaml

@ -0,0 +1,31 @@
tag: "${CLUSTER_TAG:-voitrans-a11}" #服务集群标签:同标签且 ETCD 一致才能互相发现,按 .env 的 CLUSTER_TAG 隔离不同部署
type: "gateway" #服务类型
version: 1.0
exte: #RPCX 相关配置(同一容器内 5 服务用 127.0.0.1 互通;ETCD 为独立容器,经 .env 注入容器名)
Port: 7001
Addr: 127.0.0.1:7001
ETCDServers: ["${ETCD_SERVERS:-etcd:2379}"]
UpdateInterval: 5
sys:
log:
FileName: "./log/gateway.log"
Loglevel: 5
MaxAgeTime: 7
IsDebug: ${LOG_DEBUG:-false}
# gateway 不接存储
modules:
gateway:
ListenPort: 7100
TokenKey: "${GATEWAY_TOKEN_KEY:-asd123adqeqwe}"
WhiteList:
- user_sgin
- api_getuser
- user_getappconfig
- user_getchannelapp
- user_verification
- public_info
- pay_wechatnotify
- pay_alipaynotify
Debug: ${LOG_DEBUG:-false}

206
deploy/voitrans/confs/home.yaml

@ -0,0 +1,206 @@
tag: "${CLUSTER_TAG:-voitrans-a11}" #服务集群标签,按 .env 的 CLUSTER_TAG 隔离不同部署
type: "home"
version: 1.0
exte: #RPCX 相关配置
Port: 7002
Addr: 127.0.0.1:7002
ETCDServers: ["${ETCD_SERVERS:-etcd:2379}"]
UpdateInterval: 5
sys:
log:
FileName: "./log/home.log"
Loglevel: 5
MaxAgeTime: 7
IsDebug: ${LOG_DEBUG:-false}
nats: #统计快照管道(推送到 console 统计消费端);连不上不阻断业务
URL: "${NATS_URL:-nats://nats:4222}"
Name: "home"
MaxReconnects: -1
ReconnectWait: 2
# 敏感词过滤:需把词库 txt 放入 deploy/voitrans/wordfilter/。无需要时整段删除即可。
wordfilter:
WorldFile:
- ./wordfilter/暴恐词库.txt
- ./wordfilter/补充词库.txt
- ./wordfilter/反动词库.txt
- ./wordfilter/非法网址.txt
- ./wordfilter/广告类型.txt
- ./wordfilter/老版本词库.txt
- ./wordfilter/零时-Tencent.txt
- ./wordfilter/民生词库.txt
- ./wordfilter/色情词库.txt
- ./wordfilter/色情类型.txt
- ./wordfilter/涉枪涉爆.txt
- ./wordfilter/贪腐词库.txt
- ./wordfilter/网易前端过滤敏感词库.txt
- ./wordfilter/新思想启蒙.txt
- ./wordfilter/政治类型.txt
- ./wordfilter/COVID-19词库.txt
- ./wordfilter/GFW补充词库.txt
mysql: #业务库(用户/设备/统计/任务等 per-app 业务数据)
MySQLDsn: ${DB_ADMIN_DSN:-root:li13451234@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local}
postgres: #公共库(厂家/产品/设备码/公码等跨 app 共享数据)
Dsn: ${DB_DEVICE_DSN:-postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable}
ReadDsn: ${DB_DEVICE_READ_DSN:-} #只读副本,填了就开读写分离;不填=单库
MaxPoolSize: 5
redis:
Addr: ["${REDIS_ADDR:-redis:6379}"]
Password: ${REDIS_PASSWORD:-li13451234}
DB: ${REDIS_DB:-1}
KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans} #key 应用分组前缀
sms: #短信服务
AppId: "1400971698"
SecretId: "AKIDmiaJsJr6C8sPkt22xgFdswnTyHNG1c3J"
SecretKey: "BByYxDH9oIafXCmALJ5f4GGQWcaMwPta"
SignName: "深圳市创想云启"
Template1: "2397800"
Template2: "2385564"
google: #需放置 ./smart-bluetooth-447104-2b269474bd72.json
JsonPath: ./smart-bluetooth-447104-2b269474bd72.json
deepseek:
Appkey: sk-3adfd188a3134e718bbf704f525aff17
email:
EmailType: 1
FromEmail: liwei@yunqiinnovation.com
FromName: DeepSound
Password: eNdXWrjSQbdYtwbt
firebase_auth: #需放置对应 json
ApiKeyFile: ./deepsound-e0e3d-firebase-adminsdk-fbsvc-269043a0f8.json
google_auth: #需放置对应 json
ApiKeyFile: ./deepsound-e0e3d-firebase-adminsdk-fbsvc-269043a0f8.json
facebook_auth:
AppID: "3829022037361855"
AppSecret: b7c9b0e5d2eafc9376152a5bc9cccc0a
wechat_auth:
AppID: wx4b5ec8957ad5e425
AppSecret: 9ceb39df312b3ba95a229b2aa0d15496
musicobj:
ApiBaseUrl: https://hw.music.voitrans.net
cos:
SecretID: AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA
SecretKey: OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc
AppId: "1253517901"
Region: ap-guangzhou
BucketName: "deepsound-1253517901"
BucketURL: "https://deepsound-1253517901.cos.ap-guangzhou.myqcloud.com"
wechatpay: #微信支付,需放置 ./apiclient_key.pem
AppId: wx4b5ec8957ad5e425
MchId: "1719983673"
ApiV3Key: "rV7cE1xZ9bN3mK5jH8gT0wQ2sF4dG6yU"
PrivateKeyPath: ./apiclient_key.pem
CertSerialNo: "2D27CEB52B98DDEB1CC65E4D96A041E7408F58D0"
alipay: #支付宝支付
AppID: "2021005135671159"
PrivateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCCNjHeCrB24HHGBoU3Vj/TxTxveW4pU4fktVSd5ZpACsU1vYO9VsBIiIDSOvIOTlsiXTS09940qc/6sgggyPard423Jxb/v+W9D0ZgKa/iMwdTJ2aTks1CCXyanIl3DSkyJrPfFlm2N/ZbPBvgup20QISQtnde1MRtPCMMyodDvvjkVzRYFfitzNS5fM85Ym7uKpfao32tYem0LKnMe8HUwp1AY41pzYDjzvSzWfYzOuLtvO/GnP3m8EAmGeY9WLkCU5BGNS8UJz6WcCZUqgkuAYoYjt0+qcPGIctU0RYs9KVcK1UGJs22GK6oxNHalbT2TluI8exaSDvNObMQmGJtAgMBAAECggEAAxt//SiMJYodrPgi3bs1MryWQKLJOjtAFvks18fO8pBPPIUVS8NyO0jmqh22ARllm2BWDauMpf0XdYgiIYYqPNVRkHliVyAhP23J5CmahO9q0VUzYXozY8wOKTJrJWgu/39k84H5ChwzyOGaWLabZN3VTA7OsnipZ2H0BC4hJEC1R/mJIybul7WveP1RlnYd5L9EwGqj/k0UQKeBfEltH/FBiiWlnukruxW9TjYxkydOsLB7rRYdnBCnVbdy65gi/XX/fIxVmNiHauzfO3yksktdS54VPmQHnrl0Xc38xThbbxGmMxRXuS1bZQwuxkz1iJsh4K0eKvCKFl5EnVWsgQKBgQD9bzuD7Pp1aMXHxS4REYk85TsT0peGbNFfBfKrDb42oirccELiIhKM6TDl7JGHQA3kY+wN4I8VONJFlxfjZ9neXbhZanLqNr3Z8OEjejJ7cN2+WWERIwGboaAEf5qqKFuk+hUKdoXnhFMwTnFJfUQr0EV9+aUHVKEYBK/2IFHCtQKBgQCDh6JhvPAD0b/RlzTJUnRPB2+ZCM0LZQ3VCUYVsdLrOBh+IMrv74oSDgTJYglJaOINmQKVCcVyeLahII6skOcwJrxzVOnbQowNAITWRd0BzNfqB/Sx11s5Xpomi39Uk8yeRHJmmGDudQguvJrtcQEH6QkdGZz2CSNe41LoN0Rb2QKBgFu76YUFQFtDQ2PeIaCYAl8CcnHoZmSH+vk0Q/RakyFfbbhBAlSsFJ7gwYxYJw0XyG+FI6qaP/BkkugRmtn7e0ccEWB8QYA5tFqw8C1RS/iWkA0YeSQk0a4twKggMDGrThDXXMpewHVe1mXD3wnEx+o8BwtkU7HlljIFcFPcuvuJAoGAMKuxB8+9VPx5qZn1O4YaaeALiKp8DXpgavOajo6VyNqiHZIO5khSZCnx1X9EhnicJYH4NaQ+/wPyOJ9ujCa2ldfPpjHGs47lei41+YAyrubZAs3c2y4goJMEIb0+6dIjRnF3j8VVIQgHdhvujGxjgETYTX9b2KM7BnLCDBHfZskCgYEA9Mz4RMZbmyqsZ1gtkioYmyn1MSaaVu/fMJzVwLyHi0TicXpG67kLM7KRBZq5tdySQUhq7ZVkQ+jwHxyuO9iJsKD5ye1KTgyRhh1URMe6/YLRf5170805XIfSFn0jLmQ6rWwaftcWaDp8OjMR/wJDHe6vJcqttle7IuJ6w481AQ8=
PublicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuLaTAzkBwcfy9Vg6Kj8NSNPQ5IeYyT+UKvOoPYiBw85SU7b8/gc1Lhgvo0DVfhM0MI67qf1GY69l3f6/f8mk6CcnhqC6iY0AcQnXqzQ4RBM/9s8/GCaqOohcv8LvLtbz+RlxIPu4dnTkrgEan7z+XZz7vUgHN7vQOcnPj/stfnxJeOANNLgCOYm/XDJZa8yV7lFux750d5+HE3m1jXXHAPTt0Pp+iHIzUZZkIEPaaz46tvoNi0cUf/+BJyq0DLIeWvTv2HpquVV3ZiiFYyjG3FnxWx+BFz/JCYfnPM31gQBt+EgwO3Oc9Kb5JetWrgmjgeoHwL6JIX3mCapZlJlxlwIDAQAB"
paypal: #沙盒;正式环境换 ClientID/Secret/BaseURL/WebhookID
ClientID: AUjApGHJm31MT101xA6koKAnZBZvo6ZxzRYAyXRXFQjlBYoj2i7Hg33clMIaIqyziFhmY0TX_EN_hgDL
Secret: EKlnsMTODfMFfOsnkfyifDRexw3dPk9Ldzv5b-a3Yu-Gq0cYFF7roQsrSaBv572kW9n_u25lHBDabemq
BaseURL: https://api-m.sandbox.paypal.com
WebhookID: 0PR35225Y36017238
appleiap: #苹果内购
AppStoreIssuerID: 96c4b3eb-5e5c-4719-8b07-db9cc4dc1f89
AppStoreKeyID: VS538D3ZR5
AppStorePrivateKeyPEM: |
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgTadGz/swzKnpENyr
7JW6aH32ggaFCRKSakkoVPBc0PWgCgYIKoZIzj0DAQehRANCAASafAEhHQegdDNn
TE8bPW50cqrX+cFACKK5ZHoRbIYdp2BE3Ji8NSK7mE9avnLahGhyIfyTeSc7ZLyY
4WlcCJCm
-----END PRIVATE KEY-----
AppStoreBundleID: com.saitong.voitrans
UseSandbox: true
googleiap:
ServiceAccountJSON: |
{
"type": "service_account",
"project_id": "deepsound-451512",
"private_key_id": "56adf297cf60cfa27cd31c7b35db8678164fd311",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCteoHZrBT5F82D\nbbCzDf7jlrtPLag5hZUiA50Ex4HjSQrRMi1z5a/Px6sP4/wMEj5K+huyvkRv1cGN\nFbSSMBo2HMnoQIVKHy0HTZ3g/oJxlHMeJDLz300pAfTAJx8qvauV5KD/Z0wWqrAE\nrdwiU6VZbFIoIb2XfIuB3U0v8FsaLX0+yUG+kuRWi+kKCOOPUNneS+Ppu6NGR+98\nyA94uVzBXkIdUieSpY4/6WznzeKR55UWcjsZ4EVqiZ1xoP0nduUces4Ph8kQkIEw\nX/albSUeX4yfHdKnEmQ+W1y9TfW5PSBKno452vkiI1ZivK6I2LrvCxINe8saCOko\ng8B5lTqzAgMBAAECgf8Om16Mg7frCIfTpaGtACtZqR6ONtgsOKFydKKLGc+dGsJA\nMGsxj+yTIHW4p+t0pvgmArBSXJaDDQrNSCMInefP+/Hn1U4Ofeba73U/Q70NYXKI\n3kqIdyh52bBQRMIiLIDwyME3atCLidO6H1tNT7245kmS3pnC0kgJUe5KU/KtJQPl\nHhO59v3HyBSNW5fBjj9KAoJlgLK7IAtDynupIkFik4Gt2nqI9F9wbmgwAtQx1HhT\n4mbvDEv+k/GmvZLd60lt/kM625HNnuYKm4NYzdTpk1ryERgvCpAwQj84CvWncqom\nb2RBY7SFbE//Twe8Cr7gIKmTWivyDMdp/JiPeIECgYEA2W/fcxTD143TfxEb5402\ne85XYpB8bWew4R7W888HE0BKQbGYQFi4Z3np5ogw9T3VqYd9ct5AeRq2u6ZM82OJ\n0sHxDybTzTDAPx7/ii6cz43zut2bXoEI2YyfguW+FsnVtH6ZM94DlnaUuvClbpHE\n7yWqdicT73yxQ3+T3HNfTiECgYEAzD7SYv3D4vGvgwon9rJ7+kw+KBHYeloXFu5q\n3Y6TnRLpvgxmNxEm/SYD/bRhWbsXONyPvoQiVl0v/BpKxIf4cS5OD2SikmAxfYDP\nSA7CVkrsmWwAqJiMi+yXAlq3+kmTJAjbSj1zkJjhE9BSTfeKIoo5OyLu0XsKbK+t\nyw1WJlMCgYEA2M9Ye6PE8/aTYSxqKqUAIstJDxsLNvwhvxfFNi7E20lLwd2X9vGA\nTo2d0SUTzDXwCq7zQ8qhdyBP/SSBrl8eWmzQNU99+igWSdpa7Co2ai8dTMMT3VPE\nw0gY9aintoli8isaEEHcSffLreuuyWXIfL2OD2o/0pj/GFLp2POhoIECgYEAy8P1\nE0uG7VgfuQUd37IrnAgKjuFW7qIInRkZRpbiz/c5eSBXraKQGkid/LPD18ien1z0\nj6a1tyHbKJF2fGIGy6vdtt+4jX4ogcRLh+Epovu21k14zrr6pPJRqcil+a8DfBTV\n6lgqflz3WFQieKR4jIMIG+Z7CkTNnqcaJRTWGP8CgYB5HpozVNaJDuokXobxx4dG\nTo2wUvC67EW8TtpI/GD6xS6h5n9ZAXr4e08pSnG5M3784N4mDFy83Z8nEpSPISC7\nCy+QEREzTRx6LBRMe79eqo79Y8CuHaZKnubB8xgc87zWEjufXxfe5gZX8hccTFUv\nhnIdLDXB7hgvboB/GzjJ8Q==\n-----END PRIVATE KEY-----\n",
"client_email": "deeapsound@deepsound-451512.iam.gserviceaccount.com",
"client_id": "104070580817776670469",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/deeapsound%40deepsound-451512.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
doubao: #allhelp 模块 AI 总结用
Apikey: "55242c1c-204e-4e2a-ae75-faa846180682"
Model: "doubao-1-5-pro-32k-250115"
ipinfo: #需放置 confs/ip2region_v4.xdb 与 v6.xdb
V4XdbPath: "./confs/ip2region_v4.xdb"
V6XdbPath: "./confs/ip2region_v6.xdb"
CachePolicy: "vindex"
Searchers: 10
ali_filetrans:
ApiKey: "sk-4598b2ed68e0484691b90da8c234b0f8"
modules:
analyze: #平台统计模块(推送快照到 console)
AppName: "${ANALYZE_APP_NAME:-voitrans-a11}" #须与 console 注册表中的应用名完全一致,否则不落库
Region: ${ANALYZE_REGION:-1} #本部署所属区域,取值见 pb.Region
SyncInterval: 600
Subject: "stats.event" #须与 console 端 Stat.Subject 一致
Timezone: "Asia/Shanghai"
user:
Debug: ${LOG_DEBUG:-false}
AvatarUrl: "https://api.ideapsound.com/avatar/"
TokenKey: "${GATEWAY_TOKEN_KEY:-asd123adqeqwe}"
CBCKey: gDSiGlKJIGuu84ZNe+j4FQ==
allhelp:
max_summary_process: 5
echomeet: #会议记录:四套服务商各自的转写/翻译/总结配置
Debug: ${LOG_DEBUG:-false}
DefaultService: "Auto"
ByteDance:
Callback: ""
Transcribe:
AppID: "8595121680"
Token: "ERAUYbJQBOv6M0ooFnUwo-tTWs08HWZn"
Translate:
AccessKey: "AKLTZjJmODdkODQwN2RjNDcyMWI4NWM2ZWRmNDkxY2RiYzI"
SecretKey: "T0Roa09EZGlNalZpTjJObE5HVmlZMkV3WVRkbVpUTTBORGMzTW1FNFlqYw=="
Region: "cn-beijing"
Summary:
Apikey: "55242c1c-204e-4e2a-ae75-faa846180682"
Model: "doubao-1-5-pro-32k-250115"
Ali:
Callback: ""
Transcribe:
ApiKey: "sk-4598b2ed68e0484691b90da8c234b0f8"
Translate:
AccessKeyId: "LTAI5t9dLKEg8zHLbfZJcJZn"
AccessKeySecret: "8NicfnXpHSeQa6ARvCNzXsNqZe8wr1"
Region: "cn-hangzhou"
Summary:
BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
Token: "sk-4598b2ed68e0484691b90da8c234b0f8"
Model: "qwen-plus"
Google: #需放置 ./smart-bluetooth-447104-2b269474bd72.json
Transcribe:
JsonPath: ./smart-bluetooth-447104-2b269474bd72.json
Translate:
JsonPath: ./smart-bluetooth-447104-2b269474bd72.json
Summary:
Apikey: "AQ.Ab8RN6Iek4x3Pd5NZcuLrXzrapFor_gAoO9W5H-Fg2p8F2M64g"
Model: "gemini-2.5-flash"
Microsoft: #未配置则该服务商不可用
Transcribe:
Key: ""
Region: "eastasia"
Translate:
Key: ""
Region: "eastasia"
Endpoint: "https://api.cognitive.microsofttranslator.com"
Summary:
BaseURL: ""
Token: ""
Model: ""
pay:
Debug: ${LOG_DEBUG:-false}
WxAppId: "wx5bbb7be9324f3e2d"
WxAppSecret: "4cfde33bc22b6cc6b1f8b225dd97d255"
WxMchId: "1652202222"
WxPayPayKey: "rV7cE1xZ9bN3mK5jH8gT0wQ2sF4dG6yU"

59
deploy/voitrans/confs/mcp.yaml

@ -0,0 +1,59 @@
tag: "${CLUSTER_TAG:-voitrans-a11}" #服务集群标签,按 .env 的 CLUSTER_TAG 隔离不同部署
type: "mcp"
version: 1.0
exte: #RPCX 相关配置
Port: 7004
Addr: 127.0.0.1:7004
ETCDServers: ["${ETCD_SERVERS:-etcd:2379}"]
UpdateInterval: 5
sys:
log:
FileName: "./log/mcp.log"
Loglevel: 5
MaxAgeTime: 7
IsDebug: ${LOG_DEBUG:-false}
mysql: #mcp 仅用业务库读写 allhelp_task / user 表
MySQLDsn: ${DB_ADMIN_DSN:-root:li13451234@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local}
amap:
Appkey: c653e53f94ee88a43cc979a08deae883
juhe:
SimpleWeather_ApiKey: efd8f9f391d73d7337344d26ee93d173
Finance_ApiKey: 50a101c9c5ff7c967598cea4bcc8ebbd
Toutiao_ApiKey: 035a1d052001f6f87358a159e7c44575
spotify:
ClientID: "c111d7aa47ef4fb8a60c705f117200b0"
ClientSecret: "27d9a850f1fe4d749103eec4d5a2a106"
RedirectURI: "http://localhost:8080/callback"
musicobj:
ApiBaseUrl: https://hw.music.ideapsound.com
bravesearch:
ApiKey: BSAyKXEKw6ftwPHD5HaTX4Ybr3Bfd6b
modules:
mcp:
Port: 7300
Addr: "${MCP_ADDR:-http://localhost:7300}"
TLSCertFile: ""
TLSKeyFile: ""
Groups:
GLOBAL:
- hefeng_weather
- music_search
- music_play
- add_user_task
- get_user_tasks
- cancel_user_task
OVERSEAS:
- google_map_nearbysearch
- google_map_geocode
- google_map_regeocode
- google_map_route_navigation
- tavily_web_search
CHINA:
- gaode_map_around_search
- gaode_map_geocode
- gaode_map_regeocode
- gaode_map_route_navigation
- gaode_map_search_place
- bocha_web_search

27
deploy/voitrans/confs/timer.yaml

@ -0,0 +1,27 @@
tag: "${CLUSTER_TAG:-voitrans-a11}" #服务集群标签,须与其余服务一致才能同集群
type: "timer"
version: 1.0
exte: #RPCX 相关配置(timer 也接 ETCD 参与集群)
Port: 7003
Addr: 127.0.0.1:7003
ETCDServers: ["${ETCD_SERVERS:-etcd:2379}"]
UpdateInterval: 5
sys:
log:
FileName: "./log/timer.log"
Loglevel: 5
MaxAgeTime: 7
IsDebug: ${LOG_DEBUG:-false}
cron: {} #定时驱动(无额外配置项)
redis: #timer 仅用 Redis
Addr: ["${REDIS_ADDR:-redis:6379}"]
Password: ${REDIS_PASSWORD:-li13451234}
DB: ${REDIS_DB:-1}
KeyPrefix: ${REDIS_KEY_PREFIX:-Voitrans}
modules:
timer:
Doubao_Apikey: "${TIMER_DOUBAO_APIKEY:-}"
Doubao_BotId: "${TIMER_DOUBAO_BOTID:-}"
Doubao_Role: "${TIMER_DOUBAO_ROLE:-}"

79
deploy/voitrans/dev-deploy.sh

@ -0,0 +1,79 @@
#!/usr/bin/env bash
# voitrans 开发环境:一键「构建开发镜像 → 推送 → 更新开发环境」。
#
# 业务五服务(gateway/home/api/mcp/timer)打成单镜像 voitrans-a11,推到私有仓库后远端拉取重启。
#
# 用法(在 deploy/voitrans/ 下):
# ./dev-deploy.sh # 构建并发布 voitrans-a11:dev-latest
#
# 开发版镜像始终使用固定 tag dev-latest(不接受参数覆盖),每次构建覆盖同一 tag。
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/voitrans
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根
BUILD_CONTEXT="$REPO_ROOT/apps/services" # Go 构建上下文
DOCKERFILE="$BUILD_CONTEXT/Dockerfile.voitrans"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 开发部署目标 / 私有仓库凭据 —— 写死在此(本文件纳入 git,私有仓库内可接受) ║
# ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║
# ╚══════════════════════════════════════════════════════════════════════╝
IMAGE_NAME="voitrans-a11" # 业务单镜像名
REGISTRY="docker-registry.ideapsound.com" # 私有镜像仓库
REGISTRY_USER="admin" # 仓库账号
REGISTRY_PASS="li13451234" # 仓库密码
DEV_HOST="124.222.232.218" # ← 开发服务器 IP(必填)
DEV_PORT=22
DEV_USER=root
DEV_KEY=/Users/liwei/Documents/key/earphone.pem # 本机访问开发服务器的 SSH 私钥
DEV_DIR="/home/work/voitrans-a11" # 远端部署目录
PLATFORM="${PLATFORM:-linux/amd64}"
TAG="dev-latest" # 开发版固定 tag,每次构建覆盖同一 dev-latest
ENV_FILE="$SVC_DIR/env/dev.env"
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
[ -f "$ENV_FILE" ] || { red "✗ 缺少 env/dev.env(先 cp env/dev.env.example env/dev.env 并填写)"; exit 1; }
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST(开发服务器 IP)}"
# 生成 compose/容器用 .env = dev.env(去掉 TAG/REGISTRY) + 注入 REGISTRY + 本次 TAG
gen_env() {
{ grep -vE '^(REGISTRY|TAG)=' "$ENV_FILE" || true; } > "$SVC_DIR/.env"
{
printf 'REGISTRY=%s\n' "$REGISTRY"
printf 'TAG=%s\n' "$TAG"
} >> "$SVC_DIR/.env"
}
# ── 远程 SSH 基础 ──
_ssh() { ssh -p "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEV_USER@$DEV_HOST" "$@"; }
_scp() { scp -P "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEV_USER@$DEV_HOST:$1"; }
# 1) 构建并推送镜像
blue ">>> 构建镜像 $FULL_IMAGE (context=$BUILD_CONTEXT)"
echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$FULL_IMAGE" "$BUILD_CONTEXT"
docker push "$FULL_IMAGE"
# 2) 同步配置并远端拉起
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST}"
gen_env
blue ">>> 同步配置到 $DEV_USER@$DEV_HOST:$DEV_DIR"
_ssh "mkdir -p '$DEV_DIR/confs' '$DEV_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEV_DIR/docker-compose.yml")"
_scp "$SVC_DIR/.env" "$(_dest "$DEV_DIR/.env")"
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEV_DIR/confs/")"
blue ">>> 远端拉取并重启"
_ssh "echo '$REGISTRY_PASS' | docker login '$REGISTRY' -u '$REGISTRY_USER' --password-stdin >/dev/null"
_ssh "docker network inspect 1panel-network >/dev/null 2>&1 || docker network create 1panel-network"
_ssh "cd '$DEV_DIR' && docker compose --env-file .env pull && docker compose --env-file .env up -d && docker image prune -f"
green "✓ voitrans dev 已发布: $FULL_IMAGE → $DEV_HOST:$DEV_DIR"

57
deploy/voitrans/docker-compose.yml

@ -0,0 +1,57 @@
# voitrans 业务单镜像部署
#
# voitrans-a11: gateway + home + api + mcp + timer 五个集群服务(shell 脚本管理多进程)。
# 镜像版本通过 TAG 控制;接入外部网络(默认 1panel-network),与 etcd/mysql/redis/nats 通过容器名互通。
# 不直接 docker compose 调用,统一经 dev-deploy.sh / prod-deploy.sh(它们把 env/<env>.env 落为 .env)。
services:
voitrans-a11:
image: ${REGISTRY:-docker-registry.ideapsound.com}/voitrans-a11:${TAG:-latest}
container_name: voitrans-a11
restart: unless-stopped
platform: ${PLATFORM:-linux/amd64}
networks:
- appnet
# 把整个部署目录挂为 /app(WORKDIR):内含 confs/ log/ 及各服务运行期资源(密钥/词库等,如有)
volumes:
- .:/app
# 把 .env 整体注入容器,供各 yaml 里 ${VAR} 在启动时展开(compose 默认只做文件内插值,需显式 env_file)
env_file:
- .env
environment:
TZ: Asia/Shanghai
stop_grace_period: 30s
# 端口(容器内部,对外经反代/NPM 暴露;如需直接发布改用下方 ports):
# 7001 gateway RPCX / 7100 gateway HTTP
# 7002 home RPCX / 7003 timer RPCX
# 7004 mcp RPCX / 7300 mcp HTTP
# 7005 api RPCX / 8080 api Console
expose:
- "7100"
- "7300"
- "8080"
# 如需宿主机直接访问,取消注释(左=宿主端口,右=容器端口):
# ports:
# - "7100:7100" # gateway HTTP
# - "8080:8080" # api Console
# - "7300:7300" # mcp HTTP
# 健康检查:探测 gateway HTTP 端口
healthcheck:
test: ["CMD", "sh", "-c", "nc -z 127.0.0.1 7100 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
# 外部共享网络:依赖的 etcd/mysql/redis/nats 在此网络(用容器名连接),目标机需预先存在
networks:
appnet:
name: ${DOCKER_NETWORK:-1panel-network}
external: true

45
deploy/voitrans/env/dev.env.example

@ -0,0 +1,45 @@
# voitrans · dev 开发环境(容器运行时配置)
# 仓库地址 REGISTRY 由 dev-deploy.sh 写死注入,这里不填。
# 依赖的 etcd/mysql/redis/nats 在 DOCKER_NETWORK 网络内,用容器名连接。
# ── 镜像 / 容器 ──
TAG=dev-latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
# ── 集群 / 服务发现 ──
CLUSTER_TAG=voitrans-a11-dev # 集群标签:隔离 dev 与生产,避免互相发现
ETCD_SERVERS=etcd:2379 # ETCD 容器名:端口
# ── 存储(容器名连接;密码特殊字符在 DSN 里需 URL 编码 & → %26)──
DB_ADMIN_DSN=root:li13451234@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=li13451234
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransDev
# ── NATS(统计快照推送到 console,须与 console 端一致)──
NATS_URL=nats://nats:4222
# ── 统计上报(须与 console 注册表中的应用名/区域一致)──
ANALYZE_APP_NAME=voitrans-a11-dev
ANALYZE_REGION=1
# ── 业务杂项 ──
LOG_DEBUG=true
GATEWAY_TOKEN_KEY=dev-change-me
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=123
API_SITE_NAME=Voitrans(开发)
API_CURRENCY_TYPE=0
MCP_ADDR=http://localhost:7300
# ── 可选 ──
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=
# 部署目标(DEV_HOST/USER/KEY/DIR)与仓库凭据已写死在 dev-deploy.sh 顶部,不在此文件填。

35
deploy/voitrans/env/japan.env.example

@ -0,0 +1,35 @@
# voitrans · 生产-日本 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
TAG=latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-jp
ETCD_SERVERS=etcd:2379
# ── 存储(按日本生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransJP
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-jp
ANALYZE_REGION=2 # 区域编码见 pb.Region(海外)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=1 # 海外计费美元
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

36
deploy/voitrans/env/shanghai.env.example

@ -0,0 +1,36 @@
# voitrans · 生产-上海 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
# 依赖的 etcd/mysql/redis/nats 用容器名或区域内网地址连接,按实际填写。
TAG=latest # 实际部署版本由 prod-deploy.sh <区域> <tag> 覆盖
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-sh # 各区域独立集群标签,避免跨区域互相发现
ETCD_SERVERS=etcd:2379
# ── 存储(按上海生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransSH
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-sh # 须与 console 注册表应用名一致
ANALYZE_REGION=1 # 区域编码见 pb.Region(国内)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=0
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

35
deploy/voitrans/env/singapore.env.example

@ -0,0 +1,35 @@
# voitrans · 生产-新加坡 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
TAG=latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-sg
ETCD_SERVERS=etcd:2379
# ── 存储(按新加坡生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransSG
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-sg
ANALYZE_REGION=2 # 区域编码见 pb.Region(海外)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=1
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

44
deploy/voitrans/prod-build.sh

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

105
deploy/voitrans/prod-deploy.sh

@ -0,0 +1,105 @@
#!/usr/bin/env bash
# voitrans 生产环境:部署到指定区域(选择区域 + 镜像版本 tag),支持回滚。
#
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「同步配置 + 远端拉取 + 重启」。
# 每个区域一套服务器配置(见下方 region_profile)。各区域共用同一镜像 voitrans-a11。
#
# 用法(在 deploy/voitrans/ 下):
# ./prod-deploy.sh <区域> <tag> # 部署指定版本,如 ./prod-deploy.sh shanghai 0.1.1
# ./prod-deploy.sh <区域> rollback # 回滚到上一次部署版本(与当前版本来回切换)
# 区域: shanghai | japan | singapore
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ── 私有仓库凭据(写死;公开仓库前清空 REGISTRY_PASS)──
IMAGE_NAME="voitrans-a11"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 各区域部署目标 —— 写死在此(本文件纳入 git)。公开仓库前清空 HOST。 ║
# ║ 新增区域:在此加一段 case + 在 env/ 下加 <区域>.env ║
# ╚══════════════════════════════════════════════════════════════════════╝
region_profile() {
DEPLOY_PORT=22
DEPLOY_USER=root
DEPLOY_KEY=~/.ssh/id_rsa
DEPLOY_DIR="/opt/yunyan/voitrans" # 远端部署目录(各区域机器各一份)
case "$REGION" in
shanghai) DEPLOY_HOST="" ;; # ← 上海生产服务器 IP(必填)
japan) DEPLOY_HOST="" ;; # ← 日本生产服务器 IP(必填)
singapore) DEPLOY_HOST="" ;; # ← 新加坡生产服务器 IP(必填)
*) red "✗ 未知区域: $REGION(应为 shanghai|japan|singapore)"; exit 1 ;;
esac
}
# ── 参数 ──
REGION="${1:-}"; ARG2="${2:-}"
[ -n "$REGION" ] && [ -n "$ARG2" ] || { red "用法: ./prod-deploy.sh <shanghai|japan|singapore> <tag|rollback>"; exit 1; }
if [ "$ARG2" = "rollback" ]; then MODE="rollback"; NEW_TAG=""; else MODE="deploy"; NEW_TAG="$ARG2"; fi
region_profile
: "${DEPLOY_HOST:?请在 prod-deploy.sh 的 region_profile($REGION) 段填 DEPLOY_HOST}"
ENV_FILE="$SVC_DIR/env/$REGION.env"
[ -f "$ENV_FILE" ] || { red "✗ 缺少 env/$REGION.env"; exit 1; }
[ -f "$SVC_DIR/docker-compose.yml" ] || { red "✗ 缺少 docker-compose.yml"; exit 1; }
# 取 DOCKER_NETWORK(compose 外部网络名)
DOCKER_NETWORK="$({ grep -E '^DOCKER_NETWORK=' "$ENV_FILE" || true; } | head -1 | cut -d= -f2-)"
DOCKER_NETWORK="${DOCKER_NETWORK:-1panel-network}"
# 生成 .env.infra = <区域>.env(去掉 TAG/PREV_TAG/REGISTRY) + 注入 REGISTRY。TAG/PREV_TAG 由远端管理。
gen_env_infra() {
{ grep -vE '^(REGISTRY|TAG|PREV_TAG)=' "$ENV_FILE" || true; } > "$SVC_DIR/.env.infra"
printf 'REGISTRY=%s\n' "$REGISTRY" >> "$SVC_DIR/.env.infra"
}
_ssh() { ssh -p "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" "$@"; }
_scp() { scp -P "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEPLOY_USER@$DEPLOY_HOST:$1"; }
gen_env_infra
blue ">>> [$REGION] 同步配置到 $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR ($MODE${NEW_TAG:+ $NEW_TAG})"
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml")"
_scp "$SVC_DIR/.env.infra" "$(_dest "$DEPLOY_DIR/.env.infra")"
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEPLOY_DIR/confs/")"
blue ">>> [$REGION] 远端计算版本、拉取并重启"
_ssh bash -s -- "$DEPLOY_DIR" "$MODE" "$NEW_TAG" "$REGISTRY/$IMAGE_NAME" "$REGISTRY" "$REGISTRY_USER" "$REGISTRY_PASS" "$DOCKER_NETWORK" <<'REMOTE'
set -e
DIR="$1"; MODE="$2"; NEW_TAG="$3"; IMG_PREFIX="$4"; REG="$5"; REG_USER="$6"; REG_PASS="$7"; NET="$8"
getline() { [ -f "$DIR/.env" ] && { grep -E "^$1=" "$DIR/.env" 2>/dev/null | tail -1 | cut -d= -f2- || true; } | tr -d '\r'; }
CUR_TAG="$(getline TAG)"
CUR_PREV="$(getline PREV_TAG)"
if [ "$MODE" = "rollback" ]; then
[ -n "$CUR_PREV" ] || { echo "没有 PREV_TAG,无法回滚"; exit 1; }
FINAL_TAG="$CUR_PREV"; FINAL_PREV="$CUR_TAG" # 与当前版本来回切换
else
FINAL_TAG="$NEW_TAG"
if [ -n "$CUR_TAG" ] && [ "$CUR_TAG" != "$NEW_TAG" ]; then FINAL_PREV="$CUR_TAG"; else FINAL_PREV="$CUR_PREV"; fi
fi
# 写新 .env = 同步来的基础设施配置 + PREV_TAG + TAG
cp "$DIR/.env.infra" "$DIR/.env"
[ -n "$FINAL_PREV" ] && echo "PREV_TAG=$FINAL_PREV" >> "$DIR/.env"
echo "TAG=$FINAL_TAG" >> "$DIR/.env"
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
docker pull "$IMG_PREFIX:$FINAL_TAG"
cd "$DIR" && docker compose --env-file .env up -d
docker image prune -f
echo "已部署版本 TAG=$FINAL_TAG (PREV_TAG=${FINAL_PREV:-无})"
REMOTE
green "✓ voitrans [$REGION] $MODE 完成"
Loading…
Cancel
Save