ProductionInformationStatistics.php 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use think\Db;
  5. use think\Exception;
  6. use think\Log;
  7. /**
  8. * 设备生产信息统计
  9. */
  10. class ProductionInformationStatistics extends Api
  11. {
  12. protected $noNeedLogin = ['*'];
  13. protected $noNeedRight = ['*'];
  14. /** @var string 主表 */
  15. protected $tableName = '设备_生产信息统计表';
  16. /** @var string 日志表 */
  17. protected $logTableName = '设备_生产信息统计操作日志';
  18. /** @var array 固定班组 */
  19. protected $defaultShifts = ['A班', 'B班'];
  20. /** @var string 设备基本资料表 */
  21. protected $equipmentTable = '设备_基本资料';
  22. /** @var array 可编辑业务字段 */
  23. protected $dataFields = [
  24. 'sczl_jtbh',
  25. 'sczl_bzdh',
  26. 'sczl_rq',
  27. 'theoretical_speed',
  28. 'scheduled_production',
  29. 'scheduled_maintenance',
  30. 'scheduled_handover',
  31. 'fault',
  32. 'changeover',
  33. 'proofing',
  34. 'abnormal',
  35. 'planned_output',
  36. 'actual_output',
  37. 'defective_product',
  38. ];
  39. /** @var array 字段中文名 */
  40. protected $fieldLabels = [
  41. 'sczl_jtbh' => '设备编号',
  42. 'sczl_bzdh' => '班组代号',
  43. 'sczl_rq' => '日期',
  44. 'theoretical_speed' => '理论速度',
  45. 'scheduled_production' => '计划生产时间',
  46. 'scheduled_maintenance'=> '计划保养时间',
  47. 'scheduled_handover' => '计划早会交接班时间',
  48. 'fault' => '设备故障时间',
  49. 'changeover' => '换型时间',
  50. 'proofing' => '打样时间',
  51. 'abnormal' => '异常时间',
  52. 'planned_output' => '计划完成产量',
  53. 'actual_output' => '实际产量',
  54. 'defective_product' => '不良品数量',
  55. ];
  56. /** @var array 按月汇总时求和的字段 */
  57. protected $sumFields = [
  58. 'scheduled_production',
  59. 'scheduled_maintenance',
  60. 'scheduled_handover',
  61. 'fault',
  62. 'changeover',
  63. 'proofing',
  64. 'abnormal',
  65. 'planned_output',
  66. 'actual_output',
  67. 'defective_product',
  68. ];
  69. /**
  70. * 1. 左侧菜单:年 -> 月 -> 日
  71. * @ApiMethod (GET)
  72. */
  73. public function getMenu()
  74. {
  75. if (!$this->request->isGet()) {
  76. $this->error('请求错误');
  77. }
  78. $dateList = Db::name($this->tableName)
  79. ->where('sczl_rq', 'not null')
  80. ->field("DISTINCT DATE_FORMAT(sczl_rq, '%Y-%m-%d') AS rq")
  81. ->order('rq DESC')
  82. ->select();
  83. $tree = [];
  84. foreach ($dateList as $item) {
  85. $rq = $item['rq'];
  86. if (empty($rq)) {
  87. continue;
  88. }
  89. $year = date('Y', strtotime($rq));
  90. $month = date('Y-m', strtotime($rq));
  91. if (!isset($tree[$year])) {
  92. $tree[$year] = [
  93. 'year' => $year,
  94. 'children' => [],
  95. ];
  96. }
  97. if (!isset($tree[$year]['children'][$month])) {
  98. $tree[$year]['children'][$month] = [
  99. 'month' => $month,
  100. 'children' => [],
  101. ];
  102. }
  103. if (!in_array($rq, $tree[$year]['children'][$month]['children'], true)) {
  104. $tree[$year]['children'][$month]['children'][] = $rq;
  105. }
  106. }
  107. $result = array_values($tree);
  108. foreach ($result as &$yearItem) {
  109. $yearItem['children'] = array_values($yearItem['children']);
  110. }
  111. unset($yearItem);
  112. $this->success('成功', $result);
  113. }
  114. /**
  115. * 2. 表格数据查询(联表设备基本资料)
  116. * @ApiMethod (GET)
  117. * @param string date 时间参数:Y-m-d 查日明细,Y-m 查月汇总
  118. */
  119. public function getList()
  120. {
  121. if (!$this->request->isGet()) {
  122. $this->error('请求错误');
  123. }
  124. $time = trim((string)$this->request->param('date', ''));
  125. if ($time === '') {
  126. $this->error('请传入 date 参数,格式 Y-m-d 或 Y-m');
  127. }
  128. $query = $this->parseQueryTime($time);
  129. if (!$this->hasStatsInPeriod($query)) {
  130. if ($query['type'] === 'day') {
  131. $this->error(date('Y-m-d', strtotime($query['start'])) . '没有数据,请添加后查询');
  132. }
  133. $this->error($query['month'] . '没有数据,请添加后查询');
  134. }
  135. $list = $this->buildDisplayList($query);
  136. $this->success('成功', $list);
  137. }
  138. /**
  139. * 2.1 任意两个日期之间的汇总数据(含填写字段与计算字段)
  140. * @ApiMethod (GET)
  141. * @param string start_date|start 开始日期 Y-m-d
  142. * @param string end_date|end 结束日期 Y-m-d
  143. */
  144. public function getRangeSummary()
  145. {
  146. if (!$this->request->isGet()) {
  147. $this->error('请求错误');
  148. }
  149. $params = $this->request->param();
  150. $startDate = isset($params['start_date']) ? trim($params['start_date']) : (isset($params['start']) ? trim($params['start']) : '');
  151. $endDate = isset($params['end_date']) ? trim($params['end_date']) : (isset($params['end']) ? trim($params['end']) : '');
  152. if ($startDate === '' || $endDate === '') {
  153. $this->error('请传入 start_date、end_date,格式 Y-m-d');
  154. }
  155. $startTs = strtotime($startDate);
  156. $endTs = strtotime($endDate);
  157. if ($startTs === false || date('Y-m-d', $startTs) !== $startDate) {
  158. $this->error('开始日期格式错误,请传 Y-m-d');
  159. }
  160. if ($endTs === false || date('Y-m-d', $endTs) !== $endDate) {
  161. $this->error('结束日期格式错误,请传 Y-m-d');
  162. }
  163. if ($startTs > $endTs) {
  164. $this->error('开始日期不能大于结束日期');
  165. }
  166. $query = [
  167. 'type' => 'range',
  168. 'start_date' => $startDate,
  169. 'end_date' => $endDate,
  170. 'start' => date('Y-m-d 00:00:00', $startTs),
  171. 'end' => date('Y-m-d 23:59:59', $endTs),
  172. ];
  173. if (!$this->hasStatsInPeriod($query)) {
  174. $this->error($startDate . ' 至 ' . $endDate . ' 没有数据');
  175. }
  176. $list = $this->buildDisplayList($query);
  177. $this->success('成功', [
  178. 'start_date' => $startDate,
  179. 'end_date' => $endDate,
  180. 'list' => $list,
  181. ]);
  182. }
  183. /**
  184. * 判断指定时间段内是否存在统计数据
  185. */
  186. protected function hasStatsInPeriod(array $query)
  187. {
  188. $count = Db::name($this->tableName)
  189. ->where('sczl_rq', '>=', $query['start'])
  190. ->where('sczl_rq', '<=', $query['end'])
  191. ->count();
  192. return $count > 0;
  193. }
  194. /**
  195. * 解析查询时间:Y-m-d 为日,Y-m 为月
  196. */
  197. protected function parseQueryTime($time)
  198. {
  199. if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $time)) {
  200. $timestamp = strtotime($time);
  201. if ($timestamp === false || date('Y-m-d', $timestamp) !== $time) {
  202. $this->error('日期格式错误,请传 Y-m-d,如 2026-07-23');
  203. }
  204. return [
  205. 'type' => 'day',
  206. 'start' => date('Y-m-d 00:00:00', $timestamp),
  207. 'end' => date('Y-m-d 23:59:59', $timestamp),
  208. ];
  209. }
  210. if (preg_match('/^\d{4}-\d{2}$/', $time)) {
  211. $timestamp = strtotime($time . '-01');
  212. if ($timestamp === false || date('Y-m', $timestamp) !== $time) {
  213. $this->error('月份格式错误,请传 Y-m,如 2026-07');
  214. }
  215. return [
  216. 'type' => 'month',
  217. 'month' => $time,
  218. 'start' => date('Y-m-01 00:00:00', $timestamp),
  219. 'end' => date('Y-m-t 23:59:59', $timestamp),
  220. ];
  221. }
  222. $this->error('时间格式错误,请传 Y-m-d(具体日期)或 Y-m(月份)');
  223. }
  224. /**
  225. * 供 Excel 导出使用的表格数据
  226. * @param string $time Y-m-d 或 Y-m
  227. * @return array
  228. */
  229. public function getExportData($time)
  230. {
  231. $time = trim((string)$time);
  232. if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $time)) {
  233. $timestamp = strtotime($time);
  234. if ($timestamp === false) {
  235. return [];
  236. }
  237. $query = [
  238. 'type' => 'day',
  239. 'start' => date('Y-m-d 00:00:00', $timestamp),
  240. 'end' => date('Y-m-d 23:59:59', $timestamp),
  241. ];
  242. } elseif (preg_match('/^\d{4}-\d{2}$/', $time)) {
  243. $timestamp = strtotime($time . '-01');
  244. if ($timestamp === false) {
  245. return [];
  246. }
  247. $query = [
  248. 'type' => 'month',
  249. 'month' => $time,
  250. 'start' => date('Y-m-01 00:00:00', $timestamp),
  251. 'end' => date('Y-m-t 23:59:59', $timestamp),
  252. ];
  253. } else {
  254. return [];
  255. }
  256. return $this->buildDisplayList($query);
  257. }
  258. /**
  259. * 3. 获取车间、机组、机台编号、机台名称、班组基础数据(新增弹窗预填)
  260. * 除理论速度外,其余业务字段取自 设备_产量计酬:当前机台+当前日期+当前班次之和
  261. * @ApiMethod (GET)
  262. * @param string date|sczl_rq 日期(必填,用于汇总产量计酬)
  263. */
  264. public function getBaseData()
  265. {
  266. if (!$this->request->isGet()) {
  267. $this->error('请求错误');
  268. }
  269. $params = $this->request->param();
  270. $date = isset($params['date']) ? trim($params['date']) : (isset($params['sczl_rq']) ? trim($params['sczl_rq']) : '');
  271. if ($date === '') {
  272. $this->error('参数 date 不能为空');
  273. }
  274. $date = date('Y-m-d', strtotime($date));
  275. if ($date === '1970-01-01' || $date === false) {
  276. $this->error('参数 date 格式错误');
  277. }
  278. $equipmentMap = $this->fetchEquipmentMap(true);
  279. if (empty($equipmentMap)) {
  280. $this->success('未获取到机台数据', []);
  281. }
  282. $yieldMap = $this->fetchYieldSumMapByDate($date);
  283. $list = [];
  284. foreach ($equipmentMap as $code => $equipment) {
  285. foreach ($this->defaultShifts as $shift) {
  286. $yield = isset($yieldMap[$code][$shift]) ? $yieldMap[$code][$shift] : $this->emptyYieldSum();
  287. $list[] = $this->buildAddPrefillRow($equipment, $code, $shift, $date, $yield);
  288. }
  289. }
  290. $this->success('成功', $this->sortEquipmentList($list));
  291. }
  292. /**
  293. * 按日期汇总设备_产量计酬(机台 + 班次)
  294. * @param string $date Y-m-d
  295. * @return array [机台编号][A班|B班] => sums
  296. */
  297. protected function fetchYieldSumMapByDate($date)
  298. {
  299. $start = $date . ' 00:00:00';
  300. $end = $date . ' 23:59:59';
  301. $rows = Db::name('设备_产量计酬')
  302. ->where('sczl_rq', '>=', $start)
  303. ->where('sczl_rq', '<=', $end)
  304. ->field([
  305. 'rtrim(sczl_jtbh) as sczl_jtbh',
  306. 'rtrim(sczl_bzdh) as sczl_bzdh',
  307. 'SUM(sczl_保养工时) as sczl_保养工时',
  308. 'SUM(sczl_异常停机工时) as sczl_异常停机工时',
  309. 'SUM(sczl_装版总工时) as sczl_装版总工时',
  310. 'SUM(sczl_打样总工时) as sczl_打样总工时',
  311. 'SUM(sczl_cl) as sczl_cl',
  312. 'SUM(sczl_zcfp) as sczl_zcfp',
  313. 'SUM(sczl_设备运行工时) as sczl_设备运行工时',
  314. ])
  315. ->group('sczl_jtbh, sczl_bzdh')
  316. ->select();
  317. $map = [];
  318. foreach ($rows as $row) {
  319. $code = $this->trimText($row['sczl_jtbh']);
  320. $shift = $this->normalizeShift($row['sczl_bzdh']);
  321. if ($code === '' || $shift === '') {
  322. continue;
  323. }
  324. if (!isset($map[$code])) {
  325. $map[$code] = [];
  326. }
  327. // 同一机台同一班次可能有 A / A班 等写法,normalize 后累加
  328. if (!isset($map[$code][$shift])) {
  329. $map[$code][$shift] = $this->emptyYieldSum();
  330. }
  331. $map[$code][$shift]['scheduled_maintenance'] += $this->formatNumber($row['sczl_保养工时']);
  332. $map[$code][$shift]['fault'] += $this->formatNumber($row['sczl_异常停机工时']);
  333. $map[$code][$shift]['changeover'] += $this->formatNumber($row['sczl_装版总工时']);
  334. $map[$code][$shift]['proofing'] += $this->formatNumber($row['sczl_打样总工时']);
  335. $map[$code][$shift]['actual_output'] += $this->formatNumber($row['sczl_cl']);
  336. $map[$code][$shift]['defective_product'] += $this->formatNumber($row['sczl_zcfp']);
  337. $map[$code][$shift]['scheduled_production'] += $this->formatNumber($row['sczl_设备运行工时']);
  338. }
  339. // 当班设备运行工时合计为 12 时,计划生产时间记为 11
  340. foreach ($map as $code => $shifts) {
  341. foreach ($shifts as $shift => $sums) {
  342. if (abs($sums['scheduled_production'] - 12) < 0.00001) {
  343. $map[$code][$shift]['scheduled_production'] = 11;
  344. }
  345. }
  346. }
  347. return $map;
  348. }
  349. /**
  350. * 产量计酬汇总空值
  351. * @return array
  352. */
  353. protected function emptyYieldSum()
  354. {
  355. return [
  356. 'scheduled_maintenance' => 0,
  357. 'fault' => 0,
  358. 'changeover' => 0,
  359. 'proofing' => 0,
  360. 'actual_output' => 0,
  361. 'defective_product' => 0,
  362. 'scheduled_production' => 0,
  363. ];
  364. }
  365. /**
  366. * 构建新增预填行
  367. */
  368. protected function buildAddPrefillRow(array $equipment, $code, $shift, $date, array $yield)
  369. {
  370. $scheduledMaintenance = round($this->formatNumber($yield['scheduled_maintenance']), 2);
  371. $fault = round($this->formatNumber($yield['fault']), 2);
  372. $changeover = round($this->formatNumber($yield['changeover']), 2);
  373. $proofing = round($this->formatNumber($yield['proofing']), 2);
  374. $actualOutput = round($this->formatNumber($yield['actual_output']), 2);
  375. $defectiveProduct = round($this->formatNumber($yield['defective_product']), 2);
  376. $scheduledProduction = round($this->formatNumber($yield['scheduled_production']), 2);
  377. $theoreticalSpeed = (int)$equipment['理论速度'];
  378. return [
  379. '车间' => $equipment['车间'],
  380. '机组' => $equipment['机组'],
  381. '编组' => $equipment['编组'],
  382. '机台编号' => $code,
  383. '机台名称' => $equipment['机台名称'],
  384. '设备编号' => $code,
  385. '班组' => $shift,
  386. '班组列表' => $this->defaultShifts,
  387. 'sczl_jtbh' => $code,
  388. 'sczl_bzdh' => $shift,
  389. 'sczl_rq' => $date . ' 00:00:00',
  390. 'theoretical_speed' => $theoreticalSpeed,
  391. '理论速度' => $theoreticalSpeed,
  392. 'scheduled_maintenance' => $scheduledMaintenance,
  393. '标准计划保养时间' => $scheduledMaintenance,
  394. '计划保养时间' => $scheduledMaintenance,
  395. 'fault' => $fault,
  396. '设备故障时间' => $fault,
  397. 'changeover' => $changeover,
  398. '实际换型时间' => $changeover,
  399. '换型时间' => $changeover,
  400. 'proofing' => $proofing,
  401. '实际打样时间' => $proofing,
  402. '打样时间' => $proofing,
  403. 'actual_output' => $actualOutput,
  404. '实际产量' => $actualOutput,
  405. 'defective_product' => $defectiveProduct,
  406. '不良品数量' => $defectiveProduct,
  407. 'scheduled_production' => $scheduledProduction,
  408. '计划生产时间' => $scheduledProduction,
  409. // 未从产量计酬预填的字段默认 0,便于直接提交
  410. 'scheduled_handover' => 0,
  411. '计划早会交接班时间' => 0,
  412. 'abnormal' => 0,
  413. '异常时间' => 0,
  414. 'planned_output' => 0,
  415. '计划完成产量' => 0,
  416. ];
  417. }
  418. /**
  419. * 4. 批量新增(每个机台每个班组一条)
  420. * @ApiMethod (POST)
  421. * @param array list 数据列表
  422. * @param string sys_id 操作人
  423. */
  424. public function batchAdd()
  425. {
  426. if (!$this->request->isPost()) {
  427. $this->error('请求错误');
  428. }
  429. $params = $this->request->post();
  430. $list = isset($params['list']) ? $params['list'] : [];
  431. $operator = isset($params['sys_id']) ? trim($params['sys_id']) : '';
  432. if (empty($list) || !is_array($list)) {
  433. $this->error('参数 list 不能为空');
  434. }
  435. if ($operator === '') {
  436. $this->error('参数 sys_id 不能为空');
  437. }
  438. $now = date('Y-m-d H:i:s');
  439. $insertRows = [];
  440. $logs = [];
  441. Db::startTrans();
  442. try {
  443. foreach ($list as $index => $row) {
  444. if (!is_array($row)) {
  445. throw new Exception('第 ' . ($index + 1) . ' 条数据格式错误');
  446. }
  447. $data = $this->normalizeRow($row);
  448. $this->validateRow($data, $index + 1);
  449. $exists = Db::name($this->tableName)
  450. ->where('sczl_jtbh', $data['sczl_jtbh'])
  451. ->where('sczl_bzdh', $data['sczl_bzdh'])
  452. ->where('sczl_rq', $data['sczl_rq'])
  453. ->find();
  454. if ($exists) {
  455. throw new Exception(sprintf(
  456. '第 %d 条记录已存在:设备[%s] 班组[%s] 日期[%s]',
  457. $index + 1,
  458. $data['sczl_jtbh'],
  459. $data['sczl_bzdh'],
  460. date('Y-m-d', strtotime($data['sczl_rq']))
  461. ));
  462. }
  463. $data['sys_id'] = $operator;
  464. $data['sys_rq'] = $now;
  465. $insertRows[] = $data;
  466. }
  467. foreach ($insertRows as $data) {
  468. $recordId = Db::name($this->tableName)->insertGetId($data);
  469. if (!$recordId) {
  470. throw new Exception('新增失败');
  471. }
  472. $logs = array_merge($logs, $this->buildAddLogs($recordId, $data, $operator, $now));
  473. }
  474. if (!empty($logs)) {
  475. Db::name($this->logTableName)->insertAll($logs);
  476. }
  477. Db::commit();
  478. $this->success('批量新增成功', ['count' => count($insertRows)]);
  479. } catch (Exception $e) {
  480. Db::rollback();
  481. $this->error($e->getMessage());
  482. }
  483. }
  484. /**
  485. * 5. 批量修改
  486. * @ApiMethod (POST)
  487. * @param array list 含 id 的数据列表
  488. * @param string sys_id 操作人
  489. */
  490. public function batchUpdate()
  491. {
  492. if (!$this->request->isPost()) {
  493. $this->error('请求错误');
  494. }
  495. $params = $this->request->post();
  496. $list = isset($params['list']) ? $params['list'] : [];
  497. $operator = isset($params['sys_id']) ? trim($params['sys_id']) : '';
  498. if (empty($list) || !is_array($list)) {
  499. $this->error('参数 list 不能为空');
  500. }
  501. if ($operator === '') {
  502. $this->error('参数 sys_id 不能为空');
  503. }
  504. $now = date('Y-m-d H:i:s');
  505. $logs = [];
  506. $updateCount = 0;
  507. Db::startTrans();
  508. try {
  509. foreach ($list as $index => $row) {
  510. if (!is_array($row) || empty($row['id'])) {
  511. throw new Exception('第 ' . ($index + 1) . ' 条数据缺少 id');
  512. }
  513. $id = (int)$row['id'];
  514. $old = Db::name($this->tableName)->where('id', $id)->find();
  515. if (empty($old)) {
  516. throw new Exception('第 ' . ($index + 1) . ' 条记录不存在');
  517. }
  518. $data = $this->normalizeRow($row, false);
  519. unset($data['id']);
  520. if (array_key_exists('sczl_bzdh', $row)) {
  521. if ($data['sczl_bzdh'] === '' || !in_array($data['sczl_bzdh'], $this->defaultShifts, true)) {
  522. throw new Exception('第 ' . ($index + 1) . ' 条班组代号只能为 A班 或 B班');
  523. }
  524. }
  525. $checkJtbh = isset($data['sczl_jtbh']) ? $data['sczl_jtbh'] : rtrim($old['sczl_jtbh']);
  526. $checkBzdh = isset($data['sczl_bzdh']) ? $data['sczl_bzdh'] : rtrim($old['sczl_bzdh']);
  527. $checkRq = isset($data['sczl_rq']) ? $data['sczl_rq'] : $old['sczl_rq'];
  528. $duplicate = Db::name($this->tableName)
  529. ->where('sczl_jtbh', $checkJtbh)
  530. ->where('sczl_bzdh', $checkBzdh)
  531. ->where('sczl_rq', $checkRq)
  532. ->where('id', '<>', $id)
  533. ->find();
  534. if ($duplicate) {
  535. throw new Exception(sprintf(
  536. '第 %d 条修改后与已有记录冲突:设备[%s] 班组[%s] 日期[%s]',
  537. $index + 1,
  538. $checkJtbh,
  539. $checkBzdh,
  540. date('Y-m-d', strtotime($checkRq))
  541. ));
  542. }
  543. $data['mod_id'] = $operator;
  544. $data['mod_rq'] = $now;
  545. $changed = [];
  546. foreach ($data as $field => $value) {
  547. if (!array_key_exists($field, $old)) {
  548. continue;
  549. }
  550. $oldValue = $old[$field];
  551. if ((string)$oldValue !== (string)$value) {
  552. $changed[$field] = $value;
  553. }
  554. }
  555. if (empty($changed)) {
  556. continue;
  557. }
  558. $result = Db::name($this->tableName)->where('id', $id)->update($changed + [
  559. 'mod_id' => $operator,
  560. 'mod_rq' => $now,
  561. ]);
  562. if ($result === false) {
  563. throw new Exception('第 ' . ($index + 1) . ' 条修改失败');
  564. }
  565. $logs = array_merge($logs, $this->buildUpdateLogs($id, $old, $changed, $operator, $now));
  566. $updateCount++;
  567. }
  568. if (!empty($logs)) {
  569. Db::name($this->logTableName)->insertAll($logs);
  570. }
  571. Db::commit();
  572. $this->success('批量修改成功', ['count' => $updateCount]);
  573. } catch (Exception $e) {
  574. Db::rollback();
  575. $this->error($e->getMessage());
  576. }
  577. }
  578. /**
  579. * 6. 批量删除
  580. * @ApiMethod (POST)
  581. * @param string ids 逗号分隔 id
  582. * @param string sys_id 操作人
  583. */
  584. public function batchDelete()
  585. {
  586. if (!$this->request->isPost()) {
  587. $this->error('请求错误');
  588. }
  589. $params = $this->request->post();
  590. $idsParam = isset($params['ids']) ? trim($params['ids']) : '';
  591. $operator = isset($params['sys_id']) ? trim($params['sys_id']) : '';
  592. if ($idsParam === '') {
  593. $this->error('参数 ids 不能为空');
  594. }
  595. if ($operator === '') {
  596. $this->error('参数 sys_id 不能为空');
  597. }
  598. $ids = array_values(array_filter(array_map('intval', explode(',', $idsParam))));
  599. if (empty($ids)) {
  600. $this->error('ids 格式错误');
  601. }
  602. $records = Db::name($this->tableName)->where('id', 'in', $ids)->select();
  603. if (count($records) !== count($ids)) {
  604. $this->error('部分记录不存在,请刷新后重试');
  605. }
  606. $now = date('Y-m-d H:i:s');
  607. $logs = [];
  608. Db::startTrans();
  609. try {
  610. foreach ($records as $record) {
  611. $logs = array_merge($logs, $this->buildDeleteLogs((int)$record['id'], $record, $operator, $now));
  612. }
  613. $deleteResult = Db::name($this->tableName)->where('id', 'in', $ids)->delete();
  614. if ($deleteResult === false) {
  615. throw new Exception('删除失败');
  616. }
  617. if (!empty($logs)) {
  618. Db::name($this->logTableName)->insertAll($logs);
  619. }
  620. Db::commit();
  621. $this->success('批量删除成功', ['count' => $deleteResult]);
  622. } catch (Exception $e) {
  623. Db::rollback();
  624. $this->error($e->getMessage());
  625. }
  626. }
  627. /**
  628. * 构建带计算字段和汇总行的表格数据
  629. */
  630. protected function buildDisplayList(array $query)
  631. {
  632. if ($query['type'] === 'day') {
  633. $statsRows = $this->fetchStatsRows($query['start'], $query['end']);
  634. $dateMeta = [
  635. 'query_type' => 'day',
  636. 'sczl_rq' => date('Y-m-d 00:00:00', strtotime($query['start'])),
  637. 'query_month' => '',
  638. 'query_start' => date('Y-m-d', strtotime($query['start'])),
  639. 'query_end' => date('Y-m-d', strtotime($query['end'])),
  640. ];
  641. } else {
  642. // month / range:按机台+班组汇总填写字段,再计算衍生指标
  643. $statsRows = $this->fetchMonthlyStatsRows($query['start'], $query['end']);
  644. $dateMeta = [
  645. 'query_type' => $query['type'],
  646. 'sczl_rq' => isset($query['month'])
  647. ? ($query['month'] . '-01 00:00:00')
  648. : date('Y-m-d 00:00:00', strtotime($query['start'])),
  649. 'query_month' => isset($query['month']) ? $query['month'] : '',
  650. 'query_start' => date('Y-m-d', strtotime($query['start'])),
  651. 'query_end' => date('Y-m-d', strtotime($query['end'])),
  652. ];
  653. }
  654. $equipmentMap = $this->fetchEquipmentMap(true);
  655. $statsRows = $this->enrichStatsWithEquipment($statsRows, $equipmentMap);
  656. $statsMap = [];
  657. foreach ($statsRows as $row) {
  658. $key = $this->trimText($row['sczl_jtbh']) . '|' . $this->normalizeShift($row['sczl_bzdh']);
  659. $statsMap[$key] = $row;
  660. }
  661. $detailRows = [];
  662. $processedKeys = [];
  663. foreach ($equipmentMap as $code => $equipment) {
  664. foreach ($this->defaultShifts as $shift) {
  665. $key = $code . '|' . $shift;
  666. $processedKeys[$key] = true;
  667. $stat = isset($statsMap[$key]) ? $statsMap[$key] : [];
  668. $detailRows[] = $this->buildDetailRow($equipment, $shift, $stat, $dateMeta);
  669. }
  670. }
  671. // 补充统计表中有数据但未纳入骨架的行(按设备基本资料补全车间编组)
  672. foreach ($statsMap as $key => $stat) {
  673. if (isset($processedKeys[$key])) {
  674. continue;
  675. }
  676. $code = $this->trimText($stat['sczl_jtbh']);
  677. $shift = $this->normalizeShift($stat['sczl_bzdh']);
  678. if ($code === '' || $shift === '' || !isset($equipmentMap[$code])) {
  679. continue;
  680. }
  681. $detailRows[] = $this->buildDetailRow($equipmentMap[$code], $shift, $stat, $dateMeta);
  682. }
  683. return $this->assembleListWithSummaries($detailRows);
  684. }
  685. /**
  686. * 获取设备基本资料映射(key=机台编号)
  687. */
  688. protected function fetchEquipmentMap($displayOnly = true)
  689. {
  690. $query = Db::name($this->equipmentTable)
  691. ->where('使用部门', '<>', '研发中心');
  692. if ($displayOnly) {
  693. $query->where('设备编组', '<>', '')->where('sys_sbID', '<>', '');
  694. }
  695. $equipments = $query
  696. ->field([
  697. 'rtrim(使用部门) as 车间',
  698. 'rtrim(设备编组) as 机组',
  699. 'rtrim(设备编号) as 机台编号',
  700. 'rtrim(设备名称) as 机台名称',
  701. '平均车速',
  702. ])
  703. ->order('设备编组,设备编号')
  704. ->select();
  705. $map = [];
  706. foreach ($equipments as $item) {
  707. $code = $this->trimText($item['机台编号']);
  708. if ($code === '') {
  709. continue;
  710. }
  711. $map[$code] = $this->formatEquipmentItem($item, $code);
  712. }
  713. return $map;
  714. }
  715. /**
  716. * 格式化设备基本资料项
  717. */
  718. protected function formatEquipmentItem(array $item, $code = null)
  719. {
  720. $code = $code ?: $this->trimText($item['机台编号']);
  721. $group = $this->trimText($item['机组']);
  722. return [
  723. '车间' => $this->trimText($item['车间']),
  724. '机组' => $group,
  725. '编组' => $group,
  726. '机台编号' => $code,
  727. '机台名称' => $this->trimText($item['机台名称']),
  728. '理论速度' => (int)$this->formatNumber($item['平均车速']),
  729. ];
  730. }
  731. /**
  732. * 按机台编号用设备基本资料补全统计数据
  733. */
  734. protected function enrichStatsWithEquipment(array $statsRows, array $equipmentMap)
  735. {
  736. foreach ($statsRows as &$row) {
  737. $code = $this->trimText($row['sczl_jtbh']);
  738. if ($code === '' || !isset($equipmentMap[$code])) {
  739. continue;
  740. }
  741. $equipment = $equipmentMap[$code];
  742. $row['车间'] = $equipment['车间'];
  743. $row['机组'] = $equipment['机组'];
  744. $row['编组'] = $equipment['编组'];
  745. $row['机台名称'] = $equipment['机台名称'];
  746. }
  747. unset($row);
  748. return $statsRows;
  749. }
  750. /**
  751. * 查询原始统计数据(按日)
  752. */
  753. protected function fetchStatsRows($start, $end)
  754. {
  755. return Db::name($this->tableName)
  756. ->alias('a')
  757. ->where('a.sczl_rq', '>=', $start)
  758. ->where('a.sczl_rq', '<=', $end)
  759. ->field([
  760. 'a.id',
  761. 'a.sczl_jtbh',
  762. 'a.sczl_bzdh',
  763. 'a.sczl_rq',
  764. 'a.theoretical_speed',
  765. 'a.scheduled_production',
  766. 'a.scheduled_maintenance',
  767. 'a.scheduled_handover',
  768. 'a.fault',
  769. 'a.changeover',
  770. 'a.proofing',
  771. 'a.abnormal',
  772. 'a.planned_output',
  773. 'a.actual_output',
  774. 'a.defective_product',
  775. 'a.sys_id',
  776. 'a.sys_rq',
  777. 'a.mod_id',
  778. 'a.mod_rq',
  779. ])
  780. ->select();
  781. }
  782. /**
  783. * 查询原始统计数据(按月汇总)
  784. */
  785. protected function fetchMonthlyStatsRows($start, $end)
  786. {
  787. $sumExpr = [];
  788. foreach ($this->sumFields as $field) {
  789. $sumExpr[] = "SUM(a.{$field}) as {$field}";
  790. }
  791. return Db::name($this->tableName)
  792. ->alias('a')
  793. ->where('a.sczl_rq', '>=', $start)
  794. ->where('a.sczl_rq', '<=', $end)
  795. ->field(array_merge([
  796. 'MIN(a.id) as id',
  797. 'a.sczl_jtbh',
  798. 'a.sczl_bzdh',
  799. 'MAX(a.theoretical_speed) as theoretical_speed',
  800. ], $sumExpr))
  801. ->group('a.sczl_jtbh,a.sczl_bzdh')
  802. ->select();
  803. }
  804. /**
  805. * 构建明细行
  806. */
  807. protected function buildDetailRow(array $equipment, $shift, array $stat, array $dateMeta)
  808. {
  809. $row = [
  810. 'row_type' => 'detail',
  811. 'id' => isset($stat['id']) ? (int)$stat['id'] : 0,
  812. '车间' => $equipment['车间'],
  813. '机组' => $equipment['机组'],
  814. '编组' => $equipment['编组'],
  815. '机台编号' => $equipment['机台编号'],
  816. '设备编号' => $equipment['机台编号'],
  817. '机台名称' => $equipment['机台名称'],
  818. '班组' => $shift,
  819. 'sczl_jtbh' => $equipment['机台编号'],
  820. 'sczl_bzdh' => $shift,
  821. 'sczl_rq' => $dateMeta['sczl_rq'],
  822. 'query_type' => $dateMeta['query_type'],
  823. 'query_month' => $dateMeta['query_month'],
  824. 'query_start' => isset($dateMeta['query_start']) ? $dateMeta['query_start'] : '',
  825. 'query_end' => isset($dateMeta['query_end']) ? $dateMeta['query_end'] : '',
  826. 'theoretical_speed' => !empty($stat['id'])
  827. ? (int)$this->formatNumber($stat['theoretical_speed'])
  828. : $equipment['理论速度'],
  829. 'scheduled_production' => $this->formatNumber(isset($stat['scheduled_production']) ? $stat['scheduled_production'] : 0),
  830. 'scheduled_maintenance'=> $this->formatNumber(isset($stat['scheduled_maintenance']) ? $stat['scheduled_maintenance'] : 0),
  831. 'scheduled_handover' => $this->formatNumber(isset($stat['scheduled_handover']) ? $stat['scheduled_handover'] : 0),
  832. 'fault' => $this->formatNumber(isset($stat['fault']) ? $stat['fault'] : 0),
  833. 'changeover' => $this->formatNumber(isset($stat['changeover']) ? $stat['changeover'] : 0),
  834. 'proofing' => $this->formatNumber(isset($stat['proofing']) ? $stat['proofing'] : 0),
  835. 'abnormal' => $this->formatNumber(isset($stat['abnormal']) ? $stat['abnormal'] : 0),
  836. 'planned_output' => $this->formatNumber(isset($stat['planned_output']) ? $stat['planned_output'] : 0),
  837. 'actual_output' => $this->formatNumber(isset($stat['actual_output']) ? $stat['actual_output'] : 0),
  838. 'defective_product' => $this->formatNumber(isset($stat['defective_product']) ? $stat['defective_product'] : 0),
  839. 'sys_id' => isset($stat['sys_id']) ? $this->trimText($stat['sys_id']) : '',
  840. 'sys_rq' => isset($stat['sys_rq']) ? $stat['sys_rq'] : '',
  841. 'mod_id' => isset($stat['mod_id']) ? $this->trimText($stat['mod_id']) : '',
  842. 'mod_rq' => isset($stat['mod_rq']) ? $stat['mod_rq'] : '',
  843. ];
  844. return $this->appendCalculatedFields($row);
  845. }
  846. /**
  847. * 组装明细 + 汇总行
  848. */
  849. protected function assembleListWithSummaries(array $detailRows)
  850. {
  851. $result = [];
  852. $workshopGroups = [];
  853. foreach ($detailRows as $row) {
  854. $workshop = $row['车间'];
  855. $group = $row['机组'];
  856. if (!isset($workshopGroups[$workshop])) {
  857. $workshopGroups[$workshop] = [];
  858. }
  859. if (!isset($workshopGroups[$workshop][$group])) {
  860. $workshopGroups[$workshop][$group] = [];
  861. }
  862. $workshopGroups[$workshop][$group][] = $row;
  863. }
  864. uksort($workshopGroups, function ($a, $b) {
  865. return strcmp($a, $b);
  866. });
  867. foreach ($workshopGroups as $workshop => $groups) {
  868. uksort($groups, function ($a, $b) {
  869. $orderA = $this->extractGroupOrder($a);
  870. $orderB = $this->extractGroupOrder($b);
  871. if ($orderA !== $orderB) {
  872. return $orderA <=> $orderB;
  873. }
  874. return strcmp($a, $b);
  875. });
  876. $workshopRows = [];
  877. $workshopBlock = [];
  878. foreach ($groups as $group => $groupRows) {
  879. $groupRows = $this->sortEquipmentList($groupRows);
  880. $workshopRows = array_merge($workshopRows, $groupRows);
  881. $workshopBlock[] = $this->buildSummaryRow(
  882. 'group',
  883. $workshop,
  884. $group,
  885. $this->buildGroupShortName($group) . '汇总',
  886. '/',
  887. $groupRows
  888. );
  889. foreach ($this->defaultShifts as $shift) {
  890. $shiftRows = array_values(array_filter($groupRows, function ($item) use ($shift) {
  891. return $item['班组'] === $shift;
  892. }));
  893. if (!empty($shiftRows)) {
  894. $workshopBlock[] = $this->buildSummaryRow(
  895. 'group_shift',
  896. $workshop,
  897. $group,
  898. $this->buildGroupShortName($group) . $shift,
  899. $shift,
  900. $shiftRows
  901. );
  902. }
  903. }
  904. $workshopBlock = array_merge($workshopBlock, $groupRows);
  905. }
  906. if (!empty($workshopRows)) {
  907. array_unshift(
  908. $workshopBlock,
  909. $this->buildSummaryRow(
  910. 'workshop',
  911. $workshop,
  912. '全部机组',
  913. $workshop . '全部工序合计',
  914. '/',
  915. $workshopRows
  916. )
  917. );
  918. }
  919. $result = array_merge($result, $workshopBlock);
  920. }
  921. return $result;
  922. }
  923. /**
  924. * 提取机组简称(03、卷凹机组 -> 卷凹)
  925. */
  926. protected function buildGroupShortName($group)
  927. {
  928. $group = $this->trimText($group);
  929. if (preg_match('/、(.+?)机组/u', $group, $matches)) {
  930. return $matches[1];
  931. }
  932. if (preg_match('/、(.+)/u', $group, $matches)) {
  933. return $matches[1];
  934. }
  935. return $group;
  936. }
  937. /**
  938. * 构建汇总行
  939. */
  940. protected function buildSummaryRow($rowType, $workshop, $group, $machineName, $shift, array $sourceRows)
  941. {
  942. $aggregated = $this->aggregateBaseFields($sourceRows);
  943. $row = [
  944. 'row_type' => $rowType,
  945. 'id' => 0,
  946. '车间' => $workshop,
  947. '机组' => $group,
  948. '编组' => $group,
  949. '机台编号' => '',
  950. '设备编号' => '',
  951. '机台名称' => $machineName,
  952. '班组' => $shift,
  953. 'sczl_jtbh' => '',
  954. 'sczl_bzdh' => $shift === '/' ? '' : $shift,
  955. 'sczl_rq' => $sourceRows[0]['sczl_rq'],
  956. 'query_type' => $sourceRows[0]['query_type'],
  957. 'query_month' => $sourceRows[0]['query_month'],
  958. 'theoretical_speed' => 0,
  959. 'scheduled_production' => $aggregated['scheduled_production'],
  960. 'scheduled_maintenance'=> $aggregated['scheduled_maintenance'],
  961. 'scheduled_handover' => $aggregated['scheduled_handover'],
  962. 'fault' => $aggregated['fault'],
  963. 'changeover' => $aggregated['changeover'],
  964. 'proofing' => $aggregated['proofing'],
  965. 'abnormal' => $aggregated['abnormal'],
  966. 'planned_output' => $aggregated['planned_output'],
  967. 'actual_output' => $aggregated['actual_output'],
  968. 'defective_product' => $aggregated['defective_product'],
  969. 'sys_id' => '',
  970. 'sys_rq' => '',
  971. 'mod_id' => '',
  972. 'mod_rq' => '',
  973. ];
  974. return $this->appendCalculatedFields($row, $aggregated['standard_output']);
  975. }
  976. /**
  977. * 汇总基础字段
  978. */
  979. protected function aggregateBaseFields(array $rows)
  980. {
  981. $totals = [
  982. 'scheduled_production' => 0,
  983. 'scheduled_maintenance'=> 0,
  984. 'scheduled_handover' => 0,
  985. 'fault' => 0,
  986. 'changeover' => 0,
  987. 'proofing' => 0,
  988. 'abnormal' => 0,
  989. 'planned_output' => 0,
  990. 'actual_output' => 0,
  991. 'defective_product' => 0,
  992. 'standard_output' => 0,
  993. ];
  994. foreach ($rows as $row) {
  995. foreach ($this->sumFields as $field) {
  996. $totals[$field] += $this->formatNumber($row[$field]);
  997. }
  998. $totals['standard_output'] += $this->formatNumber($row['标准产量']);
  999. }
  1000. return $totals;
  1001. }
  1002. /**
  1003. * 计算并追加衍生指标
  1004. */
  1005. protected function appendCalculatedFields(array $row, $standardOutputOverride = null)
  1006. {
  1007. $scheduledProduction = $this->formatNumber($row['scheduled_production']);
  1008. $scheduledMaintenance = $this->formatNumber($row['scheduled_maintenance']);
  1009. $scheduledHandover = $this->formatNumber($row['scheduled_handover']);
  1010. $fault = $this->formatNumber($row['fault']);
  1011. $changeover = $this->formatNumber($row['changeover']);
  1012. $proofing = $this->formatNumber($row['proofing']);
  1013. $abnormal = $this->formatNumber($row['abnormal']);
  1014. $plannedOutput = $this->formatNumber($row['planned_output']);
  1015. $actualOutput = $this->formatNumber($row['actual_output']);
  1016. $defectiveProduct = $this->formatNumber($row['defective_product']);
  1017. $theoreticalSpeed = $this->formatNumber($row['theoretical_speed']);
  1018. $loadTime = $scheduledProduction - ($scheduledMaintenance + $scheduledHandover);
  1019. if ($loadTime < 0) {
  1020. $loadTime = 0;
  1021. }
  1022. $actualRunTime = $loadTime - ($fault + $changeover + $proofing + $abnormal);
  1023. if ($actualRunTime < 0) {
  1024. $actualRunTime = 0;
  1025. }
  1026. $standardOutput = $standardOutputOverride !== null
  1027. ? $this->formatNumber($standardOutputOverride)
  1028. : ($theoreticalSpeed * $actualRunTime);
  1029. $planAchievementRate = $plannedOutput > 0
  1030. ? (($actualOutput - $defectiveProduct) / $plannedOutput)
  1031. : 0;
  1032. $timeUtilization = $loadTime > 0 ? ($actualRunTime / $loadTime) : 0;
  1033. $speedUtilization = $standardOutput > 0 ? ($actualOutput / $standardOutput) : 0;
  1034. $qualityRate = $actualOutput > 0
  1035. ? (($actualOutput - $defectiveProduct) / $actualOutput)
  1036. : 0;
  1037. $oee = $timeUtilization * $speedUtilization * $qualityRate;
  1038. $row['负荷时间'] = round($loadTime, 2);
  1039. $row['实际运行时间'] = round($actualRunTime, 2);
  1040. $row['生产计划达成率'] = $this->formatRate($planAchievementRate);
  1041. $row['标准产量'] = round($standardOutput, 2);
  1042. $row['时间利用率'] = $this->formatRate($timeUtilization);
  1043. $row['速度利用率'] = $this->formatRate($speedUtilization);
  1044. $row['质量合格率'] = $this->formatRate($qualityRate);
  1045. $row['设备综合效率'] = $this->formatRate($oee);
  1046. $row['load_time'] = $row['负荷时间'];
  1047. $row['actual_run_time'] = $row['实际运行时间'];
  1048. $row['plan_achievement_rate'] = $row['生产计划达成率'];
  1049. $row['standard_output'] = $row['标准产量'];
  1050. $row['time_utilization'] = $row['时间利用率'];
  1051. $row['speed_utilization'] = $row['速度利用率'];
  1052. $row['quality_rate'] = $row['质量合格率'];
  1053. $row['oee'] = $row['设备综合效率'];
  1054. $row['理论速度'] = (int)$theoreticalSpeed;
  1055. $row['计划生产时间'] = round($scheduledProduction, 2);
  1056. $row['计划保养时间'] = round($scheduledMaintenance, 2);
  1057. $row['计划早会交接班时间'] = round($scheduledHandover, 2);
  1058. $row['设备故障时间'] = round($fault, 2);
  1059. $row['换型时间'] = round($changeover, 2);
  1060. $row['打样时间'] = round($proofing, 2);
  1061. $row['异常时间'] = round($abnormal, 2);
  1062. $row['计划完成产量'] = round($plannedOutput, 2);
  1063. $row['实际产量'] = round($actualOutput, 2);
  1064. $row['不良品数量'] = round($defectiveProduct, 2);
  1065. return $row;
  1066. }
  1067. /**
  1068. * 格式化为百分比(0-100,保留2位)
  1069. */
  1070. protected function formatRate($value)
  1071. {
  1072. return round((float)$value * 100, 2);
  1073. }
  1074. /**
  1075. * 查询字段
  1076. */
  1077. protected function buildSelectFields($isMonth = false)
  1078. {
  1079. $fields = [
  1080. 'a.id',
  1081. 'a.sczl_jtbh',
  1082. 'a.sczl_bzdh',
  1083. 'a.sczl_rq',
  1084. 'a.theoretical_speed',
  1085. 'a.scheduled_production',
  1086. 'a.scheduled_maintenance',
  1087. 'a.scheduled_handover',
  1088. 'a.fault',
  1089. 'a.changeover',
  1090. 'a.proofing',
  1091. 'a.abnormal',
  1092. 'a.planned_output',
  1093. 'a.actual_output',
  1094. 'a.defective_product',
  1095. 'a.sys_id',
  1096. 'a.sys_rq',
  1097. 'a.mod_id',
  1098. 'a.mod_rq',
  1099. 'rtrim(b.使用部门) as 车间',
  1100. 'rtrim(b.设备编组) as 机组',
  1101. 'rtrim(b.设备名称) as 机台名称',
  1102. ];
  1103. return $fields;
  1104. }
  1105. /**
  1106. * 格式化返回列表
  1107. */
  1108. protected function formatList($list)
  1109. {
  1110. $result = [];
  1111. foreach ($list as $row) {
  1112. $result[] = [
  1113. 'id' => (int)$row['id'],
  1114. '车间' => isset($row['车间']) ? rtrim($row['车间']) : '',
  1115. '机组' => isset($row['机组']) ? rtrim($row['机组']) : '',
  1116. '机台编号' => rtrim($row['sczl_jtbh']),
  1117. '设备编号' => rtrim($row['sczl_jtbh']),
  1118. '机台名称' => isset($row['机台名称']) ? rtrim($row['机台名称']) : '',
  1119. '班组' => $this->normalizeShift($row['sczl_bzdh']),
  1120. 'sczl_jtbh' => rtrim($row['sczl_jtbh']),
  1121. 'sczl_bzdh' => $this->normalizeShift($row['sczl_bzdh']),
  1122. 'sczl_rq' => $row['sczl_rq'],
  1123. 'theoretical_speed' => $this->formatNumber($row['theoretical_speed']),
  1124. 'scheduled_production' => $this->formatNumber($row['scheduled_production']),
  1125. 'scheduled_maintenance'=> $this->formatNumber($row['scheduled_maintenance']),
  1126. 'scheduled_handover' => $this->formatNumber($row['scheduled_handover']),
  1127. 'fault' => $this->formatNumber($row['fault']),
  1128. 'changeover' => $this->formatNumber($row['changeover']),
  1129. 'proofing' => $this->formatNumber($row['proofing']),
  1130. 'abnormal' => $this->formatNumber($row['abnormal']),
  1131. 'planned_output' => $this->formatNumber($row['planned_output']),
  1132. 'actual_output' => $this->formatNumber($row['actual_output']),
  1133. 'defective_product' => $this->formatNumber($row['defective_product']),
  1134. 'sys_id' => isset($row['sys_id']) ? rtrim($row['sys_id']) : '',
  1135. 'sys_rq' => isset($row['sys_rq']) ? $row['sys_rq'] : '',
  1136. 'mod_id' => isset($row['mod_id']) ? rtrim($row['mod_id']) : '',
  1137. 'mod_rq' => isset($row['mod_rq']) ? $row['mod_rq'] : '',
  1138. 'query_type' => isset($row['query_type']) ? $row['query_type'] : 'day',
  1139. 'query_month' => isset($row['query_month']) ? $row['query_month'] : '',
  1140. ];
  1141. }
  1142. return $this->sortEquipmentList($result);
  1143. }
  1144. /**
  1145. * 按设备编组、机台编号数字排序
  1146. */
  1147. protected function sortEquipmentList(array $list)
  1148. {
  1149. usort($list, function ($a, $b) {
  1150. return $this->compareEquipmentRow($a, $b);
  1151. });
  1152. return $list;
  1153. }
  1154. /**
  1155. * 设备行排序比较
  1156. */
  1157. protected function compareEquipmentRow($a, $b)
  1158. {
  1159. $groupA = $this->trimText(isset($a['机组']) ? $a['机组'] : '');
  1160. $groupB = $this->trimText(isset($b['机组']) ? $b['机组'] : '');
  1161. $groupOrderA = $this->extractGroupOrder($groupA);
  1162. $groupOrderB = $this->extractGroupOrder($groupB);
  1163. if ($groupOrderA !== $groupOrderB) {
  1164. return $groupOrderA <=> $groupOrderB;
  1165. }
  1166. if ($groupA !== $groupB) {
  1167. return strcmp($groupA, $groupB);
  1168. }
  1169. $codeA = $this->getMachineCodeFromRow($a);
  1170. $codeB = $this->getMachineCodeFromRow($b);
  1171. $numA = $this->extractMachineNumber($codeA);
  1172. $numB = $this->extractMachineNumber($codeB);
  1173. if ($numA !== $numB) {
  1174. return $numA <=> $numB;
  1175. }
  1176. if ($codeA !== $codeB) {
  1177. return strcmp($codeA, $codeB);
  1178. }
  1179. $shiftA = isset($a['班组']) ? $a['班组'] : (isset($a['sczl_bzdh']) ? $a['sczl_bzdh'] : '');
  1180. $shiftB = isset($b['班组']) ? $b['班组'] : (isset($b['sczl_bzdh']) ? $b['sczl_bzdh'] : '');
  1181. return strcmp($shiftA, $shiftB);
  1182. }
  1183. /**
  1184. * 提取设备编组前缀序号(如 03、卷凹机组 -> 3)
  1185. */
  1186. protected function extractGroupOrder($group)
  1187. {
  1188. $group = $this->trimText($group);
  1189. if (preg_match('/^(\d+)/', $group, $matches)) {
  1190. return (int)$matches[1];
  1191. }
  1192. return PHP_INT_MAX;
  1193. }
  1194. /**
  1195. * 提取机台编号中的数字(如 YWY01# -> 1)
  1196. */
  1197. protected function extractMachineNumber($code)
  1198. {
  1199. $code = $this->trimText($code);
  1200. if (preg_match('/(\d+)/', $code, $matches)) {
  1201. return (int)$matches[1];
  1202. }
  1203. return PHP_INT_MAX;
  1204. }
  1205. /**
  1206. * 从返回行中获取机台编号
  1207. */
  1208. protected function getMachineCodeFromRow($row)
  1209. {
  1210. if (!empty($row['机台编号'])) {
  1211. return $this->trimText($row['机台编号']);
  1212. }
  1213. if (!empty($row['sczl_jtbh'])) {
  1214. return $this->trimText($row['sczl_jtbh']);
  1215. }
  1216. if (!empty($row['设备编号'])) {
  1217. return $this->trimText($row['设备编号']);
  1218. }
  1219. return '';
  1220. }
  1221. /**
  1222. * 去除字符串首尾空白(含全角空格)
  1223. */
  1224. protected function trimText($value)
  1225. {
  1226. return preg_replace('/^[\s\x{3000}]+|[\s\x{3000}]+$/u', '', (string)$value);
  1227. }
  1228. /**
  1229. * 班组名称规范化:仅支持 A班、B班
  1230. */
  1231. protected function normalizeShift($shift)
  1232. {
  1233. $shift = $this->trimText($shift);
  1234. $shift = preg_replace('/\s+/u', '', $shift);
  1235. if ($shift === '') {
  1236. return '';
  1237. }
  1238. if (preg_match('/^A(?:班)?$/iu', $shift)) {
  1239. return 'A班';
  1240. }
  1241. if (preg_match('/^B(?:班)?$/iu', $shift)) {
  1242. return 'B班';
  1243. }
  1244. return '';
  1245. }
  1246. /**
  1247. * 标准化单行数据
  1248. */
  1249. protected function normalizeRow(array $row, $requireAll = true)
  1250. {
  1251. $data = [];
  1252. foreach ($this->dataFields as $field) {
  1253. if (!array_key_exists($field, $row)) {
  1254. if (!$requireAll) {
  1255. continue;
  1256. }
  1257. $data[$field] = in_array($field, $this->sumFields, true) || $field === 'theoretical_speed' ? 0 : '';
  1258. continue;
  1259. }
  1260. if ($field === 'sczl_rq') {
  1261. $data[$field] = date('Y-m-d 00:00:00', strtotime($row[$field]));
  1262. continue;
  1263. }
  1264. if (in_array($field, ['sczl_jtbh', 'sczl_bzdh'], true)) {
  1265. $value = trim((string)$row[$field]);
  1266. $data[$field] = $field === 'sczl_bzdh' ? $this->normalizeShift($value) : $value;
  1267. continue;
  1268. }
  1269. if ($field === 'theoretical_speed') {
  1270. $data[$field] = (int)$row[$field];
  1271. continue;
  1272. }
  1273. $data[$field] = $row[$field] === '' || $row[$field] === null ? 0 : $row[$field];
  1274. }
  1275. return $data;
  1276. }
  1277. /**
  1278. * 校验单行数据
  1279. */
  1280. protected function validateRow(array $data, $lineNo)
  1281. {
  1282. if ($data['sczl_jtbh'] === '') {
  1283. throw new Exception('第 ' . $lineNo . ' 条设备编号不能为空');
  1284. }
  1285. if ($data['sczl_bzdh'] === '') {
  1286. throw new Exception('第 ' . $lineNo . ' 条班组代号不能为空,且只能为 A班 或 B班');
  1287. }
  1288. if (!in_array($data['sczl_bzdh'], $this->defaultShifts, true)) {
  1289. throw new Exception('第 ' . $lineNo . ' 条班组代号只能为 A班 或 B班');
  1290. }
  1291. if (empty($data['sczl_rq'])) {
  1292. throw new Exception('第 ' . $lineNo . ' 条日期不能为空');
  1293. }
  1294. $equipment = Db::name($this->equipmentTable)
  1295. ->where('设备编号', $data['sczl_jtbh'])
  1296. ->find();
  1297. if (empty($equipment)) {
  1298. throw new Exception('第 ' . $lineNo . ' 条设备编号不存在:' . $data['sczl_jtbh']);
  1299. }
  1300. }
  1301. /**
  1302. * 构建新增日志
  1303. */
  1304. protected function buildAddLogs($recordId, array $data, $operator, $time)
  1305. {
  1306. $logs = [];
  1307. foreach ($this->dataFields as $field) {
  1308. if (!array_key_exists($field, $data)) {
  1309. continue;
  1310. }
  1311. $logs[] = [
  1312. 'record_id' => $recordId,
  1313. 'operation_type' => 'add',
  1314. 'field_name' => isset($this->fieldLabels[$field]) ? $this->fieldLabels[$field] : $field,
  1315. 'old_value' => null,
  1316. 'new_value' => (string)$data[$field],
  1317. 'operator_id' => $operator,
  1318. 'operation_time' => $time,
  1319. ];
  1320. }
  1321. return $logs;
  1322. }
  1323. /**
  1324. * 构建修改日志
  1325. */
  1326. protected function buildUpdateLogs($recordId, array $old, array $changed, $operator, $time)
  1327. {
  1328. $logs = [];
  1329. foreach ($changed as $field => $newValue) {
  1330. if ($field === 'mod_id' || $field === 'mod_rq') {
  1331. continue;
  1332. }
  1333. $logs[] = [
  1334. 'record_id' => $recordId,
  1335. 'operation_type' => 'update',
  1336. 'field_name' => isset($this->fieldLabels[$field]) ? $this->fieldLabels[$field] : $field,
  1337. 'old_value' => isset($old[$field]) ? (string)$old[$field] : '',
  1338. 'new_value' => (string)$newValue,
  1339. 'operator_id' => $operator,
  1340. 'operation_time' => $time,
  1341. ];
  1342. }
  1343. return $logs;
  1344. }
  1345. /**
  1346. * 构建删除日志
  1347. */
  1348. protected function buildDeleteLogs($recordId, array $record, $operator, $time)
  1349. {
  1350. $logs = [];
  1351. foreach ($this->dataFields as $field) {
  1352. if (!array_key_exists($field, $record)) {
  1353. continue;
  1354. }
  1355. $logs[] = [
  1356. 'record_id' => $recordId,
  1357. 'operation_type' => 'delete',
  1358. 'field_name' => isset($this->fieldLabels[$field]) ? $this->fieldLabels[$field] : $field,
  1359. 'old_value' => (string)$record[$field],
  1360. 'new_value' => null,
  1361. 'operator_id' => $operator,
  1362. 'operation_time' => $time,
  1363. ];
  1364. }
  1365. return $logs;
  1366. }
  1367. /**
  1368. * 数值格式化
  1369. */
  1370. protected function formatNumber($value)
  1371. {
  1372. if ($value === null || $value === '') {
  1373. return 0;
  1374. }
  1375. return is_numeric($value) ? (float)$value : $value;
  1376. }
  1377. }