fix(email): 审批通过后库管通知增加明细+DEBUG日志,修复MAIL_DEFAULT_SENDER格式问题
This commit is contained in:
@ -492,8 +492,6 @@ class OutboundService:
|
||||
|
||||
ModelClass = model_map.get(d.source_table)
|
||||
if ModelClass and d.stock_id:
|
||||
# 注意:这里在循环中查询可能会有N+1问题,但考虑到单页数据量(通常每单条目不多),暂时可接受
|
||||
# 生产环境建议优化为预加载或批量查询
|
||||
try:
|
||||
stock_item = ModelClass.query.get(d.stock_id)
|
||||
if stock_item:
|
||||
@ -716,12 +714,16 @@ class OutboundApprovalService:
|
||||
# username 格式为 "姓名/账号",取姓名部分
|
||||
applicant_name = str(u.username).split('/')[0] if '/' in u.username else (u.username or str(applicant_id))
|
||||
|
||||
# ★ 发送通知,附完整物料清单
|
||||
items = approval.get_items()
|
||||
send_new_request_notify(
|
||||
to_emails=emails,
|
||||
request_no=approval.request_no,
|
||||
applicant_name=applicant_name,
|
||||
remark=approval.remark or ''
|
||||
remark=approval.remark or '',
|
||||
items=items
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# ★ 捕获所有异常,确保邮件发送失败不阻断主流程
|
||||
try:
|
||||
@ -729,6 +731,7 @@ class OutboundApprovalService:
|
||||
current_app.logger.error(f"[Email] 发送新申请通知邮件失败: {e}")
|
||||
except RuntimeError:
|
||||
# 如果不在 Flask 应用上下文内,降级为标准日志
|
||||
import logging
|
||||
logging.getLogger(__name__).error(f"[Email] 发送新申请通知邮件失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
@ -817,28 +820,85 @@ class OutboundApprovalService:
|
||||
@staticmethod
|
||||
def _notify_approval_result(approval, approver_id, action):
|
||||
"""发送审批结果通知邮件(静默处理,不阻断主流程)"""
|
||||
try:
|
||||
from app.utils.email_service import send_approval_result_notify
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 仓库管理员角色代码
|
||||
try:
|
||||
from app.utils.email_service import send_approval_result_notify, send_warehouse_dispatch_notify
|
||||
|
||||
# ★ 通过:只通知库管,附完整物料清单
|
||||
if action == 'approve':
|
||||
warehouse_role_codes = ['WAREHOUSE_MGR', 'OUTBOUND']
|
||||
|
||||
# 查询库管邮箱 + 申请人本人邮箱
|
||||
# --- 【DEBUG】精准打印调试信息 ---
|
||||
print(f"[DEBUG] === _notify_approval_result 审批通过分支 ===")
|
||||
print(f"[DEBUG] 审批单 ID={approval.id} request_no={approval.request_no}")
|
||||
print(f"[DEBUG] items_json 原始值类型: {type(approval.items_json)} 值={repr(approval.items_json)}")
|
||||
|
||||
# ★ items_json 已是 list,直接使用,不做 json.loads 解析
|
||||
items = approval.items_json if approval.items_json else []
|
||||
|
||||
# 查询库管邮箱
|
||||
emails = OutboundApprovalService._get_emails_by_identifiers(
|
||||
applicant_id=approval.applicant_id,
|
||||
role_codes=warehouse_role_codes
|
||||
)
|
||||
print(f"[DEBUG] 库管邮箱列表: {emails}")
|
||||
print(f"[DEBUG] 库管角色配置: {warehouse_role_codes}")
|
||||
|
||||
# 打印所有 SysUser 中 role 属于这两个角色的邮箱(用于诊断)
|
||||
from app.models.system import SysUser as SU
|
||||
all_warehouse_users = SU.query.filter(SU.role.in_(warehouse_role_codes)).all()
|
||||
print(f"[DEBUG] 数据库中库管角色用户数: {len(all_warehouse_users)}")
|
||||
for u in all_warehouse_users:
|
||||
print(f" -> 用户ID={u.id} role={u.role} email={u.email} username={u.username}")
|
||||
|
||||
if not emails:
|
||||
print(f"[DEBUG] 警告:无库管邮箱,跳过通知")
|
||||
return
|
||||
|
||||
# 获取申请人姓名
|
||||
applicant_name = ''
|
||||
if approval.applicant_id:
|
||||
user = SU.query.get(approval.applicant_id)
|
||||
if user:
|
||||
applicant_name = str(user.username).split('/')[0] if '/' in (user.username or '') else (user.username or '')
|
||||
|
||||
print(f"[DEBUG] 准备发送邮件 to={emails} items数量={len(items)} items内容={items}")
|
||||
|
||||
# ★ 调用库管备货通知(含完整物料明细表格)
|
||||
send_warehouse_dispatch_notify(
|
||||
to_emails=emails,
|
||||
request_no=approval.request_no,
|
||||
applicant_name=applicant_name,
|
||||
items=items
|
||||
)
|
||||
print(f"[DEBUG] send_warehouse_dispatch_notify 调用完成")
|
||||
|
||||
# ★ 驳回:只通知申请人本人
|
||||
else:
|
||||
from app.models.system import SysUser as SU2
|
||||
applicant_name = ''
|
||||
if approval.applicant_id:
|
||||
user = SU2.query.get(approval.applicant_id)
|
||||
if user:
|
||||
applicant_name = str(user.username).split('/')[0] if '/' in (user.username or '') else (user.username or '')
|
||||
emails = OutboundApprovalService._get_emails_by_identifiers(
|
||||
applicant_id=approval.applicant_id
|
||||
)
|
||||
if not emails:
|
||||
return
|
||||
|
||||
send_approval_result_notify(
|
||||
to_emails=emails,
|
||||
request_no=approval.request_no,
|
||||
is_passed=(action == 'approve'),
|
||||
reject_reason=approval.reject_reason or ''
|
||||
is_passed=False,
|
||||
reject_reason=approval.reject_reason or '',
|
||||
applicant_name=applicant_name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(f"[Email] 发送审批结果通知邮件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
logger.warning(f"[Email] 发送审批结果通知邮件失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
def get_request_list(page=1, per_page=10, applicant_id=None, status=None):
|
||||
|
||||
@ -58,19 +58,24 @@ def send_email(to_email: Union[str, List[str]], subject: str, content: str):
|
||||
"""
|
||||
cfg = _get_config()
|
||||
|
||||
print(f"[DEBUG send_email] cfg = {cfg}")
|
||||
|
||||
# 发送总开关
|
||||
if not cfg.get('enabled'):
|
||||
print(f"[Email] 邮件功能已禁用 (MAIL_ENABLED=false),跳过发送: {subject}")
|
||||
logger.info(f"[Email] 邮件功能已禁用 (MAIL_ENABLED=false),跳过发送: {subject}")
|
||||
return
|
||||
|
||||
# 配置完整性检查
|
||||
if not cfg.get('server') or not cfg.get('username') or not cfg.get('password'):
|
||||
print(f"[Email] 邮件配置不完整 server={cfg.get('server')} username={cfg.get('username')} password={'已设' if cfg.get('password') else '空'},跳过发送")
|
||||
logger.warning("[Email] 邮件配置不完整 (MAIL_SERVER/USERNAME/PASSWORD 缺失),跳过发送")
|
||||
return
|
||||
|
||||
# 标准化收件人列表
|
||||
recipients = [to_email] if isinstance(to_email, str) else [r.strip() for r in to_email if r.strip()]
|
||||
if not recipients:
|
||||
print("[Email] 收件人地址为空,跳过发送")
|
||||
logger.warning("[Email] 收件人地址为空,跳过发送")
|
||||
return
|
||||
|
||||
@ -81,6 +86,8 @@ def send_email(to_email: Union[str, List[str]], subject: str, content: str):
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
msg.attach(MIMEText(content, 'plain', 'utf-8'))
|
||||
|
||||
print(f"DEBUG: 准备向服务器提交发信请求,收件人: {recipients} 发件人: {cfg['username']}")
|
||||
|
||||
if cfg.get('use_ssl'):
|
||||
context = ssl.create_default_context()
|
||||
with smtplib.SMTP_SSL(cfg['server'], cfg.get('port', 465), context=context) as server:
|
||||
@ -96,26 +103,50 @@ def send_email(to_email: Union[str, List[str]], subject: str, content: str):
|
||||
logger.info(f"[Email] 发送成功 -> {recipients}: {subject}")
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
print(f"!!! 邮件发送核心报错: SMTPAuthenticationError - 邮箱认证失败,请检查 MAIL_USERNAME / MAIL_PASSWORD(授权码)")
|
||||
logger.error("[Email] 邮箱认证失败,请检查 MAIL_USERNAME / MAIL_PASSWORD(授权码)")
|
||||
except smtplib.SMTPRecipientsRefused as e:
|
||||
print(f"!!! 邮件发送核心报错: SMTPRecipientsRefused - 收件人被服务器拒绝: {e}")
|
||||
logger.error(f"[Email] 收件人被服务器拒绝: {e}")
|
||||
except smtplib.SMTPException as e:
|
||||
print(f"!!! 邮件发送核心报错: SMTPException - {e}")
|
||||
logger.error(f"[Email] SMTP 异常: {e}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"!!! 邮件发送核心报错: {type(e).__name__} - {e}")
|
||||
logger.error(f"[Email] 发送邮件时发生未知异常: {e}")
|
||||
|
||||
|
||||
def send_new_request_notify(to_emails: List[str], request_no: str,
|
||||
applicant_name: str = '', remark: str = ''):
|
||||
applicant_name: str = '', remark: str = '',
|
||||
items: list = None):
|
||||
"""
|
||||
通知审批人有新的出库申请单待审批
|
||||
通知审批人有新的出库申请单待审批(可附带物料清单)
|
||||
|
||||
Args:
|
||||
to_emails: 审批人邮箱列表
|
||||
request_no: 审批单号
|
||||
applicant_name: 申请人姓名
|
||||
remark: 申请备注
|
||||
items: 物料明细列表(可选)
|
||||
"""
|
||||
print(f"[DEBUG send_new_request_notify] 入参 items={items}")
|
||||
print(f"[DEBUG send_new_request_notify] items 类型={type(items)}, 长度={len(items) if items else 0}")
|
||||
|
||||
# 拼装物料表格
|
||||
rows = []
|
||||
rows.append("名称 | 规格 | 计划数量")
|
||||
rows.append("-" * 40)
|
||||
if items:
|
||||
for item in items:
|
||||
name = item.get('name', '-') or '-'
|
||||
spec = item.get('spec_model', '-') or '-'
|
||||
qty = item.get('quantity', '-') or '-'
|
||||
rows.append(f"{name} | {spec} | {qty}")
|
||||
else:
|
||||
rows.append("(无物料明细)")
|
||||
|
||||
subject = f"【待审批】出库申请单 {request_no}"
|
||||
content = f"""您好,
|
||||
|
||||
@ -125,6 +156,9 @@ def send_new_request_notify(to_emails: List[str], request_no: str,
|
||||
申请人:{applicant_name or '未知'}
|
||||
备注说明:{remark or '无'}
|
||||
|
||||
物料清单如下:
|
||||
{chr(10).join(rows)}
|
||||
|
||||
请登录仓库管理系统进行审批。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
@ -133,36 +167,84 @@ def send_new_request_notify(to_emails: List[str], request_no: str,
|
||||
|
||||
|
||||
def send_approval_result_notify(to_emails: List[str], request_no: str,
|
||||
is_passed: bool, reject_reason: str = ''):
|
||||
is_passed: bool, reject_reason: str = '',
|
||||
applicant_name: str = ''):
|
||||
"""
|
||||
通知库管和申请人审批结果
|
||||
通知审批结果
|
||||
|
||||
Args:
|
||||
to_emails: 收件人邮箱列表(库管 + 申请人)
|
||||
to_emails: 收件人邮箱列表
|
||||
request_no: 审批单号
|
||||
is_passed: 是否通过
|
||||
is_passed: 是否通过(通过时发给库管,驳回时发给申请人)
|
||||
reject_reason: 驳回原因(仅 is_passed=False 时使用)
|
||||
applicant_name: 申请人姓名(仅驳回通知时使用)
|
||||
"""
|
||||
if is_passed:
|
||||
subject = f"【已通过】出库申请单 {request_no}"
|
||||
# ★ 发给库管:提醒备货出库
|
||||
subject = f"【待出库】出库申请单 {request_no} 已审批通过"
|
||||
content = f"""您好,
|
||||
|
||||
出库申请单 {request_no} 已审批通过,请准备备货。
|
||||
出库申请单 {request_no} 已审批通过,请尽快备货出库。
|
||||
|
||||
请尽快安排出库操作。
|
||||
请登录仓库管理系统处理该出库任务。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
"""
|
||||
else:
|
||||
# ★ 发给申请人:告知被驳回
|
||||
subject = f"【已驳回】出库申请单 {request_no}"
|
||||
content = f"""您好,
|
||||
content = f"""{"尊敬的 " + applicant_name + ",您好" if applicant_name else "您好"},
|
||||
|
||||
出库申请单 {request_no} 已被驳回。
|
||||
出库申请单 {request_no} 已被审批驳回。
|
||||
|
||||
驳回原因:{reject_reason or '未填写'}
|
||||
|
||||
请登录仓库管理系统查看详情。
|
||||
请登录仓库管理系统查看详情,并根据驳回原因调整后重新提交申请。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
"""
|
||||
send_email(to_emails, subject, content)
|
||||
|
||||
|
||||
def send_warehouse_dispatch_notify(to_emails: List[str], request_no: str,
|
||||
applicant_name: str = '', items: list = None):
|
||||
"""
|
||||
通知库管备货出库(包含完整物料清单)
|
||||
|
||||
Args:
|
||||
to_emails: 库管邮箱列表
|
||||
request_no: 审批单号
|
||||
applicant_name: 申请人姓名
|
||||
items: 物料明细列表,每个元素包含 name/spec_model/warehouse_location/quantity
|
||||
"""
|
||||
print(f"[DEBUG send_warehouse_dispatch_notify] 入参 items={items}")
|
||||
print(f"[DEBUG send_warehouse_dispatch_notify] items 类型={type(items)}, 长度={len(items) if items else 0}")
|
||||
|
||||
rows = []
|
||||
rows.append("名称 | 规格 | 库位 | 计划数量")
|
||||
rows.append("-" * 50)
|
||||
if items:
|
||||
for item in items:
|
||||
name = item.get('name', '-') or '-'
|
||||
spec = item.get('spec_model', '-') or '-'
|
||||
loc = item.get('warehouse_location', '-') or '-'
|
||||
qty = item.get('quantity', '-') or '-'
|
||||
rows.append(f"{name} | {spec} | {loc} | {qty}")
|
||||
else:
|
||||
rows.append("(无物料明细)")
|
||||
|
||||
subject = f"【待出库】出库申请单 {request_no} 已审批通过"
|
||||
content = f"""您好,
|
||||
|
||||
出库申请单 {request_no} 已审批通过,请按以下清单准备备货:
|
||||
|
||||
{chr(10).join(rows)}
|
||||
|
||||
申请人:{applicant_name or '未知'}
|
||||
|
||||
请登录仓库管理系统执行"按单出库"操作。
|
||||
|
||||
此邮件由系统自动发送,请勿回复。
|
||||
"""
|
||||
send_email(to_emails, subject, content)
|
||||
print(f"DEBUG: 准备向服务器提交发信请求,收件人: {to_emails}")
|
||||
|
||||
@ -65,7 +65,7 @@ class Config:
|
||||
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系统 <wms@iris-rs.cn>')
|
||||
# 默认发件人(★ 必须与 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')
|
||||
Reference in New Issue
Block a user