302 lines
8.4 KiB
Go
302 lines
8.4 KiB
Go
package appuser
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
)
|
|
|
|
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.OperatorKeyword)
|
|
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}
|
|
if query.TargetKeyword != "" {
|
|
like := "%" + query.TargetKeyword + "%"
|
|
whereSQL += " AND (CAST(target.user_id AS CHAR) LIKE ? OR target.current_display_user_id LIKE ? OR target.username LIKE ?)"
|
|
args = append(args, like, like, like)
|
|
}
|
|
if query.OperatorKeyword != "" {
|
|
like := "%" + query.OperatorKeyword + "%"
|
|
operatorClauses := []string{"(l.operator_type = 'app_user' AND (CAST(operator_user.user_id AS CHAR) LIKE ? OR operator_user.current_display_user_id LIKE ? OR operator_user.username LIKE ?))"}
|
|
args = append(args, like, like, like)
|
|
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 ") + ")"
|
|
}
|
|
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)
|
|
return query
|
|
}
|
|
|
|
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), ",")
|
|
}
|