Files
s2f/backend/scripts/init_sample_data.py
T
freedakgmail 33b4c734aa 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 数据库迁移配置
- 添加初始化示例数据脚本
- 更新项目文档
2026-07-07 09:04:47 +08:00

556 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
示例数据初始化脚本
创建一套完整的真实业务场景示例数据:
- 企业:深圳星辰科技有限公司
- 用户: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())