| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 | <?phpnamespace Yansongda\Pay\Gateways\Wechat;use Exception;use Yansongda\Pay\Events;use Yansongda\Pay\Exceptions\BusinessException;use Yansongda\Pay\Exceptions\GatewayException;use Yansongda\Pay\Exceptions\InvalidArgumentException;use Yansongda\Pay\Exceptions\InvalidSignException;use Yansongda\Pay\Gateways\Wechat;use Yansongda\Pay\Log;use Yansongda\Supports\Collection;use Yansongda\Supports\Config;use Yansongda\Supports\Str;use Yansongda\Supports\Traits\HasHttpRequest;/** * @author yansongda <me@yansongda.cn> * * @property string appid * @property string app_id * @property string miniapp_id * @property string sub_appid * @property string sub_app_id * @property string sub_miniapp_id * @property string mch_id * @property string sub_mch_id * @property string key * @property string return_url * @property string cert_client * @property string cert_key * @property array log * @property array http * @property string mode */class Support{    use HasHttpRequest;    /**     * Wechat gateway.     *     * @var string     */    protected $baseUri;    /**     * Config.     *     * @var Config     */    protected $config;    /**     * Instance.     *     * @var Support     */    private static $instance;    /**     * Bootstrap.     *     * @author yansongda <me@yansongda.cn>     */    private function __construct(Config $config)    {        $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];        $this->config = $config;        $this->setHttpOptions();    }    /**     * __get.     *     * @author yansongda <me@yansongda.cn>     *     * @param $key     *     * @return mixed|Config|null     */    public function __get($key)    {        return $this->getConfig($key);    }    /**     * create.     *     * @author yansongda <me@yansongda.cn>     *     * @throws GatewayException     * @throws InvalidArgumentException     * @throws InvalidSignException     *     * @return Support     */    public static function create(Config $config)    {        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {            self::$instance = new self($config);            self::setDevKey();        }        return self::$instance;    }    /**     * getInstance.     *     * @author yansongda <me@yansongda.cn>     *     * @throws InvalidArgumentException     *     * @return Support     */    public static function getInstance()    {        if (is_null(self::$instance)) {            throw new InvalidArgumentException('You Should [Create] First Before Using');        }        return self::$instance;    }    /**     * clear.     *     * @author yansongda <me@yansongda.cn>     */    public static function clear()    {        self::$instance = null;    }    /**     * Request wechat api.     *     * @author yansongda <me@yansongda.cn>     *     * @param string $endpoint     * @param array  $data     * @param bool   $cert     *     * @throws GatewayException     * @throws InvalidArgumentException     * @throws InvalidSignException     */    public static function requestApi($endpoint, $data, $cert = false): Collection    {        Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data));        //xmlData需增加headers配置,否则微信方无法接收到参数        $options = [            'headers' => [                'Content-Type' => 'application/xml',            ],        ];        $certOptions = $cert ? [            'cert' => self::$instance->cert_client,            'ssl_key' => self::$instance->cert_key,        ] : [];        $result = self::$instance->post(            $endpoint,            self::toXml($data),            array_merge($options, $certOptions)        );        $result = is_array($result) ? $result : self::fromXml($result);        Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result));        return self::processingApiResult($endpoint, $result);    }    /**     * Filter payload.     *     * @author yansongda <me@yansongda.cn>     *     * @param array        $payload     * @param array|string $params     * @param bool         $preserve_notify_url     *     * @throws InvalidArgumentException     */    public static function filterPayload($payload, $params, $preserve_notify_url = false): array    {        $type = self::getTypeName($params['type'] ?? '');        $payload = array_merge(            $payload,            is_array($params) ? $params : ['out_trade_no' => $params]        );        $payload['appid'] = self::$instance->getConfig($type, '');        if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {            $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');        }        unset($payload['trade_type'], $payload['type']);        if (!$preserve_notify_url) {            unset($payload['notify_url']);        }        $payload['sign'] = self::generateSign($payload);        return $payload;    }    /**     * Generate wechat sign.     *     * @author yansongda <me@yansongda.cn>     *     * @param array $data     *     * @throws InvalidArgumentException     */    public static function generateSign($data): string    {        $key = self::$instance->key;        if (is_null($key)) {            throw new InvalidArgumentException('Missing Wechat Config -- [key]');        }        ksort($data);        $string = md5(self::getSignContent($data).'&key='.$key);        Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);        return strtoupper($string);    }    /**     * Generate sign content.     *     * @author yansongda <me@yansongda.cn>     *     * @param array $data     */    public static function getSignContent($data): string    {        $buff = '';        foreach ($data as $k => $v) {            $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';        }        Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);        return trim($buff, '&');    }    /**     * Decrypt refund contents.     *     * @author yansongda <me@yansongda.cn>     *     * @param string $contents     */    public static function decryptRefundContents($contents): string    {        return openssl_decrypt(            base64_decode($contents),            'AES-256-ECB',            md5(self::$instance->key),            OPENSSL_RAW_DATA        );    }    /**     * Convert array to xml.     *     * @author yansongda <me@yansongda.cn>     *     * @param array $data     *     * @throws InvalidArgumentException     */    public static function toXml($data): string    {        if (!is_array($data) || count($data) <= 0) {            throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');        }        $xml = '<xml>';        foreach ($data as $key => $val) {            $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :                                       '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';        }        $xml .= '</xml>';        return $xml;    }    /**     * Convert xml to array.     *     * @author yansongda <me@yansongda.cn>     *     * @param string $xml     *     * @throws InvalidArgumentException     */    public static function fromXml($xml): array    {        if (!$xml) {            throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');        }        if (PHP_VERSION_ID < 80000) {            libxml_disable_entity_loader(true);        }        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);    }    /**     * Get service config.     *     * @author yansongda <me@yansongda.cn>     *     * @param string|null $key     * @param mixed|null  $default     *     * @return mixed|null     */    public function getConfig($key = null, $default = null)    {        if (is_null($key)) {            return $this->config->all();        }        if ($this->config->has($key)) {            return $this->config[$key];        }        return $default;    }    /**     * Get app id according to param type.     *     * @author yansongda <me@yansongda.cn>     *     * @param string $type     */    public static function getTypeName($type = ''): string    {        switch ($type) {            case '':                $type = 'app_id';                break;            case 'app':                $type = 'appid';                break;            default:                $type = $type.'_id';        }        return $type;    }    /**     * Get Base Uri.     *     * @author yansongda <me@yansongda.cn>     *     * @return string     */    public function getBaseUri()    {        return $this->baseUri;    }    /**     * processingApiResult.     *     * @author yansongda <me@yansongda.cn>     *     * @param $endpoint     *     * @throws GatewayException     * @throws InvalidArgumentException     * @throws InvalidSignException     *     * @return Collection     */    protected static function processingApiResult($endpoint, array $result)    {        if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) {            throw new GatewayException('Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''), $result);        }        if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) {            throw new BusinessException('Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'], $result);        }        if (false !== strpos($endpoint, 'xdc/apiv2getsignkey') ||            false !== strpos($endpoint, 'mmpaymkttransfers') ||            self::generateSign($result) === $result['sign']) {            return new Collection($result);        }        Events::dispatch(new Events\SignFailed('Wechat', '', $result));        throw new InvalidSignException('Wechat Sign Verify FAILED', $result);    }    /**     * setDevKey.     *     * @author yansongda <me@yansongda.cn>     *     * @throws GatewayException     * @throws InvalidArgumentException     * @throws InvalidSignException     * @throws Exception     *     * @return Support     */    private static function setDevKey()    {        if (Wechat::MODE_DEV == self::$instance->mode) {            $data = [                'mch_id' => self::$instance->mch_id,                'nonce_str' => Str::random(),            ];            $data['sign'] = self::generateSign($data);            $result = self::requestApi('https://api.mch.weixin.qq.com/xdc/apiv2getsignkey/sign/getsignkey', $data);            self::$instance->config->set('key', $result['sandbox_signkey']);        }        return self::$instance;    }    /**     * Set Http options.     *     * @author yansongda <me@yansongda.cn>     */    private function setHttpOptions(): self    {        if ($this->config->has('http') && is_array($this->config->get('http'))) {            $this->config->forget('http.base_uri');            $this->httpOptions = $this->config->get('http');        }        return $this;    }}
 |