LakalaAccountService.php 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169
  1. <?php
  2. namespace bizGhs\lakala\services;
  3. use bizGhs\lakala\classes\LakalaAccountClass;
  4. use bizGhs\lakala\classes\LakalaApplicationClass;
  5. use bizGhs\lakala\classes\LakalaApplicationVersionClass;
  6. use bizGhs\lakala\classes\LakalaOperationClass;
  7. use bizGhs\shop\classes\ShopClass;
  8. use common\components\imgUtil;
  9. use common\components\lakala\LakalaOnboardingClient;
  10. use common\components\oss;
  11. use common\components\stringUtil;
  12. use common\components\util;
  13. use PhpAmqpLib\Wire\AMQPTable;
  14. use Yii;
  15. class LakalaAccountService
  16. {
  17. const RATE = '0.33';
  18. const MCC = '5992';
  19. const ACTIVITY_ID = '208'; // WECHAT_PAY -- 专业化扫码
  20. public static function list($mainId, $shopId)
  21. {
  22. $applications = LakalaApplicationClass::getAllByCondition(
  23. ['mainId' => $mainId, 'shopId' => $shopId],
  24. ['id' => SORT_DESC],
  25. '*'
  26. );
  27. $accounts = LakalaAccountClass::getAllByCondition(
  28. ['mainId' => $mainId, 'shopId' => $shopId],
  29. ['isCurrent' => SORT_DESC, 'id' => SORT_DESC],
  30. '*', null, true
  31. );
  32. foreach ($applications as &$application) {
  33. $application['formData'] = self::decode($application['formData'] ?? '');
  34. }
  35. $refreshAccountIds = [];
  36. foreach ($accounts as $account) {
  37. if ($account->wxAuthorizeState == 1 && $account->zfbAuthorizeState == 1) {
  38. continue;
  39. }
  40. $refreshAccountIds[] = $account->id;
  41. }
  42. LakalaAccountClass::dispatchAuthorizeStatesRefresh($refreshAccountIds);
  43. $accountList = [];
  44. foreach ($accounts as $account) {
  45. $accountList[] = $account->attributes;
  46. }
  47. return ['applications' => $applications, 'accounts' => $accountList];
  48. }
  49. public static function detail($id, $mainId, $shopId)
  50. {
  51. $application = self::application($id, $mainId, $shopId);
  52. $application['formData'] = self::formatFormImages(self::decode($application['formData'] ?? ''));
  53. return $application;
  54. }
  55. public static function create($mainId, $shopId, $shop, $type = LakalaApplicationClass::TYPE_ONBOARDING, $accountId = 0)
  56. {
  57. $now = date('Y-m-d H:i:s');
  58. $form = self::defaultForm($shop);
  59. if ($type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE && $accountId > 0) {
  60. $account = LakalaAccountClass::getByCondition(['id' => $accountId, 'mainId' => $mainId, 'shopId' => $shopId]);
  61. if (empty($account)) {
  62. util::fail('没有找到收款账户');
  63. }
  64. $form['mode'] = 'settlement-change';
  65. $form['settlement']['accountName'] = $account['accountName'] ?? '';
  66. $form['settlement']['accountNo'] = $account['accountNo'] ?? '';
  67. $form['settlement']['bankName'] = $account['bankName'] ?? '';
  68. }
  69. $application = LakalaApplicationClass::add([
  70. 'mainId' => $mainId,
  71. 'shopId' => $shopId,
  72. 'accountId' => $accountId,
  73. 'client' => 1, //申请来自哪个客户端,: 1销花宝 2花掌柜(设置来自销花宝)
  74. 'type' => $type,
  75. 'status' => LakalaApplicationClass::STATUS_DRAFT,
  76. 'currentStep' => $type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE ? 3 : 1,
  77. 'formData' => self::encode($form),
  78. 'createTime' => $now,
  79. 'updateTime' => $now,
  80. ], true);
  81. return self::detail($application->id, $mainId, $shopId);
  82. }
  83. public static function saveDraft($id, $mainId, $shopId, array $form, $step)
  84. {
  85. $application = self::application($id, $mainId, $shopId, true);
  86. if (!in_array($application->status, [LakalaApplicationClass::STATUS_DRAFT, LakalaApplicationClass::STATUS_REJECT, LakalaApplicationClass::STATUS_CONTRACT_PENDING], true)) {
  87. util::fail('当前申请不能修改');
  88. }
  89. $application->formData = self::encode($form);
  90. $application->currentStep = max(1, min(5, (int)$step));
  91. $application->updateTime = date('Y-m-d H:i:s');
  92. $application->save();
  93. return self::detail($id, $mainId, $shopId);
  94. }
  95. public static function deleteDraft($id, $mainId, $shopId)
  96. {
  97. $application = self::application($id, $mainId, $shopId);
  98. if ($application['status'] !== LakalaApplicationClass::STATUS_DRAFT) {
  99. util::fail('仅草稿可以删除');
  100. }
  101. LakalaApplicationClass::deleteById($id);
  102. }
  103. public static function upload($id, $mainId, $shopId, $base64, $imgType, $ocr)
  104. {
  105. self::application($id, $mainId, $shopId);
  106. if (!preg_match('/^data:image\/[a-zA-Z0-9.+-]+;base64,/', $base64)) {
  107. util::fail('图片格式错误');
  108. }
  109. $content = base64_decode(substr($base64, strpos($base64, ',') + 1), true);
  110. if ($content === false || strlen($content) > 5 * 1024 * 1024) {
  111. util::fail('图片内容无效或超过5MB');
  112. }
  113. $info = getimagesizefromstring($content);
  114. if ($info === false) {
  115. util::fail('图片内容无效');
  116. }
  117. $allowMime = [
  118. 'image/jpeg',
  119. 'image/png',
  120. 'image/gif',
  121. 'image/webp',
  122. ];
  123. if (!in_array($info['mime'], $allowMime, true)) {
  124. Yii::error('不允许上传:' . $info['mime']);
  125. util::fail('图片出错,不允许上传');
  126. }
  127. if (strlen($content) > 5 * 1024 * 1024) {
  128. util::fail('图片超过5MB');
  129. }
  130. $extension = self::imageExtension($info['mime']);
  131. $path = 'lakala-onboarding/' . $mainId . '/' . date('Ymd') . '/' . $id . '/' . stringUtil::uniqueFileName() . '.' . $extension;
  132. oss::uploadContent($path, $content);
  133. $response = (new LakalaOnboardingClient())->upload($base64, $imgType, (bool)$ocr);
  134. $lakalaUrl = $response['data']['url'] ?? '';
  135. return [
  136. 'ossPath' => self::formatOssPath($path),
  137. 'lakalaUrl' => $lakalaUrl,
  138. 'imgType' => $imgType,
  139. 'batchNo' => $response['data']['batch_no'] ?? '',
  140. 'ocrStatus' => $response['data']['status'] ?? '',
  141. 'ocrResult' => $response['data']['result'] ?? null,
  142. 'url' => $lakalaUrl,
  143. 'status' => $response['data']['status'] ?? '',
  144. 'ocr' => $response['data']['result'] ?? null,
  145. ];
  146. }
  147. public static function ocr($id, $mainId, $shopId, $batchNo, $imgType)
  148. {
  149. self::application($id, $mainId, $shopId);
  150. $response = (new LakalaOnboardingClient())->ocr($batchNo, $imgType);
  151. return $response['data'];
  152. }
  153. public static function option($type, $parentCode = '', $keyword = '')
  154. {
  155. $client = new LakalaOnboardingClient();
  156. if ($type === 'region') {
  157. return $client->query('/api/v3/tkbs/organization_parent_code', ['parent_code' => $parentCode ?: '1'])['data'];
  158. }
  159. if ($type === 'bank-region') {
  160. return $client->query('/api/v3/tkbs/organization_bank_parent_code', ['parent_code' => $parentCode ?: '1'])['data'];
  161. }
  162. if ($type === 'bank') {
  163. return $client->query('/api/v3/tkbs/bank', ['area_code' => $parentCode, 'bank_name' => $keyword])['data'];
  164. }
  165. util::fail('不支持的选项类型');
  166. }
  167. public static function applyContract($id, $mainId, $shopId)
  168. {
  169. $application = self::application($id, $mainId, $shopId, true);
  170. $form = self::decode($application->formData);
  171. self::validateContract($form);
  172. $client = new LakalaOnboardingClient();
  173. $orderNo = date('YmdHis') . substr(str_shuffle('0123456789'), 0, 8);
  174. $request = self::contractRequest($form, $orderNo, $client->orgCode());
  175. $response = $client->call('/api/v3/mms/open_api/ec/apply', $request);
  176. $application->contractOrderNo = $orderNo;
  177. $application->contractApplyId = $response['data']['ec_apply_id'] ?? '';
  178. $application->contractStatus = 'UNDONE';
  179. $application->status = LakalaApplicationClass::STATUS_CONTRACT_PENDING;
  180. $application->updateTime = date('Y-m-d H:i:s');
  181. $application->save();
  182. self::operation($application, 0, 'contract', '', $request, $response, 'UNDONE');
  183. return ['resultUrl' => $response['data']['result_url'] ?? '', 'status' => 'UNDONE'];
  184. }
  185. public static function contractStatus($id, $mainId, $shopId)
  186. {
  187. $application = self::application($id, $mainId, $shopId, true);
  188. if (empty($application->contractApplyId) || empty($application->contractOrderNo)) {
  189. util::fail('请先生成电子合同');
  190. }
  191. $client = new LakalaOnboardingClient();
  192. $response = $client->query('/api/v3/mms/open_api/ec/q_status', [
  193. 'ec_apply_id' => $application->contractApplyId,
  194. 'order_no' => $application->contractOrderNo,
  195. ]);
  196. $application->contractStatus = $response['data']['ec_status'] ?? '';
  197. $application->contractNo = $response['data']['ec_no'] ?? '';
  198. $application->updateTime = date('Y-m-d H:i:s');
  199. $application->save();
  200. return ['status' => $application->contractStatus, 'contractNo' => $application->contractNo];
  201. }
  202. public static function submit($id, $mainId, $shopId, array $form)
  203. {
  204. $application = self::application($id, $mainId, $shopId, true);
  205. if ($application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE) {
  206. return self::submitSettlementChange($application, $form);
  207. }
  208. self::validateOnboarding($form);
  209. if ($application->contractStatus !== 'COMPLETED' || empty($application->contractNo)) {
  210. util::fail('请先完成电子合同签约并刷新签约状态');
  211. }
  212. try{
  213. $client = new LakalaOnboardingClient();
  214. $request = self::merchantRequest($form, $application->contractNo, $client);
  215. $response = $client->call('/api/v3/tkbs/merchant_encry', $request, 'both');
  216. $application->formData = self::encode($form);
  217. $application->merchantNo = $response['data']['merchant_no'] ?? '';
  218. $application->customerNo = $response['data']['merchant_no'] ?? '';
  219. $application->status = LakalaApplicationClass::STATUS_WAIT_AUDIT;
  220. $application->rejectReason = '';
  221. $application->versionNo = ((int)$application->versionNo) + 1;
  222. $application->updateTime = date('Y-m-d H:i:s');
  223. $application->save();
  224. self::version($application, $request, $response);
  225. self::operation($application, 0, 'onboarding', '', $request, $response, $application->status);
  226. return self::detail($application->id, $mainId, $shopId);
  227. }catch(\Throwable $e){
  228. Yii::error('Lakala submit: ' . $e->getMessage());
  229. $errMsg = $e->getMessage();
  230. $application->status = LakalaApplicationClass::STATUS_REJECT;
  231. $application->rejectReason = $errMsg;
  232. $application->save();
  233. }
  234. }
  235. public static function submitForPlatformAudit($id, $mainId, $shopId, array $form)
  236. {
  237. $application = self::application($id, $mainId, $shopId, true);
  238. if ($application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE) {
  239. return self::submitSettlementChange($application, $form);
  240. }
  241. self::validateOnboarding($form);
  242. if ($application->contractStatus !== 'COMPLETED' || empty($application->contractNo)) {
  243. util::fail('请先完成电子合同签约并刷新签约状态');
  244. }
  245. $application->formData = self::encode($form);
  246. $application->status = LakalaApplicationClass::STATUS_PLATFORM_AUDIT;
  247. $application->rejectReason = '';
  248. $application->currentStep = 5;
  249. $application->updateTime = date('Y-m-d H:i:s');
  250. $application->save();
  251. return self::detail($application->id, $mainId, $shopId);
  252. }
  253. public static function platformList($status = '', $keyword = '')
  254. {
  255. $where = [];
  256. if ($status !== '' && $status !== '-1') {
  257. $where['status'] = $status;
  258. }
  259. if ($keyword !== '') {
  260. if (is_numeric($keyword)) {
  261. $where['id'] = (int)$keyword;
  262. } else {
  263. $where['formData'] = ['like', $keyword];
  264. }
  265. }
  266. $result = LakalaApplicationClass::getList('*', $where, ['id' => SORT_DESC]);
  267. foreach ($result['list'] as &$item) {
  268. $form = self::formatFormImages(self::decode($item['formData'] ?? ''));
  269. $item['client'] = $item['client'] == 1 ? '销花宝' : '花掌柜';
  270. $item['formData'] = $form;
  271. $item['merchantName'] = $form['merchant']['businessName'] ?? ($form['license']['licenseName'] ?? '');
  272. $item['legalName'] = $form['license']['legalName'] ?? '';
  273. $item['accountName'] = $form['settlement']['accountName'] ?? '';
  274. }
  275. return $result;
  276. }
  277. public static function platformDetail($id)
  278. {
  279. $application = LakalaApplicationClass::getById($id);
  280. if (empty($application)) {
  281. util::fail('没有找到进件申请');
  282. }
  283. $application['formData'] = self::formatFormImages(self::decode($application['formData'] ?? ''));
  284. return $application;
  285. }
  286. public static function platformSave($id, array $form, $step = 5)
  287. {
  288. $application = LakalaApplicationClass::getById($id, true);
  289. if (empty($application)) {
  290. util::fail('没有找到进件申请');
  291. }
  292. if (!in_array($application->status, [
  293. LakalaApplicationClass::STATUS_DRAFT,
  294. LakalaApplicationClass::STATUS_PLATFORM_AUDIT,
  295. LakalaApplicationClass::STATUS_REJECT,
  296. LakalaApplicationClass::STATUS_CONTRACT_PENDING,
  297. ], true)) {
  298. util::fail('当前申请不能修改');
  299. }
  300. $application->formData = self::encode($form);
  301. $application->currentStep = max(1, min(5, (int)$step));
  302. $application->updateTime = date('Y-m-d H:i:s');
  303. $application->save();
  304. return self::platformDetail($id);
  305. }
  306. public static function platformUpload($id, $base64, $imgType, $ocr = 0)
  307. {
  308. $application = LakalaApplicationClass::getById($id);
  309. if (empty($application)) {
  310. util::fail('没有找到进件申请');
  311. }
  312. return self::upload($id, $application['mainId'], $application['shopId'], $base64, $imgType, (int)$ocr);
  313. }
  314. public static function platformReject($id, $reason)
  315. {
  316. $application = LakalaApplicationClass::getById($id, true);
  317. if (empty($application)) {
  318. util::fail('没有找到进件申请');
  319. }
  320. if ($application->status !== LakalaApplicationClass::STATUS_PLATFORM_AUDIT) {
  321. util::fail('仅后台审核中的申请可以驳回');
  322. }
  323. $application->status = LakalaApplicationClass::STATUS_REJECT;
  324. $application->rejectReason = mb_substr($reason ?: '后台审核驳回', 0, 500);
  325. $application->updateTime = date('Y-m-d H:i:s');
  326. $application->save();
  327. }
  328. /**
  329. * 平台审核通过:把申请提交到拉卡拉;拉卡拉审核通过后自动刷新回写商户号/终端。
  330. * 入参 id 为进件申请主键;成功时返回申请详情。
  331. * 花掌柜申请转交 biz-hd,避免沿用销花宝 0.33% 费率。
  332. */
  333. public static function platformApprove($id)
  334. {
  335. $application = LakalaApplicationClass::getById($id, true);
  336. if (empty($application)) {
  337. util::fail('没有找到进件申请');
  338. }
  339. // 花掌柜申请必须由 biz-hd 生成 0.38% 的合同及进件费率,避免沿用销花宝 0.33%。
  340. if ((int)$application->client === 2) {
  341. return \bizHd\lakala\services\LakalaAccountService::platformApprove($id);
  342. }
  343. if ($application->status !== LakalaApplicationClass::STATUS_PLATFORM_AUDIT) {
  344. util::fail('仅后台审核中的申请可以提交拉卡拉');
  345. }
  346. $result = self::submit(
  347. $application->id,
  348. $application->mainId,
  349. $application->shopId,
  350. self::decode($application->formData)
  351. );
  352. if (empty($result)) {
  353. return $result;
  354. }
  355. // 提交成功后延时 1 分钟刷新拉卡拉审核结果(最多 3 次),不阻塞本次审核提交。
  356. self::scheduleRefreshAfterSubmit($application->id, $application->mainId, $application->shopId);
  357. return $result;
  358. }
  359. /**
  360. * 平台提交拉卡拉成功后,投递延时刷新消息(复用 limitBuyDelayExchange + stockConsumer)。
  361. * 延时 1 分钟;消费失败或仍未终态时最多再投 2 次,共 3 次直到本地 APPROVED/REJECT。
  362. */
  363. public static function scheduleRefreshAfterSubmit($id, $mainId, $shopId, $attempt = 1)
  364. {
  365. $id = intval($id);
  366. $mainId = intval($mainId);
  367. $shopId = intval($shopId);
  368. $attempt = max(1, intval($attempt));
  369. if ($id <= 0) {
  370. return false;
  371. }
  372. try {
  373. $message = serialize([
  374. 'type' => 'lakala_refresh',
  375. 'id' => $id,
  376. 'mainId' => $mainId,
  377. 'shopId' => $shopId,
  378. 'client' => 1,
  379. 'attempt' => $attempt,
  380. ]);
  381. $producer = Yii::$app->rabbitmq->getProducer('notifyProducer');
  382. $producer->publish($message, 'limitBuyDelayExchange', 'lakalaRefreshDelayRoute', [
  383. 'delivery_mode' => 2,
  384. 'content_type' => 'application/octet-stream',
  385. 'application_headers' => new AMQPTable([
  386. 'x-delay' => 60000,
  387. ]),
  388. ]);
  389. Yii::info('lakala refresh delay: id=' . $id . ' attempt=' . $attempt, __METHOD__);
  390. } catch (\Exception $e) {
  391. Yii::error('Lakala scheduleRefreshAfterSubmit fail: ' . $e->getMessage(), __METHOD__);
  392. }
  393. return true;
  394. }
  395. /**
  396. * 延时队列消费:调用与客户端「刷新状态」相同的 refresh。
  397. * 拉卡拉驳回或刷新失败只记日志;未到终态且未满 3 次则再延时 1 分钟。
  398. */
  399. public static function handleDelayedRefresh($data)
  400. {
  401. $id = intval($data['id'] ?? 0);
  402. $mainId = intval($data['mainId'] ?? 0);
  403. $shopId = intval($data['shopId'] ?? 0);
  404. $attempt = max(1, intval($data['attempt'] ?? 1));
  405. $maxAttempts = 3;
  406. try {
  407. $application = LakalaApplicationClass::getById($id, true);
  408. if (empty($application)) {
  409. return true;
  410. }
  411. if ($mainId <= 0) {
  412. $mainId = intval($application->mainId);
  413. }
  414. if ($shopId <= 0) {
  415. $shopId = intval($application->shopId);
  416. }
  417. if ($application->status === LakalaApplicationClass::STATUS_APPROVED
  418. || $application->status === LakalaApplicationClass::STATUS_REJECT) {
  419. return true;
  420. }
  421. $canRefresh = $application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE
  422. ? !empty($application->reviewRelatedId)
  423. : (!empty($application->customerNo) || !empty($application->merchantNo));
  424. if (!$canRefresh) {
  425. return true;
  426. }
  427. self::refresh($application->id, $application->mainId, $application->shopId);
  428. $application = LakalaApplicationClass::getById($id, true);
  429. if (!empty($application) && ($application->status === LakalaApplicationClass::STATUS_APPROVED
  430. || $application->status === LakalaApplicationClass::STATUS_REJECT)) {
  431. return true;
  432. }
  433. } catch (\Throwable $e) {
  434. Yii::warning('Lakala delayed refresh attempt ' . $attempt . ' failed: ' . $e->getMessage());
  435. }
  436. if ($attempt < $maxAttempts && $id > 0) {
  437. self::scheduleRefreshAfterSubmit($id, $mainId, $shopId, $attempt + 1);
  438. }
  439. return true;
  440. }
  441. public static function refresh($id, $mainId, $shopId)
  442. {
  443. $application = self::application($id, $mainId, $shopId, true);
  444. if ($application->type === LakalaApplicationClass::TYPE_SETTLEMENT_CHANGE) {
  445. return self::refreshChange($application);
  446. }
  447. if (empty($application->customerNo) && empty($application->merchantNo)) {
  448. util::fail('申请尚未提交(流程未走完)');
  449. }
  450. $client = new LakalaOnboardingClient();
  451. $response = $client->query('/api/v3/tkbs/open_merchant_info', [
  452. 'customer_no' => $application->customerNo,
  453. ], 'both'); //获取商户信息
  454. $customer = $response['data']['customer'];
  455. $status = $customer['customer_status'];
  456. if (in_array($status, ['REJECT', 'REVIEW_FAIL'], true)) {
  457. $application->status = LakalaApplicationClass::STATUS_REJECT;
  458. $application->rejectReason = $customer['audit_remark'] ?? '拉卡拉审核驳回';
  459. } elseif (in_array($status, ['OPEN', 'ACTIVITY'], true)) {
  460. $application->merchantNo = $customer['merchant_no'] ?? $application->merchantNo;
  461. $application->customerNo = $customer['customer_no'] ?? $application->customerNo;
  462. $shopInfo = $response['data']['shop_info_list'][0] ?? [];
  463. $application->shopNo = $shopInfo['shop_id'] ?? $application->shopNo;
  464. $account = self::upsertAccount($application, $response['data']);
  465. self::ensureTermOperations($application, $account, $client);
  466. self::syncTerms($application, $account, $client);
  467. if ($account->scanTermNo != '' && $account->b2bTermNo != '') {
  468. $application->status = LakalaApplicationClass::STATUS_APPROVED;
  469. }
  470. } else {
  471. $application->status = LakalaApplicationClass::STATUS_WAIT_AUDIT;
  472. }
  473. $application->updateTime = date('Y-m-d H:i:s');
  474. $application->save();
  475. return self::detail($application->id, $mainId, $shopId);
  476. }
  477. public static function setCurrent($accountId, $mainId, $shopId)
  478. {
  479. $account = LakalaAccountClass::getByCondition(['id' => $accountId, 'mainId' => $mainId, 'shopId' => $shopId], true);
  480. if (empty($account)) {
  481. util::fail('没有找到收款账户');
  482. }
  483. if (empty($account->scanTermNo) || empty($account->b2bTermNo)) {
  484. util::fail('账户业务尚未全部开通');
  485. }
  486. $transaction = Yii::$app->db->beginTransaction();
  487. try {
  488. LakalaAccountClass::updateByCondition(['shopId' => $shopId], ['isCurrent' => 0, 'updateTime' => date('Y-m-d H:i:s')]);
  489. $account->isCurrent = 1;
  490. $account->updateTime = date('Y-m-d H:i:s');
  491. $account->save();
  492. ShopClass::updateById($shopId, [
  493. 'lklSjNo' => $account->merchantNo,
  494. 'lklScanTermNo' => $account->scanTermNo,
  495. 'lklB2BTermNo' => $account->b2bTermNo,
  496. ]);
  497. $transaction->commit();
  498. } catch (\Throwable $e) {
  499. $transaction->rollBack();
  500. throw $e;
  501. }
  502. }
  503. private static function submitSettlementChange($application, array $form)
  504. {
  505. self::validateSettlement($form['settlement'] ?? []);
  506. $account = LakalaAccountClass::getByCondition(['id' => $application->accountId, 'mainId' => $application->mainId], true);
  507. if (empty($account)) {
  508. util::fail('没有找到收款账户');
  509. }
  510. $client = new LakalaOnboardingClient();
  511. $request = [
  512. 'org_code' => $client->orgCode(),
  513. 'customer_no' => $account->customerNo,
  514. 'customer_settle_update_dto' => self::settlementDto($form['settlement']),
  515. ];
  516. $response = $client->call('/api/v3/tkbs/open_merchant_update_settle_encry', $request, 'both');
  517. $application->formData = self::encode($form);
  518. $application->reviewRelatedId = $response['data']['review_related_id'] ?? '';
  519. $application->status = LakalaApplicationClass::STATUS_WAIT_AUDIT;
  520. $application->versionNo = ((int)$application->versionNo) + 1;
  521. $application->updateTime = date('Y-m-d H:i:s');
  522. $application->save();
  523. self::version($application, $request, $response);
  524. self::operation($application, $account->id, 'settlement_change', '', $request, $response, 'PASSING');
  525. return self::detail($application->id, $application->mainId, $application->shopId);
  526. }
  527. private static function refreshChange($application)
  528. {
  529. if (empty($application->reviewRelatedId)) {
  530. util::fail('变更申请尚未提交');
  531. }
  532. $client = new LakalaOnboardingClient();
  533. $response = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $application->reviewRelatedId]);
  534. $review = $response['data']['review_pass'] ?? '';
  535. if ($review === 'PASS') {
  536. $application->status = LakalaApplicationClass::STATUS_APPROVED;
  537. $account = LakalaAccountClass::getById($application->accountId, true);
  538. if (!empty($account)) {
  539. $settlement = self::decode($application->formData)['settlement'] ?? [];
  540. $account->accountName = $settlement['accountName'] ?? $account->accountName;
  541. $account->accountNo = $settlement['accountNo'] ?? $account->accountNo;
  542. $account->bankName = $settlement['bankName'] ?? $account->bankName;
  543. $account->accountType = $settlement['accountType'] ?? $account->accountType;
  544. $account->updateTime = date('Y-m-d H:i:s');
  545. $account->save();
  546. }
  547. } elseif ($review === 'UNPASS') {
  548. $application->status = LakalaApplicationClass::STATUS_REJECT;
  549. $application->rejectReason = $response['data']['review_result'] ?? '变更审核驳回';
  550. } else {
  551. $application->status = LakalaApplicationClass::STATUS_WAIT_AUDIT;
  552. }
  553. $application->updateTime = date('Y-m-d H:i:s');
  554. $application->save();
  555. return self::detail($application->id, $application->mainId, $application->shopId);
  556. }
  557. private static function ensureTermOperations($application, $account, $client)
  558. {
  559. foreach (['B2B_SYT'] as $bizType) { // 'WECHAT_PAY' -- 专业化扫码已经有了,不需要额外创建
  560. $operation = LakalaOperationClass::getByCondition([
  561. 'applicationId' => $application->id,
  562. 'operationType' => 'add_term',
  563. 'bizType' => $bizType,
  564. 'status!=' => 'FAILED',
  565. ], true);
  566. if (!empty($operation)) {
  567. continue;
  568. }
  569. $request = [
  570. 'merchant_no' => $account->merchantNo,
  571. 'bz_pos' => $bizType,
  572. 'term_num' => 1,
  573. 'shop_id' => $account->shopNo,
  574. 'org_code' => $client->orgCode(),
  575. 'fees' => self::termFees(),
  576. 'attachments' => [],
  577. ];
  578. try {
  579. $response = $client->call('/api/v3/tkbs/open_merchant_addTerm', $request, 'both');
  580. self::operation($application, $account->id, 'add_term', $bizType, $request, $response, 'PASSING');
  581. } catch (\Throwable $e) {
  582. self::operation($application, $account->id, 'add_term', $bizType, $request, [], 'FAILED', $e->getMessage());
  583. }
  584. }
  585. }
  586. private static function syncTerms($application, $account, $client)
  587. {
  588. $maxAttempts = 3;
  589. $retryInterval = 3;
  590. for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
  591. try {
  592. $operations = LakalaOperationClass::getAllByCondition(
  593. ['applicationId' => $application->id, 'operationType' => 'add_term'],
  594. null,
  595. '*',
  596. null,
  597. true
  598. );
  599. foreach ($operations as $operation) {
  600. if (!empty($operation->reviewRelatedId) && !in_array($operation->status, ['PASS', 'FAILED'], true)) {
  601. $review = $client->query('/api/v3/tkbs/customer_update_review', ['review_related_id' => $operation->reviewRelatedId]);
  602. $operation->status = $review['data']['review_pass'] ?? $operation->status;
  603. $operation->responseData = self::encode($review);
  604. $operation->errorMessage = $review['data']['review_result'] ?? '';
  605. $operation->updateTime = date('Y-m-d H:i:s');
  606. $operation->save();
  607. }
  608. }
  609. $res = $client->query('/api/v3/tkbs/open_merchant_info', [
  610. 'customer_no' => $account->customerNo ?: $application->customerNo,
  611. 'merchant_no' => $account->merchantNo ?: $application->merchantNo,
  612. ], 'both');
  613. $merchantInfo = $res['data'];
  614. $customer = $merchantInfo['customer'];
  615. $terms = self::merchantInfoTermMap($merchantInfo);
  616. $merchantNo = $customer['merchant_no'] ?? '';
  617. if ($merchantNo !== '') {
  618. $application->merchantNo = $merchantNo;
  619. $account->merchantNo = $merchantNo;
  620. }
  621. self::assignAccountTermNo($account, 'scanTermNo', $terms['scanTermNo'][0] ?? '');
  622. self::assignAccountTermNo($account, 'b2bTermNo', $terms['b2BTermNo'][0] ?? '');
  623. $account->updateTime = date('Y-m-d H:i:s');
  624. $account->save();
  625. $application->save();
  626. if ($account->scanTermNo != '' && $account->b2bTermNo != '') {
  627. LakalaAccountClass::dispatchAuthorizeStatesRefresh([$account->id]);
  628. return;
  629. } else {
  630. }
  631. } catch (\Throwable $e) {
  632. Yii::warning('Unable to sync Lakala merchant info terms (attempt ' . $attempt . '): ' . $e->getMessage());
  633. }
  634. if ($attempt < $maxAttempts) {
  635. sleep($retryInterval);
  636. }
  637. }
  638. }
  639. /**
  640. * scanTermNo / b2bTermNo 为 NOT NULL,拉卡拉尚未返回终端号时不能写成 null,否则 save 会 1048。
  641. */
  642. private static function assignAccountTermNo($account, $attribute, $termNo)
  643. {
  644. $termNo = trim((string)$termNo);
  645. if ($termNo !== '') {
  646. $account->$attribute = $termNo;
  647. }
  648. }
  649. private static function upsertAccount($application, array $remote)
  650. {
  651. $account = LakalaAccountClass::getByCondition(['mainId' => $application->mainId, 'merchantNo' => $application->merchantNo], true);
  652. $form = self::decode($application->formData);
  653. $settlement = $form['settlement'] ?? [];
  654. $license = $form['license'] ?? [];
  655. $shopInfo = $remote['shop_info_list'][0] ?? [];
  656. $data = [
  657. 'mainId' => $application->mainId,
  658. 'shopId' => $application->shopId,
  659. 'applicationId' => $application->id,
  660. 'merchantNo' => $application->merchantNo,
  661. 'customerNo' => $application->customerNo,
  662. 'shopNo' => $shopInfo['shop_id'] ?? $application->shopNo,
  663. 'accountName' => $settlement['accountName'] ?? '',
  664. 'accountNo' => $settlement['accountNo'] ?? '',
  665. 'bankName' => $settlement['bankName'] ?? '',
  666. 'accountType' => $settlement['accountType'] ?? '58',
  667. 'licenseName' => $license['licenseName'] ?? ($license['legalName'] ?? ''),
  668. 'identityExpire' => $license['legalIdEnd'] ?? '',
  669. 'wxAuthorizeState' => $account->wxAuthorizeState ?? 0,
  670. 'zfbAuthorizeState' => $account->zfbAuthorizeState ?? 0,
  671. 'status' => 'ACTIVE',
  672. 'updateTime' => date('Y-m-d H:i:s'),
  673. ];
  674. if (empty($account)) {
  675. $data['createTime'] = date('Y-m-d H:i:s');
  676. return LakalaAccountClass::add($data, true);
  677. }
  678. foreach ($data as $key => $value) {
  679. $account->$key = $value;
  680. }
  681. $account->save();
  682. return $account;
  683. }
  684. private static function defaultForm($shop)
  685. {
  686. $merchantName = $shop->merchantName ?? ($shop->shopName ?? '');
  687. $mobile = $shop->mobile ?? ($shop->telephone ?? '');
  688. return [
  689. 'mode' => 'create',
  690. 'license' => [
  691. 'merchantType' => 'TP_MERCHANT', 'licenseName' => '', 'licenseNo' => '',
  692. 'licenseStart' => '', 'licenseEnd' => '9999-12-31', 'registeredAddress' => '',
  693. 'businessContent' => '', 'legalName' => '', 'legalIdNo' => '',
  694. 'legalIdStart' => '', 'legalIdEnd' => '9999-12-31', 'attachments' => [],
  695. ],
  696. 'merchant' => [
  697. 'businessName' => ($shop->city ?? '') . $merchantName,
  698. 'provinceCode' => '', 'cityCode' => '', 'countyCode' => '',
  699. 'provinceName' => $shop->province ?? '', 'cityName' => $shop->city ?? '',
  700. 'countyName' => $shop->dist ?? '', 'address' => $shop->address ?? ($shop->fullAddress ?? ''),
  701. 'contactName' => '石少华', 'contactMobile' => '15280215347', 'email' => '479439056@qq.com',
  702. ],
  703. 'settlement' => [
  704. 'accountType' => '58', 'isLegalPerson' => true, 'accountName' => '',
  705. 'accountNo' => '', 'bankName' => '', 'bankNo' => '', 'clearingBankNo' => '',
  706. 'provinceCode' => '', 'provinceName' => '', 'cityCode' => '', 'cityName' => '',
  707. 'accountIdNo' => '', 'accountIdStart' => '', 'accountIdEnd' => '', 'attachments' => [],
  708. ],
  709. 'shop' => [
  710. 'name' => $merchantName, 'provinceCode' => '', 'cityCode' => '', 'countyCode' => '',
  711. 'provinceName' => $shop->province ?? '', 'cityName' => $shop->city ?? '',
  712. 'countyName' => $shop->dist ?? '', 'address' => $shop->address ?? ($shop->fullAddress ?? ''),
  713. 'contactName' => '', 'contactMobile' => $mobile, 'longitude' => $shop->long ?? ($shop->shopLong ?? ''),
  714. 'latitude' => $shop->lat ?? ($shop->shopLat ?? ''), 'attachments' => [],
  715. ],
  716. 'business' => ['signMobile' => $mobile ?: '15280215347', 'rate' => self::RATE, 'settleType' => 'D1', 'settlementType' => 'AUTOMATIC'],
  717. ];
  718. }
  719. /**
  720. * 商户请求数据构建
  721. * 请参照拉卡拉"新增商户进件"接口文档进行核实参数
  722. * @param array $form
  723. * @param string $contractNo
  724. * @param LakalaOnboardingClient $client
  725. * @return array
  726. */
  727. private static function merchantRequest(array $form, $contractNo, $client)
  728. {
  729. $l = $form['license']; //营业执照信息
  730. $m = $form['merchant']; //商家信息
  731. $s = $form['settlement']; //收款银行卡信息
  732. $shop = $form['shop']; //门店信息(带有门店图片信息)
  733. $fees = self::merchantFees();
  734. return [
  735. 'org_code' => $client->orgCode(),
  736. 'user_no' => $client->userNo(),
  737. 'email' => $m['email'],
  738. 'busi_code' => 'WECHAT_PAY',
  739. 'mer_reg_name' => $m['businessName'],
  740. 'mer_type' => $l['merchantType'],
  741. 'mer_name' => $m['businessName'],
  742. 'mer_addr' => $m['address'],
  743. 'province_code' => $m['provinceCode'],
  744. 'city_code' => $m['cityCode'],
  745. 'county_code' => $m['countyCode'],
  746. 'license_name' => $l['licenseName'],
  747. 'license_no' => $l['licenseNo'],
  748. 'license_dt_start' => $l['licenseStart'],
  749. 'license_dt_end' => $l['licenseEnd'],
  750. 'latitude' => $shop['longitude'],
  751. 'longtude' => $shop['latitude'],
  752. 'source' => 'APP',
  753. 'business_content' => $l['businessContent'],
  754. 'is_legal_person' => (bool)$s['isLegalPerson'],
  755. 'lar_name' => $l['legalName'], 'lar_id_type' => '01', 'lar_id_card' => $l['legalIdNo'],
  756. 'lar_id_card_start' => date('Y-m-d', strtotime($l['legalIdStart'])),
  757. 'lar_id_card_end' => date('Y-m-d', strtotime($l['legalIdEnd'])),
  758. 'contact_mobile' => $shop['contactMobile'] != '' ? $shop['contactMobile'] : $m['contactMobile'],
  759. 'contact_name' => $l['legalName'] != '' ? $l['legalName'] : ($shop['contactName'] ?? $m['contactName']),
  760. 'openning_bank_code' => $s['bankNo'],
  761. 'openning_bank_name' => $s['bankName'],
  762. 'clearing_bank_code' => $s['clearingBankNo'], 'settle_province_code' => $s['provinceCode'],
  763. 'settle_province_name' => $s['provinceName'], 'settle_city_code' => $s['cityCode'],
  764. 'settle_city_name' => $s['cityName'], 'account_no' => $s['accountNo'], 'account_name' => $s['accountName'],
  765. 'account_type' => $s['accountType'], 'account_id_card' => $s['accountIdNo'] ?: $l['legalIdNo'],
  766. 'account_id_dt_start' => $s['accountIdStart'] ?: $l['legalIdStart'],
  767. 'account_id_dt_end' => $s['accountIdEnd'] ?: $l['legalIdEnd'],
  768. 'settle_type' => 'D1', 'settlement_type' => 'AUTOMATIC', 'contract_no' => $contractNo,
  769. 'biz_content' => ['term_num' => '1', 'mcc' => self::MCC, 'activity_id' => self::activityId(), 'fees' => $fees], //业务拓展信息 -- 会创建终端(此处创建:专业化扫码)
  770. 'attchments' => self::attachments($form),
  771. ];
  772. }
  773. private static function contractRequest(array $form, $orderNo, $orgCode)
  774. {
  775. $l = $form['license'];
  776. $m = $form['merchant'];
  777. $s = $form['settlement'];
  778. $shop = $form['shop'];
  779. $b = $form['business'];
  780. $content = [];
  781. foreach (['A' => 130, 'B' => 58, 'C' => 56, 'D' => 17, 'E' => 79] as $prefix => $max) {
  782. for ($i = 1; $i <= $max; $i++) {
  783. $content[$prefix . $i] = '/';
  784. }
  785. }
  786. $content = array_merge($content, [
  787. 'A1' => $l['legalName'], 'A2' => '是', 'A6' => '是',
  788. 'A7' => '0.50', 'A8' => '20', 'A9' => '是', 'A10' => '0.60',
  789. 'A11' => '是', 'A12' => self::RATE, 'A13' => '是', 'A14' => self::RATE,
  790. 'A15' => '0.60', 'A34' => self::RATE, 'A35' => self::RATE,
  791. 'A63' => '是', 'A64' => self::RATE, 'A65' => '是', 'A66' => self::RATE,
  792. 'A116' => '自动结算',
  793. 'A117' => '是',
  794. 'A121' => ($shop['provinceName'] ?? '') . ($shop['cityName'] ?? ''),
  795. 'A124' => date('Y'), 'A125' => date('m'), 'A126' => date('d'), // 甲方签约 年/月/日
  796. 'A127' => date('Y'), 'A128' => date('m'), 'A129' => date('d'), // 乙方签约 年/月/日
  797. 'A130' => $l['legalName'], // 甲方电子签名/签字/盖章
  798. // ===== B 系列:商户注册登记 / 基本信息 / 结算账户 / 法人联系人 / 终端 / 拓展 =====
  799. 'B1' => date('Y'), 'B2' => date('m'), // 商户注册登记表 年/月
  800. 'B3' => '是',
  801. 'B8' => $l['licenseName'] ?? '/', // 基本信息:工商注册名称(中英文)
  802. 'B10' => $m['businessName'], // 基本信息:对外经营名称(中英文)
  803. 'B13' => ($m['provinceName'] ?? '') . ($m['cityName'] ?? ''),
  804. 'B14' => $l['licenseNo'] ?: '/',
  805. 'B16' => '/', //结算账户名称:同商户工商注册名称选择
  806. 'B17' => '/', //结算账户名称:其他名称选择
  807. 'B18' => $s['accountName'] ?? $l['legalName'], //结算账户名称:其他名称填写
  808. 'B19' => $s['bankName'], //开户行(含支行)
  809. 'B20' => $s['accountNo'], //结算账号
  810. 'B24' => $l['legalName'], //法定代表人/负责人姓名 TODO 是谁
  811. 'B25' => $l['licenseNo'], //法定代表人/负责人证件类型及号码 TODO 是谁
  812. 'B26' => $b['signMobile'], //法定代表人/负责人手机 TODO 是谁
  813. 'B27' => $l['legalName'],
  814. 'B28' => $m['email'],
  815. 'B29' => $l['legalIdNo'],// 联系人证件号码
  816. 'B30' => $b['signMobile'],// 联系人手机号
  817. 'B31' => '/', //分店营业名称(中英文)
  818. 'B32' => $shop['contactName'],// 网点联系人姓名
  819. 'B33' => $m['address'],
  820. 'B34' => $shop['contactMobile'], // 网点联系人手机号
  821. // ===== D 系列:各类授权书 =====
  822. 'D1' => $s['bankName'],// 授权书(银行):结算账户所属银行
  823. 'D2' => $s['accountName'] ?? $l['legalName'],// 授权书(银行):授权承诺方签字 TODO 是谁
  824. 'D3' => date('Y/m/d'),// 授权书(银行):签章/签字日期
  825. 'D4' => $s['accountName'] ?? $l['legalName'],// 授权书(开通微信、支付宝收单):承诺授权方签名 TODO 是谁
  826. 'D5' => date('Y/m/d'),// 授权书(开通微信、支付宝收单):盖章/签字日期
  827. // ===== E 系列:分银行费率明细(费率一致则不填,统一占位 "/")=====
  828. 'E78' => $s['accountName'] ?? $l['legalName'],// 电子签约服务授权委托书:承诺授权方签名 TODO 是谁
  829. 'E79' => $s['accountName'] ?? $l['legalName'],// 电子签约服务授权委托书:承诺签名或盖章 TODO 是谁
  830. ]);
  831. if ($s['isLegalPerson'] == false) { // 非法人
  832. $content = array_merge($content, [
  833. 'D6' => $l['legalName'], //授权书(数据处理方):数据处理方名称
  834. 'D7' => $shop['contactMobile'], //授权书(数据处理方):数据处理方联系方式
  835. 'D8' => $s['accountName'], //授权书(数据处理方):承诺授权方签名
  836. 'D9' => date('Y/m/d'),
  837. 'D10' => $s['accountName'], //授权书(外卡业务):承诺授权方签名
  838. 'D11' => date('Y/m/d'),
  839. ]);
  840. }
  841. return [
  842. 'order_no' => $orderNo,
  843. 'org_id' => (int)$orgCode,
  844. 'ec_type_code' => 'EC015',
  845. 'cert_type' => 'RESIDENT_ID',
  846. 'cert_name' => $l['legalName'],
  847. 'cert_no' => $l['legalIdNo'],
  848. 'mobile' => $b['signMobile'],
  849. 'business_license_no' => $l['licenseNo'],
  850. 'business_license_name' => $l['licenseName'],
  851. 'openning_bank_code' => $s['bankNo'],
  852. 'openning_bank_name' => $s['bankName'],
  853. 'acct_type_code' => $s['accountType'],
  854. 'acct_no' => $s['accountNo'],
  855. 'acct_name' => $s['accountName'],
  856. 'ec_content_parameters' => self::encode($content),
  857. 'remark' => '销花宝商户入网',
  858. ];
  859. }
  860. private static function settlementDto(array $s)
  861. {
  862. return [
  863. 'account_kind' => $s['accountType'], 'is_legal_person' => (bool)$s['isLegalPerson'],
  864. 'account_name' => $s['accountName'], 'identity_no' => $s['accountIdNo'],
  865. 'account_no' => $s['accountNo'], 'bank_no' => $s['bankNo'], 'bank_name' => $s['bankName'],
  866. 'clearing_bank_no' => $s['clearingBankNo'], 'settle_province_code' => $s['provinceCode'],
  867. 'settle_province_name' => $s['provinceName'], 'settle_city_code' => $s['cityCode'],
  868. 'settle_city_name' => $s['cityName'], 'attachments' => self::attachmentDtos($s['attachments'] ?? []),
  869. ];
  870. }
  871. private static function merchantFees()
  872. {
  873. return [
  874. ['fee_code' => 'WECHAT', 'fee_value' => self::RATE], //微信
  875. ['fee_code' => 'ALIPAY', 'fee_value' => self::RATE], //支付宝
  876. //['fee_code' => 'SCAN_PAY', 'fee_value' => self::RATE], //未知项
  877. ['fee_code' => 'DEBIT_CARD', 'fee_value' => '0.50', 'top_fee' => '20'], //借记卡费率
  878. ['fee_code' => 'CREDIT_CARD', 'fee_value' => '0.60'], //贷记卡费率
  879. ['fee_code' => 'UNIONPAY_WALLET_DEBIT_FEE', 'fee_value' => self::RATE, 'top_fee' => '20'], //银联二维码借记卡
  880. ['fee_code' => 'UNIONPAY_WALLET_CREDIT_FEE', 'fee_value' => '0.60'], //银联二维码贷记卡
  881. ['fee_code' => 'YL_DISCOUNT_CREDIT_FEE', 'fee_value' => self::RATE] // 银联二维码贷记-优惠
  882. ];
  883. }
  884. private static function activityId()
  885. {
  886. return (string)(Yii::$app->params['lakalaOnboarding']['activity_id'] ?? getenv('LAKALA_ONBOARDING_ACTIVITY_ID') ?: self::ACTIVITY_ID);
  887. }
  888. private static function termFees()
  889. {
  890. return [
  891. ['fee' => self::RATE, 'fee_type' => 'WECHAT'],
  892. ['fee' => self::RATE, 'fee_type' => 'ALIPAY'],
  893. ['fee' => 0.5, 'top_fee' => 20, 'fee_type' => 'DEBIT_CARD'],
  894. ['fee' => 0.6, 'fee_type' => 'CREDIT_CARD'],
  895. ['fee' => self::RATE, 'fee_type' => 'UNIONPAY_WALLET_DEBIT_FEE'], //银联二维码借记卡
  896. ['fee' => '0.60', 'fee_type' => 'UNIONPAY_WALLET_CREDIT_FEE'], //银联二维码贷记卡
  897. ['fee' => self::RATE, 'fee_type' => 'YL_DISCOUNT_CREDIT_FEE'] //银联二维码贷记-优惠
  898. ];
  899. }
  900. private static function attachments(array $form)
  901. {
  902. $all = [];
  903. foreach (['license', 'settlement', 'shop'] as $section) {
  904. $all = array_merge($all, self::attachmentDtos($form[$section]['attachments'] ?? [], true));
  905. }
  906. return $all;
  907. }
  908. private static function attachmentDtos(array $attachments, $merchantShape = false)
  909. {
  910. $result = [];
  911. foreach ($attachments as $attachment) {
  912. $lakalaUrl = self::attachmentLakalaUrl($attachment);
  913. if ($lakalaUrl === '' || empty($attachment['imgType'])) {
  914. continue;
  915. }
  916. $result[] = $merchantShape
  917. ? ['id' => $lakalaUrl, 'type' => $attachment['imgType']]
  918. : ['img_path' => $lakalaUrl, 'img_type' => $attachment['imgType']];
  919. }
  920. return $result;
  921. }
  922. private static function attachmentLakalaUrl(array $attachment)
  923. {
  924. return trim((string)($attachment['lakalaUrl'] ?? ($attachment['url'] ?? '')));
  925. }
  926. private static function validateContract(array $form)
  927. {
  928. self::validateOnboarding($form, false);
  929. if (empty($form['business']['signMobile'])) {
  930. util::fail('请填写签约手机号');
  931. }
  932. }
  933. private static function validateOnboarding(array $form, $includeShop = true)
  934. {
  935. $l = $form['license'] ?? []; $m = $form['merchant'] ?? []; $s = $form['settlement'] ?? []; $shop = $form['shop'] ?? [];
  936. $items = ['merchantType', 'legalName', 'legalIdNo', 'legalIdStart', 'legalIdEnd', 'businessContent'];
  937. if ($l['merchantType'] === 'TP_PERSONAL') {
  938. unset($items[5]);
  939. }
  940. foreach ($items as $key) {
  941. if (empty($l[$key])) util::fail('执照信息未填写完整');
  942. }
  943. self::requireAttachments($l['attachments'] ?? [], ['ID_CARD_FRONT', 'ID_CARD_BEHIND'], '请上传法人身份证正反面');
  944. if (($l['merchantType'] ?? '') === 'TP_MERCHANT' && (empty($l['licenseName']) || empty($l['licenseNo']))) {
  945. util::fail('企业商户请填写营业执照信息');
  946. }
  947. if (($l['merchantType'] ?? '') === 'TP_MERCHANT') {
  948. self::requireAttachments($l['attachments'] ?? [], ['BUSINESS_LICENCE'], '请上传营业执照');
  949. }
  950. foreach (['businessName', 'provinceCode', 'cityCode', 'countyCode', 'address'] as $key) {
  951. if (empty($m[$key])) util::fail('商户信息未填写完整');
  952. }
  953. self::validateSettlement($s);
  954. if ($includeShop) {
  955. foreach (['name', 'address', 'contactName', 'contactMobile', 'longitude', 'latitude'] as $key) {
  956. if (empty($shop[$key])) util::fail('门店信息未填写完整');
  957. }
  958. self::requireAttachments($shop['attachments'] ?? [], ['SHOP_OUTSIDE_IMG', 'SHOP_INSIDE_IMG', 'CHECKSTAND_IMG'], '请上传完整门店照片');
  959. }
  960. }
  961. private static function validateSettlement(array $s)
  962. {
  963. foreach (['accountType', 'accountName', 'accountNo', 'bankName', 'bankNo', 'clearingBankNo', 'provinceCode', 'cityCode'] as $key) {
  964. if (empty($s[$key])) util::fail('结算信息未填写完整');
  965. }
  966. if (($s['accountType'] ?? '58') === '57') {
  967. self::requireAttachments($s['attachments'] ?? [], ['OPENING_PERMIT'], '请上传开户许可证');
  968. } else {
  969. self::requireAttachments($s['attachments'] ?? [], ['BANK_CARD'], '请上传银行卡照片');
  970. if (empty($s['isLegalPerson'])) {
  971. self::requireAttachments($s['attachments'] ?? [], ['ID_CARD_FRONT', 'ID_CARD_BEHIND', 'LETTER_OF_AUTHORIZATION'], '请上传结算人证件与法人授权函');
  972. }
  973. }
  974. }
  975. private static function requireAttachments(array $attachments, array $types, $message)
  976. {
  977. $present = array_column($attachments, 'imgType');
  978. foreach ($types as $type) {
  979. if (!in_array($type, $present, true)) {
  980. util::fail($message);
  981. }
  982. $matched = array_values(array_filter($attachments, function ($attachment) use ($type) {
  983. return ($attachment['imgType'] ?? '') === $type && self::attachmentLakalaUrl($attachment) !== '';
  984. }));
  985. if (empty($matched)) {
  986. util::fail($message);
  987. }
  988. }
  989. }
  990. private static function version($application, array $request, array $response)
  991. {
  992. LakalaApplicationVersionClass::add([
  993. 'applicationId' => $application->id,
  994. 'mainId' => $application->mainId,
  995. 'versionNo' => $application->versionNo,
  996. 'type' => $application->type,
  997. 'formData' => $application->formData,
  998. 'requestData' => self::encode($request),
  999. 'responseData' => self::encode($response),
  1000. 'createTime' => date('Y-m-d H:i:s'),
  1001. ]);
  1002. }
  1003. private static function operation($application, $accountId, $type, $bizType, array $request, array $response, $status, $error = '')
  1004. {
  1005. return LakalaOperationClass::add([
  1006. 'applicationId' => $application->id,
  1007. 'accountId' => $accountId,
  1008. 'mainId' => $application->mainId,
  1009. 'operationType' => $type,
  1010. 'bizType' => $bizType,
  1011. 'status' => $status,
  1012. 'reviewRelatedId' => $response['data']['review_related_id'] ?? '',
  1013. 'requestData' => self::encode($request),
  1014. 'responseData' => self::encode($response),
  1015. 'errorMessage' => mb_substr($error, 0, 500),
  1016. 'createTime' => date('Y-m-d H:i:s'),
  1017. 'updateTime' => date('Y-m-d H:i:s'),
  1018. ], true);
  1019. }
  1020. private static function application($id, $mainId, $shopId, $object = false)
  1021. {
  1022. $application = LakalaApplicationClass::getByCondition(['id' => $id, 'mainId' => $mainId, 'shopId' => $shopId], $object);
  1023. if (empty($application)) {
  1024. util::fail('没有找到进件申请');
  1025. }
  1026. return $application;
  1027. }
  1028. private static function merchantInfoTermMap($value)
  1029. {
  1030. $result = [];
  1031. $terminalInfos = $value['terminal_info'] ?? [];
  1032. foreach ($terminalInfos as $term) {
  1033. if (($term['term_type_name'] ?? '') == '专业化扫码') {
  1034. $termNo = trim((string)($term['active_no_vo_list'][0]['term_no'] ?? ''));
  1035. if ($termNo !== '') {
  1036. $result['scanTermNo'][] = $termNo;
  1037. }
  1038. }
  1039. if (($term['term_type_name'] ?? '') == '聚合收银台') {
  1040. $list = $term['active_no_vo_list'] ?? [];
  1041. foreach ($list as $item) {
  1042. if (($item['busi_type_code'] ?? '') == 'QR_CODE_CARD') {
  1043. $termNo = trim((string)($item['term_no'] ?? ''));
  1044. if ($termNo !== '') {
  1045. $result['b2BTermNo'][] = $termNo;
  1046. }
  1047. }
  1048. }
  1049. }
  1050. }
  1051. return $result;
  1052. }
  1053. private static function imageExtension($mime)
  1054. {
  1055. $map = [
  1056. 'image/jpeg' => 'jpg',
  1057. 'image/png' => 'png',
  1058. 'image/gif' => 'gif',
  1059. 'image/webp' => 'webp',
  1060. ];
  1061. return $map[$mime] ?? 'png';
  1062. }
  1063. private static function encode($value)
  1064. {
  1065. return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  1066. }
  1067. private static function decode($value)
  1068. {
  1069. if (is_array($value)) return $value;
  1070. $decoded = json_decode((string)$value, true);
  1071. return is_array($decoded) ? $decoded : [];
  1072. }
  1073. private static function formatFormImages(array $form)
  1074. {
  1075. return self::formatOssPathFields($form);
  1076. }
  1077. private static function formatOssPathFields(array $data)
  1078. {
  1079. foreach ($data as $key => $value) {
  1080. if (is_array($value)) {
  1081. $data[$key] = self::formatOssPathFields($value);
  1082. continue;
  1083. }
  1084. if ($key === 'ossPath' && is_string($value) && $value !== '') {
  1085. $data[$key] = self::formatOssPath($value);
  1086. }
  1087. }
  1088. return $data;
  1089. }
  1090. private static function formatOssPath($path)
  1091. {
  1092. if (strpos($path, 'http://') === 0 || strpos($path, 'https://') === 0) {
  1093. return $path;
  1094. }
  1095. return imgUtil::groupImg($path);
  1096. }
  1097. }