m0_70156489 hai 1 semana
pai
achega
42b9741c84

+ 166 - 60
application/admin/controller/Procuremen.php

@@ -4099,6 +4099,9 @@ class Procuremen extends Backend
             $r['is_void'] = is_array($r) && $this->isPurchaseOrderDetailVoid($r);
             $r['is_picked'] = is_array($r) && ProcuremenStatus::isPodPicked($r['status'] ?? '');
             $r['oper_time_text'] = $this->resolveDetailSupplierOperTime($r);
+            if (is_array($r)) {
+                $r = $this->maskSupplierContactFields($r);
+            }
         }
         unset($r);
 
@@ -4142,7 +4145,7 @@ class Procuremen extends Backend
         $this->view->assign('processDisplayRows', $quoteView['process_rows']);
         $this->view->assign('processCount', count($mergeRows));
         $this->view->assign('detailRows', $details);
-        $this->view->assign('supplierQuoteGroups', $quoteView['supplier_groups']);
+        $this->view->assign('supplierQuoteGroups', $this->maskSupplierContactList($quoteView['supplier_groups']));
         $this->view->assign('orderQuoteTotalText', $quoteView['order_total_text']);
         $this->view->assign('orderQuoteTotalHas', $quoteView['order_total_has']);
         $this->view->assign('selectedQuoteCompany', $quoteView['selected_company']);
@@ -4289,6 +4292,148 @@ class Procuremen extends Backend
         return $out;
     }
 
