package user import ( "strings" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/lego/sys/mysql" "yunyan/pb" ) // @Summary 解绑定耳机设备 // @Description 绑定耳机设备 // @Tags User // @Accept json // @Produce json // @Security BearerAuth // @Param user body pb.UserUnBindDeviceReq true "发送绑定请求" // @Success 200 {object} comm.HttpResult{data=pb.UserUnBindDeviceResp} "成功返回" // @Router /api/home/user_unbinddevice [post] func (this *apiComp) UnBindDevice(session comm.IUserSession, req *pb.UserUnBindDeviceReq) (resp *pb.UserUnBindDeviceResp, errdata *pb.ErrorData) { var ( divice *pb.DBUserDivice err error ) // 解绑的**唯一**必要条件是「这条绑定记录属于当前用户」。 // // ⚠️ 别再把「授权码行必须存在」设成前置。绑定关系存在 userdevice(MySQL), // 授权码行存在 license_ 分表(Postgres),**两个库**天然会不一致: // 后台重新导入过 license、分表选错、MAC 写法对不上,任何一种都会让查询返回 // record not found。原实现遇到这种情况直接 return,delDevice 永远走不到—— // 表现就是「设备管理页列着它,点解绑却报『请求的资源不存在』,永远删不掉」, // 而用户完全没有自救手段。 // // 授权码行上的归属只是台账,清不掉也无害:别人再绑这台设备时, // api_binddevice.go 的接管分支(model.Status != 0 && model.Uid != 当前用户) // 会把旧绑定收拾干净。 if divice, err = this.module.model.getDevice(req.Id); err != nil { if err == mysql.ErrNoDocuments { resp = &pb.UserUnBindDeviceResp{} } else { errdata = &pb.ErrorData{ Code: pb.ErrorCode_DBError, Message: err.Error(), } } return } if divice.Uid != session.GetUserId() { resp = &pb.UserUnBindDeviceResp{} return } // 台账维护:尽力清掉授权码行上的归属,失败只记日志、不阻断解绑。 this.releaseFactoryDeviceOwner(session.GetUserId(), divice) if err = this.module.model.delDevice(divice.Id); err != nil { errdata = &pb.ErrorData{ Code: pb.ErrorCode_DBError, Message: err.Error(), } return } // 解绑冲正 -1:绑定数是「当日净增」口径,只加不减会让绑了又解的设备永久留在统计里。 // 产品/渠道取被解绑设备自身的归属(绑定时固化),保证 +1 与 -1 落在同一分区。 // 激活不冲正:激活是设备一生一次的事件,解绑也不会把授权码 status 归零。 if this.module.analyze != nil { this.module.analyze.Report(&comm.StatEvent{ Type: comm.StatEventDeviceBind, Uid: session.GetUserId(), ProductId: uint32(divice.Productid), ChannelId: divice.Channelid, Count: -1, }) } resp = &pb.UserUnBindDeviceResp{} return } // releaseFactoryDeviceOwner 把授权码行上的归属清掉(台账维护)。 // // **尽力而为**:定位不到、写不回都只记一条日志就返回,绝不阻断解绑。 // 理由见 UnBindDevice 里的说明——userdevice 与 license 分表在两个库, // 把它设成硬前置会让用户永远删不掉自己的绑定记录。 // // 定位顺序按「设备身份就是 MAC」这条口径:**先 MAC,后 License**。 // License 只用于兜底那些没有 MAC 的老记录(早年按设备码导入、未登记 MAC 的)。 // 原实现是 License 优先,而经典蓝牙耳机的 license 列存量恒为空、新写入的又等于 // MAC 本身,优先它没有任何好处,反倒在 license 与 MAC 不一致时定位到错误的行。 func (this *apiComp) releaseFactoryDeviceOwner(uid string, divice *pb.DBUserDivice) { // device_mac 是全局一张表,定位设备**不再需要 PID**(2026-09-04 合表)。 // 原先要先把 PID 弄对才能选中分表,弄不对就静默跳过归属清理。 var ( model *pb.DBAuthCode err error ) if mc := strings.TrimSpace(divice.Devicemac); mc != "" { model, err = this.module.model.getFactoryDeviceformac(mc) } else if lic := strings.TrimSpace(divice.License); lic != "" { model, err = this.module.model.getFactoryDevice(lic) } else { this.module.Warn("UnBindDevice: 绑定记录既无 MAC 也无授权码,跳过授权码归属清理", log.Field{Key: "deviceId", Value: divice.Id}) return } if err != nil { // 最常见的就是这条:device_mac 里没有这台设备(后台重导入过、MAC 写法对不上)。 // 解绑照常进行。 this.module.Warn("UnBindDevice: 设备行定位失败,已跳过归属清理,解绑照常进行", log.Field{Key: "deviceId", Value: divice.Id}, log.Field{Key: "devicemac", Value: divice.Devicemac}, log.Field{Key: "license", Value: divice.License}, log.Field{Key: "err", Value: err.Error()}) return } if model.Uid != uid { // 已经被别人接管了,不该由这次解绑去动它 return } model.Uid = "" // ⚠️ 不清 devicemac:设备码 = MAC = 分表主键 code(授权码字符串已于 2026-09-02 移除), // 清掉等于把这台设备从库里抹掉——按 MAC 反查绑定的路径再也找不到它, // 而 code 那一列还留着,两列从此对不上。 // // ⚠️ 也不清 Status:绑定礼靠 codestate==0 判「这台设备此前没被绑过」, // 归零会让同一台设备反复绑解刷出多份权益。 if err = this.module.model.saveFactoryDevice(model); err != nil { this.module.Warn("UnBindDevice: 授权码归属清理写回失败,解绑照常进行", log.Field{Key: "deviceId", Value: divice.Id}, log.Field{Key: "code", Value: model.Code}, log.Field{Key: "err", Value: err.Error()}) } }