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.
683 lines
23 KiB
683 lines
23 KiB
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"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
|
|
}
|
|
// 绑定应用(CSV,可多选):归一去重后校验都在应用中心里,避免拼错的应用名让下拉永远空着。
|
|
req.Product.Appnames = normalizeAppNames(req.Product.Appnames)
|
|
if msg := this.checkAppNames(req.Product.Appnames); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
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
|
|
}
|
|
req.Product.Appnames = normalizeAppNames(req.Product.Appnames)
|
|
if msg := this.checkAppNames(req.Product.Appnames); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
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})
|
|
}
|
|
|
|
// ---- 品牌商(写,原厂家)----
|
|
|
|
// addBrand 新增品牌商。除品牌商本身外还连带两件事:
|
|
// - 官方渠道(必做):品牌自营那条销售线,随品牌商自动建、直接启用、不配账号——
|
|
// 没有它,品牌自己直销的授权码就没有渠道维度可归属,统计与结算会全落到「无渠道」那一档;
|
|
// - 品牌商账号(可选,勾选 create_account):绑定本品牌的 identity=3 账号,仅超管可建。
|
|
//
|
|
// 账号的用户名/密码先做不落库的预校验,撞名就整单不做,避免留下「品牌商建好了、账号没建成」的半成品。
|
|
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.Brand = strings.TrimSpace(req.Brand.Brand)
|
|
if req.Brand.Brand == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "品牌名称必填")
|
|
return
|
|
}
|
|
if req.CreateAccount {
|
|
if !canCreateBoundAccount(c) {
|
|
writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可在新增品牌商时同时创建品牌账号")
|
|
return
|
|
}
|
|
// 用户名/密码都可留空:留空由服务端生成(用户名要等品牌 id 落库才能推导,
|
|
// 所以这里只对「显式填了用户名」的情况提前查重)。
|
|
if u := strings.TrimSpace(req.AccountUsername); u != "" {
|
|
if msg := this.checkNewAccountName(u); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "品牌账号"+msg)
|
|
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() // 写后刷新品牌商缓存
|
|
|
|
resp := &pb.ApiAddBrandResp{Brand: req.Brand}
|
|
ch, err := addOfficialChannel(sys, req.Brand, req.OfficialChannelName, ctxStr(c, "username"))
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "品牌商已创建,但官方渠道创建失败: "+err.Error())
|
|
return
|
|
}
|
|
resp.Channel = ch
|
|
|
|
if req.CreateAccount {
|
|
acc, err := this.openBoundAccount(pb.Identity_Agent, defaultBrandAccountName(req.Brand.Id),
|
|
req.AccountUsername, req.AccountPassword, req.AccountRemark,
|
|
req.Brand.Id, "", "品牌商", req.Brand.Brand, req.Brand.Contactmails)
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "品牌商已创建,但品牌账号创建失败: "+err.Error())
|
|
return
|
|
}
|
|
resp.Account, resp.AccountPassword = acc.Username, acc.Password
|
|
resp.MailTo, resp.MailError = acc.MailTo, acc.MailErr
|
|
}
|
|
writeOK(c, resp)
|
|
}
|
|
|
|
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 canAuditChannel(c) {
|
|
old.Sharerate = req.Brand.Sharerate
|
|
}
|
|
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})
|
|
}
|
|
|
|
// defaultBrandAccountName 品牌商账号的默认用户名:Brand-<品牌商id 十六进制大写>,如 Brand-A001。
|
|
// 与后台列表里品牌商 id 的展示形式一致,一眼能对上是哪家;撞名时 uniqueAccountName 会加后缀。
|
|
func defaultBrandAccountName(id uint32) string {
|
|
return "Brand-" + strings.ToUpper(strconv.FormatUint(uint64(id), 16))
|
|
}
|
|
|
|
// 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)
|
|
|
|
if !this.requireBrandScope(c, req.Brandid) { // 品牌商账号只能给自己生产
|
|
return
|
|
}
|
|
brand, err := dvBrand(sys, req.Brandid)
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "品牌商不存在: "+err.Error())
|
|
return
|
|
}
|
|
channel, ok := resolveProductionChannelModel(c, sys, req.Channelid, req.Brandid)
|
|
if !ok {
|
|
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,
|
|
"channelid": channel.Id,
|
|
"channelname": channel.Name,
|
|
"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 !this.requireBrandScope(c, req.Brandid) { // 品牌商账号只能给自己生产
|
|
return
|
|
}
|
|
if _, err := dvBrand(sys, req.Brandid); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "品牌商不存在: "+err.Error())
|
|
return
|
|
}
|
|
// 渠道商必选:生产时定下的归属会随批次写进每条授权码,用户绑定该设备后统计与结算都按它归属。
|
|
channel, ok := resolveProductionChannelModel(c, sys, req.Channelid, req.Brandid)
|
|
if !ok {
|
|
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,
|
|
Channelid: channel.Id,
|
|
}
|
|
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,
|
|
Channelid: channel.Id,
|
|
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,
|
|
Channelid: channel.Id,
|
|
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{})
|
|
}
|
|
|