m0_70156489 8 часов назад
Родитель
Сommit
6c4bc9ac8d

+ 2 - 2
application/admin/controller/Customer.php

@@ -166,7 +166,7 @@ class Customer extends Backend
         $scoreRaw = trim((string)($params['score'] ?? ''));
         $score = $scoreRaw === '' ? null : $scoreRaw;
         if ($score !== null && !preg_match('/^-?\d+(\.\d+)?$/', (string)$score)) {
-            $this->error('考核评分值须为数字');
+            $this->error('上年度质量评分须为数字');
         }
 
         $now = date('Y-m-d H:i:s');
@@ -263,7 +263,7 @@ class Customer extends Backend
         $scoreRaw = trim((string)($params['score'] ?? ''));
         $score = $scoreRaw === '' ? null : $scoreRaw;
         if ($score !== null && !preg_match('/^-?\d+(\.\d+)?$/', (string)$score)) {
-            $this->error('考核评分值须为数字');
+            $this->error('上年度质量评分须为数字');
         }
 
         $data = [

+ 21 - 2
application/admin/controller/Procuremen.php

@@ -7,6 +7,7 @@ use app\common\library\AliyunOss;
 use app\common\library\ProcuremenGuard;
 use app\common\library\ProcuremenOperLog;
 use app\common\library\ProcuremenStatus;
+use app\common\library\ProcuremenSupplierScore;
 use app\common\library\ProcuremenTime;
 use PHPMailer\PHPMailer\PHPMailer;
 use think\Config;
@@ -2482,6 +2483,14 @@ class Procuremen extends Backend
                 $this->addOrderLog($anchor, 'bid_open_verify', $logMsg, null);
             }
         }
