FeeService.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <?php
  2. namespace App\Module\Transfer\Services;
  3. use App\Module\Transfer\Models\TransferApp;
  4. use App\Module\Transfer\Models\TransferOrder;
  5. use App\Module\Transfer\Enums\TransferType;
  6. /**
  7. * 手续费服务类
  8. *
  9. * 负责处理Transfer模块的手续费计算、收取和统计功能
  10. */
  11. class FeeService
  12. {
  13. /**
  14. * 计算转入手续费
  15. *
  16. * @param TransferApp $app 划转应用
  17. * @param string $amount 转入金额
  18. * @return array ['fee_rate' => 手续费率, 'fee_amount' => 手续费金额, 'actual_amount' => 实际到账金额]
  19. */
  20. public static function calculateInFee(TransferApp $app, string $amount): array
  21. {
  22. return self::calculateFee(
  23. $amount,
  24. $app->fee_in_rate,
  25. $app->fee_in_min,
  26. $app->fee_in_max
  27. );
  28. }
  29. /**
  30. * 计算转出手续费
  31. *
  32. * @param TransferApp $app 划转应用
  33. * @param string $amount 转出金额
  34. * @return array ['fee_rate' => 手续费率, 'fee_amount' => 手续费金额, 'actual_amount' => 实际到账金额]
  35. */
  36. public static function calculateOutFee(TransferApp $app, string $amount): array
  37. {
  38. return self::calculateFee(
  39. $amount,
  40. $app->fee_out_rate,
  41. $app->fee_out_min,
  42. $app->fee_out_max
  43. );
  44. }
  45. /**
  46. * 计算手续费的通用方法
  47. *
  48. * @param string $amount 金额
  49. * @param float $feeRate 手续费率
  50. * @param float $minFee 最低手续费
  51. * @param float $maxFee 最高手续费
  52. * @return array
  53. */
  54. private static function calculateFee(string $amount, float $feeRate, float $minFee, float $maxFee): array
  55. {
  56. $amountDecimal = bcmul($amount, '1', 4); // 转换为4位小数
  57. // 按比例计算手续费
  58. $feeAmount = bcmul($amountDecimal, (string)$feeRate, 4);
  59. // 应用最低手续费限制
  60. if (bccomp($feeAmount, (string)$minFee, 4) < 0) {
  61. $feeAmount = bcmul((string)$minFee, '1', 4);
  62. }
  63. // 应用最高手续费限制(如果设置了)
  64. if ($maxFee > 0 && bccomp($feeAmount, (string)$maxFee, 4) > 0) {
  65. $feeAmount = bcmul((string)$maxFee, '1', 4);
  66. }
  67. // 计算实际到账金额
  68. $actualAmount = bcsub($amountDecimal, $feeAmount, 4);
  69. return [
  70. 'fee_rate' => $feeRate,
  71. 'fee_amount' => $feeAmount,
  72. 'actual_amount' => $actualAmount,
  73. ];
  74. }
  75. /**
  76. * 检查应用是否启用了手续费
  77. *
  78. * @param TransferApp $app 划转应用
  79. * @param string $type 类型:'in' 或 'out'
  80. * @return bool
  81. */
  82. public static function isFeeEnabled(TransferApp $app, string $type): bool
  83. {
  84. if ($type === 'in') {
  85. return $app->fee_in_rate > 0 || $app->fee_in_min > 0;
  86. } elseif ($type === 'out') {
  87. return $app->fee_out_rate > 0 || $app->fee_out_min > 0;
  88. }
  89. return false;
  90. }
  91. /**
  92. * 获取应用的手续费配置信息
  93. *
  94. * @param TransferApp $app 划转应用
  95. * @return array
  96. */
  97. public static function getFeeConfig(TransferApp $app): array
  98. {
  99. return [
  100. 'in' => [
  101. 'rate' => $app->fee_in_rate,
  102. 'min' => $app->fee_in_min,
  103. 'max' => $app->fee_in_max,
  104. 'enabled' => self::isFeeEnabled($app, 'in'),
  105. ],
  106. 'out' => [
  107. 'rate' => $app->fee_out_rate,
  108. 'min' => $app->fee_out_min,
  109. 'max' => $app->fee_out_max,
  110. 'enabled' => self::isFeeEnabled($app, 'out'),
  111. ],
  112. 'account_uid' => $app->fee_account_uid,
  113. ];
  114. }
  115. /**
  116. * 获取订单的手续费统计信息
  117. *
  118. * @param int $appId 应用ID(0表示所有应用)
  119. * @param string $startDate 开始日期
  120. * @param string $endDate 结束日期
  121. * @return array
  122. */
  123. public static function getFeeStatistics(int $appId = 0, string $startDate = '', string $endDate = ''): array
  124. {
  125. $query = TransferOrder::query()
  126. ->where('status', 100) // 只统计已完成的订单
  127. ->where('fee_amount', '>', 0); // 只统计有手续费的订单
  128. if ($appId > 0) {
  129. $query->where('transfer_app_id', $appId);
  130. }
  131. if ($startDate) {
  132. $query->whereDate('created_at', '>=', $startDate);
  133. }
  134. if ($endDate) {
  135. $query->whereDate('created_at', '<=', $endDate);
  136. }
  137. $orders = $query->get();
  138. $stats = [
  139. 'total_orders' => $orders->count(),
  140. 'total_fee' => $orders->sum('fee_amount'),
  141. 'avg_fee_rate' => $orders->avg('fee_rate'),
  142. 'in_orders' => $orders->where('type', TransferType::IN)->count(),
  143. 'in_fee' => $orders->where('type', TransferType::IN)->sum('fee_amount'),
  144. 'out_orders' => $orders->where('type', TransferType::OUT)->count(),
  145. 'out_fee' => $orders->where('type', TransferType::OUT)->sum('fee_amount'),
  146. 'date_range' => [
  147. 'start' => $startDate,
  148. 'end' => $endDate,
  149. ],
  150. ];
  151. return $stats;
  152. }
  153. /**
  154. * 获取应用的手续费收入统计
  155. *
  156. * @param int $appId 应用ID
  157. * @param int $days 统计天数(默认30天)
  158. * @return array
  159. */
  160. public static function getAppFeeIncome(int $appId, int $days = 30): array
  161. {
  162. $startDate = now()->subDays($days)->startOfDay();
  163. $orders = TransferOrder::where('transfer_app_id', $appId)
  164. ->where('status', 100)
  165. ->where('fee_amount', '>', 0)
  166. ->where('created_at', '>=', $startDate)
  167. ->get();
  168. $dailyStats = [];
  169. for ($i = 0; $i < $days; $i++) {
  170. $date = now()->subDays($i)->format('Y-m-d');
  171. $dayOrders = $orders->filter(function ($order) use ($date) {
  172. return $order->created_at->format('Y-m-d') === $date;
  173. });
  174. $dailyStats[$date] = [
  175. 'date' => $date,
  176. 'orders' => $dayOrders->count(),
  177. 'fee_amount' => $dayOrders->sum('fee_amount'),
  178. 'in_orders' => $dayOrders->where('type', TransferType::IN)->count(),
  179. 'in_fee' => $dayOrders->where('type', TransferType::IN)->sum('fee_amount'),
  180. 'out_orders' => $dayOrders->where('type', TransferType::OUT)->count(),
  181. 'out_fee' => $dayOrders->where('type', TransferType::OUT)->sum('fee_amount'),
  182. ];
  183. }
  184. return [
  185. 'app_id' => $appId,
  186. 'days' => $days,
  187. 'total_fee' => $orders->sum('fee_amount'),
  188. 'total_orders' => $orders->count(),
  189. 'avg_daily_fee' => $orders->sum('fee_amount') / $days,
  190. 'daily_stats' => array_reverse($dailyStats, true),
  191. ];
  192. }
  193. /**
  194. * 验证手续费配置的合法性
  195. *
  196. * @param array $config 手续费配置
  197. * @return array ['valid' => bool, 'errors' => array]
  198. */
  199. public static function validateFeeConfig(array $config): array
  200. {
  201. $errors = [];
  202. // 验证费率范围
  203. if (isset($config['fee_in_rate']) && ($config['fee_in_rate'] < 0 || $config['fee_in_rate'] > 1)) {
  204. $errors[] = '转入手续费率必须在0-1之间';
  205. }
  206. if (isset($config['fee_out_rate']) && ($config['fee_out_rate'] < 0 || $config['fee_out_rate'] > 1)) {
  207. $errors[] = '转出手续费率必须在0-1之间';
  208. }
  209. // 验证最低手续费
  210. if (isset($config['fee_in_min']) && $config['fee_in_min'] < 0) {
  211. $errors[] = '转入最低手续费不能为负数';
  212. }
  213. if (isset($config['fee_out_min']) && $config['fee_out_min'] < 0) {
  214. $errors[] = '转出最低手续费不能为负数';
  215. }
  216. // 验证最高手续费
  217. if (isset($config['fee_in_max']) && $config['fee_in_max'] < 0) {
  218. $errors[] = '转入最高手续费不能为负数';
  219. }
  220. if (isset($config['fee_out_max']) && $config['fee_out_max'] < 0) {
  221. $errors[] = '转出最高手续费不能为负数';
  222. }
  223. // 验证最低和最高手续费的关系
  224. if (isset($config['fee_in_min'], $config['fee_in_max']) &&
  225. $config['fee_in_max'] > 0 &&
  226. $config['fee_in_min'] > $config['fee_in_max']) {
  227. $errors[] = '转入最低手续费不能大于最高手续费';
  228. }
  229. if (isset($config['fee_out_min'], $config['fee_out_max']) &&
  230. $config['fee_out_max'] > 0 &&
  231. $config['fee_out_min'] > $config['fee_out_max']) {
  232. $errors[] = '转出最低手续费不能大于最高手续费';
  233. }
  234. return [
  235. 'valid' => empty($errors),
  236. 'errors' => $errors,
  237. ];
  238. }
  239. /**
  240. * 格式化手续费金额显示
  241. *
  242. * @param string|float $amount 手续费金额
  243. * @param int $decimals 小数位数
  244. * @return string
  245. */
  246. public static function formatFeeAmount($amount, int $decimals = 4): string
  247. {
  248. return number_format((float)$amount, $decimals);
  249. }
  250. /**
  251. * 格式化手续费率显示
  252. *
  253. * @param float $rate 手续费率
  254. * @param int $decimals 小数位数
  255. * @return string
  256. */
  257. public static function formatFeeRate(float $rate, int $decimals = 2): string
  258. {
  259. return number_format($rate * 100, $decimals) . '%';
  260. }
  261. }