From 0dd1d4188a3316e47da25dbdfd3594ac11651690 Mon Sep 17 00:00:00 2001 From: colaftc Date: Mon, 9 Mar 2026 22:48:06 +0800 Subject: [PATCH] server push --- docs/2026-01-11_summary.md | 111 ++++++++++++++++++ ...te_plateordertiiauploadfailure_and_more.py | 36 ++++++ ...ns_alter_printingorder_options_and_more.py | 58 +++++++++ .../migrations/0034_merge_20260123_0839.py | 14 +++ .../migrations/0035_merge_20260309_2247.py | 14 +++ ..._merchant_remove_shipment_area_and_more.py | 44 +++++++ ...lesitem_merchant_shipment_area_and_more.py | 73 ++++++++++++ 7 files changed, 350 insertions(+) create mode 100644 docs/2026-01-11_summary.md create mode 100644 printing/migrations/0032_delete_plateordertiiauploadfailure_and_more.py create mode 100644 printing/migrations/0033_alter_plateorder_options_alter_printingorder_options_and_more.py create mode 100644 printing/migrations/0034_merge_20260123_0839.py create mode 100644 printing/migrations/0035_merge_20260309_2247.py create mode 100644 shipment/migrations/0009_remove_salesitem_merchant_remove_shipment_area_and_more.py create mode 100644 shipment/migrations/0010_salesitem_merchant_shipment_area_and_more.py diff --git a/docs/2026-01-11_summary.md b/docs/2026-01-11_summary.md new file mode 100644 index 0000000..c66a3df --- /dev/null +++ b/docs/2026-01-11_summary.md @@ -0,0 +1,111 @@ +# 2026-01-11 工作日志 + +## 完成事项 + +### API 审计日志中间件 + +实现了一个用于记录特定模块POST请求的中间件,用于保存"创建"操作的历史现场。 + +#### 1. 新增 `ApiAuditLog` 模型 (`api_v1/models.py`) + +记录以下信息: +- `url`: 请求URL路径 +- `method`: HTTP方法 +- `request_data`: 请求体数据(JSON格式) +- `query_params`: URL查询参数 +- `user_id`: 操作用户ID +- `username`: 操作用户的用户名快照 +- `response_status`: HTTP响应状态码 +- `created_at`: 创建时间 + +#### 2. 新增 Celery Task (`api_v1/tasks.py`) + +- `save_api_audit_log`: 异步保存审计日志任务 +- 通过Celery队列异步执行,避免阻塞API响应 + +#### 3. 新增中间件 (`flower/middleware.py`) + +- `ApiAuditLogMiddleware`: API审计日志中间件 +- 只记录POST请求 +- 通过URL前缀白名单过滤 +- 支持multipart/form-data请求,文件字段只记录元信息(文件名、大小、类型) +- 在AuthenticationMiddleware之后运行,可获取request.user +- 对于JWT认证,手动调用JWTAuthentication获取用户信息 + +#### 4. 配置更新 (`flower/settings.py`) + +新增配置项: +```python +# API 审计日志配置 +API_AUDIT_LOG_ENABLED = True +API_AUDIT_LOG_URL_PREFIXES = [ + '/api/v1/', + '/api/v2/', + '/api/backend/', +] +``` + +中间件注册: +```python +MIDDLEWARE = [ + ... + 'flower.middleware.ApiAuditLogMiddleware', # 放在最后 +] +``` + +#### 5. 数据库迁移 + +- 迁移文件: `api_v1/migrations/0009_apiauditlog.py` + +#### 6. Admin 注册 (`api_v1/admin.py`) + +- 新增 `ApiAuditLogAdmin` 管理类 +- 支持按 URL、用户名搜索 +- 支持按方法、响应状态码、时间过滤 +- 所有字段只读,禁止添加和修改(只允许查看和删除) + +### 7. 新增 `jobs_last_status_summary` 字段 + +在 `PrintingOrderListSerializer` 中新增字段,用于按最后完成状态分组统计。 + +- **修改文件**: `api_v1/views/printing/serializers.py` +- **修改文件**: `api_v1/views/printing/views.py`(更新文档注释) + +返回格式: +```json +{ + "jobs_status_summary": [ + {"state_name": "进度二", "state_id": 2, "count": 3} + ], + "jobs_last_status_summary": [ + {"state_name": "进度一", "count": 2}, + {"state_name": "", "count": 1} + ] +} +``` + +字段说明: +- `jobs_status_summary`: 按下一待执行状态分组 +- `jobs_last_status_summary`: 按最后完成状态分组(未开始时 `state_name` 为空字符串) + +### 8. 修复 `_upsert_product` 重复数据问题 + +修改 `api_v1/tasks.py` 中的 `_upsert_product` 函数,使用 `filter().first()` 替代 `get_or_create`,容忍数据库中存在重复的 `(merchant, human_id)` 记录。 + +原因:`Product` 模型没有设置 `unique_together = ('merchant', 'human_id')` 约束,多同步源可能导致重复数据,`get_or_create` 会抛出 `MultipleObjectsReturned` 错误。 + +### 9. 修复 Docker Compose 中 Celery 服务缺少 CACHE_HOST 配置 + +在以下文件中为 `celery_worker` 和 `celery_beat` 服务添加 `CACHE_HOST` 和 `CACHE_PORT` 环境变量: + +- `docker-compose.yml` +- `docker-compose.dev.yml` +- `docker-compose.prod.yml` + +问题原因:`settings.py` 中 `CACHE_HOST` 默认值为 `localhost`,在容器化环境中无法连接到 Redis 服务。 + +## 待办事项 + +- [x] 执行数据库迁移 (`uv run python manage.py migrate`) +- [x] 测试中间件功能 +- [x] 重启 Docker 容器使配置生效 \ No newline at end of file diff --git a/printing/migrations/0032_delete_plateordertiiauploadfailure_and_more.py b/printing/migrations/0032_delete_plateordertiiauploadfailure_and_more.py new file mode 100644 index 0000000..9dd84b1 --- /dev/null +++ b/printing/migrations/0032_delete_plateordertiiauploadfailure_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.2.8 on 2026-01-21 02:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0031_plateorder_tiia_upload_failure'), + ] + + operations = [ + migrations.DeleteModel( + name='PlateOrderTiiaUploadFailure', + ), + migrations.AlterModelOptions( + name='plateorder', + options={'ordering': ['-created_at'], 'permissions': [('can_invalidate_plateorder', '可以作废开版订单'), ('can_activate_plateorder', '可以恢复开版订单')], 'verbose_name': '开版订单', 'verbose_name_plural': '开版订单'}, + ), + migrations.AlterModelOptions( + name='printingorder', + options={'permissions': [('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单')], 'verbose_name': '印染订单', 'verbose_name_plural': '印染订单'}, + ), + migrations.RemoveField( + model_name='plateorder', + name='merchant', + ), + migrations.RemoveField( + model_name='printingjob', + name='merchant', + ), + migrations.RemoveField( + model_name='printingorder', + name='merchant', + ), + ] diff --git a/printing/migrations/0033_alter_plateorder_options_alter_printingorder_options_and_more.py b/printing/migrations/0033_alter_plateorder_options_alter_printingorder_options_and_more.py new file mode 100644 index 0000000..a81a0a4 --- /dev/null +++ b/printing/migrations/0033_alter_plateorder_options_alter_printingorder_options_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.8 on 2026-01-22 01:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0024_frontend_page_and_visible_pages'), + ('printing', '0032_delete_plateordertiiauploadfailure_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='plateorder', + options={'ordering': ['-created_at'], 'permissions': [('can_invalidate_plateorder', '可以作废开版订单'), ('can_activate_plateorder', '可以恢复开版订单'), ('view_all_plateorders', '查看所有开版订单(突破客户可见性限制)')], 'verbose_name': '开版订单', 'verbose_name_plural': '开版订单'}, + ), + migrations.AlterModelOptions( + name='printingorder', + options={'permissions': [('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单'), ('view_all_printingorders', '查看所有印染订单(突破客户可见性限制)')], 'verbose_name': '印染订单', 'verbose_name_plural': '印染订单'}, + ), + migrations.AddField( + model_name='plateorder', + name='merchant', + field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='plate_orders', to='basic_info.merchant', verbose_name='商户'), + ), + migrations.AddField( + model_name='printingjob', + name='merchant', + field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='printing_jobs', to='basic_info.merchant', verbose_name='商户'), + ), + migrations.AddField( + model_name='printingorder', + name='merchant', + field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='printing_orders', to='basic_info.merchant', verbose_name='商户'), + ), + migrations.CreateModel( + name='PlateOrderTiiaUploadFailure', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('run_date', models.DateField(help_text='以任务处理的日期为准(通常为“昨天”)', verbose_name='任务日期')), + ('plate_order_id', models.PositiveBigIntegerField(db_index=True, help_text='PlateOrder.id(不使用外键,避免历史数据/删除带来的约束问题)', verbose_name='开版订单ID')), + ('error', models.TextField(blank=True, default='', verbose_name='错误摘要')), + ('details', models.JSONField(blank=True, default=list, help_text='通常存储 upload_plate_order_images_to_tencent_tiia 返回的失败条目列表', verbose_name='错误详情')), + ('attempts', models.PositiveIntegerField(default=0, verbose_name='尝试次数')), + ('last_attempt_at', models.DateTimeField(blank=True, null=True, verbose_name='最后尝试时间')), + ], + options={ + 'verbose_name': '开版订单图片上传失败记录', + 'verbose_name_plural': '开版订单图片上传失败记录', + 'ordering': ['-created_at'], + 'constraints': [models.UniqueConstraint(fields=('run_date', 'plate_order_id'), name='uniq_plateorder_tiia_fail_run_date_po')], + }, + ), + ] diff --git a/printing/migrations/0034_merge_20260123_0839.py b/printing/migrations/0034_merge_20260123_0839.py new file mode 100644 index 0000000..45430a5 --- /dev/null +++ b/printing/migrations/0034_merge_20260123_0839.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.8 on 2026-01-23 00:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0032_alter_printingorder_outgoing_date'), + ('printing', '0033_alter_plateorder_options_alter_printingorder_options_and_more'), + ] + + operations = [ + ] diff --git a/printing/migrations/0035_merge_20260309_2247.py b/printing/migrations/0035_merge_20260309_2247.py new file mode 100644 index 0000000..e1c1922 --- /dev/null +++ b/printing/migrations/0035_merge_20260309_2247.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.8 on 2026-03-09 14:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('printing', '0033_plateorder_idx_plateord_merch_pltdt'), + ('printing', '0034_merge_20260123_0839'), + ] + + operations = [ + ] diff --git a/shipment/migrations/0009_remove_salesitem_merchant_remove_shipment_area_and_more.py b/shipment/migrations/0009_remove_salesitem_merchant_remove_shipment_area_and_more.py new file mode 100644 index 0000000..4cb1bd4 --- /dev/null +++ b/shipment/migrations/0009_remove_salesitem_merchant_remove_shipment_area_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.8 on 2026-01-21 02:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('shipment', '0008_add_area_to_shipment'), + ] + + operations = [ + migrations.RemoveField( + model_name='salesitem', + name='merchant', + ), + migrations.RemoveField( + model_name='shipment', + name='area', + ), + migrations.RemoveField( + model_name='shipment', + name='cancelled_at', + ), + migrations.RemoveField( + model_name='shipment', + name='cancelled_by', + ), + migrations.RemoveField( + model_name='shipment', + name='external_id', + ), + migrations.RemoveField( + model_name='shipment', + name='merchant', + ), + migrations.RemoveField( + model_name='shipment', + name='status', + ), + migrations.DeleteModel( + name='ExternalFinishedProduct', + ), + ] diff --git a/shipment/migrations/0010_salesitem_merchant_shipment_area_and_more.py b/shipment/migrations/0010_salesitem_merchant_shipment_area_and_more.py new file mode 100644 index 0000000..8749d90 --- /dev/null +++ b/shipment/migrations/0010_salesitem_merchant_shipment_area_and_more.py @@ -0,0 +1,73 @@ +# Generated by Django 5.2.8 on 2026-01-22 01:36 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('basic_info', '0024_frontend_page_and_visible_pages'), + ('shipment', '0009_remove_salesitem_merchant_remove_shipment_area_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='salesitem', + name='merchant', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='sales_items', to='basic_info.merchant', verbose_name='所属商户'), + preserve_default=False, + ), + migrations.AddField( + model_name='shipment', + name='area', + field=models.CharField(blank=True, default='', help_text='出货地区(可空字符串)', max_length=30, verbose_name='地区'), + ), + migrations.AddField( + model_name='shipment', + name='cancelled_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='取消时间'), + ), + migrations.AddField( + model_name='shipment', + name='cancelled_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cancelled_shipments', to=settings.AUTH_USER_MODEL, verbose_name='取消人'), + ), + migrations.AddField( + model_name='shipment', + name='external_id', + field=models.CharField(blank=True, help_text='用于兼容外部遗留系统的订单号(可空)', max_length=120, null=True, verbose_name='外部订单号'), + ), + migrations.AddField( + model_name='shipment', + name='merchant', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='shipments', to='basic_info.merchant', verbose_name='所属商户'), + preserve_default=False, + ), + migrations.AddField( + model_name='shipment', + name='status', + field=models.IntegerField(choices=[(1, '待送货'), (2, '已交付'), (3, '已取消')], default=1, verbose_name='状态'), + ), + migrations.CreateModel( + name='ExternalFinishedProduct', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('style_name', models.CharField(max_length=200, verbose_name='款式名称')), + ('num_of_rolls', models.PositiveIntegerField(verbose_name='卷数')), + ('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_external_finished_products', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('shipment', models.ForeignKey(blank=True, help_text='关联的出货单(可空,兼容历史数据/逐步迁移)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='external_finished_products', to='shipment.shipment', verbose_name='出货单')), + ], + options={ + 'verbose_name': '外部成品表', + 'verbose_name_plural': '外部成品表', + 'db_table': 'external_finished_product', + 'ordering': ['-created_at'], + }, + ), + ]