All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m34s
8.2 KiB
8.2 KiB
Context
当前 internal/gateway/ 包存在以下问题:
- 仅为薄 HTTP 包装:
Client只有doRequest统一处理加密/签名/HTTP 请求,每个业务方法手动构建map[string]interface{}参数和手动 unmarshal 响应 - Handler 层架构违规:IotCard Handler(6 处)和 Device Handler(7 处)直接持有并调用
gateway.Client,跳过了 Service 层 - 无日志和重试:Gateway 调用无请求/响应日志,网络错误无重试机制
- 代码重复度高:13 个方法中大量 map 构建 + unmarshal 模式重复
当前调用链
// 违规模式(当前)
Handler.gatewayClient.XXX() → gateway.Client.doRequest() → HTTP
// 正确模式(目标)
Handler → Service.XXX() → gateway.Client.XXX() → HTTP
现有依赖关系
| 使用者 | 使用方式 | 是否合规 |
|---|---|---|
handler/admin/iot_card.go |
直接调用 gatewayClient(6 处) | ❌ |
handler/admin/device.go |
直接调用 gatewayClient(7 处) | ❌ |
service/iot_card/service.go |
通过 Service 调用(1 处:QueryCardStatus) | ✅ |
service/iot_card/stop_resume_service.go |
通过 Service 调用(2 处:StopCard、StartCard) | ✅ |
task/polling_handler.go |
Worker 任务中调用(合理) | ✅ |
pkg/queue/handler.go |
Worker 中初始化 polling_handler | ✅ |
Goals / Non-Goals
Goals:
- 消除手动 map 构建,请求结构体直接序列化为 Gateway 参数格式
- 添加泛型响应解析方法,消除重复 unmarshal 代码
- 在
doRequest中添加 Zap 日志(请求路径、耗时、错误) - 添加网络级错误重试机制(连接失败、超时等)
- 将 Handler 层 13 个 Gateway 直接调用下沉到对应 Service 层
- Handler 不再持有
gateway.Client引用
Non-Goals:
- 异步接口(device/info、device/card-info)的轮询/回调处理
- 新增未封装的 Gateway 端点
- 修改 Gateway 上游项目
- 修改 crypto.go(加密/签名逻辑不变)
- 修改响应模型的字段(不确定上游实际返回结构,保持现状)
Decisions
Decision 1:请求参数序列化方式
选择:请求结构体通过 sonic.Marshal 直接序列化后嵌入 businessData.params 字段
理由:
- 现有请求结构体(
SpeedLimitReq、WiFiReq等)已有正确的jsontag - 直接序列化后再嵌入 params,比手动构建 map 更安全、更不易出错
- 保持
doRequest的签名不变,只改变调用方式
实现:
// 之前
params := map[string]interface{}{
"cardNo": req.CardNo,
"speedLimit": req.SpeedLimit,
}
businessData := map[string]interface{}{"params": params}
// 之后(方案 A:修改 doRequest 签名)
// doRequest 接收结构体,内部自动包装为 {"params": ...}
resp, err := c.doRequest(ctx, "/device/speed-limit", req)
// doRequest 内部逻辑变更:
// businessData → 直接是 req 结构体
// 序列化时自动包装为 {"params": <req JSON>}
替代方案:保留 map 构建但提取为辅助方法 — 更侵入性小但不消除根因
Decision 2:泛型响应解析
选择:提供 doRequestWithResponse[T any] 泛型方法
func doRequestWithResponse[T any](ctx context.Context, path string, req interface{}) (*T, error) {
data, err := c.doRequest(ctx, path, req)
if err != nil {
return nil, err
}
var result T
if err := sonic.Unmarshal(data, &result); err != nil {
return nil, errors.Wrap(errors.CodeGatewayInvalidResp, err, "解析 Gateway 响应失败")
}
return &result, nil
}
理由:每个返回结构体的方法都有相同的 unmarshal 模式,泛型可以完全消除
Decision 3:日志集成
选择:在 doRequest 中注入 *zap.Logger(通过 Client 构造函数传入)
日志内容:
- 请求:路径、请求体大小
- 成功响应:路径、耗时
- 失败响应:路径、耗时、错误码、错误信息
日志级别:
- 正常请求:
Debug(避免高频日志影响性能) - 错误:
Warn(Gateway 业务错误)或Error(网络错误)
理由:
- 不用全局
zap.L(),通过构造函数注入,保持可测试性 - Debug 级别日志在生产环境默认关闭,不影响性能
Decision 4:重试机制
选择:在 doRequest 内部实现简单重试逻辑
重试条件(仅网络级错误):
- 连接失败(
net.Error的Temporary()) - 请求超时(
context.DeadlineExceeded,仅限 Client 自身超时,非用户 ctx 超时) - DNS 解析失败
不重试的情况:
- Gateway 业务错误(code != 200)
- HTTP 状态码错误(4xx、5xx)
- 用户 Context 取消
- 加密/序列化失败
参数:
- 默认最大重试 2 次(共 3 次尝试)
- 指数退避:100ms → 300ms
- 通过
WithRetry(maxRetries int)链式方法可配置
理由:不引入外部重试库(如 cenkalti/backoff),保持零新依赖
Decision 5:分层修复策略
选择:将 Handler 中的 Gateway 调用逐个迁移到对应 Service 层
IotCard Service 新增方法(6 个):
QueryGatewayStatus(ctx, iccid) → (*gateway.CardStatusResp, error)— 已有权限检查 + Gateway 调用QueryGatewayFlow(ctx, iccid) → (*gateway.FlowUsageResp, error)QueryGatewayRealname(ctx, iccid) → (*gateway.RealnameStatusResp, error)GetGatewayRealnameLink(ctx, iccid) → (*gateway.RealnameLinkResp, error)GatewayStopCard(ctx, iccid) → error— 注意:与已有的stop_resume_service.go中的StopCard区分,Handler 层的更简单(直接停卡,无业务逻辑)GatewayStartCard(ctx, iccid) → error
Device Service 新增方法(7 个):
GetGatewayInfo(ctx, identifier) → (*gateway.DeviceInfoResp, error)— 包含 identifier→IMEI 转换GetGatewaySlots(ctx, identifier) → (*gateway.SlotInfoResp, error)SetGatewaySpeedLimit(ctx, identifier, speedLimit) → errorSetGatewayWiFi(ctx, identifier, req) → errorGatewaySwitchCard(ctx, identifier, targetICCID) → errorGatewayRebootDevice(ctx, identifier) → errorGatewayResetDevice(ctx, identifier) → error
Handler 变更:
IotCardHandler移除gatewayClient字段,改为调用 Service 方法DeviceHandler移除gatewayClient字段,改为调用 Service 方法NewIotCardHandler和NewDeviceHandler签名去掉gatewayClient参数- 权限检查(
service.GetByICCID、service.GetDeviceByIdentifier)移入 Service 方法内部
Bootstrap 变更:
handlers.go:Handler 初始化不再传入gatewayClientservices.go:确保 Device Service 接收gatewayClient(当前未注入)
Decision 6:Logger 注入到 Client
选择:NewClient 增加 logger *zap.Logger 参数
func NewClient(baseURL, appID, appSecret string, logger *zap.Logger) *Client
理由:
- 保持依赖注入风格,与项目其他组件一致
- 不使用全局 logger
zap.L()
Risks / Trade-offs
| 风险 | 缓解措施 |
|---|---|
IotCard Handler 已有的停卡/复机逻辑在 stop_resume_service.go 中有更复杂的实现(包含状态更新) |
Handler 层的 StopCard/StartCard 是直接透传,与 stop_resume_service 不同。Service 层新增的方法命名区分(GatewayStopCard vs StopCardService) |
doRequest 签名变更影响所有调用方 |
doRequest 保持返回 json.RawMessage,新增 doRequestWithResponse 泛型方法,渐进迁移 |
Logger 注入改变 NewClient 签名 |
只有 cmd/api/main.go 和 cmd/worker/main.go 两处调用,改动可控 |
| 重试可能导致写操作重复执行(如 StopCard) | 仅对网络级错误重试,Gateway 已返回 response 的不重试。写操作的幂等性由 Gateway 端保证 |
| 泛型需要 Go 1.18+ | 项目已使用 Go 1.25,无兼容问题 |
Open Questions
Device Service 当前没有— 已确认需要修改gatewayClient注入,需要修改NewDeviceService构造函数- IotCard Handler 的
GatewayStopCard/GatewayStartCard与已有的stop_resume_service如何命名区分 — 方案:Handler 下沉的方法以Gateway前缀命名