404 lines
16 KiB
Markdown
404 lines
16 KiB
Markdown
---
|
||
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>
|