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.
 
 
 
 
 
 

133 lines
5.6 KiB

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_<pid> 分表(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()})
}
}