UIDBurnManageController.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. # -*- encoding: utf-8 -*-
  2. """
  3. @File : UIDBurnManageController.py
  4. @Time : 2025/7/30 08:57
  5. @Author : stephen
  6. @Email : zhangdongming@asj6.wecom.work
  7. @Software: PyCharm
  8. """
  9. import os
  10. from typing import Dict, Any
  11. from django.core import serializers
  12. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  13. from django.db.models import Q
  14. from django.http import QueryDict
  15. from django.views import View
  16. from AgentModel.models import BurnRecord, BurnEncryptedICUID
  17. from Ansjer.config import LOGGER
  18. from Object.ResponseObject import ResponseObject
  19. from Object.TokenObject import TokenObject
  20. class UIDBurnManageView(View):
  21. def get(self, request, *args, **kwargs):
  22. request.encoding = 'utf-8'
  23. operation = kwargs.get('operation')
  24. return self.validation(request.GET, request, operation)
  25. def post(self, request, *args, **kwargs):
  26. request.encoding = 'utf-8'
  27. operation = kwargs.get('operation')
  28. return self.validation(request.POST, request, operation)
  29. def delete(self, request, *args, **kwargs):
  30. request.encoding = 'utf-8'
  31. operation = kwargs.get('operation')
  32. delete = QueryDict(request.body)
  33. if not delete:
  34. delete = request.GET
  35. return self.validation(delete, request, operation)
  36. def put(self, request, *args, **kwargs):
  37. request.encoding = 'utf-8'
  38. operation = kwargs.get('operation')
  39. put = QueryDict(request.body)
  40. return self.validation(put, request, operation)
  41. def validation(self, request_dict, request, operation):
  42. """请求验证路由"""
  43. # 初始化响应对象
  44. language = request_dict.get('language', 'cn')
  45. response = ResponseObject(language, 'pc')
  46. # Token验证
  47. try:
  48. tko = TokenObject(
  49. request.META.get('HTTP_AUTHORIZATION'),
  50. returntpye='pc')
  51. if tko.code != 0:
  52. return response.json(tko.code)
  53. response.lang = tko.lang
  54. user_id = tko.userID
  55. except Exception as e:
  56. LOGGER.error(f"Token验证失败: {str(e)}")
  57. return response.json(444)
  58. if operation == 'getBurnRecordsPage':
  59. return self.get_burn_records_page(request_dict, response)
  60. elif operation == 'importBatchUids':
  61. return self.import_batch_uids(request, response)
  62. elif operation == 'addBurnRecord':
  63. return self.add_burn_record(request, response)
  64. elif operation == 'getBurnUidsPage':
  65. return self.get_burn_uids_page(request_dict, response)
  66. else:
  67. return response.json(414)
  68. @classmethod
  69. def get_burn_records_page(cls, request_dict: Dict[str, Any], response) -> Any:
  70. """
  71. 分页查询烧录记录
  72. :param cls:
  73. :param request_dict: 请求参数字典(包含分页参数和查询条件)
  74. :param response: 响应对象(用于返回JSON)
  75. :return: 分页查询结果的JSON响应
  76. """
  77. # 1. 分页参数处理与验证(严格类型转换+边界控制)
  78. try:
  79. page = int(request_dict.get('page', 1))
  80. page_size = int(request_dict.get('pageSize', 10))
  81. # 限制分页范围(避免页码为0或负数,页大小控制在1-100)
  82. page = max(page, 1)
  83. page_size = max(1, min(page_size, 100))
  84. except (ValueError, TypeError):
  85. return response.json(444, "分页参数错误(必须为整数)")
  86. # 2. 构建查询条件(解决原代码中query可能未定义的问题)
  87. query = Q() # 初始化空条件(无条件查询)
  88. order_number = request_dict.get('orderNumber', '').strip() # 去除首尾空格,避免空字符串查询
  89. if order_number:
  90. query &= Q(order_number__icontains=order_number)
  91. # 3. 获取查询集(延迟执行,不立即查询数据库)
  92. burn_qs = BurnRecord.objects.filter(query).order_by('-created_time')
  93. # 4. 分页处理(完善异常捕获)
  94. paginator = Paginator(burn_qs, page_size)
  95. try:
  96. page_obj = paginator.page(page)
  97. except PageNotAnInteger:
  98. # 若页码不是整数,返回第一页
  99. page_obj = paginator.page(1)
  100. except EmptyPage:
  101. # 若页码超出范围,返回最后一页(或空列表,根据业务需求调整)
  102. page_obj = paginator.page(paginator.num_pages)
  103. burn_list = serializers.serialize(
  104. 'python', # 输出Python字典格式
  105. page_obj,
  106. fields=['id', 'order_number', 'burn_count', 'purpose', 'created_time'] # 指定需要的字段
  107. )
  108. return response.json(
  109. 0,
  110. {
  111. 'list': burn_list,
  112. 'total': paginator.count, # 总记录数
  113. 'currentPage': page_obj.number, # 当前页码
  114. 'totalPages': paginator.num_pages, # 总页数
  115. 'pageSize': page_size # 返回实际使用的页大小(便于前端同步)
  116. }
  117. )
  118. @classmethod
  119. def import_batch_uids(cls, request, response) -> Any:
  120. """
  121. 导入批次UID
  122. :param request: HttpRequest对象(包含上传文件)
  123. :param response: 响应对象
  124. :return: JSON响应
  125. """
  126. from openpyxl import load_workbook
  127. from datetime import datetime
  128. import time
  129. # 1. 验证文件上传
  130. if 'file' not in request.FILES:
  131. return response.json(444, "请上传Excel文件")
  132. excel_file = request.FILES['file']
  133. if not excel_file.name.endswith(('.xlsx', '.xls')):
  134. return response.json(444, "只支持Excel文件(.xlsx/.xls)")
  135. # 2. 生成批次号
  136. batch_number = f"ENG{datetime.now().strftime('%Y%m%d')}"
  137. # 3. 读取Excel并处理UID
  138. try:
  139. wb = load_workbook(excel_file)
  140. ws = wb.active
  141. uids = []
  142. for row in ws.iter_rows(min_row=1, values_only=True):
  143. if row[0]: # 第一列有值
  144. uid = str(row[0]).strip() # 去空格
  145. if uid: # 非空字符串
  146. uids.append(uid)
  147. if not uids:
  148. return response.json(444, "Excel中没有有效的UID数据")
  149. # 4. 批量创建记录(使用事务保证原子性)
  150. from django.db import transaction
  151. current_time = int(time.time())
  152. try:
  153. with transaction.atomic():
  154. records = [
  155. BurnEncryptedICUID(
  156. batch_number=batch_number,
  157. uid=uid,
  158. purpose='批次导入',
  159. created_time=current_time,
  160. updated_time=current_time,
  161. status=0 # 未烧录状态
  162. )
  163. for uid in set(uids) # 去重
  164. ]
  165. BurnEncryptedICUID.objects.bulk_create(records)
  166. except Exception as e:
  167. LOGGER.error(f"导入批次UID失败: {str(e)}")
  168. return response.json(500, "导入失败,请检查数据格式")
  169. return response.json(0, {"batch_number": batch_number, "count": len(records)})
  170. except Exception as e:
  171. LOGGER.error(f"处理Excel文件失败: {str(e)}")
  172. return response.json(500, "处理Excel文件失败")
  173. @classmethod
  174. def add_burn_record(cls, request, response) -> Any:
  175. """
  176. 新增烧录记录(带UID文件) - Redis字符串优化版
  177. :param request: HttpRequest对象(包含上传文件和表单数据)
  178. :param response: 响应对象
  179. :return: JSON响应
  180. """
  181. import threading
  182. import time
  183. import os
  184. import json
  185. from uuid import uuid4
  186. from django.conf import settings
  187. from Object.RedisObject import RedisObject
  188. # 1. 参数验证
  189. request_dict = request.POST
  190. required_fields = ['order_number', 'burn_count', 'purpose']
  191. for field in required_fields:
  192. if not request_dict.get(field):
  193. return response.json(444, f"缺少必填参数: {field}")
  194. # 2. 验证文件上传
  195. if 'uid_file' not in request.FILES:
  196. return response.json(444, "请上传包含已烧录UID的Excel文件")
  197. excel_file = request.FILES['uid_file']
  198. if not excel_file.name.endswith(('.xlsx', '.xls')):
  199. return response.json(444, "只支持Excel文件(.xlsx/.xls)")
  200. try:
  201. burn_count = int(request_dict['burn_count'])
  202. if burn_count <= 0:
  203. return response.json(444, "烧录数量必须大于0")
  204. # 3. 创建任务ID并初始化Redis状态
  205. task_id = str(uuid4())
  206. redis_key = f"burn_task:{task_id}"
  207. redis_obj = RedisObject()
  208. # 保存任务基本信息到Redis (过期时间2小时)
  209. task_data = {
  210. 'status': 'pending',
  211. 'order_number': request_dict['order_number'].strip(),
  212. 'burn_count': burn_count,
  213. 'purpose': request_dict['purpose'].strip(),
  214. 'progress': 0,
  215. 'processed': 0,
  216. 'total': 0,
  217. 'start_time': int(time.time())
  218. }
  219. redis_obj.set_data(redis_key, json.dumps(task_data), 7200)
  220. # 4. 保存文件到项目static/uploadedfiles目录
  221. base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  222. upload_dir = os.path.join(base_dir, 'static', 'uploaded_files')
  223. os.makedirs(upload_dir, exist_ok=True)
  224. file_path = os.path.join(upload_dir, f"{task_id}.xlsx")
  225. with open(file_path, 'wb+') as destination:
  226. for chunk in excel_file.chunks():
  227. destination.write(chunk)
  228. # 5. 启动后台线程处理
  229. thread = threading.Thread(
  230. target=cls._process_burn_record_async,
  231. args=(task_id, file_path, redis_key),
  232. daemon=True
  233. )
  234. thread.start()
  235. return response.json(0, {
  236. "task_id": task_id,
  237. "message": "任务已提交,正在后台处理",
  238. "redis_key": redis_key
  239. })
  240. except ValueError:
  241. return response.json(444, "烧录数量必须是整数")
  242. except Exception as e:
  243. LOGGER.error(f"创建烧录任务失败: {str(e)}")
  244. return response.json(500, "创建烧录任务失败")
  245. @classmethod
  246. def _process_burn_record_async(cls, task_id, file_path, redis_key):
  247. """后台线程处理烧录记录任务"""
  248. from openpyxl import load_workbook
  249. from django.db import transaction
  250. import time
  251. import json
  252. from AgentModel.models import BurnRecord, BurnEncryptedICUID
  253. from Object.RedisObject import RedisObject
  254. redis_obj = RedisObject()
  255. try:
  256. # 获取并更新任务状态为处理中
  257. task_data = json.loads(redis_obj.get_data(redis_key))
  258. task_data['status'] = 'processing'
  259. redis_obj.set_data(redis_key, json.dumps(task_data))
  260. # 1. 读取Excel文件获取总行数
  261. wb = load_workbook(file_path)
  262. ws = wb.active
  263. total_rows = ws.max_row
  264. # 更新总行数和开始时间
  265. task_data['total'] = total_rows
  266. task_data['start_time'] = int(time.time())
  267. redis_obj.set_data(redis_key, json.dumps(task_data))
  268. # 2. 创建烧录记录
  269. with transaction.atomic():
  270. burn_record = BurnRecord(
  271. order_number=task_data['order_number'],
  272. burn_count=task_data['burn_count'],
  273. purpose=task_data['purpose'],
  274. updated_time=int(time.time()),
  275. created_time=int(time.time())
  276. )
  277. burn_record.save()
  278. task_data['burn_record_id'] = burn_record.id
  279. redis_obj.set_data(redis_key, json.dumps(task_data))
  280. # 3. 分批处理UID文件(每批300条)
  281. batch_size = 300
  282. current_time = int(time.time())
  283. processed = 0
  284. uids_batch = []
  285. for row in ws.iter_rows(min_row=1, values_only=True):
  286. if row[0]:
  287. uid = str(row[0]).strip()
  288. if uid:
  289. uids_batch.append(uid)
  290. processed += 1
  291. # 每处理100条更新一次进度
  292. if processed % 100 == 0:
  293. progress = min(99, int((processed / total_rows) * 100))
  294. task_data['progress'] = progress
  295. task_data['processed'] = processed
  296. task_data['last_update'] = int(time.time())
  297. redis_obj.set_data(redis_key, json.dumps(task_data))
  298. # 处理批次
  299. if len(uids_batch) >= batch_size:
  300. cls._update_uids_batch(
  301. uids_batch,
  302. burn_record.id,
  303. current_time,
  304. redis_key
  305. )
  306. uids_batch = []
  307. # 处理最后一批
  308. if uids_batch:
  309. cls._update_uids_batch(
  310. uids_batch,
  311. burn_record.id,
  312. current_time,
  313. redis_key
  314. )
  315. # 更新最终状态
  316. task_data['status'] = 'completed'
  317. task_data['progress'] = 100
  318. task_data['processed'] = processed
  319. task_data['end_time'] = int(time.time())
  320. redis_obj.set_data(redis_key, json.dumps(task_data))
  321. LOGGER.info(f"处理烧录记录完成,任务ID: {task_id}, 处理UID数量: {processed}")
  322. # 清理临时文件
  323. try:
  324. os.remove(file_path)
  325. except Exception as e:
  326. LOGGER.warning(f"删除临时文件失败: {str(e)}")
  327. except Exception as e:
  328. LOGGER.error(f"处理烧录记录失败: {str(e)}")
  329. task_data = {
  330. 'status': 'failed',
  331. 'error': str(e),
  332. 'end_time': int(time.time())
  333. }
  334. redis_obj.set_data(redis_key, json.dumps(task_data))
  335. @classmethod
  336. def _update_uids_batch(cls, uids_batch, burn_id, current_time, redis_key):
  337. """批量更新UID记录"""
  338. from django.db import transaction
  339. import json
  340. from Object.RedisObject import RedisObject
  341. redis_obj = RedisObject()
  342. try:
  343. with transaction.atomic():
  344. updated = BurnEncryptedICUID.objects.filter(
  345. uid__in=uids_batch
  346. ).update(
  347. burn_id=burn_id,
  348. status=1, # 烧录成功
  349. updated_time=current_time
  350. )
  351. # 更新已处理数量到Redis
  352. task_data = json.loads(redis_obj.get_data(redis_key))
  353. task_data['processed'] = task_data.get('processed', 0) + len(uids_batch)
  354. redis_obj.set_data(redis_key, json.dumps(task_data))
  355. except Exception as e:
  356. LOGGER.error(f"批量更新UID失败: {str(e)}")
  357. raise
  358. @classmethod
  359. def get_burn_uids_page(cls, request_dict: Dict[str, Any], response) -> Any:
  360. """
  361. 根据burn_id分页查询烧录UID记录
  362. :param request_dict: 请求参数字典
  363. :param response: 响应对象
  364. :return: JSON响应
  365. """
  366. # 1. 参数验证
  367. burn_id = request_dict.get('burn_id')
  368. if not burn_id:
  369. return response.json(444, "缺少burn_id参数")
  370. try:
  371. burn_id = int(burn_id)
  372. except ValueError:
  373. return response.json(444, "burn_id必须是整数")
  374. # 2. 分页参数处理
  375. try:
  376. page = int(request_dict.get('page', 1))
  377. page_size = int(request_dict.get('pageSize', 10))
  378. page = max(page, 1)
  379. page_size = max(1, min(page_size, 100))
  380. except (ValueError, TypeError):
  381. return response.json(444, "分页参数错误(必须为整数)")
  382. # 3. 查询并分页
  383. query = Q(burn_id=burn_id)
  384. uid_qs = BurnEncryptedICUID.objects.filter(query).order_by('-created_time')
  385. paginator = Paginator(uid_qs, page_size)
  386. try:
  387. page_obj = paginator.page(page)
  388. except PageNotAnInteger:
  389. page_obj = paginator.page(1)
  390. except EmptyPage:
  391. page_obj = paginator.page(paginator.num_pages)
  392. uid_list = serializers.serialize(
  393. 'python',
  394. page_obj,
  395. fields=['id', 'uid', 'batch_number', 'status', 'created_time', 'updated_time']
  396. )
  397. return response.json(
  398. 0,
  399. {
  400. 'list': uid_list,
  401. 'total': paginator.count,
  402. 'currentPage': page_obj.number,
  403. 'totalPages': paginator.num_pages,
  404. 'pageSize': page_size
  405. }
  406. )