diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java index 8402c22..dd7cb6a 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java @@ -213,7 +213,7 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService } private String buildStoredChunkFileName(Integer chunkIndex, String originalFileName) { - String normalizedSourceName = originalFileName == null ? "" : Paths.get(originalFileName.trim()).getFileName().toString(); + String normalizedSourceName = normalizeChunkSourceFileName(originalFileName); int extensionIndex = normalizedSourceName.lastIndexOf('.'); String extension = extensionIndex >= 0 ? normalizedSourceName.substring(extensionIndex) : ""; String safeExtension = extension.isBlank() ? ".bin" : extension; @@ -222,7 +222,7 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService private String buildMergedOriginalFilename(AndroidChunkUploadSessionState state, Path mergedFile) { if (state.getFileName() != null && !state.getFileName().isBlank()) { - String normalizedSourceName = Paths.get(state.getFileName().trim()).getFileName().toString(); + String normalizedSourceName = normalizeChunkSourceFileName(state.getFileName()); int extensionIndex = normalizedSourceName.lastIndexOf('.'); if (extensionIndex >= 0) { return "merged" + normalizedSourceName.substring(extensionIndex); @@ -236,11 +236,22 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService if (chunkPath == null || chunkPath.getFileName() == null) { return ".bin"; } - String fileName = chunkPath.getFileName().toString(); + String fileName = normalizeChunkSourceFileName(chunkPath.getFileName().toString()); int extensionIndex = fileName.lastIndexOf('.'); return extensionIndex >= 0 ? fileName.substring(extensionIndex) : ".bin"; } + private String normalizeChunkSourceFileName(String fileName) { + if (fileName == null) { + return ""; + } + String normalized = Paths.get(fileName.trim()).getFileName().toString(); + if (normalized.endsWith(".pending")) { + return normalized.substring(0, normalized.length() - ".pending".length()); + } + return normalized; + } + private void writeConcatListFile(Path concatList, List chunkPaths) throws IOException { List lines = new ArrayList<>(chunkPaths.size()); for (Path chunkPath : chunkPaths) { diff --git a/frontend/src/pages/business/TenantMeetingPointsSettings.tsx b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx new file mode 100644 index 0000000..80b3f43 --- /dev/null +++ b/frontend/src/pages/business/TenantMeetingPointsSettings.tsx @@ -0,0 +1,344 @@ +import { ReloadOutlined, SearchOutlined } from "@ant-design/icons"; +import { getCurrentUser } from "@/api"; +import AppPagination from "@/components/shared/AppPagination"; +import ListTable from "@/components/shared/ListTable/ListTable"; +import PageContainer from "@/components/shared/PageContainer"; +import { + getCurrentTenantMeetingPointsSetting, + pageTenantMeetingPointsSettings, + type TenantMeetingPointsSettingVO, + updateTenantMeetingPointsBalanceCheck, +} from "@/api/business/meetingPoints"; +import type { UserProfile } from "@/types"; +import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd"; +import { useEffect, useState } from "react"; + +const { Text } = Typography; + +function formatDateTime(value?: string) { + return value ? value.replace("T", " ").substring(0, 19) : "-"; +} + +function renderStatusTag(enabled?: boolean) { + return enabled === false + ? 无限余额模式 + : 校验余额模式; +} + +export default function TenantMeetingPointsSettings() { + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(false); + const [switchingTenantId, setSwitchingTenantId] = useState(null); + const [records, setRecords] = useState([]); + const [total, setTotal] = useState(0); + const [currentTenantSetting, setCurrentTenantSetting] = useState(null); + const [params, setParams] = useState({ + current: 1, + size: 20, + tenantName: "", + tenantCode: "", + balanceCheckEnabled: "", + }); + + const isPlatformAdmin = Boolean(profile?.isPlatformAdmin); + const isTenantAdmin = Boolean(profile?.isTenantAdmin); + + const loadPlatformPage = async (nextParams = params) => { + setLoading(true); + try { + const result = await pageTenantMeetingPointsSettings({ + current: nextParams.current, + size: nextParams.size, + tenantName: nextParams.tenantName || undefined, + tenantCode: nextParams.tenantCode || undefined, + balanceCheckEnabled: nextParams.balanceCheckEnabled === "" ? undefined : nextParams.balanceCheckEnabled === "true", + }); + setRecords(result.records || []); + setTotal(result.total || 0); + } finally { + setLoading(false); + } + }; + + const loadCurrentTenant = async () => { + setLoading(true); + try { + const data = await getCurrentTenantMeetingPointsSetting(); + setCurrentTenantSetting(data); + } finally { + setLoading(false); + } + }; + + const loadData = async () => { + const nextProfile = await getCurrentUser(); + setProfile(nextProfile); + if (nextProfile.isPlatformAdmin) { + await loadPlatformPage(); + return; + } + if (nextProfile.isTenantAdmin) { + await loadCurrentTenant(); + return; + } + setCurrentTenantSetting(null); + setRecords([]); + setTotal(0); + }; + + useEffect(() => { + void loadData(); + }, []); + + const handleRefresh = async () => { + if (isPlatformAdmin) { + await loadPlatformPage(); + } else if (isTenantAdmin) { + await loadCurrentTenant(); + } + message.success("已刷新租户积分校验配置"); + }; + + const confirmSwitch = (record: TenantMeetingPointsSettingVO, nextEnabled: boolean) => { + Modal.confirm({ + title: nextEnabled ? "开启余额校验" : "关闭余额校验", + content: nextEnabled + ? "开启后将按当前账面余额重新执行后续会议提交校验。" + : "关闭后将进入无限余额模式,只记录消耗和流水,不扣减账面余额。", + okText: "确认", + cancelText: "取消", + onOk: async () => { + setSwitchingTenantId(record.tenantId); + try { + await updateTenantMeetingPointsBalanceCheck(record.tenantId, { balanceCheckEnabled: nextEnabled }); + message.success("余额校验开关已更新"); + if (isPlatformAdmin) { + await loadPlatformPage(); + } else { + await loadCurrentTenant(); + } + } finally { + setSwitchingTenantId(null); + } + }, + }); + }; + + const columns = [ + { + title: "租户名称", + dataIndex: "tenantName", + key: "tenantName", + width: 180, + render: (value: string) => {value || "-"}, + }, + { + title: "租户编码", + dataIndex: "tenantCode", + key: "tenantCode", + width: 160, + render: (value: string) => {value || "-"}, + }, + { + title: "余额校验状态", + dataIndex: "balanceCheckEnabled", + key: "balanceCheckEnabled", + width: 160, + render: (value: boolean) => renderStatusTag(value), + }, + { + title: "公共账户余额", + dataIndex: "publicBalance", + key: "publicBalance", + width: 140, + render: (value: number) => value ?? 0, + }, + { + title: "公共账户累计消耗", + dataIndex: "publicTotalPointsUsed", + key: "publicTotalPointsUsed", + width: 160, + render: (value: number) => value ?? 0, + }, + { + title: "最近切换时间", + dataIndex: "lastSwitchAt", + key: "lastSwitchAt", + width: 180, + render: (value: string) => formatDateTime(value), + }, + { + title: "最近切换人", + dataIndex: "lastSwitchByName", + key: "lastSwitchByName", + width: 160, + render: (value: string) => value || "-", + }, + { + title: "操作", + key: "action", + width: 140, + fixed: "right" as const, + render: (_: unknown, record: TenantMeetingPointsSettingVO) => ( + + ), + }, + ]; + + const renderTenantAdminCard = () => { + if (!currentTenantSetting) { + return null; + } + return ( + + +
+ + {currentTenantSetting.tenantName || "当前租户"} + 租户编码:{currentTenantSetting.tenantCode || "-"} + + {renderStatusTag(currentTenantSetting.balanceCheckEnabled)} +
+ + + + + {currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"} + + + {currentTenantSetting.balanceCheckEnabled + ? "后续会议提交将按当前账面余额执行拦截与扣减。" + : "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"} + + + + + + + + + + + + 最近切换时间:{formatDateTime(currentTenantSetting.lastSwitchAt)} + 最近切换人:{currentTenantSetting.lastSwitchByName || "-"} + + + + + + +
+
+ ); + }; + + return ( + } onClick={() => void handleRefresh()}> + 刷新 + + } + toolbar={isPlatformAdmin ? ( + + setParams((prev) => ({ ...prev, tenantName: event.target.value }))} + style={{ width: 220 }} + prefix={} + allowClear + /> + setParams((prev) => ({ ...prev, tenantCode: event.target.value }))} + style={{ width: 180 }} + allowClear + /> +