+    /**
+     * 供应商联系信息脱敏(仅用于页面展示 / 只读接口响应)
+     */
+    protected function maskSupplierContactFields(array $item): array
+    {
+        if (array_key_exists('email', $item)) {
+            $item['email'] = mask_email((string)$item['email']);
+        }
+        if (array_key_exists('phone', $item)) {
+            $item['phone'] = mask_phone((string)$item['phone']);
+        }
+
+        return $item;
+    }
+
+    /**
+     * @param array<int, array<string, mixed>> $list
+     * @return array<int, array<string, mixed>>
+     */
+    protected function maskSupplierContactList(array $list): array
+    {
+        $out = [];
+        foreach ($list as $item) {
+            $out[] = is_array($item) ? $this->maskSupplierContactFields($item) : $item;
+        }
+
+        return $out;
+    }
+
+    /**
+     * 从 customer 表行构建下发用供应商联系信息(含明文邮箱/手机,仅服务端使用)
+     *
+     * @return array<string, string>|null
+     */
+    protected function buildCustomerContactPayloadFromRow($row): ?array
+    {
+        if (!is_array($row)) {
+            return null;
+        }
+        $norm = [];
+        foreach ($row as $k => $v) {
+            $norm[is_string($k) ? strtolower($k) : $k] = $v;
+        }
+        $row = $norm;
+
+        $st = $row['status'] ?? '';
+        if ($st !== '' && $st !== null && $st !== 1 && $st !== '1') {
+            return null;
+        }
+
+        $companyName = '';
+        foreach (['company_name', 'name'] as $nk) {
+            if (!empty($row[$nk])) {
+                $companyName = trim((string)$row[$nk]);
+                break;
+            }
+        }
+        if ($companyName === '') {
+            return null;
+        }
+
+        $username = '';
+        foreach (['username', 'contact', 'linkman', 'contacts'] as $uk) {
+            if (isset($row[$uk]) && trim((string)$row[$uk]) !== '') {
+                $username = trim((string)$row[$uk]);
+                break;
+            }
+        }
+
+        $email = isset($row['email']) ? trim((string)$row['email']) : '';
+        $phone = isset($row['phone']) ? trim((string)$row['phone']) : '';
+        if ($phone === '' && isset($row['account'])) {
+            $phone = trim((string)$row['account']);
+        }
+        if ($phone === '' && isset($row['mobile'])) {
+            $phone = trim((string)$row['mobile']);
+        }
+        if ($email === '' && $phone === '') {
+            return null;
+        }
+
+        $category = '';
+        foreach (['company_type', 'category', 'type_name'] as $ck) {
+            if (isset($row[$ck]) && trim((string)$row[$ck]) !== '') {
+                $category = trim((string)$row[$ck]);
+                break;
+            }
+        }
+
+        return [
+            'id'           => isset($row['id']) ? (string)$row['id'] : '',
+            'name'         => $companyName,
+            'company_name' => $companyName,
+            'username'     => $username,
+            'email'        => $email,
+            'phone'        => $phone,
+            'category'     => $category,
+            'company_type' => $category,
+        ];
+    }
+
+    /**
+     * 按 customer.id 解析供应商联系信息(服务端下发/通知用)
+     */
+    protected function resolveCustomerContactById(string $id): ?array
+    {
+        $id = trim($id);
+        if ($id === '' || !ctype_digit($id)) {
+            return null;
+        }
+        try {
+            $row = Db::table('customer')->where('id', (int)$id)->find();
+        } catch (\Throwable $e) {
+            return null;
+        }
+
+        return $this->buildCustomerContactPayloadFromRow($row);
+    }
+
+    /**
+     * 按供应商名称解析联系信息(兼容旧提交数据)
+     */
+    protected function resolveCustomerContactByCompanyName(string $name): ?array
+    {
+        $name = trim($name);
+        if ($name === '') {
+            return null;
+        }
+        try {
+            $row = Db::table('customer')
+                ->where(function ($q) use ($name) {
+                    $q->where('company_name', $name)->whereOr('name', $name);
+                })
+                ->order('id', 'desc')
+                ->find();
+        } catch (\Throwable $e) {
+            return null;
+        }
+
+        return $this->buildCustomerContactPayloadFromRow($row);
+    }
+
     /**
      * 规范化下发/确认时勾选的供应商
      *
@@ -4302,18 +4447,19 @@ class Procuremen extends Backend
             if (!is_array($c)) {
                 continue;
             }
+            $resolved = null;
+            $cid = trim((string)($c['id'] ?? ''));
+            if ($cid !== '') {
+                $resolved = $this->resolveCustomerContactById($cid);
+            }
             $name = trim((string)($c['name'] ?? $c['company_name'] ?? ''));
-            if ($name === '') {
+            if ($resolved === null && $name !== '') {
+                $resolved = $this->resolveCustomerContactByCompanyName($name);
+            }
+            if ($resolved === null) {
                 continue;
             }
-            $out[] = [
-                'name'         => $name,
-                'company_name' => $name,
-                'username'     => trim((string)($c['username'] ?? '')),
-                'email'        => trim((string)($c['email'] ?? '')),
-                'phone'        => trim((string)($c['phone'] ?? '')),
-                'company_type' => trim((string)($c['company_type'] ?? $c['category'] ?? '')),
-            ];
+            $out[] = $resolved;
         }
 
         return $out;
@@ -6360,8 +6506,8 @@ class Procuremen extends Backend
         $this->view->assign('processRows', $mergeRows);
         $this->view->assign('processDisplayRows', $this->buildAuditProcessDisplayRows($mergeRows));
         $this->view->assign('processCount', count($mergeRows));
-        $this->view->assign('quoteGroups', $quoteGroups);
-        $this->view->assign('quoteGroupsJson', json_encode($quoteGroups, JSON_UNESCAPED_UNICODE));
+        $this->view->assign('quoteGroups', $this->maskSupplierContactList($quoteGroups));
+        $this->view->assign('quoteGroupsJson', json_encode($this->maskSupplierContactList($quoteGroups), JSON_UNESCAPED_UNICODE));
         $this->view->assign('pickedCompanyName', $pickedName);
         $this->view->assign('sysRq', $this->formatProcuremenSysRqForInput($po['sys_rq'] ?? null));
         $this->view->assign('quoteDeadlineReached', $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0);
@@ -6925,45 +7071,11 @@ class Procuremen extends Backend
                 continue;
             }
 
-            $id = isset($row['id']) ? (string)$row['id'] : '';
-
-            $companyName = '';
-            foreach (['company_name', 'name'] as $nk) {
-                if (!empty($row[$nk])) {
-                    $companyName = trim((string)$row[$nk]);
-                    break;
-                }
-            }
-
-            $username = '';
-            foreach (['username', 'contact', 'linkman', 'contacts'] as $uk) {
-                if (isset($row[$uk]) && trim((string)$row[$uk]) !== '') {
-                    $username = trim((string)$row[$uk]);
-                    break;
-                }
-            }
-
-            $email = isset($row['email']) ? trim((string)$row['email']) : '';
-            $phone = isset($row['phone']) ? trim((string)$row['phone']) : '';
-            if ($phone === '' && isset($row['account'])) {
-                $phone = trim((string)$row['account']);
-            }
-            if ($phone === '' && isset($row['mobile'])) {
-                $phone = trim((string)$row['mobile']);
-            }
-            /* 邮箱、手机号均为空则无法发短信/邮件,不在下发弹窗展示 */
-            if ($email === '' && $phone === '') {
+            $payload = $this->buildCustomerContactPayloadFromRow($row);
+            if ($payload === null) {
                 continue;
             }
 
-            $category = '';
-            foreach (['company_type', 'category', 'type_name'] as $ck) {
-                if (isset($row[$ck]) && trim((string)$row[$ck]) !== '') {
-                    $category = trim((string)$row[$ck]);
-                    break;
-                }
-            }
-
             $detail = '';
             foreach ($detailColCandidates as $dk) {
                 if (!isset($row[$dk])) {
@@ -6976,17 +7088,8 @@ class Procuremen extends Backend
                 }
             }
 
-            $list[] = [
-                'id'             => $id,
-                'name'           => $companyName,
-                'company_name'   => $companyName,
-                'username'       => $username,
-                'email'          => $email,
-                'phone'          => $phone,
-                'category'       => $category,
-                'company_type'   => $category,
-                'detail'         => $detail,
-            ];
+            $payload['detail'] = $detail;
+            $list[] = $this->maskSupplierContactFields($payload);
         }
 
         $this->success('', '', $list);
