MochatForm.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. <?php
  2. /**
  3. * 重庆赤晓店信息科技有限公司
  4. * https://www.chixiaodian.com
  5. * Copyright (c) 2023 赤店商城 All rights reserved.
  6. */
  7. namespace app\modules\admin\models\mochat;
  8. use app\models\Option;
  9. use app\constants\OptionSetting;
  10. use app\models\AccountLog;
  11. use app\models\User;
  12. use app\models\SaasUser;
  13. use app\models\Cat;
  14. use app\models\Goods;
  15. use app\models\Supplier;
  16. use app\models\Attr;
  17. use app\models\AttrGroup;
  18. use app\modules\admin\models\GoodsForm;
  19. use app\models\GoodsCat;
  20. use app\models\Order;
  21. use app\models\OrderDetail;
  22. use app\models\Store;
  23. use app\models\common\CommonOrder;
  24. use app\modules\client\models\v1\BindForm;
  25. use app\modules\client\models\v1\ShareMoneyForm;
  26. use app\modules\client\models\OrderComplete;
  27. use app\models\UserShareLog;
  28. use app\models\Share;
  29. use app\models\UserShareMoney;
  30. use app\models\UserBindLog;
  31. use app\models\Level;
  32. use zmoyi\JuShuiTan\Auth\Auth;
  33. use zmoyi\JuShuiTan\Api\ApiRequest;
  34. use zmoyi\JuShuiTan\Api\Common\ServeHttp;
  35. use zmoyi\JuShuiTan\Api\Common\Util;
  36. use zmoyi\JuShuiTan\Api\Common\Client;
  37. use app\models\Coupon;
  38. use GuzzleHttp\Client as Clients;
  39. use GuzzleHttp\Exception\RequestException;
  40. use GuzzleHttp\Promise\PromiseInterface;
  41. use Psr\Http\Message\ResponseInterface;
  42. use app\modules\admin\models\WechatThirdForm;
  43. use app\jobs\MochatJob;
  44. use app\modules\admin\models\OrderListForm;
  45. class MochatForm extends Model
  46. {
  47. const MSG_TYPE_CLICK_STORE = 1;
  48. const MSG_TYPE_VIEW_GOODS = 2;
  49. const MSG_TYPE_SUBMIT_ORDER = 3;
  50. const MSG_TYPE_PAY_ORDER = 4;
  51. const MSG_TYPE_VIEW_COUPON = 5;
  52. const MSG_TYPE_GET_COUPON = 6;
  53. public static $confs = [];
  54. public $store_id;
  55. private static function sendRequest($type = 'get', $baseUrl = '', $url = '', array $options = []):array
  56. {
  57. $client = new Clients([
  58. 'timeout' => 3,
  59. 'base_uri' => $baseUrl,
  60. 'verify' => false,
  61. 'headers' => [
  62. 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8'
  63. ]
  64. ]);
  65. if($type == 'get'){
  66. $request = $client->requestAsync($type, $url, [
  67. 'query' => $options
  68. ]);
  69. }else{
  70. $request = $client->postAsync($url, [
  71. 'form_params' => $options
  72. ]);
  73. }
  74. $promise = $request->then(function (ResponseInterface $request) use ($url, $options) {
  75. $contents = $request->getBody()->getContents();
  76. debug_log(['ResponseInterface', $contents, $url, $options], __CLASS__ . '.log');
  77. return json_decode($contents, true);
  78. }, function (\Exception $exception) use ($url, $options) {
  79. if($exception instanceof RequestException){
  80. $contents = $exception->getResponse()->getBody()->getContents();
  81. debug_log(['RequestException', $contents, $url, $options], __CLASS__ . '.log');
  82. return json_decode($contents, true);
  83. }else{
  84. debug_log(['RequestException', 'ExceptionErr', $exception->getMessage()], __CLASS__ . '.log');
  85. \Yii::error([__METHOD__, __LINE__, $exception]);
  86. }
  87. });
  88. return $promise->wait();
  89. }
  90. public static function api($store_id = 0, $_a = '', $params = [], $action = '/cyy/auth/index', $type = 'post') {
  91. $config = self::conf($store_id);
  92. $baseUrl = $config['mochatUrl'];
  93. if($config['refresh_time'] < time()){
  94. $response = self::sendRequest('post', $baseUrl, $baseUrl . '/open-api/access_token/get', [
  95. 'corp_id' => $config['corp_id'],
  96. 'token' => $config['token'],
  97. ]);
  98. if(isset($response['code'])){
  99. if($response['code'] != 200){
  100. debug_log(['code!=200', 'access_token/get', $response, $store_id, $type], __CLASS__ . '.log');
  101. }else{
  102. $access_token = $response['data']['access_token'];
  103. $expires_in = $response['data']['expires_in'];
  104. self::setAccessToken($store_id, $access_token, $expires_in);
  105. $config['access_token'] = $access_token;
  106. }
  107. }else{
  108. return $response;
  109. }
  110. }
  111. $params['access_token'] || $params['access_token'] = $config['access_token'];
  112. $params['_a'] || $params['_a'] = $_a;
  113. $response = self::sendRequest($type, $baseUrl, $baseUrl . $action, $params);
  114. if(isset($response['code'])){
  115. if($response['code'] != 200){
  116. debug_log(['code!=200', $response, $store_id, $type, $action, $params], __CLASS__ . '.log');
  117. }else{
  118. $response['code'] = 0;
  119. }
  120. }
  121. return $response;
  122. }
  123. public static function conf($store_id = 0, $refresh = 0) {
  124. if(isset(self::$confs[$store_id]) && !$refresh){
  125. return self::$confs[$store_id];
  126. }
  127. $conf = Option::get(OptionSetting::MOCHAT_API, $store_id, 'store')['value'];
  128. if($conf){
  129. $conf = json_decode($conf, true);
  130. }else{
  131. $conf = [
  132. 'mochatUrl' => '',
  133. ];
  134. }
  135. self::$confs[$store_id] = $conf;
  136. return $conf;
  137. }
  138. public static function saveConf($store_id = 0, $config = []) {
  139. $config['refresh_time'] = $config['refresh_time'] < time() ? time() : $config['refresh_time'];
  140. $set = Option::set(OptionSetting::MOCHAT_API, json_encode($config), $store_id, 'store');
  141. self::conf($store_id, 1);
  142. return $set;
  143. }
  144. public static function getConf($store_id = 0, $key = '') {
  145. $config = self::conf($store_id);
  146. return $config[$key];
  147. }
  148. public static function setConf($store_id = 0, $key = '', $val = '') {
  149. $config = self::conf($store_id);
  150. if(is_string($key)){
  151. $key = [$key];
  152. $val = [$val];
  153. }
  154. foreach($key as $i => $item){
  155. $config[$item] = $val[$i];
  156. }
  157. return self::saveConf($store_id, $config);
  158. }
  159. public static function setConfCorpId($store_id = 0, $corp_id = '') {
  160. return self::setConf($store_id, 'corp_id', $corp_id);
  161. }
  162. public static function setAccessToken($store_id = 0, $access_token = '', $expires_in = '') {
  163. $refresh_time = time() + $expires_in;
  164. return self::setConf($store_id, ['access_token', 'refresh_time'], [$access_token, $refresh_time]);
  165. }
  166. public static function isopen($store_id = 0) {
  167. $isopen = self::getConf($store_id, 'isopen');
  168. return empty($isopen) ? 0 : 1;
  169. }
  170. public static function getUserByStoreSaasuser($store_id = 0, $saas_user = []) {
  171. $user = User::find()->where([
  172. 'store_id' => $store_id,
  173. 'binding' => $saas_user['mobile'],
  174. 'is_delete' => 0,
  175. ])->one();
  176. return $user;
  177. }
  178. public static function userToExternalUserid($store_id = 0, $user_id = 0) {
  179. if(empty($user_id)){
  180. return [
  181. 'code' => 1,
  182. 'msg' => '用户不存在',
  183. ];
  184. }
  185. $user = User::findOne($user_id);
  186. $saasUser = SaasUser::findOne(['mobile' => $user['binding']]);
  187. $phone = $saasUser ? $saasUser->mobile : '';
  188. $openid = $user ? $user->wechat_open_id : '';
  189. $unionid = $user ? $user->wechat_union_id : '';
  190. $contact = self::contactUnionidToExternalUserid($store_id, $phone, $openid, $unionid);
  191. return $contact;
  192. }
  193. public static function userInRoom($store_id = 317, $roomId = '', $user_id = '') {
  194. $contact = self::userToExternalUserid($store_id, $user_id);
  195. if($contact['code'] != 0){
  196. debug_log([__METHOD__, $contact], __CLASS__ . '.log');
  197. return false;
  198. }
  199. $wxExternalUserid = $contact['data']['wx_external_userid'];
  200. $roomMem = self::workRoomMembers($store_id, $roomId, 2);
  201. if($roomMem['code'] != 0){
  202. debug_log([__METHOD__, $roomMem], __CLASS__ . '.log');
  203. return false;
  204. }
  205. debug_log([__METHOD__, $wxExternalUserid, $roomMem['data']], __CLASS__ . '.log');
  206. // var_dump($roomMem['data'], $wxExternalUserid);die;
  207. foreach($roomMem['data'] as $item){
  208. if($item['wxExternalUserid'] == $wxExternalUserid){
  209. // var_dump($item);die;
  210. return true;
  211. }
  212. }
  213. return false;
  214. // return self::workInRoom($store_id, $roomId, $wxExternalUserid);
  215. }
  216. public static function sendMsg($queue = true, $store_id = 84, $type = 1, $saas_user_id = 0, $extData = []) {
  217. try{
  218. if(!self::isopen($store_id)){
  219. return;
  220. // throw new \Exception('未开启scrm功能' . $store_id);
  221. }
  222. if(!$saas_user_id){
  223. return;
  224. // throw new \Exception('$saas_user_id = 0' . $store_id);
  225. }
  226. // $queue = time();
  227. if($queue === 1 || $queue === true){
  228. $cacheK = func_get_args();
  229. $cacheV = cache()->get($cacheK);
  230. if($cacheV){
  231. debug_log([__FUNCTION__, '$cacheV', $cacheV], __CLASS__ . '.log');
  232. return;
  233. }
  234. cache()->set($cacheK, 1, 60);
  235. $queue = queue_push(new \app\jobs\mochat\SendMsgJob([
  236. 'queue_time' => time(),
  237. 'store_id' => $store_id,
  238. 'type' => $type,
  239. 'saas_user_id' => $saas_user_id,
  240. 'extData' => $extData,
  241. ]));
  242. debug_log(['sendMsg $queue', $queue], __CLASS__ . '.log');
  243. return $queue;
  244. }
  245. $queue_time = $queue;
  246. debug_log(['sendMsg', func_get_args()], __CLASS__ . '.log');
  247. $msgType = [
  248. self::MSG_TYPE_CLICK_STORE => '点击商城',
  249. self::MSG_TYPE_VIEW_GOODS => '浏览商品',
  250. self::MSG_TYPE_SUBMIT_ORDER => '下单商品',
  251. self::MSG_TYPE_PAY_ORDER => '购买商品',
  252. self::MSG_TYPE_VIEW_COUPON => '查看优惠券',
  253. self::MSG_TYPE_GET_COUPON => '领取优惠券',
  254. ];
  255. if(!isset($msgType[$type])){
  256. throw new \Exception('$type参数错误' . $type);
  257. }
  258. $saasUser = SaasUser::findOne($saas_user_id);
  259. $extData['goods_id'] && $goods = Goods::findOne($extData['goods_id']);
  260. $extData['coupon_id'] && $coupon = Coupon::findOne($extData['coupon_id']);
  261. $title = '用户' . $msgType[$type] . '提醒';
  262. $desc1 = [
  263. '客户名称:' . ($saasUser ? $saasUser->name : ''),
  264. $goods ? ('商品名称:' . $goods->name) : '',
  265. $coupon ? ('优惠券名称:' . $coupon->name) : '',
  266. '查看时间:' . date('Y-m-d H:i:s', $queue_time),
  267. ];
  268. $desc = '<div>' . implode('</div><div>', $desc1) . '</div>';
  269. $user = self::getUserByStoreSaasuser($store_id, $saasUser);
  270. $phone = $saasUser ? $saasUser->mobile : '';
  271. $openid = $user ? $user->wechat_open_id : '';
  272. $unionid = $user ? $user->wechat_union_id : '';
  273. $contact = self::contactUnionidToExternalUserid($store_id, $phone, $openid, $unionid);
  274. if($contact['code'] != 0){
  275. debug_log(['sendMsg contactUnionidToExternalUserid err', func_get_args(), $contact], __CLASS__ . '.log');
  276. // return $contact;
  277. }
  278. $wxExternalUserid = $contact['data']['wx_external_userid'];
  279. $sendMsg_wxUserId = self::getConf($store_id, 'sendMsg_wxUserId');
  280. $content =
  281. $title . "\n" .
  282. $desc . "\n";
  283. if(empty($wxExternalUserid)){
  284. $content =
  285. "【新客户提醒】\n" .
  286. "客户手机:" . $phone . "\n" .
  287. "————————\n" .
  288. $content;
  289. }else{
  290. $contactId = $contact['data']['contactId'];
  291. $agentId = self::getConf($store_id, 'agentId');
  292. $urlSidebar = self::getConf($store_id, 'mochatUrl_sidebar');
  293. $link = $urlSidebar . '/contact?agentId=' . $agentId . '&contactId=' . $contactId;
  294. $content .=
  295. "————————\n" .
  296. '<a href="' . $link . '">查看>></a>';
  297. self::contactAddFollow($store_id, $wxExternalUserid, implode(' ', $desc1), $title);
  298. }
  299. $res = self::employeeSendMsg($store_id, $content, $sendMsg_wxUserId);
  300. return $res;
  301. } catch (\Exception $ex) {
  302. \Yii::error($ex);
  303. return [
  304. 'code' => 1,
  305. 'msg' => $ex->getMessage(),
  306. ];
  307. }
  308. }
  309. public static function orderList($store_id, $wxExternalUserid = '', $roomId = '') {
  310. if($roomId){
  311. $cond = self::getRoomPhone($store_id, $roomId);
  312. }
  313. if($wxExternalUserid){
  314. $cond = self::getContactPhone($store_id, $wxExternalUserid);
  315. }
  316. if(empty($cond['mobiles']) && empty($cond['openids']) && empty($cond['unionids'])){
  317. return [
  318. 'code' => 1,
  319. 'msg' => '暂无数据',
  320. ];
  321. }
  322. $form = new OrderListForm();
  323. $form->setAttributes(all_params(), 0);
  324. $form->setAttributes($cond, 0);
  325. $form->store_id = $store_id;
  326. return $form->search();
  327. }
  328. public static function searchCond($details) {
  329. $res = [
  330. 'mobiles' => [],
  331. 'openids' => [],
  332. 'unionids' => [],
  333. ];
  334. foreach($details as $detail){
  335. if(!empty($detail['phone'])){
  336. $res['mobiles'][] = $detail['phone'];
  337. }
  338. if(!empty($detail['openid'])){
  339. $res['openids'][] = $detail['openid'];
  340. }
  341. if(!empty($detail['unionid'])){
  342. $res['unionids'][] = $detail['unionid'];
  343. }
  344. }
  345. return $res;
  346. }
  347. public static function getContactPhone($store_id = 317, $wxExternalUserids = []) {
  348. if(is_string($wxExternalUserids)){
  349. $wxExternalUserids = [$wxExternalUserids];
  350. }
  351. $list = [];
  352. foreach($wxExternalUserids as $wxExternalUserid){
  353. $detail = self::contactDetail($store_id, $wxExternalUserid);
  354. $list[] = $detail['data'];
  355. }
  356. return self::searchCond($list);
  357. }
  358. public static function getRoomPhone($store_id = 317, $roomId = '') {
  359. $res = self::workRoomMembers($store_id, $roomId);
  360. $list = [];
  361. if($res['data']){
  362. foreach($res['data'] as $item){
  363. $list[] = $item['wxExternalUserid'];
  364. }
  365. }
  366. return self::getContactPhone($store_id, $list);
  367. }
  368. public static function employeeList($store_id = 317, $name = null, $page = 1, $pageSize = 30) {
  369. $params = [
  370. 'page' => $page,
  371. 'pageSize' => $pageSize,
  372. ];
  373. $name && $params['name'] = $name;
  374. $res = self::api($store_id, '', $params, '/open-api/employee/list', 'get');
  375. return $res;
  376. }
  377. public static function contactList($store_id = 317) {
  378. $res = self::api($store_id, '', [], '/open-api/contact/list', 'get');
  379. var_dump($res);die;
  380. }
  381. public static function contactDetail($store_id = 317, $wxExternalUserid = '') {
  382. $res = self::api($store_id, '', [
  383. 'wxExternalUserid' => $wxExternalUserid,
  384. ], '/open-api/contact/detail', 'get');
  385. return $res;
  386. }
  387. public static function workRoomMembers($store_id = 317, $roomId = '', $status = 1, $type = 2) {
  388. $res = self::api($store_id, '', [
  389. 'roomId' => $roomId,
  390. 'status' => $status,
  391. 'type' => $type,
  392. ], '/open-api/workRoom/members', 'get');
  393. return $res;
  394. }
  395. public static function workRoomList($store_id = 317, $options = []) {
  396. $res = self::api($store_id, '', [
  397. 'page' => $options['page'] ?? 1,
  398. 'perPage' => $options['perPage'] ?? 30,
  399. ], '/open-api/workRoom/index', 'get');
  400. return $res;
  401. }
  402. public static function workInRoom($store_id = 317, $roomId = '', $wxExternalUserid = '') {
  403. $res = self::api($store_id, '', [
  404. 'wxExternalUserid' => $wxExternalUserid,
  405. 'roomId' => $roomId,
  406. ], '/open-api/contact/room', 'post');
  407. // var_dump($roomId, $wxExternalUserid, $res);die;
  408. if($res['code'] == 0){
  409. if($res['data'] && $res['data']['page']){
  410. return $res['data']['page']['total'] ? true : false;
  411. }
  412. }
  413. return false;
  414. }
  415. public static function contactUnionidToExternalUserid($store_id = 317, $phone = '', $openid = '', $unionid = '') {
  416. $res = self::api($store_id, '', [
  417. 'unionid' => $unionid,
  418. 'openid' => $openid,
  419. 'phone' => $phone,
  420. ], '/open-api/contact/unionidToExternalUserid', 'get');
  421. return $res;
  422. }
  423. public static function contactAddFollow($store_id = 317, $wxExternalUserid = '', $content = '', $eventName = '') {
  424. $res = self::api($store_id, '', [
  425. 'wxExternalUserid' => $wxExternalUserid,
  426. 'content' => $content,
  427. 'eventName' => $eventName,
  428. ], '/open-api/contact/addFollow');
  429. return $res;
  430. }
  431. public static function employeeSendMsg($store_id = 317, $content = '', $wxUserId = '', $msgType = 'text') {
  432. $params = [
  433. 'msgType' => $msgType,
  434. 'content' => $content,
  435. ];
  436. $wxUserId && $params['wxUserId'] = $wxUserId;
  437. $res = self::api($store_id, '', $params, '/open-api/employee/sendMsg');
  438. return $res;
  439. }
  440. }