| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- <?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();
- self::ensureIntegerColumns();
- 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();
- self::ensureIntegerColumns();
- }
- /** 历史表补质量/价格百分比字段 */
- 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` int unsigned NOT NULL DEFAULT 50 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` int unsigned NOT NULL DEFAULT 50 COMMENT '价格百分比' AFTER `price_sum`"
- );
- }
- } catch (\Throwable $e) {
- Log::write('supplier score weight columns: ' . $e->getMessage(), 'error');
- }
- }
- /** 将权重/质量分/单价合计等字段改为整数(去掉 .00) */
- protected static function ensureIntegerColumns(): void
- {
- $defs = [
- self::TABLE_RULE => [
- 'quality_weight' => "int unsigned NOT NULL DEFAULT 50 COMMENT '上年度质量评分权重(%)'",
- 'price_weight' => "int unsigned NOT NULL DEFAULT 50 COMMENT '单价评分权重(%)'",
- ],
- 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 '价格百分比'",
- ],
- ];
- foreach ($defs as $table => $cols) {
- foreach ($cols as $col => $def) {
- try {
- $info = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
- if (!is_array($info) || $info === []) {
- continue;
- }
- $type = strtolower((string)($info[0]['Type'] ?? $info[0]['type'] ?? ''));
- if (preg_match('/^(tiny|small|medium|big)?int\b/', $type)) {
- continue;
- }
- Db::execute("ALTER TABLE `{$table}` MODIFY COLUMN `{$col}` {$def}");
- } catch (\Throwable $e) {
- Log::write("supplier score int column {$table}.{$col}: " . $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:int,price_weight:int}
- */
- 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' => (int)($row['quality_weight'] ?? 50),
- 'price_weight' => (int)($row['price_weight'] ?? 50),
- ];
- }
- } catch (\Throwable $e) {
- }
- return [
- 'id' => 0,
- 'name' => '内置默认',
- 'quality_weight' => 50,
- 'price_weight' => 50,
- ];
- }
- /**
- * 按公司名批量取供应商 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 : (int)round((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 = (int)round((float)($custMap[$cn]['score'] ?? 0));
- $items[$cn] = [
- 'company_name' => $cn,
- 'customer_id' => (int)($custMap[$cn]['id'] ?? 0),
- 'quality_score' => $quality,
- 'price_sum' => $hasPrice ? (int)round($priceSum) : 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' => (int)round((float)($it['quality_score'] ?? 0)),
- 'quality_weight' => (int)($rule['quality_weight'] ?? 50),
- 'price_sum' => (int)round((float)($it['price_sum'] ?? 0)),
- 'price_weight' => (int)($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;
- }
- }
|