From 3dae20682846c8017fddbb7d02da947ee7ef4da1 Mon Sep 17 00:00:00 2001 From: DXC Date: Tue, 12 May 2026 13:42:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(outbound):=20=E5=AE=8C=E5=96=84=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E5=AE=A1=E6=89=B9=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E4=BA=BA=E4=B8=8E=E5=AE=A1=E6=89=B9=E4=BA=BA=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E6=94=B6=E5=88=B0=E9=82=AE=E4=BB=B6=EF=BC=88=E5=B8=A6=E7=89=A9?= =?UTF-8?q?=E6=96=99=E6=98=8E=E7=BB=86=EF=BC=89=EF=BC=8C=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=90=8E=E7=94=B3=E8=AF=B7=E4=BA=BA=E5=92=8C?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E5=9D=87=E6=94=B6=E5=88=B0=E5=B8=A6=E7=89=A9?= =?UTF-8?q?=E6=96=99=E6=98=8E=E7=BB=86=E7=9A=84=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory-backend/app/api/v1/auth.py | 8 +- .../app/services/outbound_service.py | 82 ++++++++++++------- inventory-backend/app/utils/email_service.py | 32 ++++++-- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/inventory-backend/app/api/v1/auth.py b/inventory-backend/app/api/v1/auth.py index 6feb10e..9100e30 100644 --- a/inventory-backend/app/api/v1/auth.py +++ b/inventory-backend/app/api/v1/auth.py @@ -326,11 +326,14 @@ def get_my_permissions(): def get_approvers(): """ 查询角色为 SUPER_ADMIN 或 SUPERVISOR 且状态为活跃的用户列表 - 返回: [{id, username, email, role}] + 返回: [{id, username, email, role, is_self}] + 其中 is_self=true 表示当前登录用户本人(用于前端标记) """ try: from app.models.system import SysUser + current_user_id = get_jwt_identity() + users = SysUser.query.filter( SysUser.role.in_(['SUPER_ADMIN', 'SUPERVISOR']), SysUser.status == 'active' @@ -343,7 +346,8 @@ def get_approvers(): 'id': u.id, 'username': u.username, 'email': u.email or '', - 'role': u.role + 'role': u.role, + 'is_self': (u.id == current_user_id) } for u in users ] }), 200 diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 2d3da1b..d5f518b 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -688,18 +688,28 @@ class OutboundApprovalService: @staticmethod def _notify_new_request(approval, applicant_id, approver_id=None): - """发送新申请通知邮件给审批人(静默处理,不阻断主流程)""" + """发送新申请通知邮件给审批人和申请人(静默处理,不阻断主流程)""" try: from flask import current_app from app.utils.email_service import send_new_request_notify + from app.models.system import SysUser - emails = [] + applicant_name = '' + applicant_emails = [] + # 1. 收集申请人信息 + if applicant_id: + user = SysUser.query.get(int(applicant_id)) + if user and user.email: + applicant_emails.append(user.email) + applicant_name = str(user.username).split('/')[0] if '/' in (user.username or '') else (user.username or str(applicant_id)) + + # 2. 收集审批人信息 + approver_emails = [] if approver_id: - # ★ 精准通知模式:直接查询指定审批人 user = SysUser.query.get(int(approver_id)) if user and user.email: - emails.append(user.email) + approver_emails.append(user.email) else: # 兜底:按角色查询 approvers = approval.get_allowed_approvers() @@ -707,37 +717,49 @@ class OutboundApprovalService: for a in approvers: if a.get('type') == 'role': role_codes.append(a.get('value', '')) - emails = OutboundApprovalService._get_emails_by_identifiers(role_codes=role_codes) + approver_emails = OutboundApprovalService._get_emails_by_identifiers(role_codes=role_codes) - if not emails: - current_app.logger.info(f"[Email] 审批单 {approval.request_no} 无审批人邮箱,跳过通知") + # 去重 + all_emails = list(set(applicant_emails + approver_emails)) + if not all_emails: + current_app.logger.info(f"[Email] 审批单 {approval.request_no} 无收件人邮箱,跳过通知") return - # 获取申请人姓名 - applicant_name = '' - if applicant_id: - u = SysUser.query.get(applicant_id) - if u: - # username 格式为 "姓名/账号",取姓名部分 - applicant_name = str(u.username).split('/')[0] if '/' in u.username else (u.username or str(applicant_id)) - - # ★ 发送通知,附完整物料清单 + # 3. 获取物料明细 items = approval.get_items() - send_new_request_notify( - to_emails=emails, - request_no=approval.request_no, - applicant_name=applicant_name, - remark=approval.remark or '', - items=items - ) + + # 4. 分别发送邮件 + if applicant_emails: + try: + send_new_request_notify( + to_emails=applicant_emails, + request_no=approval.request_no, + applicant_name=applicant_name, + remark=f"您的出库申请已提交,等待审批。{approval.remark or ''}", + items=items, + is_applicant_notify=True + ) + except Exception as e: + current_app.logger.error(f"[Email] 通知申请人失败: {e}") + + if approver_emails: + try: + send_new_request_notify( + to_emails=approver_emails, + request_no=approval.request_no, + applicant_name=applicant_name, + remark=approval.remark or '', + items=items, + is_applicant_notify=False + ) + except Exception as e: + current_app.logger.error(f"[Email] 通知审批人失败: {e}") except Exception as e: - # ★ 捕获所有异常,确保邮件发送失败不阻断主流程 try: from flask import current_app current_app.logger.error(f"[Email] 发送新申请通知邮件失败: {e}") except RuntimeError: - # 如果不在 Flask 应用上下文内,降级为标准日志 import logging logging.getLogger(__name__).error(f"[Email] 发送新申请通知邮件失败: {e}") @@ -864,17 +886,17 @@ class OutboundApprovalService: except Exception as e: logger.error(f"[Email] 通知库管失败: {e}") - # 3.2 通知申请人(已通过) + # 3.2 通知申请人(审批通过,带完整物料清单) if applicant_emails: try: - send_approval_result_notify( + send_warehouse_dispatch_notify( to_emails=applicant_emails, request_no=approval.request_no, - is_passed=True, - applicant_name=applicant_name + applicant_name=applicant_name, + items=items ) except Exception as e: - logger.error(f"[Email] 通知申请人通过失败: {e}") + logger.error(f"[Email] 通知申请人(通过)失败: {e}") elif action == 'reject': # 3.3 通知申请人(已驳回) diff --git a/inventory-backend/app/utils/email_service.py b/inventory-backend/app/utils/email_service.py index b333cf7..18bc9cc 100644 --- a/inventory-backend/app/utils/email_service.py +++ b/inventory-backend/app/utils/email_service.py @@ -120,9 +120,10 @@ def send_email(to_email: Union[str, List[str]], subject: str, content: str): def send_new_request_notify(to_emails: List[str], request_no: str, applicant_name: str = '', remark: str = '', - items: list = None): + items: list = None, is_applicant_notify: bool = False): """ 通知审批人有新的出库申请单待审批(可附带物料清单) + 或通知申请人其申请已提交(is_applicant_notify=True 时) Args: to_emails: 审批人邮箱列表 @@ -130,9 +131,9 @@ def send_new_request_notify(to_emails: List[str], request_no: str, applicant_name: 申请人姓名 remark: 申请备注 items: 物料明细列表(可选) + is_applicant_notify: True=通知申请人(标题:您的出库申请已提交),False=通知审批人(标题:您有一笔新的出库审批待处理) """ - 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}") + print(f"[DEBUG send_new_request_notify] 入参 items={items}, is_applicant_notify={is_applicant_notify}") # 拼装物料表格 rows = [] @@ -147,8 +148,29 @@ def send_new_request_notify(to_emails: List[str], request_no: str, else: rows.append("(无物料明细)") - subject = f"【待审批】出库申请单 {request_no}" - content = f"""您好, + if is_applicant_notify: + subject = f"【已提交】您的出库申请单 {request_no} 已提交" + content = f"""您好, + +您的出库申请单 {request_no} 已成功提交,等待审批。 + + 申请单号:{request_no} + 申请人:{applicant_name or '未知'} + 备注说明:{remark or '无'} + +物料清单如下: +{chr(10).join(rows)} + +--- +您可以点击下方链接查看申请状态: +https://172.16.0.198/outbound/selection +--- + +此邮件由系统自动发送,请勿回复。 +""" + else: + subject = f"【待审批】出库申请单 {request_no}" + content = f"""您好, 您有一笔新的出库审批申请待处理: