refactor(ipc): harden channels and unify IPC contracts

This commit is contained in:
test
2026-03-08 12:35:00 +08:00
parent 616e2b31a5
commit 8f3e5273e2
25 changed files with 991 additions and 1263 deletions

View File

@@ -61,16 +61,18 @@ function App(): React.JSX.Element {
try {
// Get computer name
console.log('Getting computer name...')
const name = await window.electron.auth.getComputerName()
const computerNameResult = await window.electron.auth.getComputerName()
const name = computerNameResult.success && computerNameResult.data ? computerNameResult.data : ''
console.log('Computer name:', name)
setComputerName(name)
// Try silent login
console.log('Trying silent login...')
const result = await window.electron.auth.silentLogin()
const silentLoginResult = await window.electron.auth.silentLogin()
const result = silentLoginResult.data
console.log('Silent login result:', result)
if (result.success && result.userInfo) {
if (silentLoginResult.success && result?.success && result.userInfo) {
console.log('Silent login success:', result.userInfo)
setCurrentUser({
username: result.userInfo.username,
@@ -81,8 +83,8 @@ function App(): React.JSX.Element {
if (result.requiresUserSelection) {
console.log('Admin user needs to select user')
// Load all users for selection
const users = await window.electron.auth.getAllUsers()
setAllUsers(users)
const usersResult = await window.electron.auth.getAllUsers()
setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
setShowUserSelection(true)
} else {
console.log('Setting authenticated to true')
@@ -107,19 +109,20 @@ function App(): React.JSX.Element {
const handleLogin = async (username: string, password: string): Promise<boolean> => {
try {
const result = await window.electron.auth.login({ username, password })
const loginData = result.success ? result.data : undefined
if (result.success && result.userInfo) {
if (result.success && loginData?.userInfo) {
setCurrentUser({
username: result.userInfo.username,
userType: result.userInfo.userType
username: loginData.userInfo.username,
userType: loginData.userInfo.userType
})
// Check if admin needs user selection
if (result.userInfo.userType === 'Admin') {
if (loginData.userInfo.userType === 'Admin') {
setShowLoginDialog(false)
// Load all users for selection
const users = await window.electron.auth.getAllUsers()
setAllUsers(users)
const usersResult = await window.electron.auth.getAllUsers()
setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
setShowUserSelection(true)
} else {
setIsAuthenticated(true)
@@ -144,10 +147,11 @@ function App(): React.JSX.Element {
const handleUserSelect = async (user: SelectedUserInfo) => {
try {
const result = await window.electron.auth.switchUser(user)
if (result.success) {
const switchData = result.success ? result.data : undefined
if (result.success && switchData) {
setCurrentUser({
username: result.userInfo?.username || user.username,
userType: result.userInfo?.userType || user.userType
username: switchData.userInfo?.username || user.username,
userType: switchData.userInfo?.userType || user.userType
})
setIsAuthenticated(true)
setShowUserSelection(false)

View File

@@ -252,10 +252,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
toUpdate,
toDelete
})
const payload = result.success ? (result.data as { stats?: { success?: number; failed?: number } } | undefined) : undefined
if (result.success) {
alert(
`保存完成!\n成功${result.stats?.success || 0}\n失败${result.stats?.failed || 0}`
`保存完成!\n成功${payload?.stats?.success || 0}\n失败${payload?.stats?.failed || 0}`
)
await loadData()
} else {

View File

@@ -1,13 +1,5 @@
/**
* IPC Hook for Authentication operations
*
* Provides a React-friendly interface for auth IPC calls
* with loading state, error handling, and user data management.
*/
import { useState, useCallback } from 'react'
// Types based on the user types
interface UserInfo {
id: number
username: string
@@ -38,9 +30,6 @@ interface UseAuthReturn extends UseAuthState {
reset: () => void
}
/**
* Hook for authentication operations
*/
export function useAuth(): UseAuthReturn {
const [state, setState] = useState<UseAuthState>({
loading: false,
@@ -51,156 +40,94 @@ export function useAuth(): UseAuthReturn {
const login = useCallback(async (credentials: LoginCredentials): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.login(credentials)
try {
const result = (await window.electron.ipcRenderer.invoke('auth:login', credentials)) as {
success: boolean
userInfo?: UserInfo
error?: string
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return true
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Login failed'
}))
return false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({ ...prev, loading: false, error: result.error ?? 'Login failed' }))
return false
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, [])
const silentLogin = useCallback(async (): Promise<{
success: boolean
requiresUserSelection?: boolean
}> => {
const silentLogin = useCallback(async (): Promise<{ success: boolean; requiresUserSelection?: boolean }> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.silentLogin()
try {
const result = (await window.electron.ipcRenderer.invoke('auth:silentLogin')) as {
success: boolean
userInfo?: UserInfo
error?: string
requiresUserSelection?: boolean
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return { success: true, requiresUserSelection: result.requiresUserSelection }
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || null
}))
return { success: false }
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({ ...prev, loading: false, error: result.error || null }))
return { success: false }
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return { success: true, requiresUserSelection: result.data.requiresUserSelection }
}, [])
const logout = useCallback(async (): Promise<void> => {
try {
await window.electron.ipcRenderer.invoke('auth:logout')
setState({
loading: false,
user: null,
error: null,
isAuthenticated: false
})
} catch (error) {
console.error('Logout error:', error)
}
await window.electron.auth.logout()
setState({
loading: false,
user: null,
error: null,
isAuthenticated: false
})
}, [])
const getCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
try {
const result = (await window.electron.ipcRenderer.invoke('auth:getCurrentUser')) as {
isAuthenticated: boolean
userInfo?: UserInfo
}
if (result.isAuthenticated && result.userInfo) {
const userInfo = result.userInfo
setState((prev) => ({
...prev,
user: userInfo,
isAuthenticated: true
}))
return userInfo
}
return null
} catch {
const result = await window.electron.auth.getCurrentUser()
if (!result.success || !result.data?.isAuthenticated || !result.data.userInfo) {
return null
}
setState((prev) => ({
...prev,
user: result.data!.userInfo!,
isAuthenticated: true
}))
return result.data.userInfo
}, [])
const getAllUsers = useCallback(async (): Promise<UserInfo[]> => {
try {
return (await window.electron.ipcRenderer.invoke('auth:getAllUsers')) as UserInfo[]
} catch {
return []
}
const result = await window.electron.auth.getAllUsers()
return result.success && result.data ? result.data : []
}, [])
const switchUser = useCallback(async (userInfo: UserInfo): Promise<boolean> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const result = await window.electron.auth.switchUser(userInfo)
try {
const result = (await window.electron.ipcRenderer.invoke('auth:switchUser', userInfo)) as {
success: boolean
userInfo?: UserInfo
error?: string
}
if (result.success && result.userInfo) {
setState({
loading: false,
user: result.userInfo,
error: null,
isAuthenticated: true
})
return true
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Switch user failed'
}))
return false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
if (!result.success || !result.data?.userInfo) {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Switch user failed'
}))
return false
}
setState({
loading: false,
user: result.data.userInfo,
error: null,
isAuthenticated: true
})
return true
}, [])
const isAdmin = useCallback(async (): Promise<boolean> => {
try {
return (await window.electron.ipcRenderer.invoke('auth:isAdmin')) as boolean
} catch {
return false
}
const result = await window.electron.auth.isAdmin()
return result.success && Boolean(result.data)
}, [])
const reset = useCallback(() => {

View File

@@ -85,8 +85,10 @@ export function useCleaner() {
useEffect(() => {
const initializePage = async () => {
try {
const admin = await window.electron.auth.isAdmin()
const user = await window.electron.auth.getCurrentUser()
const adminResult = await window.electron.auth.isAdmin()
const userResult = await window.electron.auth.getCurrentUser()
const admin = adminResult.success && Boolean(adminResult.data)
const user = userResult.success ? userResult.data : undefined
setIsAdmin(admin)
if (user && user.userInfo) {
setCurrentUsername(user.userInfo.username)
@@ -98,13 +100,16 @@ export function useCleaner() {
// Load managers
if (admin) {
const resp = await window.electron.materials.getManagers()
setManagers(resp.managers)
setSelectedManagers(new Set(resp.managers))
const managersPayload = resp.success ? (resp.data as { managers: string[] } | undefined) : undefined
const managerList = managersPayload?.managers ?? []
setManagers(managerList)
setSelectedManagers(new Set(managerList))
}
// Get shared Production IDs
const result = await window.electron.validation.getSharedProductionIds()
setSharedProductionIdsCount(result.productionIds.length)
const idsPayload = result.success ? (result.data as { productionIds?: string[] } | undefined) : undefined
setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0)
} catch (err) {
console.error('Initialization failed:', err)
}
@@ -159,21 +164,28 @@ export function useCleaner() {
mode: valMode === 'full' ? 'database_full' : 'database_filtered',
useSharedProductionIds: valMode === 'filtered'
})
const validationData = response.success ? (response.data as any) : null
if (response.success && response.results) {
setValidationResults(response.results)
const markedCodes = new Set(
response.results.filter((r) => r.isMarkedForDeletion).map((r) => r.materialCode)
if (response.success && validationData?.success && validationData.results) {
setValidationResults(validationData.results)
const markedCodes = new Set<string>(
validationData.results
.filter((r: ValidationResult) => r.isMarkedForDeletion)
.map((r: ValidationResult) => r.materialCode)
)
setSelectedItems(markedCodes)
if (isAdmin) {
const uniqueManagers = new Set(response.results.map((r) => r.managerName).filter(Boolean))
setManagers([...uniqueManagers])
const uniqueManagers = new Set<string>(
validationData.results
.map((r: ValidationResult) => r.managerName)
.filter((name: string) => Boolean(name))
)
setManagers(Array.from(uniqueManagers))
setSelectedManagers(uniqueManagers)
}
} else {
alert(response.error || '校验失败')
alert(response.error || validationData?.error || '校验失败')
}
} catch (err) {
alert(err instanceof Error ? err.message : '校验过程中发生未知错误')
@@ -285,14 +297,16 @@ export function useCleaner() {
if (materialsToUpsert.length > 0) {
const res = await window.electron.materials.upsertBatch(materialsToUpsert)
const payload = res.success ? (res.data as { stats?: { success?: number } } | undefined) : undefined
if (!res.success) throw new Error(res.error || '写入物料失败')
msgParts.push(`写入/更新成功:${res.stats?.success || 0}`)
msgParts.push(`写入/更新成功:${payload?.stats?.success || 0}`)
}
if (materialsToDelete.length > 0) {
const res = await window.electron.materials.delete(materialsToDelete)
const payload = res.success ? (res.data as { count?: number } | undefined) : undefined
if (!res.success) throw new Error(res.error || '删除物料失败')
msgParts.push(`删除成功:${res.count || 0}`)
msgParts.push(`删除成功:${payload?.count || 0}`)
}
alert(`操作完成!\n\n${msgParts.join('\n')}`)
@@ -300,7 +314,8 @@ export function useCleaner() {
// Reload managers if admin
if (isAdmin) {
const resp = await window.electron.materials.getManagers()
setManagers(resp.managers)
const payload = resp.success ? (resp.data as { managers?: string[] } | undefined) : undefined
setManagers(payload?.managers ?? [])
}
} catch (err) {
alert(err instanceof Error ? err.message : '操作失败')
@@ -331,12 +346,13 @@ export function useCleaner() {
try {
const cleanerDataResult = await window.electron.validation.getCleanerData()
if (!cleanerDataResult.success) {
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
if (!cleanerDataResult.success || cleanerData?.success === false) {
throw new Error(cleanerDataResult.error || '获取清理数据失败')
}
const orderNumberList = cleanerDataResult.orderNumbers || []
const materialCodeList = cleanerDataResult.materialCodes || []
const orderNumberList = cleanerData?.orderNumbers || []
const materialCodeList = cleanerData?.materialCodes || []
if (orderNumberList.length === 0)
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
@@ -349,13 +365,14 @@ export function useCleaner() {
dryRun,
headless
})
const cleanerRunData = response.success ? (response.data as any) : null
if (response.success && response.data) {
if (response.success && cleanerRunData) {
setReportData({
ordersProcessed: response.data.ordersProcessed,
materialsDeleted: response.data.materialsDeleted,
materialsSkipped: response.data.materialsSkipped,
errors: response.data.errors
ordersProcessed: cleanerRunData.ordersProcessed,
materialsDeleted: cleanerRunData.materialsDeleted,
materialsSkipped: cleanerRunData.materialsSkipped,
errors: cleanerRunData.errors
})
} else {
throw new Error(response.error || '清理失败')
@@ -390,11 +407,12 @@ export function useCleaner() {
}))
const response = await window.electron.cleaner.exportResults(exportItems)
const exportData = response.success ? (response.data as any) : null
if (response.success) {
alert(`导出成功!\n文件已保存到${response.filePath}`)
if (response.success && exportData?.success !== false) {
alert(`导出成功!\n文件已保存到${exportData?.filePath ?? ''}`)
} else {
throw new Error(response.error || '导出失败')
throw new Error(response.error || exportData?.error || '导出失败')
}
} catch (err) {
alert(err instanceof Error ? err.message : '导出过程中发生错误')

View File

@@ -1,13 +1,5 @@
/**
* IPC Hook for Validation operations
*
* Provides a React-friendly interface for validation IPC calls
* with loading state, error handling, and data management.
*/
import { useState, useCallback } from 'react'
// Types based on validation types
interface ValidationRequest {
mode: 'database_full' | 'database_filtered'
productionIdFile?: string
@@ -52,9 +44,6 @@ interface UseValidationReturn extends UseValidationState {
reset: () => void
}
/**
* Hook for validation operations
*/
export function useValidation(): UseValidationReturn {
const [state, setState] = useState<UseValidationState>({
loading: false,
@@ -63,82 +52,62 @@ export function useValidation(): UseValidationReturn {
error: null
})
const validate = useCallback(
async (request: ValidationRequest): Promise<ValidationResponse | null> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const validate = useCallback(async (request: ValidationRequest): Promise<ValidationResponse | null> => {
setState((prev) => ({ ...prev, loading: true, error: null }))
const response = await window.electron.validation.validate(request)
if (!response.success || !response.data) {
setState((prev) => ({ ...prev, loading: false, error: response.error || 'Validation failed' }))
return response.success ? null : { success: false, error: response.error }
}
try {
const result = (await window.electron.ipcRenderer.invoke(
'validation:validate',
request
)) as ValidationResponse
const result = response.data as ValidationResponse
if (!result.success) {
setState((prev) => ({ ...prev, loading: false, error: result.error || 'Validation failed' }))
return result
}
if (result.success) {
const results = result.results || null
const stats = result.stats || null
setState({
loading: false,
data: results,
stats: stats,
error: null
})
return result
} else {
setState((prev) => ({
...prev,
loading: false,
error: result.error || 'Validation failed'
}))
return result
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setState((prev) => ({ ...prev, loading: false, error: message }))
return null
}
},
[]
)
setState({
loading: false,
data: result.results || null,
stats: result.stats || null,
error: null
})
return result
}, [])
const setSharedProductionIds = useCallback(async (ids: string[]): Promise<void> => {
try {
await window.electron.ipcRenderer.invoke('validation:setSharedProductionIds', ids)
} catch (error) {
console.error('Failed to set shared production IDs:', error)
}
await window.electron.validation.setSharedProductionIds(ids)
}, [])
const getSharedProductionIds = useCallback(async (): Promise<string[]> => {
try {
const result = (await window.electron.ipcRenderer.invoke(
'validation:getSharedProductionIds'
)) as { productionIds?: string[] }
return result?.productionIds || []
} catch {
const result = await window.electron.validation.getSharedProductionIds()
if (!result.success || !result.data) {
return []
}
const payload = result.data as { productionIds?: string[] }
return payload.productionIds || []
}, [])
const getCleanerData = useCallback(async (): Promise<{
orderNumbers: string[]
materialCodes: string[]
} | null> => {
try {
const result = (await window.electron.ipcRenderer.invoke('validation:getCleanerData')) as {
success: boolean
orderNumbers?: string[]
materialCodes?: string[]
}
if (result.success) {
return {
orderNumbers: result.orderNumbers || [],
materialCodes: result.materialCodes || []
}
}
const getCleanerData = useCallback(async (): Promise<{ orderNumbers: string[]; materialCodes: string[] } | null> => {
const result = await window.electron.validation.getCleanerData()
if (!result.success || !result.data) {
return null
} catch {
}
const payload = result.data as {
success?: boolean
orderNumbers?: string[]
materialCodes?: string[]
}
if (payload.success === false) {
return null
}
return {
orderNumbers: payload.orderNumbers || [],
materialCodes: payload.materialCodes || []
}
}, [])
const reset = useCallback(() => {

View File

@@ -26,14 +26,17 @@ const SettingsPage: React.FC = () => {
try {
setIsLoading(true)
// ERP credentials are loaded from database (current user's config)
const config = await window.electron.settings.getSettings()
const response = await window.electron.settings.getSettings()
const config = response.success ? (response.data as { erp?: ErpCredentials } | undefined) : undefined
// Extract ERP credentials from the config
if (config && (config as any).erp) {
if (config?.erp) {
setCredentials({
username: (config as any).erp.username || '',
password: (config as any).erp.password || ''
username: config.erp.username || '',
password: config.erp.password || ''
})
} else if (!response.success) {
showMessage('error', response.error || '加载 ERP 配置失败')
}
setIsModified(false)
} catch (error) {
@@ -56,13 +59,14 @@ const SettingsPage: React.FC = () => {
username: credentials.username,
password: credentials.password
}
} as any)
})
const saveData = result.success ? (result.data as { success?: boolean; error?: string } | undefined) : undefined
if (result.success) {
if (result.success && saveData?.success !== false) {
setIsModified(false)
showMessage('success', 'ERP 账号密码保存成功')
} else {
showMessage('error', result.error || '保存失败')
showMessage('error', result.error || saveData?.error || '保存失败')
}
} catch (error) {
showMessage('error', '保存配置时发生错误')