m0_70156489 2 päivää sitten
vanhempi
sitoutus
49f8a7ae26

+ 174 - 3
application/admin/controller/Procuremen.php

@@ -44,6 +44,9 @@ class Procuremen extends Backend
     /** @var array<int, true>|null 单次请求内下发隐藏工序 ID 缓存 */
     protected $pickHiddenScydgySetCache = null;
 
+    /** 邮件发送日志表结构已就绪缓存键 */
+    protected const EMAIL_SEND_LOG_SCHEMA_CACHE_KEY = 'purchase_email_send_log_schema_v1';
+
     /**
      * 采购子接口均走菜单规则鉴权;下列方法在方法内用 hasProcuremenPerm 做别名校验。
      * @var array
@@ -58,6 +61,9 @@ class Procuremen extends Backend
         $this->model = new \app\admin\model\Procuremen;
         $this->maybeMigrateLegacyWflowStatusText();
         $this->assignProcuremenAuthConfig();
+        if (!Cache::get(self::EMAIL_SEND_LOG_SCHEMA_CACHE_KEY)) {
+            $this->ensurePurchaseEmailSendLogTableOnce();
+        }
 
     }
 
@@ -5984,7 +5990,7 @@ class Procuremen extends Backend
      * @param array<int, array<string, mixed>> $mergeRows
      * @param array{ccydh:string, pos:array} $bundle
      */
