diff --git a/backend/internal/handler/admin/user_handler.go b/backend/internal/handler/admin/user_handler.go index 38cc8acd..9a5a691f 100644 --- a/backend/internal/handler/admin/user_handler.go +++ b/backend/internal/handler/admin/user_handler.go @@ -84,9 +84,9 @@ func (h *UserHandler) List(c *gin.Context) { return } - out := make([]dto.User, 0, len(users)) + out := make([]dto.AdminUser, 0, len(users)) for i := range users { - out = append(out, *dto.UserFromService(&users[i])) + out = append(out, *dto.UserFromServiceAdmin(&users[i])) } response.Paginated(c, out, total, page, pageSize) } @@ -129,7 +129,7 @@ func (h *UserHandler) GetByID(c *gin.Context) { return } - response.Success(c, dto.UserFromService(user)) + response.Success(c, dto.UserFromServiceAdmin(user)) } // Create handles creating a new user @@ -155,7 +155,7 @@ func (h *UserHandler) Create(c *gin.Context) { return } - response.Success(c, dto.UserFromService(user)) + response.Success(c, dto.UserFromServiceAdmin(user)) } // Update handles updating a user @@ -189,7 +189,7 @@ func (h *UserHandler) Update(c *gin.Context) { return } - response.Success(c, dto.UserFromService(user)) + response.Success(c, dto.UserFromServiceAdmin(user)) } // Delete handles deleting a user @@ -231,7 +231,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) { return } - response.Success(c, dto.UserFromService(user)) + response.Success(c, dto.UserFromServiceAdmin(user)) } // GetUserAPIKeys handles getting user's API keys diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 3a3a18b2..e768b188 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -15,7 +15,6 @@ func UserFromServiceShallow(u *service.User) *User { ID: u.ID, Email: u.Email, Username: u.Username, - Notes: u.Notes, Role: u.Role, Balance: u.Balance, Concurrency: u.Concurrency, @@ -48,6 +47,22 @@ func UserFromService(u *service.User) *User { return out } +// UserFromServiceAdmin converts a service User to DTO for admin users. +// It includes notes - user-facing endpoints must not use this. +func UserFromServiceAdmin(u *service.User) *AdminUser { + if u == nil { + return nil + } + base := UserFromService(u) + if base == nil { + return nil + } + return &AdminUser{ + User: *base, + Notes: u.Notes, + } +} + func APIKeyFromService(k *service.APIKey) *APIKey { if k == nil { return nil diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 60e7c9bf..649cc036 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -6,7 +6,6 @@ type User struct { ID int64 `json:"id"` Email string `json:"email"` Username string `json:"username"` - Notes string `json:"notes"` Role string `json:"role"` Balance float64 `json:"balance"` Concurrency int `json:"concurrency"` @@ -19,6 +18,14 @@ type User struct { Subscriptions []UserSubscription `json:"subscriptions,omitempty"` } +// AdminUser 是管理员接口使用的 user DTO(包含敏感/内部字段)。 +// 注意:普通用户接口不得返回 notes 等管理员备注信息。 +type AdminUser struct { + User + + Notes string `json:"notes"` +} + type APIKey struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index d968951c..35862f1c 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -47,9 +47,6 @@ func (h *UserHandler) GetProfile(c *gin.Context) { return } - // 清空notes字段,普通用户不应看到备注 - userData.Notes = "" - response.Success(c, dto.UserFromService(userData)) } @@ -105,8 +102,5 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) { return } - // 清空notes字段,普通用户不应看到备注 - updatedUser.Notes = "" - response.Success(c, dto.UserFromService(updatedUser)) } diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 814668d3..c4cbc038 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -51,7 +51,6 @@ func TestAPIContracts(t *testing.T) { "id": 1, "email": "alice@example.com", "username": "alice", - "notes": "hello", "role": "user", "balance": 12.5, "concurrency": 5, diff --git a/frontend/src/api/admin/users.ts b/frontend/src/api/admin/users.ts index 44963cf9..734e3ac7 100644 --- a/frontend/src/api/admin/users.ts +++ b/frontend/src/api/admin/users.ts @@ -4,7 +4,7 @@ */ import { apiClient } from '../client' -import type { User, UpdateUserRequest, PaginatedResponse } from '@/types' +import type { AdminUser, UpdateUserRequest, PaginatedResponse } from '@/types' /** * List all users with pagination @@ -26,7 +26,7 @@ export async function list( options?: { signal?: AbortSignal } -): Promise> { +): Promise> { // Build params with attribute filters in attr[id]=value format const params: Record = { page, @@ -44,8 +44,7 @@ export async function list( } } } - - const { data } = await apiClient.get>('/admin/users', { + const { data } = await apiClient.get>('/admin/users', { params, signal: options?.signal }) @@ -57,8 +56,8 @@ export async function list( * @param id - User ID * @returns User details */ -export async function getById(id: number): Promise { - const { data } = await apiClient.get(`/admin/users/${id}`) +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/users/${id}`) return data } @@ -73,8 +72,8 @@ export async function create(userData: { balance?: number concurrency?: number allowed_groups?: number[] | null -}): Promise { - const { data } = await apiClient.post('/admin/users', userData) +}): Promise { + const { data } = await apiClient.post('/admin/users', userData) return data } @@ -84,8 +83,8 @@ export async function create(userData: { * @param updates - Fields to update * @returns Updated user */ -export async function update(id: number, updates: UpdateUserRequest): Promise { - const { data } = await apiClient.put(`/admin/users/${id}`, updates) +export async function update(id: number, updates: UpdateUserRequest): Promise { + const { data } = await apiClient.put(`/admin/users/${id}`, updates) return data } @@ -112,8 +111,8 @@ export async function updateBalance( balance: number, operation: 'set' | 'add' | 'subtract' = 'set', notes?: string -): Promise { - const { data } = await apiClient.post(`/admin/users/${id}/balance`, { +): Promise { + const { data } = await apiClient.post(`/admin/users/${id}/balance`, { balance, operation, notes: notes || '' @@ -127,7 +126,7 @@ export async function updateBalance( * @param concurrency - New concurrency limit * @returns Updated user */ -export async function updateConcurrency(id: number, concurrency: number): Promise { +export async function updateConcurrency(id: number, concurrency: number): Promise { return update(id, { concurrency }) } @@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis * @param status - New status * @returns Updated user */ -export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise { +export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise { return update(id, { status }) } diff --git a/frontend/src/components/admin/user/UserAllowedGroupsModal.vue b/frontend/src/components/admin/user/UserAllowedGroupsModal.vue index c1783fd2..825d2be5 100644 --- a/frontend/src/components/admin/user/UserAllowedGroupsModal.vue +++ b/frontend/src/components/admin/user/UserAllowedGroupsModal.vue @@ -39,10 +39,10 @@ import { ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { adminAPI } from '@/api/admin' -import type { User, Group } from '@/types' +import type { AdminUser, Group } from '@/types' import BaseDialog from '@/components/common/BaseDialog.vue' -const props = defineProps<{ show: boolean, user: User | null }>() +const props = defineProps<{ show: boolean, user: AdminUser | null }>() const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore() const groups = ref([]); const selectedIds = ref([]); const loading = ref(false); const submitting = ref(false) @@ -56,4 +56,4 @@ const handleSave = async () => { appStore.showSuccess(t('admin.users.allowedGroupsUpdated')); emit('success'); emit('close') } catch (error) { console.error('Failed to update allowed groups:', error) } finally { submitting.value = false } } - \ No newline at end of file + diff --git a/frontend/src/components/admin/user/UserApiKeysModal.vue b/frontend/src/components/admin/user/UserApiKeysModal.vue index ef098ba1..c2159ff4 100644 --- a/frontend/src/components/admin/user/UserApiKeysModal.vue +++ b/frontend/src/components/admin/user/UserApiKeysModal.vue @@ -32,10 +32,10 @@ import { ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { adminAPI } from '@/api/admin' import { formatDateTime } from '@/utils/format' -import type { User, ApiKey } from '@/types' +import type { AdminUser, ApiKey } from '@/types' import BaseDialog from '@/components/common/BaseDialog.vue' -const props = defineProps<{ show: boolean, user: User | null }>() +const props = defineProps<{ show: boolean, user: AdminUser | null }>() defineEmits(['close']); const { t } = useI18n() const apiKeys = ref([]); const loading = ref(false) @@ -44,4 +44,4 @@ const load = async () => { if (!props.user) return; loading.value = true try { const res = await adminAPI.users.getUserApiKeys(props.user.id); apiKeys.value = res.items || [] } catch (error) { console.error('Failed to load API keys:', error) } finally { loading.value = false } } - \ No newline at end of file + diff --git a/frontend/src/components/admin/user/UserBalanceModal.vue b/frontend/src/components/admin/user/UserBalanceModal.vue index c669c2a5..143350bf 100644 --- a/frontend/src/components/admin/user/UserBalanceModal.vue +++ b/frontend/src/components/admin/user/UserBalanceModal.vue @@ -29,10 +29,10 @@ import { reactive, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { adminAPI } from '@/api/admin' -import type { User } from '@/types' +import type { AdminUser } from '@/types' import BaseDialog from '@/components/common/BaseDialog.vue' -const props = defineProps<{ show: boolean, user: User | null, operation: 'add' | 'subtract' }>() +const props = defineProps<{ show: boolean, user: AdminUser | null, operation: 'add' | 'subtract' }>() const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore() const submitting = ref(false); const form = reactive({ amount: 0, notes: '' }) diff --git a/frontend/src/components/admin/user/UserEditModal.vue b/frontend/src/components/admin/user/UserEditModal.vue index 2c4b117a..70ebd2d3 100644 --- a/frontend/src/components/admin/user/UserEditModal.vue +++ b/frontend/src/components/admin/user/UserEditModal.vue @@ -56,12 +56,12 @@ import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { useClipboard } from '@/composables/useClipboard' import { adminAPI } from '@/api/admin' -import type { User, UserAttributeValuesMap } from '@/types' +import type { AdminUser, UserAttributeValuesMap } from '@/types' import BaseDialog from '@/components/common/BaseDialog.vue' import UserAttributeForm from '@/components/user/UserAttributeForm.vue' import Icon from '@/components/icons/Icon.vue' -const props = defineProps<{ show: boolean, user: User | null }>() +const props = defineProps<{ show: boolean, user: AdminUser | null }>() const emit = defineEmits(['close', 'success']) const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard() diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index fbf41898..eef022d9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -27,7 +27,6 @@ export interface FetchOptions { export interface User { id: number username: string - notes: string email: string role: 'admin' | 'user' // User role for authorization balance: number // User balance for API usage @@ -39,6 +38,11 @@ export interface User { updated_at: string } +export interface AdminUser extends User { + // 管理员备注(普通用户接口不返回) + notes: string +} + export interface LoginRequest { email: string password: string diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue index 4ce839f1..2a73a977 100644 --- a/frontend/src/views/admin/UsersView.vue +++ b/frontend/src/views/admin/UsersView.vue @@ -492,7 +492,7 @@ import Icon from '@/components/icons/Icon.vue' const { t } = useI18n() import { adminAPI } from '@/api/admin' -import type { User, UserAttributeDefinition } from '@/types' +import type { AdminUser, UserAttributeDefinition } from '@/types' import type { BatchUserUsageStats } from '@/api/admin/dashboard' import type { Column } from '@/components/common/types' import AppLayout from '@/components/layout/AppLayout.vue' @@ -637,7 +637,7 @@ const columns = computed(() => ) ) -const users = ref([]) +const users = ref([]) const loading = ref(false) const searchQuery = ref('') @@ -736,16 +736,16 @@ const showEditModal = ref(false) const showDeleteDialog = ref(false) const showApiKeysModal = ref(false) const showAttributesModal = ref(false) -const editingUser = ref(null) -const deletingUser = ref(null) -const viewingUser = ref(null) +const editingUser = ref(null) +const deletingUser = ref(null) +const viewingUser = ref(null) let abortController: AbortController | null = null // Action Menu State const activeMenuId = ref(null) const menuPosition = ref<{ top: number; left: number } | null>(null) -const openActionMenu = (user: User, e: MouseEvent) => { +const openActionMenu = (user: AdminUser, e: MouseEvent) => { if (activeMenuId.value === user.id) { closeActionMenu() } else { @@ -821,11 +821,11 @@ const handleClickOutside = (event: MouseEvent) => { // Allowed groups modal state const showAllowedGroupsModal = ref(false) -const allowedGroupsUser = ref(null) +const allowedGroupsUser = ref(null) // Balance (Deposit/Withdraw) modal state const showBalanceModal = ref(false) -const balanceUser = ref(null) +const balanceUser = ref(null) const balanceOperation = ref<'add' | 'subtract'>('add') // 计算剩余天数 @@ -998,7 +998,7 @@ const applyFilter = () => { loadUsers() } -const handleEdit = (user: User) => { +const handleEdit = (user: AdminUser) => { editingUser.value = user showEditModal.value = true } @@ -1008,7 +1008,7 @@ const closeEditModal = () => { editingUser.value = null } -const handleToggleStatus = async (user: User) => { +const handleToggleStatus = async (user: AdminUser) => { const newStatus = user.status === 'active' ? 'disabled' : 'active' try { await adminAPI.users.toggleStatus(user.id, newStatus) @@ -1022,7 +1022,7 @@ const handleToggleStatus = async (user: User) => { } } -const handleViewApiKeys = (user: User) => { +const handleViewApiKeys = (user: AdminUser) => { viewingUser.value = user showApiKeysModal.value = true } @@ -1032,7 +1032,7 @@ const closeApiKeysModal = () => { viewingUser.value = null } -const handleAllowedGroups = (user: User) => { +const handleAllowedGroups = (user: AdminUser) => { allowedGroupsUser.value = user showAllowedGroupsModal.value = true } @@ -1042,7 +1042,7 @@ const closeAllowedGroupsModal = () => { allowedGroupsUser.value = null } -const handleDelete = (user: User) => { +const handleDelete = (user: AdminUser) => { deletingUser.value = user showDeleteDialog.value = true } @@ -1061,13 +1061,13 @@ const confirmDelete = async () => { } } -const handleDeposit = (user: User) => { +const handleDeposit = (user: AdminUser) => { balanceUser.value = user balanceOperation.value = 'add' showBalanceModal.value = true } -const handleWithdraw = (user: User) => { +const handleWithdraw = (user: AdminUser) => { balanceUser.value = user balanceOperation.value = 'subtract' showBalanceModal.value = true