AgentLogic.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. <?php
  2. namespace plugin\saiadmin\app\logic\agent;
  3. use plugin\saiadmin\app\cache\UserInfoCache;
  4. use plugin\saiadmin\app\cache\UserAuthCache;
  5. use plugin\saiadmin\app\cache\UserMenuCache;
  6. use plugin\saiadmin\app\model\system\SystemDept;
  7. use plugin\saiadmin\app\model\system\SystemUser;
  8. use plugin\saiadmin\basic\think\BaseLogic;
  9. use plugin\saiadmin\exception\ApiException;
  10. use plugin\saiadmin\service\OpenSpoutWriter;
  11. use support\Response;
  12. use support\think\Db;
  13. /**
  14. * 代理列表与设备管理权分配。
  15. *
  16. * 旧系统 DeviceManagerRecord 的 last_aid/now_aid/flow_type 映射到
  17. * sa_device_manager_record,并以 sa_device.manage_agent_id 作为当前管理关系。
  18. */
  19. class AgentLogic extends BaseLogic
  20. {
  21. private const PRODUCT_CODES = [
  22. 'vehicle',
  23. 'ktv',
  24. 'bluetooth-cable',
  25. 'power-bank',
  26. 'password-cable',
  27. 'password-bluetooth-cable',
  28. ];
  29. private const PERMISSION_CODES = [
  30. 'self_operated',
  31. 'view_order',
  32. 'self_merchant',
  33. 'finish_order',
  34. 'refund_order',
  35. 'dd',
  36. ];
  37. private const TYPE_NAMES = [
  38. 1 => '充电底座',
  39. 100 => 'K霸宝',
  40. 101 => '充电宝',
  41. 102 => 'K霸宝 Blue',
  42. 103 => '密码线充',
  43. 104 => '蓝牙线充',
  44. 105 => '密码蓝牙线充',
  45. 107 => '车载充电器',
  46. ];
  47. public function __construct()
  48. {
  49. $this->model = new SystemUser();
  50. }
  51. public function indexList(array $where): array
  52. {
  53. $rows = $this->agentRows($where);
  54. $page = max(1, (int) request()->input('page', 1));
  55. $limit = min(100, max(1, (int) request()->input('limit', 10)));
  56. return [
  57. 'total' => count($rows),
  58. 'per_page' => $limit,
  59. 'current_page' => $page,
  60. 'last_page' => max(1, (int) ceil(count($rows) / $limit)),
  61. 'data' => array_slice($rows, ($page - 1) * $limit, $limit),
  62. ];
  63. }
  64. public function export(array $where): Response
  65. {
  66. $data = [];
  67. foreach ($this->agentRows($where) as $row) {
  68. $data[] = [
  69. 'id' => $row['id'],
  70. 'name' => $this->excelSafe($row['name']),
  71. 'phone' => $this->excelSafe($row['phone']),
  72. 'agent_type' => $row['agent_type'],
  73. 'region' => $this->excelSafe(implode(' ', array_filter([$row['province'], $row['city'], $row['district']]))),
  74. 'device_count' => $row['device_count'],
  75. 'subordinate_count' => $row['subordinate_count'],
  76. 'income' => (float) $row['income'],
  77. 'withdrawable' => (float) $row['withdrawable'],
  78. 'profit_rate' => $row['profit_rate'],
  79. 'status' => $row['status_name'],
  80. 'created_at' => $row['created_at'],
  81. ];
  82. }
  83. $fileName = '代理数据_' . date('Ymd_His') . '.xlsx';
  84. $writer = new OpenSpoutWriter($fileName);
  85. $writer->setWidth([10, 18, 18, 16, 32, 14, 14, 14, 14, 14, 12, 22]);
  86. $writer->setHeader([
  87. '代理ID', '代理姓名', '手机号码', '代理类型', '运营区域', '设备数',
  88. '下级总数', '总收益', '可提现金额', '分润比例', '状态', '创建时间',
  89. ]);
  90. $writer->setData($data);
  91. return response()->download($writer->returnFile(), urlencode($fileName));
  92. }
  93. public function readAgent(int $id): array
  94. {
  95. $agent = $this->findAccessibleAgent($id, false);
  96. $deptRows = SystemDept::whereNull('delete_time')->field('id,name,level')->select()->toArray();
  97. $deptMap = [];
  98. foreach ($deptRows as $dept) {
  99. $deptMap[(int) $dept['id']] = $dept;
  100. }
  101. $dept = $deptMap[(int) $agent['dept_id']] ?? [];
  102. $region = $this->regionNames($dept, $deptMap);
  103. $currentDepth = $this->deptDepth($this->adminInfo['deptList'] ?? []);
  104. $type = $this->agentType($this->deptDepth($dept) - $currentDepth);
  105. $profile = Db::table('sa_agent_profile')->where('agent_id', $id)->find() ?: [];
  106. $products = $this->decodeList($profile['product_codes'] ?? null, self::PRODUCT_CODES);
  107. if ($products === []) {
  108. $products = self::PRODUCT_CODES;
  109. }
  110. $profitRates = $this->decodeMap($profile['profit_rates'] ?? null);
  111. foreach ($products as $product) {
  112. $profitRates[$product] = (float) ($profitRates[$product] ?? 0);
  113. }
  114. return [
  115. 'id' => (int) $agent['id'],
  116. 'opening_type' => $type['code'],
  117. 'opening_type_name' => $type['name'],
  118. 'business_mode' => in_array($profile['business_mode'] ?? '', ['agent', 'employee'], true)
  119. ? (string) $profile['business_mode'] : 'agent',
  120. 'name' => (string) (($agent['realname'] ?? '') ?: $agent['username']),
  121. 'phone' => (string) ($agent['phone'] ?? ''),
  122. 'product_codes' => $products,
  123. 'profit_rates' => $profitRates,
  124. 'permissions' => $this->decodeList($profile['permissions'] ?? null, self::PERMISSION_CODES),
  125. 'province' => (string) (($profile['province'] ?? '') ?: ($region[0] === '--' ? '' : $region[0])),
  126. 'city' => (string) (($profile['city'] ?? '') ?: ($region[1] === '--' ? '' : $region[1])),
  127. 'district' => (string) (($profile['district'] ?? '') ?: ($region[2] === '--' ? '' : $region[2])),
  128. 'company_address' => (string) ($profile['company_address'] ?? ''),
  129. 'status' => (int) ($agent['status'] ?? 1),
  130. ];
  131. }
  132. public function updateAgent(int $id, array $data): bool
  133. {
  134. $agent = $this->findAccessibleAgent($id, false);
  135. $name = trim((string) ($data['name'] ?? ''));
  136. $phone = trim((string) ($data['phone'] ?? ''));
  137. if ($name === '') {
  138. throw new ApiException('请输入代理姓名');
  139. }
  140. if (!preg_match('/^1\d{10}$/', $phone)) {
  141. throw new ApiException('请输入正确的手机号码');
  142. }
  143. if ($phone !== (string) ($agent['phone'] ?? '')) {
  144. $exists = SystemUser::where('phone', $phone)->where('id', '<>', $id)->whereNull('delete_time')->value('id');
  145. if ($exists) {
  146. throw new ApiException('该手机号码已被其他账号使用');
  147. }
  148. }
  149. $businessMode = (string) ($data['business_mode'] ?? '');
  150. if (!in_array($businessMode, ['agent', 'employee'], true)) {
  151. throw new ApiException('运营模式参数错误');
  152. }
  153. $products = $this->normalizeList($data['product_codes'] ?? [], self::PRODUCT_CODES);
  154. if ($products === []) {
  155. throw new ApiException('请至少选择一种运营产品');
  156. }
  157. $permissions = $this->normalizeList($data['permissions'] ?? [], self::PERMISSION_CODES);
  158. $rawRates = is_array($data['profit_rates'] ?? null) ? $data['profit_rates'] : [];
  159. $profitRates = [];
  160. foreach ($products as $product) {
  161. $rate = $rawRates[$product] ?? null;
  162. if (!is_numeric($rate) || (float) $rate < 0 || (float) $rate > 100) {
  163. throw new ApiException('分润比例必须在 0 到 100 之间');
  164. }
  165. $profitRates[$product] = round((float) $rate, 2);
  166. }
  167. $operatorId = $this->currentAdminId();
  168. $now = date('Y-m-d H:i:s');
  169. $profileData = [
  170. 'business_mode' => $businessMode,
  171. 'product_codes' => json_encode($products, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  172. 'profit_rates' => json_encode($profitRates, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  173. 'permissions' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  174. 'province' => trim((string) ($data['province'] ?? '')),
  175. 'city' => trim((string) ($data['city'] ?? '')),
  176. 'district' => trim((string) ($data['district'] ?? '')),
  177. 'company_address' => trim((string) ($data['company_address'] ?? '')),
  178. 'updated_by' => $operatorId,
  179. 'update_time' => $now,
  180. ];
  181. if ($profileData['province'] === '' || $profileData['city'] === '' || $profileData['district'] === '') {
  182. throw new ApiException('请选择完整的运营区域');
  183. }
  184. $this->transaction(function () use ($id, $name, $phone, $operatorId, $now, $profileData) {
  185. Db::table('sa_system_user')->where('id', $id)->update([
  186. 'realname' => $name,
  187. 'phone' => $phone,
  188. 'updated_by' => $operatorId,
  189. 'update_time' => $now,
  190. ]);
  191. $profile = Db::table('sa_agent_profile')->where('agent_id', $id)->lock(true)->find();
  192. if ($profile) {
  193. Db::table('sa_agent_profile')->where('agent_id', $id)->update($profileData);
  194. } else {
  195. Db::table('sa_agent_profile')->insert(array_merge($profileData, [
  196. 'agent_id' => $id,
  197. 'created_by' => $operatorId,
  198. 'create_time' => $now,
  199. ]));
  200. }
  201. });
  202. UserInfoCache::clearUserInfo($id);
  203. return true;
  204. }
  205. /**
  206. * 禁用无关联资产和下级账号的代理。
  207. *
  208. * 旧系统的“删除代理”只停用账号,不物理删除历史记录;这里保持该语义,
  209. * 同时阻止遗留设备、商户或下级账号的孤立关联。
  210. */
  211. public function destroyAgent(int $id): void
  212. {
  213. $agent = $this->findAccessibleAgent($id, false);
  214. if ((int) $agent['status'] !== 1) {
  215. throw new ApiException('代理已删除或已停用');
  216. }
  217. $managedDeviceCount = (int) Db::table('sa_device')->whereNull('delete_time')
  218. ->where('manage_agent_id', $id)->count();
  219. $ownedDeviceCount = (int) Db::table('sa_device')->whereNull('delete_time')
  220. ->where('belong_agent_id', $id)->count();
  221. if ($managedDeviceCount > 0 || $ownedDeviceCount > 0) {
  222. throw new ApiException('该代理名下仍有设备,请先回收或转移设备后再删除');
  223. }
  224. $merchantCount = (int) Db::table('sa_merchant')->whereNull('delete_time')
  225. ->where('agent_id', $id)->count();
  226. if ($merchantCount > 0) {
  227. throw new ApiException('该代理名下仍有商户,请先转移商户后再删除');
  228. }
  229. $dept = SystemDept::where('id', (int) $agent['dept_id'])->field('id,level')->find();
  230. if ($dept) {
  231. $prefix = (string) $dept->level . (int) $dept->id . ',';
  232. $descendantDeptIds = SystemDept::whereLike('level', $prefix . '%')->column('id');
  233. if ($descendantDeptIds !== []) {
  234. $subordinateCount = (int) SystemUser::whereIn('dept_id', $descendantDeptIds)
  235. ->whereNull('delete_time')->count();
  236. if ($subordinateCount > 0) {
  237. throw new ApiException('该代理仍有下级代理,请先转移或删除下级代理后再删除');
  238. }
  239. }
  240. }
  241. $updated = Db::table('sa_system_user')->where('id', $id)->where('status', 1)
  242. ->whereNull('delete_time')->update([
  243. 'status' => 2,
  244. 'updated_by' => $this->currentAdminId(),
  245. 'update_time' => date('Y-m-d H:i:s'),
  246. ]);
  247. if ($updated !== 1) {
  248. throw new ApiException('代理删除失败,请刷新后重试');
  249. }
  250. UserInfoCache::clearUserInfo($id);
  251. UserAuthCache::clearUserAuth($id);
  252. UserMenuCache::clearUserMenu($id);
  253. }
  254. public function readContract(int $id): array
  255. {
  256. $agent = $this->findAccessibleAgent($id, false);
  257. $contract = Db::table('sa_agent_contract')->where('agent_id', $id)->find() ?: [];
  258. return [
  259. 'id' => $id,
  260. 'agent_name' => (string) (($agent['realname'] ?? '') ?: $agent['username']),
  261. 'phone' => (string) ($agent['phone'] ?? ''),
  262. 'contract_urls' => $this->decodeAttachmentUrls($contract['contract_urls'] ?? null),
  263. 'identity_urls' => $this->decodeAttachmentUrls($contract['identity_urls'] ?? null),
  264. 'extra_urls' => $this->decodeAttachmentUrls($contract['extra_urls'] ?? null),
  265. ];
  266. }
  267. public function updateContract(int $id, array $data): array
  268. {
  269. $this->findAccessibleAgent($id, false);
  270. $contractUrls = $this->normalizeAttachmentUrls($data['contract_urls'] ?? []);
  271. $identityUrls = $this->normalizeAttachmentUrls($data['identity_urls'] ?? []);
  272. $extraUrls = $this->normalizeAttachmentUrls($data['extra_urls'] ?? []);
  273. $operatorId = $this->currentAdminId();
  274. $now = date('Y-m-d H:i:s');
  275. $contractData = [
  276. 'contract_urls' => json_encode($contractUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  277. 'identity_urls' => json_encode($identityUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  278. 'extra_urls' => json_encode($extraUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  279. 'updated_by' => $operatorId,
  280. 'update_time' => $now,
  281. ];
  282. $this->transaction(function () use ($id, $operatorId, $now, $contractData) {
  283. $contract = Db::table('sa_agent_contract')->where('agent_id', $id)->lock(true)->find();
  284. if ($contract) {
  285. Db::table('sa_agent_contract')->where('agent_id', $id)->update($contractData);
  286. return;
  287. }
  288. Db::table('sa_agent_contract')->insert(array_merge($contractData, [
  289. 'agent_id' => $id,
  290. 'created_by' => $operatorId,
  291. 'create_time' => $now,
  292. ]));
  293. });
  294. return $this->readContract($id);
  295. }
  296. public function candidateDevices(array $where): array
  297. {
  298. $page = max(1, (int) request()->input('page', 1));
  299. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  300. $query = Db::table('sa_device')->alias('d')
  301. ->leftJoin('sa_merchant_device_binding b', 'b.device_id = d.id')
  302. ->whereNull('d.delete_time')
  303. ->whereNull('b.device_id')
  304. ->where('d.manage_agent_id', $this->currentAdminId())
  305. ->field('d.id,d.goods_name,d.goods_type,d.goods_sn,d.device_code,d.model,d.device_status,d.create_time');
  306. $category = trim((string) ($where['category'] ?? 'all'));
  307. if ($category === 'vehicle') {
  308. $query->where('d.goods_type', 107);
  309. } elseif ($category === 'power-bank') {
  310. $query->whereIn('d.goods_type', [1, 100, 101, 102, 103, 104, 105]);
  311. }
  312. if (($where['goods_type'] ?? '') !== '') {
  313. $query->where('d.goods_type', (int) $where['goods_type']);
  314. }
  315. $sn = trim((string) ($where['device_sn'] ?? ''));
  316. if ($sn !== '') {
  317. $query->whereLike('d.goods_sn', '%' . $sn . '%');
  318. }
  319. $code = trim((string) ($where['qr_code'] ?? ''));
  320. if ($code !== '') {
  321. $query->whereLike('d.device_code', '%' . $code . '%');
  322. }
  323. $result = $query->order('d.id', 'desc')->paginate([
  324. 'page' => $page,
  325. 'list_rows' => $limit,
  326. ])->toArray();
  327. $result['data'] = array_map(function (array $row) {
  328. $type = (int) $row['goods_type'];
  329. return [
  330. 'id' => (int) $row['id'],
  331. 'name' => (string) $row['goods_name'],
  332. 'goods_type' => $type,
  333. 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备',
  334. 'device_sn' => (string) $row['goods_sn'],
  335. 'qr_code' => (string) (($row['device_code'] ?? '') ?: $row['goods_sn']),
  336. 'model' => (string) (($row['model'] ?? '') ?: '--'),
  337. 'online' => (int) ($row['device_status'] ?? 0) === 1,
  338. 'created_at' => (string) ($row['create_time'] ?? ''),
  339. ];
  340. }, $result['data'] ?? []);
  341. return $result;
  342. }
  343. public function assignDevices(int $agentId, array $deviceSns): array
  344. {
  345. $target = $this->findAccessibleAgent($agentId);
  346. $operatorId = $this->currentAdminId();
  347. if ((int) $target['id'] === $operatorId) {
  348. throw new ApiException('不能给自己分配设备');
  349. }
  350. return $this->moveDevices($operatorId, $agentId, $deviceSns, 1, '上级分配设备');
  351. }
  352. public function recoverDevices(int $agentId, array $deviceSns): array
  353. {
  354. $this->findAccessibleAgent($agentId);
  355. return $this->moveDevices($agentId, $this->currentAdminId(), $deviceSns, 2, '上级回收设备');
  356. }
  357. public function assignmentRecords(int $agentId, array $where): array
  358. {
  359. $this->findAccessibleAgent($agentId);
  360. $currentId = $this->currentAdminId();
  361. $page = max(1, (int) request()->input('page', 1));
  362. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  363. $query = Db::table('sa_device_manager_record')->alias('r')
  364. ->leftJoin('sa_system_user last_user', 'last_user.id = r.last_agent_id')
  365. ->leftJoin('sa_system_user now_user', 'now_user.id = r.now_agent_id')
  366. ->whereIn('r.last_agent_id', [$currentId, $agentId])
  367. ->whereIn('r.now_agent_id', [$currentId, $agentId])
  368. ->field(implode(',', [
  369. 'r.id', 'r.device_id', 'r.device_sn', 'r.goods_type', 'r.last_agent_id',
  370. 'r.now_agent_id', 'r.action_type', 'r.reason', 'r.operator_id', 'r.action_time',
  371. 'last_user.realname AS last_realname', 'last_user.username AS last_username',
  372. 'now_user.realname AS now_realname', 'now_user.username AS now_username',
  373. ]));
  374. if (($where['action_type'] ?? '') !== '') {
  375. $query->where('r.action_type', (int) $where['action_type']);
  376. }
  377. if (($where['goods_type'] ?? '') !== '') {
  378. $query->where('r.goods_type', (int) $where['goods_type']);
  379. }
  380. $sn = trim((string) ($where['device_sn'] ?? ''));
  381. if ($sn !== '') {
  382. $query->whereLike('r.device_sn', '%' . $sn . '%');
  383. }
  384. $result = $query->order('r.id', 'desc')->paginate([
  385. 'page' => $page,
  386. 'list_rows' => $limit,
  387. ])->toArray();
  388. $result['data'] = array_map(function (array $row) {
  389. $type = (int) $row['goods_type'];
  390. return [
  391. 'id' => (int) $row['id'],
  392. 'device_id' => (int) $row['device_id'],
  393. 'device_sn' => (string) $row['device_sn'],
  394. 'goods_type' => $type,
  395. 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备',
  396. 'last_agent_id' => (int) $row['last_agent_id'],
  397. 'last_agent_name' => (string) (($row['last_realname'] ?? '') ?: ($row['last_username'] ?? '')),
  398. 'now_agent_id' => (int) $row['now_agent_id'],
  399. 'now_agent_name' => (string) (($row['now_realname'] ?? '') ?: ($row['now_username'] ?? '')),
  400. 'action_type' => (int) $row['action_type'],
  401. 'action_name' => (int) $row['action_type'] === 1 ? '分配' : '回收',
  402. 'reason' => (string) $row['reason'],
  403. 'action_time' => (string) $row['action_time'],
  404. ];
  405. }, $result['data'] ?? []);
  406. return $result;
  407. }
  408. private function moveDevices(int $fromAgentId, int $toAgentId, array $deviceSns, int $actionType, string $reason): array
  409. {
  410. $sns = $this->normalizeSns($deviceSns);
  411. $operatorId = $this->currentAdminId();
  412. return $this->transaction(function () use ($fromAgentId, $toAgentId, $sns, $actionType, $reason, $operatorId) {
  413. $devices = Db::table('sa_device')->whereIn('goods_sn', $sns)
  414. ->whereNull('delete_time')->lock(true)->select()->toArray();
  415. $deviceMap = [];
  416. foreach ($devices as $device) {
  417. $deviceMap[(string) $device['goods_sn']] = $device;
  418. }
  419. $ids = array_map('intval', array_column($devices, 'id'));
  420. $boundIds = $ids === [] ? [] : array_map('intval', Db::table('sa_merchant_device_binding')
  421. ->whereIn('device_id', $ids)->column('device_id'));
  422. $boundMap = array_fill_keys($boundIds, true);
  423. $success = [];
  424. $notFound = [];
  425. $notManaged = [];
  426. $alreadyTarget = [];
  427. $distributed = [];
  428. $records = [];
  429. $now = date('Y-m-d H:i:s');
  430. foreach ($sns as $sn) {
  431. if (!isset($deviceMap[$sn])) {
  432. $notFound[] = $sn;
  433. continue;
  434. }
  435. $device = $deviceMap[$sn];
  436. $deviceId = (int) $device['id'];
  437. $managerId = (int) $device['manage_agent_id'];
  438. if ($managerId === $toAgentId) {
  439. $alreadyTarget[] = $sn;
  440. continue;
  441. }
  442. if ($managerId !== $fromAgentId) {
  443. $notManaged[] = $sn;
  444. continue;
  445. }
  446. if (isset($boundMap[$deviceId])) {
  447. $distributed[] = $sn;
  448. continue;
  449. }
  450. $updated = Db::table('sa_device')->where('id', $deviceId)
  451. ->where('manage_agent_id', $fromAgentId)
  452. ->update(['manage_agent_id' => $toAgentId, 'update_time' => $now]);
  453. if ($updated !== 1) {
  454. $notManaged[] = $sn;
  455. continue;
  456. }
  457. $records[] = [
  458. 'device_id' => $deviceId,
  459. 'device_sn' => $sn,
  460. 'goods_type' => (int) $device['goods_type'],
  461. 'last_agent_id' => $fromAgentId,
  462. 'now_agent_id' => $toAgentId,
  463. 'action_type' => $actionType,
  464. 'reason' => $reason,
  465. 'operator_id' => $operatorId,
  466. 'action_time' => $now,
  467. 'created_by' => $operatorId,
  468. 'create_time' => $now,
  469. ];
  470. $success[] = $sn;
  471. }
  472. if ($records !== []) {
  473. Db::table('sa_device_manager_record')->insertAll($records);
  474. }
  475. return [
  476. 'message' => $success === [] ? '没有可处理的新设备' : ($actionType === 1 ? '成功分配 ' : '成功回收 ') . count($success) . ' 台设备',
  477. 'success_sn' => $success,
  478. 'not_find_sn' => $notFound,
  479. 'not_managed_sn' => $notManaged,
  480. 'already_target_sn' => $alreadyTarget,
  481. 'distributed_sn' => $distributed,
  482. 'all_action_sn' => $sns,
  483. ];
  484. });
  485. }
  486. private function agentRows(array $where): array
  487. {
  488. $query = $this->accessibleAgentQuery();
  489. $name = trim((string) ($where['name'] ?? ''));
  490. if ($name !== '') {
  491. $query->whereLike('u.realname|u.username', '%' . $name . '%');
  492. }
  493. $phone = trim((string) ($where['phone'] ?? ''));
  494. if ($phone !== '') {
  495. $query->whereLike('u.phone', '%' . $phone . '%');
  496. }
  497. if (($where['status'] ?? '') !== '') {
  498. $query->where('u.status', (int) $where['status']);
  499. }
  500. $rawRows = $query->field('u.id,u.username,u.realname,u.phone,u.avatar,u.dept_id,u.status,u.create_time,d.name AS dept_name,d.level AS dept_level')->select()->toArray();
  501. $ids = array_map('intval', array_column($rawRows, 'id'));
  502. if ($ids === []) {
  503. return [];
  504. }
  505. $deviceMetrics = [];
  506. foreach (Db::table('sa_device')->whereIn('manage_agent_id', $ids)->whereNull('delete_time')
  507. ->field('manage_agent_id,goods_type')->select()->toArray() as $device) {
  508. $id = (int) $device['manage_agent_id'];
  509. $deviceMetrics[$id]['total'] = ($deviceMetrics[$id]['total'] ?? 0) + 1;
  510. if ((int) $device['goods_type'] === 107) {
  511. $deviceMetrics[$id]['vehicle'] = ($deviceMetrics[$id]['vehicle'] ?? 0) + 1;
  512. } else {
  513. $deviceMetrics[$id]['charging'] = ($deviceMetrics[$id]['charging'] ?? 0) + 1;
  514. }
  515. }
  516. $merchantMetrics = [];
  517. foreach (Db::table('sa_merchant')->whereIn('agent_id', $ids)->whereNull('delete_time')
  518. ->field('agent_id,total_income,withdrawable_amount,profit_rate')->select()->toArray() as $merchant) {
  519. $id = (int) $merchant['agent_id'];
  520. $merchantMetrics[$id]['income'] = ($merchantMetrics[$id]['income'] ?? 0) + (float) $merchant['total_income'];
  521. $merchantMetrics[$id]['withdrawable'] = ($merchantMetrics[$id]['withdrawable'] ?? 0) + (float) $merchant['withdrawable_amount'];
  522. $merchantMetrics[$id]['profit_total'] = ($merchantMetrics[$id]['profit_total'] ?? 0) + (float) $merchant['profit_rate'];
  523. $merchantMetrics[$id]['merchant_count'] = ($merchantMetrics[$id]['merchant_count'] ?? 0) + 1;
  524. }
  525. $deptRows = SystemDept::whereNull('delete_time')->field('id,name,level')->select()->toArray();
  526. $deptMap = [];
  527. foreach ($deptRows as $dept) {
  528. $deptMap[(int) $dept['id']] = $dept;
  529. }
  530. $allUsers = SystemUser::whereNull('delete_time')->field('id,dept_id')->select()->toArray();
  531. $currentDepth = $this->deptDepth($this->adminInfo['deptList'] ?? []);
  532. $rows = [];
  533. foreach ($rawRows as $row) {
  534. $id = (int) $row['id'];
  535. $dept = $deptMap[(int) $row['dept_id']] ?? [];
  536. $type = $this->agentType($this->deptDepth($dept) - $currentDepth);
  537. if (($where['agent_type'] ?? '') !== '' && $where['agent_type'] !== $type['code']) {
  538. continue;
  539. }
  540. $region = $this->regionNames($dept, $deptMap);
  541. $merchant = $merchantMetrics[$id] ?? [];
  542. $merchantCount = (int) ($merchant['merchant_count'] ?? 0);
  543. $rows[] = [
  544. 'id' => $id,
  545. 'name' => (string) (($row['realname'] ?? '') ?: $row['username']),
  546. 'username' => (string) $row['username'],
  547. 'phone' => (string) ($row['phone'] ?? ''),
  548. 'avatar' => (string) ($row['avatar'] ?? ''),
  549. 'agent_type' => $type['name'],
  550. 'agent_type_code' => $type['code'],
  551. 'province' => $region[0],
  552. 'city' => $region[1],
  553. 'district' => $region[2],
  554. 'dept_name' => (string) ($row['dept_name'] ?? ''),
  555. 'device_count' => (int) ($deviceMetrics[$id]['total'] ?? 0),
  556. 'vehicle_count' => (int) ($deviceMetrics[$id]['vehicle'] ?? 0),
  557. 'charging_count' => (int) ($deviceMetrics[$id]['charging'] ?? 0),
  558. 'subordinate_count' => $this->descendantUserCount($dept, $allUsers, $deptMap),
  559. 'income' => number_format((float) ($merchant['income'] ?? 0), 2, '.', ''),
  560. 'frozen' => '0.00',
  561. 'withdrawable' => number_format((float) ($merchant['withdrawable'] ?? 0), 2, '.', ''),
  562. 'profit_rate' => $merchantCount > 0 ? round((float) $merchant['profit_total'] / $merchantCount, 2) : 0,
  563. 'status' => (int) $row['status'],
  564. 'status_name' => (int) $row['status'] === 1 ? '有效' : '无效',
  565. 'created_at' => (string) ($row['create_time'] ?? ''),
  566. ];
  567. }
  568. $sort = (string) ($where['sort'] ?? 'default');
  569. usort($rows, function (array $a, array $b) use ($sort) {
  570. return match ($sort) {
  571. 'income_desc' => (float) $b['income'] <=> (float) $a['income'],
  572. 'income_asc' => (float) $a['income'] <=> (float) $b['income'],
  573. 'created_asc' => strcmp($a['created_at'], $b['created_at']),
  574. default => strcmp($b['created_at'], $a['created_at']) ?: ($b['id'] <=> $a['id']),
  575. };
  576. });
  577. return $rows;
  578. }
  579. private function accessibleAgentQuery()
  580. {
  581. $query = Db::table('sa_system_user')->alias('u')
  582. ->leftJoin('sa_system_dept d', 'd.id = u.dept_id')
  583. ->whereNull('u.delete_time')
  584. ->where('u.id', '<>', $this->currentAdminId());
  585. if ($this->currentAdminId() !== 1) {
  586. $dept = $this->adminInfo['deptList'] ?? [];
  587. if (empty($dept['id']) || !isset($dept['level'])) {
  588. return $query->where('u.id', 0);
  589. }
  590. $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
  591. $deptIds = SystemDept::whereLike('level', $prefix . '%')->column('id');
  592. if ($deptIds === []) {
  593. return $query->where('u.id', 0);
  594. }
  595. $query->whereIn('u.dept_id', $deptIds);
  596. }
  597. return $query;
  598. }
  599. private function findAccessibleAgent(int $id, bool $requireEnabled = true): array
  600. {
  601. $agent = $this->accessibleAgentQuery()->where('u.id', $id)
  602. ->when($requireEnabled, static fn ($query) => $query->where('u.status', 1))
  603. ->field('u.id,u.realname,u.username,u.phone,u.dept_id,u.status')->find();
  604. if (!$agent) {
  605. throw new ApiException($requireEnabled ? '代理不存在、已停用或无权操作' : '代理不存在或无权操作');
  606. }
  607. return $agent;
  608. }
  609. private function normalizeList(mixed $value, array $allowed): array
  610. {
  611. if (!is_array($value)) {
  612. return [];
  613. }
  614. $values = array_values(array_unique(array_map('strval', $value)));
  615. return array_values(array_intersect($allowed, $values));
  616. }
  617. private function decodeList(mixed $value, array $allowed): array
  618. {
  619. $decoded = is_string($value) ? json_decode($value, true) : $value;
  620. return $this->normalizeList($decoded, $allowed);
  621. }
  622. private function decodeMap(mixed $value): array
  623. {
  624. $decoded = is_string($value) ? json_decode($value, true) : $value;
  625. return is_array($decoded) ? $decoded : [];
  626. }
  627. private function normalizeAttachmentUrls(mixed $value): array
  628. {
  629. if (!is_array($value)) {
  630. throw new ApiException('合同附件参数格式错误');
  631. }
  632. if (count($value) > 20) {
  633. throw new ApiException('每类合同资料最多上传 20 张图片');
  634. }
  635. $urls = [];
  636. foreach ($value as $url) {
  637. if (!is_string($url)) {
  638. throw new ApiException('合同附件地址格式错误');
  639. }
  640. $url = trim($url);
  641. if ($url === '') {
  642. continue;
  643. }
  644. if (mb_strlen($url) > 2048 || preg_match('/^javascript:/i', $url)) {
  645. throw new ApiException('合同附件地址不合法');
  646. }
  647. $urls[] = $url;
  648. }
  649. return array_values(array_unique($urls));
  650. }
  651. private function decodeAttachmentUrls(mixed $value): array
  652. {
  653. $decoded = is_string($value) ? json_decode($value, true) : $value;
  654. return is_array($decoded) ? $this->normalizeAttachmentUrls($decoded) : [];
  655. }
  656. private function normalizeSns(array $deviceSns): array
  657. {
  658. $sns = array_values(array_unique(array_filter(array_map(
  659. static fn ($sn) => trim((string) $sn),
  660. $deviceSns
  661. ))));
  662. if ($sns === []) {
  663. throw new ApiException('请至少输入一个设备 SN');
  664. }
  665. if (count($sns) > 100) {
  666. throw new ApiException('单次最多处理 100 台设备');
  667. }
  668. return $sns;
  669. }
  670. private function deptDepth(array $dept): int
  671. {
  672. $level = trim((string) ($dept['level'] ?? ''), ',');
  673. return ($level === '' ? 0 : count(explode(',', $level))) + (empty($dept['id']) ? 0 : 1);
  674. }
  675. private function agentType(int $depthDiff): array
  676. {
  677. if ($depthDiff <= 1) return ['code' => 'city-partner', 'name' => '城市合伙人'];
  678. if ($depthDiff === 2) return ['code' => 'agent', 'name' => '代理商'];
  679. return ['code' => 'salesperson', 'name' => '业务员'];
  680. }
  681. private function regionNames(array $dept, array $deptMap): array
  682. {
  683. if ($dept === []) return ['--', '--', '--'];
  684. $ids = array_filter(array_map('intval', explode(',', trim((string) ($dept['level'] ?? ''), ','))));
  685. $ids[] = (int) $dept['id'];
  686. $names = array_values(array_filter(array_map(fn ($id) => $deptMap[$id]['name'] ?? '', $ids)));
  687. return [$names[0] ?? '--', $names[1] ?? '--', $names[2] ?? '--'];
  688. }
  689. private function descendantUserCount(array $dept, array $users, array $deptMap): int
  690. {
  691. if ($dept === []) return 0;
  692. $prefix = (string) ($dept['level'] ?? '') . (int) $dept['id'] . ',';
  693. $count = 0;
  694. foreach ($users as $user) {
  695. $userDept = $deptMap[(int) $user['dept_id']] ?? [];
  696. if ($userDept !== [] && str_starts_with((string) $userDept['level'], $prefix)) $count++;
  697. }
  698. return $count;
  699. }
  700. private function currentAdminId(): int
  701. {
  702. return (int) ($this->adminInfo['id'] ?? 0);
  703. }
  704. private function excelSafe(mixed $value): string
  705. {
  706. $value = (string) $value;
  707. return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
  708. }
  709. }