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:
freedakgmail
2026-07-07 09:04:47 +08:00
parent 8487f6eadf
commit 33b4c734aa
117 changed files with 22070 additions and 295 deletions
+122
View File
@@ -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;
+96
View File
@@ -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;
+133
View File
@@ -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;
}