1
0
forked from erp-dev/erp
Files
erpnew/api_v1/models.py

390 lines
14 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.
"""
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', '开版(明道云)'
PRINTING_EXTERNAL_RECORD = 'printing_external_record', '印染订单(外部 records'
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 MDYPlateOrderStagingQiniuImageUploadAudit(ModelBase):
"""明道云开版暂存"开版图"迁移到七牛的逐图审计记录。"""
class Status(models.TextChoices):
PENDING = 'pending', '待处理'
PROCESSING = 'processing', '处理中'
SUCCESS = 'success', '成功'
FAILED = 'failed', '失败'
SKIPPED = 'skipped', '跳过'
class Source(models.TextChoices):
RAW_PLATE_IMAGES = 'raw_plate_images', 'raw.开版图'
staging = models.ForeignKey(
MDYPlateOrderStaging,
on_delete=models.CASCADE,
related_name='qiniu_image_upload_audits',
verbose_name='开版暂存记录',
)
mdy_rowid = models.CharField(max_length=64, db_index=True, verbose_name='明道云 RowID')
source = models.CharField(
max_length=50,
choices=Source.choices,
default=Source.RAW_PLATE_IMAGES,
verbose_name='图片来源',
)
attachment_index = models.PositiveIntegerField(verbose_name='附件序号')
source_field = models.CharField(max_length=64, blank=True, verbose_name='原始 URL 字段')
original_url = models.URLField(max_length=2048, verbose_name='原始图片 URL')
original_host = models.CharField(max_length=255, blank=True, db_index=True, verbose_name='原始图片域名')
qiniu_key = models.CharField(max_length=500, blank=True, verbose_name='七牛对象 Key')
qiniu_url = models.URLField(max_length=2048, blank=True, verbose_name='七牛图片 URL')
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.PENDING,
db_index=True,
verbose_name='上传状态',
)
attempts = models.PositiveIntegerField(default=0, verbose_name='尝试次数')
error = models.TextField(blank=True, verbose_name='错误信息')
file_size = models.PositiveBigIntegerField(null=True, blank=True, verbose_name='文件大小')
content_type = models.CharField(max_length=100, blank=True, verbose_name='Content-Type')
raw_attachment = models.JSONField(default=dict, blank=True, verbose_name='原始附件快照')
started_at = models.DateTimeField(null=True, blank=True, verbose_name='开始处理时间')
uploaded_at = models.DateTimeField(null=True, blank=True, verbose_name='上传成功时间')
last_attempt_at = models.DateTimeField(null=True, blank=True, verbose_name='最后尝试时间')
class Meta:
db_table = 'api_mdy_plate_order_staging_qiniu_image_upload_audit'
verbose_name = '开版暂存开版图七牛上传审计'
verbose_name_plural = '开版暂存开版图七牛上传审计'
ordering = ['-created_at']
constraints = [
models.UniqueConstraint(
fields=['staging', 'source', 'attachment_index', 'source_field', 'original_url'],
name='uniq_mdy_qiniu_staging_src_idx_url',
)
]
indexes = [
models.Index(fields=['status', 'id'], name='mdy_qiniu_status_id_idx'),
models.Index(fields=['original_url'], name='mdy_qiniu_orig_url_idx'),
models.Index(fields=['staging', 'source'], name='mdy_qiniu_staging_src_idx'),
]
def __str__(self):
return f'{self.mdy_rowid}#{self.attachment_index} {self.status}'
class PrintingExternalSyncFailure(ModelBase):
"""
外部印染 records 同步失败记录。
以外部 record.ID 为追踪主键,允许同一天重复失败时累加 attempts。
"""
run_date = models.DateField(db_index=True, verbose_name='任务日期')
external_record_id = models.PositiveBigIntegerField(db_index=True, verbose_name='外部记录ID')
external_order_id = models.CharField(max_length=100, blank=True, verbose_name='外部订单编号')
product_name = models.CharField(max_length=200, blank=True, verbose_name='外部产品名')
error = models.TextField(blank=True, verbose_name='错误摘要')
raw = models.JSONField(default=dict, blank=True, verbose_name='原始数据')
attempts = models.PositiveIntegerField(default=0, verbose_name='尝试次数')
last_attempt_at = models.DateTimeField(null=True, blank=True, verbose_name='最后尝试时间')
class Meta:
db_table = 'api_printing_external_sync_failure'
verbose_name = '外部印染同步失败记录'
verbose_name_plural = '外部印染同步失败记录'
ordering = ['-created_at']
constraints = [
models.UniqueConstraint(
fields=['run_date', 'external_record_id'],
name='uniq_printing_ext_sync_fail_run_date_record',
)
]
def __str__(self):
return f'{self.external_record_id} @ {self.run_date}'
class ApiAuditLog(models.Model):
"""API审计日志 - 记录创建/更新操作的历史现场
用于保存特定模块的POST/PUT/PATCH请求记录"创建/更新"操作的完整请求信息。
通过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、PUT、PATCH'
)
request_data = models.JSONField(
default=dict,
verbose_name='请求体数据',
help_text='请求的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}'