package console import ( "fmt" "yunyan/comm" "yunyan/lego/sys/postgres" ) // ============================ 主键改号(BID / PID) ============================ // // 方案商的 BID 与产品的 PID 都是「有含义的十六进制编码」,且都是所在表的主键。 // 建档时填错过去只能删了重建(删产品会连带丢掉版本记录),所以放开了改号。 // // ⚠️ 改主键是不可逆操作,且这两个值会烧进固件 / 决定 license 分表名,所以一律 // 「有任何引用就不许改」,而不是级联更新: // - 方案商 BID:被 product.providerid 引用; // - 产品 PID:被 production_batch.productid、license_ 分表、 // 以及各应用业务库的 userdevice.productid / product_stat.productid 引用。 // 后两者在别的库里,console 够不着也保证不了一致性——所以只要本库已经有批次或授权码, // 就说明货已经生产出去了,此时改号必然造成线上设备对不上,直接拒绝。 // // 允许改的窗口就是「刚建档、还没投产」。这也是实际会填错的时候。 // checkChipVendorIdChange 校验芯片厂商能否从 oldId 改成 newId(即改 CID)。返回空串表示可以改。 // // 与 BID/PID 不同:芯片厂商的下游只有方案商的 vendor 列,且完全在本库内,所以这里【级联更新】 // 而不是拒绝——CID 是最可能填错、也最需要事后订正的一个(新芯片方案定型前常常只有暂定值)。 func checkChipVendorIdChange(sys *appConn, oldId, newId uint32) string { if newId == 0 { return "CID 必填" } if newId == oldId { return "" } if exist, _ := dvChipVendor(sys, newId); exist != nil && exist.Id != 0 { return fmt.Sprintf("CID 0x%X 已被芯片厂商「%s」占用", newId, exist.Name) } return "" } // renameChipVendorId 改芯片厂商的 CID,并级联把方案商的 vendor 引用一起搬过去。 // 两步必须都做:只改厂商表会让名下方案商指向不存在的厂商,列表里显示成「厂商#N」。 func renameChipVendorId(oldId, newId uint32) error { if oldId == newId { return nil } if res := postgres.Exec("UPDATE "+comm.TableChipVendor+" SET id=? WHERE id=?", newId, oldId); res.Error != nil { return res.Error } if res := postgres.Exec("UPDATE "+comm.TableSolutionProvider+" SET vendor=? WHERE vendor=?", newId, oldId); res.Error != nil { return res.Error } return nil } // checkProviderIdChange 校验方案商能否从 oldId 改成 newId。返回空串表示可以改。 func checkProviderIdChange(sys *appConn, oldId, newId uint32) string { if newId == 0 { return "BID 必填" } if newId == oldId { return "" } // 新号不能已被占用 if exist, _ := dvSolutionProvider(sys, newId); exist != nil && exist.Id != 0 { return fmt.Sprintf("BID 0x%X 已被方案商「%s」占用", newId, exist.Name) } // 已有产品挂在旧号下就不许改:product.providerid 是裸整数、没有外键, // 改了那些产品会指向一个不存在的方案商,而 CID 正是从这里推导的。 n, err := dvCountProductsByProvider(sys, oldId) if err != nil { return "校验产品引用失败: " + err.Error() } if n > 0 { return fmt.Sprintf("该方案商下已有 %d 个产品,不能改 BID。请先把这些产品改挂到别的方案商下", n) } return "" } // checkProductIdChange 校验产品能否从 oldId 改成 newId。返回空串表示可以改。 func checkProductIdChange(sys *appConn, oldId, newId uint32) string { if newId == 0 { return "PID 必填" } if newId == oldId { return "" } if exist, _ := dvProduct(sys, newId); exist != nil && exist.Id != 0 { return fmt.Sprintf("PID 0x%X 已被产品「%s」占用", newId, exist.Devicename) } // 已投产就不许改:批次台账与授权码分表都按 PID 组织,且设备码已经发出去了。 var batches int64 if err := sys.AdminDB().Table(comm.TableProductionBatch).Where("productid=?", oldId).Count(&batches).Error; err != nil { return "校验生产批次失败: " + err.Error() } if batches > 0 { return fmt.Sprintf("该产品已有 %d 个生产批次,不能改 PID(授权码分表 license_%x 与已发出的设备都按它组织)", batches, oldId) } if n, err := countLicenseRows(oldId); err != nil { return "校验授权码失败: " + err.Error() } else if n > 0 { return fmt.Sprintf("该产品已生产 %d 个授权码,不能改 PID", n) } return "" } // countLicenseRows 统计某产品名下已生产的设备行数(改 PID 前的前置检查)。 // 2026-09-04 合表后设备都在 device_mac,按 productid 过滤即可。 func countLicenseRows(pid uint32) (int64, error) { var n int64 if err := postgres.Table(comm.TableDeviceMac).Where("productid=?", pid).Count(&n).Error; err != nil { return 0, err } return n, nil } // renameProviderId 执行方案商改号(先校验后调用)。用裸 UPDATE 改主键: // gorm 的 Save 是「按主键定位再更新」,改不动主键本身。 func renameProviderId(oldId, newId uint32) error { if oldId == newId { return nil } res := postgres.Exec("UPDATE "+comm.TableSolutionProvider+" SET id=? WHERE id=?", newId, oldId) return res.Error } // renameProductId 执行产品改号(先校验后调用)。产品版本表按 productid 归属, // 跟着一起搬——版本是产品的附属数据,不搬会变成谁也看不见的孤儿行。 // 授权码/批次在校验里已确认为空,无需处理。 func renameProductId(oldId, newId uint32) error { if oldId == newId { return nil } if res := postgres.Exec("UPDATE "+comm.TableProduct+" SET id=? WHERE id=?", newId, oldId); res.Error != nil { return res.Error } if res := postgres.Exec("UPDATE "+comm.TableProductVersion+" SET productid=? WHERE productid=?", newId, oldId); res.Error != nil { return res.Error } return nil }