-    protected function sendAuditSupplierNotifyChannels(array $chosen, array $mergeRows, array $bundle, bool $sendSms, bool $sendEmail): void
+    protected function sendAuditSupplierNotifyChannels(array $chosen, array $mergeRows, array $bundle, bool $sendSms, bool $sendEmail, string $emailLogScene = 'pick_issue'): void
     {
         if (!$sendSms && !$sendEmail) {
             return;
@@ -6080,6 +6086,14 @@ class Procuremen extends Backend
 
         $ctx = $this->buildIssueNotifyContext($mergeRows, $sysRqNotify);
         $notifyBundle = $this->composeSupplierNotifyBundle($chosen, $ctx, $detailLinks);
+        $notifyBundle['email_log_scene'] = $emailLogScene;
+        $notifyBundle['email_log_scydgy_id'] = (int)array_key_first($sids);
+        $poIdLog = 0;
+        $pos = $bundle['pos'] ?? [];
+        if (is_array($pos) && isset($pos[0]) && is_array($pos[0])) {
+            $poIdLog = (int)($pos[0]['id'] ?? 0);
+        }
+        $notifyBundle['email_log_purchase_order_id'] = $poIdLog;
 
         if ($sendEmail) {
             $mailTplRow = $this->loadNotifyTemplateRow('review_email');
@@ -6198,7 +6212,7 @@ class Procuremen extends Backend
                     'username'     => trim((string)($g['username'] ?? '')),
                 ];
                 try {
-                    $this->sendAuditSupplierNotifyChannels($chosen, $mergeRows, $bundle, $sendSms, $sendEmail);
+                    $this->sendAuditSupplierNotifyChannels($chosen, $mergeRows, $bundle, $sendSms, $sendEmail, 'audit_resend');
                 } catch (\Throwable $e) {
                     $notifyErrors[] = $cname . ':' . $e->getMessage();
                 }
@@ -6298,7 +6312,42 @@ class Procuremen extends Backend
         $mail->isHTML(true);
         $mail->Subject = $mailSubject;
         $mail->Body    = $mailBody;
-        $mail->send();
+        $fromEmail = trim((string)($mailConfig['addr'] ?? ''));
+        $sendOk = true;
+        $failReason = '';
+        try {
+            $mail->send();
+        } catch (\Throwable $e) {
+            $sendOk = false;
+            $failReason = $e->getMessage();
+            $this->persistEmailSendLog([
+                'from_email'          => $fromEmail,
+                'to_email'            => $toEmail,
+                'send_time'           => date('Y-m-d H:i:s'),
+                'scene'               => (string)($bundle['email_log_scene'] ?? 'pick_issue'),
+                'ccydh'               => trim((string)(($bundle['notify_vars']['ccydh'] ?? '') ?: ($bundle['ccydh'] ?? ''))),
+                'company_name'        => $companyName,
+                'mail_subject'        => $mailSubject,
+                'status'              => 0,
+                'fail_reason'         => $failReason,
+                'scydgy_id'           => (int)($bundle['email_log_scydgy_id'] ?? 0),
+                'purchase_order_id'   => (int)($bundle['email_log_purchase_order_id'] ?? 0),
+            ]);
+            throw $e;
+        }
+        $this->persistEmailSendLog([
+            'from_email'          => $fromEmail,
+            'to_email'            => $toEmail,
+            'send_time'           => date('Y-m-d H:i:s'),
+            'scene'               => (string)($bundle['email_log_scene'] ?? 'pick_issue'),
+            'ccydh'               => trim((string)(($bundle['notify_vars']['ccydh'] ?? '') ?: ($bundle['ccydh'] ?? ''))),
+            'company_name'        => $companyName,
+            'mail_subject'        => $mailSubject,
+            'status'              => 1,
+            'fail_reason'         => '',
+            'scydgy_id'           => (int)($bundle['email_log_scydgy_id'] ?? 0),
+            'purchase_order_id'   => (int)($bundle['email_log_purchase_order_id'] ?? 0),
+        ]);
     }
 
     /**
@@ -6336,6 +6385,128 @@ class Procuremen extends Backend
         Log::write('[协助下发-' . $type . '] ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'notice');
     }
 
+    protected function ensurePurchaseEmailSendLogTableOnce(): void
+    {
+        try {
+            Db::query('SELECT 1 FROM `purchase_email_send_log` LIMIT 1');
+            Cache::set(self::EMAIL_SEND_LOG_SCHEMA_CACHE_KEY, 1);
+            return;
+        } catch (\Throwable $e) {
+        }
+        $sqlFile = APP_PATH . 'extra' . DS . 'purchase_email_send_log_install.sql';
+        if (is_file($sqlFile)) {
+            $sql = file_get_contents($sqlFile);
+            if (is_string($sql) && $sql !== '') {
+                foreach (preg_split('/;\s*[\r\n]+/', $sql) as $stmt) {
+                    $stmt = trim($stmt);
+                    if ($stmt === '' || stripos($stmt, 'CREATE TABLE') === false) {
+                        continue;
+                    }
+                    try {
+                        Db::execute($stmt);
+                    } catch (\Throwable $ignore) {
+                    }
+                }
+            }
+        }
+        Cache::set(self::EMAIL_SEND_LOG_SCHEMA_CACHE_KEY, 1);
+    }
+
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected function persistEmailSendLog(array $row): void
+    {
+        try {
+            $this->ensurePurchaseEmailSendLogTableOnce();
+        } catch (\Throwable $e) {
+            return;
+        }
+        list($adminId, $adminName) = $this->GetUseName();
+        $cut = function ($s, $max) {
+            $s = (string)$s;
+            if (function_exists('mb_substr')) {
+                return mb_substr($s, 0, $max, 'UTF-8');
+            }
+
+            return strlen($s) <= $max ? $s : substr($s, 0, $max);
+        };
+        $data = [
+            'from_email'        => $cut($row['from_email'] ?? '', 128),
+            'to_email'          => $cut($row['to_email'] ?? '', 128),
+            'send_time'         => $row['send_time'] ?? date('Y-m-d H:i:s'),
+            'scene'             => $cut($row['scene'] ?? 'pick_issue', 32),
+            'ccydh'             => $cut($row['ccydh'] ?? '', 64),
+            'company_name'      => $cut($row['company_name'] ?? '', 128),
+            'mail_subject'      => $cut($row['mail_subject'] ?? '', 255),
+            'status'            => !empty($row['status']) ? 1 : 0,
+            'fail_reason'       => $cut($row['fail_reason'] ?? '', 500),
+            'admin_id'          => (int)$adminId,
+            'admin_name'        => $cut($adminName, 64),
+            'scydgy_id'         => (int)($row['scydgy_id'] ?? 0),
+            'purchase_order_id' => (int)($row['purchase_order_id'] ?? 0),
+            'createtime'        => date('Y-m-d H:i:s'),
+        ];
+        try {
+            Db::table('purchase_email_send_log')->insert($data);
+        } catch (\Throwable $e) {
+            Log::write('persistEmailSendLog: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    protected function formatEmailLogSceneText(string $scene): string
+    {
+        $map = [
+            'pick_issue'   => '初选下发',
+            'audit_resend' => '确认页重发',
+        ];
+
+        return $map[$scene] ?? $scene;
+    }
+
+    /**
+     * 邮件发送日志列表
+     */
+    public function emailLog()
+    {
+        $this->relationSearch = false;
+        $this->searchFields = 'from_email,to_email,ccydh,company_name,admin_name,mail_subject';
+        $this->request->filter(['strip_tags', 'trim']);
+        $bakModel = $this->model;
+        $this->model = new \app\admin\model\Purchaseemailsendlog;
+
+        if ($this->request->isAjax()) {
+            $this->ensurePurchaseEmailSendLogTableOnce();
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? $sort : 'id';
+            $orderDir = strtoupper((string)$order) === 'ASC' ? 'ASC' : 'DESC';
+
+            $list = $this->model
+                ->where($where)
+                ->order($sortField, $orderDir)
+                ->paginate($limit);
+
+            $rows = [];
+            foreach ($list->items() as $item) {
+                $row = is_array($item) ? $item : $item->toArray();
+                $scene = trim((string)($row['scene'] ?? ''));
+                $row['scene_text'] = $this->formatEmailLogSceneText($scene);
+                $row['status_text'] = !empty($row['status']) ? '成功' : '失败';
+                $row['to_email_masked'] = mask_email((string)($row['to_email'] ?? ''));
+                $row['from_email_masked'] = mask_email((string)($row['from_email'] ?? ''));
+                $rows[] = $row;
+            }
+
+            $this->model = $bakModel;
+
+            return json(['total' => $list->total(), 'rows' => $rows]);
+        }
+
+        $this->model = $bakModel;
+
+        return $this->view->fetch('procuremen/emaillog');
+    }
+
     /**
      * 协助下发弹窗(选多家供应商并通知)
      */

+ 16 - 0
application/admin/model/Purchaseemailsendlog.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Purchaseemailsendlog extends Model
+{
+    protected $table = 'purchase_email_send_log';
+
+    protected $autoWriteTimestamp = false;
+
+    protected $createTime = false;
+    protected $updateTime = false;
+    protected $deleteTime = false;
+}

+ 18 - 15
application/admin/view/index/login.html

@@ -21,7 +21,7 @@
             max-height: 100vh;
             overflow: hidden;
             background-color: #020b1e;
-            background-image: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login1.png');
+            background-image: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login1.png?v={$site.version|default='1.0.1'|htmlentities}');
             background-repeat: no-repeat;
             background-position: center center;
             background-size: 100% 100%;
@@ -42,7 +42,7 @@
             margin: 0 auto;
             width: min(460px, 58vw);
             height: 64px;
-            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login4.png') no-repeat center center;
+            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login4.png?v={$site.version|default='1.0.1'|htmlentities}') no-repeat center center;
             background-size: contain;
             pointer-events: none;
             user-select: none;
@@ -70,25 +70,28 @@
             display: flex;
             align-items: center;
             justify-content: flex-start;
-            overflow: visible;
+            overflow: hidden;
         }
 
