docs(03-device-system): 创建 Phase 3 设备体系完善计划(2个Plan)
This commit is contained in:
@@ -70,7 +70,11 @@ Plans:
|
||||
2. 调用设备刷新接口后,设备的在线状态、固件版本、当前使用卡(is_current)从 Gateway 实时更新
|
||||
3. tb_device_sim_binding 表含 is_current 字段,同一设备同一时刻只有一条 is_current=true 的绑定记录
|
||||
4. 设备详情接口返回数据包含 online_status、software_version 等字段,值与 Gateway 同步结果一致
|
||||
**Plans**: TBD
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 03-01-PLAN.md — DEVICE-01/02/03:DB 迁移 + 模型扩展 + Gateway SyncDeviceInfo + DTO 字段(Wave 1)
|
||||
- [ ] 03-02-PLAN.md — DEVICE-04:Asset Service 接入 sync-info,Refresh 追加设备状态同步(Wave 2)
|
||||
|
||||
---
|
||||
|
||||
|
||||
403
.planning/phases/03-device-system/03-01-PLAN.md
Normal file
403
.planning/phases/03-device-system/03-01-PLAN.md
Normal file
@@ -0,0 +1,403 @@
|
||||
---
|
||||
phase: 03-device-system
|
||||
plan: "01"
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- migrations/000090_device_fields_extension.up.sql
|
||||
- migrations/000090_device_fields_extension.down.sql
|
||||
- migrations/000091_device_sim_binding_is_current.up.sql
|
||||
- migrations/000091_device_sim_binding_is_current.down.sql
|
||||
- internal/model/device.go
|
||||
- internal/model/device_sim_binding.go
|
||||
- internal/gateway/models.go
|
||||
- internal/gateway/device.go
|
||||
- internal/model/dto/device_dto.go
|
||||
- internal/model/dto/asset_dto.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DEVICE-01
|
||||
- DEVICE-02
|
||||
- DEVICE-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "tb_device 表含 online_status / last_online_time / software_version / switch_mode / last_gateway_sync_at 字段"
|
||||
- "tb_device_sim_binding 表含 is_current 字段"
|
||||
- "Gateway Client 具备 SyncDeviceInfo() 方法"
|
||||
- "DeviceResponse DTO 新增 5 个字段;BoundCardInfo + DeviceCardBindingResponse 新增 is_current"
|
||||
- "make migrate-up 执行成功,go build ./... 编译通过"
|
||||
artifacts:
|
||||
- path: "migrations/000090_device_fields_extension.up.sql"
|
||||
provides: "tb_device 字段扩展迁移"
|
||||
- path: "migrations/000091_device_sim_binding_is_current.up.sql"
|
||||
provides: "tb_device_sim_binding.is_current 迁移"
|
||||
- path: "internal/model/device.go"
|
||||
provides: "Device 模型新增 5 字段"
|
||||
- path: "internal/model/device_sim_binding.go"
|
||||
provides: "DeviceSimBinding 模型新增 is_current"
|
||||
- path: "internal/gateway/models.go"
|
||||
provides: "SyncDeviceInfoReq / SyncDeviceInfoResp 结构定义"
|
||||
- path: "internal/gateway/device.go"
|
||||
provides: "SyncDeviceInfo() 方法"
|
||||
- path: "internal/model/dto/device_dto.go"
|
||||
provides: "DeviceResponse + DeviceCardBindingResponse 字段扩展"
|
||||
- path: "internal/model/dto/asset_dto.go"
|
||||
provides: "BoundCardInfo.IsCurrent 字段扩展"
|
||||
key_links:
|
||||
- from: "internal/gateway/device.go:SyncDeviceInfo"
|
||||
to: "internal/gateway/client.go:doRequestWithResponse"
|
||||
via: "泛型调用"
|
||||
pattern: "doRequestWithResponse\\[SyncDeviceInfoResp\\]"
|
||||
- from: "internal/model/device.go:Device"
|
||||
to: "migrations/000090"
|
||||
via: "字段对应迁移"
|
||||
pattern: "OnlineStatus.*online_status"
|
||||
---
|
||||
|
||||
<objective>
|
||||
建立设备体系的数据基础层:扩展 DB 字段(DEVICE-01)、新增 is_current 绑定标识(DEVICE-03)、对接 Gateway sync-info 接口(DEVICE-02)。
|
||||
|
||||
Purpose: Plan 2 的 Refresh 接入依赖本 Plan 的全部产出(模型字段 + Gateway 方法 + DTO 定义)
|
||||
Output: 2 个迁移对、Device/DeviceSimBinding 模型更新、Gateway SyncDeviceInfo 方法、DTO 扩展
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-device-system/03-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- 现有代码契约,executor 直接使用,无需探索代码库 -->
|
||||
|
||||
**internal/gateway/device.go(现有方法):**
|
||||
```go
|
||||
func (c *Client) GetDeviceInfo(ctx context.Context, req *DeviceInfoReq) (*DeviceInfoResp, error)
|
||||
func (c *Client) GetSlotInfo(ctx context.Context, req *DeviceInfoReq) (*SlotInfoResp, error)
|
||||
// 其他方法略
|
||||
```
|
||||
|
||||
**internal/gateway/client.go(泛型请求方法):**
|
||||
```go
|
||||
// 签名:
|
||||
func doRequestWithResponse[T any](c *Client, ctx context.Context, path string, params interface{}) (*T, error)
|
||||
// 用法(参考 GetDeviceInfo):
|
||||
return doRequestWithResponse[DeviceInfoResp](c, ctx, "/device/info", req)
|
||||
```
|
||||
|
||||
**internal/gateway/models.go 中已有的设备 DTO(参考风格):**
|
||||
```go
|
||||
type DeviceInfoReq struct {
|
||||
CardNo string `json:"cardNo,omitempty" description:"..."`
|
||||
DeviceID string `json:"deviceId,omitempty" description:"..."`
|
||||
}
|
||||
type DeviceInfoResp struct {
|
||||
IMEI string `json:"imei" description:"..."`
|
||||
OnlineStatus int `json:"onlineStatus" description:"..."`
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**internal/model/device.go 现有字段(末尾追加新字段):**
|
||||
```go
|
||||
type Device struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
VirtualNo string `gorm:"column:virtual_no;..."`
|
||||
IMEI string `gorm:"column:imei;type:varchar(20);..."`
|
||||
SN string `gorm:"column:sn;type:varchar(100);..."`
|
||||
// ... 其余字段 ...
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;..."`
|
||||
// 在 Generation 之后追加 5 个新字段
|
||||
}
|
||||
```
|
||||
|
||||
**internal/model/device_sim_binding.go 现有字段(末尾追加 is_current):**
|
||||
```go
|
||||
type DeviceSimBinding struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
DeviceID uint `gorm:"column:device_id;..."`
|
||||
IotCardID uint `gorm:"column:iot_card_id;..."`
|
||||
SlotPosition int `gorm:"column:slot_position;..."`
|
||||
BindStatus int `gorm:"column:bind_status;..."`
|
||||
BindTime *time.Time `gorm:"column:bind_time;..."`
|
||||
UnbindTime *time.Time `gorm:"column:unbind_time;..."`
|
||||
// 追加 IsCurrent
|
||||
}
|
||||
```
|
||||
|
||||
**internal/model/dto/device_dto.go 中 DeviceResponse 和 DeviceCardBindingResponse:**
|
||||
```go
|
||||
type DeviceResponse struct {
|
||||
// ... 现有字段 ...
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
// 在 UpdatedAt 之后追加 5 个新字段
|
||||
}
|
||||
|
||||
type DeviceCardBindingResponse struct {
|
||||
// ... 现有字段(末尾是 BindTime)...
|
||||
BindTime *time.Time `json:"bind_time,omitempty" description:"绑定时间"`
|
||||
// 追加 IsCurrent
|
||||
}
|
||||
```
|
||||
|
||||
**internal/model/dto/asset_dto.go 中 BoundCardInfo:**
|
||||
```go
|
||||
type BoundCardInfo struct {
|
||||
CardID uint `json:"card_id" description:"卡ID"`
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
|
||||
NetworkStatus int `json:"network_status" description:"网络状态:0停机 1开机"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态"`
|
||||
SlotPosition int `json:"slot_position,omitempty" description:"插槽位置"`
|
||||
// 追加 IsCurrent
|
||||
}
|
||||
```
|
||||
|
||||
**Gateway sync-info 接口:**
|
||||
- URL:`POST /v1/iot/openApi/device/sync-info`(业务路径参数传 `/device/sync-info`)
|
||||
- cardNo:ICCID(>15位)/ IMEI(=15位)/ SN(=11位)
|
||||
- 关键响应字段:current_iccid, software_version, switch_mode, online_status, last_online_time(ISO 8601 string), last_update_time
|
||||
|
||||
**最新迁移编号**:`000089_realname_expiry_base`,新迁移从 `000090` 开始
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: DB 迁移 + 模型字段扩展(D-0 + D-2 DB 层)</name>
|
||||
<files>
|
||||
migrations/000090_device_fields_extension.up.sql,
|
||||
migrations/000090_device_fields_extension.down.sql,
|
||||
migrations/000091_device_sim_binding_is_current.up.sql,
|
||||
migrations/000091_device_sim_binding_is_current.down.sql,
|
||||
internal/model/device.go,
|
||||
internal/model/device_sim_binding.go
|
||||
</files>
|
||||
<action>
|
||||
## 迁移文件 000090(tb_device 字段扩展)
|
||||
|
||||
创建 `migrations/000090_device_fields_extension.up.sql`:
|
||||
```sql
|
||||
-- 扩展 tb_device 设备表,新增 Gateway sync-info 同步的存储字段
|
||||
-- 这些字段有"离线意义"(设备下线后仍需保留最后状态),非实时字段(rssi/battery 等)不入库
|
||||
ALTER TABLE tb_device
|
||||
ADD COLUMN online_status INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN last_online_time TIMESTAMP NULL,
|
||||
ADD COLUMN software_version VARCHAR(100) NOT NULL DEFAULT '',
|
||||
ADD COLUMN switch_mode VARCHAR(10) NOT NULL DEFAULT '0',
|
||||
ADD COLUMN last_gateway_sync_at TIMESTAMP NULL;
|
||||
|
||||
COMMENT ON COLUMN tb_device.online_status IS '在线状态:0未知,1在线,2离线';
|
||||
COMMENT ON COLUMN tb_device.last_online_time IS '最后在线时间(来自 Gateway sync-info)';
|
||||
COMMENT ON COLUMN tb_device.software_version IS '固件版本号';
|
||||
COMMENT ON COLUMN tb_device.switch_mode IS '切卡模式:0=自动,1=手动';
|
||||
COMMENT ON COLUMN tb_device.last_gateway_sync_at IS '最后一次 sync-info 同步时间';
|
||||
```
|
||||
|
||||
创建 `migrations/000090_device_fields_extension.down.sql`:
|
||||
```sql
|
||||
ALTER TABLE tb_device
|
||||
DROP COLUMN IF EXISTS online_status,
|
||||
DROP COLUMN IF EXISTS last_online_time,
|
||||
DROP COLUMN IF EXISTS software_version,
|
||||
DROP COLUMN IF EXISTS switch_mode,
|
||||
DROP COLUMN IF EXISTS last_gateway_sync_at;
|
||||
```
|
||||
|
||||
## 迁移文件 000091(tb_device_sim_binding 新增 is_current)
|
||||
|
||||
创建 `migrations/000091_device_sim_binding_is_current.up.sql`:
|
||||
```sql
|
||||
-- 新增 is_current 字段,标识设备当前正在使用的卡(由 sync-info current_iccid 字段决定)
|
||||
ALTER TABLE tb_device_sim_binding
|
||||
ADD COLUMN is_current BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
COMMENT ON COLUMN tb_device_sim_binding.is_current IS '是否为当前使用的卡(同一设备同一时刻最多一条为 true)';
|
||||
```
|
||||
|
||||
创建 `migrations/000091_device_sim_binding_is_current.down.sql`:
|
||||
```sql
|
||||
ALTER TABLE tb_device_sim_binding
|
||||
DROP COLUMN IF EXISTS is_current;
|
||||
```
|
||||
|
||||
## 执行迁移
|
||||
|
||||
执行 `make migrate-up` 并确认:
|
||||
- `make migrate-version` 输出版本为 91,dirty=false
|
||||
- 使用 PostgreSQL MCP 验证 tb_device 新增 5 列、tb_device_sim_binding 新增 is_current
|
||||
|
||||
## 更新 internal/model/device.go
|
||||
|
||||
在 `Generation` 字段之后追加 5 个字段(保持与其他字段相同的标签格式):
|
||||
```go
|
||||
// 以下字段由 Gateway sync-info 接口同步,有离线存储意义
|
||||
OnlineStatus int `gorm:"column:online_status;type:int;not null;default:0;comment:在线状态:0未知 1在线 2离线" json:"online_status"`
|
||||
LastOnlineTime *time.Time `gorm:"column:last_online_time;comment:最后在线时间(来自 Gateway sync-info)" json:"last_online_time,omitempty"`
|
||||
SoftwareVersion string `gorm:"column:software_version;type:varchar(100);not null;default:'';comment:固件版本号" json:"software_version"`
|
||||
SwitchMode string `gorm:"column:switch_mode;type:varchar(10);not null;default:'0';comment:切卡模式:0=自动 1=手动" json:"switch_mode"`
|
||||
LastGatewaySyncAt *time.Time `gorm:"column:last_gateway_sync_at;comment:最后一次 sync-info 同步时间" json:"last_gateway_sync_at,omitempty"`
|
||||
```
|
||||
|
||||
## 更新 internal/model/device_sim_binding.go
|
||||
|
||||
在 `UnbindTime` 字段之后追加:
|
||||
```go
|
||||
IsCurrent bool `gorm:"column:is_current;type:boolean;not null;default:false;comment:是否为当前使用的卡" json:"is_current"`
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>make migrate-up && make migrate-version && go build ./...</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- migrate-version 显示版本 91,dirty=false
|
||||
- go build ./... 编译无新增错误
|
||||
- tb_device 含 5 个新字段,tb_device_sim_binding 含 is_current(可用 MCP dbhub_search_objects 验证)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Gateway SyncDeviceInfo 方法 + DTO 字段扩展(D-1 + D-2 DTO 层)</name>
|
||||
<files>
|
||||
internal/gateway/models.go,
|
||||
internal/gateway/device.go,
|
||||
internal/model/dto/device_dto.go,
|
||||
internal/model/dto/asset_dto.go
|
||||
</files>
|
||||
<action>
|
||||
## internal/gateway/models.go — 新增 SyncDeviceInfo 请求/响应结构
|
||||
|
||||
在文件末尾(`// ============ 结束 ============` 之前或文件末)追加,放在「设备相关 DTO」分组下:
|
||||
|
||||
```go
|
||||
// SyncDeviceInfoReq sync-info 同步查询设备信息请求
|
||||
// cardNo 规则:>15 位用 ICCID,=15 位用 IMEI,=11 位用 SN
|
||||
type SyncDeviceInfoReq struct {
|
||||
CardNo string `json:"card_no" description:"设备标识(ICCID/IMEI/SN)"`
|
||||
}
|
||||
|
||||
// SyncDeviceInfoResp sync-info 同步查询设备信息响应(对应 Gateway data 字段)
|
||||
// 注意:所有字段均可能为 null/零值,表示暂无数据
|
||||
type SyncDeviceInfoResp struct {
|
||||
DeviceID string `json:"device_id" description:"设备ID(IMEI/SN)"`
|
||||
DeviceName string `json:"device_name" description:"设备名称"`
|
||||
IMEI string `json:"imei" description:"IMEI号"`
|
||||
CurrentIccid string `json:"current_iccid" description:"当前使用的ICCID"`
|
||||
DeviceType string `json:"device_type" description:"设备类型:1=诺行,2=玺龙,3=迎势达"`
|
||||
SoftwareVersion string `json:"software_version" description:"软件版本号"`
|
||||
MacAddress string `json:"mac_address" description:"MAC地址"`
|
||||
SSID string `json:"ssid" description:"WiFi热点名称"`
|
||||
WifiEnabled bool `json:"wifi_enabled" description:"WiFi开关状态"`
|
||||
SwitchMode string `json:"switch_mode" description:"切卡模式:0=自动,1=手动"`
|
||||
RSSI string `json:"rssi" description:"接收信号强度"`
|
||||
BatteryLevel *int `json:"battery_level" description:"电池电量百分比(无电池时为null)"`
|
||||
OnlineStatus int `json:"online_status" description:"在线状态:1=在线,2=离线"`
|
||||
LastUpdateTime string `json:"last_update_time" description:"设备信息最后更新时间(ISO 8601)"`
|
||||
LastOnlineTime string `json:"last_online_time" description:"设备最后在线时间(ISO 8601)"`
|
||||
RunTime string `json:"run_time" description:"本次开机运行时间(秒)"`
|
||||
DailyUsage string `json:"daily_usage" description:"日使用流量(字节)"`
|
||||
}
|
||||
```
|
||||
|
||||
## internal/gateway/device.go — 新增 SyncDeviceInfo 方法
|
||||
|
||||
在文件末尾追加:
|
||||
|
||||
```go
|
||||
// SyncDeviceInfo 同步查询设备信息(直接返回,无需回调)
|
||||
// 与异步 /device/info 的区别:直接返回全量数据,无需 callbackUrl
|
||||
// POST /device/sync-info
|
||||
func (c *Client) SyncDeviceInfo(ctx context.Context, req *SyncDeviceInfoReq) (*SyncDeviceInfoResp, error) {
|
||||
if req.CardNo == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "cardNo 不能为空")
|
||||
}
|
||||
return doRequestWithResponse[SyncDeviceInfoResp](c, ctx, "/device/sync-info", req)
|
||||
}
|
||||
```
|
||||
|
||||
更新文件顶部包注释:「提供设备相关的 8 个 API 方法封装」(从 7 改为 8)
|
||||
|
||||
## internal/model/dto/device_dto.go — DeviceResponse 和 DeviceCardBindingResponse 字段扩展
|
||||
|
||||
**DeviceResponse** — 在 `UpdatedAt` 字段之后追加(per D-0):
|
||||
```go
|
||||
// 设备实时同步字段(由 sync-info 接口写入)
|
||||
OnlineStatus int `json:"online_status" description:"在线状态:0未知 1在线 2离线"`
|
||||
LastOnlineTime *time.Time `json:"last_online_time,omitempty" description:"最后在线时间"`
|
||||
SoftwareVersion string `json:"software_version" description:"固件版本号"`
|
||||
SwitchMode string `json:"switch_mode" description:"切卡模式:0=自动,1=手动"`
|
||||
LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at,omitempty" description:"最后 sync-info 同步时间"`
|
||||
```
|
||||
|
||||
**DeviceCardBindingResponse** — 在 `BindTime` 字段之后追加(per D-2):
|
||||
```go
|
||||
IsCurrent bool `json:"is_current" description:"是否为当前使用的卡"`
|
||||
```
|
||||
|
||||
## internal/model/dto/asset_dto.go — BoundCardInfo 字段扩展
|
||||
|
||||
在 `SlotPosition` 字段之后追加(per D-2,D-11 决策):
|
||||
```go
|
||||
IsCurrent bool `json:"is_current" description:"是否为当前使用的卡"`
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
执行 `go build ./...`,确认无编译错误。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go build ./...</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- SyncDeviceInfoReq / SyncDeviceInfoResp 在 internal/gateway/models.go 中定义
|
||||
- SyncDeviceInfo() 方法在 internal/gateway/device.go 中实现
|
||||
- DeviceResponse 含 5 个新字段,DeviceCardBindingResponse 含 is_current
|
||||
- BoundCardInfo 含 is_current
|
||||
- go build ./... 零新增错误
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# 1. 迁移正常
|
||||
make migrate-version
|
||||
# 期望:version=91, dirty=false
|
||||
|
||||
# 2. 编译通过
|
||||
go build ./...
|
||||
|
||||
# 3. 字段验证(通过 PostgreSQL MCP)
|
||||
# dbhub_search_objects: object_type=column, table=tb_device
|
||||
# 确认:online_status / last_online_time / software_version / switch_mode / last_gateway_sync_at
|
||||
# dbhub_search_objects: object_type=column, table=tb_device_sim_binding
|
||||
# 确认:is_current
|
||||
|
||||
# 4. Gateway 方法搜索
|
||||
grep -r "SyncDeviceInfo" internal/gateway/
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `make migrate-version` 输出 version=91, dirty=false
|
||||
- [ ] `go build ./...` 零新增编译错误
|
||||
- [ ] `internal/gateway/device.go` 含 `SyncDeviceInfo()` 方法
|
||||
- [ ] `internal/gateway/models.go` 含 `SyncDeviceInfoReq` / `SyncDeviceInfoResp`
|
||||
- [ ] `DeviceResponse` 含 5 个新字段,`DeviceCardBindingResponse` 含 `is_current`
|
||||
- [ ] `BoundCardInfo` 含 `is_current`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/03-device-system/03-01-SUMMARY.md`
|
||||
</output>
|
||||
405
.planning/phases/03-device-system/03-02-PLAN.md
Normal file
405
.planning/phases/03-device-system/03-02-PLAN.md
Normal file
@@ -0,0 +1,405 @@
|
||||
---
|
||||
phase: 03-device-system
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "03-01"
|
||||
files_modified:
|
||||
- internal/service/asset/service.go
|
||||
- internal/bootstrap/services.go
|
||||
- internal/store/postgres/device_sim_binding_store.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DEVICE-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "调用设备刷新接口后,设备的在线状态、固件版本、当前使用卡从 Gateway 实时更新"
|
||||
- "同一设备同一时刻只有一条 is_current=true 的绑定记录(事务原子更新)"
|
||||
- "sync-info 调用失败时不阻断刷新流程,仅记录 Warn 日志"
|
||||
- "go build ./... 编译通过"
|
||||
artifacts:
|
||||
- path: "internal/service/asset/service.go"
|
||||
provides: "updateDeviceFromSyncInfo 私有函数 + Refresh device 分支 sync-info 调用"
|
||||
contains: "updateDeviceFromSyncInfo"
|
||||
- path: "internal/bootstrap/services.go"
|
||||
provides: "asset.Service 注入 gatewayClient"
|
||||
- path: "internal/store/postgres/device_sim_binding_store.go"
|
||||
provides: "UpdateIsCurrentByDeviceID Store 方法(事务两步更新)"
|
||||
key_links:
|
||||
- from: "internal/service/asset/service.go:Refresh"
|
||||
to: "internal/gateway/device.go:SyncDeviceInfo"
|
||||
via: "s.gatewayClient.SyncDeviceInfo()"
|
||||
pattern: "gatewayClient\\.SyncDeviceInfo"
|
||||
- from: "internal/service/asset/service.go:updateDeviceFromSyncInfo"
|
||||
to: "internal/store/postgres/device_sim_binding_store.go:UpdateIsCurrentByDeviceID"
|
||||
via: "事务原子更新 is_current"
|
||||
pattern: "UpdateIsCurrentByDeviceID"
|
||||
---
|
||||
|
||||
<objective>
|
||||
将 Gateway sync-info 接口接入设备 Refresh 流程(DEVICE-04):在设备刷新完成绑定卡后,调用 SyncDeviceInfo 同步设备自身状态(在线状态、固件版本、is_current 当前卡标识)。
|
||||
|
||||
Purpose: 使设备详情页展示真实的 online_status、software_version、is_current 等字段(Phase 3 成功标准 2、4)
|
||||
Output: updateDeviceFromSyncInfo 私有函数 + Refresh device 分支追加 sync-info 调用
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-device-system/03-CONTEXT.md
|
||||
@.planning/phases/03-device-system/03-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- 本 Plan 依赖 Plan 01 产出的接口,executor 直接使用 -->
|
||||
|
||||
**Plan 01 产出的类型(internal/gateway/models.go):**
|
||||
```go
|
||||
type SyncDeviceInfoReq struct {
|
||||
CardNo string `json:"card_no"`
|
||||
}
|
||||
type SyncDeviceInfoResp struct {
|
||||
CurrentIccid string `json:"current_iccid"`
|
||||
SoftwareVersion string `json:"software_version"`
|
||||
SwitchMode string `json:"switch_mode"`
|
||||
OnlineStatus int `json:"online_status"` // 1=在线, 2=离线
|
||||
LastOnlineTime string `json:"last_online_time"` // ISO 8601 字符串
|
||||
LastUpdateTime string `json:"last_update_time"` // ISO 8601 字符串
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
**Plan 01 产出的方法(internal/gateway/device.go):**
|
||||
```go
|
||||
func (c *Client) SyncDeviceInfo(ctx context.Context, req *SyncDeviceInfoReq) (*SyncDeviceInfoResp, error)
|
||||
```
|
||||
|
||||
**Plan 01 产出的模型字段(internal/model/device.go):**
|
||||
```go
|
||||
OnlineStatus int // gorm:"column:online_status"
|
||||
LastOnlineTime *time.Time // gorm:"column:last_online_time"
|
||||
SoftwareVersion string // gorm:"column:software_version"
|
||||
SwitchMode string // gorm:"column:switch_mode"
|
||||
LastGatewaySyncAt *time.Time // gorm:"column:last_gateway_sync_at"
|
||||
```
|
||||
|
||||
**Plan 01 产出的模型字段(internal/model/device_sim_binding.go):**
|
||||
```go
|
||||
IsCurrent bool // gorm:"column:is_current"
|
||||
```
|
||||
|
||||
**internal/service/asset/service.go 现有 Refresh device 分支(约 295-340 行):**
|
||||
```go
|
||||
case "device":
|
||||
// 检查冷却期
|
||||
cooldownKey := constants.RedisDeviceRefreshCooldownKey(id)
|
||||
if s.redis.Exists(ctx, cooldownKey).Val() > 0 {
|
||||
return nil, errors.New(errors.CodeTooManyRequests, "刷新过于频繁,请30秒后再试")
|
||||
}
|
||||
|
||||
// 查所有绑定卡,逐一刷新
|
||||
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定卡失败")
|
||||
}
|
||||
for _, b := range bindings {
|
||||
card, cardErr := s.iotCardStore.GetByID(ctx, b.IotCardID)
|
||||
if cardErr != nil {
|
||||
logger.GetAppLogger().Warn("刷新设备绑定卡失败:查卡失败", ...)
|
||||
continue
|
||||
}
|
||||
if refreshErr := s.iotCardService.RefreshCardDataFromGateway(ctx, card.ICCID); refreshErr != nil {
|
||||
logger.GetAppLogger().Warn("刷新设备绑定卡失败:网关调用失败", ...)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置冷却 Key
|
||||
s.redis.Set(ctx, cooldownKey, 1, constants.DeviceRefreshCooldownDuration)
|
||||
|
||||
return s.GetRealtimeStatus(ctx, "device", id)
|
||||
```
|
||||
|
||||
**internal/service/asset/service.go 现有 Service 结构体(需要追加 gatewayClient):**
|
||||
```go
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
deviceStore *postgres.DeviceStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
packageUsageStore *postgres.PackageUsageStore
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
shopStore *postgres.ShopStore
|
||||
redis *redis.Client
|
||||
iotCardService IotCardRefresher
|
||||
// 追加:
|
||||
// gatewayClient *gateway.Client
|
||||
}
|
||||
|
||||
// New() 函数目前签名(需要追加 gatewayClient 参数):
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
packageUsageStore *postgres.PackageUsageStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
redisClient *redis.Client,
|
||||
iotCardService IotCardRefresher,
|
||||
) *Service
|
||||
```
|
||||
|
||||
**internal/bootstrap/services.go 现有 asset.New 调用(需要追加 deps.GatewayClient):**
|
||||
```go
|
||||
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard),
|
||||
```
|
||||
|
||||
**内置 import 路径参考:**
|
||||
- `"github.com/break/junhong_cmp_fiber/internal/gateway"`
|
||||
- `"time"` 包用于 Parse ISO 8601 字符串
|
||||
|
||||
**ISO 8601 时间格式参考(来自 Gateway 文档示例):**
|
||||
- `last_online_time: "2026-03-23T16:53:18+08:00"`
|
||||
- Parse 用 `time.Parse(time.RFC3339, str)` 即可处理带时区的 ISO 8601
|
||||
|
||||
**DeviceSimBindingStore 现有方法(store 层,需新增 UpdateIsCurrentByDeviceID):**
|
||||
```go
|
||||
func (s *DeviceSimBindingStore) ListByDeviceID(ctx context.Context, deviceID uint) ([]*model.DeviceSimBinding, error)
|
||||
// 其他方法:Create, GetByID, GetByDeviceAndCard, Unbind, CountByDeviceID...
|
||||
// 无 UpdateIsCurrentByDeviceID,需要新增
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: DeviceSimBindingStore 新增 UpdateIsCurrentByDeviceID 方法</name>
|
||||
<files>
|
||||
internal/store/postgres/device_sim_binding_store.go
|
||||
</files>
|
||||
<action>
|
||||
在 `internal/store/postgres/device_sim_binding_store.go` 末尾追加以下方法:
|
||||
|
||||
```go
|
||||
// UpdateIsCurrentByDeviceID 原子更新设备当前使用的卡标识
|
||||
// 使用事务两步操作:先全部设为 false,再将 currentIccid 对应行设为 true
|
||||
// currentIccid 为空字符串时,仅执行全部清空(无当前卡的情况)
|
||||
func (s *DeviceSimBindingStore) UpdateIsCurrentByDeviceID(ctx context.Context, deviceID uint, currentIccid string) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 第一步:将该设备所有活跃绑定的 is_current 设为 false
|
||||
if err := tx.Model(&model.DeviceSimBinding{}).
|
||||
Where("device_id = ? AND bind_status = 1", deviceID).
|
||||
Update("is_current", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 第二步:将当前 ICCID 对应的绑定记录设为 is_current = true
|
||||
if currentIccid == "" {
|
||||
return nil
|
||||
}
|
||||
// 通过子查询找到对应的 iot_card_id(避免跨表 JOIN 更新)
|
||||
var iotCardID uint
|
||||
if err := tx.Table("tb_iot_card").
|
||||
Select("id").
|
||||
Where("iccid = ? AND deleted_at IS NULL", currentIccid).
|
||||
Scan(&iotCardID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if iotCardID == 0 {
|
||||
// 未找到对应卡记录,不更新(可能卡尚未入库)
|
||||
return nil
|
||||
}
|
||||
return tx.Model(&model.DeviceSimBinding{}).
|
||||
Where("device_id = ? AND iot_card_id = ? AND bind_status = 1", deviceID, iotCardID).
|
||||
Update("is_current", true).Error
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
确保文件顶部已有 `"gorm.io/gorm"` import(已有,无需新增)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go build ./internal/store/postgres/...</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `UpdateIsCurrentByDeviceID` 方法存在于 device_sim_binding_store.go
|
||||
- 事务内两步操作:先全清 false,再按 ICCID 设 true
|
||||
- go build 编译通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Asset Service 注入 gatewayClient + 实现 updateDeviceFromSyncInfo + Refresh 接入 sync-info</name>
|
||||
<files>
|
||||
internal/service/asset/service.go,
|
||||
internal/bootstrap/services.go
|
||||
</files>
|
||||
<action>
|
||||
## 步骤 1:internal/service/asset/service.go — 结构体 + 构造函数 + Refresh 分支 + 新函数
|
||||
|
||||
**① import 块追加**(在现有 import 中添加):
|
||||
```go
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"time"
|
||||
```
|
||||
注意:`time` 可能已有,检查后选择性添加。
|
||||
|
||||
**② Service 结构体追加字段**(per D-13,D-15 决策):
|
||||
在 `iotCardService IotCardRefresher` 行之后追加:
|
||||
```go
|
||||
gatewayClient *gateway.Client // 用于调用 sync-info 同步设备信息(可为 nil,失败时仅记录日志)
|
||||
```
|
||||
|
||||
**③ New() 函数签名追加参数**:
|
||||
在 `iotCardService IotCardRefresher,` 参数之后追加:
|
||||
```go
|
||||
gatewayClient *gateway.Client,
|
||||
```
|
||||
并在 return 的结构体字面量中追加:
|
||||
```go
|
||||
gatewayClient: gatewayClient,
|
||||
```
|
||||
|
||||
**④ Refresh device 分支追加 sync-info 调用**(per D-3,D-15 决策):
|
||||
在 `s.redis.Set(ctx, cooldownKey, 1, constants.DeviceRefreshCooldownDuration)` 之后、`return s.GetRealtimeStatus(ctx, "device", id)` 之前插入:
|
||||
|
||||
```go
|
||||
// 刷新设备自身信息(在线状态、当前卡、固件版本等),失败不阻断刷新流程(per D-15)
|
||||
if s.gatewayClient != nil {
|
||||
device, devErr := s.deviceStore.GetByID(ctx, id)
|
||||
if devErr == nil {
|
||||
// IMEI 优先,无则用 SN(per D-3 注释说明)
|
||||
cardNo := device.IMEI
|
||||
if cardNo == "" {
|
||||
cardNo = device.SN
|
||||
}
|
||||
if cardNo != "" {
|
||||
if syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
|
||||
CardNo: cardNo,
|
||||
}); syncErr == nil {
|
||||
s.updateDeviceFromSyncInfo(ctx, id, syncResp)
|
||||
} else {
|
||||
logger.GetAppLogger().Warn("sync-info 调用失败",
|
||||
zap.Uint("device_id", id),
|
||||
zap.Error(syncErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⑤ 新增 updateDeviceFromSyncInfo 私有函数**(per D-13,D-14 决策):
|
||||
在 `Refresh` 函数之后追加:
|
||||
|
||||
```go
|
||||
// updateDeviceFromSyncInfo 根据 Gateway sync-info 响应更新设备状态字段和当前卡标识
|
||||
// 失败时只记录日志,不返回错误(非关键步骤)
|
||||
func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, resp *gateway.SyncDeviceInfoResp) {
|
||||
now := time.Now()
|
||||
|
||||
// 解析 LastOnlineTime(ISO 8601 字符串 → *time.Time)
|
||||
var lastOnlineTime *time.Time
|
||||
if resp.LastOnlineTime != "" {
|
||||
if t, err := time.Parse(time.RFC3339, resp.LastOnlineTime); err == nil {
|
||||
lastOnlineTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备 5 个存储字段
|
||||
updates := map[string]any{
|
||||
"online_status": resp.OnlineStatus,
|
||||
"software_version": resp.SoftwareVersion,
|
||||
"switch_mode": resp.SwitchMode,
|
||||
"last_gateway_sync_at": now,
|
||||
}
|
||||
if lastOnlineTime != nil {
|
||||
updates["last_online_time"] = lastOnlineTime
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.Device{}).
|
||||
Where("id = ?", deviceID).
|
||||
Updates(updates).Error; err != nil {
|
||||
logger.GetAppLogger().Warn("sync-info 更新设备字段失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 is_current 标识(事务原子操作,per D-14 决策)
|
||||
if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, resp.CurrentIccid); err != nil {
|
||||
logger.GetAppLogger().Warn("sync-info 更新 is_current 失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 步骤 2:internal/bootstrap/services.go — Asset.New 追加 deps.GatewayClient
|
||||
|
||||
找到以下行:
|
||||
```go
|
||||
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard),
|
||||
```
|
||||
|
||||
替换为:
|
||||
```go
|
||||
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient),
|
||||
```
|
||||
|
||||
## 最终验证
|
||||
|
||||
执行 `go build ./...` 确认编译无错误。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go build ./...</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `asset.Service` 含 `gatewayClient *gateway.Client` 字段
|
||||
- `asset.New()` 新增 `gatewayClient` 参数,bootstrap 调用处已追加 `deps.GatewayClient`
|
||||
- `updateDeviceFromSyncInfo` 函数存在:更新 5 个 device 字段 + 调用 UpdateIsCurrentByDeviceID
|
||||
- `Refresh` device 分支在绑定卡刷新后追加 sync-info 调用(nil guard + Warn 日志)
|
||||
- `go build ./...` 零新增编译错误
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
# 1. 编译验证
|
||||
go build ./...
|
||||
|
||||
# 2. Gateway 方法确认
|
||||
grep -n "SyncDeviceInfo\|updateDeviceFromSyncInfo" internal/service/asset/service.go
|
||||
|
||||
# 3. Store 方法确认
|
||||
grep -n "UpdateIsCurrentByDeviceID" internal/store/postgres/device_sim_binding_store.go
|
||||
|
||||
# 4. Bootstrap 注入确认
|
||||
grep -n "GatewayClient" internal/bootstrap/services.go | grep -i asset
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `go build ./...` 零新增编译错误
|
||||
- [ ] `internal/service/asset/service.go` 含 `updateDeviceFromSyncInfo` 函数
|
||||
- [ ] `internal/service/asset/service.go` Refresh device 分支含 `gatewayClient.SyncDeviceInfo()` 调用
|
||||
- [ ] `internal/store/postgres/device_sim_binding_store.go` 含 `UpdateIsCurrentByDeviceID` 方法
|
||||
- [ ] `internal/bootstrap/services.go` asset.New() 调用传入了 `deps.GatewayClient`
|
||||
- [ ] sync-info 失败时只记录 Warn,不影响 Refresh 主流程
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/03-device-system/03-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user