FeeService.php 11 KB

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