+        /* 左图:login2 画布留白多,用 cover 放大裁切;login3 可改 login-visual-art--fit-contain */
         .login-visual-art {
             display: block;
             flex-shrink: 0;
             height: calc(100vh - 105px);
             max-height: calc(100vh - 105px);
-            aspect-ratio: 16 / 9;
-            width: auto;
-            max-width: min(calc((100vh - 105px) * 16 / 9), calc(100vw - 500px));
+            width: min(calc((100vh - 105px) * 16 / 9), calc(100vw - 500px));
             min-width: 360px;
             min-height: 260px;
-            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login3.png') no-repeat center center;
-            background-size: contain;
+            object-fit: cover;
+            object-position: center center;
             pointer-events: none;
             user-select: none;
         }
 
+        .login-visual-art.login-visual-art--fit-contain {
+            object-fit: contain;
+        }
+
         .login-panel {
             flex: 0 0 auto;
             width: 100%;
@@ -111,7 +114,7 @@
             width: 86%;
             height: 32px;
             margin: 0 auto;
-            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login5.png') no-repeat center center;
+            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login5.png?v={$site.version|default='1.0.1'|htmlentities}') no-repeat center center;
             background-size: contain;
             pointer-events: none;
             user-select: none;
@@ -125,7 +128,7 @@
             width: 100%;
             height: 15px;
             margin: 0;
-            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login6.png') no-repeat center 54%;
+            background: url('https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login6.png?v={$site.version|default='1.0.1'|htmlentities}') no-repeat center 54%;
             background-size: 100% auto;
             pointer-events: none;
             user-select: none;
@@ -295,14 +298,14 @@
 
             .login-visual-art {
                 width: 100%;
-                max-width: 720px;
+                max-width: 900px;
                 height: auto;
                 aspect-ratio: 16 / 9;
                 min-height: 280px;
                 max-height: none;
-                margin-left: 0;
-                background-position: center center;
-                background-size: contain;
+                margin: 0 auto;
+                object-fit: cover;
+                object-position: center center;
             }
 
             .login-panel {
@@ -352,7 +355,7 @@
 
 <main class="login-main">
     <div class="login-visual">
-        <div class="login-visual-art" role="img" aria-label=""></div>
+        <img class="login-visual-art" src="https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/login2.png?v={$site.version|default='1.0.1'|htmlentities}" alt=""/>
     </div>
 
     <div class="login-panel">

+ 16 - 0
application/admin/view/procuremen/emaillog.html

@@ -0,0 +1,16 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body procuremen-email-log-page">
+        <div class="widget-body no-padding">
+            <div id="toolbar" class="toolbar">
+                <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}"><i class="fa fa-refresh"></i></a>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                   data-operate-edit="false"
+                   data-operate-del="false"
+                   width="100%">
+            </table>
+        </div>
+    </div>
+</div>

+ 24 - 0
application/extra/purchase_email_send_log_install.sql

@@ -0,0 +1,24 @@
+-- 协助采购 — 邮件发送日志(执行一次;也可由后台首次访问自动建表)
+
+CREATE TABLE IF NOT EXISTS `purchase_email_send_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `from_email` varchar(128) NOT NULL DEFAULT '' COMMENT '发件邮箱',
+  `to_email` varchar(128) NOT NULL DEFAULT '' COMMENT '收件邮箱',
+  `send_time` datetime DEFAULT NULL COMMENT '发送时间',
+  `scene` varchar(32) NOT NULL DEFAULT '' COMMENT '场景:pick_issue初选下发 audit_resend确认页重发',
+  `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
+  `company_name` varchar(128) NOT NULL DEFAULT '' COMMENT '供应商名称',
+  `mail_subject` varchar(255) NOT NULL DEFAULT '' COMMENT '邮件主题',
+  `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1成功 0失败',
+  `fail_reason` varchar(500) NOT NULL DEFAULT '' COMMENT '失败原因',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '操作人ID',
+  `admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人',
+  `scydgy_id` int(11) NOT NULL DEFAULT 0 COMMENT '工序行ID',
+  `purchase_order_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'purchase_order.id',
+  `createtime` datetime DEFAULT NULL COMMENT '记录创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_send_time` (`send_time`),
+  KEY `idx_to_email` (`to_email`),
+  KEY `idx_ccydh` (`ccydh`),
+  KEY `idx_scene` (`scene`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='协助采购邮件发送日志';

+ 1 - 1
application/extra/site.php

@@ -5,7 +5,7 @@ return array (
   'brand_logo' => 'https://a-7in6-com.oss-cn-hangzhou.aliyuncs.com/xinhua/img/logo1.png',
   'beian' => '',
   'cdnurl' => '',
-  'version' => '1.0.1',
+  'version' => '1.0.4',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'languages' => 

+ 48 - 0
public/assets/js/backend/procuremen.js

@@ -3316,6 +3316,54 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
         },
 
+        emaillog: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'procuremen/emaillog' + location.search,
+                    table: 'purchase_email_send_log',
+                }
+            });
+
+            var table = $("#table");
+            var colCenter = {align: 'center', valign: 'middle'};
+
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                sortOrder: 'desc',
+                commonSearch: true,
+                columns: [
+                    [
+                        $.extend({field: 'id', title: __('Id'), width: 60, operate: false}, colCenter),
+                        $.extend({field: 'ccydh', title: '订单号', operate: 'LIKE'}, colCenter),
+                        $.extend({field: 'send_time', title: '发送时间', operate: 'RANGE', addclass: 'datetimerange', sortable: true}, colCenter),
+                        $.extend({field: 'from_email', title: '发件邮箱', operate: 'LIKE', formatter: function (v, row) {
+                            return row.from_email_masked || v || '';
+                        }}, colCenter),
+                        $.extend({field: 'to_email', title: '供应商邮箱', operate: 'LIKE', formatter: function (v, row) {
+                            return row.to_email_masked || v || '';
+                        }}, colCenter),
+                        $.extend({field: 'company_name', title: '供应商', operate: 'LIKE'}, colCenter),
+                        $.extend({field: 'scene', title: '场景', operate: '=', searchList: {'pick_issue': '初选下发', 'audit_resend': '确认页重发'}, formatter: function (v, row) {
+                            return row.scene_text || v || '';
+                        }}, colCenter),
+                        $.extend({field: 'mail_subject', title: '邮件主题', operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content}, colCenter),
+                        $.extend({field: 'status', title: '状态', operate: '=', searchList: {'1': '成功', '0': '失败'}, formatter: function (v, row) {
+                            var t = row.status_text || (String(v) === '1' ? '成功' : '失败');
+                            return String(v) === '1' || v === 1
+                                ? '<span class="text-success">' + t + '</span>'
+                                : '<span class="text-danger">' + t + '</span>';
+                        }}, colCenter),
+                        $.extend({field: 'admin_name', title: '操作人', operate: 'LIKE'}, colCenter),
+                        $.extend({field: 'fail_reason', title: '失败原因', operate: false, class: 'autocontent', formatter: Table.api.formatter.content}, colCenter)
+                    ]
+                ]
+            });
+
+            Table.api.bindevent(table);
+        },
+
         api: {
             bindevent: function () {
                 Form.api.bindevent($('form[role=form]'));