From 35a812c05877859b04075fc3f3c5e2cfd2120208 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 3 Jan 2026 18:50:10 -0800 Subject: [PATCH] feat(authorization): add admin authorization management API and real data integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (authorization-service): - Add QueryAuthorizationsDto for query parameters (roleType, keyword, includeRevoked, page, limit) - Add queryAuthorizations method to fetch all authorizations with user info - Add GET /admin/authorizations endpoint for listing authorizations - Add POST /admin/authorizations/:id/revoke endpoint for revoking authorization Frontend (admin-web): - Add authorization.types.ts with RoleType, Authorization, and request types - Add authorizationService.ts for API calls (list, revoke, grant operations) - Add useAuthorizations.ts React Query hooks - Update authorization page to use real API data instead of mock data - Add loading/error states, pagination, and revoke reason display - Add new styles for loading, error, pagination, and date columns The authorization management page now displays all authorized users from the database with support for filtering by role type, status, and keyword search. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../admin-authorization.controller.ts | 64 +- .../src/api/dto/request/index.ts | 1 + .../dto/request/query-authorizations.dto.ts | 35 ++ .../authorization-application.service.ts | 111 ++++ .../authorization/authorization.module.scss | 97 +++ .../app/(dashboard)/authorization/page.tsx | 557 ++++++++++-------- frontend/admin-web/src/hooks/index.ts | 1 + .../admin-web/src/hooks/useAuthorizations.ts | 121 ++++ .../src/infrastructure/api/endpoints.ts | 9 + .../src/services/authorizationService.ts | 93 +++ .../src/types/authorization.types.ts | 145 +++++ frontend/admin-web/src/types/index.ts | 1 + 12 files changed, 972 insertions(+), 263 deletions(-) create mode 100644 backend/services/authorization-service/src/api/dto/request/query-authorizations.dto.ts create mode 100644 frontend/admin-web/src/hooks/useAuthorizations.ts create mode 100644 frontend/admin-web/src/services/authorizationService.ts create mode 100644 frontend/admin-web/src/types/authorization.types.ts diff --git a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts index 59436214..0759ccd9 100644 --- a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' +import { Controller, Post, Get, Body, Query, Param, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger' import { AuthorizationApplicationService } from '@/application/services' import { GrantCommunityCommand, @@ -7,6 +7,7 @@ import { GrantCityCompanyCommand, GrantAuthProvinceCompanyCommand, GrantAuthCityCompanyCommand, + RevokeAuthorizationCommand, } from '@/application/commands' import { GrantCommunityDto, @@ -14,9 +15,12 @@ import { GrantCityCompanyDto, GrantAuthProvinceCompanyDto, GrantAuthCityCompanyDto, + RevokeAuthorizationDto, + QueryAuthorizationsDto, } from '@/api/dto/request' import { CurrentUser } from '@/shared/decorators' import { JwtAuthGuard } from '@/shared/guards' +import { RoleType } from '@/domain/enums' @ApiTags('Admin Authorization') @Controller('admin/authorizations') @@ -25,6 +29,62 @@ import { JwtAuthGuard } from '@/shared/guards' export class AdminAuthorizationController { constructor(private readonly applicationService: AuthorizationApplicationService) {} + @Get() + @ApiOperation({ summary: '查询授权列表(管理员)' }) + @ApiQuery({ name: 'roleType', required: false, enum: RoleType }) + @ApiQuery({ name: 'keyword', required: false }) + @ApiQuery({ name: 'includeRevoked', required: false, type: Boolean }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiResponse({ status: 200, description: '授权列表' }) + async queryAuthorizations( + @Query() dto: QueryAuthorizationsDto, + ): Promise<{ + items: Array<{ + id: string + accountSequence: string + nickname: string + avatar: string | null + roleType: RoleType + regionName: string + status: string + benefitActive: boolean + createdAt: Date + authorizedAt: Date | null + revokedAt: Date | null + revokeReason: string | null + }> + total: number + page: number + limit: number + }> { + return this.applicationService.queryAuthorizations({ + roleType: dto.roleType, + keyword: dto.keyword, + includeRevoked: dto.includeRevoked, + page: dto.page, + limit: dto.limit, + }) + } + + @Post(':id/revoke') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '撤销授权(管理员)' }) + @ApiResponse({ status: 200, description: '撤销成功' }) + async revokeAuthorization( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Param('id') authorizationId: string, + @Body() dto: RevokeAuthorizationDto, + ): Promise<{ message: string }> { + const command = new RevokeAuthorizationCommand( + authorizationId, + dto.reason, + user.accountSequence, + ) + await this.applicationService.revokeAuthorization(command) + return { message: '授权已撤销' } + } + @Post('community') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: '授权社区(管理员)' }) diff --git a/backend/services/authorization-service/src/api/dto/request/index.ts b/backend/services/authorization-service/src/api/dto/request/index.ts index f52c5cb8..88be716e 100644 --- a/backend/services/authorization-service/src/api/dto/request/index.ts +++ b/backend/services/authorization-service/src/api/dto/request/index.ts @@ -9,3 +9,4 @@ export * from './grant-auth-city-company.dto' export * from './revoke-authorization.dto' export * from './grant-monthly-bypass.dto' export * from './self-apply-authorization.dto' +export * from './query-authorizations.dto' diff --git a/backend/services/authorization-service/src/api/dto/request/query-authorizations.dto.ts b/backend/services/authorization-service/src/api/dto/request/query-authorizations.dto.ts new file mode 100644 index 00000000..4c6ca1cb --- /dev/null +++ b/backend/services/authorization-service/src/api/dto/request/query-authorizations.dto.ts @@ -0,0 +1,35 @@ +import { IsOptional, IsString, IsEnum, IsInt, Min, Max } from 'class-validator' +import { ApiPropertyOptional } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { RoleType } from '@/domain/enums' + +export class QueryAuthorizationsDto { + @ApiPropertyOptional({ description: '授权类型', enum: RoleType }) + @IsOptional() + @IsEnum(RoleType) + roleType?: RoleType + + @ApiPropertyOptional({ description: '搜索关键词(匹配昵称、账户序列号、地区名称)' }) + @IsOptional() + @IsString() + keyword?: string + + @ApiPropertyOptional({ description: '是否包含已撤销的授权', default: false }) + @IsOptional() + includeRevoked?: boolean + + @ApiPropertyOptional({ description: '页码', default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number + + @ApiPropertyOptional({ description: '每页数量', default: 20 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number +} diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index a2a25203..64014d5b 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -3430,4 +3430,115 @@ export class AuthorizationApplicationService { if (name.length === 2) return name[0] + '*' return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1] } + + // ============ 管理员查询方法 ============ + + /** + * 查询授权列表(管理员用) + */ + async queryAuthorizations(params: { + roleType?: RoleType + keyword?: string + includeRevoked?: boolean + page?: number + limit?: number + }): Promise<{ + items: Array<{ + id: string + accountSequence: string + nickname: string + avatar: string | null + roleType: RoleType + regionName: string + status: AuthorizationStatus + benefitActive: boolean + createdAt: Date + authorizedAt: Date | null + revokedAt: Date | null + revokeReason: string | null + }> + total: number + page: number + limit: number + }> { + const page = params.page ?? 1 + const limit = params.limit ?? 20 + const includeRevoked = params.includeRevoked ?? false + + // 获取所有授权(根据是否包含已撤销) + let allAuthorizations: AuthorizationRole[] + if (params.roleType) { + allAuthorizations = await this.authorizationRepository.findAllActive(params.roleType) + } else { + allAuthorizations = await this.authorizationRepository.findAllActive() + } + + // 如果包含已撤销的,需要添加已撤销的授权 + if (includeRevoked) { + const revokedAuths = await this.authorizationRepository.findByStatus(AuthorizationStatus.REVOKED) + // 过滤 roleType + const filteredRevoked = params.roleType + ? revokedAuths.filter((a) => a.roleType === params.roleType) + : revokedAuths + allAuthorizations = [...allAuthorizations, ...filteredRevoked] + } + + // 获取用户信息 + const accountSequences = [...new Set(allAuthorizations.map((a) => a.userId.accountSequence))] + let usersMap = new Map() + + if (accountSequences.length > 0) { + try { + const usersInfoMap = await this.identityServiceClient.batchGetUserInfoBySequence(accountSequences) + usersInfoMap.forEach((userInfo, accountSequence) => { + usersMap.set(accountSequence, { nickname: userInfo.nickname, avatar: userInfo.avatarUrl ?? null }) + }) + } catch (e) { + this.logger.warn(`获取用户信息失败: ${e}`) + } + } + + // 关键词过滤 + let filteredAuthorizations = allAuthorizations + if (params.keyword) { + const keyword = params.keyword.toLowerCase() + filteredAuthorizations = allAuthorizations.filter((auth) => { + const userInfo = usersMap.get(auth.userId.accountSequence) + return ( + auth.userId.accountSequence.toLowerCase().includes(keyword) || + auth.regionName.toLowerCase().includes(keyword) || + (userInfo?.nickname && userInfo.nickname.toLowerCase().includes(keyword)) + ) + }) + } + + // 排序:按创建时间降序 + filteredAuthorizations.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + + // 分页 + const total = filteredAuthorizations.length + const startIndex = (page - 1) * limit + const pagedAuthorizations = filteredAuthorizations.slice(startIndex, startIndex + limit) + + // 构建响应 + const items = pagedAuthorizations.map((auth) => { + const userInfo = usersMap.get(auth.userId.accountSequence) + return { + id: auth.authorizationId.value, + accountSequence: auth.userId.accountSequence, + nickname: userInfo?.nickname ?? auth.userId.accountSequence, + avatar: userInfo?.avatar ?? null, + roleType: auth.roleType, + regionName: auth.regionName, + status: auth.status, + benefitActive: auth.benefitActive, + createdAt: auth.createdAt, + authorizedAt: auth.authorizedAt, + revokedAt: auth.revokedAt, + revokeReason: auth.revokeReason, + } + }) + + return { items, total, page, limit } + } } diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss index 998d2162..937293b9 100644 --- a/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss @@ -941,3 +941,100 @@ } } } + +/* 加载状态 */ +.authorization__loading { + align-self: stretch; + padding: 40px 24px; + text-align: center; + color: #6b7280; + font-size: 14px; +} + +/* 错误状态 */ +.authorization__error { + align-self: stretch; + padding: 24px; + background-color: #fef2f2; + border-radius: 6px; + color: #991b1b; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} + +.authorization__retryBtn { + cursor: pointer; + border: 1px solid #991b1b; + border-radius: 4px; + background-color: transparent; + padding: 4px 12px; + font-size: 12px; + color: #991b1b; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: #991b1b; + color: #fff; + } +} + +/* 日期列 */ +.authorization__tableCell--date { + width: 100px; + flex-shrink: 0; + font-size: 12px; + color: #6b7280; +} + +/* 撤销原因显示 */ +.authorization__revokeReason { + font-size: 12px; + color: #6b7280; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: help; +} + +/* 分页 */ +.authorization__pagination { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 16px 0; + border-top: 1px solid #e5e7eb; + margin-top: 8px; +} + +.authorization__pageBtn { + cursor: pointer; + border: 1px solid #e5e7eb; + border-radius: 6px; + background-color: #fff; + padding: 6px 12px; + font-size: 14px; + color: #1e293b; + font-family: inherit; + @include transition-fast; + + &:hover:not(:disabled) { + background-color: #f3f4f6; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.authorization__pageInfo { + font-size: 14px; + color: #6b7280; +} diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx index 9f31afc0..d85d7833 100644 --- a/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx @@ -1,127 +1,67 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { PageContainer } from '@/components/layout'; +import { toast } from '@/components/common'; import { cn } from '@/utils/helpers'; +import { useAuthorizations, useRevokeAuthorization } from '@/hooks/useAuthorizations'; +import type { RoleType, Authorization } from '@/types/authorization.types'; +import { ROLE_TYPE_LABELS } from '@/types/authorization.types'; import styles from './authorization.module.scss'; -/** - * 授权角色类型 - */ -type AuthorizationType = - | 'COMMUNITY' - | 'AUTH_PROVINCE_COMPANY' - | 'PROVINCE_COMPANY' - | 'AUTH_CITY_COMPANY' - | 'CITY_COMPANY'; - -/** - * 授权角色名称映射 - */ -const authorizationTypeLabels: Record = { - COMMUNITY: '社区', - AUTH_PROVINCE_COMPANY: '省团队', - PROVINCE_COMPANY: '正式省公司', - AUTH_CITY_COMPANY: '市团队', - CITY_COMPANY: '正式市公司', -}; - -/** - * 授权记录接口 - */ -interface AuthorizationItem { - id: string; - userId: string; - accountSequence: string; - nickname: string; - avatar?: string; - type: AuthorizationType; - region?: string; // 如: "广东省" 或 "广东省深圳市" - skipAssessment: boolean; - createdAt: string; - status: 'active' | 'revoked'; -} - -// 模拟授权数据 -const mockAuthorizations: AuthorizationItem[] = [ - { - id: '1', - userId: '12345', - accountSequence: 'D25122700001', - nickname: '张三', - type: 'PROVINCE_COMPANY', - region: '广东省', - skipAssessment: false, - createdAt: '2025-01-01', - status: 'active', - }, - { - id: '2', - userId: '12346', - accountSequence: 'D25122700002', - nickname: '李四', - type: 'AUTH_CITY_COMPANY', - region: '广东省深圳市', - skipAssessment: true, - createdAt: '2025-01-02', - status: 'active', - }, - { - id: '3', - userId: '12347', - accountSequence: 'D25122700003', - nickname: '王五', - type: 'COMMUNITY', - region: '榴莲社区A', - skipAssessment: false, - createdAt: '2025-01-03', - status: 'active', - }, -]; - /** * 授权管理页面 - * 简化版 - 保留核心授权功能 + * 使用真实 API 数据 */ export default function AuthorizationPage() { // 筛选状态 - const [filterType, setFilterType] = useState(''); - const [filterStatus, setFilterStatus] = useState<'active' | 'revoked' | ''>(''); + const [filterType, setFilterType] = useState(''); + const [filterStatus, setFilterStatus] = useState<'ACTIVE' | 'REVOKED' | ''>(''); const [searchKeyword, setSearchKeyword] = useState(''); + const [page, setPage] = useState(1); + const limit = 20; // 创建授权对话框状态 const [showCreateModal, setShowCreateModal] = useState(false); const [createForm, setCreateForm] = useState({ accountSequence: '', - type: 'COMMUNITY' as AuthorizationType, + type: 'COMMUNITY' as RoleType, region: '', skipAssessment: false, }); // 取消授权对话框状态 const [showRevokeModal, setShowRevokeModal] = useState(false); - const [revokeTarget, setRevokeTarget] = useState(null); + const [revokeTarget, setRevokeTarget] = useState(null); const [revokeReason, setRevokeReason] = useState(''); - // 筛选后的数据 - const filteredData = mockAuthorizations.filter((item) => { - if (filterType && item.type !== filterType) return false; - if (filterStatus && item.status !== filterStatus) return false; - if (searchKeyword) { - const keyword = searchKeyword.toLowerCase(); - return ( - item.nickname.toLowerCase().includes(keyword) || - item.accountSequence.toLowerCase().includes(keyword) || - (item.region && item.region.toLowerCase().includes(keyword)) - ); - } - return true; - }); + // 查询参数 + const queryParams = useMemo(() => ({ + roleType: filterType || undefined, + keyword: searchKeyword || undefined, + includeRevoked: filterStatus === 'REVOKED' ? true : filterStatus === '' ? true : false, + page, + limit, + }), [filterType, searchKeyword, filterStatus, page]); + + // 获取授权列表 + const { data, isLoading, error, refetch } = useAuthorizations(queryParams); + + // 撤销授权 mutation + const revokeMutation = useRevokeAuthorization(); + + // 根据状态筛选数据 + const filteredData = useMemo(() => { + if (!data?.items) return []; + if (!filterStatus) return data.items; + return data.items.filter(item => item.status === filterStatus); + }, [data?.items, filterStatus]); // 处理创建授权 const handleCreate = () => { // TODO: 调用API创建授权 console.log('创建授权:', createForm); + toast.info('创建授权功能开发中'); setShowCreateModal(false); setCreateForm({ accountSequence: '', @@ -132,27 +72,37 @@ export default function AuthorizationPage() { }; // 处理取消授权 - const handleRevoke = () => { - // TODO: 调用API取消授权 - console.log('取消授权:', revokeTarget, '原因:', revokeReason); - setShowRevokeModal(false); - setRevokeTarget(null); - setRevokeReason(''); + const handleRevoke = async () => { + if (!revokeTarget || !revokeReason) return; + + try { + await revokeMutation.mutateAsync({ + authorizationId: revokeTarget.id, + data: { reason: revokeReason }, + }); + toast.success('授权已撤销'); + setShowRevokeModal(false); + setRevokeTarget(null); + setRevokeReason(''); + } catch (err) { + console.error('撤销授权失败:', err); + toast.error('撤销授权失败'); + } }; // 打开取消授权对话框 - const openRevokeModal = (item: AuthorizationItem) => { + const openRevokeModal = (item: Authorization) => { setRevokeTarget(item); setShowRevokeModal(true); }; // 判断是否需要地区选择 - const needsRegion = (type: AuthorizationType) => { - return type !== 'COMMUNITY' || type === 'COMMUNITY'; + const needsRegion = (_type: RoleType) => { + return true; // 所有类型都需要地区/名称 }; // 获取地区输入提示 - const getRegionPlaceholder = (type: AuthorizationType) => { + const getRegionPlaceholder = (type: RoleType) => { switch (type) { case 'COMMUNITY': return '输入社区名称'; @@ -191,6 +141,12 @@ export default function AuthorizationPage() { ); + // 格式化日期 + const formatDate = (dateStr: string | null) => { + if (!dateStr) return '-'; + return new Date(dateStr).toLocaleDateString('zh-CN'); + }; + return (
@@ -211,11 +167,14 @@ export default function AuthorizationPage() { setFilterStatus(e.target.value as 'active' | 'revoked' | '')} + onChange={(e) => { + setFilterStatus(e.target.value as 'ACTIVE' | 'REVOKED' | ''); + setPage(1); + }} aria-label="授权状态" > - - + + setSearchKeyword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && refetch()} aria-label="关键词搜索" /> - +
- {/* 授权表格 */} -
-
-
- 头像 -
-
- 昵称 -
-
- 账户序列号 -
-
- 授权类型 -
-
- 地区/名称 -
-
- 状态 -
-
- 操作 -
+ {/* 加载状态 */} + {isLoading && ( +
加载中...
+ )} + + {/* 错误状态 */} + {error && ( +
+ 加载失败: {error instanceof Error ? error.message : '未知错误'} +
- {filteredData.map((item) => ( -
-
+ )} + + {/* 授权表格 */} + {!isLoading && !error && ( + <> +
+
-
-
- {item.nickname} -
-
- {item.accountSequence} -
-
- - {authorizationTypeLabels[item.type]} - -
-
- {item.region || '-'} -
-
- - {item.status === 'active' ? '有效' : '已撤销'} - + 头像 +
+
+ 昵称 +
+
+ 账户序列号 +
+
+ 授权类型 +
+
+ 地区/名称 +
+
+ 授权时间 +
+
+ 状态 +
+
+ 操作 +
-
- {item.status === 'active' && ( - - )} -
+
+
+
+ {item.nickname || '-'} +
+
+ {item.accountSequence} +
+
+ + {ROLE_TYPE_LABELS[item.roleType] || item.roleType} + +
+
+ {item.regionName || '-'} +
+
+ {formatDate(item.authorizedAt || item.createdAt)} +
+
+ + {item.status === 'ACTIVE' ? '有效' : '已撤销'} + +
+
+ {item.status === 'ACTIVE' && ( + + )} + {item.status === 'REVOKED' && item.revokeReason && ( + + 原因: {item.revokeReason.length > 10 ? `${item.revokeReason.slice(0, 10)}...` : item.revokeReason} + + )} +
+
+ ))} + {filteredData.length === 0 && ( +
暂无授权记录
+ )}
- ))} - {filteredData.length === 0 && ( -
暂无授权记录
- )} -
+ + {/* 分页 */} + {data && data.total > limit && ( +
+ + + 第 {page} 页 / 共 {Math.ceil(data.total / limit)} 页 (共 {data.total} 条) + + +
+ )} + + )}

