feat: 完善前后端核心功能模块
后端: - 新增认证(auth)、任务(tasks)、映射(mappings)、对账(reconciliation)、异常(exceptions)、导出(exports) API - 新增核心模块: database, security, permissions, tenant, exceptions, error_handlers - 新增数据模型: user, company, reconciliation_task, field_mapping, uploaded_file 等 - 新增服务层: ai_recognizer, file_parser, file_storage, mapping, reconciliation 等 - 添加数据库迁移脚本 前端: - 新增登录页面和仪表盘页面 - 新增任务列表、任务详情、字段映射页面 - 新增异常处理页面和规则设置页面 - 新增 API 代理路由 /api/[...path] - 新增 UI 组件库 (button, card, dialog, input, table 等) - 新增 auth 组件 (ProtectedRoute, PermissionGate) - 新增 layout 组件 (Header, Sidebar) - 新增 mapping 组件 (FieldMappingTable, AISuggestionPanel) - 新增 API 客户端和 hooks (useAsync, useToast, usePermission 等) - 新增状态管理 (auth-store, company-store, ui-store) - 集成 Tailwind CSS 和 shadcn/ui 组件库 其他: - 添加 Alembic 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
import type { ApiError, ApiResponse } from './types';
|
||||
|
||||
// 创建 Axios 实例
|
||||
const createApiClient = (): AxiosInstance => {
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 自动添加 JWT Token
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 添加企业 ID
|
||||
const companyId = localStorage.getItem('company_id');
|
||||
if (companyId && config.headers) {
|
||||
config.headers['X-Company-ID'] = companyId;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<ApiError>) => {
|
||||
// 统一错误处理
|
||||
const apiError: ApiError = {
|
||||
code: 'UNKNOWN_ERROR',
|
||||
message: '未知错误',
|
||||
};
|
||||
|
||||
if (error.response) {
|
||||
// 服务器返回错误
|
||||
const { data, status } = error.response;
|
||||
|
||||
if (data && typeof data === 'object' && 'error' in data) {
|
||||
const errorData = data as { error: { code: string; message: string; details?: unknown } };
|
||||
apiError.code = errorData.error.code;
|
||||
apiError.message = errorData.error.message;
|
||||
apiError.details = errorData.error.details;
|
||||
} else {
|
||||
apiError.code = `HTTP_${status}`;
|
||||
apiError.message = error.message;
|
||||
}
|
||||
|
||||
// 401 未授权 - 跳转登录
|
||||
if (status === 401) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('company_id');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求发送但无响应
|
||||
apiError.code = 'NETWORK_ERROR';
|
||||
apiError.message = '网络错误,请检查连接';
|
||||
} else {
|
||||
// 其他错误
|
||||
apiError.message = error.message;
|
||||
}
|
||||
|
||||
return Promise.reject(apiError);
|
||||
}
|
||||
);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
// API 客户端单例
|
||||
export const apiClient = createApiClient();
|
||||
|
||||
// 通用请求方法封装
|
||||
export const api = {
|
||||
get: async <T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> => {
|
||||
const response = await apiClient.get<T>(url, config);
|
||||
return { data: response.data, status: response.status };
|
||||
},
|
||||
|
||||
post: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> => {
|
||||
const response = await apiClient.post<T>(url, data, config);
|
||||
return { data: response.data, status: response.status };
|
||||
},
|
||||
|
||||
put: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> => {
|
||||
const response = await apiClient.put<T>(url, data, config);
|
||||
return { data: response.data, status: response.status };
|
||||
},
|
||||
|
||||
patch: async <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> => {
|
||||
const response = await apiClient.patch<T>(url, data, config);
|
||||
return { data: response.data, status: response.status };
|
||||
},
|
||||
|
||||
delete: async <T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> => {
|
||||
const response = await apiClient.delete<T>(url, config);
|
||||
return { data: response.data, status: response.status };
|
||||
},
|
||||
};
|
||||
|
||||
export default apiClient;
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* API 端点常量
|
||||
*/
|
||||
export const ENDPOINTS = {
|
||||
// 健康检查
|
||||
HEALTH: '/api/health',
|
||||
|
||||
// 认证相关
|
||||
AUTH: {
|
||||
LOGIN: '/api/auth/login',
|
||||
LOGOUT: '/api/auth/logout',
|
||||
ME: '/api/auth/me',
|
||||
REFRESH: '/api/auth/refresh',
|
||||
},
|
||||
|
||||
// 企业相关
|
||||
COMPANY: {
|
||||
LIST: '/api/companies',
|
||||
DETAIL: (id: number | string) => `/api/companies/${id}`,
|
||||
CREATE: '/api/companies',
|
||||
UPDATE: (id: number | string) => `/api/companies/${id}`,
|
||||
DELETE: (id: number | string) => `/api/companies/${id}`,
|
||||
},
|
||||
|
||||
// 用户相关
|
||||
USER: {
|
||||
LIST: '/api/users',
|
||||
DETAIL: (id: number | string) => `/api/users/${id}`,
|
||||
CREATE: '/api/users',
|
||||
UPDATE: (id: number | string) => `/api/users/${id}`,
|
||||
DELETE: (id: number | string) => `/api/users/${id}`,
|
||||
UPDATE_ROLE: (id: number | string) => `/api/users/${id}/role`,
|
||||
},
|
||||
|
||||
// 文件上传
|
||||
FILE: {
|
||||
UPLOAD: '/api/files/upload',
|
||||
DOWNLOAD: (id: number | string) => `/api/files/${id}/download`,
|
||||
DELETE: (id: number | string) => `/api/files/${id}`,
|
||||
},
|
||||
|
||||
// 任务相关
|
||||
TASK: {
|
||||
LIST: '/api/tasks',
|
||||
STATS: '/api/tasks/stats',
|
||||
DETAIL: (id: number | string) => `/api/tasks/${id}`,
|
||||
CREATE: '/api/tasks',
|
||||
UPDATE: (id: number | string) => `/api/tasks/${id}`,
|
||||
DELETE: (id: number | string) => `/api/tasks/${id}`,
|
||||
RECONCILE: (id: number | string) => `/api/reconciliation/execute/${id}`,
|
||||
RESULT: (id: number | string) => `/api/reconciliation/result/${id}`,
|
||||
EXPORT: (id: number | string) => `/api/exports/task/${id}`,
|
||||
},
|
||||
|
||||
// 异常相关
|
||||
ANOMALY: {
|
||||
LIST: '/api/anomalies',
|
||||
DETAIL: (id: number | string) => `/api/anomalies/${id}`,
|
||||
CONFIRM: (id: number | string) => `/api/anomalies/${id}/confirm`,
|
||||
IGNORE: (id: number | string) => `/api/anomalies/${id}/ignore`,
|
||||
},
|
||||
|
||||
// 分析相关
|
||||
ANALYSIS: {
|
||||
LABOR_COST: (taskId: number | string) => `/api/analysis/labor-cost/${taskId}`,
|
||||
TREND: '/api/analysis/trend',
|
||||
SUMMARY: '/api/analysis/summary',
|
||||
},
|
||||
|
||||
// 凭证相关
|
||||
VOUCHER: {
|
||||
GENERATE: (taskId: number | string) => `/api/vouchers/generate/${taskId}`,
|
||||
EXPORT: (taskId: number | string) => `/api/vouchers/export/${taskId}`,
|
||||
},
|
||||
|
||||
// 审计日志
|
||||
AUDIT: {
|
||||
LIST: '/api/audit-logs',
|
||||
},
|
||||
|
||||
// 字段映射
|
||||
MAPPING: {
|
||||
LIST: '/api/mappings',
|
||||
TASK_MAPPINGS: (taskId: number | string) => `/api/mappings/task/${taskId}`,
|
||||
RECOGNIZE: '/api/mappings/recognize',
|
||||
CONFIRM: '/api/mappings/confirm',
|
||||
DETAIL: (mappingId: number | string) => `/api/mappings/${mappingId}`,
|
||||
UPDATE: (mappingId: number | string) => `/api/mappings/${mappingId}`,
|
||||
SAVE_AS_RULE: (mappingId: number | string) => `/api/mappings/${mappingId}/save-as-rule`,
|
||||
RULES_LIST: '/api/mappings/rules/list',
|
||||
RULE_STATUS: (ruleId: number | string) => `/api/mappings/rules/${ruleId}/status`,
|
||||
RULE_DELETE: (ruleId: number | string) => `/api/mappings/rules/${ruleId}`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default ENDPOINTS;
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* API 响应类型
|
||||
*/
|
||||
export interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 错误类型
|
||||
*/
|
||||
export interface ApiError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应类型
|
||||
*/
|
||||
export interface PaginatedResponse<T = unknown> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 ID 参数
|
||||
*/
|
||||
export interface IdParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
// ===== 字段映射类型 =====
|
||||
|
||||
export interface FieldMappingResponse {
|
||||
id: number;
|
||||
company_id: number;
|
||||
file_id: number;
|
||||
source_field: string;
|
||||
standard_field: string;
|
||||
confidence: number;
|
||||
reasoning?: string;
|
||||
sample_values?: unknown[];
|
||||
is_skipped: boolean;
|
||||
confirmed: boolean;
|
||||
confirmed_by?: number;
|
||||
confirmed_at?: string;
|
||||
}
|
||||
|
||||
export interface FileMappingSummary {
|
||||
file_id: number;
|
||||
file_type: string;
|
||||
file_name: string;
|
||||
total_fields: number;
|
||||
confirmed_fields: number;
|
||||
high_confidence: number;
|
||||
medium_confidence: number;
|
||||
low_confidence: number;
|
||||
mappings: FieldMappingResponse[];
|
||||
}
|
||||
|
||||
export interface TaskMappingOverview {
|
||||
task_id: number;
|
||||
total_files: number;
|
||||
total_fields: number;
|
||||
confirmed_fields: number;
|
||||
needs_review: boolean;
|
||||
files: FileMappingSummary[];
|
||||
}
|
||||
|
||||
export interface CompanyRuleResponse {
|
||||
id: number;
|
||||
company_id: number;
|
||||
rule_type: string;
|
||||
match_condition: Record<string, unknown>;
|
||||
target_value: string;
|
||||
priority: number;
|
||||
status: string;
|
||||
description?: string;
|
||||
match_count: number;
|
||||
last_used_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConfirmMappingsRequest {
|
||||
mapping_ids: number[];
|
||||
}
|
||||
|
||||
export interface ConfirmMappingsResponse {
|
||||
confirmed_count: number;
|
||||
mappings: FieldMappingResponse[];
|
||||
}
|
||||
|
||||
export interface SaveAsRuleResponse {
|
||||
rule_id: number;
|
||||
source_field: string;
|
||||
target_field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ===== 任务类型 =====
|
||||
|
||||
export interface ReconciliationTask {
|
||||
id: number;
|
||||
company_id: number;
|
||||
name: string;
|
||||
period: string;
|
||||
status: string;
|
||||
file_ids?: Record<string, number>;
|
||||
mapping_completed: boolean;
|
||||
mapping_confirmed_at?: string;
|
||||
mapping_confirmed_by?: number;
|
||||
total_employees?: number;
|
||||
matched_count?: number;
|
||||
exception_count?: number;
|
||||
reconciliation_result?: unknown;
|
||||
error_message?: string;
|
||||
created_by?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
Reference in New Issue
Block a user