views.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. from rest_framework.views import APIView
  2. from rest_framework.response import Response
  3. from rest_framework import status
  4. from django.utils import timezone
  5. from datetime import timedelta
  6. from .services import LocationStatisticsService
  7. from .models import LocationGroupStatistics
  8. from django.db import transaction
  9. from django.db.models import Count, Q
  10. import logging
  11. logger = logging.getLogger(__name__)
  12. from .serializers import LocationStatisticsSerializer, LocationGroupStatisticsSerializer
  13. class LocationStatisticsView(APIView):
  14. """货位统计API视图"""
  15. def get(self, request):
  16. """获取货位统计信息"""
  17. warehouse_code = request.GET.get('warehouse_code')
  18. layer = request.GET.get('layer')
  19. # 尝试从参数转换层数
  20. if layer is not None:
  21. try:
  22. layer = int(layer)
  23. except ValueError:
  24. layer = None
  25. # 从数据库获取最新统计
  26. statistics = LocationStatisticsService.get_latest_statistics(warehouse_code, layer)
  27. serializer = LocationStatisticsSerializer(statistics, many=True)
  28. response_data = {
  29. 'timestamp': timezone.now().isoformat(),
  30. 'data': serializer.data
  31. }
  32. return Response(response_data)
  33. def post(self, request):
  34. """手动触发统计计算"""
  35. warehouse_code = request.data.get('warehouse_code')
  36. layer = request.data.get('layer')
  37. # 计算统计信息
  38. stats_data = LocationStatisticsService.calculate_location_statistics(warehouse_code, layer)
  39. # 保存到数据库
  40. count = LocationStatisticsService.save_statistics_to_db(stats_data)
  41. return Response({
  42. 'message': f'成功统计了{count}条记录',
  43. 'timestamp': timezone.now().isoformat()
  44. }, status=status.HTTP_201_CREATED)
  45. class LocationStatisticsHistoryView(APIView):
  46. """货位统计历史数据API视图"""
  47. def get(self, request):
  48. """获取历史统计信息"""
  49. warehouse_code = request.GET.get('warehouse_code')
  50. layer = request.GET.get('layer')
  51. hours = int(request.GET.get('hours', 24)) # 默认最近24小时
  52. end_time = timezone.now()
  53. start_time = end_time - timedelta(hours=hours)
  54. statistics = LocationStatisticsService.get_statistics_by_time_range(
  55. start_time, end_time, warehouse_code, layer
  56. )
  57. serializer = LocationStatisticsSerializer(statistics, many=True)
  58. return Response({
  59. 'period': f'{start_time} - {end_time}',
  60. 'data': serializer.data
  61. })
  62. class LocationGroupStatisticsView(APIView):
  63. """货位组统计API视图"""
  64. def get(self, request):
  65. """获取货位组统计信息"""
  66. warehouse_code = request.GET.get('warehouse_code')
  67. layer = request.GET.get('layer')
  68. # 新增过滤参数
  69. min_utilization = float(request.GET.get('min_utilization', 0)) # 最小使用率,默认0
  70. min_used_locations = int(request.GET.get('min_used_locations', 0)) # 最小已用货位数,默认0
  71. filters = {}
  72. if warehouse_code:
  73. filters['warehouse_code'] = warehouse_code
  74. if layer:
  75. try:
  76. filters['layer'] = int(layer)
  77. except ValueError:
  78. pass
  79. # 获取最新的组统计
  80. from django.db.models import Max
  81. latest_stats = LocationGroupStatistics.objects.filter(
  82. **filters
  83. ).values('warehouse_code', 'layer', 'location_group').annotate(
  84. latest_time=Max('statistic_time')
  85. )
  86. group_stats = []
  87. for stat in latest_stats:
  88. latest_record = LocationGroupStatistics.objects.filter(
  89. warehouse_code=stat['warehouse_code'],
  90. layer=stat['layer'],
  91. location_group=stat['location_group'],
  92. statistic_time=stat['latest_time']
  93. ).first()
  94. # 根据参数过滤
  95. if latest_record:
  96. # 检查使用率条件
  97. utilization_ok = latest_record.utilization_rate >= min_utilization
  98. # 检查已用货位数条件
  99. used_locations_ok = latest_record.used_locations >= min_used_locations
  100. if utilization_ok and used_locations_ok:
  101. group_stats.append(latest_record)
  102. serializer = LocationGroupStatisticsSerializer(group_stats, many=True)
  103. return Response({
  104. 'timestamp': timezone.now().isoformat(),
  105. 'filters': {
  106. 'min_utilization': min_utilization,
  107. 'min_used_locations': min_used_locations,
  108. 'total_records': len(group_stats)
  109. },
  110. 'data': serializer.data
  111. })
  112. class LocationConsistencyCheckView(APIView):
  113. """库位一致性检查API"""
  114. def post(self, request):
  115. warehouse_code = request.data.get('warehouse_code')
  116. layer = request.GET.get('layer')
  117. if int(layer) < 1 :
  118. layer = None
  119. auto_fix = request.data.get('auto_fix', False)
  120. checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
  121. result = checker.check_all()
  122. return Response({
  123. 'success': True,
  124. 'data': checker.generate_report()
  125. })
  126. class LocationConsistencyChecker:
  127. TARGET_LOCATION_TYPES = ['T5', 'T4', 'S4', 'T2', 'T1']
  128. """
  129. 库位一致性检测器
  130. 用于检测库位状态与Link记录的一致性以及库位组的状态一致性
  131. """
  132. def __init__(self, warehouse_code=None, layer=None, auto_fix=False):
  133. """
  134. 初始化检测器
  135. Args:
  136. warehouse_code: 指定仓库代码,如果为None则检测所有仓库
  137. layer: 指定楼层,如果为None则检测所有楼层
  138. auto_fix: 是否自动修复检测到的问题
  139. """
  140. self.warehouse_code = warehouse_code
  141. self.layer = layer
  142. self.auto_fix = auto_fix
  143. self.results = {
  144. 'check_time': timezone.now(),
  145. 'location_errors': [],
  146. 'group_errors': [],
  147. 'fixed_locations': [],
  148. 'fixed_groups': [],
  149. 'repair_errors': [],
  150. 'summary': {
  151. 'total_locations': 0,
  152. 'checked_locations': 0,
  153. 'error_locations': 0,
  154. 'total_groups': 0,
  155. 'checked_groups': 0,
  156. 'error_groups': 0,
  157. 'fixed_location_count': 0,
  158. 'fixed_group_count': 0
  159. }
  160. }
  161. def check_all(self):
  162. """执行所有检测"""
  163. logger.info(f"开始库位一致性检测,仓库: {self.warehouse_code}, 楼层: {self.layer}")
  164. # 检测库位状态与Link记录的一致性
  165. self.check_location_link_consistency()
  166. # 检测库位组状态一致性
  167. self.check_group_consistency()
  168. # 如果启用自动修复,执行修复
  169. if self.auto_fix:
  170. self.fix_detected_issues()
  171. logger.info(f"库位一致性检测完成,发现{self.results['summary']['error_locations']}个库位问题,"
  172. f"{self.results['summary']['error_groups']}个库位组问题")
  173. return self.results
  174. def check_location_link_consistency(self):
  175. """检测库位状态与Link记录的一致性"""
  176. from bin.models import LocationModel, LocationContainerLink
  177. # 构建查询条件
  178. filters = {'is_active': True, 'location_type__in': self.TARGET_LOCATION_TYPES}
  179. if self.warehouse_code:
  180. filters['warehouse_code'] = self.warehouse_code
  181. if self.layer is not None:
  182. filters['layer'] = self.layer
  183. # 获取库位并预取Link记录
  184. locations = LocationModel.objects.filter(**filters).prefetch_related('container_links')
  185. self.results['summary']['total_locations'] = locations.count()
  186. for location in locations:
  187. self.results['summary']['checked_locations'] += 1
  188. # 获取该库位的活跃Link记录数量
  189. active_links_count = location.container_links.filter(is_active=True).count()
  190. # 根据状态判断是否一致
  191. is_consistent, expected_status, error_type = self._check_location_consistency(
  192. location.status, active_links_count
  193. )
  194. if not is_consistent:
  195. self.results['summary']['error_locations'] += 1
  196. self.results['location_errors'].append({
  197. 'location_id': location.id,
  198. 'location_code': location.location_code,
  199. 'warehouse_code': location.warehouse_code,
  200. 'current_status': location.status,
  201. 'expected_status': expected_status,
  202. 'active_links_count': active_links_count,
  203. 'layer': location.layer,
  204. 'row': location.row,
  205. 'col': location.col,
  206. 'location_group': location.location_group,
  207. 'error_type': error_type,
  208. 'detected_at': timezone.now()
  209. })
  210. def _check_location_consistency(self, current_status, active_links_count):
  211. """
  212. 检查单个库位的状态一致性
  213. Returns:
  214. tuple: (是否一致, 预期状态, 错误类型)
  215. """
  216. if current_status == 'available':
  217. # available状态应该没有活跃的Link记录
  218. if active_links_count > 0:
  219. return False, 'occupied', 'available_but_has_links'
  220. elif current_status in ['occupied','reserved']:
  221. # occupied状态应该至少有一个活跃的Link记录
  222. if active_links_count != 1:
  223. return False, 'available', 'occupied_but_no_links'
  224. elif current_status in ['disabled', 'maintenance']:
  225. # 这些特殊状态可以有Link记录,但通常不应该有
  226. if active_links_count > 0:
  227. return False, current_status, 'special_status_has_links'
  228. return True, current_status, None
  229. def check_group_consistency(self):
  230. """检测库位组状态的一致性"""
  231. from bin.models import LocationGroupModel
  232. # 构建查询条件
  233. filters = {'is_active': True}
  234. if self.warehouse_code:
  235. filters['warehouse_code'] = self.warehouse_code
  236. if self.layer is not None:
  237. filters['layer'] = self.layer
  238. # 获取库位组并预取关联的库位
  239. groups = LocationGroupModel.objects.filter(**filters).prefetch_related('location_items')
  240. self.results['summary']['total_groups'] = groups.count()
  241. for group in groups:
  242. self.results['summary']['checked_groups'] += 1
  243. # 获取组内所有库位
  244. group_locations = group.location_items.filter(is_active=True)
  245. # 统计组内库位的状态分布
  246. status_counts = self._get_group_status_distribution(group_locations)
  247. total_locations = group_locations.count()
  248. # 根据组内库位状态推断组应该的状态
  249. is_consistent, expected_status, error_type = self._check_group_consistency(
  250. group.status, status_counts, total_locations
  251. )
  252. if not is_consistent:
  253. self.results['summary']['error_groups'] += 1
  254. self.results['group_errors'].append({
  255. 'group_id': group.id,
  256. 'group_code': group.group_code,
  257. 'group_name': group.group_name,
  258. 'warehouse_code': group.warehouse_code,
  259. 'current_status': group.status,
  260. 'expected_status': expected_status,
  261. 'total_locations': total_locations,
  262. 'status_distribution': status_counts,
  263. 'layer': group.layer,
  264. 'error_type': error_type,
  265. 'detected_at': timezone.now()
  266. })
  267. def _get_group_status_distribution(self, group_locations):
  268. """获取组内库位状态分布"""
  269. return group_locations.aggregate(
  270. available_count=Count('id', filter=Q(status='available')),
  271. occupied_count=Count('id', filter=Q(status='occupied')),
  272. disabled_count=Count('id', filter=Q(status='disabled')),
  273. reserved_count=Count('id', filter=Q(status='reserved')),
  274. maintenance_count=Count('id', filter=Q(status='maintenance'))
  275. )
  276. def _check_group_consistency(self, current_status, status_counts, total_locations):
  277. """
  278. 检查库位组状态一致性
  279. Returns:
  280. tuple: (是否一致, 预期状态, 错误类型)
  281. """
  282. if total_locations == 0:
  283. # 空组应该是available或disabled
  284. if current_status not in ['available', 'disabled']:
  285. return False, 'available', 'empty_group_wrong_status'
  286. else:
  287. # 根据库位状态分布推断组状态
  288. if status_counts['occupied_count'] == total_locations:
  289. # 所有库位都被占用,组状态应该是full
  290. if current_status != 'full':
  291. return False, 'full', 'all_occupied_but_not_full'
  292. elif status_counts['occupied_count'] > 0:
  293. # 有库位被占用,组状态应该是occupied
  294. if current_status != 'occupied':
  295. return False, 'occupied', 'has_occupied_but_wrong_status'
  296. elif status_counts['available_count'] == total_locations:
  297. # 所有库位都可用,组状态应该是available
  298. if current_status != 'available':
  299. return False, 'available', 'all_available_but_wrong_status'
  300. # 检查特殊状态
  301. elif status_counts['disabled_count'] > 0 and current_status != 'disabled':
  302. return False, 'disabled', 'has_disabled_but_wrong_status'
  303. elif status_counts['maintenance_count'] > 0 and current_status != 'maintenance':
  304. return False, 'maintenance', 'has_maintenance_but_wrong_status'
  305. return True, current_status, None
  306. def fix_detected_issues(self):
  307. """修复检测到的不一致问题"""
  308. logger.info("开始修复检测到的不一致问题")
  309. # 修复库位状态不一致
  310. if self.results['location_errors']:
  311. self._fix_location_issues()
  312. # 修复库位组状态不一致
  313. if self.results['group_errors']:
  314. self._fix_group_issues()
  315. logger.info(f"修复完成: {self.results['summary']['fixed_location_count']}个库位, "
  316. f"{self.results['summary']['fixed_group_count']}个库位组")
  317. def _fix_location_issues(self):
  318. """修复库位状态不一致问题"""
  319. from bin.models import LocationModel
  320. for error in self.results['location_errors']:
  321. try:
  322. with transaction.atomic():
  323. location = LocationModel.objects.select_for_update().get(id=error['location_id'])
  324. # 只有明确有预期状态时才修复
  325. if error['expected_status'] and error['expected_status'] != 'need_check':
  326. old_status = location.status
  327. location.status = error['expected_status']
  328. location.save()
  329. self.results['summary']['fixed_location_count'] += 1
  330. self.results['fixed_locations'].append({
  331. 'location_id': location.id,
  332. 'location_code': location.location_code,
  333. 'old_status': old_status,
  334. 'new_status': location.status,
  335. 'fixed_at': timezone.now(),
  336. 'error_type': error['error_type']
  337. })
  338. else:
  339. # 标记为需要手动检查
  340. self.results['fixed_locations'].append({
  341. 'location_id': location.id,
  342. 'location_code': location.location_code,
  343. 'status': '需要手动检查',
  344. 'reason': '无法自动确定正确状态',
  345. 'error_type': error['error_type']
  346. })
  347. except Exception as e:
  348. logger.error(f"修复库位{error.get('location_id')}时出错: {str(e)}")
  349. self.results['repair_errors'].append({
  350. 'type': 'location_fix_error',
  351. 'location_id': error.get('location_id'),
  352. 'error_message': str(e),
  353. 'error_type': error.get('error_type')
  354. })
  355. def _fix_group_issues(self):
  356. """修复库位组状态不一致问题"""
  357. from bin.models import LocationGroupModel
  358. for error in self.results['group_errors']:
  359. try:
  360. with transaction.atomic():
  361. group = LocationGroupModel.objects.select_for_update().get(id=error['group_id'])
  362. # 只有明确有预期状态时才修复
  363. if error['expected_status'] and error['expected_status'] != 'need_check':
  364. old_status = group.status
  365. group.status = error['expected_status']
  366. group.save()
  367. self.results['summary']['fixed_group_count'] += 1
  368. self.results['fixed_groups'].append({
  369. 'group_id': group.id,
  370. 'group_code': group.group_code,
  371. 'old_status': old_status,
  372. 'new_status': group.status,
  373. 'fixed_at': timezone.now(),
  374. 'error_type': error['error_type']
  375. })
  376. else:
  377. # 标记为需要手动检查
  378. self.results['fixed_groups'].append({
  379. 'group_id': group.id,
  380. 'group_code': group.group_code,
  381. 'status': '需要手动检查',
  382. 'reason': '无法自动确定正确状态',
  383. 'error_type': error.get('error_type')
  384. })
  385. except Exception as e:
  386. logger.error(f"修复库位组{error.get('group_id')}时出错: {str(e)}")
  387. self.results['repair_errors'].append({
  388. 'type': 'group_fix_error',
  389. 'group_id': error.get('group_id'),
  390. 'error_message': str(e),
  391. 'error_type': error.get('error_type')
  392. })
  393. def get_summary(self):
  394. """获取检测摘要"""
  395. return {
  396. 'check_time': self.results['check_time'],
  397. 'total_checked': {
  398. 'locations': self.results['summary']['checked_locations'],
  399. 'groups': self.results['summary']['checked_groups']
  400. },
  401. 'errors_found': {
  402. 'locations': self.results['summary']['error_locations'],
  403. 'groups': self.results['summary']['error_groups']
  404. },
  405. 'fixed': {
  406. 'locations': self.results['summary']['fixed_location_count'],
  407. 'groups': self.results['summary']['fixed_group_count']
  408. },
  409. 'has_errors': len(self.results['repair_errors']) > 0
  410. }
  411. def generate_report(self):
  412. """生成检测报告"""
  413. summary = self.get_summary()
  414. report = {
  415. 'summary': summary,
  416. 'details': {
  417. 'location_errors': self.results['location_errors'],
  418. 'group_errors': self.results['group_errors'],
  419. 'fixed_locations': self.results['fixed_locations'],
  420. 'fixed_groups': self.results['fixed_groups'],
  421. 'repair_errors': self.results['repair_errors']
  422. }
  423. }
  424. return report
  425. # 便捷函数
  426. def check_location_consistency(warehouse_code=None, layer=None, auto_fix=False):
  427. """
  428. 便捷函数:检查库位一致性
  429. Args:
  430. warehouse_code: 仓库代码
  431. layer: 楼层
  432. auto_fix: 是否自动修复
  433. Returns:
  434. dict: 检测结果
  435. """
  436. checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
  437. return checker.check_all()
  438. def get_consistency_report(warehouse_code=None, layer=None, auto_fix=False):
  439. """
  440. 获取详细的检测报告
  441. Args:
  442. warehouse_code: 仓库代码
  443. layer: 楼层
  444. auto_fix: 是否自动修复
  445. Returns:
  446. dict: 检测报告
  447. """
  448. checker = LocationConsistencyChecker(warehouse_code, layer, auto_fix)
  449. checker.check_all()
  450. return checker.generate_report()