AgentQuery.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\service;
  4. use app\miniapi\http\Fields;
  5. use app\miniapi\exception\ApiException;
  6. use app\miniapi\http\Pagination;
  7. use plugin\saiadmin\app\model\water\WaterEnterprise;
  8. use plugin\saiadmin\app\model\water\WaterOrder;
  9. use plugin\saiadmin\app\model\water\WaterLedger;
  10. use plugin\saiadmin\app\model\water\WaterCommission;
  11. use app\miniapi\auth\TestPhoneIdentityProvider;
  12. use plugin\saiadmin\app\service\agent\WaterCommissionPolicy;
  13. final class AgentQuery
  14. {
  15. public function withdraw(int $agent): array
  16. {
  17. $this->access->resource('agent', $agent);
  18. throw new ApiException(50303, '佣金提现尚未开通,未创建提现申请', 503);
  19. }
  20. public function __construct(private readonly Access $access)
  21. {
  22. }
  23. private function enterpriseIds(array $agents)
  24. {
  25. return WaterEnterprise::query()->whereIn('agent_id', $agents)->select('id');
  26. }
  27. public function dashboard(int $agent): array
  28. {
  29. $agentRow = $this->access->resource('agent', $agent);
  30. $agentIds = $this->access->agentIds($agent);
  31. $start = date('Y-m-01 00:00:00');
  32. $now = date('Y-m-d H:i:s');
  33. $orders = WaterOrder::query()->whereIn('enterprise_id', $this->enterpriseIds($agentIds));
  34. $month = (int) (clone $orders)->whereBetween('created_at', [$start, $now])->sum('amount_cents');
  35. $previousStart = date('Y-m-01 00:00:00', strtotime('first day of last month'));
  36. $previousEnd = date('Y-m-d H:i:s', strtotime($start) - 1);
  37. $previous = (int) (clone $orders)->whereBetween('created_at', [$previousStart, $previousEnd])->sum('amount_cents');
  38. $trend = [];
  39. for ($i = 6;$i >= 0;$i--) {
  40. $day = date('Y-m-d', strtotime('-' . $i . ' days'));
  41. $trend[] = [
  42. 'day' => $day,
  43. 'amount_cents' => (int) (clone $orders)->whereBetween('created_at', [$day . ' 00:00:00', min($day . ' 23:59:59', $now)])->sum('amount_cents'),
  44. ];
  45. }
  46. $customers = WaterEnterprise::query()->whereIn('agent_id', $agentIds);
  47. $pendingCommission = (int) WaterCommission::query()->where('agent_id', $agent)
  48. ->where('status', 'pending')->sum('amount_cents');
  49. return [
  50. 'agent' => [
  51. 'id' => (int) $agentRow->id,
  52. 'name' => $agentRow->name,
  53. 'region' => $agentRow->region,
  54. ],
  55. 'summary' => [
  56. 'month_amount_cents' => $month,
  57. 'previous_month_amount_cents' => $previous,
  58. 'growth_percent' => $previous > 0 ? round(($month - $previous) * 100 / $previous, 2) : null,
  59. 'customer_count' => (clone $customers)->count(),
  60. 'new_customers' => (clone $customers)->whereBetween('created_at', [$start, $now])->count(),
  61. 'team_count' => count($agentIds) - 1,
  62. 'estimated_commission_cents' => $pendingCommission,
  63. 'withdrawable_cents' => 0,
  64. 'pending_commission_cents' => $pendingCommission,
  65. ],
  66. 'trend' => $trend,
  67. 'attribution' => 'current_enterprise_agent',
  68. 'capabilities' => [
  69. 'commission' => true,
  70. 'withdrawal' => false,
  71. ],
  72. ];
  73. }
  74. public function customers(int $agent, array $input): array
  75. {
  76. $agentIds = $this->access->agentIds($agent);
  77. $page = new Pagination($input);
  78. $keyword = Fields::text($input, 'keyword', 120, false);
  79. $filter = Fields::choice($input, 'filter', ['all', 'new', 'unpaid'], 'all');
  80. $asOf = date('Y-m-d H:i:s');
  81. $base = WaterEnterprise::query()->whereIn('agent_id', $agentIds);
  82. $paid = WaterOrder::query()->where('created_at', '<=', $asOf)->select('enterprise_id');
  83. $summary = [
  84. 'all' => (clone $base)->count(),
  85. 'new' => (clone $base)->whereBetween('created_at', [date('Y-m-01 00:00:00'), date('Y-m-d H:i:s')])->count(),
  86. 'unpaid' => (clone $base)->whereNotIn('id', $paid)->count(),
  87. ];
  88. if ($filter === 'new') {
  89. $base->whereBetween('created_at', [date('Y-m-01 00:00:00'), date('Y-m-d H:i:s')]);
  90. }
  91. if ($filter === 'unpaid') {
  92. $base->whereNotIn('id', $paid);
  93. }
  94. if ($keyword !== '') {
  95. $base->where(fn ($query) => $query->where('name', 'like', '%' . $keyword . '%')->orWhere('contact', 'like', '%' . $keyword . '%')->orWhere('phone', 'like', '%' . $keyword . '%'));
  96. }
  97. $total = (clone $base)->count();
  98. $paidOrders = static fn ($query) => $query->where('created_at', '<=', $asOf);
  99. $rows = $base
  100. ->withSum(['orders as purchased_amount' => $paidOrders], 'amount_cents')
  101. ->withMax(['orders as last_purchase_at' => $paidOrders], 'created_at')
  102. ->withExists(['orders as has_purchased' => $paidOrders])
  103. ->withCount('devices')
  104. ->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()->map(function ($enterprise) {
  105. return [
  106. 'id' => (int) $enterprise->id,
  107. 'name' => $enterprise->name,
  108. 'contact' => $enterprise->contact,
  109. 'phone' => WaterPresenter::phone($enterprise->phone),
  110. 'region' => $enterprise->region,
  111. 'created_at' => $enterprise->created_at,
  112. 'amount_cents' => (int) $enterprise->purchased_amount,
  113. 'last_purchase_at' => $enterprise->last_purchase_at,
  114. 'has_purchased' => (bool) $enterprise->has_purchased,
  115. 'device_count' => (int) $enterprise->devices_count,
  116. ];
  117. })->all();
  118. return $page->result($rows, $total) + ['summary' => $summary];
  119. }
  120. public function orders(int $agent, array $input): array
  121. {
  122. $agentIds = $this->access->agentIds($agent);
  123. $page = new Pagination($input);
  124. $query = WaterOrder::query()->whereIn('enterprise_id', $this->enterpriseIds($agentIds))->where('created_at', '<=', date('Y-m-d H:i:s'));
  125. if (isset($input['enterprise_id'])) {
  126. $query->where('enterprise_id', Fields::id($input, 'enterprise_id'));
  127. }
  128. $total = (clone $query)->count();
  129. return $page->result($query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)
  130. ->get([
  131. 'id',
  132. 'enterprise_id',
  133. 'package_name',
  134. 'buckets',
  135. 'amount_cents',
  136. 'created_at',
  137. ])->toArray(), $total);
  138. }
  139. public function commissions(int $agent, array $input = []): array
  140. {
  141. $this->access->resource('agent', $agent);
  142. $page = new Pagination($input);
  143. $status = Fields::choice($input, 'status', ['all', 'pending', 'settled', 'adjustment', 'withdrawn'], 'all');
  144. $source = Fields::choice($input, 'source', ['regular', 'demo'], 'regular');
  145. if ($source === 'demo' && $agent === 7 && TestPhoneIdentityProvider::enabled()) {
  146. $fixture = WaterLedger::query()->where('enterprise_id', 49)->where('kind', 'demo_seed')
  147. ->where('reference', 'TEST-MINI-COMMISSIONS-7-20261007')->first();
  148. if ($fixture) {
  149. $data = json_decode($fixture->note, true, 512, JSON_THROW_ON_ERROR);
  150. $query = WaterLedger::query()->where('enterprise_id', 49)->where('kind', 'demo_seed')
  151. ->where('reference', 'like', 'TEST-MINI-COMMISSIONS-7-20261007-ITEM-%');
  152. if ($status !== 'all') {
  153. $query->where('note->status', $status);
  154. }
  155. $total = (clone $query)->count();
  156. $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()
  157. ->map(fn ($record) => json_decode($record->note, true, 512, JSON_THROW_ON_ERROR))->all();
  158. return array_replace($data, $page->result($items, $total));
  159. }
  160. }
  161. if ($source === 'demo') {
  162. return [
  163. 'enabled' => false,
  164. 'reason' => '当前账号没有佣金演示数据。',
  165. 'withdrawable_cents' => null,
  166. 'pending_cents' => null,
  167. 'total_cents' => null,
  168. 'withdrawn_cents' => null,
  169. 'demo' => true,
  170. ] + $page->result([], 0);
  171. }
  172. $base = WaterCommission::query()->where('agent_id', $agent);
  173. $pending = (int) (clone $base)->where('status', 'pending')->sum('amount_cents');
  174. $totalAmount = (int) (clone $base)->sum('amount_cents');
  175. $list = clone $base;
  176. if ($status !== 'all') {
  177. $list->where('status', $status);
  178. }
  179. $total = (clone $list)->count();
  180. $items = $list->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)
  181. ->get()->map(static fn (WaterCommission $row) => [
  182. 'id' => (int) $row->id,
  183. 'customer_name' => $row->enterprise_name,
  184. 'title' => '购水订单佣金(' . intdiv((int) $row->rate_basis_points, 100) . '%)',
  185. 'status' => $row->status,
  186. 'status_text' => match ($row->status) {
  187. 'pending' => '待结算',
  188. 'settled' => '已结算',
  189. 'adjustment' => '已冲减',
  190. 'withdrawn' => '已提现',
  191. default => $row->status,
  192. },
  193. 'amount_cents' => (int) $row->amount_cents,
  194. 'created_at' => $row->created_at,
  195. ])->all();
  196. return [
  197. 'enabled' => true,
  198. 'reason' => WaterCommissionPolicy::message(),
  199. 'withdrawable_cents' => 0,
  200. 'pending_cents' => $pending,
  201. 'total_cents' => $totalAmount,
  202. 'withdrawn_cents' => 0,
  203. ] + $page->result($items, $total);
  204. }
  205. }