14 KiB
扩展业务模块
本批提供八类扩展,以及分页、审计和显式数据库迁移。模块按需导入;omni.New 与业务 Handler 不绑定存储、队列或网络实现。JSON、配置和新示例使用 lowerCamelCase;Go 导出标识符和 _test.go 保持语言约定。
按本次范围决定,不集成 Orbit,不引入 Orbit SDK,也不实现工作流编排。通用消息队列与 Orbit 的持久流程、执行授权、结果订阅和人工恢复不是等价能力。业务项目以后需要 Orbit,可独立依赖它的 SDK;当前没有承诺只换队列适配器就能迁移其全部语义。
能力与边界
| 模块 | 本批实现 | 明确边界 |
|---|---|---|
openapi |
路由与 OpenAPI 3.0.3 同时注册、DTO schema、响应信封 | 显式声明请求/响应;不自动推断任意 Handler 行为,不附带 Swagger UI |
auth |
RS256/EdDSA JWT 验证与公钥轮换、可撤销 Session、Policy 接口 | 登录验证和业务授权规则由接入方提供;不自动获取 token 指定的密钥 |
telemetry |
HTTP 入站/出站、Redis、SQL spans,HTTP 指标和 traceId 日志关联 | 注入 OTel exporter/reader 后才外发;SQL Query span 不覆盖后续遍历;不接管全局 provider |
config / logx |
JSON/YAML 文件、环境变量覆盖、动态日志级别 | 热更新只开放日志级别;连接池、监听、认证等静态变更要求重启 |
resilience |
失败阈值、熔断冷却、单探针恢复、上游并发隔离 | 一个实例对应一个依赖;默认内存状态,不做跨进程全局熔断 |
middleware/redis |
跨副本限流、幂等 claim/回放、容量限制 | 专用 Redis namespace;TTL 窗口内保护不等于永久业务去重 |
queue / queue/redis |
持久消息接口、Streams 接管、有限重试、失败消息管理 | 至少一次交付,处理器必须幂等;持久性取决于 Redis 部署 |
storage |
Local、S3 兼容(含 R2 配置)、OSS 官方 SDK 适配、临时下载链接 | 云平台专属功能单独扩展;本批没有云账号实网验收 |
database/migrate |
MySQL 显式部署迁移、摘要预览、失败 dirty 阻断 | 不随应用启动自动改表;无自动 down/force;已应用文件摘要需部署方留存校对 |
audit |
可注入 Sink、字段白名单、JSON 行串行输出、错误返回 | 不是防篡改存储;与业务同事务的审计要实现事务型 Sink |
pagination |
page/pageSize、最大偏移与页长、统一结果 | offset 分页,不承诺跨页快照,不实现游标分页 |
对象存储
业务只持有 storage.Store,调用 Put、Get、Delete。构造函数属于启动组合处:
type FileService struct { Files storage.Store }
// Local:私有目录,流式临时文件 + 原子替换。
files, err := local.New("/srv/app/objects", 32<<20)
// S3:AWS/MinIO 或其他经过契约测试的 S3 兼容服务。
files, err := s3.Open(s3.Config{
Endpoint: "s3.us-east-1.amazonaws.com", Region: "us-east-1",
Bucket: "example-bucket", Secure: true, MaxBytes: 32<<20,
Credentials: credentialProvider, Transport: transport,
})
// Cloudflare R2:复用 S3 适配器,签名区域为 auto。
cfg, err := s3.R2Config(accountID, bucket, credentialProvider)
cfg.Transport = transport
files, err := s3.Open(cfg)
// OSS:单独适配官方 Go SDK v2,保留平台签名与凭据提供器。
client := ossSdk.NewClient(ossSdk.LoadDefaultConfig().
WithRegion("cn-hangzhou").
WithCredentialsProvider(ossCredentials.NewEnvironmentVariableCredentialsProvider()))
files, err := oss.New(client, bucket, 32<<20)
上方是互斥初始化片段,实际代码需逐次处理错误。import 路径分别为 go-omni/storage/local、go-omni/storage/s3、go-omni/storage/oss(完整 module 前缀 git.yuntuo.hk/k2safe/);ossSdk 与 ossCredentials 来自 github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss 及其 credentials 子包。S3 凭据来自 github.com/minio/minio-go/v7/pkg/credentials。
统一约定:key 是对象标识,不是磁盘路径;拒绝绝对路径、..、空路径段和反斜杠。Put 必须传已知长度,调用方保证与内容一致。默认单对象 32 MiB,流式读写;OSS 本适配器只做 simple upload,最大配置 5 GiB。Get 返回的 Body 由调用方关闭,读取过程也应有 context 截止。Delete 缺失对象成功;Get 缺失对象返回 storage.ErrNotFound。底层客户端/传输连接池由应用管理,不会在单次操作后关闭。
业务需要临时链接时显式依赖额外接口 storage.Presigner。S3/OSS 实现 PresignGet;Local 用 storage.NewSigner(files, downloadBaseURL, secret) 提供 HTTP 下载 Handler。链接 TTL 为 1 秒到 1 小时,私钥/临时凭据可能让云链接提前失效。签发链接前必须完成业务授权;对象 key 及 bucket 本身不是租户权限。签名链接不要写日志。首版未封装前端直传、分片续传、生命周期、ACL、CDN 自定义域名和版本管理。
R2 实现 S3 的子集,配置入口使用标准账户 endpoint;法域专属 endpoint 可显式配置。兼容性依据:R2 S3 API、R2 签名链接。OSS 使用其官方 Go SDK v2,没有假定全部 S3 行为与 OSS 等价。
验证区分:Local 契约、OSS SDK HTTP fixture、S3 SDK fixture 和 R2 签名参数属于本地测试;Linux runner 再跑真实 MinIO 上传/读取/删除与签名拒绝。**MinIO 通过不能代替 OSS/R2 实网验证。**迁移存储平台还需要复制已有对象和调整部署权限,不只是切配置。
可靠队列
backend, err := queueRedis.New(redisClient, queueRedis.Config{
Namespace: "serviceA:jobs:v1", MaxRecords: 1000,
MaxPayloadBytes: 64 << 10, DedupTTL: 24*time.Hour,
})
consumer, err := queueRedis.NewConsumer(backend, queueRedis.ConsumerConfig{
Workers: 4, MaxAttempts: 3,
TaskTimeout: 30*time.Second, VisibilityTimeout: time.Minute,
})
// ID 与内容在业务端持久化;超时后复用原 ID 和原内容。
receipt, err := backend.Publish(ctx, queue.Message{
ID: operationID, Name: "generateReport", Payload: payload,
})
// 在服务生命周期中运行;返回错误须让监控/进程管理感知。
err = consumer.Run(serviceContext, func(ctx context.Context, item queue.Delivery) error {
return generateReportIdempotently(ctx, item.ID, item.Payload)
})
业务依赖 queue.Publisher / queue.Consumer,Redis 类型留在组合处。receipt 表示受理,不代表业务成功。普通 error/panic 按配置重试,queue.Reject(code) 保留为失败消息;超过次数同样保留。DeadLetters(after, limit) 是有界分页,DiscardDead 仅在人工核对后删除匹配的失败消息,并继续保留一段提交去重窗口。错误正文不写入失败记录。需要再次处理时由业务决定新操作身份及去重策略,框架不自动无限重投。
一个 namespace 使用一个受管 consumer group;多个 consumer 可竞争消费同一队列。配置一致性检查拒绝不同的容量/去重策略和执行超时/重试策略混用。确认检查当前 pending owner,旧执行不能确认新执行;但不能阻止旧处理器已发出的外部副作用,因此仍必须业务幂等。处理器需响应 context;Go 无法强杀不配合的函数,超过 shutdownTimeout 返回错误时,最多 Workers 个处理器可能尚未退出。
容量包含待处理、失败及未过期的已完成身份,满额拒绝新 ID,保留原 ID 重报回执。完成/人工丢弃后的身份按 DedupTTL 过期;未完成/失败消息不自动清除,不用 MAXLEN 静默裁掉未消费消息。Redis 需启用合适的 AOF/复制策略与 noeviction;本批验证的是投递/接管协议,未证明 Redis 主从故障切换后零丢失。异常进程留下的无 pending consumer 元数据可在运维核实后清理,不能用 DELCONSUMER 丢掉尚未接管的 pending。
task.Runner 仍适用于本进程短任务和定时轮询,重启会丢失;需要持久交付时使用 queue。这里没有分布式 Cron、业务工作流、事务 Outbox 或 exactly-once 承诺。
认证与授权
auth.NewJWT 要求固定 issuer/audience 和可信 kid→公钥列表;只开放 RS256 / EdDSA,必须有 exp/sub,检查有效期、nbf 与 iat。ReplaceKeys 原子替换列表,轮换期可同时保留新旧 kid。JWT 签发、私钥托管、登录口令验证留给身份服务。
auth.NewSessions(cacheStore, "sessions:", time.Hour) 签发随机 opaque bearer token;存储仅使用 token 的 SHA-256 键,可 Revoke。Redis 缓存可共享 Session,内存缓存只在单实例生效且可能因容量淘汰提前登出。不要把 Session token 当普通缓存值写日志。
RequireAuth(authenticator) 在前,RequirePermission(policy, action, resource) 在后。Policy 接收已验证 Principal,业务自行校验租户、角色、资源归属。示例 examples/platform 验证了无身份 401、角色不足 403 和授权写入。
配置与日志热更新
config.LoadFile(path, prefix) 按默认值→文件→非空环境变量加载并验证。两种文件格式都使用 JSON tag 的 lowerCamelCase,duration 可以写 "5s";文件上限 1 MiB,拒绝未知字段、多文档和非法值。自定义 prefix 要包含期望的尾部下划线,例如 BILLING_。空环境变量沿用文件值,不清空已配置字段。
address: '127.0.0.1:8080'
transport: native
shutdownTimeout: 10s
log:
level: info
format: json
先按配置创建 logx.New(cfg.Log),再 config.NewReloader(path, prefix, logger)。显式调用 Reload() 或运行 Watch(ctx, time.Second, onError);后者阻塞,生命周期由应用负责。只有 log.level 允许变化,校验失败或静态字段变化保留原快照与日志级别。子 logger 共享动态级别。替换文件时建议使用临时文件 + rename。
熔断与观测
为每个上游创建 resilience.New(Config{MaxConcurrent: 16, FailureThreshold: 5});通用调用用 Do,HTTP 用 resilience.Transport{Base: transport, Breaker: breaker}。HTTP 槽位覆盖响应体读取,必须关闭 body;网络错误/5xx 计失败,冷却后只放行一个探针。不要对效果未知的写操作自行换身份重试。
telemetry.New 接受 OTel sdktrace.SpanExporter 和 sdkmetric.Reader;可注入官方 OTLP exporter、PeriodicReader 或测试用 exporter,基础包不强制选择 collector 协议。app.Use(runtime.Middleware()) 采集入站,telemetry.Transport 采集出站并传播 W3C trace context;Redis 使用 client.AddHook(telemetry.RedisHook{Runtime: runtime}),SQL 使用 telemetry.SQL{DB: db, Runtime: runtime}。业务操作/事务可用 runtime.Do(ctx, "fixedOperationName", fn) 包裹。
创建者负责在停止请求/Worker 后调用 runtime.Shutdown(ctx) 刷新并关闭 exporter/reader。指标按注册路由模板和固定方法分类;自动采集不记录 URL query、SQL 正文、Redis key、token 或业务载荷。业务自定义 span 名称同样应固定,避免用用户 ID 或对象 key。
分布式限流与幂等
middleware/redis.NewRateLimiter(client, namespace, cfg) 跨副本使用 Redis 时间与原子令牌桶;满额/存储失败返回 503,额度不足返回 429 和 Retry-After。NewIdempotencyStore 注入 middleware.IdempotencyConfig.Store;Scope 必须来自认证后的租户/用户,先认证再幂等。不同请求内容复用同一身份返回 409,已完成请求回放原响应和 requestId。
claim 在开始业务前持久保存,完成按 owner 原子检查。进程崩溃留下的 running claim 不自动按租约过期;返回 409 等待核查,Resolve 仅由运维核对后显式调用。已保存的完成/失败记录按配置 TTL 清理,TTL 过后不再提供业务去重保证。失败结果、含 Set-Cookie/no-store 的响应不用于成功回放。共享 Redis 需要 noeviction 和独立 namespace,数据丢失时不存在框架层面的重复执行保证。
迁移、审计与分页
迁移命名:0001_createUsers.up.sql,可附 .down.sql,但第一版 CLI 只执行 up。go-omni migrate --dir migrations 默认打印版本/方向/SHA-256 清单,不连接数据库;部署步骤显式传 --apply 才读取 GO_OMNI_DB_DSN 执行。迁移使用独立连接与数据库锁;MySQL DDL 可能部分提交,失败保留 dirty,禁止自动 force 或回滚。当前基于 golang-migrate,数据库版本表不保存每个历史文件校验和,部署系统应保留并校对预览清单。取消在当前有界 SQL 操作结束后停止,重试需新建 Runner。
审计:audit.New(sink, "approvedField") 建白名单,再 Record(ctx, Event{ActorID, Action, Resource, Outcome, Fields});补入服务端时间、requestId/traceId,只保留明确允许的属性,Sink 失败原样返回。JSONSink 不保证与业务提交原子一致,需要这种语义时由业务在同一数据库事务内提供 Sink/Outbox。
分页:pagination.Parse(request.URL.Query(), pagination.Config{}) 默认 page=1/pageSize=20,上限 100,最大 offset=1,000,000;拒绝重复参数、溢出和越界。pagination.NewResult(request, items, total) 输出 items/page/pageSize/total/totalPages,空列表为 []。查询与租户过滤仍由 Repository 负责。
验证入口
go test -race ./...
go vet ./...
go build ./...
python3 scripts/verifyModules.py --advanced --goproxy https://goproxy.cn,direct --output performance/advancedLinux.json
Linux runner 顺序运行编译、Redis、MinIO、MySQL 四个容器,各限 2 CPU、2 GiB、256 pids;数据阶段无外网、无发布端口,不连接现有数据库。原始结果和源码摘要以生成的 JSON/TXT 为准,未提供云凭据的 OSS/R2 不标为实网通过。