Synchronization.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use think\Db;
  5. use think\exception\HttpResponseException;
  6. /**
  7. * MES-ERP 工单同步
  8. */
  9. class Synchronization extends Api
  10. {
  11. protected $noNeedLogin = ['*'];
  12. protected $noNeedRight = ['*'];
  13. /** @var int 同步状态:待同步 */
  14. const SYNC_STATUS_PENDING = 0;
  15. /** @var int 同步状态:成功 */
  16. const SYNC_STATUS_SUCCESS = 1;
  17. /** @var int 同步状态:失败 */
  18. const SYNC_STATUS_FAILED = 2;
  19. /** @var int 操作状态:成功 */
  20. const OPERATE_STATUS_SUCCESS = 1;
  21. /** @var int 操作状态:失败 */
  22. const OPERATE_STATUS_FAILED = 2;
  23. /** @var int 操作状态:处理中 */
  24. const OPERATE_STATUS_PROCESSING = 3;
  25. /**
  26. * ERP SQL Server 表映射(请按实际 ERP 表结构填写 table 与 fields)
  27. * fields:MES 源字段名 => ERP 目标字段名;固定值以 = 开头,如 '=M' => 'is_mfg_wrk'
  28. * 报工 sfb_id 规则:K+年月日(同步日期)+四位自增,如 K2606100201
  29. */
  30. protected $erpTableMap = [
  31. // 1-工单数据:来源 工单_基本资料
  32. 'work_order' => [
  33. 'table' => 'mps_mfg_ord',
  34. 'fields' => [
  35. '订单编号' => 'ordno',
  36. '=M' => 'is_mfg_wrk',
  37. '生产款号' => 'itemno',
  38. '接单日期' => 'cr_date',
  39. '落货日期' => 'rel_date',
  40. '同步日期' => 'due_date',
  41. '订单数量' => 'qty_ord',
  42. 'remark' => 'remark',
  43. '计划制造工分' => 'curprc',
  44. '=0001' => 'ocode',
  45. ],
  46. ],
  47. // 2-工艺数据:来源 工单_基础工艺资料
  48. 'process' => [
  49. 'table' => 'mps_mfg_routing',
  50. 'fields' => [
  51. 'work_order' => 'ordno',
  52. '=M' => 'is_mfg_wrk',
  53. '生产款号' => 'itemno',
  54. 'process_code' => 'op',
  55. 'process_name' => 'routiname',
  56. 'standard_score' => 'preprc',
  57. '=0001' => 'ocode',
  58. '订单数量' => 'qty_ord',
  59. ],
  60. ],
  61. // 3-报工数据:来源工序产量核查汇总(sfb_id 按同步日期自动生成)
  62. 'report' => [
  63. 'table' => 'wfc_shift_feedback',
  64. 'fields' => [
  65. '员工编号' => 'emp_no',
  66. '=1' => 'shift_id',
  67. '订单编号' => 'ordno',
  68. '生产款号' => 'itemno',
  69. '工序号' => 'op',
  70. '工序名称' => 'routiname',
  71. '数量' => 'qty_comp',
  72. '开工日期' => 'op_begdt',
  73. '完工日期' => 'op_enddt',
  74. '填报日期' => 'tran_date',
  75. '=0202' => 'centerno',
  76. '=0001.02.02' => 'detno',
  77. '=0001' => 'ocode',
  78. '工分' => 'preprc',
  79. ],
  80. ],
  81. ];
  82. /**
  83. * 完工工单列表(含同步状态)
  84. * @ApiMethod (GET)
  85. * @param page 页码
  86. * @param limit 每页条数
  87. * @param search 搜索(订单编号/款号/款式/客户)
  88. * @param sync_status 同步状态筛选:unsynced|pending|synced|failed|cancelled
  89. */
  90. public function getCompletedWorkOrderList()
  91. {
  92. if (!$this->request->isGet()) {
  93. $this->error('请求方法错误');
  94. }
  95. $param = $this->request->param();
  96. $page = max(1, (int)(isset($param['page']) ? $param['page'] : 1));
  97. $limit = max(15, min(100, (int)(isset($param['limit']) ? $param['limit'] : 15)));
  98. $baseWhere = function ($query) use ($param) {
  99. $query->whereNull('Mod_rq')
  100. ->whereNotNull('工单完工日期')
  101. ->where('工单完工日期', '<>', '');
  102. if (!empty($param['search'])) {
  103. $query->where('订单编号|生产款号|客户编号|款式|工单完工日期', 'like', '%' . trim($param['search']) . '%');
  104. }
  105. };
  106. $countQuery = \db('工单_基本资料');
  107. $baseWhere($countQuery);
  108. $total = (int)$countQuery->count();
  109. $listQuery = \db('工单_基本资料');
  110. $baseWhere($listQuery);
  111. $list = $listQuery
  112. ->field('Uniqid,订单编号,生产款号,款式,客户编号,工单完工日期,remark,gd_statu,Sys_id,Sys_rq')
  113. ->order('工单完工日期 desc,订单编号 desc')
  114. ->page($page, $limit)
  115. ->select();
  116. if (!empty($list)) {
  117. $orderNos = array_column($list, '订单编号');
  118. $statusMap = $this->batchResolveDisplaySyncStatus($orderNos);
  119. foreach ($list as &$row) {
  120. $orderNo = $row['订单编号'];
  121. $statusInfo = isset($statusMap[$orderNo]) ? $statusMap[$orderNo] : $this->defaultDisplaySyncStatus();
  122. $row['sync_status'] = $statusInfo['code'];
  123. $row['sync_status_text'] = $statusInfo['text'];
  124. $row['同步状态'] = $statusInfo['text'];
  125. $row['last_sync_time'] = $statusInfo['sync_time'];
  126. $row['last_sync_msg'] = $statusInfo['sync_msg'];
  127. $row['last_sync_log_id'] = $statusInfo['sync_log_id'];
  128. }
  129. unset($row);
  130. if (!empty($param['sync_status'])) {
  131. $filter = $param['sync_status'];
  132. $list = array_values(array_filter($list, function ($row) use ($filter) {
  133. return $row['sync_status'] === $filter;
  134. }));
  135. }
  136. }
  137. $this->success('成功', [
  138. 'total' => $total,
  139. 'page' => $page,
  140. 'limit' => $limit,
  141. 'list' => $list ?: [],
  142. ]);
  143. }
  144. /**
  145. * 同步到 ERP
  146. * @ApiMethod (POST)
  147. * @param work_order_no 工单编号
  148. * @param sync_module 1-工单 2-工艺 3-报工 4-全量
  149. * @param operator 操作人
  150. * @param operate_source backend|api|crontab,默认 backend
  151. * @param operate_type sync|manual|auto,默认 manual
  152. */
  153. public function syncWorkOrder()
  154. {
  155. $this->executeSync('sync');
  156. }
  157. /**
  158. * 重试同步(仅上次失败时可重试)
  159. * @ApiMethod (POST)
  160. */
  161. public function retrySyncWorkOrder()
  162. {
  163. $params = $this->request->post();
  164. $workOrderNo = isset($params['work_order_no']) ? trim($params['work_order_no']) : '';
  165. if ($workOrderNo === '') {
  166. $this->error('工单编号不能为空');
  167. }
  168. $statusInfo = $this->resolveDisplaySyncStatus($workOrderNo);
  169. if ($statusInfo['code'] !== 'failed') {
  170. $this->error('仅同步失败的工单可重试');
  171. }
  172. $this->executeSync('retry');
  173. }
  174. /**
  175. * 撤回同步
  176. * @ApiMethod (POST)
  177. * @param work_order_no 工单编号
  178. * @param operator 操作人
  179. * @param reason 撤回原因
  180. */
  181. public function cancelSyncWorkOrder()
  182. {
  183. if (!$this->request->isPost()) {
  184. $this->error('请求方法错误');
  185. }
  186. $params = $this->request->post();
  187. $workOrderNo = isset($params['work_order_no']) ? trim($params['work_order_no']) : '';
  188. $operator = isset($params['operator']) ? trim($params['operator']) : '';
  189. $reason = isset($params['reason']) ? trim($params['reason']) : '';
  190. if ($workOrderNo === '') {
  191. $this->error('工单编号不能为空');
  192. }
  193. if ($operator === '') {
  194. $this->error('操作人不能为空');
  195. }
  196. $workOrder = $this->fetchWorkOrderBasic($workOrderNo);
  197. if (empty($workOrder)) {
  198. $this->error('工单不存在');
  199. }
  200. $statusInfo = $this->resolveDisplaySyncStatus($workOrderNo);
  201. if ($statusInfo['code'] !== 'synced') {
  202. $this->error('仅已同步的工单可撤回');
  203. }
  204. $operTime = date('Y-m-d H:i:s');
  205. $operateParam = json_encode([
  206. 'work_order_no' => $workOrderNo,
  207. 'reason' => $reason,
  208. ], JSON_UNESCAPED_UNICODE);
  209. $operateContent = '撤回同步工单' . $workOrderNo . ($reason !== '' ? ',原因:' . $reason : '');
  210. $operationLogId = $this->insertOperationLog([
  211. 'operate_type' => 'cancel',
  212. 'operate_user' => $operator,
  213. 'operate_user_name' => isset($params['operate_user_name']) ? $params['operate_user_name'] : $operator,
  214. 'operate_time' => $operTime,
  215. 'sync_log_id' => $statusInfo['sync_log_id'],
  216. 'work_order_no' => $workOrderNo,
  217. 'style_no' => $workOrder['生产款号'],
  218. 'operate_content' => $operateContent,
  219. 'operate_param' => $operateParam,
  220. 'operate_status' => self::OPERATE_STATUS_PROCESSING,
  221. 'operate_source' => isset($params['operate_source']) ? $params['operate_source'] : 'backend',
  222. 'operate_ip' => $this->request->ip(),
  223. 'server_ip' => isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '',
  224. ]);
  225. Db::startTrans();
  226. try {
  227. $this->deleteErpDataByWorkOrder($workOrderNo);
  228. Db::name('mes_erp_operation_log')
  229. ->where('id', $operationLogId)
  230. ->update([
  231. 'operate_status' => self::OPERATE_STATUS_SUCCESS,
  232. ]);
  233. if (!empty($statusInfo['sync_log_id'])) {
  234. Db::name('mes_erp_work_order_sync_log')
  235. ->where('id', $statusInfo['sync_log_id'])
  236. ->update([
  237. 'sync_msg' => '已撤回' . ($reason !== '' ? ':' . $reason : ''),
  238. 'update_time' => $operTime,
  239. ]);
  240. }
  241. Db::commit();
  242. } catch (HttpResponseException $e) {
  243. throw $e;
  244. } catch (\Exception $e) {
  245. Db::rollback();
  246. Db::name('mes_erp_operation_log')
  247. ->where('id', $operationLogId)
  248. ->update([
  249. 'operate_status' => self::OPERATE_STATUS_FAILED,
  250. 'error_msg' => $e->getMessage(),
  251. ]);
  252. $this->error('撤回失败:' . $e->getMessage());
  253. }
  254. $this->success('撤回成功');
  255. }
  256. /**
  257. * 同步日志列表
  258. * @ApiMethod (GET)
  259. * @param work_order_no 工单编号
  260. */
  261. public function getSyncLogList()
  262. {
  263. if (!$this->request->isGet()) {
  264. $this->error('请求方法错误');
  265. }
  266. $workOrderNo = trim($this->request->param('work_order_no', ''));
  267. if ($workOrderNo === '') {
  268. $this->error('工单编号不能为空');
  269. }
  270. $list = Db::name('mes_erp_work_order_sync_log')
  271. ->where('work_order_no', $workOrderNo)
  272. ->order('id desc')
  273. ->select();
  274. foreach ($list as &$row) {
  275. $row['sync_status_text'] = $this->syncStatusText($row['sync_status']);
  276. $row['sync_module_text'] = $this->syncModuleText($row['sync_module']);
  277. }
  278. unset($row);
  279. $this->success('成功', $list ?: []);
  280. }
  281. /**
  282. * 操作过程记录列表
  283. * @ApiMethod (GET)
  284. * @param work_order_no 工单编号
  285. * @param sync_log_id 同步日志ID(可选)
  286. */
  287. public function getOperationLogList()
  288. {
  289. if (!$this->request->isGet()) {
  290. $this->error('请求方法错误');
  291. }
  292. $workOrderNo = trim($this->request->param('work_order_no', ''));
  293. $syncLogId = $this->request->param('sync_log_id');
  294. $query = Db::name('mes_erp_operation_log')->order('id desc');
  295. if ($workOrderNo !== '') {
  296. $query->where('work_order_no', $workOrderNo);
  297. }
  298. if ($syncLogId !== null && $syncLogId !== '') {
  299. $query->where('sync_log_id', intval($syncLogId));
  300. }
  301. if ($workOrderNo === '' && ($syncLogId === null || $syncLogId === '')) {
  302. $this->error('请传入 work_order_no 或 sync_log_id');
  303. }
  304. $list = $query->select();
  305. foreach ($list as &$row) {
  306. $row['operate_status_text'] = $this->operateStatusText($row['operate_status']);
  307. }
  308. unset($row);
  309. $this->success('成功', $list ?: []);
  310. }
  311. /**
  312. * 执行同步核心逻辑
  313. * @param string $operateType sync|retry|manual|auto
  314. */
  315. private function executeSync($operateType)
  316. {
  317. if (!$this->request->isPost()) {
  318. $this->error('请求方法错误');
  319. }
  320. $params = $this->request->post();
  321. $workOrderNo = isset($params['work_order_no']) ? trim($params['work_order_no']) : '';
  322. $syncModule = isset($params['sync_module']) ? (string)$params['sync_module'] : '4';
  323. $operator = isset($params['operator']) ? trim($params['operator']) : '';
  324. if ($workOrderNo === '') {
  325. $this->error('工单编号不能为空');
  326. }
  327. if ($operator === '') {
  328. $this->error('操作人不能为空');
  329. }
  330. if (!in_array($syncModule, ['1', '2', '3', '4'], true)) {
  331. $this->error('同步模块参数错误');
  332. }
  333. $workOrder = $this->fetchWorkOrderBasic($workOrderNo);
  334. if (empty($workOrder)) {
  335. $this->error('工单不存在');
  336. }
  337. if (empty($workOrder['工单完工日期'])) {
  338. $this->error('该工单尚未完工,不能同步');
  339. }
  340. $operTime = date('Y-m-d H:i:s');
  341. $operateSource = isset($params['operate_source']) ? $params['operate_source'] : 'backend';
  342. $requestOperateType = isset($params['operate_type']) ? $params['operate_type'] : 'manual';
  343. if ($operateType === 'retry') {
  344. $requestOperateType = 'retry';
  345. }
  346. $operateParam = json_encode([
  347. 'work_order_no' => $workOrderNo,
  348. 'sync_module' => $syncModule,
  349. 'operator' => $operator,
  350. ], JSON_UNESCAPED_UNICODE);
  351. $operationLogId = $this->insertOperationLog([
  352. 'operate_type' => $requestOperateType,
  353. 'operate_user' => $operator,
  354. 'operate_user_name' => isset($params['operate_user_name']) ? $params['operate_user_name'] : $operator,
  355. 'operate_time' => $operTime,
  356. 'sync_log_id' => null,
  357. 'work_order_no' => $workOrderNo,
  358. 'style_no' => $workOrder['生产款号'],
  359. 'operate_content' => $this->buildSyncOperateContent($workOrderNo, $syncModule, $requestOperateType),
  360. 'operate_param' => $operateParam,
  361. 'operate_status' => self::OPERATE_STATUS_PROCESSING,
  362. 'operate_source' => $operateSource,
  363. 'operate_ip' => $this->request->ip(),
  364. 'server_ip' => isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '',
  365. ]);
  366. $seqRange = $this->fetchReportSeqRange($workOrderNo);
  367. $syncLogId = $this->insertSyncLog([
  368. 'work_order_no' => $workOrderNo,
  369. 'style_no' => $workOrder['生产款号'],
  370. 'complete_date' => $this->formatDateValue($workOrder['工单完工日期']),
  371. 'operator' => $operator,
  372. 'sync_time' => $operTime,
  373. 'report_start_seq' => $seqRange['start'],
  374. 'report_end_seq' => $seqRange['end'],
  375. 'sync_status' => self::SYNC_STATUS_PENDING,
  376. 'sync_module' => $syncModule,
  377. 'sync_msg' => '同步处理中',
  378. 'erp_sync_id' => '',
  379. ]);
  380. Db::name('mes_erp_operation_log')
  381. ->where('id', $operationLogId)
  382. ->update(['sync_log_id' => $syncLogId]);
  383. try {
  384. $erpSyncId = $this->pushDataToErp($workOrderNo, $syncModule, $workOrder);
  385. Db::name('mes_erp_work_order_sync_log')
  386. ->where('id', $syncLogId)
  387. ->update([
  388. 'sync_status' => self::SYNC_STATUS_SUCCESS,
  389. 'sync_msg' => '同步成功',
  390. 'erp_sync_id' => $erpSyncId,
  391. 'update_time' => date('Y-m-d H:i:s'),
  392. ]);
  393. Db::name('mes_erp_operation_log')
  394. ->where('id', $operationLogId)
  395. ->update(['operate_status' => self::OPERATE_STATUS_SUCCESS]);
  396. } catch (HttpResponseException $e) {
  397. throw $e;
  398. } catch (\Exception $e) {
  399. Db::name('mes_erp_work_order_sync_log')
  400. ->where('id', $syncLogId)
  401. ->update([
  402. 'sync_status' => self::SYNC_STATUS_FAILED,
  403. 'sync_msg' => $e->getMessage(),
  404. 'update_time' => date('Y-m-d H:i:s'),
  405. ]);
  406. Db::name('mes_erp_operation_log')
  407. ->where('id', $operationLogId)
  408. ->update([
  409. 'operate_status' => self::OPERATE_STATUS_FAILED,
  410. 'error_msg' => $e->getMessage(),
  411. ]);
  412. $this->error('同步失败:' . $e->getMessage());
  413. }
  414. $this->success('同步成功', [
  415. 'sync_log_id' => $syncLogId,
  416. 'operation_log_id' => $operationLogId,
  417. ]);
  418. }
  419. /**
  420. * 推送数据到 ERP(db2 / SQL Server)
  421. * @param string $workOrderNo
  422. * @param string $syncModule
  423. * @param array $workOrder
  424. * @return string ERP 同步标识
  425. * @throws \Exception
  426. */
  427. private function pushDataToErp($workOrderNo, $syncModule, $workOrder)
  428. {
  429. $erpDb = $this->getErpDb();
  430. $modules = $syncModule === '4' ? ['1', '2', '3'] : [$syncModule];
  431. $inserted = [];
  432. $syncDate = date('Y-m-d');
  433. $workOrder['同步日期'] = $syncDate;
  434. $erpDb->startTrans();
  435. try {
  436. foreach ($modules as $module) {
  437. if ($module === '1') {
  438. $rows = [$workOrder];
  439. $count = $this->insertErpRows($erpDb, 'work_order', $rows);
  440. $inserted['work_order'] = $count;
  441. } elseif ($module === '2') {
  442. $rows = $this->fetchWorkOrderProcessList($workOrderNo);
  443. if (empty($rows)) {
  444. throw new \Exception('工单工艺数据为空');
  445. }
  446. foreach ($rows as &$processRow) {
  447. $processRow['生产款号'] = $workOrder['生产款号'];
  448. $processRow['订单数量'] = $workOrder['订单数量'];
  449. }
  450. unset($processRow);
  451. $count = $this->insertErpRows($erpDb, 'process', $rows);
  452. $inserted['process'] = $count;
  453. } elseif ($module === '3') {
  454. $rows = $this->fetchProductionCheckRows($workOrderNo, $syncDate);
  455. if (empty($rows)) {
  456. throw new \Exception('报工数据为空,请先完成报工');
  457. }
  458. $count = $this->insertErpRows($erpDb, 'report', $rows, $syncDate);
  459. $inserted['report'] = $count;
  460. }
  461. }
  462. $erpDb->commit();
  463. } catch (\Exception $e) {
  464. $erpDb->rollback();
  465. throw $e;
  466. }
  467. return $workOrderNo . '_' . $syncModule . '_' . time();
  468. }
  469. /**
  470. * 写入 ERP 表
  471. */
  472. private function insertErpRows($erpDb, $tableKey, array $sourceRows, $syncDate = null)
  473. {
  474. $config = $this->erpTableMap[$tableKey];
  475. if (empty($config['table'])) {
  476. throw new \Exception('请先在 Synchronization.php 中配置 ERP [' . $tableKey . '] 表名');
  477. }
  478. $sfbSeq = 0;
  479. $sfbPrefix = '';
  480. if ($tableKey === 'report') {
  481. $syncDate = $syncDate ?: date('Y-m-d');
  482. $sfbPrefix = 'K' . date('ymd', strtotime($syncDate));
  483. $sfbSeq = $this->fetchMaxSfbSeq($erpDb, $sfbPrefix);
  484. }
  485. $erpRows = [];
  486. foreach ($sourceRows as $sourceRow) {
  487. $mapped = $this->mapSourceRowToErp($sourceRow, $config['fields']);
  488. if ($tableKey === 'report') {
  489. $sfbSeq++;
  490. $mapped['sfb_id'] = $sfbPrefix . str_pad((string)$sfbSeq, 4, '0', STR_PAD_LEFT);
  491. }
  492. if (!empty($mapped)) {
  493. $erpRows[] = $mapped;
  494. }
  495. }
  496. if (empty($erpRows)) {
  497. throw new \Exception('ERP [' . $tableKey . '] 字段映射未配置或映射结果为空');
  498. }
  499. $result = $erpDb->name($config['table'])->insertAll($erpRows);
  500. if ($result === false || $result === 0) {
  501. throw new \Exception('写入 ERP [' . $tableKey . '] 失败');
  502. }
  503. return (int)$result;
  504. }
  505. /**
  506. * 查询 ERP 报工表当日 sfb_id 最大序号(K+年月日+四位自增)
  507. * 须对末四位做数值 MAX,字符串 MAX 在序号≥10 时会算错导致主键冲突
  508. */
  509. private function fetchMaxSfbSeq($erpDb, $sfbPrefix)
  510. {
  511. $row = $erpDb->query(
  512. 'SELECT MAX(CAST(RIGHT(sfb_id, 4) AS INT)) AS max_seq FROM wfc_shift_feedback WHERE sfb_id LIKE ?',
  513. [$sfbPrefix . '%']
  514. );
  515. if (empty($row) || !isset($row[0]['max_seq']) || $row[0]['max_seq'] === null) {
  516. return 0;
  517. }
  518. return (int)$row[0]['max_seq'];
  519. }
  520. /**
  521. * 撤回时删除 ERP 数据(需配置表名及工单编号字段)
  522. */
  523. private function deleteErpDataByWorkOrder($workOrderNo)
  524. {
  525. $erpDb = $this->getErpDb();
  526. foreach (['work_order', 'process', 'report'] as $tableKey) {
  527. $config = $this->erpTableMap[$tableKey];
  528. if (empty($config['table'])) {
  529. continue;
  530. }
  531. $workOrderField = $this->resolveErpWorkOrderField($tableKey, $config['fields']);
  532. if ($workOrderField === '') {
  533. continue;
  534. }
  535. $erpDb->name($config['table'])->where($workOrderField, $workOrderNo)->delete();
  536. }
  537. }
  538. /**
  539. * MES 行映射为 ERP 行
  540. */
  541. private function mapSourceRowToErp(array $sourceRow, array $fieldMap)
  542. {
  543. $erpRow = [];
  544. foreach ($fieldMap as $mesField => $erpField) {
  545. if ($erpField === '' || $erpField === null) {
  546. continue;
  547. }
  548. if ($mesField !== '' && $mesField[0] === '=') {
  549. $erpRow[$erpField] = substr($mesField, 1);
  550. continue;
  551. }
  552. if (array_key_exists($mesField, $sourceRow)) {
  553. $erpRow[$erpField] = $sourceRow[$mesField];
  554. }
  555. }
  556. return $erpRow;
  557. }
  558. private function resolveErpWorkOrderField($tableKey, array $fieldMap)
  559. {
  560. $candidates = ['订单编号', 'work_order', 'work_order_no'];
  561. if ($tableKey === 'work_order') {
  562. $candidates = ['订单编号', 'work_order_no'];
  563. }
  564. foreach ($candidates as $mesField) {
  565. if (isset($fieldMap[$mesField]) && $fieldMap[$mesField] !== '') {
  566. return $fieldMap[$mesField];
  567. }
  568. }
  569. return '';
  570. }
  571. /**
  572. * 工序产量核查数据(与 WorkOrderProcess::checkProcessProduction 一致,扁平化为报工行)
  573. */
  574. private function fetchProductionCheckRows($workOrderNo, $syncDate = null)
  575. {
  576. $syncDate = $syncDate ?: date('Y-m-d');
  577. $list = Db::name('设备_工分计酬')->alias('a')
  578. ->join('工单_基本资料 b', 'a.work_order = b.订单编号 AND b.Mod_rq IS NULL', 'LEFT')
  579. ->where('a.work_order', $workOrderNo)
  580. ->whereNull('a.del_rq')
  581. ->field('b.订单编号,b.生产款号,b.款式,a.process_code as 工序号,a.process_name as 工序名称,a.staff_no as 员工编号,a.staff_name as 员工姓名,
  582. SUM(a.number) as 数量,MIN(a.date) as 开工日期,MAX(a.date) as 完工日期,
  583. SUM(a.standard_score) as 工分,SUM(a.production_score) as 工时,SUM(a.salary) as 工资')
  584. ->group('b.订单编号,b.生产款号,b.款式,a.process_code,a.process_name,a.staff_no,a.staff_name')
  585. ->order('a.process_code asc,a.staff_no asc')
  586. ->select();
  587. if (empty($list)) {
  588. return [];
  589. }
  590. $rows = [];
  591. foreach ($list as $row) {
  592. $rows[] = [
  593. '订单编号' => $row['订单编号'],
  594. '生产款号' => $row['生产款号'],
  595. '款式' => $row['款式'],
  596. '工序号' => $row['工序号'],
  597. '工序名称' => $row['工序名称'],
  598. '员工编号' => $row['员工编号'],
  599. '员工姓名' => $row['员工姓名'],
  600. '数量' => floatval($row['数量']),
  601. '开工日期' => !empty($row['开工日期']) ? date('Y-m-d', strtotime($row['开工日期'])) : '',
  602. '完工日期' => !empty($row['完工日期']) ? date('Y-m-d', strtotime($row['完工日期'])) : '',
  603. '填报日期' => $syncDate,
  604. '工分' => round(floatval($row['工分']), 4),
  605. '工时' => round(floatval($row['工时']), 4),
  606. '工资' => round(floatval($row['工资']), 4),
  607. ];
  608. }
  609. return $rows;
  610. }
  611. private function fetchWorkOrderBasic($workOrderNo)
  612. {
  613. return Db::table('工单_基本资料')
  614. ->where('订单编号', $workOrderNo)
  615. ->whereNull('Mod_rq')
  616. ->find();
  617. }
  618. private function fetchWorkOrderProcessList($workOrderNo)
  619. {
  620. return Db::table('工单_基础工艺资料')
  621. ->where('work_order', $workOrderNo)
  622. ->whereNull('del_rq')
  623. ->field('work_order,part_code,part_name,process_code,process_name,big_process,
  624. standard_hour,standard_minutes,standard_score,money,coefficient,remark')
  625. ->order('process_code')
  626. ->select();
  627. }
  628. private function fetchReportSeqRange($workOrderNo)
  629. {
  630. $range = Db::table('设备_工分计酬')
  631. ->where('work_order', $workOrderNo)
  632. ->whereNull('del_rq')
  633. ->field('MIN(id) as min_id,MAX(id) as max_id')
  634. ->find();
  635. return [
  636. 'start' => !empty($range['min_id']) ? (string)$range['min_id'] : '',
  637. 'end' => !empty($range['max_id']) ? (string)$range['max_id'] : '',
  638. ];
  639. }
  640. private function insertSyncLog(array $data)
  641. {
  642. $data['create_time'] = date('Y-m-d H:i:s');
  643. $data['update_time'] = $data['create_time'];
  644. return (int)Db::name('mes_erp_work_order_sync_log')->insertGetId($data);
  645. }
  646. private function insertOperationLog(array $data)
  647. {
  648. $data['create_time'] = date('Y-m-d H:i:s');
  649. return (int)Db::name('mes_erp_operation_log')->insertGetId($data);
  650. }
  651. private function getErpDb()
  652. {
  653. return Db::connect(config('database.db2'));
  654. }
  655. private function batchResolveDisplaySyncStatus(array $orderNos)
  656. {
  657. $orderNos = array_values(array_unique(array_filter($orderNos)));
  658. if (empty($orderNos)) {
  659. return [];
  660. }
  661. $defaultStatus = $this->defaultDisplaySyncStatus();
  662. $result = [];
  663. foreach ($orderNos as $orderNo) {
  664. $result[$orderNo] = $defaultStatus;
  665. }
  666. $latestSyncRows = Db::name('mes_erp_work_order_sync_log')
  667. ->whereIn('work_order_no', $orderNos)
  668. ->order('id desc')
  669. ->select();
  670. $latestSyncMap = [];
  671. foreach ($latestSyncRows as $row) {
  672. if (!isset($latestSyncMap[$row['work_order_no']])) {
  673. $latestSyncMap[$row['work_order_no']] = $row;
  674. }
  675. }
  676. $successSyncRows = Db::name('mes_erp_work_order_sync_log')
  677. ->whereIn('work_order_no', $orderNos)
  678. ->where('sync_status', self::SYNC_STATUS_SUCCESS)
  679. ->order('id desc')
  680. ->select();
  681. $latestSuccessSyncMap = [];
  682. foreach ($successSyncRows as $row) {
  683. if (!isset($latestSuccessSyncMap[$row['work_order_no']])) {
  684. $latestSuccessSyncMap[$row['work_order_no']] = $row;
  685. }
  686. }
  687. $cancelRows = Db::name('mes_erp_operation_log')
  688. ->whereIn('work_order_no', $orderNos)
  689. ->where('operate_type', 'cancel')
  690. ->where('operate_status', self::OPERATE_STATUS_SUCCESS)
  691. ->order('id desc')
  692. ->select();
  693. $latestCancelMap = [];
  694. foreach ($cancelRows as $row) {
  695. if (!isset($latestCancelMap[$row['work_order_no']])) {
  696. $latestCancelMap[$row['work_order_no']] = $row;
  697. }
  698. }
  699. foreach ($orderNos as $orderNo) {
  700. $latestSync = isset($latestSyncMap[$orderNo]) ? $latestSyncMap[$orderNo] : null;
  701. if (empty($latestSync)) {
  702. continue;
  703. }
  704. $latestSuccessSync = isset($latestSuccessSyncMap[$orderNo]) ? $latestSuccessSyncMap[$orderNo] : null;
  705. if (!empty($latestSuccessSync)) {
  706. $cancelAfterSync = isset($latestCancelMap[$orderNo]) ? $latestCancelMap[$orderNo] : null;
  707. if (!empty($cancelAfterSync) && $cancelAfterSync['operate_time'] >= $latestSuccessSync['sync_time']) {
  708. $result[$orderNo] = [
  709. 'code' => 'cancelled',
  710. 'text' => '已撤回',
  711. 'sync_time' => $latestSuccessSync['sync_time'],
  712. 'sync_msg' => $latestSync['sync_msg'],
  713. 'sync_log_id' => $latestSuccessSync['id'],
  714. ];
  715. continue;
  716. }
  717. $result[$orderNo] = [
  718. 'code' => 'synced',
  719. 'text' => '已同步',
  720. 'sync_time' => $latestSuccessSync['sync_time'],
  721. 'sync_msg' => $latestSuccessSync['sync_msg'],
  722. 'sync_log_id' => $latestSuccessSync['id'],
  723. ];
  724. continue;
  725. }
  726. if ((int)$latestSync['sync_status'] === self::SYNC_STATUS_FAILED) {
  727. $result[$orderNo] = [
  728. 'code' => 'failed',
  729. 'text' => '同步失败',
  730. 'sync_time' => $latestSync['sync_time'],
  731. 'sync_msg' => $latestSync['sync_msg'],
  732. 'sync_log_id' => $latestSync['id'],
  733. ];
  734. continue;
  735. }
  736. $result[$orderNo] = [
  737. 'code' => 'pending',
  738. 'text' => '待同步',
  739. 'sync_time' => $latestSync['sync_time'],
  740. 'sync_msg' => $latestSync['sync_msg'],
  741. 'sync_log_id' => $latestSync['id'],
  742. ];
  743. }
  744. return $result;
  745. }
  746. private function resolveDisplaySyncStatus($workOrderNo)
  747. {
  748. $latestSync = Db::name('mes_erp_work_order_sync_log')
  749. ->where('work_order_no', $workOrderNo)
  750. ->order('id desc')
  751. ->find();
  752. if (empty($latestSync)) {
  753. return $this->defaultDisplaySyncStatus();
  754. }
  755. $latestSuccessSync = Db::name('mes_erp_work_order_sync_log')
  756. ->where('work_order_no', $workOrderNo)
  757. ->where('sync_status', self::SYNC_STATUS_SUCCESS)
  758. ->order('id desc')
  759. ->find();
  760. if (!empty($latestSuccessSync)) {
  761. $cancelAfterSync = Db::name('mes_erp_operation_log')
  762. ->where('work_order_no', $workOrderNo)
  763. ->where('operate_type', 'cancel')
  764. ->where('operate_status', self::OPERATE_STATUS_SUCCESS)
  765. ->where('operate_time', '>=', $latestSuccessSync['sync_time'])
  766. ->order('id desc')
  767. ->find();
  768. if (!empty($cancelAfterSync)) {
  769. return [
  770. 'code' => 'cancelled',
  771. 'text' => '已撤回',
  772. 'sync_time' => $latestSuccessSync['sync_time'],
  773. 'sync_msg' => $latestSync['sync_msg'],
  774. 'sync_log_id' => $latestSuccessSync['id'],
  775. ];
  776. }
  777. return [
  778. 'code' => 'synced',
  779. 'text' => '已同步',
  780. 'sync_time' => $latestSuccessSync['sync_time'],
  781. 'sync_msg' => $latestSuccessSync['sync_msg'],
  782. 'sync_log_id' => $latestSuccessSync['id'],
  783. ];
  784. }
  785. if ((int)$latestSync['sync_status'] === self::SYNC_STATUS_FAILED) {
  786. return [
  787. 'code' => 'failed',
  788. 'text' => '同步失败',
  789. 'sync_time' => $latestSync['sync_time'],
  790. 'sync_msg' => $latestSync['sync_msg'],
  791. 'sync_log_id' => $latestSync['id'],
  792. ];
  793. }
  794. return [
  795. 'code' => 'pending',
  796. 'text' => '待同步',
  797. 'sync_time' => $latestSync['sync_time'],
  798. 'sync_msg' => $latestSync['sync_msg'],
  799. 'sync_log_id' => $latestSync['id'],
  800. ];
  801. }
  802. private function defaultDisplaySyncStatus()
  803. {
  804. return [
  805. 'code' => 'unsynced',
  806. 'text' => '未同步',
  807. 'sync_time' => '',
  808. 'sync_msg' => '',
  809. 'sync_log_id' => null,
  810. ];
  811. }
  812. private function buildSyncOperateContent($workOrderNo, $syncModule, $operateType)
  813. {
  814. $moduleText = $this->syncModuleText($syncModule);
  815. $typeMap = [
  816. 'sync' => '同步',
  817. 'retry' => '重试同步',
  818. 'manual' => '手动同步',
  819. 'auto' => '自动同步',
  820. ];
  821. $typeText = isset($typeMap[$operateType]) ? $typeMap[$operateType] : '同步';
  822. return $typeText . '工单' . $workOrderNo . ',模块:' . $moduleText;
  823. }
  824. private function syncStatusText($status)
  825. {
  826. $map = [
  827. self::SYNC_STATUS_PENDING => '待同步',
  828. self::SYNC_STATUS_SUCCESS => '同步成功',
  829. self::SYNC_STATUS_FAILED => '同步失败',
  830. ];
  831. return isset($map[(int)$status]) ? $map[(int)$status] : '未知';
  832. }
  833. private function syncModuleText($module)
  834. {
  835. $map = [
  836. '1' => '工单数据',
  837. '2' => '工艺数据',
  838. '3' => '报工数据',
  839. '4' => '全量同步',
  840. ];
  841. return isset($map[(string)$module]) ? $map[(string)$module] : (string)$module;
  842. }
  843. private function operateStatusText($status)
  844. {
  845. $map = [
  846. self::OPERATE_STATUS_SUCCESS => '成功',
  847. self::OPERATE_STATUS_FAILED => '失败',
  848. self::OPERATE_STATUS_PROCESSING => '处理中',
  849. ];
  850. return isset($map[(int)$status]) ? $map[(int)$status] : '未知';
  851. }
  852. private function formatDateValue($value)
  853. {
  854. if (empty($value)) {
  855. return null;
  856. }
  857. $timestamp = strtotime($value);
  858. return $timestamp ? date('Y-m-d', $timestamp) : null;
  859. }
  860. }