DeviceOperationLogic.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. namespace plugin\saiadmin\app\logic\operation;
  3. use DateTimeImmutable;
  4. use plugin\saiadmin\app\model\system\SystemUser;
  5. use plugin\saiadmin\app\service\DeviceOperationMetrics;
  6. use plugin\saiadmin\app\service\DeviceOperationShares;
  7. use plugin\saiadmin\app\service\DeviceProductCatalog;
  8. use plugin\saiadmin\basic\think\BaseLogic;
  9. use plugin\saiadmin\exception\ApiException;
  10. use support\think\Db;
  11. class DeviceOperationLogic extends BaseLogic
  12. {
  13. private const NET = 'CASE WHEN o.status = 5 OR o.paid_amount <= 0 THEN 0 '
  14. . 'ELSE GREATEST(o.paid_amount - o.refund_amount, 0) END';
  15. private const SETTLED = 'COALESCE(o.return_at,o.update_time,o.rent_at,o.create_time)';
  16. public function indexList(array $filters): array
  17. {
  18. $categories = (new DeviceProductCatalog())->categories();
  19. $options = $this->parseFilters($filters, $categories);
  20. $managerIds = $this->managerIds($options['scope']);
  21. $now = new DateTimeImmutable();
  22. $query = $this->deviceQuery($managerIds)->whereIn('d.goods_type', $options['goods_types']);
  23. $total = (int) (clone $query)->count();
  24. $rows = $this->loadDevicePage($query, $managerIds, $options, $now);
  25. $dailyAmounts = $this->loadDailyAmounts(array_column($rows, 'id'), $managerIds, $now);
  26. $shares = $this->loadShares($rows);
  27. foreach ($rows as &$row) {
  28. $row = array_merge($row, $shares->calculate($row, $options['category']));
  29. $row = DeviceOperationMetrics::calculate($row, $dailyAmounts[$row['id']] ?? [], $now);
  30. unset($row['merchant_relative_rate'], $row['profit_method'], $row['merchant_dept_level']);
  31. }
  32. unset($row);
  33. return [
  34. 'data' => $rows, 'total' => $total, 'current_page' => $options['page'],
  35. 'categories' => $categories, 'category' => $options['category'],
  36. 'per_page' => $options['limit'], 'last_page' => max(1, (int) ceil($total / $options['limit'])),
  37. ];
  38. }
  39. private function parseFilters(array $filters, array $categories): array
  40. {
  41. $scope = (string) ($filters['scope'] ?? 'mine');
  42. $category = (string) ($filters['category'] ?? '');
  43. $category = $category === '' ? ($categories[0]['value'] ?? '') : $category;
  44. $typesByCategory = array_column($categories, 'goods_types', 'value');
  45. if ($categories === []) {
  46. throw new ApiException('请先在数据字典维护设备产品和设备类型产品映射');
  47. }
  48. if (!in_array($scope, ['mine', 'subordinate'], true) || !isset($typesByCategory[$category])) {
  49. throw new ApiException('请选择有效的用户类别和设备类别');
  50. }
  51. $sort = (string) ($filters['sort'] ?? '');
  52. $direction = (string) ($filters['order'] ?? 'desc');
  53. $sorts = [
  54. '' => 'd.id', 'total_amount' => 'total_amount', 'total_orders' => 'total_orders',
  55. 'unit_price' => 'unit_price', 'profit_rate_value' => 'profit_rate_value',
  56. ];
  57. if (!isset($sorts[$sort]) || !in_array($direction, ['asc', 'desc'], true)) {
  58. throw new ApiException('排序参数错误');
  59. }
  60. $page = max(1, (int) ($filters['page'] ?? 1));
  61. $limit = min(100, max(1, (int) ($filters['limit'] ?? 10)));
  62. return compact('scope', 'category', 'page', 'limit', 'direction')
  63. + ['sort' => $sorts[$sort], 'goods_types' => $typesByCategory[$category]];
  64. }
  65. private function aggregateOrders(array $ids, DateTimeImmutable $now): string
  66. {
  67. $today = $now->format('Y-m-d 00:00:00');
  68. $tomorrow = $now->modify('+1 day')->format('Y-m-d 00:00:00');
  69. $yesterday = $now->modify('-1 day')->format('Y-m-d 00:00:00');
  70. $settled = self::SETTLED;
  71. $todayCondition = "$settled >= '$today' AND $settled < '$tomorrow'";
  72. $yesterdayCondition = "$settled >= '$yesterday' AND $settled < '$today'";
  73. // 数据库先聚合/排序再分页,避免只排序当前页,也不逐设备执行统计 SQL。
  74. return $this->orderQuery($ids, $now)->fieldRaw(implode(',', [
  75. 'o.device_id', 'SUM(' . self::NET . ') AS total_amount', 'COUNT(*) AS total_orders',
  76. 'SUM(CASE WHEN o.paid_amount > 0 AND o.status <> 5 THEN 1 ELSE 0 END) AS paid_orders',
  77. "SUM(CASE WHEN $todayCondition THEN " . self::NET . ' ELSE 0 END) AS today_amount',
  78. "SUM(CASE WHEN $yesterdayCondition THEN " . self::NET . ' ELSE 0 END) AS yesterday_amount',
  79. "SUM(CASE WHEN $todayCondition THEN 1 ELSE 0 END) AS today_orders",
  80. "SUM(CASE WHEN $yesterdayCondition THEN 1 ELSE 0 END) AS yesterday_orders",
  81. ]))->group('o.device_id')->buildSql();
  82. }
  83. private function loadDevicePage($query, array $ids, array $options, DateTimeImmutable $now): array
  84. {
  85. $stats = $this->aggregateOrders($ids, $now);
  86. return $query->leftJoin([$stats => 's'], 's.device_id = d.id')
  87. ->fieldRaw(implode(',', [
  88. 'd.id,d.goods_type,d.goods_sn,d.device_code,d.manage_agent_id',
  89. 'b.merchant_id,b.bind_time,m.name AS merchant_name',
  90. "COALESCE(NULLIF(manager.realname,''),manager.username,'') AS agent_name",
  91. 'd.cost_price,m.agent_id AS merchant_agent_id,m.profit_rate AS merchant_relative_rate,m.profit_method',
  92. 'merchant_dept.level AS merchant_dept_level',
  93. 'COALESCE(s.total_amount,0) AS total_amount,COALESCE(s.total_orders,0) AS total_orders',
  94. 'COALESCE(s.paid_orders,0) AS paid_orders',
  95. 'COALESCE(s.today_amount,0) AS today_amount,COALESCE(s.yesterday_amount,0) AS yesterday_amount',
  96. 'COALESCE(s.today_orders,0) AS today_orders,COALESCE(s.yesterday_orders,0) AS yesterday_orders',
  97. 'COALESCE(s.total_amount / NULLIF(s.paid_orders,0),0) AS unit_price',
  98. 'COALESCE(s.total_amount,0) / NULLIF(d.cost_price,0) * 100 AS profit_rate_value',
  99. ]))->order($options['sort'], $options['direction'])->order('d.id', 'desc')
  100. ->page($options['page'], $options['limit'])->select()->toArray();
  101. }
  102. private function loadDailyAmounts(array $deviceIds, array $ids, DateTimeImmutable $now): array
  103. {
  104. $dailyByDevice = [];
  105. if ($deviceIds !== []) {
  106. $daily = $this->orderQuery($ids, $now)->whereIn('o.device_id', $deviceIds)
  107. ->join('sa_merchant_device_binding current_binding', 'current_binding.device_id = o.device_id')
  108. ->whereRaw(self::SETTLED . ' >= current_binding.bind_time')
  109. ->whereRaw(self::SETTLED . ' <= ?', [$now->format('Y-m-d H:i:s')])
  110. ->fieldRaw('o.device_id,DATE(' . self::SETTLED . ') AS stat_date,SUM(' . self::NET . ') AS amount')
  111. ->group('o.device_id,stat_date')->order('stat_date', 'asc')->select()->toArray();
  112. foreach ($daily as $day) {
  113. $dailyByDevice[$day['device_id']][] = $day;
  114. }
  115. }
  116. return $dailyByDevice;
  117. }
  118. private function loadShares(array $rows): DeviceOperationShares
  119. {
  120. if ($rows === []) {
  121. return new DeviceOperationShares([]);
  122. }
  123. $agentIds = array_unique(array_merge(array_column($rows, 'manage_agent_id'), array_column($rows, 'merchant_agent_id')));
  124. $departmentIds = [];
  125. foreach ($rows as $row) {
  126. $departmentIds = array_merge($departmentIds, explode(',', $row['merchant_dept_level'] ?? ''));
  127. }
  128. $departmentIds = array_unique(array_filter(array_map('intval', $departmentIds)));
  129. $agents = Db::table('sa_system_user')->alias('u')->whereNull('u.delete_time')
  130. ->where(function ($query) use ($agentIds, $departmentIds) {
  131. $query->whereIn('u.id', $agentIds)->whereOr('u.dept_id', 'in', $departmentIds ?: [-1]);
  132. })
  133. ->leftJoin('sa_agent_profile profile', 'profile.agent_id = u.id')
  134. ->leftJoin('sa_system_dept dept', 'dept.id = u.dept_id AND dept.delete_time IS NULL')
  135. ->field('u.id,u.dept_id,dept.level AS dept_level,profile.profit_rates')->select()->toArray();
  136. return new DeviceOperationShares($agents);
  137. }
  138. private function deviceQuery(array $ids)
  139. {
  140. return Db::table('sa_device')->alias('d')->whereNull('d.delete_time')
  141. ->whereIn('d.manage_agent_id', $ids ?: [-1])
  142. ->leftJoin('sa_merchant_device_binding b', 'b.device_id = d.id')
  143. ->leftJoin('sa_merchant m', 'm.id = b.merchant_id AND m.delete_time IS NULL')
  144. ->leftJoin('sa_system_user manager', 'manager.id = d.manage_agent_id AND manager.delete_time IS NULL')
  145. ->leftJoin('sa_system_user merchant_agent', 'merchant_agent.id = m.agent_id AND merchant_agent.delete_time IS NULL')
  146. ->leftJoin('sa_system_dept merchant_dept', 'merchant_dept.id = merchant_agent.dept_id AND merchant_dept.delete_time IS NULL');
  147. }
  148. private function orderQuery(array $ids, DateTimeImmutable $now)
  149. {
  150. return Db::table('sa_rental_order')->alias('o')->whereNull('o.delete_time')
  151. ->whereIn('o.manage_agent_id', $ids ?: [-1])
  152. ->whereRaw('COALESCE(o.rent_at,o.create_time) <= ?', [$now->format('Y-m-d H:i:s')]);
  153. }
  154. /** 本人与下级互斥;下级沿用项目记录的部门后代权限,不能用归属权代替管理权。 */
  155. private function managerIds(string $scope): array
  156. {
  157. $id = (int) ($this->adminInfo['id'] ?? 0);
  158. if ($id <= 0) {
  159. throw new ApiException('请先登录');
  160. }
  161. if ($scope === 'mine') {
  162. return [$id];
  163. }
  164. if ($id === 1) {
  165. return array_map('intval', SystemUser::where('id', '<>', 1)->whereNull('delete_time')->column('id'));
  166. }
  167. $dept = $this->adminInfo['deptList'] ?? [];
  168. if (empty($dept['id']) || !isset($dept['level'])) {
  169. return [];
  170. }
  171. $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
  172. ->whereLike('level', (string) $dept['level'] . (int) $dept['id'] . ',%')->column('id');
  173. return $deptIds === [] ? [] : array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
  174. ->where('id', '<>', $id)->whereNull('delete_time')->column('id'));
  175. }
  176. }