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.
473 lines
16 KiB
473 lines
16 KiB
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/pb"
|
|
"yunyan/sys/tencentyun/cos"
|
|
|
|
"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 上传凭证(全局,与选中应用无关)----
|
|
|
|
func (this *serverComp) getCosToken(c *gin.Context) {
|
|
result, err := cos.Sts()
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_SystemError, "获取上传凭证失败: "+err.Error())
|
|
return
|
|
}
|
|
config := cos.GetConfig()
|
|
writeOK(c, &pb.ApiGetCosTokenResp{
|
|
Bucket: config.BucketName,
|
|
Region: config.Region,
|
|
BucketUrl: config.BucketURL,
|
|
TmpSecretId: result.Credentials.TmpSecretID,
|
|
TmpSecretKey: result.Credentials.TmpSecretKey,
|
|
SessionToken: result.Credentials.SessionToken,
|
|
Expiration: result.Expiration,
|
|
Domain: config.DomainName,
|
|
})
|
|
}
|
|
|