feat(admin): Add email search and rate limit filtering for accounts and redeem codes

- Add used_by_email column to redeem code export CSV for better user identification
- Implement rate_limited status filter in account listing with RateLimitResetAt check
- Extend redeem code search to include user email in addition to code matching
- Add API key search capability to user listing filters
- Display user email in redeem code table used_by column for improved visibility
- Update search placeholders in UI to reflect expanded search capabilities (email, username, notes, API key)
- Improve Chinese and English localization strings for search hints
This commit is contained in:
kyx236
2026-02-11 16:39:42 +08:00
parent 723102766b
commit 04a1a7c2b5
8 changed files with 29 additions and 11 deletions

View File

@@ -202,7 +202,7 @@ func (h *RedeemHandler) Export(c *gin.Context) {
writer := csv.NewWriter(&buf) writer := csv.NewWriter(&buf)
// Write header // Write header
if err := writer.Write([]string{"id", "code", "type", "value", "status", "used_by", "used_at", "created_at"}); err != nil { if err := writer.Write([]string{"id", "code", "type", "value", "status", "used_by", "used_by_email", "used_at", "created_at"}); err != nil {
response.InternalError(c, "Failed to export redeem codes: "+err.Error()) response.InternalError(c, "Failed to export redeem codes: "+err.Error())
return return
} }
@@ -213,6 +213,10 @@ func (h *RedeemHandler) Export(c *gin.Context) {
if code.UsedBy != nil { if code.UsedBy != nil {
usedBy = fmt.Sprintf("%d", *code.UsedBy) usedBy = fmt.Sprintf("%d", *code.UsedBy)
} }
usedByEmail := ""
if code.User != nil {
usedByEmail = code.User.Email
}
usedAt := "" usedAt := ""
if code.UsedAt != nil { if code.UsedAt != nil {
usedAt = code.UsedAt.Format("2006-01-02 15:04:05") usedAt = code.UsedAt.Format("2006-01-02 15:04:05")
@@ -224,6 +228,7 @@ func (h *RedeemHandler) Export(c *gin.Context) {
fmt.Sprintf("%.2f", code.Value), fmt.Sprintf("%.2f", code.Value),
code.Status, code.Status,
usedBy, usedBy,
usedByEmail,
usedAt, usedAt,
code.CreatedAt.Format("2006-01-02 15:04:05"), code.CreatedAt.Format("2006-01-02 15:04:05"),
}); err != nil { }); err != nil {

View File

@@ -448,8 +448,13 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati
q = q.Where(dbaccount.TypeEQ(accountType)) q = q.Where(dbaccount.TypeEQ(accountType))
} }
if status != "" { if status != "" {
switch status {
case "rate_limited":
q = q.Where(dbaccount.RateLimitResetAtGT(time.Now()))
default:
q = q.Where(dbaccount.StatusEQ(status)) q = q.Where(dbaccount.StatusEQ(status))
} }
}
if search != "" { if search != "" {
q = q.Where(dbaccount.NameContainsFold(search)) q = q.Where(dbaccount.NameContainsFold(search))
} }

View File

@@ -6,6 +6,7 @@ import (
dbent "github.com/Wei-Shaw/sub2api/ent" dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/redeemcode" "github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/user"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/service"
) )
@@ -106,7 +107,12 @@ func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagin
q = q.Where(redeemcode.StatusEQ(status)) q = q.Where(redeemcode.StatusEQ(status))
} }
if search != "" { if search != "" {
q = q.Where(redeemcode.CodeContainsFold(search)) q = q.Where(
redeemcode.Or(
redeemcode.CodeContainsFold(search),
redeemcode.HasUserWith(user.EmailContainsFold(search)),
),
)
} }
total, err := q.Count(ctx) total, err := q.Count(ctx)

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
dbent "github.com/Wei-Shaw/sub2api/ent" dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/apikey"
dbuser "github.com/Wei-Shaw/sub2api/ent/user" dbuser "github.com/Wei-Shaw/sub2api/ent/user"
"github.com/Wei-Shaw/sub2api/ent/userallowedgroup" "github.com/Wei-Shaw/sub2api/ent/userallowedgroup"
"github.com/Wei-Shaw/sub2api/ent/usersubscription" "github.com/Wei-Shaw/sub2api/ent/usersubscription"
@@ -191,6 +192,7 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.
dbuser.EmailContainsFold(filters.Search), dbuser.EmailContainsFold(filters.Search),
dbuser.UsernameContainsFold(filters.Search), dbuser.UsernameContainsFold(filters.Search),
dbuser.NotesContainsFold(filters.Search), dbuser.NotesContainsFold(filters.Search),
dbuser.HasAPIKeysWith(apikey.KeyContainsFold(filters.Search)),
), ),
) )
} }

View File

@@ -21,5 +21,5 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) } const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }]) const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }]) const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }])
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }]) const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }])
</script> </script>

View File

@@ -841,7 +841,7 @@ export default {
createUser: 'Create User', createUser: 'Create User',
editUser: 'Edit User', editUser: 'Edit User',
deleteUser: 'Delete User', deleteUser: 'Delete User',
searchUsers: 'Search users...', searchUsers: 'Search by email, username, notes, or API key...',
allRoles: 'All Roles', allRoles: 'All Roles',
allStatus: 'All Status', allStatus: 'All Status',
admin: 'Admin', admin: 'Admin',
@@ -2129,7 +2129,7 @@ export default {
title: 'Redeem Code Management', title: 'Redeem Code Management',
description: 'Generate and manage redeem codes', description: 'Generate and manage redeem codes',
generateCodes: 'Generate Codes', generateCodes: 'Generate Codes',
searchCodes: 'Search codes...', searchCodes: 'Search codes or email...',
allTypes: 'All Types', allTypes: 'All Types',
allStatus: 'All Status', allStatus: 'All Status',
balance: 'Balance', balance: 'Balance',

View File

@@ -865,8 +865,8 @@ export default {
editUser: '编辑用户', editUser: '编辑用户',
deleteUser: '删除用户', deleteUser: '删除用户',
deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。", deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。",
searchPlaceholder: '搜索用户邮箱用户名备注、支持模糊查询...', searchPlaceholder: '邮箱/用户名/备注/API Key 模糊搜索...',
searchUsers: '搜索用户邮箱用户名备注、支持模糊查询', searchUsers: '邮箱/用户名/备注/API Key 模糊搜索',
roleFilter: '角色筛选', roleFilter: '角色筛选',
allRoles: '全部角色', allRoles: '全部角色',
allStatus: '全部状态', allStatus: '全部状态',
@@ -2292,7 +2292,7 @@ export default {
allStatus: '全部状态', allStatus: '全部状态',
unused: '未使用', unused: '未使用',
used: '已使用', used: '已使用',
searchCodes: '搜索兑换码...', searchCodes: '搜索兑换码或邮箱...',
exportCsv: '导出 CSV', exportCsv: '导出 CSV',
deleteAllUnused: '删除全部未使用', deleteAllUnused: '删除全部未使用',
deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。', deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。',

View File

@@ -117,9 +117,9 @@
</span> </span>
</template> </template>
<template #cell-used_by="{ value }"> <template #cell-used_by="{ value, row }">
<span class="text-sm text-gray-500 dark:text-dark-400"> <span class="text-sm text-gray-500 dark:text-dark-400">
{{ value ? t('admin.redeem.userPrefix', { id: value }) : '-' }} {{ row.user?.email || (value ? t('admin.redeem.userPrefix', { id: value }) : '-') }}
</span> </span>
</template> </template>