2026-07-23 20:35:25 +08:00

210 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ""
}
}