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
+53
View File
@@ -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}')>"
)
+39
View File
@@ -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
}
+63
View File
@@ -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}')>"
+57
View File
@@ -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})>"
+98
View File
@@ -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}')>"
+43
View File
@@ -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})>"
+73
View File
@@ -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}')>"
+130
View File
@@ -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",
}
+59
View File
@@ -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}')>"
+62
View File
@@ -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", [])