2026-06-29 14:26:06 +08:00

335 lines
10 KiB
Go

package appuser
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/modules/shared"
)
type AppUserBanRecord struct {
ID int64 `json:"id"`
Target AppUserBrief `json:"target"`
Operator BanOperator `json:"operator"`
OldStatus string `json:"oldStatus"`
NewStatus string `json:"newStatus"`
Reason string `json:"reason"`
RequestID string `json:"requestId"`
CreatedAtMs int64 `json:"createdAtMs"`
}
type AppUserBrief struct {
Avatar string `json:"avatar"`
DisplayUserID string `json:"displayUserId"`
UserID string `json:"userId"`
Username string `json:"username"`
}
type BanOperator struct {
Type string `json:"type"`
AdminID string `json:"adminId,omitempty"`
Account string `json:"account,omitempty"`
Avatar string `json:"avatar,omitempty"`
DisplayUserID string `json:"displayUserId,omitempty"`
Name string `json:"name,omitempty"`
UserID string `json:"userId,omitempty"`
}
type adminOperatorProfile struct {
Account string
Name string
}
func (s *Service) ListBannedUsers(ctx context.Context, query banListQuery) ([]AppUserBanRecord, int64, error) {
if s.userDB == nil {
return nil, 0, fmt.Errorf("user mysql is not configured")
}
query = normalizeBanListQuery(query)
adminIDs, err := s.lookupAdminOperatorIDs(ctx, query.operatorAdminLookupKeyword())
if err != nil {
return nil, 0, err
}
whereSQL, args := banListWhereSQL(ctx, query, adminIDs)
total, err := countRows(ctx, s.userDB, whereSQL, args...)
if err != nil {
return nil, 0, err
}
rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(`
SELECT l.id,
l.target_user_id,
target.current_display_user_id,
COALESCE(target.username, ''),
COALESCE(target.avatar, ''),
l.old_status,
l.new_status,
l.operator_type,
l.operator_user_id,
COALESCE(operator_user.current_display_user_id, ''),
COALESCE(operator_user.username, ''),
COALESCE(operator_user.avatar, ''),
l.reason,
l.request_id,
l.created_at_ms
%s
ORDER BY l.created_at_ms DESC, l.id DESC
LIMIT ? OFFSET ?
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]AppUserBanRecord, 0, query.PageSize)
adminOperatorIDs := make([]int64, 0)
for rows.Next() {
item, adminOperatorID, err := scanBanRecord(rows)
if err != nil {
return nil, 0, err
}
if adminOperatorID > 0 {
adminOperatorIDs = append(adminOperatorIDs, adminOperatorID)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
if err := s.fillAdminOperators(ctx, items, adminOperatorIDs); err != nil {
return nil, 0, err
}
return items, total, nil
}
func banListWhereSQL(ctx context.Context, query banListQuery, adminIDs []int64) (string, []any) {
appCode := appctx.FromContext(ctx)
whereSQL := `
FROM user_status_change_logs l
INNER JOIN (
SELECT app_code, target_user_id, MAX(id) AS latest_id
FROM user_status_change_logs
WHERE app_code = ? AND new_status IN ('banned', 'disabled')
GROUP BY app_code, target_user_id
) latest ON latest.app_code = l.app_code AND latest.latest_id = l.id
INNER JOIN users target ON target.app_code = l.app_code AND target.user_id = l.target_user_id
LEFT JOIN users operator_user ON l.operator_type = 'app_user' AND operator_user.app_code = l.app_code AND operator_user.user_id = l.operator_user_id
WHERE l.app_code = ? AND target.status IN ('banned', 'disabled')`
args := []any{appCode, appCode}
nowMs := nowMillis()
if !query.TargetFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("target", "target.user_id", query.TargetFilter, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if query.TargetKeyword != "" {
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("target", "target.user_id", query.TargetKeyword, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
}
if !query.OperatorFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("operator_user", "operator_user.user_id", query.OperatorFilter, nowMs)
whereSQL += " AND (l.operator_type = 'app_user' AND " + matchSQL + ")"
args = append(args, matchArgs...)
} else if query.OperatorKeyword != "" {
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("operator_user", "operator_user.user_id", query.OperatorKeyword, nowMs)
operatorClauses := []string{"(l.operator_type = 'app_user' AND " + matchSQL + ")"}
args = append(args, matchArgs...)
if len(adminIDs) > 0 {
operatorClauses = append(operatorClauses, "(l.operator_type = 'admin' AND l.operator_user_id IN ("+placeholders(len(adminIDs))+"))")
for _, id := range adminIDs {
args = append(args, id)
}
}
whereSQL += " AND (" + strings.Join(operatorClauses, " OR ") + ")"
} else if query.OperatorAdminKeyword != "" {
if len(adminIDs) == 0 {
whereSQL += " AND 1 = 0"
} else {
whereSQL += " AND (l.operator_type = 'admin' AND l.operator_user_id IN (" + placeholders(len(adminIDs)) + "))"
for _, id := range adminIDs {
args = append(args, id)
}
}
}
if query.StartMs > 0 {
whereSQL += " AND l.created_at_ms >= ?"
args = append(args, query.StartMs)
}
if query.EndMs > 0 {
whereSQL += " AND l.created_at_ms < ?"
args = append(args, query.EndMs)
}
return whereSQL, args
}
func scanBanRecord(rows *sql.Rows) (AppUserBanRecord, int64, error) {
var item AppUserBanRecord
var targetUserID int64
var operatorType string
var operatorUserID int64
var operatorDisplayUserID string
var operatorUsername string
var operatorAvatar string
if err := rows.Scan(
&item.ID,
&targetUserID,
&item.Target.DisplayUserID,
&item.Target.Username,
&item.Target.Avatar,
&item.OldStatus,
&item.NewStatus,
&operatorType,
&operatorUserID,
&operatorDisplayUserID,
&operatorUsername,
&operatorAvatar,
&item.Reason,
&item.RequestID,
&item.CreatedAtMs,
); err != nil {
return AppUserBanRecord{}, 0, err
}
item.Target.UserID = strconv.FormatInt(targetUserID, 10)
item.Operator.Type = operatorType
if operatorType == "admin" {
item.Operator.AdminID = strconv.FormatInt(operatorUserID, 10)
item.Operator.Account = item.Operator.AdminID
return item, operatorUserID, nil
}
if operatorType == "app_user" && operatorUserID > 0 {
item.Operator.UserID = strconv.FormatInt(operatorUserID, 10)
item.Operator.DisplayUserID = operatorDisplayUserID
item.Operator.Name = operatorUsername
item.Operator.Avatar = operatorAvatar
}
return item, 0, nil
}
func (s *Service) lookupAdminOperatorIDs(ctx context.Context, keyword string) ([]int64, error) {
if s.adminDB == nil || strings.TrimSpace(keyword) == "" {
return nil, nil
}
like := "%" + strings.TrimSpace(keyword) + "%"
rows, err := s.adminDB.QueryContext(ctx, `
SELECT id
FROM admin_users
WHERE CAST(id AS CHAR) LIKE ? OR username LIKE ? OR name LIKE ?
LIMIT 200
`, like, like, like)
if err != nil {
return nil, err
}
defer rows.Close()
ids := make([]int64, 0)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func (s *Service) fillAdminOperators(ctx context.Context, items []AppUserBanRecord, adminIDs []int64) error {
if s.adminDB == nil || len(adminIDs) == 0 {
return nil
}
ids := uniquePositiveInt64s(adminIDs)
if len(ids) == 0 {
return nil
}
args := make([]any, 0, len(ids))
for _, id := range ids {
args = append(args, id)
}
rows, err := s.adminDB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, username, name
FROM admin_users
WHERE id IN (%s)
`, placeholders(len(ids))), args...)
if err != nil {
return err
}
defer rows.Close()
profiles := make(map[int64]adminOperatorProfile, len(ids))
for rows.Next() {
var id int64
var profile adminOperatorProfile
if err := rows.Scan(&id, &profile.Account, &profile.Name); err != nil {
return err
}
profiles[id] = profile
}
if err := rows.Err(); err != nil {
return err
}
for index := range items {
if items[index].Operator.Type != "admin" {
continue
}
adminID, _ := strconv.ParseInt(items[index].Operator.AdminID, 10, 64)
profile, ok := profiles[adminID]
if !ok {
continue
}
items[index].Operator.Account = profile.Account
items[index].Operator.Name = profile.Name
}
return nil
}
func normalizeBanListQuery(query banListQuery) banListQuery {
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
query.TargetKeyword = strings.TrimSpace(query.TargetKeyword)
query.OperatorKeyword = strings.TrimSpace(query.OperatorKeyword)
query.OperatorAdminKeyword = strings.TrimSpace(query.OperatorAdminKeyword)
query.TargetFilter.UserID = strings.TrimSpace(query.TargetFilter.UserID)
query.TargetFilter.DisplayUserID = strings.TrimSpace(query.TargetFilter.DisplayUserID)
query.TargetFilter.Username = strings.TrimSpace(query.TargetFilter.Username)
query.OperatorFilter.UserID = strings.TrimSpace(query.OperatorFilter.UserID)
query.OperatorFilter.DisplayUserID = strings.TrimSpace(query.OperatorFilter.DisplayUserID)
query.OperatorFilter.Username = strings.TrimSpace(query.OperatorFilter.Username)
return query
}
func (query banListQuery) operatorAdminLookupKeyword() string {
if keyword := strings.TrimSpace(query.OperatorAdminKeyword); keyword != "" {
return keyword
}
return strings.TrimSpace(query.OperatorKeyword)
}
func uniquePositiveInt64s(values []int64) []int64 {
seen := make(map[int64]struct{}, len(values))
out := make([]int64, 0, len(values))
for _, value := range values {
if value <= 0 {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
func placeholders(count int) string {
if count <= 0 {
return ""
}
return strings.TrimRight(strings.Repeat("?,", count), ",")
}