Jushuitan.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\services\erp\storage;
  12. use crmeb\basic\BaseErp;
  13. use crmeb\exceptions\AdminException;
  14. use crmeb\exceptions\ErpException;
  15. use crmeb\services\erp\storage\jushuitan\Comment;
  16. use crmeb\services\erp\storage\jushuitan\Order;
  17. use crmeb\services\erp\storage\jushuitan\Product;
  18. use crmeb\services\erp\storage\jushuitan\Stock;
  19. use EasyWeChat\Kernel\Support\Str;
  20. use think\Collection;
  21. use think\facade\Cache;
  22. use think\Response;
  23. /**
  24. * Class Jushuitan
  25. * @package crmeb\services\erp\storage
  26. */
  27. class Jushuitan extends BaseErp
  28. {
  29. //==================商家授权==================
  30. /**
  31. * 获取授权参数
  32. * @param string $state 透传数据
  33. * @return array
  34. */
  35. public function getAuthParams($state = ""): array
  36. {
  37. $params = [];
  38. //开发者应用Key
  39. $params["app_key"] = $this->accessToken->getAccount();
  40. //当前请求的时间戳【单位是秒】
  41. $params["timestamp"] = time();
  42. //透传数据 非必填
  43. $params["state"] = $state;
  44. //交互数据的编码【utf-8】目前只能传utf-8,不能不传!
  45. $params["charset"] = "utf-8";
  46. //签名
  47. $params["sign"] = $this->sign($params);
  48. //授权跳转地址
  49. $params["url"] = "https://openweb.jushuitan.com/auth";
  50. return $params;
  51. }
  52. /**
  53. * 获取AccessToken 用于验证授权回调是否成功
  54. * @return string
  55. * @throws \Exception
  56. */
  57. public function getAccessToken(): string
  58. {
  59. return $this->accessToken->getAccessToken();
  60. }
  61. /**
  62. * 设置AccessToken
  63. * @param $at
  64. * @return string
  65. */
  66. public function setAccessToken($at): string
  67. {
  68. return $this->accessToken->setAccessToken($at, 999999);
  69. }
  70. /**
  71. * 平台授权回调
  72. * @return Response
  73. */
  74. public function authCallback(): Response
  75. {
  76. $params = request()->get();
  77. //验证必要参数 返回失败
  78. if (!isset($params["app_key"]) || !isset($params["code"]) || !isset($params["sign"])) {
  79. return response(["code" => 504]);
  80. }
  81. $appKey = $params["app_key"];
  82. $code = $params["code"]; //授权码,有效期为15分钟
  83. $sign = $params["sign"];
  84. $state = isset($params["state"]) ? $params["state"] : ""; //透传数据
  85. //appKey是否匹配 不匹配返回成功-抛弃消息
  86. if ($appKey !== $this->accessToken->getAccount()) {
  87. return response(["code" => 0, "msg" => "appKey不匹配"]);
  88. }
  89. //sign验证失败 返回失败
  90. if ($sign !== $this->sign($params)) {
  91. return response(["code" => 505, "msg" => "签名错误"]);
  92. }
  93. //code换access_token
  94. $request = $this->code2accessToken($code);
  95. //缓存token
  96. $this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]);
  97. //token提前过期时间
  98. $this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60));
  99. //缓存刷新token
  100. $this->accessToken->setRefreshToken($request["refresh_token"]);
  101. return response(["code" => 0]);
  102. }
  103. /**
  104. * @return bool|mixed|null
  105. * @throws \Psr\SimpleCache\InvalidArgumentException
  106. */
  107. public function getTokenExpire()
  108. {
  109. return $this->accessToken->getTokenExpire();
  110. }
  111. /**
  112. * 授权临时code换access_token
  113. * @param $code
  114. * @return array
  115. */
  116. public function code2accessToken($code): array
  117. {
  118. $url = $this->accessToken->getApiUrl("/openWeb/auth/accessToken");
  119. //请求参数
  120. $params = [];
  121. //开发者应用Key
  122. $params["app_key"] = $this->accessToken->getAccount();
  123. //当前请求的时间戳【单位是秒】
  124. $params["timestamp"] = time();
  125. //固定值:authorization_code
  126. $params["grant_type"] = "authorization_code";
  127. //交互数据的编码【utf-8】目前只能传utf-8,不能不传!
  128. $params["charset"] = "utf-8";
  129. //授权码
  130. $params["code"] = $code;
  131. //签名
  132. $params["sign"] = $this->sign($params);
  133. try {
  134. $request = $this->accessToken::postRequest($url, $params);
  135. } catch (\Exception $e) {
  136. throw new AdminException($e->getMessage());
  137. }
  138. //处理平台响应异常
  139. $this->checkRequestError($request);
  140. return $request["data"];
  141. }
  142. /**
  143. * @param $content
  144. * @return Collection
  145. */
  146. protected function json($content)
  147. {
  148. if (false === $content) {
  149. return collect();
  150. }
  151. $data = json_decode($content, true);
  152. if (JSON_ERROR_NONE !== json_last_error()) {
  153. throw new ErpException(sprintf('Failed to parse JSON: %s', json_last_error_msg()));
  154. }
  155. return collect($data);
  156. }
  157. /**
  158. * 通过接口方式授权登录聚水潭
  159. * @param string $account
  160. * @param string $password
  161. * @return bool
  162. */
  163. public function authLogin(string $account = null, string $password = null)
  164. {
  165. if (Cache::has('erp_login_count') && Cache::get('erp_login_count') > 10) {
  166. return false;
  167. }
  168. $authParams = $this->getAuthParams();
  169. $loginInfo = $this->accessToken->postRequest('https://api.jushuitan.com/erp/webapi/UserApi/WebLogin/Passport', json_encode([
  170. 'ipAddress' => '',
  171. 'uid' => '',
  172. 'data' => [
  173. 'account' => $account ?: $this->accessToken->getAuthAccount(),
  174. 'j_d_3' => '',
  175. 'password' => $password ?: $this->accessToken->getAuthPassword(),
  176. 'v_d_144' => ''
  177. ],
  178. ]), ['Content-Type:application/json; charset=utf-8']);
  179. $loginInfo = $this->json($loginInfo);
  180. if ($loginInfo['code'] === null || $loginInfo['code'] !== 0) {
  181. if ($loginInfo['code'] === 10001) {
  182. $erpLoginCount = 0;
  183. if (Cache::has('erp_login_count')) {
  184. $erpLoginCount = Cache::get('erp_login_count', 0);
  185. $erpLoginCount++;
  186. }
  187. Cache::set('erp_login_count', $erpLoginCount, 60);
  188. }
  189. throw new ErpException($loginInfo['msg'] ?? '登录失败');
  190. }
  191. $cookie = $loginInfo['cookie'];
  192. $cookieData = [];
  193. foreach ($cookie as $k => $v) {
  194. $cookieData[] = $k . '=' . $v;
  195. }
  196. $res = $this->accessToken->postRequest('https://api.jushuitan.com/openWeb/auth/oauthAction', json_encode([
  197. 'uid' => '',
  198. 'data' => [
  199. 'app_key' => $authParams['app_key'],
  200. 'charset' => $authParams['charset'],
  201. 'sign' => $authParams['sign'],
  202. 'state' => '',
  203. 'timestamp' => $authParams['timestamp'],
  204. ],
  205. ]), [
  206. 'Content-Type:application/json',
  207. 'Cookie:' . implode(';', $cookieData),
  208. ]);
  209. $res = $this->json($res);
  210. if (!$res) {
  211. throw new ErpException('请求失败');
  212. }
  213. if ($res['code'] != 0) {
  214. throw new ErpException($res['msg'] ?? '授权失败');
  215. }
  216. return true;
  217. }
  218. /**
  219. * 刷新access_token
  220. * @return bool
  221. */
  222. public function refreshToken(): bool
  223. {
  224. $refreshToken = $this->accessToken->getRefreshToken();
  225. if (empty($refreshToken)) {
  226. throw new AdminException("请跳转授权手动授权", 610);
  227. }
  228. $url = $this->accessToken->getApiUrl("/openWeb/auth/refreshToken");
  229. //请求参数
  230. $params = [];
  231. //开发者应用Key
  232. $params["app_key"] = $this->accessToken->getAccount();
  233. //当前请求的时间戳【单位是秒】
  234. $params["timestamp"] = time();
  235. //固定值:refresh_token
  236. $params["grant_type"] = "refresh_token";
  237. //交互数据的编码【utf-8】目前只能传utf-8,不能不传!
  238. $params["charset"] = "utf-8";
  239. //更新令牌
  240. $params["refresh_token"] = $refreshToken;
  241. //固定值:all
  242. $params["scope"] = "all";
  243. //签名
  244. $params["sign"] = $this->sign($params);
  245. try {
  246. $request = $this->accessToken::postRequest($url, $params);
  247. } catch (\Exception $e) {
  248. throw new AdminException($e->getMessage());
  249. }
  250. //处理平台响应异常
  251. $this->checkRequestError($request);
  252. //缓存token
  253. $this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]);
  254. //token提前过期时间
  255. $this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60));
  256. //缓存刷新token
  257. $this->accessToken->setRefreshToken($request["refresh_token"]);
  258. return true;
  259. }
  260. //==================内部方法==================
  261. /**
  262. * 发送post请求并处理异常
  263. * @param $url
  264. * @param $params
  265. * @return mixed
  266. */
  267. public function postRequest($url, $params)
  268. {
  269. //请求平台接口
  270. $request = $this->accessToken->postRequest($url, $params);
  271. $this->checkRequestError($request);
  272. return $request;
  273. }
  274. /**
  275. * 检测平台响应异常 并将响应转换为数组
  276. * @param $request
  277. */
  278. protected function checkRequestError(&$request)
  279. {
  280. if ($request === false || empty($request)) {
  281. throw new AdminException('平台请求失败,请稍后重试');
  282. }
  283. $request = is_string($request) ? json_decode($request, true) : $request;
  284. if (empty($request) || !isset($request['code'])) {
  285. throw new AdminException('平台请求失败,请稍后重试!');
  286. }
  287. if (intval($request['code']) === 100) {
  288. throw new AdminException("请重新授权", 610);
  289. }
  290. if ($request['code'] != 0) {
  291. throw new AdminException(isset($request['msg']) ? '平台错误:' . $request['msg'] : '平台错误:发生异常,请稍后重试');
  292. }
  293. }
  294. /**
  295. * 拼装请求参数
  296. * @param array $biz
  297. * @return array
  298. * @throws \Exception
  299. * @throws \Exception
  300. */
  301. public function getParams(array $biz = []): array
  302. {
  303. //请求参数
  304. $params = [];
  305. $accessToken = null;
  306. try {
  307. $accessToken = $this->accessToken->getAccessToken();
  308. } catch (\Throwable $e) {
  309. }
  310. //刷新token
  311. if (!$accessToken) {
  312. $this->refreshToken();
  313. $accessToken = $this->accessToken->getAccessToken();
  314. }
  315. if (!$accessToken) {
  316. throw new ErpException('缺少access_token,请手动登录聚水潭开放平台进行授权登录');
  317. }
  318. //商户授权token值
  319. $params["access_token"] = $accessToken;
  320. //开发者应用Key
  321. $params["app_key"] = $this->accessToken->getAccount();
  322. //当前请求的时间戳【单位是秒】
  323. $params["timestamp"] = time();
  324. //接口版本,当前版本为【2】,目前只能传2,不能不传!
  325. $params["version"] = "2";
  326. //交互数据的编码【utf-8】目前只能传utf-8,不能不传!
  327. $params["charset"] = "utf-8";
  328. //业务请求参数,格式为jsonString
  329. if (empty($biz)) {
  330. $biz = new \ArrayObject();
  331. }
  332. $params["biz"] = json_encode($biz, JSON_UNESCAPED_UNICODE);
  333. //签名
  334. $params["sign"] = $this->sign($params);
  335. return $params;
  336. }
  337. /**
  338. * 计算签名
  339. * @param array $params
  340. * @return string
  341. */
  342. protected function sign(array $params): string
  343. {
  344. if (empty($params)) {
  345. return "";
  346. }
  347. //1.将请求参数中除 sign 外的多个键值对,根据键按照字典序排序
  348. ksort($params);
  349. //按照 "key1value1key2value2..." 的格式拼成一个字符串。
  350. $str = "";
  351. foreach ($params as $k => $v) {
  352. if ($k == null || $k == "" || $k == "sign" || $v == "") {
  353. continue;
  354. }
  355. if (is_array($v) || is_object($v)) {
  356. $v = json_encode($v, JSON_UNESCAPED_UNICODE);
  357. }
  358. $str .= $k . $v;
  359. }
  360. //2.将 app_secret 拼接在 1 中排序后的字符串前面得到待签名字符串
  361. $str = $this->accessToken->getSecret() . $str;
  362. //3.使用 MD5 算法加密待加密字符串并转为小写
  363. return bin2hex(md5($str, true));
  364. }
  365. /**
  366. * @param string $type
  367. * @return Stock|Order|Product|Comment
  368. */
  369. public function serviceDriver(string $type = '')
  370. {
  371. $namespace = '\\crmeb\\services\\erp\\storage\\jushuitan\\';
  372. $class = strpos($type, '\\') ? $type : $namespace . Str::studly($type);
  373. if (!class_exists($class)) {
  374. throw new \RuntimeException('class not exists: ' . $class);
  375. }
  376. return \think\Container::getInstance()->invokeClass($class, [$this->accessToken, $this]);
  377. }
  378. }