210 lines
7.5 KiB
Go
210 lines
7.5 KiB
Go
package appuser
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/integration/userclient"
|
||
userv1 "hyapp.local/api/proto/user/v1"
|
||
)
|
||
|
||
const maxContentBlacklistIdentifierRunes = 64
|
||
|
||
// AppUserContentBlacklistEntry 是管理端当前态读模型。
|
||
// userId 始终按字符串输出,避免 19 位 Snowflake ID 穿过 JavaScript Number 后失真。
|
||
type AppUserContentBlacklistEntry struct {
|
||
Target AppUserBrief `json:"target"`
|
||
UserStatus string `json:"userStatus"`
|
||
OperatorAdminID string `json:"operatorAdminId"`
|
||
Reason string `json:"reason"`
|
||
CreatedAtMs int64 `json:"createdAtMs"`
|
||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||
}
|
||
|
||
type AppUserContentBlacklistCandidate struct {
|
||
Target AppUserBrief `json:"target"`
|
||
UserStatus string `json:"userStatus"`
|
||
Blacklisted bool `json:"blacklisted"`
|
||
}
|
||
|
||
type contentBlacklistMutationRequest struct {
|
||
UserIdentifier string `json:"user_id"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
|
||
// ListUserContentBlacklist 通过 owner service 分页读取,不允许 admin 用直连 userDB 的方式复制治理事实。
|
||
func (s *Service) ListUserContentBlacklist(ctx context.Context, page int, pageSize int, requestID string) ([]AppUserContentBlacklistEntry, int64, error) {
|
||
if s.contentBlacklistClient == nil {
|
||
return nil, 0, fmt.Errorf("user content blacklist client is not configured")
|
||
}
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = 50
|
||
}
|
||
if pageSize > 200 {
|
||
pageSize = 200
|
||
}
|
||
response, err := s.contentBlacklistClient.ListUserContentBlacklist(ctx, &userv1.ListUserContentBlacklistRequest{
|
||
Meta: contentBlacklistRequestMeta(ctx, requestID),
|
||
Page: int32(page),
|
||
PageSize: int32(pageSize),
|
||
})
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
items := make([]AppUserContentBlacklistEntry, 0, len(response.GetItems()))
|
||
for _, item := range response.GetItems() {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
items = append(items, appUserContentBlacklistEntryFromProto(item))
|
||
}
|
||
return items, response.GetTotal(), nil
|
||
}
|
||
|
||
// ResolveUserContentBlacklistCandidate 复用 user-service 的后台通用 ID 解析:
|
||
// 永久默认短号、当前有效展示号、靓号和内部长 user_id 都收敛到同一个真实 user_id。
|
||
func (s *Service) ResolveUserContentBlacklistCandidate(ctx context.Context, identifier string, requestID string) (AppUserContentBlacklistCandidate, error) {
|
||
identifier = strings.TrimSpace(identifier)
|
||
if identifier == "" || len([]rune(identifier)) > maxContentBlacklistIdentifierRunes {
|
||
return AppUserContentBlacklistCandidate{}, fmt.Errorf("%w: 用户 ID 参数不正确", ErrInvalidArgument)
|
||
}
|
||
if s.contentBlacklistClient == nil {
|
||
return AppUserContentBlacklistCandidate{}, fmt.Errorf("user content blacklist client is not configured")
|
||
}
|
||
identity, err := s.contentBlacklistClient.ResolveAdminUserIdentifier(ctx, userclient.ResolveAdminUserIdentifierRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
UserIdentifier: identifier,
|
||
})
|
||
if err != nil {
|
||
return AppUserContentBlacklistCandidate{}, err
|
||
}
|
||
if identity == nil || identity.UserID <= 0 {
|
||
return AppUserContentBlacklistCandidate{}, ErrNotFound
|
||
}
|
||
user, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
UserID: identity.UserID,
|
||
})
|
||
if err != nil {
|
||
return AppUserContentBlacklistCandidate{}, err
|
||
}
|
||
if user == nil {
|
||
return AppUserContentBlacklistCandidate{}, ErrNotFound
|
||
}
|
||
return AppUserContentBlacklistCandidate{
|
||
Target: AppUserBrief{
|
||
UserID: strconv.FormatInt(user.UserID, 10),
|
||
DisplayUserID: user.DisplayUserID,
|
||
Username: user.Username,
|
||
Avatar: user.Avatar,
|
||
},
|
||
UserStatus: user.Status,
|
||
Blacklisted: user.ContentBlacklisted,
|
||
}, nil
|
||
}
|
||
|
||
// AddUserContentBlacklist 先用通用 ID 契约解析真实用户,再仅把真实 user_id 发送给治理 RPC。
|
||
// 这样前端输入的短号即使以后过期,也不会成为黑名单持久键。
|
||
func (s *Service) AddUserContentBlacklist(ctx context.Context, identifier string, operatorAdminID int64, reason string, requestID string) (AppUserContentBlacklistEntry, error) {
|
||
if operatorAdminID <= 0 {
|
||
return AppUserContentBlacklistEntry{}, fmt.Errorf("%w: 操作管理员参数不正确", ErrInvalidArgument)
|
||
}
|
||
candidate, err := s.ResolveUserContentBlacklistCandidate(ctx, identifier, requestID)
|
||
if err != nil {
|
||
return AppUserContentBlacklistEntry{}, err
|
||
}
|
||
userID, err := strconv.ParseInt(candidate.Target.UserID, 10, 64)
|
||
if err != nil || userID <= 0 {
|
||
return AppUserContentBlacklistEntry{}, fmt.Errorf("%w: 用户 ID 参数不正确", ErrInvalidArgument)
|
||
}
|
||
reason = strings.TrimSpace(reason)
|
||
if reason == "" {
|
||
reason = "admin_content_blacklist"
|
||
}
|
||
response, err := s.contentBlacklistClient.AdminAddUserContentBlacklist(ctx, &userv1.AdminAddUserContentBlacklistRequest{
|
||
Meta: contentBlacklistRequestMeta(ctx, requestID),
|
||
UserId: userID,
|
||
OperatorAdminId: operatorAdminID,
|
||
Reason: reason,
|
||
})
|
||
if err != nil {
|
||
return AppUserContentBlacklistEntry{}, err
|
||
}
|
||
if response.GetItem() == nil {
|
||
return AppUserContentBlacklistEntry{}, fmt.Errorf("user-service returned empty content blacklist entry")
|
||
}
|
||
return appUserContentBlacklistEntryFromProto(response.GetItem()), nil
|
||
}
|
||
|
||
// RemoveUserContentBlacklist 以内部真实 user_id 移除当前态;重复移除由 owner service 返回 removed=false,
|
||
// handler 仍按幂等成功处理,避免管理员因网络重试得到伪失败。
|
||
func (s *Service) RemoveUserContentBlacklist(ctx context.Context, userID int64, operatorAdminID int64, reason string, requestID string) (bool, error) {
|
||
if s.contentBlacklistClient == nil {
|
||
return false, fmt.Errorf("user content blacklist client is not configured")
|
||
}
|
||
if userID <= 0 || operatorAdminID <= 0 {
|
||
return false, fmt.Errorf("%w: 黑名单移除参数不正确", ErrInvalidArgument)
|
||
}
|
||
reason = strings.TrimSpace(reason)
|
||
if reason == "" {
|
||
reason = "admin_remove_content_blacklist"
|
||
}
|
||
response, err := s.contentBlacklistClient.AdminRemoveUserContentBlacklist(ctx, &userv1.AdminRemoveUserContentBlacklistRequest{
|
||
Meta: contentBlacklistRequestMeta(ctx, requestID),
|
||
UserId: userID,
|
||
OperatorAdminId: operatorAdminID,
|
||
Reason: reason,
|
||
})
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return response.GetRemoved(), nil
|
||
}
|
||
|
||
func contentBlacklistRequestMeta(ctx context.Context, requestID string) *userv1.RequestMeta {
|
||
return &userv1.RequestMeta{
|
||
RequestId: strings.TrimSpace(requestID),
|
||
Caller: "hyapp-admin-server",
|
||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||
AppCode: appctx.FromContext(ctx),
|
||
}
|
||
}
|
||
|
||
func appUserContentBlacklistEntryFromProto(item *userv1.UserContentBlacklistEntry) AppUserContentBlacklistEntry {
|
||
return AppUserContentBlacklistEntry{
|
||
Target: AppUserBrief{
|
||
UserID: strconv.FormatInt(item.GetUserId(), 10),
|
||
DisplayUserID: item.GetDisplayUserId(),
|
||
Username: item.GetUsername(),
|
||
Avatar: item.GetAvatar(),
|
||
},
|
||
UserStatus: contentBlacklistUserStatus(item.GetUserStatus()),
|
||
OperatorAdminID: strconv.FormatInt(item.GetOperatorAdminId(), 10),
|
||
Reason: item.GetReason(),
|
||
CreatedAtMs: item.GetCreatedAtMs(),
|
||
UpdatedAtMs: item.GetUpdatedAtMs(),
|
||
}
|
||
}
|
||
|
||
func contentBlacklistUserStatus(status userv1.UserStatus) string {
|
||
switch status {
|
||
case userv1.UserStatus_USER_STATUS_ACTIVE:
|
||
return "active"
|
||
case userv1.UserStatus_USER_STATUS_DISABLED:
|
||
return "disabled"
|
||
case userv1.UserStatus_USER_STATUS_BANNED:
|
||
return "banned"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|