views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. from django.http import JsonResponse
  2. from django.views.decorators.csrf import csrf_exempt
  3. from django.views.decorators.http import require_POST
  4. from .backup_utils import perform_base_backup, restore_to_base_backup
  5. import os
  6. import json
  7. import logging
  8. from datetime import datetime
  9. from apscheduler.schedulers.background import BackgroundScheduler
  10. from django.conf import settings
  11. import math
  12. from operation_log.views import log_success_operation, log_failure_operation
  13. from operation_log.models import OperationLog
  14. from django.utils import timezone
  15. import threading
  16. import sys
  17. import signal
  18. import platform
  19. import subprocess
  20. logger = logging.getLogger(__name__)
  21. # 初始化调度器
  22. scheduler = BackgroundScheduler()
  23. def scheduled_backup():
  24. """定时备份任务"""
  25. try:
  26. backup_path = perform_base_backup()
  27. logger.info(f"定时备份完成: {backup_path}")
  28. # 记录操作日志(定时任务没有request对象,直接创建日志)
  29. try:
  30. OperationLog.objects.create(
  31. operator="系统定时任务",
  32. operation_content=f"定时备份数据库完成,备份路径: {backup_path}",
  33. operation_level="other",
  34. operation_result="success",
  35. ip_address=None,
  36. user_agent="系统定时任务",
  37. request_method="CRON",
  38. request_path="/backup/scheduled",
  39. module_name="系统备份"
  40. )
  41. except Exception as log_error:
  42. logger.error(f"定时备份日志记录失败: {str(log_error)}")
  43. # 更新托盘分类任务(如果存在)
  44. try:
  45. from container.utils import update_container_categories_task,reconcile_material_history
  46. update_container_categories_task()
  47. reconcile_material_history()
  48. logger.info(f"定时更新托盘分类完成")
  49. except ImportError:
  50. logger.warning("更新托盘分类模块未找到,跳过更新")
  51. except Exception as e:
  52. logger.error(f"定时备份失败: {str(e)}")
  53. # 记录失败日志
  54. try:
  55. OperationLog.objects.create(
  56. operator="系统定时任务",
  57. operation_content=f"定时备份数据库失败: {str(e)}",
  58. operation_level="other",
  59. operation_result="failure",
  60. ip_address=None,
  61. user_agent="系统定时任务",
  62. request_method="CRON",
  63. request_path="/backup/scheduled",
  64. module_name="系统备份"
  65. )
  66. except Exception as log_error:
  67. logger.error(f"定时备份失败日志记录失败: {str(log_error)}")
  68. def scheduled_consistency_fix():
  69. """定时修复分组状态和托盘数一致性任务"""
  70. try:
  71. from location_statistics.views import LocationConsistencyChecker
  72. logger.info("开始执行定时一致性修复任务")
  73. fixed_groups = 0
  74. error_groups = 0
  75. fixed_details = 0
  76. error_details = 0
  77. group_fix_success = False
  78. detail_fix_success = False
  79. # 执行分组状态修复
  80. try:
  81. group_checker = LocationConsistencyChecker(
  82. warehouse_code=None, # None表示检查所有仓库
  83. layer=None, # None表示检查所有楼层
  84. auto_fix=True, # 启用自动修复
  85. fix_scope=['groups'] # 只修复分组状态
  86. )
  87. group_checker.check_all()
  88. group_report = group_checker.generate_report()
  89. fixed_groups = group_report['summary']['fixed']['groups']
  90. error_groups = group_report['summary']['errors_found']['groups']
  91. group_fix_success = True
  92. logger.info(f"分组状态修复完成: 发现{error_groups}个问题,修复{fixed_groups}个")
  93. except Exception as group_error:
  94. logger.error(f"分组状态修复失败: {str(group_error)}", exc_info=True)
  95. # 执行托盘明细状态修复
  96. try:
  97. detail_checker = LocationConsistencyChecker(
  98. warehouse_code=None, # None表示检查所有仓库
  99. layer=None, # None表示检查所有楼层
  100. auto_fix=True, # 启用自动修复
  101. fix_scope=['details'] # 只修复托盘明细状态
  102. )
  103. detail_checker.check_all()
  104. detail_report = detail_checker.generate_report()
  105. fixed_details = detail_report['summary']['fixed']['details']
  106. error_details = detail_report['summary']['errors_found']['details']
  107. detail_fix_success = True
  108. logger.info(f"托盘明细状态修复完成: 发现{error_details}个问题,修复{fixed_details}个")
  109. except Exception as detail_error:
  110. logger.error(f"托盘明细状态修复失败: {str(detail_error)}", exc_info=True)
  111. # 记录操作日志
  112. if group_fix_success and detail_fix_success:
  113. operation_result = "success"
  114. operation_content = f"定时一致性修复完成: 分组状态-发现{error_groups}个问题,修复{fixed_groups}个;托盘明细-发现{error_details}个问题,修复{fixed_details}个"
  115. elif group_fix_success or detail_fix_success:
  116. operation_result = "partial"
  117. operation_content = f"定时一致性修复部分完成: "
  118. if group_fix_success:
  119. operation_content += f"分组状态-发现{error_groups}个问题,修复{fixed_groups}个;"
  120. else:
  121. operation_content += "分组状态修复失败;"
  122. if detail_fix_success:
  123. operation_content += f"托盘明细-发现{error_details}个问题,修复{fixed_details}个"
  124. else:
  125. operation_content += "托盘明细修复失败"
  126. else:
  127. operation_result = "failure"
  128. operation_content = "定时一致性修复失败: 分组状态和托盘明细修复均失败"
  129. try:
  130. OperationLog.objects.create(
  131. operator="系统定时任务",
  132. operation_content=operation_content,
  133. operation_level="other",
  134. operation_result=operation_result,
  135. ip_address=None,
  136. user_agent="系统定时任务",
  137. request_method="CRON",
  138. request_path="/backup/consistency_fix",
  139. module_name="数据一致性修复"
  140. )
  141. except Exception as log_error:
  142. logger.error(f"定时一致性修复日志记录失败: {str(log_error)}")
  143. except ImportError as e:
  144. logger.warning(f"一致性修复模块未找到,跳过修复: {str(e)}")
  145. try:
  146. OperationLog.objects.create(
  147. operator="系统定时任务",
  148. operation_content=f"定时一致性修复失败: 模块未找到 - {str(e)}",
  149. operation_level="other",
  150. operation_result="failure",
  151. ip_address=None,
  152. user_agent="系统定时任务",
  153. request_method="CRON",
  154. request_path="/backup/consistency_fix",
  155. module_name="数据一致性修复"
  156. )
  157. except Exception as log_error:
  158. logger.error(f"定时一致性修复失败日志记录失败: {str(log_error)}")
  159. except Exception as e:
  160. logger.error(f"定时一致性修复失败: {str(e)}", exc_info=True)
  161. # 记录失败日志
  162. try:
  163. OperationLog.objects.create(
  164. operator="系统定时任务",
  165. operation_content=f"定时一致性修复失败: {str(e)}",
  166. operation_level="other",
  167. operation_result="failure",
  168. ip_address=None,
  169. user_agent="系统定时任务",
  170. request_method="CRON",
  171. request_path="/backup/consistency_fix",
  172. module_name="数据一致性修复"
  173. )
  174. except Exception as log_error:
  175. logger.error(f"定时一致性修复失败日志记录失败: {str(log_error)}")
  176. # 启动定时备份(每6小时执行一次)
  177. if not scheduler.running:
  178. scheduler.add_job(
  179. scheduled_backup,
  180. 'cron',
  181. hour='*/6', # 每6小时执行一次
  182. minute=0, # 在0分钟时执行
  183. id='db_backup_job'
  184. )
  185. # 启动定时一致性修复任务(每6小时执行一次)
  186. scheduler.add_job(
  187. scheduled_consistency_fix,
  188. 'cron',
  189. hour='*/6', # 每2小时执行一次
  190. minute=10, # 在10分钟时执行(避免与备份任务冲突)
  191. id='consistency_fix_job'
  192. )
  193. scheduler.start()
  194. logger.info("定时备份任务已启动")
  195. logger.info("定时一致性修复任务已启动")
  196. def get_backup_files(page=1, page_size=5):
  197. """获取备份文件列表(带分页)"""
  198. backup_dir = "E:/code/backup/postgres"
  199. all_backups = []
  200. # 遍历备份目录
  201. for root, dirs, files in os.walk(backup_dir):
  202. for file in files:
  203. if file.endswith(".backup"):
  204. file_path = os.path.join(root, file)
  205. file_size = os.path.getsize(file_path)
  206. timestamp = os.path.getmtime(file_path)
  207. all_backups.append({
  208. "name": file,
  209. "path": file_path,
  210. "size": f"{file_size / (1024 * 1024):.2f} MB",
  211. "date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
  212. })
  213. # 按时间倒序排序
  214. all_backups.sort(key=lambda x: x["date"], reverse=True)
  215. # 分页处理
  216. total_items = len(all_backups)
  217. total_pages = math.ceil(total_items / page_size)
  218. start_index = (page - 1) * page_size
  219. end_index = min(start_index + page_size, total_items)
  220. return {
  221. "backups": all_backups[start_index:end_index],
  222. "page": page,
  223. "page_size": page_size,
  224. "total_items": total_items,
  225. "total_pages": total_pages
  226. }
  227. def get_base_backups(page=1, page_size=5):
  228. """获取基础备份列表(带分页)"""
  229. base_backup_dir = "E:/code/backup/postgres/base_backup"
  230. all_backups = []
  231. # 遍历基础备份目录
  232. for dir_name in os.listdir(base_backup_dir):
  233. dir_path = os.path.join(base_backup_dir, dir_name)
  234. if os.path.isdir(dir_path):
  235. timestamp = os.path.getmtime(dir_path)
  236. all_backups.append({
  237. "name": dir_name,
  238. "path": dir_path,
  239. "date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
  240. })
  241. # 按时间倒序排序
  242. all_backups.sort(key=lambda x: x["date"], reverse=True)
  243. # 分页处理
  244. total_items = len(all_backups)
  245. total_pages = math.ceil(total_items / page_size)
  246. start_index = (page - 1) * page_size
  247. end_index = min(start_index + page_size, total_items)
  248. return {
  249. "backups": all_backups[start_index:end_index],
  250. "page": page,
  251. "page_size": page_size,
  252. "total_items": total_items,
  253. "total_pages": total_pages
  254. }
  255. @csrf_exempt
  256. @require_POST
  257. def trigger_backup(request):
  258. """手动触发备份的API接口"""
  259. try:
  260. backup_path = perform_base_backup()
  261. # 记录成功日志
  262. try:
  263. log_success_operation(
  264. request=request,
  265. operation_content=f"手动触发数据库备份,备份路径: {backup_path}",
  266. operation_level="other",
  267. module_name="系统备份"
  268. )
  269. except Exception as log_error:
  270. logger.error(f"备份成功日志记录失败: {str(log_error)}")
  271. return JsonResponse({
  272. 'status': 'success',
  273. 'message': '数据库备份完成',
  274. 'path': backup_path
  275. })
  276. except Exception as e:
  277. # 记录失败日志
  278. try:
  279. log_failure_operation(
  280. request=request,
  281. operation_content=f"手动触发数据库备份失败: {str(e)}",
  282. operation_level="other",
  283. module_name="系统备份"
  284. )
  285. except Exception as log_error:
  286. logger.error(f"备份失败日志记录失败: {str(log_error)}")
  287. return JsonResponse({
  288. 'status': 'error',
  289. 'message': str(e)
  290. }, status=500)
  291. @csrf_exempt
  292. @require_POST
  293. def list_backups(request):
  294. """获取备份文件列表API(带分页)"""
  295. try:
  296. data = json.loads(request.body)
  297. backup_type = data.get('type', 'file')
  298. page = data.get('page', 1)
  299. page_size = data.get('page_size', 5)
  300. if backup_type == 'file':
  301. result = get_backup_files(page, page_size)
  302. elif backup_type == 'base':
  303. result = get_base_backups(page, page_size)
  304. else:
  305. return JsonResponse({
  306. 'status': 'error',
  307. 'message': '无效的备份类型'
  308. }, status=400)
  309. return JsonResponse({
  310. 'status': 'success',
  311. 'data': result
  312. })
  313. except Exception as e:
  314. logger.error(f"获取备份列表失败: {str(e)}")
  315. return JsonResponse({
  316. 'status': 'error',
  317. 'message': str(e)
  318. }, status=500)
  319. @csrf_exempt
  320. @require_POST
  321. def restore_to_point(request):
  322. """执行时间点恢复API"""
  323. try:
  324. data = json.loads(request.body)
  325. base_backup = data.get('base_backup')
  326. if not base_backup or not os.path.exists(base_backup):
  327. # 记录失败日志(无效路径)
  328. try:
  329. log_failure_operation(
  330. request=request,
  331. operation_content=f"执行数据库恢复失败: 无效的基础备份路径 - {base_backup}",
  332. operation_level="other",
  333. module_name="系统备份"
  334. )
  335. except Exception as log_error:
  336. logger.error(f"恢复失败日志记录失败: {str(log_error)}")
  337. return JsonResponse({
  338. 'status': 'error',
  339. 'message': '无效的基础备份路径'
  340. }, status=400)
  341. # 暂停定时备份任务
  342. scheduler.pause_job('db_backup_job')
  343. logger.info("定时备份任务已暂停")
  344. # 执行时间点恢复
  345. restore_to_base_backup( base_backup)
  346. # 恢复定时备份任务
  347. scheduler.resume_job('db_backup_job')
  348. logger.info("定时备份任务已恢复")
  349. # 记录成功日志
  350. try:
  351. log_success_operation(
  352. request=request,
  353. operation_content=f"执行数据库时间点恢复,恢复路径: {base_backup}",
  354. operation_level="other",
  355. module_name="系统备份"
  356. )
  357. except Exception as log_error:
  358. logger.error(f"恢复成功日志记录失败: {str(log_error)}")
  359. return JsonResponse({
  360. 'status': 'success',
  361. 'message': f'已成功恢复到{base_backup}'
  362. })
  363. except Exception as e:
  364. logger.error(f"时间点恢复失败: {str(e)}")
  365. # 记录失败日志
  366. try:
  367. log_failure_operation(
  368. request=request,
  369. operation_content=f"执行数据库时间点恢复失败: {str(e)}",
  370. operation_level="other",
  371. module_name="系统备份"
  372. )
  373. except Exception as log_error:
  374. logger.error(f"恢复失败日志记录失败: {str(log_error)}")
  375. # 确保恢复定时备份任务
  376. if scheduler.get_job('db_backup_job') and scheduler.get_job('db_backup_job').next_run_time is None:
  377. scheduler.resume_job('db_backup_job')
  378. logger.info("恢复失败后定时备份任务已恢复")
  379. return JsonResponse({
  380. 'status': 'error',
  381. 'message': str(e)
  382. }, status=500)
  383. def _shutdown_system(delay=5):
  384. """延迟关闭系统"""
  385. def shutdown():
  386. try:
  387. if scheduler.running:
  388. scheduler.shutdown(wait=False)
  389. logger.info("定时任务调度器已停止")
  390. except Exception as e:
  391. logger.error(f"停止调度器失败: {str(e)}")
  392. os._exit(0)
  393. # 使用线程延迟执行关闭
  394. timer = threading.Timer(delay, shutdown)
  395. timer.daemon = True
  396. timer.start()
  397. @csrf_exempt
  398. @require_POST
  399. def shutdown_system(request):
  400. """远程关闭系统接口"""
  401. try:
  402. data = json.loads(request.body) if request.body else {}
  403. delay = int(data.get('delay', 5)) # 默认5秒延迟
  404. # 验证延迟时间范围(1-60秒)
  405. if delay < 1 or delay > 60:
  406. delay = 5
  407. _shutdown_system(delay=delay)
  408. return JsonResponse({
  409. 'status': 'success',
  410. 'message': f' {delay} ',
  411. 'delay': delay
  412. })
  413. except Exception as e:
  414. logger.error(f"处理系统关闭请求失败: {str(e)}", exc_info=True)
  415. return JsonResponse({
  416. 'status': 'error',
  417. 'message': f'{str(e)}'
  418. }, status=500)
  419. @csrf_exempt
  420. @require_POST
  421. def shutdown_computer(request):
  422. try:
  423. data = json.loads(request.body) if request.body else {}
  424. delay = int(data.get('delay', 60))
  425. if delay < 0 or delay > 600:
  426. delay = 60
  427. system = platform.system()
  428. try:
  429. if system == "Windows":
  430. subprocess.run(["shutdown", "/s", "/t", str(delay), "/f"], check=True)
  431. elif system == "Linux":
  432. subprocess.run(["shutdown", "-h", "+" + str(delay // 60) if delay >= 60 else "now"], check=True)
  433. elif system == "Darwin":
  434. subprocess.run(["sudo", "shutdown", "-h", "+" + str(delay // 60) if delay >= 60 else "now"], check=True)
  435. else:
  436. return JsonResponse({
  437. 'status': 'error',
  438. 'message': f'不支持的操作系统: {system}'
  439. }, status=400)
  440. return JsonResponse({
  441. 'status': 'success',
  442. 'message': f' {delay} 秒',
  443. 'delay': delay,
  444. 'system': system
  445. })
  446. except subprocess.CalledProcessError as e:
  447. logger.error(f"执行失败: {str(e)}")
  448. return JsonResponse({
  449. 'status': 'error',
  450. 'message': f'{str(e)}'
  451. }, status=500)
  452. except PermissionError:
  453. logger.error("关闭失败: 权限不足")
  454. return JsonResponse({
  455. 'status': 'error',
  456. 'message': '权限不足'
  457. }, status=403)
  458. except ValueError:
  459. return JsonResponse({
  460. 'status': 'error',
  461. 'message': '无效的延迟时间参数'
  462. }, status=400)
  463. except Exception as e:
  464. logger.error(f"关闭失败: {str(e)}", exc_info=True)
  465. return JsonResponse({
  466. 'status': 'error',
  467. 'message': f'处理关闭请求失败: {str(e)}'
  468. }, status=500)