帮助:在此管理所有授权用户,包括社区、省团队、市团队、正式省公司和正式市公司。 @@ -428,10 +463,10 @@ export default function AuthorizationPage() { className={styles.modal__select} value={createForm.type} onChange={(e) => - setCreateForm({ ...createForm, type: e.target.value as AuthorizationType }) + setCreateForm({ ...createForm, type: e.target.value as RoleType }) } > - {Object.entries(authorizationTypeLabels).map(([value, label]) => ( + {Object.entries(ROLE_TYPE_LABELS).map(([value, label]) => ( @@ -490,9 +525,9 @@ export default function AuthorizationPage() {

撤销授权

- 确定要撤销用户 {revokeTarget.nickname} ( + 确定要撤销用户 {revokeTarget.nickname || revokeTarget.accountSequence} ( {revokeTarget.accountSequence}) 的{' '} - {authorizationTypeLabels[revokeTarget.type]} 授权吗? + {ROLE_TYPE_LABELS[revokeTarget.roleType] || revokeTarget.roleType} 授权吗?

@@ -515,9 +550,9 @@ export default function AuthorizationPage() {
diff --git a/frontend/admin-web/src/hooks/index.ts b/frontend/admin-web/src/hooks/index.ts index 8f7ccc6d..5caae6c2 100644 --- a/frontend/admin-web/src/hooks/index.ts +++ b/frontend/admin-web/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useDashboard'; export * from './useUsers'; +export * from './useAuthorizations'; diff --git a/frontend/admin-web/src/hooks/useAuthorizations.ts b/frontend/admin-web/src/hooks/useAuthorizations.ts new file mode 100644 index 00000000..09df65c3 --- /dev/null +++ b/frontend/admin-web/src/hooks/useAuthorizations.ts @@ -0,0 +1,121 @@ +/** + * 授权管理 Hooks + * 使用 React Query 进行数据获取和缓存管理 + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { authorizationService } from '@/services/authorizationService'; +import type { + QueryAuthorizationsParams, + RevokeAuthorizationRequest, + GrantCommunityRequest, + GrantProvinceCompanyRequest, + GrantCityCompanyRequest, + GrantAuthProvinceCompanyRequest, + GrantAuthCityCompanyRequest, +} from '@/types/authorization.types'; + +/** Query Keys */ +export const authorizationKeys = { + all: ['authorizations'] as const, + list: (params: QueryAuthorizationsParams) => + [...authorizationKeys.all, 'list', params] as const, +}; + +/** + * 获取授权列表 + */ +export function useAuthorizations(params: QueryAuthorizationsParams = {}) { + return useQuery({ + queryKey: authorizationKeys.list(params), + queryFn: () => authorizationService.getList(params), + staleTime: 30 * 1000, // 30秒后标记为过期 + gcTime: 5 * 60 * 1000, // 5分钟后垃圾回收 + }); +} + +/** + * 撤销授权 + */ +export function useRevokeAuthorization() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ authorizationId, data }: { authorizationId: string; data: RevokeAuthorizationRequest }) => + authorizationService.revoke(authorizationId, data), + onSuccess: () => { + // 撤销成功后刷新所有相关列表 + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} + +/** + * 授权社区 + */ +export function useGrantCommunity() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: GrantCommunityRequest) => authorizationService.grantCommunity(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} + +/** + * 授权正式省公司 + */ +export function useGrantProvinceCompany() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: GrantProvinceCompanyRequest) => authorizationService.grantProvinceCompany(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} + +/** + * 授权正式市公司 + */ +export function useGrantCityCompany() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: GrantCityCompanyRequest) => authorizationService.grantCityCompany(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} + +/** + * 授权省团队 + */ +export function useGrantAuthProvinceCompany() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: GrantAuthProvinceCompanyRequest) => authorizationService.grantAuthProvinceCompany(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} + +/** + * 授权市团队 + */ +export function useGrantAuthCityCompany() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: GrantAuthCityCompanyRequest) => authorizationService.grantAuthCityCompany(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: authorizationKeys.all }); + }, + }); +} diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index 5bc40977..e42705b2 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -35,6 +35,15 @@ export const API_ENDPOINTS = { // 授权管理 (authorization-service) AUTHORIZATION: { + // 管理员授权管理 + ADMIN_LIST: '/v1/admin/authorizations', + ADMIN_REVOKE: (id: string) => `/v1/admin/authorizations/${id}/revoke`, + ADMIN_GRANT_COMMUNITY: '/v1/admin/authorizations/community', + ADMIN_GRANT_PROVINCE_COMPANY: '/v1/admin/authorizations/province-company', + ADMIN_GRANT_CITY_COMPANY: '/v1/admin/authorizations/city-company', + ADMIN_GRANT_AUTH_PROVINCE_COMPANY: '/v1/admin/authorizations/auth-province-company', + ADMIN_GRANT_AUTH_CITY_COMPANY: '/v1/admin/authorizations/auth-city-company', + // 其他授权端点 PROVINCE_COMPANIES: '/v1/authorizations/province-companies', CITY_COMPANIES: '/v1/authorizations/city-companies', PROVINCE_RULES: '/v1/authorizations/province-rules', diff --git a/frontend/admin-web/src/services/authorizationService.ts b/frontend/admin-web/src/services/authorizationService.ts new file mode 100644 index 00000000..5033a522 --- /dev/null +++ b/frontend/admin-web/src/services/authorizationService.ts @@ -0,0 +1,93 @@ +/** + * 授权管理服务 + * 用于管理后台查询和管理用户授权 + */ + +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; +import type { + AuthorizationListResponse, + QueryAuthorizationsParams, + RevokeAuthorizationRequest, + GrantCommunityRequest, + GrantProvinceCompanyRequest, + GrantCityCompanyRequest, + GrantAuthProvinceCompanyRequest, + GrantAuthCityCompanyRequest, +} from '@/types/authorization.types'; + +/** + * 授权管理服务 + * + * API 响应结构(经过 apiClient 拦截器解包后): + * { success: true, data: { code: "OK", message: "success", data: {...} } } + * + * 需要访问 .data.data 获取实际业务数据 + */ +export const authorizationService = { + /** + * 查询授权列表 + */ + async getList(params: QueryAuthorizationsParams = {}): Promise { + const response = await apiClient.get(API_ENDPOINTS.AUTHORIZATION.ADMIN_LIST, { params }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (response as any)?.data?.data; + return result ?? { items: [], total: 0, page: 1, limit: 20 }; + }, + + /** + * 撤销授权 + */ + async revoke(authorizationId: string, data: RevokeAuthorizationRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_REVOKE(authorizationId), data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '授权已撤销' }; + }, + + /** + * 授权社区 + */ + async grantCommunity(data: GrantCommunityRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_COMMUNITY, data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '社区授权成功' }; + }, + + /** + * 授权正式省公司 + */ + async grantProvinceCompany(data: GrantProvinceCompanyRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_PROVINCE_COMPANY, data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '正式省公司授权成功' }; + }, + + /** + * 授权正式市公司 + */ + async grantCityCompany(data: GrantCityCompanyRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_CITY_COMPANY, data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '正式市公司授权成功' }; + }, + + /** + * 授权省团队 + */ + async grantAuthProvinceCompany(data: GrantAuthProvinceCompanyRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_AUTH_PROVINCE_COMPANY, data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '省团队授权成功' }; + }, + + /** + * 授权市团队 + */ + async grantAuthCityCompany(data: GrantAuthCityCompanyRequest): Promise<{ message: string }> { + const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_AUTH_CITY_COMPANY, data); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (response as any)?.data?.data ?? { message: '市团队授权成功' }; + }, +}; + +export default authorizationService; diff --git a/frontend/admin-web/src/types/authorization.types.ts b/frontend/admin-web/src/types/authorization.types.ts new file mode 100644 index 00000000..176eca5c --- /dev/null +++ b/frontend/admin-web/src/types/authorization.types.ts @@ -0,0 +1,145 @@ +// 授权管理类型定义 + +/** + * 角色类型 + */ +export type RoleType = + | 'COMMUNITY' + | 'AUTH_PROVINCE_COMPANY' + | 'PROVINCE_COMPANY' + | 'AUTH_CITY_COMPANY' + | 'CITY_COMPANY'; + +/** + * 授权状态 + */ +export type AuthorizationStatus = 'ACTIVE' | 'REVOKED'; + +/** + * 授权信息 + */ +export interface Authorization { + id: string; + accountSequence: string; + nickname: string; + avatar: string | null; + roleType: RoleType; + regionName: string; + status: AuthorizationStatus; + benefitActive: boolean; + createdAt: string; + authorizedAt: string | null; + revokedAt: string | null; + revokeReason: string | null; +} + +/** + * 查询授权列表参数 + */ +export interface QueryAuthorizationsParams { + roleType?: RoleType; + keyword?: string; + includeRevoked?: boolean; + page?: number; + limit?: number; +} + +/** + * 授权列表响应 + */ +export interface AuthorizationListResponse { + items: Authorization[]; + total: number; + page: number; + limit: number; +} + +/** + * 撤销授权请求 + */ +export interface RevokeAuthorizationRequest { + reason: string; +} + +/** + * 授权社区请求 + */ +export interface GrantCommunityRequest { + userId: string; + accountSequence: string; + communityName: string; + skipAssessment?: boolean; +} + +/** + * 授权省公司请求 + */ +export interface GrantProvinceCompanyRequest { + userId: string; + accountSequence: string; + provinceCode: string; + provinceName: string; + skipAssessment?: boolean; +} + +/** + * 授权市公司请求 + */ +export interface GrantCityCompanyRequest { + userId: string; + accountSequence: string; + cityCode: string; + cityName: string; + skipAssessment?: boolean; +} + +/** + * 授权省团队请求 + */ +export interface GrantAuthProvinceCompanyRequest { + userId: string; + accountSequence: string; + provinceCode: string; + provinceName: string; + skipAssessment?: boolean; +} + +/** + * 授权市团队请求 + */ +export interface GrantAuthCityCompanyRequest { + userId: string; + accountSequence: string; + cityCode: string; + cityName: string; + skipAssessment?: boolean; +} + +/** + * 角色类型显示名称 + */ +export const ROLE_TYPE_LABELS: Record = { + COMMUNITY: '社区', + AUTH_PROVINCE_COMPANY: '省团队', + PROVINCE_COMPANY: '正式省公司', + AUTH_CITY_COMPANY: '市团队', + CITY_COMPANY: '正式市公司', +}; + +/** + * 获取角色类型显示名称 + */ +export function getRoleTypeLabel(roleType: RoleType): string { + return ROLE_TYPE_LABELS[roleType] || roleType; +} + +/** + * 角色类型筛选选项 + */ +export const ROLE_TYPE_OPTIONS = [ + { value: 'COMMUNITY', label: '社区' }, + { value: 'AUTH_PROVINCE_COMPANY', label: '省团队' }, + { value: 'PROVINCE_COMPANY', label: '正式省公司' }, + { value: 'AUTH_CITY_COMPANY', label: '市团队' }, + { value: 'CITY_COMPANY', label: '正式市公司' }, +] as const; diff --git a/frontend/admin-web/src/types/index.ts b/frontend/admin-web/src/types/index.ts index 3d91a0ec..b478c233 100644 --- a/frontend/admin-web/src/types/index.ts +++ b/frontend/admin-web/src/types/index.ts @@ -8,3 +8,4 @@ export * from './common.types'; export * from './dashboard.types'; export * from './pending-action.types'; export * from './withdrawal.types'; +export * from './authorization.types';