强制用户输入联系方式
This commit is contained in:
parent
db71bbfe09
commit
8df263195a
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,7 @@ message CoinSellerListItem {
|
||||
string status = 12;
|
||||
string merchant_asset_type = 13;
|
||||
int64 updated_at_ms = 14;
|
||||
string contact_info = 15;
|
||||
}
|
||||
|
||||
// UserRoleSummary 是 App 我的页和入口显隐使用的轻量角色摘要。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -86,6 +86,7 @@ message User {
|
||||
string pretty_id = 29;
|
||||
string pretty_display_user_id = 30;
|
||||
string profile_bg_img = 31;
|
||||
string contact_info = 32;
|
||||
}
|
||||
|
||||
// InviteOverview 是我的页可直接展示的邀请码和邀请计数 read model。
|
||||
@ -733,6 +734,18 @@ message UpdateUserProfileBackgroundResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
// UpdateUserContactInfoRequest 只修改用户主资料上的联系方式,供不同经营身份共用。
|
||||
message UpdateUserContactInfoRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string contact_info = 3;
|
||||
}
|
||||
|
||||
// UpdateUserContactInfoResponse 返回写入后的用户资料投影。
|
||||
message UpdateUserContactInfoResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
// ChangeUserCountryRequest 是 App 用户自助修改国家入口;受限业务身份不允许自助修改。
|
||||
message ChangeUserCountryRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -1272,6 +1285,7 @@ service UserService {
|
||||
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
||||
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
|
||||
rpc UpdateUserProfileBackground(UpdateUserProfileBackgroundRequest) returns (UpdateUserProfileBackgroundResponse);
|
||||
rpc UpdateUserContactInfo(UpdateUserContactInfoRequest) returns (UpdateUserContactInfoResponse);
|
||||
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
|
||||
|
||||
@ -28,6 +28,7 @@ const (
|
||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
||||
UserService_UpdateUserProfileBackground_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfileBackground"
|
||||
UserService_UpdateUserContactInfo_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserContactInfo"
|
||||
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
||||
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
||||
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
||||
@ -52,6 +53,7 @@ type UserServiceClient interface {
|
||||
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error)
|
||||
UpdateUserProfile(ctx context.Context, in *UpdateUserProfileRequest, opts ...grpc.CallOption) (*UpdateUserProfileResponse, error)
|
||||
UpdateUserProfileBackground(ctx context.Context, in *UpdateUserProfileBackgroundRequest, opts ...grpc.CallOption) (*UpdateUserProfileBackgroundResponse, error)
|
||||
UpdateUserContactInfo(ctx context.Context, in *UpdateUserContactInfoRequest, opts ...grpc.CallOption) (*UpdateUserContactInfoResponse, error)
|
||||
ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||
SetUserStatus(ctx context.Context, in *SetUserStatusRequest, opts ...grpc.CallOption) (*SetUserStatusResponse, error)
|
||||
@ -159,6 +161,16 @@ func (c *userServiceClient) UpdateUserProfileBackground(ctx context.Context, in
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userServiceClient) UpdateUserContactInfo(ctx context.Context, in *UpdateUserContactInfoRequest, opts ...grpc.CallOption) (*UpdateUserContactInfoResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UpdateUserContactInfoResponse)
|
||||
err := c.cc.Invoke(ctx, UserService_UpdateUserContactInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userServiceClient) ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ChangeUserCountryResponse)
|
||||
@ -244,6 +256,7 @@ type UserServiceServer interface {
|
||||
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
||||
UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error)
|
||||
UpdateUserProfileBackground(context.Context, *UpdateUserProfileBackgroundRequest) (*UpdateUserProfileBackgroundResponse, error)
|
||||
UpdateUserContactInfo(context.Context, *UpdateUserContactInfoRequest) (*UpdateUserContactInfoResponse, error)
|
||||
ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error)
|
||||
@ -288,6 +301,9 @@ func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *Update
|
||||
func (UnimplementedUserServiceServer) UpdateUserProfileBackground(context.Context, *UpdateUserProfileBackgroundRequest) (*UpdateUserProfileBackgroundResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserProfileBackground not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserContactInfo(context.Context, *UpdateUserContactInfoRequest) (*UpdateUserContactInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserContactInfo not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
||||
}
|
||||
@ -492,6 +508,24 @@ func _UserService_UpdateUserProfileBackground_Handler(srv interface{}, ctx conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserService_UpdateUserContactInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateUserContactInfoRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserServiceServer).UpdateUserContactInfo(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserService_UpdateUserContactInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserServiceServer).UpdateUserContactInfo(ctx, req.(*UpdateUserContactInfoRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserService_ChangeUserCountry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ChangeUserCountryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -661,6 +695,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "UpdateUserProfileBackground",
|
||||
Handler: _UserService_UpdateUserProfileBackground_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateUserContactInfo",
|
||||
Handler: _UserService_UpdateUserContactInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ChangeUserCountry",
|
||||
Handler: _UserService_ChangeUserCountry_Handler,
|
||||
|
||||
@ -186,6 +186,7 @@ type User struct {
|
||||
ISONumeric string `json:"isoNumeric"`
|
||||
PhoneCountryCode string `json:"phoneCountryCode"`
|
||||
CountryEnabled bool `json:"countryEnabled"`
|
||||
ContactInfo string `json:"contactInfo"`
|
||||
}
|
||||
|
||||
type GRPCClient struct {
|
||||
@ -415,6 +416,7 @@ func fromProtoUser(user *userv1.User) *User {
|
||||
ISONumeric: user.GetIsoNumeric(),
|
||||
PhoneCountryCode: user.GetPhoneCountryCode(),
|
||||
CountryEnabled: user.GetCountryEnabled(),
|
||||
ContactInfo: user.GetContactInfo(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,6 +568,7 @@ type BDProfile struct {
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
Contact string `json:"contact"`
|
||||
RegionName string `json:"regionName"`
|
||||
ParentLeaderDisplayID string `json:"parentLeaderDisplayUserId"`
|
||||
ParentLeaderUsername string `json:"parentLeaderUsername"`
|
||||
@ -593,6 +596,7 @@ type Agency struct {
|
||||
OwnerDisplayUserID string `json:"ownerDisplayUserId"`
|
||||
OwnerUsername string `json:"ownerUsername"`
|
||||
OwnerAvatar string `json:"ownerAvatar"`
|
||||
Contact string `json:"contact"`
|
||||
ParentBDDisplayUserID string `json:"parentBdDisplayUserId"`
|
||||
ParentBDUsername string `json:"parentBdUsername"`
|
||||
ParentBDAvatar string `json:"parentBdAvatar"`
|
||||
@ -614,6 +618,7 @@ type HostProfile struct {
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
Contact string `json:"contact"`
|
||||
RegionName string `json:"regionName"`
|
||||
CurrentAgencyName string `json:"currentAgencyName"`
|
||||
Diamond int64 `json:"diamond"`
|
||||
|
||||
@ -146,9 +146,9 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
|
||||
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)"
|
||||
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ? OR u.contact_info LIKE ?)"
|
||||
args = append(args, matchArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
@ -167,7 +167,7 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
SELECT bp.user_id, bp.role, `+userCountryRegionIDSQL("user_region")+`, COALESCE(bp.parent_leader_user_id, 0),
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
COALESCE(u.avatar, ''), COALESCE(u.contact_info, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
COALESCE(parent_leader.current_display_user_id, ''), COALESCE(parent_leader.username, ''),
|
||||
COALESCE(parent_leader.avatar, ''),
|
||||
COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''),
|
||||
@ -203,6 +203,7 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.Contact,
|
||||
&item.RegionName,
|
||||
&item.ParentLeaderDisplayID,
|
||||
&item.ParentLeaderUsername,
|
||||
@ -265,9 +266,9 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
|
||||
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)"
|
||||
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ? OR u.contact_info LIKE ?)"
|
||||
args = append(args, matchArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
@ -285,7 +286,7 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
SELECT bp.user_id, 'bd_leader', `+userCountryRegionIDSQL("user_region")+`, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
COALESCE(u.avatar, ''), COALESCE(u.contact_info, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
'', '', '',
|
||||
COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''),
|
||||
COALESCE(creator.avatar, ''),
|
||||
@ -320,6 +321,7 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.Contact,
|
||||
&item.RegionName,
|
||||
&item.ParentLeaderDisplayID,
|
||||
&item.ParentLeaderUsername,
|
||||
@ -362,7 +364,7 @@ func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDP
|
||||
SELECT bp.user_id, 'bd_leader', `+userCountryRegionIDSQL("user_region")+`, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
COALESCE(u.avatar, ''), COALESCE(u.contact_info, ''), `+userCountryRegionNameSQL("user_region")+`,
|
||||
COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''),
|
||||
COALESCE(creator.avatar, ''),
|
||||
(
|
||||
@ -389,6 +391,7 @@ func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDP
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.Contact,
|
||||
&item.RegionName,
|
||||
&item.CreatedByDisplayUserID,
|
||||
&item.CreatedByUsername,
|
||||
@ -829,11 +832,11 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
nowMs := time.Now().UnixMilli()
|
||||
ownerSQL, ownerArgs := shared.UserIdentityKeywordSQL("owner", "a.owner_user_id", keyword, nowMs)
|
||||
parentSQL, parentArgs := shared.UserIdentityKeywordSQL("parent_bd", "a.parent_bd_user_id", keyword, nowMs)
|
||||
whereSQL += " AND (a.name LIKE ? OR CAST(a.agency_id AS CHAR) LIKE ? OR " + ownerSQL + " OR " + parentSQL + " OR owner_region.name LIKE ?)"
|
||||
whereSQL += " AND (a.name LIKE ? OR CAST(a.agency_id AS CHAR) LIKE ? OR " + ownerSQL + " OR " + parentSQL + " OR owner_region.name LIKE ? OR owner.contact_info LIKE ?)"
|
||||
args = append(args, like, like)
|
||||
args = append(args, ownerArgs...)
|
||||
args = append(args, parentArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
@ -853,7 +856,7 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
a.name, a.status, a.join_enabled, a.max_hosts,
|
||||
a.created_by_user_id, a.created_at_ms, a.updated_at_ms,
|
||||
COALESCE(owner.current_display_user_id, ''), COALESCE(owner.username, ''),
|
||||
COALESCE(owner.avatar, ''),
|
||||
COALESCE(owner.avatar, ''), COALESCE(owner.contact_info, ''),
|
||||
COALESCE(parent_bd.current_display_user_id, ''), COALESCE(parent_bd.username, ''),
|
||||
COALESCE(parent_bd.avatar, ''), `+userCountryRegionNameSQL("owner_region")+`,
|
||||
(
|
||||
@ -890,6 +893,7 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
&item.OwnerDisplayUserID,
|
||||
&item.OwnerUsername,
|
||||
&item.OwnerAvatar,
|
||||
&item.Contact,
|
||||
&item.ParentBDDisplayUserID,
|
||||
&item.ParentBDUsername,
|
||||
&item.ParentBDAvatar,
|
||||
@ -948,11 +952,11 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
nowMs := time.Now().UnixMilli()
|
||||
hostSQL, hostArgs := shared.UserIdentityKeywordSQL("u", "hp.user_id", keyword, nowMs)
|
||||
agencyOwnerSQL, agencyOwnerArgs := shared.UserIdentityKeywordSQL("agency_owner", "a.owner_user_id", keyword, nowMs)
|
||||
whereSQL += " AND (" + hostSQL + " OR a.name LIKE ? OR " + agencyOwnerSQL + " OR user_region.name LIKE ?)"
|
||||
whereSQL += " AND (" + hostSQL + " OR a.name LIKE ? OR " + agencyOwnerSQL + " OR user_region.name LIKE ? OR u.contact_info LIKE ?)"
|
||||
args = append(args, hostArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, agencyOwnerArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
@ -971,7 +975,7 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
COALESCE(hp.current_membership_id, 0), hp.source,
|
||||
hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), `+userCountryRegionNameSQL("user_region")+`, COALESCE(a.name, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(u.contact_info, ''), `+userCountryRegionNameSQL("user_region")+`, COALESCE(a.name, ''),
|
||||
COALESCE(a.owner_user_id, 0), COALESCE(agency_owner.current_display_user_id, ''),
|
||||
COALESCE(agency_owner.username, ''), COALESCE(agency_owner.avatar, '')
|
||||
%s
|
||||
@ -1000,6 +1004,7 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.Contact,
|
||||
&item.RegionName,
|
||||
&item.CurrentAgencyName,
|
||||
&item.CurrentAgencyOwnerUserID,
|
||||
@ -1066,9 +1071,9 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
|
||||
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "csp.user_id", keyword, time.Now().UnixMilli())
|
||||
whereSQL += " AND (" + matchSQL + " OR csp.contact_info LIKE ?)"
|
||||
whereSQL += " AND (" + matchSQL + " OR u.contact_info LIKE ? OR csp.contact_info LIKE ?)"
|
||||
args = append(args, matchArgs...)
|
||||
args = append(args, like)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
@ -1084,7 +1089,7 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT csp.user_id, csp.status, csp.merchant_asset_type,
|
||||
COALESCE(csp.contact_info, ''),
|
||||
COALESCE(NULLIF(u.contact_info, ''), csp.contact_info, ''),
|
||||
csp.created_by_user_id, csp.created_at_ms, csp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(u.region_id, 0),
|
||||
@ -1147,11 +1152,9 @@ func (r *Reader) UpdateCoinSellerContact(ctx context.Context, userID int64, cont
|
||||
if r == nil || r.db == nil {
|
||||
return errUserDBNotConfigured()
|
||||
}
|
||||
if err := r.ensureCoinSellerContactColumn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// 币商联系方式现在是用户主资料,后台行内编辑和 H5 自助修改必须写同一个 users 字段,避免同一用户多身份展示不同联系方式。
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE coin_seller_profiles
|
||||
UPDATE users
|
||||
SET contact_info = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, strings.TrimSpace(contact), time.Now().UnixMilli(), appctx.FromContext(ctx), userID)
|
||||
|
||||
@ -42,6 +42,7 @@ type UserProfileClient interface {
|
||||
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
|
||||
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error)
|
||||
UpdateUserProfileBackground(ctx context.Context, req *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error)
|
||||
UpdateUserContactInfo(ctx context.Context, req *userv1.UpdateUserContactInfoRequest) (*userv1.UpdateUserContactInfoResponse, error)
|
||||
ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(ctx context.Context, req *userv1.AdminChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
|
||||
CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error)
|
||||
@ -346,6 +347,10 @@ func (c *grpcUserProfileClient) UpdateUserProfileBackground(ctx context.Context,
|
||||
return c.client.UpdateUserProfileBackground(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserProfileClient) UpdateUserContactInfo(ctx context.Context, req *userv1.UpdateUserContactInfoRequest) (*userv1.UpdateUserContactInfoResponse, error) {
|
||||
return c.client.UpdateUserContactInfo(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserProfileClient) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||
return c.client.ChangeUserCountry(ctx, req)
|
||||
}
|
||||
|
||||
@ -101,6 +101,7 @@ type UserHandlers struct {
|
||||
CompleteMyOnboarding http.HandlerFunc
|
||||
UpdateMyProfile http.HandlerFunc
|
||||
UpdateMyProfileBackground http.HandlerFunc
|
||||
UpdateMyContactInfo http.HandlerFunc
|
||||
ChangeMyCountry http.HandlerFunc
|
||||
ChangeMyDisplayUserID http.HandlerFunc
|
||||
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||
@ -447,6 +448,7 @@ func (r routes) registerUserRoutes() {
|
||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||
r.profile("/users/me/profile-bg-img", http.MethodPost, h.UpdateMyProfileBackground)
|
||||
r.profile("/users/me/contact-info", http.MethodPost, h.UpdateMyContactInfo)
|
||||
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||
r.auth("/users/me/display-id/change", "", h.ChangeMyDisplayUserID)
|
||||
r.auth("/users/me/display-id/pretty/apply", "", h.ApplyMyPrettyDisplayUserID)
|
||||
|
||||
@ -321,6 +321,7 @@ type fakeUserProfileClient struct {
|
||||
lastComplete *userv1.CompleteOnboardingRequest
|
||||
lastUpdate *userv1.UpdateUserProfileRequest
|
||||
lastBackground *userv1.UpdateUserProfileBackgroundRequest
|
||||
lastContact *userv1.UpdateUserContactInfoRequest
|
||||
lastCountry *userv1.ChangeUserCountryRequest
|
||||
lastAdminCountry *userv1.AdminChangeUserCountryRequest
|
||||
lastCreateBlock *userv1.CreateManagerUserBlockRequest
|
||||
@ -942,6 +943,15 @@ func (f *fakeUserProfileClient) UpdateUserProfileBackground(_ context.Context, r
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProfileClient) UpdateUserContactInfo(_ context.Context, req *userv1.UpdateUserContactInfoRequest) (*userv1.UpdateUserContactInfoResponse, error) {
|
||||
f.lastContact = req
|
||||
return &userv1.UpdateUserContactInfoResponse{User: &userv1.User{
|
||||
UserId: req.GetUserId(),
|
||||
ContactInfo: req.GetContactInfo(),
|
||||
UpdatedAtMs: 2200,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProfileClient) ChangeUserCountry(_ context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||
f.lastCountry = req
|
||||
if f.countryErr != nil {
|
||||
@ -4675,6 +4685,7 @@ func TestListCoinSellersUsesAuthenticatedUserRegion(t *testing.T) {
|
||||
RegionName: "East Asia",
|
||||
Status: "active",
|
||||
MerchantAssetType: "COIN_SELLER_COIN",
|
||||
ContactInfo: "+65 1234 5678",
|
||||
UpdatedAtMs: 1700000001000,
|
||||
},
|
||||
}}}
|
||||
@ -4701,7 +4712,7 @@ func TestListCoinSellersUsesAuthenticatedUserRegion(t *testing.T) {
|
||||
data := response.Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
first := items[0].(map[string]any)
|
||||
if data["total"] != float64(1) || first["user_id"] != "801" || first["display_user_id"] != "900801" || first["username"] != "Seller One" || first["country_id"] != float64(86) || first["status"] != "active" {
|
||||
if data["total"] != float64(1) || first["user_id"] != "801" || first["display_user_id"] != "900801" || first["username"] != "Seller One" || first["country_id"] != float64(86) || first["status"] != "active" || first["whatsapp_phone"] != "+65 1234 5678" || first["whatsapp_url"] != "https://wa.me/6512345678" {
|
||||
t.Fatalf("coin seller list response mismatch: %+v", response)
|
||||
}
|
||||
}
|
||||
@ -6141,6 +6152,36 @@ func TestUpdateProfileBackgroundUsesAuthenticatedUserIDAndURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateContactInfoUsesAuthenticatedUserID(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient).Routes(auth.NewVerifier("secret"))
|
||||
body := []byte(`{"whatsapp_phone":"+65 1234 5678","user_id":99}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/contact-info", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-contact")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if profileClient.lastContact == nil || profileClient.lastContact.GetUserId() != 42 {
|
||||
t.Fatalf("authenticated user_id was not propagated to contact update: %+v", profileClient.lastContact)
|
||||
}
|
||||
if profileClient.lastContact.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-contact") || profileClient.lastContact.GetContactInfo() != "+65 1234 5678" {
|
||||
t.Fatalf("contact update request mismatch: %+v", profileClient.lastContact)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data, ok := response.Data.(map[string]any)
|
||||
if response.Code != httpkit.CodeOK || !ok || data["contact_info"] != "+65 1234 5678" || data["whatsapp_phone"] != "+65 1234 5678" {
|
||||
t.Fatalf("contact update response mismatch: %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMyProfileUsesAuthenticatedUserID(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{42: {
|
||||
UserId: 42,
|
||||
@ -8917,6 +8958,7 @@ func TestProfileMutationEndpointsRequireCompletedProfileToken(t *testing.T) {
|
||||
body string
|
||||
}{
|
||||
{name: "profile_update", path: "/api/v1/users/me/profile/update", body: `{"username":"hy"}`},
|
||||
{name: "contact_info", path: "/api/v1/users/me/contact-info", body: `{"contact_info":"+65"}`},
|
||||
{name: "country_change", path: "/api/v1/users/me/country/change", body: `{"country":"US"}`},
|
||||
}
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||
UpdateMyProfile: h.updateMyProfile,
|
||||
UpdateMyProfileBackground: h.updateMyProfileBackground,
|
||||
UpdateMyContactInfo: h.updateMyContactInfo,
|
||||
ChangeMyCountry: h.changeMyCountry,
|
||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||
|
||||
@ -63,6 +63,7 @@ type userProfileData struct {
|
||||
OnboardingStatus string `json:"onboarding_status"`
|
||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||
ProfileBgImg string `json:"profile_bg_img"`
|
||||
ContactInfo string `json:"contact_info,omitempty"`
|
||||
NextCountryChangeAtMs int64 `json:"next_country_change_allowed_at_ms,omitempty"`
|
||||
}
|
||||
|
||||
@ -409,6 +410,58 @@ func (h *Handler) updateMyProfileBackground(writer http.ResponseWriter, request
|
||||
})
|
||||
}
|
||||
|
||||
type contactInfoUpdateBody struct {
|
||||
ContactInfo *string `json:"contact_info"`
|
||||
ContactInfoCamel *string `json:"contactInfo"`
|
||||
WhatsappPhone *string `json:"whatsapp_phone"`
|
||||
WhatsappPhoneCamel *string `json:"whatsappPhone"`
|
||||
Contact *string `json:"contact"`
|
||||
}
|
||||
|
||||
// updateMyContactInfo 修改当前登录用户的用户级联系方式;多个经营身份页面共用这一个字段。
|
||||
func (h *Handler) updateMyContactInfo(writer http.ResponseWriter, request *http.Request) {
|
||||
var body contactInfoUpdateBody
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
if h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
contactInfo, ok := contactInfoFromBody(body)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "contact_info is required")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userProfileClient.UpdateUserContactInfo(request.Context(), &userv1.UpdateUserContactInfoRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
ContactInfo: contactInfo,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容 H5 WhatsApp 字段命名,同时返回 profile,方便页面刷新本地用户资料。
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"contact_info": resp.GetUser().GetContactInfo(),
|
||||
"whatsapp_phone": resp.GetUser().GetContactInfo(),
|
||||
"profile": profileData(resp.GetUser(), 0),
|
||||
})
|
||||
}
|
||||
|
||||
func contactInfoFromBody(body contactInfoUpdateBody) (string, bool) {
|
||||
// 按字段是否存在判断用户是否提交,允许空字符串清空联系方式。
|
||||
for _, value := range []*string{body.ContactInfo, body.ContactInfoCamel, body.WhatsappPhone, body.WhatsappPhoneCamel, body.Contact} {
|
||||
if value != nil {
|
||||
return strings.TrimSpace(*value), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// changeMyCountry 单独修改当前用户国家;冷却期和审计由 user-service 保证。
|
||||
func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) {
|
||||
var body struct {
|
||||
@ -644,6 +697,7 @@ func profileData(user *userv1.User, nextCountryChangeAtMs int64) userProfileData
|
||||
OnboardingStatus: user.GetOnboardingStatus(),
|
||||
UpdatedAtMs: user.GetUpdatedAtMs(),
|
||||
ProfileBgImg: user.GetProfileBgImg(),
|
||||
ContactInfo: user.GetContactInfo(),
|
||||
NextCountryChangeAtMs: nextCountryChangeAtMs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package walletapi
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
@ -23,6 +24,8 @@ type coinSellerData struct {
|
||||
RegionName string `json:"region_name,omitempty"`
|
||||
Status string `json:"status"`
|
||||
MerchantAssetType string `json:"merchant_asset_type"`
|
||||
WhatsappPhone string `json:"whatsapp_phone,omitempty"`
|
||||
WhatsappURL string `json:"whatsapp_url,omitempty"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
@ -54,6 +57,7 @@ func coinSellerFromProto(item *userv1.CoinSellerListItem) coinSellerData {
|
||||
if item == nil {
|
||||
return coinSellerData{}
|
||||
}
|
||||
contactInfo := strings.TrimSpace(item.GetContactInfo())
|
||||
return coinSellerData{
|
||||
UserID: int64IDString(item.GetUserId()),
|
||||
DisplayUserID: item.GetDisplayUserId(),
|
||||
@ -68,10 +72,36 @@ func coinSellerFromProto(item *userv1.CoinSellerListItem) coinSellerData {
|
||||
RegionName: item.GetRegionName(),
|
||||
Status: item.GetStatus(),
|
||||
MerchantAssetType: item.GetMerchantAssetType(),
|
||||
WhatsappPhone: contactInfo,
|
||||
WhatsappURL: coinSellerWhatsAppURL(contactInfo),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func coinSellerWhatsAppURL(raw string) string {
|
||||
contact := strings.TrimSpace(raw)
|
||||
if contact == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(contact)
|
||||
for _, prefix := range []string{"https://wa.me/", "http://wa.me/", "whatsapp://", "https://api.whatsapp.com/", "http://api.whatsapp.com/"} {
|
||||
if strings.HasPrefix(lower, prefix) {
|
||||
return contact
|
||||
}
|
||||
}
|
||||
|
||||
var digits strings.Builder
|
||||
for _, r := range contact {
|
||||
if r >= '0' && r <= '9' {
|
||||
digits.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if digits.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
return "https://wa.me/" + digits.String()
|
||||
}
|
||||
|
||||
func int64IDString(value int64) string {
|
||||
if value <= 0 {
|
||||
return ""
|
||||
|
||||
@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
os_version VARCHAR(64) NULL COMMENT 'os版本',
|
||||
avatar VARCHAR(512) NULL COMMENT '头像',
|
||||
profile_bg_img VARCHAR(512) NOT NULL DEFAULT '' COMMENT '个人主页信息卡背景图',
|
||||
contact_info VARCHAR(128) NOT NULL DEFAULT '' COMMENT '用户联系信息',
|
||||
birth_date DATE NULL COMMENT '出生日期',
|
||||
app_version VARCHAR(64) NULL COMMENT '应用版本',
|
||||
build_number VARCHAR(64) NULL COMMENT '构建编号',
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
SET @add_user_contact_info := (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE users ADD COLUMN contact_info VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''用户联系信息'' AFTER profile_bg_img',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME = 'contact_info'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @add_user_contact_info;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -235,6 +235,7 @@ type CoinSellerListItem struct {
|
||||
RegionName string
|
||||
Status string
|
||||
MerchantAssetType string
|
||||
ContactInfo string
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ const (
|
||||
ProfileUsernameMaxRunes = 64
|
||||
// ProfileAvatarMaxRunes 必须和 users.avatar VARCHAR(512) 保持一致。
|
||||
ProfileAvatarMaxRunes = 512
|
||||
// ProfileContactInfoMaxRunes 必须和 users.contact_info VARCHAR(128) 保持一致。
|
||||
ProfileContactInfoMaxRunes = 128
|
||||
// ProfileGenderMaxRunes 必须和 users.gender VARCHAR(32) 保持一致。
|
||||
ProfileGenderMaxRunes = 32
|
||||
)
|
||||
|
||||
@ -258,6 +258,8 @@ type User struct {
|
||||
Avatar string
|
||||
// ProfileBgImg 是用户主页信息卡背景图片 URL,属于用户主资料,不属于钱包装扮或 profile_card 资源。
|
||||
ProfileBgImg string
|
||||
// ContactInfo 是用户级联系方式,Host、BD、Agency owner 和币商等经营身份共同展示同一份用户主资料。
|
||||
ContactInfo string
|
||||
// BirthDate 使用 yyyy-mm-dd 字符串表达生日,避免时区把日期偏移成前后一天。
|
||||
BirthDate string
|
||||
// RegisterAppVersion 是注册客户端版本。
|
||||
@ -642,6 +644,18 @@ type ProfileBackgroundUpdateCommand struct {
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// ProfileContactUpdateCommand 描述用户级联系方式的专用修改。
|
||||
type ProfileContactUpdateCommand struct {
|
||||
// AppCode 是资料修改所属 App。
|
||||
AppCode string
|
||||
// UserID 是要修改联系方式的用户,只能来自当前登录态或后台已解析出的目标用户。
|
||||
UserID int64
|
||||
// ContactInfo 是用于后台和经营中心展示的联系方式;空字符串表示清空。
|
||||
ContactInfo string
|
||||
// UpdatedAtMs 是本次修改时间。
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// CompleteOnboardingCommand 描述注册页一次性补齐资料的事务输入。
|
||||
type CompleteOnboardingCommand struct {
|
||||
// AppCode 是注册资料固化所属 App。
|
||||
|
||||
@ -229,6 +229,11 @@ func (r *fakeModerationRepository) UpdateUserProfileBackground(_ context.Context
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) UpdateUserContactInfo(_ context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error) {
|
||||
r.user.ContactInfo = command.ContactInfo
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) CompleteOnboarding(context.Context, userdomain.CompleteOnboardingCommand) (userdomain.User, error) {
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
@ -112,6 +112,34 @@ func (s *Service) UpdateUserProfileBackground(ctx context.Context, userID int64,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUserContactInfo 修改用户级联系方式;Host、BD、Agency owner 和币商中心共用这一个字段展示。
|
||||
func (s *Service) UpdateUserContactInfo(ctx context.Context, userID int64, contactInfo string) (userdomain.User, error) {
|
||||
if userID <= 0 {
|
||||
// 联系方式只能改当前登录用户或后台已解析出的真实用户,非法 user_id 直接拒绝,避免落到空用户。
|
||||
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||||
}
|
||||
if s.userRepository == nil {
|
||||
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||||
}
|
||||
if err := s.requireCompletedProfile(ctx, userID); err != nil {
|
||||
// 未完成注册资料的账号不能先写经营联系方式,否则后台会看到一个仍不可进入业务的半成品用户。
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
|
||||
contactInfo = strings.TrimSpace(contactInfo)
|
||||
if userdomain.ProfileStringTooLong(contactInfo, userdomain.ProfileContactInfoMaxRunes) {
|
||||
// users.contact_info 是短展示字段;提前拦截超长输入,避免 MySQL 截断造成 H5 显示值和用户输入不一致。
|
||||
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "contact_info is too long")
|
||||
}
|
||||
|
||||
return s.userRepository.UpdateUserContactInfo(ctx, userdomain.ProfileContactUpdateCommand{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
UserID: userID,
|
||||
ContactInfo: contactInfo,
|
||||
UpdatedAtMs: s.now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
// CompleteOnboarding 是注册页唯一提交入口。
|
||||
// 它必须一次性校验并写入 username/avatar/gender/country/region_id/profile_completed,避免客户端拆接口拼出半完成状态。
|
||||
func (s *Service) CompleteOnboarding(ctx context.Context, userID int64, username string, avatar string, gender string, country string, inviteCode string, requestID string) (userdomain.User, error) {
|
||||
|
||||
@ -163,3 +163,35 @@ func TestUpdateUserProfileBackgroundRejectsInvalidProfileBgImg(t *testing.T) {
|
||||
t.Fatalf("invalid profile_bg_img must be rejected, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserContactInfoPersistsUserContact(t *testing.T) {
|
||||
repository := &fakeModerationRepository{user: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 10001,
|
||||
ProfileCompleted: true,
|
||||
}}
|
||||
svc := New(repository)
|
||||
|
||||
updated, err := svc.UpdateUserContactInfo(appcode.WithContext(context.Background(), "lalu"), 10001, " +65 1234 5678 ")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateUserContactInfo failed: %v", err)
|
||||
}
|
||||
|
||||
if updated.ContactInfo != "+65 1234 5678" {
|
||||
t.Fatalf("updated contact mismatch: %+v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserContactInfoRejectsTooLong(t *testing.T) {
|
||||
repository := &fakeModerationRepository{user: userdomain.User{
|
||||
AppCode: "lalu",
|
||||
UserID: 10001,
|
||||
ProfileCompleted: true,
|
||||
}}
|
||||
svc := New(repository)
|
||||
|
||||
_, err := svc.UpdateUserContactInfo(appcode.WithContext(context.Background(), "lalu"), 10001, strings.Repeat("1", userdomain.ProfileContactInfoMaxRunes+1))
|
||||
if xerr.CodeOf(err) != xerr.InvalidArgument {
|
||||
t.Fatalf("too long contact_info must be rejected, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +55,8 @@ type UserRepository interface {
|
||||
UpdateUserProfile(ctx context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error)
|
||||
// UpdateUserProfileBackground 原子修改个人信息页背景图。
|
||||
UpdateUserProfileBackground(ctx context.Context, command userdomain.ProfileBackgroundUpdateCommand) (userdomain.User, error)
|
||||
// UpdateUserContactInfo 原子修改用户级联系方式。
|
||||
UpdateUserContactInfo(ctx context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error)
|
||||
// CompleteOnboarding 原子写注册页必填资料并标记 profile_completed。
|
||||
CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error)
|
||||
// ChangeUserCountry 原子修改用户国家、写变更日志,并返回需要立即失效的 auth session。
|
||||
|
||||
@ -183,6 +183,7 @@ func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID
|
||||
COALESCE(region.name, ''),
|
||||
seller.status,
|
||||
seller.merchant_asset_type,
|
||||
COALESCE(NULLIF(u.contact_info, ''), seller.contact_info, ''),
|
||||
seller.updated_at_ms
|
||||
FROM users u
|
||||
INNER JOIN coin_seller_profiles seller
|
||||
@ -220,6 +221,7 @@ func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID
|
||||
&item.RegionName,
|
||||
&item.Status,
|
||||
&item.MerchantAssetType,
|
||||
&item.ContactInfo,
|
||||
&item.UpdatedAtMs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -106,6 +106,7 @@ const userSelectColumns = `
|
||||
COALESCE(os_version, ''),
|
||||
COALESCE(avatar, ''),
|
||||
COALESCE(profile_bg_img, ''),
|
||||
COALESCE(contact_info, ''),
|
||||
COALESCE(DATE_FORMAT(birth_date, '%Y-%m-%d'), ''),
|
||||
COALESCE(app_version, ''),
|
||||
COALESCE(build_number, ''),
|
||||
@ -173,6 +174,7 @@ func ScanUser(scanner interface {
|
||||
&user.RegisterOSVersion,
|
||||
&user.Avatar,
|
||||
&user.ProfileBgImg,
|
||||
&user.ContactInfo,
|
||||
&user.BirthDate,
|
||||
&user.RegisterAppVersion,
|
||||
&user.RegisterBuildNumber,
|
||||
@ -246,6 +248,7 @@ func InsertUserIdentity(ctx context.Context, tx *sql.Tx, user userdomain.User, i
|
||||
os_version,
|
||||
avatar,
|
||||
profile_bg_img,
|
||||
contact_info,
|
||||
birth_date,
|
||||
app_version,
|
||||
build_number,
|
||||
@ -264,10 +267,10 @@ func InsertUserIdentity(ctx context.Context, tx *sql.Tx, user userdomain.User, i
|
||||
)
|
||||
VALUES (
|
||||
?, ?, ?, ?, ?, NULL,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`, appcode.Normalize(user.AppCode), user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), shared.NullableString(user.Username), shared.NullableString(user.Gender), shared.NullableString(user.Country), shared.NullableRegionID(user.RegionID), shared.NullableString(user.InviteCode), shared.NullableString(user.RegisterIP), shared.NullableString(user.RegisterUserAgent), shared.NullableString(user.CountryByIP), shared.NullableString(user.RegisterDeviceID), shared.NullableString(user.RegisterDevice), shared.NullableString(user.RegisterOSVersion), shared.NullableString(user.Avatar), profileBgImg, shared.NullableString(user.BirthDate), shared.NullableString(user.RegisterAppVersion), shared.NullableString(user.RegisterBuildNumber), shared.NullableString(user.RegisterSource), shared.NullableString(user.RegisterInstallChannel), shared.NullableString(user.RegisterCampaign), shared.NullableString(user.RegisterPlatform), shared.NullableString(user.Language), shared.NullableString(user.Timezone), user.ProfileCompleted, user.ProfileCompletedAtMs, onboardingStatusForInsert(user), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs)
|
||||
`, appcode.Normalize(user.AppCode), user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), shared.NullableString(user.Username), shared.NullableString(user.Gender), shared.NullableString(user.Country), shared.NullableRegionID(user.RegionID), shared.NullableString(user.InviteCode), shared.NullableString(user.RegisterIP), shared.NullableString(user.RegisterUserAgent), shared.NullableString(user.CountryByIP), shared.NullableString(user.RegisterDeviceID), shared.NullableString(user.RegisterDevice), shared.NullableString(user.RegisterOSVersion), shared.NullableString(user.Avatar), profileBgImg, strings.TrimSpace(user.ContactInfo), shared.NullableString(user.BirthDate), shared.NullableString(user.RegisterAppVersion), shared.NullableString(user.RegisterBuildNumber), shared.NullableString(user.RegisterSource), shared.NullableString(user.RegisterInstallChannel), shared.NullableString(user.RegisterCampaign), shared.NullableString(user.RegisterPlatform), shared.NullableString(user.Language), shared.NullableString(user.Timezone), user.ProfileCompleted, user.ProfileCompletedAtMs, onboardingStatusForInsert(user), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -335,6 +335,40 @@ func (r *Repository) UpdateUserProfileBackground(ctx context.Context, command us
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUserContactInfo 只修改用户级联系方式,避免 Host/BD/Agency/币商各自身份表保存多份互相漂移的数据。
|
||||
func (r *Repository) UpdateUserContactInfo(ctx context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error) {
|
||||
// 联系方式展示在多个经营身份入口,写库时锁定 users 行,确保返回快照和后续列表读取都来自同一主记录。
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
user, err := QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
return userdomain.User{}, xerr.New(xerr.NotFound, "user not found")
|
||||
}
|
||||
if err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
|
||||
user.ContactInfo = strings.TrimSpace(command.ContactInfo)
|
||||
user.UpdatedAtMs = command.UpdatedAtMs
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE users
|
||||
SET contact_info = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, user.ContactInfo, user.UpdatedAtMs, appcode.Normalize(command.AppCode), command.UserID)
|
||||
if err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return userdomain.User{}, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CompleteOnboarding 是注册页资料固化的事务入口。
|
||||
// 该方法不写 user_country_change_logs,确保首次国家选择不消耗改国家冷却窗口。
|
||||
|
||||
|
||||
@ -149,6 +149,11 @@ func (r *Repository) UpdateUserProfileBackground(ctx context.Context, command us
|
||||
return r.Repository.UserRepository().UpdateUserProfileBackground(ctx, command)
|
||||
}
|
||||
|
||||
// UpdateUserContactInfo 让测试 wrapper 继续满足业务层用户级联系方式更新接口。
|
||||
func (r *Repository) UpdateUserContactInfo(ctx context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error) {
|
||||
return r.Repository.UserRepository().UpdateUserContactInfo(ctx, command)
|
||||
}
|
||||
|
||||
// CompleteOnboarding 让测试 wrapper 继续满足业务层注册资料接口。
|
||||
func (r *Repository) CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error) {
|
||||
return r.Repository.UserRepository().CompleteOnboarding(ctx, command)
|
||||
@ -463,11 +468,11 @@ func (r *Repository) PutUser(user userdomain.User) {
|
||||
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
|
||||
current_display_user_id_expires_at_ms, username, gender, country, region_id, invite_code,
|
||||
register_ip, register_user_agent, country_by_ip, register_device_id, register_device,
|
||||
os_version, avatar, profile_bg_img, birth_date, app_version, build_number, source, install_channel,
|
||||
os_version, avatar, profile_bg_img, contact_info, birth_date, app_version, build_number, source, install_channel,
|
||||
campaign, platform, language, timezone, profile_completed, profile_completed_at_ms,
|
||||
onboarding_status, status, created_at_ms, updated_at_ms
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
app_code = VALUES(app_code),
|
||||
default_display_user_id = VALUES(default_display_user_id),
|
||||
@ -487,6 +492,7 @@ func (r *Repository) PutUser(user userdomain.User) {
|
||||
os_version = VALUES(os_version),
|
||||
avatar = VALUES(avatar),
|
||||
profile_bg_img = VALUES(profile_bg_img),
|
||||
contact_info = VALUES(contact_info),
|
||||
birth_date = VALUES(birth_date),
|
||||
app_version = VALUES(app_version),
|
||||
build_number = VALUES(build_number),
|
||||
@ -501,7 +507,7 @@ func (r *Repository) PutUser(user userdomain.User) {
|
||||
onboarding_status = VALUES(onboarding_status),
|
||||
status = VALUES(status),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, user.AppCode, user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), nullableInt64(user.CurrentDisplayUserIDExpiresAtMs), nullableString(user.Username), nullableString(user.Gender), nullableString(user.Country), nullableInt64(user.RegionID), nullableString(user.InviteCode), nullableString(user.RegisterIP), nullableString(user.RegisterUserAgent), nullableString(user.CountryByIP), nullableString(user.RegisterDeviceID), nullableString(user.RegisterDevice), nullableString(user.RegisterOSVersion), nullableString(user.Avatar), nullableString(user.ProfileBgImg), nullableString(user.BirthDate), nullableString(user.RegisterAppVersion), nullableString(user.RegisterBuildNumber), nullableString(user.RegisterSource), nullableString(user.RegisterInstallChannel), nullableString(user.RegisterCampaign), nullableString(user.RegisterPlatform), nullableString(user.Language), nullableString(user.Timezone), user.ProfileCompleted, user.ProfileCompletedAtMs, string(user.OnboardingStatus), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs)
|
||||
`, user.AppCode, user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), nullableInt64(user.CurrentDisplayUserIDExpiresAtMs), nullableString(user.Username), nullableString(user.Gender), nullableString(user.Country), nullableInt64(user.RegionID), nullableString(user.InviteCode), nullableString(user.RegisterIP), nullableString(user.RegisterUserAgent), nullableString(user.CountryByIP), nullableString(user.RegisterDeviceID), nullableString(user.RegisterDevice), nullableString(user.RegisterOSVersion), nullableString(user.Avatar), nullableString(user.ProfileBgImg), strings.TrimSpace(user.ContactInfo), nullableString(user.BirthDate), nullableString(user.RegisterAppVersion), nullableString(user.RegisterBuildNumber), nullableString(user.RegisterSource), nullableString(user.RegisterInstallChannel), nullableString(user.RegisterCampaign), nullableString(user.RegisterPlatform), nullableString(user.Language), nullableString(user.Timezone), user.ProfileCompleted, user.ProfileCompletedAtMs, string(user.OnboardingStatus), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs)
|
||||
if err != nil {
|
||||
r.t.Fatalf("seed user %d failed: %v", user.UserID, err)
|
||||
}
|
||||
|
||||
@ -73,6 +73,7 @@ func toProtoUser(user userdomain.User) *userv1.User {
|
||||
Country: user.Country,
|
||||
Avatar: user.Avatar,
|
||||
ProfileBgImg: user.ProfileBgImg,
|
||||
ContactInfo: user.ContactInfo,
|
||||
Birth: user.BirthDate,
|
||||
Language: user.Language,
|
||||
CountryId: user.CountryID,
|
||||
@ -489,6 +490,7 @@ func toProtoCoinSellerListItem(item hostdomain.CoinSellerListItem) *userv1.CoinS
|
||||
RegionName: item.RegionName,
|
||||
Status: item.Status,
|
||||
MerchantAssetType: item.MerchantAssetType,
|
||||
ContactInfo: item.ContactInfo,
|
||||
UpdatedAtMs: item.UpdatedAtMs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -672,6 +672,18 @@ func (s *Server) UpdateUserProfileBackground(ctx context.Context, req *userv1.Up
|
||||
return &userv1.UpdateUserProfileBackgroundResponse{User: toProtoUser(user)}, nil
|
||||
}
|
||||
|
||||
// UpdateUserContactInfo 修改当前用户级联系方式。
|
||||
func (s *Server) UpdateUserContactInfo(ctx context.Context, req *userv1.UpdateUserContactInfoRequest) (*userv1.UpdateUserContactInfoResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
// 联系方式是用户主资料字段,不落到具体经营身份表,避免同一用户多身份时展示不一致。
|
||||
user, err := s.userSvc.UpdateUserContactInfo(ctx, req.GetUserId(), req.GetContactInfo())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
return &userv1.UpdateUserContactInfoResponse{User: toProtoUser(user)}, nil
|
||||
}
|
||||
|
||||
// ChangeUserCountry 是 App 自助改国家入口;受限经营身份会被 service 层按当前角色摘要拦截。
|
||||
func (s *Server) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user