DouYinService.php 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. <?php
  2. namespace app\api\service;
  3. use app\adminapi\logic\external\ExternalConsultationLogic;
  4. use app\api\logic\ServiceOrderLogic;
  5. use app\common\enum\PayEnum;
  6. use app\common\logic\PayNotifyLogic;
  7. use app\common\model\Config;
  8. use app\common\model\external\DouyinOrder;
  9. use app\common\model\external\DouyinRefundOrder;
  10. use app\common\model\external\DouyinUserAuth;
  11. use app\common\model\external\ExternalConsultation;
  12. use app\common\model\external\ExternalConsultationOrder;
  13. use app\common\model\external\ExternalPlatformGoods;
  14. use app\common\model\goods\Goods;
  15. use app\common\model\goods_category\GoodsCategory;
  16. use app\common\model\recharge\RechargeOrder;
  17. use app\common\model\user\User;
  18. use app\common\model\user\UserAuth;
  19. use app\common\model\works\ServiceWork;
  20. use app\common\model\works\ServiceWorkLog;
  21. use app\common\service\ConfigService;
  22. use app\common\service\FileService;
  23. use GuzzleHttp\Client;
  24. use think\facade\Db;
  25. use think\facade\Log;
  26. class DouYinService
  27. {
  28. protected static int $terminal = \app\common\enum\user\UserTerminalEnum::DOUYIN;
  29. protected static int $external_platform_id = 6;
  30. protected CONST EXTERNAL_PLATFORM_ID = 6;
  31. // ********************************* 注册登录
  32. public static function register(array $params)
  33. {
  34. $userSn = User::createUserSn();
  35. $params['password'] = !empty($params['password'])?$params['password']:rand(100000,999999);
  36. $passwordSalt = \think\facade\Config::get('project.unique_identification');
  37. $password = create_password($params['password'], $passwordSalt);
  38. $avatar = ConfigService::get('default_image', 'user_avatar');
  39. $user = User::create([
  40. 'sn' => $userSn,
  41. 'avatar' => $avatar,
  42. 'nickname' => '用户' . $userSn,
  43. 'account' => $params['account'],
  44. 'mobile' => !empty($params['mobile'])?$params['mobile']:'',
  45. 'password' => $password,
  46. 'channel' => self::$terminal,
  47. 'user_type' => $params['user_type']??0,
  48. ]);
  49. return $user;
  50. }
  51. public static function phoneLogin(array $params)
  52. {
  53. try {
  54. $where = ['mobile' => $params['mobile']];
  55. $params['account'] = $params['mobile'];
  56. $user = User::where($where)->findOrEmpty();
  57. if ($user->isEmpty()) {
  58. //直接注册用户
  59. $params['channel'] = self::$terminal;
  60. $user = self::register($params);
  61. }
  62. //更新登录信息
  63. $user->login_time = time();
  64. $user->login_ip = request()->ip();
  65. $user->save();
  66. $userInfo = UserTokenService::setToken($user->id, self::$terminal);
  67. //返回登录信息
  68. $avatar = $user->avatar ?: Config::get('project.default_image.user_avatar');
  69. $avatar = FileService::getFileUrl($avatar);
  70. return [
  71. 'nickname' => $userInfo['nickname'],
  72. 'sn' => $userInfo['sn'],
  73. 'mobile' => $userInfo['mobile'],
  74. 'avatar' => $avatar,
  75. 'token' => $userInfo['token'],
  76. ];
  77. } catch (\Exception $e) {
  78. throw new \Exception($e->getMessage());
  79. }
  80. }
  81. public static function getDouyinUserByOpenId(array $openId)
  82. {
  83. try {
  84. $user = DouyinUserAuth::where('openid',$openId)->findOrEmpty();
  85. if ($user->isEmpty()) {
  86. //直接注册用户
  87. $params['channel'] = self::$terminal;
  88. $user = self::register($params);
  89. }
  90. //更新登录信息
  91. $user->login_time = time();
  92. $user->login_ip = request()->ip();
  93. $user->save();
  94. $userInfo = UserTokenService::setToken($user->id, self::$terminal);
  95. //返回登录信息
  96. $avatar = $user->avatar ?: Config::get('project.default_image.user_avatar');
  97. $avatar = FileService::getFileUrl($avatar);
  98. return [
  99. 'nickname' => $userInfo['nickname'],
  100. 'sn' => $userInfo['sn'],
  101. 'mobile' => $userInfo['mobile'],
  102. 'avatar' => $avatar,
  103. 'token' => $userInfo['token'],
  104. ];
  105. } catch (\Exception $e) {
  106. throw new \Exception($e->getMessage());
  107. }
  108. }
  109. // **************************** 商品管理 goods_category_id goods_id external_platform_id
  110. public static function addProduct($params)
  111. {
  112. $send_url = env('internal_api.api_url_host').'platf/dou_yin/addGoods';
  113. $res = http_request($send_url,http_build_query($params));
  114. Log::info('addProduct:'
  115. .'url:'.$send_url
  116. .'|data:'.json_encode($params,JSON_UNESCAPED_UNICODE)
  117. .'|res:'.json_encode([$res],JSON_UNESCAPED_UNICODE)
  118. );
  119. return $res?:[];
  120. }
  121. // ******************************** 订单业务
  122. public static function getOrderDetail($params)
  123. {
  124. //抖音订单信息/商品信息/预约信息(地址、时间、履约状态与信息)
  125. // $params['order_number'] user_id
  126. $order = DouyinOrder::with(['goods','serviceWork','douyinRefundOrder'])->where('order_number', $params['order_number'])->where('user_id', $params['user_id'])->findOrEmpty();
  127. if($order->isEmpty()){
  128. return [];
  129. }
  130. $orderInfo = $order->toArray();
  131. empty($orderInfo['goods']) && $orderInfo['goods'] = [];
  132. empty($orderInfo['serviceWork']) && $orderInfo['serviceWork'] = [];
  133. if($orderInfo['serviceWork'] && $orderInfo['serviceWork']['service_status'] == 3 && in_array($orderInfo['order_status'],[1,2])){
  134. $order->order_status = 4;
  135. $order->pay_status = 1;
  136. $order->save();
  137. }
  138. empty($orderInfo['douyinRefundOrder']) && $orderInfo['douyinRefundOrder'] = [];
  139. $orderInfo['book_info'] = json_decode($orderInfo['book_info']?:'{}',true);
  140. $rechargeOrder = RechargeOrder::where(['work_id'=>$orderInfo['work_id']?:0,'payment_type'=>2])->findOrEmpty();
  141. if($rechargeOrder->isEmpty()){
  142. $orderInfo['tail_order'] = [];
  143. }else{
  144. $orderInfo['tail_order'] = $rechargeOrder->toArray();
  145. // 尾款未支付时 展示尾单信息加入临时总金额用于展示
  146. if(isset($orderInfo['tail_order']['pay_status']) && $orderInfo['tail_order']['pay_status'] == 0){
  147. $orderInfo['total_amount'] += $orderInfo['tail_order']['order_amount'];
  148. }
  149. }
  150. $work_status = $orderInfo['serviceWork']['work_status']??0;
  151. $performance = [];
  152. // tmp
  153. switch ($work_status){
  154. case 0:
  155. $performance[] = ['status' => '待派单','title' => '待派单','time' => date('Y-m-d H:i:s',time())];
  156. break;
  157. case 1:
  158. case 2:
  159. case 3:
  160. $performance[] = ['status' => '待派单','title' => '待派单','time' => date('Y-m-d H:i:s',time())];
  161. $performance[] = ['status' => '已派单','title' => '已派单','time' => date('Y-m-d H:i:s',time())];
  162. break;
  163. case 4:
  164. case 5:
  165. case 6:
  166. $performance[] = ['status' => '待派单','title' => '待派单','time' => date('Y-m-d H:i:s',time())];
  167. $performance[] = ['status' => '已派单','title' => '已派单','time' => date('Y-m-d H:i:s',time())];
  168. $performance[] = ['status' => '服务中','title' => '服务中','time' => date('Y-m-d H:i:s',time())];
  169. break;
  170. case 7:
  171. case 8:
  172. $performance[] = ['status' => '待派单','title' => '待派单','time' => date('Y-m-d H:i:s',time())];
  173. $performance[] = ['status' => '已派单','title' => '已派单','time' => date('Y-m-d H:i:s',time())];
  174. $performance[] = ['status' => '服务中','title' => '服务中','time' => date('Y-m-d H:i:s',time())];
  175. $performance[] = ['status' => '已完结','title' => '已完结','time' => date('Y-m-d H:i:s',time())];
  176. break;
  177. }
  178. $orderInfo['performance'] = $performance;
  179. return $orderInfo;
  180. }
  181. public static function createOrder($params)
  182. {
  183. try {
  184. $goods_id = $params['goods_id']??0;
  185. $open_id = '_000o7ntqTR--_hCTBOBCSR_NJkyp_hiqlEK';
  186. $goods = Goods::where('id',$goods_id)->findOrEmpty();
  187. if($goods->isEmpty()){
  188. throw new \Exception('商品不存在!');
  189. }
  190. $goods = $goods->toArray();
  191. $platformGoods = ExternalPlatformGoods::where('goods_id', $goods_id)->where('external_platform_id', self::EXTERNAL_PLATFORM_ID)->findOrEmpty();
  192. if($platformGoods->isEmpty()){
  193. throw new \Exception('外部商品不存在!');
  194. }
  195. $platformGoods = $platformGoods->toArray();
  196. $quantity = $params['quantity']?:1;
  197. $appointment_time = strtotime($params['appointment_time']??date('Y-m-d H:i:s',time()));
  198. $bookStartTime = $appointment_time * 1000;
  199. $bookEndTime = ($appointment_time + (2 * 86400)) * 1000;
  200. $order_number = self::submitOrder([
  201. 'open_id'=>$open_id,
  202. 'goods_id'=>$goods_id,
  203. 'user_id'=>$params['user_id']??0,
  204. 'mobile'=>$params['user_info']['mobile']??'',
  205. 'quantity'=>$quantity
  206. ]);
  207. $data = [
  208. "total_amount" => $platformGoods['service_fee'] * $quantity * 100,
  209. "open_id" => $open_id,
  210. "out_order_no" => $order_number,
  211. "order_entry_schema" => ['path'=>'pages/order/detail','params'=>json_encode(['order_number'=>$order_number??0])],
  212. "cp_extra" => json_encode([
  213. "outShopId" => self::EXTERNAL_PLATFORM_ID,
  214. "skuId" => (string)$platformGoods['external_goods_sn'],
  215. "quantity" => $quantity,
  216. "user_id" => $params['user_id'],
  217. "user_address" => $params['user_address'],
  218. "lon" => $params['lon'],
  219. "lat" => $params['lat'],
  220. "appointment_time" => $params['appointment_time']
  221. ]),
  222. "goods_list" => [
  223. [
  224. "goods_id_type" => 1,
  225. "goods_id" => (string)$platformGoods['external_goods_sn'],
  226. "quantity" => 1
  227. ]
  228. ],
  229. "goods_book_info" => [
  230. "book_type" => 2,
  231. "cancel_policy" => 3,
  232. "cancel_advance_hour" => 5
  233. ],
  234. ];
  235. // 服务器向抖音发起创建订单
  236. $url = 'api/apps/trade/v2/order/create_order';
  237. $resData = self::toDyRequestUrl($url,$data);
  238. return $resData;
  239. } catch (\Exception $e) {
  240. throw new \Exception($e->getMessage());
  241. }
  242. }
  243. // 预下单接口 - 前端 首次/尾款
  244. public static function getPluginCreateOrderData($goods_id, $quantity, $douyinOrderId,$params)
  245. {
  246. try {
  247. $goods = Goods::where('id',$goods_id)->findOrEmpty();
  248. if($goods->isEmpty()){
  249. throw new \Exception('商品不存在!');
  250. }
  251. $goods = $goods->toArray();
  252. $platformGoods = ExternalPlatformGoods::where('goods_id', $goods_id)->where('external_platform_id', self::EXTERNAL_PLATFORM_ID)->findOrEmpty();
  253. if($platformGoods->isEmpty()){
  254. throw new \Exception('外部商品不存在!');
  255. }
  256. $platformGoods = $platformGoods->toArray();
  257. $quantity = $quantity?:1;
  258. $appointment_time = strtotime($params['appointment_time']??date('Y-m-d H:i:s',time()));
  259. $bookStartTime = $appointment_time * 1000;
  260. $bookEndTime = ($appointment_time + (2 * 86400)) * 1000;
  261. $data = [
  262. /*"goodsList" => [
  263. "quantity" => $quantity,
  264. "price" => $platformGoods['service_fee'] * 100,
  265. "goodsName" => $goods['goods_name'],
  266. "goodsPhoto" => $goods['goods_image']??'',
  267. "goodsId" => '',
  268. "goodsType" => 1
  269. ],*/
  270. "skuList" => [
  271. [
  272. "quantity" => (int)$quantity,
  273. "skuId" => (string)$platformGoods['external_goods_sn'],
  274. "skuType" => 1, // 1:商品库商品 2:非商品库商品(融合预约品走加价时,固定传2)
  275. "price" => $platformGoods['service_fee'] * 100,
  276. ]
  277. ],
  278. "bookInfo" => [
  279. "itemBookInfoList"=>[
  280. [
  281. "poiId" => '7511543640776017961',
  282. "shopName" => '亿蜂快修·武汉市',
  283. "outShopId" => (string)self::EXTERNAL_PLATFORM_ID,
  284. "skuId" => (string)$platformGoods['external_goods_sn'],
  285. "bookStartTime" => $bookStartTime?:'',
  286. "bookEndTime" => $bookEndTime?:'',
  287. ]
  288. ]
  289. ],
  290. "payment" => [
  291. "totalAmount" => $quantity * $platformGoods['service_fee'] * 100,
  292. ],
  293. //"callbackUrl" => $callbackUrl?:'',
  294. "callbackData" => [
  295. "outShopId" => self::EXTERNAL_PLATFORM_ID,
  296. "skuId" => (string)$platformGoods['external_goods_sn'],
  297. "quantity" => $quantity,
  298. "user_id" => $params['user_id'],
  299. "douyinOrderId" => (string)$douyinOrderId?:'',
  300. "user_name" => $params['user_name']??'',
  301. "mobile" => $params['mobile']??'',
  302. "user_address" => $params['user_address']??'',
  303. "lon" => $params['lon']??'',
  304. "lat" => $params['lat']??'',
  305. "appointment_time" => $params['appointment_time']??''
  306. ],
  307. /*"tradeOption" => json_encode([
  308. "life_trade_flag" => 1,
  309. "order_relation_info" => [
  310. "related_order_id" => 0, // 加价时上个订单号
  311. "relation_type" => 'multi_buy_as_one'
  312. ]
  313. ])*/
  314. ];
  315. if($douyinOrderId){ // 说明是来自首单订单即要创建尾款
  316. unset($data['skuList']);
  317. unset($data['bookInfo']);
  318. unset($data['payment']);
  319. $douyinOrder = DouyinOrder::where(['user_id'=>$params['user_id'],'dy_order_id'=>$douyinOrderId])->findOrEmpty();
  320. $rechargeOrder = RechargeOrder::where(['work_id'=>$douyinOrder->work_id,'payment_type'=>2])->findOrEmpty();
  321. if(empty($rechargeOrder->order_amount)) throw new \Exception('尾款报价不存在!');
  322. $data['skuList'] = [
  323. [
  324. "quantity" => (int)$quantity,
  325. "skuId" => (string)$goods_id,
  326. "skuType" => 2, // 1:商品库商品 2:非商品库商品(融合预约品走加价时,固定传2)
  327. "price" => $rechargeOrder->order_amount * 100,
  328. "goodsInfo" => [
  329. "goodsName"=>$goods['goods_name'],
  330. "goodsPhoto"=>$goods['goods_image'], // 商品图片链接 必填
  331. "goodsId"=> (string)$goods_id,
  332. "goodsType"=>2
  333. ],
  334. "extraInfo" =>["feeType"=>14]
  335. ]
  336. ];
  337. $data['payment'] = [
  338. "totalAmount" => $rechargeOrder->order_amount * 100,
  339. ];
  340. //$data['callbackUrl'] = env('douyin.pay_tail_notify_url')??'';
  341. $bookInfo = json_decode($douyinOrder->book_info,true);
  342. $item_order_id = $bookInfo['item_order_info_list'][0]['item_order_id']??$douyinOrderId;
  343. $data['tradeOption'] = [
  344. "life_trade_flag" => 1,
  345. "trade_mode" => '3',
  346. "order_relation_info" => [
  347. "related_order_id" => (string)$item_order_id, // 加价时上个订单号
  348. "relation_type" => 'multi_buy_as_one'
  349. ]
  350. ];
  351. }
  352. /*else{ // 创建首单订单
  353. $data['callbackUrl'] = '';
  354. }*/
  355. return $data;
  356. } catch (\Exception $e) {
  357. throw new \Exception($e->getMessage());
  358. }
  359. }
  360. // ++++++++++++++++++++ 首次/尾款 扩展点
  361. /**
  362. * 预下单扩展点
  363. * @param $params
  364. * @throws \Exception
  365. * @author liugc <466014217@qq.com>
  366. * @date 2025/6/4 14:03
  367. */
  368. public static function submitOrderNotify($params = [])
  369. {
  370. try {
  371. $params['external_platform_id'] = self::EXTERNAL_PLATFORM_ID;
  372. // order_id goods total_amount discount cp_extra create_order_time phone_num contact_name open_id
  373. $params['cp_extra'] = json_decode($params['cp_extra'], true);
  374. $user_id = $params['cp_extra']['user_id'];
  375. $user = User::where('id',$user_id)->findOrEmpty();
  376. if($params['cp_extra']['douyinOrderId']>0){ // 说明是尾款单
  377. // 创建尾款单
  378. $order_number = self::tailOrder([
  379. 'order_id'=> $params['order_id'],
  380. 'douyinOrderId'=>$params['cp_extra']['douyinOrderId'],
  381. 'user_id'=>$user_id??0,
  382. 'total_amount'=>$params['total_amount'],
  383. 'discount_amount'=>$params['discount_amount']??0,
  384. ]);
  385. $path_order_number = DouyinOrder::where(['dy_order_id'=>$params['cp_extra']['douyinOrderId']])->value('order_number')??0;
  386. $payNotifyUrl = 'https://weixiudev.kyjlkj.com/api/dou_yin/payTailNotify';
  387. }else{
  388. // 创建首单 goods_id user_info.mobile user_id quantity
  389. $params['cp_extra']['open_book_info'] = $params['open_book_info']??[];
  390. $params['cp_extra']['item_order_info_list'] = $params['item_order_info_list']??[];
  391. $order_number = self::submitOrder([
  392. 'open_id'=>$params['open_id'],
  393. 'order_id'=>$params['order_id'], // 抖音订单号
  394. 'goods_id'=>$params['cp_extra']['skuId'],
  395. 'user_id'=>$user_id??0,
  396. 'mobile'=>$user['mobile']??'',
  397. 'quantity'=>$params['cp_extra']['quantity'],
  398. 'book_info'=>$params['cp_extra'],
  399. ]);
  400. $path_order_number = $order_number;
  401. $payNotifyUrl = 'https://weixiudev.kyjlkj.com/api/dou_yin/payNotify';
  402. }
  403. return [
  404. "out_order_no" => $order_number,
  405. "order_entry_schema" => [
  406. "path" => "pages/order/detail",
  407. "params" => json_encode(['order_number' => $path_order_number])
  408. ],
  409. "pay_notify_url" => $payNotifyUrl
  410. ];
  411. } catch (\Exception $e) {
  412. throw new \Exception($e->getMessage());
  413. }
  414. }
  415. public static function refundOrderNotify($params = [])
  416. {
  417. try {
  418. // 抖音单取消
  419. $douyinOrder = DouyinOrder::where(['order_number'=>$params['out_order_no']??0])->findOrEmpty();
  420. if(!$douyinOrder->isEmpty()){
  421. $douyinOrder->status = 4;
  422. $douyinOrder->save();
  423. // 有工单则工单取消
  424. if($douyinOrder->work_id){
  425. // 工单信息
  426. $service_work = ServiceWork::where('id',$douyinOrder->work_id)->findOrEmpty();
  427. if($service_work->isEmpty()) return true;
  428. // 取消工单
  429. if($service_work->work_status < 7){
  430. //更新工单状态为已取消并退款
  431. $service_work->service_status = 4;
  432. $service_work->work_status = 9;
  433. $service_work->user_confirm_status = 5;
  434. $service_work->save();
  435. ServiceWorkLog::create([
  436. 'work_id' => $service_work->id,
  437. 'master_worker_id' => $service_work->master_worker_id,
  438. 'opera_log' => "工单:{$service_work->work_sn} 取消并终止结束服务"
  439. ]);
  440. }
  441. }
  442. }
  443. return [
  444. "out_order_no" => $params['out_order_no'],
  445. "order_entry_schema" => [
  446. "path" => "pages/order/detail",
  447. "params" => json_encode(['order_number' => $params['out_order_no']])
  448. ]
  449. ];
  450. } catch (\Exception $e) {
  451. throw new \Exception($e->getMessage());
  452. }
  453. }
  454. // ++++++++++++++++++++ 首次创单
  455. /**
  456. * 预下单扩展点-子
  457. * @param array $params goods_id user_info.mobile user_id quantity
  458. * @return array|false
  459. */
  460. public static function submitOrder($params)
  461. {
  462. Db::startTrans();
  463. try {
  464. $platformGoods = ExternalPlatformGoods::where('external_goods_sn', $params['goods_id'])->where('external_platform_id', self::EXTERNAL_PLATFORM_ID)->findOrEmpty();
  465. if($platformGoods->isEmpty()){
  466. throw new \Exception('产品不存在!');
  467. }
  468. $goods = Goods::findOrEmpty($platformGoods['goods_id']);
  469. if($goods->isEmpty()){
  470. throw new \Exception('产品不存在!');
  471. }
  472. if(empty($params['mobile'])){
  473. throw new \Exception('请先补充您的联系方式后在提交订单');
  474. }
  475. // TODO tmp防抖1m
  476. $isExist = DouyinOrder::where(['user_id'=>$params['user_id'],'goods_id'=>$goods['id']])->where('create_time','>',(time() - 60))->findOrEmpty();
  477. if(!$isExist->isEmpty()){
  478. //throw new \Exception('请勿重复下单!');
  479. }
  480. $quantity = $params['quantity']??1;
  481. //生成订单
  482. $create_data = [
  483. 'user_id' => $params['user_id'],
  484. 'mobile' => $params['mobile'],
  485. 'open_id' => $params['open_id'],
  486. 'goods_id'=>$goods['id'],
  487. 'title' => $goods['goods_name'],
  488. 'book_info' => json_encode($params['book_info']??[]),
  489. 'unit_price' => $platformGoods['service_fee'],
  490. 'quantity' => $quantity,
  491. 'total_amount' => $platformGoods['service_fee'] * $quantity,
  492. 'dy_order_id' => $params['order_id']??'',
  493. 'order_number' => generate_sn(DouyinOrder::class, 'order_number'),
  494. ];
  495. $order = DouyinOrder::create($create_data);
  496. Db::commit();
  497. return $create_data['order_number'];
  498. } catch (\Exception $e) {
  499. Db::rollback();
  500. throw new \Exception($e->getMessage());
  501. }
  502. }
  503. // 支付成功回调
  504. public static function payNotify($params)
  505. {
  506. Log::write(json_encode($params));
  507. $transaction_id = $params['order_id']??'';
  508. $rechargeOrder = RechargeOrder::where(['transaction_id'=>$transaction_id,'payment_type'=>2])->findOrEmpty();
  509. if(!$rechargeOrder->isEmpty()){
  510. return self::payTailNotify($params);
  511. }
  512. // 查询抖音订单是否完成支付
  513. if ($params['status'] === 'SUCCESS') {
  514. $params['cp_extra'] = json_decode($params['cp_extra'], true);
  515. $paid_amount = bcdiv(bcsub($params['total_amount'] ,$params['discount_amount']), '100', 2)??0;
  516. $out_order_no = $params['out_order_no'];
  517. $pay_time = time();
  518. $order = DouyinOrder::where('order_number', $out_order_no)->findOrEmpty();
  519. if(!$order->isEmpty() && empty($order->consultation_id)){
  520. // 更新充值订单状态
  521. $order->transaction_id = $transaction_id;
  522. $order->order_status = 2;
  523. $order->pay_time = $pay_time;
  524. $order->paid_amount = $paid_amount;
  525. $user = User::where('id',$order->user_id)->findOrEmpty()->toArray();
  526. $form_detail = [
  527. 'user_name' => $user['real_name']??'',
  528. 'mobile' => $user['mobile'],
  529. 'transaction_id' => $transaction_id,
  530. 'out_trade_no' => $out_order_no,
  531. 'paid_amount' => $paid_amount,
  532. 'params' => $params,
  533. ];
  534. $consultation = ExternalConsultation::create([
  535. 'external_platform_id' => self::EXTERNAL_PLATFORM_ID,
  536. 'form_detail' => json_encode($form_detail),
  537. 'user_name' => $user['real_name']??'',
  538. 'mobile' => $user['mobile'],
  539. 'goods_id' => $order->goods_id,
  540. 'amount' => $paid_amount
  541. ]);
  542. $order->consultation_id = $consultation->id;
  543. $order->save();
  544. //order_number user_address lon lat appointment_time
  545. self::reservation([
  546. 'order_number' => $out_order_no,
  547. 'user_address' => $params['cp_extra']['user_address'],
  548. 'lon' => $params['cp_extra']['lon'],
  549. 'lat' => $params['cp_extra']['lat'],
  550. 'appointment_time' => $params['cp_extra']['appointment_time'],
  551. ]);
  552. return true;
  553. }elseif (!empty($order->consultation_id)){
  554. return true;
  555. }
  556. }elseif ($params['status'] === 'CANCEL' && $params['message'] == 'TIME_OUT'){
  557. // 超时取消支付
  558. Log::info('支付超时取消:order_id'.$params['order_id']."--out_order_no:" .$params['out_order_no']);
  559. return true;
  560. }
  561. return false;
  562. }
  563. // ++++++++++++++++++++ 首次创单 end
  564. // ++++++++++++++++++++ 尾款创单
  565. /**
  566. * 预下单扩展点-子
  567. * @param array $params goods_id user_info.mobile user_id quantity
  568. * @return array|false
  569. */
  570. public static function tailOrder($params)
  571. {
  572. Db::startTrans();
  573. try {
  574. $amount = bcdiv(bcsub($params['total_amount'] ,$params['discount_amount']), '100', 2)??0;
  575. $work_id = DouyinOrder::where(['dy_order_id'=>$params['douyinOrderId']])->value('work_id')??0;
  576. $sn = '';
  577. $rechargeOrder = RechargeOrder::where(['work_id'=>$work_id,'payment_type'=>2])->findOrEmpty();
  578. if($work_id && $rechargeOrder->isEmpty()){
  579. //新增待支付尾款
  580. $sn = generate_sn(RechargeOrder::class, 'sn');
  581. $order_data = [
  582. 'order_type' => 0,
  583. 'sn' => $sn,
  584. 'order_terminal' => 1,
  585. 'work_id' => $work_id,
  586. 'user_id' => $params['user_id'],
  587. 'payment_type' => 2,
  588. 'order_total' => $amount,
  589. 'order_amount' => $amount,
  590. 'pay_status' => 0,
  591. 'paid_amount' => 0,
  592. 'pay_way' => 4
  593. ];
  594. RechargeOrder::create($order_data);
  595. }else{
  596. $sn = $rechargeOrder->sn;
  597. $rechargeOrder->transaction_id = $params['order_id']??'';
  598. $rechargeOrder->save();
  599. }
  600. Db::commit();
  601. return $sn;
  602. } catch (\Exception $e) {
  603. Db::rollback();
  604. throw new \Exception($e->getMessage());
  605. }
  606. }
  607. // 尾款支付成功回调
  608. public static function payTailNotify($params)
  609. {
  610. Log::write(json_encode($params));
  611. // 查询抖音订单是否完成支付
  612. if ($params['status'] === 'SUCCESS') {
  613. $transaction_id = $params['order_id']??'';
  614. $paid_amount = bcdiv(bcsub($params['total_amount'] ,$params['discount_amount']), '100', 2)??0;
  615. $pay_time = time();
  616. /*$out_order_no = $params['out_order_no'];
  617. $params['cp_extra'] = json_decode($params['cp_extra'], true);
  618. $work_id = DouyinOrder::where(['id'=>$params['cp_extra']['douyinOrderId']])->value('work_id')??0;
  619. $rechargeOrder = RechargeOrder::where(['work_id'=>$work_id,'sn'=>$out_order_no,'payment_type'=>2])->findOrEmpty();*/
  620. $rechargeOrder = RechargeOrder::where(['transaction_id'=>$transaction_id,'payment_type'=>2])->findOrEmpty();
  621. if(!$rechargeOrder->isEmpty()){
  622. // 更新充值订单状态
  623. $rechargeOrder->transaction_id = $transaction_id;
  624. $rechargeOrder->pay_status = 1;
  625. $rechargeOrder->pay_time = $pay_time;
  626. $rechargeOrder->paid_amount = $paid_amount;
  627. $rechargeOrder->save();
  628. // 尾款订单支付成功后续操作 fun
  629. //抖音订单服务完成
  630. $rechargeOrderPaidAmount = \app\common\model\recharge\RechargeOrder::where(['work_id'=>$rechargeOrder->work_id,'pay_status'=>1])->sum('paid_amount');
  631. $order = DouyinOrder::where(['work_id'=>$rechargeOrder->work_id])->findOrEmpty();
  632. $order->order_status = 3;
  633. $order->pay_status = 1;
  634. $order->total_amount = $rechargeOrderPaidAmount;
  635. $order->paid_amount = $rechargeOrderPaidAmount;
  636. $order->save();
  637. //工单完结
  638. self::paymentSuccessful(['sn'=>$rechargeOrder->sn,'pay_way'=>4]);
  639. return true;
  640. }
  641. }elseif ($params['status'] === 'CANCEL' && $params['message'] == 'TIME_OUT'){
  642. // 超时取消支付
  643. Log::info('支付超时取消:transaction_id'.$params['order_id']."--out_order_no:" .($params['out_order_no']??''));
  644. return true;
  645. }
  646. return false;
  647. }
  648. public static function paymentSuccessful($data = [])
  649. {
  650. try {
  651. $params = $data;
  652. $params['sn'] = mb_substr($params['sn'], 0, 18);
  653. $order = RechargeOrder::where(['sn' => $params['sn']])->findOrEmpty();
  654. if($order->isEmpty()) {
  655. throw new \Exception('内部订单不存在:'.$params['sn'],404);
  656. }
  657. if($order->pay_status == PayEnum::ISPAID) {
  658. return true;
  659. }
  660. if(!empty($params['pay_way']??'')) $params['extra']['pay_way'] = $params['pay_way'];
  661. $payNotifyLogic = PayNotifyLogic::handle('goods', $params['sn'], $params['extra']??[]);
  662. if($payNotifyLogic === true){
  663. // 用户下单后,给订单运营专员(配置固定ID)发送公众号提醒(订单信息)
  664. $order = RechargeOrder::where('sn', $params['sn'])
  665. ->where('payment_type','IN',[0,1])
  666. ->where('pay_status','=',1)
  667. ->findOrEmpty();
  668. if(!$order->isEmpty()){
  669. $workDetail = ServiceWork::findOrEmpty($order->work_id);
  670. if(!$workDetail->isEmpty()){
  671. event('Notice', [
  672. 'scene_id' => 100,
  673. 'params' => [
  674. 'user_id' => 0,
  675. 'order_id' => $workDetail['id'],
  676. 'thing3' => $workDetail['title'],
  677. 'time6' => $workDetail['appointment_time'],
  678. 'phone_number8' => asteriskString($workDetail['mobile']),
  679. 'thing5' => (iconv_strlen($workDetail['address'])>15)?(mb_substr($workDetail['address'],0,15,'UTF-8').'...'):$workDetail['address'],
  680. ]
  681. ]);
  682. }
  683. }
  684. return true;
  685. }
  686. throw new \Exception($payNotifyLogic,404);
  687. }catch(\Exception $e){
  688. throw new \Exception($e->getMessage());
  689. }
  690. }
  691. // ++++++++++++++++++++ 尾款创单 end
  692. // ******************************** 订单预约/改约
  693. // 创建预约单
  694. public static function reservation($params)
  695. {
  696. /*$lon_lat = get_address_lat_lng($params['user_address']);
  697. $params['lon'] = $lon_lat['lon'];
  698. $params['lat'] = $lon_lat['lat'];*/
  699. // $params['order_number'] user_address lon lat appointment_time
  700. Db::startTrans();
  701. try {
  702. $order = DouyinOrder::where('order_number', $params['order_number'])->findOrEmpty();
  703. if(!$order->isEmpty() && $order->consultation_id && empty($order->work_id)){
  704. $consultation = ExternalConsultation::where('id', $order->consultation_id)->findOrEmpty()->toArray();
  705. $consultation['user_name'] = $params['user_name']??$consultation['user_name'];
  706. $consultation['mobile'] = $params['mobile']??$consultation['mobile'];
  707. $consultation['user_address'] = $params['user_address'];
  708. $consultation['lon'] = $params['lon'];
  709. $consultation['lat'] = $params['lat'];
  710. $consultation['appointment_time'] = $params['appointment_time'];
  711. $consultationOrderId = ExternalConsultationLogic::order($consultation);
  712. if (false === $consultationOrderId) {
  713. throw new \Exception('预约失败');
  714. }
  715. if(!empty($consultationOrderId)){
  716. $consultationOrder = ExternalConsultationOrder::where('id', $consultationOrderId)->findOrEmpty()->toArray();
  717. $work_status = ServiceWork::where('id', $consultationOrder['work_id'])->value('work_status');
  718. $order->work_id = $consultationOrder['work_id'];
  719. $order->fulfillment_status = $work_status;
  720. $order->save();
  721. }
  722. $params['dy_order_id'] = $order->dy_order_id;
  723. $params['consultationOrderId'] = $consultationOrderId;
  724. $params['open_id'] = $order->open_id;
  725. $params['goods_id'] = $order->goods_id;
  726. }elseif ($order->consultation_id && $order->work_id){
  727. $consultationOrderId = ExternalConsultationOrder::where('work_id', $order->work_id)->where('consultation_id', $order->consultation_id)->value('id')??0;
  728. $params['dy_order_id'] = $order->dy_order_id;
  729. $params['consultationOrderId'] = $consultationOrderId;
  730. $params['open_id'] = $order->open_id;
  731. $params['goods_id'] = $order->goods_id;
  732. }else{
  733. throw new \Exception('预约失败');
  734. }
  735. Db::commit();
  736. // 抖音创建预约单
  737. //$url = 'api/apps/trade/v2/book/create_book';
  738. //$resData = self::toDyRequestUrl($url,self::getCreateBookParams($params));
  739. //book_id result
  740. // 抖音预约接单结果回调
  741. $bookInfo = json_decode($order->book_info, true);
  742. if(isset($bookInfo['open_book_info']) && $bookInfo['open_book_info']){
  743. $bookurl = 'api/apps/trade/v2/book/book_result_callback';
  744. $res = self::toDyRequestUrl($bookurl,[
  745. 'book_id' => $bookInfo['open_book_info']['book_id']??$params['dy_order_id'],
  746. 'result' => 1,
  747. ]);
  748. Log::info('book_result_callback:'.formatLogData($res));
  749. }
  750. return $order['id']??0;
  751. } catch (\Exception $e) {
  752. Db::rollback();
  753. throw new \Exception($e->getMessage());
  754. }
  755. }
  756. public static function getCreateBookParams($params)
  757. {
  758. try {
  759. $platformGoods = ExternalPlatformGoods::where('goods_id', $params['goods_id'])->where('external_platform_id', self::EXTERNAL_PLATFORM_ID)->findOrEmpty();
  760. if($platformGoods->isEmpty()){
  761. throw new \Exception('产品不存在!');
  762. }
  763. $appointment_time = strtotime($params['appointment_time']);
  764. $book_start_time = $appointment_time * 1000;
  765. $book_end_time = ($appointment_time + (2 * 86400)) * 1000;
  766. $data = [
  767. "order_id"=> (string)$params['dy_order_id']??'',
  768. "out_book_no"=> (string)$params['consultationOrderId']??'',
  769. "open_id"=> (string)$params['open_id']??'',
  770. "item_book_info_list" => [
  771. [
  772. "poi_id" => '7511543640776017961',
  773. "shop_name" => '亿蜂快修·武汉市',
  774. "ext_shop_id" => (string)self::EXTERNAL_PLATFORM_ID,
  775. "goods_id" => (string)$platformGoods->external_goods_sn,
  776. "book_start_time" => (int)$book_start_time??0,
  777. "book_end_time" => (int)$book_end_time??0,
  778. ]
  779. ]
  780. ];
  781. return $data;
  782. } catch (\Exception $e) {
  783. return [];
  784. }
  785. }
  786. public static function upReservation($params)
  787. {
  788. // $params['order_number']
  789. Db::startTrans();
  790. try {
  791. $order = DouyinOrder::where('order_number', $params['order_number'])->findOrEmpty();
  792. if(!$order->isEmpty()){
  793. // sn appointment_time
  794. $result = ServiceOrderLogic::approvalChangeAppointment(['sn'=>RechargeOrder::where('work_id', $order->work_id)->value('sn'),'appointment_time'=>$params['appointment_time']]);
  795. if (false === $result) {
  796. throw new \Exception(ServiceOrderLogic::getError());
  797. }
  798. $order->fulfillment_status = ServiceWork::where('id', $order->work_id)->value('work_status');
  799. $order->save();
  800. }
  801. Db::commit();
  802. return $order['id'];
  803. } catch (\Exception $e) {
  804. Db::rollback();
  805. throw new \Exception($e->getMessage());
  806. }
  807. }
  808. // ******************************** 订单退款
  809. public static function cancelOrder($params)
  810. {
  811. // $params['order_number']
  812. Db::startTrans();
  813. try {
  814. $douyinOrder = DouyinOrder::where('order_number', $params['order_number'])->findOrEmpty();
  815. if(!$douyinOrder->isEmpty()){
  816. if($douyinOrder->order_status == 1 && $douyinOrder->pay_status == 0){
  817. $douyinOrder->order_status = 4;
  818. $douyinOrder->save();
  819. // 有工单则工单取消
  820. if($douyinOrder->work_id){
  821. // 工单信息
  822. $service_work = ServiceWork::where('id',$douyinOrder->work_id)->findOrEmpty();
  823. if($service_work->isEmpty()) return true;
  824. // 取消工单
  825. if($service_work->work_status < 7){
  826. //更新工单状态为已取消并退款
  827. $service_work->service_status = 4;
  828. $service_work->work_status = 9;
  829. $service_work->user_confirm_status = 5;
  830. $service_work->save();
  831. ServiceWorkLog::create([
  832. 'work_id' => $service_work->id,
  833. 'master_worker_id' => $service_work->master_worker_id,
  834. 'opera_log' => "工单:{$service_work->work_sn} 取消并终止结束服务"
  835. ]);
  836. }
  837. }
  838. }else{
  839. throw new \Exception('订单状态不可取消!');
  840. }
  841. }
  842. Db::commit();
  843. return $douyinOrder['id'];
  844. } catch (\Exception $e) {
  845. Db::rollback();
  846. throw new \Exception($e->getMessage());
  847. }
  848. }
  849. // 申请退款
  850. public static function refund($params)
  851. {
  852. Db::startTrans();
  853. try {
  854. // $params['order_number'] user_id
  855. $order = DouyinOrder::with(['goods','serviceWork'])->where('order_number', $params['order_number'])->where('user_id', $params['user_id'])->findOrEmpty();
  856. if($order->isEmpty()){
  857. throw new \Exception('订单不存在');
  858. }
  859. $orderInfo = $order->toArray();
  860. $work_status = $orderInfo['serviceWork']['work_status']??0;
  861. if(3 < $work_status){
  862. throw new \Exception('该订单禁止退款');
  863. }
  864. DouyinRefundOrder::create([
  865. 'refund_number' => generate_sn(DouyinRefundOrder::class, 'refund_number'),
  866. 'order_number' => $orderInfo['order_number'],
  867. 'transaction_id' => $orderInfo['transaction_id'],
  868. 'reason' => $params['reason']??'',
  869. 'refund_status' => 0,
  870. 'user_id' => $orderInfo['user_id'],
  871. 'refund_amount' => $orderInfo['paid_amount'],
  872. ]);
  873. Db::commit();
  874. // 默认审核通过
  875. self::refundExamine(['is_examine_ok'=>'pass','order_number'=>$params['order_number']]);
  876. return true;
  877. } catch (\Exception $e) {
  878. Db::rollback();
  879. throw new \Exception($e->getMessage());
  880. }
  881. }
  882. // 后台退款审核
  883. public static function refundExamine($params)
  884. {
  885. Db::startTrans();
  886. try {
  887. // $params['order_number']
  888. $order = DouyinOrder::with(['goods','serviceWork'])->where('order_number', $params['order_number'])->findOrEmpty();
  889. if($order->isEmpty()){
  890. throw new \Exception('订单不存在');
  891. }
  892. $orderInfo = $order->toArray();
  893. //$refund_number = $params['refund_number']??'';
  894. $douyinRefundOrder = DouyinRefundOrder::where('order_number', $params['order_number'])->order('id', 'desc')->findOrEmpty();
  895. if($params['is_examine_ok'] === 'pass'){
  896. $douyinRefundOrder->refund_status = 2;
  897. RechargeOrder::where('work_id', $orderInfo['work_id'])->update([
  898. 'pay_status' => 2,
  899. 'pay_time' => 0,
  900. 'paid_amount' => 0,
  901. ]);
  902. ServiceWork::where('id', $orderInfo['work_id'])->update([
  903. 'work_status' => 0,
  904. 'user_confirm_status' => 0,
  905. 'service_status' => 4,
  906. 'work_pay_status' => 0
  907. ]);
  908. }else{
  909. $douyinRefundOrder->refund_status = 1;
  910. }
  911. $douyinRefundOrder->save();
  912. Db::commit();
  913. if($params['is_examine_ok'] === 'pass'){
  914. //通过后向抖音申请退款
  915. self::sendRefundCreate($params['order_number']);
  916. }
  917. return true;
  918. } catch (\Exception $e) {
  919. Db::rollback();
  920. throw new \Exception($e->getMessage());
  921. }
  922. }
  923. // 后台审核通过 - 发送抖音退款申请
  924. public static function sendRefundCreate($order_number)
  925. {
  926. try {
  927. // $params['order_number']
  928. $order = DouyinOrder::with(['goods','serviceWork'])->where('order_number', $order_number)->findOrEmpty();
  929. if($order->isEmpty()){
  930. throw new \Exception('订单不存在');
  931. }
  932. $orderInfo = $order->toArray();
  933. $douyinRefundOrder = DouyinRefundOrder::where('order_number', $order_number)->order('id', 'desc')->findOrEmpty();
  934. //通过后向抖音申请退款
  935. $url = 'api/trade_basic/v1/developer/refund_create/';
  936. $data = [
  937. "order_id" => $orderInfo['transaction_id'],
  938. "out_refund_no" => $douyinRefundOrder->refund_number,
  939. "cp_extra" => $orderInfo['id'].'|'.$douyinRefundOrder->id,
  940. "order_entry_schema" => [
  941. /*"path" => "page/index/index",
  942. "params" => json_encode(['refund_number'=>$douyinRefundOrder->refund_number])*/
  943. "path" => "pages/order/detail",
  944. "params" => json_encode(['order_number' => $order_number])
  945. ],
  946. "refund_total_amount " => $douyinRefundOrder->refund_amount * 100,
  947. //"notify_url" => config('douyin.refundNotifyUrl'),
  948. "refund_reason" => [
  949. [
  950. "code" => 101,
  951. "text" => "不想要了"
  952. ]
  953. ]
  954. ];
  955. $resData = self::toDyRequestUrl($url,$data);
  956. if(isset($resData['data']) && $resData['data']){
  957. $douyinRefundOrder->transaction_id = $resData['data']['refund_id'];
  958. $douyinRefundOrder->save();
  959. }
  960. return true;
  961. } catch (\Exception $e) {
  962. Log::info($e->getMessage());
  963. return false;
  964. }
  965. }
  966. public static function refundNotify($params)
  967. {
  968. Db::startTrans();
  969. try {
  970. $douyinRefundOrder = DouyinRefundOrder::where('refund_number', $params['out_refund_no'])->findOrEmpty();
  971. if($douyinRefundOrder->isEmpty()){
  972. throw new \Exception('退款订单不存在');
  973. }
  974. if($douyinRefundOrder->refund_status == 0){
  975. if($params['status'] === 'SUCCESS'){
  976. $douyinRefundOrder->refund_status = 3;
  977. DouyinOrder::where('order_number', $douyinRefundOrder->order_number)->update([
  978. 'order_status' => 4,
  979. 'pay_status' => 3,
  980. ]);
  981. }elseif($params['status'] === 'FAIL'){
  982. $douyinRefundOrder->refund_status = 4;
  983. }else{
  984. throw new \Exception('退款状态未知');
  985. }
  986. $douyinRefundOrder->save();
  987. }
  988. Db::commit();
  989. return true;
  990. } catch (\Exception $e) {
  991. Db::rollback();
  992. throw new \Exception($e->getMessage());
  993. }
  994. }
  995. public static function toDyRequestUrl($url,$data,$headers = [],$resFunction = 'extraErrorCodeReturn',$isHost = 0)
  996. {
  997. $toData = [
  998. 'url' => $url,
  999. 'data' => $data,
  1000. 'headers' => $headers,
  1001. 'resFunction' => $resFunction,
  1002. 'isHost' => $isHost
  1003. ];
  1004. $res = commonHttpClient(env('internal_api.api_url_host').'platf/dou_yin/toDyRequestUrl', $toData, 'post', 'json', ['Content-Type' => 'application/json']);
  1005. Log::info(json_encode($res));
  1006. if(isset($res['code']) && $res['code'] === 0){
  1007. Log::info("toDyRequestUrl:".json_encode($res));
  1008. return $res['data'];
  1009. }else{
  1010. Log::info("toDyRequestUrl:".$res['msg']);
  1011. throw new \Exception($res['msg']);
  1012. }
  1013. }
  1014. }