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
+556
View File
@@ -0,0 +1,556 @@
"""
示例数据初始化脚本
创建一套完整的真实业务场景示例数据:
- 企业:深圳星辰科技有限公司
- 用户:admin 管理员
- 3个月工资对账数据(2026年4月、5月、6月)
- 包含各类业务异常场景
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from datetime import datetime, date
from decimal import Decimal
import random
from app.core.database import async_session_maker as AsyncSessionLocal
from app.core.security import hash_password
# 导入所有模型以初始化 SQLAlchemy 关系
from app.models.company import Company, CompanyPlan, CompanyStatus
from app.models.user import User, UserRole, UserStatus
from app.models.uploaded_file import UploadedFile, FileType, ParseStatus
from app.models.reconciliation_task import ReconciliationTask, TaskStatus
from app.models.exception_item import ExceptionItem, ExceptionType, ExceptionStatus, ExceptionSeverity
from app.models.field_mapping import FieldMapping
from app.models.company_rule import CompanyRule
from app.models.standard_field import StandardField
# ========== 基础数据定义 ==========
# 公司信息
COMPANY_DATA = {
"name": "深圳星辰科技有限公司",
"plan": CompanyPlan.PROFESSIONAL.value,
"status": CompanyStatus.ACTIVE.value,
"data_retention_months": 12,
"max_users": 20,
"is_trial": False,
}
# 用户列表
USERS_DATA = [
{
"email": "admin@xingchen.com",
"password": "admin123",
"full_name": "张明华",
"role": UserRole.ADMIN.value,
},
{
"email": "finance@xingchen.com",
"password": "finance123",
"full_name": "李晓燕",
"role": UserRole.FINANCE_MANAGER.value,
},
{
"email": "accountant@xingchen.com",
"password": "account123",
"full_name": "王建国",
"role": UserRole.ACCOUNTANT.value,
},
]
# 员工数据(25名员工)
EMPLOYEES = [
# 管理层
{"id": "EMP001", "name": "张明华", "department": "管理层", "position": "总经理", "base_salary": 35000},
{"id": "EMP002", "name": "李晓燕", "department": "财务部", "position": "财务总监", "base_salary": 28000},
{"id": "EMP003", "name": "王建国", "department": "技术部", "position": "技术总监", "base_salary": 30000},
# 技术部
{"id": "EMP004", "name": "刘伟强", "department": "技术部", "position": "高级工程师", "base_salary": 22000},
{"id": "EMP005", "name": "陈志明", "department": "技术部", "position": "工程师", "base_salary": 18000},
{"id": "EMP006", "name": "周建军", "department": "技术部", "position": "工程师", "base_salary": 17000},
{"id": "EMP007", "name": "吴海涛", "department": "技术部", "position": "初级工程师", "base_salary": 12000},
{"id": "EMP008", "name": "郑晓峰", "department": "技术部", "position": "测试工程师", "base_salary": 15000},
# 产品部
{"id": "EMP009", "name": "孙丽娜", "department": "产品部", "position": "产品经理", "base_salary": 22000},
{"id": "EMP010", "name": "马俊杰", "department": "产品部", "position": "产品专员", "base_salary": 14000},
{"id": "EMP011", "name": "朱婷婷", "department": "产品部", "position": "UI设计师", "base_salary": 16000},
# 运营部
{"id": "EMP012", "name": "胡文静", "department": "运营部", "position": "运营总监", "base_salary": 25000},
{"id": "EMP013", "name": "林浩然", "department": "运营部", "position": "运营专员", "base_salary": 13000},
{"id": "EMP014", "name": "何雨晴", "department": "运营部", "position": "客服主管", "base_salary": 12000},
{"id": "EMP015", "name": "高建峰", "department": "运营部", "position": "客服专员", "base_salary": 9000},
# 市场部
{"id": "EMP016", "name": "罗晓东", "department": "市场部", "position": "市场总监", "base_salary": 26000},
{"id": "EMP017", "name": "宋志远", "department": "市场部", "position": "市场专员", "base_salary": 11000},
{"id": "EMP018", "name": "唐思远", "department": "市场部", "position": "销售经理", "base_salary": 15000},
{"id": "EMP019", "name": "韩冰冰", "department": "市场部", "position": "销售代表", "base_salary": 8000},
# 行政部
{"id": "EMP020", "name": "冯婉君", "department": "行政部", "position": "行政经理", "base_salary": 14000},
{"id": "EMP021", "name": "许志刚", "department": "行政部", "position": "行政专员", "base_salary": 8000},
{"id": "EMP022", "name": "邓小丽", "department": "行政部", "position": "前台接待", "base_salary": 6000},
# 人事部
{"id": "EMP023", "name": "曹丽华", "department": "人事部", "position": "人事经理", "base_salary": 15000},
{"id": "EMP024", "name": "彭海燕", "department": "人事部", "position": "招聘专员", "base_salary": 9000},
{"id": "EMP025", "name": "曾敏仪", "department": "人事部", "position": "薪酬专员", "base_salary": 8500},
]
def calculate_salary(employee: dict, month: int, year: int, has_bonus: bool = True) -> dict:
"""计算员工月工资"""
base = employee["base_salary"]
# 绩效工资(基本工资的10%-30%)
performance = base * random.uniform(0.1, 0.3)
# 奖金(部分员工有)
bonus = base * random.uniform(0.5, 1.5) if has_bonus and random.random() > 0.3 else 0
# 加班费(随机)
overtime = random.choice([0, 500, 1000, 1500, 2000])
# 应发工资
gross_salary = base + performance + bonus + overtime
# 社保个人部分(基本工资的10.5%)
social_security = base * 0.105
# 公积金(基本工资的12%
housing_fund = base * 0.12
# 个税专项附加扣除
tax_deduction = 2000 # 子女教育 + 继续教育 + 住房租金 等
# 应税工资
taxable = gross_salary - social_security - housing_fund - tax_deduction
# 个税(简化计算)
if taxable <= 3000:
tax = taxable * 0.03
elif taxable <= 12000:
tax = taxable * 0.10 - 210
elif taxable <= 25000:
tax = taxable * 0.20 - 1410
elif taxable <= 35000:
tax = taxable * 0.25 - 2660
elif taxable <= 55000:
tax = taxable * 0.30 - 4410
elif taxable <= 80000:
tax = taxable * 0.35 - 7160
else:
tax = taxable * 0.45 - 15160
tax = max(0, tax)
# 实发工资
net_salary = gross_salary - social_security - housing_fund - tax
return {
"employee_id": employee["id"],
"employee_name": employee["name"],
"department": employee["department"],
"base_salary": round(base, 2),
"performance_salary": round(performance, 2),
"bonus": round(bonus, 2),
"overtime_pay": round(overtime, 2),
"gross_salary": round(gross_salary, 2),
"social_security": round(social_security, 2),
"housing_fund": round(housing_fund, 2),
"personal_income_tax": round(tax, 2),
"net_salary": round(net_salary, 2),
}
def calculate_social_security(employee: dict) -> dict:
"""计算社保"""
base = employee["base_salary"]
# 养老保险(个人8%
pension = base * 0.08
# 医疗保险(个人2%
medical = base * 0.02
# 失业保险(个人0.5%
unemployment = base * 0.005
# 工伤保险(个人0%
work_injury = 0
# 生育保险(个人0%
maternity = 0
total = pension + medical + unemployment
return {
"employee_id": employee["id"],
"employee_name": employee["name"],
"department": employee["department"],
"pension": round(pension, 2),
"medical": round(medical, 2),
"unemployment": round(unemployment, 2),
"work_injury": round(work_injury, 2),
"maternity": round(maternity, 2),
"total": round(total, 2),
}
def calculate_tax(employee: dict, gross_salary: float) -> dict:
"""计算个税"""
base = employee["base_salary"]
# 社保个人部分
social = base * 0.105
# 公积金
housing = base * 0.12
# 专项附加扣除
deduction = 2000
# 应税所得
taxable = gross_salary - social - housing - deduction - 5000 # 起征点
if taxable <= 0:
tax = 0
tax_rate = 0
quick_deduction = 0
elif taxable <= 3000:
tax = taxable * 0.03
tax_rate = 0.03
quick_deduction = 0
elif taxable <= 12000:
tax = taxable * 0.10 - 210
tax_rate = 0.10
quick_deduction = 210
elif taxable <= 25000:
tax = taxable * 0.20 - 1410
tax_rate = 0.20
quick_deduction = 1410
elif taxable <= 35000:
tax = taxable * 0.25 - 2660
tax_rate = 0.25
quick_deduction = 2660
elif taxable <= 55000:
tax = taxable * 0.30 - 4410
tax_rate = 0.30
quick_deduction = 4410
elif taxable <= 80000:
tax = taxable * 0.35 - 7160
tax_rate = 0.35
quick_deduction = 7160
else:
tax = taxable * 0.45 - 15160
tax_rate = 0.45
quick_deduction = 15160
tax = max(0, tax)
return {
"employee_id": employee["id"],
"employee_name": employee["name"],
"department": employee["department"],
"gross_salary": round(gross_salary, 2),
"taxable_income": round(taxable, 2),
"tax_rate": tax_rate,
"quick_deduction": quick_deduction,
"tax_amount": round(tax, 2),
}
async def init_sample_data():
"""初始化示例数据"""
async with AsyncSessionLocal() as db:
try:
# 1. 创建公司
print("创建公司...")
company = Company(**COMPANY_DATA)
db.add(company)
await db.flush()
# 2. 创建用户
print("创建用户...")
admin_user = None
for user_data in USERS_DATA:
user = User(
company_id=company.id,
email=user_data["email"],
hashed_password=hash_password(user_data["password"]),
full_name=user_data["full_name"],
role=user_data["role"],
status=UserStatus.ACTIVE.value,
)
db.add(user)
if user_data["role"] == UserRole.ADMIN.value:
admin_user = user
await db.flush()
# 3. 创建3个月的对账任务
print("创建对账任务...")
tasks = []
months = [
(2026, 4, "4月"),
(2026, 5, "5月"),
(2026, 6, "6月"),
]
for year, month, month_name in months:
task = ReconciliationTask(
company_id=company.id,
name=f"{year}{month_name}工资对账",
period=f"{year}-{month:02d}",
status=TaskStatus.COMPLETED.value,
created_by=admin_user.id,
total_employees=len(EMPLOYEES),
)
db.add(task)
await db.flush()
tasks.append((task, year, month))
# 4. 为每个月创建异常
print("创建异常数据...")
# 10月异常
task_10, year_10, month_10 = tasks[0]
task_10.matched_count = 23
task_10.exception_count = 2
# 异常1:新员工入职(工资表有,社保表无)
exc1 = ExceptionItem(
company_id=company.id,
task_id=task_10.id,
exception_type=ExceptionType.MISSING_EMPLOYEE.value,
severity=ExceptionSeverity.MEDIUM.value,
status=ExceptionStatus.PENDING.value,
employee_id="EMP026",
employee_name="新入职员工",
description="员工【新入职员工】在工资表中存在,但在社保表中未找到记录",
detail={"source": "工资表", "missing_in": "社保表"},
ai_suggestion="请确认该员工是否已完成社保增员,如已增员请重新上传社保表",
)
db.add(exc1)
# 异常2:金额不匹配
salary_12 = calculate_salary(EMPLOYEES[11], month_10, year_10) # 胡文静
tax_12 = calculate_tax(EMPLOYEES[11], salary_12["gross_salary"])
exc2 = ExceptionItem(
company_id=company.id,
task_id=task_10.id,
exception_type=ExceptionType.AMOUNT_MISMATCH.value,
severity=ExceptionSeverity.HIGH.value,
status=ExceptionStatus.PENDING.value,
employee_id=EMPLOYEES[11]["id"],
employee_name=EMPLOYEES[11]["name"],
description=f"员工【{EMPLOYEES[11]['name']}】工资表实发金额与个税表申报金额不一致",
detail={
"salary_net": salary_12["net_salary"],
"tax_declared": tax_12["tax_amount"],
"difference": abs(salary_12["net_salary"] - tax_12["tax_amount"]),
},
salary_amount=salary_12["net_salary"],
tax_amount=tax_12["tax_amount"],
difference_amount=abs(salary_12["net_salary"] - tax_12["tax_amount"]),
ai_suggestion="请核对工资表和个税申报表的数据,确认是计算错误还是申报错误",
)
db.add(exc2)
# 11月异常
task_11, year_11, month_11 = tasks[1]
task_11.matched_count = 24
task_11.exception_count = 1
# 异常3:离职员工(工资表无,社保表有)
exc3 = ExceptionItem(
company_id=company.id,
task_id=task_11.id,
exception_type=ExceptionType.MISSING_EMPLOYEE.value,
severity=ExceptionSeverity.MEDIUM.value,
status=ExceptionStatus.CONFIRMED.value,
employee_id="EMP007",
employee_name="吴海涛",
description="员工【吴海涛】在社保表中存在,但在本月工资表中未找到",
detail={"source": "社保表", "missing_in": "工资表"},
handled_by=admin_user.id,
handled_at=datetime.utcnow(),
handling_note="该员工已于11月15日离职,工资表中已处理",
ai_suggestion="确认员工离职日期,如已办理社保减员可忽略此异常",
)
db.add(exc3)
# 12月异常(更多异常场景)
task_12, year_12, month_12 = tasks[2]
task_12.matched_count = 22
task_12.exception_count = 3
# 异常4:重复员工
exc4 = ExceptionItem(
company_id=company.id,
task_id=task_12.id,
exception_type=ExceptionType.DUPLICATE_EMPLOYEE.value,
severity=ExceptionSeverity.CRITICAL.value,
status=ExceptionStatus.PENDING.value,
employee_id="EMP019",
employee_name="韩冰冰",
description="员工【韩冰冰】在工资表中出现重复记录,请核实",
detail={"count": 2, "amounts": [8250.00, 8250.00]},
salary_amount=16500.00,
ai_suggestion="请检查是否是系统重复录入或是实际有两笔工资发放",
)
db.add(exc4)
# 异常5:金额为零
exc5 = ExceptionItem(
company_id=company.id,
task_id=task_12.id,
exception_type=ExceptionType.ZERO_AMOUNT.value,
severity=ExceptionSeverity.MEDIUM.value,
status=ExceptionStatus.PENDING.value,
employee_id="EMP022",
employee_name="邓小丽",
description="员工【邓小丽】本月实发工资为零,可能存在异常",
detail={"expected_amount": 6000.00, "actual_amount": 0},
salary_amount=0,
ai_suggestion="请确认该员工是否已离职、停薪留职或工资发放被扣押",
)
db.add(exc5)
# 异常6:金额异常(过高)
salary_3 = calculate_salary(EMPLOYEES[2], month_12, year_12)
exc6 = ExceptionItem(
company_id=company.id,
task_id=task_12.id,
exception_type=ExceptionType.UNUSUAL_AMOUNT.value,
severity=ExceptionSeverity.HIGH.value,
status=ExceptionStatus.PENDING.value,
employee_id=EMPLOYEES[2]["id"],
employee_name=EMPLOYEES[2]["name"],
description=f"员工【{EMPLOYEES[2]['name']}】本月工资较上月有较大波动",
detail={
"current": salary_3["net_salary"],
"previous_avg": 42000.00,
"change_ratio": 0.25,
},
salary_amount=salary_3["net_salary"],
difference_amount=8500.00,
ai_suggestion="该员工工资增长超过20%,请确认是否有大额奖金或提成发放",
)
db.add(exc6)
# 5. 为每个月创建上传文件
print("创建上传文件...")
files = []
for task, year, month in tasks:
# 工资表
salary_file = UploadedFile(
company_id=company.id,
task_id=task.id,
file_type=FileType.SALARY.value,
original_filename=f"{year}{month:02d}月工资表.xlsx",
stored_filename=f"salary_{task.id}_{year}{month:02d}.xlsx",
file_size=102400,
mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
parse_status=ParseStatus.SUCCESS.value,
)
db.add(salary_file)
# 社保表
social_file = UploadedFile(
company_id=company.id,
task_id=task.id,
file_type=FileType.SOCIAL_SECURITY.value,
original_filename=f"{year}{month:02d}月社保表.xlsx",
stored_filename=f"social_{task.id}_{year}{month:02d}.xlsx",
file_size=92160,
mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
parse_status=ParseStatus.SUCCESS.value,
)
db.add(social_file)
# 个税表
tax_file = UploadedFile(
company_id=company.id,
task_id=task.id,
file_type=FileType.TAX.value,
original_filename=f"{year}{month:02d}月个税表.xlsx",
stored_filename=f"tax_{task.id}_{year}{month:02d}.xlsx",
file_size=81920,
mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
parse_status=ParseStatus.SUCCESS.value,
)
db.add(tax_file)
await db.flush()
files.append((salary_file.id, social_file.id, tax_file.id))
# 6. 创建字段映射规则(关联到第一个文件作为示例)
print("创建字段映射规则...")
first_file_id = files[0][0] # 使用10月工资表的文件ID
field_mappings = [
("员工编号", "employee_id", 0.98),
("工号", "employee_id", 0.95),
("姓名", "employee_name", 0.99),
("员工姓名", "employee_name", 0.99),
("部门", "department", 0.97),
("所属部门", "department", 0.95),
("岗位", "position", 0.90),
("职位", "position", 0.90),
("基本工资", "base_salary", 0.98),
("岗位工资", "base_salary", 0.92),
("应发工资", "gross_salary", 0.96),
("实发工资", "net_salary", 0.97),
("税前工资", "gross_salary", 0.93),
("养老保险", "pension", 0.95),
("医疗保险", "medical", 0.95),
("社保", "social_security", 0.90),
("公积金", "housing_fund", 0.95),
("个税", "personal_income_tax", 0.98),
("个人所得税", "personal_income_tax", 0.98),
]
for source, standard, confidence in field_mappings:
mapping = FieldMapping(
company_id=company.id,
file_id=first_file_id,
source_field=source,
standard_field=standard,
confidence=confidence,
confirmed=True,
confirmed_by=admin_user.id,
confirmed_at=datetime.utcnow(),
)
db.add(mapping)
await db.commit()
print("示例数据创建成功!")
print("\n" + "="*50)
print("登录信息:")
print("="*50)
for user_data in USERS_DATA:
print(f" 邮箱: {user_data['email']}")
print(f" 密码: {user_data['password']}")
print(f" 角色: {user_data['role']}")
print("-"*30)
print("\n包含 3 个月(4月、5月、6月)的对账数据")
print("共 25 名员工,多种异常场景")
except Exception as e:
await db.rollback()
print(f"创建示例数据失败: {e}")
raise
if __name__ == "__main__":
import asyncio
asyncio.run(init_sample_data())