14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
项目概览
yunyan-sas 是一个从单产品服务 deep_server(Go 模块名 earphone)迁移而来的 monorepo SaaS 平台,目标是把单产品后端改造成可托管多个应用/产品的 SaaS。迁移时 Go 模块名 earphone → yunyan,所有 import 前缀随之变更。回复一律用中文。
仓库结构
yunyan-sas/
├── go.work # Go workspace:use ./apps/services
├── build.sh # 构建/推送镜像(被 deploy.sh 调用,也可单用)
├── apps/
│ ├── proto/ # .proto 源文件(按业务域分目录)
│ ├── services/ # Go 后端(module: yunyan, go 1.25.0)
│ │ ├── lego/ # 内部服务框架(IService/IModule/Comp + sys 子系统)
│ │ ├── sys/ # 第三方集成(aliyun/tencentyun/openai/coze/nats/...)
│ │ ├── comm/ # 跨模块公共:常量、ErrorCode、IUserSession、NATS key
│ │ ├── modules/ # 业务模块(每个 = 一组 Comp)
│ │ ├── services/ # 各服务的 main.go 入口 + conf/*.yaml
│ │ ├── pb/ # protoc 生成的 *.pb.go(DO NOT EDIT)
│ │ ├── swagger/、utils/
│ ├── admin/ # 管理后台前端(Nuxt 4 SSR + Tailwind + DaisyUI 4 + Pinia)
│ └── web/ # 用户侧前端骨架(Nuxt 4)
└── deploy/<service>/ # 每服务一份 deploy.sh + docker-compose.yml + env/
常用命令
Go 后端(在 apps/services/ 下执行)
go build ./... # 编译全部
go vet ./... # 静态检查
go test ./... # 跑全部测试(测试集中在 sys/* 与 utils/)
go test ./sys/sms/ -run TestXxx -v # 跑单个测试
go build -o /tmp/console ./services/console/ # 编译单个服务
go run ./services/console -conf ./services/console/conf/console.yaml # 本地起服务
服务列表:api、gateway、home、mcp、migu、timer(集群服务,走 ETCD/rpcx)、console(单例服务,不接 ETCD)。每个服务 main.go 用 -conf 指向自己的 conf/*.yaml,默认相对路径 ./conf/<svc>.yaml。
Admin 前端(在 apps/admin/ 下执行)
pnpm install
pnpm dev # 开发,默认 :3000
pnpm build # 产出 .output/(SSR)
本地开发需同时起 console 后端(默认代理到 127.0.0.1:8080,见下文 proxy)。超管账号见各 console.yaml 的 AdminAccount/AdminPassword。
构建镜像与部署
./build.sh console # 构建 registry.voitrans.net/starpivot-console:latest(PUSH=0 只构建不推)
./build.sh admin
# 测试环境发布(在 deploy/<服务>/ 下,服务 = app | console | admin)
cd deploy/console && ./dev-deploy.sh yunyan # 云雁测试环境(广州阿里云 8.138.86.223)
cd deploy/console && ./dev-deploy.sh along # 阿龙测试环境(阿里云 8.133.166.29)
cd deploy/console && ./dev-deploy.sh # 不带参数 → 交互选环境:lingpu | yunyan | along
# 生产:先构建带版本号的镜像,再部署
cd deploy/console && ./prod-build.sh 0.2.1 yunyan # 第 2 个参数选公司(yunyan|lingpu|along),省略则交互选
cd deploy/console && ./prod-deploy.sh 0.2.1 yunyan # 第 2 个参数选生产环境,须与构建时所选公司一致;省略则交互选
镜像仓库按公司区分,地址/账号统一在 deploy/registry-profiles.sh(换仓库只改这一处,各脚本 source 它):
yunyan→registry.voitrans.net(仓库实体在广州机 8.138.86.223)lingpu→registry.lingpu.net(灵谱香港机)along→registry.ymaikj.com(仓库实体就在阿龙测试机 8.133.166.29 上,账号是admin不是liwei);阿龙测试机与正式机 47.116.104.181 都从这里拉镜像
测试环境档案在各 dev-deploy.sh 的 env_profile():
along→ 8.133.166.29(阿里云,x86_64/Docker 24.0.9),目录/home/work/starpivot/{app,admin,console},密钥/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem该机已自带 postgres:16 / redis:7 / mysql / NPM / gitea / docker-registry,配.env时优先复用,别另起一套
生产镜像名统一为 starpivot-<服务>(admin/console/app 三个服务、各家公司都一样),映射在
registry-profiles.sh 的 prod_image_name()——build 与 deploy 共用这一份,别在各脚本里写死。
compose 的 image 由脚本注入 IMAGE_NAME 变量,同样不写死。
⚠️ app 的旧名 voitrans-a11 已于 2026-08-10 废弃,仓库里该名下的旧 tag(如 0.1.2)不会自动搬到新名,
换名后首次上线要重新 ./prod-build.sh <version> 推一版;服务器上手工维护的 docker-compose.yml
也要同步成新模板(脚本只下发 .example,不覆盖真实文件)。
注意 voitrans-a11 这个串还用在别的语义上,那些不能动:confs/*.yaml 的 CLUSTER_TAG(etcd 集群标签,
改了服务互相发现不到)、home.yaml 的 ANALYZE_APP_NAME(须与 console 注册表应用名一致)、
prod-deploy.sh APPS 里的远端目录 /home/work/voitrans-a11。
admin/console 的 prod-deploy.sh 已改为多生产环境档案(env_profile(),同 dev-deploy.sh 的写法):
yunyan→ 139.224.212.237(上海阿里云,与业务后端 app 同机),目录/home/work/starpivot/{admin,console},密钥/Users/liwei1dao/work/密钥/release_shanghai.pem(2026-08-05 由美国机 47.253.88.66 迁来;美国机上的yunyan-admin/console-a11容器仍在跑,待验证后停,脚本已不再指向它)lingpu→ 8.166.114.65(广州阿里云),目录/home/work/starpivot/{admin,console},密钥~/.ssh/lingpu-gz.pemalong→ 47.116.104.181(阿龙正式机,2026-08-16 补),目录/home/work/starpivot/{admin,console},密钥/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem(与测试机同一把);镜像走阿龙仓库registry.ymaikj.com
app 的 prod-deploy.sh 是按区域(region_profile()):shanghai|japan|singapore 是云雁三地,新增 along → 47.116.104.181。
该函数除服务器连接信息外还给 REGION_COMPANY(这个区域拉哪家仓库,云雁三地=yunyan、阿龙=along;可用环境变量 COMPANY 覆盖)。
远端目录与容器名一律按「应用」区分(2026-08-18 起):目录只由 APPS 表给(ym → /home/work/ym-a11),
区域里不再有 DEPLOY_DIR_OVERRIDE(阿龙原先覆盖成 /home/work/starpivot/app,导致部 ym 时落错目录、还和已有容器撞名,已移除;
阿龙机上那份旧的 /home/work/starpivot/app 部署保持原样、不再由脚本管理)。容器名由应用 key 推导为 <应用>-a11,
经 CONTAINER_NAME(同时作 COMPOSE_PROJECT_NAME)注入 docker-compose.yml 的 container_name: ${CONTAINER_NAME:-starpivot-app},
部署后写回服务器 .env。compose 里的 service 名固定为 app,别写成具体应用名。
dev(dev-deploy.sh)仍是每台机单份部署,走默认容器名 starpivot-app;admin/console 的 compose 也尚未参数化。
docker-compose.yml 由 prod-deploy.sh 按应用现生成再下发:模板里 service 名是占位的 app,下发时
sed 改写成本应用名(ym → ym-a11:),于是服务器上 service 名 = 容器名 = <应用>-a11。
下发前与远端文件逐字节比对,一致就跳过、不一致才备份 .bak 后覆盖(纯"存在就不碰"会让旧 compose 永不更新——
ym 首次部署容器一直叫 starpivot-app 正是如此)。service 名变更后旧容器占着 container_name,
远端脚本会按 compose 标签识别并 docker rm -f 掉再重建。服务器上只需维护 .env 与 confs/*.yaml。
新应用首次部署 checklist(2026-08-18 部署 ym 时逐条踩出来的):
confs/home.yaml的wordfilter.WorldFile若列了词库 txt,服务器上必须真有这些文件,否则 homepanic: init sys.wordfilter err: no found file:...→ entrypoint 杀容器 → 无限重启。词库不在 git 里, 不需要就保持注释(模板已默认注释)。表象是 api 报Table '<库>.userdevice' doesn't exist—— userdevice/user 等表由 home 里的 user 模块启动时CreateTable建,home 起不来就永远没表,别去查数据库,先看 panic。confs/ip2region_v4.xdb/v6.xdb:sys.ipinfo启动必需,缺了同样 panic。现在 prod-deploy 会自动下发(仅远端缺失时)。.env别照抄模板:CLUSTER_TAG、ANALYZE_APP_NAME、REDIS_KEY_PREFIX都要换成本应用的身份, 其中ANALYZE_APP_NAME必须等于 console 注册表app_registry.app_name,否则按应用作用域的配置全部落到全局默认。- console 后台要先把该应用登记进
app_registry(阿龙正式机 2026-08-18 时该表还是空的)。
dev-deploy.sh <环境> 只做镜像部署:构建推送 → 远端 pull 重启,不上传 服务器上的真实 .env / confs/*.yaml(只下发 *.example 模板)。
后端架构(lego 框架)
整套后端围绕 lego 自研框架,核心是 Service → Module → Comp 三层 + 反射注册路由。
- 启动:
lego.Run(service, modules...)(lego/lego.go)依次Init → Start → Run → Destroy。 - 两种 Service 基类:
lego/base/rpcx(集群服务):接 ETCD 做服务发现,服务间用 rpcx RPC 互调。api/gateway/home/mcp/migu/timer用它,且OnInstallComp装services.NewHttpRouteComp()。lego/base/single(单例服务):不接 ETCD/集群,只驱动单个模块。console用它,主库 Supabase Postgres + 独立 Redis + NATS,维护app_registry注册表,按"选中应用"动态建到目标应用库的连接。
- Module → Comp:模块(如
modules/api)在OnInstallComp里RegisterComp一组组件:apiComp(HTTP 入口)、modelComp(数据访问)、各种 consumer/interceptor。见 modules/api/module.go。 - HTTP 路由靠反射自动注册(modules/comp_httpgate.go):继承
modules.MCompHttpGate的组件,其每个导出方法只要签名是func (c *apiComp) MethodName(session comm.IUserSession, req *pb.XxxReq) (*pb.XxxResp, *pb.ErrorData)就会被注册成路由
<module>_<methodname小写>(如api_getfactorys)。新增接口 = 新建modules/<mod>/api_xxx.go加一个符合签名的方法即可,无需手动注册。后缀Check的方法不注册。 - 错误处理约定:成功填
resp;失败构造&pb.ErrorData{Code: pb.ErrorCode_XXX, Message: ...}并return。 - pb 包:
pb/*.pb.go由apps/proto/下.proto经protoc+protoc-gen-gov1.36 生成,手动改 .pb.go 无效;改协议要改.proto再重新生成。注意 console 模块有本地 gorm struct(非 pb)的 model 文件,见modules/console/model_*.go。
Admin 前端约定(关键,易踩坑)
- API 代理:server/middleware/proxy.ts 把
/web/api/*和/console/api/*代理到CONSOLE_BACKEND(容器内由 env 注入,本地默认http://127.0.0.1:8080)。 useApi().webApi(method, data)(app/composables/useApi.ts):- 已自动拼成
/web/api/api_${method}并已解包data(成功返回data本体;失败throw;code:18自动登出)。 - ⚠️ 调用时
method不要带api_前缀,否则变成api_api_xxx→code:11。 - 方法名以
get/list/load/query/fetch/search/count/stat/export/check开头视为只读,不弹全屏遮罩;其余(add/update/del/save/create…)视为写操作,请求期间套遮罩防重复提交。 - 选中应用通过请求头
X-App-Id传给后端。
- 已自动拼成
- 应用下拉统一走
api_getmyapps(超管/管理员例外,仍用console/api/apps/list):后端按账号绑定下发可选部署。品牌商/渠道商账号的可见应用不靠手工勾选,而是由「名下产品绑定的应用」推导——产品的应用归属存DBProduct.appnames(应用名 CSV,产品管理页多选),见 scope_app.go。 - 后端返回常多包一层:如
getfactorys → {factorys}、getproducts → {products}、getproduct → {product};保存 payload 也常包一层(厂家{Factory:{...}}、产品{product:{...}});统计 trend 日期字段是stat_day。列表 ID 多以十六进制展示(id.toString(16).toUpperCase())。 - 样式:
app/assets/css/console.css由 nuxt.config 引用;DaisyUI 固定 4.x。
配置与密钥约定(重要)
- 各
conf/*.yaml与deploy/<svc>/deploy.sh中写死了真实的 DSN / 密钥 / 服务器 IP / 仓库密码,且纳入 git——这是用户在私有仓库下明确接受的取舍。修改这些文件时注意它们会随仓库提交。 - ⚠️ 本仓库若要公开,必须先清理
deploy.sh的deploy_profile(DEPLOY_HOST/REGISTRY_PASS)与各*.yaml里的真实凭据。 - 改部署逻辑要同步两份
deploy/console/deploy.sh和deploy/admin/deploy.sh(内容相同,建议改一份再 cp 覆盖)。
迁移上下文
源项目在 /Users/liwei/work/go/yunyan/deep_server_up/(及 go_earphone/server/deep_server/)。从旧代码移植接口时,注意 import 路径由 earphone/... 改为 yunyan/...,并核对接口签名是否符合上文反射注册约定。