@@ -7099,6 +7202,9 @@ class Procuremen extends Backend
             } else {
                 $r['createtime_text'] = '';
             }
+            if (is_array($r)) {
+                $r = $this->maskSupplierContactFields($r);
+            }
         }
         unset($r);
 
@@ -7126,7 +7232,7 @@ class Procuremen extends Backend
         $this->view->assign('confirmProcessCount', $confirmProcessCount);
         $this->view->assign('orderCcydh', $confirmBundle['ccydh'] ?? '');
         $this->view->assign('processDisplayRows', $processDisplayRows);
-        $this->view->assign('confirmPickGroups', $confirmPickGroups);
+        $this->view->assign('confirmPickGroups', $this->maskSupplierContactList($confirmPickGroups));
         $this->view->assign('confirmPickCount', count($confirmPickGroups));
         $this->view->assign('pickedCompanyName', $pickedCompanyName);
         $sysRqDisplay = '—';

+ 2 - 2
application/admin/view/procuremen/audit_issue.html

@@ -243,8 +243,8 @@
                 </td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.name|default=''|htmlentities}</td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.username|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.email|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.phone|default=''|htmlentities}</td>
+                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($co['email']) ? $co['email'] : ''))}</td>
+                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($co['phone']) ? $co['phone'] : ''))}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ln.amount_quote_pending)"} audit-quote-pending{elseif condition="$ln.amount_filled neq 1"} audit-quote-empty{/if}">{$ln.unit_price_text|default='未填写'|htmlentities}</td>

+ 4 - 4
application/admin/view/procuremen/details_fragment.html

@@ -402,8 +402,8 @@
                     {$sg.company_name|default=''|htmlentities}
                 </td>
                 <td{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.username|default=''|htmlentities}</td>
-                <td{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.email|default=''|htmlentities}</td>
-                <td{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.phone|default=''|htmlentities}</td>
+                <td{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($sg['email']) ? $sg['email'] : ''))}</td>
+                <td{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($sg['phone']) ? $sg['phone'] : ''))}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>
                 <td class="text-center">{$ln.unit_price_text|default=''|htmlentities}</td>
@@ -435,8 +435,8 @@
             <tr>
                 <td>{$dr.company_name|default=''|htmlentities}</td>
                 <td>{$dr.username|default=''|htmlentities}</td>
