简短说明:本仓库包含一个示例后端服务 product-query-svc(支持 in-memory 与 Postgres),数据库迁移需通过 golang-migrate 执行(不再使用嵌入式迁移),以及用于本地开发的 Tilt + kind 配置与最小 Helm chart(已补全)。 服务目前提供商品 CRUD 及评论功能,评论支持多用户查看、作者更新/删除,并通过 OpenAPI 严格校验暴露接口。
项目采用按“应用 + 适配器”的分层组织,便于替换实现与独立演进。
.
├── api/ # OpenAPI 定义与代码生成配置
│ ├── openapi.yaml # 主 OpenAPI 入口
│ ├── oapi-config.yaml # oapi-codegen 配置
│ ├── generate.go # go generate 指令
│ ├── paths/ # 路径/接口片段
│ ├── schemas/ # 数据结构(Schema)
│ └── responses/ # 响应体定义
├── apps/
│ └── product-query-svc/ # 应用层与适配器
│ ├── domain/ # 领域模型与领域错误
│ ├── ports/ # 端口(接口),抽象仓储与服务
│ ├── application/ # 应用服务实现(业务编排)
│ │ ├── product/ # 商品相关用例
│ │ └── comment/ # 商品评论用例
│ └── adapters/
│ ├── inbound/http/ # OpenAPI 严格服务 + 路由装配 + 轻量 handler
│ └── outbound/
│ ├── inmem/ # 内存仓储实现(开发/测试)
│ └── postgres/ # Postgres 仓储与迁移文件
├── backend/
│ └── cmd/product-query-svc/ # 可执行入口(main.go),装配路由/依赖
├── charts/product-query-svc/ # 最小 Helm Chart(含迁移 Job 与 ConfigMap)
├── k8s/ # 直接应用的 Kubernetes 清单(Service/Deployment/Postgres)
├── kind/ # kind 本地集群配置
├── scripts/ # 脚本(初始化迁移、kind 集群启动等)
├── test/ # 单元/集成测试
├── bin/ # 本地构建产物输出(make build)
├── Dockerfile # 服务镜像构建
├── Tiltfile # 本地开发编排(构建、端口转发、迁移)
├── Makefile # 常用命令封装(构建、代码生成、迁移)
└── .env.dev.example # 本地开发环境变量示例
说明:
- 目录遵循“端口与适配器”(Hexagonal/Clean Architecture)思路,
ports定义接口(inbound/outbound),app为用例实现,adapters/*提供适配器实现;domain保持纯净,可复用。 - 数据库迁移统一放在
apps/product-query-svc/adapters/outbound/postgres/migrations,通过 golang-migrate 执行(脚本/Make/Tilt/Helm 均已支持)。 - 生产部署建议使用 Helm Chart;本仓库同时保留了
k8s/便于直接 kubectl 应用与调试。
- 代码生成统一使用
oapi-codegen strict-server:api/oapi-config.yaml只保留严格服务输出,避免手写 handler 接口。每次变更 OpenAPI 需执行go generate ./api重新生成gopracticeapi.gen.go。 - 请求校验前移到 OpenAPI:所有参数/请求体验证(
minimum/maxLength/enum等)写在api目录的 schema/parameter 中,由github.com/oapi-codegen/nethttp-middleware提供的OapiRequestValidator中间件统一拦截。 - Handler 职责“三件套”(
apps/product-query-svc/adapters/inbound/http/handler_*.go):- 从生成的强类型
RequestObject中取出入参(无需重复校验); - 调用对应的应用服务(
application/*); - 利用
response_helpers.go中的ok*/xxxError辅助函数返回严格的响应类型(仅 2xx/4xx)。
- 从生成的强类型
- 跨操作共享错误映射:
response_helpers.go负责把领域错误映射成具体的 OpenAPI 响应类型,并封装标准错误载荷;新增业务错误时只需在此扩展。 - 统一路由出口:
NewAPIHandler会加载内嵌的 Swagger、挂载必需的中间件(含请求校验)并包装 strict server;在main.go、集成测试与internal/testutil中均通过该函数装配,保持行为一致。
-
端口转发就绪
- 打开 Tilt UI,确认
product-query-svc资源为绿色/Ready,且显示端口转发到http://localhost:8080。
- 打开 Tilt UI,确认
-
健康检查
curl -i http://localhost:8080/healthz
# 期望: HTTP/1.1 200 OK,响应体: ok⚡ 快速 API 测试(可复制/点击运行)
前提:服务已监听 http://localhost:8080,已安装 curl 与 jq。
- GET /healthz(健康检查)
curl -i http://localhost:8080/healthz- POST /products(创建商品)
curl -s -X POST http://localhost:8080/products \
-H 'Content-Type: application/json' \
-d '{"name":"Sample Plan","price":123.45}' | jq- PUT /products/{id}(整资源更新,示例使用已存在的 id)
curl -s -X PUT http://localhost:8080/products/1 \
-H 'Content-Type: application/json' \
-d '{"name":"Updated Plan","price":199.99}' | jq- GET /products/{id}(按 ID 查询,示例使用已种子或上一步创建/更新的 id)
# 如果使用迁移种子数据(Postgres),通常 1 为 Basic Plan
curl -s http://localhost:8080/products/1 | jq- GET /products/search(分页搜索;注意 q 至少 3 个字符)
curl -s 'http://localhost:8080/products/search?q=pro&page=1&pageSize=10' | jq- DELETE /products/{id}(删除;示例:先创建临时商品再删除)
ID=$(curl -s -X POST http://localhost:8080/products \
-H 'Content-Type: application/json' \
-d '{"name":"Temp Item","price":1.99}' | jq -r '.id'); \
echo "created id=$ID"; \
curl -i -X DELETE http://localhost:8080/products/$ID; \
echo; \
curl -i http://localhost:8080/products/$ID # 期望 404- POST /products/{id}/comments(新增评论,需提供已有用户 ID)
COMMENT_ID=$(curl -s -X POST http://localhost:8080/products/1/comments \
-H 'Content-Type: application/json' \
-d '{"userId":1,"content":"Great product!"}' | jq -r '.id'); \
echo "comment id=$COMMENT_ID"- GET /products/{id}/comments(查看评论列表,默认按创建时间倒序)
curl -s http://localhost:8080/products/1/comments | jq- PUT /products/{id}/comments/{commentId}(更新评论内容,
userId需放在查询参数且与原作者一致)
curl -s -X PUT "http://localhost:8080/products/1/comments/${COMMENT_ID}?userId=1" \
-H 'Content-Type: application/json' \
-d '{"userId":1,"content":"Updated feedback"}' | jq- DELETE /products/{id}/comments/{commentId}(删除评论,同样需要
userId查询参数)
curl -i -X DELETE "http://localhost:8080/products/1/comments/${COMMENT_ID}?userId=1"🧪 使用临时 Docker Postgres 跑集成测试(避免 5432 端口冲突)
前置:本机已安装 Docker。
一键运行(自动起容器 → 迁移 → 运行带 Postgres 的集成测试 → 清理容器):
make test-integration-docker或直接运行脚本,并自定义 go test 目标/参数:
bash scripts/test-integration-docker.sh ./test -run Postgres脚本要点:
- 使用
docker run -P启动 postgres:16-alpine,随机映射宿主端口,避免与 Tilt 的 5432 冲突。 - 通过
migrate/migrate容器在同一网络命名空间内执行迁移。 - 自动导出
DATABASE_URL为宿主上的随机端口,并运行 go test。 - 需要单独验证仓储层(含评论 CRUD)的 Docker 集成测试时,可运行
make test-repo-docker(依赖本机 Docker);若暂不具备条件,可设置SKIP_DOCKER_TESTS=1 make test-repo-docker跳过实际容器启动。
- 插入演示数据(Postgres)
# 连接数据库(Tilt 将 Postgres 转发到本机 5432)
psql "postgres://app:app_password@localhost:5432/productdb?sslmode=disable"
# 在 psql 中执行:
insert into products(name, price, tags)
values ('Basic Plan',9900,ARRAY['starter','subscription']),
('Pro Plan',19900,ARRAY['professional','subscription']),
('Enterprise Plan',49900,ARRAY['enterprise','subscription']);
# 再次验证
\q
curl -s 'http://localhost:8080/products/search?q=pro&page=1&pageSize=10' | jq
curl -s http://localhost:8080/products/1 | jq-
迁移是否成功
- 在 Tilt UI 查看
db-migrate资源日志,确认up成功。 - 或进入 psql 执行
\dt检查是否存在products表。
- 在 Tilt UI 查看
-
Pod/日志排查
kubectl -n -dev get pods
kubectl -n -dev logs deploy/product-query-svc
kubectl -n -dev logs statefulset/postgres- Go == 1.24
- Docker
- kubectl、helm、tilt、psql 客户端、IDE(
- 启动 Postgres(示例):
docker run --name -postgres \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=app_password \
-e POSTGRES_DB=productdb \
-p 5432:5432 -d postgres:16-alpine- 设置环境变量:
export DATABASE_URL="postgres://app:app_password@localhost:5432/productdb?sslmode=disable"
export HTTP_ADDRESS=":8080"
export LOG_LEVEL=debug- 运行服务(开发):
cd backend/cmd/product-query-svc
go run .或构建后运行:
go build -o bin/product-query-svc ./backend/cmd/product-query-svc
./bin/product-query-svc说明:项目包含迁移文件(apps/product-query-svc/adapters/outbound/postgres/migrations),请统一使用 golang-migrate 工具管理数据库 schema。
快捷初始化(迁移包含测试数据):
# 使用脚本自动运行迁移(优先本机 migrate CLI,缺失则用 docker 镜像)
DATABASE_URL="postgres://app:app_password@localhost:5432/productdb?sslmode=disable" \
bash scripts/db-init.sh
# 或使用 Makefile 包装目标
make db-init说明:迁移序列包含 000002_seed_test_data.up.sql,会插入示例数据(幂等)。回滚同名 .down.sql 可清理。
- 启动本地 kind 集群(仓库脚本):
bash scripts/kind-up.sh- 启动 Tilt(构建镜像并部署):
tilt up-
访问服务:
- Tilt 端口转发:http://localhost:8080
- 可选 NodePort(若在 kind 配置中映射):http://localhost:30080
-
连接数据库:
psql "postgres://app:app_password@localhost:5432/productdb"本项目的迁移文件位于 apps/product-query-svc/adapters/outbound/postgres/migrations,支持三种方式执行迁移:
-
手动本地执行(开发态)
- 命令:
make migrate-up MIGRATE_URL="postgres://app:app_password@localhost:5432/productdb?sslmode=disable" - 依赖:本机安装
migrateCLI(可选 Homebrew:brew install golang-migrate)。
- 命令:
-
通过 Tilt 自动迁移(当前默认,推荐开发态)
- Tiltfile 配置了
local_resource('db-migrate', ...),会在postgres端口转发就绪后,使用 Docker 运行migrate/migrate容器来执行迁移。 - 无需在本机安装
migrateCLI,但需要本机 Docker 正常运行。 - 连接串默认使用
host.docker.internal:5432访问 Tilt 的本地端口转发(macOS/Windows Docker Desktop 默认可用)。 - 如需修改连接串,可在 Tiltfile 顶部调整
MIGRATE_URL。
- Tiltfile 配置了
-
在 K8s/Helm 中执行(集群内)
- 可选:用 Helm hook 或 Job 在集群内运行
migrate/migrate,DATABASE_URL使用集群内 Service(例如postgres.-dev.svc.cluster.local)。需要的话可以补充该 Job。
- 可选:用 Helm hook 或 Job 在集群内运行
常见避坑:
-
“migrate: command not found”
- 原因:旧实现会在本机直接执行
migrateCLI;如果没安装就会报错。 - 现状:Tiltfile 已改为通过
docker run migrate/migrate ...执行,无需本机安装 CLI,但需要 Docker。
- 原因:旧实现会在本机直接执行
-
Linux 下
host.docker.internal不可用- 方案1:在 Docker 命令中添加
--add-host=host.docker.internal:host-gateway;或 - 方案2:使用
--network host并将MIGRATE_URL的主机名改为localhost(注意该方案在 macOS/Windows 不通用)。
- 方案1:在 Docker 命令中添加
-
5432 端口占用冲突
- Tilt 会把集群内 Postgres 端口转发到你本机 5432;如果你本地已有 Postgres 占用该端口,则端口转发失败。
- 解决:停掉本地 Postgres,或修改端口转发/连接串(例如改为 15432,并同步调整 Tiltfile 的
MIGRATE_URL)。
-
凭据与安全
- 目前开发态在 ConfigMap/值文件中包含了示例凭据,便于演示。
- 生产/共享环境应将
DATABASE_URL放入 Secret,并在迁移/应用中引用 Secret。
-
Distroless 镜像与热更新
- 当前服务镜像为 distroless,不支持容器内直接热重载,Tilt 将走重新构建/重启流程,这是预期行为。
仓库包含最小 Helm chart:charts/product-query-svc,可用如下命令部署:
helm install product-query-svc ./charts/product-query-svc -f ./charts/product-query-svc/values.yamlHelm 迁移 Job:
- Chart 内置了一个
post-install, post-upgrade的迁移 Job(使用migrate/migrate镜像)。 - 迁移文件位于 Chart 下的
migrations/目录(通过 ConfigMap 挂载到容器/migrations)。 - 默认启用(
values.yaml: migrations.enabled=true)。如需关闭,设置--set migrations.enabled=false。 - 数据库连接串(DATABASE_URL)
- 生产默认通过 Secret 注入:
values.database.secret.enabled=true,并在 Deployment/Job 中secretKeyRef读取。 - 如果已有现成 Secret,设置
values.database.secret.name与values.database.secret.key即可。 - 若需要 Chart 自动创建 Secret,设置
values.database.secret.create=true并提供values.database.url(作为 stringData)。 - 开发环境下仍可回退读取
values.env.DATABASE_URL(不推荐用于生产)。
- 生产默认通过 Secret 注入:
docker build -t product-query-svc:dev .注:Dockerfile 默认构建 backend/cmd/product-query-svc 的二进制,用于镜像/部署。
展开查看生成说明
- 必要文件:api/openapi.yaml、api/paths/、api/schemas/、api/responses/*
- 配置文件:api/oapi-config.yaml
- 生成命令(示例):
go generate ./api
# 或者根据 generate.go 的 //go:generate 指定路径-
生成后的
adapters/inbound/http/api.gen.go禁止手动修改;需要调整校验或字段时改 OpenAPI 资源并重新生成。 -
HTTP handler 只能依赖生成的
StrictServerInterface,其实现位于handler_*.go,必须配合response_helpers.go和request_mappers.go使用。 -
NewAPIHandler会自动加载最新的 Swagger 并注册OapiRequestValidator中间件,生产/测试入口都应通过该函数获取路由。 -
建议:将生成步骤写入 Makefile 或 CI,团队协同时要约定是否把生成产物纳入版本控制(两种策略均可)。
将改动分为小而原子的一系列 commit,以下为推荐批次。点击展开查看每个批次应包含的文件及示例 commit message。
批次 1 — 基础 infra / tooling(Tilt / kind /Docker / scripts / Helm / .env)
- 相关文件:Tiltfile、kind/, k8s/, Dockerfile、.dockerignore、Makefile、.env.dev.example、charts/
- 建议命令:
- git add Tiltfile kind/ k8s/ Dockerfile .dockerignore Makefile .env.dev.example charts/
- git commit -m "infra: add Tilt, kind, k8s manifests, Dockerfile, helper scripts and Helm chart"
批次 2 — DB 配置与迁移
- 相关文件:apps/product-query-svc/adapters/outbound/postgres/(migrations、product_repository.go)
- 建议 commit message:"db: add Postgres migrations and adapters (migrations embedded via //go:embed)"
批次 3 — 核心应用层(domain、ports、app)
- 相关文件:apps/product-query-svc/domain/ apps/product-query-svc/ports/ apps/product-query-svc/application/
- 建议 commit message:"app: add domain models, service implementation and ports for product-query-svc"
批次 4 — 适配器:in-memory repo 与 HTTP handlers
- 相关文件:apps/product-query-svc/adapters/outbound/inmem/ apps/product-query-svc/adapters/inbound/http/
- 建议 commit message:"feat: add in-memory repo and HTTP handlers for product endpoints"
批次 5 — 后端入口 / wiring / router
- 相关文件:backend/cmd/product-query-svc、apps/product-query-svc/adapters/inbound/http/
- 建议 commit message:"chore: add service main and HTTP wiring (router & handlers)"
批次 6 — 文档与测试
- 相关文件:readme.md、test/
- 建议 commit message:"docs: add README startup steps and basic tests"
批次 7 — 依赖(go.mod / go.sum)
- 在本地运行:go mod tidy、go test ./...
- 提交:git add go.mod go.sum && git commit -m "chore: update go.mod and go.sum after tidy"
常见错误与排查
- "FATAL: database "app" does not exist":确认 Postgres 启动时环境变量 POSTGRES_DB 与服务的 DATABASE_URL 中数据库名一致(示例使用 productdb);或手动创建数据库。
- Docker 构建报 "go.mod: unknown directive: tool":请使用与 go.mod 中 toolchain 对齐的 Go 版本(本项目使用 1.24)。
- Lens 中看不到资源:确认 Lens 使用的 kubeconfig 与 kubectl 当前上下文一致,并且查看正确命名空间(-dev)。