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_ 分表。 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 ) // 渠道商必选:归属随批次写进每条授权码,用户绑定该设备后统计与结算都按它归属。 // (品牌商 2026-09-12 下线,原先这里还先校验一次品牌商存在。) channel, errdata := this.resolveProductionChannel(req.Channelid) 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 }