Remove application license gate

This commit is contained in:
2026-07-02 12:58:44 +08:00
parent 9c5cbc4637
commit 2da2121829
29 changed files with 51 additions and 1923 deletions
+5 -31
View File
@@ -3802,29 +3802,6 @@ input[type="checkbox"] {
min-width: 0;
}
.status-license-chip {
display: inline-flex;
align-items: center;
padding: 3px 7px;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-panel-muted);
color: var(--color-text-secondary);
line-height: 1.2;
}
.status-license-chip.ok {
border-color: rgba(22, 163, 74, 0.24);
background: rgba(22, 163, 74, 0.08);
color: #166534;
}
.status-license-chip.fail {
border-color: rgba(220, 38, 38, 0.24);
background: rgba(220, 38, 38, 0.08);
color: #991b1b;
}
.status-actions {
display: flex;
justify-content: flex-end;
@@ -3891,12 +3868,6 @@ input[type="checkbox"] {
white-space: nowrap;
}
.status-license {
font-size: 0.75em;
color: var(--color-text-muted);
white-space: nowrap;
}
.status-task-bar {
width: 120px;
height: 6px;
@@ -6194,12 +6165,15 @@ input[type="checkbox"] {
.production-workspace-shell .dinsar-production-shell,
.production-workspace-shell .dinsar-products-page {
width: 100%;
max-width: none;
margin-left: 0;
margin-right: 0;
box-sizing: border-box;
}
.production-workspace-shell .dinsar-production-shell {
.production-workspace-shell .dinsar-production-shell,
.production-workspace-shell .dinsar-products-page {
padding: 0;
}
@@ -7090,7 +7064,7 @@ input[type="checkbox"] {
}
.dinsar-products-catalog-section .dinsar-catalog-workspace {
grid-template-columns: minmax(360px, 420px) minmax(0, 1fr);
grid-template-columns: minmax(380px, 0.36fr) minmax(0, 1fr);
}
.sbas-products-page {
+7 -37
View File
@@ -184,22 +184,12 @@ function App() {
// --- Zustand stores ---
const {
currentUser, setCurrentUser, authChecked, setAuthChecked,
licenseStatus, setLicenseStatus, licenseLoading, setLicenseLoading,
licenseUploadStatus, setLicenseUploadStatus, licenseFileName, setLicenseFileName,
healthStatus, setHealthStatus, healthLoading, setHealthLoading, healthError, setHealthError,
} = useAuthStore(useShallow((state) => ({
currentUser: state.currentUser,
setCurrentUser: state.setCurrentUser,
authChecked: state.authChecked,
setAuthChecked: state.setAuthChecked,
licenseStatus: state.licenseStatus,
setLicenseStatus: state.setLicenseStatus,
licenseLoading: state.licenseLoading,
setLicenseLoading: state.setLicenseLoading,
licenseUploadStatus: state.licenseUploadStatus,
setLicenseUploadStatus: state.setLicenseUploadStatus,
licenseFileName: state.licenseFileName,
setLicenseFileName: state.setLicenseFileName,
healthStatus: state.healthStatus,
setHealthStatus: state.setHealthStatus,
healthLoading: state.healthLoading,
@@ -405,13 +395,12 @@ function App() {
const floodPairPreviewLayerRef = useRef(null);
const floodVectorLayersRef = useRef({});
const mapRegionLayerRef = useRef(null);
const prevLicenseOkRef = useRef(false);
const prevAppReadyRef = useRef(false);
const initializeAppDataRef = useRef(null);
const addLogRef = useRef(addLog);
const handleTaskCompletionRef = useRef(null);
const updateLayerTooltipRef = useRef(null);
const radarSearchRequestSeqRef = useRef(0);
const licenseFileRef = useRef(null);
const foundPairsRef = useRef(foundPairs);
const hazardLayersRef = useRef({});
const dinsarResultLayersRef = useRef({});
@@ -712,14 +701,10 @@ function App() {
const {
handleLoginSuccess,
handleLogout,
fetchLicenseStatus,
fetchHealthStatus,
handleLicenseUpload,
} = useAppAuthLifecycle({
ensureCanOperate,
clearRadarSearchResults,
radarSearchRequestSeqRef,
prevLicenseOkRef,
aoeLayerRef,
activeLayersRef,
radarPreviewLayersRef,
@@ -727,13 +712,9 @@ function App() {
setCurrentUser,
setAuthChecked,
setPendingTaskIds,
setLicenseLoading,
setLicenseStatus,
setHealthLoading,
setHealthError,
setHealthStatus,
setLicenseFileName,
setLicenseUploadStatus,
setAoiLayer,
setAllData,
setRadarPagination,
@@ -838,14 +819,14 @@ function App() {
}, [baseLayerKey, currentUser?.id]);
useEffect(() => {
if (licenseLoading || !authChecked) return;
const isOk = !!licenseStatus?.ok && !!currentUser;
const prevOk = prevLicenseOkRef.current;
if (isOk && !prevOk) {
if (!authChecked) return;
const isReady = !!currentUser;
const wasReady = prevAppReadyRef.current;
if (isReady && !wasReady) {
initializeAppDataRef.current?.();
}
prevLicenseOkRef.current = isOk;
}, [licenseLoading, licenseStatus?.ok, authChecked, currentUser]);
prevAppReadyRef.current = isReady;
}, [authChecked, currentUser]);
const {
fetchDinsarResults,
@@ -917,7 +898,6 @@ function App() {
handleCancelActiveTasks,
} = useGlobalTaskControl({
currentUser,
licenseOk: !!licenseStatus?.ok,
activeTasks,
setActiveTasks,
setRuntimeSummary,
@@ -2002,7 +1982,6 @@ function App() {
const selectedPairsCount = foundPairs.filter(p => p.isSelected).length;
const hasEnoughRadarScenesForPlanning = radarImagingDates.length >= 2;
const licenseOk = !!licenseStatus?.ok;
const avgTaskProgress = activeTasks.length
? Math.round(activeTasks.reduce((sum, t) => sum + (t.progress || 0), 0) / activeTasks.length)
: 0;
@@ -2118,7 +2097,6 @@ function App() {
activeTasks={activeTasks}
avgTaskProgress={avgTaskProgress}
runtimeSummary={runtimeSummary}
licenseStatus={licenseStatus}
healthStatus={healthStatus}
healthLoading={healthLoading}
healthError={healthError}
@@ -2136,7 +2114,6 @@ function App() {
currentUser={currentUser}
language={language}
apiEndpoint={apiClient.defaults.baseURL}
licenseOk={licenseOk}
foundPairs={foundPairs}
dinsarTotal={dinsarPagination.total}
selectedPairsCount={selectedPairsCount}
@@ -2192,14 +2169,7 @@ function App() {
onPairingAoiModeChange={handlePairingAoiModeChange}
onPairingProvinceChange={handlePairingProvinceChange}
onPairingCityChange={handlePairingCityChange}
licenseLoading={licenseLoading}
licenseStatus={licenseStatus}
isAdmin={isAdmin}
licenseFileRef={licenseFileRef}
onUploadFile={handleLicenseUpload}
onRefreshLicenseStatus={fetchLicenseStatus}
licenseFileName={licenseFileName}
licenseUploadStatus={licenseUploadStatus}
activeTasks={activeTasks}
runtimeSummary={runtimeSummary}
showCancelTask={showCancelTask}
-3
View File
@@ -12,9 +12,6 @@ const ACTION_LABELS = {
logout: '退出登录',
user_created: '创建用户',
user_updated: '更新用户',
license_uploaded: '上传授权文件',
license_upload_failed: '上传授权失败',
license_refreshed: '刷新授权状态',
task_queued: '任务入队',
batch_created: '创建任务批次',
batch_marked_complete: '批次标记完成',
-19
View File
@@ -27,21 +27,8 @@ const LoginPage = ({ onLoginSuccess }) => {
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [licenseStatus, setLicenseStatus] = useState(null);
const [activeSlide, setActiveSlide] = useState(0);
useEffect(() => {
const checkLicense = async () => {
try {
const response = await apiClient.get('/license/status');
setLicenseStatus(response.data || null);
} catch {
setLicenseStatus({ ok: false, reason: '无法获取授权状态' });
}
};
checkLicense();
}, []);
useEffect(() => {
const timer = window.setInterval(() => {
setActiveSlide((current) => (current + 1) % loginSlides.length);
@@ -85,12 +72,6 @@ const LoginPage = ({ onLoginSuccess }) => {
<p className="login-subtitle">内网受控访问请使用管理员分配的账号登录</p>
</div>
{licenseStatus && !licenseStatus.ok && (
<div className="login-alert warn">
当前授权状态异常{licenseStatus.reason || '未授权'}
</div>
)}
<form onSubmit={handleSubmit} className="login-form-grid">
<label className="login-field">
<span>用户名</span>
-1
View File
@@ -1,6 +1,5 @@
export { default as apiClient } from './client';
export * as authApi from './auth';
export * as licenseApi from './license';
export * as tasksApi from './tasks';
export * as healthApi from './health';
export * as radarApi from './radar';
-5
View File
@@ -1,5 +0,0 @@
import apiClient from './client';
export const getLicenseStatus = () => apiClient.get('/license/status').then(r => r.data);
export const uploadLicense = (formData) => apiClient.post('/license/upload', formData).then(r => r.data);
export const refreshLicense = () => apiClient.post('/license/refresh').then(r => r.data);
-106
View File
@@ -1,106 +0,0 @@
export default function LicenseOverlay({
licenseLoading,
licenseStatus,
isAdmin,
licenseFileRef,
onUploadFile,
onRefreshStatus,
licenseFileName,
licenseUploadStatus,
}) {
if (!licenseLoading && licenseStatus?.ok) {
return null;
}
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(15, 23, 42, 0.82)',
zIndex: 2000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<div style={{
width: 'min(560px, 90%)',
background: '#ffffff',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 20px 60px rgba(15, 23, 42, 0.35)',
}}>
<h3 style={{ marginTop: 0 }}>
{licenseLoading ? '正在验证授权...' : '系统未授权'}
</h3>
{!licenseLoading && (
<>
<p style={{ color: '#475569', marginBottom: '12px' }}>
失败原因{licenseStatus?.reason || '授权无效或已过期,请联系管理员。'}
</p>
{isAdmin ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '12px' }}>
<input
type="file"
ref={licenseFileRef}
accept=".lic"
onChange={(e) => onUploadFile(e.target.files?.[0])}
style={{ display: 'none' }}
/>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
<button
className="primary-btn"
onClick={() => licenseFileRef.current && licenseFileRef.current.click()}
style={{ padding: '8px 14px' }}
>
选择授权文件
</button>
<button
className="secondary-btn"
onClick={onRefreshStatus}
style={{ padding: '8px 14px' }}
>
刷新授权状态
</button>
<span style={{ fontSize: '0.85em', color: '#64748b' }}>
{licenseFileName ? `已选择: ${licenseFileName}` : '未选择文件'}
</span>
</div>
{licenseUploadStatus?.message && (
<div style={{
fontSize: '0.85em',
color:
licenseUploadStatus.type === 'error'
? '#dc2626'
: licenseUploadStatus.type === 'success'
? '#16a34a'
: '#475569',
}}>
{licenseUploadStatus.message}
</div>
)}
</div>
) : (
<div style={{ marginBottom: '12px', color: '#b45309', background: '#fffbeb', border: '1px solid #fcd34d', padding: '8px 10px', borderRadius: '6px' }}>
当前账号无上传授权权限请联系管理员处理授权文件
</div>
)}
<div style={{ marginTop: '10px', background: '#f8fafc', padding: '10px 12px', borderRadius: '8px', border: '1px solid #e2e8f0' }}>
<div style={{ fontWeight: 600, marginBottom: '6px', color: '#334155' }}>授权使用说明</div>
<ol style={{ margin: 0, paddingLeft: '18px', color: '#475569', fontSize: '0.85em' }}>
<li>确认已在服务器 .env 中配置 LICENSE_SECRET LICENSE_PUBLIC_KEY</li>
<li>上传有效的 .lic 授权文件与当前机器指纹匹配</li>
<li>点击刷新授权状态确认授权已生效</li>
</ol>
</div>
</>
)}
{licenseLoading && (
<p style={{ color: '#64748b' }}>请稍候正在与授权文件进行校验</p>
)}
</div>
</div>
);
}
@@ -1,6 +1,5 @@
import { Suspense, lazy } from 'react';
import { useShallow } from 'zustand/react/shallow';
import LicenseOverlay from '../LicenseOverlay';
import { useDinsarStore, usePairingStore, useUiStore } from '../../store';
import { useI18n } from '../../i18n/I18nContext';
import { formatYmd } from '../../utils/appUiHelpers';
@@ -17,14 +16,7 @@ export default function AppOverlays({
onPairingAoiModeChange,
onPairingProvinceChange,
onPairingCityChange,
licenseLoading,
licenseStatus,
isAdmin,
licenseFileRef,
onUploadFile,
onRefreshLicenseStatus,
licenseFileName,
licenseUploadStatus,
activeTasks,
runtimeSummary,
showCancelTask,
@@ -75,17 +67,6 @@ export default function AppOverlays({
</Suspense>
)}
<LicenseOverlay
licenseLoading={licenseLoading}
licenseStatus={licenseStatus}
isAdmin={isAdmin}
licenseFileRef={licenseFileRef}
onUploadFile={onUploadFile}
onRefreshStatus={onRefreshLicenseStatus}
licenseFileName={licenseFileName}
licenseUploadStatus={licenseUploadStatus}
/>
{showDataInfo && (
<Suspense fallback={<ModalLoadingFallback message="正在加载数据详情..." />}>
<LazyDataInfoModal
+1 -2
View File
@@ -41,7 +41,6 @@ export default function AppSidePanel({
currentUser,
language,
apiEndpoint,
licenseOk,
foundPairs,
dinsarTotal,
selectedPairsCount,
@@ -236,7 +235,7 @@ export default function AppSidePanel({
apiEndpoint={apiEndpoint}
onTaskStart={taskPanel.onTaskStart}
readOnly={isReadOnlyUser}
enabled={!!currentUser && licenseOk}
enabled={!!currentUser}
/>
</Suspense>
</div>
@@ -1,6 +1,5 @@
import { memo } from 'react';
import defaultLogoUrl from '../../logo.jpg';
import { formatUtc } from '../../utils/appUiHelpers';
const ORGANIZATION_NAME = import.meta.env.VITE_APP_ORG_NAME || '黑龙江省自然资源卫星应用技术中心';
const SYSTEM_NAME = import.meta.env.VITE_APP_SYSTEM_NAME || '雷达数据生产管理系统';
@@ -8,17 +7,14 @@ const SYSTEM_TAGLINE = import.meta.env.VITE_APP_SYSTEM_TAGLINE || '科研工程
const LOGO_URL = import.meta.env.VITE_APP_LOGO_URL || defaultLogoUrl;
function AppStatusHeader({
language,
currentUser,
isAdmin,
isReadOnlyUser,
activeTasks,
avgTaskProgress,
runtimeSummary,
licenseStatus,
onLogout,
}) {
const licenseOk = !!licenseStatus?.ok;
const worker = runtimeSummary?.worker || {};
const jobs = runtimeSummary?.jobs || {};
const scan = runtimeSummary?.scan || {};
@@ -68,14 +64,6 @@ function AppStatusHeader({
<div className="status-system-meta" aria-label="系统状态摘要">
<span>{SYSTEM_TAGLINE}</span>
<span className={`status-license-chip ${licenseOk ? 'ok' : 'fail'}`}>
{licenseOk ? '已授权' : '未授权'}
</span>
{licenseStatus?.expires_at && (
<span className="status-license">
授权至 {formatUtc(licenseStatus.expires_at, language)}
</span>
)}
</div>
<div className="status-actions">
+1 -49
View File
@@ -38,10 +38,8 @@ const requestWithTransientRetry = async (
};
export default function useAppAuthLifecycle({
ensureCanOperate,
clearRadarSearchResults,
radarSearchRequestSeqRef,
prevLicenseOkRef,
aoeLayerRef,
activeLayersRef,
radarPreviewLayersRef,
@@ -49,13 +47,9 @@ export default function useAppAuthLifecycle({
setCurrentUser,
setAuthChecked,
setPendingTaskIds,
setLicenseLoading,
setLicenseStatus,
setHealthLoading,
setHealthError,
setHealthStatus,
setLicenseFileName,
setLicenseUploadStatus,
setAoiLayer,
setAllData,
setRadarPagination,
@@ -113,7 +107,6 @@ export default function useAppAuthLifecycle({
clearRadarSearchResults();
setCurrentUser(null);
setPendingTaskIds([]);
prevLicenseOkRef.current = false;
setAuthChecked(true);
}
}, [
@@ -122,29 +115,9 @@ export default function useAppAuthLifecycle({
setHasRadarSearched,
setCurrentUser,
setPendingTaskIds,
prevLicenseOkRef,
setAuthChecked,
]);
const fetchLicenseStatus = useCallback(async () => {
try {
setLicenseLoading(true);
const response = await requestWithTransientRetry(() => apiClient.get('/license/status'));
const data = response.data || {};
if (!data.ok && !data.reason) {
data.reason = '未授权';
}
setLicenseStatus(data);
} catch (error) {
setLicenseStatus({
ok: false,
reason: error.response?.data?.detail || '无法获取授权状态',
});
} finally {
setLicenseLoading(false);
}
}, [setLicenseLoading, setLicenseStatus]);
const fetchHealthStatus = useCallback(async (options = {}) => {
const { refresh = false, silent = false } = options;
try {
@@ -167,22 +140,6 @@ export default function useAppAuthLifecycle({
}
}, [setHealthLoading, setHealthError, setHealthStatus]);
const handleLicenseUpload = useCallback(async (file) => {
if (!file) return;
if (!ensureCanOperate()) return;
try {
setLicenseFileName(file.name);
setLicenseUploadStatus({ type: 'info', message: '正在上传授权文件...' });
const form = new FormData();
form.append('file', file);
const response = await apiClient.post('/license/upload', form);
setLicenseUploadStatus({ type: 'success', message: response.data?.message || '授权文件已上传' });
await fetchLicenseStatus();
} catch (error) {
setLicenseUploadStatus({ type: 'error', message: error.response?.data?.detail || '授权文件上传失败' });
}
}, [ensureCanOperate, setLicenseFileName, setLicenseUploadStatus, fetchLicenseStatus]);
useEffect(() => {
const interceptorId = apiClient.interceptors.response.use(
(response) => response,
@@ -210,7 +167,6 @@ export default function useAppAuthLifecycle({
}));
setCurrentUser(null);
setAuthChecked(true);
prevLicenseOkRef.current = false;
}
return Promise.reject(error);
}
@@ -229,13 +185,11 @@ export default function useAppAuthLifecycle({
setRadarPagination,
setCurrentUser,
setAuthChecked,
prevLicenseOkRef,
]);
useEffect(() => {
fetchCurrentUser();
fetchLicenseStatus();
}, [fetchCurrentUser, fetchLicenseStatus]);
}, [fetchCurrentUser]);
useEffect(() => {
const startupTimer = setTimeout(() => {
@@ -253,8 +207,6 @@ export default function useAppAuthLifecycle({
return {
handleLoginSuccess,
handleLogout,
fetchLicenseStatus,
fetchHealthStatus,
handleLicenseUpload,
};
}
+2 -3
View File
@@ -7,7 +7,6 @@ const TERMINAL_TASK_STATUSES = new Set(['COMPLETED', 'PARTIAL_SUCCESS', 'FAILED'
export default function useGlobalTaskControl({
currentUser,
licenseOk,
activeTasks,
setActiveTasks,
setRuntimeSummary,
@@ -139,7 +138,7 @@ export default function useGlobalTaskControl({
}, [handleTasksUpdate, normalizeRuntimeSummary]);
useEffect(() => {
if (!currentUser || !licenseOk) return;
if (!currentUser) return;
// Initial fetch
syncActiveTasks();
@@ -179,7 +178,7 @@ export default function useGlobalTaskControl({
if (es) es.close();
if (fallbackInterval) clearInterval(fallbackInterval);
};
}, [currentUser, licenseOk, syncActiveTasks, handleTasksUpdate]);
}, [currentUser, syncActiveTasks, handleTasksUpdate]);
const handleCancelActiveTasks = useCallback(() => {
if (!cancelTaskPwd || activeTasks.length === 0) return;
-3
View File
@@ -3,12 +3,9 @@
{ zh: '请稍候,系统正在验证会话。', en: 'Please wait, verifying your session.' },
{ zh: '雷达数据生产管理系统', en: 'Radar Data Production Management System' },
{ zh: '科研工程模式', en: 'Research Engineering Mode' },
{ zh: '已授权', en: 'Licensed' },
{ zh: '未授权', en: 'Unlicensed' },
{ zh: '管理员', en: 'Admin' },
{ zh: '只读账号', en: 'Read-only account' },
{ zh: '任务', en: 'Tasks' },
{ zh: '授权至', en: 'License until' },
{ zh: '刷新自检状态', en: 'Refresh health status' },
{ zh: '自检中...', en: 'Checking...' },
{ zh: '刷新自检', en: 'Refresh Health' },
-8
View File
@@ -7,19 +7,11 @@ const s = (set, key) => (v) =>
export const useAuthStore = create((set) => ({
currentUser: null,
authChecked: false,
licenseStatus: { ok: false, reason: '', expires_at: null },
licenseLoading: true,
licenseUploadStatus: { type: '', message: '' },
licenseFileName: '',
healthStatus: null,
healthLoading: false,
healthError: '',
setCurrentUser: s(set, 'currentUser'),
setAuthChecked: s(set, 'authChecked'),
setLicenseStatus: s(set, 'licenseStatus'),
setLicenseLoading: s(set, 'licenseLoading'),
setLicenseUploadStatus: s(set, 'licenseUploadStatus'),
setLicenseFileName: s(set, 'licenseFileName'),
setHealthStatus: s(set, 'healthStatus'),
setHealthLoading: s(set, 'healthLoading'),
setHealthError: s(set, 'healthError'),