TransferOrderHelper.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <?php
  2. namespace App\Module\Transfer\AdminControllers\Helper;
  3. use App\Module\Transfer\Models\TransferOrder;
  4. use App\Module\Transfer\Enums\TransferStatus;
  5. use App\Module\Transfer\Enums\TransferType;
  6. use Dcat\Admin\Grid;
  7. use Dcat\Admin\Show;
  8. use Dcat\Admin\Form;
  9. /**
  10. * 划转订单管理辅助类
  11. */
  12. class TransferOrderHelper
  13. {
  14. /**
  15. * 配置Grid表格
  16. */
  17. public static function grid(Grid $grid): void
  18. {
  19. $grid->column('id', 'ID')->sortable();
  20. $grid->column('transfer_app.title', '应用名称')->limit(15);
  21. $grid->column('out_order_id', '外部订单ID')->copyable();
  22. $grid->column('user_id', '用户ID')->link(function ($value) {
  23. return admin_url("users/{$value}");
  24. });
  25. $grid->column('out_user_id', '外部用户ID')->limit(15);
  26. $grid->column('type', '类型')->display(function ($value) {
  27. $typeMap = [
  28. 1 => '转入',
  29. 2 => '转出',
  30. ];
  31. $labelMap = [
  32. 1 => 'success',
  33. 2 => 'warning',
  34. ];
  35. // 处理枚举对象
  36. $typeValue = is_object($value) ? $value->value : $value;
  37. $text = $typeMap[$typeValue] ?? '未知';
  38. $label = $labelMap[$typeValue] ?? 'default';
  39. return "<span class=\"label label-{$label}\">{$text}</span>";
  40. });
  41. $grid->column('status', '状态')->display(function ($value) {
  42. $statusMap = [
  43. 1 => '已创建',
  44. 20 => '处理中',
  45. 30 => '已回调',
  46. 100 => '已完成',
  47. -1 => '失败',
  48. ];
  49. $labelMap = [
  50. 1 => 'info',
  51. 20 => 'warning',
  52. 30 => 'primary',
  53. 100 => 'success',
  54. -1 => 'danger',
  55. ];
  56. // 处理枚举对象
  57. $statusValue = is_object($value) ? $value->value : $value;
  58. $text = $statusMap[$statusValue] ?? '未知';
  59. $label = $labelMap[$statusValue] ?? 'default';
  60. return "<span class=\"label label-{$label}\">{$text}</span>";
  61. });
  62. $grid->column('out_amount', '外部金额')->display(function ($value) {
  63. return number_format($value, 4);
  64. });
  65. $grid->column('amount', '内部金额')->display(function ($value) {
  66. return number_format($value, 4);
  67. });
  68. // 手续费信息
  69. $grid->column('fee_info', '手续费信息')->display(function () {
  70. if ($this->fee_amount > 0) {
  71. $feeRate = number_format($this->fee_rate * 100, 2) . '%';
  72. $feeAmount = number_format($this->fee_amount, 4);
  73. $actualAmount = number_format($this->actual_amount, 4);
  74. return "费率: {$feeRate}<br>手续费: {$feeAmount}<br>实际: {$actualAmount}";
  75. }
  76. return '无手续费';
  77. });
  78. $grid->column('exchange_rate', '汇率')->display(function ($value) {
  79. return number_format($value, 4);
  80. });
  81. $grid->column('created_at', '创建时间')->display(function ($value) {
  82. return $value ? $value->format('Y-m-d H:i:s') : '-';
  83. })->sortable();
  84. $grid->column('completed_at', '完成时间')->display(function ($value) {
  85. return $value ? $value->format('Y-m-d H:i:s') : '-';
  86. });
  87. // 处理时长
  88. $grid->column('duration', '处理时长')->display(function () {
  89. if ($this->completed_at && $this->created_at) {
  90. // 确保计算正确的时间差(完成时间 - 创建时间)
  91. $seconds = $this->completed_at->getTimestamp() - $this->created_at->getTimestamp();
  92. // 如果时间差为负数或0,显示为0秒
  93. if ($seconds <= 0) {
  94. return '0秒';
  95. }
  96. if ($seconds < 60) {
  97. return $seconds . '秒';
  98. } elseif ($seconds < 3600) {
  99. return round($seconds / 60, 1) . '分钟';
  100. } else {
  101. return round($seconds / 3600, 1) . '小时';
  102. }
  103. }
  104. return '-';
  105. });
  106. // 筛选器
  107. $grid->filter(function (Grid\Filter $filter) {
  108. $filter->equal('transfer_app_id', '应用')->select(
  109. \App\Module\Transfer\Models\TransferApp::pluck('title', 'id')
  110. );
  111. $filter->equal('type', '类型')->select([
  112. 1 => '转入',
  113. 2 => '转出',
  114. ]);
  115. $filter->equal('status', '状态')->select([
  116. 1 => '已创建',
  117. 20 => '处理中',
  118. 30 => '已回调',
  119. 100 => '已完成',
  120. -1 => '失败',
  121. ]);
  122. $filter->equal('user_id', '用户ID');
  123. $filter->like('out_order_id', '外部订单ID');
  124. $filter->like('out_user_id', '外部用户ID');
  125. $filter->between('amount', '内部金额');
  126. $filter->between('created_at', '创建时间')->datetime();
  127. });
  128. // 操作按钮
  129. $grid->actions(function (Grid\Displayers\Actions $actions) {
  130. $actions->disableDelete(); // 禁用删除
  131. $actions->disableEdit(); // 禁用编辑
  132. // 重新处理按钮
  133. if ($this->canRetry()) {
  134. $actions->append('<a href="javascript:void(0)" class="btn btn-xs btn-outline-warning retry-order" data-id="'.$this->id.'">重新处理</a>');
  135. }
  136. // 手动完成按钮(仅转出订单)
  137. if ($this->isTransferOut() && !$this->isFinalStatus()) {
  138. $actions->append('<a href="javascript:void(0)" class="btn btn-xs btn-outline-success manual-complete" data-id="'.$this->id.'">手动完成</a>');
  139. }
  140. });
  141. // 批量操作
  142. $grid->batchActions(function (Grid\Tools\BatchActions $batch) {
  143. $batch->add(new \App\Module\Transfer\AdminControllers\Tools\RetryOrderTool());
  144. });
  145. // 工具栏
  146. $grid->tools(function (Grid\Tools $tools) {
  147. $tools->append('<a href="javascript:void(0)" class="btn btn-sm btn-primary" onclick="showStats()">统计信息</a>');
  148. $tools->append(new \App\Module\Transfer\AdminControllers\Tools\ExportOrderTool());
  149. });
  150. // 默认排序
  151. $grid->model()->orderBy('created_at', 'desc');
  152. }
  153. /**
  154. * 配置Show详情
  155. */
  156. public static function show(Show $show): void
  157. {
  158. $show->field('id', 'ID');
  159. $show->divider();
  160. $show->field('transfer_app.title', '应用名称');
  161. $show->field('out_order_id', '外部订单ID');
  162. $show->field('out_user_id', '外部用户ID');
  163. $show->field('user_id', '内部用户ID');
  164. $show->field('type', '订单类型')->using([
  165. 1 => '转入',
  166. 2 => '转出',
  167. ]);
  168. $show->field('status', '订单状态')->using([
  169. 1 => '已创建',
  170. 20 => '处理中',
  171. 30 => '已回调',
  172. 100 => '已完成',
  173. -1 => '失败',
  174. ]);
  175. $show->divider();
  176. $show->field('out_amount', '外部金额')->as(function ($value) {
  177. return number_format($value, 10);
  178. });
  179. $show->field('amount', '内部金额')->as(function ($value) {
  180. return number_format($value, 10);
  181. });
  182. $show->field('exchange_rate', '使用汇率')->as(function ($value) {
  183. return number_format($value, 4);
  184. });
  185. // 手续费信息
  186. $show->field('fee_rate', '手续费率')->as(function ($value) {
  187. return number_format($value * 100, 2) . '%';
  188. });
  189. $show->field('fee_amount', '手续费金额')->as(function ($value) {
  190. return number_format($value, 4);
  191. });
  192. $show->field('actual_amount', '实际到账金额')->as(function ($value) {
  193. return number_format($value, 4);
  194. });
  195. $show->field('has_fee', '是否收取手续费')->as(function () {
  196. return $this->fee_amount > 0 ? '是' : '否';
  197. });
  198. $show->divider();
  199. $show->field('currency_id', '货币类型')->using([
  200. 1 => '金币',
  201. 2 => '钻石',
  202. 3 => '人民币',
  203. 4 => '美元',
  204. ]);
  205. $show->field('fund_id', '资金账户类型');
  206. $show->divider();
  207. $show->field('callback_data', '回调数据')->json();
  208. $show->divider();
  209. $show->field('error_message', '错误信息');
  210. $show->field('remark', '备注信息');
  211. $show->divider();
  212. $show->field('created_at', '创建时间')->as(function ($value) {
  213. if (!$value) return '-';
  214. if (is_string($value)) {
  215. // 如果是ISO格式字符串,转换为友好格式
  216. try {
  217. return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
  218. } catch (\Exception $e) {
  219. return $value; // 如果解析失败,返回原值
  220. }
  221. }
  222. return $value->format('Y-m-d H:i:s');
  223. });
  224. $show->field('processed_at', '处理时间')->as(function ($value) {
  225. if (!$value) return '-';
  226. if (is_string($value)) {
  227. try {
  228. return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
  229. } catch (\Exception $e) {
  230. return $value;
  231. }
  232. }
  233. return $value->format('Y-m-d H:i:s');
  234. });
  235. $show->field('callback_at', '回调时间')->as(function ($value) {
  236. if (!$value) return '-';
  237. if (is_string($value)) {
  238. try {
  239. return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
  240. } catch (\Exception $e) {
  241. return $value;
  242. }
  243. }
  244. return $value->format('Y-m-d H:i:s');
  245. });
  246. $show->field('completed_at', '完成时间')->as(function ($value) {
  247. if (!$value) return '-';
  248. if (is_string($value)) {
  249. try {
  250. return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
  251. } catch (\Exception $e) {
  252. return $value;
  253. }
  254. }
  255. return $value->format('Y-m-d H:i:s');
  256. });
  257. $show->field('updated_at', '更新时间')->as(function ($value) {
  258. if (!$value) return '-';
  259. if (is_string($value)) {
  260. try {
  261. return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s');
  262. } catch (\Exception $e) {
  263. return $value;
  264. }
  265. }
  266. return $value->format('Y-m-d H:i:s');
  267. });
  268. // 处理时长
  269. $show->field('processing_duration', '处理时长')->as(function () {
  270. if ($this->completed_at && $this->created_at) {
  271. return $this->completed_at->diffForHumans($this->created_at, true);
  272. }
  273. return '未完成';
  274. });
  275. }
  276. /**
  277. * 获取状态统计
  278. */
  279. public static function getStatusStats(): array
  280. {
  281. $stats = TransferOrder::selectRaw('status, COUNT(*) as count, SUM(amount) as amount')
  282. ->groupBy('status')
  283. ->get()
  284. ->keyBy('status');
  285. $result = [];
  286. foreach (TransferStatus::cases() as $status) {
  287. $stat = $stats->get($status->value);
  288. $result[] = [
  289. 'status' => $status->getDescription(),
  290. 'count' => $stat ? number_format($stat->count) : 0,
  291. 'amount' => $stat ? number_format($stat->amount, 2) : '0.00',
  292. 'color' => $status->getColor(),
  293. ];
  294. }
  295. return $result;
  296. }
  297. /**
  298. * 获取类型统计
  299. */
  300. public static function getTypeStats(): array
  301. {
  302. $stats = TransferOrder::selectRaw('type, COUNT(*) as count, SUM(amount) as amount')
  303. ->groupBy('type')
  304. ->get()
  305. ->keyBy('type');
  306. $result = [];
  307. foreach (TransferType::cases() as $type) {
  308. $stat = $stats->get($type->value);
  309. $result[] = [
  310. 'type' => $type->getDescription(),
  311. 'count' => $stat ? number_format($stat->count) : 0,
  312. 'amount' => $stat ? number_format($stat->amount, 2) : '0.00',
  313. 'color' => $type->getColor(),
  314. ];
  315. }
  316. return $result;
  317. }
  318. /**
  319. * 获取今日统计
  320. */
  321. public static function getTodayStats(): array
  322. {
  323. $today = now()->startOfDay();
  324. $total = TransferOrder::where('created_at', '>=', $today)->count();
  325. $completed = TransferOrder::where('created_at', '>=', $today)
  326. ->where('status', TransferStatus::COMPLETED)->count();
  327. $failed = TransferOrder::where('created_at', '>=', $today)
  328. ->where('status', TransferStatus::FAILED)->count();
  329. $amount = TransferOrder::where('created_at', '>=', $today)->sum('amount');
  330. $feeAmount = TransferOrder::where('created_at', '>=', $today)->sum('fee_amount');
  331. return [
  332. 'total' => number_format($total),
  333. 'completed' => number_format($completed),
  334. 'failed' => number_format($failed),
  335. 'success_rate' => $total > 0 ? number_format($completed / $total * 100, 1) . '%' : '0%',
  336. 'amount' => number_format($amount, 2),
  337. 'fee_amount' => number_format($feeAmount, 2),
  338. ];
  339. }
  340. /**
  341. * 获取手续费统计
  342. */
  343. public static function getFeeStats(): array
  344. {
  345. $stats = TransferOrder::selectRaw('
  346. COUNT(*) as total_orders,
  347. SUM(fee_amount) as total_fee,
  348. AVG(fee_rate) as avg_fee_rate,
  349. SUM(CASE WHEN type = 1 THEN fee_amount ELSE 0 END) as in_fee,
  350. SUM(CASE WHEN type = 2 THEN fee_amount ELSE 0 END) as out_fee,
  351. COUNT(CASE WHEN type = 1 THEN 1 END) as in_orders,
  352. COUNT(CASE WHEN type = 2 THEN 1 END) as out_orders
  353. ')
  354. ->where('status', TransferStatus::COMPLETED)
  355. ->where('fee_amount', '>', 0)
  356. ->first();
  357. return [
  358. 'total_orders' => number_format($stats->total_orders ?? 0),
  359. 'total_fee' => number_format($stats->total_fee ?? 0, 4),
  360. 'avg_fee_rate' => number_format(($stats->avg_fee_rate ?? 0) * 100, 2) . '%',
  361. 'in_fee' => number_format($stats->in_fee ?? 0, 4),
  362. 'out_fee' => number_format($stats->out_fee ?? 0, 4),
  363. 'in_orders' => number_format($stats->in_orders ?? 0),
  364. 'out_orders' => number_format($stats->out_orders ?? 0),
  365. ];
  366. }
  367. }