fix(email): 审批通过后库管通知增加明细+DEBUG日志,修复MAIL_DEFAULT_SENDER格式问题

This commit is contained in:
DXC
2026-04-28 16:46:12 +08:00
parent 183b93012e
commit ccbce82c2e
3 changed files with 178 additions and 36 deletions

View File

@ -492,8 +492,6 @@ class OutboundService:
ModelClass = model_map.get(d.source_table) ModelClass = model_map.get(d.source_table)
if ModelClass and d.stock_id: if ModelClass and d.stock_id:
# 注意这里在循环中查询可能会有N+1问题但考虑到单页数据量通常每单条目不多暂时可接受
# 生产环境建议优化为预加载或批量查询
try: try:
stock_item = ModelClass.query.get(d.stock_id) stock_item = ModelClass.query.get(d.stock_id)
if stock_item: if stock_item:
@ -716,12 +714,16 @@ class OutboundApprovalService:
# username 格式为 "姓名/账号",取姓名部分 # username 格式为 "姓名/账号",取姓名部分
applicant_name = str(u.username).split('/')[0] if '/' in u.username else (u.username or str(applicant_id)) 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( send_new_request_notify(
to_emails=emails, to_emails=emails,
request_no=approval.request_no, request_no=approval.request_no,
applicant_name=applicant_name, applicant_name=applicant_name,
remark=approval.remark or '' remark=approval.remark or '',
items=items
) )
except Exception as e: except Exception as e:
# ★ 捕获所有异常,确保邮件发送失败不阻断主流程 # ★ 捕获所有异常,确保邮件发送失败不阻断主流程
try: try:
@ -729,6 +731,7 @@ class OutboundApprovalService:
current_app.logger.error(f"[Email] 发送新申请通知邮件失败: {e}") current_app.logger.error(f"[Email] 发送新申请通知邮件失败: {e}")
except RuntimeError: except RuntimeError:
# 如果不在 Flask 应用上下文内,降级为标准日志 # 如果不在 Flask 应用上下文内,降级为标准日志
import logging
logging.getLogger(__name__).error(f"[Email] 发送新申请通知邮件失败: {e}") logging.getLogger(__name__).error(f"[Email] 发送新申请通知邮件失败: {e}")
@staticmethod @staticmethod
@ -817,28 +820,85 @@ class OutboundApprovalService:
@staticmethod @staticmethod
def _notify_approval_result(approval, approver_id, action): def _notify_approval_result(approval, approver_id, action):
"""发送审批结果通知邮件(静默处理,不阻断主流程)""" """发送审批结果通知邮件(静默处理,不阻断主流程)"""
import logging
logger = logging.getLogger(__name__)
try: try:
from app.utils.email_service import send_approval_result_notify from app.utils.email_service import send_approval_result_notify, send_warehouse_dispatch_notify
# 仓库管理员角色代码 # ★ 通过:只通知库管,附完整物料清单
warehouse_role_codes = ['WAREHOUSE_MGR', 'OUTBOUND'] if action == 'approve':
warehouse_role_codes = ['WAREHOUSE_MGR', 'OUTBOUND']
# 查询库管邮箱 + 申请人本人邮箱 # --- 【DEBUG】精准打印调试信息 ---
emails = OutboundApprovalService._get_emails_by_identifiers( print(f"[DEBUG] === _notify_approval_result 审批通过分支 ===")
applicant_id=approval.applicant_id, print(f"[DEBUG] 审批单 ID={approval.id} request_no={approval.request_no}")
role_codes=warehouse_role_codes print(f"[DEBUG] items_json 原始值类型: {type(approval.items_json)} 值={repr(approval.items_json)}")
)
if not emails: # ★ items_json 已是 list直接使用不做 json.loads 解析
return items = approval.items_json if approval.items_json else []
# 查询库管邮箱
emails = OutboundApprovalService._get_emails_by_identifiers(
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=False,
reject_reason=approval.reject_reason or '',
applicant_name=applicant_name
)
send_approval_result_notify(
to_emails=emails,
request_no=approval.request_no,
is_passed=(action == 'approve'),
reject_reason=approval.reject_reason or ''
)
except Exception as e: except Exception as e:
logging.getLogger(__name__).warning(f"[Email] 发送审批结果通知邮件失败: {e}") import traceback
traceback.print_exc()
logger.warning(f"[Email] 发送审批结果通知邮件失败: {e}")
@staticmethod @staticmethod
def get_request_list(page=1, per_page=10, applicant_id=None, status=None): def get_request_list(page=1, per_page=10, applicant_id=None, status=None):

