package console import ( "fmt" "math" "strings" "time" "yunyan/comm" "yunyan/pb" "github.com/gin-gonic/gin" ) // defaultFactoryContactMail 厂家联系邮箱缺省必含项(与 modules/api 一致)。 const defaultFactoryContactMail = "358030594@qq.com" // 设备管理域写操作 handlers。逻辑从 modules/api 对应 handler 移植,去掉 RPCX session 与 // scope 白名单(console 仅超管),数据访问改为作用于传入的"选中应用"连接 sys。 // 覆盖老 console 前端调用的写接口:updateproduct/delproduct/(add|del)productversion/ // createfactorydevics/disablefactorydevics/revokefactorydevics,以及全局的 getcostoken。 // ---- 产品 / 版本 ---- 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: 把该版本设为产品当前版本并持久化。 // (modules/api 原 handler 这里只改内存对象、漏了落库,console 补上以让"启用"生效。) 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 } factory, err := dvFactory(sys, req.Product.Factoryid) 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(factory.Productlists) == 0 { factory.Productlists = fmt.Sprintf("%d", req.Product.Id) } else { factory.Productlists = fmt.Sprintf("%s,%d", factory.Productlists, req.Product.Id) } if err = dvSaveFactory(sys, factory); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } // 注:modules/api 这里还会 RpcBroadcast 通知 home 热重载配置;console 不接 RPCX, // 如需应用感知,走 /console/api/config/notify 发 NATS 事件。 this.module.deviceCache.refreshProducts() // 新增产品 + 改了厂家 Productlists,刷新两者缓存 this.module.deviceCache.refreshFactorys() writeOK(c, &pb.ApiAddProductResp{Product: req.Product}) } // ---- 厂家(写)---- func (this *serverComp) addFactory(c *gin.Context, sys *appConn) { var req pb.ApiAddFactoryReq _ = c.ShouldBindJSON(&req) if req.Factory == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "factory 必填") return } req.Factory.Contactmails = ensureDefaultContactMail(req.Factory.Contactmails) if err := dvAddFactory(sys, req.Factory); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshFactorys() // 写后刷新厂家缓存 writeOK(c, &pb.ApiAddFactoryResp{Factory: req.Factory}) } func (this *serverComp) updateFactory(c *gin.Context, sys *appConn) { var req pb.ApiUpdateFactoryReq _ = c.ShouldBindJSON(&req) if req.Factory == nil || req.Factory.Id == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "factory id is required") return } old, err := dvFactory(sys, req.Factory.Id) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } // 只覆盖可编辑字段;probatch / productlists 等服务端维护字段不接受前端写入, // 防止表单缺字段或携旧值把生产批次号/产品列表覆盖回旧值,导致后续生产撞批次撞码。 old.Factory = req.Factory.Factory old.Factoryflag = req.Factory.Factoryflag old.Description = req.Factory.Description old.Contactmails = req.Factory.Contactmails if err = dvSaveFactory(sys, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshFactorys() // 写后刷新厂家缓存 writeOK(c, &pb.ApiUpdateFactoryResp{Factory: 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, defaultFactoryContactMail) { has = true } cleaned = append(cleaned, m) } if !has { cleaned = append([]string{defaultFactoryContactMail}, cleaned...) } return strings.Join(cleaned, ",") } // ---- 厂家设备(授权码批次)---- func (this *serverComp) createFactoryDevics(c *gin.Context, sys *appConn) { var req pb.ApiCreateFactoryDevicsReq _ = c.ShouldBindJSON(&req) var ( startmac, endmac, code string err error models = make([]*pb.DBFactoryDevics, 0, req.Number) ) modelFactory, err := dvFactory(sys, req.Factoryid) 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 } modelFactory.Probatch++ // 每副产品需要生产的设备码份数(0或1视为单份)。 codeCount := product.Devicecodecount if codeCount == 0 { codeCount = 1 } switch req.Createtype { case 1: // 生产模式 total := req.Number * codeCount if startmac, err = generateCustomMAC(uint16(req.Productid), byte(modelFactory.Probatch), uint32(1)); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } if endmac, err = generateCustomMAC(uint16(req.Productid), byte(modelFactory.Probatch), total*2+uint32(math.Ceil(float64(total)/float64(200)))); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } for i := 1; i <= int(total); i++ { if code, err = comm.GenerateLicense(uint16(req.Productid), byte(modelFactory.Probatch), uint32(i)); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } device := &pb.DBFactoryDevics{ Code: code, Codetype: pb.Codetype_AUTHCODE, Productid: req.Productid, Factoryid: req.Factoryid, Probatch: modelFactory.Probatch, Number: uint32(i), Createtime: time.Now().Unix(), } if product.Devicetype == pb.DeviceType_Classic_bluetooth_Headset { // 经典蓝牙:码即 MAC if endmac, err = generateCustomMAC(uint16(req.Productid), byte(modelFactory.Probatch), uint32(i)); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } device.Codetype = pb.Codetype_DEVICECODE device.Devicemac = endmac } models = append(models, device) } case 2: // 导入模式 for i, v := range strings.Split(req.Macaddresses, ",") { if code, err = comm.GenerateLicense(uint16(req.Productid), byte(modelFactory.Probatch), uint32(i+1)); err != nil { writeErr(c, pb.ErrorCode_SystemError, err.Error()) return } models = append(models, &pb.DBFactoryDevics{ Code: code, Codetype: pb.Codetype_DEVICECODE, Productid: req.Productid, Factoryid: req.Factoryid, Probatch: modelFactory.Probatch, Number: uint32(i + 1), Createtime: time.Now().Unix(), Devicemac: v, }) } } if err = dvAddFactoryDevics(sys, product.Id, models); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if err = dvSaveFactory(sys, modelFactory); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } this.module.deviceCache.refreshFactorys() // 批次号 Probatch 变更,刷新厂家缓存 if err = dvAddFactoryDeliveryNote(sys, &pb.DBFactoryDeliveryNote{ Ts: time.Now().Unix(), Factoryid: req.Factoryid, Productid: req.Productid, Probatch: modelFactory.Probatch, Startmac: startmac, Endmac: endmac, Devicetnum: uint32(len(models)), }); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiCreateFactoryDevicsResp{ Factoryid: req.Factoryid, Productid: req.Productid, Startmac: startmac, Endmac: endmac, Probatch: modelFactory.Probatch, Number: uint32(len(models)), Devices: models, }) } func (this *serverComp) disableFactoryDevics(c *gin.Context, sys *appConn) { var req pb.ApiDisableFactoryDevicsReq _ = 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 := dvSetFactoryDevicsDisabled(sys, req.Productid, req.Codes, disabled) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiDisableFactoryDevicsResp{Affected: affected}) } // revokeFactoryDevics 撤销一个生产批次:校验出货单存在 → 批次内无已用设备码 → // 删除该批次全部设备码 + 出货单。不回退 factory.probatch(已分发批次号不复用)。 func (this *serverComp) revokeFactoryDevics(c *gin.Context, sys *appConn) { var req pb.ApiRevokeFactoryDevicsReq _ = c.ShouldBindJSON(&req) if req.Factoryid == 0 || req.Productid == 0 || req.Probatch == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "factoryid/productid/probatch 不能为空") return } note, err := dvFactoryDeliveryNote(sys, req.Factoryid, req.Probatch) if err != nil || note == nil || note.Productid != req.Productid { writeErr(c, pb.ErrorCode_DBError, "未找到指定生产批次") return } used, err := dvCountUsedFactoryDevicsByBatch(sys, req.Productid, req.Probatch) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if used > 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "该批次已有设备码被激活/绑定,无法撤销") return } affected, err := dvDelFactoryDevicsByBatch(sys, req.Productid, req.Probatch) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if err = dvDelFactoryDeliveryNote(sys, req.Factoryid, req.Productid, req.Probatch); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiRevokeFactoryDevicsResp{Deleted: uint32(affected)}) } // ---- 设备码重置(单条 / 批量 / 按用户)---- // resetOneLicense 把一条 License 重置到初始未用状态(单条/批量共享): // 校验 License 取 pid(旧码对不上则回退固定 45058)→ 若已绑定用户则删其 userdevice → // 清 license 行(status/uid/devicemac/usedtime) → 若原为激活态则扣减 product_stat 激活数。 // 与 modules/api 的 resetOneLicense 同口径——console 旧版只清 license 行、漏了后两步,已统一修正。 func resetOneLicense(sys *appConn, license string) error { pid, _, _, err := comm.ValidateLicense(license) if err != nil { pid = uint16(45058) } model, err := dvFactoryDevic(sys, uint32(pid), license) 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 = dvUpdateFactoryDevic(sys, uint32(pid), model); err != nil { return err } if wasActivated && model.Productid > 0 { _ = dvDecrProductStatActivated(sys, model.Productid) } return nil } // batchResetLicenseStatus 批量重置 License,逐条统计成功/失败数(单条失败不阻断其余)。 func (this *serverComp) batchResetLicenseStatus(c *gin.Context, sys *appConn) { var req pb.ApiBatchResetLicenseStatusReq _ = c.ShouldBindJSON(&req) resp := &pb.ApiBatchResetLicenseStatusResp{} for _, license := range req.Licenses { if err := resetOneLicense(sys, license); 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 := dvFactoryDevicForUid(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 = dvUpdateFactoryDevics(sys, req.Productid, models); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiRestUserDevicsResp{}) } // ---- 文件直传凭证 ---- // // 原「腾讯云 COS 上传凭证(getCosToken)」已下线,改为阿里云 OSS 预签名 PUT URL, // 路由 api_getcostoken → serverComp.getUploadURL(见 api_upload.go)。