feat: 新增昵称增加好友和注册用户昵称时的昵称判断
This commit is contained in:
parent
55104d5aef
commit
f426b84c0a
@ -36,14 +36,14 @@ func NewSocialController(dubboClient *client.Client) (*SocialController, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendFriendRequest 发送好友请求
|
// SendFriendRequest 发送好友请求(支持按昵称搜索)
|
||||||
// @Summary 发送好友请求
|
// @Summary 发送好友请求
|
||||||
// @Description 向指定用户发送好友请求
|
// @Description 向指定用户发送好友请求,或按昵称搜索匹配用户
|
||||||
// @Tags social
|
// @Tags social
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @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
|
// @Success 200 {object} response.Response
|
||||||
// @Router /api/v1/social/friend-requests [post]
|
// @Router /api/v1/social/friend-requests [post]
|
||||||
func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
||||||
@ -62,8 +62,10 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
|||||||
|
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
var req struct {
|
var req struct {
|
||||||
FriendUserID int64 `json:"friend_user_id" binding:"required"`
|
FriendUserID int64 `json:"friend_user_id"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
SearchMode bool `json:"search_mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@ -71,6 +73,16 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
|||||||
return
|
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
|
// 设置上下文和 Dubbo attachments
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -81,15 +93,20 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 调用 RPC
|
// 调用 RPC
|
||||||
resp, err := ctrl.socialService.SendFriendRequest(ctx, &pbSocial.SendFriendRequestRequest{
|
rpcReq := &pbSocial.SendFriendRequestRequest{
|
||||||
FriendUserId: req.FriendUserID,
|
FriendUserId: req.FriendUserID,
|
||||||
Message: req.Message,
|
Message: req.Message,
|
||||||
})
|
Nickname: req.Nickname,
|
||||||
|
SearchMode: req.SearchMode,
|
||||||
|
}
|
||||||
|
resp, err := ctrl.socialService.SendFriendRequest(ctx, rpcReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Error("SendFriendRequest RPC failed",
|
logger.Logger.Error("SendFriendRequest RPC failed",
|
||||||
zap.Int64("user_id", userID.(int64)),
|
zap.Int64("user_id", userID.(int64)),
|
||||||
zap.Int64("friend_user_id", req.FriendUserID),
|
zap.Int64("friend_user_id", req.FriendUserID),
|
||||||
|
zap.String("nickname", req.Nickname),
|
||||||
|
zap.Bool("search_mode", req.SearchMode),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
response.Error(c, http.StatusInternalServerError, "服务调用失败")
|
response.Error(c, http.StatusInternalServerError, "服务调用失败")
|
||||||
@ -102,6 +119,28 @@ func (ctrl *SocialController) SendFriendRequest(c *gin.Context) {
|
|||||||
return
|
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{
|
response.Success(c, gin.H{
|
||||||
"request_id": resp.RequestId,
|
"request_id": resp.RequestId,
|
||||||
"status": resp.Status,
|
"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"`
|
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)
|
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"` // 请求附带消息(可选)
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -368,13 +370,28 @@ func (x *SendFriendRequestRequest) GetMessage() string {
|
|||||||
return ""
|
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 {
|
type SendFriendRequestResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
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
|
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"` // 请求状态
|
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"` // 创建时间
|
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"` // 过期时间
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -444,6 +461,13 @@ func (x *SendFriendRequestResponse) GetExpiresAt() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *SendFriendRequestResponse) GetMatchedUsers() []*FanProfileSearchResult {
|
||||||
|
if x != nil {
|
||||||
|
return x.MatchedUsers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 获取好友请求列表
|
// 获取好友请求列表
|
||||||
type GetFriendRequestsRequest struct {
|
type GetFriendRequestsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -2165,10 +2189,13 @@ const file_social_proto_rawDesc = "" +
|
|||||||
"\rfriend_avatar\x18\n" +
|
"\rfriend_avatar\x18\n" +
|
||||||
" \x01(\tR\ffriendAvatar\x12(\n" +
|
" \x01(\tR\ffriendAvatar\x12(\n" +
|
||||||
"\x10friend_fan_level\x18\v \x01(\x05R\x0efriendFanLevel\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" +
|
"\x18SendFriendRequestRequest\x12$\n" +
|
||||||
"\x0efriend_user_id\x18\x01 \x01(\x03R\ffriendUserId\x12\x18\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" +
|
"\x19SendFriendRequestResponse\x120\n" +
|
||||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
|
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -2177,7 +2204,8 @@ const file_social_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" +
|
"created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" +
|
||||||
"\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" +
|
"\x18GetFriendRequestsRequest\x12\x12\n" +
|
||||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" +
|
"\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" +
|
||||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x12\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{
|
var file_social_proto_depIdxs = []int32{
|
||||||
33, // 0: topfans.social.SendFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 0: topfans.social.SendFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 1: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse
|
20, // 1: topfans.social.SendFriendRequestResponse.matched_users:type_name -> topfans.social.FanProfileSearchResult
|
||||||
0, // 2: topfans.social.GetFriendRequestsResponse.items:type_name -> topfans.social.FriendRequest
|
33, // 2: topfans.social.GetFriendRequestsResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 3: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
0, // 3: topfans.social.GetFriendRequestsResponse.items:type_name -> topfans.social.FriendRequest
|
||||||
33, // 4: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 4: topfans.social.HandleFriendRequestResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
1, // 5: topfans.social.GetFriendListResponse.items:type_name -> topfans.social.Friendship
|
33, // 5: topfans.social.GetFriendListResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 6: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse
|
1, // 6: topfans.social.GetFriendListResponse.items:type_name -> topfans.social.Friendship
|
||||||
33, // 7: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 7: topfans.social.DeleteFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 8: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 8: topfans.social.SetFriendRemarkResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 9: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 9: topfans.social.CheckFriendshipResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 10: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 10: topfans.social.GetFriendCountResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
20, // 11: topfans.social.SearchUserForFriendResponse.user:type_name -> topfans.social.FanProfileSearchResult
|
33, // 11: topfans.social.SearchUserForFriendResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 12: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse
|
20, // 12: topfans.social.SearchUserForFriendResponse.user:type_name -> topfans.social.FanProfileSearchResult
|
||||||
21, // 13: topfans.social.GetRandomUsersResponse.users:type_name -> topfans.social.RandomUser
|
33, // 13: topfans.social.GetRandomUsersResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 14: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse
|
21, // 14: topfans.social.GetRandomUsersResponse.users:type_name -> topfans.social.RandomUser
|
||||||
24, // 15: topfans.social.GetUsersPagedResponse.users:type_name -> topfans.social.PagedUser
|
33, // 15: topfans.social.GetUsersPagedResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 16: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
24, // 16: topfans.social.GetUsersPagedResponse.users:type_name -> topfans.social.PagedUser
|
||||||
33, // 17: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 17: topfans.social.LikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
33, // 18: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
|
33, // 18: topfans.social.UnlikeAssetResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
2, // 19: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest
|
33, // 19: topfans.social.CheckAssetLikeResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
4, // 20: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest
|
2, // 20: topfans.social.SocialService.SendFriendRequest:input_type -> topfans.social.SendFriendRequestRequest
|
||||||
6, // 21: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest
|
4, // 21: topfans.social.SocialService.GetFriendRequests:input_type -> topfans.social.GetFriendRequestsRequest
|
||||||
8, // 22: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest
|
6, // 22: topfans.social.SocialService.HandleFriendRequest:input_type -> topfans.social.HandleFriendRequestRequest
|
||||||
10, // 23: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest
|
8, // 23: topfans.social.SocialService.GetFriendList:input_type -> topfans.social.GetFriendListRequest
|
||||||
12, // 24: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest
|
10, // 24: topfans.social.SocialService.DeleteFriend:input_type -> topfans.social.DeleteFriendRequest
|
||||||
14, // 25: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest
|
12, // 25: topfans.social.SocialService.SetFriendRemark:input_type -> topfans.social.SetFriendRemarkRequest
|
||||||
16, // 26: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest
|
14, // 26: topfans.social.SocialService.CheckFriendship:input_type -> topfans.social.CheckFriendshipRequest
|
||||||
18, // 27: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest
|
16, // 27: topfans.social.SocialService.GetFriendCount:input_type -> topfans.social.GetFriendCountRequest
|
||||||
22, // 28: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest
|
18, // 28: topfans.social.SocialService.SearchUserForFriend:input_type -> topfans.social.SearchUserForFriendRequest
|
||||||
25, // 29: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest
|
22, // 29: topfans.social.SocialService.GetRandomUsers:input_type -> topfans.social.GetRandomUsersRequest
|
||||||
27, // 30: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest
|
25, // 30: topfans.social.SocialService.GetUsersPaged:input_type -> topfans.social.GetUsersPagedRequest
|
||||||
29, // 31: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest
|
27, // 31: topfans.social.SocialService.LikeAsset:input_type -> topfans.social.LikeAssetRequest
|
||||||
31, // 32: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest
|
29, // 32: topfans.social.SocialService.UnlikeAsset:input_type -> topfans.social.UnlikeAssetRequest
|
||||||
3, // 33: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse
|
31, // 33: topfans.social.SocialService.CheckAssetLike:input_type -> topfans.social.CheckAssetLikeRequest
|
||||||
5, // 34: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse
|
3, // 34: topfans.social.SocialService.SendFriendRequest:output_type -> topfans.social.SendFriendRequestResponse
|
||||||
7, // 35: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse
|
5, // 35: topfans.social.SocialService.GetFriendRequests:output_type -> topfans.social.GetFriendRequestsResponse
|
||||||
9, // 36: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse
|
7, // 36: topfans.social.SocialService.HandleFriendRequest:output_type -> topfans.social.HandleFriendRequestResponse
|
||||||
11, // 37: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse
|
9, // 37: topfans.social.SocialService.GetFriendList:output_type -> topfans.social.GetFriendListResponse
|
||||||
13, // 38: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse
|
11, // 38: topfans.social.SocialService.DeleteFriend:output_type -> topfans.social.DeleteFriendResponse
|
||||||
15, // 39: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse
|
13, // 39: topfans.social.SocialService.SetFriendRemark:output_type -> topfans.social.SetFriendRemarkResponse
|
||||||
17, // 40: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse
|
15, // 40: topfans.social.SocialService.CheckFriendship:output_type -> topfans.social.CheckFriendshipResponse
|
||||||
19, // 41: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse
|
17, // 41: topfans.social.SocialService.GetFriendCount:output_type -> topfans.social.GetFriendCountResponse
|
||||||
23, // 42: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse
|
19, // 42: topfans.social.SocialService.SearchUserForFriend:output_type -> topfans.social.SearchUserForFriendResponse
|
||||||
26, // 43: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse
|
23, // 43: topfans.social.SocialService.GetRandomUsers:output_type -> topfans.social.GetRandomUsersResponse
|
||||||
28, // 44: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse
|
26, // 44: topfans.social.SocialService.GetUsersPaged:output_type -> topfans.social.GetUsersPagedResponse
|
||||||
30, // 45: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse
|
28, // 45: topfans.social.SocialService.LikeAsset:output_type -> topfans.social.LikeAssetResponse
|
||||||
32, // 46: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse
|
30, // 46: topfans.social.SocialService.UnlikeAsset:output_type -> topfans.social.UnlikeAssetResponse
|
||||||
33, // [33:47] is the sub-list for method output_type
|
32, // 47: topfans.social.SocialService.CheckAssetLike:output_type -> topfans.social.CheckAssetLikeResponse
|
||||||
19, // [19:33] is the sub-list for method input_type
|
34, // [34:48] is the sub-list for method output_type
|
||||||
19, // [19:19] is the sub-list for extension type_name
|
20, // [20:34] is the sub-list for method input_type
|
||||||
19, // [19:19] is the sub-list for extension extendee
|
20, // [20:20] is the sub-list for extension type_name
|
||||||
0, // [0:19] is the sub-list for field 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() }
|
func init() { file_social_proto_init() }
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package validator
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/topfans/backend/pkg/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -11,14 +13,16 @@ const (
|
|||||||
// MaxPasswordLength 最大密码长度
|
// MaxPasswordLength 最大密码长度
|
||||||
MaxPasswordLength = 50
|
MaxPasswordLength = 50
|
||||||
// MinNicknameLength 最小昵称长度
|
// MinNicknameLength 最小昵称长度
|
||||||
MinNicknameLength = 1
|
MinNicknameLength = 2
|
||||||
// MaxNicknameLength 最大昵称长度
|
// MaxNicknameLength 最大昵称长度
|
||||||
MaxNicknameLength = 50
|
MaxNicknameLength = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// 中国手机号正则表达式
|
// 中国手机号正则表达式
|
||||||
mobileRegex = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
mobileRegex = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
||||||
|
// 昵称正则:中文、英文、数字、下划线,首字符不能是数字或下划线
|
||||||
|
nicknameRegex = regexp.MustCompile(`^[一-龥a-zA-Z][一-龥a-zA-Z0-9_]*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateMobile 验证手机号格式
|
// ValidateMobile 验证手机号格式
|
||||||
@ -53,14 +57,26 @@ func ValidateNickname(nickname string) (bool, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nickname = strings.TrimSpace(nickname)
|
nickname = strings.TrimSpace(nickname)
|
||||||
if len(nickname) < MinNicknameLength {
|
// 使用 rune 计数,支持中文等多字节字符
|
||||||
|
runeCount := len([]rune(nickname))
|
||||||
|
if runeCount < MinNicknameLength {
|
||||||
return false, "nickname too short"
|
return false, "nickname too short"
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nickname) > MaxNicknameLength {
|
if runeCount > MaxNicknameLength {
|
||||||
return false, "nickname too long"
|
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, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,8 @@ message Friendship {
|
|||||||
message SendFriendRequestRequest {
|
message SendFriendRequestRequest {
|
||||||
int64 friend_user_id = 1; // 好友用户ID(要添加的用户ID)
|
int64 friend_user_id = 1; // 好友用户ID(要添加的用户ID)
|
||||||
string message = 2; // 请求附带消息(可选)
|
string message = 2; // 请求附带消息(可选)
|
||||||
|
string nickname = 3; // 昵称(用于按昵称搜索,与 friend_user_id 二选一)
|
||||||
|
bool search_mode = 4; // 搜索模式(true=仅搜索返回匹配用户,false=正常发送请求)
|
||||||
}
|
}
|
||||||
|
|
||||||
message SendFriendRequestResponse {
|
message SendFriendRequestResponse {
|
||||||
@ -62,6 +64,7 @@ message SendFriendRequestResponse {
|
|||||||
string status = 3; // 请求状态
|
string status = 3; // 请求状态
|
||||||
int64 created_at = 4; // 创建时间
|
int64 created_at = 4; // 创建时间
|
||||||
int64 expires_at = 5; // 过期时间
|
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获取粉丝档案
|
||||||
GetFanProfileByUserIDAndStarID(userID, starID int64) (*models.FanProfile, error)
|
GetFanProfileByUserIDAndStarID(userID, starID int64) (*models.FanProfile, error)
|
||||||
|
|
||||||
|
// GetFanProfilesByNickname 根据昵称模糊搜索粉丝档案(同一明星下)
|
||||||
|
// starID: 明星ID
|
||||||
|
// nickname: 昵称关键词
|
||||||
|
// limit: 返回数量限制
|
||||||
|
GetFanProfilesByNickname(starID int64, nickname string, limit int) ([]*models.FanProfile, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RandomUserInfo 随机用户信息
|
// RandomUserInfo 随机用户信息
|
||||||
@ -498,3 +504,25 @@ func (r *socialRepositoryImpl) GetFanProfileByUserIDAndStarID(userID, starID int
|
|||||||
}
|
}
|
||||||
return &profile, nil
|
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="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">
|
<view class="search-wrapper">
|
||||||
<image class="search-icon" src="/static/icon/search.png" mode="aspectFit"></image>
|
<image class="search-icon" src="/static/icon/search.png" mode="aspectFit"></image>
|
||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchUid"
|
v-model="searchUid"
|
||||||
placeholder="输入uid进行用户搜索"
|
:placeholder="searchMode === 'uid' ? '输入uid进行用户搜索' : '输入昵称进行用户搜索'"
|
||||||
type="number"
|
:type="searchMode === 'uid' ? 'number' : 'text'"
|
||||||
/>
|
/>
|
||||||
<view
|
<view
|
||||||
v-show="searchUid"
|
v-show="searchUid"
|
||||||
class="clear-btn"
|
class="clear-btn"
|
||||||
@click="clearSearch"
|
@click="clearSearch"
|
||||||
>
|
>
|
||||||
<image class="clear-icon" src="/static/icon/cancel.png" mode="aspectFit"></image>
|
<image class="clear-icon" src="/static/icon/cancel.png" mode="aspectFit"></image>
|
||||||
@ -146,37 +164,77 @@
|
|||||||
<view v-if="isSearching" class="search-status">
|
<view v-if="isSearching" class="search-status">
|
||||||
<text class="status-text">搜索中...</text>
|
<text class="status-text">搜索中...</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索错误 -->
|
<!-- 搜索错误 -->
|
||||||
<view v-else-if="searchError" class="search-status">
|
<view v-else-if="searchError" class="search-status">
|
||||||
<text class="error-text">{{ searchError }}</text>
|
<text class="error-text">{{ searchError }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索结果卡片 -->
|
<!-- UID搜索结果卡片 -->
|
||||||
<view v-else-if="searchResult" class="friend-card">
|
<view v-else-if="searchMode === 'uid' && searchResult" class="friend-card">
|
||||||
<view class="friend-avatar">
|
<view class="friend-avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
:userId="searchResult.user_id"
|
:userId="searchResult.user_id"
|
||||||
:nickname="searchResult.nickname"
|
:nickname="searchResult.nickname"
|
||||||
:avatarUrl="searchResult.avatar_url"
|
:avatarUrl="searchResult.avatar_url"
|
||||||
:size="100"
|
:size="100"
|
||||||
:borderWidth="4"
|
:borderWidth="4"
|
||||||
:showLevel="true"
|
:showLevel="true"
|
||||||
:level="searchResult.fan_level"
|
:level="searchResult.fan_level"
|
||||||
:enableCache="false"
|
:enableCache="false"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="friend-info">
|
<view class="friend-info">
|
||||||
<text class="friend-nickname">{{ searchResult.nickname }}</text>
|
<text class="friend-nickname">{{ searchResult.nickname }}</text>
|
||||||
<text class="friend-uid">UID: {{ searchResult.user_id }}</text>
|
<text class="friend-uid">UID: {{ searchResult.user_id }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="friend-actions">
|
<view class="friend-actions">
|
||||||
<button class="add-friend-btn" @click="handleAddFriend(searchResult.user_id, searchResult.nickname)">
|
<button class="add-friend-btn" @click="handleAddFriend(searchResult.user_id, searchResult.nickname)">
|
||||||
添加好友
|
添加好友
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</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>
|
<view class="divider"></view>
|
||||||
@ -359,9 +417,15 @@
|
|||||||
|
|
||||||
// 搜索UID
|
// 搜索UID
|
||||||
const searchUid = ref('');
|
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 searchError = ref(''); // 搜索错误信息
|
||||||
const isSearching = ref(false); // 搜索中状态
|
const isSearching = ref(false); // 搜索中状态
|
||||||
let searchTimer = null; // 搜索防抖定时器
|
let searchTimer = null; // 搜索防抖定时器
|
||||||
@ -520,6 +584,15 @@
|
|||||||
// 清空搜索框
|
// 清空搜索框
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
searchUid.value = '';
|
searchUid.value = '';
|
||||||
|
nicknameSearchResults.value = [];
|
||||||
|
searchResult.value = null;
|
||||||
|
searchError.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换搜索模式
|
||||||
|
const switchSearchMode = (mode) => {
|
||||||
|
searchMode.value = mode;
|
||||||
|
clearSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听搜索框变化,实现自动搜索
|
// 监听搜索框变化,实现自动搜索
|
||||||
@ -528,32 +601,58 @@
|
|||||||
if (searchTimer) {
|
if (searchTimer) {
|
||||||
clearTimeout(searchTimer);
|
clearTimeout(searchTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空之前的结果
|
// 清空之前的结果
|
||||||
searchResult.value = null;
|
searchResult.value = null;
|
||||||
|
nicknameSearchResults.value = [];
|
||||||
searchError.value = '';
|
searchError.value = '';
|
||||||
|
|
||||||
// 如果搜索框为空,直接返回
|
// 如果搜索框为空,直接返回
|
||||||
if (!newValue || newValue.trim() === '') {
|
if (!newValue || newValue.trim() === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置新的定时器,500ms 后执行搜索
|
// 设置新的定时器,500ms 后执行搜索
|
||||||
searchTimer = setTimeout(async () => {
|
searchTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
isSearching.value = true;
|
isSearching.value = true;
|
||||||
const res = await searchUserApi(newValue.trim());
|
|
||||||
|
if (searchMode.value === 'uid') {
|
||||||
if (res.code === 200 && res.data) {
|
// UID 搜索模式
|
||||||
// 解析头像URL
|
const res = await searchUserApi(newValue.trim());
|
||||||
const realAvatarUrl = res.data.avatar_url
|
|
||||||
? await getFriendAvatarRealUrl(res.data.avatar_url)
|
if (res.code === 200 && res.data) {
|
||||||
: '';
|
// 解析头像URL
|
||||||
searchResult.value = {
|
const realAvatarUrl = res.data.avatar_url
|
||||||
...res.data,
|
? await getFriendAvatarRealUrl(res.data.avatar_url)
|
||||||
avatar_url: realAvatarUrl
|
: '';
|
||||||
};
|
searchResult.value = {
|
||||||
searchError.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) {
|
} catch (error) {
|
||||||
// 处理 404 错误
|
// 处理 404 错误
|
||||||
@ -656,6 +755,18 @@
|
|||||||
};
|
};
|
||||||
return statusMap[status] || status;
|
return statusMap[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取关系状态文本
|
||||||
|
const getRelationStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'stranger': '陌生人',
|
||||||
|
'friend': '已是好友',
|
||||||
|
'pending_sent': '已发送请求',
|
||||||
|
'pending_received': '待接受',
|
||||||
|
'rejected': '已被拒绝'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
// 加载收到的好友请求
|
// 加载收到的好友请求
|
||||||
const loadReceivedRequests = async (page) => {
|
const loadReceivedRequests = async (page) => {
|
||||||
@ -1156,6 +1267,62 @@
|
|||||||
transition: transform 0.2s ease;
|
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 {
|
.clear-btn:active {
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -264,6 +264,7 @@ import { onReady } from "@dcloudio/uni-app";
|
|||||||
import Header from '../components/Header.vue';
|
import Header from '../components/Header.vue';
|
||||||
import Avatar from '../components/Avatar.vue';
|
import Avatar from '../components/Avatar.vue';
|
||||||
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi, getOssPresignedUrlApi } from '@/utils/api';
|
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 { clearAvatarCache, downloadAndCacheAvatar } from '@/utils/avatarCache';
|
||||||
import GuideListModal from '@/components/GuideListModal.vue';
|
import GuideListModal from '@/components/GuideListModal.vue';
|
||||||
import GuideOverlay from '@/components/GuideOverlay.vue';
|
import GuideOverlay from '@/components/GuideOverlay.vue';
|
||||||
@ -479,17 +480,10 @@ const confirmChangeNickname = async () => {
|
|||||||
const trimmedNickname = newNickname.value.trim();
|
const trimmedNickname = newNickname.value.trim();
|
||||||
|
|
||||||
// 验证昵称
|
// 验证昵称
|
||||||
if (!trimmedNickname) {
|
const validation = validateNickname(trimmedNickname);
|
||||||
|
if (!validation.valid) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请输入昵称',
|
title: validation.message,
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmedNickname.length > 20) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '昵称不能超过20个字符',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -693,17 +687,10 @@ const confirmAddIdentity = async () => {
|
|||||||
|
|
||||||
// 验证昵称
|
// 验证昵称
|
||||||
const trimmedNickname = newIdentityNickname.value.trim();
|
const trimmedNickname = newIdentityNickname.value.trim();
|
||||||
if (!trimmedNickname) {
|
const validation = validateNickname(trimmedNickname);
|
||||||
|
if (!validation.valid) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请输入昵称',
|
title: validation.message,
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmedNickname.length > 20) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '昵称不能超过20个字符',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -53,6 +53,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Avatar from '../components/Avatar.vue';
|
import Avatar from '../components/Avatar.vue';
|
||||||
import { checkNicknameApi } from '@/utils/api.js';
|
import { checkNicknameApi } from '@/utils/api.js';
|
||||||
|
import { validateNickname } from '@/utils/validator.js';
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const nickname = ref('');
|
const nickname = ref('');
|
||||||
@ -86,24 +87,17 @@ const goBack = () => {
|
|||||||
// 下一步
|
// 下一步
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
// 验证昵称
|
// 验证昵称
|
||||||
if (!nickname.value || !nickname.value.trim()) {
|
const trimmedNickname = nickname.value.trim();
|
||||||
errorMessage.value = '请输入昵称';
|
const validation = validateNickname(trimmedNickname);
|
||||||
|
if (!validation.valid) {
|
||||||
|
errorMessage.value = validation.message;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请输入昵称',
|
title: validation.message,
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nickname.value.trim().length > 20) {
|
|
||||||
errorMessage.value = '昵称不能超过20个字符';
|
|
||||||
uni.showToast({
|
|
||||||
title: '昵称不能超过20个字符',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage.value = '';
|
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({
|
return request({
|
||||||
url: '/api/v1/social/friend-requests',
|
url: '/api/v1/social/friend-requests',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data
|
||||||
friend_user_id: friendUserId
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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