diff --git a/.gitignore b/.gitignore index a2ac47b..1ca160d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /runtime/ +.env +.idea +.Ds_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/crm_php.iml b/.idea/crm_php.iml deleted file mode 100644 index 2ae7265..0000000 --- a/.idea/crm_php.iml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8e8947d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index a9eba08..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/application/common/wework/api/Api.php b/application/common/wework/api/Api.php new file mode 100644 index 0000000..8b8406f --- /dev/null +++ b/application/common/wework/api/Api.php @@ -0,0 +1,66 @@ +corpId = $corpId; + $this->corpSecret = $corpSecret; + } + + /** + * 获取客户详情 + * @param $externalUserId + * @return mixed + */ + function contactInfo($externalUserId) { + return $this->get(self::$contactInfo, [ + 'access_token' => $this->getAccessToken(), + 'external_userid' => $externalUserId + ]); + } + + /** + * 获取accessToken + * @return false|mixed + */ + protected function getAccessToken() { + if ($this->accessToken == '') { + $this->accessToken = Cache::get('accessToken:' . $this->corpId); + if ($this->accessToken == '') { + $result = $this->get(self::$getAccessToken, [ + 'corpid' => $this->corpId, + 'corpsecret' => $this->corpSecret + ]); + if (isset($result['access_token']) && $result['access_token']) { + Cache::set('accessToken:' . $this->corpId, $result['access_token'],$result['expires_in']-60); + $this->accessToken = $result['access_token']; + } + } + } + return $this->accessToken; + } + + + + protected function get($action, $data) { + return json_decode(Curl::get(self::$baseUrl . $action,null,$data), true); + } +} \ No newline at end of file diff --git a/application/common/wework/api/Curl.php b/application/common/wework/api/Curl.php new file mode 100644 index 0000000..06dac1c --- /dev/null +++ b/application/common/wework/api/Curl.php @@ -0,0 +1,110 @@ + $val) { + $sets[] = $key . '=' . urlencode($val); + } + $data = implode('&', $sets); + } + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + + $method = strtolower($method); + if ('post' == $method) { + curl_setopt($ch, CURLOPT_POST, true); + } elseif ('put' == $method) { + //curl_setopt($ch,CURLOPT_PUT,true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + } +// curl_setopt($ch, CURLOPT_PROXY, "socks5://127.0.0.1:18080"); + + //获取头部信息 + if ($isHeader) { + curl_setopt($ch, CURLOPT_HEADER, 1); + } + + $output = curl_exec($ch); + if ($output === false) { + var_dump(curl_error($ch)); + } + curl_close($ch); + return $output; + } +} \ No newline at end of file diff --git a/application/common/wework/callback/ErrorCode.php b/application/common/wework/callback/ErrorCode.php new file mode 100644 index 0000000..b4bb964 --- /dev/null +++ b/application/common/wework/callback/ErrorCode.php @@ -0,0 +1,35 @@ + + *
  • -40001: 签名验证错误
  • + *
  • -40002: xml解析失败
  • + *
  • -40003: sha加密生成签名失败
  • + *
  • -40004: encodingAesKey 非法
  • + *
  • -40005: corpid 校验错误
  • + *
  • -40006: aes 加密失败
  • + *
  • -40007: aes 解密失败
  • + *
  • -40008: 解密后得到的buffer非法
  • + *
  • -40009: base64加密失败
  • + *
  • -40010: base64解密失败
  • + *
  • -40011: 生成xml失败
  • + * + */ +class ErrorCode +{ + public static $OK = 0; + public static $ValidateSignatureError = -40001; + public static $ParseXmlError = -40002; + public static $ComputeSignatureError = -40003; + public static $IllegalAesKey = -40004; + public static $ValidateCorpidError = -40005; + public static $EncryptAESError = -40006; + public static $DecryptAESError = -40007; + public static $IllegalBuffer = -40008; + public static $EncodeBase64Error = -40009; + public static $DecodeBase64Error = -40010; + public static $GenReturnXmlError = -40011; +} + +?> \ No newline at end of file diff --git a/application/common/wework/callback/PKCS7Encoder.php b/application/common/wework/callback/PKCS7Encoder.php new file mode 100644 index 0000000..7919f30 --- /dev/null +++ b/application/common/wework/callback/PKCS7Encoder.php @@ -0,0 +1,51 @@ + PKCS7Encoder::$block_size) { + $pad = 0; + } + return substr($text, 0, (strlen($text) - $pad)); + } + +} diff --git a/application/common/wework/callback/Prpcrypt.php b/application/common/wework/callback/Prpcrypt.php new file mode 100644 index 0000000..74fc70f --- /dev/null +++ b/application/common/wework/callback/Prpcrypt.php @@ -0,0 +1,110 @@ +key = base64_decode($k . '='); + $this->iv = substr($this->key, 0, 16); + + } + + /** + * 加密 + * + * @param $text + * @param $receiveId + * @return array + */ + public function encrypt($text, $receiveId) + { + try { + //拼接 + $text = $this->getRandomStr() . pack('N', strlen($text)) . $text . $receiveId; + //添加PKCS#7填充 + $pkc_encoder = new PKCS7Encoder; + $text = $pkc_encoder->encode($text); + //加密 + if (function_exists('openssl_encrypt')) { + $encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv); + } else { + $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $this->key, base64_decode($text), MCRYPT_MODE_CBC, $this->iv); + } + return array(ErrorCode::$OK, $encrypted); + } catch (Exception $e) { + print $e; + return array(MyErrorCode::$EncryptAESError, null); + } + } + + /** + * 解密 + * + * @param $encrypted + * @param $receiveId + * @return array + */ + public function decrypt($encrypted, $receiveId) + { + try { + //解密 + if (function_exists('openssl_decrypt')) { + $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv); + } else { + $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->key, base64_decode($encrypted), MCRYPT_MODE_CBC, $this->iv); + } + } catch (Exception $e) { + return array(ErrorCode::$DecryptAESError, null); + } + try { + //删除PKCS#7填充 + $pkc_encoder = new PKCS7Encoder; + $result = $pkc_encoder->decode($decrypted); + if (strlen($result) < 16) { + return array(); + } + //拆分 + $content = substr($result, 16, strlen($result)); + $len_list = unpack('N', substr($content, 0, 4)); + $xml_len = $len_list[1]; + $xml_content = substr($content, 4, $xml_len); + $from_receiveId = substr($content, $xml_len + 4); + } catch (Exception $e) { + print $e; + return array(ErrorCode::$IllegalBuffer, null); + } + if ($from_receiveId != $receiveId) { + return array(ErrorCode::$ValidateCorpidError, null); + } + return array(0, $xml_content); + } + + /** + * 生成随机字符串 + * + * @return string + */ + private function getRandomStr() + { + $str = ''; + $str_pol = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyl'; + $max = strlen($str_pol) - 1; + for ($i = 0; $i < 16; $i++) { + $str .= $str_pol[mt_rand(0, $max)]; + } + return $str; + } +} diff --git a/application/common/wework/callback/SHA1.php b/application/common/wework/callback/SHA1.php new file mode 100644 index 0000000..00fd1ad --- /dev/null +++ b/application/common/wework/callback/SHA1.php @@ -0,0 +1,34 @@ +m_sToken = $token; + $this->m_sEncodingAesKey = $encodingAesKey; + $this->m_sReceiveId = $receiveId; + } + + /* + *验证URL + *@param sMsgSignature: 签名串,对应URL参数的msg_signature + *@param sTimeStamp: 时间戳,对应URL参数的timestamp + *@param sNonce: 随机串,对应URL参数的nonce + *@param sEchoStr: 随机串,对应URL参数的echostr + *@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + *@return:成功0,失败返回对应的错误码 + */ + public function VerifyURL($sMsgSignature, $sTimeStamp, $sNonce, $sEchoStr, &$sReplyEchoStr) + { + if (strlen($this->m_sEncodingAesKey) != 43) { + return ErrorCode::$IllegalAesKey; + } + + $pc = new Prpcrypt($this->m_sEncodingAesKey); + //verify msg_signature + $sha1 = new SHA1(); + $array = $sha1->getSHA1($this->m_sToken, $sTimeStamp, $sNonce, $sEchoStr); + $ret = $array[0]; + + if ($ret != 0) { + return $ret; + } + + $signature = $array[1]; + if ($signature != $sMsgSignature) { + return ErrorCode::$ValidateSignatureError; + } + + $result = $pc->decrypt($sEchoStr, $this->m_sReceiveId); + if ($result[0] != 0) { + return $result[0]; + } + $sReplyEchoStr = $result[1]; + + return ErrorCode::$OK; + } + /** + * 将公众平台回复用户的消息加密打包. + *
      + *
    1. 对要发送的消息进行AES-CBC加密
    2. + *
    3. 生成安全签名
    4. + *
    5. 将消息密文和安全签名打包成xml格式
    6. + *
    + * + * @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串 + * @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp + * @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce + * @param &$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, + * 当return返回0时有效 + * + * @return int 成功0,失败返回对应的错误码 + */ + public function EncryptMsg($sReplyMsg, $sTimeStamp, $sNonce, &$sEncryptMsg) + { + $pc = new Prpcrypt($this->m_sEncodingAesKey); + + //加密 + $array = $pc->encrypt($sReplyMsg, $this->m_sReceiveId); + $ret = $array[0]; + if ($ret != 0) { + return $ret; + } + + if ($sTimeStamp == null) { + $sTimeStamp = time(); + } + $encrypt = $array[1]; + + //生成安全签名 + $sha1 = new SHA1; + $array = $sha1->getSHA1($this->m_sToken, $sTimeStamp, $sNonce, $encrypt); + $ret = $array[0]; + if ($ret != 0) { + return $ret; + } + $signature = $array[1]; + + //生成发送的xml + $xmlparse = new XMLParse; + $sEncryptMsg = $xmlparse->generate($encrypt, $signature, $sTimeStamp, $sNonce); + return ErrorCode::$OK; + } + + + /** + * 检验消息的真实性,并且获取解密后的明文. + *
      + *
    1. 利用收到的密文生成安全签名,进行签名验证
    2. + *
    3. 若验证通过,则提取xml中的加密消息
    4. + *
    5. 对消息进行解密
    6. + *
    + * + * @param $msgSignature string 签名串,对应URL参数的msg_signature + * @param $timestamp string 时间戳 对应URL参数的timestamp + * @param $nonce string 随机串,对应URL参数的nonce + * @param $postData string 密文,对应POST请求的数据 + * @param &$msg string 解密后的原文,当return返回0时有效 + * + * @return int 成功0,失败返回对应的错误码 + */ + public function DecryptMsg($sMsgSignature, $sTimeStamp = null, $sNonce, $sPostData, &$sMsg) + { + if (strlen($this->m_sEncodingAesKey) != 43) { + return ErrorCode::$IllegalAesKey; + } + + $pc = new Prpcrypt($this->m_sEncodingAesKey); + + //提取密文 + $xmlparse = new XMLParse; + $array = $xmlparse->extract($sPostData); + $ret = $array[0]; + + if ($ret != 0) { + return $ret; + } + + if ($sTimeStamp == null) { + $sTimeStamp = time(); + } + + $encrypt = $array[1]; + + //验证安全签名 + $sha1 = new SHA1; + $array = $sha1->getSHA1($this->m_sToken, $sTimeStamp, $sNonce, $encrypt); + $ret = $array[0]; + + if ($ret != 0) { + return $ret; + } + + $signature = $array[1]; + if ($signature != $sMsgSignature) { + return ErrorCode::$ValidateSignatureError; + } + + $result = $pc->decrypt($encrypt, $this->m_sReceiveId); + if ($result[0] != 0) { + return $result[0]; + } + $sMsg = $result[1]; + + return ErrorCode::$OK; + } + +} + diff --git a/application/common/wework/callback/XMLParse.php b/application/common/wework/callback/XMLParse.php new file mode 100644 index 0000000..d6f7fc3 --- /dev/null +++ b/application/common/wework/callback/XMLParse.php @@ -0,0 +1,61 @@ +loadXML($xmltext); + $array_e = $xml->getElementsByTagName('Encrypt'); + $encrypt = $array_e->item(0)->nodeValue; + return array(0, $encrypt); + } catch (Exception $e) { + print $e . "\n"; + return array(ErrorCode::$ParseXmlError, null); + } + } + + /** + * 生成xml消息 + * @param string $encrypt 加密后的消息密文 + * @param string $signature 安全签名 + * @param string $timestamp 时间戳 + * @param string $nonce 随机字符串 + */ + public function generate($encrypt, $signature, $timestamp, $nonce) + { + $format = " + + +%s + +"; + return sprintf($format, $encrypt, $signature, $timestamp, $nonce); + } + +} + +// +// Test +/* +$sPostData = ""; +$xmlparse = new XMLParse; +$array = $xmlparse->extract($sPostData); +var_dump($array); +*/ + + +?> diff --git a/application/crm/controller/Callback.php b/application/crm/controller/Callback.php new file mode 100644 index 0000000..4538223 --- /dev/null +++ b/application/crm/controller/Callback.php @@ -0,0 +1,86 @@ +isPost()) { + $sReqMsgSig = Request::instance()->get('msg_signature'); + $sReqTimeStamp = Request::instance()->get('timestamp'); + $sReqNonce = Request::instance()->get('nonce'); + + $sReqData =Request::instance()->getContent(); + $sMsg = ""; // 解析之后的明文 + + $errCode = $wxcpt->DecryptMsg($sReqMsgSig, $sReqTimeStamp, $sReqNonce, $sReqData, $sMsg); + if ($errCode == 0) { + // 解密成功,sMsg即为xml格式的明文 + $simpleXMLElement = simplexml_load_string($sMsg,'SimpleXMLElement', LIBXML_NOCDATA); + switch ($simpleXMLElement->Event->__toString()) { + case 'change_external_contact': + $api = new Api(config('wework.corpId'), config('wework.corpSecret')); + $contactInfo = $api->contactInfo('wm9nLQEAAA6lshIXRN5xdd1iZjqevSyA'); + foreach ($contactInfo['follow_user'] as $contactUserInfo) { + if ($contactUserInfo['userid'] == $simpleXMLElement->UserID->__toString()) { + $customerInfo = model('Customer')->where('name', $contactUserInfo['remark_corp_name'])->find(); + if ($customerInfo) { + $contactsInfo = model('Contacts')->where([ + 'name' => $contactUserInfo['remark'], + 'customer_id' => $customerInfo['customer_id'] + ])->find(); + if (!$contactsInfo) { + $param = [ + 'business_id' => null, + 'create_user_id' => 1, + 'owner_user_id' => 1, + 'customer_id' => $customerInfo['customer_id'], + 'name' => $contactUserInfo['remark'], + 'mobile' => $contactUserInfo['remark_mobiles'][0], + ]; + if (model('Contacts')->createData($param)) { + Log::record('联系人添加成功'); + } else { + Log::record('联系人添加失败'); + } + } + } + } + } + break; + } + } else { + print("ERR: " . $errCode . "\n\n"); + } + } else { + $sVerifyMsgSig = Request::instance()->get('msg_signature'); + $sVerifyTimeStamp = Request::instance()->get('timestamp'); + $sVerifyNonce = Request::instance()->get('nonce'); + $sVerifyEchoStr = Request::instance()->get('echostr'); + // 需要返回的明文 + $sEchoStr = ""; + + $errCode = $wxcpt->VerifyURL($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sVerifyEchoStr, $sEchoStr); + if ($errCode == 0) { + echo ($sEchoStr); + // 验证URL成功,将sEchoStr返回 + // HttpUtils.SetResponce($sEchoStr); + } else { + print("ERR: " . $errCode . "\n\n"); + } + } + } +} \ No newline at end of file diff --git a/application/crm/model/Contacts.php b/application/crm/model/Contacts.php index 0f0e9b5..c90b3ff 100644 --- a/application/crm/model/Contacts.php +++ b/application/crm/model/Contacts.php @@ -313,13 +313,13 @@ class Contacts extends Common $businessId = $param['business_id']; unset($param['business_id']); $fieldModel = new \app\admin\model\Field(); - + // 数据验证 - $validateResult = $this->fieldDataValidate($param, $this->name, $param['create_user_id']); - if (!empty($validateResult)) { - $this->error = $validateResult; - return false; - } +// $validateResult = $this->fieldDataValidate($param, $this->name, $param['create_user_id']); +// if (!empty($validateResult)) { +// $this->error = $validateResult; +// return false; +// } # 处理客户首要联系人 $primaryStatus = Db::name('crm_contacts')->where('customer_id', $param['customer_id'])->value('contacts_id'); diff --git a/config/config.php b/config/config.php index 86051c5..a53cef8 100644 --- a/config/config.php +++ b/config/config.php @@ -4,6 +4,8 @@ // +---------------------------------------------------------------------- // | Author: // +---------------------------------------------------------------------- +use think\Env; + error_reporting(E_ERROR | E_WARNING | E_PARSE); return [ @@ -183,7 +185,7 @@ return [ // 驱动方式 'type' => 'redis', // 连接地址 - 'host' => '127.0.0.1', + 'host' => Env::get('cache_host','127.0.0.1'), // 端口 'port' => 6379, // 密码 @@ -271,5 +273,12 @@ return [ // 商机状态组列表缓存时间(秒) 'business_status_cache_time' => 1800, + 'wework' => [ + 'corpId' => Env::get('wework_corpId',''), + 'corpSecret' => Env::get('wework_corpSecret',''), + 'token' => Env::get('wework_token',''), + 'encodingAesKey' => Env::get('wework_encodingAesKey',''), + ], + 'public_key' => 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkqKFcAQtIp4rlkB5LOMnViyVY/hhA6x0R9ftwtEXsAFu4hBZrm9txdEvxSrDCUsx3Zwv/gdimeOzTtfSKffdoE/DwllNP9Zu6nsr2kGRgPrRwjtlO+j2FOM0b9UY1SQ/bWE+a9oQL2jL9xMSbtX1xG/+HcMo1bT+pa6FNQzs3egmvMt75/jaxINPSraj4kgNFawSBk7qDBEqDYiQwtPTuaNW1YZIs++/gZHsCRgGs/JrAbxNpl7+v/+Z503I3I2rs/8eUM5d16NXR8M7vtobUDCTIiQOgRahO8WMadgFlwavyVCYhy/TBXyj5RUfWaS26LrEN3vkj4TjoJu5m9LQ5QIDAQAB', ]; diff --git a/config/database.php b/config/database.php index 8c67704..ac67535 100644 --- a/config/database.php +++ b/config/database.php @@ -3,13 +3,13 @@ return [ // 数据库类型 'type' => 'mysql', // 服务器地址 - 'hostname' => '127.0.0.1', + 'hostname' => 'mysql', // 数据库名 - 'database' => 'test_com', + 'database' => 'wkcrm', // 用户名 - 'username' => 'test_com', + 'username' => 'wkcrm', // 密码 - 'password' => 'YNKdaMS2XBHKcAh7', + 'password' => 'lpC049ZvZdmNIzDvbY0rXKgl', // 端口 'hostport' => '3306', // 连接dsn diff --git a/config/route_crm.php b/config/route_crm.php index 4a4da78..973af3a 100644 --- a/config/route_crm.php +++ b/config/route_crm.php @@ -522,6 +522,10 @@ return [ 'crm/setting/appMenuConfig' => ['crm/setting/appMenuConfig', ['method' => 'POST']], //办公数量 'crm/setting/oaNumber' => ['crm/setting/oaNumber', ['method' => 'POST']], + + // 企业微信回调 + 'crm/callback/index' => ['crm/callback/index', ['method' => 'POST|GET']], + // MISS路由 '__miss__' => 'admin/base/miss', ];