Files
KCGL/inventory-backend/config.py
yueli ded5dfd7b3 feat(report): MOM 系统日报(纯模板,不接 AI)
每天 17:30(北京时间)自动汇总当日 新增/修改/删除、出库、借库,
渲染为纯文本邮件发送。

为什么不用 AI
--------------
此前那份日报由 Dify(外部 AI 服务,见 api/v1/ai_proxy.py)生成,
好处是能写人话,代价是:
  · 外部依赖一断,整份日报就没了 —— 这正是"AI 坏了"之后发生的事;
  · 按次计费、走网络、要管密钥;
  · **会在报表里编数字**,而日报是给人做决策用的,数字必须可信。
日报是固定格式的结构化统计(计数 + 清单),本来也不需要创造力。
数据全在库里:audit_logs / trans_outbound / trans_borrow。

口径与审计页保持一致
--------------------
  · action 经 canon_action() 归一化(历史数据里 CREATE 与「新增」混用,
    不归一会漏掉早期数据);
  · 默认排除 system 占位账号(与审计页默认的"真实用户"视图一致)。
  · 凡有上限处一律显式写明「另有 N 条」,不静默截断。
  · 变更值全部翻译成人类可读:状态码按模块查表、审批人/物料 ID → 名称、
    布尔 → 是/否、人名串规整。翻不动就原样显示 —— 报表里的错译比裸数字
    更难被发现,也更危险。

★ 顺带修掉两个既有缺陷(日报每天都会走这条路径,不修会一直中招):

1) 定时任务被 8 个 worker 重复执行
   gunicorn.conf.py 配了 8 worker,而 run.py 在**模块级**启动 APScheduler
   —— gunicorn 未开 preload_app,每个 worker 独立 import 一次 run.py,
   于是每个 worker 各起一份调度器,同一 cron 任务被并发执行 8 次。
   现有库存预警邮件因此每天重复发 8 封。
   新增 app/utils/job_lock.py:用 PostgreSQL 会话级咨询锁做跨进程互斥,
   抢到锁的 worker 才执行,其余静默跳过。不引入新组件(compose 里没有
   redis,redis_client 恒为 None,相关装饰器全程 fail-open,指望不上)。
   实测 8 线程并发抢锁恰好 1 个成功。

2) 邮箱授权码被打印进日志
   email_service.py 的 print(f"[DEBUG send_email] cfg = {cfg}") 会把含
   password 的整个配置打到 stdout → docker logs。改为只打非敏感字段。

新增配置:MAIL_DAILY_REPORT_RECIPIENTS(逗号分隔,默认 duxingchen@iris-rs.cn)
2026-09-23 17:16:26 +08:00

