fix: use region for invite code binding
This commit is contained in:
parent
bea31229ce
commit
4b02562864
@ -6,7 +6,7 @@
|
||||
|
||||
- 每个用户有一个可展示、可复制的永久邀请码。
|
||||
- 新用户注册或完成资料时可以选填邀请码。
|
||||
- 非空邀请码必须能解析到同一 `app_code` 下的有效邀请人。
|
||||
- 非空邀请码必须能解析到同一 `app_code`、同一 `region_id` 下的有效邀请人。
|
||||
- 邀请关系一旦绑定不可被客户端修改。
|
||||
- 邀请关系必须落 MySQL;Redis 只能做短 TTL 查询缓存。
|
||||
- 邀请统计必须拆成 `invite_count` 和 `valid_invite_count`:前者表示成功填码绑定关系的人数,后者表示被邀请人累计充值达到后台配置门槛的人数。
|
||||
@ -39,7 +39,7 @@
|
||||
### Code Ownership
|
||||
|
||||
- 邀请码属于用户,不属于设备、session、三方账号或 Agency。
|
||||
- 邀请码按 `app_code` 隔离;`lalu` 的邀请码不能邀请 `popoo` 用户。
|
||||
- 邀请码按 `app_code + region_id` 隔离;`lalu` 的邀请码不能邀请 `popoo` 用户,不同区域用户也不能互相绑定邀请关系。
|
||||
- 用户被禁用或封禁后,默认不允许继续作为新邀请人的有效 code owner;历史邀请关系不删除。
|
||||
- 用户自己的邀请码创建后长期稳定。后续如果允许换码,必须保留旧码和历史关系快照。
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
| --- | --- |
|
||||
| 注册不填邀请码 | 创建用户,不创建邀请关系 |
|
||||
| 注册填写有效邀请码 | 创建用户,并绑定 `inviter_user_id` |
|
||||
| 注册填写不存在、停用、跨 App 或 owner 非 active 的邀请码 | 返回 `INVALID_INVITE_CODE`,不静默忽略 |
|
||||
| 注册填写不存在、停用、跨 App、跨区域或 owner 非 active 的邀请码 | 返回 `INVALID_INVITE_CODE`,不静默忽略 |
|
||||
| 用户填写自己的邀请码 | 返回 `INVALID_INVITE_CODE` |
|
||||
| 已有邀请关系的用户再次提交邀请码 | 不修改原关系 |
|
||||
| 已完成注册用户提交邀请码 | 不修改原关系;如果通过 onboarding 入口提交,按已完成资料逻辑不处理邀请码 |
|
||||
@ -359,7 +359,7 @@ CREATE TABLE user_invite_counters (
|
||||
3. 创建 `users`。
|
||||
4. 创建用户自己的 `user_invite_codes`。
|
||||
5. 如果请求带 `invite_code`,解析并锁定 code owner 的 `users` 行。
|
||||
6. 校验 owner active、非自己、同 `app_code`。
|
||||
6. 校验 owner active、非自己、同 `app_code`、同 `region_id`。
|
||||
7. 插入 `user_invite_relations`。
|
||||
8. 创建 `third_party_identities` 和 `auth_sessions`。
|
||||
9. 写 `UserRegistered` / `UserInvited` outbox 事件。
|
||||
@ -521,6 +521,7 @@ Redis miss 或异常时必须回 MySQL 或 fail-closed,不能把不存在当
|
||||
| 新用户填有效邀请码 | 注册成功,关系表写入 inviter |
|
||||
| 新用户填不存在邀请码 | 返回 `INVALID_INVITE_CODE`,不创建完成关系 |
|
||||
| 邀请码跨 App | 返回 `INVALID_INVITE_CODE` |
|
||||
| 邀请码跨区域 | 返回 `INVALID_INVITE_CODE` |
|
||||
| 邀请人 disabled/banned | 返回 `INVALID_INVITE_CODE` |
|
||||
| 用户尝试填自己的邀请码 | 返回 `INVALID_INVITE_CODE` |
|
||||
| 已绑定关系后再次提交邀请码 | 原关系不变 |
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// SearchInviteReferrer 通过邀请码搜索可绑定邀请人。
|
||||
// 搜索结果只允许同 App、同国家、已完成资料且 active 的普通用户,避免 H5 枚举跨国家用户资料。
|
||||
// 邀请归因按 app 和区域隔离;国家只是用户资料快照,不能把同区域的跨国家邀请误判为不存在。
|
||||
func (s *Service) SearchInviteReferrer(ctx context.Context, userID int64, inviteCode string) (userdomain.User, error) {
|
||||
inviteCode = invitedomain.NormalizeCode(inviteCode)
|
||||
if userID <= 0 {
|
||||
@ -30,12 +30,9 @@ func (s *Service) SearchInviteReferrer(ctx context.Context, userID int64, invite
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if !current.ProfileCompleted {
|
||||
// 当前账号未完成资料时没有稳定国家边界,不能用这个入口搜索邀请人。
|
||||
// H5 绑定入口只服务已完成资料用户;注册资料页绑定走 CompleteOnboarding 的原子写链路。
|
||||
return userdomain.User{}, xerr.New(xerr.ProfileRequired, "profile required")
|
||||
}
|
||||
if current.Country == "" {
|
||||
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "current user country is required")
|
||||
}
|
||||
|
||||
inviter, err := s.userRepository.FindInviteReferrerByCode(ctx, appcode.FromContext(ctx), inviteCode)
|
||||
if err != nil {
|
||||
@ -82,8 +79,8 @@ func validateInviteReferrerPair(invited userdomain.User, inviter userdomain.User
|
||||
if !inviter.ProfileCompleted || !inviter.CanLogin() {
|
||||
return xerr.New(xerr.NotFound, "inviter not found")
|
||||
}
|
||||
if invited.Country == "" || inviter.Country == "" || invited.Country != inviter.Country {
|
||||
// 产品要求只能搜索和绑定本国家邀请人;跨国家统一返回 not found,避免泄漏目标用户是否存在。
|
||||
if invited.RegionID != inviter.RegionID {
|
||||
// 产品边界是同区域邀请;跨区域统一按未找到处理,避免 H5 用搜索接口枚举其他区域用户。
|
||||
return xerr.New(xerr.NotFound, "inviter not found")
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||||
)
|
||||
|
||||
func TestSearchInviteReferrerAllowsDifferentCountryInSameRegion(t *testing.T) {
|
||||
repository := &fakeModerationRepository{
|
||||
user: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 330307556184428544,
|
||||
Country: "SG",
|
||||
RegionID: 3,
|
||||
ProfileCompleted: true,
|
||||
Status: userdomain.StatusActive,
|
||||
},
|
||||
inviteReferrer: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 312900014781243392,
|
||||
Country: "AT",
|
||||
RegionID: 3,
|
||||
ProfileCompleted: true,
|
||||
Status: userdomain.StatusActive,
|
||||
},
|
||||
}
|
||||
svc := New(repository)
|
||||
|
||||
found, err := svc.SearchInviteReferrer(appcode.WithContext(context.Background(), "lalu"), 330307556184428544, " a5uzc4mt ")
|
||||
if err != nil {
|
||||
t.Fatalf("same-region invite referrer search failed: %v", err)
|
||||
}
|
||||
if found.UserID != 312900014781243392 {
|
||||
t.Fatalf("same-region invite referrer mismatch: %+v", found)
|
||||
}
|
||||
if repository.lastFindAppCode != "lalu" || repository.lastFindInviteCode != "A5UZC4MT" {
|
||||
t.Fatalf("invite code lookup should use normalized app/code: app=%q code=%q", repository.lastFindAppCode, repository.lastFindInviteCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchInviteReferrerRejectsDifferentRegion(t *testing.T) {
|
||||
repository := &fakeModerationRepository{
|
||||
user: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 10001,
|
||||
Country: "SG",
|
||||
RegionID: 3,
|
||||
ProfileCompleted: true,
|
||||
Status: userdomain.StatusActive,
|
||||
},
|
||||
inviteReferrer: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 10002,
|
||||
Country: "US",
|
||||
RegionID: 4,
|
||||
ProfileCompleted: true,
|
||||
Status: userdomain.StatusActive,
|
||||
},
|
||||
}
|
||||
svc := New(repository)
|
||||
|
||||
_, err := svc.SearchInviteReferrer(appcode.WithContext(context.Background(), "lalu"), 10001, "A5UZC4MT")
|
||||
if xerr.CodeOf(err) != xerr.NotFound {
|
||||
t.Fatalf("different-region invite referrer must be hidden as not found, err=%v", err)
|
||||
}
|
||||
}
|
||||
@ -104,11 +104,15 @@ func TestSetUserStatusSurfacesAccessDenylistError(t *testing.T) {
|
||||
|
||||
type fakeModerationRepository struct {
|
||||
user userdomain.User
|
||||
inviteReferrer userdomain.User
|
||||
sessionIDs []string
|
||||
countrySessionIDs []string
|
||||
lastCommand UserStatusCommand
|
||||
lastProfileCommand userdomain.ProfileUpdateCommand
|
||||
lastCountryCommand userdomain.CountryChangeCommand
|
||||
lastFindAppCode string
|
||||
lastFindInviteCode string
|
||||
lastBindInvite userdomain.BindInviteReferrerCommand
|
||||
countryChangeCalls int
|
||||
lastReport userdomain.Report
|
||||
}
|
||||
@ -211,7 +215,12 @@ func (r *fakeModerationRepository) BatchGetUsers(context.Context, []int64) (map[
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) FindInviteReferrerByCode(context.Context, string, string) (userdomain.User, error) {
|
||||
func (r *fakeModerationRepository) FindInviteReferrerByCode(_ context.Context, appCode string, inviteCode string) (userdomain.User, error) {
|
||||
r.lastFindAppCode = appCode
|
||||
r.lastFindInviteCode = inviteCode
|
||||
if r.inviteReferrer.UserID > 0 {
|
||||
return r.inviteReferrer, nil
|
||||
}
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
@ -242,7 +251,8 @@ func (r *fakeModerationRepository) CompleteOnboarding(context.Context, userdomai
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) BindInviteReferrer(context.Context, userdomain.BindInviteReferrerCommand) (userdomain.User, error) {
|
||||
func (r *fakeModerationRepository) BindInviteReferrer(_ context.Context, command userdomain.BindInviteReferrerCommand) (userdomain.User, error) {
|
||||
r.lastBindInvite = command
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ type UserRepository interface {
|
||||
BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error)
|
||||
// GetInviteAttribution 按被邀请用户读取邀请归因快照;无关系时返回 Found=false。
|
||||
GetInviteAttribution(ctx context.Context, invitedUserID int64) (invitedomain.Attribution, error)
|
||||
// FindInviteReferrerByCode 按 active 邀请码读取归属用户,供 H5 绑定页搜索同国家邀请人。
|
||||
// FindInviteReferrerByCode 按 active 邀请码读取归属用户,供 H5 绑定页搜索同区域邀请人。
|
||||
FindInviteReferrerByCode(ctx context.Context, appCode string, inviteCode string) (userdomain.User, error)
|
||||
// ListUserIDs 按稳定 user_id 游标读取低频后台任务目标用户。
|
||||
ListUserIDs(ctx context.Context, filter userdomain.UserIDPageFilter) ([]int64, error)
|
||||
@ -61,7 +61,7 @@ type UserRepository interface {
|
||||
UpdateUserContactInfo(ctx context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error)
|
||||
// CompleteOnboarding 原子写注册页必填资料并标记 profile_completed。
|
||||
CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error)
|
||||
// BindInviteReferrer 原子绑定当前用户的邀请人,并在事务内复核双方国家一致。
|
||||
// BindInviteReferrer 原子绑定当前用户的邀请人,并在事务内复核双方同区域和账号可用性。
|
||||
BindInviteReferrer(ctx context.Context, command userdomain.BindInviteReferrerCommand) (userdomain.User, error)
|
||||
// ChangeUserCountry 原子修改用户国家、写变更日志,并返回需要立即失效的 auth session。
|
||||
ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, []string, error)
|
||||
|
||||
@ -90,6 +90,10 @@ func BindRelation(ctx context.Context, tx *sql.Tx, command invitedomain.BindComm
|
||||
if code.OwnerUserID == command.InvitedUserID || code.OwnerStatus != "active" {
|
||||
return invitedomain.Binding{}, invalidInviteCode()
|
||||
}
|
||||
if code.OwnerRegionID != command.InvitedRegionID {
|
||||
// 邀请归因按区域隔离;注册、资料补全和 H5 后补绑定都必须共享同一条写入边界。
|
||||
return invitedomain.Binding{}, invalidInviteCode()
|
||||
}
|
||||
|
||||
existing, exists, err := relationForInvitedForUpdate(ctx, tx, command.AppCode, command.InvitedUserID)
|
||||
if err != nil {
|
||||
@ -331,10 +335,11 @@ func (r *Repository) ApplyRechargeEvent(ctx context.Context, event invitedomain.
|
||||
}
|
||||
|
||||
type inviteCodeRow struct {
|
||||
InviteCodeID int64
|
||||
Code string
|
||||
OwnerUserID int64
|
||||
OwnerStatus string
|
||||
InviteCodeID int64
|
||||
Code string
|
||||
OwnerUserID int64
|
||||
OwnerRegionID int64
|
||||
OwnerStatus string
|
||||
}
|
||||
|
||||
type relationRow struct {
|
||||
@ -381,13 +386,13 @@ func primaryCodeForUpdate(ctx context.Context, tx *sql.Tx, appCode string, owner
|
||||
func resolveActiveCodeForUpdate(ctx context.Context, tx *sql.Tx, appCode string, code string) (inviteCodeRow, error) {
|
||||
var row inviteCodeRow
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT c.invite_code_id, c.code, c.owner_user_id, u.status
|
||||
SELECT c.invite_code_id, c.code, c.owner_user_id, COALESCE(u.region_id, 0), u.status
|
||||
FROM user_invite_codes c
|
||||
INNER JOIN users u ON u.app_code = c.app_code AND u.user_id = c.owner_user_id
|
||||
WHERE c.app_code = ? AND c.code = ? AND c.status = ?
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
`, appCode, code, invitedomain.CodeStatusActive).Scan(&row.InviteCodeID, &row.Code, &row.OwnerUserID, &row.OwnerStatus)
|
||||
`, appCode, code, invitedomain.CodeStatusActive).Scan(&row.InviteCodeID, &row.Code, &row.OwnerUserID, &row.OwnerRegionID, &row.OwnerStatus)
|
||||
if err == sql.ErrNoRows {
|
||||
return inviteCodeRow{}, invalidInviteCode()
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp/pkg/xerr"
|
||||
invitedomain "hyapp/services/user-service/internal/domain/invite"
|
||||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||||
"hyapp/services/user-service/internal/testutil/mysqltest"
|
||||
@ -111,6 +112,65 @@ func TestInviteRelationCountersAndRechargeValidity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteOnboardingAllowsDifferentCountryInSameRegion(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := context.Background()
|
||||
|
||||
createCompletedUserWithRegion(t, repo, 10011, "163011", "AT", 3, 1000)
|
||||
createUser(t, repo, 10012, "163012", 1001)
|
||||
inviter, err := repo.GetUser(ctx, 10011)
|
||||
if err != nil {
|
||||
t.Fatalf("get inviter failed: %v", err)
|
||||
}
|
||||
|
||||
invited, err := repo.CompleteOnboarding(ctx, userdomain.CompleteOnboardingCommand{
|
||||
AppCode: "lalu",
|
||||
UserID: 10012,
|
||||
Username: "same-region",
|
||||
Avatar: "https://cdn.example/same-region.png",
|
||||
Gender: "female",
|
||||
Country: "SG",
|
||||
RegionID: 3,
|
||||
InviteCode: inviter.InviteOverview.MyInviteCode,
|
||||
RequestID: "req-same-region-onboarding",
|
||||
CompletedAtMs: 2000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("complete onboarding with same-region invite failed: %v", err)
|
||||
}
|
||||
if !invited.InviteBinding.Bound || invited.InviteBinding.InviterUserID != 10011 {
|
||||
t.Fatalf("same-region onboarding invite binding mismatch: %+v", invited.InviteBinding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteOnboardingRejectsDifferentRegionInvite(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := context.Background()
|
||||
|
||||
createCompletedUserWithRegion(t, repo, 10021, "163021", "AT", 3, 1000)
|
||||
createUser(t, repo, 10022, "163022", 1001)
|
||||
inviter, err := repo.GetUser(ctx, 10021)
|
||||
if err != nil {
|
||||
t.Fatalf("get inviter failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = repo.CompleteOnboarding(ctx, userdomain.CompleteOnboardingCommand{
|
||||
AppCode: "lalu",
|
||||
UserID: 10022,
|
||||
Username: "different-region",
|
||||
Avatar: "https://cdn.example/different-region.png",
|
||||
Gender: "female",
|
||||
Country: "US",
|
||||
RegionID: 4,
|
||||
InviteCode: inviter.InviteOverview.MyInviteCode,
|
||||
RequestID: "req-different-region-onboarding",
|
||||
CompletedAtMs: 2000,
|
||||
})
|
||||
if xerr.CodeOf(err) != xerr.InvalidInviteCode {
|
||||
t.Fatalf("different-region invite must be invalid invite code, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteRechargeValidityAcceptsAllRechargeSources(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := context.Background()
|
||||
@ -170,6 +230,35 @@ func TestInviteRechargeValidityAcceptsAllRechargeSources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createCompletedUserWithRegion(t *testing.T, repo *mysqltest.Repository, userID int64, displayUserID string, country string, regionID int64, nowMs int64) {
|
||||
t.Helper()
|
||||
if err := repo.CreateUserWithIdentity(context.Background(), userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: userID,
|
||||
DefaultDisplayUserID: displayUserID,
|
||||
CurrentDisplayUserID: displayUserID,
|
||||
CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||||
Username: "completed-user",
|
||||
Country: country,
|
||||
RegionID: regionID,
|
||||
ProfileCompleted: true,
|
||||
ProfileCompletedAtMs: nowMs,
|
||||
Status: userdomain.StatusActive,
|
||||
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
||||
CreatedAtMs: nowMs,
|
||||
UpdatedAtMs: nowMs,
|
||||
}, userdomain.Identity{
|
||||
AppCode: "lalu",
|
||||
UserID: userID,
|
||||
DisplayUserID: displayUserID,
|
||||
DefaultDisplayUserID: displayUserID,
|
||||
DisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||||
Status: userdomain.DisplayUserIDStatusActive,
|
||||
}); err != nil {
|
||||
t.Fatalf("create completed user %d failed: %v", userID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, repo *mysqltest.Repository, userID int64, displayUserID string, nowMs int64) {
|
||||
t.Helper()
|
||||
if err := repo.CreateUserWithIdentity(context.Background(), userdomain.User{
|
||||
|
||||
@ -50,7 +50,7 @@ func (r *Repository) FindInviteReferrerByCode(ctx context.Context, appCode strin
|
||||
}
|
||||
|
||||
// BindInviteReferrer 在资料完成后绑定当前用户的邀请人。
|
||||
// 这里必须锁双方 users 行并在同一事务内复核国家,不能只信 H5 先前搜索结果。
|
||||
// 这里必须锁双方 users 行并在同一事务内复核区域和账号可用性,不能只信 H5 先前搜索结果。
|
||||
func (r *Repository) BindInviteReferrer(ctx context.Context, command userdomain.BindInviteReferrerCommand) (userdomain.User, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return userdomain.User{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
@ -189,8 +189,8 @@ func validateLockedInviteReferrerPair(invited userdomain.User, inviter userdomai
|
||||
if !inviter.ProfileCompleted || !inviter.CanLogin() {
|
||||
return xerr.New(xerr.NotFound, "inviter not found")
|
||||
}
|
||||
if invited.Country == "" || inviter.Country == "" || invited.Country != inviter.Country {
|
||||
// 绑定入口和搜索入口都必须执行同国家限制,避免直接 POST 邀请码绕过搜索态校验。
|
||||
if invited.RegionID != inviter.RegionID {
|
||||
// 邀请绑定按区域隔离;跨区域统一隐藏邀请人存在性,避免直接 POST 邀请码绕过搜索态校验。
|
||||
return xerr.New(xerr.NotFound, "inviter not found")
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -30,7 +30,46 @@ func TestFindInviteReferrerByCodeDoesNotAmbiguouslySelectUserProjection(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindInviteReferrerAllowsDifferentCountryInSameRegion(t *testing.T) {
|
||||
repo := mysqltest.NewRepository(t)
|
||||
ctx := context.Background()
|
||||
|
||||
createInviteReferrerUserInRegion(t, repo, 12011, "166011", "AT", 3, 1000)
|
||||
createInviteReferrerUserInRegion(t, repo, 12012, "166012", "SG", 3, 1001)
|
||||
inviter, err := repo.GetUser(ctx, 12011)
|
||||
if err != nil {
|
||||
t.Fatalf("get inviter failed: %v", err)
|
||||
}
|
||||
|
||||
updated, err := repo.BindInviteReferrer(ctx, userdomain.BindInviteReferrerCommand{
|
||||
AppCode: "lalu",
|
||||
InvitedUserID: 12012,
|
||||
InviteCode: inviter.InviteOverview.MyInviteCode,
|
||||
RequestID: "req-same-region-bind",
|
||||
BoundAtMs: 2000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bind invite referrer in same region failed: %v", err)
|
||||
}
|
||||
if !updated.InviteBinding.Bound || updated.InviteBinding.InviterUserID != 12011 {
|
||||
t.Fatalf("same-region invite binding mismatch: %+v", updated.InviteBinding)
|
||||
}
|
||||
|
||||
attribution, err := repo.GetInviteAttribution(ctx, 12012)
|
||||
if err != nil {
|
||||
t.Fatalf("get invite attribution failed: %v", err)
|
||||
}
|
||||
if !attribution.Found || attribution.InviterUserID != 12011 || attribution.InviteCode != inviter.InviteOverview.MyInviteCode {
|
||||
t.Fatalf("same-region attribution mismatch: %+v", attribution)
|
||||
}
|
||||
}
|
||||
|
||||
func createInviteReferrerUser(t *testing.T, repo *mysqltest.Repository, userID int64, displayUserID string, nowMs int64) {
|
||||
t.Helper()
|
||||
createInviteReferrerUserInRegion(t, repo, userID, displayUserID, "SG", 3, nowMs)
|
||||
}
|
||||
|
||||
func createInviteReferrerUserInRegion(t *testing.T, repo *mysqltest.Repository, userID int64, displayUserID string, country string, regionID int64, nowMs int64) {
|
||||
t.Helper()
|
||||
err := repo.CreateUserWithIdentity(context.Background(), userdomain.User{
|
||||
AppCode: "lalu",
|
||||
@ -40,7 +79,8 @@ func createInviteReferrerUser(t *testing.T, repo *mysqltest.Repository, userID i
|
||||
CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||||
Username: "invite-referrer",
|
||||
Gender: "male",
|
||||
Country: "SG",
|
||||
Country: country,
|
||||
RegionID: regionID,
|
||||
ProfileCompleted: true,
|
||||
ProfileCompletedAtMs: nowMs,
|
||||
Status: userdomain.StatusActive,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user