每天 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)
121 lines
6.3 KiB
Python
121 lines
6.3 KiB
Python
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', '')
|