|
|
@@ -10,13 +10,13 @@ use think\Log;
|
|
|
*
|
|
|
* 表分工:
|
|
|
* - supplier_service_score:订单明细(开标后按 ccydh×供应商写入,含本单总分 score)
|
|
|
- * - supplier_service_final_score:月度汇总(ym×供应商;score=当月订单平均总分;final_score=人工最终得分)
|
|
|
+ * - supplier_service_final_score:月度评审表(开标后写入本月询价供应商名称;分项与等级人工填写,已存在不覆盖)
|
|
|
*
|
|
|
- * 总分 = 各分项×对应权重%(权重填 0 则该项不参与)
|
|
|
- * 商务/技术得分 =(综合评分 + 月度评分)/ 2;
|
|
|
- * 单价项按本单工序单价合计参与加权;权重按百分比计入总分。
|
|
|
- * 工期合计 = 同一供应商本单各工序预估工期之和;交货期评分值 = (本单最短合计 / 本供应商合计) × 100
|
|
|
- * 权重取 supplier_score_rule 中 is_default=1 的规则;开标验证通过后写入订单明细表,并同步月度总分
|
|
|
+ * 总分 = 商务/技术得分×质量% + 价格得分 [+ 交货得分×交货%]
|
|
|
+ * 价格得分 = (本单最低单价合计 / 本供应商单价合计) × 价格权重% × 100
|
|
|
+ * (计算过程保留四位小数,得分保留两位)
|
|
|
+ * 商务/技术得分 = 月度评审表「商务/技术得分」(当月按上月起往前查,都无则默认 50)
|
|
|
+ * 权重取 supplier_score_rule 中 is_default=1 的规则;开标验证通过后写入订单明细评分,并补齐月度评审供应商名称
|
|
|
*/
|
|
|
class ProcuremenSupplierScore
|
|
|
{
|
|
|
@@ -71,21 +71,26 @@ class ProcuremenSupplierScore
|
|
|
self::ensureFinalScoreTable();
|
|
|
}
|
|
|
|
|
|
- /** 月度汇总表(总分 score + 人工最终得分 final_score) */
|
|
|
+ /** 月度记录表(商务技术分 / 价格分 / 最终得分,仅本页展示与导出) */
|
|
|
public static function ensureFinalScoreTable(): void
|
|
|
{
|
|
|
$ddl = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_FINAL . "` (
|
|
|
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
|
|
`ym` char(7) NOT NULL DEFAULT '' COMMENT '年月 YYYY-MM',
|
|
|
`company_name` varchar(128) NOT NULL DEFAULT '' COMMENT '供应商名称',
|
|
|
- `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '月度总分(当月订单平均)',
|
|
|
+ `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '月度总分(兼容旧字段)',
|
|
|
+ `quality_score` decimal(10,2) DEFAULT NULL COMMENT '商务技术分(人工,空=未填)',
|
|
|
+ `price_score` decimal(10,2) DEFAULT NULL COMMENT '价格分(人工,空=未填)',
|
|
|
+ `delivery_score` decimal(10,2) DEFAULT NULL COMMENT '交货分(人工,空=未填)',
|
|
|
`final_score` decimal(10,2) DEFAULT NULL COMMENT '最终得分(人工,空=未填)',
|
|
|
+ `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D',
|
|
|
+ `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改',
|
|
|
`createtime` datetime DEFAULT NULL,
|
|
|
`updatetime` datetime DEFAULT NULL,
|
|
|
PRIMARY KEY (`id`),
|
|
|
UNIQUE KEY `uk_ym_company` (`ym`,`company_name`),
|
|
|
KEY `idx_ym` (`ym`)
|
|
|
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商月度评分汇总'";
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商月度评审记录'";
|
|
|
try {
|
|
|
Db::execute($ddl);
|
|
|
} catch (\Throwable $e) {
|
|
|
@@ -94,36 +99,63 @@ class ProcuremenSupplierScore
|
|
|
self::ensureMonthlyScoreColumns();
|
|
|
}
|
|
|
|
|
|
- /** 历史月度表补总分字段,并将 final_score 改为可空 */
|
|
|
+ /** 历史月度表补商务技术分/价格分等字段 */
|
|
|
protected static function ensureMonthlyScoreColumns(): void
|
|
|
{
|
|
|
- try {
|
|
|
- $cols = Db::query("SHOW COLUMNS FROM `" . self::TABLE_FINAL . "` LIKE 'score'");
|
|
|
- if (!is_array($cols) || $cols === []) {
|
|
|
- Db::execute(
|
|
|
- "ALTER TABLE `" . self::TABLE_FINAL . "` ADD COLUMN `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '月度总分(当月订单平均)' AFTER `company_name`"
|
|
|
- );
|
|
|
+ $adds = [
|
|
|
+ 'score' => "ADD COLUMN `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '月度总分(兼容旧字段)' AFTER `company_name`",
|
|
|
+ 'quality_score' => "ADD COLUMN `quality_score` decimal(10,2) DEFAULT NULL COMMENT '商务技术分(人工,空=未填)' AFTER `score`",
|
|
|
+ 'price_score' => "ADD COLUMN `price_score` decimal(10,2) DEFAULT NULL COMMENT '价格分(人工,空=未填)' AFTER `quality_score`",
|
|
|
+ 'delivery_score' => "ADD COLUMN `delivery_score` decimal(10,2) DEFAULT NULL COMMENT '交货分(人工,空=未填)' AFTER `price_score`",
|
|
|
+ 'score_grade' => "ADD COLUMN `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D' AFTER `final_score`",
|
|
|
+ 'qp_manual' => "ADD COLUMN `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改' AFTER `score_grade`",
|
|
|
+ ];
|
|
|
+ foreach ($adds as $col => $ddl) {
|
|
|
+ try {
|
|
|
+ $cols = Db::query("SHOW COLUMNS FROM `" . self::TABLE_FINAL . "` LIKE '{$col}'");
|
|
|
+ if (!is_array($cols) || $cols === []) {
|
|
|
+ Db::execute("ALTER TABLE `" . self::TABLE_FINAL . "` {$ddl}");
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::write("supplier monthly {$col} column: " . $e->getMessage(), 'error');
|
|
|
}
|
|
|
- } catch (\Throwable $e) {
|
|
|
- Log::write('supplier monthly score column: ' . $e->getMessage(), 'error');
|
|
|
}
|
|
|
- try {
|
|
|
- $info = Db::query("SHOW COLUMNS FROM `" . self::TABLE_FINAL . "` LIKE 'final_score'");
|
|
|
- if (is_array($info) && $info !== []) {
|
|
|
- $null = strtoupper((string)($info[0]['Null'] ?? ''));
|
|
|
- if ($null !== 'YES') {
|
|
|
- Db::execute(
|
|
|
- "ALTER TABLE `" . self::TABLE_FINAL . "` MODIFY COLUMN `final_score` decimal(10,2) DEFAULT NULL COMMENT '最终得分(人工,空=未填)'"
|
|
|
- );
|
|
|
+ // 分数字段允许 NULL:空=未填,列表不显示 0
|
|
|
+ foreach ([
|
|
|
+ 'quality_score' => '商务技术分(人工,空=未填)',
|
|
|
+ 'price_score' => '价格分(人工,空=未填)',
|
|
|
+ 'delivery_score' => '交货分(人工,空=未填)',
|
|
|
+ 'final_score' => '最终得分(人工,空=未填)',
|
|
|
+ ] as $col => $comment) {
|
|
|
+ try {
|
|
|
+ $info = Db::query("SHOW COLUMNS FROM `" . self::TABLE_FINAL . "` LIKE '{$col}'");
|
|
|
+ if (is_array($info) && $info !== []) {
|
|
|
+ $null = strtoupper((string)($info[0]['Null'] ?? ''));
|
|
|
+ if ($null !== 'YES') {
|
|
|
+ Db::execute(
|
|
|
+ "ALTER TABLE `" . self::TABLE_FINAL . "` MODIFY COLUMN `{$col}` decimal(10,2) DEFAULT NULL COMMENT '{$comment}'"
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::write("supplier monthly {$col} null: " . $e->getMessage(), 'error');
|
|
|
}
|
|
|
- } catch (\Throwable $e) {
|
|
|
- Log::write('supplier monthly final_score null: ' . $e->getMessage(), 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 规范化评分等级:空 / A / B / C / D */
|
|
|
+ public static function normalizeScoreGrade($raw): string
|
|
|
+ {
|
|
|
+ $g = strtoupper(trim((string)$raw));
|
|
|
+ if (in_array($g, ['A', 'B', 'C', 'D'], true)) {
|
|
|
+ return $g;
|
|
|
+ }
|
|
|
+
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 按订单明细重算并写入某月月度总分(保留已填最终得分)
|
|
|
+ * 按订单明细重算并写入某月评审记录(商务技术分/价格分;保留已填最终得分)
|
|
|
*
|
|
|
* @param array<int, string>|null $onlyCompanies 仅同步这些供应商;null=当月全部
|
|
|
*/
|
|
|
@@ -136,7 +168,9 @@ class ProcuremenSupplierScore
|
|
|
self::ensureSchema();
|
|
|
self::ensureFinalScoreTable();
|
|
|
try {
|
|
|
- $query = Db::table(self::TABLE_SCORE)->where('ym', $ym)->field('company_name,score');
|
|
|
+ $query = Db::table(self::TABLE_SCORE)
|
|
|
+ ->where('ym', $ym)
|
|
|
+ ->field('company_name,score,quality_score,price_score,lead_score');
|
|
|
if (is_array($onlyCompanies) && $onlyCompanies !== []) {
|
|
|
$names = [];
|
|
|
foreach ($onlyCompanies as $n) {
|
|
|
@@ -152,9 +186,30 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
$rows = $query->select();
|
|
|
} catch (\Throwable $e) {
|
|
|
- Log::write('supplier monthly sync read: ' . $e->getMessage(), 'error');
|
|
|
+ // 无 lead_score 列时回退
|
|
|
+ try {
|
|
|
+ $query = Db::table(self::TABLE_SCORE)
|
|
|
+ ->where('ym', $ym)
|
|
|
+ ->field('company_name,score,quality_score,price_score');
|
|
|
+ if (is_array($onlyCompanies) && $onlyCompanies !== []) {
|
|
|
+ $names = [];
|
|
|
+ foreach ($onlyCompanies as $n) {
|
|
|
+ $n = trim((string)$n);
|
|
|
+ if ($n !== '') {
|
|
|
+ $names[$n] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($names === []) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $query->where('company_name', 'in', array_keys($names));
|
|
|
+ }
|
|
|
+ $rows = $query->select();
|
|
|
+ } catch (\Throwable $e2) {
|
|
|
+ Log::write('supplier monthly sync read: ' . $e2->getMessage(), 'error');
|
|
|
|
|
|
- return;
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
if (!is_array($rows)) {
|
|
|
return;
|
|
|
@@ -169,29 +224,54 @@ class ProcuremenSupplierScore
|
|
|
continue;
|
|
|
}
|
|
|
if (!isset($agg[$cn])) {
|
|
|
- $agg[$cn] = ['sum' => 0.0, 'cnt' => 0];
|
|
|
+ $agg[$cn] = [
|
|
|
+ 'score_sum' => 0.0,
|
|
|
+ 'quality_sum' => 0.0,
|
|
|
+ 'price_sum' => 0.0,
|
|
|
+ 'delivery_sum' => 0.0,
|
|
|
+ 'cnt' => 0,
|
|
|
+ ];
|
|
|
}
|
|
|
- $agg[$cn]['sum'] += (float)($r['score'] ?? 0);
|
|
|
+ $agg[$cn]['score_sum'] += (float)($r['score'] ?? 0);
|
|
|
+ $agg[$cn]['quality_sum'] += (float)($r['quality_score'] ?? 0);
|
|
|
+ $agg[$cn]['price_sum'] += (float)($r['price_score'] ?? 0);
|
|
|
+ $agg[$cn]['delivery_sum'] += (float)($r['lead_score'] ?? 0);
|
|
|
$agg[$cn]['cnt']++;
|
|
|
}
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
foreach ($agg as $cn => $a) {
|
|
|
- $avg = $a['cnt'] > 0 ? round($a['sum'] / $a['cnt'], 2) : 0.0;
|
|
|
+ $cnt = (int)$a['cnt'];
|
|
|
+ $avgScore = $cnt > 0 ? round($a['score_sum'] / $cnt, 2) : 0.0;
|
|
|
+ $avgQuality = $cnt > 0 ? round($a['quality_sum'] / $cnt, 2) : 0.0;
|
|
|
+ $avgPrice = $cnt > 0 ? round($a['price_sum'] / $cnt, 2) : 0.0;
|
|
|
+ $avgDelivery = $cnt > 0 ? round($a['delivery_sum'] / $cnt, 2) : 0.0;
|
|
|
try {
|
|
|
$exists = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
if (is_array($exists) && $exists !== []) {
|
|
|
- Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update([
|
|
|
- 'score' => $avg,
|
|
|
+ $upd = [
|
|
|
+ 'score' => $avgScore,
|
|
|
'updatetime' => $now,
|
|
|
- ]);
|
|
|
+ ];
|
|
|
+ // 人工改过商务技术分/价格分/交货分后,同步不再覆盖
|
|
|
+ if ((int)($exists['qp_manual'] ?? 0) !== 1) {
|
|
|
+ $upd['quality_score'] = $avgQuality;
|
|
|
+ $upd['price_score'] = $avgPrice;
|
|
|
+ $upd['delivery_score'] = $avgDelivery;
|
|
|
+ }
|
|
|
+ Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update($upd);
|
|
|
} else {
|
|
|
Db::table(self::TABLE_FINAL)->insert([
|
|
|
- 'ym' => $ym,
|
|
|
- 'company_name' => $cn,
|
|
|
- 'score' => $avg,
|
|
|
- 'final_score' => null,
|
|
|
- 'createtime' => $now,
|
|
|
- 'updatetime' => $now,
|
|
|
+ 'ym' => $ym,
|
|
|
+ 'company_name' => $cn,
|
|
|
+ 'score' => $avgScore,
|
|
|
+ 'quality_score' => $avgQuality,
|
|
|
+ 'price_score' => $avgPrice,
|
|
|
+ 'delivery_score' => $avgDelivery,
|
|
|
+ 'final_score' => null,
|
|
|
+ 'score_grade' => '',
|
|
|
+ 'qp_manual' => 0,
|
|
|
+ 'createtime' => $now,
|
|
|
+ 'updatetime' => $now,
|
|
|
]);
|
|
|
}
|
|
|
} catch (\Throwable $e) {
|
|
|
@@ -240,9 +320,135 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 读取某月月度汇总(总分 + 最终得分)
|
|
|
+ * 读取库中可选分数:NULL/空串 → null;合法数字 → float
|
|
|
+ *
|
|
|
+ * @param mixed $raw
|
|
|
+ */
|
|
|
+ public static function readOptionalScoreValue($raw): ?float
|
|
|
+ {
|
|
|
+ if ($raw === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (is_string($raw) && trim($raw) === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!is_numeric($raw)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return round((float)$raw, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 可选分数展示:空不显示 0
|
|
|
+ *
|
|
|
+ * @param float|null $n
|
|
|
+ */
|
|
|
+ public static function formatOptionalScore(?float $n): string
|
|
|
+ {
|
|
|
+ if ($n === null) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ return self::formatScore($n);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开标后把本月询价供应商名称写入月度评审表:不存在则插入空行,已存在不改(分数人工填)
|
|
|
+ *
|
|
|
+ * @param array<int, string> $companyNames
|
|
|
+ * @return int 新插入条数
|
|
|
+ */
|
|
|
+ public static function ensureMonthlySupplierNames(string $ym, array $companyNames): int
|
|
|
+ {
|
|
|
+ $ym = trim($ym);
|
|
|
+ if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ $uniq = [];
|
|
|
+ foreach ($companyNames as $n) {
|
|
|
+ $n = trim((string)$n);
|
|
|
+ if ($n !== '') {
|
|
|
+ $uniq[$n] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($uniq === []) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ self::ensureFinalScoreTable();
|
|
|
+ $now = date('Y-m-d H:i:s');
|
|
|
+ $inserted = 0;
|
|
|
+ foreach (array_keys($uniq) as $cn) {
|
|
|
+ try {
|
|
|
+ $exists = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
+ if (is_array($exists) && $exists !== []) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Db::table(self::TABLE_FINAL)->insert([
|
|
|
+ 'ym' => $ym,
|
|
|
+ 'company_name' => $cn,
|
|
|
+ 'score' => 0,
|
|
|
+ 'quality_score' => null,
|
|
|
+ 'price_score' => null,
|
|
|
+ 'delivery_score' => null,
|
|
|
+ 'final_score' => null,
|
|
|
+ 'score_grade' => '',
|
|
|
+ 'qp_manual' => 0,
|
|
|
+ 'createtime' => $now,
|
|
|
+ 'updatetime' => $now,
|
|
|
+ ]);
|
|
|
+ $inserted++;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ // 并发下唯一键冲突视为已存在
|
|
|
+ if (stripos($e->getMessage(), 'Duplicate') === false) {
|
|
|
+ Log::write('supplier monthly ensure name: ' . $e->getMessage(), 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $inserted;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按当月订单评分表中的供应商,补齐月度评审表名称(仅新增空行)
|
|
|
+ */
|
|
|
+ public static function ensureMonthlySupplierNamesFromOrderYm(string $ym): void
|
|
|
+ {
|
|
|
+ $ym = trim($ym);
|
|
|
+ if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ self::ensureSchema();
|
|
|
+ self::ensureFinalScoreTable();
|
|
|
+ try {
|
|
|
+ $rows = Db::table(self::TABLE_SCORE)
|
|
|
+ ->where('ym', $ym)
|
|
|
+ ->field('company_name')
|
|
|
+ ->group('company_name')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!is_array($rows) || $rows === []) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $names = [];
|
|
|
+ foreach ($rows as $r) {
|
|
|
+ if (!is_array($r)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $cn = trim((string)($r['company_name'] ?? ''));
|
|
|
+ if ($cn !== '') {
|
|
|
+ $names[] = $cn;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ self::ensureMonthlySupplierNames($ym, $names);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 读取某月评审记录(仅读月度表;先按当月开标/询价供应商补齐名称)
|
|
|
*
|
|
|
- * @return array<string, array{score:float,final_score:?float,final_saved:int}>
|
|
|
+ * @return array<string, array{score:float,quality_score:?float,price_score:?float,delivery_score:?float,final_score:?float,final_saved:int,score_grade:string}>
|
|
|
*/
|
|
|
public static function loadMonthlyMapByYm(string $ym): array
|
|
|
{
|
|
|
@@ -251,9 +457,13 @@ class ProcuremenSupplierScore
|
|
|
return [];
|
|
|
}
|
|
|
self::ensureFinalScoreTable();
|
|
|
- self::syncMonthlyScoresForYm($ym);
|
|
|
+ // 本月询价(开标后已写入订单评分表)的供应商:名称不存在则加空行,已存在不动
|
|
|
+ self::ensureMonthlySupplierNamesFromOrderYm($ym);
|
|
|
try {
|
|
|
- $rows = Db::table(self::TABLE_FINAL)->where('ym', $ym)->field('company_name,score,final_score')->select();
|
|
|
+ $rows = Db::table(self::TABLE_FINAL)
|
|
|
+ ->where('ym', $ym)
|
|
|
+ ->field('company_name,score,quality_score,price_score,delivery_score,final_score,score_grade')
|
|
|
+ ->select();
|
|
|
} catch (\Throwable $e) {
|
|
|
return [];
|
|
|
}
|
|
|
@@ -269,11 +479,18 @@ class ProcuremenSupplierScore
|
|
|
if ($cn === '') {
|
|
|
continue;
|
|
|
}
|
|
|
- $hasFinal = array_key_exists('final_score', $r) && $r['final_score'] !== null && $r['final_score'] !== '';
|
|
|
+ $quality = self::readOptionalScoreValue($r['quality_score'] ?? null);
|
|
|
+ $price = self::readOptionalScoreValue($r['price_score'] ?? null);
|
|
|
+ $delivery = self::readOptionalScoreValue($r['delivery_score'] ?? null);
|
|
|
+ $final = self::readOptionalScoreValue($r['final_score'] ?? null);
|
|
|
$out[$cn] = [
|
|
|
- 'score' => round((float)($r['score'] ?? 0), 2),
|
|
|
- 'final_score' => $hasFinal ? round((float)$r['final_score'], 2) : null,
|
|
|
- 'final_saved' => $hasFinal ? 1 : 0,
|
|
|
+ 'score' => round((float)($r['score'] ?? 0), 2),
|
|
|
+ 'quality_score' => $quality,
|
|
|
+ 'price_score' => $price,
|
|
|
+ 'delivery_score' => $delivery,
|
|
|
+ 'final_score' => $final,
|
|
|
+ 'final_saved' => $final !== null ? 1 : 0,
|
|
|
+ 'score_grade' => self::normalizeScoreGrade($r['score_grade'] ?? ''),
|
|
|
];
|
|
|
}
|
|
|
|
|
|
@@ -281,9 +498,30 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 批量保存某月最终得分(不覆盖月度总分 score)
|
|
|
+ * 解析可选分数:空=null;非法=false;合法=float
|
|
|
*
|
|
|
- * @param array<int, array{company_name?:string,final_score?:mixed}> $items
|
|
|
+ * @param mixed $raw
|
|
|
+ * @return float|null|false
|
|
|
+ */
|
|
|
+ protected static function parseOptionalScore($raw)
|
|
|
+ {
|
|
|
+ if ($raw === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (is_string($raw) && trim($raw) === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!is_numeric($raw)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return round((float)$raw, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量保存某月评审分:商务技术分 / 价格分 / 交货分 / 最终得分 / 评分等级
|
|
|
+ *
|
|
|
+ * @param array<int, array{company_name?:string,quality_score?:mixed,price_score?:mixed,delivery_score?:mixed,final_score?:mixed,score_grade?:mixed}> $items
|
|
|
* @return array{done:int,saved:int,cleared:int,error:string}
|
|
|
*/
|
|
|
public static function saveFinalScoresForYm(string $ym, array $items): array
|
|
|
@@ -317,56 +555,54 @@ class ProcuremenSupplierScore
|
|
|
if ($cn === '') {
|
|
|
continue;
|
|
|
}
|
|
|
- $raw = $it['final_score'] ?? '';
|
|
|
- if ($raw === null || (is_string($raw) && trim($raw) === '')) {
|
|
|
- try {
|
|
|
- $exists = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
- if (is_array($exists) && $exists !== []) {
|
|
|
- Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update([
|
|
|
- 'final_score' => null,
|
|
|
- 'updatetime' => $now,
|
|
|
- ]);
|
|
|
- }
|
|
|
- $result['cleared']++;
|
|
|
- $result['done']++;
|
|
|
- } catch (\Throwable $e) {
|
|
|
- Log::write('supplier final score clear: ' . $e->getMessage(), 'error');
|
|
|
- $result['error'] = $e->getMessage();
|
|
|
- }
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (!is_numeric($raw)) {
|
|
|
+ $grade = self::normalizeScoreGrade($it['score_grade'] ?? '');
|
|
|
+ $quality = self::parseOptionalScore($it['quality_score'] ?? null);
|
|
|
+ $price = self::parseOptionalScore($it['price_score'] ?? null);
|
|
|
+ $delivery = self::parseOptionalScore($it['delivery_score'] ?? null);
|
|
|
+ $final = self::parseOptionalScore($it['final_score'] ?? null);
|
|
|
+ if ($quality === false || $price === false || $delivery === false || $final === false) {
|
|
|
continue;
|
|
|
}
|
|
|
- $final = round((float)$raw, 2);
|
|
|
+ $hasQp = array_key_exists('quality_score', $it)
|
|
|
+ || array_key_exists('price_score', $it)
|
|
|
+ || array_key_exists('delivery_score', $it);
|
|
|
try {
|
|
|
$exists = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
+ $upd = [
|
|
|
+ 'final_score' => $final,
|
|
|
+ 'score_grade' => $grade,
|
|
|
+ 'updatetime' => $now,
|
|
|
+ ];
|
|
|
+ if ($hasQp) {
|
|
|
+ // 空值存 NULL,列表显示空白而不是 0
|
|
|
+ $upd['quality_score'] = $quality;
|
|
|
+ $upd['price_score'] = $price;
|
|
|
+ $upd['delivery_score'] = $delivery;
|
|
|
+ $upd['score'] = round(($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0), 2);
|
|
|
+ $upd['qp_manual'] = 1;
|
|
|
+ }
|
|
|
if (is_array($exists) && $exists !== []) {
|
|
|
- Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update([
|
|
|
- 'final_score' => $final,
|
|
|
- 'updatetime' => $now,
|
|
|
+ Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update($upd);
|
|
|
+ } else {
|
|
|
+ Db::table(self::TABLE_FINAL)->insert([
|
|
|
+ 'ym' => $ym,
|
|
|
+ 'company_name' => $cn,
|
|
|
+ 'score' => $hasQp ? round(($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0), 2) : 0,
|
|
|
+ 'quality_score' => $hasQp ? $quality : null,
|
|
|
+ 'price_score' => $hasQp ? $price : null,
|
|
|
+ 'delivery_score' => $hasQp ? $delivery : null,
|
|
|
+ 'final_score' => $final,
|
|
|
+ 'score_grade' => $grade,
|
|
|
+ 'qp_manual' => $hasQp ? 1 : 0,
|
|
|
+ 'createtime' => $now,
|
|
|
+ 'updatetime' => $now,
|
|
|
]);
|
|
|
+ }
|
|
|
+ if ($final === null && $grade === '' && !$hasQp) {
|
|
|
+ $result['cleared']++;
|
|
|
} else {
|
|
|
- // 无月度行时先按订单回填总分,再写入最终得分
|
|
|
- self::syncMonthlyScoresForYm($ym, [$cn]);
|
|
|
- $exists2 = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
- if (is_array($exists2) && $exists2 !== []) {
|
|
|
- Db::table(self::TABLE_FINAL)->where('id', (int)($exists2['id'] ?? 0))->update([
|
|
|
- 'final_score' => $final,
|
|
|
- 'updatetime' => $now,
|
|
|
- ]);
|
|
|
- } else {
|
|
|
- Db::table(self::TABLE_FINAL)->insert([
|
|
|
- 'ym' => $ym,
|
|
|
- 'company_name' => $cn,
|
|
|
- 'score' => 0,
|
|
|
- 'final_score' => $final,
|
|
|
- 'createtime' => $now,
|
|
|
- 'updatetime' => $now,
|
|
|
- ]);
|
|
|
- }
|
|
|
+ $result['saved']++;
|
|
|
}
|
|
|
- $result['saved']++;
|
|
|
$result['done']++;
|
|
|
} catch (\Throwable $e) {
|
|
|
Log::write('supplier final score save: ' . $e->getMessage(), 'error');
|
|
|
@@ -440,7 +676,6 @@ class ProcuremenSupplierScore
|
|
|
self::TABLE_SCORE => [
|
|
|
'quality_score' => "int NOT NULL DEFAULT 0 COMMENT '上年度质量评分'",
|
|
|
'quality_weight' => "int unsigned NOT NULL DEFAULT 50 COMMENT '质量百分比'",
|
|
|
- 'price_sum' => "int NOT NULL DEFAULT 0 COMMENT '本单工序单价合计'",
|
|
|
'price_weight' => "int unsigned NOT NULL DEFAULT 50 COMMENT '价格百分比'",
|
|
|
'lead_days_sum' => "int NOT NULL DEFAULT 0 COMMENT '本单工序工期合计(天)'",
|
|
|
'lead_weight' => "int unsigned NOT NULL DEFAULT 0 COMMENT '交货期百分比'",
|
|
|
@@ -463,6 +698,20 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ // 单价合计需保留小数(如 0.25),不可用 int
|
|
|
+ try {
|
|
|
+ $info = Db::query("SHOW COLUMNS FROM `" . self::TABLE_SCORE . "` LIKE 'price_sum'");
|
|
|
+ if (is_array($info) && $info !== []) {
|
|
|
+ $type = strtolower((string)($info[0]['Type'] ?? $info[0]['type'] ?? ''));
|
|
|
+ if (preg_match('/^(tiny|small|medium|big)?int\b/', $type)) {
|
|
|
+ Db::execute(
|
|
|
+ "ALTER TABLE `" . self::TABLE_SCORE . "` MODIFY COLUMN `price_sum` decimal(12,4) NOT NULL DEFAULT 0.0000 COMMENT '本单工序单价合计'"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::write('supplier score price_sum decimal: ' . $e->getMessage(), 'error');
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
protected static function ensureDefaultRule(): void
|
|
|
@@ -540,7 +789,92 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 按公司名批量取供应商 id / 综合评分 / 月度评分
|
|
|
+ * 规范年月;空则取当前月
|
|
|
+ */
|
|
|
+ public static function normalizeYm(?string $ym): string
|
|
|
+ {
|
|
|
+ $ym = trim((string)$ym);
|
|
|
+ if (preg_match('/^\d{4}-\d{2}$/', $ym)) {
|
|
|
+ return $ym;
|
|
|
+ }
|
|
|
+
|
|
|
+ return date('Y-m');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上一自然月 YYYY-MM
|
|
|
+ */
|
|
|
+ public static function previousYm(string $ym): string
|
|
|
+ {
|
|
|
+ $ym = self::normalizeYm($ym);
|
|
|
+ $ts = strtotime($ym . '-01');
|
|
|
+ if ($ts === false) {
|
|
|
+ $ts = time();
|
|
|
+ }
|
|
|
+
|
|
|
+ return date('Y-m', strtotime('-1 month', $ts));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从月度评审表取商务技术分:当月按上月起往前查,都没有则默认 50
|
|
|
+ *
|
|
|
+ * @param array<int, string> $companyNames
|
|
|
+ * @return array<string, float> company_name => quality_score
|
|
|
+ */
|
|
|
+ public static function loadReviewQualityScoreMap(array $companyNames, ?string $asOfYm = null): array
|
|
|
+ {
|
|
|
+ $asOfYm = self::normalizeYm($asOfYm);
|
|
|
+ $default = 50.0;
|
|
|
+ $map = [];
|
|
|
+ $names = [];
|
|
|
+ foreach ($companyNames as $n) {
|
|
|
+ $n = trim((string)$n);
|
|
|
+ if ($n !== '') {
|
|
|
+ $names[$n] = true;
|
|
|
+ $map[$n] = $default;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($names === []) {
|
|
|
+ return $map;
|
|
|
+ }
|
|
|
+ self::ensureFinalScoreTable();
|
|
|
+ try {
|
|
|
+ // 当月按上个月起查:ym < asOfYm,且商务技术分已填写
|
|
|
+ $rows = Db::table(self::TABLE_FINAL)
|
|
|
+ ->where('company_name', 'in', array_keys($names))
|
|
|
+ ->where('ym', '<', $asOfYm)
|
|
|
+ ->whereNotNull('quality_score')
|
|
|
+ ->field('company_name,ym,quality_score')
|
|
|
+ ->order('ym', 'desc')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return $map;
|
|
|
+ }
|
|
|
+ if (!is_array($rows)) {
|
|
|
+ return $map;
|
|
|
+ }
|
|
|
+ $seen = [];
|
|
|
+ foreach ($rows as $r) {
|
|
|
+ if (!is_array($r)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $cn = trim((string)($r['company_name'] ?? ''));
|
|
|
+ if ($cn === '' || isset($seen[$cn])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $score = self::readOptionalScoreValue($r['quality_score'] ?? null);
|
|
|
+ if ($score === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $seen[$cn] = true;
|
|
|
+ $map[$cn] = round((float)$score, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按公司名批量取供应商 id(兼容旧调用保留评分字段结构)
|
|
|
*
|
|
|
* @param array<int, string> $companyNames
|
|
|
* @return array<string, array{id:int,score:float,monthly_score:?float,quality_avg:float}>
|
|
|
@@ -561,18 +895,10 @@ class ProcuremenSupplierScore
|
|
|
try {
|
|
|
$rows = Db::table('customer')
|
|
|
->where('company_name', 'in', array_keys($names))
|
|
|
- ->field('id,company_name,score,monthly_score')
|
|
|
+ ->field('id,company_name')
|
|
|
->select();
|
|
|
} catch (\Throwable $e) {
|
|
|
- // 无 monthly_score 列时回退只取综合评分
|
|
|
- try {
|
|
|
- $rows = Db::table('customer')
|
|
|
- ->where('company_name', 'in', array_keys($names))
|
|
|
- ->field('id,company_name,score')
|
|
|
- ->select();
|
|
|
- } catch (\Throwable $e2) {
|
|
|
- return $map;
|
|
|
- }
|
|
|
+ return $map;
|
|
|
}
|
|
|
if (!is_array($rows)) {
|
|
|
return $map;
|
|
|
@@ -585,27 +911,11 @@ class ProcuremenSupplierScore
|
|
|
if ($cn === '') {
|
|
|
continue;
|
|
|
}
|
|
|
- $compRaw = $r['score'] ?? null;
|
|
|
- $monthRaw = $r['monthly_score'] ?? null;
|
|
|
- $hasComp = !($compRaw === null || $compRaw === '');
|
|
|
- $hasMonth = !($monthRaw === null || $monthRaw === '');
|
|
|
- $comp = $hasComp ? (float)$compRaw : null;
|
|
|
- $month = $hasMonth ? (float)$monthRaw : null;
|
|
|
- if ($hasComp && $hasMonth) {
|
|
|
- $avg = ((float)$comp + (float)$month) / 2;
|
|
|
- } elseif ($hasComp) {
|
|
|
- $avg = (float)$comp;
|
|
|
- } elseif ($hasMonth) {
|
|
|
- $avg = (float)$month;
|
|
|
- } else {
|
|
|
- $avg = 0.0;
|
|
|
- }
|
|
|
$map[$cn] = [
|
|
|
'id' => (int)($r['id'] ?? 0),
|
|
|
- 'score' => $hasComp ? (float)$comp : 0.0,
|
|
|
- 'monthly_score' => $hasMonth ? (float)$month : null,
|
|
|
- // 商务/技术得分:综合分与月度分平均值
|
|
|
- 'quality_avg' => round($avg, 2),
|
|
|
+ 'score' => 0.0,
|
|
|
+ 'monthly_score' => null,
|
|
|
+ 'quality_avg' => 50.0,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
@@ -616,12 +926,13 @@ class ProcuremenSupplierScore
|
|
|
* 从报价组计算本单各供应商得分(不写库)
|
|
|
*
|
|
|
* @param array<int, array<string, mixed>> $quoteGroups loadAuditSupplierQuoteGroups 结构(name/lines[].amount)
|
|
|
+ * @param string|null $asOfYm 订单所属月;商务/技术分从该月的上月起往前查评审表
|
|
|
* @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
|
|
|
+ public static function calculateForQuoteGroups(array $quoteGroups, ?array $rule = null, ?string $asOfYm = null): array
|
|
|
{
|
|
|
$rule = $rule ?: self::getDefaultRule();
|
|
|
$qw = max(0, (float)($rule['quality_weight'] ?? 50));
|
|
|
@@ -649,6 +960,7 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
}
|
|
|
$custMap = self::loadCustomerScoreMap($names);
|
|
|
+ $reviewMap = self::loadReviewQualityScoreMap($names, $asOfYm);
|
|
|
|
|
|
$items = [];
|
|
|
$priceSums = [];
|
|
|
@@ -692,7 +1004,8 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- $quality = (float)($custMap[$cn]['quality_avg'] ?? 0);
|
|
|
+ // 商务/技术得分:月度评审「商务技术分」,当月按上月起往前,默认 50
|
|
|
+ $quality = (float)($reviewMap[$cn] ?? 50);
|
|
|
$psum = $hasPrice ? round($priceSum, 2) : 0;
|
|
|
$lsum = $hasLead ? (int)$leadSum : 0;
|
|
|
$items[$cn] = [
|
|
|
@@ -740,28 +1053,30 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
}
|
|
|
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);
|
|
|
+ // 价格得分 = (最低单价合计 / 本供应商单价合计) × 价格权重% × 100
|
|
|
+ if ($pw > 0 && !empty($it['has_price']) && $it['price_sum'] > 0 && $minSum !== null && $minSum > 0) {
|
|
|
+ $ratio = round($minSum / (float)$it['price_sum'], 4);
|
|
|
+ $it['price_score'] = round($ratio * ($pw / 100) * 100, 2);
|
|
|
} else {
|
|
|
$it['price_score'] = 0.0;
|
|
|
}
|
|
|
$it['price_score_text'] = self::formatScore($it['price_score']);
|
|
|
|
|
|
if ($leadEnabled && !empty($it['has_lead']) && $it['lead_days_sum'] > 0 && $minLead !== null && $minLead > 0) {
|
|
|
- $it['lead_score'] = round(($minLead / $it['lead_days_sum']) * 100, 2);
|
|
|
+ $leadRatio = round($minLead / (float)$it['lead_days_sum'], 4);
|
|
|
+ $it['lead_score'] = round($leadRatio * 100, 2);
|
|
|
} else {
|
|
|
$it['lead_score'] = 0.0;
|
|
|
}
|
|
|
$it['lead_score_text'] = self::formatScore($it['lead_score']);
|
|
|
|
|
|
- // 总分 = 商务/技术得分×质量% + 工序单价合计×价格% [+ 交货期分×交货%]
|
|
|
+ // 总分 = 商务/技术得分×质量% + 价格得分(已含价格权重) [+ 交货得分×交货%]
|
|
|
$total = 0.0;
|
|
|
if ($qw > 0) {
|
|
|
$total += $it['quality_score'] * ($qw / 100);
|
|
|
}
|
|
|
if ($pw > 0) {
|
|
|
- $total += (float)$it['price_sum'] * ($pw / 100);
|
|
|
+ $total += (float)$it['price_score'];
|
|
|
}
|
|
|
if ($lw > 0) {
|
|
|
$total += $it['lead_score'] * ($lw / 100);
|
|
|
@@ -824,32 +1139,38 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 总分留痕文案:59 = (100×50%)+(18×50%)
|
|
|
- * 权重大于 0 的项才写入;数值取商务/技术得分、工序单价合计(及交货期分)
|
|
|
+ * 总分简要文案(仅总分数字;分项见独立字段)
|
|
|
*
|
|
|
* @param array<string, mixed> $hit calculate/loadSaved 单项
|
|
|
*/
|
|
|
public static function formatScoreDetailText(array $hit): string
|
|
|
{
|
|
|
- $scoreText = self::formatScore($hit['score'] ?? 0);
|
|
|
+ return self::formatScore($hit['score'] ?? 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分项得分展示文案(确认/详情表格用)
|
|
|
+ *
|
|
|
+ * @param array<string, mixed> $hit
|
|
|
+ */
|
|
|
+ public static function formatScorePartsHtml(array $hit): string
|
|
|
+ {
|
|
|
$qw = (float)($hit['quality_weight'] ?? 0);
|
|
|
$pw = (float)($hit['price_weight'] ?? 0);
|
|
|
$lw = (float)($hit['lead_weight'] ?? 0);
|
|
|
- $parts = [];
|
|
|
+ $lines = [];
|
|
|
if ($qw > 0) {
|
|
|
- $parts[] = '(' . self::formatScore($hit['quality_score'] ?? 0) . '×' . self::formatWeightPercent($qw) . '%)';
|
|
|
+ $lines[] = '商务/技术 ' . self::formatScore($hit['quality_score'] ?? 0);
|
|
|
}
|
|
|
if ($pw > 0) {
|
|
|
- $parts[] = '(' . self::formatScore($hit['price_sum'] ?? 0) . '×' . self::formatWeightPercent($pw) . '%)';
|
|
|
+ $lines[] = '价格得分 ' . self::formatScore($hit['price_score'] ?? 0);
|
|
|
}
|
|
|
if ($lw > 0) {
|
|
|
- $parts[] = '(' . self::formatScore($hit['lead_score'] ?? 0) . '×' . self::formatWeightPercent($lw) . '%)';
|
|
|
- }
|
|
|
- if ($parts === []) {
|
|
|
- return $scoreText;
|
|
|
+ $lines[] = '交货得分 ' . self::formatScore($hit['lead_score'] ?? 0);
|
|
|
}
|
|
|
+ $lines[] = '总分 ' . self::formatScore($hit['score'] ?? 0);
|
|
|
|
|
|
- return $scoreText . ' = ' . implode('+', $parts);
|
|
|
+ return implode("\n", $lines);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -869,16 +1190,21 @@ class ProcuremenSupplierScore
|
|
|
$parts[] = '(商务/技术得分×' . self::formatWeightPercent($qw) . '%)';
|
|
|
}
|
|
|
if ($pw > 0) {
|
|
|
- $parts[] = '(工序单价合计×' . self::formatWeightPercent($pw) . '%)';
|
|
|
+ $parts[] = '(价格得分)';
|
|
|
}
|
|
|
if ($lw > 0) {
|
|
|
$parts[] = '(交货得分×' . self::formatWeightPercent($lw) . '%)';
|
|
|
}
|
|
|
- if ($parts === []) {
|
|
|
- return '(总分)=(商务/技术得分×50%)+(工序单价合计×50%)';
|
|
|
+ $formula = $parts === []
|
|
|
+ ? '(总分)=(商务/技术得分×50%)+(价格得分)'
|
|
|
+ : '(总分)=' . implode('+', $parts);
|
|
|
+ $formula .= ';商务/技术得分取月度评审「商务/技术得分」(当月按上月起往前查,无则默认50)';
|
|
|
+ if ($pw > 0) {
|
|
|
+ $formula .= ';价格得分=(最低单价合计/本供应商单价合计)×'
|
|
|
+ . self::formatWeightPercent($pw) . '%×100';
|
|
|
}
|
|
|
|
|
|
- return '(总分)=' . implode('+', $parts);
|
|
|
+ return $formula;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -939,13 +1265,14 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
self::ensureSchema();
|
|
|
$rule = self::getDefaultRule();
|
|
|
- $calc = self::calculateForQuoteGroups($quoteGroups, $rule);
|
|
|
- if ($calc === []) {
|
|
|
- return;
|
|
|
- }
|
|
|
if ($ym === null || $ym === '') {
|
|
|
$ym = date('Y-m');
|
|
|
}
|
|
|
+ $ym = self::normalizeYm($ym);
|
|
|
+ $calc = self::calculateForQuoteGroups($quoteGroups, $rule, $ym);
|
|
|
+ if ($calc === []) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
try {
|
|
|
Db::table(self::TABLE_SCORE)->where('ccydh', $ccydh)->delete();
|
|
|
@@ -960,7 +1287,7 @@ class ProcuremenSupplierScore
|
|
|
'rank_no' => (int)($it['rank_no'] ?? 0),
|
|
|
'quality_score' => (int)round((float)($it['quality_score'] ?? 0)),
|
|
|
'quality_weight' => (int)($rule['quality_weight'] ?? 50),
|
|
|
- 'price_sum' => (int)round((float)($it['price_sum'] ?? 0)),
|
|
|
+ 'price_sum' => round((float)($it['price_sum'] ?? 0), 4),
|
|
|
'price_weight' => (int)($rule['price_weight'] ?? 50),
|
|
|
'price_score' => (float)($it['price_score'] ?? 0),
|
|
|
'lead_days_sum' => (int)round((float)($it['lead_days_sum'] ?? 0)),
|
|
|
@@ -975,15 +1302,24 @@ class ProcuremenSupplierScore
|
|
|
if ($rows !== []) {
|
|
|
Db::table(self::TABLE_SCORE)->insertAll($rows);
|
|
|
}
|
|
|
- // 订单明细变更后,同步刷新当月月度总分
|
|
|
- $companies = [];
|
|
|
+ // 开标后:本月询价供应商名称写入月度评审表(不存在才加空行,已有记录不覆盖)
|
|
|
+ $monthNames = [];
|
|
|
+ foreach ($quoteGroups as $g) {
|
|
|
+ if (!is_array($g)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $cn = trim((string)($g['name'] ?? $g['company_name'] ?? ''));
|
|
|
+ if ($cn !== '') {
|
|
|
+ $monthNames[] = $cn;
|
|
|
+ }
|
|
|
+ }
|
|
|
foreach ($rows as $row) {
|
|
|
$cn = trim((string)($row['company_name'] ?? ''));
|
|
|
if ($cn !== '') {
|
|
|
- $companies[] = $cn;
|
|
|
+ $monthNames[] = $cn;
|
|
|
}
|
|
|
}
|
|
|
- self::syncMonthlyScoresForYm($ym, $companies !== [] ? $companies : null);
|
|
|
+ self::ensureMonthlySupplierNames($ym, $monthNames);
|
|
|
} catch (\Throwable $e) {
|
|
|
Log::write('supplier service score save: ' . $e->getMessage(), 'error');
|
|
|
}
|
|
|
@@ -1021,7 +1357,7 @@ class ProcuremenSupplierScore
|
|
|
$score = (float)($r['score'] ?? 0);
|
|
|
$rank = (int)($r['rank_no'] ?? 0);
|
|
|
$qualityScore = (int)round((float)($r['quality_score'] ?? 0));
|
|
|
- $priceSum = (int)round((float)($r['price_sum'] ?? 0));
|
|
|
+ $priceSum = round((float)($r['price_sum'] ?? 0), 4);
|
|
|
$priceScore = (float)($r['price_score'] ?? 0);
|
|
|
$qw = (int)($r['quality_weight'] ?? 50);
|
|
|
$pw = (int)($r['price_weight'] ?? 50);
|
|
|
@@ -1036,7 +1372,7 @@ class ProcuremenSupplierScore
|
|
|
'quality_score_text' => (string)$qualityScore,
|
|
|
'quality_weight' => $qw,
|
|
|
'price_sum' => $priceSum,
|
|
|
- 'price_sum_text' => (string)$priceSum,
|
|
|
+ 'price_sum_text' => self::formatScore($priceSum),
|
|
|
'price_weight' => $pw,
|
|
|
'price_score' => $priceScore,
|
|
|
'price_score_text' => self::formatScore($priceScore),
|
|
|
@@ -1067,6 +1403,7 @@ class ProcuremenSupplierScore
|
|
|
$g['service_score'] = '';
|
|
|
$g['service_rank_text'] = '';
|
|
|
$g['service_score_text'] = '';
|
|
|
+ $g['service_score_parts_text'] = '';
|
|
|
$g['service_quality_score'] = '';
|
|
|
$g['service_quality_score_text'] = '';
|
|
|
$g['service_price_sum'] = '';
|
|
|
@@ -1106,9 +1443,10 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
// 每次按最新规则重算并落库,避免历史错误总分残留
|
|
|
- $saved = self::calculateForQuoteGroups($quoteGroups);
|
|
|
+ $ym = date('Y-m');
|
|
|
+ $saved = self::calculateForQuoteGroups($quoteGroups, null, $ym);
|
|
|
try {
|
|
|
- self::saveForOrder($ccydh, $quoteGroups);
|
|
|
+ self::saveForOrder($ccydh, $quoteGroups, $ym);
|
|
|
} catch (\Throwable $e) {
|
|
|
}
|
|
|
foreach ($quoteGroups as &$g) {
|
|
|
@@ -1122,6 +1460,7 @@ class ProcuremenSupplierScore
|
|
|
$g['service_score'] = (float)($hit['score'] ?? 0);
|
|
|
$g['service_rank_text'] = (string)($hit['rank_text'] ?? '');
|
|
|
$g['service_score_text'] = self::formatScoreDetailText($hit);
|
|
|
+ $g['service_score_parts_text'] = self::formatScorePartsHtml($hit);
|
|
|
$qs = round((float)($hit['quality_score'] ?? 0), 2);
|
|
|
$ps = (float)($hit['price_score'] ?? 0);
|
|
|
$psum = round((float)($hit['price_sum'] ?? 0), 2);
|
|
|
@@ -1129,7 +1468,7 @@ class ProcuremenSupplierScore
|
|
|
$ls = (float)($hit['lead_score'] ?? 0);
|
|
|
$lsum = (int)round((float)($hit['lead_days_sum'] ?? 0));
|
|
|
$g['service_quality_score'] = $qs;
|
|
|
- $g['service_quality_score_text'] = (string)($hit['quality_score_text'] ?? $qs);
|
|
|
+ $g['service_quality_score_text'] = (string)($hit['quality_score_text'] ?? self::formatScore($qs));
|
|
|
$g['service_price_sum'] = $psum;
|
|
|
$g['service_price_sum_text'] = (string)($hit['price_sum_text'] ?? $psum);
|
|
|
$g['service_price_score'] = $ps;
|