View File

@ -58,19 +58,24 @@ def send_email(to_email: Union[str, List[str]], subject: str, content: str):
""" """
cfg = _get_config() cfg = _get_config()
print(f"[DEBUG send_email] cfg = {cfg}")
# 发送总开关 # 发送总开关
if not cfg.get('enabled'): if not cfg.get('enabled'):
print(f"[Email] 邮件功能已禁用 (MAIL_ENABLED=false),跳过发送: {subject}")
logger.info(f"[Email] 邮件功能已禁用 (MAIL_ENABLED=false),跳过发送: {subject}") logger.info(f"[Email] 邮件功能已禁用 (MAIL_ENABLED=false),跳过发送: {subject}")
return return
# 配置完整性检查 # 配置完整性检查
if not cfg.get('server') or not cfg.get('username') or not cfg.get('password'): 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 缺失),跳过发送") logger.warning("[Email] 邮件配置不完整 (MAIL_SERVER/USERNAME/PASSWORD 缺失),跳过发送")
return return
# 标准化收件人列表 # 标准化收件人列表
recipients = [to_email] if isinstance(to_email, str) else [r.strip() for r in to_email if r.strip()] recipients = [to_email] if isinstance(to_email, str) else [r.strip() for r in to_email if r.strip()]
if not recipients: if not recipients:
print("[Email] 收件人地址为空,跳过发送")
logger.warning("[Email] 收件人地址为空,跳过发送") logger.warning("[Email] 收件人地址为空,跳过发送")
return 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['Subject'] = Header(subject, 'utf-8')
msg.attach(MIMEText(content, 'plain', 'utf-8')) msg.attach(MIMEText(content, 'plain', 'utf-8'))
print(f"DEBUG: 准备向服务器提交发信请求,收件人: {recipients} 发件人: {cfg['username']}")
if cfg.get('use_ssl'): if cfg.get('use_ssl'):
context = ssl.create_default_context() context = ssl.create_default_context()
with smtplib.SMTP_SSL(cfg['server'], cfg.get('port', 465), context=context) as server: 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}") logger.info(f"[Email] 发送成功 -> {recipients}: {subject}")
except smtplib.SMTPAuthenticationError: except smtplib.SMTPAuthenticationError:
print(f"!!! 邮件发送核心报错: SMTPAuthenticationError - 邮箱认证失败,请检查 MAIL_USERNAME / MAIL_PASSWORD授权码")
logger.error("[Email] 邮箱认证失败,请检查 MAIL_USERNAME / MAIL_PASSWORD授权码") logger.error("[Email] 邮箱认证失败,请检查 MAIL_USERNAME / MAIL_PASSWORD授权码")
except smtplib.SMTPRecipientsRefused as e: except smtplib.SMTPRecipientsRefused as e:
print(f"!!! 邮件发送核心报错: SMTPRecipientsRefused - 收件人被服务器拒绝: {e}")
logger.error(f"[Email] 收件人被服务器拒绝: {e}") logger.error(f"[Email] 收件人被服务器拒绝: {e}")
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
print(f"!!! 邮件发送核心报错: SMTPException - {e}")
logger.error(f"[Email] SMTP 异常: {e}") logger.error(f"[Email] SMTP 异常: {e}")
except Exception as e: except Exception as e:
import traceback
traceback.print_exc()
print(f"!!! 邮件发送核心报错: {type(e).__name__} - {e}")
logger.error(f"[Email] 发送邮件时发生未知异常: {e}") logger.error(f"[Email] 发送邮件时发生未知异常: {e}")
def send_new_request_notify(to_emails: List[str], request_no: str, 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: Args:
to_emails: 审批人邮箱列表 to_emails: 审批人邮箱列表
request_no: 审批单号 request_no: 审批单号
applicant_name: 申请人姓名 applicant_name: 申请人姓名
remark: 申请备注 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}" subject = f"【待审批】出库申请单 {request_no}"
content = f"""您好, content = f"""您好,
@ -125,6 +156,9 @@ def send_new_request_notify(to_emails: List[str], request_no: str,
申请人:{applicant_name or '未知'} 申请人:{applicant_name or '未知'}
备注说明:{remark 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, 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: Args:
to_emails: 收件人邮箱列表(库管 + 申请人) to_emails: 收件人邮箱列表
request_no: 审批单号 request_no: 审批单号
is_passed: 是否通过 is_passed: 是否通过(通过时发给库管,驳回时发给申请人)
reject_reason: 驳回原因(仅 is_passed=False 时使用) reject_reason: 驳回原因(仅 is_passed=False 时使用)
applicant_name: 申请人姓名(仅驳回通知时使用)
""" """
if is_passed: if is_passed:
subject = f"【已通过】出库申请单 {request_no}" # ★ 发给库管:提醒备货出库
subject = f"【待出库】出库申请单 {request_no} 已审批通过"
content = f"""您好, content = f"""您好,
出库申请单 {request_no} 已审批通过,请准备备货 出库申请单 {request_no} 已审批通过,请尽快备货出库
尽快安排出库操作 登录仓库管理系统处理该出库任务
此邮件由系统自动发送,请勿回复。 此邮件由系统自动发送,请勿回复。
""" """
else: else:
# ★ 发给申请人:告知被驳回
subject = f"【已驳回】出库申请单 {request_no}" subject = f"【已驳回】出库申请单 {request_no}"
content = f"""您好 content = f"""{"尊敬的 " + applicant_name + ",您好" if applicant_name else "您好"}
出库申请单 {request_no} 已被驳回。 出库申请单 {request_no} 已被审批驳回。
驳回原因:{reject_reason or '未填写'} 驳回原因:{reject_reason or '未填写'}
请登录仓库管理系统查看详情。 请登录仓库管理系统查看详情,并根据驳回原因调整后重新提交申请
此邮件由系统自动发送,请勿回复。 此邮件由系统自动发送,请勿回复。
""" """
send_email(to_emails, subject, content) 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}")

View File

@ -65,7 +65,7 @@ class Config:
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() in ('true', '1', 'yes') MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'false').lower() in ('true', '1', 'yes')
# 是否启用 SSL (465 端口通常需要,阿里邮箱必须启用 SSL) # 是否启用 SSL (465 端口通常需要,阿里邮箱必须启用 SSL)
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'true').lower() in ('true', '1', 'yes') MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'true').lower() in ('true', '1', 'yes')
# 默认发件人(显示名称 <邮箱地址>,阿里要求 MAIL_USERNAME 与此处邮箱地址完全一致 # 默认发件人(★ 必须与 MAIL_USERNAME 完全一致,否则阿里邮件服务器会拒绝
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', 'WMS系统 <wms@iris-rs.cn>') MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', 'wms@iris-rs.cn')
# 是否启用邮件发送功能(开发环境可设为 false 禁用) # 是否启用邮件发送功能(开发环境可设为 false 禁用)
MAIL_ENABLED = os.getenv('MAIL_ENABLED', 'true').lower() in ('true', '1', 'yes') MAIL_ENABLED = os.getenv('MAIL_ENABLED', 'true').lower() in ('true', '1', 'yes')