data_send.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import calendar
  2. from datetime import datetime
  3. import utils
  4. from models.process_data import ProcessData
  5. from models.process_result_data import ProcessResultData, InstrumentData
  6. from stores.data_store_interface import IDataStore
  7. class DataSend:
  8. _error_arr = []
  9. _email_area_arr = []
  10. _email_area_virtual_arr = []
  11. @property
  12. def store(self) -> IDataStore:
  13. return self._store
  14. def __init__(self, store: IDataStore):
  15. self._store = store
  16. self._email_area_arr = self.store.query_all_emails()
  17. self._email_area_virtual_arr = self.store.query_all_virtual_emails()
  18. def send(self) -> None:
  19. self._error_arr = []
  20. list = self.store.query_to_send()
  21. utils.get_logger().info(f"开始发送邮件,数量为 {len(list)}")
  22. for item in list:
  23. self._send_item(item)
  24. if len(self._error_arr) > 0:
  25. self._send_email_no_found()
  26. def send_report_current_month(self):
  27. # 查询当月的数据
  28. start_date, end_date = self._get_first_and_last_day_of_current_month()
  29. self._send_reports(start_date, end_date)
  30. def send_report_prev_month(self):
  31. # 查询上月的数据
  32. start_date, end_date = self._get_first_and_last_day_of_prev_month()
  33. self._send_reports(start_date, end_date)
  34. def _send_reports(self, start_date, end_date):
  35. utils.get_logger().info(
  36. f"开始发送中标报告邮件,开始日期:{start_date.strftime("%Y-%m-%d")},结束日期:{end_date.strftime("%Y-%m-%d")}"
  37. )
  38. email = self.store.query_master_email()
  39. if not email:
  40. utils.get_logger().error("没有找到master email")
  41. return
  42. items = self.store.query_to_report_by_date(start_date, end_date)
  43. title_prev = utils.get_config_value("email.report_title_prev", "【中标报告】")
  44. title = f"{start_date.month}月中标结果报告"
  45. body = self._build_report_email_html(title, items)
  46. attach_path = self._gen_report_exlecl(title, items)
  47. flag = utils.send_email(email, f"{title_prev} {title}", body, True, attach_path)
  48. if flag:
  49. utils.get_logger().info("发送中标报告邮件成功")
  50. def _send_item(self, item: ProcessData) -> None:
  51. utils.get_logger().info(f"开始发送邮件,地区为:{item.city} ,URL为 {item.url}")
  52. email = self._get_email_by_area(item.city)
  53. if not email:
  54. utils.get_logger().error(f"{item.city} 下没有找到email")
  55. if item.city not in self._error_arr:
  56. self._error_arr.append(item.city)
  57. return
  58. title_prev = utils.get_config_value("email.title_prev", "【招标信息】")
  59. body = self._build_email_html(item)
  60. flag = utils.send_email(
  61. email, f"{title_prev} {item.title}", body, True, item.attach_path
  62. )
  63. if flag:
  64. self.store.set_send(item.no)
  65. def _get_email_by_area(
  66. self, area: str, count: int = 0, virtual_area: str = None
  67. ) -> str:
  68. email = None
  69. area_str = (
  70. area.replace("省", "").replace("市", "").replace("区", "").replace("县", "")
  71. )
  72. for area_item in self._email_area_arr:
  73. if area_str in area_item.area:
  74. email = area_item.email
  75. if virtual_area:
  76. new_area = f"{area_item.area},{virtual_area}"
  77. self.store.update_area_email_area_by_name(area_item.name, new_area)
  78. self._email_area_arr = self.store.query_all_emails()
  79. break
  80. if not email and count < 3:
  81. area_name = self._get_email_by_area_virtual(area_str)
  82. if area_name:
  83. virtual_area = (
  84. f"{area_str},{virtual_area}" if virtual_area else area_str
  85. )
  86. email = self._get_email_by_area(area_name, count + 1, virtual_area)
  87. return email
  88. def _get_email_by_area_virtual(self, area: str) -> str:
  89. name = None
  90. for area_item in self._email_area_virtual_arr:
  91. if area in area_item.area:
  92. name = area_item.name
  93. break
  94. return name
  95. @staticmethod
  96. def _build_email_html(item: ProcessData, other: str = "") -> str:
  97. html_body = f"""
  98. <html>
  99. <head>
  100. <style>
  101. body {{
  102. background-color: #f4f4f9;
  103. font-family: Arial, sans-serif;
  104. margin: 0;
  105. padding: 20px;
  106. }}
  107. h1 {{
  108. text-align: center;
  109. color: #333;
  110. }}
  111. .container {{
  112. max-width: 600px;
  113. margin: 0 auto;
  114. background-color: #fff;
  115. padding: 20px;
  116. border-radius: 8px;
  117. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  118. }}
  119. .button-container {{
  120. text-align: center;
  121. margin-top: 20px;
  122. }}
  123. .button {{
  124. display: inline-block;
  125. padding: 10px 20px;
  126. font-size: 16px;
  127. color: #fff!important;
  128. background-color: #007bff;
  129. text-decoration: none;
  130. border-radius: 5px;
  131. transition: background-color 0.3s;
  132. }}
  133. .button:hover {{
  134. background-color: #0056b3;
  135. }}
  136. .system {{
  137. color: #aaa;
  138. }}
  139. </style>
  140. </head>
  141. <body>
  142. <div class="container">
  143. <h1>{item.title}</h1>
  144. <p><strong>招标编号:</strong> {item.no if item.no else ""}</p>
  145. <p><strong>项目区域:</strong> {item.provice if item.provice else ""}{item.city if item.city else ""}</p>
  146. <p><strong>相关设备:</strong> {item.devices if item.devices else ""}</p>
  147. <p><strong>开标时间:</strong> {item.date if item.date else ""}</p>
  148. <p><strong>开标地点:</strong> {item.address if item.address else ""}</p>
  149. <p><strong>发布日期:</strong> {item.release_date if item.release_date else ""}</p>
  150. <p><strong>标书摘要:</strong> {item.summary if item.summary else ""}</p>
  151. <div class="button-container">
  152. <a href="{item.url}" class="button">查看详情</a>
  153. </div>
  154. <div>
  155. <h3>{other}</h3>
  156. </div>
  157. <p class="system">本邮件由系统自动发送,请勿回复。</p>
  158. </div>
  159. </body>
  160. </html>
  161. """
  162. return html_body
  163. def _build_report_email_html(self, title, items: list[ProcessResultData]) -> str:
  164. body = self._build_report_email_body(items)
  165. html = f"""
  166. <html>
  167. <head>
  168. <style>
  169. body {{
  170. background-color: #f4f4f9;
  171. font-family: Arial, sans-serif;
  172. margin: 0;
  173. padding: 20px;
  174. }}
  175. h1 {{
  176. text-align: center;
  177. color: #333;
  178. }}
  179. .container {{
  180. max-width: 1200px;
  181. margin: 0 auto;
  182. background-color: #fff;
  183. padding: 20px;
  184. border-radius: 8px;
  185. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  186. }}
  187. .system {{
  188. color: #aaa;
  189. font-size: 80%;
  190. }}
  191. .table-container {{
  192. overflow-x: auto;
  193. width: 100%;
  194. }}
  195. .table {{
  196. width: 1200px;
  197. background-color: #ffffff;
  198. border: 1px solid #dddddd;
  199. border-radius: 8px;
  200. margin-bottom: 20px;
  201. padding: 20px;
  202. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  203. border-collapse: collapse;
  204. }}
  205. .table th, .table td {{
  206. padding: 5px;
  207. border-bottom: 1px solid #dddddd;
  208. word-wrap: break-word;
  209. text-align: center;
  210. font-size:12px;
  211. }}
  212. .table th:not(:first-child), .table td:not(:first-child) ,.table tr.instrument-row th,.table tr.instrument-row td{{
  213. border-left: 1px solid #dddddd;
  214. }}
  215. .table th {{
  216. padding: 10px;
  217. background-color: #f8f9fa;
  218. font-weight: bold;
  219. font-size:14px;
  220. }}
  221. .table tr:last-child td {{
  222. border-bottom: none;
  223. }}
  224. .table td a{{
  225. color: #007bff;
  226. }}
  227. </style>
  228. </head>
  229. <body>
  230. <div class="container">
  231. <h1>{title}</h1>
  232. {body}
  233. <p class="system">本邮件由系统自动发送,请勿回复。</p>
  234. </div>
  235. </body>
  236. </html>
  237. """
  238. return html
  239. def _build_report_email_body(self, items: list[ProcessResultData]) -> str:
  240. if not items:
  241. return ""
  242. body = """
  243. <div class="table-container">
  244. <table class="table">
  245. <tr>
  246. <th style="width:200px" rowspan="2">项目名称</th>
  247. <th style="width:100px" rowspan="2">地区</th>
  248. <th colspan="6" style="width:800px">中标设备</th>
  249. <th style="width:100px" rowspan="2">公告日期</th>
  250. </tr>
  251. <tr class='instrument-row'>
  252. <th style="180px">仪器名称</th>
  253. <th style="200px">仪器厂商</th>
  254. <th style="130px">仪器型号</th>
  255. <th style="80px">数量</th>
  256. <th style="140px">单价(元)</th>
  257. <th>中标单位</th>
  258. </tr>
  259. """
  260. for item in items:
  261. body += self._gen_report_body_item(item)
  262. body += "</table></div>"
  263. return body
  264. def _gen_report_body_item(self, item: ProcessResultData):
  265. if not item:
  266. return ""
  267. row_count = len(item.instruments) if item.instruments else 1
  268. html = f"""
  269. <tr>
  270. <td rowspan="{row_count}"><a title="点击查看详情" href="{item.url}">{item.title}</a></td>
  271. <td rowspan="{row_count}">{item.provice if item.provice else ''}{item.city if item.city else ''}</td>
  272. {self._gen_report_body_item_instrument(item.instruments[0] if item.instruments else None)}
  273. <td rowspan="{row_count}">{item.date if item.date else ''}</td>
  274. </tr>
  275. """
  276. if row_count > 1:
  277. for instrument in item.instruments[1:]:
  278. html += f"""
  279. <tr class="instrument-row">
  280. {self._gen_report_body_item_instrument(instrument)}
  281. </tr>
  282. """
  283. return html
  284. @staticmethod
  285. def _gen_report_body_item_instrument(instrument):
  286. if not instrument:
  287. return '<td colspan="6">无设备信息</td>'
  288. return f"""
  289. <td>{instrument.name if instrument.name else ''}</td>
  290. <td>{instrument.manufacturer if instrument.manufacturer else ''}</td>
  291. <td>{instrument.model if instrument.model else ''}</td>
  292. <td>{instrument.quantity if instrument.quantity else ''}</td>
  293. <td>{instrument.unit_price if instrument.unit_price else ''}</td>
  294. <td>{instrument.company if instrument.company else ''}</td>
  295. """
  296. def _gen_report_exlecl(self, title, items: list[ProcessResultData]) -> str:
  297. all_data = []
  298. for item in items:
  299. if item.instruments:
  300. # 获取第一台仪器的数据
  301. first_instrument = item.instruments[0]
  302. all_data.append(self._gen_report_row_data(item, first_instrument))
  303. # 处理剩余的仪器
  304. for instrument in item.instruments[1:]:
  305. all_data.append(self._gen_report_row_data(None, instrument))
  306. else:
  307. # 如果没有仪器,只添加 ProcessResultData 的字段
  308. all_data.append(self._gen_report_row_data(item, None))
  309. return utils.save_reort_excel(all_data, title)
  310. @staticmethod
  311. def _gen_report_row_data(
  312. data: ProcessResultData | None, instrument: InstrumentData | None
  313. ):
  314. return {
  315. "项目编号": data.no if data and data.no else "",
  316. "项目名称": data.title if data and data.title else "",
  317. "公告日期": data.date if data and data.date else "",
  318. "招标省份": data.provice if data and data.provice else "",
  319. "招标城市": data.city if data and data.city else "",
  320. "中标单位名称": instrument.company if instrument.company else "",
  321. "仪器名称": instrument.name if instrument and instrument.name else "",
  322. "仪器厂商": (
  323. instrument.manufacturer
  324. if instrument and instrument.manufacturer
  325. else ""
  326. ),
  327. "仪器型号": instrument.model if instrument and instrument.model else "",
  328. "数量": instrument.quantity if instrument and instrument.quantity else "",
  329. "单价": (
  330. instrument.unit_price if instrument and instrument.unit_price else ""
  331. ),
  332. "公告摘要": data.summary if data and data.summary else "",
  333. "URL": data.url if data and data.url else "",
  334. }
  335. def _send_email_no_found(self) -> None:
  336. email = utils.get_config_value("email.error_email")
  337. utils.get_logger().info(f"开始发送区域邮箱未匹配邮件: {email}")
  338. if not email:
  339. return
  340. title = "Warning: 相关地区没有匹配到邮箱,请及时添加相关配置"
  341. content = "以下区域中没有配置邮箱:\n\n "
  342. content += "、".join(self._error_arr)
  343. content += "\n\n请及时添加相关配置。"
  344. utils.send_email(email, title, content, False, None)
  345. @staticmethod
  346. def _get_first_and_last_day_of_current_month():
  347. # 获取当前日期
  348. today = datetime.today()
  349. # 获取这个月的第一天
  350. first_day_of_current_month = datetime(today.year, today.month, 1, 0, 0, 0)
  351. # 获取这个月的最后一天
  352. _, last_day = calendar.monthrange(today.year, today.month)
  353. last_day_of_current_month = datetime(
  354. today.year, today.month, last_day, 23, 59, 59
  355. )
  356. return first_day_of_current_month, last_day_of_current_month
  357. @staticmethod
  358. def _get_first_and_last_day_of_prev_month():
  359. # 获取当前日期
  360. today = datetime.today()
  361. # 获取上个月的年份和月份
  362. if today.month == 1:
  363. prev_month_year = today.year - 1
  364. prev_month = 12
  365. else:
  366. prev_month_year = today.year
  367. prev_month = today.month - 1
  368. # 获取上个月的第一天
  369. first_day_prev_month = datetime(prev_month_year, prev_month, 1, 0, 0, 0)
  370. # 获取上个月的最后一天
  371. _, last_day = calendar.monthrange(prev_month_year, prev_month)
  372. last_day_of_prev_month = datetime(
  373. prev_month_year, prev_month, last_day, 23, 59, 59
  374. )
  375. return first_day_prev_month, last_day_of_prev_month