forked from erp-dev/erp
288 lines
9.4 KiB
Python
288 lines
9.4 KiB
Python
"""
|
||
API v1 通用模型
|
||
"""
|
||
import os
|
||
import uuid
|
||
from django.db import models
|
||
from django.contrib.auth import get_user_model
|
||
from django.db.models.fields.json import KeyTextTransform
|
||
from flower.common import ModelBase
|
||
from basic_info import models as basic_info_models
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
def upload_file_path(instance, filename):
|
||
"""
|
||
生成随机化的文件上传路径
|
||
|
||
格式: uploads/YYYY/MM/DD/uuid.ext
|
||
"""
|
||
# 获取文件扩展名
|
||
ext = os.path.splitext(filename)[1].lower()
|
||
# 生成随机文件名
|
||
random_filename = f"{uuid.uuid4().hex}{ext}"
|
||
# 返回完整路径
|
||
from datetime import datetime
|
||
now = datetime.now()
|
||
return f"uploads/{now.year}/{now.month:02d}/{now.day:02d}/{random_filename}"
|
||
|
||
|
||
class UploadedFile(ModelBase):
|
||
"""
|
||
通用文件上传记录
|
||
|
||
用于存储无法归类到具体业务的文件上传记录
|
||
"""
|
||
path = models.FileField(
|
||
upload_to=upload_file_path,
|
||
max_length=500,
|
||
verbose_name='文件路径',
|
||
help_text='文件存储路径,包含前缀、随机文件名和后缀'
|
||
)
|
||
owner = models.ForeignKey(
|
||
User,
|
||
on_delete=models.CASCADE,
|
||
related_name='uploaded_files',
|
||
verbose_name='上传者'
|
||
)
|
||
is_deleted = models.BooleanField(
|
||
default=False,
|
||
verbose_name='是否已删除',
|
||
help_text='软删除标记'
|
||
)
|
||
original_filename = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
verbose_name='原始文件名',
|
||
help_text='用户上传时的原始文件名'
|
||
)
|
||
file_size = models.BigIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='文件大小(字节)'
|
||
)
|
||
content_type = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
verbose_name='文件类型',
|
||
help_text='MIME类型,如 image/jpeg'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'api_uploaded_file'
|
||
verbose_name = '上传文件'
|
||
verbose_name_plural = '上传文件'
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f"{self.original_filename or self.path.name} (by {self.owner.username})"
|
||
|
||
@property
|
||
def file_url(self):
|
||
"""获取文件访问URL"""
|
||
if self.path:
|
||
return self.path.url
|
||
return None
|
||
|
||
|
||
class DataSync(ModelBase):
|
||
"""记录外部数据同步的执行情况"""
|
||
|
||
class TableName(models.TextChoices):
|
||
PRODUCT = 'product', '产品'
|
||
CUSTOMER = 'customer', '客户'
|
||
PLATE_ORDER = 'plate_order', '开版(明道云)'
|
||
MDY_PLATE_ORDER_STAGING_TIIA_UPLOAD = 'mdy_plate_order_staging_tiia_upload', '开版暂存-腾讯图库上传'
|
||
|
||
table_name = models.CharField(max_length=50, choices=TableName.choices, verbose_name='同步目标')
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
null=True,
|
||
blank=True,
|
||
related_name='data_sync_records',
|
||
verbose_name='所属商户',
|
||
)
|
||
page_index = models.PositiveIntegerField(default=1, verbose_name='页码')
|
||
page_size = models.PositiveIntegerField(default=100, verbose_name='每页数量')
|
||
total_count = models.PositiveIntegerField(default=0, verbose_name='远端总数')
|
||
synced_rows = models.PositiveIntegerField(default=0, verbose_name='本次同步数量')
|
||
last_ctime = models.DateTimeField(null=True, blank=True, verbose_name='最新数据时间')
|
||
last_rowid = models.CharField(max_length=64, blank=True, verbose_name='最新 RowID')
|
||
note = models.CharField(max_length=255, blank=True, verbose_name='备注')
|
||
|
||
class Meta:
|
||
db_table = 'api_data_sync'
|
||
verbose_name = '数据同步记录'
|
||
verbose_name_plural = '数据同步记录'
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f'{self.table_name} @ {self.created_at:%Y-%m-%d %H:%M:%S}'
|
||
|
||
|
||
class MDYPlateOrderStaging(ModelBase):
|
||
"""明道云"开版数据表"同步暂存表(不影响 printing.PlateOrder)。
|
||
|
||
设计目标:
|
||
- 与现有业务模型完全隔离,避免影响业务数据与流程
|
||
- 仅保留 rowid 便于检索排查,其余信息全部落在 raw(JSON) 中
|
||
- 随着同步深入再逐步"属性化"字段(从 raw 实时提取/映射)
|
||
"""
|
||
|
||
mdy_rowid = models.CharField(
|
||
max_length=64,
|
||
unique=True,
|
||
db_index=True,
|
||
verbose_name='明道云 RowID',
|
||
help_text='明道云行记录的 rowid(全局唯一)',
|
||
)
|
||
ctime = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
verbose_name='明道云创建时间',
|
||
help_text='明道云系统字段 ctime(用于排序/增量定位)',
|
||
)
|
||
utime = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
verbose_name='明道云更新时间',
|
||
help_text='明道云系统字段 utime(用于排序/增量定位)',
|
||
)
|
||
raw = models.JSONField(
|
||
default=dict,
|
||
blank=True,
|
||
verbose_name='原始行数据',
|
||
help_text='getFilterRows 返回的整行原始数据(包含各 controlId 字段)',
|
||
)
|
||
related = models.JSONField(
|
||
default=list,
|
||
blank=True,
|
||
verbose_name='关联表数据',
|
||
help_text='开版相关跨表数据(碾平 list[dict]),用于排查与后续属性化提取',
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'api_mdy_plate_order_staging'
|
||
verbose_name = '明道云开版暂存'
|
||
verbose_name_plural = '明道云开版暂存'
|
||
ordering = ['-created_at']
|
||
indexes = [
|
||
# 明道云字段:62d52f4b8d2972284492dd0e(开版表"订单id/设计编号")
|
||
# 用于在暂存表中按该字段快速检索
|
||
models.Index(
|
||
KeyTextTransform('62d52f4b8d2972284492dd0e', 'raw'),
|
||
name='api_mdy_po_raw_id_idx',
|
||
),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f'{self.mdy_rowid}'
|
||
|
||
|
||
class MDYPlateOrderStagingTiiaUploadFailure(ModelBase):
|
||
"""
|
||
明道云开版暂存(MDYPlateOrderStaging)图片上传到腾讯云 TIIA 失败记录。
|
||
|
||
说明:
|
||
- 用于定时任务/批处理的失败落库,便于后续排查与补偿重试
|
||
- 以 (run_date, mdy_rowid) 唯一,避免同一天重复刷屏
|
||
"""
|
||
|
||
run_date = models.DateField(db_index=True, verbose_name='任务日期')
|
||
mdy_rowid = models.CharField(max_length=64, db_index=True, verbose_name='明道云 RowID')
|
||
staging_id = models.PositiveBigIntegerField(null=True, blank=True, db_index=True, verbose_name='暂存表ID')
|
||
error = models.TextField(blank=True, verbose_name='错误摘要')
|
||
details = models.JSONField(
|
||
default=list,
|
||
blank=True,
|
||
verbose_name='错误详情',
|
||
help_text='通常存储上传 service 返回的失败条目列表',
|
||
)
|
||
attempts = models.PositiveIntegerField(default=0, verbose_name='尝试次数')
|
||
last_attempt_at = models.DateTimeField(null=True, blank=True, verbose_name='最后尝试时间')
|
||
|
||
class Meta:
|
||
db_table = 'api_mdy_plate_order_staging_tiia_upload_failure'
|
||
verbose_name = '开版暂存图片上传失败记录'
|
||
verbose_name_plural = '开版暂存图片上传失败记录'
|
||
ordering = ['-created_at']
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=['run_date', 'mdy_rowid'],
|
||
name='uniq_mdy_po_staging_tiia_fail_run_date_rowid',
|
||
)
|
||
]
|
||
|
||
def __str__(self):
|
||
return f'{self.run_date} {self.mdy_rowid}'
|
||
|
||
|
||
class ApiAuditLog(models.Model):
|
||
"""API审计日志 - 记录创建操作的历史现场
|
||
|
||
用于保存特定模块的POST请求,记录"创建"操作的完整请求信息。
|
||
通过Celery异步写入,避免影响API响应性能。
|
||
"""
|
||
url = models.CharField(
|
||
max_length=500,
|
||
verbose_name='请求URL',
|
||
help_text='完整的请求路径'
|
||
)
|
||
method = models.CharField(
|
||
max_length=10,
|
||
verbose_name='请求方法',
|
||
help_text='HTTP方法,如POST'
|
||
)
|
||
request_data = models.JSONField(
|
||
default=dict,
|
||
verbose_name='请求体数据',
|
||
help_text='POST请求的body数据(JSON格式)'
|
||
)
|
||
query_params = models.JSONField(
|
||
default=dict,
|
||
verbose_name='查询参数',
|
||
help_text='URL查询参数'
|
||
)
|
||
user_id = models.IntegerField(
|
||
null=True,
|
||
blank=True,
|
||
db_index=True,
|
||
verbose_name='用户ID',
|
||
help_text='操作用户的ID'
|
||
)
|
||
username = models.CharField(
|
||
max_length=150,
|
||
blank=True,
|
||
default='',
|
||
verbose_name='用户名',
|
||
help_text='操作用户的用户名快照'
|
||
)
|
||
response_status = models.IntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='响应状态码',
|
||
help_text='HTTP响应状态码'
|
||
)
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
db_index=True,
|
||
verbose_name='创建时间'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'api_audit_log'
|
||
verbose_name = 'API审计日志'
|
||
verbose_name_plural = 'API审计日志'
|
||
ordering = ['-created_at']
|
||
indexes = [
|
||
models.Index(fields=['user_id', 'created_at']),
|
||
models.Index(fields=['url', 'created_at']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f'{self.method} {self.url} ({self.username or "anonymous"}) - {self.response_status}'
|