mirror of
https://github.com/rnvm9wjdtj-bot/myaps_api.git
synced 2026-06-02 05:54:40 +00:00
20260517
This commit is contained in:
@@ -4,12 +4,12 @@ Redis 监控采集器
|
||||
用于采集 Redis 连接池和操作的监控指标
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
from apps.common.utils.redis_pool_manager import get_redis_pool_manager
|
||||
from globalobjects import logger as log_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = log_config.get_logger(__name__)
|
||||
|
||||
|
||||
class RedisCollector:
|
||||
|
||||
@@ -9,6 +9,11 @@ import logging
|
||||
from collections import deque
|
||||
from typing import Set, List, Dict, Optional
|
||||
|
||||
# 延迟导入避免循环依赖
|
||||
def _get_logger():
|
||||
from globalobjects import logger as log_config
|
||||
return log_config.get_logger(__name__)
|
||||
|
||||
|
||||
class LogStreamManager:
|
||||
"""全局日志流管理器 - 单例模式,确保所有模块使用同一个实例"""
|
||||
@@ -84,7 +89,7 @@ class LogStreamService:
|
||||
async with self._lock:
|
||||
for queue in self._active_connections:
|
||||
await queue.put(None)
|
||||
logging.info("[日志流] 服务已停止")
|
||||
_get_logger().info("[日志流] 服务已停止")
|
||||
|
||||
def _add_log_handler(self):
|
||||
"""添加自定义日志处理器(不影响现有处理器)"""
|
||||
|
||||
+38
-34
@@ -8,8 +8,11 @@ from enum import Enum
|
||||
|
||||
from tortoise.models import Model as TortoiseBaseModel
|
||||
from tortoise import fields
|
||||
from globalobjects import logger as log_config
|
||||
|
||||
|
||||
|
||||
logger = log_config.get_logger(__name__)
|
||||
# ==============================================
|
||||
# 状态枚举定义
|
||||
# ==============================================
|
||||
@@ -17,11 +20,11 @@ from tortoise import fields
|
||||
class StagingStatus(str, Enum):
|
||||
"""缓冲表数据状态"""
|
||||
PENDING = ("pending", "待处理", "warning")
|
||||
COMPLIANCE_PASS = ("compliance_pass", "基本校验通过", "info")
|
||||
COMPLIANCE_ERROR = ("compliance_error", "基本校验错误", "danger")
|
||||
RELATION_PASS = ("relation_pass", "联合校验通过", "success")
|
||||
RELATION_ERROR = ("relation_error", "联合校验错误", "warning")
|
||||
APPROVED = ("approved", "已审批", "primary")
|
||||
COMPLIANCE_PASS = ("compliance_pass", "初检通过", "info")
|
||||
COMPLIANCE_ERROR = ("compliance_error", "初检错误", "danger")
|
||||
RELATION_PASS = ("relation_pass", "联检通过", "success")
|
||||
RELATION_ERROR = ("relation_error", "联检错误", "warning")
|
||||
SYNC_ERROR = ("sync_error", "推送失败", "warning")
|
||||
SYNCED = ("synced", "已推送", "secondary")
|
||||
|
||||
def __new__(cls, value, label, color):
|
||||
@@ -33,15 +36,6 @@ class StagingStatus(str, Enum):
|
||||
|
||||
def __str__(self):
|
||||
return self._value_
|
||||
|
||||
@classmethod
|
||||
def from_legacy(cls, legacy_status: str) -> 'StagingStatus':
|
||||
"""从旧状态转换到新状态"""
|
||||
mapping = {
|
||||
'validated': cls.RELATION_PASS,
|
||||
'rejected': cls.RELATION_ERROR,
|
||||
}
|
||||
return mapping.get(legacy_status, cls(legacy_status))
|
||||
|
||||
@classmethod
|
||||
def get_meta(cls, value: str) -> Optional[Dict[str, str]]:
|
||||
@@ -241,9 +235,9 @@ def extract_business_keys_from_model(model_class: Type[TortoiseBaseModel]) -> Li
|
||||
从正式表模型自动提取业务主键
|
||||
|
||||
优先级:
|
||||
1. unique_together 约束(业务主键)
|
||||
1. unique_together 约束(业务主键)- 最高优先级
|
||||
2. unique=True 的字段
|
||||
3. 主键字段(pk,排除自增ID)
|
||||
3. 主键字段(pk,排除自增ID和虚拟主键vid)
|
||||
|
||||
Args:
|
||||
model_class: 正式表模型类(如 TMaterial)
|
||||
@@ -251,24 +245,38 @@ def extract_business_keys_from_model(model_class: Type[TortoiseBaseModel]) -> Li
|
||||
Returns:
|
||||
业务主键字段列表
|
||||
"""
|
||||
|
||||
meta = getattr(model_class, '_meta', None)
|
||||
model_name = model_class.__name__ if model_class else 'Unknown'
|
||||
|
||||
# 优先使用 unique_together(业务主键)
|
||||
if meta:
|
||||
unique_together = getattr(meta, 'unique_together', None)
|
||||
if unique_together and len(unique_together) > 0:
|
||||
return list(unique_together[0])
|
||||
if not meta:
|
||||
logger.warning(f"[业务主键提取] {model_name}: 无_meta属性,返回空列表")
|
||||
return []
|
||||
|
||||
# 其次检查 unique=True 的字段
|
||||
for field_name, field in model_class._meta.fields_map.items():
|
||||
# 优先级1:unique_together 约束(业务主键)
|
||||
unique_together = getattr(meta, 'unique_together', None)
|
||||
if unique_together and len(unique_together) > 0:
|
||||
keys = list(unique_together[0])
|
||||
logger.debug(f"[业务主键提取] {model_name}: 从unique_together提取 -> {keys}")
|
||||
return keys
|
||||
|
||||
# 优先级2:unique=True 的字段
|
||||
for field_name, field in meta.fields_map.items():
|
||||
if getattr(field, 'unique', False):
|
||||
logger.debug(f"[业务主键提取] {model_name}: 从unique字段提取 -> [{field_name}]")
|
||||
return [field_name]
|
||||
|
||||
# 最后检查主键(排除自增ID和虚拟主键vid)
|
||||
pk_field = model_class._meta.pk
|
||||
if pk_field and pk_field.model_field_name not in ('id', 'vid'):
|
||||
return [pk_field.model_field_name]
|
||||
# 优先级3:主键字段(排除自增ID和虚拟主键vid)
|
||||
pk_field = meta.pk
|
||||
if pk_field:
|
||||
pk_name = pk_field.model_field_name
|
||||
if pk_name not in ('id', 'vid'):
|
||||
logger.debug(f"[业务主键提取] {model_name}: 从主键字段提取 -> [{pk_name}]")
|
||||
return [pk_name]
|
||||
else:
|
||||
logger.warning(f"[业务主键提取] {model_name}: 主键为自增字段'{pk_name}',跳过(请在模型中配置unique_together)")
|
||||
|
||||
logger.warning(f"[业务主键提取] {model_name}: 未找到业务主键,返回空列表")
|
||||
return []
|
||||
|
||||
|
||||
@@ -300,9 +308,6 @@ def extract_display_name_from_model(model_class: Type[TortoiseBaseModel]) -> str
|
||||
return class_name
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def convert_record_to_lowercase(record_dict: Dict, model_class) -> Dict:
|
||||
"""
|
||||
将记录的字段名从数据库格式(大驼峰)转换为API格式(小写)
|
||||
@@ -370,11 +375,11 @@ def get_suggestion(error_type: ErrorType) -> str:
|
||||
|
||||
TABLE_PROCESS_ORDER = [
|
||||
"t_material",
|
||||
"t_workcenter",
|
||||
"t_mold",
|
||||
"t_mat_ver",
|
||||
"t_workcenter",
|
||||
"t_mat_wc",
|
||||
"t_mat_wc_bom",
|
||||
"t_mold",
|
||||
"t_mat_wc_mold",
|
||||
]
|
||||
|
||||
@@ -662,8 +667,7 @@ class BusinessRule:
|
||||
try:
|
||||
return self.validate_func(data)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"业务规则执行异常: {self.name}, {e}")
|
||||
logger.warning(f"业务规则执行异常: {self.name}, {e}")
|
||||
return False
|
||||
|
||||
def create_error(self, staging_id: int) -> Dict[str, Any]:
|
||||
|
||||
@@ -179,23 +179,88 @@ def auto_generate_filter_categories_from_extraction(
|
||||
|
||||
|
||||
|
||||
# 页面键到表键的映射
|
||||
_PAGE_KEY_TO_TABLE_KEY = {
|
||||
"material": "t_material",
|
||||
"workcenter": "t_workcenter",
|
||||
"mat-ver": "t_mat_ver",
|
||||
"mat-wc": "t_mat_wc",
|
||||
"mat-wc-bom": "t_mat_wc_bom",
|
||||
"mold": "t_mold",
|
||||
"mat-wc-mold": "t_mat_wc_mold"
|
||||
# ==============================================
|
||||
# 表展示配置(用于前端导航和页面渲染)
|
||||
# ==============================================
|
||||
|
||||
TABLE_DISPLAY_CONFIG = {
|
||||
"t_material": {
|
||||
"route": "material",
|
||||
"icon": "material",
|
||||
"description": "物料主数据管理,包含物料号、描述、类型等信息",
|
||||
"gradient": ("#0d6efd", "#0dcaf0"),
|
||||
"page_title": "物料数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或描述...",
|
||||
},
|
||||
"t_workcenter": {
|
||||
"route": "workcenter",
|
||||
"icon": "workcenter",
|
||||
"description": "工作中心管理,包含产能、瓶颈标识等信息",
|
||||
"gradient": ("#198754", "#20c997"),
|
||||
"page_title": "工作中心数据清洗管理",
|
||||
"keyword_placeholder": "搜索工作中心或描述...",
|
||||
},
|
||||
"t_mat_ver": {
|
||||
"route": "mat-ver",
|
||||
"icon": "version",
|
||||
"description": "物料版本管理,定义不同生产版本的批量范围",
|
||||
"gradient": ("#fd7e14", "#ffc107"),
|
||||
"page_title": "产线版本数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或版本号...",
|
||||
},
|
||||
"t_mat_wc": {
|
||||
"route": "mat-wc",
|
||||
"icon": "route",
|
||||
"description": "工艺路线管理,定义物料生产的工序流程",
|
||||
"gradient": ("#6f42c1", "#d63384"),
|
||||
"page_title": "工艺路线数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或工作中心...",
|
||||
},
|
||||
"t_mat_wc_bom": {
|
||||
"route": "mat-wc-bom",
|
||||
"icon": "bom",
|
||||
"description": "物料清单管理,定义产品的组成结构和用量",
|
||||
"gradient": ("#dc3545", "#fd7e14"),
|
||||
"page_title": "BOM数据清洗管理",
|
||||
"keyword_placeholder": "搜索父件或子件料号...",
|
||||
},
|
||||
"t_mold": {
|
||||
"route": "mold",
|
||||
"icon": "mold",
|
||||
"description": "模具主数据管理,包含模具类型、状态、穴数等",
|
||||
"gradient": ("#343a40", "#6c757d"),
|
||||
"page_title": "模具数据清洗管理",
|
||||
"keyword_placeholder": "搜索模具编号或描述...",
|
||||
},
|
||||
"t_mat_wc_mold": {
|
||||
"route": "mat-wc-mold",
|
||||
"icon": "link",
|
||||
"description": "机台与模具的关联关系管理",
|
||||
"gradient": ("#0dcaf0", "#20c997"),
|
||||
"page_title": "机台模具数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或模具编号...",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ==============================================
|
||||
# 系统公共字段(每个表都显示)
|
||||
# ==============================================
|
||||
|
||||
SYSTEM_COMMON_COLUMNS_PREFIX = [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
]
|
||||
|
||||
SYSTEM_COMMON_COLUMNS_SUFFIX = [
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"},
|
||||
]
|
||||
|
||||
|
||||
# 前端配置(列配置等)
|
||||
# TODO: 可以进一步优化,从模型中提取更多信息
|
||||
_PAGE_COLUMNS_CONFIG = {
|
||||
"t_material": [
|
||||
{"field": "_status", "title": "状态"},
|
||||
{"field": "_createtime", "title": "创建时间", "sortable": True},
|
||||
"t_material": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "materialno", "title": "物料号", "sortable": True, "readOnly": True},
|
||||
{"field": "description", "title": "物料描述"},
|
||||
{"field": "size", "title": "规格"},
|
||||
@@ -229,32 +294,26 @@ _PAGE_COLUMNS_CONFIG = {
|
||||
{"field": "free1", "title": "自定义1"},
|
||||
{"field": "free2", "title": "自定义2"},
|
||||
{"field": "free3", "title": "自定义3"},
|
||||
{"field": "_source_system", "title": "来源"}
|
||||
],
|
||||
"t_workcenter": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
{"field": "workcenter", "title": "工作中心", "width": "120px", "sortable": True, "readOnly": True},
|
||||
{"field": "description", "title": "描述", "width": "200px"},
|
||||
{"field": "bottleneck", "title": "瓶颈", "width": "80px"},
|
||||
{"field": "finite", "title": "有限产能", "width": "100px"},
|
||||
{"field": "capacity", "title": "产能", "width": "100px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
],
|
||||
"t_mat_ver": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_workcenter": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "workcenter", "title": "工作中心", "sortable": True, "readOnly": True},
|
||||
{"field": "description", "title": "描述"},
|
||||
{"field": "bottleneck", "title": "瓶颈"},
|
||||
{"field": "finite", "title": "有限产能"},
|
||||
{"field": "capacity", "title": "产能"},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_mat_ver": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "materialno", "title": "物料号", "width": "120px", "sortable": True, "readOnly": True},
|
||||
{"field": "matver", "title": "版本号", "width": "80px", "sortable": True},
|
||||
{"field": "description", "title": "描述", "width": "200px"},
|
||||
{"field": "active", "title": "激活", "width": "80px"},
|
||||
{"field": "lotfrom", "title": "批量下限", "width": "100px"},
|
||||
{"field": "lotto", "title": "批量上限", "width": "100px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
],
|
||||
"t_mat_wc": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
{"field": "refno", "title": "MTO订单号/认证线", "width": "150px"},
|
||||
{"field": "active", "title": "激活", "width": "80px"},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_mat_wc": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "materialno", "title": "物料号", "width": "120px", "sortable": True, "readOnly": True},
|
||||
{"field": "matver", "title": "版本号", "width": "80px"},
|
||||
{"field": "itemno", "title": "工序号", "width": "80px"},
|
||||
@@ -262,37 +321,29 @@ _PAGE_COLUMNS_CONFIG = {
|
||||
{"field": "sf", "title": "串并行", "width": "80px"},
|
||||
{"field": "basesec", "title": "基础工时", "width": "100px"},
|
||||
{"field": "sortno", "title": "排序", "width": "80px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
],
|
||||
"t_mat_wc_bom": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_mat_wc_bom": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "productno", "title": "父件号", "width": "100px", "readOnly": True},
|
||||
{"field": "matver", "title": "版本号", "width": "80px"},
|
||||
{"field": "itemno", "title": "工序号", "width": "80px"},
|
||||
{"field": "materialno", "title": "子件号", "width": "100px"},
|
||||
{"field": "workcenter", "title": "工作中心", "width": "100px"},
|
||||
{"field": "qty", "title": "用量", "width": "80px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
],
|
||||
"t_mold": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_mold": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "moldno", "title": "模具号", "width": "120px", "sortable": True, "readOnly": True},
|
||||
{"field": "description", "title": "描述", "width": "200px"},
|
||||
{"field": "cavity", "title": "穴数", "width": "80px"},
|
||||
{"field": "count", "title": "台数", "width": "80px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
],
|
||||
"t_mat_wc_mold": [
|
||||
{"field": "_status", "title": "状态", "width": "80px"},
|
||||
{"field": "_createtime", "title": "创建时间", "width": "180px", "sortable": True},
|
||||
{"field": "moldname", "title": "描述"},
|
||||
{"field": "moldnum", "title": "穴数", "width": "80px"},
|
||||
{"field": "qty", "title": "台数", "width": "80px"},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
|
||||
"t_mat_wc_mold": SYSTEM_COMMON_COLUMNS_PREFIX + [
|
||||
{"field": "materialno", "title": "物料号", "width": "100px", "readOnly": True},
|
||||
{"field": "workcenter", "title": "工作中心", "width": "100px"},
|
||||
{"field": "itemno", "title": "工序号", "width": "80px"},
|
||||
{"field": "moldno", "title": "模具号", "width": "100px"},
|
||||
{"field": "_source_system", "title": "来源", "width": "80px"}
|
||||
]
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
}
|
||||
|
||||
# 筛选字段配置
|
||||
@@ -302,11 +353,11 @@ _PAGE_FILTER_CONFIG = {
|
||||
"number_fields": ["leadday", "expday", "grday", "price", "phantommin", "firmday", "daygap", "lotfix", "lotmin", "lotmax"]
|
||||
},
|
||||
"t_workcenter": {
|
||||
"string_fields": ["workcenter", "description"],
|
||||
"number_fields": ["capacity"]
|
||||
"string_fields": ["workcenter", "workcentername"],
|
||||
"number_fields": ["capnum"]
|
||||
},
|
||||
"t_mat_ver": {
|
||||
"string_fields": ["materialno", "matver", "description"],
|
||||
"string_fields": ["materialno", "matver"],
|
||||
"number_fields": ["lotfrom", "lotto"]
|
||||
},
|
||||
"t_mat_wc": {
|
||||
@@ -314,12 +365,12 @@ _PAGE_FILTER_CONFIG = {
|
||||
"number_fields": ["basesec", "sortno"]
|
||||
},
|
||||
"t_mat_wc_bom": {
|
||||
"string_fields": ["productno", "materialno", "workcenter", "matver", "itemno"],
|
||||
"string_fields": ["productno", "materialno", "matver", "itemno"],
|
||||
"number_fields": ["qty"]
|
||||
},
|
||||
"t_mold": {
|
||||
"string_fields": ["moldno", "description"],
|
||||
"number_fields": ["cavity", "count"]
|
||||
"string_fields": ["moldno", "moldname"],
|
||||
"number_fields": ["moldnum", "qty"]
|
||||
},
|
||||
"t_mat_wc_mold": {
|
||||
"string_fields": ["materialno", "workcenter", "itemno", "moldno"],
|
||||
@@ -341,8 +392,13 @@ def generate_generic_page_config(page_key: str) -> Optional[Dict[str, Any]]:
|
||||
Returns:
|
||||
页面配置字典,如果不支持该页面则返回 None
|
||||
"""
|
||||
# 从页面键获取表键
|
||||
table_key = _PAGE_KEY_TO_TABLE_KEY.get(page_key)
|
||||
# 从页面键获取表键(通过 route 匹配)
|
||||
table_key = None
|
||||
for tk, config in TABLE_DISPLAY_CONFIG.items():
|
||||
if config["route"] == page_key:
|
||||
table_key = tk
|
||||
break
|
||||
|
||||
if not table_key:
|
||||
return None
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ STAGING_TABLE_CONFIG = {
|
||||
"label_field": "workcentername"
|
||||
},
|
||||
],
|
||||
"display_name": "工艺路线",
|
||||
"display_name": "工序",
|
||||
"validator": lambda cleaner, data, staging_id: cleaner.validate_mat_wc(data, staging_id),
|
||||
"business_rules": [
|
||||
{
|
||||
@@ -964,18 +964,19 @@ class StagingProcessor:
|
||||
logger.info(f"已清空正式表: {target_table_name} (账套: {target_db_name})")
|
||||
|
||||
# 统一使用 batch_size 分批查询
|
||||
query = f'SELECT * FROM "{staging_table_name}" WHERE "_status" = $1 AND ("_retry_count" IS NULL OR "_retry_count" < $2) LIMIT $3'
|
||||
result = await pg_conn.execute_query(query, ("relation_pass", max_retries, batch_size))
|
||||
# 允许 relation_pass 和 sync_error 状态的数据进行同步
|
||||
query = f'SELECT * FROM "{staging_table_name}" WHERE "_status" IN ($1, $2) AND ("_retry_count" IS NULL OR "_retry_count" < $3) LIMIT $4'
|
||||
result = await pg_conn.execute_query(query, ("relation_pass", "sync_error", max_retries, batch_size))
|
||||
records_to_sync = result[1] if result[1] else []
|
||||
|
||||
# 检查retry_count分布
|
||||
retry_check = await pg_conn.execute_query(
|
||||
f'SELECT "_retry_count", COUNT(*) as cnt FROM "{staging_table_name}" WHERE "_status" = $1 GROUP BY "_retry_count"',
|
||||
("relation_pass",)
|
||||
# 检查各状态数量分布
|
||||
status_check = await pg_conn.execute_query(
|
||||
f'SELECT "_status", COUNT(*) as cnt FROM "{staging_table_name}" WHERE "_status" IN ($1, $2) GROUP BY "_status"',
|
||||
("relation_pass", "sync_error")
|
||||
)
|
||||
retry_dist = {row["_retry_count"]: row["cnt"] for row in retry_check[1]} if retry_check[1] else {}
|
||||
status_dist = {row["_status"]: row["cnt"] for row in status_check[1]} if status_check[1] else {}
|
||||
|
||||
logger.info(f"同步查询: 表={staging_table_name}, 状态=relation_pass, 重试<{max_retries}, 批次={batch_size}, 找到{len(records_to_sync)}条记录, retry分布={retry_dist}")
|
||||
logger.info(f"同步查询: 表={staging_table_name}, 状态=relation_pass/sync_error, 重试<{max_retries}, 批次={batch_size}, 找到{len(records_to_sync)}条记录, 状态分布={status_dist}")
|
||||
|
||||
if not records_to_sync:
|
||||
return stats
|
||||
@@ -1039,7 +1040,7 @@ class StagingProcessor:
|
||||
|
||||
try:
|
||||
update_query = f'UPDATE "{staging_table_name}" SET "_retry_count" = $1, "_error_msg" = $2, "_status" = $3 WHERE "_staging_id" = $4'
|
||||
await pg_conn.execute_query(update_query, (retry_count, error_json, "relation_error", staging_id))
|
||||
await pg_conn.execute_query(update_query, (retry_count, error_json, "sync_error", staging_id))
|
||||
except Exception as update_err:
|
||||
logger.error(f"更新失败记录状态时出错: {update_err}")
|
||||
|
||||
@@ -1114,7 +1115,7 @@ class StagingProcessor:
|
||||
"error_message": f"数据重复:存在相同主键的记录,被去重丢弃"
|
||||
}], ensure_ascii=False)
|
||||
update_query = f'UPDATE "{staging_table_name}" SET "_status" = $1, "_error_msg" = $2 WHERE "_staging_id" = $3'
|
||||
await pg_conn.execute_query(update_query, ("relation_error", error_json, staging_id))
|
||||
await pg_conn.execute_query(update_query, ("sync_error", error_json, staging_id))
|
||||
|
||||
# 标记成功同步的记录
|
||||
if synced_staging_ids:
|
||||
@@ -1130,7 +1131,7 @@ class StagingProcessor:
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"同步失败 [{table_name}] 账套={target_db_name}: {str(e)}")
|
||||
logger.error(f"推送失败 [{table_name}] 账套={target_db_name}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
stats["failed"] = len(data_list)
|
||||
for staging_id in staging_ids:
|
||||
@@ -1140,10 +1141,10 @@ class StagingProcessor:
|
||||
"error_type": "sync_error",
|
||||
"error_field": None,
|
||||
"error_value": None,
|
||||
"error_message": f"同步失败: {str(e)}"
|
||||
"error_message": f"推送失败: {str(e)}"
|
||||
}], ensure_ascii=False)
|
||||
update_query = f'UPDATE "{staging_table_name}" SET "_retry_count" = $1, "_error_msg" = $2 WHERE "_staging_id" = $3'
|
||||
await pg_conn.execute_query(update_query, (retry_count, error_json, staging_id))
|
||||
update_query = f'UPDATE "{staging_table_name}" SET "_retry_count" = $1, "_error_msg" = $2, "_status" = $3 WHERE "_staging_id" = $4'
|
||||
await pg_conn.execute_query(update_query, (retry_count, error_json, "sync_error", staging_id))
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Dict, Optional, Literal
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Query, Body, HTTPException, status, Request, UploadFile, File
|
||||
|
||||
from ._base import StagingStatus, INTERNAL_FIELDS, EXCLUDE_FIELDS, convert_record_to_lowercase, generate_validation_rules_doc
|
||||
from ._base import StagingStatus, INTERNAL_FIELDS, EXCLUDE_FIELDS, TABLE_PROCESS_ORDER, convert_record_to_lowercase, generate_validation_rules_doc
|
||||
from .staging_models import (
|
||||
TMaterialStaging, TWorkcenterStaging, TMatVerStaging,
|
||||
TMatWcStaging, TMatWcBomStaging, TMoldStaging, TMatWcMoldStaging,
|
||||
@@ -30,6 +30,7 @@ MONITOR_STATUS_FIELDS = (
|
||||
StagingStatus.COMPLIANCE_ERROR.value,
|
||||
StagingStatus.RELATION_PASS.value,
|
||||
StagingStatus.RELATION_ERROR.value,
|
||||
StagingStatus.SYNC_ERROR.value,
|
||||
StagingStatus.SYNCED.value,
|
||||
)
|
||||
|
||||
@@ -235,17 +236,10 @@ async def validate_staging(
|
||||
processor = StagingProcessor(db_name)
|
||||
stats = await processor.process_staging(table_name, batch_size)
|
||||
|
||||
# 添加兼容字段,确保前端可以正确处理
|
||||
stats_compat = {
|
||||
**stats,
|
||||
"validated": stats.get("relation_pass", 0),
|
||||
"rejected": (stats.get("relation_error", 0) + stats.get("compliance_error", 0))
|
||||
}
|
||||
|
||||
return standard_response(
|
||||
success=1,
|
||||
message=f"校验完成",
|
||||
data=stats_compat
|
||||
data=stats
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"校验失败 [{table_name}]: {str(e)}")
|
||||
@@ -259,28 +253,13 @@ async def validate_all_staging(
|
||||
db_name: str = Query(THIS_DB_NAME, description="账套")
|
||||
):
|
||||
"""按依赖顺序校验所有缓冲表数据"""
|
||||
table_order = [
|
||||
"t_material",
|
||||
"t_workcenter",
|
||||
"t_mold",
|
||||
"t_mat_ver",
|
||||
"t_mat_wc",
|
||||
"t_mat_wc_bom",
|
||||
"t_mat_wc_mold",
|
||||
]
|
||||
|
||||
try:
|
||||
processor = StagingProcessor(db_name)
|
||||
all_stats = {}
|
||||
|
||||
for table_name in table_order:
|
||||
for table_name in TABLE_PROCESS_ORDER:
|
||||
stats = await processor.process_staging(table_name, batch_size)
|
||||
# 添加兼容字段
|
||||
all_stats[table_name] = {
|
||||
**stats,
|
||||
"validated": stats.get("relation_pass", 0),
|
||||
"rejected": (stats.get("relation_error", 0) + stats.get("compliance_error", 0))
|
||||
}
|
||||
all_stats[table_name] = stats
|
||||
|
||||
return standard_response(
|
||||
success=1,
|
||||
@@ -443,7 +422,7 @@ async def sync_to_production(
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"同步失败 [{table_name}]: {str(e)}")
|
||||
logger.error(f"推送失败 [{table_name}]: {str(e)}")
|
||||
return standard_response(success=0, message=str(e))
|
||||
|
||||
|
||||
@@ -455,21 +434,11 @@ async def sync_all_to_production(
|
||||
db_name: str = Query(THIS_DB_NAME, description="账套")
|
||||
):
|
||||
"""按依赖顺序同步所有缓冲表数据到正式表"""
|
||||
table_order = [
|
||||
"t_material",
|
||||
"t_workcenter",
|
||||
"t_mold",
|
||||
"t_mat_ver",
|
||||
"t_mat_wc",
|
||||
"t_mat_wc_bom",
|
||||
"t_mat_wc_mold",
|
||||
]
|
||||
|
||||
try:
|
||||
processor = StagingProcessor(db_name)
|
||||
all_stats = {}
|
||||
|
||||
for table_name in table_order:
|
||||
for table_name in TABLE_PROCESS_ORDER:
|
||||
stats = await processor.sync_to_production(table_name, batch_size, max_retries)
|
||||
all_stats[table_name] = stats
|
||||
|
||||
@@ -724,60 +693,6 @@ async def get_staging_status(
|
||||
return standard_response(success=0, message=error_detail)
|
||||
|
||||
|
||||
@rt.patch("/approve/{table_name}/{staging_id}", summary="审批缓冲表数据")
|
||||
async def approve_staging(
|
||||
request: Request,
|
||||
table_name: str,
|
||||
staging_id: int,
|
||||
# db_name: str = Query(MYAPS_MAIN_DB, description="账套") # 未使用,已注释
|
||||
):
|
||||
"""手动审批通过缓冲表记录"""
|
||||
try:
|
||||
staging_model = STAGING_MODEL_MAPPING.get(table_name)
|
||||
if not staging_model:
|
||||
raise ValueError(f"未知的缓冲表: {table_name}")
|
||||
|
||||
record = await staging_model.get(_staging_id=staging_id)
|
||||
record._status = StagingStatus.APPROVED
|
||||
await record.save()
|
||||
|
||||
return standard_response(success=1, message="审批通过")
|
||||
except Exception as e:
|
||||
logger.error(f"审批失败: {str(e)}")
|
||||
return standard_response(success=0, message=str(e))
|
||||
|
||||
|
||||
@rt.patch("/reject/{table_name}/{staging_id}", summary="拒绝缓冲表数据")
|
||||
async def reject_staging(
|
||||
request: Request,
|
||||
table_name: str,
|
||||
staging_id: int,
|
||||
reason: str = Query(..., description="拒绝原因"),
|
||||
):
|
||||
"""手动拒绝缓冲表记录"""
|
||||
try:
|
||||
staging_model = STAGING_MODEL_MAPPING.get(table_name)
|
||||
if not staging_model:
|
||||
raise ValueError(f"未知的缓冲表: {table_name}")
|
||||
|
||||
record = await staging_model.get(_staging_id=staging_id)
|
||||
record._status = StagingStatus.REJECTED
|
||||
error_json = json.dumps([{
|
||||
"staging_id": staging_id,
|
||||
"error_type": "manual_reject",
|
||||
"error_field": None,
|
||||
"error_value": None,
|
||||
"error_message": reason
|
||||
}], ensure_ascii=False)
|
||||
record._error_msg = error_json
|
||||
await record.save()
|
||||
|
||||
return standard_response(success=1, message="已拒绝")
|
||||
except Exception as e:
|
||||
logger.error(f"拒绝操作失败: {str(e)}")
|
||||
return standard_response(success=0, message=str(e))
|
||||
|
||||
|
||||
@rt.delete("/clear/{table_name}", summary="清空缓冲表")
|
||||
async def clear_staging(
|
||||
request: Request,
|
||||
@@ -833,6 +748,7 @@ async def get_monitor_summary(request: Request):
|
||||
COUNT(*) FILTER (WHERE "_status" = '{StagingStatus.COMPLIANCE_ERROR.value}') as {StagingStatus.COMPLIANCE_ERROR.value},
|
||||
COUNT(*) FILTER (WHERE "_status" = '{StagingStatus.RELATION_PASS.value}') as {StagingStatus.RELATION_PASS.value},
|
||||
COUNT(*) FILTER (WHERE "_status" = '{StagingStatus.RELATION_ERROR.value}') as {StagingStatus.RELATION_ERROR.value},
|
||||
COUNT(*) FILTER (WHERE "_status" = '{StagingStatus.SYNC_ERROR.value}') as {StagingStatus.SYNC_ERROR.value},
|
||||
COUNT(*) FILTER (WHERE "_status" = '{StagingStatus.SYNCED.value}') as {StagingStatus.SYNCED.value},
|
||||
MAX("_createtime") as last_created,
|
||||
MAX("_synced_time") as last_synced
|
||||
@@ -849,6 +765,7 @@ async def get_monitor_summary(request: Request):
|
||||
StagingStatus.COMPLIANCE_ERROR.value: row.get(StagingStatus.COMPLIANCE_ERROR.value, 0),
|
||||
StagingStatus.RELATION_PASS.value: row.get(StagingStatus.RELATION_PASS.value, 0),
|
||||
StagingStatus.RELATION_ERROR.value: row.get(StagingStatus.RELATION_ERROR.value, 0),
|
||||
StagingStatus.SYNC_ERROR.value: row.get(StagingStatus.SYNC_ERROR.value, 0),
|
||||
StagingStatus.SYNCED.value: row.get(StagingStatus.SYNCED.value, 0),
|
||||
MONITOR_TIME_FIELDS[0]: row.get(MONITOR_TIME_FIELDS[0]).isoformat() if row.get(MONITOR_TIME_FIELDS[0]) else None,
|
||||
MONITOR_TIME_FIELDS[1]: row.get(MONITOR_TIME_FIELDS[1]).isoformat() if row.get(MONITOR_TIME_FIELDS[1]) else None,
|
||||
@@ -941,13 +858,13 @@ async def retry_failed_records(
|
||||
raise ValueError(f"未知的缓冲表: {table_name}")
|
||||
|
||||
records = await staging_model.filter(
|
||||
_status=StagingStatus.REJECTED,
|
||||
_status=StagingStatus.SYNC_ERROR,
|
||||
_retry_count__lt=max_retry
|
||||
)
|
||||
|
||||
reset_count = 0
|
||||
for record in records:
|
||||
record._status = StagingStatus.VALIDATED
|
||||
record._status = StagingStatus.PENDING
|
||||
record._retry_count = 0
|
||||
record._error_msg = None
|
||||
await record.save()
|
||||
|
||||
+82
-82
@@ -350,13 +350,13 @@ class ProtoMatGrp(TortoiseBaseModel):
|
||||
|
||||
class ProtoMatVer(TortoiseBaseModel):
|
||||
# vid = fields.IntField(primary_key=True)
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64) # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4) # Field name made lowercase.
|
||||
lotfrom = fields.IntField(source_field='LotFrom', blank=True, null=True) # Field name made lowercase.
|
||||
lotto = fields.IntField(source_field='LotTo', blank=True, null=True) # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64, description='物料号') # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4, description='版本号') # Field name made lowercase.
|
||||
lotfrom = fields.IntField(source_field='LotFrom', blank=True, null=True, description='批量下限') # Field name made lowercase.
|
||||
lotto = fields.IntField(source_field='LotTo', blank=True, null=True, description='批量上限') # Field name made lowercase.
|
||||
priority = fields.IntField(source_field='Priority', blank=True, null=True) # Field name made lowercase.
|
||||
refno = fields.CharField(source_field='RefNo', max_length=64, blank=True, null=True) # Field name made lowercase.
|
||||
active = fields.CharField(source_field='Active', max_length=1, blank=True, null=True) # Field name made lowercase.
|
||||
refno = fields.CharField(source_field='RefNo', max_length=64, blank=True, null=True, description='MTO订单号/认证线') # Field name made lowercase.
|
||||
active = fields.CharField(source_field='Active', max_length=1, blank=True, null=True, description='激活') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
class Meta:
|
||||
@@ -367,16 +367,16 @@ class ProtoMatVer(TortoiseBaseModel):
|
||||
|
||||
class ProtoMatWc(TortoiseBaseModel):
|
||||
# vid = fields.IntField(primary_key=True)
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64) # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4) # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6) # Field name made lowercase.
|
||||
workcenter = fields.CharField(source_field='WorkCenter', max_length=32, description='工作中心,机台') # Field name made lowercase.
|
||||
sortno = fields.IntField(source_field='SortNo', description='唯一') # Field name made lowercase.
|
||||
basesec = fields.IntField(source_field='BaseSec') # Field name made lowercase.
|
||||
fixqty = fields.IntField(source_field='FixQty') # Field name made lowercase.
|
||||
fixsec = fields.IntField(source_field='FixSec') # Field name made lowercase.
|
||||
sf = fields.CharField(source_field='SF', max_length=1, blank=True, null=True, description='S=串行, F=并行') # Field name made lowercase.
|
||||
offsetsec = fields.IntField(source_field='OffSetSec', blank=True, null=True) # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64, description='物料号') # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4, description='版本号') # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6, description='工序号') # Field name made lowercase.
|
||||
workcenter = fields.CharField(source_field='WorkCenter', max_length=32, description='工作中心') # Field name made lowercase.
|
||||
sortno = fields.IntField(source_field='SortNo', description='排序') # Field name made lowercase.
|
||||
basesec = fields.IntField(source_field='BaseSec', description='节拍') # Field name made lowercase.
|
||||
fixqty = fields.IntField(source_field='FixQty', description='额定量') # Field name made lowercase.
|
||||
fixsec = fields.IntField(source_field='FixSec', description='额定时间(秒)') # Field name made lowercase.
|
||||
sf = fields.CharField(source_field='SF', max_length=1, blank=True, null=True, description='串并行') # Field name made lowercase.
|
||||
offsetsec = fields.IntField(source_field='OffSetSec', blank=True, null=True, description='偏置±秒') # Field name made lowercase.
|
||||
rate = fields.FloatField(source_field='Rate', blank=True, null=True, description='配比') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
sys_stamp = fields.DatetimeField(source_field='Sys_Stamp', blank=True, null=True) # Field name made lowercase.
|
||||
@@ -389,16 +389,16 @@ class ProtoMatWc(TortoiseBaseModel):
|
||||
|
||||
class ProtoMatWcBom(TortoiseBaseModel):
|
||||
# vid = fields.IntField(primary_key=True)
|
||||
productno = fields.CharField(source_field='ProductNo', max_length=64) # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4) # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6) # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64) # Field name made lowercase.
|
||||
qty = fields.FloatField(source_field='Qty') # Field name made lowercase.
|
||||
offsethour = fields.IntField(source_field='OffsetHour') # Field name made lowercase.
|
||||
productno = fields.CharField(source_field='ProductNo', max_length=64, description='父件号') # Field name made lowercase.
|
||||
matver = fields.CharField(source_field='MatVer', max_length=4, description='版本号') # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6, description='工序号') # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64, description='子件号') # Field name made lowercase.
|
||||
qty = fields.FloatField(source_field='Qty', description='用量') # Field name made lowercase.
|
||||
offsethour = fields.IntField(source_field='OffsetHour', description='偏置±小时') # Field name made lowercase.
|
||||
treeno = fields.IntField(source_field='TreeNo', blank=True, null=True) # Field name made lowercase.
|
||||
mto = fields.CharField(source_field='MTO', max_length=1, blank=True, null=True, description='Y/N') # Field name made lowercase.
|
||||
scrap = fields.FloatField(source_field='Scrap', blank=True, null=True, description='%') # Field name made lowercase.
|
||||
alt = fields.CharField(source_field='Alt', max_length=1, blank=True, null=True, description='Y/N是否是替代') # Field name made lowercase.
|
||||
mto = fields.CharField(source_field='MTO', max_length=1, blank=True, null=True, description='是否MTO') # Field name made lowercase.
|
||||
scrap = fields.FloatField(source_field='Scrap', blank=True, null=True, description='报废率') # Field name made lowercase.
|
||||
alt = fields.CharField(source_field='Alt', max_length=1, blank=True, null=True, description='是否是替代') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
sys_stamp = fields.DatetimeField(source_field='Sys_Stamp', blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
@@ -444,13 +444,13 @@ class ProtoMatWcData(TortoiseBaseModel):
|
||||
class ProtoMatWcMold(TortoiseBaseModel):
|
||||
# pk = fields.CompositePrimaryKey('MaterialNo', 'WorkCenter', 'MoldNo')
|
||||
# vid = fields.IntField(primary_key=True)
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64, description='产品') # Field name made lowercase.
|
||||
workcenter = fields.CharField(source_field='WorkCenter', max_length=32, description='机台') # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6, description='工序项目') # Field name made lowercase.
|
||||
moldno = fields.CharField(source_field='MoldNo', max_length=32) # Field name made lowercase.
|
||||
basesec = fields.IntField(source_field='BaseSec', blank=True, null=True, description='UPH(Units Per Hour)每小时产量') # Field name made lowercase.
|
||||
fixsec = fields.IntField(source_field='FixSec', blank=True, null=True) # Field name made lowercase.
|
||||
priority = fields.IntField(source_field='Priority', blank=True, null=True) # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', max_length=64, description='物料号') # Field name made lowercase.
|
||||
workcenter = fields.CharField(source_field='WorkCenter', max_length=32, description='工作中心') # Field name made lowercase.
|
||||
itemno = fields.CharField(source_field='ItemNo', max_length=6, description='工序号') # Field name made lowercase.
|
||||
moldno = fields.CharField(source_field='MoldNo', max_length=32, description='模具号') # Field name made lowercase.
|
||||
basesec = fields.IntField(source_field='BaseSec', blank=True, null=True, description='节拍') # Field name made lowercase.
|
||||
fixsec = fields.IntField(source_field='FixSec', blank=True, null=True, description='额定时间(秒)') # Field name made lowercase.
|
||||
priority = fields.IntField(source_field='Priority', blank=True, null=True, description='优先级') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
class Meta:
|
||||
@@ -475,39 +475,39 @@ class ProtoMatWcSwitch(TortoiseBaseModel):
|
||||
|
||||
|
||||
class ProtoMaterial(TortoiseBaseModel):
|
||||
materialno = fields.CharField(source_field='MaterialNo', unique=True, max_length=64, description='物料') # Field name made lowercase.
|
||||
description = fields.CharField(source_field='Description', max_length=128) # Field name made lowercase.
|
||||
size = fields.CharField(source_field='Size', max_length=128, blank=True, null=True) # Field name made lowercase.
|
||||
materialno = fields.CharField(source_field='MaterialNo', unique=True, max_length=64, description='物料号') # Field name made lowercase.
|
||||
description = fields.CharField(source_field='Description', max_length=128, description='物料描述') # Field name made lowercase.
|
||||
size = fields.CharField(source_field='Size', max_length=128, blank=True, null=True, description='规格') # Field name made lowercase.
|
||||
plant = fields.CharField(source_field='Plant', max_length=32, description='工厂') # Field name made lowercase.
|
||||
planner = fields.CharField(source_field='Planner', max_length=64, blank=True, null=True) # Field name made lowercase.
|
||||
fifo = fields.IntField(source_field='FIFO', description='Y-FIFO ,N-最近原则') # Field name made lowercase.
|
||||
leadday = fields.IntField(source_field='LeadDay') # Field name made lowercase.
|
||||
expday = fields.IntField(source_field='ExpDay', blank=True, null=True) # Field name made lowercase.
|
||||
grday = fields.IntField(source_field='GRDay') # Field name made lowercase.
|
||||
abc = fields.CharField(source_field='ABC', max_length=8, blank=True, null=True) # Field name made lowercase.
|
||||
unit = fields.CharField(source_field='Unit', max_length=8, blank=True, null=True, description='KG/L/g') # Field name made lowercase.
|
||||
price = fields.DecimalField(source_field='Price', max_digits=10, decimal_places=2, blank=True, null=True) # Field name made lowercase.
|
||||
groupno = fields.CharField(source_field='GroupNo', max_length=32, blank=True, null=True) # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=1, blank=True, null=True) # Field name made lowercase.
|
||||
phantom = fields.CharField(source_field='Phantom', max_length=1, blank=True, null=True, description='Y/N') # Field name made lowercase.
|
||||
phantommin = fields.IntField(source_field='PhantomMin', description='Phantom Offset Time(Minute)') # Field name made lowercase.
|
||||
firmday = fields.IntField(source_field='FirmDay', blank=True, null=True) # Field name made lowercase.
|
||||
daygap = fields.IntField(source_field='DayGap', blank=True, null=True, description='MTO Split') # Field name made lowercase.
|
||||
candelay = fields.CharField(source_field='CanDelay', max_length=1, blank=True, null=True, description='Y/N') # Field name made lowercase.
|
||||
lotsize = fields.CharField(source_field='LotSize', max_length=2, blank=True, null=True, description='EX/FX/D1/D2/D3/D4/D5/D6/W1/W2/W3/W4/M1/M2/VB') # Field name made lowercase.
|
||||
lotfix = fields.FloatField(source_field='LotFix', blank=True, null=True, description='Fixed LotSize') # Field name made lowercase.
|
||||
lotmin = fields.FloatField(source_field='LotMin', blank=True, null=True, description='Minimum Lot Size') # Field name made lowercase.
|
||||
lotmax = fields.FloatField(source_field='LotMax', blank=True, null=True, description='Maximum Lot Size') # Field name made lowercase.
|
||||
lotround = fields.FloatField(source_field='LotRound', blank=True, null=True, description='Rounding value') # Field name made lowercase.
|
||||
lotss = fields.FloatField(source_field='LotSS', blank=True, null=True, description='Safty Stock') # Field name made lowercase.
|
||||
lotpoint = fields.FloatField(source_field='LotPoint', blank=True, null=True, description='trigger Point') # Field name made lowercase.
|
||||
lottop = fields.FloatField(source_field='LotTop', blank=True, null=True, description='Top Value') # Field name made lowercase.
|
||||
planitem = fields.CharField(source_field='PlanItem', max_length=32, blank=True, null=True, description='FC PlanItem, PlanGroup') # Field name made lowercase.
|
||||
preday = fields.IntField(source_field='PreDay', blank=True, null=True, description='FC PreDay') # Field name made lowercase.
|
||||
subday = fields.IntField(source_field='SubDay', blank=True, null=True, description='FC SubDay') # Field name made lowercase.
|
||||
free1 = fields.CharField(source_field='Free1', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
free2 = fields.CharField(source_field='Free2', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
free3 = fields.CharField(source_field='Free3', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
planner = fields.CharField(source_field='Planner', max_length=64, blank=True, null=True, description='计划员') # Field name made lowercase.
|
||||
fifo = fields.IntField(source_field='FIFO', description='FIFO') # Field name made lowercase.
|
||||
leadday = fields.IntField(source_field='LeadDay', description='提前期') # Field name made lowercase.
|
||||
expday = fields.IntField(source_field='ExpDay', blank=True, null=True, description='保质期') # Field name made lowercase.
|
||||
grday = fields.IntField(source_field='GRDay', description='质检期') # Field name made lowercase.
|
||||
abc = fields.CharField(source_field='ABC', max_length=8, blank=True, null=True, description='ABC分类') # Field name made lowercase.
|
||||
unit = fields.CharField(source_field='Unit', max_length=8, blank=True, null=True, description='单位') # Field name made lowercase.
|
||||
price = fields.DecimalField(source_field='Price', max_digits=10, decimal_places=2, blank=True, null=True, description='价格') # Field name made lowercase.
|
||||
groupno = fields.CharField(source_field='GroupNo', max_length=32, blank=True, null=True, description='型号') # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=1, blank=True, null=True, description='类型') # Field name made lowercase.
|
||||
phantom = fields.CharField(source_field='Phantom', max_length=1, blank=True, null=True, description='虚拟件') # Field name made lowercase.
|
||||
phantommin = fields.IntField(source_field='PhantomMin', description='虚拟时间') # Field name made lowercase.
|
||||
firmday = fields.IntField(source_field='FirmDay', blank=True, null=True, description='固定天数') # Field name made lowercase.
|
||||
daygap = fields.IntField(source_field='DayGap', blank=True, null=True, description='拆分天数') # Field name made lowercase.
|
||||
candelay = fields.CharField(source_field='CanDelay', max_length=1, blank=True, null=True, description='可延迟') # Field name made lowercase.
|
||||
lotsize = fields.CharField(source_field='LotSize', max_length=2, blank=True, null=True, description='批量策略') # Field name made lowercase.
|
||||
lotfix = fields.FloatField(source_field='LotFix', blank=True, null=True, description='固定批') # Field name made lowercase.
|
||||
lotmin = fields.FloatField(source_field='LotMin', blank=True, null=True, description='最小批') # Field name made lowercase.
|
||||
lotmax = fields.FloatField(source_field='LotMax', blank=True, null=True, description='最大批') # Field name made lowercase.
|
||||
lotround = fields.FloatField(source_field='LotRound', blank=True, null=True, description='取整值') # Field name made lowercase.
|
||||
lotss = fields.FloatField(source_field='LotSS', blank=True, null=True, description='安全库存') # Field name made lowercase.
|
||||
lotpoint = fields.FloatField(source_field='LotPoint', blank=True, null=True, description='订货点') # Field name made lowercase.
|
||||
lottop = fields.FloatField(source_field='LotTop', blank=True, null=True, description='最大库存') # Field name made lowercase.
|
||||
planitem = fields.CharField(source_field='PlanItem', max_length=32, blank=True, null=True, description='产品组') # Field name made lowercase.
|
||||
preday = fields.IntField(source_field='PreDay', blank=True, null=True, description='向前冲销') # Field name made lowercase.
|
||||
subday = fields.IntField(source_field='SubDay', blank=True, null=True, description='向后冲销') # Field name made lowercase.
|
||||
free1 = fields.CharField(source_field='Free1', max_length=255, blank=True, null=True, description='自定义1') # Field name made lowercase.
|
||||
free2 = fields.CharField(source_field='Free2', max_length=255, blank=True, null=True, description='自定义2') # Field name made lowercase.
|
||||
free3 = fields.CharField(source_field='Free3', max_length=255, blank=True, null=True, description='自定义3') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
sys_user = fields.CharField(source_field='Sys_User', max_length=32, blank=True, null=True) # Field name made lowercase.
|
||||
sys_date = fields.DatetimeField(source_field='Sys_Date', blank=True, null=True) # Field name made lowercase.
|
||||
@@ -601,12 +601,12 @@ class ProtoMaterialNa(TortoiseBaseModel):
|
||||
|
||||
|
||||
class ProtoMold(TortoiseBaseModel):
|
||||
moldno = fields.CharField(source_field='MoldNo', unique=True, max_length=32) # Field name made lowercase.
|
||||
moldname = fields.CharField(source_field='MoldName', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=8, blank=True, null=True, description="'注塑','冲压','压铸','夹具'") # Field name made lowercase.
|
||||
status = fields.CharField(source_field='Status', max_length=8, blank=True, null=True, description="'空闲','生产中','维修中','报废'") # Field name made lowercase.
|
||||
moldnum = fields.IntField(source_field='MoldNum', blank=True, null=True, description='模具穴数') # Field name made lowercase.
|
||||
qty = fields.IntField(source_field='Qty', blank=True, null=True, description='模具台数') # Field name made lowercase.
|
||||
moldno = fields.CharField(source_field='MoldNo', unique=True, max_length=32, description='模具号') # Field name made lowercase.
|
||||
moldname = fields.CharField(source_field='MoldName', max_length=255, blank=True, null=True, description='模具名称') # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=8, blank=True, null=True, description="类型") # Field name made lowercase.
|
||||
status = fields.CharField(source_field='Status', max_length=8, blank=True, null=True, description="状态") # Field name made lowercase.
|
||||
moldnum = fields.IntField(source_field='MoldNum', blank=True, null=True, description='穴数') # Field name made lowercase.
|
||||
qty = fields.IntField(source_field='Qty', blank=True, null=True, description='台数') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
class Meta:
|
||||
@@ -1282,20 +1282,20 @@ class ProtoVendor(TortoiseBaseModel):
|
||||
|
||||
|
||||
class ProtoWorkcenter(TortoiseBaseModel):
|
||||
workcenter = fields.CharField(source_field='WorkCenter', unique=True, max_length=32) # Field name made lowercase.
|
||||
workcentername = fields.CharField(source_field='WorkCenterName', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
pri_wc = fields.IntField(source_field='Pri_WC', blank=True, null=True, description='Planning时,多个WorkCenter的优先级选定') # Field name made lowercase.
|
||||
bottleneck = fields.CharField(source_field='Bottleneck', max_length=1, blank=True, null=True, description='Y/N') # Field name made lowercase.
|
||||
sortno = fields.CharField(source_field='SortNo', max_length=4, blank=True, null=True) # Field name made lowercase.
|
||||
plant = fields.CharField(source_field='Plant', max_length=32, blank=True, null=True) # Field name made lowercase.
|
||||
location = fields.CharField(source_field='Location', max_length=32, blank=True, null=True) # Field name made lowercase.
|
||||
finite = fields.CharField(source_field='Finite', max_length=1, blank=True, null=True, description='Y/N') # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=32, blank=True, null=True) # Field name made lowercase.
|
||||
capnum = fields.IntField(source_field='CapNum', blank=True, null=True) # Field name made lowercase.
|
||||
capmax = fields.IntField(source_field='CapMax', blank=True, null=True) # Field name made lowercase.
|
||||
workcenter = fields.CharField(source_field='WorkCenter', unique=True, max_length=32, description='工作中心编号') # Field name made lowercase.
|
||||
workcentername = fields.CharField(source_field='WorkCenterName', max_length=255, blank=True, null=True, description='名称') # Field name made lowercase.
|
||||
pri_wc = fields.IntField(source_field='Pri_WC', blank=True, null=True, description='优先级') # Field name made lowercase.
|
||||
bottleneck = fields.CharField(source_field='Bottleneck', max_length=1, blank=True, null=True, description='是否瓶颈') # Field name made lowercase.
|
||||
sortno = fields.CharField(source_field='SortNo', max_length=4, blank=True, null=True, description='序号') # Field name made lowercase.
|
||||
plant = fields.CharField(source_field='Plant', max_length=32, blank=True, null=True, description='工厂') # Field name made lowercase.
|
||||
location = fields.CharField(source_field='Location', max_length=32, blank=True, null=True, description='车间') # Field name made lowercase.
|
||||
finite = fields.CharField(source_field='Finite', max_length=1, blank=True, null=True, description='有限') # Field name made lowercase.
|
||||
type = fields.CharField(source_field='Type', max_length=32, blank=True, null=True, description='首页显示') # Field name made lowercase.
|
||||
capnum = fields.IntField(source_field='CapNum', blank=True, null=True, description='默认机台数') # Field name made lowercase.
|
||||
capmax = fields.IntField(source_field='CapMax', blank=True, null=True, description='最大机台数') # Field name made lowercase.
|
||||
worker = fields.FloatField(source_field='Worker', blank=True, null=True, description='工时') # Field name made lowercase.
|
||||
setupno = fields.CharField(source_field='SetupNo', max_length=6, blank=True, null=True, description='切换组') # Field name made lowercase.
|
||||
grpno = fields.CharField(source_field='GrpNo', max_length=6, blank=True, null=True, description='同类') # Field name made lowercase.
|
||||
grpno = fields.CharField(source_field='GrpNo', max_length=6, blank=True, null=True, description='同组号') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -59,9 +59,9 @@ class AcceptMaterial(BaseModel):
|
||||
unit: str = Field(..., description='单位', example="PCS")
|
||||
price: Decimal = Field(0, description="价格", ge=0, example=100.50)
|
||||
groupno: str = Field("", description="型号", example="G001")
|
||||
type: gc.EfEnum = Field(... if MYAPS_VERSION == 'P' else None, example="E", description="物料类型 E-自制件 F-采购件")
|
||||
type: gc.EfEnum = Field(... if MYAPS_VERSION == 'P' else None, example="E", description="物料类型")
|
||||
phantom: gc.YesNoEnum = Field(pdv.MAT_PHANTOM, example="N", description='虚拟件')
|
||||
phantommin: int = Field(pdv.MAT_PHANTOMMIN, ge=0, description='虚拟时间(Minute)', example=0)
|
||||
phantommin: int = Field(pdv.MAT_PHANTOMMIN, ge=0, description='虚拟时间(分)', example=0)
|
||||
firmday: int = Field(pdv.MAT_FIRMDAY, ge=0, description="固定天数", example=0)
|
||||
daygap: int = Field(pdv.MAT_DAYGAP, ge=0, description='MTO拆分天数', example=1)
|
||||
candelay: gc.YesNoEnum = Field(pdv.MAT_CANDELAY, example="N", description='可否延迟')
|
||||
@@ -422,7 +422,7 @@ class AcceptMatWcBom(BaseModel):
|
||||
treeno: int = Field(None, description='层级', example=1)
|
||||
mto: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='MTO')
|
||||
scrap: float = Field(0, ge=0, description='报废率%', example=0.0)
|
||||
alt: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='Y/N是否是替代')
|
||||
alt: gc.YesNoEnum = Field(gc.YesNoEnum.NO, example="N", description='是否是替代')
|
||||
memo: str = Field(None, max_length=255, description='备注', example="标准BOM组件")
|
||||
denominator: Optional[float | str] = Field(None, description='用量分母', example=1)
|
||||
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ if THIS_DB_NAME:
|
||||
},
|
||||
"min_size": 3,
|
||||
"max_size": 10,
|
||||
"use_tz": False,
|
||||
"use_tz": True,
|
||||
}
|
||||
TORTOISE_ORM_CONFIG["apps"]["data_opt_models"] = {
|
||||
"models": ["apps.data_opt.mds.staging_models", "aerich.models"],
|
||||
|
||||
+59
-43
@@ -5,48 +5,68 @@ from apps.data_opt.routers import rt as do_rt
|
||||
from apps.data_opt.mds.staging_routers import rt as mds_rt
|
||||
from apps.common.monitor.routers import router as monitor_rt
|
||||
from apps.common.help.routers import router as help_rt
|
||||
from apps.data_opt.mds.config_generator import TABLE_DISPLAY_CONFIG
|
||||
from apps.data_opt.mds.staging_cleaner import STAGING_TABLE_CONFIG
|
||||
import os
|
||||
import json
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# MDS 页面配置字典
|
||||
MDS_PAGE_CONFIG = {
|
||||
"material": {
|
||||
"page_title": "物料数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或描述...",
|
||||
"config_file": "material.config.js"
|
||||
},
|
||||
"workcenter": {
|
||||
"page_title": "工作中心数据清洗管理",
|
||||
"keyword_placeholder": "搜索工作中心或描述...",
|
||||
"config_file": "workcenter.config.js"
|
||||
},
|
||||
"mat-ver": {
|
||||
"page_title": "产线版本数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或版本号...",
|
||||
"config_file": "mat-ver.config.js"
|
||||
},
|
||||
"mat-wc": {
|
||||
"page_title": "工艺路线数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或工作中心...",
|
||||
"config_file": "mat-wc.config.js"
|
||||
},
|
||||
"mat-wc-bom": {
|
||||
"page_title": "BOM数据清洗管理",
|
||||
"keyword_placeholder": "搜索父件或子件料号...",
|
||||
"config_file": "mat-wc-bom.config.js"
|
||||
},
|
||||
"mold": {
|
||||
"page_title": "模具数据清洗管理",
|
||||
"keyword_placeholder": "搜索模具编号或描述...",
|
||||
"config_file": "mold.config.js"
|
||||
},
|
||||
"mat-wc-mold": {
|
||||
"page_title": "机台模具数据清洗管理",
|
||||
"keyword_placeholder": "搜索物料号或模具编号...",
|
||||
"config_file": "mat-wc-mold.config.js"
|
||||
}
|
||||
}
|
||||
|
||||
def get_mds_page_config():
|
||||
"""从 TABLE_DISPLAY_CONFIG 生成页面配置"""
|
||||
config = {}
|
||||
for table_key, display_config in TABLE_DISPLAY_CONFIG.items():
|
||||
route = display_config["route"]
|
||||
config[route] = {
|
||||
"page_title": display_config["page_title"],
|
||||
"keyword_placeholder": display_config["keyword_placeholder"],
|
||||
"table_key": table_key,
|
||||
}
|
||||
return config
|
||||
|
||||
MDS_PAGE_CONFIG = get_mds_page_config()
|
||||
|
||||
|
||||
def render_mds_index():
|
||||
"""动态渲染 MDS 首页导航"""
|
||||
template_path = os.path.join(BASE_DIR, "static", "mds", "index.html")
|
||||
with open(template_path, "r", encoding="utf-8") as f:
|
||||
template = f.read()
|
||||
|
||||
# 生成导航列表 HTML
|
||||
nav_items = []
|
||||
for table_key, display_config in TABLE_DISPLAY_CONFIG.items():
|
||||
route = display_config["route"]
|
||||
gradient = display_config["gradient"]
|
||||
# 从 STAGING_TABLE_CONFIG 获取 display_name
|
||||
display_name = STAGING_TABLE_CONFIG.get(table_key, {}).get("display_name", table_key)
|
||||
nav_item = f'''
|
||||
<a href="/mds/{route}" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, {gradient[0]}, {gradient[1]});">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8.186 1.113a.5.5 0 0 0 0 1l1.5 1.5a.5.5 0 0 0 1 0l1.5-1.5a.5.5 0 0 0 0-1l-1.5-1.5a.5.5 0 0 0-1 0zM4 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 5 4zm2 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 7 4zm2 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 9 4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">{display_name}</div>
|
||||
<div class="table-desc">{display_config["description"]}</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>'''
|
||||
nav_items.append(nav_item)
|
||||
|
||||
# 最后一个移除 border-bottom
|
||||
nav_items[-1] = nav_items[-1].replace('class="table-link border-bottom"', 'class="table-link"')
|
||||
|
||||
nav_html = '\n'.join(nav_items)
|
||||
html = template.replace('{nav_items}', nav_html)
|
||||
return html
|
||||
|
||||
|
||||
def render_mds_page(page_key):
|
||||
"""使用模板渲染MDS页面"""
|
||||
@@ -62,11 +82,9 @@ def render_mds_page(page_key):
|
||||
frontend_config = get_cached_config(page_key)
|
||||
|
||||
# 准备替换变量
|
||||
import json
|
||||
replacements = {
|
||||
"{page_title}": config["page_title"],
|
||||
"{keyword_placeholder}": config["keyword_placeholder"],
|
||||
"{config_file}": config["config_file"],
|
||||
"{MDS_PAGE_CONFIG}": json.dumps(frontend_config, ensure_ascii=False) if frontend_config else "null"
|
||||
}
|
||||
|
||||
@@ -121,9 +139,7 @@ def register_routes(app):
|
||||
# MDS 数据清洗页面路由
|
||||
@app.get("/mds", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def mds_index():
|
||||
file_path = os.path.join(BASE_DIR, "static", "mds", "index.html")
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
return render_mds_index()
|
||||
|
||||
@app.get("/mds/material", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def mds_material():
|
||||
|
||||
@@ -45,7 +45,7 @@ risky_function() # 异常被记录,程序继续
|
||||
|
||||
```python
|
||||
logger.success("推送订单", "订单001", "共10条")
|
||||
logger.fail("同步失败", "仓库A", "网络超时")
|
||||
logger.fail("推送失败", "仓库A", "网络超时")
|
||||
logger.query("订单表", count=100)
|
||||
logger.insert("日志表", count=5)
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union, TypeVar, Generic
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import time
|
||||
@@ -9,6 +8,11 @@ import time
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
# 延迟导入避免循环依赖
|
||||
def _get_logger():
|
||||
from globalobjects import logger as log_config
|
||||
return log_config.get_logger(__name__)
|
||||
|
||||
class JSONManager:
|
||||
"""基础的JSON文件管理器"""
|
||||
|
||||
@@ -50,7 +54,7 @@ class JSONManager:
|
||||
with self.filepath.open('r', encoding=self.encoding) as f:
|
||||
self._data = json.load(f)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
logging.warning(f"无法加载 {self.filepath},使用空数据")
|
||||
_get_logger().warning(f"无法加载 {self.filepath},使用空数据")
|
||||
self._data = {}
|
||||
|
||||
def _save(self) -> None:
|
||||
@@ -78,14 +82,14 @@ class JSONManager:
|
||||
|
||||
except PermissionError as e:
|
||||
if attempt < max_retries - 1:
|
||||
logging.warning(f"文件被占用,第 {attempt + 1} 次重试: {e}")
|
||||
_get_logger().warning(f"文件被占用,第 {attempt + 1} 次重试: {e}")
|
||||
time.sleep(retry_delay * (attempt + 1))
|
||||
continue
|
||||
else:
|
||||
logging.error(f"保存失败,已重试 {max_retries} 次: {e}")
|
||||
_get_logger().error(f"保存失败,已重试 {max_retries} 次: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"保存失败: {e}")
|
||||
_get_logger().error(f"保存失败: {e}")
|
||||
raise
|
||||
|
||||
def get(self,
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Optional, Dict, Any, List
|
||||
from logging.handlers import TimedRotatingFileHandler, QueueHandler, QueueListener
|
||||
|
||||
|
||||
from core.settings import USE_LOGURU
|
||||
# 日志流处理器列表 - 用于存储外部注册的日志流处理器
|
||||
_log_stream_handlers: List[logging.Handler] = []
|
||||
|
||||
@@ -2308,6 +2307,8 @@ def _check_use_loguru() -> bool:
|
||||
2. 如果 USE_LOGURU=true 或未设置,尝试使用 V2
|
||||
3. 如果 loguru 未安装,自动回退到 V1
|
||||
"""
|
||||
from core.settings import USE_LOGURU
|
||||
|
||||
global _use_loguru
|
||||
if _use_loguru is None:
|
||||
try:
|
||||
|
||||
@@ -307,16 +307,6 @@ span.badge.status-badge.status-badge-pending {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
span.badge.status-badge.status-badge-validated {
|
||||
background-color: var(--success-color) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
span.badge.status-badge.status-badge-rejected {
|
||||
background-color: var(--danger-color) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
span.badge.status-badge.status-badge-synced {
|
||||
background-color: var(--info-color) !important;
|
||||
color: #000 !important;
|
||||
|
||||
+1
-113
@@ -70,119 +70,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="/mds/material" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #0d6efd, #0dcaf0);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8.186 1.113a.5.5 0 0 0 0 1l1.5 1.5a.5.5 0 0 0 1 0l1.5-1.5a.5.5 0 0 0 0-1l-1.5-1.5a.5.5 0 0 0-1 0zM4 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 5 4zm2 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 7 4zm2 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1A.5.5 0 0 0 9 4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">物料数据</div>
|
||||
<div class="table-desc">物料主数据管理,包含物料号、描述、类型等信息</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/workcenter" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #198754, #20c997);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M9 5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1H9zM4 6a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5A.5.5 0 0 1 4 6zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5A.5.5 0 0 1 4 8zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">工作中心</div>
|
||||
<div class="table-desc">工作中心管理,包含产能、瓶颈标识等信息</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/mat-ver" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #fd7e14, #ffc107);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm-2 11.5v-6a.5.5 0 0 1 1 0v6a.5.5 0 0 1-1 0zm-3 0v-4a.5.5 0 0 1 1 0v4a.5.5 0 0 1-1 0zm-2 0v-2a.5.5 0 0 1 1 0v2a.5.5 0 0 1-1 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">产线版本</div>
|
||||
<div class="table-desc">物料版本管理,定义不同生产版本的批量范围</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/mat-wc" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #6f42c1, #d63384);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 0h1v15h15v1H0V0Zm14.848 2.646a.5.5 0 0 1 .706 0l.647.646a.5.5 0 0 1 0 .708l-4.5 4.5a.5.5 0 0 1-.708 0l-.646-.646a.5.5 0 0 1 0-.708l4.5-4.5ZM8 7a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3A.5.5 0 0 0 8 7Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">工艺路线</div>
|
||||
<div class="table-desc">工艺路线管理,定义物料生产的工序流程</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/mat-wc-bom" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #dc3545, #fd7e14);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L8 2.207l6.646 6.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.707 1.5Z"/>
|
||||
<path d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">物料清单 (BOM)</div>
|
||||
<div class="table-desc">物料清单管理,定义产品的组成结构和用量</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/mold" class="table-link border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #343a40, #6c757d);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1Zm3 4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">模具</div>
|
||||
<div class="table-desc">模具主数据管理,包含模具类型、状态、穴数等</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/mds/mat-wc-mold" class="table-link">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="table-icon" style="background: linear-gradient(135deg, #0dcaf0, #20c997);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 0h1v15h15v1H0V0Zm3 3h10v10H3V3Zm2 2v6h6V5H5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="table-info ms-3">
|
||||
<div class="table-title">机台模具关联</div>
|
||||
<div class="table-desc">机台与模具的关联关系管理</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#ccc" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{nav_items}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center py-3 bg-light">
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
/**
|
||||
* @file api-client.js
|
||||
* @description API调用封装层 - 统一错误处理、请求拦截、响应格式化
|
||||
* @author Frontend Team
|
||||
* @version 1.0.0
|
||||
* @date 2026-05-14
|
||||
* @requires ./common.js
|
||||
*/
|
||||
|
||||
class ApiClient {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {string} [options.baseUrl='/api/mds'] - API基础URL
|
||||
* @param {number} [options.timeout=30000] - 请求超时时间(ms)
|
||||
* @param {Object} [options.headers] - 默认请求头
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.baseUrl = options.baseUrl || '/api/mds';
|
||||
this.timeout = options.timeout || 30000;
|
||||
this.defaultHeaders = options.headers || {};
|
||||
this.requestInterceptors = [];
|
||||
this.responseInterceptors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
* @param {Function} interceptor - 拦截器函数 (config) => config
|
||||
*/
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应拦截器
|
||||
* @param {Function} interceptor - 拦截器函数 (response) => response
|
||||
*/
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行请求拦截
|
||||
* @param {Object} config - 请求配置
|
||||
* @returns {Object} 处理后的配置
|
||||
*/
|
||||
async executeRequestInterceptors(config) {
|
||||
let result = { ...config };
|
||||
for (const interceptor of this.requestInterceptors) {
|
||||
result = await interceptor(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行响应拦截
|
||||
* @param {Object} response - 响应对象
|
||||
* @returns {Object} 处理后的响应
|
||||
*/
|
||||
async executeResponseInterceptors(response) {
|
||||
let result = { ...response };
|
||||
for (const interceptor of this.responseInterceptors) {
|
||||
result = await interceptor(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {string} endpoint - API端点路径
|
||||
* @param {Object} [options] - 请求选项
|
||||
* @param {string} [options.method='GET'] - HTTP方法
|
||||
* @param {Object} [options.data] - 请求体数据
|
||||
* @param {Object} [options.params] - URL参数
|
||||
* @param {Object} [options.headers] - 额外请求头
|
||||
* @param {number} [options.timeout] - 超时时间
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async request(endpoint, options = {}) {
|
||||
const {
|
||||
method = 'GET',
|
||||
data = null,
|
||||
params = null,
|
||||
headers = {},
|
||||
timeout = this.timeout
|
||||
} = options;
|
||||
|
||||
// 构建完整URL
|
||||
let url = this.baseUrl + endpoint;
|
||||
|
||||
// 处理URL参数
|
||||
if (params && typeof params === 'object') {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
if (queryString) {
|
||||
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求配置
|
||||
const requestConfig = {
|
||||
method: method.toUpperCase(),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.defaultHeaders,
|
||||
...headers
|
||||
},
|
||||
signal: AbortController.timeout(timeout).signal
|
||||
};
|
||||
|
||||
// 处理请求体
|
||||
if (data !== null && method !== 'GET' && method !== 'HEAD') {
|
||||
requestConfig.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
// 执行请求拦截
|
||||
const processedConfig = await this.executeRequestInterceptors({
|
||||
url,
|
||||
...requestConfig
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, processedConfig);
|
||||
|
||||
// 解析响应
|
||||
let responseData;
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
responseData = await response.json();
|
||||
} else if (contentType && contentType.includes('text/')) {
|
||||
responseData = await response.text();
|
||||
} else {
|
||||
responseData = await response.blob();
|
||||
}
|
||||
|
||||
// 执行响应拦截
|
||||
const result = await this.executeResponseInterceptors({
|
||||
success: response.ok ? 1 : 0,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: responseData,
|
||||
message: response.ok ? 'success' : (responseData?.message || response.statusText)
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError') {
|
||||
return {
|
||||
success: 0,
|
||||
status: 408,
|
||||
message: '请求超时',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
return {
|
||||
success: 0,
|
||||
status: 0,
|
||||
message: '网络请求失败,请检查网络连接',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: 0,
|
||||
status: error.status || 500,
|
||||
message: error.message || '请求失败',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} [params] - URL参数
|
||||
* @param {Object} [options] - 额外选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async get(endpoint, params = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'GET',
|
||||
params,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async post(endpoint, data = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async put(endpoint, data = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async patch(endpoint, data = null, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PATCH',
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} [options] - 额外选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async delete(endpoint, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {File} file - 文件对象
|
||||
* @param {Object} [params] - URL参数
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async upload(endpoint, file, params = null) {
|
||||
let url = this.baseUrl + endpoint;
|
||||
|
||||
if (params && typeof params === 'object') {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
if (queryString) {
|
||||
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: AbortController.timeout(this.timeout).signal
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
return {
|
||||
success: response.ok ? 1 : 0,
|
||||
status: response.status,
|
||||
data: responseData.data,
|
||||
message: response.ok ? 'success' : (responseData.message || response.statusText)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: 0,
|
||||
status: 0,
|
||||
message: error.message || '文件上传失败',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const apiClient = new ApiClient();
|
||||
|
||||
// 添加请求日志拦截器
|
||||
apiClient.addRequestInterceptor(async (config) => {
|
||||
console.debug(`[API Request] ${config.method} ${config.url}`);
|
||||
return config;
|
||||
});
|
||||
|
||||
// 添加响应日志拦截器
|
||||
apiClient.addResponseInterceptor(async (response) => {
|
||||
console.debug(`[API Response] ${response.status} - ${response.message}`);
|
||||
return response;
|
||||
});
|
||||
+28
-120
@@ -23,7 +23,7 @@ const STAGING_STATUS = {
|
||||
},
|
||||
COMPLIANCE_PASS: {
|
||||
value: 'compliance_pass',
|
||||
label: '基本校验通过',
|
||||
label: '初检通过',
|
||||
colorClass: 'text-info',
|
||||
bgClass: 'bg-info',
|
||||
badgeClass: 'status-badge status-badge-compliance_pass',
|
||||
@@ -31,7 +31,7 @@ const STAGING_STATUS = {
|
||||
},
|
||||
COMPLIANCE_ERROR: {
|
||||
value: 'compliance_error',
|
||||
label: '基本校验错误',
|
||||
label: '初检错误',
|
||||
colorClass: 'text-danger',
|
||||
bgClass: 'bg-danger',
|
||||
badgeClass: 'status-badge status-badge-compliance_error',
|
||||
@@ -39,7 +39,7 @@ const STAGING_STATUS = {
|
||||
},
|
||||
RELATION_PASS: {
|
||||
value: 'relation_pass',
|
||||
label: '联合校验通过',
|
||||
label: '联检通过',
|
||||
colorClass: 'text-success',
|
||||
bgClass: 'bg-success',
|
||||
badgeClass: 'status-badge status-badge-relation_pass',
|
||||
@@ -47,12 +47,20 @@ const STAGING_STATUS = {
|
||||
},
|
||||
RELATION_ERROR: {
|
||||
value: 'relation_error',
|
||||
label: '联合校验错误',
|
||||
label: '联检错误',
|
||||
colorClass: 'text-warning',
|
||||
bgClass: 'bg-warning',
|
||||
badgeClass: 'status-badge status-badge-relation_error',
|
||||
icon: 'alert-circle'
|
||||
},
|
||||
SYNC_ERROR: {
|
||||
value: 'sync_error',
|
||||
label: '推送失败',
|
||||
colorClass: 'text-warning',
|
||||
bgClass: 'bg-warning',
|
||||
badgeClass: 'status-badge status-badge-sync_error',
|
||||
icon: 'exclamation-triangle'
|
||||
},
|
||||
SYNCED: {
|
||||
value: 'synced',
|
||||
label: '已推送',
|
||||
@@ -63,36 +71,6 @@ const STAGING_STATUS = {
|
||||
}
|
||||
};
|
||||
|
||||
// 旧状态映射(兼容历史数据)
|
||||
const LEGACY_STATUS_MAP = {
|
||||
'validated': 'relation_pass',
|
||||
'rejected': 'compliance_error'
|
||||
};
|
||||
|
||||
const STATUS_COLORS = {
|
||||
'pending': 'pending',
|
||||
'compliance_pass': 'compliance_pass',
|
||||
'compliance_error': 'compliance_error',
|
||||
'relation_pass': 'relation_pass',
|
||||
'relation_error': 'relation_error',
|
||||
'synced': 'synced',
|
||||
// 兼容旧状态
|
||||
'validated': 'validated',
|
||||
'rejected': 'rejected'
|
||||
};
|
||||
|
||||
const STATUS_TEXTS = {
|
||||
'pending': '待处理',
|
||||
'compliance_pass': '基本校验通过',
|
||||
'compliance_error': '基本校验错误',
|
||||
'relation_pass': '联合校验通过',
|
||||
'relation_error': '联合校验错误',
|
||||
'synced': '已推送',
|
||||
// 兼容旧状态
|
||||
'validated': '校验通过',
|
||||
'rejected': '校验失败'
|
||||
};
|
||||
|
||||
async function callApi(endpoint, method = 'GET', data = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
@@ -115,56 +93,6 @@ async function callApi(endpoint, method = 'GET', data = null) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================== 状态元数据加载(阶段一新增)====================
|
||||
let STAGING_STATUS_META = null;
|
||||
let STAGING_META_LOADED = false;
|
||||
|
||||
/**
|
||||
* 从后端加载状态元数据
|
||||
* @param {boolean} forceReload - 强制重新加载
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function loadStatusMeta(forceReload = false) {
|
||||
if (STAGING_STATUS_META && !forceReload) {
|
||||
return STAGING_STATUS_META;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await callApi('/status-meta');
|
||||
if (response && response.success === 1) {
|
||||
STAGING_STATUS_META = {};
|
||||
response.data.forEach(item => {
|
||||
STAGING_STATUS_META[item.value] = item;
|
||||
});
|
||||
STAGING_META_LOADED = true;
|
||||
console.log('状态元数据加载成功:', Object.keys(STAGING_STATUS_META));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载状态元数据失败,使用硬编码 fallback', e);
|
||||
STAGING_META_LOADED = true;
|
||||
}
|
||||
|
||||
return STAGING_STATUS_META;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待状态元数据加载完成
|
||||
* @param {number} timeout - 超时时间(毫秒)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function waitForStatusMeta(timeout = 3000) {
|
||||
const startTime = Date.now();
|
||||
while (!STAGING_META_LOADED && (Date.now() - startTime) < timeout) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时自动加载状态元数据(非阻塞)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStatusMeta().catch(e => console.warn('自动加载状态元数据失败', e));
|
||||
});
|
||||
|
||||
function handleResponse(response, onSuccess, onError) {
|
||||
if (response.success === 1) {
|
||||
if (onSuccess) onSuccess(response);
|
||||
@@ -204,19 +132,7 @@ function createToastContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
function formatDate(dateStr, includeSecond = false) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
@@ -224,8 +140,12 @@ function formatDateTime(dateStr) {
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
const second = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
|
||||
if (includeSecond) {
|
||||
const second = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,30 +154,18 @@ function formatDateTime(dateStr) {
|
||||
* @returns {Object} 状态配置对象
|
||||
*/
|
||||
function getStatusConfig(status) {
|
||||
// 兼容旧状态
|
||||
const normalizedStatus = LEGACY_STATUS_MAP[status] || status;
|
||||
|
||||
// 优先使用后端元数据
|
||||
const meta = STAGING_STATUS_META?.[normalizedStatus];
|
||||
if (meta) {
|
||||
return {
|
||||
value: meta.value,
|
||||
label: meta.label,
|
||||
colorClass: `text-${meta.color}`,
|
||||
bgClass: `bg-${meta.color}`,
|
||||
badgeClass: `badge bg-${meta.color}`,
|
||||
icon: 'circle'
|
||||
};
|
||||
const statusConfig = STAGING_STATUS[status.toUpperCase()];
|
||||
if (statusConfig) {
|
||||
return statusConfig;
|
||||
}
|
||||
|
||||
// Fallback到旧的硬编码
|
||||
const colorKey = STATUS_COLORS[normalizedStatus] || STATUS_COLORS[status] || 'secondary';
|
||||
return STAGING_STATUS[normalizedStatus.toUpperCase()] || {
|
||||
// 未知状态
|
||||
return {
|
||||
value: status,
|
||||
label: STATUS_TEXTS[normalizedStatus] || STATUS_TEXTS[status] || status,
|
||||
colorClass: `text-${colorKey}`,
|
||||
bgClass: `bg-${colorKey}`,
|
||||
badgeClass: `status-badge status-badge-${colorKey}`
|
||||
label: status,
|
||||
colorClass: 'text-secondary',
|
||||
bgClass: 'bg-secondary',
|
||||
badgeClass: 'badge bg-secondary'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ class DataTable {
|
||||
const classes = [];
|
||||
|
||||
// 状态相关样式
|
||||
if (row._status === 'compliance_error' || row._status === 'rejected') {
|
||||
if (row._status === 'compliance_error') {
|
||||
classes.push('table-row-rejected');
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ class DataTable {
|
||||
|
||||
// 时间字段
|
||||
if (['_createtime', '_updatetime', '_synced_time'].includes(col.field)) {
|
||||
return `<span class="font-mono">${formatDateTime(value)}</span>`;
|
||||
return `<span class="font-mono">${formatDate(value, true)}</span>`;
|
||||
}
|
||||
|
||||
// 空值处理
|
||||
@@ -478,7 +478,7 @@ class DataTable {
|
||||
* 渲染状态单元格
|
||||
*/
|
||||
renderStatusCell(status, row) {
|
||||
if (status === 'compliance_error' || status === 'rejected') {
|
||||
if (status === 'compliance_error') {
|
||||
return `
|
||||
<span class="status-error-cell" data-error-json="${escapeHtml(row._error_msg || '[]')}">
|
||||
${formatStatus(status)}
|
||||
@@ -1210,7 +1210,7 @@ class DataTable {
|
||||
}
|
||||
|
||||
if (['_createtime', '_updatetime', '_synced_time'].includes(field)) {
|
||||
return formatDateTime(value);
|
||||
return formatDate(value, true);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -496,11 +496,13 @@ class MDSPageController {
|
||||
}
|
||||
|
||||
const stats = response.data;
|
||||
const validatedCount = stats.relation_pass || stats.validated || 0;
|
||||
const relationPassCount = stats.relation_pass || 0;
|
||||
const syncErrorCount = stats.sync_error || 0;
|
||||
const retryExceeded = stats.retry_exceeded || 0;
|
||||
|
||||
if (validatedCount === 0) {
|
||||
showMessage('没有【联合校验通过】的记录可推送', 'warning');
|
||||
if (relationPassCount === 0 && syncErrorCount === 0) {
|
||||
showMessage('没有【联合校验通过】或【同步失败】的记录可推送', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
let resetRetry = false;
|
||||
@@ -508,11 +510,11 @@ class MDSPageController {
|
||||
resetRetry = confirm(`有${retryExceeded}条记录的重试次数已达上限,是否重置重试次数后推送?\n\n点击"确定"重置并推送,点击"取消"跳过这些记录`);
|
||||
}
|
||||
|
||||
const { mode, targetDbs } = await this.showSyncModeDialog(validatedCount);
|
||||
const { mode, targetDbs } = await this.showSyncModeDialog(relationPassCount, syncErrorCount);
|
||||
if (!mode || !targetDbs || targetDbs.length === 0) return;
|
||||
|
||||
const targetDbParam = targetDbs.join(',');
|
||||
const totalCount = validatedCount * targetDbs.length;
|
||||
const totalCount = (relationPassCount + syncErrorCount) * targetDbs.length;
|
||||
|
||||
showProgress(mode === 'incremental' ? '增量推送中' : '刷新推送中', totalCount);
|
||||
|
||||
@@ -576,10 +578,13 @@ class MDSPageController {
|
||||
this.statusCard.refresh();
|
||||
}
|
||||
|
||||
async showSyncModeDialog(validatedCount) {
|
||||
async showSyncModeDialog(relationPassCount, syncErrorCount) {
|
||||
const dbListResponse = await callApi('/dblist');
|
||||
const dbList = dbListResponse.success === 1 ? dbListResponse.data : [];
|
||||
|
||||
const totalCount = relationPassCount + syncErrorCount;
|
||||
const hasSyncError = syncErrorCount > 0;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="syncModeModal" tabindex="-1">
|
||||
@@ -590,6 +595,12 @@ class MDSPageController {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${hasSyncError ? `
|
||||
<div class="alert alert-info small mb-3">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
将推送 <strong>${relationPassCount}</strong> 条【联合校验通过】+ <strong>${syncErrorCount}</strong> 条【同步失败】的记录
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">目标账套</label>
|
||||
<div class="border rounded p-2" style="max-height: 150px; overflow-y: auto;">
|
||||
@@ -608,7 +619,7 @@ class MDSPageController {
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="syncMode" id="modeIncremental" value="incremental" checked>
|
||||
<label class="form-check-label" for="modeIncremental">
|
||||
<strong>增量推送</strong> <span class="badge bg-primary">${validatedCount}条</span>
|
||||
<strong>增量推送</strong> <span class="badge bg-primary">${totalCount}条</span>
|
||||
</label>
|
||||
<div class="text-muted small mt-1">仅推送校验通过的新数据,保留正式表现有数据</div>
|
||||
</div>
|
||||
@@ -617,7 +628,7 @@ class MDSPageController {
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="syncMode" id="modeRefresh" value="refresh">
|
||||
<label class="form-check-label" for="modeRefresh">
|
||||
<strong>刷新推送</strong> <span class="badge bg-warning text-dark">${validatedCount}条</span>
|
||||
<strong>刷新推送</strong> <span class="badge bg-warning text-dark">${totalCount}条</span>
|
||||
</label>
|
||||
<div class="text-muted small mt-1">清空正式表后,重新推送校验通过的数据</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ class StatusCard {
|
||||
<div class="card status-card" data-status="compliance_pass">
|
||||
<div class="card-body text-center">
|
||||
<div class="status-number text-info" id="compliancePassCount">-</div>
|
||||
<div class="status-label">基本校验通过</div>
|
||||
<div class="status-label">初检通过</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ class StatusCard {
|
||||
<div class="card status-card" data-status="compliance_error">
|
||||
<div class="card-body text-center">
|
||||
<div class="status-number text-danger" id="complianceErrorCount">-</div>
|
||||
<div class="status-label">基本校验错误</div>
|
||||
<div class="status-label">初检错误</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ class StatusCard {
|
||||
<div class="card status-card" data-status="relation_pass">
|
||||
<div class="card-body text-center">
|
||||
<div class="status-number text-success" id="relationPassCount">-</div>
|
||||
<div class="status-label">联合校验通过</div>
|
||||
<div class="status-label">联检通过</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +75,15 @@ class StatusCard {
|
||||
<div class="card status-card" data-status="relation_error">
|
||||
<div class="card-body text-center">
|
||||
<div class="status-number text-warning" id="relationErrorCount">-</div>
|
||||
<div class="status-label">联合校验错误</div>
|
||||
<div class="status-label">联检错误</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card status-card" data-status="sync_error">
|
||||
<div class="card-body text-center">
|
||||
<div class="status-number text-warning" id="syncErrorCount">-</div>
|
||||
<div class="status-label">推送失败</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,6 +157,7 @@ class StatusCard {
|
||||
{ status: 'compliance_error', elementId: 'complianceErrorCount' },
|
||||
{ status: 'relation_pass', elementId: 'relationPassCount' },
|
||||
{ status: 'relation_error', elementId: 'relationErrorCount' },
|
||||
{ status: 'sync_error', elementId: 'syncErrorCount' },
|
||||
{ status: 'synced', elementId: 'syncedCount' }
|
||||
];
|
||||
|
||||
@@ -182,6 +191,7 @@ class StatusCard {
|
||||
+ this.getStatusCount('compliance_error')
|
||||
+ this.getStatusCount('relation_pass')
|
||||
+ this.getStatusCount('relation_error')
|
||||
+ this.getStatusCount('sync_error')
|
||||
+ this.getStatusCount('synced');
|
||||
}
|
||||
|
||||
@@ -191,18 +201,7 @@ class StatusCard {
|
||||
* @returns {number} 计数值
|
||||
*/
|
||||
getStatusCount(status) {
|
||||
// 优先从stats获取
|
||||
if (this.stats[status] !== undefined) {
|
||||
return this.stats[status];
|
||||
}
|
||||
|
||||
// 兼容旧状态命名
|
||||
const legacyMap = {
|
||||
'relation_pass': this.stats.validated,
|
||||
'compliance_error': this.stats.rejected
|
||||
};
|
||||
|
||||
return legacyMap[status] || 0;
|
||||
return this.stats[status] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,7 +214,7 @@ class StatusCard {
|
||||
const count = parseInt(numberElement.textContent) || 0;
|
||||
|
||||
// 根据状态和数量添加视觉反馈
|
||||
if (status === 'compliance_error' || status === 'relation_error') {
|
||||
if (status === 'compliance_error' || status === 'relation_error' || status === 'sync_error') {
|
||||
if (count > 0) {
|
||||
card.classList.add('status-card-error');
|
||||
} else {
|
||||
@@ -276,11 +275,11 @@ class StatusCard {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可推送数量
|
||||
* 获取可推送数量(联合校验通过 + 同步失败)
|
||||
* @returns {number} 可推送数量
|
||||
*/
|
||||
getReadyToSyncCount() {
|
||||
return this.getStatusCount('relation_pass');
|
||||
return this.getStatusCount('relation_pass') + this.getStatusCount('sync_error');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,8 +77,9 @@
|
||||
<select class="form-select form-select-sm" id="statusFilter" style="width:100px">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待处理</option>
|
||||
<option value="validated">校验通过</option>
|
||||
<option value="rejected">校验失败</option>
|
||||
<option value="relation_pass">校验通过</option>
|
||||
<option value="compliance_error">校验失败</option>
|
||||
<option value="sync_error">推送失败</option>
|
||||
<option value="synced">已推送</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -247,21 +248,12 @@
|
||||
<script src="/static/mds/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/mds/js/common.js"></script>
|
||||
<script src="/static/mds/js/modal-manager.js"></script>
|
||||
<script src="/static/mds/js/api-client.js"></script>
|
||||
<script src="/static/mds/js/data-table.js"></script>
|
||||
<script src="/static/mds/js/status-card.js"></script>
|
||||
<!-- 条件加载配置:优先使用嵌入的配置,否则加载外部文件 -->
|
||||
<script>
|
||||
var MDS_PAGE_CONFIG = {MDS_PAGE_CONFIG};
|
||||
</script>
|
||||
<script>
|
||||
if (MDS_PAGE_CONFIG === null) {
|
||||
// 如果没有嵌入配置,则动态加载外部文件
|
||||
var script = document.createElement('script');
|
||||
script.src = '/static/mds/configs/{config_file}';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
</script>
|
||||
<script src="/static/mds/js/mds-page-controller.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
Reference in New Issue
Block a user