package console import ( "fmt" "strings" "time" "yunyan/comm" "yunyan/pb" "yunyan/utils/license" "yunyan/utils/mac" "github.com/gin-gonic/gin" ) // defaultBrandContactMail 品牌商联系邮箱缺省必含项(与 modules/api 一致)。 const defaultBrandContactMail = "358030594@qq.com" // 设备管理域写操作 handlers。console 仅超管,数据访问作用于传入的"选中应用"连接 sys。 // 生产模型:授权码核心 = productid + number(产品级全局序号,按 product.serialcursor 原子累加分配), // 生产批次独立成 production_batch 台账;授权码不再编入批次数据。 // ---- 产品 / 版本 ---- func (this *serverComp) updateProduct(c *gin.Context, sys *appConn) { var req pb.ApiUpdateProductReq _ = c.ShouldBindJSON(&req) if req.Product == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "product 必填") return } if _, err := dvProduct(sys, req.Product.Id); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "产品不存在") return } // 指定 versionid 时,从版本表同步版本/描述/升级包地址,前端无需重复传。 if req.Product.Versionid != 0 { ver, verr := dvProductVersion(sys, req.Product.Versionid) if verr != nil || ver == nil || ver.Productid != req.Product.Id { writeErr(c, pb.ErrorCode_ReqParameterError, "versionid 无效") return } req.Product.Version = ver.Version req.Product.Updatedescription = ver.Description req.Product.Updatepackageaddress = ver.Updatepackageaddress } if err := dvUpdateProduct(sys, req.Product); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshProducts() // 写后刷新产品缓存 writeOK(c, &pb.ApiUpdateProductResp{}) } func (this *serverComp) delProduct(c *gin.Context, sys *appConn) { var req pb.ApiDelProductReq _ = c.ShouldBindJSON(&req) if err := dvDelProduct(sys, req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshProducts() // 写后刷新产品缓存 writeOK(c, &pb.ApiDelProductResp{}) } func (this *serverComp) addProductVersion(c *gin.Context, sys *appConn) { var req pb.ApiAddProductVersionReq _ = c.ShouldBindJSON(&req) if req.Version == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "version 必填") return } if err := dvAddProductVersion(sys, req.Version); err != nil { // Insert 后回填 Version.Id writeErr(c, pb.ErrorCode_DBError, err.Error()) return } // Isuse: 把该版本设为产品当前版本并持久化。 if req.Isuse { product, err := dvProduct(sys, req.Version.Productid) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } product.Version = req.Version.Version product.Updatedescription = req.Version.Description product.Updatepackageaddress = req.Version.Updatepackageaddress product.Versionid = req.Version.Id if err = dvUpdateProduct(sys, product); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } this.module.deviceCache.refreshProductVersions() // 写后刷新版本缓存 if req.Isuse { this.module.deviceCache.refreshProducts() // Isuse 改了产品当前版本,连带刷新产品缓存 } writeOK(c, &pb.ApiAddProductVersionResp{Version: req.Version}) } func (this *serverComp) delProductVersion(c *gin.Context, sys *appConn) { var req pb.ApiDelProductVersionReq _ = c.ShouldBindJSON(&req) if err := dvDelProductVersion(sys, req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshProductVersions() // 写后刷新版本缓存 writeOK(c, &pb.ApiDelProductVersionResp{}) } func (this *serverComp) addProduct(c *gin.Context, sys *appConn) { var req pb.ApiAddProductReq _ = c.ShouldBindJSON(&req) if req.Product == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "product 必填") return } brand, err := dvBrand(sys, req.Product.Brandid) if err != nil { writeErr(c, pb.ErrorCode_DBError, "品牌商不存在: "+err.Error()) return } if err = dvAddProduct(sys, req.Product); err != nil { // Insert 后回填 Product.Id + 建 license 分表 writeErr(c, pb.ErrorCode_DBError, err.Error()) return } // 把新产品 id 追加进品牌商的产品列表。 if len(brand.Productlists) == 0 { brand.Productlists = fmt.Sprintf("%d", req.Product.Id) } else { brand.Productlists = fmt.Sprintf("%s,%d", brand.Productlists, req.Product.Id) } if err = dvSaveBrand(sys, brand); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshProducts() // 新增产品 + 改了品牌商 Productlists,刷新两者缓存 this.module.deviceCache.refreshBrands() writeOK(c, &pb.ApiAddProductResp{Product: req.Product}) } // ---- 方案商(写)---- func (this *serverComp) addSolutionProvider(c *gin.Context, sys *appConn) { var req pb.ApiAddSolutionProviderReq _ = c.ShouldBindJSON(&req) if req.Provider == nil || strings.TrimSpace(req.Provider.Name) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "provider name 必填") return } // 方案商 id 是手工指定的有含义编码(十六进制看,如 0xC101=杰里01方案),不自增: // 不填就会插成 id=0,第二条再插直接主键冲突,所以这里必须挡住。 if req.Provider.Id == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "provider id 必填(手工指定,如 0xC101;新增请用 C 开头段)") return } if old, _ := dvSolutionProvider(sys, req.Provider.Id); old != nil && old.Id != 0 { writeErr(c, pb.ErrorCode_ReqParameterError, fmt.Sprintf("方案商 id %X 已存在(%s)", old.Id, old.Name)) return } req.Provider.Createtime = time.Now().Unix() if err := dvAddSolutionProvider(sys, req.Provider); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiAddSolutionProviderResp{Provider: req.Provider}) } func (this *serverComp) updateSolutionProvider(c *gin.Context, sys *appConn) { var req pb.ApiUpdateSolutionProviderReq _ = c.ShouldBindJSON(&req) if req.Provider == nil || req.Provider.Id == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "provider id is required") return } old, err := dvSolutionProvider(sys, req.Provider.Id) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } old.Name = req.Provider.Name old.Description = req.Provider.Description old.Contactmails = req.Provider.Contactmails old.Vendor = req.Provider.Vendor // 所属厂商分组(杰里/蓝汛/恒玄) if err = dvSaveSolutionProvider(sys, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiUpdateSolutionProviderResp{Provider: old}) } func (this *serverComp) delSolutionProvider(c *gin.Context, sys *appConn) { var req pb.ApiDelSolutionProviderReq _ = c.ShouldBindJSON(&req) if err := dvDelSolutionProvider(sys, req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiDelSolutionProviderResp{Id: req.Id}) } // ---- 品牌商(写,原厂家)---- func (this *serverComp) addBrand(c *gin.Context, sys *appConn) { var req pb.ApiAddBrandReq _ = c.ShouldBindJSON(&req) if req.Brand == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "brand 必填") return } req.Brand.Contactmails = ensureDefaultContactMail(req.Brand.Contactmails) if err := dvAddBrand(sys, req.Brand); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshBrands() // 写后刷新品牌商缓存 writeOK(c, &pb.ApiAddBrandResp{Brand: req.Brand}) } func (this *serverComp) updateBrand(c *gin.Context, sys *appConn) { var req pb.ApiUpdateBrandReq _ = c.ShouldBindJSON(&req) if req.Brand == nil || req.Brand.Id == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "brand id is required") return } old, err := dvBrand(sys, req.Brand.Id) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } // 只覆盖可编辑字段;productlists 等服务端维护字段不接受前端写入,防止表单缺字段把产品列表覆盖回旧值。 old.Brand = req.Brand.Brand old.Providerid = req.Brand.Providerid old.Description = req.Brand.Description old.Contactmails = req.Brand.Contactmails if err = dvSaveBrand(sys, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshBrands() // 写后刷新品牌商缓存 writeOK(c, &pb.ApiUpdateBrandResp{Brand: old}) } // ensureDefaultContactMail 确保品牌商联系邮箱列表里含默认邮箱(去空、去重,默认置首)。 func ensureDefaultContactMail(mails string) string { parts := strings.Split(mails, ",") cleaned := make([]string, 0, len(parts)+1) has := false for _, p := range parts { m := strings.TrimSpace(p) if m == "" { continue } if strings.EqualFold(m, defaultBrandContactMail) { has = true } cleaned = append(cleaned, m) } if !has { cleaned = append([]string{defaultBrandContactMail}, cleaned...) } return strings.Join(cleaned, ",") } // ---- 授权码生产(产品级序号 + 独立批次台账)---- // genBatchNo 生成 6 位可读批次号(防碰撞重试;仅作展示标签,兜底空串)。 func genBatchNo(sys *appConn) string { for i := 0; i < 5; i++ { code, err := comm.GeneratePublicCode() if err != nil { continue } if existing, e := dvProductionBatches(sys, "batchno=?", code); e == nil && len(existing) == 0 { return code } } return "" } // previewAuthCodes 生产预检:按产品类型算出最终授权码数量、预分配批次号、预估序号/MAC 区间, // 供前端「二次确认」界面按邮件规格展示。不落库、不占用序号游标——序号/MAC 区间按当前游标估算, // 正式生产(createAuthCodes 回传本接口给的 batchno)时以实际原子分配为准。 func (this *serverComp) previewAuthCodes(c *gin.Context, sys *appConn) { var req pb.ApiCreateAuthCodesReq _ = c.ShouldBindJSON(&req) brand, err := dvBrand(sys, req.Brandid) if err != nil { writeErr(c, pb.ErrorCode_DBError, "品牌商不存在: "+err.Error()) return } product, err := dvProduct(sys, req.Productid) if err != nil { writeErr(c, pb.ErrorCode_DBError, "产品不存在: "+err.Error()) return } codeCount := product.Devicecodecount if codeCount == 0 { codeCount = 1 } isClassicBT := product.Devicetype == pb.DeviceType_Classic_bluetooth_Headset var total int64 switch req.Createtype { case 1: total = int64(req.Number) * int64(codeCount) case 2: total = int64(len(strings.Split(req.Macaddresses, ","))) default: writeErr(c, pb.ErrorCode_ReqParameterError, "createtype 无效(1 生产 / 2 导入)") return } if total <= 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "生成数量必须大于 0") return } // 预估序号区间:当前游标 +1 ~ +total(正式生产时原子分配,并发生产时以实际为准)。 start := product.Serialcursor + 1 end := product.Serialcursor + total startmac, endmac := "", "" if isClassicBT || req.Createtype == 2 { startmac = mac.Generate(uint16(req.Productid), uint32(start)) endmac = mac.Generate(uint16(req.Productid), uint32(end)) } writeOK(c, gin.H{ "batchno": genBatchNo(sys), "brandid": req.Brandid, "brandname": brand.Brand, "productid": req.Productid, "productname": product.Devicename, "codetype": map[bool]string{true: "设备码(含MAC)", false: "授权码"}[isClassicBT || req.Createtype == 2], "codecount": codeCount, "total": total, "serialstart": start, "serialend": end, "startmac": startmac, "endmac": endmac, }) } func (this *serverComp) createAuthCodes(c *gin.Context, sys *appConn) { var req pb.ApiCreateAuthCodesReq _ = c.ShouldBindJSON(&req) if _, err := dvBrand(sys, req.Brandid); err != nil { writeErr(c, pb.ErrorCode_DBError, "品牌商不存在: "+err.Error()) return } product, err := dvProduct(sys, req.Productid) if err != nil { writeErr(c, pb.ErrorCode_DBError, "产品不存在: "+err.Error()) return } // 每副产品需要生产的授权码份数(0或1视为单份)。 codeCount := product.Devicecodecount if codeCount == 0 { codeCount = 1 } isClassicBT := product.Devicetype == pb.DeviceType_Classic_bluetooth_Headset var total int64 var importMacs []string switch req.Createtype { case 1: // 生产模式 total = int64(req.Number) * int64(codeCount) case 2: // 导入模式(外部 MAC 列表) importMacs = strings.Split(req.Macaddresses, ",") total = int64(len(importMacs)) default: writeErr(c, pb.ErrorCode_ReqParameterError, "createtype 无效(1 生产 / 2 导入)") return } if total <= 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "生成数量必须大于 0") return } // 原子分配产品级序号区间 [start, end]。 end, err := dvAllocSerial(sys, req.Productid, total) if err != nil { writeErr(c, pb.ErrorCode_DBError, "分配序号失败: "+err.Error()) return } start := end - total + 1 // 设备码(经典蓝牙/导入模式)展示 MAC 区间。 startmac, endmac := "", "" if isClassicBT || req.Createtype == 2 { startmac = mac.Generate(uint16(req.Productid), uint32(start)) endmac = mac.Generate(uint16(req.Productid), uint32(end)) } // 先建生产批次,再把批次号回填到每条授权码。批次号优先用预览确认流程回传的 // req.Batchno(保证二次确认界面展示的号 = 实际生产的号),未传则现生成; // production_batch.batchno 唯一索引兜底防撞。 batchno := strings.TrimSpace(req.Batchno) if batchno == "" { batchno = genBatchNo(sys) } batch := &pb.DBProductionBatch{ Ts: time.Now().Unix(), Brandid: req.Brandid, Productid: req.Productid, Serialstart: start, Serialend: end, Startmac: startmac, Endmac: endmac, Devicenum: uint32(total), Batchno: batchno, Remark: req.Remark, } if err = dvAddProductionBatch(sys, batch); err != nil { // Insert 回填 batch.Id writeErr(c, pb.ErrorCode_DBError, err.Error()) return } models := make([]*pb.DBAuthCode, 0, total) for i := int64(0); i < total; i++ { serial := start + i code, gerr := license.GenerateV2(uint16(req.Productid), uint32(serial)) if gerr != nil { writeErr(c, pb.ErrorCode_SystemError, gerr.Error()) return } row := &pb.DBAuthCode{ Code: code, Codetype: pb.Codetype_AUTHCODE, Brandid: req.Brandid, Productid: req.Productid, Number: serial, Batchno: batch.Batchno, Createtime: time.Now().Unix(), } switch req.Createtype { case 1: if isClassicBT { // 经典蓝牙:码即设备码,写 MAC row.Codetype = pb.Codetype_DEVICECODE row.Devicemac = mac.Generate(uint16(req.Productid), uint32(serial)) } case 2: // 导入模式:外部 MAC row.Codetype = pb.Codetype_DEVICECODE row.Devicemac = strings.TrimSpace(importMacs[i]) } models = append(models, row) } if err = dvAddAuthCodes(sys, req.Productid, models); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiCreateAuthCodesResp{ Brandid: req.Brandid, Productid: req.Productid, Batchno: batch.Batchno, Startmac: startmac, Endmac: endmac, Serialstart: start, Serialend: end, Number: uint32(total), Devices: models, }) } func (this *serverComp) disableAuthCodes(c *gin.Context, sys *appConn) { var req pb.ApiDisableAuthCodesReq _ = c.ShouldBindJSON(&req) if req.Productid == 0 || len(req.Codes) == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "productid/codes 不能为空") return } // 只接受 0(正常) 或 -1(禁用)。 disabled := int32(0) if req.Disabled != 0 { disabled = -1 } affected, err := dvSetAuthCodesDisabled(sys, req.Productid, req.Codes, disabled) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiDisableAuthCodesResp{Affected: affected}) } // revokeProductionBatch 撤销一个生产批次:校验批次存在 → 批次内无已用授权码 → // 删除该批次全部授权码 + 批次台账。已消耗的产品级序号不回退(序号只增不复用)。 func (this *serverComp) revokeProductionBatch(c *gin.Context, sys *appConn) { var req pb.ApiRevokeProductionBatchReq _ = c.ShouldBindJSON(&req) if req.Productid == 0 || req.Batchno == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "productid/batchno 不能为空") return } batch, err := dvProductionBatch(sys, req.Batchno) if err != nil || batch == nil || batch.Productid != req.Productid { writeErr(c, pb.ErrorCode_DBError, "未找到指定生产批次") return } used, err := dvCountUsedAuthCodesByBatch(sys, req.Productid, req.Batchno) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if used > 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "该批次已有授权码被激活/绑定,无法撤销") return } affected, err := dvDelAuthCodesByBatch(sys, req.Productid, req.Batchno) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if err = dvDelProductionBatch(sys, req.Batchno); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiRevokeProductionBatchResp{Deleted: uint32(affected)}) } // ---- 授权码重置(单条 / 批量 / 按用户)---- // resetOneLicense 把一条授权码重置到初始未用状态(单条/批量共享): // 取 pid(对不上则回退固定 45058)→ 若已绑定用户则删其 userdevice → // 清授权码行(status/uid/devicemac/usedtime) → 若原为激活态则扣减 product_stat 激活数。 func resetOneLicense(sys *appConn, code string) error { pid, err := license.ProductID(code) if err != nil { pid = uint16(45058) } model, err := dvAuthCode(sys, uint32(pid), code) if err != nil { return err } wasActivated := model.Uid != "" if wasActivated { if err = dvDelUserDevice(sys, model.Uid); err != nil { return err } } model.Status = 0 model.Uid = "" model.Devicemac = "" model.Usedtime = 0 if err = dvUpdateAuthCode(sys, uint32(pid), model); err != nil { return err } if wasActivated && model.Productid > 0 { _ = dvDecrProductStatActivated(sys, model.Productid) } return nil } // batchResetLicenseStatus 批量重置授权码,逐条统计成功/失败数(单条失败不阻断其余)。 func (this *serverComp) batchResetLicenseStatus(c *gin.Context, sys *appConn) { var req pb.ApiBatchResetLicenseStatusReq _ = c.ShouldBindJSON(&req) resp := &pb.ApiBatchResetLicenseStatusResp{} for _, code := range req.Licenses { if err := resetOneLicense(sys, code); err != nil { resp.FailCount++ } else { resp.SuccessCount++ } } writeOK(c, resp) } // restFactoryDevics 按用户重置:删该用户的 userdevice,并把他在该产品下持有的授权码全部清回未用态。 func (this *serverComp) restFactoryDevics(c *gin.Context, sys *appConn) { var req pb.ApiRestUserDevicsReq _ = c.ShouldBindJSON(&req) if err := dvDelUserDevice(sys, req.Uid); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } models, err := dvAuthCodesForUid(sys, req.Productid, req.Uid) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } for _, m := range models { m.Status = 0 m.Uid = "" m.Devicemac = "" } if err = dvUpdateAuthCodes(sys, req.Productid, models); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiRestUserDevicsResp{}) }