ProductSurveillanceJobs.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <?php
  2. namespace App\Jobs\Manager\CollectData\Statistics;
  3. use Illuminate\Bus\Queueable;
  4. use Illuminate\Contracts\Queue\ShouldBeUnique;
  5. use Illuminate\Contracts\Queue\ShouldQueue;
  6. use Illuminate\Foundation\Bus\Dispatchable;
  7. use Illuminate\Queue\InteractsWithQueue;
  8. use Illuminate\Queue\SerializesModels;
  9. use App\Models\Manager\Collect\Product as ProductModel;
  10. use App\Models\Manager\Process\LowPriceGoods as ProcessLowPriceGoodsModel;
  11. use App\Models\Manager\Process\ScrapeData as ScrapeDataModel;
  12. use App\Models\Manager\WashConfig\LowPriceGoods as WashConfigLowPriceGoodsModel;
  13. use App\Models\Manager\Process\ViolationProduct as ProcessViolationProductModel;
  14. use App\Models\Manager\WashConfig\ViolationProduct as WashConfigViolationProductModel;
  15. use App\Models\Manager\Process\ViolationStore as ProcessViolationStoreModel;
  16. use App\Models\Manager\WashConfig\ViolationStore as WashConfigViolationStoreModel;
  17. use App\Models\Manager\Collect\SurveillanceWarningNotice as SurveillanceWarningNoticeModel;
  18. use App\Jobs\Manager\Process\SendEmailJobs;
  19. use Illuminate\Support\Facades\Cache;
  20. use App\Facades\Servers\Logs\Log;
  21. /**
  22. * 商品数据-监控采集清洗情况
  23. * @author: 唐远望
  24. * @version: 1.0
  25. * @date: 2026-06-03
  26. */
  27. class ProductSurveillanceJobs implements ShouldQueue
  28. {
  29. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  30. public $tries = 3; // 限制重试次数
  31. public $timeout = 600; // 10分钟超时
  32. protected $message_data;
  33. /**
  34. * Create a new job instance.
  35. *
  36. * @return void
  37. */
  38. public function __construct(array $message_data)
  39. {
  40. $this->message_data = $message_data;
  41. }
  42. /**
  43. * Execute the job.
  44. *
  45. * @return void
  46. */
  47. public function handle()
  48. {
  49. try {
  50. $ProductModel = new ProductModel();
  51. $ScrapeDataModel = new ScrapeDataModel();
  52. $ProcessLowPriceGoodsModel = new ProcessLowPriceGoodsModel();
  53. $WashConfigLowPriceGoodsModel = new WashConfigLowPriceGoodsModel();
  54. $ProcessViolationProductModel = new ProcessViolationProductModel();
  55. $WashConfigViolationProductModel = new WashConfigViolationProductModel();
  56. $ProcessViolationStoreModel = new ProcessViolationStoreModel();
  57. $WashConfigViolationStoreModel = new WashConfigViolationStoreModel();
  58. $SurveillanceWarningNoticeModel = new SurveillanceWarningNoticeModel();
  59. //获取已经开启邮箱通知的用户
  60. $email_notice_list = $SurveillanceWarningNoticeModel->where([['email', '!=', '']])->where('status', 0)->get()->toarray();
  61. if (empty($email_notice_list)) return true;
  62. $company_id = isset($this->message_data['company_id']) ? $this->message_data['company_id'] : 0; //品牌方公司ID
  63. $company_info = $this->message_data['company_info'];
  64. $start_time = $this->message_data['start_time'];
  65. $end_time = $this->message_data['end_time'];
  66. $start_time_string = date('Y-m-d H:i:s', $start_time);
  67. $end_time_string = date('Y-m-d H:i:s', $end_time);
  68. $key_name = 'LowPriceProductSurveillanceJobs_' . $company_id . '_' . $start_time . '_' . $end_time;
  69. $is_end_select = Cache::get($key_name);
  70. if (!empty($is_end_select)) return true;
  71. $config_collect_product_content = ''; //配置采集商品任务:
  72. $actual_collect_product_content = ''; //实际采集到的商品:
  73. $config_low_product_content = ''; //配置的低价清洗商品:
  74. $cleaned_low_product_content = ''; //实际清洗出来的低价商品:
  75. $config_violation_product_content = ''; //配置的违规清洗商品:
  76. $cleaned_violation_product_content = ''; //实际清洗出来的违规商品:
  77. $config_violation_store_content = ''; //配置的违规店铺:
  78. $cleaned_violation_store_content = ''; //实际清洗出来的违规店铺:
  79. $current_time = time();
  80. // 获取采集时间是星期几
  81. $now_week = date('w', $current_time);
  82. if ($now_week == 0) {
  83. $now_week = 7;
  84. }
  85. //配置采集商品任务商品查询
  86. $where = [];
  87. $where[] = ['company_id', '=', $company_id];
  88. $where[] = ['status', '=', 0];
  89. $need_config_product_data = [];
  90. $config_product_data_info = $ProductModel->WhereRaw("FIND_IN_SET(?, sampling_cycle)", [$now_week])->select(['product_name', 'sampling_start_time', 'sampling_end_time'])->where($where)->get()->toArray();
  91. if (empty($config_product_data_info)) {
  92. $config_collect_product_content .= '无' . ';';
  93. //没有采集任务不处理
  94. return true;
  95. } else {
  96. $config_collect_product_list = [];
  97. foreach ($config_product_data_info as $product_name) {
  98. $sampling_start_time = $product_name['sampling_start_time'];
  99. $sampling_end_time = $product_name['sampling_end_time'];
  100. if ($sampling_start_time != '' && $sampling_end_time != '' && ($current_time < $sampling_start_time || $current_time > $sampling_end_time)) {
  101. continue; //如果当前时间小于采集开始时间或者大于采集结束时间则跳过
  102. }
  103. $need_config_product_data[$product_name['product_name']] = $product_name;
  104. }
  105. if (empty($need_config_product_data)) {
  106. return true; //没有采集任务不处理
  107. }
  108. foreach ($need_config_product_data as $product_name) {
  109. $config_collect_product_content .= $product_name['product_name'] . ';';
  110. //实际采集到的商品
  111. $where = [];
  112. $where[] = ['insert_time', '>=', $start_time_string];
  113. $where[] = ['insert_time', '<=', $end_time_string];
  114. $where[] = ['enterprise_id', '=', $company_id];
  115. $where[] = ['product_name', 'like', '%' . $product_name['product_name'] . '%']; // 增加模糊匹配
  116. $actual_product_data_count = $ScrapeDataModel->where($where)->count();
  117. if ($actual_product_data_count < 1) {
  118. $actual_collect_product_content .= '无' . ';';
  119. } else {
  120. $actual_collect_product_content .= $product_name['product_name'] . ':数量' . $actual_product_data_count . ';';
  121. }
  122. }
  123. }
  124. //配置的低价清洗商品
  125. $where = [];
  126. $where[] = ['company_id', '=', $company_id];
  127. $where[] = ['status', '=', 0];
  128. $config_low_product_data_info = $WashConfigLowPriceGoodsModel->select('product_name')->where($where)->groupBy('product_name')->get()->toArray();
  129. if (empty($config_low_product_data_info)) {
  130. $config_low_product_content .= '无' . ';';
  131. } else {
  132. foreach ($config_low_product_data_info as $product_name) {
  133. $config_low_product_content .= $product_name['product_name'] . ';';
  134. //实际清洗出来的低价商品
  135. $where = [];
  136. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  137. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  138. $where[] = ['company_id', '=', $company_id];
  139. $where[] = ['product_name', 'like', '%' . $product_name['product_name'] . '%']; // 增加模糊匹配
  140. $cleaned_product_data_count = $ProcessLowPriceGoodsModel->where($where)->count();
  141. if ($cleaned_product_data_count < 1) {
  142. $cleaned_low_product_content .= '无' . ';';
  143. } else {
  144. $cleaned_low_product_content .= $product_name['product_name'] . ':数量' . $cleaned_product_data_count . ';';
  145. }
  146. }
  147. }
  148. //配置的违规清洗商品
  149. $where = [];
  150. $where[] = ['company_id', '=', $company_id];
  151. $where[] = ['insert_time', '>=', $start_time_string];
  152. $config_violation_product_data_info = $WashConfigViolationProductModel->select('product_name')->where($where)->groupBy('product_name')->get()->toArray();
  153. if (empty($config_violation_product_data_info)) {
  154. $config_violation_product_content .= '无' . ';';
  155. } else {
  156. foreach ($config_violation_product_data_info as $product_name) {
  157. $config_violation_product_content .= $product_name['product_name'] . ';';
  158. //实际清洗出来的违规商品
  159. $where = [];
  160. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  161. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  162. $where[] = ['company_id', '=', $company_id];
  163. $where[] = ['product_name', 'like', '%' . $product_name['product_name'] . '%']; // 增加模糊匹配
  164. $cleaned_product_data_count = $ProcessViolationProductModel->where($where)->count();
  165. if ($cleaned_product_data_count < 1) {
  166. $cleaned_violation_product_content .= '无' . ';';
  167. } else {
  168. $cleaned_violation_product_content .= $product_name['product_name'] . ':数量' . $cleaned_product_data_count . ';';
  169. }
  170. }
  171. }
  172. //配置的违规店铺
  173. $where = [];
  174. $where[] = ['company_id', '=', $company_id];
  175. $where[] = ['insert_time', '<=', $end_time_string];
  176. $config_violation_store_data_info = $WashConfigViolationStoreModel->select('store_name')->where($where)->groupBy('store_name')->get()->toArray();
  177. if (empty($config_violation_store_data_info)) {
  178. $config_violation_store_content .= '无' . ';';
  179. } else {
  180. foreach ($config_violation_store_data_info as $store_name) {
  181. $config_violation_store_content .= $store_name['store_name'] . ';';
  182. //实际清洗出来的违规店铺
  183. $where = [];
  184. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  185. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  186. $where[] = ['company_id', '=', $company_id];
  187. $where[] = ['store_name', 'like', '%' . $store_name['store_name'] . '%']; // 增加模糊匹配
  188. $cleaned_store_data_count = $ProcessViolationStoreModel->where($where)->count();
  189. if ($cleaned_store_data_count < 1) {
  190. $cleaned_violation_store_content .= '无' . ';';
  191. } else {
  192. $cleaned_violation_store_content .= $store_name['store_name'] . ':数量' . $cleaned_store_data_count . ';';
  193. }
  194. }
  195. }
  196. //合并展示内容
  197. $email_title = '【' . $company_info['company_name'] . '】' . "数据监控-采集和清洗情况通知";
  198. $email_content = $config_collect_product_content . $actual_collect_product_content . $config_low_product_content . $cleaned_low_product_content .
  199. $config_violation_product_content . $cleaned_violation_product_content . $config_violation_store_content . $cleaned_violation_store_content;
  200. $email_content_html = $this->buildEmailHtml(
  201. $config_collect_product_content,
  202. $actual_collect_product_content,
  203. $config_low_product_content,
  204. $cleaned_low_product_content,
  205. $config_violation_product_content,
  206. $cleaned_violation_product_content,
  207. $config_violation_store_content,
  208. $cleaned_violation_store_content
  209. );
  210. // echo $email_content_html; exit;
  211. //发送预警通知
  212. foreach ($email_notice_list as $email_notice) {
  213. $message_data = [
  214. 'email' => $email_notice['email'],
  215. 'email_content' => json_encode(['title' => $email_title, 'content' => $email_content_html]),
  216. ];
  217. SendEmailJobs::dispatch(['notice_data_info' => $message_data]);
  218. }
  219. //记录日志
  220. } catch (\Exception $e) {
  221. print_r($e->getMessage());
  222. exit;
  223. Log::info('job_error', '低价商品数据数据-监控采集清洗情况队列失败', ['data' => $this->message_data, 'error' => $e->getMessage()]);
  224. }
  225. }
  226. /**
  227. * 构建美观的邮件HTML内容
  228. */
  229. private function buildEmailHtml(
  230. $config_collect,
  231. $actual_collect,
  232. $config_low,
  233. $cleaned_low,
  234. $config_violation_product,
  235. $cleaned_violation_product,
  236. $config_violation_store,
  237. $cleaned_violation_store
  238. ) {
  239. $html = '
  240. <!DOCTYPE html>
  241. <html>
  242. <head>
  243. <meta charset="UTF-8">
  244. <style>
  245. body {
  246. font-family: "Microsoft YaHei", Arial, sans-serif;
  247. background-color: #f5f7fa;
  248. margin: 0;
  249. padding: 20px;
  250. }
  251. .email-container {
  252. max-width: 800px;
  253. margin: 0 auto;
  254. background-color: #ffffff;
  255. border-radius: 12px;
  256. overflow: hidden;
  257. box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  258. }
  259. .email-header {
  260. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  261. color: white;
  262. padding: 30px 20px;
  263. text-align: center;
  264. }
  265. .email-header h1 {
  266. margin: 0;
  267. font-size: 24px;
  268. font-weight: 600;
  269. }
  270. .email-header p {
  271. margin: 10px 0 0;
  272. opacity: 0.9;
  273. font-size: 14px;
  274. }
  275. .email-body {
  276. padding: 30px 25px;
  277. }
  278. .section {
  279. margin-bottom: 25px;
  280. border-left: 4px solid;
  281. padding: 12px 20px;
  282. background-color: #f8f9fc;
  283. border-radius: 8px;
  284. }
  285. .section-title {
  286. font-size: 16px;
  287. font-weight: 600;
  288. margin-bottom: 12px;
  289. display: flex;
  290. align-items: center;
  291. gap: 8px;
  292. }
  293. .section-title .icon {
  294. font-size: 20px;
  295. }
  296. .section-content {
  297. font-size: 14px;
  298. color: #555;
  299. line-height: 1.8;
  300. word-break: break-all;
  301. }
  302. .product-item {
  303. display: inline-block;
  304. background: #e8ecf1;
  305. padding: 4px 12px;
  306. border-radius: 20px;
  307. margin: 4px 6px 4px 0;
  308. font-size: 13px;
  309. }
  310. .product-item strong {
  311. color: #667eea;
  312. }
  313. .empty-data {
  314. color: #999;
  315. font-style: italic;
  316. }
  317. .separator {
  318. border-top: 1px dashed #e0e4e8;
  319. margin: 20px 0;
  320. }
  321. .email-footer {
  322. background-color: #f8f9fc;
  323. padding: 15px 25px;
  324. text-align: center;
  325. font-size: 12px;
  326. color: #999;
  327. border-top: 1px solid #eef2f6;
  328. }
  329. .badge {
  330. display: inline-block;
  331. background: #667eea;
  332. color: white;
  333. font-size: 12px;
  334. padding: 2px 8px;
  335. border-radius: 12px;
  336. margin-left: 8px;
  337. }
  338. .stats-number {
  339. font-weight: 700;
  340. color: #667eea;
  341. font-size: 16px;
  342. }
  343. </style>
  344. </head>
  345. <body>
  346. <div class="email-container">
  347. <div class="email-header">
  348. <h1>数据监控报告</h1>
  349. <p>采集与清洗情况通知</p>
  350. </div>
  351. <div class="email-body">
  352. ' . $this->buildSection('🎯 配置采集商品任务', 'config_collect', $config_collect, '#667eea') . '
  353. ' . $this->buildSection('✅ 实际采集到的商品', 'actual_collect', $actual_collect, '#48bb78') . '
  354. <div class="separator"></div>
  355. ' . $this->buildSection('⚙️ 配置的低价清洗商品', 'config_low', $config_low, '#ed8936') . '
  356. ' . $this->buildSection('🔍 实际清洗出来的低价商品', 'cleaned_low', $cleaned_low, '#ed8936') . '
  357. <div class="separator"></div>
  358. ' . $this->buildSection('⚠️ 配置的违规清洗商品', 'config_violation_product', $config_violation_product, '#e53e3e') . '
  359. ' . $this->buildSection('🚫 实际清洗出来的违规商品', 'cleaned_violation_product', $cleaned_violation_product, '#e53e3e') . '
  360. <div class="separator"></div>
  361. ' . $this->buildSection('🏪 配置的违规店铺', 'config_violation_store', $config_violation_store, '#9b59b6') . '
  362. ' . $this->buildSection('🚷 实际清洗出来的违规店铺', 'cleaned_violation_store', $cleaned_violation_store, '#9b59b6') . '
  363. </div>
  364. <div class="email-footer">
  365. <p>此邮件由系统自动发送,请勿直接回复 | 数据统计时间:' . date('Y-m-d H:i:s') . '</p>
  366. </div>
  367. </div>
  368. </body>
  369. </html>';
  370. return $html;
  371. }
  372. /**
  373. * 构建单个数据区块
  374. */
  375. private function buildSection($title, $type, $content, $color)
  376. {
  377. $icon = $this->getIconByType($type);
  378. $formattedContent = $this->formatContent($content);
  379. return '
  380. <div class="section" style="border-left-color: ' . $color . ';">
  381. <div class="section-title">
  382. <span class="icon"></span>
  383. <span>' . $title . '</span>
  384. </div>
  385. <div class="section-content">
  386. ' . $formattedContent . '
  387. </div>
  388. </div>';
  389. }
  390. /**
  391. * 格式化内容,将分号分隔的数据转换为美观的标签形式
  392. */
  393. private function formatContent($content)
  394. {
  395. // 如果内容为空或者只包含分号
  396. if (empty($content) || trim($content) === ';' || trim($content) === '无;') {
  397. return '<span class="empty-data">📭 暂无数据</span>';
  398. }
  399. // 检查是否包含"无;"的情况
  400. if (strpos($content, '无;') !== false && strlen($content) <= 6) {
  401. return '<span class="empty-data">📭 暂无数据</span>';
  402. }
  403. // 移除末尾的分号
  404. $content = rtrim($content, ';');
  405. // 按分号分割
  406. $items = explode(';', $content);
  407. $html = '';
  408. foreach ($items as $item) {
  409. $item = trim($item);
  410. if (empty($item)) continue;
  411. // 跳过"无"项
  412. if ($item === '无') continue;
  413. // 检查是否包含数量信息(格式:商品名:数量N)
  414. if (preg_match('/^(.+):数量(\d+)$/u', $item, $matches)) {
  415. $productName = trim($matches[1]);
  416. $count = $matches[2];
  417. $html .= '<span class="product-item"><strong>' . htmlspecialchars($productName) . '</strong> <span class="stats-number">' . $count . '</span> 件</span>';
  418. } else {
  419. // 普通商品名
  420. $html .= '<span class="product-item">' . htmlspecialchars($item) . '</span>';
  421. }
  422. }
  423. return $html ?: '<span class="empty-data">📭 暂无数据</span>';
  424. }
  425. /**
  426. * 根据类型获取对应的图标
  427. */
  428. private function getIconByType($type)
  429. {
  430. $icons = [
  431. 'config_collect' => '📋',
  432. 'actual_collect' => '📦',
  433. 'config_low' => '💰',
  434. 'cleaned_low' => '💎',
  435. 'config_violation_product' => '⚠️',
  436. 'cleaned_violation_product' => '🚫',
  437. 'config_violation_store' => '🏪',
  438. 'cleaned_violation_store' => '🔨',
  439. ];
  440. return $icons[$type] ?? '📌';
  441. }
  442. public function failed(\Throwable $exception)
  443. {
  444. Log::info('job_error', '低价商品数据数据-监控采集清洗情况队列完全失败', ['data' => $this->message_data, 'error' => $exception->getMessage()]);
  445. }
  446. }