+
+        // 开标通过后计算并写入供应商服务评分
+        try {
+            $quoteGroups = $this->loadAuditSupplierQuoteGroups($bundle);
+            ProcuremenSupplierScore::saveForOrder($ccydh, $quoteGroups, date('Y-m', strtotime($now) ?: time()));
+        } catch (\Throwable $e) {
+            Log::write('bid open supplier score: ' . $e->getMessage(), 'error');
+        }
     }
 
     /**
@@ -2729,7 +2738,10 @@ class Procuremen extends Backend
             $byCompany[$cn]['display_rowspan'] = $total > 0 ? ($total + 2) : 0;
         }
 
-        return array_values($byCompany);
+        $groups = array_values($byCompany);
+        $ccydh = trim((string)($bundle['ccydh'] ?? ''));
+
+        return ProcuremenSupplierScore::attachToQuoteGroups($groups, $ccydh, (bool)$quoteVisible);
     }
 
     /**
@@ -2960,7 +2972,9 @@ class Procuremen extends Backend
             $out[] = $g;
         }
 
-        return $out;
+        $ccydh = trim((string)($bundle['ccydh'] ?? ''));
+
+        return ProcuremenSupplierScore::attachToQuoteGroups($out, $ccydh, (bool)$quoteVisible);
     }
 
     /**
@@ -4859,6 +4873,7 @@ class Procuremen extends Backend
             || ProcuremenStatus::isWflowApproved($main['wflow_status'] ?? '');
         $this->view->assign('showSupplierBidResult', $showSupplierBidResult ? 1 : 0);
         $this->view->assign('operLogs', $operLogs);
+        $this->view->assign('supplierScoreRuleText', ProcuremenSupplierScore::formatRuleFormulaText());
 
         return [
             'ok'    => $main !== [] || $details !== [],
@@ -5801,6 +5816,7 @@ class Procuremen extends Backend
 
             return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
         });
+        $supplierGroups = ProcuremenSupplierScore::attachToQuoteGroups($supplierGroups, $ccydh, (bool)$quoteVisible);
 
         return [
             'process_rows'     => $processRows,
@@ -7537,6 +7553,7 @@ class Procuremen extends Backend
         }
         $this->view->assign('deliveryDeadline', $deliveryDeadlineForView);
         $this->view->assign('bidOpenVerified', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
+        $this->view->assign('supplierScoreRuleText', ProcuremenSupplierScore::formatRuleFormulaText());
         $quoteDeadlineReached = $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0;
         $this->view->assign('quoteDeadlineReached', $quoteDeadlineReached);
         $this->view->assign('canAppendSupplier', $this->canProcuremenAppendSupplierForBundle($bundle) ? 1 : 0);
@@ -8621,6 +8638,8 @@ class Procuremen extends Backend
         $this->view->assign('confirmPickGroups', $this->maskSupplierContactList($confirmPickGroups));
         $this->view->assign('confirmPickCount', count($confirmPickGroups));
         $this->view->assign('pickedCompanyName', $pickedCompanyName);
+        $this->view->assign('bidOpenVerified', ($isPurchaseConfirm && $this->isProcuremenBidOpenVerifiedForBundle($confirmBundle)) ? 1 : 0);
+        $this->view->assign('supplierScoreRuleText', ProcuremenSupplierScore::formatRuleFormulaText());
         $sysRqDisplay = '—';
         if ($isPurchaseConfirm) {
             $sysRqDisplay = $this->formatProcuremenSysRqForDisplay(

+ 47 - 0
application/admin/controller/Procuremenmenu.php

@@ -89,6 +89,8 @@ class Procuremenmenu extends Backend
             ['name' => 'procuremensms/index', 'title' => '短信模版维护', 'icon' => 'fa fa-commenting-o', 'weigh' => 87],
             ['name' => 'procuremenarchive/index', 'title' => '历史存证档案查询', 'icon' => 'fa fa-archive', 'weigh' => 86],
             ['name' => 'procuremenexport/index', 'title' => '月度报表导出列表', 'icon' => 'fa fa-file-excel-o', 'weigh' => 85],
+            ['name' => 'supplierscorerule/index', 'title' => '供应商评分规则', 'icon' => 'fa fa-sliders', 'weigh' => 84],
+            ['name' => 'supplierservicescore/index', 'title' => '供应商服务评分', 'icon' => 'fa fa-trophy', 'weigh' => 83],
         ];
         foreach ($menus as $m) {
             if (Db::name('auth_rule')->where('name', $m['name'])->find()) {
@@ -185,6 +187,51 @@ class Procuremenmenu extends Backend
                     $added++;
                 }
             }
+            if ($m['name'] === 'supplierscorerule/index') {
+                foreach ([
+                    ['name' => 'supplierscorerule/add', 'title' => '添加'],
+                    ['name' => 'supplierscorerule/edit', 'title' => '编辑'],
+                    ['name' => 'supplierscorerule/del', 'title' => '删除'],
+                    ['name' => 'supplierscorerule/setdefault', 'title' => '设为默认'],
+                ] as $cr) {
+                    if (Db::name('auth_rule')->where('name', $cr['name'])->find()) {
+                        continue;
+                    }
+                    Db::name('auth_rule')->insert([
+                        'type'       => 'file',
+                        'pid'        => $menuId,
+                        'name'       => $cr['name'],
+                        'title'      => $cr['title'],
+                        'icon'       => 'fa fa-circle-o',
+                        'ismenu'     => 0,
+                        'status'     => 'normal',
+                        'createtime' => $t,
+                        'updatetime' => $t,
+                    ]);
+                    $added++;
+                }
+            }
+            if ($m['name'] === 'supplierservicescore/index') {
+                foreach ([
+                    ['name' => 'supplierservicescore/export', 'title' => '导出供应商评审表'],
+                ] as $cr) {
+                    if (Db::name('auth_rule')->where('name', $cr['name'])->find()) {
+                        continue;
+                    }
+                    Db::name('auth_rule')->insert([
+                        'type'       => 'file',
+                        'pid'        => $menuId,
+                        'name'       => $cr['name'],
+                        'title'      => $cr['title'],
+                        'icon'       => 'fa fa-circle-o',
+                        'ismenu'     => 0,
+                        'status'     => 'normal',
+                        'createtime' => $t,
+                        'updatetime' => $t,
+                    ]);
+                    $added++;
+                }
+            }
         }
         \think\Cache::rm('__menu__');
         $this->success('菜单安装完成,新增节点 ' . $added . ' 个。请刷新后台并到「权限管理」为角色勾选新菜单。');

+ 184 - 0
application/admin/controller/Supplierscorerule.php

@@ -0,0 +1,184 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\library\ProcuremenSupplierScore;
+use think\Db;
+use think\exception\PDOException;
+use think\exception\ValidateException;
+use Exception;
+
+/**
+ * 供应商服务评分权重规则
+ *
+ * @icon fa fa-sliders
+ */
+class Supplierscorerule extends Backend
+{
+    /** @var \app\admin\model\Supplierscorerule */
+    protected $model = null;
+
+    protected $searchFields = 'id';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        ProcuremenSupplierScore::ensureSchema();
+        $this->model = new \app\admin\model\Supplierscorerule;
+    }
+
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $list = $this->model->where($where)->order($sort, $order)->paginate($limit);
+            $rows = [];
+            foreach ($list as $row) {
+                $arr = $row->toArray();
+                $arr['is_default_text'] = !empty($arr['is_default']) ? '默认' : '';
+                $rows[] = $arr;
+            }
+
+            return json(['total' => $list->total(), 'rows' => $rows]);
+        }
+
+        return $this->view->fetch();
+    }
+
+    public function add()
+    {
+        if (!$this->request->isPost()) {
+            return $this->view->fetch();
+        }
+        $params = $this->request->post('row/a');
+        if (empty($params)) {
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $data = $this->normalizeRuleParams($params);
+        $now = date('Y-m-d H:i:s');
+        $data['createtime'] = $now;
+        $data['updatetime'] = $now;
+        Db::startTrans();
+        try {
+            $result = $this->model->allowField(true)->save($data);
+            if ($result === false) {
+                throw new Exception($this->model->getError());
+            }
+            if (!empty($data['is_default'])) {
+                $this->clearOtherDefaults((int)$this->model->id);
+            }
+            Db::commit();
+        } catch (ValidateException|PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        $this->success();
+    }
+
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if (!$this->request->isPost()) {
+            $this->view->assign('row', $row);
+
+            return $this->view->fetch();
+        }
+        $params = $this->request->post('row/a');
+        if (empty($params)) {
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $data = $this->normalizeRuleParams($params);
+        $data['updatetime'] = date('Y-m-d H:i:s');
+        Db::startTrans();
+        try {
+            $result = $row->allowField(true)->save($data);
+            if ($result === false) {
+                throw new Exception($row->getError());
+            }
+            if (!empty($data['is_default'])) {
+                $this->clearOtherDefaults((int)$row->id);
+            }
+            Db::commit();
+        } catch (ValidateException|PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        $this->success();
+    }
+
+    /**
+     * 设为默认计算规则
+     */
+    public function setdefault($ids = null)
+    {
+        $id = (int)($ids ?: $this->request->param('ids', 0));
+        if ($id <= 0) {
+            $this->error('参数错误');
+        }
+        $row = $this->model->get($id);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        Db::startTrans();
+        try {
+            $this->model->where('id', '>', 0)->update(['is_default' => 0, 'updatetime' => date('Y-m-d H:i:s')]);
+            $row->save(['is_default' => 1, 'updatetime' => date('Y-m-d H:i:s')]);
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        $this->success('已设为默认规则');
+    }
+
+    /**
+     * @param array<string, mixed> $params
+     * @return array<string, mixed>
+     */
+    protected function normalizeRuleParams(array $params): array
+    {
+        $qw = trim((string)($params['quality_weight'] ?? ''));
+        $pw = trim((string)($params['price_weight'] ?? ''));
+        if ($qw === '' || !is_numeric($qw) || (float)$qw < 0) {
+            $this->error('质量分%须为非负数字');
+        }
+        if ($pw === '' || !is_numeric($pw) || (float)$pw < 0) {
+            $this->error('价格分%须为非负数字');
+        }
+        if ((float)$qw + (float)$pw <= 0) {
+            $this->error('两项权重之和须大于 0');
+        }
+        $status = trim((string)($params['status'] ?? 'normal'));
+        if (!in_array($status, ['normal', 'hidden'], true)) {
+            $status = 'normal';
+        }
+        $qwNum = round((float)$qw, 2);
+        $pwNum = round((float)$pw, 2);
+        $qwLabel = rtrim(rtrim(sprintf('%.2F', $qwNum), '0'), '.');
+        $pwLabel = rtrim(rtrim(sprintf('%.2F', $pwNum), '0'), '.');
+
+        return [
+            'name'           => '质量分' . $qwLabel . '%+价格分' . $pwLabel . '%',
+            'quality_weight' => $qwNum,
+            'price_weight'   => $pwNum,
+            'is_default'     => !empty($params['is_default']) ? 1 : 0,
+            'status'         => $status,
+        ];
+    }
+
+    protected function clearOtherDefaults(int $keepId): void
+    {
+        if ($keepId <= 0) {
+            return;
+        }
+        $this->model->where('id', 'neq', $keepId)->update([
+            'is_default' => 0,
+            'updatetime' => date('Y-m-d H:i:s'),
+        ]);
+    }
+}

+ 227 - 0
application/admin/controller/Supplierservicescore.php

@@ -0,0 +1,227 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\library\ProcuremenSupplierScore;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use think\Db;
+
+/**
+ * 供应商服务评分表(开标后按订单落库)
+ *
+ * @icon fa fa-trophy
+ */
+class Supplierservicescore extends Backend
+{
+    /** @var \app\admin\model\Supplierservicescore */
+    protected $model = null;
+
+    protected $searchFields = 'ccydh,company_name';
+
+    /** 有列表权限即可导出(避免仅缺子权限导致按钮可见却下载失败) */
+    protected $noNeedRight = ['export'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        ProcuremenSupplierScore::ensureSchema();
+        $this->model = new \app\admin\model\Supplierservicescore;
+    }
+
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $sort = trim((string)$sort);
+            $order = strtolower(trim((string)$order)) === 'asc' ? 'asc' : 'desc';
+            // 同一订单内总分从高到低;默认按订单号分组
+            if ($sort === '' || $sort === 'ccydh' || $sort === 'id') {
+                $query = $this->model->where($where)->order('ccydh', $order)->order('score', 'desc');
+            } elseif ($sort === 'score' || $sort === 'score_text' || $sort === 'rank_no') {
+                $query = $this->model->where($where)->order('score', $order)->order('ccydh', 'desc');
+            } else {
+                $query = $this->model->where($where)->order($sort, $order)->order('ccydh', 'desc')->order('score', 'desc');
+            }
+            $list = $query->paginate($limit);
+            $ruleMap = [];
+            $defaultRule = ProcuremenSupplierScore::getDefaultRule();
+            $rows = [];
+            foreach ($list as $row) {
+                $arr = $row->toArray();
+                $arr['score_text'] = ProcuremenSupplierScore::formatScore($arr['score'] ?? 0);
+                $qw = $arr['quality_weight'] ?? null;
+                $pw = $arr['price_weight'] ?? null;
+                if ($qw === null || $qw === '' || $pw === null || $pw === '') {
+                    $rid = (int)($arr['rule_id'] ?? 0);
+                    if ($rid > 0 && !isset($ruleMap[$rid])) {
+                        try {
+                            $rr = Db::table('supplier_score_rule')->where('id', $rid)->find();
+                            $ruleMap[$rid] = is_array($rr) ? $rr : null;
+                        } catch (\Throwable $e) {
+                            $ruleMap[$rid] = null;
+                        }
+                    }
+                    $rule = ($rid > 0 && is_array($ruleMap[$rid] ?? null)) ? $ruleMap[$rid] : $defaultRule;
+                    if ($qw === null || $qw === '') {
+                        $qw = $rule['quality_weight'] ?? 50;
+                    }
+                    if ($pw === null || $pw === '') {
+                        $pw = $rule['price_weight'] ?? 50;
+                    }
+                }
+                $arr['quality_weight'] = $qw;
+                $arr['price_weight'] = $pw;
+                $arr['quality_weight_text'] = rtrim(rtrim(sprintf('%.2F', (float)$qw), '0'), '.') . '%';
+                $arr['price_weight_text'] = rtrim(rtrim(sprintf('%.2F', (float)$pw), '0'), '.') . '%';
+                $rows[] = $arr;
+            }
+
+            return json(['total' => $list->total(), 'rows' => $rows]);
+        }
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 导出供应商评审表:按月份汇总供应商,总分从高到低
+     * 列:序号、供应商名称、总分(对应原模板「评审等级」位置)
+     */
+    public function export()
+    {
+        $ym = trim((string)$this->request->param('ym', ''));
+        if (!preg_match('/^(\d{4})-(\d{2})$/', $ym, $m)) {
+            $this->error('请选择查询月份');
+        }
+        $year = (int)$m[1];
+        $month = (int)$m[2];
+        if ($month < 1 || $month > 12) {
+            $this->error('查询月份无效');
+        }
+
+        ProcuremenSupplierScore::ensureSchema();
+        try {
+            $rows = Db::table('supplier_service_score')
+                ->where('ym', $ym)
+                ->field('company_name, score')
+                ->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (!is_array($rows)) {
+            $rows = [];
+        }
+
+        // 同一供应商当月多条:取平均总分
+        $byCompany = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $name = trim((string)($r['company_name'] ?? ''));
+            if ($name === '') {
+                continue;
+            }
+            if (!isset($byCompany[$name])) {
+                $byCompany[$name] = ['sum' => 0.0, 'cnt' => 0];
+            }
+            $byCompany[$name]['sum'] += (float)($r['score'] ?? 0);
+            $byCompany[$name]['cnt']++;
+        }
+        $list = [];
+        foreach ($byCompany as $name => $agg) {
+            $avg = $agg['cnt'] > 0 ? round($agg['sum'] / $agg['cnt'], 2) : 0.0;
+            $list[] = [
+                'company_name' => $name,
+                'score'        => $avg,
+                'score_text'   => ProcuremenSupplierScore::formatScore($avg),
+            ];
+        }
+        usort($list, function ($a, $b) {
+            $sa = (float)($a['score'] ?? 0);
+            $sb = (float)($b['score'] ?? 0);
+            if ($sa !== $sb) {
+                return $sb <=> $sa;
+            }
+
+            return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
+        });
+
+        $title = $year . '年' . $month . '月外协加工供应商评审表';
+        $spreadsheet = new Spreadsheet();
+        $sheet = $spreadsheet->getActiveSheet();
+        $sheet->setTitle('供应商评审表');
+
+        // 样式对齐原纸质/Excel 评审表:大标题、居中、黑框、行高加高
+        $fontName = '宋体';
+        $borderStyle = [
+            'borders' => [
+                'allBorders' => [
+                    'borderStyle' => Border::BORDER_THIN,
+                    'color'       => ['rgb' => '000000'],
+                ],
+            ],
+        ];
+        $centerAlign = [
+            'horizontal' => Alignment::HORIZONTAL_CENTER,
+            'vertical'   => Alignment::VERTICAL_CENTER,
+            'wrapText'   => true,
+        ];
+
+        $sheet->mergeCells('A1:C1');
+        $sheet->setCellValue('A1', $title);
+        $sheet->getRowDimension(1)->setRowHeight(36);
+        $sheet->getStyle('A1')->getFont()->setName($fontName)->setBold(true)->setSize(20);
+        $sheet->getStyle('A1')->getAlignment()->applyFromArray($centerAlign);
+
+        $sheet->setCellValue('A2', '序号');
+        $sheet->setCellValue('B2', '供应商名称');
+        $sheet->setCellValue('C2', '总分');
+        $sheet->getRowDimension(2)->setRowHeight(26);
+        $sheet->getStyle('A2:C2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
+        $sheet->getStyle('A2:C2')->getAlignment()->applyFromArray($centerAlign);
+
+        $rowNum = 3;
+        if ($list === []) {
+            $sheet->mergeCells('A3:C3');
+            $sheet->setCellValue('A3', '(该月暂无供应商服务评分数据)');
+            $sheet->getRowDimension(3)->setRowHeight(26);
+            $sheet->getStyle('A3')->getFont()->setName($fontName)->setSize(12);
+            $sheet->getStyle('A3')->getAlignment()->applyFromArray($centerAlign);
+            $rowNum = 4;
+        } else {
+            $i = 1;
+            foreach ($list as $item) {
+                $sheet->setCellValue('A' . $rowNum, $i);
+                $sheet->setCellValue('B' . $rowNum, (string)($item['company_name'] ?? ''));
+                $sheet->setCellValue('C' . $rowNum, (string)($item['score_text'] ?? ''));
+                $sheet->getRowDimension($rowNum)->setRowHeight(24);
+                $rowNum++;
+                $i++;
+            }
+        }
+
+        $lastRow = max(2, $rowNum - 1);
+        $sheet->getStyle('A1:C' . $lastRow)->applyFromArray($borderStyle);
+        $sheet->getStyle('A3:C' . $lastRow)->getFont()->setName($fontName)->setSize(12);
+        $sheet->getStyle('A3:C' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
+        $sheet->getColumnDimension('A')->setWidth(12);
+        $sheet->getColumnDimension('B')->setWidth(48);
+        $sheet->getColumnDimension('C')->setWidth(16);
+        $sheet->getPageSetup()->setFitToPage(true)->setFitToWidth(1)->setFitToHeight(0);
+
+        $filename = $title . '.xlsx';
+        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+        header('Content-Disposition: attachment;filename="' . rawurlencode($filename) . '"');
+        header('Cache-Control: max-age=0');
+        $writer = new Xlsx($spreadsheet);
+        $writer->save('php://output');
+        $spreadsheet->disconnectWorksheets();
+        unset($spreadsheet);
+        exit;
+    }
+}

+ 1 - 1
application/admin/lang/zh-cn/customer.php

@@ -8,7 +8,7 @@ return [
     'Password'     => '密码',
     'Email'        => '邮箱',
     'Phone'        => '手机号',
-    'Score'        => '考核评分值',
+    'Score'        => '上年度质量评分',
     'Company_type' => '业务分类',
     'Createtime'   => '创建时间',
     'Updatetime'   => '更新时间',

+ 15 - 0
application/admin/model/Supplierscorerule.php

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

+ 15 - 0
application/admin/model/Supplierservicescore.php

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

+ 1 - 1
application/admin/view/general/profile/index.html

@@ -64,7 +64,7 @@
                         </div>
                         <div class="form-group">
                             <label for="mobile" class="control-label">{:__('Mobile')}:</label>
-                            <input type="text" class="form-control" id="mobile" name="row[mobile]" value="{$admin.mobile|htmlentities}" maxlength="11" placeholder="11位手机号,用于开标验证等短信通知" data-rule="mobile"/>
+                            <input type="text" class="form-control" id="mobile" name="row[mobile]" value="{$admin.mobile|htmlentities}" maxlength="11" placeholder="11位手机号" data-rule="mobile"/>
                         </div>
                         <div class="form-group">
                             <label for="email" class="control-label">{:__('Email')}:</label>

+ 23 - 0
application/admin/view/procuremen/audit_issue.html

@@ -20,6 +20,19 @@
         border: 1px solid #faebcc;
         border-radius: 3px;
     }
+    .audit-issue-wrap .audit-score-rule-tip {
+        margin: 0 0 8px;
+        padding: 6px 10px;
+        font-size: 12px;
+        line-height: 1.5;
+        color: #31708f;
+        background: #f0f7fb;
+        border: 1px solid #d9edf7;
+        border-radius: 3px;
+    }
+    .audit-issue-wrap .audit-score-rule-tip strong {
+        color: #245269;
+    }
     .audit-issue-wrap .audit-table-wrap {
         /* 不在表格区域内部滚动,由弹窗页面整体滚动 */
         max-height: none;
@@ -365,6 +378,12 @@
         <strong>重要提示:</strong>可先预选供应商,但点击「确认」前须完成开标验证。确认后将进入采购终审(本步<strong>不发送短信通知</strong>)。
         需再次提醒报价时,请在右侧<strong>操作</strong>列点击「发邮件」或「发短信」。
     </p>
+    {if !empty($bidOpenVerified)}
+    <p class="audit-score-rule-tip">
+        <i class="fa fa-info-circle"></i>
+        <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(质量分×50%)+(价格分×50%)'|htmlentities}
+    </p>
+    {/if}
     <div class="audit-table-wrap audit-quote-table-wrap">
         <table class="table table-bordered table-condensed audit-table audit-quote-pick-table audit-quote-detail-table">
             <thead>
@@ -374,6 +393,8 @@
                 <th style="width:88px;">姓名</th>
                 <th style="width:100px;">邮箱</th>
                 <th style="width:120px;">手机号</th>
+                <th class="text-center" style="width:56px;">排名</th>
+                <th class="text-center" style="width:72px;" title="{$supplierScoreRuleText|default=''|htmlentities}">评分</th>
                 <th style="width:130px;">工序名称</th>
                 <th class="text-center" style="width:130px;">单价</th>
                 <th class="text-center" style="width:130px;">预估工期</th>
@@ -398,6 +419,8 @@
                 <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;">{: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>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_score_text|default=''|htmlentities}{/if}</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>

+ 21 - 3
application/admin/view/procuremen/details_fragment.html

@@ -158,7 +158,7 @@
         font-size: 12px;
         table-layout: fixed;
         width: 100%;
-        min-width: 820px;
+        min-width: 880px;
         border-collapse: collapse;
     }
     .procuremen-details-wrap .details-process-table th {
@@ -175,6 +175,13 @@
         word-wrap: break-word;
         border: 1px solid #ddd !important;
     }
+    .procuremen-details-wrap .details-process-table th.col-order-no,
+    .procuremen-details-wrap .details-process-table td.col-order-no {
+        width: 150px;
+        white-space: nowrap;
+        word-wrap: normal;
+        word-break: keep-all;
+    }
     .procuremen-details-wrap .details-process-table th.text-center,
     .procuremen-details-wrap .details-process-table td.text-center {
         text-align: center;
@@ -428,7 +435,7 @@
         <table class="table table-bordered table-condensed details-process-table">
             <thead>
             <tr>
-                <th class="text-center" style="width:100px;">订单号</th>
+                <th class="text-center col-order-no">订单号</th>
                 <th style="min-width:160px;">印件名称</th>
                 <th style="width:88px;">工序名称</th>
                 <th class="text-center" style="width:80px;">单位</th>
@@ -443,7 +450,7 @@
             {volist name="processDisplayRows" id="pr"}
             <tr>
                 {if $pr.show_order_cells}
-                <td class="text-center"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''|htmlentities}</td>
+                <td class="text-center col-order-no"{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CCYDH|default=''|htmlentities}</td>
                 <td{if condition="$pr.order_rowspan gt 1"} rowspan="{$pr.order_rowspan}"{/if}>{$pr.CYJMC|default=''|htmlentities}</td>
                 {/if}
                 <td>{$pr.CGYMC|default=''|htmlentities}</td>
@@ -465,6 +472,13 @@
     </div>
 
     <div class="section-title">供应商</div>
+    {notempty name="supplierScoreRuleText"}
+    <p class="text-muted" style="margin:-4px 0 8px;font-size:12px;line-height:1.5;">
+        <i class="fa fa-info-circle"></i>
+        <strong>评分规则:</strong>{$supplierScoreRuleText|htmlentities}
+        <span class="text-muted">(开标后可见)</span>
+    </p>
+    {/notempty}
     <div class="details-supplier-table-wrap table-responsive">
         <table class="table table-bordered table-condensed detail-mini detail-supplier-table">
             <thead>
@@ -473,6 +487,8 @@
                 <th class="col-supplier-user" style="width:72px;">姓名</th>
                 <th class="col-supplier-email" style="width:100px;">邮箱</th>
                 <th class="col-supplier-phone" style="width:120px;">手机号</th>
+                <th class="text-center" style="width:56px;">排名</th>
+                <th class="text-center" style="width:72px;" title="{$supplierScoreRuleText|default=''|htmlentities}">评分</th>
                 <th style="width:120px;">工序名称</th>
                 <th class="text-center col-unit-price" style="width:130px;">单价</th>
                 <th class="text-center col-lead-days" style="width:130px;">预估工期</th>
@@ -494,6 +510,8 @@
                 <td class="col-supplier-user"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.username|default=''|htmlentities}</td>
                 <td class="col-supplier-email"{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 class="col-supplier-phone"{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>
+                <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.service_rank_text|default=''|htmlentities}</td>
+                <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.service_score_text|default=''|htmlentities}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>
                 <td class="text-center col-unit-price{if condition="!empty($ln.amount_quote_pending)"} detail-quote-pending{elseif condition="empty($ln.amount_filled) || $ln.amount_filled neq 1"} detail-quote-empty{/if}">{$ln.unit_price_text|default='未填写'|htmlentities}</td>

+ 11 - 1
application/admin/view/procuremen/outward_detail.html

@@ -323,6 +323,12 @@
     <p class="audit-notify-tip">
         审批通过:将向中标供应商发送「已通过」短信、向未中标供应商发送「未通过」短信。
     </p>
+    {if !empty($bidOpenVerified)}
+    <p class="audit-score-rule-tip" style="margin:0 0 8px;color:#666;">
+        <i class="fa fa-info-circle"></i>
+        <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(质量分×50%)+(价格分×50%)'|htmlentities}
+    </p>
+    {/if}
     <p class="outward-confirm-block-title">供应商({$confirmPickCount|default=0} 家)</p>
     <div class="outward-confirm-company-wrap">
         <table class="table table-bordered table-condensed outward-confirm-company-table outward-confirm-readonly">
@@ -333,6 +339,8 @@
                 <th style="width:88px;">姓名</th>
                 <th style="min-width:160px;">邮箱</th>
                 <th style="width:120px;">手机号</th>
+                <th class="text-center" style="width:56px;">排名</th>
+                <th class="text-center" style="width:72px;" title="{$supplierScoreRuleText|default=''|htmlentities}">总分</th>
                 <th style="width:120px;">工序名称</th>
                 <th class="text-center" style="width:112px;">单价</th>
                 <th class="text-center" style="width:88px;">预估工期</th>
@@ -357,6 +365,8 @@
                 <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;">{: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>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
                 {/if}
                 <td class="outward-process-name">{$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>
@@ -385,7 +395,7 @@
             {/volist}
             {empty name="confirmPickGroups"}
             <tr>
-                <td colspan="11" class="text-center text-muted">暂无供应商报价</td>
+                <td colspan="13" class="text-center text-muted">暂无供应商报价</td>
             </tr>
             {/empty}
             </tbody>

+ 38 - 0
application/admin/view/supplierscorerule/add.html

@@ -0,0 +1,38 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2"><span class="text-danger">*</span> 质量分%:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input class="form-control" name="row[quality_weight]" type="number" step="0.01" min="0" value="" data-rule="required" placeholder="请输入">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2"><span class="text-danger">*</span> 价格分%:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input class="form-control" name="row[price_weight]" type="number" step="0.01" min="0" value="" data-rule="required" placeholder="请输入">
+            <span class="help-block">总分 = 质量分×质量分% + 价格分×价格分%(两项按比例折算)</span>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">设为默认:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[is_default]" value="0">
+            <label class="checkbox-inline"><input type="checkbox" name="row[is_default]" value="1"> 开标计算使用此规则</label>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">状态:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select class="form-control" name="row[status]">
+                <option value="normal" selected>正常</option>
+                <option value="hidden">隐藏</option>
+            </select>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 38 - 0
application/admin/view/supplierscorerule/edit.html

@@ -0,0 +1,38 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2"><span class="text-danger">*</span> 质量分%:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input class="form-control" name="row[quality_weight]" type="number" step="0.01" min="0" value="{$row.quality_weight|htmlentities}" data-rule="required">
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2"><span class="text-danger">*</span> 价格分%:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input class="form-control" name="row[price_weight]" type="number" step="0.01" min="0" value="{$row.price_weight|htmlentities}" data-rule="required">
+            <span class="help-block">总分 = 质量分×质量分% + 价格分×价格分%(两项按比例折算)</span>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">设为默认:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[is_default]" value="0">
+            <label class="checkbox-inline"><input type="checkbox" name="row[is_default]" value="1"{if $row.is_default} checked{/if}> 开标计算使用此规则</label>
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">状态:</label>
+        <div class="col-xs-12 col-sm-8">
+            <select class="form-control" name="row[status]">
+                <option value="normal"{if $row.status=='normal'} selected{/if}>正常</option>
+                <option value="hidden"{if $row.status=='hidden'} selected{/if}>隐藏</option>
+            </select>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 17 - 0
application/admin/view/supplierscorerule/index.html

@@ -0,0 +1,17 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+    <div class="panel-body">
+        <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>
+                <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('supplierscorerule/add')?'':'hide'}" title="{:__('Add')}"><i class="fa fa-plus"></i> {:__('Add')}</a>
+                <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('supplierscorerule/edit')?'':'hide'}" title="{:__('Edit')}"><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('supplierscorerule/del')?'':'hide'}" title="{:__('Delete')}"><i class="fa fa-trash"></i> {:__('Delete')}</a>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                   data-operate-edit="{:$auth->check('supplierscorerule/edit')}"
+                   data-operate-del="{:$auth->check('supplierscorerule/del')}"
+                   width="100%"></table>
+        </div>
+    </div>
+</div>

+ 18 - 0
application/admin/view/supplierservicescore/index.html

@@ -0,0 +1,18 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+    <div class="panel-body">
+        <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>
+                <span style="display:inline-block;margin-left:10px;vertical-align:middle;">
+                    <a href="javascript:;" class="btn btn-success btn-export-review" id="btn-export-review" title="按月份导出供应商评审表">
+                        <i class="fa fa-download"></i> 导出供应商评审表
+                    </a>
+                    <label style="margin:0 6px 0 10px;font-weight:normal;" for="export-review-ym">查询月份</label>
+                    <input type="month" id="export-review-ym" class="form-control input-sm" style="display:inline-block;width:150px;height:30px;vertical-align:middle;" title="选择月份后自动查询该月评分"/>
+                </span>
+            </div>
+            <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
+        </div>
+    </div>
+</div>

+ 522 - 0
application/common/library/ProcuremenSupplierScore.php

@@ -0,0 +1,522 @@
+<?php
+
+namespace app\common\library;
+
+use think\Db;
+use think\Log;
+
+/**
+ * 协助采购 — 供应商服务评分
+ *
+ * 总分 = 上年度质量评分×质量权重% + 单价评分值×单价权重%
+ * 单价合计 = 同一供应商本单各工序单价之和;单价评分值 = (本单最低合计 / 本供应商合计) × 100
+ * 权重取 supplier_score_rule 中 is_default=1 的规则;开标验证通过后写入 supplier_service_score
+ */
+class ProcuremenSupplierScore
+{
+    public const TABLE_RULE = 'supplier_score_rule';
+    public const TABLE_SCORE = 'supplier_service_score';
+
+    /** @var bool|null */
+    protected static $schemaReady = null;
+
+    public static function ensureSchema(): void
+    {
+        if (self::$schemaReady === true) {
+            return;
+        }
+        try {
+            Db::query('SELECT 1 FROM `' . self::TABLE_RULE . '` LIMIT 1');
+            Db::query('SELECT 1 FROM `' . self::TABLE_SCORE . '` LIMIT 1');
+            self::$schemaReady = true;
+            self::ensureDefaultRule();
+            self::ensureScoreWeightColumns();
+
+            return;
+        } catch (\Throwable $e) {
+        }
+        $sqlFile = APP_PATH . 'extra' . DIRECTORY_SEPARATOR . 'supplier_score_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) {
+                    }
+                }
+            }
+        }
+        self::$schemaReady = true;
+        self::ensureDefaultRule();
+        self::ensureScoreWeightColumns();
+    }
+
+    /** 历史表补质量/价格百分比字段 */
+    protected static function ensureScoreWeightColumns(): void
+    {
+        try {
+            $cols = Db::query("SHOW COLUMNS FROM `" . self::TABLE_SCORE . "` LIKE 'quality_weight'");
+            if (!is_array($cols) || $cols === []) {
+                Db::execute(
+                    "ALTER TABLE `" . self::TABLE_SCORE . "` ADD COLUMN `quality_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '质量百分比' AFTER `quality_score`"
+                );
+            }
+            $cols2 = Db::query("SHOW COLUMNS FROM `" . self::TABLE_SCORE . "` LIKE 'price_weight'");
+            if (!is_array($cols2) || $cols2 === []) {
+                Db::execute(
+                    "ALTER TABLE `" . self::TABLE_SCORE . "` ADD COLUMN `price_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '价格百分比' AFTER `price_sum`"
+                );
+            }
+        } catch (\Throwable $e) {
+            Log::write('supplier score weight columns: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    protected static function ensureDefaultRule(): void
+    {
+        try {
+            $cnt = (int)Db::table(self::TABLE_RULE)->count();
+            if ($cnt > 0) {
+                $hasDefault = Db::table(self::TABLE_RULE)->where('is_default', 1)->find();
+                if (!is_array($hasDefault)) {
+                    $first = Db::table(self::TABLE_RULE)->order('id', 'asc')->find();
+                    if (is_array($first)) {
+                        Db::table(self::TABLE_RULE)->where('id', (int)$first['id'])->update([
+                            'is_default' => 1,
+                            'updatetime' => date('Y-m-d H:i:s'),
+                        ]);
+                    }
+                }
+
+                return;
+            }
+            $now = date('Y-m-d H:i:s');
+            Db::table(self::TABLE_RULE)->insert([
+                'name'           => '默认规则(质量50%+单价50%)',
+                'quality_weight' => 50,
+                'price_weight'   => 50,
+                'is_default'     => 1,
+                'status'         => 'normal',
+                'createtime'     => $now,
+                'updatetime'     => $now,
+            ]);
+        } catch (\Throwable $e) {
+            Log::write('supplier score ensureDefaultRule: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * @return array{id:int,name:string,quality_weight:float,price_weight:float}
+     */
+    public static function getDefaultRule(): array
+    {
+        self::ensureSchema();
+        try {
+            $row = Db::table(self::TABLE_RULE)
+                ->where('is_default', 1)
+                ->where('status', 'normal')
+                ->order('id', 'desc')
+                ->find();
+            if (!is_array($row)) {
+                $row = Db::table(self::TABLE_RULE)->where('status', 'normal')->order('id', 'asc')->find();
+            }
+            if (is_array($row)) {
+                return [
+                    'id'             => (int)($row['id'] ?? 0),
+                    'name'           => trim((string)($row['name'] ?? '')),
+                    'quality_weight' => (float)($row['quality_weight'] ?? 50),
+                    'price_weight'   => (float)($row['price_weight'] ?? 50),
+                ];
+            }
+        } catch (\Throwable $e) {
+        }
+
+        return [
+            'id'             => 0,
+            'name'           => '内置默认',
+            'quality_weight' => 50.0,
+            'price_weight'   => 50.0,
+        ];
+    }
+
+    /**
+     * 按公司名批量取供应商 id / 上年度质量评分
+     *
+     * @param array<int, string> $companyNames
+     * @return array<string, array{id:int,score:float}>
+     */
+    public static function loadCustomerScoreMap(array $companyNames): array
+    {
+        $map = [];
+        $names = [];
+        foreach ($companyNames as $n) {
+            $n = trim((string)$n);
+            if ($n !== '') {
+                $names[$n] = true;
+            }
+        }
+        if ($names === []) {
+            return $map;
+        }
+        try {
+            $rows = Db::table('customer')
+                ->where('company_name', 'in', array_keys($names))
+                ->field('id,company_name,score')
+                ->select();
+        } catch (\Throwable $e) {
+            return $map;
+        }
+        if (!is_array($rows)) {
+            return $map;
+        }
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $cn = trim((string)($r['company_name'] ?? ''));
+            if ($cn === '') {
+                continue;
+            }
+            $raw = $r['score'] ?? null;
+            $score = ($raw === null || $raw === '') ? 0.0 : (float)$raw;
+            $map[$cn] = [
+                'id'    => (int)($r['id'] ?? 0),
+                'score' => $score,
+            ];
+        }
+
+        return $map;
+    }
+
+    /**
+     * 从报价组计算本单各供应商得分(不写库)
+     *
+     * @param array<int, array<string, mixed>> $quoteGroups loadAuditSupplierQuoteGroups 结构(name/lines[].amount)
+     * @return array<string, array{
+     *   company_name:string,customer_id:int,quality_score:float,price_sum:float,
+     *   price_score:float,score:float,rank_no:int,score_text:string,rank_text:string
+     * }>
+     */
+    public static function calculateForQuoteGroups(array $quoteGroups, ?array $rule = null): array
+    {
+        $rule = $rule ?: self::getDefaultRule();
+        $qw = (float)($rule['quality_weight'] ?? 50);
+        $pw = (float)($rule['price_weight'] ?? 50);
+        $wSum = $qw + $pw;
+        if ($wSum <= 0) {
+            $qw = 50;
+            $pw = 50;
+            $wSum = 100;
+        }
+
+        $names = [];
+        foreach ($quoteGroups as $g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
+            if ($cn !== '') {
+                $names[] = $cn;
+            }
+        }
+        $custMap = self::loadCustomerScoreMap($names);
+
+        $items = [];
+        $priceSums = [];
+        foreach ($quoteGroups as $g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
+            if ($cn === '') {
+                continue;
+            }
+            $priceSum = 0.0;
+            $hasPrice = false;
+            foreach (($g['lines'] ?? []) as $ln) {
+                if (!is_array($ln)) {
+                    continue;
+                }
+                if (!empty($ln['amount_quote_pending'])) {
+                    continue;
+                }
+                $am = trim((string)($ln['amount'] ?? ''));
+                if ($am === '' || $am === '0' || $am === '0.00') {
+                    continue;
+                }
+                if (!is_numeric($am)) {
+                    continue;
+                }
+                $priceSum += (float)$am;
+                $hasPrice = true;
+            }
+            $quality = (float)($custMap[$cn]['score'] ?? 0);
+            $items[$cn] = [
+                'company_name'   => $cn,
+                'customer_id'    => (int)($custMap[$cn]['id'] ?? 0),
+                'quality_score'  => $quality,
+                'price_sum'      => $hasPrice ? $priceSum : 0.0,
+                'has_price'      => $hasPrice,
+                'price_score'    => 0.0,
+                'score'          => 0.0,
+                'rank_no'        => 0,
+                'score_text'     => '',
+                'rank_text'      => '',
+            ];
+            if ($hasPrice && $priceSum > 0) {
+                $priceSums[$cn] = $priceSum;
+            }
+        }
+
+        $minSum = null;
+        foreach ($priceSums as $ps) {
+            if ($minSum === null || $ps < $minSum) {
+                $minSum = $ps;
+            }
+        }
+        foreach ($items as $cn => &$it) {
+            if (!empty($it['has_price']) && $it['price_sum'] > 0 && $minSum !== null && $minSum > 0) {
+                $it['price_score'] = round(($minSum / $it['price_sum']) * 100, 2);
+            } else {
+                $it['price_score'] = 0.0;
+            }
+            $it['score'] = round(
+                $it['quality_score'] * ($qw / $wSum) + $it['price_score'] * ($pw / $wSum),
+                2
+            );
+            $it['score_text'] = self::formatScore($it['score']);
+        }
+        unset($it);
+
+        $sorted = $items;
+        uasort($sorted, function ($a, $b) {
+            $sa = (float)($a['score'] ?? 0);
+            $sb = (float)($b['score'] ?? 0);
+            if ($sa !== $sb) {
+                return $sb <=> $sa;
+            }
+
+            return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
+        });
+        $rank = 0;
+        foreach ($sorted as $cn => $row) {
+            $rank++;
+            $items[$cn]['rank_no'] = $rank;
+            $items[$cn]['rank_text'] = (string)$rank;
+        }
+
+        return $items;
+    }
+
+    public static function formatScore($n): string
+    {
+        $s = rtrim(rtrim(sprintf('%.2F', (float)$n), '0'), '.');
+
+        return $s === '' ? '0' : $s;
+    }
+
+    /**
+     * 评分规则文案(用于确认页 / 详情展示)
+     *
+     * @param array{quality_weight?:float|int|string,price_weight?:float|int|string}|null $rule
+     */
+    public static function formatRuleFormulaText(?array $rule = null): string
+    {
+        $rule = $rule ?: self::getDefaultRule();
+        $qw = self::formatWeightPercent($rule['quality_weight'] ?? 50);
+        $pw = self::formatWeightPercent($rule['price_weight'] ?? 50);
+
+        return '(总分)=(质量分×' . $qw . '%)+(价格分×' . $pw . '%)';
+    }
+
+    protected static function formatWeightPercent($n): string
+    {
+        $s = rtrim(rtrim(sprintf('%.2F', (float)$n), '0'), '.');
+
+        return $s === '' ? '0' : $s;
+    }
+
+    /**
+     * 开标验证通过后:计算并写入供应商服务评分表
+     *
+     * @param array<int, array<string, mixed>> $quoteGroups
+     */
+    public static function saveForOrder(string $ccydh, array $quoteGroups, ?string $ym = null): void
+    {
+        $ccydh = trim($ccydh);
+        if ($ccydh === '') {
+            return;
+        }
+        self::ensureSchema();
+        $rule = self::getDefaultRule();
+        $calc = self::calculateForQuoteGroups($quoteGroups, $rule);
+        if ($calc === []) {
+            return;
+        }
+        if ($ym === null || $ym === '') {
+            $ym = date('Y-m');
+        }
+        $now = date('Y-m-d H:i:s');
+        try {
+            Db::table(self::TABLE_SCORE)->where('ccydh', $ccydh)->delete();
+            $rows = [];
+            foreach ($calc as $it) {
+                $rows[] = [
+                    'customer_id'    => (int)($it['customer_id'] ?? 0),
+                    'company_name'   => (string)($it['company_name'] ?? ''),
+                    'ym'             => $ym,
+                    'ccydh'          => $ccydh,
+                    'score'          => (float)($it['score'] ?? 0),
+                    'rank_no'        => (int)($it['rank_no'] ?? 0),
+                    'quality_score'  => (float)($it['quality_score'] ?? 0),
+                    'quality_weight' => (float)($rule['quality_weight'] ?? 50),
+                    'price_sum'      => (float)($it['price_sum'] ?? 0),
+                    'price_weight'   => (float)($rule['price_weight'] ?? 50),
+                    'price_score'    => (float)($it['price_score'] ?? 0),
+                    'rule_id'        => (int)($rule['id'] ?? 0),
+                    'createtime'     => $now,
+                    'updatetime'     => $now,
+                ];
+            }
+            if ($rows !== []) {
+                Db::table(self::TABLE_SCORE)->insertAll($rows);
+            }
+        } catch (\Throwable $e) {
+            Log::write('supplier service score save: ' . $e->getMessage(), 'error');
+        }
+    }
+
+    /**
+     * 读取某订单已落库评分;无则返回空
+     *
+     * @return array<string, array<string, mixed>>
+     */
+    public static function loadSavedByCcydh(string $ccydh): array
+    {
+        $ccydh = trim($ccydh);
+        if ($ccydh === '') {
+            return [];
+        }
+        self::ensureSchema();
+        try {
+            $rows = Db::table(self::TABLE_SCORE)->where('ccydh', $ccydh)->select();
+        } catch (\Throwable $e) {
+            return [];
+        }
+        if (!is_array($rows)) {
+            return [];
+        }
+        $out = [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            $cn = trim((string)($r['company_name'] ?? ''));
+            if ($cn === '') {
+                continue;
+            }
+            $score = (float)($r['score'] ?? 0);
+            $rank = (int)($r['rank_no'] ?? 0);
+            $out[$cn] = [
+                'company_name'  => $cn,
+                'customer_id'   => (int)($r['customer_id'] ?? 0),
+                'quality_score' => (float)($r['quality_score'] ?? 0),
+                'price_sum'     => (float)($r['price_sum'] ?? 0),
+                'price_score'   => (float)($r['price_score'] ?? 0),
+                'score'         => $score,
+                'rank_no'       => $rank,
+                'score_text'    => self::formatScore($score),
+                'rank_text'     => $rank > 0 ? (string)$rank : '',
+            ];
+        }
+
+        return $out;
+    }
+
+    /**
+     * 把排名/评分挂到报价组上(开标后可见)
+     *
+     * @param array<int, array<string, mixed>> $quoteGroups
+     * @return array<int, array<string, mixed>>
+     */
+    public static function attachToQuoteGroups(array $quoteGroups, string $ccydh, bool $quoteVisible): array
+    {
+        if (!$quoteVisible || $quoteGroups === []) {
+            foreach ($quoteGroups as &$g) {
+                if (!is_array($g)) {
+                    continue;
+                }
+                $g['service_rank'] = '';
+                $g['service_score'] = '';
+                $g['service_rank_text'] = '';
+                $g['service_score_text'] = '';
+                $g['show_service_score'] = 0;
+            }
+            unset($g);
+
+            return $quoteGroups;
+        }
+
+        $saved = self::loadSavedByCcydh($ccydh);
+        if ($saved === []) {
+            $saved = self::calculateForQuoteGroups($quoteGroups);
+            // 历史已开标但尚未落库:补写一次
+            try {
+                self::saveForOrder($ccydh, $quoteGroups);
+                $saved = self::loadSavedByCcydh($ccydh) ?: $saved;
+            } catch (\Throwable $e) {
+            }
+        }
+        foreach ($quoteGroups as &$g) {
+            if (!is_array($g)) {
+                continue;
+            }
+            $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
+            $hit = $saved[$cn] ?? null;
+            if (is_array($hit)) {
+                $g['service_rank'] = (int)($hit['rank_no'] ?? 0);
+                $g['service_score'] = (float)($hit['score'] ?? 0);
+                $g['service_rank_text'] = (string)($hit['rank_text'] ?? '');
+                $g['service_score_text'] = (string)($hit['score_text'] ?? '');
+                $g['show_service_score'] = 1;
+            } else {
+                $g['service_rank'] = '';
+                $g['service_score'] = '';
+                $g['service_rank_text'] = '';
+                $g['service_score_text'] = '';
+                $g['show_service_score'] = 1;
+            }
+        }
+        unset($g);
+
+        // 开标后按排名升序(第1名在最前);无排名的放最后
+        usort($quoteGroups, function ($a, $b) {
+            $ra = is_array($a) ? (int)($a['service_rank'] ?? 0) : 0;
+            $rb = is_array($b) ? (int)($b['service_rank'] ?? 0) : 0;
+            $ha = $ra > 0;
+            $hb = $rb > 0;
+            if ($ha !== $hb) {
+                return $ha ? -1 : 1;
+            }
+            if ($ha && $ra !== $rb) {
+                return $ra <=> $rb;
+            }
+            $sa = is_array($a) ? (float)($a['service_score'] ?? 0) : 0.0;
+            $sb = is_array($b) ? (float)($b['service_score'] ?? 0) : 0.0;
+            if ($sa !== $sb) {
+                return $sb <=> $sa;
+            }
+            $na = is_array($a) ? trim((string)($a['name'] ?? $a['company_name'] ?? '')) : '';
+            $nb = is_array($b) ? trim((string)($b['name'] ?? $b['company_name'] ?? '')) : '';
+
+            return strcmp($na, $nb);
+        });
+
+        return $quoteGroups;
+    }
+}

+ 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.10.24',
+  'version' => '1.10.30',
   'timezone' => 'Asia/Shanghai',
   'forbiddenip' => '',
   'languages' => 

+ 36 - 0
application/extra/supplier_score_install.sql

@@ -0,0 +1,36 @@
+-- 供应商评分权重规则(多条,设一条为默认)
+CREATE TABLE IF NOT EXISTS `supplier_score_rule` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(64) NOT NULL DEFAULT '' COMMENT '规则名称',
+  `quality_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '上年度质量评分权重(%)',
+  `price_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '单价评分权重(%)',
+  `is_default` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=默认计算规则',
+  `status` varchar(16) NOT NULL DEFAULT 'normal' COMMENT 'normal/hidden',
+  `createtime` datetime DEFAULT NULL,
+  `updatetime` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_default` (`is_default`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商服务评分权重规则';
+
+-- 供应商服务评分表(开标验证通过后按订单写入)
+CREATE TABLE IF NOT EXISTS `supplier_service_score` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `customer_id` int unsigned NOT NULL DEFAULT 0 COMMENT '供应商ID(customer.id)',
+  `company_name` varchar(128) NOT NULL DEFAULT '' COMMENT '供应商名称',
+  `ym` char(7) NOT NULL DEFAULT '' COMMENT '对应年月 YYYY-MM',
+  `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
+  `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '总分',
+  `rank_no` int unsigned NOT NULL DEFAULT 0 COMMENT '本单排名',
+  `quality_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '上年度质量评分',
+  `quality_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '质量百分比',
+  `price_sum` decimal(14,4) NOT NULL DEFAULT 0.0000 COMMENT '本单工序单价合计',
+  `price_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '价格百分比',
+  `price_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '单价评分值',
+  `rule_id` int unsigned NOT NULL DEFAULT 0 COMMENT '所用规则ID',
+  `createtime` datetime DEFAULT NULL,
+  `updatetime` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ccydh_company` (`ccydh`,`company_name`),
+  KEY `idx_customer_ym` (`customer_id`,`ym`),
+  KEY `idx_ccydh` (`ccydh`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商服务评分表';

+ 4 - 0
application/extra/supplier_service_score_weight_columns.sql

@@ -0,0 +1,4 @@
+-- 供应商服务评分表补:质量百分比、价格百分比
+ALTER TABLE `supplier_service_score`
+  ADD COLUMN `quality_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '质量百分比' AFTER `quality_score`,
+  ADD COLUMN `price_weight` decimal(5,2) NOT NULL DEFAULT 50.00 COMMENT '价格百分比' AFTER `price_sum`;

+ 81 - 0
public/assets/js/backend/supplierscorerule.js

@@ -0,0 +1,81 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'supplierscorerule/index' + location.search,
+                    add_url: 'supplierscorerule/add',
+                    edit_url: 'supplierscorerule/edit',
+                    del_url: 'supplierscorerule/del',
+                    multi_url: '',
+                    table: 'supplier_score_rule'
+                }
+            });
+            var table = $('#table');
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                sortOrder: 'desc',
+                columns: [[
+                    {checkbox: true},
+                    {field: 'id', title: 'ID', width: 60, sortable: true},
+                    {field: 'quality_weight', title: '质量分%', operate: 'BETWEEN', sortable: true},
+                    {field: 'price_weight', title: '价格分%', operate: 'BETWEEN', sortable: true},
+                    {
+                        field: 'is_default',
+                        title: '默认',
+                        searchList: {1: '默认', 0: '否'},
+                        formatter: function (v) {
+                            return parseInt(v, 10) === 1
+                                ? '<span class="label label-success">默认</span>'
+                                : '';
+                        }
+                    },
+                    {
+                        field: 'status',
+                        title: '状态',
+                        searchList: {normal: '正常', hidden: '隐藏'},
+                        formatter: Table.api.formatter.status
+                    },
+                    {
+                        field: 'operate',
+                        title: __('Operate'),
+                        table: table,
+                        events: Table.api.events.operate,
+                        buttons: [{
+                            name: 'setdefault',
+                            text: '设为默认',
+                            title: '设为默认',
+                            classname: 'btn btn-xs btn-warning btn-ajax',
+                            icon: 'fa fa-check',
+                            url: 'supplierscorerule/setdefault',
+                            confirm: '确认将此规则设为开标计算默认?',
+                            success: function () {
+                                table.bootstrapTable('refresh');
+                                return false;
+                            },
+                            visible: function (row) {
+                                return parseInt(row.is_default, 10) !== 1;
+                            }
+                        }],
+                        formatter: Table.api.formatter.operate
+                    }
+                ]]
+            });
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($('form[role=form]'));
+            }
+        }
+    };
+    return Controller;
+});

+ 179 - 0
public/assets/js/backend/supplierservicescore.js

@@ -0,0 +1,179 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+    var Controller = {
+        index: function () {
+            Table.api.init({
+                extend: {
+                    index_url: 'supplierservicescore/index' + location.search,
+                    table: 'supplier_service_score'
+                }
+            });
+
+            function pad2(n) {
+                return (n < 10 ? '0' : '') + n;
+            }
+            function currentYm() {
+                var d = new Date();
+                return d.getFullYear() + '-' + pad2(d.getMonth() + 1);
+            }
+            function normalizeYm(raw) {
+                var v = String(raw || '').trim();
+                if (/^\d{4}-\d{2}$/.test(v)) {
+                    return v;
+                }
+                // 兼容界面显示成 2026年07月
+                var m = v.match(/^(\d{4})\D+(\d{1,2})/);
+                if (m) {
+                    return m[1] + '-' + pad2(parseInt(m[2], 10));
+                }
+                return '';
+            }
+            function resolveYm() {
+                var v = normalizeYm($('#export-review-ym').val());
+                if (v) {
+                    return v;
+                }
+                var $filterYm = $('input[name="ym"], .commonsearch-table input[name="ym"]').first();
+                if ($filterYm.length) {
+                    v = normalizeYm($filterYm.val());
+                    if (v) {
+                        return v;
+                    }
+                }
+                return currentYm();
+            }
+
+            var $ymInput = $('#export-review-ym');
+            if ($ymInput.length && !normalizeYm($ymInput.val())) {
+                $ymInput.val(currentYm());
+            }
+
+            var table = $('#table');
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'ccydh',
+                sortOrder: 'desc',
+                queryParams: function (params) {
+                    var filter = {};
+                    var op = {};
+                    try {
+                        filter = typeof params.filter === 'string' ? (JSON.parse(params.filter || '{}') || {}) : (params.filter || {});
+                        op = typeof params.op === 'string' ? (JSON.parse(params.op || '{}') || {}) : (params.op || {});
+                    } catch (e) {
+                        filter = {};
+                        op = {};
+                    }
+                    var keyword = String(params.search || '').trim();
+                    // 有关键词时跨月查(订单号/供应商名),避免被「查询月份」卡住查不到
+                    if (!keyword) {
+                        var ym = resolveYm();
+                        if (ym) {
+                            filter.ym = ym;
+                            op.ym = '=';
+                        }
+                    } else if (filter.ym) {
+                        delete filter.ym;
+                        if (op.ym) {
+                            delete op.ym;
+                        }
+                    }
+                    params.filter = JSON.stringify(filter);
+                    params.op = JSON.stringify(op);
+                    return params;
+                },
+                columns: [[
+                    {field: 'ccydh', title: '订单号', operate: 'LIKE', sortable: true},
+                    {field: 'company_name', title: '供应商名称', operate: 'LIKE'},
+                    {field: 'rank_no', title: '排名', sortable: true, width: 70},
+                    {
+                        field: 'score',
+                        title: '总分',
+                        operate: 'BETWEEN',
+                        sortable: true,
+                        formatter: function (value, row) {
+                            return row.score_text != null && row.score_text !== '' ? row.score_text : value;
+                        }
+                    },
+                    {
+                        field: 'quality_score',
+                        title: '上年度质量评分',
+                        operate: 'BETWEEN',
+                        formatter: function (value) {
+                            if (value === null || value === undefined || value === '') {
+                                return '';
+                            }
+                            return String(parseInt(value, 10));
+                        }
+                    },
+                    {
+                        field: 'quality_weight',
+                        title: '质量分%',
+                        operate: 'BETWEEN',
+                        sortable: true,
+                        formatter: function (value) {
+                            if (value === null || value === undefined || value === '') {
+                                return '';
+                            }
+                            return String(parseInt(value, 10));
+                        }
+                    },
+                    {
+                        field: 'price_sum',
+                        title: '单价合计',
+                        operate: 'BETWEEN',
+                        formatter: function (value) {
+                            if (value === null || value === undefined || value === '') {
+                                return '';
+                            }
+                            return String(parseInt(value, 10));
+                        }
+                    },
+                    {
+                        field: 'price_weight',
+                        title: '价格分%',
+                        operate: 'BETWEEN',
+                        sortable: true,
+                        formatter: function (value) {
+                            if (value === null || value === undefined || value === '') {
+                                return '';
+                            }
+                            return String(parseInt(value, 10));
+                        }
+                    },
+                    {field: 'ym', title: '年月', operate: false, width: 90}
+                ]]
+            });
+            Table.api.bindevent(table);
+
+            // 选择月份后立即按该月查询列表
+            $(document).off('change.supplierScoreYm', '#export-review-ym').on('change.supplierScoreYm', '#export-review-ym', function () {
+                var ym = normalizeYm($(this).val());
+                if (ym && $(this).val() !== ym) {
+                    $(this).val(ym);
+                }
+                table.bootstrapTable('refresh', {pageNumber: 1});
+            });
+
+            $(document).off('click.supplierScoreExport', '#btn-export-review').on('click.supplierScoreExport', '#btn-export-review', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+                var ym = resolveYm();
+                if (!/^\d{4}-\d{2}$/.test(ym)) {
+                    if (typeof Toastr !== 'undefined') {
+                        Toastr.error('请选择查询月份');
+                    } else {
+                        Layer.msg('请选择查询月份');
+                    }
+                    return false;
+                }
+                if ($ymInput.length) {
+                    $ymInput.val(ym);
+                }
+                var url = Fast.api.fixurl('supplierservicescore/export') + '?ym=' + encodeURIComponent(ym);
+                window.location.href = url;
+                return false;
+            });
+        }
+    };
+    return Controller;
+});