v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成

- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
hankin
2026-03-16 07:31:37 +00:00
commit 423baff73b
2578 changed files with 824643 additions and 0 deletions
View File
+55
View File
@@ -0,0 +1,55 @@
"""
新增 ORM 模型 — ai_chat_sessions / sales_logs / ai_report_drafts
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, SmallInteger, String, Text, func
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
class AiChatSession(Base):
__tablename__ = "ai_chat_sessions"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False)
role: Mapped[str] = mapped_column(String(10), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
msg_type: Mapped[str] = mapped_column(String(20), default="text")
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
class SalesLog(Base):
__tablename__ = "sales_logs"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
salesperson_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False)
customer_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=True)
content: Mapped[str] = mapped_column(Text, nullable=False)
log_date: Mapped[date] = mapped_column(Date, default=date.today)
contact_ids: Mapped[list | None] = mapped_column(JSONB, default=list, nullable=True)
ai_processed: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
class AiReportDraft(Base):
__tablename__ = "ai_report_drafts"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
author_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False)
report_type: Mapped[str] = mapped_column(String(20), nullable=False)
period_start: Mapped[date] = mapped_column(Date, nullable=False)
period_end: Mapped[date] = mapped_column(Date, nullable=False)
content_md: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[str] = mapped_column(String(20), default="draft")
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
+10
View File
@@ -0,0 +1,10 @@
"""
SQLAlchemy 声明式基类
"""
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""所有 ORM Model 的共同父类"""
pass
+68
View File
@@ -0,0 +1,68 @@
"""
CRM 客户域 ORM 模型 —— 映射 crm_customers 表
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Numeric, SmallInteger, String, Text, func
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class CrmCustomer(Base):
__tablename__ = "crm_customers"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(200), nullable=False)
level: Mapped[str] = mapped_column(String(1), default="C")
industry: Mapped[str | None] = mapped_column(String(100), nullable=True)
contact: Mapped[str | None] = mapped_column(String(50), nullable=True)
phone: Mapped[str | None] = mapped_column(String(30), nullable=True)
email: Mapped[str | None] = mapped_column(String(100), nullable=True)
address: Mapped[str | None] = mapped_column(Text, nullable=True)
ai_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0)
ai_persona: Mapped[dict | None] = mapped_column(JSONB, default=dict, nullable=True)
owner_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
status: Mapped[int] = mapped_column(SmallInteger, default=1)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
owner: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
contacts: Mapped[list["CrmContact"]] = relationship("CrmContact", back_populates="customer", lazy="selectin")
class CrmContact(Base):
"""客户联系人子表 (V5.0) — 映射 crm_contacts"""
__tablename__ = "crm_contacts"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
customer_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=False
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
phone: Mapped[str | None] = mapped_column(String(30), nullable=True)
title: Mapped[str | None] = mapped_column(String(100), nullable=True)
ai_buyer_persona: Mapped[dict | None] = mapped_column(JSONB, default=dict, nullable=True)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
# 关系
customer: Mapped["CrmCustomer"] = relationship("CrmCustomer", back_populates="contacts")
+96
View File
@@ -0,0 +1,96 @@
"""
ERP 供应链域 ORM 模型
映射: erp_product_categories / erp_product_skus / erp_inventory_flows
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Integer,
Numeric,
SmallInteger,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class ProductCategory(Base):
__tablename__ = "erp_product_categories"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
parent_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_categories.id"), nullable=True
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
class ProductSku(Base):
__tablename__ = "erp_product_skus"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
category_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_categories.id"), nullable=True
)
sku_code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
name: Mapped[str] = mapped_column(String(200), nullable=False)
spec: Mapped[str | None] = mapped_column(String(100), nullable=True)
standard_price: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
stock_qty: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
warning_threshold: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
unit: Mapped[str] = mapped_column(String(20), default="")
status: Mapped[int] = mapped_column(SmallInteger, default=1)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
category: Mapped[ProductCategory | None] = relationship(
"ProductCategory", lazy="selectin"
)
class InventoryFlow(Base):
__tablename__ = "erp_inventory_flows"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
change_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
reason: Mapped[str] = mapped_column(String(50), nullable=False)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
operator_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
sku: Mapped[ProductSku | None] = relationship("ProductSku", lazy="selectin")
operator: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
+152
View File
@@ -0,0 +1,152 @@
"""
财务票据域 ORM 模型
映射: fin_invoice_pool / fin_expense_records / fin_expense_details
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class FinInvoicePool(Base):
__tablename__ = "fin_invoice_pool"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
uploader_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
file_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
merchant_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
invoice_date: Mapped[date | None] = mapped_column(Date, nullable=True)
type: Mapped[str] = mapped_column(String(30), nullable=False, default="expense")
ai_extracted_data: Mapped[dict] = mapped_column(JSONB, default=dict)
is_used: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
uploader: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
class FinExpenseRecord(Base):
__tablename__ = "fin_expense_records"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
system_no: Mapped[str] = mapped_column(String(30), unique=True, nullable=False)
applicant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False
)
total_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="draft")
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
approved_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
approved_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
applicant: Mapped["SysUser"] = relationship( # noqa: F821
"SysUser", foreign_keys=[applicant_id], lazy="selectin"
)
approver: Mapped["SysUser | None"] = relationship( # noqa: F821
"SysUser", foreign_keys=[approved_by], lazy="selectin"
)
details: Mapped[list[FinExpenseDetail]] = relationship(
"FinExpenseDetail", back_populates="expense_record", lazy="selectin"
)
class FinExpenseDetail(Base):
__tablename__ = "fin_expense_details"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
expense_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("fin_expense_records.id"), nullable=False
)
invoice_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("fin_invoice_pool.id"), nullable=True
)
expense_desc: Mapped[str | None] = mapped_column(String(500), nullable=True)
expense_date: Mapped[date | None] = mapped_column(Date, nullable=True)
original_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
offset_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
expense_record: Mapped[FinExpenseRecord] = relationship(
"FinExpenseRecord", back_populates="details"
)
invoice: Mapped[FinInvoicePool | None] = relationship(
"FinInvoicePool", lazy="selectin"
)
class FinSalesInvoice(Base):
"""销项发票表(AR 应收账款核心)"""
__tablename__ = "finance_sales_invoices"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
issuer: Mapped[str] = mapped_column(String(200), nullable=False)
receiver_customer_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=False
)
invoice_number: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
billing_date: Mapped[date] = mapped_column(Date, nullable=False)
payment_status: Mapped[str] = mapped_column(
String(20), nullable=False, default="未回款"
)
payment_date: Mapped[date | None] = mapped_column(Date, nullable=True)
payment_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# Relationships
receiver_customer: Mapped["CrmCustomer"] = relationship( # noqa: F821
"CrmCustomer", lazy="selectin"
)
creator: Mapped["SysUser | None"] = relationship( # noqa: F821
"SysUser", lazy="selectin"
)
+88
View File
@@ -0,0 +1,88 @@
"""
ERP 交易域 ORM 模型
映射: erp_orders / erp_order_items
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class ErpOrder(Base):
__tablename__ = "erp_orders"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
order_no: Mapped[str] = mapped_column(String(30), unique=True, nullable=False)
customer_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=False
)
salesperson_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
total_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
shipping_state: Mapped[str] = mapped_column(
String(20), nullable=False, default="pending"
)
payment_state: Mapped[str] = mapped_column(
String(20), nullable=False, default="unpaid"
)
paid_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
order_date: Mapped[date] = mapped_column(Date, default=date.today)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
customer: Mapped["CrmCustomer"] = relationship("CrmCustomer", lazy="selectin") # noqa: F821
salesperson: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
items: Mapped[list[ErpOrderItem]] = relationship(
"ErpOrderItem", back_populates="order", lazy="selectin"
)
class ErpOrderItem(Base):
__tablename__ = "erp_order_items"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
order_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_orders.id"), nullable=False
)
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
sub_total: Mapped[float] = mapped_column(Numeric(14, 2), nullable=False)
shipped_qty: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
order: Mapped[ErpOrder] = relationship("ErpOrder", back_populates="items")
sku: Mapped["ProductSku"] = relationship("ProductSku", lazy="selectin") # noqa: F821
+86
View File
@@ -0,0 +1,86 @@
"""
ERP 物流域 ORM 模型
映射: erp_shipping_records / erp_shipping_items
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class ErpShippingRecord(Base):
__tablename__ = "erp_shipping_records"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
shipping_no: Mapped[str] = mapped_column(String(30), unique=True, nullable=False)
order_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_orders.id"), nullable=False
)
carrier: Mapped[str | None] = mapped_column(String(100), nullable=True)
tracking_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="transit")
ship_date: Mapped[date] = mapped_column(Date, default=date.today)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
operator_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
order: Mapped["ErpOrder"] = relationship("ErpOrder", lazy="selectin") # noqa: F821
operator: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
items: Mapped[list[ErpShippingItem]] = relationship(
"ErpShippingItem", back_populates="shipping_record", lazy="selectin"
)
class ErpShippingItem(Base):
__tablename__ = "erp_shipping_items"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
shipping_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_shipping_records.id"), nullable=False
)
order_item_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_order_items.id"), nullable=False
)
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
shipped_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
shipping_record: Mapped[ErpShippingRecord] = relationship(
"ErpShippingRecord", back_populates="items"
)
order_item: Mapped["ErpOrderItem"] = relationship("ErpOrderItem", lazy="selectin") # noqa: F821
sku: Mapped["ProductSku"] = relationship("ProductSku", lazy="selectin") # noqa: F821
+99
View File
@@ -0,0 +1,99 @@
"""
RBAC 权限域 ORM 模型 —— 映射到已有的 sys_departments / sys_roles / sys_users 表
注意: 表已由 schema.sql 创建,这里仅做 ORM 映射,不使用 metadata.create_all
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, SmallInteger, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class SysDepartment(Base):
__tablename__ = "sys_departments"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
parent_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_departments.id"), nullable=True
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
sort_order: Mapped[int] = mapped_column(SmallInteger, default=0)
status: Mapped[int] = mapped_column(SmallInteger, default=1)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 自引用关系
children: Mapped[list[SysDepartment]] = relationship(
"SysDepartment", back_populates="parent", lazy="selectin"
)
parent: Mapped[SysDepartment | None] = relationship(
"SysDepartment", back_populates="children", remote_side=[id], lazy="selectin"
)
class SysRole(Base):
__tablename__ = "sys_roles"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
role_name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
data_scope: Mapped[str] = mapped_column(String(20), nullable=False, default="self")
menu_keys: Mapped[dict] = mapped_column(JSONB, default=list)
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[int] = mapped_column(SmallInteger, default=1)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
class SysUser(Base):
__tablename__ = "sys_users"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
dept_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_departments.id"), nullable=True
)
role_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_roles.id"), nullable=True
)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
real_name: Mapped[str | None] = mapped_column(String(50), nullable=True)
phone: Mapped[str | None] = mapped_column(String(20), nullable=True)
email: Mapped[str | None] = mapped_column(String(100), nullable=True)
avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
status: Mapped[int] = mapped_column(SmallInteger, default=1)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
department: Mapped[SysDepartment | None] = relationship(
"SysDepartment", lazy="selectin"
)
role: Mapped[SysRole | None] = relationship("SysRole", lazy="selectin")