-                <td>{$dr.email|default=''|htmlentities}</td>
-                <td>{$dr.phone|default=''|htmlentities}</td>
+                <td>{:htmlentities(mask_email(isset($dr['email']) ? $dr['email'] : ''))}</td>
+                <td>{:htmlentities(mask_phone(isset($dr['phone']) ? $dr['phone'] : ''))}</td>
                 <td></td>
                 <td class="text-center">{$dr.amount|default=''|htmlentities}</td>
                 <td class="text-center">{$dr.delivery_ymd|default=''|htmlentities}</td>

+ 4 - 4
application/admin/view/procuremen/outward_detail.html

@@ -270,8 +270,8 @@
                 </td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.name|default=''|htmlentities}</td>
                 <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.username|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.email|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.phone|default=''|htmlentities}</td>
+                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($co['email']) ? $co['email'] : ''))}</td>
+                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($co['phone']) ? $co['phone'] : ''))}</td>
                 {/if}
                 <td>{$ql.cgymc|default=''|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ql.amount_quote_pending)"} outward-quote-empty{elseif condition="$ql.amount_filled neq 1"} outward-quote-empty{/if}">{$ql.unit_price_text|default='未填写'|htmlentities}</td>
@@ -345,8 +345,8 @@
                 <td class="col-amount">{$r.amount|default=''}</td>
                 <td class="col-delivery">{$r.delivery|default=''}</td>
                 <td>{$r.company_name|default=''}</td>
-                <td>{$r.email|default=''}</td>
-                <td>{$r.phone|default=''}</td>
+                <td>{:htmlentities(mask_email(isset($r['email']) ? $r['email'] : ''))}</td>
+                <td>{:htmlentities(mask_phone(isset($r['phone']) ? $r['phone'] : ''))}</td>
                 <td class="col-time">{$r.createtime_text|default=''}</td>
             </tr>
             {/volist}

+ 51 - 0
application/common.php

@@ -29,6 +29,57 @@ if (!function_exists('__')) {
     }
 }
 
