diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java index 7c6482d..b958c10 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidAuthServiceImpl.java @@ -9,6 +9,7 @@ import com.imeeting.service.android.AndroidAuthService; import com.imeeting.service.android.AndroidDeviceBindingService; import com.imeeting.service.biz.LicenseService; import com.unisbase.common.exception.BusinessException; +import com.unisbase.common.exception.ErrorCodeEnum; import com.unisbase.dto.InternalAuthCheckResponse; import com.unisbase.security.LoginUser; import com.unisbase.service.TokenValidationService; @@ -205,14 +206,14 @@ public class AndroidAuthServiceImpl implements AndroidAuthService { private InternalAuthCheckResponse validateToken(String token) { String resolvedToken = normalizeToken(token); if (!StringUtils.hasText(resolvedToken)) { - throw new RuntimeException("Missing Android access token"); + throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),"Missing Android access token"); } InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(resolvedToken); if (authResult == null || !authResult.isValid()) { - throw new RuntimeException(authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage()); + throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage()); } if (authResult.getUserId() == null || authResult.getTenantId() == null) { - throw new RuntimeException("Android access token is missing user or tenant context"); + throw new BusinessException(ErrorCodeEnum.UNAUTHORIZED.getCode(),"Android access token is missing user or tenant context"); } return authResult; } diff --git a/frontend/src/pages/business/MeetingPointsManagement.tsx b/frontend/src/pages/business/MeetingPointsManagement.tsx index f6e5d62..ece57e1 100644 --- a/frontend/src/pages/business/MeetingPointsManagement.tsx +++ b/frontend/src/pages/business/MeetingPointsManagement.tsx @@ -1,11 +1,10 @@ -import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; +import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons"; import { listUsers } from "@/api"; import { Button, Card, Col, Descriptions, - Empty, Form, Input, InputNumber, @@ -16,6 +15,7 @@ import { Space, Statistic, Table, + Tabs, Tag, Typography, } from "antd"; @@ -36,7 +36,7 @@ import { } from "@/api/business/meetingPoints"; import type { SysUser } from "@/types"; -const { Text, Title } = Typography; +const { Text } = Typography; const POINTS_TYPE_OPTIONS = [ { label: "全部类型", value: "" }, @@ -93,6 +93,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { if (!overview) { return []; } + const isAdmin = Boolean(overview.admin); const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC; const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL; @@ -113,14 +114,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { key: "public-balance", title: "公共账户余额", value: overview.publicBalance ?? 0, - accent: "linear-gradient(135deg, rgba(18, 87, 241, 0.14), rgba(18, 87, 241, 0.02))", + accent: "rgba(24, 144, 255, 0.08)", note: "用于统一分配和公共扣费", }, { key: "public-used", - title: "公共账户累计消耗积分", + title: "公共账户累计消耗", value: overview.publicTotalPointsUsed ?? 0, - accent: "linear-gradient(135deg, rgba(11, 132, 98, 0.14), rgba(11, 132, 98, 0.02))", + accent: "rgba(82, 196, 26, 0.08)", note: "公共账户历史累计扣减", }, ); @@ -132,14 +133,14 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { key: "personal-balance", title: isAdmin ? "个人账户余额汇总" : "个人账户余额", value: overview.personalBalance ?? 0, - accent: "linear-gradient(135deg, rgba(148, 77, 255, 0.14), rgba(148, 77, 255, 0.02))", + accent: "rgba(114, 46, 209, 0.08)", note: isAdmin ? "管理员视角下的个人账户汇总" : "当前账号可用积分", }, { key: "personal-used", - title: isAdmin ? "个人账户累计消耗积分汇总" : "个人账户累计消耗积分", + title: isAdmin ? "个人账户累计消耗汇总" : "个人账户累计消耗", value: overview.personalTotalPointsUsed ?? 0, - accent: "linear-gradient(135deg, rgba(237, 108, 2, 0.14), rgba(237, 108, 2, 0.02))", + accent: "rgba(250, 140, 22, 0.08)", note: isAdmin ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减", }, ); @@ -149,7 +150,7 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { key: "charge-count", title: "累计消耗次数", value: overview.totalChargeCount ?? 0, - accent: "linear-gradient(135deg, rgba(48, 48, 48, 0.12), rgba(48, 48, 48, 0.02))", + accent: "rgba(38, 38, 38, 0.06)", note: "已发生扣费的总结记录数", }); @@ -168,6 +169,11 @@ export default function MeetingPointsManagement() { const [transferOpen, setTransferOpen] = useState(false); const [detail, setDetail] = useState(null); const [users, setUsers] = useState([]); + const [contentTab, setContentTab] = useState("ledger"); + const [personalAccountPagination, setPersonalAccountPagination] = useState({ + current: 1, + pageSize: 10, + }); const [params, setParams] = useState({ current: 1, size: 20, @@ -180,8 +186,46 @@ export default function MeetingPointsManagement() { const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC; const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL; const showTransferButton = isAdmin && !isPublicOnly; - const showPersonalGallery = isAdmin && !isPublicOnly; - const summaryCards = buildSummaryCards(overview); + const showPersonalAccountSection = Boolean(overview) && !isPublicOnly; + const summaryCards = useMemo(() => buildSummaryCards(overview), [overview]); + + const personalAccountRows = useMemo(() => { + if (!overview || isPublicOnly) { + return []; + } + + if (isAdmin) { + return overview.personalAccounts || []; + } + + return [ + { + userId: -1, + displayName: "当前账号", + currentBalance: overview.personalBalance ?? 0, + totalPointsUsed: overview.personalTotalPointsUsed ?? 0, + }, + ]; + }, [overview, isAdmin, isPublicOnly]); + + const pagedPersonalAccounts = useMemo(() => { + const start = (personalAccountPagination.current - 1) * personalAccountPagination.pageSize; + const end = start + personalAccountPagination.pageSize; + return personalAccountRows.slice(start, end); + }, [personalAccountPagination, personalAccountRows]); + + useEffect(() => { + setPersonalAccountPagination((prev) => { + const maxPage = Math.max(1, Math.ceil(personalAccountRows.length / prev.pageSize)); + return prev.current > maxPage ? { ...prev, current: maxPage } : prev; + }); + }, [personalAccountRows.length]); + + useEffect(() => { + if (!isPersonalOnly && contentTab !== "ledger") { + setContentTab("ledger"); + } + }, [contentTab, isPersonalOnly]); const loadOverview = async () => { const data = await getMeetingPointsOverview(); @@ -267,118 +311,205 @@ export default function MeetingPointsManagement() { } }; - const columns = useMemo( - () => [ - { - title: "用户", - dataIndex: "ownerUserName", - key: "ownerUserName", - width: 140, - render: (value: string) => {value || "-"}, - }, - { - title: "扣费账户", - dataIndex: "chargeAccountType", - key: "chargeAccountType", - width: 120, - render: (value: string) => {getAccountTypeLabel(value)}, - }, - { - title: "消耗类型", - dataIndex: "pointsType", - key: "pointsType", - width: 100, - render: (value: string) => {getPointsTypeLabel(value)}, - }, - { - title: "消耗积分", - dataIndex: "consumedPoints", - key: "consumedPoints", - width: 110, - render: (value: number) => {value ?? 0}, - }, - { - title: "会议标题", - dataIndex: "meetingTitle", - key: "meetingTitle", - ellipsis: true, - render: (value: string) => {value || "-"}, - }, - { - title: "触发类型", - dataIndex: "chargeTriggerType", - key: "chargeTriggerType", - width: 130, - render: (value: string) => {getChargeTriggerLabel(value)}, - }, - { - title: "消耗时间", - dataIndex: "createdAt", - key: "createdAt", - width: 180, - render: (value: string) => {formatDateTime(value)}, - }, - { - title: "操作", - key: "action", - width: 88, - fixed: "right" as const, - render: (_: unknown, record: MeetingPointsLedgerListItemVO) => ( - - ), - }, - ], - [], + const ledgerColumns = [ + { + title: "用户", + dataIndex: "ownerUserName", + key: "ownerUserName", + width: 140, + render: (value: string) => {value || "-"}, + }, + { + title: "扣费账户", + dataIndex: "chargeAccountType", + key: "chargeAccountType", + width: 120, + render: (value: string) => {getAccountTypeLabel(value)}, + }, + { + title: "消耗类型", + dataIndex: "pointsType", + key: "pointsType", + width: 100, + render: (value: string) => {getPointsTypeLabel(value)}, + }, + { + title: "消耗积分", + dataIndex: "consumedPoints", + key: "consumedPoints", + width: 110, + render: (value: number) => {value ?? 0}, + }, + { + title: "会议标题", + dataIndex: "meetingTitle", + key: "meetingTitle", + ellipsis: true, + render: (value: string) => {value || "-"}, + }, + { + title: "触发类型", + dataIndex: "chargeTriggerType", + key: "chargeTriggerType", + width: 130, + render: (value: string) => {getChargeTriggerLabel(value)}, + }, + { + title: "消耗时间", + dataIndex: "createdAt", + key: "createdAt", + width: 180, + render: (value: string) => {formatDateTime(value)}, + }, + { + title: "操作", + key: "action", + width: 88, + fixed: "right" as const, + render: (_: unknown, record: MeetingPointsLedgerListItemVO) => ( + + ), + }, + ]; + + const personalAccountColumns = [ + { + title: "序号", + key: "index", + width: 80, + render: (_: unknown, __: MeetingPointsPersonalAccountVO, index: number) => + (personalAccountPagination.current - 1) * personalAccountPagination.pageSize + index + 1, + }, + { + title: "账户", + key: "displayName", + width: 260, + render: (_: unknown, record: MeetingPointsPersonalAccountVO) => ( + + {record.displayName || record.username || `用户 #${record.userId}`} + {record.username ? `@${record.username}` : `ID ${record.userId}`} + + ), + }, + { + title: "当前余额", + dataIndex: "currentBalance", + key: "currentBalance", + width: 140, + render: (value: number) => {value ?? 0}, + }, + { + title: "累计消耗", + dataIndex: "totalPointsUsed", + key: "totalPointsUsed", + width: 140, + render: (value: number) => {value ?? 0}, + }, + { + title: "账户类型", + key: "accountType", + width: 120, + render: () => 个人账户, + }, + ]; + + const chargeItemColumns = [ + { + title: "阶段", + dataIndex: "chargeStage", + key: "chargeStage", + render: (value: string) => {getPointsTypeLabel(value)}, + }, + { + title: "扣费账户", + dataIndex: "accountType", + key: "accountType", + render: (value: string) => getAccountTypeLabel(value), + }, + { + title: "账户用户ID", + dataIndex: "accountUserId", + key: "accountUserId", + }, + { + title: "扣费积分", + dataIndex: "chargedPoints", + key: "chargedPoints", + }, + { + title: "扣费前余额", + dataIndex: "balanceBefore", + key: "balanceBefore", + }, + { + title: "扣费后余额", + dataIndex: "balanceAfter", + key: "balanceAfter", + }, + ]; + + const ledgerTableContent = ( +
+
+ + rowKey="id" + columns={ledgerColumns} + dataSource={records} + loading={loading} + totalCount={total} + scroll={{ x: 1200,y: 400 }} + pagination={false} + /> +
+
+ { + const nextParams = { ...params, current: page, size: pageSize }; + setParams(nextParams); + void loadPage(nextParams); + }} + /> +
+
); - const chargeItemColumns = useMemo( - () => [ - { - title: "阶段", - dataIndex: "chargeStage", - key: "chargeStage", - render: (value: string) => {getPointsTypeLabel(value)}, - }, - { - title: "扣费账户", - dataIndex: "accountType", - key: "accountType", - render: (value: string) => getAccountTypeLabel(value), - }, - { - title: "账户用户ID", - dataIndex: "accountUserId", - key: "accountUserId", - }, - { - title: "扣费积分", - dataIndex: "chargedPoints", - key: "chargedPoints", - }, - { - title: "扣费前余额", - dataIndex: "balanceBefore", - key: "balanceBefore", - }, - { - title: "扣费后余额", - dataIndex: "balanceAfter", - key: "balanceAfter", - }, - ], - [], + const personalAccountTableContent = ( +
+
+ + rowKey="userId" + columns={personalAccountColumns} + dataSource={pagedPersonalAccounts} + totalCount={personalAccountRows.length} + scroll={{ x: 1200,y: 400 }} + pagination={false} + /> +
+
+ { + setPersonalAccountPagination({ current: page, pageSize }); + }} + /> +
+
); return ( - 当前模式:{getAccountModeLabel(overview?.accountMode)} - 优先级:{getChargePriorityLabel(overview?.chargePriority)} {showTransferButton ? (