FeeService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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 FeeCalculatedEvent ['fee_rate' => 手续费率, 'fee_amount' => 手续费金额, 'actual_amount' => 实际到账金额]
  23. */
  24. public static function calculateInFee(TransferApp $app, string $amount, array $context = []): FeeCalculatedEvent
  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 = []): FeeCalculatedEvent
  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 FeeCalculatedEvent
  48. */
  49. private static function calculateFeeWithEvents(TransferApp $app, string $amount, string $type, array $context = []): FeeCalculatedEvent
  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. feeAmount: $baseResult['fee_amount'],
  69. context: $context
  70. );
  71. // 触发事件,允许其他模块修改手续费
  72. Event::dispatch($event);
  73. // 触发计算完成事件
  74. $calculatedEvent = FeeCalculatedEvent::fromCalculatingEvent($event);
  75. return $calculatedEvent;
  76. }
  77. /**
  78. * 计算手续费的通用方法
  79. *
  80. * @param string $amount 金额
  81. * @param float $feeRate 手续费率
  82. * @param float $minFee 最低手续费
  83. * @param float $maxFee 最高手续费
  84. * @return array
  85. */
  86. private static function calculateFee(string $amount, float $feeRate, float $minFee, float $maxFee): array
  87. {
  88. $amountDecimal = bcmul($amount, '1', 4); // 转换为4位小数
  89. // 按比例计算手续费
  90. $feeAmount = bcmul($amountDecimal, (string)$feeRate, 4);
  91. // 应用最低手续费限制
  92. if (bccomp($feeAmount, (string)$minFee, 4) < 0) {
  93. $feeAmount = bcmul((string)$minFee, '1', 4);
  94. }
  95. // 应用最高手续费限制(如果设置了)
  96. if ($maxFee > 0 && bccomp($feeAmount, (string)$maxFee, 4) > 0) {
  97. $feeAmount = bcmul((string)$maxFee, '1', 4);
  98. }
  99. // 计算实际到账金额
  100. $actualAmount = bcsub($amountDecimal, $feeAmount, 4);
  101. return [
  102. 'fee_rate' => $feeRate,
  103. 'fee_amount' => $feeAmount,
  104. 'actual_amount' => $actualAmount,
  105. ];
  106. }
  107. /**
  108. * 检查应用是否启用了手续费
  109. *
  110. * @param TransferApp $app 划转应用
  111. * @param string $type 类型:'in' 或 'out'
  112. * @return bool
  113. */
  114. public static function isFeeEnabled(TransferApp $app, string $type): bool
  115. {
  116. if ($type === 'in') {
  117. return $app->fee_in_rate > 0 || $app->fee_in_min > 0;
  118. } elseif ($type === 'out') {
  119. return $app->fee_out_rate > 0 || $app->fee_out_min > 0;
  120. }
  121. return false;
  122. }
  123. /**
  124. * 获取应用的手续费配置信息
  125. *
  126. * @param TransferApp $app 划转应用
  127. * @return array
  128. */
  129. public static function getFeeConfig(TransferApp $app): array
  130. {
  131. return [
  132. 'in' => [
  133. 'rate' => $app->fee_in_rate,
  134. 'min' => $app->fee_in_min,
  135. 'max' => $app->fee_in_max,
  136. 'enabled' => self::isFeeEnabled($app, 'in'),
  137. ],
  138. 'out' => [
  139. 'rate' => $app->fee_out_rate,
  140. 'min' => $app->fee_out_min,
  141. 'max' => $app->fee_out_max,
  142. 'enabled' => self::isFeeEnabled($app, 'out'),
  143. ],
  144. 'account_uid' => $app->fee_account_uid,
  145. ];
  146. }
  147. /**
  148. * 获取订单的手续费统计信息
  149. *
  150. * @param int $appId 应用ID(0表示所有应用)
  151. * @param string $startDate 开始日期
  152. * @param string $endDate 结束日期
  153. * @return array
  154. */
  155. public static function getFeeStatistics(int $appId = 0, string $startDate = '', string $endDate = ''): array
  156. {
  157. $query = TransferOrder::query()
  158. ->where('status', 100) // 只统计已完成的订单
  159. ->where('fee_amount', '>', 0); // 只统计有手续费的订单
  160. if ($appId > 0) {
  161. $query->where('transfer_app_id', $appId);
  162. }
  163. if ($startDate) {
  164. $query->whereDate('created_at', '>=', $startDate);
  165. }
  166. if ($endDate) {
  167. $query->whereDate('created_at', '<=', $endDate);
  168. }
  169. $orders = $query->get();
  170. $stats = [
  171. 'total_orders' => $orders->count(),
  172. 'total_fee' => $orders->sum('fee_amount'),
  173. 'avg_fee_rate' => $orders->avg('fee_rate'),
  174. 'in_orders' => $orders->where('type', TransferType::IN)->count(),
  175. 'in_fee' => $orders->where('type', TransferType::IN)->sum('fee_amount'),
  176. 'out_orders' => $orders->where('type', TransferType::OUT)->count(),
  177. 'out_fee' => $orders->where('type', TransferType::OUT)->sum('fee_amount'),
  178. 'date_range' => [
  179. 'start' => $startDate,
  180. 'end' => $endDate,
  181. ],
  182. ];
  183. return $stats;
  184. }
  185. /**
  186. * 获取应用的手续费收入统计
  187. *
  188. * @param int $appId 应用ID
  189. * @param int $days 统计天数(默认30天)
  190. * @return array
  191. */
  192. public static function getAppFeeIncome(int $appId, int $days = 30): array
  193. {
  194. $startDate = now()->subDays($days)->startOfDay();
  195. $orders = TransferOrder::where('transfer_app_id', $appId)
  196. ->where('status', 100)
  197. ->where('fee_amount', '>', 0)
  198. ->where('created_at', '>=', $startDate)
  199. ->get();
  200. $dailyStats = [];
  201. for ($i = 0; $i < $days; $i++) {
  202. $date = now()->subDays($i)->format('Y-m-d');
  203. $dayOrders = $orders->filter(function ($order) use ($date) {
  204. return $order->created_at->format('Y-m-d') === $date;
  205. });
  206. $dailyStats[$date] = [
  207. 'date' => $date,
  208. 'orders' => $dayOrders->count(),
  209. 'fee_amount' => $dayOrders->sum('fee_amount'),
  210. 'in_orders' => $dayOrders->where('type', TransferType::IN)->count(),
  211. 'in_fee' => $dayOrders->where('type', TransferType::IN)->sum('fee_amount'),
  212. 'out_orders' => $dayOrders->where('type', TransferType::OUT)->count(),
  213. 'out_fee' => $dayOrders->where('type', TransferType::OUT)->sum('fee_amount'),
  214. ];
  215. }
  216. return [
  217. 'app_id' => $appId,
  218. 'days' => $days,
  219. 'total_fee' => $orders->sum('fee_amount'),
  220. 'total_orders' => $orders->count(),
  221. 'avg_daily_fee' => $orders->sum('fee_amount') / $days,
  222. 'daily_stats' => array_reverse($dailyStats, true),
  223. ];
  224. }
  225. /**
  226. * 验证手续费配置的合法性
  227. *
  228. * @param array $config 手续费配置
  229. * @return array ['valid' => bool, 'errors' => array]
  230. */
  231. public static function validateFeeConfig(array $config): array
  232. {
  233. $errors = [];
  234. // 验证费率范围
  235. if (isset($config['fee_in_rate']) && ($config['fee_in_rate'] < 0 || $config['fee_in_rate'] > 1)) {
  236. $errors[] = '转入手续费率必须在0-1之间';
  237. }
  238. if (isset($config['fee_out_rate']) && ($config['fee_out_rate'] < 0 || $config['fee_out_rate'] > 1)) {
  239. $errors[] = '转出手续费率必须在0-1之间';
  240. }
  241. // 验证最低手续费
  242. if (isset($config['fee_in_min']) && $config['fee_in_min'] < 0) {
  243. $errors[] = '转入最低手续费不能为负数';
  244. }
  245. if (isset($config['fee_out_min']) && $config['fee_out_min'] < 0) {
  246. $errors[] = '转出最低手续费不能为负数';
  247. }
  248. // 验证最高手续费
  249. if (isset($config['fee_in_max']) && $config['fee_in_max'] < 0) {
  250. $errors[] = '转入最高手续费不能为负数';
  251. }
  252. if (isset($config['fee_out_max']) && $config['fee_out_max'] < 0) {
  253. $errors[] = '转出最高手续费不能为负数';
  254. }
  255. // 验证最低和最高手续费的关系
  256. if (isset($config['fee_in_min'], $config['fee_in_max']) &&
  257. $config['fee_in_max'] > 0 &&
  258. $config['fee_in_min'] > $config['fee_in_max']) {
  259. $errors[] = '转入最低手续费不能大于最高手续费';
  260. }
  261. if (isset($config['fee_out_min'], $config['fee_out_max']) &&
  262. $config['fee_out_max'] > 0 &&
  263. $config['fee_out_min'] > $config['fee_out_max']) {
  264. $errors[] = '转出最低手续费不能大于最高手续费';
  265. }
  266. return [
  267. 'valid' => empty($errors),
  268. 'errors' => $errors,
  269. ];
  270. }
  271. /**
  272. * 格式化手续费金额显示
  273. *
  274. * @param string|float $amount 手续费金额
  275. * @param int $decimals 小数位数
  276. * @return string
  277. */
  278. public static function formatFeeAmount($amount, int $decimals = 4): string
  279. {
  280. return number_format((float)$amount, $decimals);
  281. }
  282. /**
  283. * 格式化手续费率显示
  284. *
  285. * @param float $rate 手续费率
  286. * @param int $decimals 小数位数
  287. * @return string
  288. */
  289. public static function formatFeeRate(float $rate, int $decimals = 2): string
  290. {
  291. return number_format($rate * 100, $decimals) . '%';
  292. }
  293. /**
  294. * 转移手续费到指定用户
  295. *
  296. * 将手续费从指定应用的手续费账户转移到目标用户账户
  297. *
  298. * @param int $appId 出钱的应用ID(从该应用的手续费账户扣除)
  299. * @param int $targetUserId 目标用户ID(收钱的)
  300. * @param float $amount 金额(钱数)
  301. * @param int $orderId 订单ID(划转依据)
  302. * @return bool|string 成功返回true,失败返回错误信息
  303. */
  304. public static function transfer(int $appId, int $targetUserId, float $amount, int $orderId)
  305. {
  306. // 检查金额是否有效
  307. if ($amount <= 0) {
  308. return '转移金额必须大于0';
  309. }
  310. // 获取应用配置
  311. $app = TransferApp::find($appId);
  312. if (!$app) {
  313. return '应用不存在';
  314. }
  315. // 获取手续费收取账户UID
  316. $feeAccountUid = $app->getFeeAccountUid();
  317. // 检查目标用户是否存在
  318. $targetUser = \App\Module\User\Models\User::find($targetUserId);
  319. if (!$targetUser) {
  320. return '目标用户不存在';
  321. }
  322. // 创建资金服务实例(从手续费账户转出)
  323. $fundService = new \App\Module\Fund\Services\FundService($feeAccountUid, $app->fund_id);
  324. // 构建备注信息
  325. $remark = "手续费转移-{$app->title}-订单{$orderId}";
  326. try {
  327. // 使用trade方法进行资金转移
  328. $result = $fundService->trade(
  329. $targetUserId,
  330. $amount,
  331. 'fee_transfer',
  332. $orderId,
  333. $remark
  334. );
  335. if (is_string($result)) {
  336. // 转移失败
  337. \UCore\Helper\Logger::error("手续费转移失败", [
  338. 'app_id' => $appId,
  339. 'fee_account_uid' => $feeAccountUid,
  340. 'target_user_id' => $targetUserId,
  341. 'amount' => $amount,
  342. 'order_id' => $orderId,
  343. 'error' => $result
  344. ]);
  345. return $result;
  346. }
  347. // 转移成功,记录日志
  348. \UCore\Helper\Logger::info("手续费转移成功", [
  349. 'app_id' => $appId,
  350. 'app_title' => $app->title,
  351. 'fee_account_uid' => $feeAccountUid,
  352. 'target_user_id' => $targetUserId,
  353. 'target_username' => $targetUser->username,
  354. 'amount' => $amount,
  355. 'order_id' => $orderId,
  356. 'fund_id' => $app->fund_id
  357. ]);
  358. return true;
  359. } catch (\Exception $e) {
  360. // 异常处理
  361. \UCore\Helper\Logger::error("手续费转移异常", [
  362. 'app_id' => $appId,
  363. 'target_user_id' => $targetUserId,
  364. 'amount' => $amount,
  365. 'order_id' => $orderId,
  366. 'exception' => $e->getMessage(),
  367. 'trace' => $e->getTraceAsString()
  368. ]);
  369. return '手续费转移异常: ' . $e->getMessage();
  370. }
  371. }
  372. }