This commit is contained in:
@@ -74,6 +74,82 @@ DB_PASSWORD='<密码>' DB_NAME=<库名> DB_SSLMODE=<模式> \
|
||||
|
||||
迁移失败时不启动新二进制;按失败迁移的事务状态决定处理,必要时恢复已确认可用的数据库备份。启动失败时覆盖回部署前备份的二进制,再恢复数据库备份(如迁移已改变数据库)。
|
||||
|
||||
|
||||
### 单槽单卡当前卡标识核对与受控修复
|
||||
|
||||
本节仅供生产维护者在已备份、已留存核对清单且具备回滚条件的维护窗口执行。Agent 只可通过 dbhub 做只读核对,不得执行下述写操作。
|
||||
|
||||
先执行只读核对,候选必须同时满足:设备未删除、最大槽位为 1、只有一条未删除的有效绑定、且没有任何有效绑定被标记为当前使用卡。
|
||||
|
||||
```sql
|
||||
SELECT b.id AS binding_id, b.device_id, b.iot_card_id
|
||||
FROM tb_device_sim_binding AS b
|
||||
JOIN tb_device AS d ON d.id = b.device_id AND d.deleted_at IS NULL
|
||||
WHERE b.deleted_at IS NULL
|
||||
AND b.bind_status = 1
|
||||
AND d.max_sim_slots = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM tb_device_sim_binding AS current_binding
|
||||
WHERE current_binding.device_id = b.device_id
|
||||
AND current_binding.deleted_at IS NULL
|
||||
AND current_binding.bind_status = 1
|
||||
AND current_binding.is_current
|
||||
)
|
||||
AND (
|
||||
SELECT count(*)
|
||||
FROM tb_device_sim_binding AS active_binding
|
||||
WHERE active_binding.device_id = b.device_id
|
||||
AND active_binding.deleted_at IS NULL
|
||||
AND active_binding.bind_status = 1
|
||||
) = 1
|
||||
ORDER BY b.device_id, b.id;
|
||||
```
|
||||
|
||||
维护者审核并留存候选清单后,在事务中把批准的 `binding_id` 显式填入 `approved`。更新语句会再次核对全部条件;受影响行数与批准清单不一致时必须回滚,不得扩大条件或直接按设备全量更新。
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
WITH approved(binding_id) AS (
|
||||
VALUES
|
||||
(CAST(<binding_id> AS bigint))
|
||||
), eligible AS (
|
||||
SELECT b.id
|
||||
FROM tb_device_sim_binding AS b
|
||||
JOIN approved AS a ON a.binding_id = b.id
|
||||
JOIN tb_device AS d ON d.id = b.device_id AND d.deleted_at IS NULL
|
||||
WHERE b.deleted_at IS NULL
|
||||
AND b.bind_status = 1
|
||||
AND d.max_sim_slots = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM tb_device_sim_binding AS current_binding
|
||||
WHERE current_binding.device_id = b.device_id
|
||||
AND current_binding.deleted_at IS NULL
|
||||
AND current_binding.bind_status = 1
|
||||
AND current_binding.is_current
|
||||
)
|
||||
AND (
|
||||
SELECT count(*)
|
||||
FROM tb_device_sim_binding AS active_binding
|
||||
WHERE active_binding.device_id = b.device_id
|
||||
AND active_binding.deleted_at IS NULL
|
||||
AND active_binding.bind_status = 1
|
||||
) = 1
|
||||
FOR UPDATE OF b
|
||||
)
|
||||
UPDATE tb_device_sim_binding AS target
|
||||
SET is_current = TRUE, updated_at = NOW()
|
||||
FROM eligible
|
||||
WHERE target.id = eligible.id
|
||||
RETURNING target.id AS binding_id, target.device_id, target.iot_card_id;
|
||||
|
||||
-- 核对返回行数、设备卡列表与资产信息;不一致时执行 ROLLBACK。
|
||||
-- 全部一致后才执行 COMMIT。
|
||||
```
|
||||
|
||||
修复后必须确认每个设备至多一条未删除的有效绑定满足 `is_current = TRUE`,并抽样核对设备卡列表和个人客户资产信息均按最新绑定事实返回。回滚时仅对本次留存清单中的绑定恢复 `is_current = FALSE`,不得影响运营商观测或显式切卡产生的其他当前卡事实。
|
||||
### 商户池支付路由发布与回滚
|
||||
|
||||
本节是维护者操作清单,不是已执行证据。迁移、生产发布、Redis 操作和富友真实渠道核验均由维护者执行;本轮未执行,不能以本地构建替代。
|
||||
|
||||
@@ -679,6 +679,7 @@
|
||||
"capability": "asset-device",
|
||||
"requirements": [
|
||||
"asset-device::设备多卡关系",
|
||||
"asset-device::单槽单卡设备绑定时初始化当前卡标识",
|
||||
"identity-access::数据范围拒绝"
|
||||
],
|
||||
"classification": "behavior"
|
||||
@@ -1405,7 +1406,8 @@
|
||||
"capability": "personal-customer",
|
||||
"requirements": [
|
||||
"personal-customer::资产归属查询",
|
||||
"personal-customer::资产信息展示当前可用或已用完主套餐"
|
||||
"personal-customer::资产信息展示当前可用或已用完主套餐",
|
||||
"personal-customer::缺少当前使用卡时的设备实名判定"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
@@ -2023,6 +2025,7 @@
|
||||
"capability": "asset-device",
|
||||
"requirements": [
|
||||
"asset-device::设备多卡关系",
|
||||
"asset-device::单槽单卡设备绑定时初始化当前卡标识",
|
||||
"identity-access::数据范围拒绝"
|
||||
],
|
||||
"classification": "behavior"
|
||||
@@ -2053,6 +2056,7 @@
|
||||
"capability": "asset-device",
|
||||
"requirements": [
|
||||
"asset-device::设备多卡关系",
|
||||
"asset-device::单槽单卡设备绑定时初始化当前卡标识",
|
||||
"identity-access::数据范围拒绝"
|
||||
],
|
||||
"classification": "behavior"
|
||||
@@ -4644,4 +4648,14 @@
|
||||
],
|
||||
"classification": "behavior"
|
||||
}
|
||||
,
|
||||
{
|
||||
"entry_type": "cli",
|
||||
"entry": "docs/deployment/production-runbook.md",
|
||||
"capability": "asset-device",
|
||||
"requirements": [
|
||||
"asset-device::存量当前卡标识的核对与受控修复"
|
||||
],
|
||||
"classification": "behavior"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5653,4 +5653,93 @@
|
||||
"exit_status": 0
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"capability": "asset-device",
|
||||
"requirement": "单槽单卡设备绑定时初始化当前卡标识",
|
||||
"spec": "openspec/specs/asset-device/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/devices/{virtual_no}/cards",
|
||||
"/api/admin/devices/import"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/handler/admin/device.go",
|
||||
"internal/task/device_import.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/service/device/binding.go",
|
||||
"internal/task/device_import.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/model/device_sim_binding.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/store/postgres/device_sim_binding_store.go"
|
||||
],
|
||||
"verification": {
|
||||
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/service/device/binding.go\", \"if device.MaxSimSlots == 1\"), (\"internal/service/device/binding.go\", \"if activeCount == 1\"), (\"internal/service/device/binding.go\", \"Update(\\\"is_current\\\", true)\"), (\"internal/task/device_import.go\", \"initializeCurrentCard := device.MaxSimSlots == 1 && len(validCards) == 1\"), (\"internal/task/device_import.go\", \"IsCurrent: initializeCurrentCard\")]; missing=[f\"{p} :: {t}\" for p,t in checks if t not in Path(p).read_text()]; assert not missing, missing; print(\"OK: asset-device::单槽单卡设备绑定时初始化当前卡标识\")'",
|
||||
"literal_output": [
|
||||
"OK: asset-device::单槽单卡设备绑定时初始化当前卡标识"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "asset-device",
|
||||
"requirement": "存量当前卡标识的核对与受控修复",
|
||||
"spec": "openspec/specs/asset-device/spec.md",
|
||||
"entries": [
|
||||
"docs/deployment/production-runbook.md"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"docs/deployment/production-runbook.md:单槽单卡当前卡标识核对与受控修复"
|
||||
],
|
||||
"application_service_query": [
|
||||
"docs/deployment/production-runbook.md:只读候选核对与事务内条件复核"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/model/device_sim_binding.go:DeviceSimBinding.IsCurrent"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"tb_device_sim_binding",
|
||||
"tb_device"
|
||||
],
|
||||
"verification": {
|
||||
"command": "python3 -c 'from pathlib import Path; text=Path(\"docs/deployment/production-runbook.md\").read_text(); checks=[\"### 单槽单卡当前卡标识核对与受控修复\", \"d.max_sim_slots = 1\", \"current_binding.is_current\", \"count(*)\", \"WITH approved(binding_id) AS\", \"FOR UPDATE OF b\", \"RETURNING target.id AS binding_id\", \"受影响行数与批准清单不一致时必须回滚\"]; missing=[t for t in checks if t not in text]; assert not missing, missing; print(\"OK: asset-device::存量当前卡标识的核对与受控修复\")'",
|
||||
"literal_output": [
|
||||
"OK: asset-device::存量当前卡标识的核对与受控修复"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "personal-customer",
|
||||
"requirement": "缺少当前使用卡时的设备实名判定",
|
||||
"spec": "openspec/specs/personal-customer/spec.md",
|
||||
"entries": [
|
||||
"/api/c/v1/asset/info"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/routes/personal.go",
|
||||
"internal/handler/app/client_asset.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/service/asset/service.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/model/device_sim_binding.go:DeviceSimBinding.IsCurrent",
|
||||
"pkg/constants:RealNameStatusVerified"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/store/postgres/device_sim_binding_store.go",
|
||||
"internal/store/postgres/iot_card_store.go"
|
||||
],
|
||||
"verification": {
|
||||
"command": "python3 -c 'from pathlib import Path; text=Path(\"internal/service/asset/service.go\").read_text(); checks=[\"hasCurrentCard := false\", \"if isCurrentMap[c.ID]\", \"if !hasCurrentCard\", \"if c.RealNameStatus == constants.RealNameStatusVerified\", \"resp.RealNameStatus = constants.RealNameStatusVerified\"]; missing=[t for t in checks if t not in text]; assert not missing, missing; print(\"OK: personal-customer::缺少当前使用卡时的设备实名判定\")'",
|
||||
"literal_output": [
|
||||
"OK: personal-customer::缺少当前使用卡时的设备实名判定"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user