feat: 新增昵称增加好友和注册用户昵称时的昵称判断
This commit is contained in:
parent
55104d5aef
commit
f426b84c0a
@ -36,14 +36,14 @@ func NewSocialController(dubboClient *client.Client) (*SocialController, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendFriendRequest 发送好友请求
|
||||
// SendFriendRequest 发送好友请求(支持按昵称搜索)
|
||||
// @Summary 发送好友请求
|
||||
// @Description 向指定用户发送好友请求
|
||||
// @Description 向指定用户发送好友请求,或按昵称搜索匹配用户
|
||||
// @Tags social
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body object{friend_user_id=integer,message=string} true "好友请求参数"
|
||||
// @Param request body object{friend_user_id=integer,message=string,nickname=string,search_mode=boolean} true "好友请求参数"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /api/v1/social/friend-requests [post]
|
||||
func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||
@ -62,8 +62,10 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||
|
||||
// 解析请求参数
|
||||
var req struct {
|
||||
FriendUserID int64 `json:"friend_user_id" binding:"required"`
|
||||
FriendUserID int64 `json:"friend_user_id"`
|
||||
Message string `json:"message"`
|
||||
Nickname string `json:"nickname"`
|
||||
SearchMode bool `json:"search_mode"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@ -71,6 +73,16 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数:search_mode=true 时需要 nickname,否则需要 friend_user_id
|
||||
if req.SearchMode && req.Nickname == "" {
|
||||
response.Error(c, http.StatusBadRequest, "搜索模式下昵称不能为空")
|
||||
return
|
||||
}
|
||||
if !req.SearchMode && req.FriendUserID == 0 && req.Nickname == "" {
|
||||
response.Error(c, http.StatusBadRequest, "friend_user_id 或 nickname 不能同时为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置上下文和 Dubbo attachments
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@ -81,15 +93,20 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||
})
|
||||
|
||||
// 调用 RPC
|
||||
resp, err := ctrl.socialService.SendFriendRequest(ctx, &pbSocial.SendFriendRequestRequest{
|
||||
rpcReq := &pbSocial.SendFriendRequestRequest{
|
||||
FriendUserId: req.FriendUserID,
|
||||
Message: req.Message,
|
||||
})
|
||||
Nickname: req.Nickname,
|
||||
SearchMode: req.SearchMode,
|
||||
}
|
||||
resp, err := ctrl.socialService.SendFriendRequest(ctx, rpcReq)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.Error("SendFriendRequest RPC failed",
|
||||
zap.Int64("user_id", userID.(int64)),
|
||||
zap.Int64("friend_user_id", req.FriendUserID),
|
||||
zap.String("nickname", req.Nickname),
|
||||
zap.Bool("search_mode", req.SearchMode),
|
||||
zap.Error(err),
|
||||
)
|
||||
response.Error(c, http.StatusInternalServerError, "服务调用失败")
|
||||
@ -102,6 +119,28 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是搜索模式,返回匹配用户列表
|
||||
if req.SearchMode {
|
||||
users := make([]map[string]interface{}, 0, len(resp.MatchedUsers))
|
||||
for _, user := range resp.MatchedUsers {
|
||||
users = append(users, map[string]interface{}{
|
||||
"fan_profile_id": user.FanProfileId,
|
||||
"user_id": user.UserId,
|
||||
"nickname": user.Nickname,
|
||||
"avatar": user.Avatar,
|
||||
"fan_level": user.FanLevel,
|
||||
"relationship_status": user.RelationshipStatus,
|
||||
"can_send_request": user.CanSendRequest,
|
||||
"cooldown_ends_at": user.CooldownEndsAt,
|
||||
})
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"users": users,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 正常发送请求模式
|
||||
response.Success(c, gin.H{
|
||||
"request_id": resp.RequestId,
|
||||
"status": resp.Status,
|
||||
|
||||
151
backend/pkg/filter/sensitive_filter.go
Normal file
151
backend/pkg/filter/sensitive_filter.go
Normal file
@ -0,0 +1,151 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SensitiveFilter 敏感词过滤器(DFA 算法)
|
||||
type SensitiveFilter struct {
|
||||
mu sync.RWMutex
|
||||
root *node
|
||||
pattern map[string]bool // 存储完整敏感词用于快速查找
|
||||
}
|
||||
|
||||
type node struct {
|
||||
children map[rune]*node
|
||||
isEnd bool
|
||||
}
|
||||
|
||||
var (
|
||||
defaultFilter *SensitiveFilter
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// NewSensitiveFilter 创建新的敏感词过滤器
|
||||
func NewSensitiveFilter() *SensitiveFilter {
|
||||
return &SensitiveFilter{
|
||||
root: &node{children: make(map[rune]*node)},
|
||||
pattern: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefault 获取全局默认过滤器(单例模式)
|
||||
func GetDefault() *SensitiveFilter {
|
||||
once.Do(func() {
|
||||
defaultFilter = NewSensitiveFilter()
|
||||
// 加载默认敏感词列表
|
||||
defaultFilter.LoadWordList(defaultWordList)
|
||||
})
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
// LoadWordList 加载敏感词列表
|
||||
func (f *SensitiveFilter) LoadWordList(words []string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
for _, word := range words {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
f.pattern[word] = true
|
||||
f.insert(word)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// insert 将词语插入到 DFA 树中
|
||||
func (f *SensitiveFilter) insert(word string) {
|
||||
current := f.root
|
||||
for _, char := range word {
|
||||
if _, ok := current.children[char]; !ok {
|
||||
current.children[char] = &node{children: make(map[rune]*node)}
|
||||
}
|
||||
current = current.children[char]
|
||||
}
|
||||
current.isEnd = true
|
||||
}
|
||||
|
||||
// Contains 检测文本中是否包含敏感词
|
||||
func (f *SensitiveFilter) Contains(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
return f.search(text)
|
||||
}
|
||||
|
||||
// search 在文本中搜索敏感词
|
||||
func (f *SensitiveFilter) search(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
current := f.root
|
||||
j := i
|
||||
for j < len(text) {
|
||||
char := rune(text[j])
|
||||
if child, ok := current.children[char]; ok {
|
||||
current = child
|
||||
if current.isEnd {
|
||||
return true
|
||||
}
|
||||
j++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FindAll 查找文本中所有敏感词
|
||||
func (f *SensitiveFilter) FindAll(text string) []string {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
var results []string
|
||||
for i := 0; i < len(text); i++ {
|
||||
current := f.root
|
||||
j := i
|
||||
word := ""
|
||||
for j < len(text) {
|
||||
char := rune(text[j])
|
||||
if child, ok := current.children[char]; ok {
|
||||
current = child
|
||||
word += string(char)
|
||||
if current.isEnd {
|
||||
results = append(results, word)
|
||||
}
|
||||
j++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// 默认敏感词列表(初期使用基础敏感词,后续可扩展为从数据库或配置加载)
|
||||
var defaultWordList = []string{
|
||||
// 政治敏感词(示例,实际需根据需求配置)
|
||||
"台独",
|
||||
"藏独",
|
||||
"疆独",
|
||||
"分裂",
|
||||
"颠覆",
|
||||
// 色情低俗(示例)
|
||||
"色情",
|
||||
"黄色",
|
||||
"赌博",
|
||||
"诈骗",
|
||||
// 其他违禁词(示例)
|
||||
"暴力",
|
||||
"恐怖",
|
||||
"毒品",
|
||||
"枪支",
|
||||
}
|
||||
@ -320,6 +320,8 @@ type SendFriendRequestRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FriendUserId int64 `protobuf:"varint,1,opt,name=friend_user_id,json=friendUserId,proto3" json:"friend_user_id,omitempty"` // 好友用户ID(要添加的用户ID)
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 请求附带消息(可选)
|
||||
Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"` // 昵称(用于按昵称搜索,与 friend_user_id 二选一)
|
||||
SearchMode bool `protobuf:"varint,4,opt,name=search_mode,json=searchMode,proto3" json:"search_mode,omitempty"` // 搜索模式(true=仅搜索返回匹配用户,false=正常发送请求)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -368,13 +370,28 @@ func (x *SendFriendRequestRequest) GetMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SendFriendRequestRequest) GetNickname() string {
|
||||
if x != nil {
|
||||
return x.Nickname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SendFriendRequestRequest) GetSearchMode() bool {
|
||||
if x != nil {
|
||||
return x.SearchMode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type SendFriendRequestResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
RequestId int64 `protobuf:"varint,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // 创建的请求ID
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // 请求状态
|
||||
CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // 创建时间
|
||||
ExpiresAt int64 `protobuf:"varint,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // 过期时间
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
RequestId int64 `protobuf:"varint,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // 创建的请求ID
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // 请求状态
|
||||
CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // 创建时间
|
||||
ExpiresAt int64 `protobuf:"varint,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // 过期时间
|
||||
MatchedUsers []*FanProfileSearchResult `protobuf:"bytes,6,rep,name=matched_users,json=matchedUsers,proto3" json:"matched_users,omitempty"` // 匹配的用户列表(仅 search_mode=true 时返回)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -444,6 +461,13 @@ func (x *SendFriendRequestResponse) GetExpiresAt() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SendFriendRequestResponse) GetMatchedUsers() []*FanProfileSearchResult {
|
||||
if x != nil {
|
||||
return x.MatchedUsers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取好友请求列表
|
||||
type GetFriendRequestsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -2165,10 +2189,13 @@ const file_social_proto_rawDesc = "" +
|
||||
"\rfriend_avatar\x18\n" +
|
||||
" \x01(\tR\ffriendAvatar\x12(\n" +
|
||||
"\x10friend_fan_level\x18\v \x01(\x05R\x0efriendFanLevel\x12#\n" +
|
||||
"\rfriend_social\x18\f \x01(\x05R\ffriendSocial\"Z\n" +
|
||||
"\rfriend_social\x18\f \x01(\x05R\ffriendSocial\"\x97\x01\n" +
|
||||
"\x18SendFriendRequestRequest\x12$\n" +
|
||||
"\x0efriend_user_id\x18\x01 \x01(\x03R\ffriendUserId\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\"\xc2\x01\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\x1a\n" +
|
||||
"\bnickname\x18\x03 \x01(\tR\bnickname\x12\x1f\n" +
|
||||
"\vsearch_mode\x18\x04 \x01(\bR\n" +
|
||||
"searchMode\"\x8f\x02\n" +
|
||||
"\x19SendFriendRequestResponse\x120\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
|
||||
"\n" +
|
||||
@ -2177,7 +2204,8 @@ const file_social_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" +
|
||||
"\n" +
|
||||
"expires_at\x18\x05 \x01(\x03R\texpiresAt\"w\n" +
|
||||
"expires_at\x18\x05 \x01(\x03R\texpiresAt\x12K\n" +
|
||||
"\rmatched_users\x18\x06 \x03(\v2&.topfans.social.FanProfileSearchResultR\fmatchedUsers\"w\n" +
|
||||
"\x18GetFriendRequestsRequest\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" +
|
||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" +
|
||||
@ -2353,57 +2381,58 @@ var file_social_proto_goTypes = []any{
|
||||
}
|
||||
var file_social_proto_depIdxs = []int32{
|
||||
33, // 0: topfans.social.SendFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 1: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse
|
||||
0, // 2: topfans.social.GetFriendRequestsResponse.items:type_name -> topfans.social.FriendRequest
|
||||
33, // 3: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 4: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse
|
||||
1, // 5: topfans.social.GetFriendListResponse.items:type_name -> topfans.social.Friendship
|
||||
33, // 6: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 7: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 8: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 9: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 10: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||
20, // 11: topfans.social.SearchUserForFriendResponse.user:type_name -> topfans.social.FanProfileSearchResult
|
||||
33, // 12: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse
|
||||
21, // 13: topfans.social.GetRandomUsersResponse.users:type_name -> topfans.social.RandomUser
|
||||
33, // 14: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse
|
||||
24, // 15: topfans.social.GetUsersPagedResponse.users:type_name -> topfans.social.PagedUser
|
||||
33, // 16: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 17: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 18: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
|
||||
2, // 19: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest
|
||||
4, // 20: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest
|
||||
6, // 21: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest
|
||||
8, // 22: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest
|
||||
10, // 23: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest
|
||||
12, // 24: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest
|
||||
14, // 25: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest
|
||||
16, // 26: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest
|
||||
18, // 27: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest
|
||||
22, // 28: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest
|
||||
25, // 29: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest
|
||||
27, // 30: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest
|
||||
29, // 31: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest
|
||||
31, // 32: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest
|
||||
3, // 33: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse
|
||||
5, // 34: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse
|
||||
7, // 35: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse
|
||||
9, // 36: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse
|
||||
11, // 37: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse
|
||||
13, // 38: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse
|
||||
15, // 39: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse
|
||||
17, // 40: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse
|
||||
19, // 41: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse
|
||||
23, // 42: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse
|
||||
26, // 43: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse
|
||||
28, // 44: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse
|
||||
30, // 45: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse
|
||||
32, // 46: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse
|
||||
33, // [33:47] is the sub-list for method output_type
|
||||
19, // [19:33] is the sub-list for method input_type
|
||||
19, // [19:19] is the sub-list for extension type_name
|
||||
19, // [19:19] is the sub-list for extension extendee
|
||||
0, // [0:19] is the sub-list for field type_name
|
||||
20, // 1: topfans.social.SendFriendRequestResponse.matched_users:type_name -> topfans.social.FanProfileSearchResult
|
||||
33, // 2: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse
|
||||
0, // 3: topfans.social.GetFriendRequestsResponse.items:type_name -> topfans.social.FriendRequest
|
||||
33, // 4: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 5: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse
|
||||
1, // 6: topfans.social.GetFriendListResponse.items:type_name -> topfans.social.Friendship
|
||||
33, // 7: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 8: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 9: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 10: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 11: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||
20, // 12: topfans.social.SearchUserForFriendResponse.user:type_name -> topfans.social.FanProfileSearchResult
|
||||
33, // 13: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse
|
||||
21, // 14: topfans.social.GetRandomUsersResponse.users:type_name -> topfans.social.RandomUser
|
||||
33, // 15: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse
|
||||
24, // 16: topfans.social.GetUsersPagedResponse.users:type_name -> topfans.social.PagedUser
|
||||
33, // 17: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 18: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||
33, // 19: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
|
||||
2, // 20: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest
|
||||
4, // 21: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest
|
||||
6, // 22: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest
|
||||
8, // 23: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest
|
||||
10, // 24: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest
|
||||
12, // 25: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest
|
||||
14, // 26: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest
|
||||
16, // 27: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest
|
||||
18, // 28: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest
|
||||
22, // 29: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest
|
||||
25, // 30: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest
|
||||
27, // 31: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest
|
||||
29, // 32: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest
|
||||
31, // 33: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest
|
||||
3, // 34: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse
|
||||
5, // 35: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse
|
||||
7, // 36: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse
|
||||
9, // 37: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse
|
||||
11, // 38: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse
|
||||
13, // 39: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse
|
||||
15, // 40: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse
|
||||
17, // 41: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse
|
||||
19, // 42: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse
|
||||
23, // 43: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse
|
||||
26, // 44: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse
|
||||
28, // 45: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse
|
||||
30, // 46: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse
|
||||
32, // 47: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse
|
||||
34, // [34:48] is the sub-list for method output_type
|
||||
20, // [20:34] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_social_proto_init() }
|
||||
|
||||
@ -3,6 +3,8 @@ package validator
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/topfans/backend/pkg/filter"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -11,14 +13,16 @@ const (
|
||||
// MaxPasswordLength 最大密码长度
|
||||
MaxPasswordLength = 50
|
||||
// MinNicknameLength 最小昵称长度
|
||||
MinNicknameLength = 1
|
||||
MinNicknameLength = 2
|
||||
// MaxNicknameLength 最大昵称长度
|
||||
MaxNicknameLength = 50
|
||||
MaxNicknameLength = 20
|
||||
)
|
||||
|
||||
var (
|
||||
// 中国手机号正则表达式
|
||||
mobileRegex = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
||||
// 昵称正则:中文、英文、数字、下划线,首字符不能是数字或下划线
|
||||
nicknameRegex = regexp.MustCompile(`^[一-龥a-zA-Z][一-龥a-zA-Z0-9_]*$`)
|
||||
)
|
||||
|
||||
// ValidateMobile 验证手机号格式
|
||||
@ -53,14 +57,26 @@ func ValidateNickname(nickname string) (bool, string) {
|
||||
}
|
||||
|
||||
nickname = strings.TrimSpace(nickname)
|
||||
if len(nickname) < MinNicknameLength {
|
||||
// 使用 rune 计数,支持中文等多字节字符
|
||||
runeCount := len([]rune(nickname))
|
||||
if runeCount < MinNicknameLength {
|
||||
return false, "nickname too short"
|
||||
}
|
||||
|
||||
if len(nickname) > MaxNicknameLength {
|
||||
if runeCount > MaxNicknameLength {
|
||||
return false, "nickname too long"
|
||||
}
|
||||
|
||||
// 格式校验:中文、英文、数字、下划线,首字符不能是数字或下划线
|
||||
if !nicknameRegex.MatchString(nickname) {
|
||||
return false, "invalid nickname format"
|
||||
}
|
||||
|
||||
// 敏感词校验
|
||||
if filter.GetDefault().Contains(nickname) {
|
||||
return false, "nickname contains sensitive words"
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,8 @@ message Friendship {
|
||||
message SendFriendRequestRequest {
|
||||
int64 friend_user_id = 1; // 好友用户ID(要添加的用户ID)
|
||||
string message = 2; // 请求附带消息(可选)
|
||||
string nickname = 3; // 昵称(用于按昵称搜索,与 friend_user_id 二选一)
|
||||
bool search_mode = 4; // 搜索模式(true=仅搜索返回匹配用户,false=正常发送请求)
|
||||
}
|
||||
|
||||
message SendFriendRequestResponse {
|
||||
@ -62,6 +64,7 @@ message SendFriendRequestResponse {
|
||||
string status = 3; // 请求状态
|
||||
int64 created_at = 4; // 创建时间
|
||||
int64 expires_at = 5; // 过期时间
|
||||
repeated FanProfileSearchResult matched_users = 6; // 匹配的用户列表(仅 search_mode=true 时返回)
|
||||
}
|
||||
|
||||
// 获取好友请求列表
|
||||
|
||||
@ -95,6 +95,12 @@ type SocialRepository interface {
|
||||
|
||||
// GetFanProfileByUserIDAndStarID 根据userID和starID获取粉丝档案
|
||||
GetFanProfileByUserIDAndStarID(userID, starID int64) (*models.FanProfile, error)
|
||||
|
||||
// GetFanProfilesByNickname 根据昵称模糊搜索粉丝档案(同一明星下)
|
||||
// starID: 明星ID
|
||||
// nickname: 昵称关键词
|
||||
// limit: 返回数量限制
|
||||
GetFanProfilesByNickname(starID int64, nickname string, limit int) ([]*models.FanProfile, error)
|
||||
}
|
||||
|
||||
// RandomUserInfo 随机用户信息
|
||||
@ -498,3 +504,25 @@ func (r *socialRepositoryImpl) GetFanProfileByUserIDAndStarID(userID, starID int
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
// GetFanProfilesByNickname 根据昵称模糊搜索粉丝档案(同一明星下)
|
||||
func (r *socialRepositoryImpl) GetFanProfilesByNickname(starID int64, nickname string, limit int) ([]*models.FanProfile, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10 // 默认返回10条
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50 // 最大限制50条
|
||||
}
|
||||
|
||||
var profiles []*models.FanProfile
|
||||
err := r.db.Where("star_id = ? AND is_active = ? AND nickname LIKE ?", starID, true, "%"+nickname+"%").
|
||||
Order("level DESC, updated_at DESC").
|
||||
Limit(limit).
|
||||
Find(&profiles).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
@ -122,18 +122,36 @@
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<view class="card-content add-friend-card-content">
|
||||
<!-- 搜索模式切换 -->
|
||||
<view class="search-mode-switch">
|
||||
<view
|
||||
class="mode-btn"
|
||||
:class="{ active: searchMode === 'uid' }"
|
||||
@click="switchSearchMode('uid')"
|
||||
>
|
||||
<text class="mode-btn-text">UID搜索</text>
|
||||
</view>
|
||||
<view
|
||||
class="mode-btn"
|
||||
:class="{ active: searchMode === 'nickname' }"
|
||||
@click="switchSearchMode('nickname')"
|
||||
>
|
||||
<text class="mode-btn-text">昵称搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-wrapper">
|
||||
<image class="search-icon" src="/static/icon/search.png" mode="aspectFit"></image>
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchUid"
|
||||
placeholder="输入uid进行用户搜索"
|
||||
type="number"
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="searchUid"
|
||||
:placeholder="searchMode === 'uid' ? '输入uid进行用户搜索' : '输入昵称进行用户搜索'"
|
||||
:type="searchMode === 'uid' ? 'number' : 'text'"
|
||||
/>
|
||||
<view
|
||||
v-show="searchUid"
|
||||
class="clear-btn"
|
||||
<view
|
||||
v-show="searchUid"
|
||||
class="clear-btn"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<image class="clear-icon" src="/static/icon/cancel.png" mode="aspectFit"></image>
|
||||
@ -146,37 +164,77 @@
|
||||
<view v-if="isSearching" class="search-status">
|
||||
<text class="status-text">搜索中...</text>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 搜索错误 -->
|
||||
<view v-else-if="searchError" class="search-status">
|
||||
<text class="error-text">{{ searchError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果卡片 -->
|
||||
<view v-else-if="searchResult" class="friend-card">
|
||||
<view class="friend-avatar">
|
||||
<Avatar
|
||||
:userId="searchResult.user_id"
|
||||
:nickname="searchResult.nickname"
|
||||
:avatarUrl="searchResult.avatar_url"
|
||||
:size="100"
|
||||
:borderWidth="4"
|
||||
:showLevel="true"
|
||||
:level="searchResult.fan_level"
|
||||
:enableCache="false"
|
||||
/>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-nickname">{{ searchResult.nickname }}</text>
|
||||
<text class="friend-uid">UID: {{ searchResult.user_id }}</text>
|
||||
</view>
|
||||
<view class="friend-actions">
|
||||
<button class="add-friend-btn" @click="handleAddFriend(searchResult.user_id, searchResult.nickname)">
|
||||
添加好友
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- UID搜索结果卡片 -->
|
||||
<view v-else-if="searchMode === 'uid' && searchResult" class="friend-card">
|
||||
<view class="friend-avatar">
|
||||
<Avatar
|
||||
:userId="searchResult.user_id"
|
||||
:nickname="searchResult.nickname"
|
||||
:avatarUrl="searchResult.avatar_url"
|
||||
:size="100"
|
||||
:borderWidth="4"
|
||||
:showLevel="true"
|
||||
:level="searchResult.fan_level"
|
||||
:enableCache="false"
|
||||
/>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-nickname">{{ searchResult.nickname }}</text>
|
||||
<text class="friend-uid">UID: {{ searchResult.user_id }}</text>
|
||||
</view>
|
||||
<view class="friend-actions">
|
||||
<button class="add-friend-btn" @click="handleAddFriend(searchResult.user_id, searchResult.nickname)">
|
||||
添加好友
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称搜索结果列表 -->
|
||||
<view v-else-if="searchMode === 'nickname' && nicknameSearchResults.length > 0" class="nickname-results">
|
||||
<text class="results-count">找到 {{ nicknameSearchResults.length }} 个匹配用户</text>
|
||||
<view
|
||||
v-for="user in nicknameSearchResults"
|
||||
:key="user.user_id"
|
||||
class="friend-card"
|
||||
>
|
||||
<view class="friend-avatar">
|
||||
<Avatar
|
||||
:userId="user.user_id"
|
||||
:nickname="user.nickname"
|
||||
:avatarUrl="user.avatar_url"
|
||||
:size="100"
|
||||
:borderWidth="4"
|
||||
:showLevel="true"
|
||||
:level="user.fan_level"
|
||||
:enableCache="false"
|
||||
/>
|
||||
</view>
|
||||
<view class="friend-info">
|
||||
<text class="friend-nickname">{{ user.nickname }}</text>
|
||||
<text class="friend-uid">UID: {{ user.user_id }}</text>
|
||||
<text class="user-relation-status" :class="'status-' + user.relationship_status">
|
||||
{{ getRelationStatusText(user.relationship_status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="friend-actions">
|
||||
<button
|
||||
v-if="user.can_send_request"
|
||||
class="add-friend-btn"
|
||||
@click="handleAddFriend(user.user_id, user.nickname)"
|
||||
>
|
||||
添加好友
|
||||
</button>
|
||||
<text v-else class="cannot-add-hint">无法添加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<view class="divider"></view>
|
||||
@ -359,9 +417,15 @@
|
||||
|
||||
// 搜索UID
|
||||
const searchUid = ref('');
|
||||
|
||||
|
||||
// 搜索模式:uid / nickname
|
||||
const searchMode = ref('uid');
|
||||
|
||||
// 昵称搜索结果列表
|
||||
const nicknameSearchResults = ref([]);
|
||||
|
||||
// 搜索相关
|
||||
const searchResult = ref(null); // 搜索结果用户
|
||||
const searchResult = ref(null); // 搜索结果用户(UID模式)
|
||||
const searchError = ref(''); // 搜索错误信息
|
||||
const isSearching = ref(false); // 搜索中状态
|
||||
let searchTimer = null; // 搜索防抖定时器
|
||||
@ -520,6 +584,15 @@
|
||||
// 清空搜索框
|
||||
const clearSearch = () => {
|
||||
searchUid.value = '';
|
||||
nicknameSearchResults.value = [];
|
||||
searchResult.value = null;
|
||||
searchError.value = '';
|
||||
};
|
||||
|
||||
// 切换搜索模式
|
||||
const switchSearchMode = (mode) => {
|
||||
searchMode.value = mode;
|
||||
clearSearch();
|
||||
};
|
||||
|
||||
// 监听搜索框变化,实现自动搜索
|
||||
@ -528,32 +601,58 @@
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
|
||||
|
||||
// 清空之前的结果
|
||||
searchResult.value = null;
|
||||
nicknameSearchResults.value = [];
|
||||
searchError.value = '';
|
||||
|
||||
|
||||
// 如果搜索框为空,直接返回
|
||||
if (!newValue || newValue.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 设置新的定时器,500ms 后执行搜索
|
||||
searchTimer = setTimeout(async () => {
|
||||
try {
|
||||
isSearching.value = true;
|
||||
const res = await searchUserApi(newValue.trim());
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
// 解析头像URL
|
||||
const realAvatarUrl = res.data.avatar_url
|
||||
? await getFriendAvatarRealUrl(res.data.avatar_url)
|
||||
: '';
|
||||
searchResult.value = {
|
||||
...res.data,
|
||||
avatar_url: realAvatarUrl
|
||||
};
|
||||
searchError.value = '';
|
||||
|
||||
if (searchMode.value === 'uid') {
|
||||
// UID 搜索模式
|
||||
const res = await searchUserApi(newValue.trim());
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
// 解析头像URL
|
||||
const realAvatarUrl = res.data.avatar_url
|
||||
? await getFriendAvatarRealUrl(res.data.avatar_url)
|
||||
: '';
|
||||
searchResult.value = {
|
||||
...res.data,
|
||||
avatar_url: realAvatarUrl
|
||||
};
|
||||
searchError.value = '';
|
||||
}
|
||||
} else {
|
||||
// 昵称搜索模式
|
||||
const res = await sendFriendRequestApi(null, newValue.trim(), true);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
// 解析每个用户的头像URL
|
||||
const users = res.data.users || [];
|
||||
const processedUsers = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const realAvatarUrl = user.avatar
|
||||
? await getFriendAvatarRealUrl(user.avatar)
|
||||
: '';
|
||||
return {
|
||||
...user,
|
||||
avatar_url: realAvatarUrl
|
||||
};
|
||||
})
|
||||
);
|
||||
nicknameSearchResults.value = processedUsers;
|
||||
searchError.value = '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理 404 错误
|
||||
@ -656,6 +755,18 @@
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 获取关系状态文本
|
||||
const getRelationStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'stranger': '陌生人',
|
||||
'friend': '已是好友',
|
||||
'pending_sent': '已发送请求',
|
||||
'pending_received': '待接受',
|
||||
'rejected': '已被拒绝'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 加载收到的好友请求
|
||||
const loadReceivedRequests = async (page) => {
|
||||
@ -1156,6 +1267,62 @@
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 搜索模式切换 */
|
||||
.search-mode-switch {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
}
|
||||
|
||||
.mode-btn-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mode-btn.active .mode-btn-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 昵称搜索结果 */
|
||||
.nickname-results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-relation-status {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.cannot-add-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
@ -264,6 +264,7 @@ import { onReady } from "@dcloudio/uni-app";
|
||||
import Header from '../components/Header.vue';
|
||||
import Avatar from '../components/Avatar.vue';
|
||||
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi, getOssPresignedUrlApi } from '@/utils/api';
|
||||
import { validateNickname } from '@/utils/validator.js';
|
||||
import { clearAvatarCache, downloadAndCacheAvatar } from '@/utils/avatarCache';
|
||||
import GuideListModal from '@/components/GuideListModal.vue';
|
||||
import GuideOverlay from '@/components/GuideOverlay.vue';
|
||||
@ -479,17 +480,10 @@ const confirmChangeNickname = async () => {
|
||||
const trimmedNickname = newNickname.value.trim();
|
||||
|
||||
// 验证昵称
|
||||
if (!trimmedNickname) {
|
||||
const validation = validateNickname(trimmedNickname);
|
||||
if (!validation.valid) {
|
||||
uni.showToast({
|
||||
title: '请输入昵称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedNickname.length > 20) {
|
||||
uni.showToast({
|
||||
title: '昵称不能超过20个字符',
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
@ -693,17 +687,10 @@ const confirmAddIdentity = async () => {
|
||||
|
||||
// 验证昵称
|
||||
const trimmedNickname = newIdentityNickname.value.trim();
|
||||
if (!trimmedNickname) {
|
||||
const validation = validateNickname(trimmedNickname);
|
||||
if (!validation.valid) {
|
||||
uni.showToast({
|
||||
title: '请输入昵称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedNickname.length > 20) {
|
||||
uni.showToast({
|
||||
title: '昵称不能超过20个字符',
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
import { ref } from 'vue';
|
||||
import Avatar from '../components/Avatar.vue';
|
||||
import { checkNicknameApi } from '@/utils/api.js';
|
||||
import { validateNickname } from '@/utils/validator.js';
|
||||
|
||||
// 响应式数据
|
||||
const nickname = ref('');
|
||||
@ -86,24 +87,17 @@ const goBack = () => {
|
||||
// 下一步
|
||||
const handleNext = async () => {
|
||||
// 验证昵称
|
||||
if (!nickname.value || !nickname.value.trim()) {
|
||||
errorMessage.value = '请输入昵称';
|
||||
const trimmedNickname = nickname.value.trim();
|
||||
const validation = validateNickname(trimmedNickname);
|
||||
if (!validation.valid) {
|
||||
errorMessage.value = validation.message;
|
||||
uni.showToast({
|
||||
title: '请输入昵称',
|
||||
title: validation.message,
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (nickname.value.trim().length > 20) {
|
||||
errorMessage.value = '昵称不能超过20个字符';
|
||||
uni.showToast({
|
||||
title: '昵称不能超过20个字符',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
errorMessage.value = '';
|
||||
|
||||
// 防止重复点击
|
||||
|
||||
@ -239,13 +239,21 @@ export function getSentFriendRequestsApi(page = 1, pageSize = 10) {
|
||||
}
|
||||
|
||||
// 发送好友请求(添加好友)
|
||||
export function sendFriendRequestApi(friendUserId) {
|
||||
// friendUserId: 好友用户ID(可选)
|
||||
// nickname: 昵称(可选,与 friendUserId 二选一)
|
||||
// searchMode: 是否为搜索模式(true=仅搜索返回匹配用户,false=正常发送请求)
|
||||
export function sendFriendRequestApi(friendUserId = null, nickname = '', searchMode = true) {
|
||||
const data = {}
|
||||
if (searchMode && nickname) {
|
||||
data.nickname = nickname
|
||||
data.search_mode = true
|
||||
} else if (friendUserId) {
|
||||
data.friend_user_id = friendUserId
|
||||
}
|
||||
return request({
|
||||
url: '/api/v1/social/friend-requests',
|
||||
method: 'POST',
|
||||
data: {
|
||||
friend_user_id: friendUserId
|
||||
}
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -39,3 +39,32 @@ export function validatePassword(password) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证昵称(2-20字符,中文、英文、数字、下划线,首字符不能是数字或下划线)
|
||||
export function validateNickname(nickname) {
|
||||
if (!nickname) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请输入昵称'
|
||||
}
|
||||
}
|
||||
// 使用 [...nickname].length 计算字符数(支持中文)
|
||||
const len = [...nickname].length
|
||||
if (len < 2 || len > 20) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '昵称长度为2-20字符'
|
||||
}
|
||||
}
|
||||
// 格式校验:中文、英文、数字、下划线,首字符不能是数字或下划线
|
||||
if (!/^[一-龥a-zA-Z][一-龥a-zA-Z0-9_]*$/.test(nickname)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '昵称只能使用中文、英文、数字、下划线,首字符不能是数字或下划线'
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user