121 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
from datetime import timedelta
class Config:
# =========================================================
# 1. 基础路径与安全配置
# =========================================================
# 获取当前文件所在目录的绝对路径 (用于定位 uploads 文件夹等)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
# Flask 的基础密钥 (用于 Session, Flash 消息等安全签名)
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-1234')
# =========================================================
# 2. 数据库配置
# =========================================================
# 优先读取 .env 中的 'DATABASE_URL'。
# 如果读不到,才回退使用默认的 localhost 连接字符串。
SQLALCHEMY_DATABASE_URI = os.getenv(
'DATABASE_URL',
'postgresql://postgres:1234@localhost:5432/inventory_system'
)
# 关闭 SQLAlchemy 的事件追踪,减少内存消耗 (推荐设为 False)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# =========================================================
# 3. JWT 配置 (修复 500 报错的核心区域)
# =========================================================
# 【核心】必须设置 JWT_SECRET_KEY,否则 create_access_token 会报错
# 逻辑:优先读环境变量,读不到就用默认字符串
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'default-jwt-secret-key-if-missing')
# Access Token 有效期: 2 小时
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=2)
# Refresh Token 有效期: 7 天
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7)
# =========================================================
# 4. 文件上传配置
# =========================================================
# 上传文件存储路径
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
# 限制最大上传 16MB
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
# =========================================================
# 5. Redis 配置 (用于单设备登录互踢)
# =========================================================
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
# =========================================================
# 6. 邮件配置
# =========================================================
# 发件人邮箱(阿里企业邮箱)
MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'wms@iris-rs.cn')
# 发件人邮箱密码 / 授权码
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', 'Q7nYyyESWlaThKjx')
# SMTP 服务器地址(阿里企业邮发信服务器)
MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.mxhichina.com')
# SMTP 端口(阿里邮箱使用 SSL 465)
MAIL_PORT = int(os.getenv('MAIL_PORT', 465))
# 是否启用 TLS (587 端口通常需要)
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() in ('true', '1', 'yes')
# 是否启用 SSL (465 端口通常需要,阿里邮箱必须启用 SSL)
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'true').lower() in ('true', '1', 'yes')
# 默认发件人(★ 必须与 MAIL_USERNAME 完全一致,否则阿里邮件服务器会拒绝)
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', 'wms@iris-rs.cn')
# 是否启用邮件发送功能(开发环境可设为 false 禁用)
MAIL_ENABLED = os.getenv('MAIL_ENABLED', 'true').lower() in ('true', '1', 'yes')
# MOM 系统日报收件人(逗号分隔多个)。留空则日报任务跳过发送并在日志告警。
MAIL_DAILY_REPORT_RECIPIENTS = os.getenv(
'MAIL_DAILY_REPORT_RECIPIENTS', 'duxingchen@iris-rs.cn'
)
# =========================================================
# 7. Track 系统 Webhook 通知配置 (发送侧)
# =========================================================
# Track 系统 Webhook 接收地址(未配置则静默跳过,不通知)
TRACK_WEBHOOK_URL = os.getenv('TRACK_WEBHOOK_URL', '')
# 与 Track 接收端严格对齐的 API Key(请求头字段名必须为 X-API-Key)
TRACK_WEBHOOK_KEY = os.getenv('TRACK_WEBHOOK_KEY', '')
# =========================================================
# 8. Track 系统查询接口配置 (拉取侧)
# =========================================================
# Track 后端基础地址(扫码入库时用 httpx 查询产品信息)
TRACK_API_URL = os.getenv('TRACK_API_URL', 'http://track_backend:8000')
# Track 出库 webhook 接收地址(发货出库时通知 Track 标记"已出库")
TRACK_OUTBOUND_WEBHOOK_URL = os.getenv('TRACK_OUTBOUND_WEBHOOK_URL', '')
# =========================================================
# 9. 多 Track 实例路由表 (IRIS / LICA)
# =========================================================
# JSON 结构: {"IRIS": {"api": "...", "inbound": "...", "outbound": "..."}, "LICA": {...}}
# 由 material_base.company_name 决定业务落到哪个 Track 实例。
# 缺省为空字典 → 全部回落到上面第 7/8 节的扁平变量,行为与改造前一致。
#
# ⚠️ 解析失败直接让进程起不来(fail fast)——路由表写坏却静默回落,
# 会造成"某些公司悄悄断链",比启动报错难排查得多。
try:
TRACK_ROUTES = json.loads(os.getenv('TRACK_ROUTES', '{}') or '{}')
except ValueError as e:
raise RuntimeError(f'TRACK_ROUTES 不是合法 JSON,请检查环境变量: {e}')
# =========================================================
# 10. 对内接口配置 (接收侧:Track → MOM)
# =========================================================
# Track 调 MOM 内部接口(如生产报废受理)的共享密钥,请求头 X-API-Key。
#
# ★ 与 TRACK_WEBHOOK_KEY 刻意分离,不复用:
# · 方向相反 —— 那个是 MOM 发给 Track 的凭证,这个是 Track 发给 MOM 的;
# · 权限不同 —— 本密钥能发起报废审批(间接影响台账与成本),
# 万一泄漏,两边各自轮换即可,不会连坐。
#
# ★ 未配置时内部接口一律返回 503(Fail-Closed),**不做静默放行** ——
# 一个默认开着的写接口,比一个没配好的接口危险得多。
MOM_INTERNAL_API_KEY = os.getenv('MOM_INTERNAL_API_KEY', '')