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.
 
 
 
 
 
 

188 lines
6.8 KiB

package api
import (
"fmt"
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/lego/sys/postgres"
"yunyan/pb"
"yunyan/utils/devcode"
"yunyan/utils/mac"
)
// checkProductionFactory 校验批次要记的代工厂:0=不指定直接放行;非 0 时必须存在且未停用。
// 直接查表而不建 model 层:oem_factory 是后台(console 模块)维护的档案,api 这边只读一个存在性,
// 为它在 api 模块再复制一份 struct 反而会两处定义漂移。
func (this *apiComp) checkProductionFactory(id uint32) *pb.ErrorData {
if id == 0 {
return nil // 可选:存量流程没有工厂概念,不强制填
}
var n int64
if err := postgres.Table(comm.TableOemFactory).Where("id=? AND enabled=?", id, true).Count(&n).Error; err != nil {
return &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
}
if n == 0 {
return &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "代工厂不存在或已停用,请到后台「工厂管理」确认"}
}
return nil
}
// CreateAuthCodes 创建授权码/设备码(一次生产 = 一个批次)。
//
// 序号采用「产品级全局游标」:每次生产原子地从 product.serialcursor 上分配一段连续区间
// [start, end],不再随批次重置,也不再依赖厂家维度的 probatch 计数器。
// 每次生产落一条 production_batch 台账(记录序号区间/MAC区间/数量),
// 再把该批次的全部授权码写入 license_<productid十六进制> 分表。
func (this *apiComp) CreateAuthCodes(session comm.IUserSession, req *pb.ApiCreateAuthCodesReq) (resp *pb.ApiCreateAuthCodesResp, errdata *pb.ErrorData) {
var (
err error
product *pb.DBProduct
now = time.Now().Unix()
pid = req.Productid
)
// 校验品牌商存在
if _, err = this.module.model.factory(req.Brandid); err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
return
}
// 渠道商必选:归属随批次写进每条授权码,用户绑定该设备后统计与结算都按它归属。
channel, errdata := this.resolveProductionChannel(req.Channelid, req.Brandid)
if errdata != nil {
return
}
// 代工厂可选(0=不指定):只落在批次台账上,不写进每条授权码——
// 换厂是批次级的事,授权码里再存一份只会在改厂时产生两处不一致。
if errdata = this.checkProductionFactory(req.Factoryid); errdata != nil {
return
}
if product, err = this.module.model.product(pid); err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
return
}
// 每副产品需要生产的设备码份数(0或1视为单份)
codeCount := product.Devicecodecount
if codeCount == 0 {
codeCount = 1
}
// 计算本次生产的设备总数
var (
total int64
macaddresses []string
)
switch req.Createtype {
case 1: // 生产模式:数量 * 每副份数
total = int64(req.Number) * int64(codeCount)
case 2: // 导入模式:按逗号拆分的 MAC 份数
macaddresses = strings.Split(req.Macaddresses, ",")
total = int64(len(macaddresses))
default:
errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "createtype 仅支持 1(生产) 或 2(导入)"}
return
}
if total <= 0 {
errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "生成数量必须大于0"}
return
}
// 原子分配产品级序号区间 [start, end]
end, err := this.module.model.allocSerialRange(pid, total)
if err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
this.module.Error("allocSerialRange Fail!", log.Field{Key: "err", Value: err.Error()})
return
}
start := end - total + 1
// ⚠️ 一条设备 = 一个 MAC,code 就是 MAC(2026-09-02 起,与 modules/console 同一口径)。
// 授权码字符串已彻底移除:绑定/禁用/重置/结算反查批次全走 MAC,
// MAC 前 2 字节就是 PID,产品/批次/渠道都能从它查回来,不需要第二套标识。
models := make([]*pb.DBAuthCode, 0, total)
for i := int64(0); i < total; i++ {
device := &pb.DBAuthCode{
Codetype: pb.Codetype_DEVICECODE,
Brandid: req.Brandid,
Channelid: channel.Id,
Productid: pid,
Number: start + i,
Createtime: now,
}
if req.Createtype == 1 {
// MAC 只是 (PID,序号) 的一种编码,与设备是不是经典蓝牙无关,所有产品一视同仁。
device.Devicemac = mac.Generate(uint16(pid), uint32(start+i))
} else { // 导入模式:外部已烧录好的 MAC
norm, ok := devcode.NormalizeMac(macaddresses[i])
if !ok {
errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError,
Message: fmt.Sprintf("第 %d 个 MAC 格式不对: %s", i+1, macaddresses[i])}
return
}
device.Devicemac = norm
}
device.Code = device.Devicemac
models = append(models, device)
}
// MAC 区间只在【生产模式】下有意义:MAC 由 (PID, 序号) 算出,首尾即真实边界。
// 导入模式的 MAC 来自外部列表、彼此不连续,留空——存一对按序号算出来、库里根本不存在的
// 假地址,比空着更有害。(与 modules/console 同一口径。)
var startmac, endmac string
if req.Createtype == 1 {
startmac = mac.Generate(uint16(pid), uint32(start))
endmac = mac.Generate(uint16(pid), uint32(end))
}
// 先落批次台账。批次号 = 6 位可读短码(batchno 唯一索引兜底防碰撞),授权码用它关联批次。
// 优先用预览确认流程回传的 req.Batchno(保证二次确认展示的号 = 实际生产的号),未传则现生成。
batchno := strings.TrimSpace(req.Batchno)
if batchno == "" {
batchno, _ = comm.GeneratePublicCode()
}
batch := &pb.DBProductionBatch{
Ts: now,
Brandid: req.Brandid,
Channelid: channel.Id,
Productid: pid,
Serialstart: start,
Serialend: end,
Startmac: startmac,
Endmac: endmac,
Devicenum: uint32(total),
Batchno: batchno,
Remark: req.Remark,
Factoryid: req.Factoryid,
}
if err = this.module.model.addproductionbatch(batch); err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
this.module.Error("addproductionbatch Fail!", log.Field{Key: "err", Value: err.Error()})
return
}
// 回填批次号后批量写入分表
for _, row := range models {
row.Batchno = batchno
}
if err = this.module.model.addfactoryDevics(pid, models); err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()}
this.module.Error("addauthcodes Fail!", log.Field{Key: "err", Value: err.Error()})
return
}
resp = &pb.ApiCreateAuthCodesResp{
Brandid: req.Brandid,
Channelid: channel.Id,
Productid: pid,
Batchno: batchno,
Startmac: startmac,
Endmac: endmac,
Serialstart: start,
Serialend: end,
Number: uint32(total),
Devices: models,
}
return
}