From 943a522233b190e8f9a20f7d04097636253f80e7 Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Thu, 15 Oct 2026 14:38:30 +0800 Subject: [PATCH] =?UTF-8?q?user=EF=BC=9A=E6=B8=B8=E5=AE=A2=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=201000=20=E5=A4=A9=E4=BC=9A=E5=91=98=E5=92=8C=201000?= =?UTF-8?q?=20=E7=AE=97=E5=8A=9B=EF=BC=8C=E5=AD=98=E9=87=8F=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3=E7=9A=84=E5=8F=AA=E8=A1=A5=E4=B8=80=E6=AC=A1=E5=88=B0?= =?UTF-8?q?=E5=BA=95=E7=BA=BF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- apps/services/modules/user/api_sgin.go | 16 ++++-- apps/services/modules/user/model_user.go | 49 +++++++++++++++++++ apps/services/modules/user/newuser_gift.go | 43 ++++++++++++++++ .../modules/user/newuser_gift_test.go | 26 ++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) diff --git a/apps/services/modules/user/api_sgin.go b/apps/services/modules/user/api_sgin.go index 9e8300b6..dc6e6b78 100644 --- a/apps/services/modules/user/api_sgin.go +++ b/apps/services/modules/user/api_sgin.go @@ -243,9 +243,15 @@ func (this *apiComp) Sgin(session comm.IUserSession, req *pb.UserSginReq) (resp // 新用户开户礼:VIP 天数 + 算力,两个数额由后台「会员与算力」页配置。 // 在 add 之前赋值,随用户一起落库,不需要额外一次写。见 newuser_gift.go。 // 参数取一次传给发放与流水两处,保证两者用同一份快照。 + // 游客不走这档配置,固定 1000 天 VIP + 1000 算力。 giftNow := time.Now().Unix() - giftParams := comm.LoadOpsParams() - applyNewUserGift(user, giftNow, giftParams) + var giftParams comm.OpsParams + if req.Stype == pb.SginTyoe_Tourists { + applyTouristEntitlement(user, giftNow) + } else { + giftParams = comm.LoadOpsParams() + applyNewUserGift(user, giftNow, giftParams) + } if err = this.module.model.add(user); err != nil { errdata = &pb.ErrorData{ Code: pb.ErrorCode_DBError, @@ -253,7 +259,11 @@ func (this *apiComp) Sgin(session comm.IUserSession, req *pb.UserSginReq) (resp } return } - logNewUserGift(user.Uid, giftNow, giftParams) + if req.Stype == pb.SginTyoe_Tourists { + logTouristEntitlement(user.Uid, giftNow) + } else { + logNewUserGift(user.Uid, giftNow, giftParams) + } // 新增用户埋点 if this.module.analyze != nil { this.module.analyze.Report(&comm.StatEvent{Type: comm.StatEventRegister, Uid: user.Uid}) diff --git a/apps/services/modules/user/model_user.go b/apps/services/modules/user/model_user.go index aee90914..5392e403 100644 --- a/apps/services/modules/user/model_user.go +++ b/apps/services/modules/user/model_user.go @@ -41,10 +41,59 @@ func (this *modelUserComp) Init(service core.IService, module core.IModule, comp this.module.Errorln(err) } this.migrateLegacyBucketsToCompute() + this.grantTouristEntitlement() return } +// touristEntitlementKey 游客权益补发标记。存在即不再跑,避免把花掉的算力再补回 1000。 +const touristEntitlementKey = "TOURIST_ENTITLEMENT_1000" + +// 与 findtouristformac 同一口径:所有登录身份都空,才是游客。NULL 与空串都算空。 +const touristIdentitySQL = "COALESCE(phone,'')='' AND COALESCE(mail,'')='' AND COALESCE(account,'')='' " + + "AND COALESCE(wxopenid,'')='' AND COALESCE(googleopenid,'')='' " + + "AND COALESCE(facebookopenid,'')='' AND COALESCE(appleopenid,'')=''" + +// grantTouristEntitlement 给存量游客补到 1000 天 VIP、1000 算力。只跑一次。 +// +// 只抬底:会员已经更长、算力已经更多的不动。禁用账号不补。 +// 比较用的是「此刻起 1000 天」,所以会员剩 10 天的会被续到 1000 天,剩 2000 天的保持不动。 +func (this *modelUserComp) grantTouristEntitlement() { + marker := &pb.DBAppConfigItem{} + if err := mysql.FindOne(comm.TableAppConfig, marker, "`key`=?", touristEntitlementKey); err == nil { + return + } else if err != mysql.ErrNoDocuments { + this.module.Warn("游客权益:读标记失败,本次跳过", log.Field{Key: "err", Value: err.Error()}) + return + } + sec := touristVipDays * 24 * 60 * 60 + res := mysql.Exec( + "UPDATE "+comm.TableUser+" SET "+ + "computebalance = GREATEST(computebalance, ?), "+ + "vipexptime = GREATEST(vipexptime, UNIX_TIMESTAMP() + ?) "+ + "WHERE "+touristIdentitySQL+" AND state = ? "+ + "AND (computebalance < ? OR vipexptime < UNIX_TIMESTAMP() + ?)", + touristCompute, sec, int32(pb.UserState_Normal), touristCompute, sec) + if res.Error != nil { + this.module.Error("游客权益:补发失败", log.Field{Key: "err", Value: res.Error.Error()}) + return + } + if err := mysql.Insert(comm.TableAppConfig, &pb.DBAppConfigItem{ + Group: "compute", Key: touristEntitlementKey, Value: "1", + Description: "存量游客已补到 1000 天 VIP + 1000 算力(自动迁移标记,勿删)", + }); err != nil { + // 同一容器里 home/api 会各跑一次。另一边已经写过标记就不必再警告。 + again := &pb.DBAppConfigItem{} + if mysql.FindOne(comm.TableAppConfig, again, "`key`=?", touristEntitlementKey) == nil { + this.module.Infof("游客权益:已为 %d 个游客补到底线", res.RowsAffected) + return + } + this.module.Warn("游客权益:写标记失败,下次启动会再补一次", log.Field{Key: "err", Value: err.Error()}) + return + } + this.module.Infof("游客权益:已为 %d 个游客补到底线", res.RowsAffected) +} + // legacyComputeMigratedKey config 表里的一次性迁移标记;存在即不再跑。 const legacyComputeMigratedKey = "COMPUTE_LEGACY_MIGRATED" diff --git a/apps/services/modules/user/newuser_gift.go b/apps/services/modules/user/newuser_gift.go index 0adfcc00..e846a7e3 100644 --- a/apps/services/modules/user/newuser_gift.go +++ b/apps/services/modules/user/newuser_gift.go @@ -35,6 +35,49 @@ func applyNewUserGift(user *pb.DBUser, now int64, p comm.OpsParams) { user.Computebalance = p.NewUserGiftCompute } +// 游客权益:固定 1000 天 VIP + 1000 算力,不走后台「开户礼」配置。 +// +// 游客没有手机号/支付账号,后台那档开户礼(默认 30 天 + 100 算力)不够用。 +// 只抬底、不削顶:已经比这个多的(更长的会员、更多的算力)保持原值。 +// 新建账号从零值抬上来就是正好 1000;存量补发用同一套比较,避免把花掉的算力再补回去—— +// 存量只在启动时补一次,见 modelUserComp.grantTouristEntitlement。 +const ( + touristVipDays int64 = 1000 + touristCompute int64 = 1000 +) + +// applyTouristEntitlement 把游客权益写进尚未落库的新游客。 +func applyTouristEntitlement(user *pb.DBUser, now int64) { + if user == nil { + return + } + user.Vipexptime, user.Computebalance = raiseTouristEntitlement(user.Vipexptime, user.Computebalance, now) +} + +// raiseTouristEntitlement 会员到期与算力向游客底线靠拢。返回值可直接写回用户。 +func raiseTouristEntitlement(vipExp, compute, now int64) (int64, int64) { + floorExp := now + touristVipDays*24*60*60 + if vipExp < floorExp { + vipExp = floorExp + } + if compute < touristCompute { + compute = touristCompute + } + return vipExp, compute +} + +// logTouristEntitlement 记一条游客开户流水。礼包已随用户落库,流水写失败不回滚。 +func logTouristEntitlement(uid string, now int64) { + _ = mysql.Insert(comm.TableUserUseLog, &pb.DBUserUseLog{ + Uid: uid, + Ts: now, + Logtype: pb.UserLogType_ActivityReward, + Addvipday: touristVipDays, + Compute: touristCompute, + Extra: "tourist entitlement", + }) +} + // logNewUserGift 记一条活动奖励流水,便于后台对账「送出去多少」。 // 礼包已经随用户一起落库了,这条流水写失败不回滚、只忽略。 func logNewUserGift(uid string, now int64, p comm.OpsParams) { diff --git a/apps/services/modules/user/newuser_gift_test.go b/apps/services/modules/user/newuser_gift_test.go index 60d5c38e..3bd583de 100644 --- a/apps/services/modules/user/newuser_gift_test.go +++ b/apps/services/modules/user/newuser_gift_test.go @@ -52,6 +52,32 @@ func TestApplyNewUserGiftComputeOnly(t *testing.T) { // 老用户不该被碰:applyNewUserGift 只在「新建用户」分支调用, // 这里顺带守住它是直接赋值而非累加(新对象都是零值,累加与赋值等价; // 万一将来有人挪去老用户路径,这个测试会提醒他先想清楚语义)。 +func TestApplyTouristEntitlementFloor(t *testing.T) { + const now int64 = 1_700_000_000 + floorExp := now + touristVipDays*24*60*60 + + fresh := &pb.DBUser{} + applyTouristEntitlement(fresh, now) + if fresh.Vipexptime != floorExp || fresh.Computebalance != touristCompute { + t.Fatalf("新游客应正好 1000 天 + 1000 算力,得到 vip=%d compute=%d", fresh.Vipexptime, fresh.Computebalance) + } + if fresh.Tradeintegral != 0 || fresh.Meetintegral != 0 || fresh.Aichatintegral != 0 { + t.Fatalf("游客权益不应再写旧额度桶") + } + + rich := &pb.DBUser{Vipexptime: floorExp + 86400, Computebalance: touristCompute + 50} + applyTouristEntitlement(rich, now) + if rich.Vipexptime != floorExp+86400 || rich.Computebalance != touristCompute+50 { + t.Fatalf("已经更高的不应被削低,得到 vip=%d compute=%d", rich.Vipexptime, rich.Computebalance) + } + + short := &pb.DBUser{Vipexptime: now + 10*86400, Computebalance: 20} + applyTouristEntitlement(short, now) + if short.Vipexptime != floorExp || short.Computebalance != touristCompute { + t.Fatalf("不足的应抬到游客底线,得到 vip=%d compute=%d", short.Vipexptime, short.Computebalance) + } +} + func TestApplyNewUserGiftIsAssignNotAccumulate(t *testing.T) { const now int64 = 1_700_000_000 p := comm.DefaultOpsParams()