ProductSurveillanceJobs.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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', 'platform', '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. //实际采集到的所有商品
  97. $where = [];
  98. $where[] = ['insert_time', '>=', $start_time_string];
  99. $where[] = ['insert_time', '<=', $end_time_string];
  100. $where[] = ['enterprise_id', '=', $company_id];
  101. $scrape_data_list = $ScrapeDataModel->where($where)->get()->toarray();
  102. foreach ($config_product_data_info as $product_data) {
  103. $sampling_start_time = $product_data['sampling_start_time'];
  104. $sampling_end_time = $product_data['sampling_end_time'];
  105. $platform_id_data = explode(',', $product_data['platform']);
  106. if ($sampling_start_time != '' && $sampling_end_time != '' && ($current_time < $sampling_start_time || $current_time > $sampling_end_time)) {
  107. continue; //如果当前时间小于采集开始时间或者大于采集结束时间则跳过
  108. }
  109. if (isset($need_config_product_data[$product_data['product_name']])) {
  110. $platform_id_data_merge = array_merge($need_config_product_data[$product_data['product_name']], $platform_id_data);
  111. $platform_id_data_merge = array_unique($platform_id_data_merge);
  112. $need_config_product_data[$product_data['product_name']] = $platform_id_data_merge;
  113. } else {
  114. $need_config_product_data[$product_data['product_name']] = $platform_id_data;
  115. }
  116. }
  117. if (empty($need_config_product_data)) {
  118. return true; //没有采集任务不处理
  119. }
  120. //更新平台名称
  121. foreach ($need_config_product_data as $product_name => $platform_id_data) {
  122. $platform_name_list = '';
  123. $platform_product_name_sting = '';
  124. foreach ($platform_id_data as $platform_id) {
  125. $platform_name = $this->platform($platform_id);
  126. //实际采集到的商品
  127. $where = [];
  128. $where[] = ['insert_time', '>=', $start_time_string];
  129. $where[] = ['insert_time', '<=', $end_time_string];
  130. $where[] = ['enterprise_id', '=', $company_id];
  131. $where[] = ['platform_id', '=', $platform_id];
  132. $where[] = ['product_name', 'like', '%' . $product_name . '%']; // 增加模糊匹配
  133. $actual_product_data_count = 0;
  134. if (!empty($scrape_data_list)) {
  135. foreach ($scrape_data_list as $scrape_data) {
  136. if ($scrape_data['platform_id'] == $platform_id && strpos($scrape_data['product_name'], $product_name) !== false) {
  137. $actual_product_data_count++;
  138. }
  139. }
  140. }
  141. $platform_product_name_sting .= '【' . $platform_name . '】' . ':' . $actual_product_data_count . '条';
  142. $platform_name_list .= $platform_name . '/';
  143. }
  144. $actual_collect_product_content .= $product_name . $platform_product_name_sting . ';';
  145. $product_name_sting = $product_name . '【' . $platform_name_list . '】';
  146. $config_collect_product_content .= $product_name_sting . ';';
  147. }
  148. }
  149. //配置的低价清洗商品
  150. $where = [];
  151. $where[] = ['company_id', '=', $company_id];
  152. $where[] = ['status', '=', 0];
  153. $config_low_product_data_info = $WashConfigLowPriceGoodsModel->select('product_name')->where($where)->groupBy('product_name')->get()->toArray();
  154. if (empty($config_low_product_data_info)) {
  155. $config_low_product_content .= '无' . ';';
  156. } else {
  157. //实际清洗出来的低价商品
  158. $where = [];
  159. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  160. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  161. $where[] = ['company_id', '=', $company_id];
  162. $process_low_priceGoods = $ProcessLowPriceGoodsModel->where($where)->get()->toarray();
  163. foreach ($config_low_product_data_info as $product_name) {
  164. $config_low_product_content .= $product_name['product_name'] . ';';
  165. $cleaned_product_data_count = 0;
  166. if (!empty($process_low_priceGoods)) {
  167. foreach ($process_low_priceGoods as $process_low_priceGood) {
  168. if (strpos($process_low_priceGood['product_name'], $product_name['product_name']) !== false) {
  169. $cleaned_product_data_count++;
  170. }
  171. }
  172. }
  173. $cleaned_low_product_content .= $product_name['product_name'] . ':数量' . $cleaned_product_data_count . '条;';
  174. }
  175. }
  176. //配置的违规清洗商品
  177. $where = [];
  178. $where[] = ['company_id', '=', $company_id];
  179. $where[] = ['insert_time', '>=', $start_time_string];
  180. $config_violation_product_data_info = $WashConfigViolationProductModel->select('product_name')->where($where)->groupBy('product_name')->get()->toArray();
  181. if (empty($config_violation_product_data_info)) {
  182. $config_violation_product_content .= '无' . ';';
  183. } else {
  184. //实际清洗出来的违规商品
  185. $where = [];
  186. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  187. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  188. $where[] = ['company_id', '=', $company_id];
  189. $process_violation_products = $ProcessViolationProductModel->where($where)->get()->toarray();
  190. foreach ($config_violation_product_data_info as $product_name) {
  191. $config_violation_product_content .= $product_name['product_name'] . ';';
  192. $cleaned_product_data_count = 0;
  193. if (!empty($process_violation_products)) {
  194. foreach ($process_violation_products as $process_violation_product) {
  195. if (strpos($process_violation_product['product_name'], $product_name['product_name']) !== false) {
  196. $cleaned_product_data_count++;
  197. }
  198. }
  199. $cleaned_violation_product_content .= $product_name['product_name'] . ':数量' . $cleaned_product_data_count . '条;';
  200. }
  201. }
  202. }
  203. //配置的违规店铺
  204. $where = [];
  205. $where[] = ['company_id', '=', $company_id];
  206. $where[] = ['insert_time', '<=', $end_time_string];
  207. $config_violation_store_data_info = $WashConfigViolationStoreModel->select('store_name')->where($where)->groupBy('store_name')->get()->toArray();
  208. if (empty($config_violation_store_data_info)) {
  209. $config_violation_store_content .= '无' . ';';
  210. } else {
  211. //实际清洗出来的违规店铺
  212. $where = [];
  213. $where[] = ['insert_time', '>=', strtotime($start_time_string)];
  214. $where[] = ['insert_time', '<=', strtotime($end_time_string)];
  215. $where[] = ['company_id', '=', $company_id];
  216. $process_violation_store = $ProcessViolationStoreModel->where($where)->get()->toarray();
  217. foreach ($config_violation_store_data_info as $store_name) {
  218. $config_violation_store_content .= $store_name['store_name'] . ';';
  219. $cleaned_store_data_count = 0;
  220. if (!empty($process_violation_store)) {
  221. foreach ($process_violation_store as $process_violation_store) {
  222. if (strpos($process_violation_store['store_name'], $store_name['store_name']) !== false) {
  223. $cleaned_store_data_count++;
  224. }
  225. }
  226. }
  227. $cleaned_violation_store_content .= $store_name['store_name'] . ':数量' . $cleaned_store_data_count . '条;';
  228. }
  229. }
  230. //合并展示内容
  231. $email_title = '【' . $company_info['company_name'] . '】' . "数据监控-采集和清洗情况通知";
  232. $email_content = $config_collect_product_content . $actual_collect_product_content . $config_low_product_content . $cleaned_low_product_content .
  233. $config_violation_product_content . $cleaned_violation_product_content . $config_violation_store_content . $cleaned_violation_store_content;
  234. $email_content_html = $this->buildEmailHtml(
  235. $config_collect_product_content,
  236. $actual_collect_product_content,
  237. $config_low_product_content,
  238. $cleaned_low_product_content,
  239. $config_violation_product_content,
  240. $cleaned_violation_product_content,
  241. $config_violation_store_content,
  242. $cleaned_violation_store_content,
  243. $start_time_string,
  244. $end_time_string
  245. );
  246. // echo $email_content_html;
  247. //发送预警通知
  248. foreach ($email_notice_list as $email_notice) {
  249. $message_data = [
  250. 'email' => $email_notice['email'],
  251. 'email_content' => json_encode(['title' => $email_title, 'content' => $email_content_html]),
  252. ];
  253. SendEmailJobs::dispatch(['notice_data_info' => $message_data]);
  254. }
  255. //记录日志
  256. } catch (\Exception $e) {
  257. Log::info('job_error', '低价商品数据数据-监控采集清洗情况队列失败', ['data' => $this->message_data, 'error' => $e->getMessage()]);
  258. }
  259. }
  260. /**
  261. * platform 平台ID定义
  262. *
  263. */
  264. private function platform($platform_id)
  265. {
  266. //平台0=全部,1=淘宝,2=京东,3=拼多多,4=美团,5=药师帮,6=1药城,7=药久久,8=药易购,9=药帮忙,10=熊猫药药11=药房网
  267. switch ($platform_id) {
  268. case 1:
  269. return '淘宝';
  270. case 2:
  271. return '京东';
  272. case 3:
  273. return '拼多多';
  274. case 4:
  275. return '美团';
  276. case 5:
  277. return '药师帮';
  278. case 6:
  279. return '1药城';
  280. case 7:
  281. return '药久久';
  282. case 8:
  283. return '药易购';
  284. case 9:
  285. return '药帮忙';
  286. case 10:
  287. return '熊猫药药';
  288. case 11:
  289. return '药房网';
  290. default:
  291. return '';
  292. }
  293. }
  294. /**
  295. * 构建美观的邮件HTML内容(内联样式版本)
  296. */
  297. private function buildEmailHtml(
  298. $config_collect,
  299. $actual_collect,
  300. $config_low,
  301. $cleaned_low,
  302. $config_violation_product,
  303. $cleaned_violation_product,
  304. $config_violation_store,
  305. $cleaned_violation_store,
  306. $start_time_string,
  307. $end_time_string
  308. ) {
  309. $html = '
  310. <!DOCTYPE html>
  311. <html>
  312. <head>
  313. <meta charset="UTF-8">
  314. </head>
  315. <body style="font-family: \'Microsoft YaHei\', Arial, sans-serif; background-color: #f5f7fa; margin: 0; padding: 20px;">
  316. <div style="mix-width: 800px; width: 96%; margin: 0 auto; background-color: #ffffff; border-radius: 12px; overflow: hidden;">
  317. <div style="background: #667eea; color: white; padding: 30px 20px; text-align: center;">
  318. <h1 style="margin: 0; font-size: 24px; font-weight: 600;">数据监控报告</h1>
  319. <p style="margin: 10px 0 0; opacity: 0.9; font-size: 14px;">' . $start_time_string . ' - ' . $end_time_string . ' 采集与清洗情况通知</p>
  320. </div>
  321. <div style="padding: 30px 25px;">
  322. ' . $this->buildSectionInline('🎯 配置采集商品任务', $config_collect, '#667eea') . '
  323. ' . $this->buildSectionInline('✅ 实际采集到的商品', $actual_collect, '#48bb78') . '
  324. <div style="border-top: 1px dashed #e0e4e8; margin: 20px 0;"></div>
  325. ' . $this->buildSectionInline('⚙️ 配置的低价清洗商品', $config_low, '#ed8936') . '
  326. ' . $this->buildSectionInline('🔍 实际清洗出来的低价商品', $cleaned_low, '#ed8936') . '
  327. <div style="border-top: 1px dashed #e0e4e8; margin: 20px 0;"></div>
  328. ' . $this->buildSectionInline('⚠️ 配置的违规清洗商品', $config_violation_product, '#e53e3e') . '
  329. ' . $this->buildSectionInline('🚫 实际清洗出来的违规商品', $cleaned_violation_product, '#e53e3e') . '
  330. <div style="border-top: 1px dashed #e0e4e8; margin: 20px 0;"></div>
  331. ' . $this->buildSectionInline('🏪 配置的违规店铺', $config_violation_store, '#9b59b6') . '
  332. ' . $this->buildSectionInline('🚷 实际清洗出来的违规店铺', $cleaned_violation_store, '#9b59b6') . '
  333. </div>
  334. <div style="background-color: #f8f9fc; padding: 15px 25px; text-align: center; font-size: 12px; color: #999; border-top: 1px solid #eef2f6;">
  335. <p style="margin: 0;">此邮件由系统自动发送,请勿直接回复 | 数据统计时间:' . date('Y-m-d H:i:s') . '</p>
  336. </div>
  337. </div>
  338. </body>
  339. </html>';
  340. return $html;
  341. }
  342. /**
  343. * 构建单个数据区块(内联样式版本)
  344. */
  345. private function buildSectionInline($title, $content, $color)
  346. {
  347. $formattedContent = $this->formatContentInline($content);
  348. return '
  349. <div style="margin-bottom: 25px; border-left: 4px solid ' . $color . '; padding: 12px 20px; background-color: #f8f9fc; border-radius: 8px;">
  350. <div style="font-size: 16px; font-weight: 600; margin-bottom: 12px;">
  351. ' . htmlspecialchars($title) . '
  352. </div>
  353. <div style="font-size: 14px; color: #555; line-height: 1.8; word-break: break-all;">
  354. ' . $formattedContent . '
  355. </div>
  356. </div>';
  357. }
  358. /**
  359. * 格式化内容(内联样式版本)
  360. */
  361. private function formatContentInline($content)
  362. {
  363. // 如果内容为空或者只包含分号
  364. if (empty($content) || trim($content) === ';' || trim($content) === '无;') {
  365. return '<span style="color: #999; font-style: italic;">📭 暂无数据</span>';
  366. }
  367. // 检查是否包含"无;"的情况
  368. if (strpos($content, '无;') !== false && strlen($content) <= 6) {
  369. return '<span style="color: #999; font-style: italic;">📭 暂无数据</span>';
  370. }
  371. // 移除末尾的分号
  372. $content = rtrim($content, ';');
  373. // 按分号分割
  374. $items = explode(';', $content);
  375. $html = '';
  376. foreach ($items as $item) {
  377. $item = trim($item);
  378. if (empty($item)) continue;
  379. // 跳过"无"项
  380. if ($item === '无') continue;
  381. // 普通商品名
  382. $html .= '<span style="display: inline-block; background: #e8ecf1; padding: 4px 12px; border-radius: 20px; margin: 4px 6px 4px 0; font-size: 13px;">' . htmlspecialchars($item) . '</span>';
  383. }
  384. return $html ?: '<span style="color: #999; font-style: italic;">📭 暂无数据</span>';
  385. }
  386. public function failed(\Throwable $exception)
  387. {
  388. Log::info('job_error', '低价商品数据数据-监控采集清洗情况队列完全失败', ['data' => $this->message_data, 'error' => $exception->getMessage()]);
  389. }
  390. }