DepositRefundLogic.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <?php
  2. namespace plugin\saiadmin\app\logic\fund;
  3. use DateTime;
  4. use plugin\saiadmin\app\model\system\SystemUser;
  5. use plugin\saiadmin\basic\think\BaseLogic;
  6. use plugin\saiadmin\exception\ApiException;
  7. use plugin\saiadmin\service\OpenSpoutWriter;
  8. use support\Response;
  9. use support\think\Db;
  10. /**
  11. * 押金退还任务查询与导出。
  12. */
  13. class DepositRefundLogic extends BaseLogic
  14. {
  15. private const SOURCE_NAMES = [1 => '微信', 2 => '支付宝'];
  16. private const REFUND_TYPE_NAMES = [0 => '原路退回', 1 => '退回钱包'];
  17. private const STATUS_NAMES = [0 => '待退还', 1 => '已退还', 2 => '退款异常'];
  18. private const DEVICE_NAMES = [
  19. 1 => '充电底座',
  20. 100 => 'K霸宝',
  21. 101 => '充电宝',
  22. 102 => 'K霸宝 Blue',
  23. 103 => '密码线充',
  24. 104 => '蓝牙线充',
  25. 105 => '密码蓝牙线充',
  26. 107 => '车载充电器',
  27. ];
  28. private const EXPORT_LIMIT = 50000;
  29. public function indexList(array $filters): array
  30. {
  31. $page = max(1, (int) request()->input('page', 1));
  32. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  33. $result = $this->buildQuery($filters)
  34. ->order('r.apply_time', 'desc')
  35. ->order('r.id', 'desc')
  36. ->paginate(['page' => $page, 'list_rows' => $limit])
  37. ->toArray();
  38. $result['data'] = array_map(fn (array $row): array => $this->formatRecord($row), $result['data'] ?? []);
  39. $result['summary'] = $this->summary($filters);
  40. return $result;
  41. }
  42. public function export(array $filters): Response
  43. {
  44. $query = $this->buildQuery($filters);
  45. $count = (int) (clone $query)->count();
  46. if ($count > self::EXPORT_LIMIT) {
  47. throw new ApiException('导出记录超过 50000 条,请缩小筛选范围');
  48. }
  49. $rows = $query->order('r.apply_time', 'desc')->order('r.id', 'desc')->select()->toArray();
  50. $data = [];
  51. foreach ($rows as $row) {
  52. $record = $this->formatRecord($row);
  53. $data[] = [
  54. 'order_sn' => $this->excelSafe($record['order_sn']),
  55. 'user_id' => $record['user_id'],
  56. 'nickname' => $this->excelSafe($record['nickname']),
  57. 'phone' => $this->excelSafe($record['phone']),
  58. 'source_name' => $record['source_name'],
  59. 'device_type_name' => $record['device_type_name'],
  60. 'deposit_amount' => $record['deposit_amount'],
  61. 'consumption_amount' => $record['consumption_amount'],
  62. 'refund_amount' => $record['refund_amount'],
  63. 'apply_time' => $record['apply_time'],
  64. 'scheduled_refund_time' => $record['scheduled_refund_time'],
  65. 'refund_time' => $record['refund_time'],
  66. 'refund_type_name' => $record['refund_type_name'],
  67. 'status_name' => $record['status_name'],
  68. 'manager_name' => $this->excelSafe($record['manager_name']),
  69. 'remark' => $this->excelSafe($record['remark']),
  70. ];
  71. }
  72. $fileName = '退押记录_' . date('Ymd_His') . '.xlsx';
  73. $writer = new OpenSpoutWriter($fileName);
  74. $writer->setWidth([24, 12, 22, 18, 10, 18, 14, 14, 14, 22, 22, 22, 14, 12, 18, 36]);
  75. $writer->setHeader([
  76. '订单号', '用户ID', '用户昵称', '手机号', '来源', '设备类型', '押金金额', '消费金额',
  77. '退款金额', '申请时间', '计划退款时间', '实际退款时间', '退款类型', '状态', '管理代理', '备注',
  78. ]);
  79. $writer->setData($data);
  80. return response()->download($writer->returnFile(), urlencode($fileName));
  81. }
  82. private function buildQuery(array $filters)
  83. {
  84. $query = Db::table('sa_deposit_refund_record')->alias('r')
  85. ->leftJoin('sa_rental_order o', 'o.id = r.order_id AND o.delete_time IS NULL')
  86. ->leftJoin('sa_rental_user u', 'u.id = r.user_id AND u.delete_time IS NULL')
  87. ->leftJoin('sa_system_user a', 'a.id = r.manage_agent_id AND a.delete_time IS NULL')
  88. ->whereNull('r.delete_time')
  89. ->field(implode(',', [
  90. 'r.id', 'r.order_id', 'r.order_sn', 'r.user_id', 'r.manage_agent_id', 'r.source',
  91. 'r.device_type', 'r.deposit_amount', 'r.consumption_amount', 'r.refund_amount',
  92. 'r.apply_time', 'r.scheduled_refund_time', 'r.refund_time', 'r.refund_type',
  93. 'r.status', 'r.remark', 'u.nickname', 'u.phone', 'u.avatar',
  94. 'o.user_name AS order_user_name', 'o.user_phone AS order_user_phone',
  95. 'a.username AS manager_username', 'a.realname AS manager_realname',
  96. ]));
  97. $managerIds = $this->accessibleManagerIds();
  98. if (is_array($managerIds)) {
  99. if ($managerIds === []) {
  100. $query->where('r.id', 0);
  101. } else {
  102. $query->whereIn('r.manage_agent_id', $managerIds);
  103. }
  104. }
  105. $status = trim((string) ($filters['status'] ?? ''));
  106. if ($status !== '') {
  107. if (!ctype_digit($status) || !isset(self::STATUS_NAMES[(int) $status])) {
  108. throw new ApiException('退押状态不支持');
  109. }
  110. $query->where('r.status', (int) $status);
  111. }
  112. $source = trim((string) ($filters['source'] ?? ''));
  113. if ($source !== '') {
  114. if (!ctype_digit($source) || !isset(self::SOURCE_NAMES[(int) $source])) {
  115. throw new ApiException('订单来源不支持');
  116. }
  117. $query->where('r.source', (int) $source);
  118. }
  119. $refundType = trim((string) ($filters['refund_type'] ?? ''));
  120. if ($refundType !== '') {
  121. if (!ctype_digit($refundType) || !isset(self::REFUND_TYPE_NAMES[(int) $refundType])) {
  122. throw new ApiException('退款类型不支持');
  123. }
  124. $query->where('r.refund_type', (int) $refundType);
  125. }
  126. $orderSn = trim((string) ($filters['order_sn'] ?? ''));
  127. if ($orderSn !== '') {
  128. if (mb_strlen($orderSn) > 64) {
  129. throw new ApiException('订单号不能超过 64 个字符');
  130. }
  131. $query->whereLike('r.order_sn', '%' . $orderSn . '%');
  132. }
  133. $userId = trim((string) ($filters['user_id'] ?? ''));
  134. if ($userId !== '') {
  135. if (!ctype_digit($userId) || (int) $userId <= 0) {
  136. throw new ApiException('用户 ID 格式不正确');
  137. }
  138. $query->where('r.user_id', (int) $userId);
  139. }
  140. [$startDate, $endDate] = $this->dateRange($filters);
  141. if ($startDate !== '') {
  142. $query->where('r.apply_time', '>=', $startDate . ' 00:00:00');
  143. }
  144. if ($endDate !== '') {
  145. $query->where('r.apply_time', '<=', $endDate . ' 23:59:59');
  146. }
  147. return $query;
  148. }
  149. private function summary(array $filters): array
  150. {
  151. $query = $this->buildQuery($filters);
  152. $query->removeOption('field');
  153. $rows = $query->field(implode(',', [
  154. 'COUNT(*) AS record_count',
  155. 'COALESCE(SUM(r.refund_amount), 0) AS total_refund_amount',
  156. 'COALESCE(SUM(CASE WHEN r.status = 1 THEN r.refund_amount ELSE 0 END), 0) AS refunded_amount',
  157. 'COALESCE(SUM(CASE WHEN r.status <> 1 THEN r.refund_amount ELSE 0 END), 0) AS pending_amount',
  158. ]))->select()->toArray();
  159. $row = $rows[0] ?? [];
  160. return [
  161. 'record_count' => (int) ($row['record_count'] ?? 0),
  162. 'total_refund_amount' => round((float) ($row['total_refund_amount'] ?? 0), 2),
  163. 'refunded_amount' => round((float) ($row['refunded_amount'] ?? 0), 2),
  164. 'pending_amount' => round((float) ($row['pending_amount'] ?? 0), 2),
  165. ];
  166. }
  167. private function formatRecord(array $row): array
  168. {
  169. $nickname = trim((string) ($row['nickname'] ?? ''));
  170. if ($nickname === '') {
  171. $nickname = trim((string) ($row['order_user_name'] ?? '')) ?: '未设置昵称';
  172. }
  173. $phone = trim((string) ($row['phone'] ?? ''));
  174. if ($phone === '') {
  175. $phone = trim((string) ($row['order_user_phone'] ?? ''));
  176. }
  177. $managerName = trim((string) ($row['manager_realname'] ?? ''));
  178. if ($managerName === '') {
  179. $managerName = trim((string) ($row['manager_username'] ?? ''));
  180. }
  181. $source = (int) ($row['source'] ?? 0);
  182. $deviceType = (int) ($row['device_type'] ?? 0);
  183. $refundType = (int) ($row['refund_type'] ?? 0);
  184. $status = (int) ($row['status'] ?? 0);
  185. return [
  186. 'id' => (int) ($row['id'] ?? 0),
  187. 'order_id' => (int) ($row['order_id'] ?? 0),
  188. 'order_sn' => (string) ($row['order_sn'] ?? ''),
  189. 'user_id' => (int) ($row['user_id'] ?? 0),
  190. 'nickname' => $nickname,
  191. 'phone' => $phone,
  192. 'avatar' => (string) ($row['avatar'] ?? ''),
  193. 'manage_agent_id' => (int) ($row['manage_agent_id'] ?? 0),
  194. 'manager_name' => $managerName !== '' ? $managerName : '管理员#' . (int) ($row['manage_agent_id'] ?? 0),
  195. 'source' => $source,
  196. 'source_name' => self::SOURCE_NAMES[$source] ?? '未知',
  197. 'device_type' => $deviceType,
  198. 'device_type_name' => self::DEVICE_NAMES[$deviceType] ?? '其他设备',
  199. 'deposit_amount' => round((float) ($row['deposit_amount'] ?? 0), 2),
  200. 'consumption_amount' => round((float) ($row['consumption_amount'] ?? 0), 2),
  201. 'refund_amount' => round((float) ($row['refund_amount'] ?? 0), 2),
  202. 'apply_time' => (string) ($row['apply_time'] ?? ''),
  203. 'scheduled_refund_time' => (string) ($row['scheduled_refund_time'] ?? ''),
  204. 'refund_time' => (string) ($row['refund_time'] ?? ''),
  205. 'refund_type' => $refundType,
  206. 'refund_type_name' => self::REFUND_TYPE_NAMES[$refundType] ?? '其他',
  207. 'status' => $status,
  208. 'status_name' => self::STATUS_NAMES[$status] ?? '未知',
  209. 'remark' => (string) ($row['remark'] ?? ''),
  210. ];
  211. }
  212. /** @return array{0:string,1:string} */
  213. private function dateRange(array $filters): array
  214. {
  215. $startDate = trim((string) ($filters['start_date'] ?? ''));
  216. $endDate = trim((string) ($filters['end_date'] ?? ''));
  217. foreach ([['开始', $startDate], ['结束', $endDate]] as [$label, $date]) {
  218. if ($date === '') {
  219. continue;
  220. }
  221. $parsed = DateTime::createFromFormat('!Y-m-d', $date);
  222. if (!$parsed || $parsed->format('Y-m-d') !== $date) {
  223. throw new ApiException($label . '日期格式不正确');
  224. }
  225. }
  226. if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
  227. throw new ApiException('开始日期不能晚于结束日期');
  228. }
  229. return [$startDate, $endDate];
  230. }
  231. /** @return int[]|null */
  232. private function accessibleManagerIds(): ?array
  233. {
  234. $currentId = (int) ($this->adminInfo['id'] ?? 0);
  235. if ($currentId === 1) {
  236. return null;
  237. }
  238. if ($currentId <= 0) {
  239. return [];
  240. }
  241. $managerIds = [$currentId];
  242. $dept = $this->adminInfo['deptList'] ?? [];
  243. if (empty($dept['id']) || !isset($dept['level'])) {
  244. return $managerIds;
  245. }
  246. $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
  247. $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
  248. ->whereLike('level', $prefix . '%')->column('id');
  249. if ($deptIds !== []) {
  250. $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
  251. ->whereNull('delete_time')->column('id')));
  252. }
  253. return array_values(array_unique($managerIds));
  254. }
  255. private function excelSafe(mixed $value): string
  256. {
  257. $value = (string) $value;
  258. return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
  259. }
  260. }