You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

414 lines
15 KiB

package console
import (
"strings"
"yunyan/comm"
"yunyan/pb"
)
// 设备管理域数据访问。sys.AdminDB() 恒为 console 主库(supabase)——方案商/品牌商/产品/授权码/公码/批次都在这;
// 只有 dvDelUserDevice / dvDecrProductStatActivated 走 sys.ServiceDB()(选中应用的业务库,重置授权码时清用户绑定)。
// 生产模型:授权码核心 = productid + number(产品级全局序号);生产批次独立成表 production_batch。
// ---- 方案商(solution_provider)----
func dvSolutionProviders(sys *appConn) (models []*pb.DBSolutionProvider, err error) {
models = make([]*pb.DBSolutionProvider, 0)
err = sys.AdminDB().Find(comm.TableSolutionProvider, &models, "")
return
}
func dvSolutionProvider(sys *appConn, id uint32) (model *pb.DBSolutionProvider, err error) {
model = &pb.DBSolutionProvider{}
err = sys.AdminDB().FindOne(comm.TableSolutionProvider, model, "id=?", id)
return
}
func dvAddSolutionProvider(sys *appConn, model *pb.DBSolutionProvider) (err error) {
err = sys.AdminDB().Insert(comm.TableSolutionProvider, model)
return
}
func dvSaveSolutionProvider(sys *appConn, model *pb.DBSolutionProvider) (err error) {
err = sys.AdminDB().Save(comm.TableSolutionProvider, model)
return
}
func dvDelSolutionProvider(sys *appConn, id uint32) (err error) {
err = sys.AdminDB().Delete(comm.TableSolutionProvider, "id=?", id)
return
}
// dvCountProductsByProvider 统计挂在某方案商下的产品数,改 BID / 删方案商前用它挡住产生孤儿产品。
func dvCountProductsByProvider(sys *appConn, providerId uint32) (n int64, err error) {
err = sys.AdminDB().Table(comm.TableProduct).Where("providerid=?", providerId).Count(&n).Error
return
}
// ---- 品牌商(brand,原厂家)/ 产品(下拉只读)----
func dvBrands(sys *appConn) (models []*pb.DBBrand, err error) {
models = make([]*pb.DBBrand, 0)
err = sys.AdminDB().Find(comm.TableBrand, &models, "")
return
}
func dvBrand(sys *appConn, id uint32) (model *pb.DBBrand, err error) {
model = &pb.DBBrand{}
// 用 FindOne:品牌商不存在时返回 ErrRecordNotFound,存在性校验才有效(Find 对空结果不报错)。
err = sys.AdminDB().FindOne(comm.TableBrand, model, "id=?", id)
return
}
func dvAddBrand(sys *appConn, model *pb.DBBrand) (err error) {
err = sys.AdminDB().Insert(comm.TableBrand, model)
return
}
func dvDelBrand(sys *appConn, id uint32) (err error) {
err = sys.AdminDB().Delete(comm.TableBrand, "id=?", id)
return
}
func dvSaveBrand(sys *appConn, model *pb.DBBrand) (err error) {
err = sys.AdminDB().Save(comm.TableBrand, model)
return
}
// ---- 渠道商(channel)----
//
// 渠道商挂在品牌商下,id 为 8 位可读短码(主键)。新增走审批:status=0 待审核时不可用于生产。
// dvChannels 按条件列出渠道商。brandid=0 不限品牌商;status<0 不限状态;keyword 匹配名称或 id。
func dvChannels(sys *appConn, brandid uint32, status int32, keyword string) (models []*pb.DBChannel, err error) {
models = make([]*pb.DBChannel, 0)
where := make([]string, 0, 3)
args := make([]interface{}, 0, 3)
if brandid != 0 {
// brandid=0 的是「全局渠道商」(后台新增渠道商已不再选品牌商),任何品牌商都看得到、用得上。
// 只写 brandid=? 会让品牌商账号除了自家官方渠道之外什么也看不见。
where = append(where, "(brandid=? OR brandid=0)")
args = append(args, brandid)
}
if status >= 0 {
where = append(where, "status=?")
args = append(args, status)
}
if kw := strings.TrimSpace(keyword); kw != "" {
where = append(where, "(name LIKE ? OR id LIKE ?)")
args = append(args, "%"+kw+"%", "%"+strings.ToUpper(kw)+"%")
}
err = sys.AdminDB().Find(comm.TableChannel, &models, strings.Join(where, " AND "), args...)
return
}
// dvChannel 按 id 取渠道商。用 FindOne:不存在时返回 ErrRecordNotFound,存在性校验才有效。
func dvChannel(sys *appConn, id string) (model *pb.DBChannel, err error) {
model = &pb.DBChannel{}
err = sys.AdminDB().FindOne(comm.TableChannel, model, "id=?", id)
return
}
func dvAddChannel(sys *appConn, model *pb.DBChannel) (err error) {
err = sys.AdminDB().Insert(comm.TableChannel, model)
return
}
func dvSaveChannel(sys *appConn, model *pb.DBChannel) (err error) {
err = sys.AdminDB().Save(comm.TableChannel, model)
return
}
func dvDelChannel(sys *appConn, id string) (err error) {
err = sys.AdminDB().Delete(comm.TableChannel, "id=?", id)
return
}
// dvCountBatchesByChannel 统计某渠道商已产生的生产批次数(删除渠道商前的引用完整性检查)。
func dvCountBatchesByChannel(sys *appConn, id string) (n int64, err error) {
err = sys.AdminDB().Table(comm.TableProductionBatch).Where("channelid=?", id).Count(&n).Error
return
}
// genChannelId 生成未被占用的渠道商短码(8 位)。重试 5 次兜底极小概率碰撞,
// 最终仍以主键唯一约束为准。
func genChannelId(sys *appConn) string {
for i := 0; i < 5; i++ {
code, err := comm.GenerateChannelId()
if err != nil {
continue
}
if _, e := dvChannel(sys, code); e != nil { // 查不到 = 未占用
return code
}
}
code, _ := comm.GenerateChannelId()
return code
}
// dvAddProduct 新增产品并为其建好 license 分表(license_%x,%x 为回填后的产品 id)。
func dvAddProduct(sys *appConn, product *pb.DBProduct) (err error) {
// 设备统一住全局 device_mac 表(2026-09-04 合表),新增产品**不再建 license 分表**。
return sys.AdminDB().Insert(comm.TableProduct, product) // Insert 后回填 product.Id
}
func dvProducts(sys *appConn) (models []*pb.DBProduct, err error) {
models = make([]*pb.DBProduct, 0)
err = sys.AdminDB().Find(comm.TableProduct, &models, "")
return
}
func dvProduct(sys *appConn, id uint32) (model *pb.DBProduct, err error) {
model = &pb.DBProduct{}
err = sys.AdminDB().FindOne(comm.TableProduct, model, "id=?", id)
return
}
func dvProductVersions(sys *appConn, pid uint32) (models []*pb.DBProductVersion, err error) {
models = make([]*pb.DBProductVersion, 0)
err = sys.AdminDB().Find(comm.TableProductVersion, &models, "productid=?", pid)
return
}
// dvAllProductVersions 取全部产品版本(缓存全量预热用,不按 productid 过滤)。
func dvAllProductVersions(sys *appConn) (models []*pb.DBProductVersion, err error) {
models = make([]*pb.DBProductVersion, 0)
err = sys.AdminDB().Find(comm.TableProductVersion, &models, "")
return
}
// ---- 产品 / 版本(写)----
func dvUpdateProduct(sys *appConn, model *pb.DBProduct) (err error) {
err = sys.AdminDB().Save(comm.TableProduct, model)
return
}
func dvDelProduct(sys *appConn, id uint32) (err error) {
err = sys.AdminDB().Delete(comm.TableProduct, "id=?", id)
return
}
func dvAddProductVersion(sys *appConn, version *pb.DBProductVersion) (err error) {
err = sys.AdminDB().Insert(comm.TableProductVersion, version)
return
}
func dvProductVersion(sys *appConn, id uint32) (model *pb.DBProductVersion, err error) {
model = &pb.DBProductVersion{}
err = sys.AdminDB().FindOne(comm.TableProductVersion, model, "id=?", id)
return
}
func dvDelProductVersion(sys *appConn, id uint32) (err error) {
err = sys.AdminDB().Delete(comm.TableProductVersion, "id=?", id)
return
}
// ---- 产品级序号游标(原子分配)----
// dvAllocSerial 原子地为产品分配 n 个连续序号,返回区间末尾 end(区间 = [end-n+1, end])。
// 用 UPDATE ... RETURNING 保证并发下不撞号;顺带修掉原「读-改-写 probatch」的竞态。
func dvAllocSerial(sys *appConn, pid uint32, n int64) (end int64, err error) {
err = sys.AdminDB().Raw(
"UPDATE "+comm.TableProduct+" SET serialcursor = serialcursor + ? WHERE id = ? RETURNING serialcursor",
n, pid,
).Scan(&end).Error
return
}
// ---- 授权码(写):批量创建 / 封禁 / 批次撤销(按 product 分表 license_%x)----
// dvAddAuthCodes 批量写入授权码。列多、单条 INSERT 占位符上限 65535,按 1000 行分批。
func dvAddAuthCodes(sys *appConn, pid uint32, models []*pb.DBAuthCode) (err error) {
if len(models) == 0 {
return nil
}
err = sys.AdminDB().Table(comm.TableDeviceMac).CreateInBatches(models, 1000).Error
return
}
// dvSetAuthCodesDisabled 批量设封禁状态(disabled: 0 正常, -1 禁用),返回更新条数。
func dvSetAuthCodesDisabled(sys *appConn, pid uint32, codes []string, disabled int32) (affected int64, err error) {
// ⚠️ 合表后没有分表这层天然隔离,批量更新必须显式带 productid。
q, a := comm.ScopeToProduct(pid, "code IN ?", []interface{}{codes})
tx := sys.AdminDB().Table(comm.TableDeviceMac).Where(q, a...).Update("disabled", disabled)
affected = tx.RowsAffected
err = tx.Error
return
}
// dvCountUsedAuthCodesByBatch 统计某生产批次中已被使用(status>0 或 uid 非空)的授权码数。
func dvCountUsedAuthCodesByBatch(sys *appConn, pid uint32, batchno string) (used int64, err error) {
q, a := comm.ScopeToProduct(pid, "batchno=? AND (status>0 OR uid<>'')", []interface{}{batchno})
err = sys.AdminDB().Table(comm.TableDeviceMac).Where(q, a...).Count(&used).Error
return
}
// dvDelAuthCodesByBatch 删除某生产批次的全部授权码,返回删除条数。
func dvDelAuthCodesByBatch(sys *appConn, pid uint32, batchno string) (affected int64, err error) {
// ⚠️ 删除必须带 productid,理由同 dvSetAuthCodesDisabled。
q, a := comm.ScopeToProduct(pid, "batchno=?", []interface{}{batchno})
tx := sys.AdminDB().Table(comm.TableDeviceMac).Where(q, a...).Delete(&pb.DBAuthCode{})
affected = tx.RowsAffected
err = tx.Error
return
}
// dvAuthCodesForUid 取某用户在某产品下持有的全部设备(pid 传 0 = 不限产品)。
func dvAuthCodesForUid(sys *appConn, pid uint32, uid string) (models []*pb.DBAuthCode, err error) {
models = make([]*pb.DBAuthCode, 0)
q, a := comm.ScopeToProduct(pid, "uid=?", []interface{}{uid})
err = sys.AdminDB().Find(comm.TableDeviceMac, &models, q, a...)
return
}
// dvUpdateAuthCodes 批量保存设备行。按主键(code)保存,主键全局唯一,不需要产品作用域。
func dvUpdateAuthCodes(sys *appConn, _ uint32, models []*pb.DBAuthCode) (err error) {
if len(models) == 0 {
return nil
}
err = sys.AdminDB().Save(comm.TableDeviceMac, &models)
return
}
// dvAuthCodesPaged 分页查询授权码(ORDER BY + LIMIT/OFFSET),返回命中总数。
func dvAuthCodesPaged(sys *appConn, pid uint32, query, orderClause string, limit, offset int, args ...interface{}) (models []*pb.DBAuthCode, total int64, err error) {
models = make([]*pb.DBAuthCode, 0)
query, args = comm.ScopeToProduct(pid, query, args)
tx := sys.AdminDB().Table(comm.TableDeviceMac)
if query != "" {
tx = tx.Where(query, args...)
}
if err = tx.Count(&total).Error; err != nil {
return
}
tx2 := sys.AdminDB().Table(comm.TableDeviceMac)
if query != "" {
tx2 = tx2.Where(query, args...)
}
if orderClause != "" {
tx2 = tx2.Order(orderClause)
}
if limit > 0 {
tx2 = tx2.Limit(limit)
}
if offset > 0 {
tx2 = tx2.Offset(offset)
}
err = tx2.Find(&models).Error
return
}
func dvAuthCode(sys *appConn, pid uint32, code string) (model *pb.DBAuthCode, err error) {
model = &pb.DBAuthCode{}
// code 是全局主键,pid 只作额外校验(传 0 即不校验)。
q, a := comm.ScopeToProduct(pid, "code=?", []interface{}{code})
err = sys.AdminDB().FindOne(comm.TableDeviceMac, &model, q, a...)
return
}
func dvUpdateAuthCode(sys *appConn, _ uint32, model *pb.DBAuthCode) (err error) {
err = sys.AdminDB().Save(comm.TableDeviceMac, &model)
return
}
// ---- 用户设备 / 产品统计(业务库 ServiceDB)----
// 注意:userdevice、product_stat 在应用的业务库(ServiceDB),不在设备库(AdminDB)。
// 重置授权码时要连带删掉用户绑定、扣减激活统计,否则留下孤儿绑定、仪表盘数据偏高。
func dvDelUserDevice(sys *appConn, uid string) (err error) {
err = sys.ServiceDB().Delete(comm.TableUserdevice, "uid=?", uid)
return
}
// dvDecrProductStatActivated 把 product_stat.activated_count 原地减 1(下限 0)。
func dvDecrProductStatActivated(sys *appConn, productid uint32) (err error) {
err = sys.ServiceDB().Exec(
"UPDATE "+comm.TableProductStat+
" SET activated_count = GREATEST(0, activated_count - 1)"+
" WHERE productid = ?",
productid,
).Error
return
}
// ---- 生产批次台账(production_batch)----
func dvProductionBatch(sys *appConn, batchno string) (model *pb.DBProductionBatch, err error) {
model = &pb.DBProductionBatch{}
err = sys.AdminDB().FindOne(comm.TableProductionBatch, model, "batchno=?", batchno)
return
}
// dvAddProductionBatch 写入一条生产批次;Insert 后回填自增 Id。
func dvAddProductionBatch(sys *appConn, model *pb.DBProductionBatch) (err error) {
err = sys.AdminDB().Insert(comm.TableProductionBatch, model)
return
}
func dvDelProductionBatch(sys *appConn, batchno string) (err error) {
err = sys.AdminDB().Delete(comm.TableProductionBatch, "batchno=?", batchno)
return
}
func dvProductionBatches(sys *appConn, query interface{}, args ...interface{}) (models []*pb.DBProductionBatch, err error) {
models = make([]*pb.DBProductionBatch, 0)
err = sys.AdminDB().Find(comm.TableProductionBatch, &models, query, args...)
return
}
// ---- 品牌商公码 ----
// dvFactoryPublicCodes 按品牌商 + 状态过滤(brandid=0 不过滤品牌商,statusFilter<0 不过滤状态)。
func dvFactoryPublicCodes(sys *appConn, brandid uint32, statusFilter int32) (models []*pb.DBFactoryPublicCode, err error) {
models = make([]*pb.DBFactoryPublicCode, 0)
tx := sys.AdminDB().Table(comm.TableFactoryPublicCode)
if brandid != 0 {
tx = tx.Where("brandid=?", brandid)
}
if statusFilter >= 0 {
tx = tx.Where("status=?", statusFilter)
}
err = tx.Order("createtime DESC").Find(&models).Error
return
}
func dvFactoryPublicCode(sys *appConn, code string) (model *pb.DBFactoryPublicCode, err error) {
model = &pb.DBFactoryPublicCode{}
err = sys.AdminDB().FindOne(comm.TableFactoryPublicCode, model, "code=?", code)
return
}
func dvAddFactoryPublicCode(sys *appConn, model *pb.DBFactoryPublicCode) (err error) {
err = sys.AdminDB().Insert(comm.TableFactoryPublicCode, model)
return
}
func dvSaveFactoryPublicCode(sys *appConn, model *pb.DBFactoryPublicCode) (err error) {
err = sys.AdminDB().Save(comm.TableFactoryPublicCode, model)
return
}
func dvDelFactoryPublicCode(sys *appConn, code string) (err error) {
err = sys.AdminDB().Delete(comm.TableFactoryPublicCode, "code=?", code)
return
}
// dvAuthCodesByCodes 查这批 code(=MAC) 已经存在哪些,返回已存在的 code。
// 导入 MAC 前的查重用:MAC 就是主键,撞了会让整批 INSERT 失败而序号游标已经推进过。
//
// ⚠️ **全局查重,不按产品**(2026-09-04 合表后):一台设备的 MAC 是它的唯一身份,
// 同一个 MAC 出现在两个产品名下会让「按 MAC 反查设备」出现歧义,
// 也就是绑定时到底算哪个产品说不清。所以跨产品重复也要在导入时挡住。
// pid 参数保留只为调用方签名不变。
func dvAuthCodesByCodes(sys *appConn, _ uint32, codes []string) (out []string, err error) {
out = make([]string, 0)
if len(codes) == 0 {
return out, nil
}
err = sys.AdminDB().Table(comm.TableDeviceMac).Where("code IN ?", codes).Pluck("code", &out).Error
return
}