package user import ( "context" "yunyan/comm" "yunyan/lego/core" "yunyan/lego/core/cbase" "yunyan/lego/sys/mysql" "yunyan/lego/sys/postgres" redissys "yunyan/lego/sys/redis" "yunyan/pb" "fmt" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // 代理模型 type modelUserComp struct { cbase.ModuleCompBase module *User } // 组件初始化接口 func (this *modelUserComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) { this.ModuleCompBase.Init(service, module, comp, opt) this.module = module.(*User) if err = mysql.CreateTable(comm.TableUser, &pb.DBUser{}); err != nil { this.module.Errorln(err) } if err = mysql.CreateTable(comm.TableUserdevice, &pb.DBUserDivice{}); err != nil { this.module.Errorln(err) } if err = mysql.CreateTable(comm.TableUserStatistics, &pb.DBUserStatistics{}); err != nil { this.module.Errorln(err) } if err = mysql.CreateTable(comm.TableUserUseLog, &pb.DBUserUseLog{}); err != nil { this.module.Errorln(err) } return } func (this *modelUserComp) findforid(uid string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "uid=?", uid) return } func (this *modelUserComp) findformac(mac string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "phonemac=?", mac) return } func (this *modelUserComp) findforphone(phone string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "phone=?", phone) return } func (this *modelUserComp) findformail(mail string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "mail=?", mail) return } func (this *modelUserComp) findforwx(openid string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "wxopenid=?", openid) return } func (this *modelUserComp) findforgoogle(openid string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "googleopenid=?", openid) return } func (this *modelUserComp) findforapple(openid string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "appleopenid=?", openid) return } func (this *modelUserComp) findforfacebook(openid string) (user *pb.DBUser, err error) { user = &pb.DBUser{} err = mysql.FindOne(comm.TableUser, user, "facebookopenid=?", openid) return } func (this *modelUserComp) add(user *pb.DBUser) (err error) { err = mysql.Insert(comm.TableUser, user) return } func (this *modelUserComp) save(user *pb.DBUser) (err error) { err = mysql.Save(comm.TableUser, user) return } // 软注销:清理用户业务数据 + 个人痕迹,保留身份锚点与 Isgiveaway // 保留 user 行作为防刷锚点,下次同一身份再登录时可复活 func (this *modelUserComp) cancel(user *pb.DBUser) (err error) { uid := user.Uid // 1) 删除用户模块自己管的三张表(按 uid) if err = mysql.Delete(comm.TableUserdevice, "uid=?", uid); err != nil { return } if err = mysql.Delete(comm.TableUserStatistics, "uid=?", uid); err != nil { return } if err = mysql.Delete(comm.TableUserUseLog, "uid=?", uid); err != nil { return } // 2) 跨模块:allhelp_task 表、chat_summary 表、echomeet_record 表(同一 ServiceDB) if err = mysql.Delete(comm.TableAllhelpTask, "uid=?", uid); err != nil { return } if err = mysql.Delete(comm.TableChatSummary, "uid=?", uid); err != nil { return } if err = mysql.Delete(comm.TableEchomeetRecord, "uid=?", uid); err != nil { return } // 3) 清理 DBUser 上的业务字段与 PII,保留身份锚点、Createtime、Isgiveaway user.Name = "" user.Avatar = "" user.Phonemodel = "" user.Phonemac = "" user.Viplv = 0 user.Vipexptime = 0 user.Aichatintegral = 0 user.Aichattotalintegral = 0 user.Tradeintegral = 0 user.Tradetotalintegral = 0 user.Meetintegral = 0 user.Meettotalintegral = 0 user.State = pb.UserState_Cancelled user.Canceltime = time.Now().Unix() // 保留:Uid / Phone / Mail / Wxopenid / Googleopenid / Facebookopenid / Appleopenid // Createtime / Isgiveaway / Isactivatecode / Language / Channel / Version / Lastsgintime err = mysql.Save(comm.TableUser, user) return } // 添加设备 func (this *modelUserComp) addDevice(divice *pb.DBUserDivice) (err error) { err = mysql.Insert(comm.TableUserdevice, divice) return } // 获取用户设备列表 func (this *modelUserComp) getDevices(uid string) (divices []*pb.DBUserDivice, err error) { divices = make([]*pb.DBUserDivice, 0) err = mysql.Find(comm.TableUserdevice, &divices, "uid=?", uid) return } // 获取用户设备列表 func (this *modelUserComp) getDevice(id uint64) (divice *pb.DBUserDivice, err error) { divice = &pb.DBUserDivice{} err = mysql.FindOne(comm.TableUserdevice, divice, "id=?", id) return } // 保存用户设备 func (this *modelUserComp) saveDevice(divice *pb.DBUserDivice) (err error) { err = mysql.Save(comm.TableUserdevice, divice) return } // 获取用户设备列表 func (this *modelUserComp) delDevice(id uint64) (err error) { err = mysql.Delete(comm.TableUserdevice, "id=?", id) return } // ------------------- // 合作商绑定 // getFactoryDevice / getFactoryDeviceformac 是绑定/解绑路径上的"读后写同一行"校验读, // 用 FindOnePrimary 强制走主库,避免读到只读副本复制延迟内的旧状态造成重复绑定竞态。 func (this *modelUserComp) getFactoryDevice(pid uint32, code string) (model *pb.DBAuthCode, err error) { model = &pb.DBAuthCode{} err = postgres.FindOnePrimary(fmt.Sprintf("%s_%x", comm.TableLicense, pid), model, "code=?", code) return } func (this *modelUserComp) getFactoryDeviceformac(pid uint32, devicemac string) (model *pb.DBAuthCode, err error) { model = &pb.DBAuthCode{} err = postgres.FindOnePrimary(fmt.Sprintf("%s_%x", comm.TableLicense, pid), model, "productid=? and devicemac=?", pid, devicemac) return } func (this *modelUserComp) saveFactoryDevice(pid uint32, code *pb.DBAuthCode) (err error) { err = postgres.Save(fmt.Sprintf("%s_%x", comm.TableLicense, pid), code) return } func (this *modelUserComp) saveCode(addr string, code string) (err error) { err = redissys.Conn().Set(context.Background(), redissys.RKey(fmt.Sprintf("%s:%s", comm.Redis_Verification, addr)), code, time.Minute*2).Err() return } func (this *modelUserComp) getCode(addr string) (code string, err error) { code, err = redissys.Conn().Get(context.Background(), redissys.RKey(fmt.Sprintf("%s:%s", comm.Redis_Verification, addr))).Result() return } func (this *modelUserComp) getProduct(id int32) (model *pb.DBProduct, err error) { model = &pb.DBProduct{} err = postgres.FindOne(comm.TableProduct, model, "id=?", id) return } func (this *modelUserComp) getProducts() (models []*pb.DBProduct, err error) { models = make([]*pb.DBProduct, 0) err = postgres.Find(comm.TableProduct, &models, "") return } func (this *modelUserComp) getProductForName(devicename string) (model *pb.DBProduct, err error) { model = &pb.DBProduct{} err = postgres.FindOne(comm.TableProduct, model, "devicename=?", devicename) return } func (this *modelUserComp) getProductVersions(pid uint32) (models []*pb.DBProductVersion, err error) { models = make([]*pb.DBProductVersion, 0) err = postgres.Find(comm.TableProductVersion, &models, "productid=?", pid) return } // 渠道分发配置的数据源已从「应用业务库 mysql 的 channelapp 表」迁到「console 主库 postgres 的 // channel_app 表」(comm.ChannelApp),按 app_name 作用域读、应用行覆盖全局行——与 agent / 第三方服务 // 下发同语义。下发给客户端的仍是 pb.DBChannelApp,由 toPbChannelApp 做映射。 // loadChannelApps 读本应用作用域 + 全局默认的全部渠道配置行。 // 应用名走 comm.AppName()(APP_NAME 优先、回退 ANALYZE_APP_NAME),否则只填了统计应用名的 // 部署会读不到自己那一层配置,只剩全局默认行。 func (this *modelUserComp) loadChannelApps() ([]*comm.ChannelApp, error) { app := comm.AppName() rows := make([]*comm.ChannelApp, 0) err := postgres.Find(comm.TableChannelAppCfg, &rows, "(app_name=? OR app_name='')", app) if err != nil && err != postgres.ErrNoDocuments { return nil, err } return rows, nil } // toPbChannelApp 把后台配置映射成下发结构;nil 进 nil 出,让上层照旧返回空。 func toPbChannelApp(c *comm.ChannelApp) *pb.DBChannelApp { if c == nil { return nil } return &pb.DBChannelApp{ Channel: c.Channel, Version: c.Version, Description: c.Description, Address: c.Address, Tourists: c.Tourists, Allowskipdevicebinding: c.AllowSkipDeviceBinding, Paychannels: c.PayChannels, Minversion: c.MinVersion, Forceupdate: c.ForceUpdate, Changelog: c.Changelog, Reviewversion: c.ReviewVersion, Status: c.Status, } } // getChannelAppCfg 取该渠道生效的原始配置(应用行覆盖全局行);未配置或处于草稿态返回 nil。 // 返回 comm.ChannelApp 而非 pb 结构,是为了让接口层能用 NeedForceUpdate/IsReviewing 做升级判定。 func (this *modelUserComp) getChannelAppCfg(channel int32) (*comm.ChannelApp, error) { rows, err := this.loadChannelApps() if err != nil { return nil, err } picked := comm.PickChannelApp(rows, channel) if picked == nil || !picked.Enable || picked.Status == comm.ChannelStatusDraft { return nil, nil } return picked, nil } func (this *modelUserComp) getChannelApp(channel int32) (model *pb.DBChannelApp, err error) { cfg, err := this.getChannelAppCfg(channel) if err != nil { return nil, err } return toPbChannelApp(cfg), nil } // getChannelApps 下发全部渠道:同一渠道应用行覆盖全局行;草稿状态不下发。 func (this *modelUserComp) getChannelApps() (models []*pb.DBChannelApp, err error) { rows, err := this.loadChannelApps() if err != nil { return nil, err } seen := map[int32]bool{} models = make([]*pb.DBChannelApp, 0, len(rows)) for _, r := range rows { if seen[r.Channel] { continue } seen[r.Channel] = true picked := comm.PickChannelApp(rows, r.Channel) if picked == nil || !picked.Enable || picked.Status == comm.ChannelStatusDraft { continue } models = append(models, toPbChannelApp(picked)) } return models, nil } func (this *modelUserComp) getStatistics(uid string) (statistics *pb.DBUserStatistics, err error) { statistics = &pb.DBUserStatistics{Uid: uid} mysql.FindOne(comm.TableUserStatistics, statistics, "uid=?", uid) return } func (this *modelUserComp) updateStatistics(statistics *pb.DBUserStatistics) (err error) { err = mysql.Table(comm.TableUserStatistics). Clauses(clause.OnConflict{UpdateAll: true}). Create(statistics).Error return } // 添加积分日志 func (this *modelUserComp) addIntegralLog(log *pb.DBUserUseLog) (err error) { err = mysql.Insert(comm.TableUserUseLog, log) return } // ------------------- // 厂家公码(公码定义在 AdminDB;防重领靠 DBUser.Isgiveaway,审计靠 DBUserUseLog) func (this *modelUserComp) getFactoryPublicCode(code string) (model *pb.DBFactoryPublicCode, err error) { model = &pb.DBFactoryPublicCode{} err = postgres.FindOne(comm.TableFactoryPublicCode, model, "code=?", code) return } // 原子自增 useduses 并防超发:仅当 maxuses=0 或 useduses