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,53 @@
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class AuditAction(str, Enum):
|
||||
"""审计操作类型"""
|
||||
CREATE = "CREATE"
|
||||
UPDATE = "UPDATE"
|
||||
DELETE = "DELETE"
|
||||
VIEW = "VIEW"
|
||||
EXPORT = "EXPORT"
|
||||
LOGIN = "LOGIN"
|
||||
LOGOUT = "LOGOUT"
|
||||
UPLOAD = "UPLOAD"
|
||||
DOWNLOAD = "DOWNLOAD"
|
||||
|
||||
|
||||
class AuditLog(BaseModel):
|
||||
"""
|
||||
审计日志模型
|
||||
记录所有关键业务操作
|
||||
|
||||
Attributes:
|
||||
company_id: 企业 ID
|
||||
user_id: 用户 ID
|
||||
action: 操作类型
|
||||
resource_type: 资源类型 (如 company, user, task, file)
|
||||
resource_id: 资源 ID
|
||||
details: 操作详情 (JSON)
|
||||
ip_address: IP 地址
|
||||
user_agent: User-Agent
|
||||
"""
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
resource_type: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
resource_id: Mapped[str | None] = mapped_column(String(200), nullable=True, index=True)
|
||||
details: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
ip_address: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<AuditLog(id={self.id}, company_id={self.company_id}, "
|
||||
f"action='{self.action}', resource_type='{self.resource_type}')>"
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import DateTime, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class BaseModel(Base):
|
||||
"""
|
||||
所有数据库模型的基类
|
||||
包含通用字段:id, created_at, updated_at
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
将模型转换为字典
|
||||
"""
|
||||
return {
|
||||
column.name: getattr(self, column.name)
|
||||
for column in self.__table__.columns
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Boolean, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class CompanyPlan(str, Enum):
|
||||
"""企业套餐类型"""
|
||||
FREE = "免费版"
|
||||
BASIC = "基础版"
|
||||
PROFESSIONAL = "专业版"
|
||||
ENTERPRISE = "企业版"
|
||||
|
||||
|
||||
class CompanyStatus(str, Enum):
|
||||
"""企业状态"""
|
||||
ACTIVE = "正常"
|
||||
SUSPENDED = "暂停"
|
||||
EXPIRED = "已过期"
|
||||
DELETED = "已删除"
|
||||
|
||||
|
||||
class Company(BaseModel):
|
||||
"""
|
||||
企业模型 - 多租户隔离的核心实体
|
||||
|
||||
Attributes:
|
||||
name: 企业名称
|
||||
plan: 套餐类型
|
||||
data_retention_months: 数据保留月数
|
||||
status: 企业状态
|
||||
max_users: 最大用户数
|
||||
is_trial: 是否试用
|
||||
"""
|
||||
|
||||
__tablename__ = "companies"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
|
||||
plan: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
default=CompanyPlan.FREE.value
|
||||
)
|
||||
data_retention_months: Mapped[int] = mapped_column(Integer, nullable=False, default=12)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default=CompanyStatus.ACTIVE.value,
|
||||
index=True,
|
||||
)
|
||||
max_users: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
|
||||
is_trial: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
# 关系
|
||||
users = relationship("User", back_populates="company")
|
||||
field_mappings = relationship("FieldMapping", back_populates="company")
|
||||
rules = relationship("CompanyRule", back_populates="company")
|
||||
tasks = relationship("ReconciliationTask", back_populates="company")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Company(id={self.id}, name='{self.name}', plan='{self.plan}')>"
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
企业规则模型
|
||||
|
||||
用于存储企业自定义的映射规则,实现规则复用
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Boolean, DateTime, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class RuleType(str, Enum):
|
||||
"""规则类型"""
|
||||
FIELD_MAPPING = "FIELD_MAPPING" # 字段映射规则
|
||||
ACCOUNT_MAPPING = "ACCOUNT_MAPPING" # 科目映射规则
|
||||
DEPARTMENT_MAPPING = "DEPARTMENT_MAPPING" # 部门映射规则
|
||||
|
||||
|
||||
class RuleStatus(str, Enum):
|
||||
"""规则状态"""
|
||||
ACTIVE = "ACTIVE" # 启用
|
||||
INACTIVE = "INACTIVE" # 停用
|
||||
|
||||
|
||||
class CompanyRule(BaseModel):
|
||||
"""
|
||||
企业规则模型
|
||||
|
||||
存储企业自定义的业务规则,用于自动应用
|
||||
"""
|
||||
|
||||
__tablename__ = "company_rules"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
rule_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="规则类型")
|
||||
match_condition: Mapped[dict] = mapped_column(JSON, nullable=False, comment="匹配条件")
|
||||
target_value: Mapped[str] = mapped_column(String(100), nullable=False, comment="目标值")
|
||||
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="优先级,数字越大优先级越高")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default=RuleStatus.ACTIVE.value, comment="状态")
|
||||
description: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment="规则描述")
|
||||
match_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="匹配次数")
|
||||
last_used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
created_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
updated_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# 关系
|
||||
company = relationship("Company", back_populates="rules")
|
||||
creator = relationship("User", foreign_keys=[created_by])
|
||||
updater = relationship("User", foreign_keys=[updated_by])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<CompanyRule(type='{self.rule_type}', target='{self.target_value}', priority={self.priority})>"
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
异常项模型
|
||||
|
||||
存储对账过程中检测到的异常
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text, DateTime, JSON, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class ExceptionType(str, Enum):
|
||||
"""异常类型"""
|
||||
# 第一阶段:工资表内部异常
|
||||
MISSING_EMPLOYEE = "MISSING_EMPLOYEE" # 员工缺失(三个表之一缺失该员工)
|
||||
AMOUNT_MISMATCH = "AMOUNT_MISMATCH" # 金额不匹配
|
||||
ZERO_AMOUNT = "ZERO_AMOUNT" # 金额为零
|
||||
NEGATIVE_AMOUNT = "NEGATIVE_AMOUNT" # 负数金额
|
||||
UNUSUAL_AMOUNT = "UNUSUAL_AMOUNT" # 金额异常(过大或过小)
|
||||
DUPLICATE_EMPLOYEE = "DUPLICATE_EMPLOYEE" # 重复员工
|
||||
|
||||
# 第二阶段:银行实发对账(预留)
|
||||
BANK_MISMATCH = "BANK_MISMATCH" # 银行实发与工资表不匹配
|
||||
BANK_MISSING = "BANK_MISSING" # 银行记录缺失
|
||||
BANK_EXTRA = "BANK_EXTRA" # 银行多余记录
|
||||
|
||||
|
||||
class ExceptionStatus(str, Enum):
|
||||
"""异常状态"""
|
||||
PENDING = "PENDING" # 待处理
|
||||
CONFIRMED = "CONFIRMED" # 已确认
|
||||
IGNORED = "IGNORED" # 已忽略
|
||||
RESOLVED = "RESOLVED" # 已解决
|
||||
|
||||
|
||||
class ExceptionSeverity(str, Enum):
|
||||
"""异常严重程度"""
|
||||
LOW = "LOW" # 低
|
||||
MEDIUM = "MEDIUM" # 中
|
||||
HIGH = "HIGH" # 高
|
||||
CRITICAL = "CRITICAL" # 严重
|
||||
|
||||
|
||||
class ExceptionItem(BaseModel):
|
||||
"""
|
||||
异常项模型
|
||||
|
||||
记录对账过程中检测到的各类异常
|
||||
"""
|
||||
|
||||
__tablename__ = "exception_items"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
task_id: Mapped[int] = mapped_column(Integer, ForeignKey("reconciliation_tasks.id"), nullable=False, index=True)
|
||||
|
||||
# 异常基本信息
|
||||
exception_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="异常类型")
|
||||
severity: Mapped[str] = mapped_column(String(20), nullable=False, default=ExceptionSeverity.MEDIUM.value, comment="严重程度")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default=ExceptionStatus.PENDING.value, index=True, comment="状态")
|
||||
|
||||
# 关联的员工信息
|
||||
employee_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True, comment="员工ID")
|
||||
employee_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="员工姓名")
|
||||
|
||||
# 异常详情
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, comment="异常描述")
|
||||
detail: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="详细信息")
|
||||
|
||||
# 涉及的金额
|
||||
salary_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="工资表金额")
|
||||
social_security_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="社保表金额")
|
||||
tax_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="个税表金额")
|
||||
bank_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="银行实发金额")
|
||||
|
||||
# 差异金额
|
||||
difference_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True, comment="差异金额")
|
||||
|
||||
# 建议处理方式
|
||||
suggested_action: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment="建议处理方式")
|
||||
ai_suggestion: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="AI 分析建议")
|
||||
|
||||
# 处理信息
|
||||
handled_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
handled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
handling_note: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="处理备注")
|
||||
|
||||
# 关系
|
||||
company = relationship("Company", backref="exceptions")
|
||||
task = relationship("ReconciliationTask", backref="exceptions")
|
||||
handler = relationship("User", foreign_keys=[handled_by])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ExceptionItem(id={self.id}, type='{self.exception_type}', status='{self.status}')>"
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
字段映射模型
|
||||
|
||||
用于存储 AI 识别的字段映射结果
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Float, Boolean, DateTime, JSON, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class FieldMapping(BaseModel):
|
||||
"""
|
||||
字段映射模型
|
||||
|
||||
记录源字段到标准字段的映射关系
|
||||
"""
|
||||
|
||||
__tablename__ = "field_mappings"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
file_id: Mapped[int] = mapped_column(Integer, ForeignKey("uploaded_files.id"), nullable=False, index=True)
|
||||
source_field: Mapped[str] = mapped_column(String(100), nullable=False, comment="源字段名")
|
||||
standard_field: Mapped[str] = mapped_column(String(100), nullable=False, comment="标准字段名")
|
||||
confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.0, comment="置信度 0-1")
|
||||
confirmed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否已确认")
|
||||
confirmed_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
confirmed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
sample_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="样例值")
|
||||
reasoning: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="AI 判断依据")
|
||||
is_skipped: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否跳过")
|
||||
|
||||
# 关系
|
||||
company = relationship("Company", back_populates="field_mappings")
|
||||
file = relationship("UploadedFile", back_populates="field_mappings")
|
||||
confirmed_by_user = relationship("User", foreign_keys=[confirmed_by])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<FieldMapping(source='{self.source_field}' -> standard='{self.standard_field}', confidence={self.confidence})>"
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
对账任务模型
|
||||
|
||||
用于管理工资对账任务的生命周期
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, DateTime, JSON, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""任务状态"""
|
||||
CREATED = "CREATED" # 已创建
|
||||
FILES_UPLOADED = "FILES_UPLOADED" # 文件已上传
|
||||
PARSING = "PARSING" # 解析中
|
||||
WAITING_MAPPING_CONFIRM = "WAITING_MAPPING_CONFIRM" # 等待字段映射确认
|
||||
MAPPING_CONFIRMED = "MAPPING_CONFIRMED" # 字段映射已确认
|
||||
RECONCILING = "RECONCILING" # 对账中
|
||||
COMPLETED = "COMPLETED" # 完成
|
||||
FAILED = "FAILED" # 失败
|
||||
|
||||
|
||||
class ReconciliationTask(BaseModel):
|
||||
"""
|
||||
对账任务模型
|
||||
|
||||
管理一次完整的工资对账流程
|
||||
"""
|
||||
|
||||
__tablename__ = "reconciliation_tasks"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="任务名称")
|
||||
period: Mapped[str] = mapped_column(String(20), nullable=False, index=True, comment="对账期间,如 2024-01")
|
||||
status: Mapped[str] = mapped_column(String(30), nullable=False, default=TaskStatus.CREATED.value, index=True)
|
||||
|
||||
# 文件信息 (JSON 格式存储)
|
||||
file_ids: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="关联的文件ID")
|
||||
|
||||
# 字段映射状态
|
||||
mapping_completed: Mapped[bool] = mapped_column(Integer, nullable=False, default=False)
|
||||
mapping_confirmed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
mapping_confirmed_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# 对账结果摘要
|
||||
total_employees: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
matched_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
exception_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
|
||||
# 结果数据 (JSON)
|
||||
reconciliation_result: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 错误信息
|
||||
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 确认信息
|
||||
created_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 关系
|
||||
company = relationship("Company", back_populates="tasks")
|
||||
creator = relationship("User", foreign_keys=[created_by])
|
||||
confirm_user = relationship("User", foreign_keys=[mapping_confirmed_by])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ReconciliationTask(id={self.id}, name='{self.name}', status='{self.status}')>"
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
标准字段定义
|
||||
|
||||
定义工资表、社保表、个税表的标准字段
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StandardField(str, Enum):
|
||||
"""标准字段枚举"""
|
||||
|
||||
# ===== 公共字段 =====
|
||||
EMPLOYEE_NAME = "员工姓名" # 员工姓名
|
||||
EMPLOYEE_ID = "工号" # 工号
|
||||
DEPARTMENT = "部门" # 部门
|
||||
POSITION = "岗位" # 岗位/职位
|
||||
|
||||
# ===== 工资表字段 =====
|
||||
BASE_SALARY = "基本工资" # 基本工资
|
||||
BONUS = "奖金" # 奖金
|
||||
ALLOWANCE = "补贴" # 补贴
|
||||
OVERTIME_PAY = "加班费" # 加班费
|
||||
DEDUCTION = "扣款" # 扣款
|
||||
GROSS_SALARY = "应发工资" # 应发工资
|
||||
NET_SALARY = "实发工资" # 实发工资/净工资
|
||||
BANK_CARD = "银行账号" # 银行账号
|
||||
ID_CARD = "身份证号" # 身份证号
|
||||
|
||||
# ===== 社保表字段 =====
|
||||
SOCIAL_SECURITY_BASE = "社保基数" # 社保基数
|
||||
PENSION_INSURANCE = "养老保险" # 养老保险(个人)
|
||||
MEDICAL_INSURANCE = "医疗保险" # 医疗保险(个人)
|
||||
UNEMPLOYMENT_INSURANCE = "失业保险" # 失业保险(个人)
|
||||
HOUSING_FUND = "公积金" # 住房公积金(个人)
|
||||
PENSION_INSURANCE_COMPANY = "养老保险(公司)" # 养老保险(公司)
|
||||
MEDICAL_INSURANCE_COMPANY = "医疗保险(公司)" # 医疗保险(公司)
|
||||
UNEMPLOYMENT_INSURANCE_COMPANY = "失业保险(公司)" # 失业保险(公司)
|
||||
HOUSING_FUND_COMPANY = "公积金(公司)" # 住房公积金(公司)
|
||||
SOCIAL_SECURITY_TOTAL = "社保合计" # 社保合计
|
||||
|
||||
# ===== 个税表字段 =====
|
||||
TAXABLE_INCOME = "应税收入" # 应税收入
|
||||
PRE_TAX_DEDUCTION = "税前扣除" # 税前扣除(三险一金等)
|
||||
TAX_FREE_INCOME = "免税收入" # 免税收入
|
||||
TAX_EXEMPT_INCOME = "税前减免" # 税前减免
|
||||
QUICK_DEDUCTION = "速算扣除数" # 速算扣除数
|
||||
TAX_AMOUNT = "应缴个税" # 应缴个人所得税
|
||||
TAX_PAID = "已缴个税" # 已缴个人所得税
|
||||
AFTER_TAX_INCOME = "税后收入" # 税后收入
|
||||
|
||||
|
||||
# 标准字段分组
|
||||
FIELD_GROUPS = {
|
||||
"公共字段": [
|
||||
StandardField.EMPLOYEE_NAME,
|
||||
StandardField.EMPLOYEE_ID,
|
||||
StandardField.DEPARTMENT,
|
||||
StandardField.POSITION,
|
||||
],
|
||||
"工资表字段": [
|
||||
StandardField.BASE_SALARY,
|
||||
StandardField.BONUS,
|
||||
StandardField.ALLOWANCE,
|
||||
StandardField.OVERTIME_PAY,
|
||||
StandardField.DEDUCTION,
|
||||
StandardField.GROSS_SALARY,
|
||||
StandardField.NET_SALARY,
|
||||
StandardField.BANK_CARD,
|
||||
StandardField.ID_CARD,
|
||||
],
|
||||
"社保表字段": [
|
||||
StandardField.SOCIAL_SECURITY_BASE,
|
||||
StandardField.PENSION_INSURANCE,
|
||||
StandardField.MEDICAL_INSURANCE,
|
||||
StandardField.UNEMPLOYMENT_INSURANCE,
|
||||
StandardField.HOUSING_FUND,
|
||||
StandardField.PENSION_INSURANCE_COMPANY,
|
||||
StandardField.MEDICAL_INSURANCE_COMPANY,
|
||||
StandardField.UNEMPLOYMENT_INSURANCE_COMPANY,
|
||||
StandardField.HOUSING_FUND_COMPANY,
|
||||
StandardField.SOCIAL_SECURITY_TOTAL,
|
||||
],
|
||||
"个税表字段": [
|
||||
StandardField.TAXABLE_INCOME,
|
||||
StandardField.PRE_TAX_DEDUCTION,
|
||||
StandardField.TAX_FREE_INCOME,
|
||||
StandardField.TAX_EXEMPT_INCOME,
|
||||
StandardField.QUICK_DEDUCTION,
|
||||
StandardField.TAX_AMOUNT,
|
||||
StandardField.TAX_PAID,
|
||||
StandardField.AFTER_TAX_INCOME,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# 字段类型映射
|
||||
FIELD_TYPES = {
|
||||
StandardField.EMPLOYEE_NAME.value: "string",
|
||||
StandardField.EMPLOYEE_ID.value: "string",
|
||||
StandardField.DEPARTMENT.value: "string",
|
||||
StandardField.POSITION.value: "string",
|
||||
StandardField.BASE_SALARY.value: "number",
|
||||
StandardField.BONUS.value: "number",
|
||||
StandardField.ALLOWANCE.value: "number",
|
||||
StandardField.OVERTIME_PAY.value: "number",
|
||||
StandardField.DEDUCTION.value: "number",
|
||||
StandardField.GROSS_SALARY.value: "number",
|
||||
StandardField.NET_SALARY.value: "number",
|
||||
StandardField.BANK_CARD.value: "string",
|
||||
StandardField.ID_CARD.value: "string",
|
||||
StandardField.SOCIAL_SECURITY_BASE.value: "number",
|
||||
StandardField.PENSION_INSURANCE.value: "number",
|
||||
StandardField.MEDICAL_INSURANCE.value: "number",
|
||||
StandardField.UNEMPLOYMENT_INSURANCE.value: "number",
|
||||
StandardField.HOUSING_FUND.value: "number",
|
||||
StandardField.PENSION_INSURANCE_COMPANY.value: "number",
|
||||
StandardField.MEDICAL_INSURANCE_COMPANY.value: "number",
|
||||
StandardField.UNEMPLOYMENT_INSURANCE_COMPANY.value: "number",
|
||||
StandardField.HOUSING_FUND_COMPANY.value: "number",
|
||||
StandardField.SOCIAL_SECURITY_TOTAL.value: "number",
|
||||
StandardField.TAXABLE_INCOME.value: "number",
|
||||
StandardField.PRE_TAX_DEDUCTION.value: "number",
|
||||
StandardField.TAX_FREE_INCOME.value: "number",
|
||||
StandardField.TAX_EXEMPT_INCOME.value: "number",
|
||||
StandardField.QUICK_DEDUCTION.value: "number",
|
||||
StandardField.TAX_AMOUNT.value: "number",
|
||||
StandardField.TAX_PAID.value: "number",
|
||||
StandardField.AFTER_TAX_INCOME.value: "number",
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, BigInteger, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class FileType(str, Enum):
|
||||
"""文件类型"""
|
||||
SALARY = "工资表"
|
||||
SOCIAL_SECURITY = "社保表"
|
||||
TAX = "个税表"
|
||||
|
||||
|
||||
class ParseStatus(str, Enum):
|
||||
"""解析状态"""
|
||||
PENDING = "待解析"
|
||||
PARSING = "解析中"
|
||||
SUCCESS = "解析成功"
|
||||
FAILED = "解析失败"
|
||||
|
||||
|
||||
class UploadedFile(BaseModel):
|
||||
"""
|
||||
上传文件模型
|
||||
|
||||
Attributes:
|
||||
company_id: 企业ID
|
||||
task_id: 对账任务ID(可选)
|
||||
file_type: 文件类型
|
||||
original_filename: 原始文件名
|
||||
stored_filename: 存储文件名
|
||||
file_size: 文件大小(字节)
|
||||
mime_type: MIME类型
|
||||
parse_status: 解析状态
|
||||
parse_error: 解析错误信息
|
||||
"""
|
||||
|
||||
__tablename__ = "uploaded_files"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
task_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
file_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
original_filename: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
stored_filename: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||
file_size: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
mime_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
parse_status: Mapped[str] = mapped_column(String(20), nullable=False, default=ParseStatus.PENDING.value, index=True)
|
||||
parse_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# 关系
|
||||
field_mappings: Mapped[List["FieldMapping"]] = relationship(
|
||||
"FieldMapping", back_populates="file", lazy="selectin"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UploadedFile(id={self.id}, filename='{self.original_filename}', type='{self.file_type}')>"
|
||||
@@ -0,0 +1,62 @@
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, JSON, String, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
"""用户角色"""
|
||||
ADMIN = "管理员"
|
||||
FINANCE_MANAGER = "财务主管"
|
||||
ACCOUNTANT = "会计"
|
||||
CASHIER = "出纳"
|
||||
HR = "人事"
|
||||
|
||||
|
||||
class UserStatus(str, Enum):
|
||||
"""用户状态"""
|
||||
ACTIVE = "正常"
|
||||
DISABLED = "禁用"
|
||||
LOCKED = "锁定"
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""
|
||||
用户模型
|
||||
|
||||
Attributes:
|
||||
company_id: 企业ID
|
||||
email: 邮箱(登录账号)
|
||||
hashed_password: 加密后的密码
|
||||
full_name: 姓名
|
||||
role: 角色
|
||||
permissions: 权限列表(JSONB)
|
||||
status: 状态
|
||||
last_login_at: 最后登录时间
|
||||
"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id"), nullable=False, index=True)
|
||||
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
|
||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
full_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default=UserRole.ACCOUNTANT.value)
|
||||
permissions: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default=UserStatus.ACTIVE.value, index=True)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# 关系
|
||||
company = relationship("Company", back_populates="users")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User(id={self.id}, email='{self.email}', role='{self.role}')>"
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""检查用户是否有指定权限"""
|
||||
if not self.permissions:
|
||||
return False
|
||||
return permission in self.permissions.get("permissions", [])
|
||||
Reference in New Issue
Block a user