+if (!function_exists('mask_email')) {
+
+    /**
+     * 邮箱脱敏:保留本地部分前三位,其余用 * 代替,域名完整保留
+     * 例:14906570@qq.com → 149***@qq.com
+     */
+    function mask_email($email)
+    {
+        $email = trim((string)$email);
+        if ($email === '') {
+            return '';
+        }
+        $at = strpos($email, '@');
+        if ($at === false) {
+            if (mb_strlen($email, 'UTF-8') <= 3) {
+                return $email;
+            }
+            return mb_substr($email, 0, 3, 'UTF-8') . '***';
+        }
+        $local = mb_substr($email, 0, $at, 'UTF-8');
+        $domain = mb_substr($email, $at + 1, null, 'UTF-8');
+        if (mb_strlen($local, 'UTF-8') <= 3) {
+            return $local . '@' . $domain;
+        }
+        return mb_substr($local, 0, 3, 'UTF-8') . '***@' . $domain;
+    }
+}
+
+if (!function_exists('mask_phone')) {
+
+    /**
+     * 手机号脱敏:前三位 + 星号 + 后四位
+     * 例:18981046362 → 189****6362
+     */
+    function mask_phone($phone)
+    {
+        $phone = preg_replace('/\s+/', '', trim((string)$phone));
+        if ($phone === '') {
+            return '';
+        }
+        $len = strlen($phone);
+        if ($len <= 7) {
+            if ($len <= 3) {
+                return $phone;
+            }
+            return substr($phone, 0, 3) . str_repeat('*', $len - 3);
+        }
+        return substr($phone, 0, 3) . str_repeat('*', $len - 7) . substr($phone, -4);
+    }
+}
+
 if (!function_exists('format_bytes')) {
 
     /**

+ 61 - 7
public/assets/js/backend/procuremen.js

@@ -1,5 +1,59 @@
 define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
 
+    function maskProcuremenEmail(email) {
+        email = (email == null ? '' : String(email)).trim();
+        if (!email) {
+            return '';
+        }
+        var at = email.indexOf('@');
+        if (at < 0) {
+            return email.length <= 3 ? email : email.slice(0, 3) + '***';
+        }
+        var local = email.slice(0, at);
+        var domain = email.slice(at + 1);
+        if (local.length <= 3) {
+            return local + '@' + domain;
+        }
+        return local.slice(0, 3) + '***@' + domain;
+    }
+
+    function maskProcuremenPhone(phone) {
+        phone = (phone == null ? '' : String(phone)).replace(/\s+/g, '').trim();
+        if (!phone) {
+            return '';
+        }
+        var len = phone.length;
+        if (len <= 7) {
+            return len <= 3 ? phone : phone.slice(0, 3) + new Array(len - 2).join('*');
+        }
+        return phone.slice(0, 3) + new Array(len - 6).join('*') + phone.slice(-4);
+    }
+
+    function procuremenReviewCompanyKey(c) {
+        if (!c || typeof c !== 'object') {
+            return '';
+        }
+        var id = String(c.id || '').trim();
+        if (id) {
+            return 'id:' + id;
+        }
+        return [String(c.name || c.company_name || ''), String(c.username || '')].join('\x01');
+    }
+
+    function procuremenReviewCompanyPayload(c) {
+        if (!c || typeof c !== 'object') {
+            return {};
+        }
+        return {
+            id: c.id || '',
+            name: c.name || c.company_name || '',
+            company_name: c.company_name || c.name || '',
+            username: c.username || '',
+            company_type: c.company_type || c.category || '',
+            category: c.category || c.company_type || ''
+        };
+    }
+
     function procuremenLayerTop() {
         try {
             if (typeof top !== 'undefined' && top.Layer) {
@@ -2097,14 +2151,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     if (!c || typeof c !== 'object') {
                         return;
                     }
-                    var k = [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                    var k = procuremenReviewCompanyKey(c);
                     if (k && seen[k]) {
                         return;
                     }
                     if (k) {
                         seen[k] = true;
                     }
-                    selected.push(c);
+                    selected.push(procuremenReviewCompanyPayload(c));
                 });
                 return selected;
             }
@@ -2419,7 +2473,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 delete reviewSelectedMap[k];
                 $('#review-company-tbody .review-company-cb').each(function () {
                     var c = $(this).data('company');
-                    var kk = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                    var kk = procuremenReviewCompanyKey(c);
                     if (kk === k) {
                         $(this).prop('checked', false);
                     }
@@ -2514,7 +2568,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 $.each(list, function (i, c) {
                     var $cb = $('<input type="checkbox" class="review-company-cb"/>');
                     $cb.data('company', c);
-                    var k = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                    var k = procuremenReviewCompanyKey(c);
                     if (k && reviewSelectedMap[k]) {
                         $cb.prop('checked', true);
                     }
@@ -2599,7 +2653,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         $.each(list, function (i, c) {
                             var $cb = $('<input type="checkbox" class="review-company-cb"/>');
                             $cb.data('company', c);
-                            var k2 = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                            var k2 = procuremenReviewCompanyKey(c);
                             if (k2 && reviewSelectedMap[k2]) {
                                 $cb.prop('checked', true);
                             }
@@ -2628,7 +2682,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             $('#review-company-tbody').off('change.reviewCb').on('change.reviewCb', '.review-company-cb', function () {
                 var c = $(this).data('company');
-                var k = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                var k = procuremenReviewCompanyKey(c);
                 var $cbsC = $('#review-company-tbody .review-company-cb');
                 var $masterC = $('#review-check-all');
                 if (!k) {
@@ -2782,7 +2836,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         $.each(list, function (i, c) {
                             var $cb = $('<input type="checkbox" class="review-company-cb"/>');
                             $cb.data('company', c);
-                            var k3 = (!c || typeof c !== 'object') ? '' : [String(c.email || ''), String(c.phone || ''), String(c.name || c.company_name || '')].join('\x01');
+                            var k3 = procuremenReviewCompanyKey(c);
                             if (k3 && reviewSelectedMap[k3]) {
                                 $cb.prop('checked', true);
                             }