[
  {
    "path": ".gitattributes",
    "content": "* text=auto\n\n/build export-ignore\n/tests export-ignore\n/docs export-ignore\n/.github export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n.scrutinizer.yml export-ignore\nphpstan.neon export-ignore\nphpunit.php export-ignore\nphpunit.xml export-ignore\nphpunit.xml.dist export-ignore\npint.json export-ignore\n.php-cs-fixer.dist.php  export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [overtrue]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "## 我用的环境\n\n* PHP 版本：\n* overtrue/wechat 版本：\n* 是否使用了框架？框架名称：\n\n## 问题及现象\n\n<!--\n\n描述你的问题现象，报错**贴截图**粘贴或者贴具体信息，提供**必要的代码段**\n\n如果你不提供相关的代码，我不会做任何应答，直接 close，感谢！\n\n请正确使用 Markdown: https://guides.github.com/features/mastering-markdown\n\n-->\n\n\n<!-- Love wechat? Please consider supporting our collective:\n👉  https://opencollective.com/wechat/donate -->\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "# This is a basic workflow to help you get started with Actions\n\nname: Deploy\n\n# Controls when the workflow will run\non:\n  # Triggers the workflow on push event but only for the 6.x branch(required the secrets environment)\n  push:\n    branches: [ 6.x ]\n    tags-ignore:\n      - '*'\n  pull_request:\n    branches: [ 6.x ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"build\"\n  build:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n    \n    env:\n      domain: \"https://easywechat.com/\"\n      local_dir: ./docs/.vitepress/dist/\n      remote_dir: /\n      thread: 10\n      region: ap-guangzhou\n      bucket: \"easywechat-1252049834\"\n      ignore: \"./.git*,*.DS_Store\"\n      COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }}\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n          run_install: false\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '>=22.12.0'\n          cache: 'pnpm'\n          cache-dependency-path: 'docs/pnpm-lock.yaml'\n      - name: Install dependencies\n        run: |\n          cd docs\n          pnpm install\n      - name: Build\n        run: |\n          cd docs\n          patch -p1 -i .vitepress/vitepress+1.6.3.patch\n          pnpm run build\n      - name: Install coscmd\n        if: env.COS_SECRET_ID\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install setuptools\n          python -m pip install https://github.com/tencentyun/coscmd/archive/refs/heads/master.zip\n      - name: Upload to cos\n        if: env.COS_SECRET_ID\n        run: |\n          coscmd config -a \"${{secrets.COS_SECRET_ID}}\" -s \"${{secrets.COS_SECRET_KEY}}\" -b $bucket -r $region -m $thread \n          coscmd upload -r -s $local_dir $remote_dir --ignore \"$ignore\"\n      - name: Refresh CDN\n        if: env.COS_SECRET_ID\n        env:\n          COS_SECRET_KEY: ${{secrets.COS_SECRET_KEY}}\n        run: ./docs/purge-caches\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\non: [push, pull_request]\n\njobs:\n  phpstan:\n    name: PHPStan\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@master\n    - name: Install Dependencies\n      run: composer install --no-progress\n    - name: Run PHPStan\n      run: ./vendor/bin/phpstan analyse --no-progress\n\n  php_cs_fixer:\n    name: PHP-CS-Fxier\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@master\n    - name: Install Dependencies\n      run: composer install --no-progress\n    - name: Run PHP-CS-Fxier\n      run: composer check-style\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\non: [push, pull_request]\n\njobs:\n  phpunit:\n    name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }}\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        php_version:\n          - 8.0\n          - 8.1\n          - 8.2\n          - 8.3\n          - 8.4\n          - 8.5\n        perfer:\n          - stable\n          - lowest\n    steps:\n    - uses: actions/checkout@master\n    - name: Install Dependencies\n      run: composer update --prefer-dist --no-interaction --no-suggest --prefer-${{ matrix.perfer }}\n    - name: Run PHPUnit\n      run: ./vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "*.DS_Store\n/vendor\nsftp-config.json\n/*.php\n!.php-cs-fixer.dist.php\n/.idea\n/coverage\n/.split\n/composer.lock\n.php-cs-fixer.cache\n.phpunit.result.cache\ncghooks.lock\ndocs/dist/\ndocs/.vuepress/.temp/\ndocs/.vuepress/.cache/\n.vercel\n.phplint-cache\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribute\n\n## Introduction\n\nFirst, thank you for considering contributing to wechat! It's people like you that make the open source community such a great community! 😊\n\nWe welcome any type of contribution, not only code. You can help with \n- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)\n- **Marketing**: writing blog posts, howto's, printing stickers, ...\n- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...\n- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.\n- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat).\n\n## Your First Contribution\n\nWorking on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).\n\n## Submitting code\n\nAny code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.\n\n## Code review process\n\nThe bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.\nIt is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?\n\n## Financial contributions\n\nWe also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat).\nAnyone can file an expense. If the expense makes sense for the development of the community, it will be \"merged\" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.\n\n## Questions\n\nIf you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!).\nYou can also reach us at hello@wechat.opencollective.com.\n\n## Credits\n\n### Contributors\n\nThank you to all the people who have already contributed to wechat!\n<a href=\"graphs/contributors\"><img src=\"https://opencollective.com/wechat/contributors.svg?width=890\" /></a>\n\n\n### Backers\n\nThank you to all our backers! [[Become a backer](https://opencollective.com/wechat#backer)]\n\n<a href=\"https://opencollective.com/wechat#backers\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/backers.svg?width=890\"></a>\n\n\n### Sponsors\n\nThank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/wechat#sponsor))\n\n<a href=\"https://opencollective.com/wechat/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/wechat/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/wechat/sponsor/9/avatar.svg\"></a>\n\n<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template -->"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) overtrue <i@overtrue.me>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# [EasyWeChat](https://easywechat.com)\n\n📦 一个 PHP 微信开发 SDK，开源 SaaS 平台提供商 [微擎](https://www.w7.cc/) 旗下开源产品。\n\n[![Test Status](https://github.com/w7corp/easywechat/workflows/Test/badge.svg)](https://github.com/w7corp/easywechat/actions)\n[![Lint Status](https://github.com/w7corp/easywechat/workflows/Lint/badge.svg)](https://github.com/w7corp/easywechat/actions)\n[![Latest Stable Version](https://poser.pugx.org/w7corp/easywechat/v/stable.svg)](https://packagist.org/packages/w7corp/easywechat)\n[![Latest Unstable Version](https://poser.pugx.org/w7corp/easywechat/v/unstable.svg)](https://packagist.org/packages/w7corp/easywechat)\n[![Total Downloads](https://poser.pugx.org/w7corp/easywechat/downloads)](https://packagist.org/packages/w7corp/easywechat)\n[![License](https://poser.pugx.org/w7corp/easywechat/license)](https://packagist.org/packages/w7corp/easywechat)\n\n## 环境需求\n\n- PHP >= 8.0.2\n- [Composer](https://getcomposer.org/) >= 2.0\n\n## 安装\n\n```bash\ncomposer require w7corp/easywechat\n```\n\n## 使用示例\n\n基本使用（以公众号服务端为例）:\n\n```php\n<?php\n\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exxx',\n    'secret' => 'f1c242f4f28f735d4687abb469072xxx',\n    'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n    'token' => 'easywechat',\n];\n\n$app = new Application($config);\n\n$server = $app->getServer();\n\n$server->with(fn() => \"您好！EasyWeChat！\");\n\n$response = $server->serve();\n```\n\n## 文档和链接\n\n[官网](https://easywechat.com) · [讨论](https://github.com/w7corp/easywechat/discussions) · [更新策略](https://github.com/w7corp/easywechat/security/policy)\n\n## :heart: 支持我\n\n如果你喜欢我的项目并想支持它，[点击这里 :heart:](https://github.com/sponsors/overtrue)\n\n\n## 可爱的贡献者们\n\n<a href=\"https://github.com/w7corp/easywechat/graphs/contributors\"><img src=\"https://opencollective.com/wechat/contributors.svg?width=890\" /></a>\n\n## License\n\nMIT\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# 更新策略 Security Policy\n\n## 支持的版本 Supported Versions\n\n| Version | 状态          |\n| ------- | ------------------ |\n| 6.x   | ✅ 维护中|\n| 5.x   | 🚨 仅安全修复，不建议旧项目升级至 6.x |\n| 4.x  | ❌ 不再维护，建议升级至 5.x（仅 OAuth 方法有变化） |\n| 3.x   | ❌ 不再维护               |\n| 2.x   | ❌ 不再维护               |\n| 1.x   | ❌ 不再维护               |\n\n## 漏洞报告 Reporting a Vulnerability\n\n如果你发现了安全漏洞，请发邮件给我 `anzhengchao@gmail.com`，或者提 issue。\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"w7corp/easywechat\",\n  \"description\": \"微信SDK\",\n  \"keywords\": [\n    \"easywechat\",\n    \"wechat\",\n    \"weixin\",\n    \"weixin-sdk\",\n    \"sdk\"\n  ],\n  \"license\": \"MIT\",\n  \"authors\": [\n    {\n      \"name\": \"overtrue\",\n      \"email\": \"anzhengchao@gmail.com\"\n    }\n  ],\n  \"require\": {\n    \"php\": \">=8.0.2\",\n    \"ext-fileinfo\": \"*\",\n    \"ext-openssl\": \"*\",\n    \"ext-simplexml\": \"*\",\n    \"ext-libxml\": \"*\",\n    \"ext-curl\": \"*\",\n    \"nyholm/psr7\": \"^1.5\",\n    \"nyholm/psr7-server\": \"^1.0\",\n    \"overtrue/socialite\": \"^3.5.4|^4.0.1\",\n    \"psr/simple-cache\": \"^1.0|^2.0|^3.0\",\n    \"psr/http-client\": \"^1.0\",\n    \"symfony/cache\": \"^5.4|^6.0|^7.0\",\n    \"symfony/http-foundation\": \"^5.4|^6.0|^7.0\",\n    \"symfony/psr-http-message-bridge\": \"^2.1.2|^6.4.0|^7.1\",\n    \"symfony/http-client\": \"^5.4|^6.0|^7.0\",\n    \"symfony/mime\": \"^5.4|^6.0|^7.0\",\n    \"symfony/polyfill-php81\": \"^1.25\",\n    \"thenorthmemory/xml\": \"^1.0\"\n  },\n  \"require-dev\": {\n    \"mikey179/vfsstream\": \"^1.6\",\n    \"mockery/mockery\": \"^1.4.4\",\n    \"phpstan/phpstan\": \"^1.0 | ^2\",\n    \"phpunit/phpunit\": \"^9.5\",\n    \"symfony/var-dumper\": \"^5.2|^6|^7\",\n    \"jetbrains/phpstorm-attributes\": \"^1.0\",\n    \"laravel/pint\": \"^1.2\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"EasyWeChat\\\\\": \"src/\"\n    }\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"EasyWeChat\\\\Tests\\\\\": \"tests/\"\n    }\n  },\n  \"scripts\": {\n    \"post-merge\": \"composer install\",\n    \"phpstan\": \"phpstan analyse --memory-limit=-1\",\n    \"check-style\": \"vendor/bin/pint --test\",\n    \"fix-style\": \"vendor/bin/pint\",\n    \"test\": \"phpunit --colors\"\n  },\n  \"conflict\": {\n    \"overtrue/wechat\": \"*\"\n  },\n  \"config\": {\n    \"allow-plugins\": {\n      \"composer/package-versions-deprecated\": true,\n      \"php-http/discovery\": true\n    }\n  }\n}\n"
  },
  {
    "path": "docs/.editorconfig",
    "content": "[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "node_modules/\npackage-lock.json\nyarn-error.log\n.vitepress/dist/\n.vitepress/cache/\n"
  },
  {
    "path": "docs/.npmrc",
    "content": "auto-install-peers=true\n"
  },
  {
    "path": "docs/.prettierrc",
    "content": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\",\n  \"printWidth\": 75\n}\n"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "content": "import path from 'path'\nimport versions from './versions'\n\nconst latest = versions[0]\n\nconst nav = [\n  {\n    text: '首页',\n    link: '/'\n  },\n  {\n    text: '文档',\n    activeMatch: `^/([0-9]\\.x)/`,\n    items: versions.map((version) => ({\n      text: version,\n      link: `/${version}/`\n    }))\n  },\n  {\n    text: '视频',\n    link: 'https://wiki.w7.cc/college/collectiondetail/3'\n  },\n  {\n    text: '讨论',\n    link: 'https://github.com/w7corp/easywechat/discussions'\n  },\n  {\n    text: '赞助',\n    link: 'https://github.com/sponsors/overtrue'\n  },\n  {\n    text: '蓝P站',\n    link: 'https://wechatpay.im/'\n  },\n]\n\nexport const sidebar = versions.reduce(\n  (sidebars, version) => ({\n    ...sidebars,\n    [`/${version}/`]: require(path.join(\n      __dirname,\n      `../src/${version}/sidebar`\n    ))\n  }),\n  {}\n)\n\nexport default {\n  lang: 'zh-CN',\n  title: 'EasyWeChat',\n  description: '一个 PHP 微信开发 SDK',\n  srcDir: 'src',\n  srcExclude: [],\n  scrollOffset: 'header',\n  metaChunk: true,\n\n  head: [\n    ['link', { rel: 'icon', href: '/favicon.svg' }],\n    ['meta', { name: 'twitter:site', content: '@easywechat' }],\n    ['meta', { name: 'twitter:card', content: 'summary' }],\n    [\n      'meta',\n      {\n        name: 'twitter:image',\n        content: 'https://easywechat.com/logo.svg'\n      }\n    ],\n    // google analytics, without tracing dev\n    ...(process?.argv?.[2] === 'dev' ? [] : [\n      [\n        'script',\n        { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-ZVHYZEP1SR' }\n      ],\n      [\n        'script',\n        {},\n        `window.dataLayer = window.dataLayer || [];\n        function gtag(){dataLayer.push(arguments);}\n        gtag('js', new Date());\n        gtag('config', 'G-ZVHYZEP1SR');`\n      ],\n    ]),\n    // end google analytics\n  ],\n\n  markdown: {\n    codeCopyButtonTitle: '复制',\n    lineNumbers: true,\n  },\n\n  themeConfig: {\n    nav,\n    sidebar,\n\n    logo: '/logo-icon.svg',\n\n    algolia: {\n      placeholder: '搜索文档',\n      translations: {\n        button: {\n          buttonText: '搜索文档',\n          buttonAriaLabel: '搜索文档'\n        },\n        modal: {\n          searchBox: {\n            resetButtonTitle: '清除查询条件',\n            resetButtonAriaLabel: '清除查询条件',\n            cancelButtonText: '取消',\n            cancelButtonAriaLabel: '取消'\n          },\n          startScreen: {\n            recentSearchesTitle: '搜索历史',\n            noRecentSearchesText: '没有搜索历史',\n            saveRecentSearchButtonTitle: '保存至搜索历史',\n            removeRecentSearchButtonTitle: '从搜索历史中移除',\n            favoriteSearchesTitle: '收藏',\n            removeFavoriteSearchButtonTitle: '从收藏中移除'\n          },\n          errorScreen: {\n            titleText: '无法获取结果',\n            helpText: '你可能需要检查你的网络连接'\n          },\n          footer: {\n            selectText: '选择',\n            navigateText: '切换',\n            closeText: '关闭',\n            searchByText: '搜索提供者'\n          },\n          noResultsScreen: {\n            noResultsText: '无法找到相关结果',\n            suggestedQueryText: '你可以尝试查询',\n            reportMissingResultsText: '你认为该查询应该有结果？',\n            reportMissingResultsLinkText: '点击反馈'\n          },\n        },\n      },\n      indexName: 'easywechat',\n      appId: 'X3KJL5SQXD',\n      apiKey: '5c5ba71b35c48411f245bef4c695fc36'\n      // searchParameters: {\n      //   facetFilters: ['version:v3']\n      // }\n    },\n\n    returnToTopLabel: '回到顶部',\n    sidebarMenuLabel: '菜单',\n    darkModeSwitchLabel: '主题模式',\n    lightModeSwitchTitle: '浅色模式',\n    darkModeSwitchTitle: '深色模式',\n\n    outline: {\n      level: [2, 3],\n      label: '页面导航',\n    },\n\n    docFooter: {\n      prev: '上一页',\n      next: '下一页'\n    },\n\n    notFound: {\n      title: '未找到',\n      quote: '您所访问的页面未找到，或者已失效',\n      linkLabel: '返回首页',\n      linkText: '返回首页',\n    },\n\n    // carbonAds: {\n    //   code: '',\n    //   placement: ''\n    // },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/w7corp/easywechat' },\n      { icon: 'twitter', link: 'https://twitter.com/overtrue666' },\n      {\n        icon: {\n          svg: `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0,0,256,256\">\n                <g transform=\"translate(-7.68,-7.68) scale(1.06,1.06)\"><g fill=\"currentColor\" fill-rule=\"nonzero\" stroke=\"none\" stroke-width=\"1\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"10\" stroke-dasharray=\"\" stroke-dashoffset=\"0\" font-family=\"none\" font-weight=\"none\" font-size=\"none\" text-anchor=\"none\" style=\"mix-blend-mode: normal\"><g transform=\"scale(4,4)\"><path d=\"M46.846,7.021c-1.357,-0.069 -2.725,0.03 -4.063,0.315c-1.24,0.266 -2.061,1.506 -1.797,2.756c0.264,1.25 1.494,2.077 2.735,1.811c3.819,-0.817 7.979,0.345 10.782,3.465c2.793,3.13 3.545,7.441 2.344,11.182c-0.391,1.221 0.273,2.52 1.484,2.914c1.201,0.394 2.5,-0.285 2.891,-1.496c1.68,-5.266 0.664,-11.27 -3.281,-15.67c-2.96,-3.298 -7.013,-5.08 -11.095,-5.277zM26.142,13.951c-4.19,0.453 -10.04,3.74 -15.235,8.977c-5.655,5.708 -8.907,11.782 -8.907,17.008c0,10.001 12.706,16.064 25.158,16.064c16.319,0 27.189,-9.577 27.189,-17.166c0,-4.587 -3.819,-7.195 -7.266,-8.268c-0.85,-0.256 -1.455,-0.374 -1.016,-1.496c0.957,-2.431 1.113,-4.567 0.078,-6.063c-1.943,-2.805 -7.334,-2.658 -13.438,-0.079c0,0 -1.895,0.906 -1.406,-0.63c0.938,-3.041 0.762,-5.611 -0.703,-7.087c-1.036,-1.044 -2.55,-1.467 -4.454,-1.26zM46.455,15.211c-0.664,-0.03 -1.299,0.02 -1.953,0.157c-1.074,0.226 -1.797,1.358 -1.563,2.441c0.234,1.073 1.279,1.732 2.344,1.496c1.279,-0.276 2.735,0.138 3.672,1.181c0.938,1.043 1.182,2.451 0.781,3.701c-0.332,1.043 0.215,2.175 1.25,2.52c1.035,0.335 2.168,-0.217 2.5,-1.26c0.82,-2.559 0.283,-5.492 -1.641,-7.638c-1.434,-1.604 -3.397,-2.51 -5.39,-2.598zM28.486,28.518c8.301,0.295 14.981,4.488 15.548,10.237c0.645,6.575 -6.866,12.717 -16.798,13.701c-9.932,0.984 -18.575,-3.582 -19.22,-10.157c-0.645,-6.575 6.944,-12.717 16.876,-13.701c1.24,-0.129 2.412,-0.119 3.594,-0.08zM24.579,33.4c-3.594,0.345 -6.973,2.441 -8.516,5.591c-2.09,4.282 -0.088,9.075 4.688,10.63c4.951,1.604 10.782,-0.886 12.813,-5.512c2.002,-4.518 -0.498,-9.124 -5.391,-10.394c-1.181,-0.305 -2.392,-0.433 -3.594,-0.315zM26.845,38.913c0.156,0 0.244,0.02 0.391,0.079c0.606,0.226 0.879,0.896 0.547,1.496c-0.352,0.6 -1.113,0.876 -1.719,0.63c-0.596,-0.246 -0.801,-0.906 -0.469,-1.496c0.264,-0.444 0.781,-0.7 1.25,-0.709zM22.235,40.33c0.42,0.01 0.869,0.069 1.25,0.236c1.553,0.669 2.041,2.461 1.094,4.016c-0.957,1.545 -2.979,2.284 -4.531,1.575c-1.524,-0.699 -1.973,-2.51 -1.016,-4.016c0.713,-1.122 1.953,-1.831 3.203,-1.811z\"></path></g></g></g>\n                </svg>`\n        },\n        ariaLabel: 'Weibo',\n        link: 'https://weibo.com/44294631'\n      }\n    ],\n\n    editLink: {\n      pattern:\n        'https://github.com/w7corp/EasyWeChat/edit/6.x/docs/src/:path',\n      text: '帮助我们改善此页面！'\n    },\n\n    license: {\n      text: 'MIT License',\n      link: 'https://opensource.org/licenses/MIT'\n    },\n    copyright: `Copyright © 2013-${new Date().getFullYear()} 微擎 <a class=\"ml-4\" href=\"https://beian.miit.gov.cn/\" target=\"_blank\">皖ICP备19002904号-6</a>`\n  },\n\n  vite: {\n    define: {\n      __VUE_OPTIONS_API__: false\n    },\n    optimizeDeps: {\n      include: ['gsap', 'dynamics.js'],\n      exclude: []\n    },\n    // @ts-ignore\n    ssr: {\n      external: []\n    },\n    server: {\n      host: true,\n      fs: {\n        // for when developing with locally linked theme\n        allow: ['../..']\n      }\n    },\n    json: {\n      stringify: true\n    }\n  },\n\n  vue: {\n    reactivityTransform: true\n  }\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/components/Banner.vue",
    "content": "<script setup>\n/**\n * Adding a new banner:\n * 1. uncomment the banner slot in ../index.ts\n * 2. uncomment and update BANNER_ID in ../../inlined-scripts/restorePreferences.ts\n * 3. update --vt-banner-height if necessary\n */\n\nlet open = $ref(true)\n\n/**\n * Call this if the banner is dismissible\n */\nfunction dismiss() {\n  open = false\n  document.documentElement.classList.add('banner-dismissed')\n  localStorage.setItem(`vue-docs-banner-${__VUE_BANNER_ID__}`, 'true')\n}\n</script>\n\n<template>\n  <div class=\"banner\" v-if=\"open\"></div>\n</template>\n\n<style>\nhtml:not(.banner-dismissed) {\n  --vt-banner-height: 24px;\n}\n</style>\n\n<style scoped>\n.banner {\n  position: fixed;\n  z-index: var(--vp-z-index-banner);\n  box-sizing: border-box;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: var(--vt-banner-height);\n  line-height: var(--vt-banner-height);\n  text-align: center;\n  font-size: 12px;\n  font-weight: 600;\n  color: #fff;\n  background-color: var(--vt-c-green);\n}\n\n.banner-dismissed .banner {\n  display: none;\n}\n\na {\n  text-decoration: underline;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/theme/components/Footer.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useData } from 'vitepress';\n\nconst { theme } = useData()\n</script>\n\n<template>\n  <div class=\"text-center border-t dark:border-black leading-loose py-6 text-xs\">\n    <p v-if=\"theme.license\" class=\"license\">\n      Released under the\n      <a class=\"link\" :href=\"theme.license.link\" no-icon>\n        {{ theme.license.text }} </a>.\n    </p>\n    <p v-if=\"theme.copyright\" class=\"copyright\" v-html=\"theme.copyright\"></p>\n  </div>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/theme/components/SponsorsAside.vue",
    "content": "<script setup lang=\"ts\">\nimport SponsorsGroup from './SponsorsGroup.vue'\nimport { useData } from 'vitepress'\nconst { frontmatter } = useData()\n</script>\n\n<template>\n  <div v-if=\"frontmatter.sponsors !== false\">\n    <a class=\"sponsors-aside-text\">Sponsors</a>\n    <SponsorsGroup tier=\"special\" />\n    <SponsorsGroup tier=\"platinum\" />\n  </div>\n</template>\n\n<style>\na.sponsors-aside-text {\n  color: var(--vt-c-text-3);\n  display: block;\n  margin: 3em 0 1em;\n  font-weight: 700;\n  font-size: 11px;\n  text-transform: uppercase;\n  letter-spacing: 0.4px;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/theme/components/SponsorsGroup.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineProps, onMounted, onUnmounted, ref } from 'vue';\ninterface Sponsor {\n  url: string\n  img: string\n  name: string\n}\n\ninterface SponsorData {\n  special: Sponsor[]\n  platinum: Sponsor[]\n  platinum_china: Sponsor[]\n  gold: Sponsor[]\n  silver: Sponsor[]\n  bronze: Sponsor[]\n}\n\n// shared data across instances so we load only once\nlet data = ref<SponsorData>()\nlet pending = false\n\nconst { tier, placement = 'aside' } = defineProps<{\n  tier: keyof SponsorData\n  placement?: 'aside' | 'page' | 'landing'\n}>()\n\nlet container = ref<HTMLElement>()\nlet visible = ref(false)\n\nonMounted(async () => {\n  // only render when entering view\n  const observer = new IntersectionObserver(\n    (entries) => {\n      if (entries[0].isIntersecting) {\n        visible = true\n        observer.disconnect()\n      }\n    },\n    { rootMargin: '0px 0px 300px 0px' }\n  )\n  observer.observe(container)\n  onUnmounted(() => observer.disconnect())\n\n  // load data\n  if (!pending) {\n    pending = true\n    // data = await (await fetch(`${base}/data.json`)).json()\n  }\n})\n</script>\n\n<template>\n  <div ref=\"container\" class=\"sponsor-container\" :class=\"[tier.startsWith('plat') ? 'platinum' : tier, placement]\">\n    <template v-if=\"data && visible\">\n      <a v-for=\"{ url, img, name } of data[tier]\" class=\"sponsor-item\" :href=\"url\" target=\"_blank\" rel=\"sponsored noopener\">\n        <picture v-if=\"img.endsWith('png')\">\n          <source type=\"image/avif\" :srcset=\"`${base}/images/${img.replace(/\\.png$/, '.avif')}`\" />\n          <img :src=\"`${base}/images/${img}`\" :alt=\"name\" />\n        </picture>\n        <img v-else :src=\"`${base}/images/${img}`\" :alt=\"name\" />\n      </a>\n    </template>\n    <a v-if=\"placement !== 'page' && tier !== 'special'\" href=\"https://github.com/sponsors/overtrue\" class=\"sponsor-item action\">Your logo</a>\n  </div>\n</template>\n\n<style scoped>\n.sponsor-container {\n  --max-width: 100%;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(var(--max-width), 1fr));\n  column-gap: 4px;\n}\n\n.sponsor-container.platinum {\n  --max-width: 240px;\n}\n\n.sponsor-container.gold {\n  --max-width: 180px;\n}\n\n.sponsor-container.silver {\n  --max-width: 140px;\n}\n\n.sponsor-item {\n  margin: 2px 0;\n  background-color: var(--vt-c-white-soft);\n  display: flex;\n  justify-content: space-around;\n  align-items: center;\n  border-radius: 2px;\n  transition: background-color 0.2s ease;\n  height: calc(var(--max-width) / 2 - 6px);\n}\n\n.sponsor-item.action {\n  font-size: 11px;\n  color: var(--vt-c-text-3);\n}\n\n.sponsor-item img {\n  max-width: calc(var(--max-width) - 30px);\n  max-height: calc(var(--max-width) / 2 - 20px);\n}\n\n.special .sponsor-item {\n  height: 160px;\n}\n\n.special .sponsor-item img {\n  max-width: 300px;\n  max-height: 150px;\n}\n\n/* dark mode */\n.dark .aside .sponsor-item,\n.dark .landing .sponsor-item {\n  background-color: var(--vt-c-bg-soft);\n}\n\n.aside .sponsor-item img,\n.landing .sponsor-item img {\n  transition: filter 0.2s ease;\n}\n\n.dark .aside .sponsor-item img,\n.dark .landing .sponsor-item img {\n  filter: grayscale(1) invert(1);\n}\n\n.dark .aside .sponsor-item:hover,\n.dark .landing .sponsor-item:hover {\n  color: var(--vt-c-indigo);\n  background-color: var(--vt-c-white-mute);\n}\n\n.dark .sponsor-item:hover img {\n  filter: none;\n}\n\n/* aside mode (on content pages) */\n.sponsor-container.platinum.aside {\n  --max-width: 110px;\n  column-gap: 1px;\n}\n\n.aside .sponsor-item {\n  margin: 1px 0;\n}\n\n.aside .special .sponsor-item {\n  width: 100%;\n  height: 60px;\n}\n\n.aside .special .sponsor-item img {\n  width: 120px;\n}\n\n.aside .platinum .sponsor-item {\n  width: 111px;\n  height: 50px;\n}\n\n.aside .platinum .sponsor-item img {\n  max-width: 88px;\n}\n\n/* narrow, aside will be hidden under this state so it's mutually exclusive */\n@media (max-width: 720px) {\n  .sponsor-container.platinum {\n    --max-width: 180px;\n  }\n\n  .sponsor-container.gold {\n    --max-width: 140px;\n  }\n\n  .sponsor-container.silver {\n    --max-width: 120px;\n  }\n}\n\n@media (max-width: 480px) {\n  .sponsor-container.platinum {\n    --max-width: 150px;\n  }\n\n  .sponsor-container.gold {\n    --max-width: 120px;\n  }\n\n  .sponsor-container.silver {\n    --max-width: 100px;\n  }\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/theme/components/VersionTag.vue",
    "content": "<template>\n  <sup\n    class=\"bg-green-500 text-xs text-white px-2 py-1 rounded-lg align-top rounded-bl-none\"\n    title=\"该特性需要更新到此版本可用\"\n    ><slot\n  /></sup>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "content": "import './styles/index.css'\nimport { h, App } from 'vue'\nimport SponsorsAside from './components/SponsorsAside.vue'\nimport VersionTag from './components/VersionTag.vue'\nimport Footer from './components/Footer.vue'\nimport DefaultTheme from 'vitepress/theme'\n\nexport default Object.assign({\n  ...DefaultTheme,\n  Layout: () => {\n    // @ts-ignore\n    return h(DefaultTheme.Layout, null, {\n      // banner: () => h(Banner),\n      'aside-mid': () => h(SponsorsAside),\n      'layout-bottom': () => h(Footer)\n    })\n  },\n  enhanceApp({ app }: { app: App }) {\n    app.component('version-tag', VersionTag)\n  }\n})\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/badges.css",
    "content": ".vt-badge.wip:before {\n  content: 'WIP';\n}\n\n.vt-badge.ts {\n  background-color: #3178c6;\n}\n.vt-badge.ts:before {\n  content: 'TS';\n}\n\n.vt-badge.dev-only,\n.vt-badge.experimental {\n  color: var(--vt-c-text-light-1);\n  background-color: var(--vt-c-yellow);\n}\n\n.vt-badge.dev-only:before {\n  content: 'Dev only';\n}\n\n.vt-badge.experimental:before {\n  content: 'Experimental';\n}\n\n.vt-badge[data-text]:before {\n  content: attr(data-text);\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/index.css",
    "content": "@import './layout.css';\n@import './pages.css';\n@import './badges.css';\n@import './options-boxes.css';\n@import './inline-demo.css';\n@import './utilities.css';\n@import './style-guide.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/inline-demo.css",
    "content": ".vt-doc a[href^=\"https://sfc.vuejs.org\"]:before\n{\n  content: '▶';\n  width: 20px;\n  height: 20px;\n  display: inline-block;\n  border-radius: 10px;\n  vertical-align: middle;\n  position: relative;\n  top: -2px;\n  color: var(--vt-c-green);\n  border: 2px solid var(--vt-c-green);\n  margin-right: 8px;\n  margin-left: 4px;\n  line-height: 15px;\n  padding-left: 4.5px;\n  font-size: 11px;\n}\n\n.demo {\n  padding: 22px 24px;\n  border-radius: 8px;\n  box-shadow: var(--vt-shadow-2);\n  margin-bottom: 1.2em;\n  transition: background-color 0.5s ease;\n}\n\n.dark .demo {\n  background-color: var(--vt-c-bg-soft);\n}\n\n.demo p {\n  margin: 0;\n}\n\n.demo button {\n  background-color: var(--vt-c-bg-mute);\n  transition: background-color 0.5s;\n  padding: 5px 12px;\n  border: 1px solid var(--vt-c-divider);\n  border-radius: 8px;\n  font-size: 0.9em;\n  font-weight: 600;\n}\n\n.demo button + button {\n  margin-left: 1em;\n}\n\n.demo input,\n.demo textarea,\n.demo select {\n  border: 1px solid var(--vt-c-divider);\n  border-radius: 4px;\n  padding: 0.2em 0.6em;\n  margin-top: 10px;\n  background: transparent;\n  transition: background-color 0.5s;\n}\n\n.dark .demo select {\n  background: var(--vt-c-bg-soft);\n}\n\n.dark .demo select option {\n  background: transparent;\n}\n\n.demo input:not([type]):focus,\n.demo textarea:focus,\n.demo select:focus {\n  outline: 1px solid blue;\n}\n\n.demo select {\n  /* this was set by normalize.css */\n  -webkit-appearance: listbox;\n}\n\n.demo label {\n  margin: 0 1em 0 0.4em;\n}\n\n.demo select[multiple] {\n  width: 100px;\n}\n\n.demo h1 {\n  margin: 10px 0 0;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/layout.css",
    "content": ".VPContent,\n.VPContent .VPContentPage,\n.VPContent .VPContentPage main,\n.VPContent .VPContentPage main>div,\n.VPContent .VPContentPage main>div>div {\n  @apply flex-1 flex flex-col;\n}\n\n.dark .im-home {\n  --im-gradient-p1: #8356dc;\n  --im-gradient-p3: #5908a6;\n  --im-gradient-p2: #044f1e;\n  --im-gradient-p4: #49bcb7;\n}\n\n.im-home {\n  --im-gradient-p1: #ecd8ff;\n  --im-gradient-p2: #e8fca7;\n  --im-gradient-p3: #dafbe1;\n  --im-gradient-p4: #ffd8b5;\n  background: linear-gradient(-40deg, var(--im-gradient-p1), var(--im-gradient-p2), var(--im-gradient-p3), var(--im-gradient-p4));\n  background-size: 120% 120%;\n}\n\n.im-home .top {\n  --vp-nav-bg-color: transparent;\n  --vp-c-bg: transparent;\n  --vp-c-gutter: transparent;\n  --vp-c-bg-alt: transparent;\n  --vp-c-divider: transparent;\n}\n\n.im-home .VPContent {\n  justify-content: center;\n}\n\n.im-home .bash-composer:before {\n  content: \"$\";\n  letter-spacing: .35rem;\n  opacity: 1;\n  animation: im-blink 1s ease infinite;\n}\n\n@keyframes im-blink {\n  0% {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0;\n  }\n\n  100% {\n    opacity: 8;\n  }\n}\n\n.im-home .border-t {\n  border-top-color: transparent;\n}\n\n.dark .im-home .dark\\:border-black {\n  --tw-border-opacity: 0;\n}\n\n.DocSearch-Container {\n  backdrop-filter: blur(10px);\n}\n\n.VPContent.has-sidebar .edit-info:not(:has(>.last-updated)) {\n  justify-content: flex-end;\n  text-align: right;\n}\n\n.vp-code-group .tabs::-webkit-scrollbar,\n.vp-code-group .tabs::-webkit-scrollbar-track,\n.vp-doc [class*=\"language-\"] pre::-webkit-scrollbar,\n.vp-doc [class*=\"language-\"] pre::-webkit-scrollbar-track,\n.VPSidebar::-webkit-scrollbar,\n.VPSidebar::-webkit-scrollbar-track {\n  background-color: initial\n}\n\n.vp-code-group .tabs::-webkit-scrollbar-thumb:active,\n.vp-code-group .tabs::-webkit-scrollbar-thumb:hover,\n.vp-doc [class*=\"language-\"] pre::-webkit-scrollbar-thumb:active,\n.vp-doc [class*=\"language-\"] pre::-webkit-scrollbar-thumb:hover,\n.VPSidebar::-webkit-scrollbar-thumb:active,\n.VPSidebar::-webkit-scrollbar-thumb:hover {\n  background-color: var(--vp-c-text-1) !important\n}\n\n.vp-code-group .tabs:active::-webkit-scrollbar-thumb,\n.vp-code-group .tabs:focus-within::-webkit-scrollbar-thumb,\n.vp-code-group .tabs:focus::-webkit-scrollbar-thumb,\n.vp-code-group .tabs:hover::-webkit-scrollbar-thumb,\n.vp-code-group .tabs:active::-webkit-scrollbar-thumb,\n.vp-doc [class*=\"language-\"] pre:focus-within::-webkit-scrollbar-thumb,\n.vp-doc [class*=\"language-\"] pre:focus::-webkit-scrollbar-thumb,\n.vp-doc [class*=\"language-\"] pre:hover::-webkit-scrollbar-thumb,\n.VPSidebar:active::-webkit-scrollbar-thumb,\n.VPSidebar:focus-within::-webkit-scrollbar-thumb,\n.VPSidebar:focus::-webkit-scrollbar-thumb,\n.VPSidebar:hover::-webkit-scrollbar-thumb {\n  background-clip: content-box;\n  background-color: var(--vp-c-text-3);\n  border: 4px solid #0000;\n  border-radius: 10px\n}\n\n.custom-block:not(.im-badges)[class^=\"im-\"] {\n  display: flex;\n  align-items: center;\n}\n\n.custom-block:not(.im-badges)[class^=\"im-\"] .custom-block-title {\n  font-size: calc(var(--vp-custom-block-font-size, 14px) * 1.3);\n  letter-spacing: .25em;\n}\n\n.custom-block.info.im-badges {\n  background-color: inherit;\n  margin: 0;\n  padding: 0;\n}\n\n.custom-block.info.im-badges .custom-block-title {\n  display: none;\n}\n\n.custom-block.info.im-badges p:not(.custom-block-title) {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.custom-block.info.im-badges p:not(.custom-block-title) img {\n  margin: 0 .25em .25em 0;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/options-boxes.css",
    "content": ".next-steps {\n  margin-top: 3rem;\n}\n\n.next-steps .vt-box {\n  border: 1px solid var(--vt-c-bg-soft);\n}\n\n.next-steps .vt-box:hover {\n  border-color: var(--vt-c-green-light);\n  transition: border-color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.vt-doc .next-steps-link {\n  font-size: 20px;\n  line-height: 1.4;\n  letter-spacing: -0.02em;\n  margin-bottom: 0.75em;\n  display: block;\n  color: var(--vt-c-green);\n}\n\n.vt-doc .next-steps-caption {\n  margin-bottom: 0;\n  color: var(--vt-c-text-2);\n  transition: color 0.5s;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/pages.css",
    "content": "/* always show anchors on /api/ and /style-guide/ pages */\n.vt-doc.api h2 .header-anchor,\n.vt-doc.style-guide h2 .header-anchor {\n  opacity: 1;\n}\n\n.vt-doc.sponsor h3 {\n  text-align: center;\n  padding-bottom: 1em;\n  border-bottom: 1px solid var(--vt-c-divider-light);\n}\n\n.vt-doc.sponsor h3 .header-anchor {\n  display: none;\n}\n\n.custom-block.details.bg-transparent {\n  background-color: transparent\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/style-guide.css",
    "content": ".style-example {\n  border-radius: 8px 8px 12px 12px;\n  margin: 1.6em 0;\n  padding: 1.6em 1.6em 0.1px;\n  position: relative;\n  border: 1px solid transparent;\n  transition: background-color 0.25s ease, border-color 0.25s ease;\n}\n\n.vt-doc .style-example h3 {\n  margin: 0;\n  font-size: 1.1em;\n}\n\n.style-example-bad {\n  background: #f7e8e8;\n}\n.dark .style-example-bad {\n  background: transparent;\n  border-color: var(--vt-c-red);\n}\n\n.style-example-bad h3 {\n  color: var(--vt-c-red);\n}\n\n.style-example-good {\n  background: #ecfaf7;\n}\n.dark .style-example-good {\n  background: transparent;\n  border-color: var(--vt-c-green);\n}\n\n.style-example-good h3 {\n  color: var(--vt-c-green);\n}\n\n.details summary {\n  font-weight: bold !important;\n}\n\n.style-verb {\n  font-size: 0.6em;\n  display: inline-block;\n  border-radius: 6px;\n  font-size: 0.65em;\n  line-height: 1;\n  font-weight: 600;\n  padding: 0.35em 0.4em 0.3em;\n  position: relative;\n  top: -0.15em;\n  margin-right: 0.5em;\n  color: var(--vt-c-bg);\n  transition: color 0.5s;\n  background-color: var(--vt-c-brand);\n}\n\n.style-verb.avoid {\n  background-color: var(--vt-c-red);\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/styles/utilities.css",
    "content": ".nowrap {\n  white-space: nowrap;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n"
  },
  {
    "path": "docs/.vitepress/versions.ts",
    "content": "export default [\"6.x\", \"5.x\", \"4.x\", \"3.x\"];\n"
  },
  {
    "path": "docs/.vitepress/vitepress+1.6.3.patch",
    "content": "diff --git a/node_modules/vitepress/dist/node/chunk-Zsoi3j4v.js b/node_modules/vitepress/dist/node/chunk-Zsoi3j4v.js\nindex 8cfe39e..f37e255 100644\n--- a/node_modules/vitepress/dist/node/chunk-Zsoi3j4v.js\n+++ b/node_modules/vitepress/dist/node/chunk-Zsoi3j4v.js\n@@ -35149,16 +35149,17 @@ function createContainer(klass, defaultTitle, md) {\n     {\n       render(tokens, idx, _options, env) {\n         const token = tokens[idx];\n-        const info = token.info.trim().slice(klass.length).trim();\n-        const attrs = md.renderer.renderAttrs(token);\n         if (token.nesting === 1) {\n+          token.attrJoin(\"class\", `${klass} custom-block`);\n+          const attrs = md.renderer.renderAttrs(token);\n+          const info = token.info.trim().slice(klass.length).trim();\n           const title = md.renderInline(info || defaultTitle, {\n             references: env.references\n           });\n           if (klass === \"details\")\n-            return `<details class=\"${klass} custom-block\"${attrs}><summary>${title}</summary>\n+            return `<details ${attrs}><summary>${title}</summary>\n `;\n-          return `<div class=\"${klass} custom-block\"${attrs}><p class=\"custom-block-title\">${title}</p>\n+          return `<div ${attrs}><p class=\"custom-block-title\">${title}</p>\n `;\n         } else return klass === \"details\" ? `</details>\n ` : `</div>\n@@ -35731,8 +35732,12 @@ async function createMarkdownRenderer(srcDir, options = {}, base = \"/\", logger =\n     { target: \"_blank\", rel: \"noreferrer\", ...options.externalLinks },\n     base\n   ).use(lineNumberPlugin, options.lineNumbers);\n+  const orgi = md.renderer.rules.table_open;\n   md.renderer.rules.table_open = function(tokens, idx, options2, env, self) {\n-    return '<table tabindex=\"0\">\\n';\n+    tokens[idx].attrGet(\"tabindex\") ?? tokens[idx].attrJoin(\"tabindex\", \"0\");\n+    return orgi\n+      ? orgi(tokens, idx, options2, env, self)\n+      : self.renderToken(tokens, idx, options2);\n   };\n   if (options.gfmAlerts !== false) {\n     md.use(gitHubAlertsPlugin);\n"
  },
  {
    "path": "docs/README.md",
    "content": "# easywechat.com\n\n## Contributing\n\nThis site is built with [VitePress](https://github.com/vuejs/vitepress) and depends on [@vue/theme](https://github.com/vuejs/vue-theme). Site content is written in Markdown format located in `src`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.\n\nFor local development, [pnpm](https://pnpm.io/) is preferred as package manager:\n\n```bash\npnpm i\npnpm run dev\n```\n\n## Working on the content\n\n- See VitePress docs on supported [Markdown Extensions](https://vitepress.vuejs.org/guide/markdown.html) and the ability to [use Vue syntax inside markdown](https://vitepress.vuejs.org/guide/using-vue.html).\n\n## Working on the theme\n\nIf changes need to made for the theme, check out the [instructions for developing the theme alongside the docs](https://github.com/vuejs/vue-theme#developing-with-real-content).\n"
  },
  {
    "path": "docs/env.d.ts",
    "content": "/// <reference types=\"vitepress/client\" />\n/// <reference types=\"vue/macros-global\" />\n\ndeclare module '@overtrue/easywechat-theme/config' {\n  import { UserConfig } from 'vitepress'\n  const config: () => Promise<UserConfig>\n  export default config\n}\n\ndeclare module '@overtrue/easywechat-theme/highlight' {\n  const createHighlighter: () => Promise<(input: string) => string>\n  export default createHighlighter\n}\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vitepress dev\",\n    \"build\": \"vitepress build\",\n    \"serve\": \"vitepress serve\",\n    \"purge-caches\": \"./purge-caches\",\n    \"preinstall\": \"npx only-allow pnpm\"\n  },\n  \"dependencies\": {\n    \"autoprefixer\": \"^10.4.21\",\n    \"clipboard\": \"^2.0.11\",\n    \"dynamics.js\": \"^1.1.5\",\n    \"gsap\": \"^3.13.0\",\n    \"tailwindcss\": \"^3.4.18\"\n  },\n  \"devDependencies\": {\n    \"@types/markdown-it\": \"^14.1.2\",\n    \"@types/node\": \"^22.18.12\",\n    \"postcss\": \"^8.5.6\",\n    \"postcss-import\": \"^16.1.1\",\n    \"tenyun\": \"^0.14.0\",\n    \"vitepress\": \"1.6.3\"\n  },\n  \"packageManager\": \"pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee\"\n}\n"
  },
  {
    "path": "docs/postcss.config.js",
    "content": "module.exports = {\n  plugins: [\n    require('postcss-import'),\n    require(\"tailwindcss\")(\"./tailwind.config.js\"),\n    require(\"autoprefixer\"),\n    require('postcss-nested')\n  ]\n}"
  },
  {
    "path": "docs/purge-caches",
    "content": "#!/usr/bin/env node\n\nconst { default: TenYun } = require('tenyun');\n\nconst tc = new TenYun(process.env.COS_SECRET_ID ?? '', process.env.COS_SECRET_KEY ?? '');\n\ntc.cdn.PurgePathCache({\n    Paths: [process.env.domain ?? ''],\n    FlushType: 'flush',\n})\n.then(({ data }) => {\n    console.log(data);\n\n    return tc.cdn.DescribePurgeTasks({\n        TaskId: data.TaskId,\n        PurgeType: 'path',\n    });\n})\n.then(({ data }) => console.log(data));\n"
  },
  {
    "path": "docs/src/3.x/access_token.md",
    "content": "# Access Token\n\n\nSDK 中有一个 [Access Token](https://github.com/overtrue/wechat/blob/master/src/Core/AccessToken.php) 对象，它是一个全局使用的东西，请把它与 OAuth 中的 code 换取的 Access Token 区别开。\n\n我们一个 SDK 应用在初始化以后，你可以在任何时机从应用中拿到该配置下的 Access Token 实例：\n\n```php\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    //...\n];\n\n$app = new Application($options);\n\n// 获取 access token 实例\n$accessToken = $app->access_token; // EasyWeChat\\Core\\AccessToken 实例\n$token = $accessToken->getToken(); // token 字符串\n$token = $accessToken->getToken(true); // 强制重新从微信服务器获取 token.\n```\n\n## 修改 `$app` 的 Access Token\n\n```php\n$app['access_token']->setToken($newAccessToken, $expires);\n```\n\n例如：\n\n```php\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675');\n// 或者指定过期时间\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675', 3600);  // 单位：秒\n```\n\n## 设置 AccessToken 的缓存\n\n你也可以自定义 token 的缓存方式，把一个实现了 `Doctrine\\Common\\Cache\\Cache` 缓存接口的实例作为 AccessToken 构造函数的第三个参数传入即可：\n\n本项目使用 [doctrine/cache](https://github.com/doctrine/cache) 来完成缓存工作，它支持几乎目前所有的缓存引擎。\n\n以 Redis 为例：\n\n```php\n\nuse Doctrine\\Common\\Cache\\RedisCache; // RedisCache 实例了 `Doctrine\\Common\\Cache\\Cache` 接口\n\n$cache = new RedisCache();\n\n// 创建 redis 实例\n$redis = new Redis();\n$redis->connect('redis_host', 6379);\n\n$cache->setRedis($redis);\n\n$app->access_token->setCache($cache);\n```\n\n"
  },
  {
    "path": "docs/src/3.x/accounts.md",
    "content": "# 账号接入\n\n\n如果你想使用本项目接入多个公众号，在本程序中，您可以为每个帐号都设置一个id，此id对应了该帐号的appid、token等信息。\n如下表\n\n| id | appId | secret | 其它... |\n| --- | --- | --- | --- |\n| 1 | `wx3cf0f39249eb0e60` | `f28f735d4f1c242f4687abb469072a29` | ... |\n| 2 | `wx49eb0e63cf0f39s2` | `8f735d4687abb469f1c2422a29f4f207` | ... |\n| N | `wx5cfeb0e60f392490` | `35f8f27d46f1c242f487a9072a29bb46` | ... |\n\n在微信公众平台的设置中，您可以将您帐号中平台的 `url` 设置为 `您的网址/?id=xxx`，如:\n\n```\nhttp://www.example.com/wechat?id=1\n```\n\n而在程序入口处，根据 `id` 查找对应帐号的 `appid` 和 其它信息， 传入 'Overtrue\\Wechat\\Server'，完成初始化。\n"
  },
  {
    "path": "docs/src/3.x/anaylsis.md",
    "content": "# 数据统计与分析\n\n\n通过数据接口，开发者可以获取与公众平台官网统计模块类似但更灵活的数据，还可根据需要进行高级处理。\n\n> 1. 接口侧的公众号数据的数据库中仅存储了 **2014年12月1日之后**的数据，将查询不到在此之前的日期，即使有查到，也是不可信的脏数据；\n> 2. 请开发者在调用接口获取数据后，将数据保存在自身数据库中，即加快下次用户的访问速度，也降低了微信侧接口调用的不必要损耗。\n> 3. 额外注意，获取图文群发每日数据接口的结果中，只有**中间页阅读人数+原文页阅读人数+分享转发人数+分享转发次数+收藏次数 >=3** 的结果才会得到统计，过小的阅读量的图文消息无法统计。\n\n### 获取实例\n\n```php\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n//...\n\n$app = new Application($options);\n$stats = $app->stats;\n```\n\n## API\n\n    $from   example: `2014-02-13` 获取数据的起始日期\n    $to     example: `2014-02-18` 获取数据的结束日期，`$to`允许设置的最大值为昨日\n\n    `$from` 和 `$to` 的差值需小于 “最大时间跨度”（比如最大时间跨度为 1 时，`$from` 和 `$to` 的差值只能为 0，才能小于 1 ），否则会报错\n\n+ `array userSummary($from, $to)` 获取用户增减数据, 最大时间跨度：**7**;\n+ `array userCumulate($from, $to)` 获取累计用户数据, 最大时间跨度：**7**;\n+ `array articleSummary($from, $to)` 获取图文群发每日数据, 最大时间跨度：**1**;\n+ `array articleTotal($from, $to)` 获取图文群发总数据, 最大时间跨度：**1**;\n+ `array userReadSummary($from, $to)` 获取图文统计数据, 最大时间跨度：**3**;\n+ `array userReadHourly($from, $to)` 获取图文统计分时数据, 最大时间跨度：**1**;\n+ `array userShareSummary($from, $to)` 获取图文分享转发数据, 最大时间跨度：**7**;\n+ `array userShareHourly($from, $to)` 获取图文分享转发分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageSummary($from, $to)` 获取消息发送概况数据, 最大时间跨度：**7**;\n+ `array upstreamMessageHourly($from, $to)` 获取消息发送分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageWeekly($from, $to)` 获取消息发送周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageMonthly($from, $to)` 获取消息发送月数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistSummary($from, $to)` 获取消息发送分布数据, 最大时间跨度：**15**;\n+ `array upstreamMessageDistWeekly($from, $to)` 获取消息发送分布周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistMonthly($from, $to)` 获取消息发送分布月数据, 最大时间跨度：**30**;\n+ `array interfaceSummary($from, $to)` 获取接口分析数据, 最大时间跨度：**30**;\n+ `array interfaceSummaryHourly($from, $to)` 获取接口分析分时数据, 最大时间跨度：**1**;\n\nexample:\n\n```php\n$userSummary = $stats->userSummary('2014-12-07', '2014-12-08');\n\nvar_dump($userSummary);\n//\n//[\n//    {\n//        \"ref_date\": \"2014-12-07\",\n//        \"user_source\": 0,\n//        \"new_user\": 0,\n//        \"cancel_user\": 0\n//    }\n//    //后续还有ref_date在begin_date和end_date之间的数据\n// ]\n\n```\n\n更多详细内容与协议说明，请查看微信官方文档：http://mp.weixin.qq.com/wiki/ **数据统计** 章节\n"
  },
  {
    "path": "docs/src/3.x/broadcast.md",
    "content": "# 群发\n\n\n微信的群发消息接口有各种乱七八糟的注意事项及限制，具体请阅读微信官方文档：http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$broadcast = $app->broadcast;\n\n```\n\n## API\n\n> 注意：\n\n    下面提到的 `$messageType` 、`$message` 可以是：\n\n    - `$messageType = Broadcast::MSG_TYPE_NEWS;` 图文消息类型，所对应的 `$message` 为 media_id\n    - `$messageType = Broadcast::MSG_TYPE_TEXT;` 文本消息类型，所对应的 `$message` 为一个文本字符串\n    - `$messageType = Broadcast::MSG_TYPE_VOICE;` 语音消息类型，所对应的 `$message` 为 media_id\n    - `$messageType = Broadcast::MSG_TYPE_IMAGE;` 图片消息类型，所对应的 `$message` 为 media_id\n    - `$messageType = Broadcast::MSG_TYPE_CARD;` 卡券消息类型，所对应的 `$message` 为 card_id\n    - `$messageType = Broadcast::MSG_TYPE_VIDEO;` 视频消息为两种情况：\n        - 视频消息类型，群发视频消息给**组或预览群发视频消息**给用户时所对应的 `$message` 为`media_id`\n        - 群发视频消息**给指定用户**时所对应的 `$message` 为一个数组 `['MEDIA_ID', 'TITLE', 'DESCRIPTION']`\n\n\n### 群发消息给所有粉丝\n\n```php\n$broadcast->send($messageType, $message);\n\n// 别名方式\n$broadcast->sendText(\"大家好！欢迎使用 EasyWeChat。\");\n$broadcast->sendNews($mediaId);\n$broadcast->sendVoice($mediaId);\n$broadcast->sendImage($mediaId);\n//视频：\n// - 群发给组用户，或者预览群发视频时 $message 为 media_id\n// - 群发给指定用户时为数组：[$media_Id, $title, $description]\n$broadcast->sendVideo($message);\n$broadcast->sendCard($cardId);\n```\n\n### 群发消息给指定组\n\n```php\n$broadcast->send($messageType, $message, $groupId);\n\n// 别名方式\n$broadcast->sendText($text, $groupId);\n$broadcast->sendNews($mediaId, $groupId);\n$broadcast->sendVoice($mediaId, $groupId);\n$broadcast->sendImage($mediaId, $groupId);\n$broadcast->sendVideo($message, $groupId);\n$broadcast->sendCard($cardId, $groupId);\n```\n\n### 群发消息给指定用户\n\n至少两个用户的openid，必须是数组。\n\n```php\n$broadcast->send($messageType, $message, [$openId1, $openId2]);\n\n// 别名方式\n$broadcast->sendText($text, [$openId1, $openId2]);\n$broadcast->sendNews($mediaId, [$openId1, $openId2]);\n$broadcast->sendVoice($mediaId, [$openId1, $openId2]);\n$broadcast->sendImage($mediaId, [$openId1, $openId2]);\n$broadcast->sendVideo($message, [$openId1, $openId2]);\n$broadcast->sendCard($cardId, [$openId1, $openId2]);\n```\n\n### 发送预览群发消息给指定的 `openId` 用户\n\n```php\n$broadcast->preview($messageType, $message, $openId);\n\n// 别名方式\n$broadcast->previewText($text, $openId);\n$broadcast->previewNews($mediaId, $openId);\n$broadcast->previewVoice($mediaId, $openId);\n$broadcast->previewImage($mediaId, $openId);\n$broadcast->previewVideo($message, $openId);\n$broadcast->previewCard($cardId, $openId);\n```\n\n### 发送预览群发消息给指定的微信号用户\n\n```php\n$broadcast->previewByName($messageType, $message, $wxname);\n\n// 别名方式\n$broadcast->previewTextByName($text, $wxname);\n$broadcast->previewNewsByName($mediaId, $wxname);\n$broadcast->previewVoiceByName($mediaId, $wxname);\n$broadcast->previewImageByName($mediaId, $wxname);\n$broadcast->previewVideoByName($message, $wxname);\n$broadcast->previewCardByName($cardId, $wxname);\n```\n\n### 删除群发消息\n\n```php\n$broadcast->delete($msgId);\n```\n\n### 查询群发消息发送状态\n\n```php\n$broadcast->status($msgId);\n```\n\n有关群发信息的更多细节请参考微信官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/3.x/cache.md",
    "content": "# 缓存\n\n\n本项目使用 [doctrine/cache](https://github.com/doctrine/cache) 来完成缓存工作，它支持基本目前所有的缓存引擎。\n\n在我们的 SDK 中的所有缓存默认使用文件缓存，缓存路径取决于 PHP 的临时目录，如果你需要自定义缓存，那么你需要做如下的事情：\n\n你可以参考[doctrine/cache官方文档](http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/caching.html)来替换掉应用中默认的缓存配置：\n\n> 以 redis 为例\n> 请先安装 redis 拓展：https://github.com/phpredis/phpredis\n\n```php\n\nuse Doctrine\\Common\\Cache\\RedisCache;\n\n$cacheDriver = new RedisCache();\n\n// 创建 redis 实例\n$redis = new Redis();\n$redis->connect('redis_host', 6379);\n\n$cacheDriver->setRedis($redis);\n\n$options = [\n    'debug'  => false,\n    'app_id' => $wechatInfo['app_id'],\n    'secret' => $wechatInfo['app_secret'],\n    'token'  => $wechatInfo['token'],\n    'aes_key' => $wechatInfo['aes_key'], // 可选\n    'cache'   => $cacheDriver,\n];\n\n$wechatApp = new Application($options);\n```\n\n### Laravel 中使用\n\n在 Laravel 中框架使用 [predis/predis](https://github.com/nrk/predis)，那么我们就得使用 `Doctrine\\Common\\Cache\\PredisCache`：\n\n```php\n\nuse Doctrine\\Common\\Cache\\PredisCache;\n\n$predis = app('redis')->connection();// connection($name), $name 默认为 `default`\n$cacheDriver = new PredisCache($predis);\n\n$app->cache = $cacheDriver;\n```\n\n> 上面提到的 `app('redis')->connection($name)`, 这里的 `$name` 是 laravel 项目中配置文件 `database.php` 中 `redis` 配置名 `default`：https://github.com/laravel/laravel/blob/master/config/database.php#L118\n> 如果你使用的其它连接，对应传名称就好了。\n> 如果你在使用Laravel 5.4，应将`$predis = app('redis')->connection();`修改为：`$predis = app('redis')->connection()->client();`\n\n## 使用自定义的缓存方式\n\n如果你发现 doctrine 提供的几十种缓存方式都满足不了你的需求的话，那么你可以自己建立一个类来完成缓存操作，前提这个类得实现接口：[Doctrine\\Common\\Cache\\Cache](https://github.com/doctrine/cache/blob/master/lib/Doctrine/Common/Cache/Cache.php)\n\n该接口有以下方法需要实现：\n\n```php\n   public function fetch($id);    // 读取缓存\n   public function contains($id);  // 检查是否存在缓存\n   public function save($id, $data, $lifeTime = 0);   // 设置缓存\n   public function delete($id);  // 删除缓存\n   public function getStats(); // 获取状态\n```\n\n下面为一个示例：\n\n```php\n<?php\n\nuse Doctrine\\Common\\Cache\\Cache as CacheInterface;\n\nclass MyCacheDriver implements CacheInterface\n{\n    public function fetch($id)\n    {\n        // 你自己从你想实现的存储方式读取并返回\n    }\n\n    public function contains($id)\n    {\n        // 同理 返回存在与否 bool 值\n    }\n\n    public function save($id, $data, $lifeTime = 0)\n    {\n        // 用你的方式存储该缓存内容即可\n    }\n\n    public function delete($id)\n    {\n        // 删除并返回 bool 值\n    }\n\n    public function getStats()\n    {\n        // 这个你可以不用实现，返回 null 即可\n    }\n}\n```\n\n然后实例化你的缓存类并在 EasyWeChat 里使用它：\n\n```php\n$myCacheDriver = new MyCacheDriver();\n\n$config = [\n    //...\n    'cache'   => $myCacheDriver,\n];\n\n$wechatApp = new Application($options);\n```\n\nOK，这样就完成了自定义缓存的操作。\n"
  },
  {
    "path": "docs/src/3.x/card.md",
    "content": "# 卡券\n-\n\n> Version `>=3.1.2`\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$card = $app->card;\n```\n\n\n## API列表\n\n### 获取卡券颜色\n\n```php\n$card->getColors();\n```\n\nexample:\n\n```php\n$result = $card->getColors();\n```\n\n\n\n### 创建卡券\n\n创建卡券接口是微信卡券的基础接口，用于创建一类新的卡券，获取card_id，创建成功并通过审核后，商家可以通过文档提供的其他接口将卡券下发给用户，每次成功领取，库存数量相应扣除。\n\n```php\n$card->create($cardType, $baseInfo, $especial);\n```\n\n- `cardType` string - 是要添加卡券的类型\n- `baseInfo` array  - 为卡券的基本数据\n- `especial` array  - 是扩展字段\n\nexample:\n\n```php\n<?php\n\n\t$cardType = 'GROUPON';\n\n    $baseInfo = [\n        'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/2aJY6aCPatSeibYAyy7yct9zJXL9WsNVL4JdkTbBr184gNWS6nibcA75Hia9CqxicsqjYiaw2xuxYZiaibkmORS2oovdg/0',\n        'brand_name' => '测试商户造梦空间',\n        'code_type' => 'CODE_TYPE_QRCODE',\n        'title' => '测试',\n        'sub_title' => '测试副标题',\n        'color' => 'Color010',\n        'notice' => '测试使用时请出示此券',\n        'service_phone' => '15311931577',\n        'description' => \"测试不可与其他优惠同享\\n如需团购券发票，请在消费时向商户提出\\n店内均可使用，仅限堂食\",\n\n        'date_info' => [\n          'type' => 'DATE_TYPE_FIX_TERM',\n          'fixed_term' => 90, //表示自领取后多少天内有效，不支持填写0\n          'fixed_begin_term' => 0, //表示自领取后多少天开始生效，领取后当天生效填写0。\n        ],\n\n        'sku' => [\n          'quantity' => '0', //自定义code时设置库存为0\n        ],\n\n        'location_id_list' => ['461907340'],  //获取门店位置poi_id，具备线下门店的商户为必填\n\n        'get_limit' => 1,\n        'use_custom_code' => true, //自定义code时必须为true\n        'get_custom_code_mode' => 'GET_CUSTOM_CODE_MODE_DEPOSIT',  //自定义code时设置\n        'bind_openid' => false,\n        'can_share' => true,\n        'can_give_friend' => false,\n        'center_title' => '顶部居中按钮',\n        'center_sub_title' => '按钮下方的wording',\n        'center_url' => 'http://www.qq.com',\n        'custom_url_name' => '立即使用',\n        'custom_url' => 'http://www.qq.com',\n        'custom_url_sub_title' => '6个汉字tips',\n        'promotion_url_name' => '更多优惠',\n        'promotion_url' => 'http://www.qq.com',\n        'source' => '造梦空间',\n      ];\n\n    $especial = [\n      'deal_detail' => 'deal_detail',\n    ];\n\n    $result = $card->create($cardType, $baseInfo, $especial);\n```\n\n\n\n### 创建二维码\n\n开发者可调用该接口生成一张卡券二维码供用户扫码后添加卡券到卡包。\n\n自定义Code码的卡券调用接口时，POST数据中需指定code，非自定义code不需指定，指定openid同理。指定后的二维码只能被用户扫描领取一次。\n\n```php\n$card->QRCode($cards);\n```\n\n- `cards` array - 卡券相关信息\n\nexample:\n\n```php\n//领取单张卡券\n$cards = [\n    'action_name' => 'QR_CARD',\n    'expire_seconds' => 1800,\n    'action_info' => [\n      'card' => [\n        'card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY',\n        'is_unique_code' => false,\n        'outer_id' => 1,\n      ],\n    ],\n  ];\n\n$result = $card->QRCode($cards);\n```\n\n```php\n//领取多张卡券\n$cards = [\n    'action_name' => 'QR_MULTIPLE_CARD',\n    'action_info' => [\n      'multiple_card' => [\n        'card_list' => [\n          ['card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY'],\n        ],\n      ],\n    ],\n  ];\n\n$result = $card->QRCode($cardList);\n```\n\n请求成功返回值示例：\n\n```php\narray(4) {\n  [\"ticket\"]=>\n  string(96) \"gQHa7joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xLzdrUFlQMHJsV3Zvanc5a2NzV1N5AAIEJUVyVwMEAKd2AA==\"\n  [\"expire_seconds\"]=>\n  int(7776000)\n  [\"url\"]=>\n  string(43) \"http://weixin.qq.com/q/7kPYP0rlWvojw9kcsWSy\"\n  [\"show_qrcode_url\"]=>\n  string(151) \"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQHa7joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xLzdrUFlQMHJsV3Zvanc5a2NzV1N5AAIEJUVyVwMEAKd2AA%3D%3D\"\n}\n```\n\n成功返回值列表说明：\n\n|       参数名       | 描述                                       |\n| :-------------: | :--------------------------------------- |\n|     ticket      | 获取的二维码ticket，凭借此ticket调用[通过ticket换取二维码接口](http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1443433542&token=&lang=zh_CN)可以在有效时间内换取二维码。 |\n| expire_seconds  | 二维码的有效时间                                 |\n|       url       | 二维码图片解析后的地址，开发者可根据该地址自行生成需要的二维码图片        |\n| show_qrcode_url | 二维码显示地址，点击后跳转二维码页面                       |\n\n\n\n### ticket 换取二维码图片\n\n获取二维码 ticket 后，开发者可用 ticket 换取二维码图片。\n\n```php\n$card->showQRCode($ticket);\n```\n\n- `ticket` string  - 获取的二维码 ticket，凭借此 ticket 可以在有效时间内换取二维码。\n\nexample:\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$result = $card->showQRCode($ticket);\n```\n\n\n### ticket 换取二维码链接\n\n```php\n$card->getQRCodeUrl($ticket);  //获取的二维码ticket\n```\n\nexample:\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$card->getQRCodeUrl($ticket);\n```\n\n### JSAPI 卡券批量下发到用户\n\n微信卡券：JSAPI 卡券\n\n```php\n$cards = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'outer_id' => 2],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'outer_id' => 3],\n];\n$json = $card->jsConfigForAssign($cards); // 返回 json 格式\n```\n\n返回 json，在模板里的用法：\n\n```html\nwx.addCard({\n    cardList: <?= $json ?>, // 需要打开的卡券列表\n    success: function (res) {\n        var cardList = res.cardList; // 添加的卡券列表信息\n    }\n});\n```\n\n### 创建货架接口\n\n开发者需调用该接口创建货架链接，用于卡券投放。创建货架时需填写投放路径的场景字段。\n\n```php\n$card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cards);\n```\n\n- `banner` string -页面的 banner 图;\n- `pageTitle` string - 页面的 title\n- `canShare` bool - 页面是不是可以分享，true 或 false\n- `scene`  string - 投放页面的场景值，具体值请参考下面的 example\n- `cards`  array - 卡券列表，每个元素有两个字段\n\nexample:\n\n```php\n$banner     = 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFN';\n$pageTitle = '惠城优惠大派送';\n$canShare  = true;\n\n//SCENE_NEAR_BY          附近\n//SCENE_MENU             自定义菜单\n//SCENE_QRCODE             二维码\n//SCENE_ARTICLE             公众号文章\n//SCENE_H5                 h5页面\n//SCENE_IVR                 自动回复\n//SCENE_CARD_CUSTOM_CELL 卡券自定义cell\n$scene = 'SCENE_NEAR_BY';\n\n$cardList = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/test.png'],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/aa.jpg'],\n];\n\n$result = $card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cardList);\n```\n\n\n\n### 导入code接口\n\n在自定义code卡券成功创建并且通过审核后，必须将自定义code按照与发券方的约定数量调用导入code接口导入微信后台。\n\n```php\n$card->deposit($card_id, $code);\n```\n\n- `cardId` string - 要导入code的卡券ID\n- `code` string - 要导入微信卡券后台的自定义 code，最多100个\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n$code    = ['11111', '22222', '33333'];\n\n$result = $card->deposit($cardId, $code);\n```\n\n\n\n### 查询导入code数目\n\n```php\n$card->getDepositedCount($cardId);  //要导入code的卡券ID\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->getDepositedCount($cardId);\n```\n\n\n\n### 核查code接口\n\n为了避免出现导入差错，强烈建议开发者在查询完code数目的时候核查code接口校验code导入微信后台的情况。\n\n```php\n$card->checkCode($cardId, $code);\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$code = ['807732265476', '22222', '33333'];\n\n$result = $card->checkCode($cardId, $code);\n```\n\n\n\n### 图文消息群发卡券\n\n特别注意：目前该接口仅支持填入非自定义code的卡券,自定义code的卡券需先进行code导入后调用。\n\n```php\n$card->getHtml($cardId);\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->getHtml($cardId);\n```\n\n\n\n### 设置测试白名单\n\n同时支持“openid”、“username”两种字段设置白名单，总数上限为10个。\n\n```php\n$card->setTestWhitelist($openids); // 使用 openid\n$card->setTestWhitelistByUsername($usernames); // 使用 username\n```\n\n- `openids` array - 测试的openid列表\n- `usernames` array  - 测试的微信号列表\n\nexample:\n\n```php\n// by openid\n$openids   = [$openId, $openId2, $openid3...];\n$result = $card->setTestWhitelist($openids);\n\n// by username\n$usernames = ['tianye0327', 'iovertrue'];\n$result = $card->setTestWhitelistByUsername($usernames);\n```\n\n### 查询Code接口\n\n```php\n$card->getCode($code, $checkConsume, $cardId);\n```\n\n- checkConsume  是否校验code核销状态，true和false\n\nexample:\n\n```php\n$code          = '736052543512';\n$checkConsume = true;\n$cardId       = 'pdkJ9uDgnm0pKfrTb1yV0dFMO_Gk';\n\n$result = $card->getCode($code, $checkConsume, $cardId);\n```\n\n\n\n### 核销Code接口\n\n```php\n$card->consume($code);\n\n// 或者指定 cardId\n\n$card->consume($code, $cardId);\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uDmhkLj6l5bm3cq9iteQBck';\n$code    = '789248558333';\n\n$result = $card->consume($code);\n\n//或\n\n$result = $card->consume($code, $cardId);\n```\n\n\n\n### Code解码接口\n\n```php\n$card->decryptCode($encryptedCode);\n```\n\nexample:\n\n```php\n$encryptedCode = 'XXIzTtMqCxwOaawoE91+VJdsFmv7b8g0VZIZkqf4GWA60Fzpc8ksZ/5ZZ0DVkXdE';\n\n$result = $card->decryptCode($encryptedCode);\n```\n\n\n\n### 获取用户已领取卡券接口\n\n用于获取用户卡包里的，属于该appid下所有**可用卡券，包括正常状态和未生效状态**。\n\n```php\n$card->getUserCards($openid, $cardId);\n```\n\nexample:\n\n```php\n$openid  = 'odkJ9uDUz26RY-7DN1mxkznfo9xU';\n$cardId = ''; //卡券ID。不填写时默认查询当前appid下的卡券。\n\n$result = $card->getUserCards($openid, $cardId);\n```\n\n\n\n### 查看卡券详情\n\n开发者可以调用该接口查询某个card_id的创建信息、审核状态以及库存数量。\n\n```php\n$card->getCard($cardId);\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY';\n\n$result = $card->getCard($cardId);\n```\n\n\n\n### 批量查询卡列表\n\n```php\n$card->lists($offset, $count, $statusList);\n```\n\n- `offset` int - 查询卡列表的起始偏移量，从0开始\n- `count` int - 需要查询的卡片的数量\n- `statusList` -  支持开发者拉出指定状态的卡券列表，详见example\n\nexample:\n\n```php\n$offset      = 0;\n$count       = 10;\n\n//CARD_STATUS_NOT_VERIFY,待审核；\n//CARD_STATUS_VERIFY_FAIL,审核失败；\n//CARD_STATUS_VERIFY_OK，通过审核；\n//CARD_STATUS_USER_DELETE，卡券被商户删除；\n//CARD_STATUS_DISPATCH，在公众平台投放过的卡券；\n$statusList = 'CARD_STATUS_VERIFY_OK';\n\n$result = $card->lists($offset, $count, $statusList);\n```\n\n\n\n### 更改卡券信息接口\n\n支持更新所有卡券类型的部分通用字段及特殊卡券中特定字段的信息。\n\n```php\n$card->update($cardId, $type, $baseInfo);\n```\n\n- `type` string - 卡券类型\n\nexample:\n\n```php\n$cardId = 'pdkJ9uCzKWebwgNjxosee0ZuO3Os';\n\n$type = 'groupon';\n\n$baseInfo = [\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/2aJY6aCPatSeibYAyy7yct9zJXL9WsNVL4JdkTbBr184gNWS6nibcA75Hia9CqxicsqjYiaw2xuxYZiaibkmORS2oovdg/0',\n    'center_title' => '顶部居中按钮',\n    'center_sub_title' => '按钮下方的wording',\n    'center_url' => 'http://www.baidu.com',\n    'custom_url_name' => '立即使用',\n    'custom_url' => 'http://www.qq.com',\n    'custom_url_sub_title' => '6个汉字tips',\n    'promotion_url_name' => '更多优惠',\n    'promotion_url' => 'http://www.qq.com',\n];\n\n$result = $card->update($cardId, $type, $baseInfo);\n```\n\n\n\n### 设置微信买单接口\n\n```php\n$card->setPayCell($cardId, $isOpen);\n```\n\n- `isOpen` string - 是否开启买单功能，填 true/false，不填默认 true\n\nexample:\n\n```php\n$cardId = 'pdkJ9uH7u11R-Tu1kilbaW_zDFow';\n$isOpen = true;\n\n$result = $card->setPayCell($cardId, $isOpen);\n```\n\n\n\n### 修改库存接口\n\n```php\n$card->increaseStock($cardId, $amount); // 增加库存\n$card->reductStock($cardId, $amount); // 减少库存\n```\n\n- `cardId` string - 卡券 ID\n- `amount` int - 修改多少库存\n\nexample:\n\n```php\n$cardId = 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY';\n\n$result = $card->increaseStock($cardId, 100);\n```\n\n\n### 更改Code接口\n\n```php\n$card->updateCode($code, $newCode, $cardId);\n```\n\n- `newCode` string - 变更后的有效Code码\n\nexample:\n\n```php\n$code     = '148246271394';\n$newCode = '659266965266';\n$cardId  = '';\n\n$result = $card->updateCode($code, $newCode, $cardId);\n```\n\n\n\n### 删除卡券接口\n\n```php\n$card->delete($cardId);\n```\n\nexample:\n\n```php\n$cardId = 'pdkJ9uItT7iUpBp4GjZp8Cae0Vig';\n\n$result = $card->delete($cardId);\n```\n\n\n\n### 设置卡券失效\n\n```php\n$card->disable($code, $cardId);\n```\n\nexample:\n\n```php\n$code    = '736052543512';\n$cardId = '';\n\n$result = $card->disable($code, $cardId);\n```\n\n\n\n### 会员卡接口激活\n\n```php\n$result = $card->activate($info);\n```\n\n- `info` - 需要激活的会员卡信息\n\nexample:\n\n```php\n$activate = [\n      'membership_number'        => '357898858', //会员卡编号，由开发者填入，作为序列号显示在用户的卡包里。可与Code码保持等值。\n      'code'                     => '916679873278', //创建会员卡时获取的初始code。\n      'activate_begin_time'      => '1397577600', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式\n      'activate_end_time'        => '1422724261', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。\n      'init_bonus'               => '持白金会员卡到店消费，可享8折优惠。', //初始积分，不填为0。\n      'init_balance'             => '持白金会员卡到店消费，可享8折优惠。', //初始余额，不填为0。\n      'init_custom_field_value1' => '白银', //创建时字段custom_field1定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value2' => '9折', //创建时字段custom_field2定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value3' => '200', //创建时字段custom_field3定义类型的初始值，限制为4个汉字，12字节。\n];\n\n$result = $card->activate($activate);\n```\n\n\n\n### 设置开卡字段接口\n\n```php\n$card->activateUserForm($cardId, $requiredForm, $optionalForm);\n```\n\n- `requiredForm` array - 会员卡激活时的必填选项\n- `optionalForm` array - 会员卡激活时的选填项\n\nexample:\n\n```php\n$cardId = 'pdkJ9uJYAyfLXsUCwI2LdH2Pn1AU';\n\n$requiredForm = [\n    'required_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_MOBILE',\n            'USER_FORM_INFO_FLAG_LOCATION',\n            'USER_FORM_INFO_FLAG_BIRTHDAY',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n];\n\n$optionalForm = [\n    'optional_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_EMAIL',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n];\n\n$result = $card->activateUserForm($cardId, $requiredForm, $optionalForm);\n```\n\n\n\n### 拉取会员信息接口\n\n```php\n$card->getMemberCardUser($cardId, $code);\n```\n\nexample:\n\n```php\n$cardId = 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8';\n$code    = '916679873278';\n\n$result = $card->getMemberCardUser($cardId, $code);\n```\n\n\n\n### 更新会员信息\n\n```php\n$card->updateMemberCardUser($updateUser);\n```\n\n- `updateUser` array - 可以更新的会员信息\n\nexample:\n\n```php\n$updateUser = [\n    'code'                => '916679873278', //卡券Code码。\n    'card_id'             => 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8', //卡券ID。\n    'record_bonus'        => '消费30元，获得3积分', //商家自定义积分消耗记录，不超过14个汉字。\n    'bonus'               => '100', //需要设置的积分全量值，传入的数值会直接显示，如果同时传入add_bonus和bonus,则前者无效。\n    'balance'             => '持白金会员卡到店消费，可享8折优惠。', //需要设置的余额全量值，传入的数值会直接显示，如果同时传入add_balance和balance,则前者无效。\n    'record_balance'      => '持白金会员卡到店消费，可享8折优惠。', //商家自定义金额消耗记录，不超过14个汉字。\n    'custom_field_value1' => '100', //创建时字段custom_field1定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value2' => '200', //创建时字段custom_field2定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value3' => '300', //创建时字段custom_field3定义类型的最新数值，限制为4个汉字，12字节。\n];\n\n$result = $card->updateMemberCardUser($updateUser);\n```\n\n\n\n### 添加子商户\n\n```php\n$card->craeteSubMerchant($brandName, $logoUrl, $protocol, $endTime, $primaryCategoryId, $secondaryCategoryId, $agreementMediaId, $operatorMediaId, $appId); \n```\n\n- `brand_name` string - 子商户名称（12个汉字内），该名称将在制券时填入并显示在卡券页面上\n- `logo_url`  string - 子商户 logo，可通过上传 logo 接口获取。该 logo 将在制券时填入并显示在卡券页面上\n- `protocol`  string - 授权函ID，即通过上传临时素材接口上传授权函后获得的 meida_id\n- `primary_category_id`  int - 一级类目id,可以通过本文档中接口查询\n- `secondary_category_id` int - 二级类目id，可以通过本文档中接口查询\n- `agreement_media_id`  string - 营业执照或个体工商户营业执照彩照或扫描件\n- `operator_media_id`  string - 营业执照内登记的经营者身份证彩照或扫描件\n- `app_id`  string - 子商户的公众号 app_id，配置后子商户卡券券面上的 app_id 为该 app_id, app_id 须经过认证\n\nexample:\n\n```php\n$info = [\n    'brand_name' => 'overtrue',\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0',\n    'protocol' => 'qIqwTfzAdJ_1-VJFT0fIV53DSY4sZY2WyhkzZzbV498Qgdp-K5HJtZihbHLS0Ys0',\n    'end_time' => '1438990559',\n    'primary_category_id' => 1,\n    'secondary_category_id' => 101,\n    'agreement_media_id' => '',\n    'operator_media_id' => '',\n    'app_id' => '',\n];\n\n$result = $card->createSubMerchant($info);\n```\n\n### 更新子商户\n\n```php\n$card->updateSubMerchant($merchantId, $info);\n```\n\n- `$merchantId` int - 子商户 ID\n- `$info` array - 参数与创建子商户参数一样\n\nexample:\n\n```php\n$info = [\n  //...\n];\n$result = $card->updateSubMerchant('12', $info);\n```\n\n### 卡券开放类目查询接口\n\n```php\n$card->getCategories();\n```\n\nexample:\n\n```php\n$result = $card->getCategories();\n```\n\n关于卡券接口的使用请参阅官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/3.x/configuration.md",
    "content": "# 配置\n\n\n在前面我们已经讲过，初始化 SDK 的时候方法就是创建一个 `EasyWeChat\\Foundation\\Application` 实例：\n\n```php\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n   // ...\n];\n\n$app = new Application($options);\n\n/**\n* 如果想要在Application实例化完成之后, 修改某一个options的值,\n* 比如服务商+子商户支付回调场景, 所有子商户订单支付信息都是通过同一个服务商的$option 配置进来的,\n* 当oauth在微信端验证完成之后, 可以通过动态设置merchant_id来区分具体是哪个子商户\n*/\n$app['config']->set('oauth.callback','wechat/oauthcallback/'. $sub_merchant_id->id);\n```\n\n那么配置的具体选项有哪些，下面是一个完整的列表：\n\n```php\n<?php\n\nreturn [\n    /**\n     * Debug 模式，bool 值：true/false\n     *\n     * 当值为 false 时，所有的日志都不会记录\n     */\n    'debug'  => true,\n\n    /**\n     * 账号基本信息，请从微信公众平台/开放平台获取\n     */\n    'app_id'  => 'your-app-id',         // AppID\n    'secret'  => 'your-app-secret',     // AppSecret\n    'token'   => 'your-token',          // Token\n    'aes_key' => '',                    // EncodingAESKey，安全模式与兼容模式下请一定要填写！！！\n\n    /**\n     * 日志配置\n     *\n     * level: 日志级别, 可选为：\n     *         debug/info/notice/warning/error/critical/alert/emergency\n     * permission：日志文件权限(可选)，默认为null（若为null值,monolog会取0644）\n     * file：日志文件位置(绝对路径!!!)，要求可写权限\n     */\n    'log' => [\n        'level'      => 'debug',\n        'permission' => 0777,\n        'file'       => '/tmp/easywechat.log',\n    ],\n\n    /**\n     * OAuth 配置\n     *\n     * scopes：公众平台（snsapi_userinfo / snsapi_base），开放平台：snsapi_login\n     * callback：OAuth授权完成后的回调页地址\n     */\n    'oauth' => [\n        'scopes'   => ['snsapi_userinfo'],\n        'callback' => '/examples/oauth_callback.php',\n    ],\n\n    /**\n     * 微信支付\n     */\n    'payment' => [\n        'merchant_id'        => 'your-mch-id',\n        'key'                => 'key-for-signature',\n        'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n        'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n        // 'device_info'     => '013467007045764',\n        // 'sub_app_id'      => '',\n        // 'sub_merchant_id' => '',\n        // ...\n    ],\n\n    /**\n     * Guzzle 全局设置\n     *\n     * 更多请参考： http://docs.guzzlephp.org/en/latest/request-options.html\n     */\n    'guzzle' => [\n        'timeout' => 3.0, // 超时时间（秒）\n        //'verify' => false, // 关掉 SSL 认证（强烈不建议！！！）\n    ],\n];\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n\n## 日志文件\n\n配置文件里的`/tmp/...`是绝对路径\n\n如果在 windows 下，去把它改成`C:\\foo\\bar`的形式，\n如果是 Linux ，你已经懂了……\n\n如果需要按日独立存储，可以配置成`'file'  => storage_path('/tmp/easywechat/easywechat_'.date('Ymd').'.log'),`\n\n其它同理……\n\n"
  },
  {
    "path": "docs/src/3.x/contributing.md",
    "content": "# 贡献代码\n\n## 开发\n\n我们欢迎广大开发者贡献大家的智慧，让我们共同让它变得更完美.\n\n### 开始之前\n\n请严格遵循以下代码标准:\n\n- [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md).\n- 使用 4 个空格作为缩进。\n\n### 流程\n\n1. Fork [overtrue/wechat](https://github.com/overtrue/wechat) 到本地.\n2. 创建新的分支：\n\n   ```bash\n   git checkout -b new_feature\n   ```\n\n3. 编写代码。\n4. Push 到你的分支:\n\n   ```bash\n   git push origin new_feature\n   ```\n\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n> 注意：注释请使用英文\n\n## 更新文档\n\n我们的文档也是开源的，源代码在 [w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/master/docs)。\n\n### 流程\n\n1. Fork [w7corp/EasyWeChat](https://github.com/w7corp/EasyWeChat)\n2. Clone 到你的电脑：\n\n   ```bash\n   git clone https://github.com/[你的账号]/EasyWeChat\n   cd docs\n   ```\n\n3. 创建新的分支，编辑文档\n4. Push 到你的分支。\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n## 报告 Bug\n\n当你在使用过程中遇到问题，请查阅 [疑难解答](troubleshooting.html) 或者在这里提问 [GitHub](https://github.com/overtrue/wechat/issues). 如果还是不能解决你的问题，请到 GitHub 联系我们。\n\n[overtrue/wechat]: https://github.com/overtrue/wechat\n"
  },
  {
    "path": "docs/src/3.x/events.md",
    "content": "# 事件\n\n\n> 注意：3.0 起，所有服务端的入口（**消息与事件**）都已经合并为一个方法来处理：`setMessageHandler()`\n\n### 在服务端接收用户端产生的事件\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$server = $app->server;\n\n$server->setMessageHandler(function($message){\n    // 注意，这里的 $message 不仅仅是用户发来的消息，也可能是事件\n    // 当 $message->MsgType 为 event 时为事件\n    if ($message->MsgType == 'event') {\n        # code...\n        switch ($message->Event) {\n            case 'subscribe':\n                # code...\n                break;\n\n            default:\n                # code...\n                break;\n        }\n    }\n});\n\n$response = $server->serve();\n\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n> 注意：`$response` 是一个对象，不要直接 echo.\n\n更多请参考：[服务端](server.html)\n\n关于事件类型请参考微信官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/3.x/index.md",
    "content": "> 👋🏼 您当前浏览的文档为 3.x，其它版本的文档请参考：[6.x](/6.x/)、[5.x](/5.x/)、[4.x](/4.x/)\n\n# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n## 环境要求\n\n- PHP >= 5.5.9\n- [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n- [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n- [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\n# 参与贡献\n\n1. fork 当前库到你的名下\n2. 切换到你想要修改的分支，`zh-cn` 或者 `en`\n3. 在你的本地修改完成审阅过后提交到你的仓库的对应分支\n4. 提交 PR 并描述你的修改，等待合并\n\n# License\n\nMIT\n"
  },
  {
    "path": "docs/src/3.x/installation.md",
    "content": "# 安装\n\n\n## 环境要求\n\n- PHP >= 5.5.9\n- [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n- [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n- [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php) 素材管理模块需要用到\n\n\nLaravel 5 拓展包: [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n## 安装\n\n使用 [composer](http://getcomposer.org/):\n\n```shell\n$ composer require overtrue/wechat:~3.1 -vvv\n```"
  },
  {
    "path": "docs/src/3.x/integration.md",
    "content": "# 在框架中使用\n\nEasyWeChat 是一个通用的 Composer 包，所以不需要对框架单独做修改，只要支持 Composer 就能直接使用，当然了，为了更方便的使用，我们收集了以下框架单独提供的拓展包：\n\n## Laravel\n\n- [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n\n## Symfony\n\n- [lilocon/WechatBundle](https://github.com/lilocon/WechatBundle)\n\n## Yii\n\n- [max-wen/yii2-easy-wechat](https://github.com/max-wen/yii2-easy-wechat)\n\n## CI\n\nTODO\n\n## Phalcon\n\nTODO\n\n... more\n\n"
  },
  {
    "path": "docs/src/3.x/js.md",
    "content": "# JSSDK\n\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n//...\n$app = new Application($options);\n\n$js = $app->js;\n```\n\n## API\n\n- `$js->config(array $APIs, $debug = false, $beta = false, $json = true);` 获取JSSDK的配置数组，默认返回 JSON 字符串，当 `$json` 为 `false` 时返回数组，你可以直接使用到网页中。\n- `$js->setUrl($url)` 设置当前URL，如果不想用默认读取的URL，可以使用此方法手动设置，通常不需要。\n\nexample:\n\n我们可以生成js配置文件：\n\n```js\n<script src=\"http://res.wx.qq.com/open/js/jweixin-1.0.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\n    wx.config(<?php echo $js->config(array('onMenuShareQQ', 'onMenuShareWeibo'), true) ?>);\n</script>\n```\n结果如下：\n\n```js\n<script src=\"http://res.wx.qq.com/open/js/jweixin-1.0.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\nwx.config({\n    debug: true,\n    appId: 'wx3cf0f39249eb0e60',\n    timestamp: 1430009304,\n    nonceStr: 'qey94m021ik',\n    signature: '4F76593A4245644FAE4E1BC940F6422A0C3EC03E',\n    jsApiList: ['onMenuShareQQ', 'onMenuShareWeibo']\n});\n</script>\n```\n\n更多 JSSDK 的使用请参考 [微信官方文档](http://mp.weixin.qq.com/wiki/) 中 **JSSDK章节**"
  },
  {
    "path": "docs/src/3.x/lucky-money.md",
    "content": "# 红包\n\n\n你在阅读本文之前确认你已经仔细阅读了：[微信支付 | 现金红包文档 ](https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_1)。\n\n## 配置\n\n与支付接口一样，红包接口也需要配置如下参数，需要特别注意的是，红包相关的全部接口**都需要使用 SSL 证书**，因此** cert_path 以及 cert_key 必须正确配置**。\n\n```php\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    // payment\n    'payment' => [\n        'merchant_id'        => 'your-mch-id',\n        'key'                => 'key-for-signature',\n        'cert_path'          => 'path/to/your/cert.pem',\n        'key_path'           => 'path/to/your/key',\n        // ...\n    ],\n];\n\n$app = new Application($options);\n\n$luckyMoney = $app->lucky_money;\n```\n\n## 发送红包\n\n微信的现金红包分为**普通红包**和**裂变红包**两类。SDK 中对其分别进行了封装，同时也提供了一个统一的调用方法。\n\n**默认情况下，通过接口发送的红包金额应该在200元以内，但可以通过在调用发送接口时传递场景 ID (scene_id)来发送特定场景的红包，不同场景红包可以由商户自己登录商户平台设置最大金额。scene_id 的可选值及对应含义可参阅微信支付官方文档。**\n\n### 通用发送接口\n\n```php\n<?php\n\n$luckyMoneyData = [\n    'mch_billno'       => 'xy123456',\n    'send_name'        => '测试红包',\n    're_openid'        => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'        => 1,  //普通红包固定为1，裂变红包不小于3\n    'total_amount'     => 100,  //单位为分，普通红包不小于100，裂变红包不小于300\n    'wishing'          => '祝福语',\n    'client_ip'        => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'         => '测试活动',\n    'remark'           => '测试备注',\n    // ...\n];\n\n$result = $luckyMoney->send($luckyMoneyData, \\EasyWeChat\\Payment\\LuckyMoney\\API::TYPE_NORMAL);\n或\n$result = $luckyMoney->send($luckyMoneyData, \\EasyWeChat\\Payment\\LuckyMoney\\API::TYPE_GROUP);\n\n```\n\n> 不同类型红包所传参数有所差别，请参考官方文档中参数列表。\n\n\n### 发送普通红包接口\n\n```php\n<?php\n\n$luckyMoneyData = [\n    'mch_billno'       => 'xy123456',\n    'send_name'        => '测试红包',\n    're_openid'        => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'        => 1,  //固定为1，可不传\n    'total_amount'     => 100,  //单位为分，不小于100\n    'wishing'          => '祝福语',\n    'client_ip'        => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'         => '测试活动',\n    'remark'           => '测试备注',\n    // ...\n];\n\n$result = $luckyMoney->sendNormal($luckyMoneyData);\n\n```\n\n### 发送裂变红包接口\n\n```php\n<?php\n\n$luckyMoneyData = [\n    'mch_billno'       => 'xy123456',\n    'send_name'        => '测试红包',\n    're_openid'        => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'        => 3,  //不小于3\n    'total_amount'     => 300,  //单位为分，不小于300\n    'wishing'          => '祝福语',\n    'act_name'         => '测试活动',\n    'remark'           => '测试备注',\n    'amt_type'         => 'ALL_RAND',  //可不传\n    // ...\n];\n\n$result = $luckyMoney->sendGroup($luckyMoneyData);\n\n```\n\n## 红包预下单接口\n\n红包预下单接口是为摇一摇红包接口配合使用的，在开发摇一摇周边的摇红包相关功能时，需要调用本接口获取红包单号。详情参见[官方文档](http://mp.weixin.qq.com/wiki/7/0ddd50ed2421b99fedd071281c074aab.html#.E7.BA.A2.E5.8C.85.E9.A2.84.E4.B8.8B.E5.8D.95.E6.8E.A5.E5.8F.A3)\n\n\n```php\n<?php\n\n$luckyMoneyData = [\n    'hb_type'          => 'NORMAL',  //NORMAL 或 GROUP\n    'mch_billno'       => 'xy123456',\n    'send_name'        => '测试红包',\n    're_openid'        => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'        => 1,  //普通红包固定为1，裂变红包不小于3\n    'total_amount'     => 100,  //单位为分，普通红包不小于100，裂变红包不小于300\n    'wishing'          => '祝福语',\n    'client_ip'        => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'         => '测试活动',\n    'remark'           => '测试备注',\n    'amt_type'         => 'ALL_RAND',\n    // ...\n];\n\n$result = $luckyMoney->prepare($luckyMoneyData);\n\n```\n\n## 查询红包信息\n\n用于商户对已发放的红包进行查询红包的具体信息以及领取情况 ，普通红包和裂变包均使用这一接口进行查询。\n\n```php\n$mchBillNo = \"商户系统内部的订单号（mch_billno）\";\n$luckyMoney->query($mchBillNo);\n```\n"
  },
  {
    "path": "docs/src/3.x/material.md",
    "content": "# 素材管理\n\n\n在微信里的图片，音乐，视频等等都需要先上传到微信服务器作为素材才可以在消息中使用。\n\n> 请注意：\n\n>     1. 限制：\n>       - 图片（image）: 1M，支持 bmp/png/jpeg/jpg/gif 格式\n>       - 语音（voice）：2M，播放长度不超过 60s，支持 mp3/wma/wav/amr 格式\n>       - 视频（video）：10MB，支持MP4格式\n>       - 缩略图（thumb）：64KB，支持JPG格式\n\n>     2. `media_id` 是可复用的；\n>     3. 素材分为 `临时素材` 与 `永久素材`， 临时素材媒体文件在后台保存时间为3天，即 3 天后 `media_id` 失效；\n>     4. 新增的永久素材也可以在公众平台官网素材管理模块中看到；\n>     5. 永久素材的数量是有上限的，请谨慎新增。图文消息素材和图片素材的上限为5000，其他类型为1000；\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n$app = new Application($options);\n\n// 永久素材\n$material = $app->material;\n// 临时素材\n$temporary = $app->material_temporary;\n```\n\n## 永久素材 API：\n\n### 上传图片:\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$result = $material->uploadImage(\"/path/to/your/image.jpg\");  // 请使用绝对路径写法！除非你正确的理解了相对路径（好多人是没理解对的）！\nvar_dump($result);\n// {\n//    \"media_id\":MEDIA_ID,\n//    \"url\":URL\n// }\n```\n\n> `url` 只有上传图片素材有返回值。\n\n### 上传声音\n\n语音**大小不超过 5M**，**长度不超过 60 秒**，支持 `mp3/wma/wav/amr` 格式。\n\n```php\n$result = $material->uploadVoice(\"/path/to/your/voice.mp3\"); // 请使用绝对路径写法！除非你正确的理解了相对路径（好多人是没理解对的）！\n$mediaId = $result->media_id;\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传视频\n\n```php\n$result = $material->uploadVideo(\"/path/to/your/video.mp4\", \"视频标题\", \"视频描述\"); // 请使用绝对路径写法！除非你正确的理解了相对路径（好多人是没理解对的）！\n$mediaId = $result->media_id;\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$result = $material->uploadThumb(\"/path/to/your/thumb.jpg\"); // 请使用绝对路径写法！除非你正确的理解了相对路径（好多人是没理解对的）！\n$mediaId = $result->media_id;\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传永久图文消息\n\n图文消息没有临时一说。\n\n```php\nuse EasyWeChat\\Message\\Article;\n// 上传单篇图文\n$article = new Article([\n    'title' => 'xxx',\n    'thumb_media_id' => $mediaId,\n    //...\n  ]);\n$material->uploadArticle($article);\n\n// 或者多篇图文\n$material->uploadArticle([$article, $article2, ...]);\n```\n\n### 修改永久图文消息\n\n有三个参数：\n\n- `$mediaId` 要更新的文章的 `mediaId`\n- `$article` 文章内容，`Article` 实例或者 全字段数组\n- `$index` 要更新的文章在图文消息中的位置（多图文消息时，此字段才有意义，单图片忽略此参数），第一篇为 0；\n\n```php\n$result = $material->updateArticle($mediaId, new Article(...));\n$mediaId = $result->media_id;\n\n// or\n\n$result = $material->updateArticle($mediaId, [\n    'title'          => 'xxx',\n    'thumb_media_id' => 'xxx',\n    // ...\n  ]);\n\n// 指定更新多图文中的第 2 篇\n$result = $material->updateArticle($mediaId, new Article(...), 1); // 第 2 篇\n```\n\n\n### 上传永久文章内容图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n返回值中 url 就是上传图片的 URL，可用于后续群发中，放置到图文消息中。\n\n```php\n$result = $material->uploadArticleImage($path);\n$url = $result->url;\n//{\n//    \"url\":  \"http://mmbiz.qpic.cn/mmbiz/gLO17UPS6FS2xsypf378iaNhWacZ1G1UplZYWEYfwvuU6Ont96b1roYsCNFwaRrSaKTPCUdBK9DgEHicsKwWCBRQ/0\"\n//}\n```\n\n### 获取永久素材\n\n```php\n$resource = $material->get($mediaId);\n```\n\n如果请求的素材为图文消息，则响应如下：\n\n```\n{\n \"news_item\": [\n       {\n       \"title\":TITLE,\n       \"thumb_media_id\"::THUMB_MEDIA_ID,\n       \"show_cover_pic\":SHOW_COVER_PIC(0/1),\n       \"author\":AUTHOR,\n       \"digest\":DIGEST,\n       \"content\":CONTENT,\n       \"url\":URL,\n       \"content_source_url\":CONTENT_SOURCE_URL\n       },\n       //多图文消息有多篇文章\n    ]\n  }\n```\n\n如果返回的是视频消息素材，则内容如下：\n\n```\n{\n  \"title\":TITLE,\n  \"description\":DESCRIPTION,\n  \"down_url\":DOWN_URL,\n}\n```\n\n其他类型的素材消息，则响应的直接为素材的内容，开发者可以自行保存为文件。例如\n\n```\n$image = $material->get($mediaId);\nfile_put_contents('/foo/abc.jpg', $image);\n```\n\n### 获取永久素材列表\n\n参考：[微信公众平台开发者文档：获取永久素材列表](http://mp.weixin.qq.com/wiki/12/2108cd7aafff7f388f41f37efa710204.html)\n\n- `$type`   素材的类型，图片（`image`）、视频（`video`）、语音 （`voice`）、图文（`news`）\n- `$offset` 从全部素材的该偏移位置开始返回，可选，默认 `0`，0 表示从第一个素材 返回\n- `$count`  返回素材的数量，可选，默认 `20`, 取值在 1 到 20 之间\n\n```php\n$material->lists($type, $offset, $count);\n```\n\nexample:\n\n```\n$lists = $material->lists('image', 0, 10);\n```\n\n图片、语音、视频 等类型的返回如下\n\n```\n{\n   \"total_count\": TOTAL_COUNT,\n   \"item_count\": ITEM_COUNT,\n   \"item\": [{\n       \"media_id\": MEDIA_ID,\n       \"name\": NAME,\n       \"update_time\": UPDATE_TIME,\n       \"url\":URL\n   },\n   //可能会有多个素材\n   ]\n}\n```\n\n永久图文消息素材列表的响应如下：\n\n```\n{\n   \"total_count\": TOTAL_COUNT,\n   \"item_count\": ITEM_COUNT,\n   \"item\": [{\n       \"media_id\": MEDIA_ID,\n       \"content\": {\n           \"news_item\": [{\n               \"title\": TITLE,\n               \"thumb_media_id\": THUMB_MEDIA_ID,\n               \"show_cover_pic\": SHOW_COVER_PIC(0 / 1),\n               \"author\": AUTHOR,\n               \"digest\": DIGEST,\n               \"content\": CONTENT,\n               \"url\": URL,\n               \"content_source_url\": CONTETN_SOURCE_URL\n           },\n           //多图文消息会在此处有多篇文章\n           ]\n        },\n        \"update_time\": UPDATE_TIME\n    },\n    //可能有多个图文消息item结构\n  ]\n}\n```\n\n\n### 获取素材计数\n\n```php\n$stats = $material->stats();\n\n// {\n//   \"voice_count\":COUNT,\n//   \"video_count\":COUNT,\n//   \"image_count\":COUNT,\n//   \"news_count\":COUNT\n// }\n```\n\n### 删除永久素材；\n\n```php\n$material->delete($mediaId);\n```\n\n\n## 临时素材 API\n\n上传的临时多媒体文件有格式和大小限制，如下：\n\n- 图片（image）: 1M，支持 `JPG` 格式\n- 语音（voice）：2M，播放长度不超过 `60s`，支持 `AMR\\MP3` 格式\n- 视频（video）：10MB，支持 `MP4` 格式\n- 缩略图（thumb）：64KB，支持 `JPG` 格式\n\n### 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$temporary->uploadImage($path);\n```\n\n### 上传声音\n\n```php\n$temporary->uploadVoice($path);\n```\n\n### 上传视频\n\n```php\n$temporary->uploadVideo($path, $title, $description);\n```\n\n### 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$temporary->uploadThumb($path);\n```\n\n### 获取临时素材内容\n\n比如图片、视频、声音等二进制流内容。\n\n```php\n$content = $temporary->getStream($mediaId);\nfile_put_contents('/tmp/abc.jpg', $content);// 请使用绝对路径写法！除非你正确的理解了相对路径（好多人是没理解对的）！\n```\n\n### 下载临时素材到本地\n\n其实就是上一个 API 的封装。\n\n```php\n$temporary->download($mediaId, \"/tmp/\", \"abc.jpg\");\n```\n\n参数说明：\n\n  - `$directory` 为目标目录，\n  - `$filename` 为新的文件名，可以为空，默认使用 `$mediaId` 作为文件名。\n\n\n更多请参考 [微信官方文档](http://mp.weixin.qq.com/wiki) `素材管理` 章节"
  },
  {
    "path": "docs/src/3.x/menu.md",
    "content": "# 自定义菜单\n\n\n3.0 的菜单组件有所简化，相比 2.x 版本变化如下：\n\n- 去除 `MenuItem` 类，创建菜单直接使用数组不再支持 `callback` 与 `MenuItem` 类似的繁杂的方式\n- `set()` 方法与 `addConditional()` 合并为一个方法 `add()`\n- `get()` 改名为 `all()`\n- `delete()` 与 `deleteById()` 合并为 `destroy()`\n- 所有 API 的返回值（非调用失败情况）均为官方文档原样返回（Collection形式），不再取返回值中部分 `key` 返回。\n  > 例如原来的 `get()` 方法，官方返回的数组为: `{ menu: [...]}`，SDK 取了其中的 `menu` 内容作为返回值，在 3.0 后将直接整体返回。\n\n## 获取菜单模块实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$menu = $app->menu;\n```\n\n## API 列表\n\n### 读取（查询）已设置菜单\n\n微信的菜单读取有两个不同的方式：\n\n一种叫 **[查询菜单](http://mp.weixin.qq.com/wiki/5/f287d1a5b78a35a8884326312ac3e4ed.html)**，在 SDK 中以 `all()` 方法来调用：\n\n```php\n$menus = $menu->all();\n```\n\n另外一种叫 **[获取自定义菜单](http://mp.weixin.qq.com/wiki/14/293d0cb8de95e916d1216a33fcb81fd6.html)**，使用 `current()` 方法来调用：\n\n```php\n$menus = $menu->current();\n```\n\n### 添加菜单\n\n#### 添加普通菜单\n\n```php\n$buttons = [\n    [\n        \"type\" => \"click\",\n        \"name\" => \"今日歌曲\",\n        \"key\"  => \"V1001_TODAY_MUSIC\"\n    ],\n    [\n        \"name\"       => \"菜单\",\n        \"sub_button\" => [\n            [\n                \"type\" => \"view\",\n                \"name\" => \"搜索\",\n                \"url\"  => \"http://www.soso.com/\"\n            ],\n            [\n                \"type\" => \"view\",\n                \"name\" => \"视频\",\n                \"url\"  => \"http://v.qq.com/\"\n            ],\n            [\n                \"type\" => \"click\",\n                \"name\" => \"赞一下我们\",\n                \"key\" => \"V1001_GOOD\"\n            ],\n        ],\n    ],\n];\n$menu->add($buttons);\n```\n\n以上将会创建一个普通菜单。\n\n#### 添加个性化菜单\n\n与创建普通菜单不同的是，需要在 `add()` 方法中将个性化匹配规则作为第二个参数传进去：\n\n```php\n$buttons = [\n    // ...\n];\n$matchRule = [\n    \"tag_id\" => \"2\",\n    \"sex\" => \"1\",\n    \"country\" => \"中国\",\n    \"province\" => \"广东\",\n    \"city\" => \"广州\",\n    \"client_platform_type\" => \"2\",\n    \"language\" => \"zh_CN\"\n];\n$menu->add($buttons, $matchRule);\n```\n\n### 删除菜单\n\n有两种删除方式，一种是**全部删除**，另外一种是**根据菜单 ID 来删除**(删除个性化菜单时用，ID 从查询接口获取)：\n\n```php\n$menu->destroy(); // 全部\n$menu->destroy($menuId);\n```\n\n### 测试个性化菜单\n\n```php\n$menus = $menu->test($userId);\n```\n\n> `$userId` 可以是粉丝的 OpenID，也可以是粉丝的微信号。\n\n返回 `$menus` 与指定的 `$userId` 匹配的菜单项。\n\n更多关于微信自定义菜单 API 请参考： http://mp.weixin.qq.com/wiki `自定义菜单` 章节。\n"
  },
  {
    "path": "docs/src/3.x/merchant_payment.md",
    "content": "# 企业支付\n\n\n你在阅读本文之前确认你已经仔细阅读了：[微信支付 | 企业付款文档 ](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1)。\n\n## 配置\n\n与其他支付接口一样，企业支付接口也需要配置如下参数，需要特别注意的是，企业支付相关的全部接口 **都需要使用 SSL 证书**，因此 **cert_path 以及 cert_key 必须正确配置**。\n\n```php\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    'app_id' => 'your-app-id',\n    // payment\n    'payment' => [\n        'merchant_id'        => 'your-mch-id',\n        'key'                => 'key-for-signature',\n        'cert_path'          => 'path/to/your/cert.pem',\n        'key_path'           => 'path/to/your/key',\n        // ...\n    ],\n];\n\n$app = new Application($options);\n\n$merchantPay = $app->merchant_pay;\n```\n\n## 企业付款\n\n企业付款使用的余额跟微信支付的收款并非同一账户，请注意充值。\n\n### 发送接口\n\n```php\n<?php\n\n$merchantPayData = [\n        'partner_trade_no' => str_random(16), //随机字符串作为订单号，跟红包和支付一个概念。\n        'openid' => $openid, //收款人的openid\n        'check_name' => 'NO_CHECK',  //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK\n        're_user_name'=>'张三',     //OPTION_CHECK FORCE_CHECK 校验实名的时候必须提交\n        'amount' => 100,  //单位为分\n        'desc' => '企业付款',\n        'spbill_create_ip' => '192.168.0.1',  //发起交易的IP地址\n    ];\n$result = $merchantPay->send($merchantPayData);\n\n```\n\n> 更多参数请参考官方文档中参数列表。\n\n## 查询付款信息\n\n用于商户对已发放的企业支付进行查询企业支付的具体信息。\n\n```php\n$partnerTradeNo = \"商户系统内部的订单号（partner_trade_no）\";\n$merchantPay->query($partnerTradeNo);\n```\n"
  },
  {
    "path": "docs/src/3.x/message-transfer.md",
    "content": "# 多客服消息转发\n\n\n\n多客服的消息转发绝对是超级的简单，转发的消息类型为 `transfer`：\n\n```php\n\n\n  // 转发收到的消息给客服\n  $server->setMessageHandler(function($message) {\n      return new \\EasyWeChat\\Message\\Transfer();\n  });\n\n  $result = $server->serve();\n\n  echo $result;\n```\n\n当然，你也可以指定转发给某一个客服：\n\n```php\n$server->setMessageHandler(function($message) {\n    $transfer = new \\EasyWeChat\\Message\\Transfer();\n    $transfer->account($account);// 或者 $transfer->to($account);\n\n    return $transfer;\n});\n```\n\n更多请参考 [微信官方文档](http://mp.weixin.qq.com/wiki/) **多客服消息转发** 章节"
  },
  {
    "path": "docs/src/3.x/messages.md",
    "content": "# 消息\n\n\n我把微信的 API 里的所有“消息”都按类型抽象出来了，也就是说，你不用区分它是回复消息还是主动推送消息，免去了你去手动拼装微信那帮 SB 那么恶心的 XML 以及乱七八糟命名不统一的 JSON 了，我替你承受这份苦，不要问是谁，我是雷锋他弟弟，雷管。\n\n在阅读以下内容时请忽略是**接收消息**还是**回复消息**，后面我会给你讲它们的区别。\n\n## 消息类型\n\n消息分为以下几种：`文本`、`图片`、`视频`、`声音`、`链接`、`坐标`、`图文`、`文章` 和一种特殊的 `原始消息`。\n\n另外还有一种特殊的消息类型：**素材消息**，用于群发或者客服时发送已有素材用。\n\n> 注意：回复消息与客服消息里的图文类型为：**图文**，群发与素材中的图文为**文章**\n\n所有的消息类都在 `EasyWeChat\\Message` 这个命名空间下， 下面我们来分开讲解：\n\n### 文本消息\n\n属性列表：\n\n```\n- content 文本内容\n```\n\n```php\n<?php\n\nuse EasyWeChat\\Message\\Text;\n\n$text = new Text(['content' => '您好！overtrue。']);\n\n// or\n$text = new Text();\n$text->content = '您好！overtrue。';\n\n// or\n$text = new Text();\n$text->setAttribute('content', '您好！overtrue。');\n```\n\n### 图片消息\n\n属性列表：\n\n```\n- media_id 媒体资源 ID\n```\n\n```php\n<?php\n\nuse EasyWeChat\\Message\\Image;\n\n$text = new Image(['media_id' => $mediaId]);\n\n// or\n$text = new Image();\n$text->media_id = $mediaId; // or $text->mediaId = $media;\n\n// or\n$text = new Image();\n$text->setAttribute('media_id', $mediaId);\n```\n\n\n### 视频消息\n\n属性列表：\n\n```\n- title 标题\n- description 描述\n- media_id 媒体资源 ID\n- thumb_media_id 封面资源 ID\n```\n\n```php\n<?php\n\nuse EasyWeChat\\Message\\Video;\n\n$video = new Video([\n        'title' => $title,\n        'media_id' => $mediaId,\n        'description' => '...',\n        // ...\n    ]);\n\n// or\n$video = new Video();\n$video->media_id = $mediaId; // or $video->mediaId = $media;\n$video->description = 'video description...'; // or $video->description = $description;\n// ...\n\n// or\n$video = new Video();\n$video->setAttribute('media_id', $mediaId);\n// ...\n```\n\n### 声音消息\n\n属性列表：\n\n```\n- media_id 媒体资源 ID\n```\n\n```php\n<?php\n\nuse EasyWeChat\\Message\\Voice;\n\n$voice = new Voice(['media_id' => $mediaId]);\n\n// or\n$voice = new Voice();\n$voice->media_id = $mediaId; // or $voice->mediaId = $media;\n\n// or\n$voice = new Voice();\n$voice->setAttribute('media_id', $mediaId);\n```\n\n### 链接消息\n\n> 微信目前不支持回复链接消息\n\n### 坐标消息\n\n> 微信目前不支持回复坐标消息\n\n### 图文消息\n\n属性列表：\n\n```\n- title 标题\n- description 描述\n- image 图片链接\n- url 链接 URL\n```\n\n```php\n<?php\nuse EasyWeChat\\Message\\News;\n\n$news = new News([\n        'title'       => $title,\n        'description' => '...',\n        'url'         => $url,\n        'image'       => $image,\n        // ...\n    ]);\n\n// or\n$news = new News();\n$news->title = 'EasyWeChat';\n$news->description = '微信 SDK ...';\n// ...\n\n```\n\n### 文章消息\n\n属性列表：\n\n```\n- title 标题\n- author 作者\n- content 具体内容\n- thumb_media_id 图文消息的封面图片素材id（必须是永久mediaID）\n- digest 图文消息的摘要，仅有单图文消息才有摘要，多图文此处为空\n- source_url 来源 URL\n- show_cover 是否显示封面，0 为 false，即不显示，1 为 true，即显示\n```\n\n```php\n<?php\nuse EasyWeChat\\Message\\Article;\n\n$article = new Article([\n        'title'   => 'EasyWeChat',\n        'author'  => 'overtrue',\n        'content' => 'EasyWeChat 是一个开源的微信 SDK，它... ...',\n        // ...\n    ]);\n\n// or\n$article = new Article();\n$article->title   = 'EasyWeChat';\n$article->author  = 'overtrue';\n$article->content = '微信 SDK ...';\n// ...\n```\n\n\n### 素材消息\n\n素材消息用于群发与客服消息时使用。\n\n属性就一个：`media_id`。\n\n在构造时有两个参数：\n\n- `$type` 素材类型，目前只支持：`mpnews`、 `mpvideo`、`voice`、`image` 等。\n- `$mediaId` 素材 ID，从接口查询或者上传后得到。\n\n\n```php\nuse EasyWeChat\\Message\\Material;\n\n$material = new Material('mpnews', $mediaId);\n```\n\n以上呢，是所有微信支持的基本消息类型。\n\n> 需要注意的是，你不需要关心微信的消息字段叫啥，因为这里我们使用了更标准的命名，然后最终在中间做了转换，所以你不需要关注。\n\n### 原始消息\n\n原始消息是一种特殊的消息，它的场景是：**你不想使用其它消息类型，你想自己手动拼消息**。比如，回复消息时，你想自己拼 XML，那么你就直接用它就可以了：\n\n```php\nuse EasyWeChat\\Message\\Raw;\n\n$message = new Raw('<xml>\n<ToUserName><![CDATA[toUser]]></ToUserName>\n<FromUserName><![CDATA[fromUser]]></FromUserName>\n<CreateTime>12345678</CreateTime>\n<MsgType><![CDATA[image]]></MsgType>\n<Image>\n<MediaId><![CDATA[media_id]]></MediaId>\n</Image>\n</xml>');\n```\n\n比如，你要用于客服消息(客服消息是JSON结构)：\n\n```php\nuse EasyWeChat\\Message\\Raw;\n\n$message = new Raw('{\n    \"touser\":\"OPENID\",\n    \"msgtype\":\"text\",\n    \"text\":\n    {\n         \"content\":\"Hello World\"\n    }\n}');\n```\n\n总之，就是直接写微信接口要求的格式内容就好，此类型消息在 SDK 中不存在转换行为，所以请注意不要写错格式。\n\n## 在 SDK 中使用消息\n\n### 在服务端回复消息\n\n在 [服务端](server.html) 一节中，我们讲了回复消息的写法：\n\n```php\n// ... 前面部分省略\n$app = new Application($options);\n$server = $app->server;\n\n$server->setMessageHandler(function ($message) {\n    return \"您好！欢迎关注我!\";\n});\n\n$server->serve()->send();\n```\n\n上面 `return` 了一句普通的文本内容，这里只是为了方便大家，实际上最后会有一个隐式转换为 `Text` 类型的动作。\n\n如果你要回复其它类型的消息，就需要返回一个具体的实例了，比如回复一个图片类型的消息：\n\n```php\nuse EasyWeChat\\Message\\Image;\n// ...\n$server->setMessageHandler(function ($message) {\n    return new Image(['media_id' => '........']);\n});\n// ...\n```\n\n#### 回复多图文消息\n\n多图文消息其实就是单图文消息的一个数组而已了：\n\n```php\nuse EasyWeChat\\Message\\News;\n\n// ...\n$server->setMessageHandler(function ($message) {\n    $news1 = new News(...);\n    $news2 = new News(...);\n    $news3 = new News(...);\n    $news4 = new News(...);\n\n    return [$news1, $news2, $news3, $news4];\n});\n// ...\n```\n\n\n### 作为客服消息发送\n\n在客服消息里的使用也一样，都是直接传入消息实例即可：\n\n```php\nuse EasyWeChat\\Message\\Text;\n\n$message = new Text(['content' => 'Hello world!']);\n\n$result = $app->staff->message($message)->to($openId)->send();\n//...\n```\n\n#### 发送多图文消息\n\n多图文消息其实就是单图文消息的一个数组而已了：\n\n```php\n$news1 = new News(...);\n$news2 = new News(...);\n$news3 = new News(...);\n$news4 = new News(...);\n\n$app->staff->message([$news1, $news2, $news3, $news4])->to($openId)->send();\n```\n\n### 群发消息\n\n请参考：[群发消息](broadcast.html)\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer.html)\n"
  },
  {
    "path": "docs/src/3.x/mini_program.md",
    "content": "title: 小程序\n---\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    // ...\n    'mini_program' => [\n        'app_id'   => 'component-app-id',\n        'secret'   => 'component-app-secret',\n        'token'    => 'component-token',\n        'aes_key'  => 'component-aes-key'\n        ],\n    // ...\n    ];\n\n$app = new Application($options);\n$miniProgram = $app->mini_program;\n```\n\n## 登录\n\n### 通过 Code 换取 SessionKey\n\n```php\n// 3.2 版本\n$miniProgram->user->getSessionKey($code);\n// 3.3 版本\n$miniProgram->sns->getSessionKey($code);\n```\n\n## 加密数据解密\n\n```php\n$miniProgram->encryptor->decryptData($sessionKey, $iv, $encryptData);\n```\n\n## 数据分析\n\n### API\n\n- `summaryTrend($from, $to)` 概况趋势，限定查询1天数据，即 `$from` 要与 `$to` 相同；\n- `dailyVisitTrend($from, $to)` 访问日趋势，限定查询1天数据，即 `$from` 要与 `$to` 相同；\n- `weeklyVisitTrend($from, $to)` 访问周趋势， `$from` 为周一日期， `$to` 为周日日期；\n- `monthlyVisitTrend($from, $to)` 访问月趋势， `$from` 为月初日期， `$to` 为月末日期；\n- `visitDistribution($from, $to)` 访问分布，限定查询1天数据，即 `$from` 要与 `$to` 相同；\n- `dailyRetainInfo($from, $to)` 访问日留存，限定查询1天数据，即 `$from` 要与 `$to` 相同；\n- `weeklyRetainInfo($from, $to)` 访问周留存， `$from` 为周一日期， `$to` 为周日日期；\n- `montylyRetainInfo($from, $to)` 访问月留存， `$from` 为月初日期， `$to` 为月末日期；\n- `visitPage($from, $to)` 访问页面，限定查询1天数据，即 `$from` 要与 `$to` 相同；\n\n### 代码示例\n\n```php\n$miniProgram->stats->summaryTrend('20170313', '20170313');\n```\n"
  },
  {
    "path": "docs/src/3.x/miscellaneous.md",
    "content": "# 其它\n\n\n### 其它"
  },
  {
    "path": "docs/src/3.x/notice.md",
    "content": "# 模板消息\n\n模板消息仅用于公众号向用户发送重要的服务通知，只能用于符合其要求的服务场景中，如信用卡刷卡通知，商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$notice = $app->notice;\n```\n\n### API\n\n- `boolean setIndustry($industryId1, $industryId2)` 修改账号所属行业；\n- `array getIndustry()` 返回所有支持的行业列表，用于做下拉选择行业可视化更新；\n- `string  addTemplate($shortId)` 添加模板并获取模板 ID；\n- `collection send($message)` 发送模板消息, 返回消息 ID；\n- `array  getPrivateTemplates()` 获取所有模板列表；\n- `array  deletePrivateTemplate($templateId)` 删除指定 ID 的模板。\n\n非链接调用方法：\n\n```php\n$messageId = $notice->send([\n        'touser' => 'user-openid',\n        'template_id' => 'template-id',\n        'url' => 'xxxxx',\n        'data' => [\n            //...\n        ],\n    ]);\n```\n\n链式调用方法:\n\n    设置模板ID：template / templateId / uses\n    设置接收者openId: to / receiver\n    设置详情链接：url / link / linkTo\n    设置模板数据：data / with\n\n    以上方法都支持 `withXXX` 与 `andXXX` 形式链式调用\n\n```php\n$messageId = $notice->to($userOpenId)->uses($templateId)->andUrl($url)->data($data)->send();\n// 或者\n$messageId = $notice->to($userOpenId)->url($url)->template($templateId)->andData($data)->send();\n// 或者\n$messageId = $notice->withTo($userOpenId)->withUrl($url)->withTemplate($templateId)->withData($data)->send();\n// 或者\n$messageId = $notice->to($userOpenId)->url($url)->withTemplateId($templateId)->send();\n// ... ...\n```\n\n## 示例:\n\n### 模板\n\n```\n{{ first.DATA }}\n\n商品明细：\n\n名称：{{ name.DATA }}\n价格：{{ price.DATA }}\n\n{{ remark.DATA }}\n```\n\n发送模板消息：\n\n```php\n$userId = 'OPENID';\n$templateId = 'ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY';\n$url = 'http://overtrue.me';\n$data = array(\n         \"first\"  => \"恭喜你购买成功！\",\n         \"name\"   => \"巧克力\",\n         \"price\"  => \"39.8元\",\n         \"remark\" => \"欢迎再次购买！\",\n        );\n\n$result = $notice->uses($templateId)->withUrl($url)->andData($data)->andReceiver($userId)->send();\nvar_dump($result);\n\n// {\n//      \"errcode\":0,\n//      \"errmsg\":\"ok\",\n//      \"msgid\":200228332\n//  }\n```\n\n## 模板数据\n\n为了方便大家开发，我们拓展支持以下格式的模板数据，其它格式的数据可能会导致接口调用失败：\n\n- 所有数据项颜色一样的（这是方便的一种方式）:\n\n  ```php\n  $data = array(\n      \"first\"    => \"恭喜你购买成功！\",\n      \"keynote1\" => \"巧克力\",\n      \"keynote2\" => \"39.8元\",\n      \"keynote3\" => \"2014年9月16日\",\n      \"remark\"   => \"欢迎再次购买！\",\n  );\n  ```\n\n  默认颜色为'#173177', 你可以通过 `defaultColor($color)` 来修改\n\n- 独立设置每个模板项颜色的：\n\n  - 简便型：\n\n    ```php\n    $data = array(\n        \"first\"    => array(\"恭喜你购买成功！\", '#555555'),\n        \"keynote1\" => array(\"巧克力\", \"#336699\"),\n        \"keynote2\" => array(\"39.8元\", \"#FF0000\"),\n        \"keynote3\" => array(\"2014年9月16日\", \"#888888\"),\n        \"remark\"   => array(\"欢迎再次购买！\", \"#5599FF\"),\n    );\n    ```\n\n  - 复杂型（也是微信官方唯一支持的方式，估计没有人想这么用）：\n\n    ```php\n    $data = array(\n        \"first\"    => array(\"value\" => \"恭喜你购买成功！\", \"color\" => '#555555'),\n        \"keynote1\" => array(\"value\" => \"巧克力\", \"color\" => \"#336699\"),\n        \"keynote2\" => array(\"value\" => \"39.8元\",\"color\" => \"#FF0000\"),\n        \"keynote3\" => array(\"value\" => \"2014年9月16日\", \"color\" => \"#888888\"),\n        \"remark\"   => array(\"value\" => \"欢迎再次购买！\", \"color\" => \"#5599FF\"),\n    );\n    ```\n\n关于模板消息的使用请参考 [微信官方文档](http://mp.weixin.qq.com/wiki/)\n"
  },
  {
    "path": "docs/src/3.x/oauth.md",
    "content": "# 网页授权\n\n## 关于 OAuth2.0\n\nOAuth 是一个关于授权（authorization）的开放网络标准，在全世界得到广泛应用，目前的版本是 2.0 版。\n\n```\n\n     +--------+                               +---------------+\n     |        |--(A)- Authorization Request ->|   Resource    |\n     |        |                               |     Owner     |\n     |        |<-(B)-- Authorization Grant ---|               |\n     |        |                               +---------------+\n     |        |\n     |        |                               +---------------+\n     |        |--(C)-- Authorization Grant -->| Authorization |\n     | Client |                               |     Server    |\n     |        |<-(D)----- Access Token -------|               |\n     |        |                               +---------------+\n     |        |\n     |        |                               +---------------+\n     |        |--(E)----- Access Token ------>|    Resource   |\n     |        |                               |     Server    |\n     |        |<-(F)--- Protected Resource ---|               |\n     +--------+                               +---------------+\n                      OAuth 授权流程\n```\n\n> 摘自：[RFC 6749](https://datatracker.ietf.org/doc/rfc6749/?include_text=1)\n\n步骤解释：\n\n    （A）用户打开客户端以后，客户端要求用户给予授权。\n    （B）用户同意给予客户端授权。\n    （C）客户端使用上一步获得的授权，向认证服务器申请令牌。\n    （D）认证服务器对客户端进行认证以后，确认无误，同意发放令牌。\n    （E）客户端使用令牌，向资源服务器申请获取资源。\n    （F）资源服务器确认令牌无误，同意向客户端开放资源。\n\n关于 OAuth 协议我们就简单了解到这里，如果还有不熟悉的同学，请 [Google 相关资料](https://www.google.com.hk/?gws_rd=ssl#safe=strict&q=OAuth2)\n\n## 微信 OAuth\n\n在微信里的 OAuth 其实有两种：[公众平台网页授权获取用户信息](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html)、[开放平台网页登录](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN)。\n\n它们的区别有两处，授权地址不同，`scope` 不同。\n\n- **公众平台网页授权获取用户信息**\n\n  **授权 URL**: `https://open.weixin.qq.com/connect/oauth2/authorize`\n  **Scopes**: `snsapi_base` 与 `snsapi_userinfo`\n\n- **开放平台网页登录**\n\n  **授权 URL**: `https://open.weixin.qq.com/connect/qrconnect`\n  **Scopes**: `snsapi_login`\n\n他们的逻辑都一样：\n\n1. 用户尝试访问一个我们的业务页面，例如: `/user/profile`\n2. 如果用户已经登录，则正常显示该页面\n3. 系统检查当前访问的用户并未登录（从 session 或者其它方式检查），则跳转到**跳转到微信授权服务器**（上面的两种中一种**授权 URL**），并告知微信授权服务器我的**回调 URL（redirect_uri=callback.php)**，此时用户看到蓝色的授权确认页面（`scope` 为 `snsapi_base` 时不显示）\n4. 用户点击确定完成授权，浏览器跳转到**回调 URL**: `callback.php` 并带上 `code`： `?code=CODE&state=STATE`。\n5. 在 `callback.php` 中得到 `code` 后，通过 `code` 再次向微信服务器请求得到 **网页授权 access_token** 与 `openid`\n6. 你可以选择拿 `openid` 去请求 API 得到用户信息（可选）\n7. 将用户信息写入 SESSION。\n8. 跳转到第 3 步写入的 `target_url` 页面（`/user/profile`）。\n\n> 看懵了？没事，使用 SDK，你不用管这么多。:smile:\n>\n> 注意，上面的第 3 步：redirect_uri=callback.php 实际上我们会在 `callback.php` 后面还会带上授权目标页面 `user/profile`，所以完整的 `redirect_uri` 应该是下面的这样的 PHP 去拼出来：`'redirect_uri='.urlencode('callback.php?target=user/profile')`\n> 结果：redirect_uri=callback.php%3Ftarget%3Duser%2Fprofile\n\n## 逻辑组成\n\n从上面我们所描述的授权流程来看，我们至少有 3 个页面：\n\n1. **业务页面**，也就是需要授权才能访问的页面。\n2. **发起授权页**，此页面其实可以省略，可以做成一个中间件，全局检查未登录就发起授权。\n3. **授权回调页**，接收用户授权后的状态，并获取用户信息，写入用户会话状态（SESSION）。\n\n## 开始之前\n\n在开始之前请一定要记住，先登录公众号后台，找到**边栏 “开发”** 模块下的 **“接口权限”**，点击 **“网页授权获取用户基本信息”** 后面的修改，添加你的网页授权域名。\n\n> 如果你的授权地址为：`http://www.abc.com/xxxxx`，那么请填写 `www.abc.com`，也就是说请填写与网址匹配的域名，前者如果填写 `abc.com` 是通过不了的。\n\n## SDK 中 OAuth 模块的 API\n\n在 SDK 中，我们使用名称为 `oauth` 的模块来完成授权服务，我们主要用到以下两个 API：\n\n### 发起授权\n\n```php\n$response = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->redirect();\n```\n\n当你的应用是分布式架构且没有会话保持的情况下，你需要自行设置请求对象以实现会话共享。比如在 [Laravel](http://laravel.com) 框架中支持 Session 储存在 Redis 中，那么需要这样：\n\n```php\n$response = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->setRequest($request)\n                          ->redirect();\n\n//回调后获取user时也要设置$request对象\n//$user = $app->oauth->setRequest($request)->user();\n```\n\n它的返回值 `$response` 是一个 [Symfony\\Component\\HttpFoundation\\RedirectResponse](http://api.symfony.com/3.0/Symfony/Component/HttpFoundation/RedirectResponse.html) 实例。\n\n你可以选择在框架中做一些正确的响应，比如在 [Laravel](http://laravel.com) 框架中控制器方法是要求返回响应值的，那么你就直接:\n\n```php\nreturn $response;\n```\n\n在有的框架 (比如 yii2) 中是直接 `echo` 或者 `$this->display()` 这种的时候，你就直接：\n\n```php\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n### 获取已授权用户\n\n```php\n$user = $app->oauth->user();\n// $user 可以用的方法:\n// $user->getId();  // 对应微信的 OPENID\n// $user->getNickname(); // 对应微信的 nickname\n// $user->getName(); // 对应微信的 nickname\n// $user->getAvatar(); // 头像网址\n// $user->getOriginal(); // 原始API返回的结果\n// $user->getToken(); // access_token， 比如用于地址共享时使用\n```\n\n返回的 `$user` 是 [Overtrue\\Socialite\\User](https://github.com/overtrue/socialite/blob/master/src/User.php) 对象，你可以从该对象拿到[更多的信息](https://github.com/overtrue/socialite#user-interface)。\n\n> :pray: 注意：`$user` 里没有 `openid`， `$user->id` 便是 `openid`.\n> 如果你想拿微信返回给你的原样的全部信息，请使用：$user->getOriginal();\n\n当 `scope` 为 `snsapi_base` 时 `$oauth->user();` 对象里只有 `id`，没有其它信息。\n\n## 网页授权实例\n\n我们这里来用原生 PHP 写法举个例子，`oauth_callback` 是我们的授权回调 URL (未 urlencode 编码的 URL), `user/profile` 是我们需要授权才能访问的页面，它的 PHP 代码如下：\n\n```php\n// http://easywechat.com/user/profile\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n$config = [\n  // ...\n  'oauth' => [\n      'scopes'   => ['snsapi_userinfo'],\n      'callback' => '/oauth_callback',\n  ],\n  // ..\n];\n\n$app = new Application($config);\n$oauth = $app->oauth;\n\n// 未登录\nif (empty($_SESSION['wechat_user'])) {\n\n  $_SESSION['target_url'] = 'user/profile';\n\n  return $oauth->redirect();\n  // 这里不一定是return，如果你的框架action不是返回内容的话你就得使用\n  // $oauth->redirect()->send();\n}\n\n// 已经登录过\n$user = $_SESSION['wechat_user'];\n\n// ...\n\n```\n\n授权回调页：\n\n```php\n// http://easywechat.com/oauth_callback\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n$config = [\n  // ...\n];\n\n$app = new Application($config);\n$oauth = $app->oauth;\n\n// 获取 OAuth 授权结果用户信息\n$user = $oauth->user();\n\n$_SESSION['wechat_user'] = $user->toArray();\n\n$targetUrl = empty($_SESSION['target_url']) ? '/' : $_SESSION['target_url'];\n\nheader('location:'. $targetUrl); // 跳转到 user/profile\n```\n\n上面的例子呢都是基于 `$_SESSION` 来保持会话的，在微信客户端中，你可以结合 COOKIE 来存储，但是有效期平台不一样时间也不一样，好像 Android 的失效会快一些，不过基本也够用了。\n\n更多关于微信网页授权 API 请参考： http://mp.weixin.qq.com/wiki/\n更多开放平台网页登录请参考：https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN\n"
  },
  {
    "path": "docs/src/3.x/open_platform.md",
    "content": "# 微信开放平台\n\n\n### 实例化\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    // ...\n    'open_platform' => [\n        'app_id'   => 'component-app-id',\n        'secret'   => 'component-app-secret',\n        'token'    => 'component-token',\n        'aes_key'  => 'component-aes-key'\n        ],\n    // ...\n    ];\n\n$app = new Application($options);\n$openPlatform = $app->open_platform;\n```\n\n### 监听微信服务器推送事件\n\n公众号第三方平台推送的有四个事件：授权成功(`authorized`)，授权更新(`updateauthorized`)，授权取消（`unauthorized`），以及 `component_verify_ticket`。\n\n本 SDK 默认处理方式为：\n\n- `authorized` / `updateauthorized`: 获取授权方(Authorizer)的所有信息，并缓存 `authorizer_access_token` 和 `authorizer_refresh_token`，授权方的信息则需要开发者手动处理。\n- `unauthorized`: 删除 `authorizer_access_token` 和 `authorizer_refresh_token` 的缓存。\n- `component_verify_ticket`: 缓存 `component_veirfy_ticket`。\n\n当然也允许自定义处理这些事件，不过以上默认处理仍然会先执行，为的是帮助开发者免去缓存的困扰。\n\n```php\n// 默认处理方式\n$openPlatform->server->serve();\n\n// 自定义处理\n$openPlatform->server->setMessageHandler(function($event) {\n    // 事件类型常量定义在 \\EasyWeChat\\OpenPlatform\\Guard 类里\n    switch ($event->InfoType) {\n        case 'authorized':\n            // ...\n        case 'unauthorized':\n            // ...\n        case 'updateauthorized':\n            // ...\n        case 'component_verify_ticket':\n            // ...\n    }\n});\n$openPlatform->server->serve();\n\n// 或者\n$openPlatform->server->listen(function ($event) {\n    switch ($event->InfoType) {\n        // ...\n    }\n});\n```\n\n#### 授权成功，授权更新\n\n这两个事件下，SDK 默认抓取了所有授权方所有的信息，并缓存 `authorizer_access_token` 和 `authorizer_refresh_token`，授权方的信息为原微信 API 的返回结果，由开发者自行处理，比如保存到数据库。\n\n```php\n// 自定义处理\n// 其中 $event 变量里有微信推送事件本身的信息，也有授权方所有的信息。\n$openPlatform->server->setMessageHandler(function($event) {\n    // 事件类型常量定义在 \\EasyWeChat\\OpenPlatform\\Guard 类里\n    switch ($event->InfoType) {\n        case 'authorized':\n            // 授权信息，主要是 token 和授权域\n            $info1 = $event->authorization_info;\n            // 授权方信息，就是授权方公众号的信息了\n            $info2 = $event->authorizer_info;\n    }\n});\n```\n\n目前 SDK 对这两个事件的处理方式没有区别。\n\n#### 授权取消\n\nSDK 默认处理：删除 `authorizer_access_token` 和 `authorizer_refresh_token` 的缓存。开发者可以自行处理数据库删除授权方信息等操作。\n\n#### 推送 component_verify_ticket\n\n在公众号第三方平台创建审核通过后，微信服务器会向其“授权事件接收URL”每隔10分钟定时推送 `component_verify_ticket`。SDK 内部已实现缓存 `component_veirfy_ticket`，无需开发者另行缓存。\n\n注：需要在URL路由中写上触发代码，并且注册路由后需要等待微信服务器推送 `component_verify_ticket`，才能进行后续操作，否则报\"Component verify ticket does not exists.\"\n\n### 调用 API\n\n#### 设置授权方的 App Id\n\n开发者必须设置授权方来调用 API。\n\n```php\n$openPlatform = new Application($options)->open_platform;\n\n// 加载授权方信息，比如 $authorizer = Authorizer::find($id);\n$authorizerAppId = $authorizer->app_id;\n$authorizerRefreshToken = $authorizer->refresh_token;\n\n$app = $openPlatform->createAuthorizerApplication($authorizerAppId, $authorizerRefreshToken);\n// 然后调用方法和普通调用一致。\n// ...\n```\n\n### 授权 API\n\n#### 获取预授权网址\n\n```php\n// 直接跳转\n$response = $openPlatform->pre_auth->redirect('https://domain.com/callback');\n\n// 获取跳转的链接\n$response->getTargetUrl();\n```\n\n用户授权后会带上 `code` 跳转到 `redirect` 指定的链接。\n\n#### 使用授权码换取公众号的接口调用凭据和授权信息\n\n```php\n// 使用授权码换取公众号的接口调用凭据和授权信息\n// Optional: $authorizationCode 不传值时会自动获取 URL 中 auth_code 值\n$openPlatform->getAuthorizationInfo($authorizationCode = null);\n```\n\n#### 获取授权方的公众号帐号基本信息\n\n```php\n$openPlatform->getAuthorizerInfo($authorizerAppId);\n```\n\n#### 获取授权方的选项设置信息\n\n```php\n$openPlatform->getAuthorizerOption($authorizerAppId, $optionName);\n```\n\n#### 设置授权方的选项信息\n\n```php\n$openPlatform->setAuthorizerOption($authorizerAppId, $optionName, $optionValue);\n```\n"
  },
  {
    "path": "docs/src/3.x/overview.md",
    "content": "# EasyWeChat\n\n## EasyWeChat 是什么？\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。\n\nEasyWeChat 的安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n### 环境需求\n\n- PHP >= 5.5.9 (其实你不必惊讶，PHP 7 的时代了)\n- [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n- [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n- [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php) 素材管理模块需要用到\n\n### 加入我们\n\n<a target=\"_blank\" href=\"http://shang.qq.com/wpa/qunwpa?idkey=b4dcf3ec51a7e8c3c3a746cf450ce59895e5c4ec4fbcb0f80c2cd97c3c6e63e9\"><img border=\"0\" src=\"http://pub.idqqimg.com/wpa/images/group.png\" alt=\"EasyWeChat SDK 交流群\" title=\"EasyWeChat SDK 交流群\"></a> ID: 319502940\n\n> 为了避免广告及不看文档用户，加群需要付费，所以请使用 能支持群费的客户端。\n> 另外：付费加群不代表我们有责任在群里回答你的问题，所以请认真阅读微信官方文档与 SDK 使用文档再使用，否则提的低级问题不会有人理你\n> 不喜勿加，谢谢！\n> 除非你发现了明确的 Bug，否则不要在群里 @ 我，否则直接 T 人（当然了，不退群费）:pray:\n\n你有以下两种方式加入到我们中来，为广大开发者提供更优质的免费开源的服务：\n\n- **贡献代码**：我们 3.0 的代码都在 [overtrue/wechat](https://github.com/overtrue/wechat) ，你可以提交 PR 到任何一个项目，当然，前提是代码质量必须是 OK 的。\n- **翻译或补充文档**：我们的文档在：[w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/master/docs)，你可以选择补充文档或者参与英文文档的翻译，目前有 `zh-cn` 与 `en` 两个分支，你可以提交对应的 PR 到目标分支参与翻译工作。\n\n### 开始之前\n\n本 SDK 不是一个全新再造的东西，所以我不会从 0 开始教会你开发微信，你完全有必要在使用本 SDK 前做好以下工作：\n\n- 具备 PHP 基础知识，不要连闭包是啥都不明白，可以参考我在知乎的回答: [想要开发自己的 PHP 框架需要那些知识储备？](http://www.zhihu.com/question/26635323/answer/33812516)\n- 熟悉 PHP 常见的知识：自动加载、composer 的使用、JSON 处理、Curl 的使用等；\n- **仔细阅读并看懂** （不是**看过**，是**看明白+看完** :exclamation:） [微信官方文档](http://mp.weixin.qq.com/wiki/13/80a1a25adbc46faf2716774c423b3151.html) [微信开放平台文档](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318292&token=&lang=zh_CN)；\n- 明白微信接口的组成，自有服务器、微信服务器、公众号（还有其它各种号）、测试号、以及通信原理（交互过程）；\n- 了解基本的 HTTP 协议，Header 头、请求方式（GET\\POST\\PUT\\PATCH\\DELETE）等；\n- 基本的 Debug 技能，查看 php 日志，nginx 日志等。\n\n如果你不具备这些知识，请不要使用，因为用起来会比较痛苦。\n\n另外你有必要看一下以下的链接：\n\n- https://phphub.org/topics/535\n- http://laravel-china.github.io/php-the-right-way/\n\n如果你在群里问以下类似的问题，这真的是你没有做好上面的工作：\n\n- \"为啥我的不行啊，请问服务器日志怎么看啊？\"\n- \"请问这是什么原因啊？[结果/报错截图]\"\n- \"请问这个 SDK 怎么用啊？\"\n- \"谁能告诉我这个 SDK 是怎么安装的啊？\"\n- \"怎么接收用户发的消息啊？\"\n- \"为啥我的报这个错啊：Class XXXX not found...\"\n- ...\n\n我们专门针对一些容易出现的通用问题已经做了汇总： [疑难解答](troubleshooting) ，如果你在问题疑难解答没找到你出现的问题，那么可以在这里提问 [GitHub](https://github.com/overtrue/wechat/issues)，提问请描述清楚你用的版本，你的做法是什么，不然别人没法帮你。\n\n最后，请 **不要在 QQ 单独找我提问**，除非你是发现了明显的 bug。有问题先审查代码，看文档, 再 google，然后 去群里发个问题，带上你的代码，重现流程，大家有空的会帮忙你解答。谢谢合作！:pray:\n\n### 打赏支持\n\n这是一个开源的项目，我们没有收费服务，你如果觉得你从中获益，简化了你的开发工作，你可以 [打赏](https://github.com/sponsors/overtrue) 来支持我们。\n"
  },
  {
    "path": "docs/src/3.x/payment.md",
    "content": "# 支付\n\n你在阅读本文之前确认你已经仔细阅读了：[微信支付 | 商户平台开发文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)。\n\n网友贡献的教程：[小能手马闯 set 发布在 Laravel-China 的文章《基于 Laravel5.1 LTS 版的微信开发》](https://laravel-china.org/topics/3146)\n\n## 配置\n\n配置在前面的例子中已经提到过了，支付的相关配置如下：\n\n```php\n<?php\n\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    // 前面的appid什么的也得保留哦\n    'app_id' => 'xxxx',\n    // ...\n\n    // payment\n    'payment' => [\n        'merchant_id'        => 'your-mch-id',\n        'key'                => 'key-for-signature',\n        'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n        'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n        'notify_url'         => '默认的订单回调地址',       // 你也可以在下单时单独设置来想覆盖它\n        // 'device_info'     => '013467007045764',\n        // 'sub_app_id'      => '',\n        // 'sub_merchant_id' => '',\n        // ...\n    ],\n];\n\n$app = new Application($options);\n\n$payment = $app->payment;\n```\n\n## 创建订单\n\n### 正常模式\n\n```php\n<?php\n\nuse EasyWeChat\\Payment\\Order;\n\n$attributes = [\n    'trade_type'       => 'JSAPI', // JSAPI，NATIVE，APP...\n    'body'             => 'iPad mini 16G 白色',\n    'detail'           => 'iPad mini 16G 白色',\n    'out_trade_no'     => '1217752501201407033233368018',\n    'total_fee'        => 5388, // 单位：分\n    'notify_url'       => 'http://easywechat.com/order-notify', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'openid'           => '当前用户的 openid', // trade_type=JSAPI，此参数必传，用户在商户appid下的唯一标识，\n    // ...\n];\n\n$order = new Order($attributes);\n\n```\n\n### 子服务商模式\n\n```php\n<?php\n\nuse EasyWeChat\\Payment\\Order;\n\n$attributes = [\n    'trade_type'       => 'JSAPI', // JSAPI，NATIVE，APP...\n    'body'             => 'iPad mini 16G 白色',\n    'detail'           => 'iPad mini 16G 白色',\n    'out_trade_no'     => '1217752501201407033233368018',\n    'total_fee'        => 5388, // 单位：分\n    'notify_url'       => 'http://easywechat.com/order-notify', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'sub_openid'        => '当前用户的 openid', // 如果传入sub_openid, 请在实例化Application时, 同时传入$sub_app_id, $sub_merchant_id\n    // ...\n];\n\n$order = new Order($attributes);\n\n```\n\n通知 url 必须为直接可访问的 url，不能携带参数。示例：notify_url：“https://pay.weixin.qq.com/wxpay/pay.action”\n\n## 下单接口\n\n### 刷卡支付\n\n[官方文档](https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10)\n\n```php\n$result = $payment->pay($order);\n```\n\n> 也许你需要生成二维码图片，参考页面底部相关的包推荐\n\n## 统一下单\n\n[公众号支付](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)、[扫码支付](https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1)、[APP 支付](https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=9_1) 都统一使用此接口完成订单的创建。\n\n```php\n$result = $payment->prepare($order);\nif ($result->return_code == 'SUCCESS' && $result->result_code == 'SUCCESS'){\n    $prepayId = $result->prepay_id;\n}\n```\n\n## 支付结果通知\n\n在用户成功支付后，微信服务器会向该 **订单中设置的回调 URL** 发起一个 POST 请求，请求的内容为一个 XML。里面包含了所有的详细信息，具体请参考：\n[支付结果通用通知](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7)\n\n在本 SDK 中处理回调真的再简单不过了，请求验证你就不用管了，SDK 已经为你做好了，你只需要关注业务即可：\n\n```php\n$response = $app->payment->handleNotify(function($notify, $successful){\n    // 你的逻辑\n    return true; // 或者错误消息\n});\n\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n这里需要注意的有几个点：\n\n1. `handleNotify` 只接收一个 [`callable`](http://php.net/manual/zh/language.types.callable.php) 参数，通常用一个匿名函数即可。\n2. 该匿名函数接收两个参数，这两个参数分别为：\n\n   - `$notify` 为封装了通知信息的 `EasyWeChat\\Support\\Collection` 对象，前面已经讲过这里就不赘述了，你可以以对象或者数组形式来读取通知内容，比如：`$notify->total_fee` 或者 `$notify['total_fee']`。\n   - `$successful` 这个参数其实就是判断 **用户是否付款成功了**（result_code == 'SUCCESS'）\n\n3. 该函数返回值就是告诉微信 **“我是否处理完成”**，如果你返回一个 `false` 或者一个具体的错误消息，那么微信会在稍后再次继续通知你，直到你明确的告诉它：“我已经处理完成了”，在函数里 `return true;` 代表处理完成。\n\n4. `handleNotify` 返回值 `$response` 是一个 Response 对象，如果你要直接输出，使用 `$response->send()`, 在一些框架里不是输出而是返回：`return $response`。\n\n通常我们的处理逻辑大概是下面这样（**以下只是伪代码**）：\n\n```php\n$response = $app->payment->handleNotify(function($notify, $successful){\n    // 使用通知里的 \"微信支付订单号\" 或者 \"商户订单号\" 去自己的数据库找到订单\n    $order = 查询订单($notify->out_trade_no);\n\n    if (!$order) { // 如果订单不存在\n        return 'Order not exist.'; // 告诉微信，我已经处理完了，订单没找到，别再通知我了\n    }\n\n    // 如果订单存在\n    // 检查订单是否已经更新过支付状态\n    if ($order->paid_at) { // 假设订单字段“支付时间”不为空代表已经支付\n        return true; // 已经支付成功了就不再更新了\n    }\n\n    // 用户是否支付成功\n    if ($successful) {\n        // 不是已经支付状态则修改为已经支付状态\n        $order->paid_at = time(); // 更新支付时间为当前时间\n        $order->status = 'paid';\n    } else { // 用户支付失败\n        $order->status = 'paid_fail';\n    }\n\n    $order->save(); // 保存订单\n\n    return true; // 返回处理完成\n});\n\nreturn $response;\n```\n\n> 注意：请把 “支付成功与否” 与 “是否处理完成” 分开，它俩没有必然关系。\n> 比如：微信通知你用户支付完成，但是支付失败了(result_code 为 'FAIL')，你应该**更新你的订单为支付失败**，但是要**告诉微信处理完成**。\n\n## 撤销订单 API\n\n目前只有 **刷卡支付** 有此功能。\n\n> 调用支付接口后请勿立即调用撤销订单 API，建议支付后至少 15s 后再调用撤销订单接口。\n\n```php\n$orderNo = \"商户系统内部的订单号（out_trade_no）\";\n$payment->reverse($orderNo);\n```\n\n或者：\n\n```php\n\n$orderNo = \"微信的订单号（transaction_id）\";\n$payment->reverseByTransactionId($orderNo);\n```\n\n## 查询订单\n\n该接口提供所有微信支付订单的查询，商户可以通过该接口主动查询订单状态，完成下一步的业务逻辑。\n\n需要调用查询接口的情况：\n\n- 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；\n- 调用支付接口后，返回系统错误或未知交易状态情况；\n- 调用被扫支付 API，返回 USERPAYING 的状态；\n- 调用关单或撤销接口 API 之前，需确认支付状态；\n\n```php\n$orderNo = \"商户系统内部的订单号（out_trade_no）\";\n$payment->query($orderNo);\n```\n\n或者：\n\n```php\n\n$orderNo = \"微信的订单号（transaction_id）\";\n$payment->queryByTransactionId($orderNo);\n```\n\n## 关闭订单\n\n> 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为 5 分钟。\n\n```php\n$orderNo = \"商户系统内部的订单号（out_trade_no）\";\n$payment->close($orderNo);\n```\n\n## 申请退款\n\n当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，微信支付将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。\n\n注意：\n\n> 1、交易时间超过一年的订单无法提交退款；\n> 2、微信支付退款支持单笔交易分多次退款，多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。一笔退款失败后重新提交，要采用原来的退款单号。总退款金额不能超过用户实际支付金额。\n\n```php\n$payment->refund(订单号，退款单号，总金额，退款金额，操作员，退款单号类型(out_trade_no/transaction_id)，退款账户(REFUND_SOURCE_UNSETTLED_FUNDS/REFUND_SOURCE_RECHARGE_FUNDS))\n```\n\n参考：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4\n\n例子：\n\n```php\n# 1. 使用商户订单号退款\n$result = $payment->refund($orderNo, $refundNo, 100); // 总金额 100 退款 100，操作员：商户号\n// or\n$result = $payment->refund($orderNo, $refundNo, 100, 80); // 总金额 100， 退款 80，操作员：商户号\n// or\n$result = $payment->refund($orderNo, $refundNo, 100, 80, 1900000109); // 总金额 100， 退款 80，操作员：1900000109\n// or\n$result = $payment->refund($orderNo, $refundNo, 100, 80, 1900000109, 'out_trade_no'); // 总金额 100， 退款 80，操作员：1900000109, 退款单号：使用商户订单号退款\n// or\n$result = $payment->refund($orderNo, $refundNo, 100, 80, 1900000109, 'out_trade_no', 'REFUND_SOURCE_RECHARGE_FUNDS'); // 总金额 100， 退款 80，操作员：1900000109, 退款单号：使用商户订单号退款, 退款账户：可用余额退款\n\n# 2. 使用 TransactionId 退款\n$result = $payment->refundByTransactionId($transactionId, $refundNo, 100); // 总金额 100 退款 100，操作员：商户号\n// or\n$result = $payment->refundByTransactionId($transactionId, $refundNo, 100, 80); // 总金额 100， 退款 80，操作员：商户号\n// or\n$result = $payment->refundByTransactionId($transactionId, $refundNo, 100, 80, 1900000109); // 总金额 100， 退款 80，操作员：1900000109\n// or\n$result = $payment->refundByTransactionId($transactionId, $refundNo, 100, 80, 1900000109, 'REFUND_SOURCE_RECHARGE_FUNDS'); // 总金额 100， 退款 80，操作员：1900000109，退款账户：可用余额退款\n```\n\n> $refundNo 为商户退款单号，自己生成用于自己识别即可。\n\n## 查询退款\n\n提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用零钱支付的退款 20 分钟内到账，银行卡支付的退款 3 个工作日后重新查询退款状态。\n\n```php\n$result = $payment->queryRefund($outTradeNo);\n// or\n$result = $payment->queryRefundByTransactionId($transactionId);\n// or\n$result = $payment->queryRefundByRefundNo($outRefundNo);\n// or\n$result = $payment->queryRefundByRefundId($refundId);\n```\n\n## 下载对账单\n\n```php\n$bill = $payment->downloadBill('20140603')->getContents(); // type: ALL\n// or\n$bill = $payment->downloadBill('20140603', 'SUCCESS')->getContents(); // type: SUCCESS\n// bill 为 csv 格式的内容\n\n// 保存为文件\nfile_put_contents('YOUR/PATH/TO/bill-20140603.csv', $bill);\n```\n\n第二个参数为类型：\n\n- **ALL**：返回当日所有订单信息（默认值）\n- **SUCCESS**：返回当日成功支付的订单\n- **REFUND**：返回当日退款订单\n- **REVOKED**：已撤销的订单\n\n## 测速上报\n\n```php\n$payment->report($api, $timeConsuming, $resultCode, $returnCode);\n// or\n$payment->report($api, $timeConsuming, $resultCode, $returnCode, [\n        'err_code'     => 'xxxx',\n        'err_code_des' => '...',\n        'out_trade_no' => '...',\n        'user_ip'      => '...',\n    ]);\n```\n\n## 转换短链接\n\n```php\n$shortUrl = $payment->urlShorten('http://easywechat.com');\n```\n\n## 授权码查询 OPENID 接口\n\n```php\n$response = $payment->authCodeToOpenId($authCode);\n$response->openid;\n```\n\n## 生成支付 JS 配置\n\n有两种发起支付的方式：[WeixinJSBridge](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6), [JSSDK](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN)\n\n1. WeixinJSBridge:\n\n   ```php\n   $json = $payment->configForPayment($prepayId); // 返回 json 字符串，如果想返回数组，传第二个参数 false\n   ```\n\n   javascript:\n\n   ```js\n   ...\n   WeixinJSBridge.invoke(\n          'getBrandWCPayRequest', <?= $json ?>,\n          function(res){\n              if(res.err_msg == \"get_brand_wcpay_request:ok\" ) {\n                   // 使用以上方式判断前端返回,微信团队郑重提示：\n                   // res.err_msg将在用户支付成功后返回\n                   // ok，但并不保证它绝对可靠。\n              }\n          }\n      );\n   ...\n   ```\n\n2. JSSDK:\n\n   ```php\n   $config = $payment->configForJSSDKPayment($prepayId); // 返回数组\n   ```\n\n   javascript:\n\n   ```js\n   wx.chooseWXPay({\n       timestamp: <?= $config['timestamp'] ?>,\n       nonceStr: '<?= $config['nonceStr'] ?>',\n       package: '<?= $config['package'] ?>',\n       signType: '<?= $config['signType'] ?>',\n       paySign: '<?= $config['paySign'] ?>', // 支付签名\n       success: function (res) {\n           // 支付成功后的回调函数\n       }\n   });\n   ```\n\n## 生成共享收货地址 JS 配置\n\n1. 发起 OAuth 授权：\n\n```php\nuse EasyWeChat\\Support\\Url as UrlHelper;\n\n// 检查当前不是微信 oauth 的回调，则跳过去授权\n// 注意，授权回调地址为当前页\nif (empty($_GET['code'])) {\n    $currentUrl = UrlHelper::current(); // 获取当前页 URL\n    $response = $app->oauth->scopes(['snsapi_base'])->redirect($currentUrl);\n\n    return $response; // or echo $response;\n\n}\n// 授权回来\n$oauthUser = $app->oauth->user();\n$token = $oauthUser->getAccessToken();\n$configForPickAddress = $payment->configForShareAddress($token);\n\n// 拿着这个生成好的配置 $configForPickAddress 去订单页（或者直接显示订单页）写 js 调用了\n// ...\n```\n\n## 生成 APP 支付配置\n\n```php\n$config = $payment->configForAppPayment($prepayId);\n```\n\n`$config` 为数组格式，你可以用 API 返回给客户端\n\n# 二维码生成工具推荐\n\n你也许需要生成二维码，那么以下这些供参考：\n\n- https://github.com/endroid/QrCode\n- https://github.com/Bacon/BaconQrCode\n- https://github.com/SimpleSoftwareIO/simple-qrcode (Bacon/BaconQrCode 的 Laravel 版本)\n- https://github.com/aferrandini/PHPQRCode\n"
  },
  {
    "path": "docs/src/3.x/poi.md",
    "content": "# 门店\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$poi = $app->poi;\n```\n\n## 创建门店\n\n用 POI 接口新建门店时所使用的图片 url 必须为微信自己域名的 url,因此需要先用上传图片接 口上传图片并获取 url,再创建门店。上传的图片限制文件大小限制 1MB,支持 JPG 格式，图片接口请参考：[TODO](/)\n\n```php\n$poi->create($baseInfo);\n```\n\n- `$baseInfo` 为门店的基本信息数组\n\nexample:\n\n```php\n<?php\n\n$info = array(\n         \"sid\"             => \"33788392\",\n         \"business_name\"   => \"麦当劳\",\n         \"branch_name\"     => \"艺苑路店\",\n         \"province\"        => \"广东省\",\n         \"city\"            => \"广州市\",\n         \"district\"        => \"海珠区\",\n         \"address\"         => \"艺苑路 11 号\",\n         \"telephone\"       => \"020-12345678\",\n         \"categories\"      => array(\"美食,快餐小吃\"),\n         \"offset_type\"     => 1,\n         \"longitude\"       => 115.32375,\n         \"latitude\"        => 25.097486,\n         \"photo_list\"      => array(\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                             ),\n         \"recommend\"       => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         \"special\"         => \"免费 wifi,外卖服务\",\n         \"introduction\"    => \"麦当劳是全球大型跨国连锁餐厅,1940 年创立于美国,在世界上大约拥有 3  万间分店。主要售卖汉堡包,以及薯条、炸鸡、汽水、冰品、沙拉、水果等 快餐食品\",\n         \"open_time\"       => \"8:00-20:00\",\n         \"avg_price\"       => 35,\n    );\n\n$result = $poi->create($info); // true or exception\n```\n\n> 注意：新创建的门店在审核通过后,会以事件形式推送给商户填写的回调 URL\n\n## 获取指定门店信息\n\n```php\n$poi->get($poiId);\n```\n\n- `$poiId` 为门店 ID\n\nexample:\n\n```php\n$info = $poi->get(271262077);\nvar_dump($info->business_name); // 麦当劳\nvar_dump($info->introduction); // 麦当劳是全球大型跨国连锁餐厅...\nvar_dump($info->toArray());// array('business_name' => '麦当劳', 'branch_name' => '艺苑路店', ...);\n```\n\n## 获取门店列表\n\n```php\n$poi->lists($begin, $limit);// begin:0, limit:10\n```\n\n- `$begin` 就是查询起点，`MySQL` 里的 `offset`；\n- `$limit` 查询条数，同 `MySQL` 里的 `limit`；\n\n> 两参数均可选\n\nexample:\n\n```php\n$pois = $poi->lists(0, 2);// 取2条记录\n//\n//[\n//  {\n//    \"sid\": \"100\",\n//    \"poi_id\": \"271864249\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"艺苑路店\",\n//    \"address\": \"艺苑路 11 号\",\n//    \"available_state\": 3\n//  },\n//  {\n//    \"sid\": \"101\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"赤岗路店\",\n//    \"address\": \"赤岗路 102 号\",\n//    \"available_state\": 4\n//  }\n//]\n```\n\n## 修改门店信息\n\n商户可以通过该接口,修改门店的服务信息,包括:图片列表、营业时间、推荐、特色服务、简 介、人均价格、电话 7 个字段。目前基础字段包括(名称、坐标、地址等不可修改)。\n\n```php\n$poi->update($poiId, $data);\n```\n\n- `$poiId` 为门店 ID\n- `$data` 需要更新的部分数据，**若有填写内容则为覆盖更新,若无内容则视为不 修改,维持原有内容。photo_list 字段为全列表覆盖,若需要增加图片,需将之前图片同样放入 list 中,在其后增加新增图片。如:已有 A、B、C 三张图片,又要增加 D、E 两张图,则需要调 用该接口,photo_list 传入 A、B、C、D、E 五张图片的链接。**\n\nexample:\n\n```php\n$data = array(\n         \"telephone\" => \"020-12345678\",\n         \"recommend\" => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         //...\n        );\n\n$res = $poi->update(271262077, $data); //true or exception\n```\n\n## 删除门店\n\n```php\n$poi->delete($poiId);\n```\n\nexample:\n\n```php\n$poi->delete(271262077);// true or exception\n```\n\n## 错误码\n\n- `invalid categories` 分类不合法,必须严格按照附表的分类填写\n- `invalid photo url` 图片 url 不合法,必须使用接口 1 的图片上传 接口所获取的 url\n- `poi audit state must be approved` 门店状态必须未审核通过\n- `invalid poiid` poi_id 不正确\n- `invalid args` 参数不正确,请检查 json 字段\n- `system error` 系统错误,请稍后重试\n"
  },
  {
    "path": "docs/src/3.x/qrcode.md",
    "content": "# 二维码\n\n\n目前有2种类型的二维码：\n\n1. 临时二维码，是有过期时间的，最长可以设置为在二维码生成后的**30天**后过期，但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景\n2. 永久二维码，是无过期时间的，但数量较少（目前为最多10万个）。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$qrcode = $app->qrcode;\n```\n\n\n## API\n\n+ `Bag temporary($sceneId, $expireSeconds = null)` 创建临时二维码；\n+ `Bag forever($sceneValue)` 创建永久二维码\n+ `Bag card(array $card)` 创建卡券二维码\n+ `string url($ticket)` 获取二维码网址，用法： `<img src=\"<?php $qrcode->url($qrTicket); ?>\">`；\n\n### 创建临时二维码\n\n```php\n$result = $qrcode->temporary(56, 6 * 24 * 3600);\n\n$ticket = $result->ticket;// 或者 $result['ticket']\n$expireSeconds = $result->expire_seconds; // 有效秒数\n$url = $result->url; // 二维码图片解析后的地址，开发者可根据该地址自行生成需要的二维码图片\n```\n\n### 创建永久二维码\n\n```php\n$result = $qrcode->forever(56);// 或者 $qrcode->forever(\"foo\");\n\n$ticket = $result->ticket; // 或者 $result['ticket']\n$url = $result->url;\n```\n\n### 获取二维码网址\n\n```php\n$url = $qrcode->url($ticket);\n```\n\n### 创建卡券二维码\n\n```php\n$qrcode->card($card);\n```\n\n### 获取二维码内容\n\n```php\n$url = $qrcode->url($ticket);\n\n$content = file_get_contents($url); // 得到二进制图片内容\n\nfile_put_contents(__DIR__ . '/code.jpg', $content); // 写入文件\n```\n"
  },
  {
    "path": "docs/src/3.x/releases.md",
    "content": "# 升级日志\n\n\n## 3.0\n\n- 新的架构\n- 重写代码\n- 更低的耦合\n- 更规范的代码\n- 更友好的调试支持\n- 更完善的文档"
  },
  {
    "path": "docs/src/3.x/reply.md",
    "content": "# 自动回复\n\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$reply = $app->reply;\n```\n\n## 获取当前设置的回复规则\n\n```php\n$reply->current();\n```"
  },
  {
    "path": "docs/src/3.x/roadmap.md",
    "content": "# 路线图\n\n## 3.1\n\n- 微信小店\n- 新的卡券\n- 设备管理\n\n## 3.0\n\n- 全新的架构，更清晰的模块拆分\n- Debug 优化，便于快速定位问题\n- 统一返回微信 API 原值，以 Collection 类装载，便于便捷操作\n- 大量模块重写\n- 全新的支付模块\n- 多类型缓存系统\n- 新的命名空间：EasyWeChat\n- 专属网站与更清晰与文档：http://easywechat.com\n\n## 2.1\n\n- 新增支付与红包\n\n## 2.0\n\n- 仅问题修复，不再增加新功能；\n\n## 1.0\n\n- 不推荐使用。\n- 已停止维护。\n"
  },
  {
    "path": "docs/src/3.x/semantic.md",
    "content": "# 语义理解\n\n\n微信开放平台语义理解接口调用（http请求）简单方便，用户无需掌握语义理解及相关技术，只需根据自己的产品特点，选择相应的服务即可搭建一套智能语义服务。\n\n## 获取实例\n\n```php\n<?php\n\n// ... 前面部分省略\n\n$app = new Application($options);\n\n$semantic = $app->semantic;\n```\n\n## API\n\n+ `query($keyword, $categories, $other)` 语义理解:\n\n  + `$keyword` 为关键字\n  + `$categories` 需要使用的服务类型，数组或者多个用 “，” 隔开字符吕，不能为空;\n  + `$other` 为其它属性：\n    + `latitude`  `float`  纬度坐标，与经度同时传入；与城市二选一传入\n    + `longitude`  `float`  经度坐标，与纬度同时传入；与城市二选一传入\n    + `city`   `string`  城市名称，与经纬度二选一传入\n    + `region` `string`  区域名称，在城市存在的情况下可省；与经纬度二选一传入\n    + `uid`  `string` 用户唯一id（非开发者id），用户区分公众号下的不同用户（建议填入用户openid），如果为空，则无法使用上下文理解功能。appid和uid同时存在的情况下，才可以使用上下文理解功能。\n\n> 注：单类别意图比较明确，识别的覆盖率比较大，所以如果只要使用特定某个类别，建议将category只设置为该类别。\n\nexample:\n\n```php\n$result = $semantic->query('查一下明天从北京到上海的南航机票', \"flight,hotel\", array('city' => '北京', 'uid' => '123456'));\n// 查询参数：\n// {\n//    \"query\":\"查一下明天从北京到上海的南航机票\",\n//    \"city\":\"北京\",\n//    \"category\": \"flight,hotel\",\n//    \"appid\":\"wxaaaaaaaaaaaaaaaa\",\n//    \"uid\":\"123456\"\n// }\n```\n返回值示例：\n\n```json\n{\n    \"errcode\":0,\n    \"query\":\"查一下明天从北京到上海的南航机票\",\n    \"type\":\"flight\",\n    \"semantic\":{\n        \"details\":{\n            \"start_loc\":{\n                \"type\":\"LOC_CITY\",\n                \"city\":\"北京市\",\n                \"city_simple\":\"北京\",\n                \"loc_ori\":\"北京\"\n                },\n            \"end_loc\": {\n                \"type\":\"LOC_CITY\",\n                \"city\":\"上海市\",\n                \"city_simple\":\"上海\",\n                \"loc_ori\":\"上海\"\n              },\n            \"start_date\": {\n                \"type\":\"DT_ORI\",\n                \"date\":\"2014-03-05\",\n                \"date_ori\":\"明天\"\n              },\n           \"airline\":\"中国南方航空公司\"\n        },\n    \"intent\":\"SEARCH\"\n}\n```\n\n更多详细内容与协议说明，请查看 [微信官方文档](http://mp.weixin.qq.com/wiki/)"
  },
  {
    "path": "docs/src/3.x/server.md",
    "content": "# 服务端\n\n\n我们在入门小教程一节以服务端为例讲解了一个基本的消息的处理，这里就不再讲服务器验证的流程了，请直接参考前面的入门实例即可。\n\n服务端的作用呢，在整个微信开发中主要是负责 **[接收用户发送过来的消息](http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html)**，还有 **[用户触发的一系列事件](http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html)**。\n\n首先我们得厘清一下消息与事件的回复，当你收到用户消息后（消息由微信服务器推送到你的服务器），在你对消息进行一些处理后，不管是选择回复一个消息还是什么不都回给用户，你也应该给微信服务器一个 “答复”，如果是选择回复一条消息，就直接返回一个消息xml就好，如果选择不作任何回复，你也得回复一个空字符串或者字符串 `SUCCESS`（不然用户就会看到 `该公众号暂时无法提供服务`）。\n\n## 基本使用\n\n在 SDK 中呢，使用 `setMessageHandler(callable $callback)` 来设置消息处理函数：\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n// 从项目实例中得到服务端应用实例。\n$server = $app->server;\n\n$server->setMessageHandler(function ($message) {\n    // $message->FromUserName // 用户的 openid\n    // $message->MsgType // 消息类型：event, text....\n    return \"您好！欢迎关注我!\";\n});\n\n$response = $server->serve();\n\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n这里我们使用 `setMessageHandler` 传入了一个 **闭包（[Closure](http://php.net/manual/en/class.closure.php)）**，该闭包接收一个参数 `$message` 为消息对象（Collection），这里需要注意的时，与 2.0 不同，2.0 当中我们对消息与事件做了区分，还对消息进行了分类（按 MsgType）。在 3.0 后，**所有的消息包括事件都会使用 `setMessageHandler` 来处理**，也就是说你可能需要在里面进行一些判断，例如：\n\n```php\n$server->setMessageHandler(function ($message) {\n    switch ($message->MsgType) {\n        case 'event':\n            return '收到事件消息';\n            break;\n        case 'text':\n            return '收到文字消息';\n            break;\n        case 'image':\n            return '收到图片消息';\n            break;\n        case 'voice':\n            return '收到语音消息';\n            break;\n        case 'video':\n            return '收到视频消息';\n            break;\n        case 'location':\n            return '收到坐标消息';\n            break;\n        case 'link':\n            return '收到链接消息';\n            break;\n        // ... 其它消息\n        default:\n            return '收到其它消息';\n            break;\n    }\n\n    // ...\n});\n```\n\n当然，因为这里 `setMessageHandler` 接收一个 [`callable`](http://php.net/manual/zh/language.types.callable.php) 的参数，所以你不一定要传入一个 Closure 闭包，你可以选择传入一个函数名，一个 `[$class, $method]` 或者 `Foo::bar` 这样的类型。\n\n> :heart: 注意，默认没有验证是否为微信的请求，部署上线建议关掉 debug 模式。\n\n某些情况，我们需要直接使用 `$message` 参数，那么怎么在 `setMessageHandler` 闭包外调用呢？\n\n```php\n    $message = $server->getMessage();\n```\n> 注意：`$message` 是一个数组类型的数据，使用的时候这样使用：`$message['ToUserName']`\n\n## 请求消息的属性\n\n当你接收到用户发来的消息时，可能会提取消息中的相关属性，那么请参考：\n\n请求消息基本属性(以下所有消息都有的基本属性)：\n\n    $message->ToUserName    接收方帐号（该公众号 ID）\n    $message->FromUserName  发送方帐号（OpenID, 代表用户的唯一标识）\n    $message->CreateTime    消息创建时间（时间戳）\n    $message->MsgId         消息 ID（64位整型）\n\n### 文本：\n\n    $message->MsgType  text\n    $message->Content  文本消息内容\n\n### 图片：\n\n    $message->MsgType  image\n    $message->PicUrl   图片链接\n\n### 语音：\n\n    $message->MsgType        voice\n    $message->MediaId        语音消息媒体id，可以调用多媒体文件下载接口拉取数据。\n    $message->Format         语音格式，如 amr，speex 等\n    $message->Recognition * 开通语音识别后才有\n\n    > 请注意，开通语音识别后，用户每次发送语音给公众号时，微信会在推送的语音消息XML数据包中，增加一个 `Recongnition` 字段\n\n### 视频：\n\n    $message->MsgType       video\n    $message->MediaId       视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n    $message->ThumbMediaId  视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 小视频：\n\n    $message->MsgType     shortvideo\n    $message->MediaId     视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n    $message->ThumbMediaId    视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 事件：\n\n    $message->MsgType     event\n    $message->Event       事件类型 （如：subscribe(订阅)、unsubscribe(取消订阅) ...， CLICK 等）\n\n    # 扫描带参数二维码事件\n    $message->EventKey    事件KEY值，比如：qrscene_123123，qrscene_为前缀，后面为二维码的参数值\n    $message->Ticket      二维码的 ticket，可用来换取二维码图片\n\n    # 上报地理位置事件\n    $message->Latitude    23.137466   地理位置纬度\n    $message->Longitude   113.352425  地理位置经度\n    $message->Precision   119.385040  地理位置精度\n\n    # 自定义菜单事件\n    $message->EventKey    事件KEY值，与自定义菜单接口中KEY值对应，如：CUSTOM_KEY_001, www.qq.com\n\n### 地理位置：\n\n    $message->MsgType     location\n    $message->Location_X  地理位置纬度\n    $message->Location_Y  地理位置经度\n    $message->Scale       地图缩放大小\n    $message->Label       地理位置信息\n\n### 链接：\n\n    $message->MsgType      link\n    $message->Title        消息标题\n    $message->Description  消息描述\n    $message->Url          消息链接\n\n## 回复消息\n\n回复的消息可以为 `null`，此时 SDK 会返回给微信一个 \"SUCCESS\"，你也可以回复一个普通字符串，比如：`欢迎关注 overtrue.`，此时 SDK 会对它进行一个封装，产生一个 [`EasyWeChat\\Message\\Text`](https://github.com/EasyWeChat/message/blob/master/src/Text.php) 类型的消息并在最后的 `$server->serve();` 时生成对应的消息 XML 格式。\n\n如果你想返回一个自己手动拼的原生 XML 格式消息，请返回一个 [`EasyWeChat\\Message\\Raw`](https://github.com/EasyWeChat/message/blob/master/src/Raw.php) 实例即可。\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer.html)\n\n关于消息的使用，请参考 [`消息`](messages.html) 章节。\n"
  },
  {
    "path": "docs/src/3.x/shake-around.md",
    "content": "# 摇一摇周边\n\n\n摇一摇周边是微信在线下的全新功能, 为线下商户提供近距离连接用户的能力, 并支持线下商户向周边用户提供个性化营销、互动及信息推荐等服务。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$shakearound = $app->shakearound;\n\n```\n\n## API\n\n> 特别提醒：\n1、下述所有的接口调用的方法参数都要严格按照方法参数前的类型传入相应类型的实参，否则可能会得到非预期的结果。\n2、涉及需要传入设备id（$deviceIdentifier）的参数时，该参数是一个以 `device_id` 或包含 `uuid` `major` `minor` 为key的关联数组。\n3、涉及需要传入设备id列表（$deviceIdentifiers）的参数时，该参数是一个二维数组，第一层为索引类型，第二层为关联类型（$deviceIdentifier）。\n\n```php\n// 参数$deviceIdentifier的实参形式：\n['device_id' => 10097]\n// 或\n[\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12102,\n]\n// 参数$deviceIdentifiers的实参形式：\n[\n    ['device_id' => 10097],\n    ['device_id' => 10098],\n]\n// 或\n[\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12102,\n    ],\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12103,\n    ]\n]\n```\n\n### 开通摇一摇周边\n\n> 提示：\n若不是做 [公众号第三方平台](https://open.weixin.qq.com/cgi-bin/frame?t=home/wx_plugin_tmpl&lang=zh_CN) 开发，建议直接在微信管理后台申请开通摇一摇周边功能。\n\n#### 申请开通\n\n申请开通摇一摇周边功能。成功提交申请请求后，工作人员会在三个工作日内完成审核。若审核不通过，可以重新提交申请请求。若是审核中，请耐心等待工作人员审核，在审核中状态不能再提交申请请求。\n\n方法\n\n> $shakearound->register(string $name, string $tel, string $email, string $industryId, array $certUrls [, $reason = ''])\n\n参数\n\n> $name 联系人姓名，不超过20汉字或40个英文字母\n$tel 联系人电话\n$email 联系人邮箱\n$industryId 平台定义的行业代号，具体请查看链接 [行业代号](http://3gimg.qq.com/shake_nearby/Qualificationdocuments.html)\n$certUrls 相关资质文件的图片url，图片需先上传至微信侧服务器，用“素材管理-上传图片素材”接口上传图片，返回的图片URL再配置在此处；当不需要资质文件时，请传入空数组\n$reason 可选，申请理由，不超过250汉字或500个英文字母\n\n> 注意：\n1、相关资质文件的图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material.html) 接口混淆。\n2、行业代码请务必传入**字符串**类型的实参，否则以数字0开头的行业代码将会被当成八进制数处理（将转换为十进制数），这可能不是期望的。\n\n示例\n\n```php\n$result = $shakearound->register('zhang_san', '13512345678', 'weixin123@qq.com', '0118', [], 'test');\n\n/* 返回结果\n{\n   \"data\": {\n\n   },\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data) // 空数组\nvar_dump($result->errcode) // 0\nvar_dump($result->errmsg) // success.\n```\n\n#### 查询审核状态\n\n查询已经提交的开通摇一摇周边功能申请的审核状态。在申请提交后，工作人员会在三个工作日内完成审核。\n\n方法\n\n> $shakearound->getStatus()\n\n参数\n\n> 无\n\n示例\n\n```php\n$result = $shakearound->getStatus();\n\n/* 返回结果\n{\n    \"data\": {\n        \"apply_time\": 1432026025,\n        \"audit_comment\": \"test\",\n        \"audit_status\": 1,\n        \"audit_time\": 0\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['audit_comment']) // test\n```\n\n#### 获取摇一摇的设备及用户信息\n\n获取设备信息，包括UUID、major、minor，以及距离、openID等信息。\n\n方法\n\n> $shakearound->getShakeInfo(string $ticket [, int $needPoi = null])\n\n参数\n\n> $ticket 摇周边业务的ticket，可在摇到的URL中得到，ticket生效时间为30分钟，每一次摇都会重新生成新的ticket\n$needPoi 可选，是否需要返回门店poi_id，传1则返回，否则不返回\n\n示例\n\n```php\n$result = $shakearound->getShakeInfo('6ab3d8465166598a5f4e8c1b44f44645', 1);\n\n/* 返回结果\n{\n   \"data\": {\n       \"page_id \": 14211,\n       \"beacon_info\": {\n           \"distance\": 55.00620700469034,\n           \"major\": 10001,\n           \"minor\": 19007,\n           \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n       },\n       \"openid\": \"oVDmXjp7y8aG2AlBuRpMZTb1-cmA\",\n       \"poi_id\":1234\n   },\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['page_id']) // 14211\nvar_dump($result->data['beacon_info']['distance']) // 55.00620700469034\n```\n\n### 设备管理\n\n#### 申请设备ID\n\n申请配置设备所需的UUID、Major、Minor。申请成功后返回批次ID，可用返回的批次ID通过“查询设备ID申请状态”接口查询目前申请的审核状态。\n一个公众账号最多可申请100000个设备ID，如需申请的设备ID数超过最大限额，请邮件至zhoubian@tencent.com，邮件格式如下：\n\n> 标题：申请提升设备ID额度\n内容：\n1、公众账号名称及appid（wx开头的字符串，在mp平台可查看）\n2、用途\n3、预估需要多少设备ID\n\n方法\n\n> $shakearound->device()->apply(int $quantity, string $reason [, string $comment = '' [, int $poiId = null]])\n\n参数\n\n> $quantity 申请的设备ID的数量，单次新增设备超过500个，需走人工审核流程\n$reason 申请理由，不超过100个汉字或200个英文字母\n$comment 可选，备注，不超过15个汉字或30个英文字母\n$poiId 可选，设备关联的门店ID，关联门店后，在门店1KM的范围内有优先摇出信息的机会\n\n示例\n\n```php\n$result = $shakearound->device()->apply(3, '测试', '测试专用', 1234);\n\n/* 返回结果\n{\n    \"data\": {\n        \"apply_id\": 123,\n        \"audit_status\": 1,\n        \"audit_comment\": \"审核中\"\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['apply_id']) // 123\n```\n\n#### 查询设备ID申请审核状态\n\n查询设备ID申请的审核状态。若单次申请的设备ID数量小于等于500个，系统会进行快速审核；若单次申请的设备ID数量大于500个，则在三个工作日内完成审核。\n\n方法\n\n> $shakearound->device()->getStatus(int $applyId)\n\n参数\n\n> $applyId 批次ID，申请设备ID时所返回的批次ID\n\n示例\n\n```php\n$result = $shakearound->device()->getStatus(123);\n\n/* 返回结果\n{\n    \"data\": {\n        \"apply_time\": 1432026025,\n        \"audit_comment\": \"test\",\n        \"audit_status\": 1,\n        \"audit_time\": 0\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['audit_status']) // 1\n```\n\n#### 编辑设备信息\n\n> 仅能修改设备的备注信息。\n\n方法\n\n> $shakearound->device()->update(array $deviceIdentifier, string $comment)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$comment 设备的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->device()->update(['device_id' => 10011], 'test');\n// 或\n$result = $shakearound->device()->update(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                          'major' => 1002,\n                                          'minor' => 1223,\n], 'test');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 配置设备与门店/其他公众账号门店的关联关系\n\n关联本公众账号门店时，支持创建门店后直接关联在设备上，无需为审核通过状态，摇周边后台自动更新门店的最新信息和状态。\n关联其他公众账号门店时，支持设备关联其他公众账号的门店，门店需为审核通过状态。\n\n> 因为第三方门店不归属本公众账号，所以未保存到设备详情中，查询设备列表接口与获取摇周边的设备及用户信息接口不会返回第三方门店。\n\n方法\n\n> $shakearound->device()->bindLocation(array $deviceIdentifier, $poiId [, $type = 1 [, $poiAppid = null]])\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$poiId 设备关联的门店ID，关联门店后，在门店1KM的范围内有优先摇出信息的机会。当值为0时，将清除设备已关联的门店ID\n$type 可选，为1时，关联的门店和设备归属于同一公众账号；为2时，关联的门店为其他公众账号的门店\n$poiAppid 可选，当$type为1时该参数为必填\n\n示例\n\n```php\n// 关联本公众账号门店\n$result = $shakearound->device()->bindLocation(['device_id' => 10011], 1231);\n// 或\n$result = $shakearound->device()->bindLocation(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                'major' => 1002,\n                                                'minor' => 1223,\n], 1231);\n\n// 关联其他公众账号门店\n// wxappid为关联门店所归属的公众账号的APPID\n$result = $shakearound->device()->bindLocation(['device_id' => 10011], 1231, 2, 'wxappid');\n// 或\n$result = $shakearound->device()->bindLocation(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                'major' => 1002,\n                                                'minor' => 1223,\n], 1231, 2, 'wxappid');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 查询设备列表\n\n查询已有的设备ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。\n\n##### 根据设备id批量取回设备数据\n\n方法\n\n> $shakearound->device()->fetchByIds(array $deviceIdentifiers)\n\n参数\n\n> $deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->device()->fetchByIds([\n                                                ['device_id' => 10097],\n                                                ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->device()->fetchByIds([\n                                                ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                 'major' => 10001,\n                                                 'minor' => 12102,],\n                                                ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                 'major' => 10001,\n                                                 'minor' => 12103,]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"comment\": \"\",\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_id\": 0,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            },\n            {\n                \"comment\": \"\",\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_appid\":\"wxe3813f5d8c546fc7\"\n                \"poi_id\": 123,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            }\n        ],\n        \"total_count\": 151\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['devices'][0][device_id]) // 10097\nvar_dump($result->data['total_count']) // 151\n```\n\n##### 分页批量取回设备数据\n\n方法\n\n> $shakearound->device()->pagination(int $lastSeen, int $count)\n\n参数\n\n> $lastSeen 前一次查询列表末尾的设备编号device_id，第一次查询last_seen为0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device()->pagination(10097, 3);\n\n// 返回结果同上\n```\n\n##### 根据申请时的批次ID分页批量取回设备数据\n\n方法\n\n> $shakearound->device()->fetchByApplyId(int $applyId, int $lastSeen, int $count)\n\n参数\n\n> $applyId 批次ID，申请设备ID时所返回的批次ID\n$lastSeen 前一次查询列表末尾的设备编号device_id，第一次查询lastSeen为0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device()->fetchByApplyId(1231, 10097, 3);\n\n// 返回结果同上\n```\n\n### 页面管理\n\n#### 新增页面\n\n新增摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。其中，图片必须为用素材管理接口上传至微信侧服务器后返回的链接。\n\n> 注意：\n图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material.html) 接口混淆。\n\n方法\n\n> $shakearound->page()->add(string $title, string $description, strig $pageUrl, string $iconUrl [, string $comment = ''])\n\n参数\n\n> $title 在摇一摇页面展示的主标题，不超过6个汉字或12个英文字母\n$description 在摇一摇页面展示的副标题，不超过7个汉字或14个英文字母\n$pageUrl 点击进去的超链接\n$iconUrl 在摇一摇页面展示的图片。图片需先上传至微信侧服务器，用“素材管理-上传图片素材”接口上传图片，返回的图片URL再配置在此处\n$comment 可选，页面的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->page()->add('主标题', '副标题', 'https://zb.weixin.qq.com', 'http://3gimg.qq.com/shake_nearby/dy/icon', 'test');\n\n/* 返回结果\n{\n   \"data\": {\n       \"page_id\": 28840\n   }\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['page_id']) // 28840\n```\n\n#### 编辑页面信息\n\n编辑摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n方法\n\n> $shakearound->page()->update(int $pageId, string $title, string $description, string $pageUrl, string $iconUrl [, string $comment = ''])\n\n参数\n\n> $pageId 摇周边页面唯一ID\n$title 在摇一摇页面展示的主标题，不超过6个汉字或12个英文字母\n$description 在摇一摇页面展示的副标题，不超过7个汉字或14个英文字母\n$pageUrl 点击进去的超链接\n$iconUrl 在摇一摇页面展示的图片。图片需先上传至微信侧服务器，用“素材管理-上传图片素材”接口上传图片，返回的图片URL再配置在此处\n$comment 可选，页面的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->page()->add(28840, '主标题', '副标题', 'https://zb.weixin.qq.com', 'http://3gimg.qq.com/shake_nearby/dy/icon', 'test');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 查询页面列表\n\n查询已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n##### 根据页面id批量取回页面数据\n\n方法\n\n> $shakearound->page()->fetchByIds(array $pageIds)\n\n参数\n\n> $pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->page()->fetchByIds([28840, 28842]);\n\n/* 返回结果\n{\n   \"data\": {\n       \"pages\": [\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28840,\n               \"page_url\": \"http://xw.qq.com/testapi1\",\n               \"title\": \"测试1\"\n           },\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28842,\n               \"page_url\": \"http://xw.qq.com/testapi2\",\n               \"title\": \"测试2\"\n           }\n       ],\n       \"total_count\": 2\n   },\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['pages'][0]['title']) // 测试1\nvar_dump($result->data['total_count']) // 2\n```\n\n##### 分页批量取回页面数据\n\n方法\n\n> $shakearound->page()->pagination(int $begin, int $count)\n\n参数\n\n> $begin 页面列表的起始索引值\n$count 待查询的页面数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->page()->pagination(0,2);\n\n// 返回结果同上\n```\n\n#### 删除页面\n\n删除已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n> 注意：\n只有页面与设备没有关联关系时，才可被删除。\n\n方法\n\n> $shakearound->page()->delete(int $pageId)\n\n参数\n\n> $pageId 页面的id\n\n示例\n\n```php\n$result = $shakearound->page()->delete(34567);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n### 素材管理\n\n上传在摇一摇功能中需使用到的图片素材，素材保存在微信侧服务器上。图片格式限定为：jpg,jpeg,png,gif。\n若图片为在摇一摇页面展示的图片，则其素材为 `icon` 类型的图片，图片大小建议 `120px*120 px` ，限制不超过 `200 px *200 px` ，图片需为 `正方形` 。\n若图片为申请开通摇一摇周边功能需要上传的资质文件图片，则其素材为 `license` 类型的图片，图片的文件大小不超过 `2MB` ，尺寸不限，形状不限。\n\n方法\n\n> $shakearound->material()->uploadImage(string $path [, string $type = 'icon'])\n\n参数\n\n> $path 图片所在路径\n$type 可选，值为icon或license\n\n示例\n\n```php\n$result = $shakearound->material()->uploadImage(__DIR__ . '/stubs/image.jpg');\n\n/* 返回结果\n{\n    \"data\": {\n        \"pic_url\": http://shp.qpic.cn/wechat_shakearound_pic/0/1428377032e9dd2797018cad79186e03e8c5aec8dc/120\"\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['pic_url']) // http://shp.qpic.cn/wechat_shakearound_pic/0/1428377032e9dd2797018cad79186e03e8c5aec8dc/120\n```\n\n### 管理设备与页面的关系\n\n通过接口申请的设备ID，需先配置页面，若未配置页面，则摇不出页面信息。\n\n#### 配置设备与页面的关联关系\n\n配置完成后，在此设备的信号范围内，即可摇出关联的页面信息。\n若设备配置多个页面，则随机出现页面信息。一个设备最多可配置30个关联页面。\n\n> 注意：\n1、配置时传入该设备需要关联的页面的id列表，该设备原有的关联关系将被直接清除。\n2、页面的id列表允许为空（**传入空数组**），当页面的id列表为空时则会清除该设备的所有关联关系。\n\n方法\n\n> $shakearound->relation()->bindPage(array $deviceIdentifier, array $pageIds)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->relation()->bindPage(['device_id' => 10011], [12345, 23456, 334567]);\n// 或\n$result = $shakearound->relation()->bindPage(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                              'major' => 1002,\n                                              'minor' => 1223,\n], [12345, 23456, 334567]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 查询设备与页面的关联关系\n\n##### 查询指定设备所关联的页面\n\n根据设备ID或完整的UUID、Major、Minor查询该设备所关联的所有页面信息\n\n方法\n\n> $shakearound->relation()->getPageByDeviceId(array $deviceIdentifier [, boolean $raw = false])\n\n> 注意：\n该方法默认对返回的数据进行处理后返回一个包含页面id的索引数组。若要返回和 `getDeviceByPageId` 方法类似的数据，请传入 `true` 作为第二个参数。\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$raw 可选，当为true时，返回值和getDeviceByPageId方法类似，否则返回页面的id列表（索引数组，无关联时为空数组）\n\n示例\n\n```php\n$result = $shakearound->relation()->getPageByDeviceId(['device_id' => 10011]);\n// 或\n$result = $shakearound->relation()->getPageByDeviceId(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                       'major' => 1002,\n                                                       'minor' => 1223,\n]);\n\n// 返回结果\nvar_dump($result) // [50054,50055]\n```\n\n##### 查询指定页面所关联的设备\n\n指定页面ID分页查询该页面所关联的所有的设备信息\n\n方法\n\n> $shakearound->relation()->getDeviceByPageId(int $pageId, int $begin, int $count)\n\n参数\n\n> $pageId 指定的页面id\n$begin 关联关系列表的起始索引值\n$count 待查询的关联关系数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->relation()->getDeviceByPageId(50054, 0, 3);\n\n/* 返回结果\n{\n  \"data\": {\n      \"relations\": [\n          {\n              \"device_id\": 797994,\n              \"major\": 10001,\n              \"minor\": 10023,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          },\n          {\n              \"device_id\": 797995,\n              \"major\": 10001,\n              \"minor\": 10024,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          }\n      ],\n      \"total_count\": 2\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['relations'][0]['device_id']) // 797994\nvar_dump($result->data['total_count']) // 2\n```\n\n### 摇一摇数据统计\n\n> 此接口无法获取当天的数据，最早只能获取前一天的数据。\n由于系统在凌晨处理前一天的数据，太早调用此接口可能获取不到数据，建议在早上8：00之后调用此接口。\n\n#### 以设备为维度的数据统计\n\n查询单个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n> 注意：\n查询的最长时间跨度为30天。只能查询最近90天的数据。\n\n方法\n\n> $shakearound->stats()->deviceSummary(array $deviceIdentifier, int $beginDate, int $endDate)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats()->deviceSummary(['device_id' => 10011], 1425052800, 1425139200);\n// 或\n$result = $shakearound->stats()->deviceSummary(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                'major' => 1002,\n                                                'minor' => 1223,\n], 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data[0]['ftime']) // 1425052800\n```\n\n#### 批量查询设备统计数据\n\n查询指定时间商家帐号下的每个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n> 只能查询最近90天内的数据，且一次只能查询一天。\n\n> 注意：\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的设备，不在结果列表中返回。\n\n方法\n\n> $shakearound->stats()->batchDeviceSummary(int $timestamp, int $pageIndex)\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats()->batchDeviceSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            },\n            {\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            }\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['devices'][0]['device_id']) // 10097\nvar_dump($result->total_count) // 151\n```\n\n#### 以页面为维度的数据统计\n\n查询单个页面通过摇周边摇出来的人数、次数，点击摇周边页面的人数、次数\n\n> 注意：\n查询的最长时间跨度为30天。只能查询最近90天的数据。\n\n方法\n\n> $shakearound->stats()->pageSummary(int $pageId, int $beginDate, int $endDate);\n\n参数\n\n> $pageId 指定页面的页面ID\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats()->pageSummary(12345, 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data[1]['ftime']) // 1425139200\n```\n#### 批量查询页面统计数据\n\n查询指定时间商家帐号下的每个页面进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n> 注意：\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的页面，不在结果列表中返回。\n\n方法\n\n> $shakearound->stats()->batchPageSummary(int $timestamp, int $pageIndex);\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats()->batchPageSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"pages\": [\n            {\n                \"page_id\":1234\n                \"click_pv\": 1,\n                \"click_uv\": 3,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n            {\n                \"page_id\":5678\n                \"click_pv\": 1,\n                \"click_uv\": 2,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['pages'][0]['click_uv']) // 3\nvar_dump($result->total_count) // 151\n```\n\n### 设备分组管理\n\n调用H5页面获取设备信息 JS API接口，需要先把设备分组，微信客户端只会返回已在分组中的设备信息。\n\n#### 新增分组\n\n新建设备分组，每个帐号下最多只有1000个分组。\n\n方法\n\n> $shakearound->group()->add(string $name)\n\n参数\n\n> $name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group()->add('test');\n\n/* 返回结果\n{\n  \"data\": {\n      \"group_id\" : 123,\n      \"group_name\" : \"test\"\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['group_id']) // 123\nvar_dump($result->data['group_name']) // test\n```\n\n#### 编辑分组信息\n\n编辑设备分组信息，目前只能修改分组名。\n\n方法\n\n> $shakearound->group()->update(int $groupId, string $name)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group()->update(123, 'newName');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 删除分组\n\n删除设备分组，若分组中还存在设备，则不能删除成功。需把设备移除以后，才能删除。\n\n> 在执行删除前，最好先使用 `getDetails` 方法查询分组详情，若分组内有设备，先使用 `removeDevice` 方法移除。\n\n方法\n\n> $shakearound->group()->delete(int $groupId)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n\n示例\n\n```php\n$result = $shakearound->group()->delete(123);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 查询分组列表\n\n查询账号下所有的分组。\n\n方法\n\n> $shakearound->group()->lists(int $begin, int $count)\n\n参数\n\n> $begin 分组列表的起始索引值\n$count 待查询的分组数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group()->lists(0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"groups\":[\n            {\n                \"group_id\" : 123,\n                \"group_name\" : \"test1\"\n            },\n            {\n                \"group_id\" : 124,\n                \"group_name\" : \"test2\"\n            }\n        ],\n        \"total_count\": 100\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['groups'][1]['group_name']) // test2\nvar_dump($result->data['total_count']) // 100\n```\n\n#### 查询分组详情\n\n查询分组详情，包括分组名，分组id，分组里的设备列表。\n\n方法\n\n> $shakearound->group()->getDetails(int $groupId, int $begin, int $count)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$begin 分组里设备的起始索引值\n$count 待查询的分组里设备的数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group()->getDetails(123, 0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"group_id\" : 123,\n        \"group_name\" : \"test\",\n        \"total_count\": 100,\n        \"devices\" :[\n            {\n                \"device_id\" : 123456,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10001,\n                \"comment\" : \"test device1\",\n                \"poi_id\" : 12345,\n            },\n            {\n                \"device_id\" : 123457,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10002,\n                \"comment\" : \"test device2\",\n                \"poi_id\" : 12345,\n            }\n        ]\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->data['devices'][0]['comment']) // test device1\nvar_dump($result->data['total_count']) // 100\n```\n\n#### 添加设备到分组\n\n添加设备到分组，每个分组能够持有的设备上限为10000，并且每次添加操作的添加上限为1000。\n\n> 只有在摇周边申请的设备才能添加到分组。\n\n方法\n\n> $shakearound->group()->addDevice(int $groupId, array $deviceIdentifiers)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group()->addDevice(123, [\n                                                    ['device_id' => 10097],\n                                                    ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->group()->addDevice(123, [\n                                                    ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                    'major' => 10001,\n                                                    'minor' => 12102,],\n                                                    ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                    'major' => 10001,\n                                                    'minor' => 12103,]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n#### 从分组中移除设备\n\n从分组中移除设备，每次删除操作的上限为1000。\n\n方法\n\n> $shakearound->group()->removeDevice(int $groupId, array $deviceIdentifiers)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group()->removeDevice(123, [\n                                                    ['device_id' => 10097],\n                                                    ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->group()->removeDevice(123, [\n                                                    ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                    'major' => 10001,\n                                                    'minor' => 12102,],\n                                                    ['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                    'major' => 10001,\n                                                    'minor' => 12103,]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n### 摇一摇红包\n\n微信官方目前暂停了摇红包接口，该接口可能会有所调整，故而暂时不提供该接口的封装。\n\n> 官方公告详情请至： [关于摇红包接口暂停的公告](https://zb.weixin.qq.com/nearby/announce.xhtml?announceId=10047)\n\n### 摇一摇事件通知\n\n用户进入摇一摇界面，在“周边”页卡下摇一摇时，微信会把这个事件推送到开发者填写的URL（登录公众平台进入开发者中心设置）。推送内容包含摇一摇时“周边”页卡展示出来的页面所对应的设备信息，以及附近最多五个属于该公众账号的设备的信息。当摇出列表时，此事件不推送。\n\n> 摇一摇事件的事件类型：ShakearoundUserShake\n关于事件的处理请移步： [事件](events.html)\n\n### 摇一摇周边错误码\n\n> 摇周边错误码请移步： [错误码](https://mp.weixin.qq.com/wiki?action=doc&id=mp1443448163&t=0.17525333335674986)\n\n有关摇一摇周边接口信息的更多细节请参考微信官方文档相应条目： [微信官方文档](http://mp.weixin.qq.com/wiki/)\n"
  },
  {
    "path": "docs/src/3.x/short-url.md",
    "content": "# 短网址服务\n\n\n主要使用场景： 开发者用于生成二维码的原链接（商品、支付二维码等）太长导致扫码速度和成功率下降，将原长链接通过此接口转成短链接再生成二维码将大大提升扫码速度和成功率。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$url = $app->url;\n```\n\n## API\n\n+ `shorten($url)` 长链接转短链接\n\nexample:\n\n```php\n$shortUrl = $url->shorten('http://overtrue.me/open-source');\n//\n```\n\n微信官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/3.x/sidebar.js",
    "content": "exports = module.exports = [\n  {\n    text: '开始使用',\n    items: [\n      { text: '概述', link: '/3.x/overview.html' },\n      { text: '安装', link: '/3.x/installation.html' },\n      { text: '小教程', link: '/3.x/tutorial.html' },\n      { text: '配置', link: '/3.x/configuration.html' },\n      { text: '在框架中使用', link: '/3.x/integration.html' },\n      { text: '常见问题汇总', link: '/3.x/troubleshooting.html' }\n    ]\n  },\n  {\n    text: '基本使用',\n    items: [\n      { text: '服务端', link: '/3.x/server.html' },\n      { text: '消息', link: '/3.x/messages.html' },\n      { text: '多客服消息转发', link: '/3.x/message-transfer.html' },\n      { text: '事件', link: '/3.x/events.html' },\n      { text: '群发消息', link: '/3.x/broadcast.html' },\n      { text: '模板消息', link: '/3.x/notice.html' },\n      { text: '用户', link: '/3.x/user.html' },\n      { text: '用户标签', link: '/3.x/user-tag.html' },\n      { text: '用户组', link: '/3.x/user-group.html' },\n      { text: '网页授权', link: '/3.x/oauth.html' },\n      { text: '素材管理', link: '/3.x/material.html' },\n      { text: '菜单', link: '/3.x/menu.html' },\n      { text: 'JSSDK', link: '/3.x/js.html' },\n      { text: '支付', link: '/3.x/payment.html' },\n      { text: '企业支付', link: '/3.x/merchant_payment.html' },\n      { text: '红包', link: '/3.x/lucky-money.html' },\n      { text: '卡券', link: '/3.x/card.html' },\n      { text: '小店', link: '/3.x/store.html' },\n      { text: '门店', link: '/3.x/poi.html' },\n      { text: '客服', link: '/3.x/staff.html' },\n      { text: '数据统计与分析', link: '/3.x/anaylsis.html' },\n      { text: '二维码', link: '/3.x/qrcode.html' },\n      { text: '短网址', link: '/3.x/short-url.html' },\n      { text: '小程序', link: '/3.x/mini_program.html' },\n      { text: '语义理解', link: '/3.x/semantic.html' },\n      { text: '自动回复', link: '/3.x/reply.html' },\n      { text: '开放平台', link: '/3.x/open_platform.html' }\n    ]\n  },\n  {\n    text: '自定义',\n    items: [\n      { text: 'Access Token', link: '/3.x/access_token.html' },\n      { text: '缓存', link: '/3.x/cache.html' }\n    ]\n  },\n  {\n    text: '其他',\n    items: [\n      { text: '问题解答', link: '/3.x/troubleshooting.html' },\n      { text: '贡献', link: '/3.x/contributing.html' },\n      { text: '更新日志', link: '/3.x/releases.html' },\n      { text: '路线图', link: '/3.x/roadmap.html' }\n    ]\n  }\n]\n"
  },
  {
    "path": "docs/src/3.x/staff.md",
    "content": "# 客服\n\n\n> 2016.06.28 已经更新为新版多客服 API\n> 请更新到 3.1 版本： composer require \"overtrue/wechat:~3.1\"\n\n微信的客服才能发送消息或者群发消息，而且还有时效限制，真恶心的说。。。\n\n## 客服管理\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$staff = $app->staff; // 客服管理\n```\n\n## API\n\n### 获取所有客服账号列表\n\n```php\n$staff->lists();\n```\n\n### 获取所有在线的客服账号列表\n\n```php\n$staff->onlines();\n```\n\n### 添加客服帐号\n\n```php\n$staff->create('foo@test', '客服1');\n```\n\n### 修改客服帐号\n\n```php\n$staff->update('foo@test', '客服1');\n```\n\n### 删除客服帐号\n\n```php\n$staff->delete('foo@test');\n```\n\n### 设置客服帐号的头像\n\n```php\n$staff->avatar('foo@test', $avatarPath); // $avatarPath 为本地图片路径，非 URL\n```\n\n### 获取客服聊天记录 `NEW`\n\n```php\n$staff->records($startTime, $endTime, $pageIndex, $pageSize);\n\n// example: $records = $staff->records('2015-06-07', '2015-06-21', 1, 20);\n```\n\n### 主动发送消息给用户\n\n```php\n$staff->message($message)->to($openId)->send();\n```\n\n> `$message` 为消息对象，请参考：[消息](messages.html)\n\n### 指定客服发送消息\n\n```php\n$staff->message($message)->by('account@test')->to($openId)->send();\n```\n> `$message` 为消息对象，请参考：[消息](messages.html)\n\n## 客服会话控制\n\n> 客服会话为新版 API 功能\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n// ...\n$app = new Application($options);\n\n$session = $app->staff_session; // 客服会话管理\n```\n\n## 创建会话\n\n```php\n$session->create('test1@test', 'OPENID');\n```\n\n### 关闭会话\n\n```php\n$session->close('test1@test', 'OPENID');\n```\n\n### 获取客户会话状态\n\n```php\n$session->get('OPENID');\n```\n\n### 获取客服会话列表\n\n```php\n$session->lists('test1@test');\n```\n\n### 获取未接入会话列表\n\n```php\n$session->waiters();\n```\n\n\n关于更多客服接口信息请参考微信官方文档：http://mp.weixin.qq.com/wiki\n"
  },
  {
    "path": "docs/src/3.x/store.md",
    "content": "# 门店\n\n\nTODO"
  },
  {
    "path": "docs/src/3.x/troubleshooting.md",
    "content": "# 疑难解答\n\n\n在微信公众平台开发的道路上，遍布着各种大大小小的坑，有的人掉坑里，几经折腾又爬出来了，然后拍拍屁股走人。然而坑还在那里，还会继续有后来人掉进去……\n\n这，是我们不愿看到的。\n\n所以在这里，我们将陆续将微信开发中可能遇到的各种疑难问题进行汇总，并给出对应的解决办法。一般情况下，这些问题都可以对号入座，轻松地解决。但也不排除特殊情况，这时候你遇到的问题与文中某一个症状一致，但文中所给的解决方案并不凑效，这种情况下就需要发挥你自己的智慧，去……折腾了……\n\n我们期待这一版块为各位的开发带来便利，同时也希望各位本着开源、分享的精神对其进行补充和完善，将各种坑一一填小、填平，让微信开发变得不那么痛苦，甚至，变成一件快乐的事……\n\n# 一些服务器基本设施问题：\n\n- 时区不对， 使用命令 `date` 可以在服务器上查看当前时间，如果发现时区不对则需要修改时区：[Setting The Correct Timezone In CentOS And Ubuntu Servers With NTP](https://www.liberiangeek.net/2013/02/setting-the-correct-timezone-in-centos-and-ubuntu-servers-with-ntp/)\n    - ...\n\n\n## curl: (60) SSL certificate problem: unable to get local issuer certificate\n\n这是 SSL 证书问题所致，在使用 SDK 调用微信支付等相关的操作时可能会遇到报 “SSL certificate problem: unable to get local issuer certificate” 的错误。\n\n微信公众平台提供的文档中建议对部分较敏感的操作接口使用 https 协议进行访问，例如微信支付和红包等接口中涉及到操作商户资金的一些操作。\nwechat SDK 遵循了官方建议，所以在调用这些接口时，除了按照官方文档设置操作证书文件外，还需要保证服务器正确安装了 CA 证书。\n\n1. 下载 CA 证书\n\n  你可以从 http://curl.haxx.se/ca/cacert.pem 下载 或者 使用[微信官方提供的证书](https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=4_3)中的 CA 证书 `rootca.pem` 也是同样的效果。\n\n2. 在 `php.ini` 中配置 CA 证书\n\n  只需要将上面下载好的 CA 证书放置到您的服务器上某个位置，然后修改 `php.ini` 的 `curl.cainfo` 为该路径（**绝对路径！**），重启 `php-fpm` 服务即可。\n\n  ```\n  curl.cainfo = /path/to/downloaded/cacert.pem\n  ```\n  > 注意证书文件**路径为绝对路径**！以自己实际情况为准。\n\n  其它修改 HTTP 类源文件的方式是不允许的。\n\n## cURL error 56: SSLRead() return error -9806\n\n目前在 OSX 下，发现使用 HomeBrew 装的 PHP 7.0 有这个问题，解决方案是重新 brew 安装 PHP：\n\n```shell\n$ brew install homebrew/php/php70 --with-homebrew-openssl --with-homebrew-curl --without-snmp -vvv\n```\n\n验证：\n\n```shell\n$ php -i | grep 'OpenSSL support'\n\nOpenSSL support => enabled\nOpenSSL support => enabled\n```\n\n\n## 支付失败！当前页面的 URL 未注册\n\n这是由于微信支付授权目录未正确配置引起的。此时开发者应该登录微信公众平台，进入**【微信支付】->【开发设置】**进行设置。\n\n1. 公众号可添加3个支付授权目录，满足不同应用使用同一个公众号进行支付的业务需求。\n\n2. 正确的**【支付授权目录】**应以 `http://` 或 `https://` 开头，并以正斜杠 `/` 结尾，授权目录所包含的域名**必须经过 ICP 备案**。\n\n3. 支付授权目录需**细化至二级或三级目录**。\n\n4. 所有**实际调起微信支付请求的页面都必须要所配置的支付授权目录之下**。\n\n5. 在开发过程中，也可以使用测试授权目录进行开发测试，此时还**应该将参与测试的个人微信号添加到测试白名单中**，否则将出现对应的错误提示……\n\n> 配置前请先理解**页面**、**目录**、**URL **以及**域名**等几个基本概念，并对自己所使用的框架的路由机制有一个大致了解。这样你才会知道自己正在配置的参数是个啥玩意儿，有什么卵用…… :smile:\n\n\n## redirect_url 参数错误\n\n这是由于程序使用了**网页授权**而公众号没有正确配置**【网页授权域名】**所致。此时你需要登录[微信公众平台](https://mp.weixin.qq.com/)，在【开发】->【接口权限】页面找到**网页授权获取用户基本信息**进行配置并保存。\n\n1. 网页授权域名应该为通过 ICP 备案的有效域名，否则保存时无法通过安全监测。\n\n2. 网页授权域名即程序完成授权获得授权  code 后跳转到的页面的域名，一般情况下为你的业务域名。\n\n3. 网页授权域名配置成功后会立即生效。\n\n4. 公众号的网页授权域名只可配置一个，请合理规划你的业务，否则你会发现……授权域名不够用哈。\n\n\n## [JSAPI] config: invalid url domain\n在使用 JS-SDK 进行开发时，每个页面都需要调用 wx.config() 方法配置 JSPAI 参数。如果没有正确配置 **JSAPI 安全域名**并且开启了调试模式，此时就报此错误。遇到这个问题时，开发者需要登录微信公众平台，进入【公众号设置】->【功能设置】页面，将项目所使用的域名添加至 **【JSAPI 安全域名】**列表中。\n\n1. 一个公众号同时最多可绑定**三个**安全域名，并且这些域名必须为通过 **ICP 备案**的**一级或一级以上**的有效域名。\n\n2. JSAPI 安全域名每个月**限修改三次**，修改任何一个都算，所以，请谨慎操作。\n\n3. 如果需要使用 JSAPI 调起支付功能，则支付目录必须也在所配置的**安全域名之下**，并且需要将支付目录添加至**支付授权目录**。\n\n## token验证失败、向公众号发送消息无任何反应\n\n相信对接公众号一般是微信开发者进行开发过程中最先进行的工作，而在这看似简单的配置操作中，也可能会掉坑里。\n最常见的两种情况就如下：\n\n1. 确认你 “**启用**” 了开发模式， token 验证通过不代表启用，保存后也不代表启用。看到红色 “**停用**” 才真正的是启用了。\n\n2. 配置好URL(服务器地址)以及Token(令牌)后，点击保存时提示**token验证失败**，出现这种情况的原因有多种，其中之一便是网络不稳定，所以**可尝试多次保存**，若始终无法通过再排查其它可能因素。\n\n3. 配置保存成功之后，向公众号发送消息无任何反应，自己的消息处理程序也没有被调用的记录（无对应日志）。这种情况下如果你尝试**反复停用和启用服务器配置**，可能突然间惊奇地了现，问题莫名其妙的解决了。\n\n4. 使用在线调试工具的消息接口，http://mp.weixin.qq.com/debug/， 只要返回绿色的“**请求成功**”，就代表你的代码没有问题，请**重复上面第3项**再测试。\n\n5. **如果你在用什么本地开发工具，或者什么 ngrok 代理到本机这样的开发方式，那么失败就很正常了，微信服务器到你机器的网络延迟太大（还是用服务器开发吧）。**\n\n> 请开发者理解服务器 TOKEN 验证原理（官方文档有说明）并谨记服务器验证时使用 GET 方式访问，而公众平台向你的服务器发送消息/数据则使用 POST 方式，所以服务器验证成功之后，在某些启用了 CSRF 验证的框架里，接收消息时可能还会遇到 CSRF 相关的问题，请根据自己项目实际情况进行排查。\n> 另外有的朋友的 Laravel 里使用了 laravel-debugbar，这个组件的原理是在页面输出时在后面添加 HTML 来实现的，所以它会改变我们返回给微信的内容，此时要么卸载，要么禁用掉它。\n\n\n## Maximum function nesting level of '100' reached, aborting!\n\n在使用了 Xdebug 的环境下可能出现这个问题。这是由于 Xdebug 限制函数嵌套的最大层级数（默认为100），当嵌套次数达到该值便会触发 Xdebug 跳出嵌套并报此错误。\n\n为避免这个问题，**可以将 Xdebug 的 max_nesting_level 参数适当设置大一些**，通常设置为200就可以了（当然可根据自己实际情况设置为更大的值）。\n\n如下，修改 php.ini 配置文件后，重启 Apache 或 php-fpm 服务即可。\n\n```\nxdebug.max_nesting_level=200\n```\n"
  },
  {
    "path": "docs/src/3.x/tutorial.md",
    "content": "# 快速开始\n\n在我们已经安装完成后，即可很快的开始使用它了，当然你还是有必要明白 PHP 基本知识，如命名空间等，我这里就不赘述了。\n\n我们以完成服务器端验证与接收响应用户发送的消息为例来演示,首先你有必要了解一下微信交互的运行流程：\n\n```\n                                     +-----------------+                       +---------------+\n    +----------+                     |                 |    POST/GET/PUT       |               |\n    |          | ------------------> |                 | ------------------->  |               |\n    |   user   |                     |  wechat server  |                       |  your server  |\n    |          | < - - - - - - - - - |                 |                       |               |\n    +----------+                     |                 | <- - - - - - - - - -  |               |\n                                     +-----------------+                       +---------------+\n```\n\n那么我们要做的就是图中 **微信服务器把用户消息转到我们的自有服务器（虚线返回部分）** 后的处理过程。\n\n## 服务端验证\n\n在微信接入开始有一个 “服务器验证” 的过程，这一步呢，其实就是微信服务器向我们服务器发起一个请求（上图实线部分），传了一个名称为 `echostr` 的字符串过来，我们只需要原样返回就好了。\n\n你也知道，微信后台只能填写一个服务器地址，所以 **服务器验证** 与 **消息的接收与回复**，都在这一个链接内完成交互。\n\n考虑到这些，我已经把验证这一步给封装到 SDK 里了，你可以完全忽略这一步。\n\n下面我们来配置一个基本的服务端，这里假设我们自己的服务器域名叫 `easywechat.com`，我们在服务器上准备这么一个文件`server.php`:\n\n// server.php\n\n```php\n<?php\n\ninclude __DIR__ . '/vendor/autoload.php'; // 引入 composer 入口文件\n\nuse EasyWeChat\\Foundation\\Application;\n\n$options = [\n    'debug'  => true,\n    'app_id' => 'your-app-id',\n    'secret' => 'you-secret',\n    'token'  => 'easywechat',\n\n\n    // 'aes_key' => null, // 可选\n\n    'log' => [\n        'level' => 'debug',\n        'file'  => '/tmp/easywechat.log', // XXX: 绝对路径！！！！\n    ],\n\n    //...\n];\n\n$app = new Application($options);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); // Laravel 里请使用：return $response;\n\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n\n一个服务端带验证功能的代码已经完成，当然没有对消息做处理，别着急，后面我们再讲。\n\n我们先来分析上面的代码：\n\n```php\n<?php\n\n// 这行代码是引入 `composer` 的入口文件，这样我们的类才能正常加载。\ninclude __DIR__ . '/vendor/autoload.php';\n\n// 引入我们的主项目的入口类。\nuse EasyWeChat\\Foundation\\Application;\n\n// 一些配置\n$options = [...];\n\n// 使用配置来初始化一个项目。\n$app = new Application($options);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n最后这一行我有必要详细讲一下：\n\n> 1.  我们的 `$app->server->serve()` 就是执行服务端业务了，那么它的返回值呢，是一个 `Symfony\\Component\\HttpFoundation\\Response` 实例。\n> 2.  我这里是直接调用了它的 `send()` 方法，它就是直接输出了，我们在一些框架就不能直接输出了，那你就直接拿到 Response 实例后做相应的操作即可，比如 Laravel 里你就可以直接 `return $app->server->serve();`\n\nOK, 有了上面的代码，那么请你按 **[微信官方的接入指引](http://mp.weixin.qq.com/wiki/17/2d4265491f12608cd170a95559800f2d.html)** 操作，并相应修改上面的 `$options` 的配置。\n\n> URL 就是我们的 `http://easywechat.com/server.php`，这里我是举例哦，你可不要填写我的域名。\n\n这样，点击提交验证就 OK 了。\n\n> :heart: 请一定要将微信后台的开发者模式 “**启用**” ！！！！！！看到红色 “**停用**” 才真正的是启用了。\n\n## 接收 & 回复用户消息\n\n那服务端验证通过了，我们就来试一下接收消息吧。\n\n> 在刚刚上面代码最后一行 `$app->server->serve()->send();` 前面，我们调用 `$app->server` 的 `setMessageHandler()` 方法来注册一个消息处理函数，这里用到了 **[PHP 闭包](http://php.net/manual/zh/functions.anonymous.php)** 的知识，如果你不熟悉赶紧补课去。\n\n```php\n// ...\n\n$server->setMessageHandler(function ($message) {\n    return \"您好！欢迎关注我!\";\n});\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); // Laravel 里请使用：return $response;\n\n```\n\n> 注意：send() 方法里已经包含 echo 了，请不要再加 echo 在前面。\n\n好吧，打开你的微信客户端，向你的公众号发送任意一条消息，你应该会收到回复：`您好！欢迎关注我!`。\n\n> 没有收到回复？看到了“你的公众号暂时无法提供服务” ？， 好，那检查一下你的日志吧，日志在哪儿？我们的配置里写了日志路径了(`'/tmp/easywechat.log'`)。 没有这个文件？看看权限哦。\n\n一个基本的服务端验证就完成了。\n\n## 总结\n\n1. 所有的服务都通过主入口 `EasyWeChat\\Foundation\\Application` 类来获取：\n\n```php\n$app = new Application($options);\n\n// services...\n$server = $app->server;\n$user   = $app->user;\n$oauth  = $app->oauth;\n\n// ... js/menu/staff/material/qrcode/notice/stats...\n\n```\n\n2. 所有的 API 返回值均为 [`EasyWeChat\\Support\\Collection`](https://github.com/EasyWeChat/support/blob/master/src/Collection.php) 类，这个类是个什么东西呢？\n\n它实现了一些 **[PHP 预定义接口](http://php.net/manual/zh/reserved.interfaces.php)**，比如：[`ArrayAccess`](http://php.net/manual/zh/class.arrayaccess.php)、[`Serializable`](http://php.net/manual/zh/class.serializable.php) 等。\n\n有啥好处呢？它让我们操作起返回值来更方便，比如：\n\n```php\n$userService = $app->user; // 用户API\n\n$user = $userService->get($openId);\n\n// $user 便是一个 EasyWeChat\\Support\\Collection 实例\n$user['nickname'];\n$user->nickname;\n$user->get('nickname');\n\n//...\n```\n\n还有这些方便的操作：检查是否存在某个属性 `$user->has('email')`、元素个数 `$user->count()`，还有返回数组 `$user->toArray()` ，生成 JSON `$user->toJSON()` 等。\n\n## 最后\n\n希望你在使用本 SDK 的时候能忘记微信官方给你的痛苦，同时如果你发现 SDK 的不足，欢迎提交 PR 或者给我[提建议 & 报告问题](https://github.com/overtrue/wechat/issues)。\n\n祝你生活愉快！\n"
  },
  {
    "path": "docs/src/3.x/user-group.md",
    "content": "# 用户组\n\n\n用户组的使用就非常简单了，基本的增删改查。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$group = $app->user_group; // $user['user_group']\n```\n\n## API\n\n### 获取所有分组\n\n```php\n$group->lists();\n```\n\nexample:\n\n```php\n$groups = $group->lists();\n\n// {\n//     \"groups\": [\n//         {\n//             \"id\": 0,\n//             \"name\": \"未分组\",\n//             \"count\": 72596\n//         },\n//         {\n//             \"id\": 1,\n//             \"name\": \"黑名单\",\n//             \"count\": 36\n//         },\n//         ...\n//     ]\n// }\n\nvar_dump($groups->groups[0]['name']) // “未分组”\n```\n\n### 创建分组\n\n```php\n$group->create($name);\n```\n\nexample:\n\n```php\n$group->create($name);\n```\n\n### 修改分组信息\n\n```php\n$group->update($groupId, $name);\n```\n\nexample:\n\n```php\n$group->update($groupId, \"新的组名\");\n```\n\n### 删除分组\n\n```php\n$group->delete($groupId);\n```\n\nexample:\n\n```php\n$group->delete($groupId);\n```\n\n### 移动单个用户到指定分组\n\n```php\n$group->moveUser($openId, $groupId);\n```\n\nexample:\n\n```php\n$group->moveUser($openId, $groupId);\n```\n\n### 批量移动用户到指定分组\n\n```php\n$group->moveUsers(array $openIds, $groupId);\n```\n\nexample:\n\n```php\n$openIds = [$openId1, $openId2, $openId3 ...];\n$group->moveUsers($openIds, $groupId);\n```\n\n关于用户管理请参考微信官方文档：http://mp.weixin.qq.com/wiki/ `用户管理` 章节。\n"
  },
  {
    "path": "docs/src/3.x/user-tag.md",
    "content": "# 用户标签\n\n\n用户标签的使用就非常简单了，基本的增删改查。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$tag = $app->user_tag; // $user['user_tag']\n```\n\n## API\n\n### 获取所有标签\n\n```php\n$tag->lists();\n```\n\nexample:\n\n```php\n$tags = $tag->lists();\n\n// {\n//     \"tags\": [\n//         {\n//             \"id\": 0,\n//             \"name\": \"标签1\",\n//             \"count\": 72596\n//         },\n//         {\n//             \"id\": 1,\n//             \"name\": \"标签2\",\n//             \"count\": 36\n//         },\n//         ...\n//     ]\n// }\n\nvar_dump($tags->tags[0]['name']) // “标签1”\n```\n\n### 创建标签\n\n```php\n$tag->create($name);\n```\n\nexample:\n\n```php\n$tag->create('测试标签');\n```\n\n### 修改标签信息\n\n```php\n$tag->update($tagId, $name);\n```\n\nexample:\n\n```php\n$tag->update(12, \"新的名称\");\n```\n\n### 删除标签\n\n```php\n$tag->delete($tagId);\n```\n\nexample:\n\n```php\n$tag->delete($tagId);\n```\n\n### 获取指定 openid 用户身上的标签\n\n```php\n$userTags = $tag->userTags($openId);\n//\n// {\n//     \"tagid_list\":[\"标签1\",\"标签2\"]\n// }\n```\n\n### 获取标签下粉丝列表\n\n```php\n$tag->usersOfTag($tagId, $nextOpenId = '');\n// $nextOpenId：第一个拉取的OPENID，不填默认从头开始拉取\n\n// {\n//   \"count\":2,//这次获取的粉丝数量\n//   \"data\":{//粉丝列表\n//      \"openid\":[\n//          \"ocYxcuAEy30bX0NXmGn4ypqx3tI0\",\n//          \"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"\n//      ]\n//   },\n//   \"next_openid\":\"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"//拉取列表最后一个用户的openid\n// }\n```\n\n### 批量为用户打标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$tag->batchTagUsers($openIds, $tagId);\n```\n\n\n### 批量为用户取消标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$tag->batchUntagUsers($openIds, $tagId);\n```\n\n关于用户管理请参考微信官方文档：http://mp.weixin.qq.com/wiki/ `用户管理` 章节。\n"
  },
  {
    "path": "docs/src/3.x/user.md",
    "content": "# 用户\n\n\n用户信息的获取是微信开发中比较常用的一个功能了，以下所有的用户信息的获取与更新，都是**基于微信的 `openid` 的，并且是已关注当前账号的**，其它情况可能无法正常使用。\n\n## 获取实例\n\n```php\n<?php\nuse EasyWeChat\\Foundation\\Application;\n\n// ...\n\n$app = new Application($options);\n\n$userService = $app->user;\n```\n\n## API 列表\n\n### 获取用户信息\n\n```php\n$userService->get($openId);\n$userService->batchGet($openIds);\n```\n\n获取单个：\n\n```php\n$user = $userService->get($openId);\n\necho $user->nickname; // or $user['nickname']\n```\n\n获取多个：\n\n```php\n$users = $userService->batchGet([$openId1, $openId2, ...]);\n```\n\n### 获取用户列表\n\n```php\n$userService->lists($nextOpenId = null);  // $nextOpenId 可选\n```\n\n example:\n\n ```php\n $users = $userService->lists();\n\n // result\n {\n  \"total\": 2,\n  \"count\": 2,\n  \"data\": {\n    \"openid\": [\n      \"\",\n      \"OPENID1\",\n      \"OPENID2\"\n    ]\n  },\n  \"next_openid\": \"NEXT_OPENID\"\n}\n\n$users->total; // 2\n ```\n\n### 修改用户备注\n\n```php\n$userService->remark($openId, $remark); // 成功返回boolean\n```\n\nexample:\n\n```php\n$userService->remark($openId, \"僵尸粉\");\n```\n\n### 获取用户所属用户组ID\n\n```php\n$userService->group($openId);\n```\n\nexample:\n\n```php\n$userGroupId = $userService->group($openId);\n```\n\n## 其它\n\n- [用户标签](user-tag.html)\n- [用户分组](user-group.html)\n\n关于用户管理请参考微信官方文档：http://mp.weixin.qq.com/wiki/ `用户管理` 章节。"
  },
  {
    "path": "docs/src/4.x/basic-services/content_security.md",
    "content": "# 内容安全接口\n\n## 文本安全内容检测\n\n用于校验一段文本是否含有违法内容。\n\n### 频率限制\n\n单个appid调用上限为2000次/分钟，1,000,000次/天\n\n### 调用示例\n\n```php\n// 传入要检测的文本内容，长度不超过500K字节\n$content = '你好';\n\n$result = $app->content_security->checkText($content);\n\n// 正常返回 0\n{\n    \"errcode\": \"0\",\n    \"errmsg\": \"ok\"\n}\n\n//当 $content 内含有敏感信息，则返回 87014\n{\n    \"errcode\": 87014,\n    \"errmsg\": \"risky content\"\n}\n```\n\n## 图片安全内容检测\n\n用于校验一张图片是否含有敏感信息。如涉黄、涉及敏感人脸（通常是政治人物）。\n\n### 频率限制\n\n单个appid调用上限为1000次/分钟，100,000次/天\n\n### 调用示例\n\n```php\n// 所传参数为要检测的图片文件的绝对路径，图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334，同时文件大小以不超过 300K 为宜，否则可能报错\n$result = $app->content_security->checkImage('/path/to/the/image');\n\n// 正常返回 0\n{\n    \"errcode\": \"0\",\n    \"errmsg\": \"ok\"\n}\n\n// 当图片文件内含有敏感内容，则返回 87014\n{\n    \"errcode\": 87014,\n    \"errmsg\": \"risky content\"\n}\n```\n\n## 重要说明\n\n目前上述两个接口仅支持在小程序中使用，示例中的 `$app` 表示小程序实例，即:\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 下面为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::miniProgram($config);\n```\n"
  },
  {
    "path": "docs/src/4.x/basic-services/jssdk.md",
    "content": "# JSSDK\n\n微信 JSSDK 官方文档：https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115\n\n## API\n\n#### 获取JSSDK的配置数组\n\n```php\n$app->jssdk->buildConfig(array $APIs, $debug = false, $beta = false, $json = true);\n```\n\n默认返回 JSON 字符串，当 `$json` 为 `false` 时返回数组，你可以直接使用到网页中。\n\n#### 设置当前URL\n\n```php\n$app->jssdk->setUrl($url)\n```\n如果不想用默认读取的URL，可以使用此方法手动设置，通常不需要。\n\n\n#### 示例\n\n我们可以生成js配置文件：\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\n    wx.config(<?php echo $app->jssdk->buildConfig(array('updateAppMessageShareData', 'updateTimelineShareData'), true) ?>);\n</script>\n```\n结果如下：\n\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\nwx.config({\n    debug: true, // 请在上线前删除它\n    appId: 'wx3cf0f39249eb0e60',\n    timestamp: 1430009304,\n    nonceStr: 'qey94m021ik',\n    signature: '4F76593A4245644FAE4E1BC940F6422A0C3EC03E',\n    jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']\n});\n</script>\n```\n\n"
  },
  {
    "path": "docs/src/4.x/basic-services/media.md",
    "content": "# 临时素材\n\n上传的临时多媒体文件有格式和大小限制，如下：\n\n> - 图片（image）: 2M，支持 `JPG` 格式\n> - 语音（voice）：2M，播放长度不超过 `60s`，支持 `AMR\\MP3` 格式\n> - 视频（video）：10MB，支持 `MP4` 格式\n> - 缩略图（thumb）：64KB，支持 `JPG` 格式\n\n## 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$app->media->uploadImage($path);\n```\n\n## 上传声音\n\n```php\n$app->media->uploadVoice($path);\n```\n\n## 上传视频\n\n```php\n$app->media->uploadVideo($path, $title, $description);\n```\n\n## 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$app->media->uploadThumb($path);\n```\n\n## 上传群发视频\n\n上传视频获取 `media_id` 用以创建群发消息用。\n\n```php\n$app->media->uploadVideoForBroadcasting($path, $title, $description);\n\n//{\n//  \"media_id\": \"rF4UdIMfYK3efUfyoddYRMU50zMiRmmt_l0kszupYh_SzrcW5Gaheq05p_lHuOTQ\",\n//  \"title\": \"TITLE\",\n//  \"description\": \"Description\"\n//}\n```\n\n## 创建群发消息\n\n不要与上面 **上传群发视频** 搞混了，上面一个是上传视频得到 `media_id`，这个是使用该 `media_id` 加标题描述 **创建一条消息素材** 用来发送给用户。详情参见：[消息群发](../official-account/broadcasting.md)\n\n```php\n$app->media->createVideoForBroadcasting($mediaId, $title, $description);\n\n//{\n//  \"type\":\"video\",\n//  \"media_id\":\"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc\",\n//  \"created_at\":1398848981\n//}\n```\n\n## 获取临时素材内容\n\n比如图片、语音等二进制流内容，响应为 `EasyWeChat\\Kernel\\Http\\StreamResponse` 实例。\n\n```php\n$stream = $app->media->get($mediaId);\n\nif ($stream instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n  // 以内容 md5 为文件名存到本地\n  $stream->save('保存目录');\n\n  // 自定义文件名，不需要带后缀\n  $stream->saveAs('保存目录', '文件名');\n}\n```\n\n## 获取 JSSDK 上传的高清语音\n\n```php\n$stream = $app->media->getJssdkMedia($mediaId);\n$stream->saveAs('保存目录', 'custom-name.speex');\n```\n"
  },
  {
    "path": "docs/src/4.x/basic-services/qrcode.md",
    "content": "# 二维码\n\n目前有 2 种类型的二维码：\n\n1. 临时二维码，是有过期时间的，最长可以设置为在二维码生成后的 **30天**后过期，但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景\n2. 永久二维码，是无过期时间的，但数量较少（目前为最多10万个）。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。\n\n## 创建临时二维码\n\n```php\n$result = $app->qrcode->temporary('foo', 6 * 24 * 3600);\n\n// Array\n// (\n//     [ticket] => gQFD8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTmFjVTRWU3ViUE8xR1N4ajFwMWsAAgS2uItZAwQA6QcA\n//     [expire_seconds] => 518400\n//     [url] => http://weixin.qq.com/q/02NacU4VSubPO1GSxj1p1k\n// )\n```\n\n## 创建永久二维码\n\n```php\n$result = $app->qrcode->forever(56);// 或者 $app->qrcode->forever(\"foo\");\n// Array\n// (\n//     [ticket] => gQFD8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTmFjVTRWU3ViUE8xR1N4ajFwMWsAAgS2uItZAwQA6QcA\n//     [url] => http://weixin.qq.com/q/02NacU4VSubPO1GSxj1p1k\n// )\n```\n\n## 获取二维码网址\n\n```php\n$url = $app->qrcode->url($ticket);\n// https://api.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET\n```\n\n## 获取二维码内容\n\n```php\n$url = $app->qrcode->url($ticket);\n\n$content = file_get_contents($url); // 得到二进制图片内容\n\nfile_put_contents(__DIR__ . '/code.jpg', $content); // 写入文件\n```\n"
  },
  {
    "path": "docs/src/4.x/basic-services/url.md",
    "content": "# 短网址服务\n\n主要使用场景： 开发者用于生成二维码的原链接（商品、支付二维码等）太长导致扫码速度和成功率下降，将原长链接通过此接口转成短链接再生成二维码将大大提升扫码速度和成功率。\n\n## 长链接转短链接\n\n```php\n$shortUrl = $app->url->shorten('https://easywechat.com');\n//\n(\n    [errcode] => 0\n    [errmsg] => ok\n    [short_url] => https://w.url.cn/s/Aq7jWrd\n)\n```"
  },
  {
    "path": "docs/src/4.x/client.md",
    "content": "# API 调用\n\n该方法将 API 交由开发者自行调用，微信有部分新的接口4.x并未全部兼容支持,可以使用该方案去自行封装接口：\n\n例如URL Link接口\n\n```php\n\n$response = $app->httpPostJson('wxa/generate_urllink',[\n    'path' => 'pages/index/index',\n    'is_expire' => true,\n    'expire_type' => 1,\n    'expire_interval' => 1\n]);\n```\n\n## 语法说明\n\n```php\nhttpGet(string $uri, array $query = [])\nhttpPostJson(string $uri, array $data = [], array $query = [])\n```\n\n\n\n### GET\n\n```php\n$response = $app->httpGet('/cgi-bin/user/list', [\n    'next_openid' => 'OPENID1',\n]);\n```\n\n### POST\n\n```php\n$response = $app->httpPostJson('/cgi-bin/user/info/updateremark', [\n    \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n    \"remark\" => \"pangzi\"\n]);\n```\n\n\n\n"
  },
  {
    "path": "docs/src/4.x/contributing.md",
    "content": "# 贡献代码\n\n## 开发\n\n我们欢迎广大开发者贡献大家的智慧，让我们共同让它变得更完美.\n\n### 开始之前\n\n请严格遵循以下代码标准:\n\n> - [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md).\n> - 使用 4 个空格作为缩进。\n\n### 流程\n\n1. Fork [overtrue/wechat](https://github.com/overtrue/wechat) 到本地.\n2. 创建新的分支：\n\n```shell\n    $ git checkout -b new_feature\n```\n\n3. 编写代码。\n4. Push 到你的分支:\n\n```shell\n    $ git push origin new_feature\n```\n\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n> 注意：注释请使用英文\n\n## 更新文档\n\n我们的文档也是开源的，源代码在 [w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/master/docs)。\n\n### 流程\n\n1. Fork [w7corp/EasyWeChat](https://github.com/w7corp/easywechat)\n2. Clone 到你的电脑：\n\n```shell\n    $ git clone https://github.com/<username>/site.git\n    $ cd docs\n```\n\n3. 创建新的分支，编辑文档\n4. Push 到你的分支。\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n## 报告 Bug\n\n当你在使用过程中遇到问题，请查阅 [疑难解答](troubleshooting.html) 或者在这里提问 [GitHub](https://github.com/overtrue/wechat/issues). 如果还是不能解决你的问题，请到 GitHub 联系我们。\n\n[overtrue/wechat]: https://github.com/overtrue/wechat\n"
  },
  {
    "path": "docs/src/4.x/customize/access_token.md",
    "content": "# Access Token\n\n\n我们一个 SDK 应用在初始化以后，你可以在任何时机从应用中拿到该配置下的 Access Token 实例：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    //...\n];\n\n$app = Factory::officialAccount($config);\n\n// 获取 access token 实例\n$accessToken = $app->access_token;\n$token = $accessToken->getToken(); // token 数组  token['access_token'] 字符串\n$token = $accessToken->getToken(true); // 强制重新从微信服务器获取 token.\n```\n\n## 修改 `$app` 的 Access Token\n\n```php\n$app['access_token']->setToken($newAccessToken, 7200);\n```\n\n例如：\n\n```php\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675');\n// 或者指定过期时间\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675', 3600);  // 单位：秒\n```\n"
  },
  {
    "path": "docs/src/4.x/customize/cache.md",
    "content": "# 缓存\n\n\n本项目使用 [symfony/cache](https://github.com/symfony/cache) 来完成缓存工作，它支持基本目前所有的缓存引擎。\n\n在我们的 SDK 中的所有缓存默认使用文件缓存，缓存路径取决于 PHP 的临时目录，如果你需要自定义缓存，那么你需要做如下的事情：\n\n你可以参考[symfony/cache官方文档](https://symfony.com/doc/current/components/cache.html) 来替换掉应用中默认的缓存配置：\n\n\n## 以 redis 为例\n\n\n### Symfony 4.3 + \n\n> 请先安装 redis 拓展：`composer require predis/predis`\n\n```php\n\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\n\n// 创建 redis 实例\n$client = new \\Predis\\Client('tcp://10.0.0.1:6379');\n\n// 创建缓存实例\n$cache = new RedisAdapter($client);\n\n// 替换应用中的缓存\n$app->rebind('cache', $cache);\n```\n\n### Symfony 3.4 + \n\n> 请先安装 redis 拓展：https://github.com/phpredis/phpredis\n\n```php\n\nuse Symfony\\Component\\Cache\\Simple\\RedisCache;\n\n// 创建 redis 实例\n$redis = new Redis();\n$redis->connect('redis_host', 6379);\n\n// 创建缓存实例\n$cache = new RedisCache($redis);\n\n// 替换应用中的缓存\n$app->rebind('cache', $cache);\n```\n\n\n### Laravel 中使用\n\n在 Laravel 中框架使用 [predis/predis](https://github.com/nrk/predis)：\n\n### Symfony 4.3 + \n\n> 请先安装 redis 拓展：`composer require predis/predis`\n\n```php\n\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\n\n// 创建缓存实例\n$cache = new RedisAdapter(app('redis')->connection()->client());\n$app->rebind('cache', $cache);\n\n```\n\n### Symfony 3.4 + \n\n```php\n\nuse Symfony\\Component\\Cache\\Simple\\RedisCache;\n\n$predis = app('redis')->connection()->client(); // connection($name), $name 默认为 `default`\n$cache = new RedisCache($predis);\n\n$app->rebind('cache', $cache);\n```\n\n> 上面提到的 `app('redis')->connection($name)`, 这里的 `$name` 是 laravel 项目中配置文件 `database.php` 中 `redis` 配置名 `default`：https://github.com/laravel/laravel/blob/master/config/database.php#L118\n> 如果你使用的其它连接，对应传名称就好了。\n\n## 使用自定义的缓存方式\n\n如果你发现 symfony 提供的十几种缓存方式都满足不了你的需求的话，那么你可以自己建立一个类来完成缓存操作，前提这个类得实现接口：[PSR-16](http://www.php-fig.org/psr/psr-16/)\n\n该接口有以下方法需要实现：\n\n```php\n   public function get($key, $default = null);\n   public function set($key, $value, $ttl = null);\n   public function delete($key);\n   public function clear();\n   public function getMultiple($keys, $default = null);\n   public function setMultiple($values, $ttl = null);\n   public function deleteMultiple($keys);\n   public function has($key);\n```\n\n下面为一个示例：\n\n```php\n<?php\n\nuse Psr\\SimpleCache\\CacheInterface;\n\nclass MyCustomCache implements CacheInterface\n{\n    public function get($key, $default = null)\n    {\n        // your code\n    }\n\n    public function set($key, $value, $ttl = null)\n    {\n        // your code\n    }\n\n    public function delete($key)\n    {\n        // your code\n    }\n\n    public function clear()\n    {\n        // your code\n    }\n\n    public function getMultiple($keys, $default = null)\n    {\n        // your code\n    }\n\n    public function setMultiple($values, $ttl = null)\n    {\n        // your code\n    }\n\n    public function deleteMultiple($keys)\n    {\n        // your code\n    }\n\n    public function has($key)\n    {\n        // your code\n    }\n}\n```\n\n然后实例化你的缓存类并在 EasyWeChat 里使用它：\n\n```php\n$app->rebind('cache', new MyCustomCache());\n```\n\nOK，这样就完成了自定义缓存的操作。\n"
  },
  {
    "path": "docs/src/4.x/customize/replace-service.md",
    "content": "# 自定义服务模块\n\n由于使用了容器模式来组织各模块的实例，意味着你可以比较容易的替换掉已经有的服务，以公众号服务为例：\n\n```php\n\n<...>\n\n$app = Factory::officialAccount($config);\n\n$app->rebind('request', new MyCustomRequest(...)); \n```\n\n这里的 `request` 为 SDK 内部服务名称。\n"
  },
  {
    "path": "docs/src/4.x/index.md",
    "content": "> 👋🏼 您当前浏览的文档为 4.x，其它版本的文档请参考：[6.x](/6.x/)、[5.x](/5.x/)、[3.x](/3.x/)\n\n# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n## 环境要求\n\n> - PHP >= 7.0\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\nLaravel 5 拓展包: [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n# 参与贡献\n\n1. fork 当前库到你的名下\n2. 选择你想要修改的语言版本，`zh-CN` 或者 `en`\n3. 在你的本地修改完成审阅过后提交到你的仓库\n4. 提交 PR 并描述你的修改，等待合并\n\n# License\n\nMIT\n"
  },
  {
    "path": "docs/src/4.x/installation.md",
    "content": "# 安装\n\n\n## 环境要求\n\n> - PHP >= 7.0\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\nLaravel 5 拓展包: [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n## 安装\n\n使用 [composer](http://getcomposer.org/):\n\n```shell\n$ composer require overtrue/wechat:~4.0 -vvv\n```"
  },
  {
    "path": "docs/src/4.x/integration.md",
    "content": "# 在框架中使用\n\nEasyWeChat 是一个通用的 Composer 包，所以不需要对框架单独做修改，只要支持 Composer 就能直接使用，当然了，为了更方便的使用，我们收集了以下框架单独提供的拓展包：\n\n## Laravel\n\n>  - [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n\n## Symfony\n\n>  - [lilocon/WechatBundle](https://github.com/lilocon/WechatBundle)\n\n## Yii\n\n> - [jianyan74/yii2-easy-wechat](https://github.com/jianyan74/yii2-easy-wechat) 适用于 EasyWeChat 4.x \n> - [max-wen/yii2-easy-wechat](https://github.com/max-wen/yii2-easy-wechat) 适用于 EasyWeChat 3.x \n\n## ThinkPHP\n\n>  - [naixiaoxin/think-wechat](https://github.com/qiqizjl/think-wechat) 适用于 EasyWeChat 4.x\n>  - [zyan/think-wechat](https://github.com/aa24615/think-wechat) 适用于 EasyWeChat 4.x/5.x\n\n\n## CI\n\nTODO\n\n## Phalcon\n\nTODO\n\n... more\n\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/certficates.md",
    "content": "# 获取平台证书\n调用获取平台证书接口之前，请前往微信支付商户平台升级API证书，升级后才可成功调用本接口。\n\n```php\n// 获取到证书后可以做缓存处理，无需每次重新获取\n$response = $app->certficates->get(bool $returnRaw = false);\n\n// 获取到平台证书后，可以直接使用 setCertificate 方法把证书配置追加到配置项里面去\n$app->setCertificate(string $certificate, string $serialNo);\n```\n> $returnRaw 不填默认为false时，请确保你的PHP已安装了sodium扩展    \n> 返回值：固定array格式的解密后的证书信息\n\n> $returnRaw 传入true时     \n> 返回值：Response对象`$response->getBody()->getContents();`获取到微信返回xml原始数据\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/index.md",
    "content": "# 小微商户\n\n你在阅读本文之前确认你已经仔细阅读了：[微信小微商户专属接口文档](https://pay.weixin.qq.com/wiki/doc/api/xiaowei.php?chapter=19_2)。\n\nPS: ⚠️ 因系统升级，腾讯暂时关闭了小微商户接口，恢复时间未定。调用提交申请接口会提示「PARAM_ERROR」，详细说明可参见[微信开放平台相关帖子](https://developers.weixin.qq.com/community/develop/doc/0000a0ffc9ce28bd4bc9999ba5b800)\n\n\n## 配置\n\n小微商户整体接口调用方式相对于其他微信接口略有不同，配置时请勿填错，相关配置如下：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'mch_id'           => 'your-mch-id', // 服务商的商户号\n    'key'              => 'key-for-signature', // API 密钥\n    'apiv3_key'        => 'APIv3-key-for-signature', // APIv3 密钥\n    // API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'        => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'         => 'path/to/your/key', // XXX: 绝对路径！！！！\n    // 以下两项配置在获取证书接口时可为空，在调用入驻接口前请先调用获取证书接口获取以下两项配置,如果获取过证书可以直接在这里配置，也可参照本文档获取平台证书章节中示例\n    // 'serial_no'     => '获取证书接口获取到的平台证书序列号',\n    // 'certificate'   => '获取证书接口获取到的证书内容'\n    \n    // 以下为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array'\n    'appid'            => 'wx931386123456789e' // 服务商的公众账号 ID\n];\n\n$app = Factory::microMerchant($config);\n\n```\n\n\n`$app` 在所有相关小微商户的文档都是指 `Factory::microMerchant` 得到的实例，就不在每个页面单独写了。\n\n## 使用时值得注意的地方：\n1、小微商户所有接口中以下列出参数 `version`, `mch_id`, `nonce_str`, `sign`, `sign_type`, `cert_sn` 可不用传入。\n\n2、所有敏感信息无需手动加密，sdk会在调用接口前自动完成加密\n\n3、在调用入驻等需要敏感信息加密的接口前请先调用获取证书接口然后把配置填入配置项\n\n4、入驻成功获取到子商户号后需帮助子商户调用配置修改等接口可以先调用以下方法，方便调用修改等接口时无需再次传入子商户号\n```php\n// $subMchId 为子商户号\n// $appid    服务商的公众账号 ID\n$app->setSubMchId(string $subMchId, string $appId = '');\n```\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/material.md",
    "content": "# 商户信息修改\n## 修改结算银行卡\n\n```php\n$response = $app->material->setSettlementCard([\n    // 'sub_mch_id' => '1230000109',\n    'account_number' => '银行卡号',\n    'bank_name' => '开户银行全称（含支行）',\n    'account_bank' => '开户银行',\n    'bank_address_code' => '开户银行省市编码',\n]);\n```\n## 修改联系信息\n\n```php\n$response = $app->material->updateContact([\n    // 'sub_mch_id' => '1230000109',\n    'mobile_phone' => '手机号',\n    'email' => '邮箱',\n    'merchant_name' => '商户简称',\n]);\n```\n\n> 以上接口调用过 `setSubMchId` 方法则无需传入 `sub_mch_id` 参数"
  },
  {
    "path": "docs/src/4.x/micro-merchant/media.md",
    "content": "# 图片上传\n上传证件照片。支持 jpeg、jpg、bmp、png 格式，图片大小不超过2M。\n\n```php\n// $path string 图片路径\n$response = $app->media->upload($path);\n```\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/merchant-config.md",
    "content": "# 小微商户配置\n\n## 关注功能配置\n\n```php\n$response = $app->merchantConfig->setFollowConfig(string $subAppId, string $subscribeAppId, string $receiptAppId = '', string $subMchId = '');\n```\n> 注意：`subscribe_appid`，`receipt_appid` 两个参数二选一，两个都填的话SDK默认选第一个，具体请参考小微商户专属文档\n\n## 开发配置新增支付目录\n\n```php\n$response = $app->merchantConfig->addPath(string $jsapiPath, string $appId = '', string $subMchId = '');\n```\n\n## 新增对应APPID关联\n\n```php\n$response = $app->merchantConfig->bindAppId(string $subAppId, string $appId = '', string $subMchId = '');\n```\n\n## 开发配置查询\n\n```php\n$response = $app->merchantConfig->getConfig(string $subMchId = '', string $appId = '');\n```\n\n> 以上接口调用过 `setSubMchId` 方法并且两个参数都传入过 则无需传入 `sub_mch_id` 和 `appid` 参数"
  },
  {
    "path": "docs/src/4.x/micro-merchant/submit-application.md",
    "content": "# 商户入驻\n## 申请入驻\n\n使用申请入驻接口提交你的小微商户资料。\n\n```php\n$result = $app->submitApplication([\n    'business_code' => '123456', // 业务申请编号\n    'id_card_copy'  => 'media_id', // 身份证人像面照片\n    // ...\n    // 参数太多就不一一列出，自行根据 (小微商户专属文档 -> 申请入驻api) 填写\n]);\n```\n\n## 查询申请状态\n\n使用申请入驻接口提交小微商户资料后，一般5分钟左右可以通过该查询接口查询具体的申请结果。\n\n```php\n$applymentId = '商户申请单号(applyment_id 申请入驻接口返回)';\n$businessCode = '业务申请编号(business_code)';\n$app->getStatus(string $applymentId, string $businessCode = '');\n```\n> 商户申请单号和业务申请编号填写一个就行了，当 `applyment_id` 已填写时，`business_code` 字段无效。\n\n当查询申请状态为待签约，接口会一并返回签约二维码，服务商需引导商户使用本人微信扫码完成协议签署。\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/upgrade.md",
    "content": "# 商户升级\n## 提交升级申请单\n\n使用“提交升级申请单”接口为小微商户发起升级流程，根据商户实际情况可升级为个体户、企业、党政、机关及事业单位、其他组织。。\n\n```php\n$result = $app->upgrade([\n    'organization_type' => '2', // 主体类型\n    'business_license_copy'  => 'media_id', // 营业执照扫描件\n    // ...\n    // 参数太多就不一一列出，自行根据 (小微商户专属文档 -> 提交升级申请单API) 填写\n]);\n```\n\n## 查询升级申请单状态\n使用“提交升级申请单”接口后，可不定期调用此接口查询申请单状态（建议提交申请后1分钟查询），直至申请单为“完成”状态。\n\n1)若申请状态为待账户验证，请按接口中的指引完成账户验证\n\n2)若申请状态为审核中，微信支付会在2个工作日内完成资料审核\n\n3)若申请状态为待签约，接口会返回签约二维码\n\n```php\n$app->getUpgradeStatus(string $subMchId = '');\n```\n> 调用该接口前调用过 `setSubMchId` 方法则无需传入 `$subMchId` 参数\n"
  },
  {
    "path": "docs/src/4.x/micro-merchant/withdraw.md",
    "content": "# 提现相关\n\n## 查询提现状态\n\n```php\n$response = $app->withdraw->queryWithdrawalStatus($date, $subMchId = '');\n```\n## 重新发起提现\n\n```php\n$response = $app->withdraw->requestWithdraw($date, $subMchId = '');\n```\n\n> 以上接口调用过 `setSubMchId` 方法则无需传入 `sub_mch_id` 参数"
  },
  {
    "path": "docs/src/4.x/mini-program/app_code.md",
    "content": "# 小程序码\n\n## 获取小程序码\n\n### 接口A: 适用于需要的码数量较少的业务场景\n\nAPI:\n\n```\n$app->app_code->get(string $path, array $optional = []);\n```\n\n其中 `$optional` 为以下可选参数：\n\n>  - **width** Int - 默认 430 二维码的宽度\n>  - **auto_color**  默认 false  自动配置线条颜色，如果颜色依然是黑色，则说明不建议配置主色调\n>  - **line_color**  数组，`auto_color` 为 `false` 时生效，使用 rgb 设置颜色 例如 ，示例：`[\"r\" => 0,\"g\" => 0,\"b\" => 0]`。\n\n示例代码：\n\n```php\n$response = $app->app_code->get('path/to/page');\n// 或者\n$response = $app->app_code->get('path/to/page', [\n    'width' => 600,\n    //...\n]);\n\n// 或者指定颜色\n$response = $app->app_code->get('path/to/page', [\n    'width' => 600,\n    'line_color' => [\n        'r' => 105,\n        'g' => 166,\n        'b' => 134,\n    ],\n]);\n\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败时为数组或者你指定的 API 返回格式\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n### 接口B：适用于需要的码数量极多，或仅临时使用的业务场景\n\nAPI:\n\n```\n$app->app_code->getUnlimit(string $scene, array $optional = []);\n```\n\n> 其中 $scene 必填，$optinal 与 get 方法一致，多一个 page 参数。\n\n示例代码：\n\n```php\n$response = $app->app_code->getUnlimit('scene-value', [\n    'page'  => 'path/to/page',\n    'width' => 600,\n]);\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败为数组或你指定的 API 返回类型\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n## 获取小程序二维码\n\nAPI:\n\n```\n$app->app_code->getQrCode(string $path, int $width = null);\n```\n\n> 其中 $path 必填，其余参数可留空。\n\n示例代码：\n\n```php\n$response = $app->app_code->getQrCode('/path/to/page');\n\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败为数组或你指定的 API 返回类型\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n##\n"
  },
  {
    "path": "docs/src/4.x/mini-program/auth.md",
    "content": "# 微信登录\n\n## 根据 jsCode 获取用户 session 信息\n\nAPI:\n\n```php\n$app->auth->session(string $code);\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/customer_service.md",
    "content": "# 客服消息\n\n## 获取实例\n\n```php\n$service = $app->customer_service;\n```\n\n> 使用方法详看公众号-客服消息章节。\n\n"
  },
  {
    "path": "docs/src/4.x/mini-program/data_cube.md",
    "content": "# 数据统计与分析\n\n获取小程序概况趋势：\n\n```php\n$app->data_cube->summaryTrend('20170313', '20170313')\n```\n开始日期与结束日期的格式为 yyyymmdd。\n\n## API\n\n>  - `summaryTrend(string $from, string $to);` 概况趋势\n>  - `dailyVisitTrend(string $from, string $to);` 访问日趋势\n>  - `weeklyVisitTrend(string $from, string $to);` 访问周趋势\n>  - `monthlyVisitTrend(string $from, string $to);` 访问月趋势\n>  - `visitDistribution(string $from, string $to);` 访问分布\n>  - `dailyRetainInfo(string $from, string $to);` 访问日留存\n>  - `weeklyRetainInfo(string $from, string $to);` 访问周留存\n>  - `monthlyRetainInfo(string $from, string $to);` 访问月留存\n>  - `visitPage(string $from, string $to);` 访问页面\n>  - `userPortrait(string $from, string $to);` 用户画像分布数据\n\n"
  },
  {
    "path": "docs/src/4.x/mini-program/decrypt.md",
    "content": "# 微信小程序消息解密\n\n## 比如获取电话等功能，信息是加密的，需要解密。\n\nAPI:\n\n```php\n$decryptedData = $app->encryptor->decryptData($session, $iv, $encryptedData);\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/express.md",
    "content": "# 物流助手 电子面单\n\n## 获取支持的快递公司列表\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getAllDelivery.html\n\n```php\n\n$app->express->listProviders();\n\n{\n  \"count\": 8,\n  \"data\": [\n    {\n      \"delivery_id\": \"BEST\",\n      \"delivery_name\": \"百世快递\"\n    },\n    ...\n  ]\n}\n\n```\n\n## 生成运单\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.addOrder.html\n\n```php\n\n$app->express->createWaybill($data);\n\n\n// 成功返回\n\n{\n  \"order_id\": \"01234567890123456789\",\n  \"waybill_id\": \"123456789\",\n  \"waybill_data\": [\n    {\n      \"key\": \"SF_bagAddr\",\n      \"value\": \"广州\"\n    },\n    {\n      \"key\": \"SF_mark\",\n      \"value\": \"101- 07-03 509\"\n    }\n  ]\n}\n\n// 失败返回\n\n{\n  \"errcode\": 9300501,\n  \"errmsg\": \"delivery logic fail\",\n  \"delivery_resultcode\": 10002,\n  \"delivery_resultmsg\": \"客户密码不正确\"\n}\n\n```\n\n## 取消运单\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.cancelOrder.html\n\n```php\n$app->express->deleteWaybill($data);\n\n```\n\n## 获取运单数据\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getOrder.html\n\n```php\n$app->express->getWaybill($data);\n\n```\n\n## 查询运单轨迹\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getPath.html\n\n```php\n$app->express->getWaybillTrack($data);\n\n```\n\n## 获取电子面单余额。\n\n仅在使用加盟类快递公司时，才可以调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getQuota.html\n\n```php\n\n$app->express->getBalance($deliveryId, $bizId);\n\n// 例如：\n\n$app->express->getBalance('YTO', 'xyz');\n```\n\n## 绑定打印员\n\n若需要使用微信打单 PC 软件，才需要调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.updatePrinter.html\n\n```php\n$app->express->bindPrinter($openid);\n```\n\n## 解绑打印员\n\n若需要使用微信打单 PC 软件，才需要调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.updatePrinter.html\n\n```php\n$app->express->unbindPrinter($openid);\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/index.md",
    "content": "# 小程序\n\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 下面为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::miniProgram($config);\n```\n\n`$app` 在所有相关小程序的文档都是指 `Factory::miniProgram` 得到的实例，就不在每个页面单独写了。\n"
  },
  {
    "path": "docs/src/4.x/mini-program/nearby_poi.md",
    "content": "# 附近的小程序\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.add.html\n\n## 添加地点\n\n```php\n$params = [\n    'kf_info' => '{\"open_kf\":true,\"kf_headimg\":\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"kf_name\":\"Harden\"}',\n    'pic_list' => '{\"list\":[\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITRneE5FS9uYruXGMmrtmhsBySwddEWUGOibG8Ze2NT5E3Dyt79I0htNg/0?wx_fmt=jpeg\"]}',\n    'service_infos' => '{\"service_infos\":[{\"id\":2,\"type\":1,\"name\":\"快递\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"},{\"id\":0,\"type\":2,\"name\":\"自定义\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"}]}',\n    'store_name' => '羊村小马烧烤',\n    'contract_phone' => '111111111',\n    'hour' => '00:00-11:11',\n    'company_name' => '深圳市腾讯计算机系统有限公司',\n    'credential' => '156718193518281',\n    'address' => '新疆维吾尔自治区克拉玛依市克拉玛依区碧水路15-1-8号(碧水云天广场)',\n    'qualification_list' => '3LaLzqiTrQcD20DlX_o-OV1-nlYMu7sdVAL7SV2PrxVyjZFZZmB3O6LPGaYXlZWq',\n];\n\n$app->nearby_poi->add($params);\n```\n\n## 更新地点\n\n```php\n$poiId = 'xxxxxxxx';\n\n$params = [\n    'kf_info' => '{\"open_kf\":true,\"kf_headimg\":\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"kf_name\":\"Harden\"}',\n    'pic_list' => '{\"list\":[\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITRneE5FS9uYruXGMmrtmhsBySwddEWUGOibG8Ze2NT5E3Dyt79I0htNg/0?wx_fmt=jpeg\"]}',\n    'service_infos' => '{\"service_infos\":[{\"id\":2,\"type\":1,\"name\":\"快递\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"},{\"id\":0,\"type\":2,\"name\":\"自定义\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"}]}',\n    'contract_phone' => '111111111',\n    'hour' => '00:00-11:11',\n    'company_name' => '深圳市腾讯计算机系统有限公司',\n    'credential' => '156718193518281',\n    'address' => '新疆维吾尔自治区克拉玛依市克拉玛依区碧水路15-1-8号(碧水云天广场)',\n    'qualification_list' => '3LaLzqiTrQcD20DlX_o-OV1-nlYMu7sdVAL7SV2PrxVyjZFZZmB3O6LPGaYXlZWq',\n];\n\n$app->nearby_poi->update($poiId, $params);\n```\n\n## 删除地点\n\n```php\n$poiId = 'xxxxxxxx';\n\n$app->nearby_poi->delete($poiId);\n```\n\n## 地点列表\n\n```php\n$page = 1;\n$pageRows = 10;\n\n$app->nearby_poi->list($page, $pageRows);\n```\n\n## 设置地点展示状态\n\n```php\n$poiId = 'xxxxxxxx';\n$status = 0; // 0: 不展示，1：展示\n\n$app->nearby_poi->setVisibility($poiId, $status);\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/plugin.md",
    "content": "# 插件管理\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.applyPlugin.html\n\n## 申请使用插件\n\n```php\n$pluginAppId = 'xxxxxxxxx';\n\n$app->plugin->apply($pluginAppId);\n```\n\n## 删除已添加的插件\n\n```php\n$pluginAppId = 'xxxxxxxxx';\n\n$app->plugin->unbind($pluginAppId);\n```\n\n## 查询已添加的插件\n\n```php\n$app->plugin->list();\n```\n\n## 获取当前所有插件使用方\n\n```php\n$page = 1;\n$size = 10;\n\n$app->plugin_dev->getUsers($page, $size);\n```\n\n## 同意插件使用申请\n\n```php\n$appId = 'wxxxxxxxxxxxxxx';\n\n$app->plugin_dev->agree($appId);\n```\n\n## 拒绝插件使用申请\n\n```php\n$app->plugin_dev->refuse('拒绝理由');\n```\n\n## 删除已拒绝的申请者\n\n```php\n$app->plugin_dev->delete();\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/soter.md",
    "content": "# 生物认证\n\n## 生物认证秘钥签名验证\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/soter/soter.verifySignature.html\n\n```php\n$app->soter->verifySignature($openid, $json, $signature);\n```\n\n返回值示例:\n```json\n{\n    \"is_ok\": true\n}\n```\n\n参数说明:\n\n> - string $openid - 用户 openid\n> - string $json - 通过 [wx.startSoterAuthentication](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/soter/wx.startSoterAuthentication.html) 成功回调获得的 resultJSON 字段\n> - string $signature - 通过 [wx.startSoterAuthentication](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/soter/wx.startSoterAuthentication.html) 成功回调获得的 resultJSONSignature 字段"
  },
  {
    "path": "docs/src/4.x/mini-program/subscribe_message.md",
    "content": "# 订阅消息\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html\n\n## 组合模板并添加至帐号下的个人模板库\n\n```php\n$tid = 563;     // 模板标题 id，可通过接口获取，也可登录小程序后台查看获取\n$kidList = [1, 2];      // 开发者自行组合好的模板关键词列表，可以通过 `getTemplateKeywords` 方法获取\n$sceneDesc = '提示用户图书到期';    // 服务场景描述，非必填\n\n$app->subscribe_message->addTemplate($tid, $kidList, $sceneDesc);\n```\n\n## 删除帐号下的个人模板\n\n```php\n$templateId = 'bDmywsp2oEHjwAadTGKkUHpC0RgBVPvfAM7Cu1s03z8';\n\n$app->subscribe_message->deleteTemplate($templateId);\n```\n\n## 获取小程序账号的类目\n\n```php\n$app->subscribe_message->getCategory();\n```\n\n## 获取模板标题的关键词列表\n\n```php\n$tid = 563;     // 模板标题 id，可通过接口获取，也可登录小程序后台查看获取\n\n$app->subscribe_message->getTemplateKeywords($tid);\n```\n\n## 获取帐号所属类目下的公共模板标题\n\n```php\n$ids = [612, 613];  // 类目 id\n$start = 0;         // 用于分页，表示从 start 开始。从 0 开始计数。  \n$limit = 30;        // 用于分页，表示拉取 limit 条记录。最大为 30。\n\n$app->subscribe_message->getTemplateTitles($ids, $start, $limit);\n```\n\n## 获取当前帐号下的个人模板列表\n\n```php\n$app->subscribe_message->getTemplates();\n```\n\n## 发送订阅消息\n\n```php\n$data = [\n    'template_id' => 'bDmywsp2oEHjwAadTGKkUJ-eJEiMiOf7H-dZ7wjdw80', // 所需下发的订阅模板id\n    'touser' => 'oSyZp5OBNPBRhG-7BVgWxbiNZm',     // 接收者（用户）的 openid\n    'page' => '',       // 点击模板卡片后的跳转页面，仅限本小程序内的页面。支持带参数,（示例index?foo=bar）。该字段不填则模板无跳转。\n    'data' => [         // 模板内容，格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }\n        'date01' => [\n            'value' => '2019-12-01',\n        ],\n        'number01' => [\n            'value' => 10,\n        ],\n    ],\n];\n\n$app->subscribe_message->send($data);\n```\n"
  },
  {
    "path": "docs/src/4.x/mini-program/template_message.md",
    "content": "# 模板消息\n\n## 获取小程序模板库标题列表\n\n```\n$app->template_message->list($offset, $count);\n```\n\n## 获取模板库某个模板标题下关键词库\n\n```\n$app->template_message->get($id);\n```\n\n## 组合模板并添加至帐号下的个人模板库\n\n```\n$app->template_message->add($id, $keywordIdList);\n```\n\n## 获取帐号下已存在的模板列表\n\n```\n$app->template_message->getTemplates($offset, $count);\n```\n\n## 删除帐号下的某个模板\n\n```\n$app->template_message->delete($templateId);\n```\n\n## 发送模板消息\n\n```php\n$app->template_message->send([\n    'touser' => 'user-openid',\n    'template_id' => 'template-id',\n    'page' => 'index',\n    'form_id' => 'form-id',\n    'data' => [\n        'keyword1' => 'VALUE',\n        'keyword2' => 'VALUE2',\n        // ...\n    ],\n]);\n```\n"
  },
  {
    "path": "docs/src/4.x/miscellaneous.md",
    "content": "# 其它\n\n\n### 其它"
  },
  {
    "path": "docs/src/4.x/official-account/accounts.md",
    "content": "# 多账号接入\n\n如果你想使用本项目接入多个公众号，在本程序中，您可以为每个帐号都设置一个 id，此 id 对应了该帐号的 appid、token 等信息。\n如下表\n\n| id   | appId                | secret                             | 其它... |\n| ---- | -------------------- | ---------------------------------- | ----- |\n| 1    | `wx3cf0f39249eb0e60` | `f28f735d4f1c242f4687abb469072a29` | ...   |\n| 2    | `wx49eb0e63cf0f39s2` | `8f735d4687abb469f1c2422a29f4f207` | ...   |\n| N    | `wx5cfeb0e60f392490` | `35f8f27d46f1c242f487a9072a29bb46` | ...   |\n\n在微信公众平台的设置中，您可以将您帐号中平台的 `url` 设置为 `您的网址/?id=xxx`，如:\n\n```\nhttp://easywechat.com/wechat?id=1\n```\n\n而在程序入口处，根据 `id` 查找对应帐号的 `appid` 和 其它信息来创建配置数组创建实例即可。\n"
  },
  {
    "path": "docs/src/4.x/official-account/base.md",
    "content": "# 基础接口\n\n## 清理接口调用次数\n\n> 此接口官方有每月调用限制，不可随意调用\n\n```php\n$app->base->clearQuota();\n```\n\n## 获取微信服务器 IP (或IP段)\n\n```php\n$app->base->getValidIps();\n```"
  },
  {
    "path": "docs/src/4.x/official-account/broadcasting.md",
    "content": "# 群发\n\n微信的群发消息接口有各种乱七八糟的注意事项及限制，具体请阅读微信官方文档。\n\n## 发送消息\n\n以下所有方法均有第二个参数 `$to` 用于指定接收对象：\n\n>  - 当 `$to` 为整型时为标签 id\n>  - 当 `$to` 为数组时为用户的 openid 列表（至少两个用户的 openid）\n>  - 当 `$to` 为 `null` 时表示全部用户\n\n```php\n$app->broadcasting->sendMessage(Message $message, array | int $to = null);\n```\n\n下面的别名方法 `sendXXX` 都是基于上面 `sendMessage` 方法的封装。\n\n### 文本消息\n\n```php\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\");\n\n// 指定目标用户\n// 至少两个用户的 openid，必须是数组。\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\", [$openid1, $openid2]);\n\n// 指定标签组用户\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\", $tagId); // $tagId 必须是整型数字\n```\n\n### 图文消息\n\n```php\n$app->broadcasting->sendNews($mediaId);\n$app->broadcasting->sendNews($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendNews($mediaId, $tagId);\n```\n\n### 图片消息\n\n```php\n$app->broadcasting->sendImage($mediaId);\n$app->broadcasting->sendImage($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendImage($mediaId, $tagId);\n```\n\n### 语音消息\n\n```php\n$app->broadcasting->sendVoice($mediaId);\n$app->broadcasting->sendVoice($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendVoice($mediaId, $tagId);\n```\n\n### 视频消息\n\n用于群发的视频消息，需要先创建消息对象，\n\n```php\n// 1. 先上传视频素材用于群发：\n$video = '/path/to/video.mp4';\n$videoMedia = $app->media->uploadVideoForBroadcasting($video, '视频标题', '视频描述');\n\n// 结果如下：\n//{\n//  \"type\":\"video\",\n//  \"media_id\":\"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc\",\n//  \"created_at\":1398848981\n//}\n\n// 2. 使用上面得到的 media_id 群发视频消息\n$app->broadcasting->sendVideo($videoMedia['media_id']);\n```\n\n### 卡券消息\n\n```php\n$app->broadcasting->sendCard($cardId);\n$app->broadcasting->sendCard($cardId, [$openid1, $openid2]);\n$app->broadcasting->sendCard($cardId, $tagId);\n```\n\n### 发送预览群发消息给指定的 `openId` 用户\n\n```php\n$app->broadcasting->previewText($text, $openId);\n$app->broadcasting->previewNews($mediaId, $openId);\n$app->broadcasting->previewVoice($mediaId, $openId);\n$app->broadcasting->previewImage($mediaId, $openId);\n$app->broadcasting->previewVideo($message, $openId);\n$app->broadcasting->previewCard($cardId, $openId);\n```\n\n### 发送预览群发消息给指定的微信号用户\n\n> $wxanme 是用户的微信号，比如：notovertrue\n\n```php\n$app->broadcasting->previewTextByName($text, $wxname);\n$app->broadcasting->previewNewsByName($mediaId, $wxname);\n$app->broadcasting->previewVoiceByName($mediaId, $wxname);\n$app->broadcasting->previewImageByName($mediaId, $wxname);\n$app->broadcasting->previewVideoByName($message, $wxname);\n$app->broadcasting->previewCardByName($cardId, $wxname);\n```\n\n### 删除群发消息\n\n```php\n$app->broadcasting->delete($msgId);\n```\n\n### 查询群发消息发送状态\n\n```php\n$app->broadcasting->status($msgId);\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/card.md",
    "content": "# 卡券\n\n-\n\n## 获取实例\n\n```php\n$card = $app->card;\n```\n\n## 通用功能\n\n### 获取卡券颜色\n\n```php\n$card->colors();\n```\n\n### 卡券开放类目查询\n\n```php\n$card->categories();\n```\n\n### 创建卡券\n\n创建卡券接口是微信卡券的基础接口，用于创建一类新的卡券，获取 card_id，创建成功并通过审核后，商家可以通过文档提供的其他接口将卡券下发给用户，每次成功领取，库存数量相应扣除。\n\n```php\n$card->create($cardType = 'member_card', array $attributes);\n```\n\n> - `attributes` array 卡券信息\n\n示例：\n\n```php\n<?php\n\n $cardType = 'GROUPON';\n\n    $attributes = [\n      'base_info' => [\n          'brand_name' => '微信餐厅',\n          'code_type' => 'CODE_TYPE_TEXT',\n          'title' => '132元双人火锅套餐',\n          //...\n      ],\n      'advanced_info' => [\n          'use_condition' => [\n              'accept_category' => '鞋类',\n              'reject_category' => '阿迪达斯',\n              'can_use_with_other_discount' => true,\n          ],\n          //...\n      ],\n    ];\n\n$result = $card->create($cardType, $attributes);\n```\n\n### 获取卡券详情\n\n```php\n$cardInfo = $card->get($cardId);\n```\n\n### 批量查询卡列表\n\n```php\n$card->list($offset = 0, $count = 10, $statusList = 'CARD_STATUS_VERIFY_OK');\n```\n\n> - `offset` int - 查询卡列表的起始偏移量，从 0 开始\n> - `count` int - 需要查询的卡片的数量\n> - `statusList` - 支持开发者拉出指定状态的卡券列表，详见 example\n\n示例：\n\n```php\n// CARD_STATUS_NOT_VERIFY, 待审核；\n// CARD_STATUS_VERIFY_FAIL, 审核失败；\n// CARD_STATUS_VERIFY_OK， 通过审核；\n// CARD_STATUS_USER_DELETE，卡券被商户删除；\n// CARD_STATUS_DISPATCH，在公众平台投放过的卡券；\n\n$result = $card->list($offset, $count, 'CARD_STATUS_NOT_VERIFY');\n```\n\n### 更改卡券信息接口\n\n支持更新所有卡券类型的部分通用字段及特殊卡券中特定字段的信息。\n\n```php\n$card->update($cardId, $type, $attributes = []);\n```\n\n> - `type` string - 卡券类型\n\n示例：\n\n```php\n$cardId = 'pdkJ9uCzKWebwgNjxosee0ZuO3Os';\n\n$type = 'groupon';\n\n$attributes = [\n  'base_info' => [\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/2aJY6aCPatSeibYAyy7yct9zJXL9WsNVL4JdkTbBr184gNWS6nibcA75Hia9CqxicsqjYiaw2xuxYZiaibkmORS2oovdg/0',\n    'center_title' => '顶部居中按钮',\n    'center_sub_title' => '按钮下方的wording',\n    'center_url' => 'http://easywechat.com',\n    'custom_url_name' => '立即使用',\n    'custom_url' => 'http://www.qq.com',\n    'custom_url_sub_title' => '6个汉字tips',\n    'promotion_url_name' => '更多优惠',\n    'promotion_url' => 'http://www.qq.com',\n  ],\n  //...\n];\n\n$result = $card->update($cardId, $type, $attributes);\n```\n\n### 删除卡券\n\n```php\n$card->delete($cardId);\n```\n\n### 创建二维码\n\n开发者可调用该接口生成一张卡券二维码供用户扫码后添加卡券到卡包。\n\n自定义 Code 码的卡券调用接口时，POST 数据中需指定 code，非自定义 code 不需指定，指定 openid 同理。指定后的二维码只能被用户扫描领取一次。\n\n```php\n$card->createQrCode($cards);\n```\n\n> - `cards` array - 卡券相关信息\n\n示例：\n\n```php\n// 领取单张卡券\n$cards = [\n    'action_name' => 'QR_CARD',\n    'expire_seconds' => 1800,\n    'action_info' => [\n      'card' => [\n        'card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY',\n        'is_unique_code' => false,\n        'outer_id' => 1,\n      ],\n    ],\n  ];\n\n$result = $card->createQrCode($cards);\n```\n\n```php\n// 领取多张卡券\n$cards = [\n    'action_name' => 'QR_MULTIPLE_CARD',\n    'action_info' => [\n      'multiple_card' => [\n        'card_list' => [\n          ['card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY'],\n        ],\n      ],\n    ],\n  ];\n\n$result = $card->createQrCode($cards);\n```\n\n请求成功返回值示例：\n\n```json\n{\n  \"errcode\": 0,\n  \"errmsg\": \"ok\",\n  \"ticket\": \"gQHB8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0JIV3lhX3psZmlvSDZmWGVMMTZvAAIEsNnKVQMEIAMAAA==\", //获取ticket后需调用换取二维码接口获取二维码图片，详情见字段说明。\n  \"expire_seconds\": 1800,\n  \"url\": \"http://weixin.qq.com/q/BHWya_zlfioH6fXeL16o \",\n  \"show_qrcode_url\": \"https://mp.weixin.qq.com/cgi-bin/showqrcode?  ticket=gQH98DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0czVzRlSWpsamlyM2plWTNKVktvAAIE6SfgVQMEgDPhAQ%3D%3D\"\n}\n```\n\n### ticket 换取二维码图片\n\n获取二维码 ticket 后，开发者可用 ticket 换取二维码图片。\n\n```php\n$card->getQrCode($ticket);\n```\n\n> - `ticket` string> - 获取的二维码 ticket，凭借此 ticket 可以在有效时间内换取二维码。\n\n示例：\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$result = $card->getQrCode($ticket);\n```\n\n### ticket 换取二维码链接\n\n```php\n$card->getQrCodeUrl($ticket);\n```\n\n示例：\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$card->getQrCodeUrl($ticket);\n```\n\n### 创建货架接口\n\n开发者需调用该接口创建货架链接，用于卡券投放。创建货架时需填写投放路径的场景字段。\n\n```php\n$card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cards);\n```\n\n> - `banner` string -页面的 banner 图;\n> - `pageTitle` string - 页面的 title\n> - `canShare` bool - 页面是不是可以分享，true 或 false\n> - `scene` string - 投放页面的场景值，具体值请参考下面的 example\n> - `cards` array - 卡券列表，每个元素有两个字段\n\n示例：\n\n```php\n$banner = 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFN';\n$pageTitle = '惠城优惠大派送';\n$canShare  = true;\n\n//SCENE_NEAR_BY          附近\n//SCENE_MENU             自定义菜单\n//SCENE_QRCODE             二维码\n//SCENE_ARTICLE             公众号文章\n//SCENE_H5                 h5页面\n//SCENE_IVR                 自动回复\n//SCENE_CARD_CUSTOM_CELL 卡券自定义cell\n$scene = 'SCENE_NEAR_BY';\n\n$cardList = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/test.png'],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/aa.jpg'],\n];\n\n$result = $card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cardList);\n```\n\n### 图文消息群发卡券\n\n> 特别注意：目前该接口仅支持填入非自定义 code 的卡券,自定义 code 的卡券需先进行 code 导入后调用。\n\n```php\n$card->getHtml($cardId);\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->getHtml($cardId);\n```\n\n### 设置测试白名单\n\n同时支持“openid”、“username”两种字段设置白名单，总数上限为 10 个。\n\n```php\n$card->setTestWhitelist($openids); // 使用 openid\n$card->setTestWhitelistByName($usernames); // 使用 username\n```\n\n> - `openids` array - 测试的 openid 列表\n> - `usernames` array> - 测试的微信号列表\n\n示例：\n\n```php\n// by openid\n$openids   = [$openId, $openId2, $openid3...];\n$result = $card->setTestWhitelist($openids);\n\n// by username\n$usernames = ['tianye0327', 'iovertrue'];\n$result = $card->setTestWhitelistByName($usernames);\n```\n\n### 获取用户已领取卡券接口\n\n用于获取用户卡包里的，属于该 appid 下所有**可用卡券，包括正常状态和未生效状态**。\n\n```php\n$card->getUserCards($openid, $cardId);\n```\n\n示例：\n\n```php\n$openid  = 'odkJ9uDUz26RY-7DN1mxkznfo9xU';\n$cardId = ''; // 卡券ID。不填写时默认查询当前 appid 下的卡券。\n\n$result = $card->getUserCards($openid, $cardId);\n```\n\n### 设置微信买单接口\n\n```php\n$card->setPayCell($cardId, $isOpen = true);\n```\n\n> - `isOpen` string - 是否开启买单功能，填 true/false，不填默认 true\n\n示例：\n\n```php\n$cardId = 'pdkJ9uH7u11R-Tu1kilbaW_zDFow';\n\n$result = $card->setPayCell($cardId); // isOpen = true\n$result = $card->setPayCell($cardId, $isOpen);\n```\n\n### 修改库存接口\n\n```php\n$card->increaseStock($cardId, $amount); // 增加库存\n$card->reductStock($cardId, $amount); // 减少库存\n```\n\n> - `cardId` string - 卡券 ID\n> - `amount` int - 修改多少库存\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY';\n\n$result = $card->increaseStock($cardId, 100);\n```\n\n## 卡券 Code\n\n### 导入 code 接口\n\n在自定义 code 卡券成功创建并且通过审核后，必须将自定义 code 按照与发券方的约定数量调用导入 code 接口导入微信后台。\n\n```php\n$card->code->deposit($cardId, $codes);\n```\n\n> - `cardId` string - 要导入 code 的卡券 ID\n> - `codes` array - 要导入微信卡券后台的自定义 code，最多 100 个\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n$codes    = ['11111', '22222', '33333'];\n\n$result = $card->code->deposit($cardId, $codes);\n```\n\n### 查询导入 code 数目\n\n```php\n$card->code->getDepositedCount($cardId);  // 要导入 code 的卡券 ID\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->code->getDepositedCount($cardId);\n```\n\n### 核查 code 接口\n\n为了避免出现导入差错，强烈建议开发者在查询完 code 数目的时候核查 code 接口校验 code 导入微信后台的情况。\n\n```php\n$card->code->check($cardId, $codes);\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$codes = ['807732265476', '22222', '33333'];\n\n$result = $card->code->check($cardId, $codes);\n```\n\n### 查询 Code 接口\n\n```php\n$card->code->get($code, $cardId, $checkConsume = true);\n```\n\n> - checkConsume 是否校验 code 核销状态，true 和 false\n\n示例：\n\n```php\n$code = '736052543512';\n$cardId = 'pdkJ9uDgnm0pKfrTb1yV0dFMO_Gk';\n\n$result = $card->code->get($code, $cardId);\n$result = $card->code->get($code, $cardId, false); // check_consume = false\n```\n\n### 核销 Code 接口\n\n```php\n$card->code->consume($code);\n// 或者指定 cardId\n$card->code->consume($code, $cardId);\n```\n\n示例：\n\n```php\n$code = '789248558333';\n$cardId = 'pdkJ9uDmhkLj6l5bm3cq9iteQBck';\n\n$result = $card->code->consume($code);\n// 或\n$result = $card->code->consume($code, $cardId);\n```\n\n### Code 解码接口\n\n```php\n$card->code->decrypt($encryptedCode);\n```\n\n示例：\n\n```php\n$encryptedCode = 'XXIzTtMqCxwOaawoE91+VJdsFmv7b8g0VZIZkqf4GWA60Fzpc8ksZ/5ZZ0DVkXdE';\n\n$result = $card->code->decrypt($encryptedCode);\n```\n\n### 更改 Code 接口\n\n```php\n$card->code->update($code, $newCode, $cardId);\n```\n\n> - `newCode` string - 变更后的有效 Code 码\n\n示例：\n\n```php\n$code = '148246271394';\n$newCode = '659266965266';\n$cardId = '';\n\n$result = $card->code->update($code, $newCode, $cardId);\n```\n\n### 设置卡券失效\n\n```php\n$card->code->disable($code, $cardId);\n```\n\n示例：\n\n```php\n$code    = '736052543512';\n$cardId = '';\n\n$result = $card->code->disable($code, $cardId);\n```\n\n## 通用卡券\n\n## 卡券激活\n\n```php\n$result = $card->general_card->activate($info);\n```\n\n## 撤销激活\n\n```php\n$result = $card->general_card->deactivate(string $cardId, string $code);\n```\n\n## 更新用户信息\n\n```php\n$result = $card->general_card->updateUser(array $info);\n```\n\n## 会员卡\n\n### 会员卡激活\n\n```php\n$result = $card->member_card->activate($info);\n```\n\n> - `info` - 需要激活的会员卡信息\n\n示例：\n\n```php\n$info = [\n      'membership_number'        => '357898858', //会员卡编号，由开发者填入，作为序列号显示在用户的卡包里。可与Code码保持等值。\n      'code'                     => '916679873278', //创建会员卡时获取的初始code。\n      'activate_begin_time'      => '1397577600', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式\n      'activate_end_time'        => '1422724261', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。\n      'init_bonus'               => '持白金会员卡到店消费，可享8折优惠。', //初始积分，不填为0。\n      'init_balance'             => '持白金会员卡到店消费，可享8折优惠。', //初始余额，不填为0。\n      'init_custom_field_value1' => '白银', //创建时字段custom_field1定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value2' => '9折', //创建时字段custom_field2定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value3' => '200', //创建时字段custom_field3定义类型的初始值，限制为4个汉字，12字节。\n];\n\n$result = $card->member_card->activate($info);\n```\n\n### 设置开卡字段\n\n```php\n$card->member_card->setActivationForm($cardId, $settings);\n```\n\n> - `settings` array - 会员卡激活时的选项\n\n示例：\n\n```php\n$cardId = 'pdkJ9uJYAyfLXsUCwI2LdH2Pn1AU';\n\n$settings = [\n    'required_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_MOBILE',\n            'USER_FORM_INFO_FLAG_LOCATION',\n            'USER_FORM_INFO_FLAG_BIRTHDAY',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n    'optional_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_EMAIL',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n];\n\n$result = $card->member_card->setActivationForm($cardId, $settings);\n```\n\n### 拉取会员信息\n\n```php\n$card->member_card->getUser($cardId, $code);\n```\n\n示例：\n\n```php\n$cardId = 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8';\n$code    = '916679873278';\n\n$result = $card->member_card->getUser($cardId, $code);\n```\n\n### 更新会员信息\n\n```php\n$card->member_card->updateUser($info);\n```\n\n> - `info` array - 可以更新的会员信息\n\n示例：\n\n```php\n$info = [\n    'code'                => '916679873278', //卡券Code码。\n    'card_id'             => 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8', //卡券ID。\n    'record_bonus'        => '消费30元，获得3积分', //商家自定义积分消耗记录，不超过14个汉字。\n    'bonus'               => '100', //需要设置的积分全量值，传入的数值会直接显示，如果同时传入add_bonus和bonus,则前者无效。\n    'balance'             => '持白金会员卡到店消费，可享8折优惠。', //需要设置的余额全量值，传入的数值会直接显示，如果同时传入add_balance和balance,则前者无效。\n    'record_balance'      => '持白金会员卡到店消费，可享8折优惠。', //商家自定义金额消耗记录，不超过14个汉字。\n    'custom_field_value1' => '100', //创建时字段custom_field1定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value2' => '200', //创建时字段custom_field2定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value3' => '300', //创建时字段custom_field3定义类型的最新数值，限制为4个汉字，12字节。\n];\n\n$result = $card->member_card->updateUser($info);\n```\n\n## 子商户\n\n### 添加子商户\n\n```php\n$card->sub_merchant->create(array $attributes); \n```\n\n示例：\n\n```php\n$attributes = [\n    'brand_name' => 'overtrue',\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0',\n    'protocol' => 'qIqwTfzAdJ_1-VJFT0fIV53DSY4sZY2WyhkzZzbV498Qgdp-K5HJtZihbHLS0Ys0',\n    'end_time' => '1438990559',\n    'primary_category_id' => 1,\n    'secondary_category_id' => 101,\n    'agreement_media_id' => '',\n    'operator_media_id' => '',\n    'app_id' => '',\n];\n\n$result = $card->sub_merchant->create($attributes);\n```\n\n### 更新子商户\n\n```php\n$card->sub_merchant->update(int $merchantId, array $info);\n```\n\n> - `$merchantId` int - 子商户 ID\n> - `$info` array - 参数与创建子商户参数一样\n\n示例：\n\n```php\n$info = [\n  //...\n];\n$result = $card->sub_merchant->update('12', $info);\n```\n\n## 特殊票券\n\n### 机票值机\n\n```php\n$card->boarding_pass->checkin(array $params);\n```\n\n### 更新会议门票 - 更新用户\n\n```php\n$card->meeting_ticket->updateUser(array $params);\n```\n\n### 更新电影门票 - 更新用户\n\n```php\n$card->movie_ticket->updateUser(array $params);\n```\n\n## JSAPI\n\n### 卡券批量下发到用户\n\n```php\n$cards = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'outer_id' => 2],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'outer_id' => 3],\n];\n$json = $card->jssdk->assign($cards); // 返回 json 格式\n```\n\n返回 json，在模板里的用法：\n\n```html\nwx.addCard({ cardList:\n<?= $json ?>, // 需要打开的卡券列表 success: function (res) { var cardList = res.cardList; // 添加的卡券列表信息 } });\n```\n\n### 获取 Ticket\n\n```php\n$card->jssdk->getTicket();\n// 强制刷新\n$card->jssdk->getTicket(true);\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/comment.md",
    "content": "# 评论数据管理\n\n\n\n## 打开已群发文章评论\n\n```php\n$app->comment->open($msgId, $index = null);\n```\n\n## 关闭已群发文章评论\n\n```php\n$app->comment->close($msgId, $index = null);\n```\n\n## 查看指定文章的评论数据\n\n```php\n$app->comment->list(string $msgId, int $index, int $begin, int $count, int $type = 0);\n```\n\n## 将评论标记精选\n\n```php\n$app->comment->markElect(string $msgId, int $index, int $commentId);\n```\n\n## 将评论取消精选\n\n```php\n$app->comment->unmarkElect(string $msgId, int $index, int $commentId);\n```\n\n## 删除评论\n\n```php\n$app->comment->delete(string $msgId, int $index, int $commentId);\n```\n\n## 回复评论\n\n```php\n$app->comment->reply(string $msgId, int $index, int $commentId, string $content);\n```\n\n## 删除回复\n\n```php\n$app->comment->deleteReply(string $msgId, int $index, int $commentId);\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/configuration.md",
    "content": "# 配置\n\n常用的配置参数会比较少，因为除非你有特别的定制，否则基本上默认值就可以了：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    //...\n];\n\n$app = Factory::officialAccount($config);\n```\n\n下面是一个完整的配置样例：\n\n> 不建议你在配置的时候弄这么多，用到啥就配置啥才是最好的，因为大部分用默认值即可。\n\n```php\n<?php\n\nreturn [\n    /**\n     * 账号基本信息，请从微信公众平台/开放平台获取\n     */\n    'app_id'  => 'your-app-id',         // AppID\n    'secret'  => 'your-app-secret',     // AppSecret\n    'token'   => 'your-token',          // Token\n    'aes_key' => '',                    // EncodingAESKey，兼容与安全模式下请一定要填写！！！\n\n     /**\n      * 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n      * 使用自定义类名时，构造函数将会接收一个 `EasyWeChat\\Kernel\\Http\\Response` 实例\n      */\n    'response_type' => 'array',\n\n    /**\n     * 日志配置\n     *\n     * level: 日志级别, 可选为：\n     *         debug/info/notice/warning/error/critical/alert/emergency\n     * path：日志文件位置(绝对路径!!!)，要求可写权限\n     */\n    'log' => [\n        'default' => 'dev', // 默认使用的 channel，生产环境可以改为下面的 prod\n        'channels' => [\n            // 测试环境\n            'dev' => [\n                'driver' => 'single',\n                'path' => '/tmp/easywechat.log',\n                'level' => 'debug',\n            ],\n            // 生产环境\n            'prod' => [\n                'driver' => 'daily',\n                'path' => '/tmp/easywechat.log',\n                'level' => 'info',\n            ],\n        ],\n    ],\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * http://docs.guzzlephp.org/en/stable/request-config.html\n     *\n     * - retries: 重试次数，默认 1，指定当 http 请求失败时重试的次数。\n     * - retry_delay: 重试延迟间隔（单位：ms），默认 500\n     * - log_template: 指定 HTTP 日志模板，请参考：https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php\n     */\n    'http' => [\n        'max_retries' => 1,\n        'retry_delay' => 500,\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n    ],\n\n    /**\n     * OAuth 配置\n     *\n     * scopes：公众平台（snsapi_userinfo / snsapi_base），开放平台：snsapi_login\n     * callback：OAuth授权完成后的回调页地址\n     */\n    'oauth' => [\n        'scopes'   => ['snsapi_userinfo'],\n        'callback' => '/examples/oauth_callback.php',\n    ],\n];\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n\n## 日志配置\n\n你可以配置多个日志的 channel，每个 channel 里的 `driver` 对应不同的日志驱动，内置可用的 `driver` 如下表：\n\n名称 | 描述\n------------- | -------------\n`stack` | 复合型，可以包含下面多种驱动的混合模式\n`single` | 基于 `StreamHandler` 的单一文件日志，参数有 `path`，`level`\n`daily` | 基于 `RotatingFileHandler` 按日期生成日志文件，参数有 `path`，`level`，`days`(默认 7 天)\n`slack` | 基于 `SlackWebhookHandler` 的 Slack 组件，参数请参考源码：[LogManager.php](https://github.com/overtrue/wechat/blob/master/src/Kernel/Log/LogManager.php#L247)\n`syslog` | 基于 `SyslogHandler` Monolog 驱动，参数有 `facility` 默认为 `LOG_USER`，`level`\n`errorlog` | 记录日志到系统错误日志，基于 `ErrorLogHandler`，参数有 `type`，默认为 `ErrorLogHandler::OPERATING_SYSTEM`\n\n### 自定义日志驱动\n\n由于日志使用的是 [Monolog](https://github.com/Seldaek/monolog)，所以，除了默认的文件式日志外，你可以自定义日志处理器：\n\n```php\nuse Monolog\\Logger;\nuse Monolog\\Handler\\RotatingFileHandler;\n\n\n// 注册自定义日志\n$app->logger->extend('mylog', function($app, $config){\n    return new Logger($this->parseChannel($config), [\n        $this->prepareHandler(new RotatingFileHandler(\n            $config['path'], $config['days'], $this->level($config)\n        )),\n    ]);\n});\n```\n\n>  在你自定义的闭包函数中，可以使用 `EasyWeChat\\Kernel\\Log\\LogManager` 中的方法，具体请查看 SDK 源代码。\n\n配置文件中在 `driver` 部分即可使用你自定义的驱动了：\n\n```php\n'log' => [\n    'default' => 'dev', // 默认使用的 channel，生产环境可以改为下面的 prod\n    'channels' => [\n        // 测试环境\n        'dev' => [\n            'driver' => 'mylog',\n            'path' => '/tmp/easywechat.log',\n            'level' => 'debug',\n            'days' => 5,\n        ],\n\n        //...\n    ],\n],\n```\n\n"
  },
  {
    "path": "docs/src/4.x/official-account/customer_service.md",
    "content": "# 客服\n\n使用客服系统可以向用户发送消息以及群发消息，客服的管理等功能。\n\n## 客服管理\n\n### 获取所有客服\n\n```php\n$app->customer_service->list();\n```\n\n### 获取所有在线的客服\n\n```php\n$app->customer_service->online();\n```\n\n### 添加客服\n\n```php\n$app->customer_service->create('foo@test', '客服1');\n```\n\n### 修改客服\n\n```php\n$app->customer_service->update('foo@test', '客服1');\n```\n\n### 删除账号\n\n```php\n$app->customer_service->delete('foo@test');\n```\n\n### 设置客服头像\n\n```php\n$app->customer_service->setAvatar('foo@test', $avatarPath); // $avatarPath 为本地图片路径，非 URL\n```\n\n### 获取客服与客户聊天记录\n\n```php\n$app->customer_service->messages($startTime, $endTime, $msgId = 1, $number = 10000);\n```\n\n示例:\n\n```php\n$records = $app->customer_service->messages('2015-06-07', '2015-06-21', 1, 20000);\n```\n\n### 主动发送消息给用户\n\n```php\n$app->customer_service->message($message)->to($openId)->send();\n```\n\n> `$message` 为消息对象或文本，请参考：[消息](messages)\n\n示例：\n\n```php\n$app->customer_service->message('hello')\n                  >  ->to('oV-gpwdOIwSI958m9osAhGBFxxxx')\n                  >  ->send();\n```\n\n### 指定客服发送消息\n\n```php\n$app->customer_service->message($message)\n                      >  ->from('account@test')\n                      >  ->to($openId)\n                      >  ->send();\n```\n> `$message` 为消息对象或文本，请参考：[消息](messages.html)\n\n示例：\n\n```php\n$app->customer_service->message('hello')\n                  >  ->from('kf2001@gh_176331xxxx')\n                  >  ->to('oV-gpwdOIwSI958m9osAhGBFxxxx')\n                  >  ->send();\n```\n\n### 邀请微信用户加入客服\n\n以账号 `foo@test` 邀请 微信号 为 `xxxx` 的微信用户加入客服。\n\n```php\n$app->customer_service->invite('foo@test', 'xxxx');\n```\n\n## 客服会话控制\n\n## 创建会话\n\n```php\n$app->customer_service_session->create('test1@test', 'OPENID');\n```\n\n### 关闭会话\n\n```php\n$app->customer_service_session->close('test1@test', 'OPENID');\n```\n\n### 获取客户会话状态\n\n```php\n$app->customer_service_session->get('OPENID');\n```\n\n### 获取客服会话列表\n\n```php\n$app->customer_service_session->list('test1@test');\n```\n\n### 获取未接入会话列表\n\n```php\n$app->customer_service_session->waiting();\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/data_cube.md",
    "content": "# 数据统计与分析\n\n通过数据接口，开发者可以获取与公众平台官网统计模块类似但更灵活的数据，还可根据需要进行高级处理。\n\n>\n> 1. 接口侧的公众号数据的数据库中仅存储了 **2014年12月1日之后**的数据，将查询不到在此之前的日期，即使有查到，也是不可信的脏数据；\n> 2. 请开发者在调用接口获取数据后，将数据保存在自身数据库中，即加快下次用户的访问速度，也降低了微信侧接口调用的不必要损耗。\n> 3. 额外注意，获取图文群发每日数据接口的结果中，只有**中间页阅读人数+原文页阅读人数+分享转发人数+分享转发次数+收藏次数 >=3** 的结果才会得到统计，过小的阅读量的图文消息无法统计。\n\n## 示例\n\n```php\n$userSummary = $app->data_cube->userSummary('2014-12-07', '2014-12-08');\n\nvar_dump($userSummary);\n//\n//[\n//    {\n//        \"ref_date\": \"2014-12-07\",\n//        \"user_source\": 0,\n//        \"new_user\": 0,\n//        \"cancel_user\": 0\n//    }\n//    //后续还有ref_date在begin_date和end_date之间的数据\n// ]\n\n```\n\n## API\n\n    $from   示例： `2014-02-13` 获取数据的起始日期\n    $to     示例： `2014-02-18` 获取数据的结束日期，`$to`允许设置的最大值为昨日\n\n    `$from` 和 `$to` 的差值需小于 “最大时间跨度”（比如最大时间跨度为 1 时，`$from` 和 `$to` 的差值只能为 0，才能小于 1 ），否则会报错\n\n+ `array userSummary(string $from, string $to)` 获取用户增减数据, 最大时间跨度：**7**;\n+ `array userCumulate(string $from, string $to)` 获取累计用户数据, 最大时间跨度：**7**;\n+ `array articleSummary(string $from, string $to)` 获取图文群发每日数据, 最大时间跨度：**1**;\n+ `array articleTotal(string $from, string $to)` 获取图文群发总数据, 最大时间跨度：**1**;\n+ `array userReadSummary(string $from, string $to)` 获取图文统计数据, 最大时间跨度：**3**;\n+ `array userReadHourly(string $from, string $to)` 获取图文统计分时数据, 最大时间跨度：**1**;\n+ `array userShareSummary(string $from, string $to)` 获取图文分享转发数据, 最大时间跨度：**7**;\n+ `array userShareHourly(string $from, string $to)` 获取图文分享转发分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageSummary(string $from, string $to)` 获取消息发送概况数据, 最大时间跨度：**7**;\n+ `array upstreamMessageHourly(string $from, string $to)` 获取消息发送分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageWeekly(string $from, string $to)` 获取消息发送周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageMonthly(string $from, string $to)` 获取消息发送月数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistSummary(string $from, string $to)` 获取消息发送分布数据, 最大时间跨度：**15**;\n+ `array upstreamMessageDistWeekly(string $from, string $to)` 获取消息发送分布周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistMonthly(string $from, string $to)` 获取消息发送分布月数据, 最大时间跨度：**30**;\n+ `array interfaceSummary(string $from, string $to)` 获取接口分析数据, 最大时间跨度：**30**;\n+ `array interfaceSummaryHourly(string $from, string $to)` 获取接口分析分时数据, 最大时间跨度：**1**;\n+ `array cardSummary(string $from, string $to, int $condSource = 0)` 获取普通卡券分析分时数据, 最大时间跨度：**1**;\n+ `array freeCardSummary(string $from, string $to, int $condSource = 0, string $cardId = '')` 获取免费券分析分时数据, 最大时间跨度：**1**;\n+ `array memberCardSummary(string $from, string $to, int $condSource = 0)` 获取会员卡分析分时数据, 最大时间跨度：**1**;\n"
  },
  {
    "path": "docs/src/4.x/official-account/events.md",
    "content": "# 事件\n\n\n\n更多请参考：[服务端](server.html)\n\n关于事件类型请参考微信官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/4.x/official-account/goods.md",
    "content": "# 返佣商品\n\n> 微信文档：https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&key=11533749572M9ODP&version=1&lang=zh_CN&platform=2\n\n## 导入商品\n\n每次调用支持批量导入不超过1000条的商品信息。每分钟单个商户全局调用次数不得超过200次。每天调用次数不得超过100万次。每次请求包大小不超过2M。\n\n```php\n$data = [\n    [\n        'pid' => 'pid001',\n        'image_info' => [\n            'main_image_list' => [\n                [\n                    'url' => 'http://www.google.com/a.jpg',\n                ],\n                [\n                    'url' => 'http://www.google.com/b.jpg',\n                ],\n            ],\n        ],\n        \n        //...\n    ],\n    \n    //...\n];\n\n$result = $app->goods->add($data);\n\n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"status_ticket\": \"115141102647330200\"\n//}\n```\n\n`status_ticket` 用于获取此次导入的详细结果。\n\n## 更新商品\n\n更新时，字段不填代表不更新该字段（此处的字段不填，代表无此字段，而不是把字段的值设为空，设为空即代表更新该字段为空）。\n\n对于字符串类型的选填字段，如副标题，若清空不展示，则可设置为空；对于数字类型的选填字段，如原价，若清空不展示，则需设置为0。\n\n> 基本字段更新中 `pid` 为必填字段，且无法修改\n\n```php\n$data = [\n    [\n        'pid' => 'pid001',\n        'image_info' => [\n            'main_image_list' => [\n                [\n                    'url' => 'http://www.baidu.com/c.jpg',\n                ],\n                [\n                    'url' => 'http://www.baidu.com/d.jpg',\n                ],\n            ],\n        ],\n        \n        //...\n    ],\n    \n    //...\n];\n \n$result = $app->goods->update($data);\n \n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"status_ticket\": \"115141102647330200\"\n//}\n```\n\n> 说明：导入商品和更新商品使用的是同一个接口。\n \n## 查询导入/更新商品状态\n \n用于查询导入或更新商品的结果，当导入或更新商品失败时，若为系统错误可进行重试；若为其他错误，请排查解决后进行重试。\n\n```php\n$status_ticket = '115141102647330200';\n\n$result = $app->goods->status($status_ticket);\n\n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"result\": {\n//        \"succ_cnt\": 2,\n//        \"fail_cnt\": 0,\n//        \"total_cnt\": 2,\n//        \"progress\": \"100.00%\",\n//        \"statuses\": [\n//            {\n//                \"pid\": \"pid001\",\n//                \"ret\": 0,\n//                \"err_msg\": \"success\",\n//                \"err_msg_zh_cn\": \"成功\"\n//            },\n//            {\n//                \"pid\": \"pid002\",\n//                \"ret\": 0,\n//                \"err_msg\": \"success\",\n//                \"err_msg_zh_cn\": \"成功\"\n//            }\n//        ]\n//    }\n//}\n```\n\n## 获取单个商品信息\n\n使用该接口获取已导入的商品信息，供验证信息及抽查导入情况使用。\n\n```php\n$pid = 'pid001';\n\n$app->goods->get($pid);\n```\n\n> 返回结果中的 `product` 字段内容与 `导入商品接口` 字段一致，导入时未设置的值有可能获取时仍会返回，但显示为空\n\n## 分页获取商品信息\n\n使用该接口可获取已导入的全量商品信息，供全量验证信息使用。\n\n```php\n$context = '';  // page 为 1 时传空即可。当 page 大于 1 时必填，填入上一次访问本接口返回的 page_context。\n$page = 1;      // 页码\n$size = 10;     // 每页数据大小，目前限制为100以内，注意一次全量验证过程中该参数的值需保持不变\n\n$app->goods->list($context, $page, $size);\n```\n\n> 返回结果中的 `product` 字段内容与 `导入商品接口` 字段一致，导入时未设置的值有可能获取时仍会返回，但显示为空。\n> `page_context` 字段用于获取下一页数据时使用。\n"
  },
  {
    "path": "docs/src/4.x/official-account/index.md",
    "content": "## 公众号\n\n公众号的各模块相对比较统一，用法如下：\n\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n    \n    //...\n];\n\n$app = Factory::officialAccount($config);\n```\n\n`$app` 在所有相关公众号的文档都是指 `Factory::officialAccount` 得到的实例，就不在每个页面单独写了。\n"
  },
  {
    "path": "docs/src/4.x/official-account/material.md",
    "content": "# 素材管理\n\n在微信里的图片，音乐，视频等等都需要先上传到微信服务器作为素材才可以在消息中使用。\n\n### 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$result = $app->material->uploadImage(\"/path/to/your/image.jpg\");\n// {\n//    \"media_id\":MEDIA_ID,\n//    \"url\":URL\n// }\n```\n\n> `url` 只有上传图片素材有返回值。\n\n### 上传语音\n\n语音 **大小不超过 5M**，**长度不超过 60 秒**，支持 `mp3/wma/wav/amr` 格式。\n\n```php\n$result = $app->material->uploadVoice(\"/path/to/your/voice.mp3\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传视频\n\n```php\n$result = $app->material->uploadVideo(\"/path/to/your/video.mp4\", \"视频标题\", \"视频描述\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$result = $app->material->uploadThumb(\"/path/to/your/thumb.jpg\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传图文消息\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Article;\n\n// 上传单篇图文\n$article = new Article([\n    'title' => 'xxx',\n    'thumb_media_id' => $mediaId,\n    //...\n  ]);\n$app->material->uploadArticle($article);\n\n// 或者多篇图文\n$app->material->uploadArticle([$article, $article2, ...]);\n```\n\n### 修改图文消息\n\n有三个参数：\n\n> - `$mediaId` 要更新的文章的 `mediaId`\n> - `$article` 文章内容，`Article` 实例或者 全字段数组\n> - `$index` 要更新的文章在图文消息中的位置（多图文消息时，此字段才有意义，单图片忽略此参数），第一篇为 0；\n\n```php\n$result = $app->material->updateArticle($mediaId, new Article(...));\n\n// or\n\n$result = $app->material->updateArticle($mediaId, [\n   'title' => 'EasyWeChat 4.0 发布了！',\n    'thumb_media_id' => 'qQFxUQGO21Li4YrSn3MhnrqtRp9Zi3cbM9uBsepvDmE', // 封面图片 mediaId\n    'author' => 'overtrue', // 作者\n    'show_cover' => 1, // 是否在文章内容显示封面图片\n    'digest' => '这里是文章摘要',\n    'content' => '这里是文章内容，你可以放很长的内容',\n    'source_url' => 'https://easywechat.com',\n  ]);\n\n// 指定更新多图文中的第 2 篇\n$result = $app->material->updateArticle($mediaId, new Article(...), 1); // 第 2 篇\n```\n\n### 上传图文消息图片\n\n返回值中 url 就是上传图片的 URL，可用于后续群发中，放置到图文消息中。\n\n```php\n$result = $app->material->uploadArticleImage($path);\n//{\n//    \"url\":  \"http://mmbiz.qpic.cn/mmbiz/gLO17UPS6FS2xsypf378iaNhWacZ1G1UplZYWEYfwvuU6Ont96b1roYsCNFwaRrSaKTPCUdBK9DgEHicsKwWCBRQ/0\"\n//}\n```\n\n### 获取永久素材\n\n```php\n$resource = $app->material->get($mediaId);\n```\n\n如果请求的素材为图文消息，则响应如下：\n\n```json\n{\n \"news_item\": [\n       {\n       \"title\":TITLE,\n       \"thumb_media_id\"::THUMB_MEDIA_ID,\n       \"show_cover_pic\":SHOW_COVER_PIC(0/1),\n       \"author\":AUTHOR,\n       \"digest\":DIGEST,\n       \"content\":CONTENT,\n       \"url\":URL,\n       \"content_source_url\":CONTENT_SOURCE_URL\n       },\n       //多图文消息有多篇文章\n    ]\n  }\n```\n\n如果返回的是视频消息素材，则内容如下：\n\n```json\n{\n  \"title\": TITLE,\n  \"description\": DESCRIPTION,\n  \"down_url\": DOWN_URL\n}\n```\n\n其他类型的素材消息，则响应为 `EasyWeChat\\Kernel\\Http\\StreamResponse` 实例，开发者可以自行保存为文件。例如\n\n```php\n$stream = $app->material->get($mediaId);\n\nif ($stream instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    // 以内容 md5 为文件名\n    $stream->save('保存目录');\n\n    // 自定义文件名，不需要带后缀\n    $stream->saveAs('保存目录', '文件名');\n}\n```\n\n### 获取永久素材列表\n\n> - `$type` 素材的类型，图片（`image`）、视频（`video`）、语音 （`voice`）、图文（`news`）\n> - `$offset` 从全部素材的该偏移位置开始返回，可选，默认 `0`，0 表示从第一个素材 返回\n> - `$count` 返回素材的数量，可选，默认 `20`, 取值在 1 到 20 之间\n\n```php\n$app->material->list($type, $offset, $count);\n```\n\n示例：\n\n```php\n$list = $app->material->list('image', 0, 10);\n```\n\n图片、语音、视频 等类型的返回如下\n\n```json\n{\n  \"total_count\": TOTAL_COUNT,\n  \"item_count\": ITEM_COUNT,\n  \"item\": [\n    {\n      \"media_id\": MEDIA_ID,\n      \"name\": NAME,\n      \"update_time\": UPDATE_TIME,\n      \"url\": URL\n    }\n    //可能会有多个素材\n  ]\n}\n```\n\n永久图文消息素材列表的响应如下：\n\n```json\n{\n  \"total_count\": TOTAL_COUNT,\n  \"item_count\": ITEM_COUNT,\n  \"item\": [\n    {\n      \"media_id\": MEDIA_ID,\n      \"content\": {\n        \"news_item\": [\n          {\n            \"title\": TITLE,\n            \"thumb_media_id\": THUMB_MEDIA_ID,\n            \"show_cover_pic\": SHOW_COVER_PIC(0 / 1),\n            \"author\": AUTHOR,\n            \"digest\": DIGEST,\n            \"content\": CONTENT,\n            \"url\": URL,\n            \"content_source_url\": CONTETN_SOURCE_URL\n          }\n          //多图文消息会在此处有多篇文章\n        ]\n      },\n      \"update_time\": UPDATE_TIME\n    }\n    //可能有多个图文消息item结构\n  ]\n}\n```\n\n### 获取素材计数\n\n```php\n$stats = $app->material->stats();\n\n// {\n//   \"voice_count\":COUNT,\n//   \"video_count\":COUNT,\n//   \"image_count\":COUNT,\n//   \"news_count\":COUNT\n// }\n```\n\n### 删除永久素材\n\n```php\n$app->material->delete($mediaId);\n```\n\n### 文章预览\n\n文章预览请参阅 “消息群发” 章节。\n"
  },
  {
    "path": "docs/src/4.x/official-account/menu.md",
    "content": "# 自定义菜单\n\n## 读取（查询）已设置菜单\n\n\n```php\n$list = $app->menu->list();\n```\n\n## 获取当前菜单\n\n```php\n$current = $app->menu->current();\n```\n\n## 添加菜单\n\n### 添加普通菜单\n\n```php\n$buttons = [\n    [\n        \"type\" => \"click\",\n        \"name\" => \"今日歌曲\",\n        \"key\"  => \"V1001_TODAY_MUSIC\"\n    ],\n    [\n        \"name\"       => \"菜单\",\n        \"sub_button\" => [\n            [\n                \"type\" => \"view\",\n                \"name\" => \"搜索\",\n                \"url\"  => \"http://www.soso.com/\"\n            ],\n            [\n                \"type\" => \"view\",\n                \"name\" => \"视频\",\n                \"url\"  => \"http://v.qq.com/\"\n            ],\n            [\n                \"type\" => \"click\",\n                \"name\" => \"赞一下我们\",\n                \"key\" => \"V1001_GOOD\"\n            ],\n        ],\n    ],\n];\n$app->menu->create($buttons);\n```\n\n以上将会创建一个普通菜单。\n\n### 添加个性化菜单\n\n与创建普通菜单不同的是，需要在 `create()` 方法中将个性化匹配规则作为第二个参数传进去：\n\n```php\n$buttons = [\n    // ...\n];\n$matchRule = [\n    \"tag_id\" => \"2\",\n    \"sex\" => \"1\",\n    \"country\" => \"中国\",\n    \"province\" => \"广东\",\n    \"city\" => \"广州\",\n    \"client_platform_type\" => \"2\",\n    \"language\" => \"zh_CN\"\n];\n$app->menu->create($buttons, $matchRule);\n```\n\n## 删除菜单\n\n有两种删除方式，一种是**全部删除**，另外一种是**根据菜单 ID 来删除**(删除个性化菜单时用，ID 从查询接口获取)：\n\n```php\n$app->menu->delete(); // 全部\n$app->menu->delete($menuId);\n```\n\n## 测试个性化菜单\n\n```php\n$app->menu->match($userId);\n```\n\n> `$userId` 可以是粉丝的 OpenID，也可以是粉丝的微信号。\n\n返回 `$menu` 与指定的 `$userId` 匹配的菜单项。\n"
  },
  {
    "path": "docs/src/4.x/official-account/message-transfer.md",
    "content": "# 多客服消息转发\n\n多客服的消息转发绝对是超级的简单，转发的消息类型为 `transfer`：\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Transfer;\n\n// 转发收到的消息给客服\n$app->server->push(function($message) {\n  return new Transfer();\n});\n\n$response = $app->server->serve();\n```\n\n当然，你也可以指定转发给某一个客服：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Transfer;\n\n$app->server->push(function($message) {\n    return new Transfer($account);\n});\n```"
  },
  {
    "path": "docs/src/4.x/official-account/messages.md",
    "content": "# 消息\n\n我把微信的 API 里的所有“消息”都按类型抽象出来了，也就是说，你不用区分它是回复消息还是主动推送消息，免去了你去手动拼装微信的 XML 以及乱七八糟命名不统一的 JSON 了。\n\n在阅读以下内容时请忽略是 **接收消息** 还是 **回复消息**，后面我会给你讲它们的区别。\n\n## 消息类型\n\n消息分为以下几种：`文本`、`图片`、`视频`、`声音`、`链接`、`坐标`、`图文`、`文章` 和一种特殊的 `原始消息`。\n\n另外还有一种特殊的消息类型：**素材消息**，用于群发或者客服时发送已有素材用。\n\n> 注意：回复消息与客服消息里的图文类型为：**图文**，群发与素材中的图文为**文章**\n\n所有的消息类都在 `EasyWeChat\\Kernel\\Messages` 这个命名空间下， 下面我们来分开讲解：\n\n### 文本消息\n\n属性列表：\n\n> - `content` 文本内容\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Text;\n\n$text = new Text('您好！overtrue。');\n\n// or\n$text = new Text();\n$text->content = '您好！overtrue。';\n\n// or\n$text = new Text();\n$text->setAttribute('content', '您好！overtrue。');\n```\n\n### 图片消息\n\n属性列表：\n\n```\n- media_id 媒体资源 ID\n```\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Image;\n\n$image = new Image($mediaId);\n```\n\n### 视频消息\n\n属性列表：\n\n> - `title` 标题\n> - `description` 描述\n> - `media_id` 媒体资源 ID\n> - `thumb_media_id` 封面资源 ID\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Video;\n\n$video = new Video($mediaId, [\n        'title' => $title,\n        'description' => '...',\n    ]);\n```\n\n### 声音消息\n\n属性列表：\n\n> - `media_id` 媒体资源 ID\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Voice;\n\n$voice = new Voice($mediaId);\n```\n\n### 链接消息\n\n> 复链接消息\n\n### 坐标消息\n\n> 复坐标消息\n\n### 图文消息\n\n图文消息分为 `NewsItem` 与 `News`，`NewsItem` 为图文内容条目。\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n`NewsItem` 属性：\n\n> - `title` 标题\n> - `description` 描述\n> - `image` 图片链接\n> - `url` 链接 URL\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\News;\nuse EasyWeChat\\Kernel\\Messages\\NewsItem;\n\n$items = [\n    new NewsItem([\n        'title'       => $title,\n        'description' => '...',\n        'url'         => $url,\n        'image'       => $image,\n        // ...\n    ]),\n];\n$news = new News($items);\n```\n\n### 文章\n\n属性列表：\n\n> - `title` 标题\n> - `author` 作者\n> - `content` 具体内容\n> - `thumb_media_id` 图文消息的封面图片素材 id（必须是永久 mediaID）\n> - `digest` 图文消息的摘要，仅有单图文消息才有摘要，多图文此处为空\n> - `source_url` 来源 URL\n> - `show_cover` 是否显示封面，0 为 false，即不显示，1 为 true，即显示\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Article;\n\n$article = new Article([\n        'title'   => 'EasyWeChat',\n        'author'  => 'overtrue',\n        'content' => 'EasyWeChat 是一个开源的微信 SDK，它... ...',\n        // ...\n    ]);\n\n// or\n$article = new Article();\n$article->title   = 'EasyWeChat';\n$article->author  = 'overtrue';\n$article->content = '微信 SDK ...';\n// ...\n```\n\n### 素材消息\n\n素材消息用于群发与客服消息时使用。\n\n> 素材消息不支持被动回复，如需被动回复素材消息，首先组装后，再 News 方法返回。\n\n属性就一个：`media_id`。\n\n在构造时有两个参数：\n\n> - `$type` 素材类型，目前只支持：`mpnews`、 `mpvideo`、`voice`、`image` 等。\n> - `$mediaId` 素材 ID，从接口查询或者上传后得到。\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Media;\n\n$media = new Media($mediaId, 'mpnews');\n```\n\n以上呢，是所有微信支持的基本消息类型。\n\n> 需要注意的是，你不需要关心微信的消息字段叫啥，因为这里我们使用了更标准的命名，然后最终在中间做了转换，所以你不需要关注。\n\n### 原始消息\n\n原始消息是一种特殊的消息，它的场景是：**你不想使用其它消息类型，你想自己手动拼消息**。比如，回复消息时，你想自己拼 XML，那么你就直接用它就可以了：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Raw;\n\n$message = new Raw('<xml>\n<ToUserName><![CDATA[toUser]]></ToUserName>\n<FromUserName><![CDATA[fromUser]]></FromUserName>\n<CreateTime>12345678</CreateTime>\n<MsgType><![CDATA[image]]></MsgType>\n<Image>\n<MediaId><![CDATA[media_id]]></MediaId>\n</Image>\n</xml>');\n```\n\n比如，你要用于客服消息(客服消息是 JSON 结构)：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Raw;\n\n$message = new Raw('{\n    \"touser\":\"OPENID\",\n    \"msgtype\":\"text\",\n    \"text\":\n    {\n         \"content\":\"Hello World\"\n    }\n}');\n```\n\n总之，就是直接写微信接口要求的格式内容就好，此类型消息在 SDK 中不存在转换行为，所以请注意不要写错格式。\n\n## 在 SDK 中使用消息\n\n### 在服务端回复消息\n\n在 [服务端](server) 一节中，我们讲了回复消息的写法：\n\n```php\n// ... 前面部分省略\n$app->server->push(function ($message) {\n    return \"您好！欢迎关注我!\";\n});\n\n$response = $server->serve();\n```\n\n上面 `return` 了一句普通的文本内容，这里只是为了方便大家，实际上最后会有一个隐式转换为 `Text` 类型的动作。\n\n如果你要回复其它类型的消息，就需要返回一个具体的实例了，比如回复一个图片类型的消息：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Image;\n// ...\n$app->server->push(function ($message) {\n    return new Image('media-id');\n});\n// ...\n```\n\n#### 回复多图文消息\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n多图文消息其实就是单图文消息的一个数组而已了：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\News;\nuse EasyWeChat\\Kernel\\Messages\\NewsItem;\n\n// ...\n$app->server->push(function ($message) {\n   $news = new NewsItem(...);\n   return new News([$news]);\n});\n// ...\n```\n\n### 作为客服消息发送\n\n在客服消息里的使用也一样，都是直接传入消息实例即可：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Text;\n\n$message = new Text('Hello world!');\n\n$result = $app->customer_service->message($message)->to($openId)->send();\n//...\n```\n\n#### 发送多图文消息\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n多图文消息其实就是单图文消息组成的一个 News 对象而已：\n\n```php\n$news1 = new NewsItem(...);\n$news = new News([$news1]);\n\n$app->customer_service->message($news)->to($openId)->send();\n```\n\n### 群发消息\n\n请参考：[群发消息](broadcasting)\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer)\n"
  },
  {
    "path": "docs/src/4.x/official-account/oauth.md",
    "content": "# 网页授权\n\n## 关于 OAuth2.0\n\nOAuth 是一个关于授权（authorization）的开放网络标准，在全世界得到广泛应用，目前的版本是 2.0 版。\n\n<img src=\"https://user-images.githubusercontent.com/1472352/29310178-5a7a91cc-81df-11e7-9468-b66e150bfba1.png\" alt=\"\" style=\"max-width: 500px\">\n\n> 摘自：[RFC 6749](https://datatracker.ietf.org/doc/rfc6749/?include_text=1)\n\n步骤解释：\n\n    （A）用户打开客户端以后，客户端要求用户给予授权。\n    （B）用户同意给予客户端授权。\n    （C）客户端使用上一步获得的授权，向认证服务器申请令牌。\n    （D）认证服务器对客户端进行认证以后，确认无误，同意发放令牌。\n    （E）客户端使用令牌，向资源服务器申请获取资源。\n    （F）资源服务器确认令牌无误，同意向客户端开放资源。\n\n关于 OAuth 协议我们就简单了解到这里，如果还有不熟悉的同学，请 [Google 相关资料](https://www.google.com.hk/?gws_rd=ssl#safe=strict&q=OAuth2)\n\n## 微信 OAuth\n\n在微信里的 OAuth 其实有两种：[公众平台网页授权获取用户信息](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html)、[开放平台网页登录](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN)。\n\n它们的区别有两处，授权地址不同，`scope` 不同。\n\n> - **公众平台网页授权获取用户信息**\n\n    **授权 URL**: `https://open.weixin.qq.com/connect/oauth2/authorize`\n    **Scopes**: `snsapi_base` 与 `snsapi_userinfo`\n\n> - **开放平台网页登录**\n\n    **授权 URL**: `https://open.weixin.qq.com/connect/qrconnect`\n    **Scopes**: `snsapi_login`\n\n他们的逻辑都一样：\n\n1. 用户尝试访问一个我们的业务页面，例如: `/user/profile`\n2. 如果用户已经登录，则正常显示该页面\n3. 系统检查当前访问的用户并未登录（从 session 或者其它方式检查），则跳转到**跳转到微信授权服务器**（上面的两种中一种**授权 URL** ），并告知微信授权服务器我的**回调 URL（redirect_uri=callback.php)**，此时用户看到蓝色的授权确认页面（`scope` 为 `snsapi_base` 时不显示）\n4. 用户点击确定完成授权，浏览器跳转到**回调 URL**: `callback.php` 并带上 `code`： `?code=CODE&state=STATE`。\n5. 在 `callback.php` 中得到 `code` 后，通过 `code` 再次向微信服务器请求得到 **网页授权 access_token** 与 `openid`\n6. 你可以选择拿 `openid` 去请求 API 得到用户信息（可选）\n7. 将用户信息写入 SESSION。\n8. 跳转到第 3 步写入的 `target_url` 页面（`/user/profile`）。\n\n> 看懵了？没事，使用 SDK，你不用管这么多。:smile:\n>\n> 注意，上面的第 3 步：redirect_uri=callback.php 实际上我们会在 `callback.php` 后面还会带上授权目标页面 `user/profile`，所以完整的 `redirect_uri` 应该是下面的这样的 PHP 去拼出来：`'redirect_uri='.urlencode('callback.php?target=user/profile')`\n> 结果：redirect_uri=callback.php%3Ftarget%3Duser%2Fprofile\n\n## 逻辑组成\n\n从上面我们所描述的授权流程来看，我们至少有 3 个页面：\n\n1. **业务页面**，也就是需要授权才能访问的页面。\n2. **发起授权页**，此页面其实可以省略，可以做成一个中间件，全局检查未登录就发起授权。\n3. **授权回调页**，接收用户授权后的状态，并获取用户信息，写入用户会话状态（SESSION）。\n\n## 开始之前\n\n在开始之前请一定要记住，先登录公众号后台，找到**边栏 “开发”** 模块下的 **“接口权限”**，点击 **“网页授权获取用户基本信息”** 后面的修改，添加你的网页授权域名。\n\n> 如果你的授权地址为：`http://www.abc.com/xxxxx`，那么请填写 `www.abc.com`，也就是说请填写与网址匹配的域名，前者如果填写 `abc.com` 是通过不了的。\n\n## SDK 中 OAuth 模块的 API\n\n在 SDK 中，我们使用名称为 `oauth` 的模块来完成授权服务，我们主要用到以下两个 API：\n\n### 发起授权\n\n```php\n$response = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->redirect();\n```\n\n当你的应用是分布式架构且没有会话保持的情况下，你需要自行设置请求对象以实现会话共享。比如在 [Laravel](http://laravel.com) 框架中支持 Session 储存在 Redis 中，那么需要这样：\n\n```php\n$response = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->setRequest($request)\n                          ->redirect();\n\n//回调后获取user时也要设置$request对象\n//$user = $app->oauth->setRequest($request)->user();\n```\n\n当然你也可以在发起授权的时候指定回调 URL，比如设置回调 URL 为当前页面：\n\n```php\n$response = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->redirect($request->fullUrl());\n```\n\n它的返回值 `$response` 是一个 [Symfony\\Component\\HttpFoundation\\RedirectResponse](http://api.symfony.com/3.0/Symfony/Component/HttpFoundation/RedirectResponse.html) 实例。\n\n你可以选择在框架中做一些正确的响应，比如在 [Laravel](http://laravel.com) 框架中控制器方法是要求返回响应值的，那么你就直接:\n\n```php\nreturn $response;\n```\n\n在有的框架 (比如 yii2) 中是直接 `echo` 或者 `$this->display()` 这种的时候，你就直接：\n\n```php\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n### 获取已授权用户\n\n```php\n$user = $app->oauth->user();\n// $user 可以用的方法:\n// $user->getId();  // 对应微信的 OPENID\n// $user->getNickname(); // 对应微信的 nickname\n// $user->getName(); // 对应微信的 nickname\n// $user->getAvatar(); // 头像网址\n// $user->getOriginal(); // 原始API返回的结果\n// $user->getToken(); // access_token， 比如用于地址共享时使用\n```\n\n返回的 `$user` 是 [Overtrue\\Socialite\\User](https://github.com/overtrue/socialite/blob/master/src/User.php) 对象，你可以从该对象拿到[更多的信息](https://github.com/overtrue/socialite#user-interface)。\n\n> r`里没有`openid`， `$user->id` 便是 `openid`.\n> 如果你想拿微信返回给你的原样的全部信息，请使用：$user->getOriginal();\n\n当 `scope` 为 `snsapi_base` 时 `$oauth->user();` 对象里只有 `id`，没有其它信息。\n\n## 网页授权实例\n\n我们这里来用原生 PHP 写法举个例子，`oauth_callback` 是我们的授权回调 URL (未 urlencode 编码的 URL), `user/profile` 是我们需要授权才能访问的页面，它的 PHP 代码如下：\n\n```php\n// http://easywechat.com/user/profile\n<?php\n\nuse EasyWeChat\\Factory;\n\n$config = [\n  // ...\n  'oauth' => [\n      'scopes'   => ['snsapi_userinfo'],\n      'callback' => '/oauth_callback',\n  ],\n  // ..\n];\n\n$app = Factory::officialAccount($config);\n$oauth = $app->oauth;\n\n// 未登录\nif (empty($_SESSION['wechat_user'])) {\n\n  $_SESSION['target_url'] = 'user/profile';\n\n  return $oauth->redirect();\n  // 这里不一定是return，如果你的框架action不是返回内容的话你就得使用\n  // $oauth->redirect()->send();\n}\n\n// 已经登录过\n$user = $_SESSION['wechat_user'];\n\n// ...\n\n```\n\n授权回调页：\n\n```php\n// http://easywechat.com/oauth_callback\n<?php\n\nuse EasyWeChat\\Factory;\n\n$config = [\n  // ...\n];\n\n$app = Factory::officialAccount($config);\n$oauth = $app->oauth;\n\n// 获取 OAuth 授权结果用户信息\n$user = $oauth->user();\n\n$_SESSION['wechat_user'] = $user->toArray();\n\n$targetUrl = empty($_SESSION['target_url']) ? '/' : $_SESSION['target_url'];\n\nheader('location:'. $targetUrl); // 跳转到 user/profile\n```\n\n上面的例子呢都是基于 `$_SESSION` 来保持会话的，在微信客户端中，你可以结合 COOKIE 来存储，但是有效期平台不一样时间也不一样，好像 Android 的失效会快一些，不过基本也够用了。\n"
  },
  {
    "path": "docs/src/4.x/official-account/poi.md",
    "content": "# 门店\n\n## 创建门店\n\n用 POI 接口新建门店时所使用的图片 url 必须为微信自己域名的 url,因此需要先用上传图片接 口上传图片并获取 url,再创建门店。上传的图片限制文件大小限制 1MB,支持 JPG 格式，图片接口请参考：[临时素材](../basic-services/media.md)\n\n```php\n$app->poi->create($baseInfo);\n```\n\n> - `$baseInfo` 为门店的基本信息数组\n\n示例：\n\n```php\n<?php\n\n$info = array(\n         \"sid\"             => \"33788392\",\n         \"business_name\"   => \"麦当劳\",\n         \"branch_name\"     => \"艺苑路店\",\n         \"province\"        => \"广东省\",\n         \"city\"            => \"广州市\",\n         \"district\"        => \"海珠区\",\n         \"address\"         => \"艺苑路 11 号\",\n         \"telephone\"       => \"020-12345678\",\n         \"categories\"      => array(\"美食,快餐小吃\"),\n         \"offset_type\"     => 1,\n         \"longitude\"       => 115.32375,\n         \"latitude\"        => 25.097486,\n         \"photo_list\"      => array(\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                             ),\n         \"recommend\"       => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         \"special\"         => \"免费 wifi,外卖服务\",\n         \"introduction\"    => \"麦当劳是全球大型跨国连锁餐厅,1940 年创立于美国,在世界上大约拥有 3  万间分店。主要售卖汉堡包,以及薯条、炸鸡、汽水、冰品、沙拉、水果等 快餐食品\",\n         \"open_time\"       => \"8:00-20:00\",\n         \"avg_price\"       => 35,\n    );\n\n$result = $app->poi->create($info); // true or exception\n```\n\n> 注意：新创建的门店在审核通过后,会以事件形式推送给商户填写的回调 URL\n\n## 获取指定门店信息\n\n```php\n$app->poi->get($poiId);\n```\n\n> - `$poiId` 为门店 ID\n\n示例：\n\n```php\n$info = $app->poi->get(271262077);\n```\n\n## 获取门店列表\n\n```php\n$app->poi->list($begin, $limit);// begin:0, limit:10\n```\n\n> - `$begin` 就是查询起点，`MySQL` 里的 `offset`；\n> - `$limit` 查询条数，同 `MySQL` 里的 `limit`；\n\n> 两参数均可选\n\n示例：\n\n```php\n$pois = $app->poi->list(0, 2);// 取2条记录\n//\n//[\n//  {\n//    \"sid\": \"100\",\n//    \"poi_id\": \"271864249\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"艺苑路店\",\n//    \"address\": \"艺苑路 11 号\",\n//    \"available_state\": 3\n//  },\n//  {\n//    \"sid\": \"101\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"赤岗路店\",\n//    \"address\": \"赤岗路 102 号\",\n//    \"available_state\": 4\n//  }\n//]\n```\n\n## 修改门店信息\n\n商户可以通过该接口,修改门店的服务信息,包括:图片列表、营业时间、推荐、特色服务、简 介、人均价格、电话 7 个字段。目前基础字段包括(名称、坐标、地址等不可修改)。\n\n```php\n$app->poi->update($poiId, $data);\n```\n\n> - `$poiId` 为门店 ID\n> - `$data` 需要更新的部分数据，**若有填写内容则为覆盖更新,若无内容则视为不 修改,维持原有内容。photo_list 字段为全列表覆盖,若需要增加图片,需将之前图片同样放入 list 中,在其后增加新增图片。如:已有 A、B、C 三张图片,又要增加 D、E 两张图,则需要调 用该接口,photo_list 传入 A、B、C、D、E 五张图片的链接。**\n\n示例：\n\n```php\n$data = array(\n         \"telephone\" => \"020-12345678\",\n         \"recommend\" => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         //...\n        );\n\n$res = $app->poi->update(271262077, $data); //true or exception\n```\n\n## 删除门店\n\n```php\n$app->poi->delete($poiId);\n```\n\n示例：\n\n```php\n$app->poi->delete(271262077);// true or exception\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/reply.md",
    "content": "# 自动回复\n\n## 获取当前设置的回复规则\n\n```php\n$app->auto_reply->current();\n```"
  },
  {
    "path": "docs/src/4.x/official-account/semantic.md",
    "content": "# 语义理解\n\n> 貌似此接口已经下线，调用无正确返回值\n\n+ `query($keyword, $categories, $optional = [])` 语义理解:\n\n  + `$keyword` 为关键字\n  + `$categories` 需要使用的服务类型，多个用 “,” 隔开字符串，不能为空;\n  + `$optional` 为其它属性：\n    + `latitude`  `float`  纬度坐标，与经度同时传入；与城市二选一传入\n    + `longitude`  `float`  经度坐标，与纬度同时传入；与城市二选一传入\n    + `city`   `string`  城市名称，与经纬度二选一传入\n    + `region` `string`  区域名称，在城市存在的情况下可省；与经纬度二选一传入\n    + `uid`  `string` 用户唯一id（非开发者id），用户区分公众号下的不同用户（建议填入用户openid），如果为空，则无法使用上下文理解功能。appid和uid同时存在的情况下，才可以使用上下文理解功能。\n\n> 注：单类别意图比较明确，识别的覆盖率比较大，所以如果只要使用特定某个类别，建议将 category 只设置为该类别。\n\n示例：\n\n```php\n$result = $app->semantic->query('查一下明天从北京到上海的南航机票', \"flight,hotel\", array('city' => '北京', 'uid' => '123456'));\n// 查询参数：\n// {\n//    \"query\":\"查一下明天从北京到上海的南航机票\",\n//    \"city\":\"北京\",\n//    \"category\": \"flight,hotel\",\n//    \"appid\":\"wxaaaaaaaaaaaaaaaa\",\n//    \"uid\":\"123456\"\n// }\n```\n返回值示例：\n\n```json\n{\n    \"errcode\":0,\n    \"query\":\"查一下明天从北京到上海的南航机票\",\n    \"type\":\"flight\",\n    \"semantic\":{\n        \"details\":{\n            \"start_loc\":{\n                \"type\":\"LOC_CITY\",\n                \"city\":\"北京市\",\n                \"city_simple\":\"北京\",\n                \"loc_ori\":\"北京\"\n                },\n            \"end_loc\": {\n                \"type\":\"LOC_CITY\",\n                \"city\":\"上海市\",\n                \"city_simple\":\"上海\",\n                \"loc_ori\":\"上海\"\n              },\n            \"start_date\": {\n                \"type\":\"DT_ORI\",\n                \"date\":\"2014-03-05\",\n                \"date_ori\":\"明天\"\n              },\n           \"airline\":\"中国南方航空公司\"\n        },\n    \"intent\":\"SEARCH\"\n}\n```"
  },
  {
    "path": "docs/src/4.x/official-account/server.md",
    "content": "# 服务端\n\n我们在入门小教程一节以服务端为例讲解了一个基本的消息的处理，这里就不再讲服务器验证的流程了，请直接参考前面的入门实例即可。\n\n服务端的作用呢，在整个微信开发中主要是负责 **[接收用户发送过来的消息](http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html)**，还有 **[用户触发的一系列事件](http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html)**。\n\n首先我们得理清消息与事件的回复逻辑，当你收到用户消息后（消息由微信服务器推送到你的服务器），在你对消息进行一些处理后，不管是选择回复一个消息还是什么不都回给用户，你也应该给微信服务器一个 “答复”，如果是选择回复一条消息，就直接返回一个消息xml就好，如果选择不作任何回复，你也得回复一个空字符串或者字符串 `SUCCESS`（不然用户就会看到 `该公众号暂时无法提供服务`）。\n\n## 基本使用\n\n在 SDK 中使用 `$app->server->push(callable $callback)` 来设置消息处理器：\n\n```php\n$app->server->push(function ($message) {\n    // $message['FromUserName'] // 用户的 openid\n    // $message['MsgType'] // 消息类型：event, text....\n    return \"您好！欢迎使用 EasyWeChat\";\n});\n\n// 在 laravel 中：\n$response = $app->server->serve();\n\n// $response 为 `Symfony\\Component\\HttpFoundation\\Response` 实例\n// 对于需要直接输出响应的框架，或者原生 PHP 环境下\n$response->send();\n\n// 而 laravel 中直接返回即可：\n\nreturn $response;\n```\n\n这里我们使用 `push` 传入了一个 **闭包（[Closure](http://php.net/manual/en/class.closure.php)）**，该闭包接收一个参数 `$message` 为消息对象（类型取决于你的配置中 `response_type`），你可以在全局消息处理器中对消息类型进行筛选：\n\n```php\n$app->server->push(function ($message) {\n    switch ($message['MsgType']) {\n        case 'event':\n            return '收到事件消息';\n            break;\n        case 'text':\n            return '收到文字消息';\n            break;\n        case 'image':\n            return '收到图片消息';\n            break;\n        case 'voice':\n            return '收到语音消息';\n            break;\n        case 'video':\n            return '收到视频消息';\n            break;\n        case 'location':\n            return '收到坐标消息';\n            break;\n        case 'link':\n            return '收到链接消息';\n            break;\n        case 'file':\n            return '收到文件消息';\n        // ... 其它消息\n        default:\n            return '收到其它消息';\n            break;\n    }\n\n    // ...\n});\n```\n\n当然，因为这里 `push` 接收一个 [`callable`](http://php.net/manual/zh/language.types.callable.php) 的参数，所以你不一定要传入一个 Closure 闭包，你可以选择传入一个函数名，一个 `[$class, $method]` 或者 `Foo::bar` 这样的类型。\n\n某些情况，我们需要直接使用 `$message` 参数，那么怎么在 `push` 的闭包外调用呢？\n\n```php\n    $message = $server->getMessage();\n```\n>  注意：`$message` 的类型取决于你的配置中 `response_type`\n\n## 注册多个消息处理器\n\n有时候你可能需要对消息记日志，或者一系列的自定义操作，你可以注册多个 handler：\n\n```php\n$app->server->push(MessageLogHandler::class);\n$app->server->push(MessageReplyHandler::class);\n$app->server->push(OtherHandler::class);\n$app->server->push(...);\n```\n\n1. 最后一个非空返回值将作为最终应答给用户的消息内容，如果中间某一个 handler 返回值 false, 则将终止整个调用链，不会调用后续的 handlers。\n2. 传入的自定义 Handler 类需要实现 `\\EasyWeChat\\Kernel\\Contracts\\EventHandlerInterface`。\n\n## 注册指定消息类型的消息处理器\n\n我们想对特定类型的消息应用不同的处理器，可以在第二个参数传入类型筛选：\n\n> 注意，第二个参数必须是 `\\EasyWeChat\\Kernel\\Messages\\Message` 类的常量。\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Message;\n\n$app->server->push(ImageMessageHandler::class, Message::IMAGE); // 图片消息\n$app->server->push(TextMessageHandler::class, Message::TEXT); // 文本消息\n\n// 同时处理多种类型的处理器\n$app->server->push(MediaMessageHandler::class, Message::VOICE|Message::VIDEO|Message::SHORT_VIDEO); // 当消息为 三种中任意一种都可触发\n```\n\n## 请求消息的属性\n\n当你接收到用户发来的消息时，可能会提取消息中的相关属性，参考：\n\n请求消息基本属性(以下所有消息都有的基本属性)：\n\n>>  - `ToUserName`    接收方帐号（该公众号 ID）\n>>  - `FromUserName`  发送方帐号（OpenID, 代表用户的唯一标识）\n>>  - `CreateTime`    消息创建时间（时间戳）\n>>  - `MsgId`        消息 ID（64位整型）\n\n### 文本：\n\n>  - `MsgType`  text\n>  - `Content`  文本消息内容\n\n### 图片：\n\n>  - `MsgType`  image\n>  - `MediaId`  图片消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `PicUrl`   图片链接\n\n### 语音：\n\n>  - `MsgType`        voice\n>  - `MediaId`        语音消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `Format`         语音格式，如 amr，speex 等\n>  - `Recognition`  * 开通语音识别后才有\n\n  > 识别后，用户每次发送语音给公众号时，微信会在推送的语音消息XML数据包中，增加一个 `Recongnition` 字段\n\n### 视频：\n\n>  - `MsgType`       video\n>  - `MediaId`       视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `ThumbMediaId`  视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 小视频：\n\n>  - `MsgType`     shortvideo\n>  - `MediaId`     视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `ThumbMediaId`    视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 事件：\n\n>  - `MsgType`     event\n>  - `Event`       事件类型 （如：subscribe(订阅)、unsubscribe(取消订阅) ...， CLICK 等）\n\n#### 扫描带参数二维码事件\n>  - `EventKey`    事件KEY值，比如：qrscene_123123，qrscene_为前缀，后面为二维码的参数值\n>  - `Ticket`      二维码的 ticket，可用来换取二维码图片\n\n#### 上报地理位置事件\n>  - `Latitude`    23.137466   地理位置纬度\n>  - `Longitude`   113.352425  地理位置经度\n>  - `Precision`   119.385040  地理位置精度\n\n#### 自定义菜单事件\n>  - `EventKey`    事件KEY值，与自定义菜单接口中KEY值对应，如：CUSTOM_KEY_001, www.qq.com\n\n### 地理位置：\n\n>  - `MsgType`     location\n>  - `Location_X`  地理位置纬度\n>  - `Location_Y`  地理位置经度\n>  - `Scale`       地图缩放大小\n>  - `Label`       地理位置信息\n\n### 链接：\n\n>  - `MsgType`      link\n>  - `Title`        消息标题\n>  - `Description`  消息描述\n>  - `Url`          消息链接\n\n### 文件：\n\n  `MsgType`      file\n  `Title`        文件名\n  `Description`  文件描述，可能为null\n  `FileKey`      文件KEY\n  `FileMd5`      文件MD5值\n  `FileTotalLen` 文件大小，单位字节\n\n## 回复消息\n\n回复的消息可以为 `null`，此时 SDK 会返回给微信一个 \"SUCCESS\"，你也可以回复一个普通字符串，比如：`欢迎关注 overtrue.`，此时 SDK 会对它进行一个封装，产生一个 [`EasyWeChat\\Kernel\\Messages\\Text`](https://github.com/EasyWeChat/message/blob/master/src/Kernel/Messages/Text.php) 类型的消息并在最后的 `$app->server->serve();` 时生成对应的消息 XML 格式。\n\n如果你想返回一个自己手动拼的原生 XML 格式消息，请返回一个 [`EasyWeChat\\Kernel\\Messages\\Raw`](https://github.com/EasyWeChat/message/blob/master/src/Kernel/Messages/Raw.php) 实例即可。\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer)\n\n关于消息的使用，请参考 [`消息`](messages) 章节。\n"
  },
  {
    "path": "docs/src/4.x/official-account/shake-around.md",
    "content": "# 摇一摇周边\n\n\n摇一摇周边是微信在线下的全新功能, 为线下商户提供近距离连接用户的能力, 并支持线下商户向周边用户提供个性化营销、互动及信息推荐等服务。\n\n## 获取实例\n\n```php\n$shakearound = $app->shake_around;\n\n```\n\n## 说明\n\n> 特别提醒：\n1、下述所有的接口调用的方法参数都要严格按照方法参数前的类型传入相应类型的实参，否则可能会得到非预期的结果。\n2、涉及需要传入设备id（$deviceIdentifier）的参数时，该参数是一个以 `device_id` 或包含 `uuid` `major` `minor` 为key的关联数组。\n3、涉及需要传入设备id列表（$deviceIdentifiers）的参数时，该参数是一个二维数组，第一层为索引类型，第二层为关联类型（$deviceIdentifier）。\n\n```php\n// 参数 $deviceIdentifier 的实参形式：\n['device_id' => 10097]\n// 或\n[\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12102,\n]\n// 参数$deviceIdentifiers的实参形式：\n[\n    ['device_id' => 10097],\n    ['device_id' => 10098],\n]\n// 或\n[\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12102,\n    ],\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12103,\n    ]\n]\n```\n\n## 开通摇一摇周边\n\n> 提示：\n若不是做 [公众号第三方平台](https://open.weixin.qq.com/cgi-bin/frame?t=home/wx_plugin_tmpl&lang=zh_CN) 开发，建议直接在微信管理后台申请开通摇一摇周边功能。\n\n### 申请开通\n\n申请开通摇一摇周边功能。成功提交申请请求后，工作人员会在三个工作日内完成审核。若审核不通过，可以重新提交申请请求。若是审核中，请耐心等待工作人员审核，在审核中状态不能再提交申请请求。\n\n方法\n\n```php\n$shakearound->register($data)\n```\n\n>  注意：\n1、相关资质文件的图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material) 接口混淆。\n2、行业代码请务必传入**字符串**类型的实参，否则以数字0开头的行业代码将会被当成八进制数处理（将转换为十进制数），这可能不是期望的。\n\n### 查询审核状态\n\n查询已经提交的开通摇一摇周边功能申请的审核状态。在申请提交后，工作人员会在三个工作日内完成审核。\n\n方法\n\n```php\n$shakearound->status()\n```\n\n### 获取摇一摇的设备及用户信息\n\n获取设备信息，包括UUID、major、minor，以及距离、openID等信息。\n\n方法\n\n```php\n$shakearound->user($ticket);\n// 或者需要返回门店poi_id\n$shakearound->user($ticket, true);\n```\n\n## 设备管理\n\n### 申请设备 ID\n\n申请配置设备所需的UUID、Major、Minor。申请成功后返回批次ID，可用返回的批次ID通过“查询设备ID申请状态”接口查询目前申请的审核状态。\n一个公众账号最多可申请100000个设备ID，如需申请的设备ID数超过最大限额，请邮件至zhoubian@tencent.com，邮件格式如下：\n\n> 标题：申请提升设备ID额度\n内容：\n1、公众账号名称及appid（wx开头的字符串，在mp平台可查看）\n2、用途\n3、预估需要多少设备ID\n\n方法\n\n```php\n$shakearound->device->apply($data)\n```\n\n### 查询设备 ID 申请审核状态\n\n查询设备ID申请的审核状态。若单次申请的设备ID数量小于等于500个，系统会进行快速审核；若单次申请的设备ID数量大于500个，则在三个工作日内完成审核。\n\n方法\n\n```php\n$shakearound->device->status($applyId) // $applyId 批次ID，申请设备ID时所返回的批次ID\n```\n\n### 编辑设备信息\n\n> 仅能修改设备的备注信息。\n\n方法\n\n```php\n$shakearound->device->update(array $deviceIdentifier, string $comment)\n```\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$comment 设备的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->device->update(['device_id' => 10011], 'test');\n// 或\n$result = $shakearound->device->update(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                          'major' => 1002,\n                                          'minor' => 1223,\n], 'test');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result['errcode']) // 0\n```\n\n### 配置设备与门店/其他公众账号门店的关联关系\n\n关联本公众账号门店时，支持创建门店后直接关联在设备上，无需为审核通过状态，摇周边后台自动更新门店的最新信息和状态。\n关联其他公众账号门店时，支持设备关联其他公众账号的门店，门店需为审核通过状态。\n\n> 因为第三方门店不归属本公众账号，所以未保存到设备详情中，查询设备列表接口与获取摇周边的设备及用户信息接口不会返回第三方门店。\n\n方法\n\n```php\n$shakearound->device->bindPoi(array $deviceIdentifier, $poiId)\n\n//或者 绑定第三方\n$shakearound->device->bindThirdPoi(array $deviceIdentifier, $poiId, $poiAppId)\n```\n\n参数\n\n> $deviceIdentifier 设备 id，设备编号 device_id 或 UUID、major、minor 的关联数组，若二者都填，则以设备编号为优先\n$poiId 设备关联的门店 ID，关联门店后，在门店 1KM 的范围内有优先摇出信息的机会。当值为0时，将清除设备已关联的门店 ID\n$poiAppId 关联门店所归属的公众账号的 APP ID\n\n示例\n\n```php\n// 关联本公众账号门店\n$result = $shakearound->device->bindLocation(['device_id' => 10011], 1231);\n// 或\n$result = $shakearound->device->bindLocation([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n], 1231);\n\n// 关联其他公众账号门店\n// wxappid 为关联门店所归属的公众账号的 APP ID\n$result = $shakearound->device->bindThirdPoi(['device_id' => 10011], 1231, 'wxappid');\n\n// 或\n$result = $shakearound->device->bindThirdPoi([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n], 1231, 'wxappid');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 查询设备列表\n\n查询已有的设备 ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。\n\n### 根据设备id批量取回设备数据\n\n方法\n\n> $shakearound->device->listByIds(array $deviceIdentifiers)\n\n参数\n\n> $deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->device->listByIds([\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->device->listByIds([\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n   'major' => 10001,\n   'minor' => 12102,\n  ],\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n   'major' => 10001,\n   'minor' => 12103,\n  ]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"comment\": \"\",\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_id\": 0,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            },\n            {\n                \"comment\": \"\",\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_appid\":\"wxe3813f5d8c546fc7\"\n                \"poi_id\": 123,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            }\n        ],\n        \"total_count\": 151\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 分页批量取回设备数据\n\n方法\n\n```php\n$shakearound->device->list(int $lastId, int $count)\n```\n\n参数\n\n> $lastId 前一次查询列表末尾的设备编号 device_id，第一次查询 lastId 为 0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device->list(10097, 3);\n\n// 返回结果同上\n```\n\n### 根据申请时的批次 ID 分页批量取回设备数据\n\n方法\n\n> $shakearound->device->listByApplyId(int $applyId, int $lastId, int $count)\n\n参数\n\n> $applyId 批次ID，申请设备ID时所返回的批次ID\n$lastId 前一次查询列表末尾的设备编号device_id，第一次查询 lastId 为 0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device->listByApplyId(1231, 10097, 3);\n\n// 返回结果同上\n```\n\n## 页面管理\n\n### 新增页面\n\n新增摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。其中，图片必须为用素材管理接口上传至微信侧服务器后返回的链接。\n\n>\n图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material) 接口混淆。\n\n方法\n\n```php\n$shakearound->page->create($data)\n```\n\n参数\n\n> $title 在摇一摇页面展示的主标题，不超过6个汉字或12个英文字母\n$description 在摇一摇页面展示的副标题，不超过7个汉字或14个英文字母\n$pageUrl 点击进去的超链接\n$iconUrl 在摇一摇页面展示的图片。图片需先上传至微信侧服务器，用“素材管理-上传图片素材”接口上传图片，返回的图片URL再配置在此处\n$comment 可选，页面的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->page->create($data);\n\n/* 返回结果\n{\n   \"data\": {\n       \"page_id\": 28840\n   }\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 编辑页面信息\n\n编辑摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n方法\n\n```php\n$shakearound->page->update(int $pageId, array $data)\n```\n\n参数\n\n> $pageId 摇周边页面唯一ID\n$data 需要更新的信息\n\n示例\n\n```php\n$result = $shakearound->page->update(28840, [\n    'title' => '主标题',\n    'description' => '副标题',\n    //...\n]);\n```\n\n## 查询页面列表\n\n查询已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n### 根据页面id批量取回页面数据\n\n方法\n\n```php\n$shakearound->page->listByIds(array $pageIds)\n```\n\n参数\n\n> $pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->page->listByIds([28840, 28842]);\n\n/* 返回结果\n{\n   \"data\": {\n       \"pages\": [\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28840,\n               \"page_url\": \"http://xw.qq.com/testapi1\",\n               \"title\": \"测试1\"\n           },\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28842,\n               \"page_url\": \"http://xw.qq.com/testapi2\",\n               \"title\": \"测试2\"\n           }\n       ],\n       \"total_count\": 2\n   },\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 分页批量取回页面数据\n\n方法\n\n```php\n$shakearound->page->list(int $begin, int $count)\n```\n\n参数\n\n> $begin 页面列表的起始索引值\n$count 待查询的页面数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->page->list(0,2);\n\n// 返回结果同上\n```\n\n### 删除页面\n\n删除已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n>\n只有页面与设备没有关联关系时，才可被删除。\n\n方法\n\n```php\n$shakearound->page->delete(int $pageId)\n```\n\n参数\n\n> $pageId 页面的id\n\n示例\n\n```php\n$result = $shakearound->page->delete(34567);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 素材管理\n\n上传在摇一摇功能中需使用到的图片素材，素材保存在微信侧服务器上。图片格式限定为：jpg,jpeg,png,gif。\n若图片为在摇一摇页面展示的图片，则其素材为 `icon` 类型的图片，图片大小建议 `120px*120 px` ，限制不超过 `200 px *200 px` ，图片需为 `正方形` 。\n若图片为申请开通摇一摇周边功能需要上传的资质文件图片，则其素材为 `license` 类型的图片，图片的文件大小不超过 `2MB` ，尺寸不限，形状不限。\n\n方法\n\n> $shakearound->material->uploadImage(string $path [, string $type = 'icon'])\n\n参数\n\n> $path 图片所在路径\n$type 可选，值为icon或license\n\n示例\n\n```php\n$result = $shakearound->material->uploadImage(__DIR__ . '/stubs/image.jpg');\n\n/* 返回结果\n{\n    \"data\": {\n        \"pic_url\": http://shp.qpic.cn/wechat_shakearound_pic/0/1428377032e9dd2797018cad79186e03e8c5aec8dc/120\"\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 管理设备与页面的关系\n\n通过接口申请的设备ID，需先配置页面，若未配置页面，则摇不出页面信息。\n\n### 配置设备与页面的关联关系\n\n配置完成后，在此设备的信号范围内，即可摇出关联的页面信息。\n若设备配置多个页面，则随机出现页面信息。一个设备最多可配置30个关联页面。\n\n>\n1、配置时传入该设备需要关联的页面的id列表，该设备原有的关联关系将被直接清除。\n2、页面的id列表允许为空（**传入空数组**），当页面的id列表为空时则会清除该设备的所有关联关系。\n\n方法\n\n> $shakearound->relation->bindPage(array $deviceIdentifier, array $pageIds)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->relation->bindPage(['device_id' => 10011], [12345, 23456, 334567]);\n// 或\n$result = $shakearound->relation->bindPage(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                              'major' => 1002,\n                                              'minor' => 1223,\n], [12345, 23456, 334567]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n### 查询设备与页面的关联关系\n\n#### 查询指定设备所关联的页面\n\n根据设备ID或完整的UUID、Major、Minor查询该设备所关联的所有页面信息\n\n方法\n\n> $shakearound->relation->listByDeviceId(array $deviceIdentifier [, boolean $raw = false])\n\n>\n该方法默认对返回的数据进行处理后返回一个包含页面id的索引数组。若要返回和 `getDeviceByPageId` 方法类似的数据，请传入 `true` 作为第二个参数。\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$raw 可选，当为true时，返回值和getDeviceByPageId方法类似，否则返回页面的id列表（索引数组，无关联时为空数组）\n\n示例\n\n```php\n$result = $shakearound->relation->listByDeviceId(['device_id' => 10011]);\n// 或\n$result = $shakearound->relation->listByDeviceId([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n]);\n\n// 返回结果\nvar_dump($result) // [50054,50055]\n```\n\n##### 查询指定页面所关联的设备\n\n指定页面ID分页查询该页面所关联的所有的设备信息\n\n方法\n\n> $shakearound->relation->listByPageId(int $pageId, int $begin, int $count)\n\n参数\n\n> $pageId 指定的页面id\n$begin 关联关系列表的起始索引值\n$count 待查询的关联关系数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->relation->listByPageId(50054, 0, 3);\n\n/* 返回结果\n{\n  \"data\": {\n      \"relations\": [\n          {\n              \"device_id\": 797994,\n              \"major\": 10001,\n              \"minor\": 10023,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          },\n          {\n              \"device_id\": 797995,\n              \"major\": 10001,\n              \"minor\": 10024,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          }\n      ],\n      \"total_count\": 2\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 摇一摇数据统计\n\n> 此接口无法获取当天的数据，最早只能获取前一天的数据。\n由于系统在凌晨处理前一天的数据，太早调用此接口可能获取不到数据，建议在早上8：00之后调用此接口。\n\n### 以设备为维度的数据统计\n\n查询单个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n>\n查询的最长时间跨度为 30 天。只能查询最近 90 天的数据。\n\n方法\n\n> $shakearound->stats->deviceSummary(array $deviceIdentifier, int $beginDate, int $endDate)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats->deviceSummary(['device_id' => 10011], 1425052800, 1425139200);\n// 或\n$result = $shakearound->stats->deviceSummary(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                'major' => 1002,\n                                                'minor' => 1223,\n], 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 批量查询设备统计数据\n\n查询指定时间商家帐号下的每个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n> 只能查询最近90天内的数据，且一次只能查询一天。\n\n>\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的设备，不在结果列表中返回。\n\n方法\n\n```php\n$shakearound->stats->devicesSummary(int $timestamp, int $pageIndex)\n```\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats->devicesSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            },\n            {\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            }\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 以页面为维度的数据统计\n\n查询单个页面通过摇周边摇出来的人数、次数，点击摇周边页面的人数、次数\n\n> 注意：\n查询的最长时间跨度为30天。只能查询最近90天的数据。\n\n方法\n\n```php\n$shakearound->stats->pageSummary(int $pageId, int $beginDate, int $endDate);\n```\n\n参数\n\n> $pageId 指定页面的页面ID\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats->pageSummary(12345, 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n### 批量查询页面统计数据\n\n查询指定时间商家帐号下的每个页面进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n>\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的页面，不在结果列表中返回。\n\n方法\n\n```php\n$shakearound->stats->pagesSummary(int $timestamp, int $pageIndex);\n```\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats->pagesSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"pages\": [\n            {\n                \"page_id\":1234\n                \"click_pv\": 1,\n                \"click_uv\": 3,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n            {\n                \"page_id\":5678\n                \"click_pv\": 1,\n                \"click_uv\": 2,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 设备分组管理\n\n调用H5页面获取设备信息 JS API接口，需要先把设备分组，微信客户端只会返回已在分组中的设备信息。\n\n### 新增分组\n\n新建设备分组，每个帐号下最多只有1000个分组。\n\n方法\n\n```php\n$shakearound->group->create(string $name)\n\n参数\n\n> $name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group->create('test');\n\n/* 返回结果\n{\n  \"data\": {\n      \"group_id\" : 123,\n      \"group_name\" : \"test\"\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 编辑分组信息\n\n编辑设备分组信息，目前只能修改分组名。\n\n方法\n\n> $shakearound->group->update(int $groupId, string $name)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group->update(123, 'newName');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 删除分组\n\n删除设备分组，若分组中还存在设备，则不能删除成功。需把设备移除以后，才能删除。\n\n> 在执行删除前，最好先使用 `get` 方法查询分组详情，若分组内有设备，先使用 `removeDevices` 方法移除。\n\n方法\n\n```php\n$shakearound->group->delete(int $groupId)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n\n示例\n\n```php\n$result = $shakearound->group->delete(123);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 查询分组列表\n\n查询账号下所有的分组。\n\n方法\n\n```php\n$shakearound->group->list(int $begin, int $count)\n```\n\n参数\n\n> $begin 分组列表的起始索引值\n$count 待查询的分组数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group->list(0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"groups\":[\n            {\n                \"group_id\" : 123,\n                \"group_name\" : \"test1\"\n            },\n            {\n                \"group_id\" : 124,\n                \"group_name\" : \"test2\"\n            }\n        ],\n        \"total_count\": 100\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 查询分组详情\n\n查询分组详情，包括分组名，分组id，分组里的设备列表。\n\n方法\n\n```php\n$shakearound->group->get(int $groupId, int $begin, int $count)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$begin 分组里设备的起始索引值\n$count 待查询的分组里设备的数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group->get(123, 0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"group_id\" : 123,\n        \"group_name\" : \"test\",\n        \"total_count\": 100,\n        \"devices\" :[\n            {\n                \"device_id\" : 123456,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10001,\n                \"comment\" : \"test device1\",\n                \"poi_id\" : 12345,\n            },\n            {\n                \"device_id\" : 123457,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10002,\n                \"comment\" : \"test device2\",\n                \"poi_id\" : 12345,\n            }\n        ]\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 添加设备到分组\n\n添加设备到分组，每个分组能够持有的设备上限为10000，并且每次添加操作的添加上限为1000。\n\n> 只有在摇周边申请的设备才能添加到分组。\n\n方法\n\n> $shakearound->group->addDevices(int $groupId, array $deviceIdentifiers)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group->addDevices(123, [\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n\n// 或\n$result = $shakearound->group->addDevices(123, [\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12102,\n  ],\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12103,\n  ]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 从分组中移除设备\n\n从分组中移除设备，每次删除操作的上限为 1000。\n\n方法\n\n```php\n$shakearound->group->removeDevices(int $groupId, array $deviceIdentifiers)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group->removeDevices(123, [\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->group->removeDevices(123, [\n    [\n      'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n      'major' => 10001,\n      'minor' => 12102,\n    ],\n    [\n      'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n      'major' => 10001,\n      'minor' => 12103,\n    ]\n]);\n```\n\n## 摇一摇事件通知\n\n用户进入摇一摇界面，在“周边”页卡下摇一摇时，微信会把这个事件推送到开发者填写的URL（登录公众平台进入开发者中心设置）。推送内容包含摇一摇时“周边”页卡展示出来的页面所对应的设备信息，以及附近最多五个属于该公众账号的设备的信息。当摇出列表时，此事件不推送。\n\n> 摇一摇事件的事件类型：ShakearoundUserShake\n\n关于事件的处理请移步： [事件](events)\n\n"
  },
  {
    "path": "docs/src/4.x/official-account/store.md",
    "content": "# 门店小程序\n\n## 拉取门店小程序类目\n\n```php\n$app->store->categories();\n```\n\n## 创建门店小程序\n\n> 说明：创建门店小程序提交后需要公众号管理员确认通过后才可进行审核。如果主管理员 24 小时超时未确认，才能再次提交。\n\n```php\n$app->store->createMerchant($baseInfo);\n```\n\n> - `$baseInfo` 为门店小程序的基本信息数组，**`qualification_list` 字段为类目相关证件的临时素材 `mediaid` 如果 `second_catid` 对应的 `sensitive_type` 为 1 ，则 `qualification_list` 字段需要填 支持 0~5 个 `mediaid`，例如 `mediaid1`。`headimg_mediaid` 字段为头像 --- 临时素材 `mediaid`。`mediaid` 用现有的 `media/upload` 接口得到的,获取链接： [临时素材](../basic-services/media.md) ( 支持 PNG\\JPEG\\JPG\\GIF 格式的图片，后续加上其他格式)**\n\n示例：\n\n```php\n\n$info = [\n    \"first_catid\"        => 476, //categories 接口获取的一级类目id\n    \"second_catid\"       => 477, //categories 接口获取的二级类目id\n    \"qualification_list\" =>  \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"headimg_mediaid\"    => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"nickname\"           => \"hardenzhang308\",\n    \"intro\"              => \"hardenzhangtest\",\n    \"org_code\"           => \"\",\n    \"other_files\"        => \"\"\n];\n\n$result = $app->store->createMerchant($info);\n```\n\n> 注意：创建门店小程序的审核结果,会以事件形式推送给商户填写的回调 URL\n\n## 查询门店小程序审核结果\n\n```php\n$app->store->getStatus($baseInfo);\n```\n\n## 修改门店小程序信息\n\n```php\n$app->store->updateMerchant($data);\n```\n\n> - `$data` 需要更新的部分数据，目前仅支持门店头像和门店小程序介绍，**若有填写内容则为覆盖更新,若无内容则视为不修改,维持原有内容。`headimg_mediaid`、`intro` 字段参考创建门店小程序**\n\n示例：\n\n```php\n$data = [\n    \"headimg_mediaid\" => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"intro\"           => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n];\n\n$result = $app->store->updateMerchant($data);\n```\n\n## 从腾讯地图拉取省市区信息\n\n```php\n$app->store->districts();\n```\n\n## 在腾讯地图中搜索门店\n\n```php\n$app->store->searchFromMap($districtId, $keyword);\n```\n\n> - `$districtId` 为从腾讯地图拉取的地区 `id`\n> - `$keyword` 为搜索的关键词\n\n## 在腾讯地图中创建门店\n\n```php\n$app->store->createFromMap($baseInfo);\n```\n\n示例：\n\n```php\n$baseInfo = [\n    \"name\"       => \"hardenzhang\",\n    \"longitude\"  => \"113.323753357\",\n    \"latitude\"   => \"23.0974903107\",\n    \"province\"   => \"广东省\",\n    \"city\"       => \"广州市\",\n    \"district\"   => \"海珠区\",\n    \"address\"    => \"TIT\",\n    \"category\"   => \"类目1:类目2\",\n    \"telephone\"  => \"12345678901\",\n    \"photo\"      => \"http://mmbiz.qpic.cn/mmbiz_png/tW66AWE2K6ECFPcyAcIZTG8RlcR0sAqBibOm8gao5xOoLfIic9ZJ6MADAktGPxZI7MZLcadZUT36b14NJ2cHRHA/0?wx_fmt=png\",\n    \"license\"    => \"http://mmbiz.qpic.cn/mmbiz_png/tW66AWE2K6ECFPcyAcIZTG8RlcR0sAqBibOm8gao5xOoLfIic9ZJ6MADAktGPxZI7MZLcadZUT36b14NJ2cHRHA/0?wx_fmt=png\",\n    \"introduct\"  => \"test\",\n    \"districtid\" => \"440105\",\n];\n```\n\n> - `$baseInfo`: 门店相关信息\n\n> 事件推送 --- 腾讯地图中创建门店的审核结果。腾讯地图审核周期为 3 个工作日，请在期间内留意审核结果事件推送。提交后未当即返回事件推送即为审核中，请耐心等待。\n\n## 添加门店\n\n```php\n$app->store->create($baseInfo);\n```\n\n示例：\n\n```php\n$baseInfo = [\n    \"poi_id\"             => \"\",\n    \"map_poi_id\"         => \"2880741500279549033\",\n    \"pic_list\"           => \"['list' => ['http://mmbiz.qpic.cn/mmbiz_jpg/tW66AWvE2K4EJxIYOVpiaGOkfg0iayibiaP2xHOChvbmKQD5uh8ymibbEKlTTPmjTdQ8ia43sULLeG1pT2psOfPic4kTw/0?wx_fmt=jpeg']]\",\n    \"contract_phone\"     => \"1111222222\",\n    \"credential\"         => \"22883878-0\",\n    \"qualification_list\" => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\"\n];\n```\n\n> - `$baseInfo`: 门店相关信息。`pic_list` 门店图片，可传多张图片 `pic_list`\n\n> 事件推送 - 创建门店的审核结果\n\n## 更新门店信息\n\n```php\n$app->store->update($baseInfo);\n```\n\n> - `$baseInfo`: 门店相关信息。\n\n> 果要更新门店的图片，实际相当于走一次重新为门店添加图片的流程，之前的旧图片会全部废弃。并且如果重新添加的图片中有与之前旧图片相同的，此时这个图片不需要重新审核。\n"
  },
  {
    "path": "docs/src/4.x/official-account/template_message.md",
    "content": "# 模板消息\n\n模板消息仅用于公众号向用户发送重要的服务通知，只能用于符合其要求的服务场景中，如信用卡刷卡通知，商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。\n\n## 修改账号所属行业\n\n```php\n$app->template_message->setIndustry($industryId1, $industryId2);\n```\n\n## 获取支持的行业列表\n\n```php\n$app->template_message->getIndustry();\n```\n\n## 添加模板\n\n在公众号后台获取 `$shortId` 并添加到账户。\n\n```php\n$app->template_message->addTemplate($shortId);\n```\n\n## 获取所有模板列表\n\n```php\n$app->template_message->getPrivateTemplates();\n```\n\n## 删除模板\n\n```php\n$app->template_message->deletePrivateTemplate($templateId);\n```\n\n## 发送模板消息\n\n```php\n$app->template_message->send([\n        'touser' => 'user-openid',\n        'template_id' => 'template-id',\n        'url' => 'https://easywechat.com',\n        'miniprogram' => [\n                'appid' => 'xxxxxxx',\n                'pagepath' => 'pages/xxx',\n        ],\n        'data' => [\n            'key1' => 'VALUE',\n            'key2' => 'VALUE2',\n            ...\n        ],\n    ]);\n```\n\n> 如果 url 和 miniprogram 字段都传，会优先跳转小程序。\n\n## 发送一次性订阅消息\n\n```php\n$app->template_message->sendSubscription([\n        'touser' => 'user-openid',\n        'template_id' => 'template-id',\n        'url' => 'https://easywechat.com',\n        'scene' => 1000,\n        'data' => [\n            'key1' => 'VALUE',\n            'key2' => 'VALUE2',\n            ...\n        ],\n    ]);\n```\n\n> 如果你想为发送的内容字段指定颜色，你可以将 \"data\" 部分写成下面 4 种不同的样式，不写 `color` 将会是默认黑色：\n\n```php\n'data' => [\n    'foo' => '你好',  // 不需要指定颜色\n    'bar' => ['你好', '#F00'], // 指定为红色\n    'baz' => ['value' => '你好', 'color' => '#550038'], // 与第二种一样\n    'zoo' => ['value' => '你好'], // 与第一种一样\n]\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/tutorial.md",
    "content": "# 快速开始\n\n在我们已经安装完成后，即可很快的开始使用它了，当然你还是有必要明白 PHP 基本知识，如命名空间等，我这里就不赘述了。\n\n我们以完成服务器端验证与接收响应用户发送的消息为例来演示,首先你有必要了解一下微信交互的运行流程：\n\n```\n                                 +-----------------+                       +---------------+\n+----------+                     |                 |    POST/GET/PUT       |               |\n|          | ------------------> |                 | ------------------->  |               |\n|   user   |                     |  wechat server  |                       |  your server  |\n|          | < - - - - - - - - - |                 |                       |               |\n+----------+                     |                 | <- - - - - - - - - -  |               |\n                                 +-----------------+                       +---------------+\n\n```\n\n那么我们要做的就是图中 **微信服务器把用户消息转到我们的自有服务器（虚线返回部分）** 后的处理过程。\n\n## 服务端验证\n\n在微信接入开始有一个 “服务器验证” 的过程，这一步呢，其实就是微信服务器向我们服务器发起一个请求（上图实线部分），传了一个名称为 `echostr` 的字符串过来，我们只需要原样返回就好了。\n\n你也知道，微信后台只能填写一个服务器地址，所以 **服务器验证** 与 **消息的接收与回复**，都在这一个链接内完成交互。\n\n考虑到这些，我已经把验证这一步给封装到 SDK 里了，你可以完全忽略这一步。\n\n下面我们来配置一个基本的服务端，这里假设我们自己的服务器域名叫 `easywechat.com`，我们在服务器上准备这么一个文件`server.php`:\n\n// server.php\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0xxx',\n    'secret' => 'f1c242f4f28f735d4687abb469072xxx',\n    'token' => 'TestToken',\n    'response_type' => 'array',\n    //...\n];\n\n$app = Factory::officialAccount($config);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send();exit; // Laravel 里请使用：return $response;\n\n```\n\n> :heart: 安全模式下请一定要配置 `aes_key`\n\n一个服务端带验证功能的代码已经完成，当然没有对消息做处理，别着急，后面我们再讲。\n\n我们先来分析上面的代码：\n\n```php\n// 引入我们的主项目工厂类。\nuse EasyWeChat\\Factory;\n\n// 一些配置\n$config = [...];\n\n// 使用配置来初始化一个公众号应用实例。\n$app = Factory::officialAccount($config);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); exit; // Laravel 里请使用：return $response;\n```\n\n最后这一行我有必要详细讲一下：\n\n> 1.  我们的 `$app->server->serve()` 就是执行服务端业务了，那么它的返回值是一个 `Symfony\\Component\\HttpFoundation\\Response` 实例。\n> 2.  我这里是直接调用了它的 `send()` 方法，它就是直接输出（echo）了，我们在一些框架就不能直接输出了，那你就直接拿到 Response 实例后做相应的操作即可，比如 Laravel 里你就可以直接 `return $app->server->serve();`\n\nOK, 有了上面的代码，那么请你按 **[微信官方的接入指引](http://mp.weixin.qq.com/wiki/)** 在公众号后台完成配置并启用，并相应修改上面的 `$config` 的相关配置。\n\n> URL 就是我们的 `http://easywechat.com/server.php`，这里我是举例哦，你可不要填写我的域名。\n\n这样，点击提交验证就 OK 了。\n\n> :heart: 请一定要将微信后台的开发者模式 “**启用**” ！！！！！！看到红色 “**停用**” 才真正的是启用了。\n> 最后，请不要用浏览器访问这个地址，它是给微信服务器访问的，不是给人访问的。\n\n## 接收 & 回复用户消息\n\n那服务端验证通过了，我们就来试一下接收消息吧。\n\n> 在刚刚上面代码最后一行 `$app->server->serve()->send();` 前面，我们调用 `$app->server` 的 `push()` 方法来注册一个消息处理器，这里用到了 **[PHP 闭包](http://php.net/manual/zh/functions.anonymous.php)** 的知识，如果你不熟悉赶紧补课去。\n\n```php\n// ...\n\n$app->server->push(function ($message) {\n    return \"您好！欢迎使用 EasyWeChat!\";\n});\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); // Laravel 里请使用：return $response;\n\n```\n\n> 注意：send() 方法里已经包含 echo 了，请不要再加 echo 在前面。\n\n好吧，打开你的微信客户端，向你的公众号发送任意一条消息，你应该会收到回复：`您好！欢迎使用 EasyWeChat!`。\n\n> 到了“你的公众号暂时无法提供服务” ？好，那检查一下你的日志吧，日志在哪儿？我们的配置里写了日志路径了(`__DIR__.'/wechat.log'`)。 没有这个文件？看看权限哦。\n\n> 注意：在 Laravel 框架应用时，因 POST 请求默认会有 CSRF 验证，所以需要在 `App\\Http\\Middleware\\VerifyCsrfToken` 的 `except` 数组中添加微信请求，否则会提示“你的公众号暂时无法提供服务”。\n\n一个基本的服务端验证就完成了。\n\n## 总结\n\n1. 所有的应用服务都通过主入口 `EasyWeChat\\Factory` 类来创建：\n\n```php\n\n// 公众号\n$app = Factory::officialAccount($config);\n\n// 小程序\n$app = Factory::miniProgram($config);\n\n// 开放平台\n$app = Factory::openPlatform($config);\n\n// 企业微信\n$app = Factory::work($config);\n\n// 企业微信开放平台\n$app = Factory::openWork($config);\n\n// 微信支付\n$app = Factory::payment($config);\n\n```\n\n## 最后\n\n希望你在使用本 SDK 的时候如果你发现 SDK 的不足，欢迎提交 PR 或者给我[提建议 & 报告问题](https://github.com/overtrue/wechat/issues)。\n"
  },
  {
    "path": "docs/src/4.x/official-account/user-tag.md",
    "content": "# 用户标签\n\n## 获取所有标签\n\n```php\n$app->user_tag->list();\n```\n\n示例：\n\n```php\n$tags = $app->user_tag->list();\n\n// {\n//     \"tags\": [\n//         {\n//             \"id\": 0,\n//             \"name\": \"标签1\",\n//             \"count\": 72596\n//         },\n//         {\n//             \"id\": 1,\n//             \"name\": \"标签2\",\n//             \"count\": 36\n//         },\n//         ...\n//     ]\n// }\n```\n\n## 创建标签\n\n```php\n$app->user_tag->create($name);\n```\n\n示例：\n\n```php\n$app->user_tag->create('测试标签');\n```\n\n## 修改标签信息\n\n```php\n$app->user_tag->update($tagId, $name);\n```\n\n示例：\n\n```php\n$app->user_tag->update(12, \"新的名称\");\n```\n\n## 删除标签\n\n```php\n$app->user_tag->delete($tagId);\n```\n\n## 获取指定 openid 用户所属的标签\n\n```php\n$userTags = $app->user_tag->userTags($openId);\n//\n// {\n//     \"tagid_list\":[\"标签1\",\"标签2\"]\n// }\n```\n\n## 获取标签下用户列表\n\n```php\n$app->user_tag->usersOfTag($tagId, $nextOpenId = '');\n// $nextOpenId：第一个拉取的OPENID，不填默认从头开始拉取\n\n// {\n//   \"count\":2, // 这次获取的粉丝数量\n//   \"data\":{ // 粉丝列表\n//      \"openid\":[\n//          \"ocYxcuAEy30bX0NXmGn4ypqx3tI0\",\n//          \"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"\n//      ]\n//   },\n//   \"next_openid\":\"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"//拉取列表最后一个用户的openid\n// }\n```\n\n## 批量为用户添加标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$app->user_tag->tagUsers($openIds, $tagId);\n```\n\n\n## 批量为用户移除标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$app->user_tag->untagUsers($openIds, $tagId);\n```\n"
  },
  {
    "path": "docs/src/4.x/official-account/user.md",
    "content": "# 用户\n\n用户信息的获取是微信开发中比较常用的一个功能了，以下所有的用户信息的获取与更新，都是**基于微信的 `openid` 的，并且是已关注当前账号的**，其它情况可能无法正常使用。\n\n## 获取用户信息\n\n获取单个：\n\n```php\n$user = $app->user->get($openId);\n```\n\n获取多个：\n\n```php\n$users = $app->user->select([$openId1, $openId2, ...]);\n```\n\n## 获取用户列表\n\n```php\n$app->user->list($nextOpenId = null);  // $nextOpenId 可选\n```\n\n示例：\n\n```php\n $users = $app->user->list();\n\n// result\n {\n  \"total\": 2,\n  \"count\": 2,\n  \"data\": {\n    \"openid\": [\n      \"OPENID1\",\n      \"OPENID2\"\n    ]\n  },\n  \"next_openid\": \"NEXT_OPENID\"\n}\n```\n\n## 修改用户备注\n\n```php\n$app->user->remark($openId, $remark); // 成功返回boolean\n```\n\n示例：\n\n```php\n$app->user->remark($openId, \"僵尸粉\");\n```\n\n## 拉黑用户\n\n```php\n$app->user->block('openidxxxxx');\n// 或者多个用户\n$app->user->block(['openid1', 'openid2', 'openid3', ...]);\n```\n\n## 取消拉黑用户\n\n```php\n$app->user->unblock('openidxxxxx');\n// 或者多个用户\n$app->user->unblock(['openid1', 'openid2', 'openid3', ...]);\n```\n\n## 获取黑名单\n\n```php\n$app->user->blacklist($beginOpenid = null); // $beginOpenid 可选\n```\n\n## 账号迁移 openid 转换\n\n账号迁移请从这里了解：https://kf.qq.com/product/weixinmp.html#hid=2488\n\n微信用户关注不同的公众号，对应的 OpenID 是不一样的，迁移成功后，粉丝的 OpenID 以目标帐号（即新公众号）对应的 OpenID 为准。但开发者可以通过开发接口转换 OpenID，开发文档可以参考：\n提供一个 openid 转换的 API 接口，当帐号迁移后，可以通过该接口：\n\n1. 将原帐号粉丝的 openid 转换为新帐号的 openid。\n2. 将有授权关系用户的 openid 转换为新帐号的 openid。\n3. 将卡券关联用户的 openid 转换为新帐号的 openid。\n\n> - ◆ 原帐号：准备要迁移的帐号，当审核完成且管理员确认后即被回收。\n> - ◆ 新帐号：用来接纳粉丝的帐号。新帐号在整个流程中均能正常使用。\n\n一定要按照下面的步骤来操作。\n\n1. 一定要在原帐号被冻结之前，最好是准备提交审核前，获取原帐号的用户列表。如果没有原帐号的用户列表，用不了转换工具。如果原账号被回收，这时候也没办法调用接口获取用户列表。\n\n如何获取用户列表见这里：https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840\n\n2. 转换 openid 的 API 接口如下，可在帐号迁移审核完成后开始调用，并最多保留 15 天。若帐号迁移没完成，调用时无返回结果或报错。帐号迁移 15 天后，该转换接口将会失效、无法拉取到数据。\n\n```php\n$app->user->changeOpenid($oldAppId, $openidList);\n```\n\n返回值样例：\n\n```json\n{\n  \"errcode\": 0,\n  \"errmsg\": \"ok\",\n  \"result_list\": [\n    {\n      \"ori_openid\": \"oEmYbwN-n24jxvk4Sox81qedINkQ\",\n      \"new_openid\": \"o2FwqwI9xCsVadFah_HtpPfaR-X4\",\n      \"err_msg\": \"ok\"\n    },\n    {\n      \"ori_openid\": \"oEmYbwH9uVd4RKJk7ZZg6SzL6tTo\",\n      \"err_msg\": \"ori_openid error\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/src/4.x/open-platform/authorizer-delegate.md",
    "content": "# 代授权方实现业务\n\n> 授权方已经把公众号、小程序授权给你的开放平台第三方平台了，接下来的代授权方实现业务只需一行代码即可获得授权方实例。\n\n## 实例化\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // ...\n];\n\n$openPlatform = Factory::openPlatform($config);\n```\n\n### 获取授权方实例\n\n```php\n// 代公众号实现业务\n$officialAccount = $openPlatform->officialAccount(string $appId, string $refreshToken);\n// 代小程序实现业务\n$miniProgram = $openPlatform->miniProgram(string $appId, string $refreshToken);\n```\n\n> $appId 为授权方公众号 APPID，非开放平台第三方平台 APPID\n>\n> $refreshToken 为授权方的 refresh_token，可通过 [获取授权方授权信息](#) 接口获得。\n\n### 帮助授权方管理开放平台账号\n\n```php\n// 代公众号实现业务\n$account = $officialAccount->account;\n// 代小程序实现业务\n$account = $miniProgram->account;\n\n// 创建开放平台账号\n// 并绑定公众号或小程序\n$result = $account->create();\n\n// 将公众号或小程序绑定到指定开放平台帐号下\n$result = $account->bindTo($openAppId);\n\n// 将公众号/小程序从开放平台帐号下解绑\n$result = $account->unbindFrom($openAppid);\n\n// 获取公众号/小程序所绑定的开放平台帐号\n$result = $account->getBinding();\n```\n\n> 授权第三方平台注册的开放平台帐号只可用于获取用户 unionid 实现用户身份打通。\n>\n>  第三方平台不可操作（包括绑定/解绑）通过 open.weixin.qq.com 线上流程注册的开放平台帐号。\n>\n>  公众号只可将此权限集授权给一个第三方平台，授权互斥。\n\n接下来的 API 调用等操作和公众号、小程序的开发一致，请移步到[公众号](#)或[小程序](#)开发章节继续进行开发吧。\n\n### 代码示例\n\n```php\n// 假设你的公众号消息与事件接收 URL 为：https://easywechat.com/$APPID$/callback ...\n\nRoute::post('{appId}/callback', function ($appId) {\n    // ...\n    $officialAccount = $openPlatform->officialAccount($appId);\n    $server = $officialAccount->server; // ❗️❗️  这里的 server 为授权方的 server，而不是开放平台的 server，请注意！！！\n\n    $server->push(function () {\n        return 'Welcome!';\n    });\n\n    return $server->serve();\n});\n\n// 调用授权方业务例子\nRoute::get('how-to-use', function () {\n    $officialAccount = $openPlatform->officialAccount('已授权的公众号 APPID', 'Refresh-token');\n    // 获取用户列表：\n    $officialAccount->user->list();\n\n    $miniProgram = $openPlatform->miniProgram('已授权的小程序 APPID', 'Refresh-token');\n    // 根据 code 获取 session\n    $miniProgram->auth->session('js-code');\n    // 其他同理\n});\n```\n"
  },
  {
    "path": "docs/src/4.x/open-platform/index.md",
    "content": "# 微信开放平台第三方平台\n\n此页涉及接口信息与说明请参见：[授权流程技术说明 - 官方文档](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=)\n\n# 微信开放平台第三方平台\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Factory;\n\n$config = [\n  'app_id'   => '开放平台第三方平台 APPID',\n  'secret'   => '开放平台第三方平台 Secret',\n  'token'    => '开放平台第三方平台 Token',\n  'aes_key'  => '开放平台第三方平台 AES Key'\n];\n\n$openPlatform = Factory::openPlatform($config);\n```\n\n## 获取用户授权页 URL\n\n```php\n$openPlatform->getPreAuthorizationUrl('https://easywechat.com/callback'); // 传入回调URI即可\n```\n\n## 使用授权码换取接口调用凭据和授权信息\n\n在用户在授权页授权流程完成后，授权页会自动跳转进入回调URI，并在URL参数中返回授权码和过期时间，如：(https://easywechat.com/callback?auth_code=xxx&expires_in=600)\n\n```php\n$openPlatform->handleAuthorize(string $authCode = null);\n```\n\n> $authCode 不传的时候会获取 url 中的 auth_code 参数值\n\n## 获取授权方的帐号基本信息\n\n```php\n$openPlatform->getAuthorizer(string $appId);\n```\n\n## 获取授权方的选项设置信息\n\n```php\n$openPlatform->getAuthorizerOption(string $appId, string $name);\n```\n\n## 设置授权方的选项信息\n\n```php\n$openPlatform->setAuthorizerOption(string $appId, string $name, string $value);\n```\n\n> 该API用于获取授权方的公众号或小程序的选项设置信息，如：地理位置上报，语音识别开关，多客服开关。注意，获取各项选项设置信息，需要有授权方的授权，详见权限集说明。\n\n\n## 获取已授权的授权方列表\n\n```php\n$openPlatform->getAuthorizers(int $offset = 0, int $count = 500)\n```\n"
  },
  {
    "path": "docs/src/4.x/open-platform/server.md",
    "content": "# 服务端\n\n## 第三方平台推送事件\n\n公众号第三方平台推送的有四个事件：\n\n> 如已经授权的公众号、小程序再次进行授权，而未修改已授权的权限的话，是没有相关事件推送的。\n\n​\t授权成功 `authorized`\n\n​\t授权更新 `updateauthorized`\n\n​\t授权取消 `unauthorized`\n\n​\tVerifyTicket  `component_verify_ticket`\n\nSDK 默认会处理事件 `component_verify_ticket` ，并会缓存 `verify_ticket` 所以如果你暂时不需要处理其他事件，直接这样使用即可：\n\n```php\n$server = $openPlatform->server;\n\nreturn $server->serve();\n```\n\n## 自定义消息处理器\n\n> *消息处理器详细说明见公众号开发 - 服务器一节*\n\n```php\nuse EasyWeChat\\OpenPlatform\\Server\\Guard;\n\n$server = $openPlatform->server;\n\n// 处理授权成功事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_AUTHORIZED);\n\n// 处理授权更新事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_UPDATE_AUTHORIZED);\n\n// 处理授权取消事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_UNAUTHORIZED);\n```\n\n### 示例（Laravel 框架）\n\n```php\n// 假设你的开放平台第三方平台设置的授权事件接收 URL 为: https://easywechat.com/open-platform （其他事件推送同样会推送到这个 URL）\nRoute::post('open-platform', function () { // 关闭 CSRF\n    // $openPlatform 为你实例化的开放平台对象，此处省略实例化步骤\n    return $openPlatform->server->serve(); // Done!\n});\n\n// 处理事件\nuse EasyWeChat\\OpenPlatform\\Server\\Guard;\nRoute::post('open-platform', function () {\n    $server = $openPlatform->server;\n    // 处理授权成功事件，其他事件同理\n    $server->push(function ($message) {\n        // $message 为微信推送的通知内容，不同事件不同内容，详看微信官方文档\n        // 获取授权公众号 AppId： $message['AuthorizerAppid']\n        // 获取 AuthCode：$message['AuthorizationCode']\n        // 然后进行业务处理，如存数据库等...\n    }, Guard::EVENT_AUTHORIZED);\n\n    return $server->serve();\n});\n```\n"
  },
  {
    "path": "docs/src/4.x/open-work/index.md",
    "content": "# 企业微信第三方服务商\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Factory;\n\n$config = [\n     'corp_id'              => '服务商的corpid',\n     'secret'               => '服务商的secret，在服务商管理后台可见',\n     'suite_id'             => '以ww或wx开头应用id',\n     'suite_secret'         => '应用secret',\n     'token'                => '应用的Token',\n     'aes_key'              => '应用的EncodingAESKey',\n     'reg_template_id'      => '注册定制化模板ID',\n     'redirect_uri_install' => '安装应用的回调url（可选）', \n     'redirect_uri_single'  => '单点登录回调url （可选）', \n     'redirect_uri_oauth'   => '网页授权第三方回调url （可选）', \n     \n];\n\n$app = Factory::openWork($config);\n```\n\n"
  },
  {
    "path": "docs/src/4.x/open-work/provider.md",
    "content": "# 服务商相关接口\n\n## 单点登录\n\n\n### 获取从第三方单点登录连接\n\n```php\n$app->provider->getLoginUrl(string $redirectUri = '', string $userType = 'admin', string $state = ''); //$redirectUri 回调地址  $userType支持登录的类型\n```\n\n### 获取登录用户信息\n\n```php\n$app->provider->getLoginInfo(string $authCode); //$authCode oauth2.0授权企业微信管理员登录产生的code，最长为512字节。只能使用一次，5分钟未被使用自动过期\n```\n\n## 注册定制化 \n\n### 获取注册码\n\n```php\n$app->provider->getRegisterCode(\n                        string $corpName = '', //企业名称\n                        string $adminName = '',//管理员姓名\n                        string $adminMobile = '',//管理员手机号\n                        string $state = ''//自定义的状态值\n                    ); \n```\n\n### 获取注册Uri\n\n```php\n$app->provider->getRegisterUri(string $registerCode = ''); //$registerCode 注册码\n```\n\n### 查询注册状态\n\n```php\n$app->provider->getRegisterInfo(string $registerCode); //$registerCode 注册码\n```\n\n### 设置授权应用可见范围\n\n```php\n$app->provider->setAgentScope(\n                        string $accessToken, //查询注册状态接口返回的access_token\n                        string $agentId, //\t授权方应用id\n                        array $allowUser = [], //应用可见范围（成员）若未填该字段，则清空可见范围中成员列表\n                        array $allowParty = [], //\t应用可见范围（部门）若未填该字段，则清空可见范围中部门列表\n                        array $allowTag = [] //应用可见范围（标签）若未填该字段，则清空可见范围中标签列表\n                    )\n```\n\n### 设置通讯录同步完成\n\n```php\n$app->provider->contactSyncSuccess(string $accessToken); //$accessToken //查询注册状态接口返回的access_token\n```\n"
  },
  {
    "path": "docs/src/4.x/open-work/server.md",
    "content": "# 服务端\n\n## 企业微信第三方回调协议\n\nSDK 默认会处理事件 `suite_ticket` ，并会缓存 `suite_ticket`\n\n> 需要注意的是：授权成功、变更授权、取消授权通知时间的响应必须在 1000ms 内完成，以保证用户安装应用的体验。建议在接收到此事件时 立即回应企业微信，之后再做相关业务的处理。\n\n```php\n$server = $app->server;\n\n$server->push(function ($message) {\n    switch ($message['InfoType']) {\n        //推送suite_ticket\n        case 'suite_ticket':\n            break;\n        //授权成功通知\n        case 'create_auth':\n            break;\n        //变更授权通知\n        case 'cancel_auth':\n            break;\n        //通讯录事件通知\n        case 'change_contact':\n            switch ($message['ChangeType']){\n                case 'create_user':\n                    return '新增成员事件';\n                    break;\n                case 'update_user':\n                    return '更新成员事件';\n                    break;\n                 case 'delete_user':\n                    return '删除成员事件';\n                    break;\n                 case 'create_party':\n                    return '新增部门事件';\n                    break;\n                 case 'update_party':\n                    return '更新部门事件';\n                    break;\n                 case 'delete_party':\n                    return '删除部门事件';\n                    break;\n                 case 'update_tag':\n                    return '标签成员变更事件';\n                    break;\n            }\n            break;\n        default:\n            return 'fail';\n            break;\n    }\n});\n$response = $server->serve();\n$response->send();\n\n```\n"
  },
  {
    "path": "docs/src/4.x/open-work/service.md",
    "content": "# 第三方应用接口\n\n\n## 获取预授权码\n\n```php\n$app->corp->getPreAuthCode();\n```\n\n## 设置授权配置\n\n```php\n$app->corp->setSession(string $preAuthCode, array $sessionInfo);\n```\n\n## 获取企业永久授权码\n\n```php\n$app->corp->getPermanentByCode(string $preAuthCode); //传入临时授权码\n```\n\n## 获取企业授权信息\n\n```php\n$app->corp->getAuthorization(string $authCorpId, string $permanentCode); //$authCorpId 授权的企业corp_id $permanentCode 授权的永久授权码\n```\n\n## 获取应用的管理员列表\n\n```php\n$app->corp->getManagers(string $authCorpId, string $agentId); //$authCorpId 授权的企业corp_id  $agentId 授权方安装的应用agentid\n```\n\n##  网页授权登录第三方\n\n### 构造第三方oauth2链接\n\n```php\n//$redirectUri 回调uri 这里可以覆盖 默认读取配置文件\n//$scope 应用授权作用域。\n//$state 自定义安全值\n$app->corp->getOAuthRedirectUrl(string $redirectUri = '', string $scope = 'snsapi_userinfo', string $state = null); \n```\n\n### 第三方根据code获取企业成员信息\n\n```php\n$app->corp->getUserByCode(string $code); \n```\n\n### 第三方使用user_ticket获取成员详情\n\n```php\n$app->corp->getUserByTicket(string $userTicket); \n```\n"
  },
  {
    "path": "docs/src/4.x/open-work/work.md",
    "content": "# 企业\n\n\n### 获取授权企业的相关信息\n\n\n```php\n\n$work = $app->work('授权企业的corp_id','授权企业的永久授权码');\n\n```\n\n然后就可以像企业微信一样 获取相关的数据信息 "
  },
  {
    "path": "docs/src/4.x/overview.md",
    "content": "# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。\n\nEasyWeChat 的安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n### 环境需求\n\n> - PHP >= 7.0\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\n### 加入我们\n\n[EasyWeChat SDK 交流群](http://shang.qq.com/wpa/qunwpa?idkey=b4dcf3ec51a7e8c3c3a746cf450ce59895e5c4ec4fbcb0f80c2cd97c3c6e63e9) ID: 319502940\n\n> 为了避免广告及不看文档用户，加群需要付费，所以请使用 能支持群费的客户端。\n> 另外：付费加群不代表我们有责任在群里回答你的问题，所以请认真阅读微信官方文档与 SDK 使用文档再使用，否则提的低级问题不会有人理你\n> 不喜勿加，谢谢！\n> 除非你发现了明确的 Bug，否则不要在群里 @ 我 :pray:\n\n你有以下两种方式加入到我们中来，为广大开发者提供更优质的免费开源的服务：\n\n> - **贡献代码**：我们的代码都在 [overtrue/wechat](https://github.com/overtrue/wechat) ，你可以提交 PR 到任何一个项目，当然，前提是代码质量必须是 OK 的。\n> - **翻译或补充文档**：我们的文档在：[w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/master/docs)，你可以选择补充文档或者参与英文文档的翻译，目前有 `zh-cn` 与 `en` 两个分支，你可以提交对应的 PR 到目标分支参与翻译工作。\n\n### 开始之前\n\n我们提供了视频教程：<https://easywechat.com/tutorials> 当然，我还是建议你具备以下基础知识，否则可能没有那么快上手。\n\n本 SDK 不是一个全新再造的东西，所以我不会从 0 开始教会你开发微信，你完全有必要在使用本 SDK 前做好以下工作：\n\n> - 具备 PHP 基础知识，不要连闭包是啥都不明白，可以参考我在知乎的回答: [想要开发自己的 PHP 框架需要那些知识储备？](http://www.zhihu.com/question/26635323/answer/33812516)\n> - 熟悉 PHP 常见的知识：自动加载、composer 的使用、JSON 处理、Curl 的使用等；\n> - **仔细阅读并看懂**[微信官方文档](http://mp.weixin.qq.com/wiki/13/80a1a25adbc46faf2716774c423b3151.html) [微信开放平台文档](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318292&token=&lang=zh_CN)；\n> - 明白微信接口的组成，自有服务器、微信服务器、公众号（还有其它各种号）、测试号、以及通信原理（交互过程）；\n> - 了解基本的 HTTP 协议，Header 头、请求方式（GET\\POST\\PUT\\PATCH\\DELETE）等；\n> - 基本的 Debug 技能，查看 php 日志，nginx 日志等。\n\n如果你不具备这些知识，请不要使用，因为用起来会比较痛苦。\n\n另外你有必要看一下以下的链接：\n\n> - <https://learnku.com/laravel/t/535/assertion-people-who-do-not-understand-the-wisdom-of-asking-questions-will-not-graduate-from-junior-programmers>\n> - <http://laravel-china.github.io/php-the-right-way/>\n\n如果你在群里问以下类似的问题，这真的是你没有做好上面的工作：\n\n> - \"为啥我的不行啊，请问服务器日志怎么看啊？\"\n> - \"请问这是什么原因啊？[结果/报错截图]\"\n> - \"请问这个 SDK 怎么用啊？\"\n> - \"谁能告诉我这个 SDK 是怎么安装的啊？\"\n> - \"怎么接收用户发的消息啊？\"\n> - \"为啥我的报这个错啊：Class XXXX not found...\"\n> - ...\n\n我们专门针对一些容易出现的通用问题已经做了汇总： [疑难解答](troubleshooting.md) ，如果你在问题疑难解答没找到你出现的问题，那么可以在这里提问 [GitHub](https://github.com/overtrue/wechat/issues)，提问请描述清楚你用的版本，你的做法是什么，不然别人没法帮你。\n\n> 要在 QQ 单独找我提问\\*\\*，除非你是发现了明显的 bug。有问题先审查代码，看文档, 再 google，然后 去群里发个问题，带上你的代码，重现流程，大家有空的会帮忙你解答。谢谢合作！:pray:\n\n### 打赏支持\n\n这是一个开源的项目，我们没有收费服务，你如果觉得你从中获益，简化了你的开发工作，你可以 [打赏](https://github.com/sponsors/overtrue) 来支持我们。\n"
  },
  {
    "path": "docs/src/4.x/payment/bill.md",
    "content": "# 对账单\n\n## 下载对账单\n\n> 调用参数正确会返回一个 `EasyWeChat\\Kernel\\Http\\StreamResponse` 对象，否则会返回相应错误信息\n\nExample:\n\n```php\n$bill = $app->bill->get('20140603'); // type: ALL\n// or\n$bill = $app->bill->get('20140603', 'SUCCESS'); // type: SUCCESS\n\n// 调用正确，`$bill` 为 csv 格式的内容，保存为文件：\n$bill->saveAs('your/path/to', 'file-20140603.csv');\n```\n\n第二个参数为账单类型，参考：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6 中 `bill_type`，默认为 `ALL`\n"
  },
  {
    "path": "docs/src/4.x/payment/contract.md",
    "content": "# 签约\n\n## 公众号签约\n\n> 参数 `appid`, `version`, `timestamp`, `sign` 可不用传入\n\n```php\n$result = $app->contract->web([\n    'mch_id' => '1200009811',\n    'plan_id' => '12535',\n    'contract_code' => '100000',\n    'contract_display_account' => '微信代扣',\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action',\n]);\n```\n\n## APP 签约\n\n```php\n$result = $app->contract->app(array $params);\n```\n\n## H5 签约\n\n```php\n$result = $app->contract->h5(array $params);\n```\n\n## 小程序签约\n\n```php\n$result = $app->jssdk->contractConfig(array $params);\n```\n\n## 申请扣款\n\n```php\n$result = $app->contract->apply(array $params);\n```\n\n## 申请解约\n\n```php\n$result = $app->contract->delete(array $params);\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/index.md",
    "content": "# 支付\n\n你在阅读本文之前确认你已经仔细阅读了：[微信支付 | 商户平台开发文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)。\n\n## 配置\n\n配置在前面的例子中已经提到过了，支付的相关配置如下：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',   // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥)\n\n    // 如需使用敏感接口（如退款、发送红包等）需要配置 API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n];\n\n$app = Factory::payment($config);\n```\n\n### 服务商\n\n#### 设置子商户信息\n\n```php\n$app->setSubMerchant('sub-merchant-id', 'sub-app-id');  // 子商户 AppID 为可选项\n```\n\n### 刷卡支付\n\n[官方文档](https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10)\n\n```php\n$result = $app->pay([\n    'body' => 'image形象店-深圳腾大- QQ公仔',\n    'out_trade_no' => '1217752501201407033233368018',\n    'total_fee' => 888,\n    'auth_code' => '120061098828009406',\n]);\n```\n\n## 授权码查询 OPENID 接口\n\n```php\n$app->authCodeToOpenid($authCode);\n```\n\n## 沙箱模式\n\n微信支付沙箱环境，是提供给微信支付商户的开发者，用于模拟支付及回调通知。以验证商户是否理解回调通知、账单格式，以及是否对异常做了正确的处理。EasyWeChat SDK 对于这一功能进行了封装，开发者只需一步即可在沙箱模式和常规模式间切换，方便开发与最终的部署。\n\n```php\n// 在实例化的时候传入配置即可\n$app = Factory::payment([\n    // ...\n    'sandbox' => true, // 设置为 false 或注释则关闭沙箱模式\n]);\n\n// 判断当前是否为沙箱模式：\nbool $app->inSandbox();\n```\n\n> 特别注意，沙箱模式对于测试用例有严格要求，若使用的用例与规定不符，将导致测试失败。具体用例要求可关注公众号“微信支付商户接入验收助手”（WXPayAssist）查看。\n"
  },
  {
    "path": "docs/src/4.x/payment/jssdk.md",
    "content": "# JSSDK\n\nJSSDK 模块用于生成调起微信支付以及共享收货地址的调用所需的配置参数。\n\n## 配置\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 前面的appid什么的也得保留哦\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',\n    'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n    // 'device_info'     => '013467007045764',\n    // 'sub_app_id'      => '',\n    // 'sub_merchant_id' => '',\n    // ...\n];\n\n$payment = Factory::payment($config);\n\n$jssdk = $payment->jssdk;\n```\n\n## 生成支付 JS 配置\n\n有三种发起支付的方式：[WeixinJSBridge](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6), [JSSDK](https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_1), [小程序](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7)\n\n1. WeixinJSBridge:\n\n    ```php\n    $json = $jssdk->bridgeConfig($prepayId); // 返回 json 字符串，如果想返回数组，传第二个参数 false\n    ```\n\n    javascript:\n\n    ```js\n    ...\n    WeixinJSBridge.invoke(\n           'getBrandWCPayRequest', <?= $json ?>,\n           function(res){\n               if(res.err_msg == \"get_brand_wcpay_request:ok\" ) {\n                    // 使用以上方式判断前端返回,微信团队郑重提示：\n                    // res.err_msg将在用户支付成功后返回\n                    // ok，但并不保证它绝对可靠。\n               }\n           }\n       );\n    ...\n    ```\n\n2. JSSDK:\n\n    ```php\n    $config = $jssdk->sdkConfig($prepayId); // 返回数组\n    ```\n\n    javascript:\n\n    ```js\n    wx.chooseWXPay({\n        timestamp: <?= $config['timestamp'] ?>,\n        nonceStr: '<?= $config['nonceStr'] ?>',\n        package: '<?= $config['package'] ?>',\n        signType: '<?= $config['signType'] ?>',\n        paySign: '<?= $config['paySign'] ?>', // 支付签名\n        success: function (res) {\n            // 支付成功后的回调函数\n        }\n    });\n    ```\n\n3. 小程序:\n\n    ```php\n    $config = $jssdk->bridgeConfig($prepayId, false); // 返回数组\n    ```\n\n    javascript:\n\n    ```js\n    wx.requestPayment({\n        timeStamp: <?= $config['timeStamp'] ?>, //注意 timeStamp 的格式\n        nonceStr: '<?= $config['nonceStr'] ?>',\n        package: '<?= $config['package'] ?>',\n        signType: '<?= $config['signType'] ?>',\n        paySign: '<?= $config['paySign'] ?>', // 支付签名\n        success: function (res) {\n            // 支付成功后的回调函数\n        }\n    });\n    ```\n\n## 生成共享收货地址 JS 配置\n\n1. 发起 OAuth 授权，获取用户 `$accessToken`,参考网页授权章节。\n\n2. 使用 `$accessToken` 获取配置\n\n```php\n$configForPickAddress = $jssdk->shareAddressConfig($token);\n\n// 拿着这个生成好的配置 $configForPickAddress 去订单页（或者直接显示订单页）写 js 调用了\n// ...\n```\n\n## 生成 APP 支付配置\n\n```php\n$config = $jssdk->appConfig($prepayId);\n```\n\n`$config` 为数组格式，你可以用 API 返回给客户端\n\n# 二维码生成工具推荐\n\n你也许需要生成二维码，那么以下这些供参考：\n\n>  - https://github.com/endroid/QrCode\n>  - https://github.com/Bacon/BaconQrCode\n>  - https://github.com/SimpleSoftwareIO/simple-qrcode (Bacon/BaconQrCode 的 Laravel 版本)\n>  - https://github.com/aferrandini/PHPQRCode\n"
  },
  {
    "path": "docs/src/4.x/payment/micropay.md",
    "content": "# 付款码支付\n\n## 配置\n\n> 请务必先熟悉流程：<https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_1>\n\n\n```php\n$result = $app->pay([\n    'body' => 'image形象店-深圳腾大- QQ公仔',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'auth_code' => '120061098828009406', // 扫码支付付款码，设备读取用户微信中的条码或者二维码信息\n]);\n```\n\n#### 支付结果\n\n付款码支付方式没有回调通知，支付结果直接返回，请参考：[微信付款码支付文档](https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_1) 更新您的订单状态。\n"
  },
  {
    "path": "docs/src/4.x/payment/notify.md",
    "content": "# 通知\n\n## 支付结果通知\n\n在用户成功支付后，微信服务器会向该 **订单中设置的回调 URL** 发起一个 POST 请求，请求的内容为一个 XML。里面包含了所有的详细信息，具体请参考：[支付结果通知](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7)\n\n而对于用户的退款操作，在退款成功之后也会有一个异步回调通知。\n\n本 SDK 内预置了相关方法，以方便开发者处理这些通知，具体用法如下：\n\n只需要在控制器中使用 `handlePaidNotify()` 方法，在其中对自己的业务进行处理并向微信服务器发送一个响应。\n\n```php\n$response = $app->handlePaidNotify(function ($message, $fail) {\n    // 你的逻辑\n    return true;\n    // 或者错误消息\n    $fail('Order not exists.');\n});\n\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n这里需要注意的有几个点：\n\n0. 退款结果通知和扫码支付通知的使用方法均类似。\n1. `handlePaidNotify` 只接收一个 [`Closure`](http://php.net/manual/zh/class.closure.php) 匿名函数。\n2. 该匿名函数接收两个参数，这两个参数分别为：\n\n   > - `$message` 为微信推送过来的通知信息，为一个数组；\n   > - `$fail` 为一个函数，触发该函数可向微信服务器返回对应的错误信息，**微信会稍后重试再通知**。\n\n3. 该函数返回值就是告诉微信 **“我是否处理完成”**。如果你触发 `$fail` 函数，那么微信会在稍后再次继续通知你，直到你明确的告诉它：“我已经处理完成了”，**只有**在函数里 `return true;` 才代表处理完成。\n\n4. `handlePaidNotify` 返回值 `$response` 是一个 Response 对象，如果你要直接输出，使用 `$response->send()`, 在一些框架里（如 Laravel）不是输出而是返回：`return $response`。\n\n通常我们的处理逻辑大概是下面这样（**以下只是伪代码**）：\n\n```php\n$response = $app->handlePaidNotify(function($message, $fail){\n    // 使用通知里的 \"微信支付订单号\" 或者 \"商户订单号\" 去自己的数据库找到订单\n    $order = 查询订单($message['out_trade_no']);\n\n    if (!$order || $order->paid_at) { // 如果订单不存在 或者 订单已经支付过了\n        return true; // 告诉微信，我已经处理完了，订单没找到，别再通知我了\n    }\n\n    ///////////// <- 建议在这里调用微信的【订单查询】接口查一下该笔订单的情况，确认是已经支付 /////////////\n\n    if ($message['return_code'] === 'SUCCESS') { // return_code 表示通信状态，不代表支付状态\n        // 用户是否支付成功\n        if (array_get($message, 'result_code') === 'SUCCESS') {\n            $order->paid_at = time(); // 更新支付时间为当前时间\n            $order->status = 'paid';\n\n        // 用户支付失败\n        } elseif (array_get($message, 'result_code') === 'FAIL') {\n            $order->status = 'paid_fail';\n        }\n    } else {\n        return $fail('通信失败，请稍后再通知我');\n    }\n\n    $order->save(); // 保存订单\n\n    return true; // 返回处理完成\n});\n\n$response->send(); // return $response;\n```\n\n> 注意：请把 “支付成功与否” 与 “是否处理完成” 分开，它俩没有必然关系。\n> 比如：微信通知你用户支付完成，但是支付失败了(result_code 为 'FAIL')，你应该**更新你的订单为支付失败**，但是要**告诉微信处理完成**。\n\n## 退款结果通知\n\n使用示例：\n\n```php\n$response = $app->handleRefundedNotify(function ($message, $reqInfo, $fail) {\n    // 其中 $message['req_info'] 获取到的是加密信息\n    // $reqInfo 为 message['req_info'] 解密后的信息\n    // 你的业务逻辑...\n    return true; // 返回 true 告诉微信“我已处理完成”\n    // 或返回错误原因 $fail('参数格式校验错误');\n});\n\n$response->send();\n```\n\n## 扫码支付通知\n\n扫码支付【模式一】：https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4\n\n```php\n// 扫码支付通知接收第三个参数 `$alert`，如果触发该函数，会返回“业务错误”到微信服务器，触发 `$fail` 则返回“通信错误”\n$response = $app->handleScannedNotify(function ($message, $fail, $alert) use ($app) {\n    // 如：$alert('商品已售空');\n    // 如业务流程正常，则要调用“统一下单”接口，并返回 prepay_id 字符串，代码如下\n    $result = $app->order->unify([\n        'trade_type' => 'NATIVE',\n        'product_id' => $message['product_id'],\n        // ...\n    ]);\n\n    return $result['prepay_id'];\n});\n\n$response->send();\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/order.md",
    "content": "# 订单\n\n## 统一下单\n\n没错，什么 H5 支付，公众号支付，扫码支付，支付中签约，全部都是用这个接口下单。\n\n> 参数 `appid`, `mch_id`, `nonce_str`, `sign`, `sign_type` 可不用传入\n\n> 服务商模式下, 需使用 `sub_openid`, 并传入`sub_mch_id` 和`sub_appid`\n\n```php\n$result = $app->order->unify([\n    'body' => '腾讯充值中心-QQ会员充值',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型\n    'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o',\n]);\n\n//如trade_type = APP\n//需要进行二次签名\n(new \\EasyWeChat\\Payment\\Jssdk\\Client($app))->appConfig($result['prepay_id']);\n\n// $result:\n//{\n//    \"return_code\": \"SUCCESS\",\n//    \"return_msg\": \"OK\",\n//    \"appid\": \"wx2421b1c4390ec4sb\",\n//    \"mch_id\": \"10000100\",\n//    \"nonce_str\": \"IITRi8Iabbblz1J\",\n//    \"openid\": \"oUpF8uMuAJO_M2pxb1Q9zNjWeSs6o\",\n//    \"sign\": \"7921E432F65EB8ED0CE9755F0E86D72F2\",\n//    \"result_code\": \"SUCCESS\",\n//    \"prepay_id\": \"wx201411102639507cbf6ffd8b0779950874\",\n//    \"trade_type\": \"JSAPI\"\n//}\n```\n\n**第二个参数**为是否[支付中签约](https://pay.weixin.qq.com/wiki/doc/api/pap.php?chapter=18_13&index=5)，默认 `false`\n\n> 支付中签约相关参数 `contract_mchid`, `contract_appid`, `request_serial` 可不用传入\n\n```php\n$isContract = true;\n\n$result = $app->order->unify([\n    'body' => '腾讯充值中心-QQ会员充值',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型\n    'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o',\n\n    'plan_id' => 123,// 协议模板id\n    'contract_code' => 100001256,// 签约协议号\n    'contract_display_account' => '腾讯充值中心',// 签约用户的名称\n    'contract_notify_url' => 'http://easywechat.com/contract_notify'\n], $isContract);\n\n//$result:\n//{\n//  \"return_code\": \"SUCCESS\",\n//  \"return_msg\": \"OK\",\n//  \"appid\": \"wx123456\",\n//  \"mch_id\": \"10000100\",\n//  \"nonce_str\": \"CfOcMkDFblzulYvI\",\n//  \"sign\": \"B53F4AFEE7FA6AD5739581486A5CB9C9\",\n//  \"result_code\": \"SUCCESS\",\n//  \"prepay_id\": \"wx08175759731015754a5c13791522969400\",\n//  \"trade_type\": \"JSAPI\",\n//  \"plan_id\": \"123\",\n//  \"request_serial\": \"1565258279\",\n//  \"contract_code\": \"100001256\",\n//  \"contract_display_account\": \"腾讯充值中心\",\n//  \"out_trade_no\": \"201908088195558331565258279\",\n//  \"contract_result_code\": \"SUCCESS\"\n//}\n```\n\n## 查询订单\n\n该接口提供所有微信支付订单的查询，商户可以通过该接口主动查询订单状态，完成下一步的业务逻辑。\n\n需要调用查询接口的情况：\n\n> - 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；\n> - 调用支付接口后，返回系统错误或未知交易状态情况；\n> - 调用被扫支付 API，返回 USERPAYING 的状态；\n> - 调用关单或撤销接口 API 之前，需确认支付状态；\n\n### 根据商户订单号查询\n\n```php\n$app->order->queryByOutTradeNumber(\"商户系统内部的订单号（out_trade_no）\");\n```\n\n### 根据微信订单号查询\n\n```php\n$app->order->queryByTransactionId(\"微信订单号（transaction_id）\");\n```\n\n## 关闭订单\n\n> 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为 5 分钟。\n\n```php\n$app->order->close(商户系统内部的订单号（out_trade_no）);\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/profit-sharing.md",
    "content": "# 分账\n> 官方文档 https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_1&index=1\n\n```php\nuse EasyWeChat\\Factory;\n$config = [\n\t'app_id'     => '***',\n\t\"secret\"     => \"***\",\n\t'mch_id'     => '***',\n\t'key'        => '***',\n\t'cert_path'  => 'cert.pem',\n\t'key_path'   => 'key.pem',\n\t'notify_url' => 'http://***.com/notify.php',\n];\n$payment = Factory::payment($config);\n```\n\n### 添加接收方\n\n> 商户发起添加分账接收方请求，后续可通过发起分账请求将结算后的钱分到该分账接收方。\n\n```php\n$receiver = [\n\t\"type\"          => \"PERSONAL_OPENID\",\n\t\"account\"       => \"…………\",//PERSONAL_OPENID：个人openid\n\t\"name\"          => \"张三\",//接收方真实姓名\n\t\"relation_type\" => \"PARTNER\"\n];\n$payment->profit_sharing->addReceiver($receiver);\n$receiver = [\n\t\"type\"          => \"MERCHANT_ID\",\n\t\"account\"       => \"132456798\",//MERCHANT_ID：商户ID\n\t\"name\"          => \"商户全称\",//商户全称\n\t\"relation_type\" => \"PARTNER\"\n];\n$payment->profit_sharing->addReceiver($receiver);\n```\n\n### 删除接收方\n\n```php\n$payment->profit_sharing->deleteReceiver($receiver);\n```\n\n### 单次分账\n\n```php\n$transaction_id = \"***\";\n$out_trade_no = \"***\";\n$receivers = [\n\t[\n\t\t\"type\"        => \"PERSONAL_OPENID\",\n\t\t\"account\"     => \"***\",\n\t\t\"amount\"      => 2,\n\t\t\"description\" => \"分到个人\"\n\t],\n\t[\n\t\t\"type\"        => \"MERCHANT_ID\",\n\t\t\"account\"     => \"***\",\n\t\t\"amount\"      => 1,\n\t\t\"description\" => \"分到商户\"\n\t]\n];\n$sharing = $payment->profit_sharing->share($transaction_id,$out_trade_no,$receivers);\n```\n\n### 多次分账\n\n```php\n$payment->profit_sharing->multiShare($transaction_id,$out_trade_no,$receivers);\n```\n\n### 多次分账完结\n\n```php\n$params = [\n\t\"transaction_id\" => \"\",\n\t\"out_order_no\"   => \"\",\n\t\"description\"    => \"\"\n];\n$payment->profit_sharing->markOrderAsFinished($params);\n```\n\n### 分账查询\n\n```php\n$res = $payment->profit_sharing->query($transaction_id,$out_trade_no);\n```\n\n> 查询结果\n\n```\nArray\n(\n    [return_code] => SUCCESS\n    [result_code] => SUCCESS\n    [mch_id] => ***\n    [nonce_str] => 38e92cbe2790642f\n    [sign] => 8904B6440C58785540950F2911500F55C9A94CAC75790B0721B9AA470E6BF9A8\n    [transaction_id] => 4200000589202007249764665257\n    [out_order_no] => 202007241544057945\n    [order_id] => 30000103702020072402011591464\n    [status] => FINISHED\n    [receivers] => [{\"type\":\"MERCHANT_ID\",\"account\":\"***\",\"amount\":7,\"description\":\"解冻给分账方\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172033\"},{\"type\":\"PERSONAL_OPENID\",\"account\":\"***\",\"amount\":2,\"description\":\"分到个人1\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172033\"},{\"type\":\"PERSONAL_OPENID\",\"account\":\"***-g4\",\"amount\":1,\"description\":\"分到郭\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172034\"}]\n)\n```\n\n### 分账退回\n\n```php\n$out_trade_no = \"***\";//退款订单号\n$out_return_no = \"***\";//系统内部退款单号\n$return_amount = 1;\n$return_account = \"***-g4\";\n$description = \"订单取消\";\n$payment->profit_sharing->returnShare($out_trade_no,$out_return_no,$return_amount,$return_account,$description);\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/redpack.md",
    "content": "# 红包\n\n\n在阅读本文之前确认你已经仔细阅读了：[微信支付 | 现金红包文档 ](https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_1)。\n\n## 配置\n\n与支付接口一样，红包接口也需要配置如下参数，需要特别注意的是，红包相关的全部接口**都需要使用 SSL 证书**，因此**cert_path 以及 cert_key 必须正确配置**。\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id'    => 'you-app-id',\n    'mch_id'    => 'your-mch-id',\n    'key'       => 'key-for-signature',\n    'cert_path' => 'path/to/your/cert.pem',\n    'key_path'  => 'path/to/your/key',\n    // ...\n];\n\n$payment = Factory::payment($config);\n\n$redpack = $payment->redpack;\n```\n\n## 发送红包\n\n微信的现金红包分为**普通红包**和**裂变红包**两类。SDK 中对其分别进行了封装，同时也提供了一个统一的调用方法。\n\n**默认情况下，通过接口发送的红包金额应该在200元以内，但可以通过在调用发送接口时传递场景 ID (scene_id)来发送特定场景的红包，不同场景红包可以由商户自己登录商户平台设置最大金额。scene_id 的可选值及对应含义可参阅微信支付官方文档。**\n\n\n### 发送普通红包接口\n\n```php\n$redpackData = [\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 1,  //固定为1，可不传\n    'total_amount' => 100,  //单位为分，不小于100\n    'wishing'      => '祝福语',\n    'client_ip'    => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    // ...\n];\n\n$result = $redpack->sendNormal($redpackData);\n```\n\n### 发送裂变红包接口\n\n```php\n$redpackData = [\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 3,  //不小于3\n    'total_amount' => 300,  //单位为分，不小于300\n    'wishing'      => '祝福语',\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    'amt_type'     => 'ALL_RAND',  //可不传\n    // ...\n];\n\n$result = $redpack->sendGroup($redpackData);\n```\n\n## 红包预下单接口\n\n红包预下单接口是为摇一摇红包接口配合使用的，在开发摇一摇周边的摇红包相关功能时，需要调用本接口获取红包单号。详情参见[官方文档](http://mp.weixin.qq.com/wiki/7/0ddd50ed2421b99fedd071281c074aab.html#.E7.BA.A2.E5.8C.85.E9.A2.84.E4.B8.8B.E5.8D.95.E6.8E.A5.E5.8F.A3)\n\n\n```php\n$redpackData = [\n    'hb_type'      => 'NORMAL',  //NORMAL 或 GROUP\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 1,  //普通红包固定为1，裂变红包不小于3\n    'total_amount' => 100,  //单位为分，普通红包不小于100，裂变红包不小于300\n    'wishing'      => '祝福语',\n    'client_ip'    => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    'amt_type'     => 'ALL_RAND',\n    // ...\n];\n\n$result = $redpack->prepare($redpackData);\n```\n\n## 查询红包信息\n\n用于商户对已发放的红包进行查询红包的具体信息以及领取情况 ，普通红包和裂变包均使用这一接口进行查询。\n\n```php\n$mchBillNo = \"商户系统内部的订单号（mch_billno）\";\n$redpack->info($mchBillNo);\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/refund.md",
    "content": "# 退款\n\n## 申请退款\n\n当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，微信支付将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。\n\n注意：\n\n> 1、交易时间超过一年的订单无法提交退款；\n> 2、微信支付退款支持单笔交易分多次退款，多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。一笔退款失败后重新提交，要采用原来的退款单号。总退款金额不能超过用户实际支付金额。\n\n参考：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4\n\n### 根据微信订单号退款\n\n```php\n// 参数分别为：微信订单号、商户退款单号、订单金额、退款金额、其他参数\n$app->refund->byTransactionId(string $transactionId, string $refundNumber, int $totalFee, int $refundFee, array $config = []);\n\n// Example:\n$result = $app->refund->byTransactionId('transaction-id-xxx', 'refund-no-xxx', 10000, 10000, [\n    // 可在此处传入其他参数，详细参数见微信支付文档\n    'refund_desc' => '商品已售完',\n]);\n\n```\n### 根据商户订单号退款\n\n```php\n// 参数分别为：商户订单号、商户退款单号、订单金额、退款金额、其他参数\n$app->refund->byOutTradeNumber(string $number, string $refundNumber, int $totalFee, int $refundFee, array $config = []);\n\n// Example:\n$result = $app->refund->byOutTradeNumber('out-trade-no-xxx', 'refund-no-xxx', 20000, 1000, [\n    // 可在此处传入其他参数，详细参数见微信支付文档\n    'refund_desc' => '退运费',\n]);\n```\n\n> $refundNumber 为商户退款单号，自己生成用于自己识别即可。\n\n## 查询退款\n\n提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用零钱支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。\n\n可通过 4 种不同类型的单号查询：\n\n>  - 微信订单号 => `queryByTransactionId($transactionId)`\n>  - 商户订单号 => `queryByOutTradeNumber($outTradeNumber)`\n>  - 商户退款单号 => `queryByOutRefundNumber($outRefundNumber)`\n>  - 微信退款单号 => `queryByRefundId($refundId)`\n"
  },
  {
    "path": "docs/src/4.x/payment/reverse.md",
    "content": "# 撤销订单\n\n目前只有 **刷卡支付** 有此功能。\n\n> 调用支付接口后请勿立即调用撤销订单API，建议支付后至少15s后再调用撤销订单接口。\n\n## 通过内部订单号撤销订单\n\n```php\n$app->reverse->byOutTradeNumber(\"商户系统内部的订单号（out_trade_no）\");\n```\n\n## 通过微信订单号撤销订单\n\n```php\n$app->reverse->byTransactionId(\"微信的订单号（transaction_id）\");\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/scan-pay.md",
    "content": "## 扫码支付\n\n### 模式一：先生成产品二维码，扫码下单后支付\n\n> 请务必先熟悉流程：<https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4>\n\n#### 生成产品二维码内容\n\n```php\n$content = $app->scheme($productId); // $productId 为你的产品/商品ID，用于回调时带回，自己识别即可\n\n//结果示例：weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX\n```\n\n将 `$content` 生成二维码，SDK 并不内置二维码生成库，使用你熟悉的工具创建二维码即可，比如 PHP 部分有以下工具可以选择：\n\n> - <https://github.com/endroid/qr-code>\n> - <https://github.com/SimpleSoftwareIO/simple-qrcode>\n> - <https://github.com/aferrandini/PHPQRCode>\n\n#### 处理回调\n\n当用户扫码时，你的回调接口会收到一个通知，调用[统一下单接口](https://easywechat.com/docs/master/zh-CN/payment/order)创建订单后返回 `prepay_id`，你可以使用下面的代码处理扫码通知：\n\n```php\n// 扫码支付通知接收第三个参数 `$alert`，如果触发该函数，会返回“业务错误”到微信服务器，触发 `$fail` 则返回“通信错误”\n$response = $app->handleScannedNotify(function ($message, $fail, $alert) use ($app) {\n    // 如：$alert('商品已售空');\n    // 如业务流程正常，则要调用“统一下单”接口，并返回 prepay_id 字符串，代码如下\n    $result = $app->order->unify([\n        'trade_type' => 'NATIVE',\n        'product_id' => $message['product_id'], // $message['product_id'] 则为生成二维码时的产品 ID\n        // ...\n    ]);\n\n    return $result['prepay_id'];\n});\n\n$response->send();\n```\n\n用户在手机上付完钱以后，你会再收到**付款结果通知**，这时候请参考：[处理微信支付通知](https://easywechat.com/docs/master/zh-CN/payment/notify) 更新您的订单状态。\n\n### 模式二：先下单，生成订单后创建二维码\n\n> ：<https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5>\n\n#### 根据用户选购的商品生成订单\n\n调用[统一下单接口](https://easywechat.com/docs/master/zh-CN/payment/order)创建订单：\n\n```php\n$result = $app->order->unify([\n      'trade_type' => 'NATIVE',\n      'product_id' => $message['product_id'], // $message['product_id'] 则为生成二维码时的产品 ID\n      // ...\n  ]);\n```\n\n#### 生成二维码\n\n> 版本 4.1.7+ 支持\n\n从上一步得到的 `$result['code_url']` 得到二维码内容：\n\n将 `$result['code_url']` 生成二维码图片向用户展示即可扫码，生成工具上面自己找一下即可。 SDK 不内置\n\n#### 支付通知\n\n这种方式的通知就只有**付款结果通知**了，这时候请参考：[处理微信支付通知](https://easywechat.com/docs/master/zh-CN/payment/notify) 更新您的订单状态。\n"
  },
  {
    "path": "docs/src/4.x/payment/security.md",
    "content": "# 安全与风控\n\n> EasyWeChat 4.0.7+\n\n## 获取 RSA 公钥\n\n```php\n$result = $app->security->getPublicKey();\n\n// 存成文件\n\nfile_put_contents('./public.pem', $result);\n```\n\n将会得到 PKCS#1 格式密钥：\n\n```\n-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEArT82k67xybiJS9AD8nNAeuDYdrtCRaxkS6cgs8L9h83eqlDTlrdw\nzBVSv5V4imTq/URbXn4K0V/KJ1TwDrqOI8hamGB0fvU13WW1NcJuv41RnJVua0QA\nlS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+2kUWG94FccArNnBtBqqvFncXgQsm98JB\n3a62NbS1ePP/hMI7Kkz+JNMyYsWkrOUFDCXAbSZkWBJekY4nGZtK1erqGRve8Jbx\nTWirAm/s08rUrjOuZFA21/EI2nea3DidJMTVnXVPY2qcAjF+595shwUKyTjKB8v1\nREPB3hPF1Z75O6LwuLfyPiCrCTmVoyfqjwIDAQAB\n-----END RSA PUBLIC KEY-----\n```\n\n使用 OpenSSL 转换 PKCS#1 为 PKCS#8 格式密钥：\n\n```shell\nopenssl rsa -RSAPublicKey_in -in public.pem -out public.pem\n```\n\nPKCS#8 格式密钥：\n\n```\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT82k67xybiJS9AD8nNA\neuDYdrtCRaxkS6cgs8L9h83eqlDTlrdwzBVSv5V4imTq/URbXn4K0V/KJ1TwDrqO\nI8hamGB0fvU13WW1NcJuv41RnJVua0QAlS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+\nlkUWG94FccArNnBtBqqvFncXgQsm98JB3a42NbS1ePP/hMI7Kkz+JNMyYsWkrOUF\nDCXAbSZkWBJekY4nGZtK1erqGRve8JbxTWirAm/s08rUrjOuZFA21/EI2nea3Did\nJMTVnXVPY2qcAjF+595shwUKyTjKB8v1REPB3hPF1Z75O6LwuLfyPiCrCTmVoyfq\njwIDAQAB\n-----END PUBLIC KEY-----\n```\n"
  },
  {
    "path": "docs/src/4.x/payment/transfer.md",
    "content": "# 企业付款\n\n> EasyWeChat 4.0.7+\n\n该模块需要用到双向证书，请参考：https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=4_3\n\n## 企业付款到用户零钱\n\n```php\n$app->transfer->toBalance([\n    'partner_trade_no' => '1233455', // 商户订单号，需保持唯一性(只能是字母或者数字，不能包含有符号)\n    'openid' => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'check_name' => 'FORCE_CHECK', // NO_CHECK：不校验真实姓名, FORCE_CHECK：强校验真实姓名\n    're_user_name' => '王小帅', // 如果 check_name 设置为FORCE_CHECK，则必填用户真实姓名\n    'amount' => 10000, // 企业付款金额，单位为分\n    'desc' => '理赔', // 企业付款操作说明信息。必填\n]);\n```\n\n## 查询付款到零钱的订单\n\n```php\n$partnerTradeNo = 1233455;\n$app->transfer->queryBalanceOrder($partnerTradeNo);\n```\n\n\n## 企业付款到银行卡\n\n企业付款到银行卡需要对银行卡号与姓名进行 RSA 加密，所以这里需要先下载 RSA 公钥到本地（服务器），我们提供了一个命令行工具：[EasyWeChat/console](https://github.com/EasyWeChat/console)，请使用 composer 安装完成。\n\n```bash\n$ composer require easywechat/console -vvv\n```\n\n然后，在项目根目录执行以下命令下载公钥：\n\n```bash\n$ ./vendor/bin/easywechat payment:rsa_public_key \\\n  >  --mch_id=14339221228 \\\n  >  --api_key=36YTbDmLgyQ52noqdxgwGiYy \\\n  >  --cert_path=/Users/overtrue/www/demo/apiclient_cert.pem \\\n  >  --key_path=/Users/overtrue/www/demo/apiclient_key.pem\n```\n\n将会在当前目录生成一个 `./public-14339221228.pem` 文件，你可以将它移动到敏感目录，然后在支付配置文件中加如以下选项：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',   // API 密钥\n\n    // 如需使用敏感接口（如退款、发送红包等）需要配置 API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'          => '/path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => '/path/to/your/key',      // XXX: 绝对路径！！！！\n\n    // 将上面得到的公钥存放路径填写在这里\n    'rsa_public_key_path' => '/path/to/your/rsa/publick/key/public-14339221228.pem', // <<<------------------------\n\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n];\n\n$app = Factory::payment($config);\n```\n\n```php\n$result = $app->transfer->toBankCard([\n    'partner_trade_no' => '1229222022',\n    'enc_bank_no' => '6214830901234564', // 银行卡号\n    'enc_true_name' => '安正超',   // 银行卡对应的用户真实姓名\n    'bank_code' => '1001', // 银行编号\n    'amount' => 100,  // 单位：分\n    'desc' => '测试',\n]);\n\n```\n\n## 查询付款到银行卡的订单\n\n```php\n$partnerTradeNo = 1233455;\n$app->transfer->queryBankCardOrder($partnerTradeNo);\n```\n\n"
  },
  {
    "path": "docs/src/4.x/sidebar.js",
    "content": "exports = module.exports = [\n  {\n    text: '开始使用',\n    collapsible: true,\n    items: [\n      { text: '概述', link: '/4.x/overview.html' },\n      { text: '安装', link: '/4.x/installation.html' },\n      { text: '在框架中使用', link: '/4.x/integration.html' },\n      { text: '常见问题汇总', link: '/4.x/troubleshooting.html' }\n    ]\n  },\n  {\n    text: '公众号',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/official-account/index.html' },\n      { text: '快速开始', link: '/4.x/official-account/tutorial.html' },\n      { text: '配置', link: '/4.x/official-account/configuration.html' },\n      { text: '基础接口', link: '/4.x/official-account/base.html' },\n      { text: '服务端', link: '/4.x/official-account/server.html' },\n      { text: '消息', link: '/4.x/official-account/messages.html' },\n      {\n        text: '多客服消息转发',\n        link: '/4.x/official-account/message-transfer'\n      },\n      {\n        text: '消息群发',\n        link: '/4.x/official-account/broadcasting.html'\n      },\n      {\n        text: '模板消息',\n        link: '/4.x/official-account/template_message.html'\n      },\n      { text: '用户', link: '/4.x/official-account/user.html' },\n      { text: '用户标签', link: '/4.x/official-account/user-tag.html' },\n      { text: '网页授权', link: '/4.x/official-account/oauth.html' },\n      { text: 'JSSDK', link: '/4.x/basic-services/jssdk.html' },\n      { text: '临时素材', link: '/4.x/basic-services/media.html' },\n      { text: '二维码', link: '/4.x/basic-services/qrcode.html' },\n      { text: '短网址', link: '/4.x/basic-services/url.html' },\n      { text: '素材管理', link: '/4.x/official-account/material.html' },\n      { text: '菜单', link: '/4.x/official-account/menu.html' },\n      { text: '卡券', link: '/4.x/official-account/card.html' },\n      { text: '门店', link: '/4.x/official-account/poi.html' },\n      {\n        text: '客服',\n        link: '/4.x/official-account/customer_service.html'\n      },\n      {\n        text: '摇一摇周边',\n        link: '/4.x/official-account/shake-around.html'\n      },\n      {\n        text: '数据统计与分析',\n        link: '/4.x/official-account/data_cube.html'\n      },\n      { text: '语义理解', link: '/4.x/official-account/semantic.html' },\n      { text: '自动回复', link: '/4.x/official-account/reply.html' },\n      { text: '评论数据管理', link: '/4.x/official-account/comment.html' },\n      { text: '返佣商品', link: '/4.x/official-account/goods.html' }\n    ]\n  },\n  {\n    text: '微信支付',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/payment/index.html' },\n      { text: '订单', link: '/4.x/payment/order.html' },\n      { text: '退款', link: '/4.x/payment/refund.html' },\n      { text: '账单', link: '/4.x/payment/bill.html' },\n      { text: '通知', link: '/4.x/payment/notify.html' },\n      { text: '红包', link: '/4.x/payment/redpack.html' },\n      { text: '付款码支付', link: '/4.x/payment/micropay.html' },\n      { text: '扫码支付', link: '/4.x/payment/scan-pay.html' },\n      { text: 'JSSDK', link: '/4.x/payment/jssdk.html' },\n      { text: '企业付款', link: '/4.x/payment/transfer.html' },\n      { text: '撤销订单', link: '/4.x/payment/reverse.html' },\n      { text: '安全工具', link: '/4.x/payment/security.html' },\n      { text: '分账', link: '/4.x/payment/profit-sharing.html' }\n    ]\n  },\n  {\n    text: '小程序',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/mini-program/index.html' },\n      { text: '小程序码', link: '/4.x/mini-program/app_code.html' },\n      {\n        text: '客服消息',\n        link: '/4.x/mini-program/customer_service.html'\n      },\n      { text: '数据统计与分析', link: '/4.x/mini-program/data_cube.html' },\n      { text: '微信登录', link: '/4.x/mini-program/auth.html' },\n      {\n        text: '模板消息',\n        link: '/4.x/mini-program/template_message.html'\n      },\n      { text: '消息解密', link: '/4.x/mini-program/decrypt.html' },\n      { text: '物流助手', link: '/4.x/mini-program/express.html' },\n      { text: '生物认证', link: '/4.x/mini-program/soter.html' },\n      { text: '插件管理', link: '/4.x/mini-program/plugin.html' },\n      { text: '附近的小程序', link: '/4.x/mini-program/nearby_poi.html' },\n      {\n        text: '订阅消息',\n        link: '/4.x/mini-program/subscribe_message.html'\n      }\n    ]\n  },\n  {\n    text: '开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/open-platform/index.html' },\n      { text: '服务端', link: '/4.x/open-platform/server.html' },\n      {\n        text: '代授权',\n        link: '/4.x/open-platform/authorizer-delegate.html'\n      }\n    ]\n  },\n  {\n    text: '企业微信',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/wework/index.html' },\n      { text: '服务端', link: '/4.x/wework/server.html' },\n      { text: '应用管理', link: '/4.x/wework/agents.html' },\n      { text: '消息发送', link: '/4.x/wework/message.html' },\n      { text: '通讯录', link: '/4.x/wework/contacts.html' },\n      { text: '网页授权', link: '/4.x/wework/oauth.html' },\n      {\n        text: '客户联系(原外部联系人)',\n        link: '/4.x/wework/external-contact.html'\n      },\n      { text: '自定义菜单', link: '/4.x/wework/menu.html' },\n      { text: '素材管理', link: '/4.x/wework/media.html' },\n      { text: 'OA', link: '/4.x/wework/oa.html' },\n      { text: '企业互联', link: '/4.x/wework/corp-group.html' },\n      { text: '会话内容存档', link: '/4.x/wework/msg-audit.html' },\n      { text: '电子发票', link: '/4.x/wework/invoice.html' },\n      { text: '小程序', link: '/4.x/wework/mini-program.html' },\n      { text: 'JSSDK', link: '/4.x/wework/jssdk.html' },\n      { text: '群机器人', link: '/4.x/wework/group-robot.html' },\n      { text: '移动端', link: '/4.x/wework/mobile.html' }\n    ]\n  },\n  {\n    text: '企业微信开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/open-work/index.html' },\n      { text: '服务商接口', link: '/4.x/open-work/provider.html' },\n      { text: '服务商', link: '/4.x/open-work/server.html' },\n      { text: '第三方应用接口', link: '/4.x/open-work/service.html' },\n      { text: '企业相关', link: '/4.x/open-work/work.html' }\n    ]\n  },\n  {\n    text: '小微商户',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/4.x/micro-merchant/index.html' },\n      {\n        text: '获取平台证书',\n        link: '/4.x/micro-merchant/certficates.html'\n      },\n      { text: '图片上传', link: '/4.x/micro-merchant/media.html' },\n      {\n        text: '商户入驻',\n        link: '/4.x/micro-merchant/submit-application.html'\n      },\n      { text: '商户升级', link: '/4.x/micro-merchant/upgrade.html' },\n      { text: '商户信息修改', link: '/4.x/micro-merchant/material.html' },\n      { text: '提现相关', link: '/4.x/micro-merchant/withdraw.html' },\n      {\n        text: '商户配置',\n        link: '/4.x/micro-merchant/merchant-config.html'\n      }\n    ]\n  },\n  {\n    text: '自定义',\n    collapsible: true,\n    items: [\n      { text: 'Access Token', link: '/4.x/customize/access_token.html' },\n      { text: '缓存', link: '/4.x/customize/cache.html' },\n      { text: '模块替换', link: '/4.x/customize/replace-service.html' }\n    ]\n  },\n  {\n    text: '通用',\n    collapsible: true,\n    items: [\n      { text: 'API 调用', link: '/4.x/client.html' }\n    ]\n  },\n  {\n    text: '其他',\n    collapsible: true,\n    items: [\n      { text: '常见问题', link: '/4.x/troubleshooting.html' },\n      { text: '参与贡献', link: '/4.x/contributing.html' }\n    ]\n  }\n]\n"
  },
  {
    "path": "docs/src/4.x/troubleshooting.md",
    "content": "# 疑难解答\n\n在微信公众平台开发的道路上，遍布着各种大大小小的坑，有的人掉坑里，几经折腾又爬出来了，然后拍拍屁股走人。然而坑还在那里，还会继续有后来人掉进去……\n\n这，是我们不愿看到的。\n\n所以在这里，我们将陆续将微信开发中可能遇到的各种疑难问题进行汇总，并给出对应的解决办法。一般情况下，这些问题都可以对号入座，轻松地解决。但也不排除特殊情况，这时候你遇到的问题与文中某一个症状一致，但文中所给的解决方案并不凑效，这种情况下就需要发挥你自己的智慧，去……折腾了……\n\n我们期待这一版块为各位的开发带来便利，同时也希望各位本着开源、分享的精神对其进行补充和完善，将各种坑一一填小、填平，让微信开发变得不那么痛苦，甚至，变成一件快乐的事……\n\n## 时区不对\n\n使用命令 `date` 可以在服务器上查看当前时间，如果发现时区不对则需要修改时区：[Setting The Correct Timezone In CentOS And Ubuntu Servers With NTP](https://www.liberiangeek.net/2013/02/setting-the-correct-timezone-in-centos-and-ubuntu-servers-with-ntp/)\n\n## curl: (60) SSL certificate problem: unable to get local issuer certificate\n\n这是 SSL 证书问题所致，在使用 SDK 调用微信支付等相关的操作时可能会遇到报 “SSL certificate problem: unable to get local issuer certificate” 的错误。\n\n微信公众平台提供的文档中建议对部分较敏感的操作接口使用 https 协议进行访问，例如微信支付和红包等接口中涉及到操作商户资金的一些操作。\nwechat SDK 遵循了官方建议，所以在调用这些接口时，除了按照官方文档设置操作证书文件外，还需要保证服务器正确安装了 CA 证书。\n\n1. 下载 CA 证书\n\n你可以从 http://curl.haxx.se/ca/cacert.pem 下载 或者 使用[微信官方提供的证书](https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=4_3)中的 CA 证书 `rootca.pem` 也是同样的效果。\n\n2. 在 `php.ini` 中配置 CA 证书\n\n只需要将上面下载好的 CA 证书放置到您的服务器上某个位置，然后修改 `php.ini` 的 `curl.cainfo` 为该路径（**绝对路径！**），重启 `php-fpm` 服务即可。\n\n```ini\ncurl.cainfo = /path/to/downloaded/cacert.pem\n```\n\n> 注意证书文件**路径为绝对路径**！以自己实际情况为准。\n\n其它修改 HTTP 类源文件的方式是不允许的。\n\n## cURL error 56: SSLRead() return error -9806\n\n目前在 OSX 下，发现使用 HomeBrew 装的 PHP 7.0 有这个问题，解决方案是重新 brew 安装 PHP：\n\n```shell\n$ brew install homebrew/php/php70 --with-homebrew-openssl --with-homebrew-curl --without-snmp -vvv\n```\n\n验证：\n\n```shell\n$ php -i | grep 'OpenSSL support'\n\nOpenSSL support => enabled\nOpenSSL support => enabled\n```\n\n## 支付失败！当前页面的 URL 未注册\n\n这是由于微信支付授权目录未正确配置引起的。此时开发者应该登录微信公众平台，进入**【微信支付】->【开发设置】**进行设置。\n\n1. 公众号可添加 3 个支付授权目录，满足不同应用使用同一个公众号进行支付的业务需求。\n\n2. 正确的**【支付授权目录】**应以 `http://` 或 `https://` 开头，并以正斜杠 `/` 结尾，授权目录所包含的域名**必须经过 ICP 备案**。\n\n3. 支付授权目录需**细化至二级或三级目录**。\n\n4. 所有**实际调起微信支付请求的页面都必须要所配置的支付授权目录之下**。\n\n5. 在开发过程中，也可以使用测试授权目录进行开发测试，此时还**应该将参与测试的个人微信号添加到测试白名单中**，否则将出现对应的错误提示……\n\n> \\*页面**、**目录**、**URL **以及**域名\\*\\*等几个基本概念，并对自己所使用的框架的路由机制有一个大致了解。这样你才会知道自己正在配置的参数是个啥玩意儿，有什么卵用…… :smile:\n\n## redirect_url 参数错误\n\n这是由于程序使用了**网页授权**而公众号没有正确配置**【网页授权域名】**所致。此时你需要登录[微信公众平台](https://mp.weixin.qq.com/)，在【开发】->【接口权限】页面找到**网页授权获取用户基本信息**进行配置并保存。\n\n1. 网页授权域名应该为通过 ICP 备案的有效域名，否则保存时无法通过安全监测。\n\n2. 网页授权域名即程序完成授权获得授权 code 后跳转到的页面的域名，一般情况下为你的业务域名。\n\n3. 网页授权域名配置成功后会立即生效。\n\n4. 公众号的网页授权域名只可配置一个，请合理规划你的业务，否则你会发现……授权域名不够用哈。\n\n## [JSAPI] config: invalid url domain\n\n在使用 JS-SDK 进行开发时，每个页面都需要调用 wx.config() 方法配置 JSPAI 参数。如果没有正确配置 **JSAPI 安全域名**并且开启了调试模式，此时就报此错误。遇到这个问题时，开发者需要登录微信公众平台，进入【公众号设置】->【功能设置】页面，将项目所使用的域名添加至 **【JSAPI 安全域名】**列表中。\n\n1. 一个公众号同时最多可绑定**三个**安全域名，并且这些域名必须为通过 **ICP 备案**的**一级或一级以上**的有效域名。\n\n2. JSAPI 安全域名每个月**限修改三次**，修改任何一个都算，所以，请谨慎操作。\n\n3. 如果需要使用 JSAPI 调起支付功能，则支付目录必须也在所配置的**安全域名之下**，并且需要将支付目录添加至**支付授权目录**。\n\n## token 验证失败、向公众号发送消息无任何反应\n\n相信对接公众号一般是微信开发者进行开发过程中最先进行的工作，而在这看似简单的配置操作中，也可能会掉坑里。\n最常见的两种情况就如下：\n\n1. 确认你 “**启用**” 了开发模式， token 验证通过不代表启用，保存后也不代表启用。看到红色 “**停用**” 才真正的是启用了。\n\n2. 配置好 URL(服务器地址)以及 Token(令牌)后，点击保存时提示**token 验证失败**，出现这种情况的原因有多种，其中之一便是网络不稳定，所以**可尝试多次保存**，若始终无法通过再排查其它可能因素。\n\n3. 配置保存成功之后，向公众号发送消息无任何反应，自己的消息处理程序也没有被调用的记录（无对应日志）。这种情况下如果你尝试**反复停用和启用服务器配置**，可能突然间惊奇地了现，问题莫名其妙的解决了。\n\n4. 使用在线调试工具的消息接口，http://mp.weixin.qq.com/debug/， 只要返回绿色的“**请求成功**”，就代表你的代码没有问题，请**重复上面第 3 项**再测试。\n\n5. **如果你在用什么本地开发工具，或者什么 ngrok 代理到本机这样的开发方式，那么失败就很正常了，微信服务器到你机器的网络延迟太大（还是用服务器开发吧）。**\n\n> 器 TOKEN 验证原理（官方文档有说明）并谨记服务器验证时使用 GET 方式访问，而公众平台向你的服务器发送消息/数据则使用 POST 方式，所以服务器验证成功之后，在某些启用了 CSRF 验证的框架里，接收消息时可能还会遇到 CSRF 相关的问题，请根据自己项目实际情况进行排查。\n> 另外有的朋友的 Laravel 里使用了 laravel-debugbar，这个组件的原理是在页面输出时在后面添加 HTML 来实现的，所以它会改变我们返回给微信的内容，此时要么卸载，要么禁用掉它。\n\n## Maximum function nesting level of '100' reached, aborting!\n\n在使用了 Xdebug 的环境下可能出现这个问题。这是由于 Xdebug 限制函数嵌套的最大层级数（默认为 100），当嵌套次数达到该值便会触发 Xdebug 跳出嵌套并报此错误。\n\n为避免这个问题，**可以将 Xdebug 的 max_nesting_level 参数适当设置大一些**，通常设置为 200 就可以了（当然可根据自己实际情况设置为更大的值）。\n\n如下，修改 php.ini 配置文件后，重启 Apache 或 php-fpm 服务即可。\n\n```ini\nxdebug.max_nesting_level=200\n```\n\n## 扫码支付 获取商户订单信息超时或商户返回 httpcode 非 200!\n\n1.确定签名正确,使用 SDK 基本上不会出什么问题 2.微信调用扫码支付回调链接,使用 POST 方式,确定服务器回调方法是否取消 csrf 验证\n\n## Request access_token fail:{\"errcode\":61023,\"errmsg\":\"refresh_token is invalid hint: [zDNUIA07582974]\"}！\n\n在用户授权时会获得该 authorizer_refresh_token 刷新令牌，而当缓存或数据库存储的该 authorizer_refresh_token 刷新令牌丢失后，可能会出现该问题，微信文档中说明\n\n1.接口调用凭据刷新令牌（在授权的公众号具备 API 权限时，才有此返回值），刷新令牌主要用于第三方平台获取和刷新已授权用户的 access_token，只会在授权时刻提供，请妥善保存。\n\n2.一旦丢失，只能让用户重新授权，才能再次拿到新的刷新令牌(https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=)。\n\n3.为避免该问题，请将存储该刷新令牌的缓存有效期设置为 0(永久存储)，并尽量不要去将该缓存或数据库清空。\n\n如下：以 redis 为例。\n\n```php\n'expire'     => 0,\n```\n\n## 替换 Handler，让 easywechat 支持协程\n\n在 Swoft、IMI 等基于 Swoole 的协程框架中使用 easywechat 时，不免会有一个问题，就是 guzzle 客户端内置的 handler 不支持协程的问题。\n这里，提供一个办法主动替换容器内的 guzzle_handler.\n\n```php\n$app = Factory::miniProgram($config);\n$app['guzzle_handler'] = CoroutineHandler::class;\n```\n\n鉴于有些同学找不到可用的 `CoroutineHandler`，这里提供几个，供大家使用。\n\n- hyperf/guzzle\n- yurunsoft/guzzle-swoole\n- mix/guzzle-hook\n"
  },
  {
    "path": "docs/src/4.x/wework/agents.md",
    "content": "# 应用管理\n\n> 企业微信在 17 年 11 月对 API 进行了大量的改动，应用管理部分已经没啥用了\n\n应用管理是企业微信中比较特别的地方，因为它的使用是不基于应用的，或者说基于任何一个应用都能访问这些 API，所以在用法上是直接调用 work 实例的 `agent` 属性。\n\n```php\n$config = [\n    ...\n];\n\n$app = Factory::work($config);\n```\n\n## 应用列表\n\n```php\n$agents = $app->agent->list(); // 测试拿不到内容\n```\n\n## 应用详情\n\n```php\n$agents = $app->agent->get($agentId); // 只能传配置文件中的 id，API 改动所致\n```\n\n## 设置应用\n\n```php\n$agents = $app->agent->set($agentId, ['foo' => 'bar']);\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/contacts.md",
    "content": "# 通讯录\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 通讯录的 secret\n    //...\n];\n\n$contacts = Factory::work($config);\n```\n\n## 成员管理\n### 创建成员\n\n```php\n$data = [\n    \"userid\" => \"overtrue\",\n    \"name\" => \"超哥\",\n    \"english_name\" => \"overtrue\"\n    \"mobile\" => \"1818888888\",\n];\n$contacts->user->create($data);\n```\n\n### 读取成员\n\n```php\n$contacts->user->get('overtrue');\n```\n\n### 更新成员\n\n```php\n$contacts->user->update('overtrue', [\n    \"isleader\": 0,\n    'position' => 'PHP 酱油工程师',\n    //...\n]);\n```\n\n### 删除成员\n\n```php\n$contacts->user->delete('overtrue');\n// 或者删除多个\n$contacts->user->delete(['overtrue', 'zhangsan', 'wangwu']);\n```\n\n### 获取部门成员\n\n```php\n$contacts->user->getDepartmentUsers($departmentId);\n// 递归获取子部门下面的成员\n$contacts->user->getDepartmentUsers($departmentId, true);\n```\n\n### 获取部门成员详情\n\n```php\n$contacts->user->getDetailedDepartmentUsers($departmentId);\n// 递归获取子部门下面的成员\n$contacts->user->getDetailedDepartmentUsers($departmentId, true);\n```\n\n### 用户 ID 转为 openid\n\n```php\n$contacts->user->userIdToOpenid($userId);\n// 或者指定应用 ID\n$contacts->user->userIdToOpenid($userId, $agentId);\n```\n\n### openid 转为用户 ID\n\n```php\n$contacts->user->openidToUserId($openid);\n```\n\n### 手机号转为用户 ID\n\n```php\n$contacts->user->mobileToUserId($mobile);\n```\n\n### 二次验证\n\n企业在成员验证成功后，调用如下接口即可让成员加入成功\n\n```php\n$contacts->user->accept($userId);\n```\n\n### 邀请成员\n\n企业可通过接口批量邀请成员使用企业微信，邀请后将通过短信或邮件下发通知。\n\n```php\n$params = [\n    'user' => ['UserID1', 'UserID2', 'UserID3'],    // 成员ID列表, 最多支持1000个\n    'party' => ['PartyID1', 'PartyID2'],            // 部门ID列表，最多支持100个\n    'tag' => ['TagID1', 'TagID2'],                  // 标签ID列表，最多支持100个\n];\n\n$contacts->user->invite($params);\n```\n\n> `user`, `party`, `tag` 三者不能同时为空\n\n### 获取邀请二维码\n\n```php\n$sizeType = 1;  // qrcode尺寸类型，1: 171 x 171; 2: 399 x 399; 3: 741 x 741; 4: 2052 x 2052\n\n$contacts->user->getInvitationQrCode($sizeType);\n```\n\n## 部门管理\n\n### 创建部门\n\n```php\n$contacts->department->create([\n        'name' => '广州研发中心',\n        'parentid' => 1,\n        'order' => 1,\n        'id' => 2,\n    ]);\n```\n\n### 更新部门\n\n```php\n$contacts->department->update($id, [\n        'name' => '广州研发中心',\n        'parentid' => 1,\n        'order' => 1,\n    ]);\n```\n\n### 删除部门\n\n```php\n$contacts->department->delete($id);\n```\n\n### 获取部门列表\n\n```php\n$contacts->department->list();\n// 获取指定部门及其下的子部门\n$contacts->department->list($id);\n```\n\n## 标签管理\n\n### 创建标签\n\n```php\n$contacts->tag->create($tagName, $tagId);\n```\n\n### 更新标签名字\n\n```php\n$contacts->tag->update($tagId, $tagName);\n```\n\n### 删除标签\n\n```php\n$contacts->tag->delete($tagId);\n```\n\n### 获取标签列表\n\n```php\n$contacts->tag->list();\n```\n\n### 获取标签成员(标签详情)\n\n```php\n$contacts->tag->get($tagId);\n```\n\n### 增加标签成员\n\n```php\n$contacts->tag->tagUsers($tagId, [$userId1, $userId2, ...]);\n\n// 指定部门\n$contacts->tag->tagDepartments($tagId, [$departmentId1, $departmentId2, ...]);\n```\n\n\n### 删除标签成员\n\n```php\n$contacts->tag->untagUsers($tagId, [$userId1, $userId2, ...]);\n\n// 指定部门\n$contacts->tag->untagDepartments($tagId, [$departmentId1, $departmentId2, ...]);\n```\n\n\n\n\n"
  },
  {
    "path": "docs/src/4.x/wework/external-contact.md",
    "content": "# 外部联系人管理\n\n## 获取实例\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    ...\n];\n\n$app = Factory::work($config);\n\n// 基础接口\n$app->external_contact;\n\n// 「联系我」\n$app->contact_way;\n\n// 消息管理\n$app->external_contact_message;\n\n// 数据统计\n$app->external_contact_statistics;\n```\n\n## 基础接口\n\n### 获取配置了客户联系功能的成员列表\n\n```php\n$app->external_contact->getFollowUsers();\n```\n\n### 获取外部联系人列表\n\n```php\n$userId = 'zhangsan';\n\n$app->external_contact->list($userId);\n```\n\n### 获取外部联系人详情\n\n```php\n$externalUserId = 'woAJ2GCAAAXtWyujaWJHDDGi0mACH71w';\n\n$app->external_contact->get($externalUserId);\n```\n\n\n### 获取离职成员的客户列表\n\n```php\n$pageId = 0;\n$pageSize = 1000;\n$app->external_contact->getUnassigned($pageId, $pageSize);\n```\n\n### 离职成员的外部联系人再分配\n\n```php\n$externalUserId = 'woAJ2GCAAAXtWyujaWJHDDGi0mACH71w';\n$handoverUserId = 'zhangsan';\n$takeoverUserId = 'lisi';\n\n$app->external_contact->transfer($externalUserId, $handoverUserId, $takeoverUserId);\n```\n\n\n## 配置客户联系「联系我」方式\n\n>  注意：\n> 1. 通过API添加的「联系我」不会在管理端进行展示。\n> 2. 每个企业可通过API最多配置10万个「联系我」。\n> 3. 截止 2019-06-21 官方文档没有提供获取所有「联系我」列表的接口，请开发者注意自行保管处理 configId，避免无法溯源。\n\n### 增加「联系我」方式\n\n```php\n$type = 1;\n$scene = 1;\n$config = [\n   'style' => 1,\n   'remark' => '渠道客户',\n   'skip_verify' => true,\n   'state' => 'teststate',\n   'user' => ['UserID1', 'UserID2', 'UserID3'],\n];\n\n$app->contact_way->create($type, $scene, $config);\n\n// {\n//   \"errcode\": 0,\n//   \"errmsg\": \"ok\",\n//   \"config_id\":\"42b34949e138eb6e027c123cba77fad7\"　　\n// }\n```\n\n### 获取「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$app->contact_way->get($configId);\n```\n\n### 更新「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$config = [\n   'style' => 1,\n   'remark' => '渠道客户2',\n   'skip_verify' => true,\n   'state' => 'teststate2',\n   'user' => ['UserID4', 'UserID5', 'UserID6'],\n];\n\n$app->contact_way->update($configId, $config);\n```\n\n### 删除「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$app->contact_way->delete($configId);\n```\n## 客户朋友圈\n\n### 企业发表内容到客户的朋友圈\n\n```php\n$msg = [\n\t\"text\" => [\n\t\t\"content\" => \"文本消息内容\"\n\t],\n\t\"attachments\" => [\n\t\t[\n\t\t\t\"msgtype\" => \"image\",\n\t\t\t\"image\" => [\n\t\t\t\t\"media_id\" => \"MEDIA_ID\"\n\t\t\t]\n\t\t],\n\t\t[\n\t\t\t\"msgtype\" => \"video\",\n\t\t\t\"video\" => [\n\t\t\t\t\"media_id\" => \"MEDIA_ID\"\n\t\t\t]\n\t\t],\n\t\t[\n\t\t\t\"msgtype\" => \"link\",\n\t\t\t\"link\" => [\n\t\t\t\t\"title\" => \"消息标题\",\n\t\t\t\t\"url\" => \"https://example.link.com/path\",\n\t\t\t\t\"media_id\" => \"MEDIA_ID\"\n\t\t\t]\n\t\t]\n\t],\n \t\"visible_range\" => [\n\t\t\"sender_list\" => [\n\t\t\t\"user_list\" => [\"zhangshan\",\"lisi\"],\n\t\t\t\"department_list\" => [2,3]\n\t\t],\n\t\t\"external_contact_list\" => [\n\t\t\t\"tag_list\" => [ \"etXXXXXXXXXX\", \"etYYYYYYYYYY\"]\n\t\t]\n\t]\n];\n\n$app->external_contact_message->moments($msg);\n\n//{\n//\t\"errcode\":0,\n//\t\"errmsg\":\"ok\",\n//\t\"jobid\":\"xxxx\"\n//}\n```\n\n\n## 消息管理\n\n### 添加企业群发消息模板\n\n```php\n$msg = [\n    'external_userid' => [\n        'woAJ2GCAAAXtWyujaWJHDDGi0mACas1w',\n        'wmqfasd1e1927831291723123109r712',\n    ],\n    'sender' => 'zhangsan',\n    'text' => [\n        'content' => '文本消息内容',\n    ],\n    'image' => [\n        'media_id' => 'MEDIA_ID',\n    ],\n    'link' => [\n        'title' => '消息标题',\n        'picurl' => 'https://example.pic.com/path',\n        'desc' => '消息描述',\n        'url' => 'https://example.link.com/path',\n    ],\n    'miniprogram' => [\n        'title' => '消息标题',\n        'pic_media_id' => 'MEDIA_ID',\n        'appid' => 'wx8bd80126147df384',\n        'page' => '/path/index',\n    ],\n];\n\n$app->external_contact_message->submit($msg);\n\n// {\n//     \"errcode\": 0,\n//     \"errmsg\": \"ok\",\n//     \"fail_list\":[\"wmqfasd1e19278asdasdasd\"],\n//     \"msgid\":\"msgGCAAAXtWyujaWJHDDGi0mACas1w\"\n// }\n```\n\n### 获取企业群发消息发送结果\n\n```php\n$msgId = 'msgGCAAAXtWyujaWJHDDGi0mACas1w';\n\n$app->external_contact_message->get($msgId);\n```\n\n### 发送新客户欢迎语\n\n```php\n$welcomeCode = 'WELCOMECODE';\n\n$msg = [\n    'text' => [\n        'content' => '文本消息内容',\n    ],\n    'image' => [\n        'media_id' => 'MEDIA_ID',\n    ],\n    'link' => [\n        'title' => '消息标题',\n        'picurl' => 'https://example.pic.com/path',\n        'desc' => '消息描述',\n        'url' => 'https://example.link.com/path',\n    ],\n    'miniprogram' => [\n        'title' => '消息标题',\n        'pic_media_id' => 'MEDIA_ID',\n        'appid' => 'wx8bd80126147df384',\n        'page' => '/path/index',\n    ],\n];\n\n$app->external_contact_message->sendWelcome($welcomeCode, $msg);\n```\n\n\n## 数据统计\n\n### 获取员工行为数据\n\n```php\n$userIds = [\n    'zhangsan',\n    'lisi'\n];\n\n$from = 1536508800;\n$to = 1536940800;\n\n$app->external_contact_statistics->userBehavior($userIds, $from, $to);\n```\n\n\n"
  },
  {
    "path": "docs/src/4.x/wework/group-robot.md",
    "content": "# 群机器人\n\n## 使用说明\n\n使用前必须先在群组里面添加机器人，然后将 `Webhook 地址` 中的 `key` 取出来，作为示例中 `$groupKey` 的值。\n\n> Webhook 地址示例：<https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=`ab4f609a-3feb-427c-ae9d-b319ca712d36`>\n\n> 微信文档：<https://work.weixin.qq.com/api/doc#90000/90136/91770>\n\n## 发送文本类型消息\n\n快速发送文本消息\n\n```php\n// 获取 Messenger 实例\n$messenger = $app->group_robot_messenger;\n\n// 群组 key\n$groupKey = 'ab4f609a-3feb-427c-ae9d-b319ca712d36';\n\n$messenger->message('大家好，我是本群的\"喝水提醒小助手\"')->toGroup($groupKey)->send();\n// 或者写成\n$messenger->toGroup($groupKey)->send('大家好，我是本群的\"喝水提醒小助手\"');\n```\n\n使用 `Text` 发送文本消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Text;\n\n// 准备消息\n$text = new Text('hello');\n\n// 发送\n$messenger->message($text)->toGroup($groupKey)->send();\n```\n\n@某人：\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Text;\n\n// 通过构造函数传参\n$text = new Text('hello', 'her-cat', '18700000000');\n//$text = new Text('hello', ['her-cat', 'overtrue'], ['18700000000', '18700000001']);\n\n// 通过 userId\n$text->mention('her-cat');\n//$text->mention(['her-cat', 'overtrue']);\n\n// 通过手机号\n$text->mentionByMobile('18700000000');\n//$text->mentionByMobile(['18700000000', '18700000001']);\n\n// @所有人\n$text->mention('@all');\n//$text->mentionByMobile('@all');\n\n$messenger->message($text)->toGroup($groupKey)->send();\n```\n\n## 发送 Markdown 类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Markdown;\n\n$content = '\n# 标题一\n## 标题二\n<font color=\"info\">绿色</font>\n<font color=\"comment\">灰色</font>\n<font color=\"warning\">橙红色</font>\n> 引用文字\n';\n\n$markdown = new Markdown($content);\n\n$messenger->message($markdown)->toGroup($groupKey)->send();\n```\n\n## 发送图片类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Image;\n\n$img = file_get_contents('http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png');\n\n$image = new Image(base64_encode($img), md5($img));\n\n$result = $messenger->message($image)->toGroup($groupKey)->send();\n```\n\n## 发送图文类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\News;\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\NewsItem;\n\n$items = [\n    new NewsItem([\n        'title' => '中秋节礼品领取',\n        'description' => '今年中秋节公司有豪礼相送',\n        'url' => 'https://easywechat.com',\n        'image' => 'http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png',\n    ]),\n\n    //...\n];\n\n$news = new News($items);\n\n$messenger->message($news)->toGroup($groupKey)->send();\n```\n\n## 其他方式\n\n使用 `group_robot` 发送消息。\n\n```php\n$app->group_robot->message('大家好，我是本群的\"喝水提醒小助手\"')->toGroup($groupKey)->send();\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/index.md",
    "content": "## 企业微信\n\n企业微信的使用与公众号以及其它几个应用的使用方式都是一致的，使用 `\\EasyWeChat\\Factory::work($config)` 来初始化：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n\n    'agent_id' => 100020, // 如果有 agend_id 则填写\n    'secret'   => 'xxxxxxxxxx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::work($config);\n```\n\n然后你就可以用 `$app` 来调用企业微信的服务了。"
  },
  {
    "path": "docs/src/4.x/wework/invoice.md",
    "content": "# 电子发票\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n## 查询电子发票\n\nhttps://work.weixin.qq.com/api/doc#11631\n\nAPI:\n\n```php\nmixed get(string $cardId, string $encryptCode)\n```\n\nexample:\n\n```php\n$app->invoice->get('CARDID', 'ENCRYPTCODE');\n```\n\n## 批量查询电子发票\n\nhttps://work.weixin.qq.com/api/doc#11974\n\nAPI:\n\n```php\nmixed select(array $invoices)\n```\n\n> $invoices: 发票参数列表\n\nexample:\n\n```php\n$invoices = [\n    [\"card_id\" => \"CARDID1\", \"encrypt_code\" => \"ENCRYPTCODE1\"],\n    [\"card_id\" => \"CARDID2\", \"encrypt_code\" => \"ENCRYPTCODE2\"]\n];\n\n$app->invoice->select($invoices);\n```\n\n## 更新发票状态\n\nhttps://work.weixin.qq.com/api/doc#11633\n\nAPI:\n\n```php\nmixed update(string $cardId, string $encryptCode, string $status)\n```\n\n> $status: 发报销状态\n>\n> > - INVOICE_REIMBURSE_INIT：发票初始状态，未锁定；\n> > - INVOICE_REIMBURSE_LOCK：发票已锁定，无法重复提交报销;\n> > - INVOICE_REIMBURSE_CLOSURE:发票已核销，从用户卡包中移除\n\n## 批量更新发票状态\n\nhttps://work.weixin.qq.com/api/doc#11633\n\nAPI:\n\n```php\nmixed batchUpdate(array $invoices, string $openid, string $status)\n```\n\nexample:\n\n```php\n$invoices = [\n    [\"card_id\" => \"CARDID1\", \"encrypt_code\" => \"ENCRYPTCODE1\"],\n    [\"card_id\" => \"CARDID2\", \"encrypt_code\" => \"ENCRYPTCODE2\"]\n];\n$openid = 'oV-gpwSU3xlMXbq0PqqRp1xHu9O4';\n\n$status = 'INVOICE_REIMBURSE_CLOSURE';\n\n$app->invoice->batchUpdate($invoices, $openid, $status)\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/media.md",
    "content": "# 临时素材\n\n它的使用是不基于应用的，或者说基于任何一个应用都能访问这些 API，所以在用法上是直接调用 work 实例的 `media` 属性：\n\n**上传的媒体文件限制：**\n\n所有文件size必须大于5个字节\n\n>  - 图片（image）：2MB，支持JPG,PNG格式\n>  - 语音（voice）：2MB，播放长度不超过60s，支持AMR格式\n>  - 视频（video）：10MB，支持MP4格式\n>  - 普通文件（file）：20MB\n\n## 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$app->media->uploadImage($path); // $path 为本地文件路径\n```\n\n## 上传声音\n\n```php\n$app->media->uploadVoice($path);\n```\n\n## 上传视频\n\n```php\n$app->media->uploadVideo($path, $title, $description);\n```\n\n## 上传普通文件\n\n```php\n$app->media->uploadFile($path);\n```\n\n## 获取素材\n\n```php\n$app->media->get($mediaId);\n```\n\n## 上传附件资源\n\n所有文件size必须大于5个字节\n\n目前 商品图册只支持图片类型； 朋友圈只支持图片、视频类型\n\n>  - 图片（image）：10MB，支持JPG,PNG格式，朋友圈类型图片不超过1440 x 1080\n>  - 视频（video） ：10MB，支持MP4格式，朋友圈类型视频时长不超过30秒\n>  - 文件（file） ：10MB\n\n```php\n$app->media->uploadAttachmentResources($path, 'image', 1);\n```"
  },
  {
    "path": "docs/src/4.x/wework/menu.md",
    "content": "# 自定义菜单\n\n自定义菜单是指为单个应用设置自定义菜单功能，所以在使用时请注意调用正确的应用实例。\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n    //...\n];\n$app = Factory::work($config);\n```\n\n## 创建菜单\n\n```php\n$menus = [\n    'button' => [\n        [\n            'name' => '首页',\n            'type' => 'view',\n            'url' => 'https://easywechat.com'\n        ],\n        [\n            'name' => '关于我们',\n            'type' => 'view',\n            'url' => 'https://easywechat.com/about'\n        ],\n        //...\n    ],\n];\n\n$app->menu->create($menus);\n```\n\n## 获取菜单\n\n```php\n$app->menu->get();\n```\n\n## 删除菜单\n\n```php\n$app->menu->delete();\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/message.md",
    "content": "# 消息\n\n## 主动发送消息\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\TextCard;\n\n\n// 获取 Messenger 实例\n$messenger = $app->messenger;\n\n// 准备消息\n$message = new TextCard([\n    'title' => '你的请假单审批通过', \n    'description' => '单号：1928373, ....', \n    'url' => 'http://easywechat.com/oa/....'\n]);\n\n// 发送\n$messenger->message($message)->toUser('overtrue')->send();\n\n```\n\n你也可以很方便的发送普通文本消息：\n\n```php\n$messenger->message('你的请假单（单号：1928373）已经审批通过！')->toUser('overtrue')->send();\n// 或者写成\n$messenger->toUser('overtrue')->send('你的请假单（单号：1928373）已经审批通过！');\n```\n\n## 接收消息\n\n被动接收消息，与回复消息，请参考：[服务端](server)\n"
  },
  {
    "path": "docs/src/4.x/wework/oa.md",
    "content": "# OA\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n## 获取打卡数据\n\nAPI:\n\n```php\nmixed checkinRecords(int $startTime, int $endTime, array $userList, int $type = 3)\n```\n\n> $type: 打卡类型 1：上下班打卡；2：外出打卡；3：全部打卡\n\n```php\n// 全部打卡数据\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"]);\n\n// 获取上下班打卡\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"], 1);\n\n// 获取外出打卡\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"], 2);\n```\n\n## 获取审批数据\n\nAPI:\n\n```php\nmixed approvalRecords(int $startTime, int $endTime, int $nextNumber = null)\n```\n\n> $nextNumber: 第一个拉取的审批单号，不填从该时间段的第一个审批单拉取\n\n```php\n$app->oa->approvalRecords(1492617600, 1492790400);\n\n// 指定第一个拉取的审批单号，不填从该时间段的第一个审批单拉取\n$app->oa->approvalRecords(1492617600, 1492790400, '201704240001');\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/oauth.md",
    "content": "# OAuth\n\n> 此文档为企业微信内部应用开发的网页授权\n\n[企业微信官方文档](https://work.weixin.qq.com/api/doc#90000/90135/91020)\n\n创建实例：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n    'agent_id' => 100001,\n];\n\n$app = Factory::work($config);\n```\n\n## 跳转授权\n\n```php\n// $callbackUrl 为授权回调地址\n$callbackUrl = 'https://xxx.xxx'; // 需设置可信域名\n\n// 返回一个 redirect 实例\n$redirect = $app->oauth->redirect($callbackUrl);\n\n// 获取企业微信跳转目标地址\n$targetUrl = $redirect->getTargetUrl();\n\n// 直接跳转到企业微信授权\n$redirect->send();\n```\n\n## 获取授权用户信息\n\n在回调页面中，你可以使用以下方式获取授权者信息：\n\n```php\n$user = $app->oauth->detailed()->user();\n\n// 获取用户信息\n$user->getId(); // 对应企业微信英文名（userid）\n$user->getOriginal(); // 获取企业微信接口返回的原始信息\n```\n\n获取用户其他信息需调用通讯录接口，参考：[企业微信通讯录 API](https://github.com/EasyWeChat/docs/blob/master/wework/contacts.md)\n\n```php\n\n```\n"
  },
  {
    "path": "docs/src/4.x/wework/server.md",
    "content": "## 服务端\n\n我们在企业微信应用开启接收消息的功能，将设置页面的 token 与 aes key 配置到 agents 下对应的应用内：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n\n    'agent_id' => 100022,\n    'secret'   => 'xxxxxxxxxx',\n\n    // server config\n    'token' => 'xxxxxxxxx',\n    'aes_key' => 'xxxxxxxxxxxxxxxxxx',\n\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n接着配置服务端与公众号的服务端用法一样：\n\n```php\n$app->server->push(function(){\n    return 'Hello easywechat.';\n});\n\n$response = $app->server->serve();\n\n$response->send();\n```\n\n`$response` 为 `Symfony\\Component\\HttpFoundation\\Response` 实例，根据你的框架情况来决定如何处理响应。\n"
  },
  {
    "path": "docs/src/5.x/basic-services/content_security.md",
    "content": "# 内容安全接口\n\n## 文本安全内容检测\n\n用于校验一段文本是否含有违法内容。\n\n### 频率限制\n\n单个appid调用上限为2000次/分钟，1,000,000次/天\n\n### 调用示例\n\n```php\n// 传入要检测的文本内容，长度不超过500K字节\n$content = '你好';\n\n$result = $app->content_security->checkText($content);\n\n// 正常返回 0\n{\n    \"errcode\": \"0\",\n    \"errmsg\": \"ok\"\n}\n\n//当 $content 内含有敏感信息，则返回 87014\n{\n    \"errcode\": 87014,\n    \"errmsg\": \"risky content\"\n}\n```\n\n## 图片安全内容检测\n\n用于校验一张图片是否含有敏感信息。如涉黄、涉及敏感人脸（通常是政治人物）。\n\n### 频率限制\n\n单个appid调用上限为1000次/分钟，100,000次/天\n\n### 调用示例\n\n```php\n// 所传参数为要检测的图片文件的绝对路径，图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334，同时文件大小以不超过 300K 为宜，否则可能报错\n$result = $app->content_security->checkImage('/path/to/the/image');\n\n// 正常返回 0\n{\n    \"errcode\": \"0\",\n    \"errmsg\": \"ok\"\n}\n\n// 当图片文件内含有敏感内容，则返回 87014\n{\n    \"errcode\": 87014,\n    \"errmsg\": \"risky content\"\n}\n```\n\n## 重要说明\n\n目前上述两个接口仅支持在小程序中使用，示例中的 `$app` 表示小程序实例，即:\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 下面为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::miniProgram($config);\n```\n"
  },
  {
    "path": "docs/src/5.x/basic-services/jssdk.md",
    "content": "# JSSDK\n\n微信 JSSDK 官方文档：https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115\n\n## API\n\n#### 获取JSSDK的配置数组\n\n```php\n$app->jssdk->buildConfig(array $APIs, $debug = false, $beta = false, $json = true, array $openTagList = []);\n```\n\n默认返回 JSON 字符串，当 `$json` 为 `false` 时返回数组，你可以直接使用到网页中。\n\n#### 设置当前URL\n\n```php\n$app->jssdk->setUrl($url)\n```\n如果不想用默认读取的URL，可以使用此方法手动设置，通常不需要。\n\n\n#### 示例\n\n我们可以生成js配置文件：\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\n    wx.config(<?php echo $app->jssdk->buildConfig(array('updateAppMessageShareData', 'updateTimelineShareData'), true) ?>);\n</script>\n```\n结果如下：\n\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\nwx.config({\n    debug: true, // 请在上线前删除它\n    appId: 'wx3cf0f39249eb0e60',\n    timestamp: 1430009304,\n    nonceStr: 'qey94m021ik',\n    signature: '4F76593A4245644FAE4E1BC940F6422A0C3EC03E',\n    jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']\n});\n</script>\n```\n\n"
  },
  {
    "path": "docs/src/5.x/basic-services/media.md",
    "content": "# 临时素材\n\n上传的临时多媒体文件有格式和大小限制，如下：\n\n> - 图片（image）: 2M，支持 `JPG` 格式\n> - 语音（voice）：2M，播放长度不超过 `60s`，支持 `AMR\\MP3` 格式\n> - 视频（video）：10MB，支持 `MP4` 格式\n> - 缩略图（thumb）：64KB，支持 `JPG` 格式\n\n## 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$app->media->uploadImage($path);\n```\n\n## 上传声音\n\n```php\n$app->media->uploadVoice($path);\n```\n\n## 上传视频\n\n```php\n$app->media->uploadVideo($path, $title, $description);\n```\n\n## 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$app->media->uploadThumb($path);\n```\n\n## 上传群发视频\n\n上传视频获取 `media_id` 用以创建群发消息用。\n\n```php\n$app->media->uploadVideoForBroadcasting($path, $title, $description);\n\n//{\n//  \"media_id\": \"rF4UdIMfYK3efUfyoddYRMU50zMiRmmt_l0kszupYh_SzrcW5Gaheq05p_lHuOTQ\",\n//  \"title\": \"TITLE\",\n//  \"description\": \"Description\"\n//}\n```\n\n## 创建群发消息\n\n不要与上面 **上传群发视频** 搞混了，上面一个是上传视频得到 `media_id`，这个是使用该 `media_id` 加标题描述 **创建一条消息素材** 用来发送给用户。详情参见：[消息群发](../official-account/broadcasting.md)\n\n```php\n$app->media->createVideoForBroadcasting($mediaId, $title, $description);\n\n//{\n//  \"type\":\"video\",\n//  \"media_id\":\"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc\",\n//  \"created_at\":1398848981\n//}\n```\n\n## 获取临时素材内容\n\n比如图片、语音等二进制流内容，响应为 `EasyWeChat\\Kernel\\Http\\StreamResponse` 实例。\n\n```php\n$stream = $app->media->get($mediaId);\n\nif ($stream instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n  // 以内容 md5 为文件名存到本地\n  $stream->save('保存目录');\n\n  // 自定义文件名，不需要带后缀\n  $stream->saveAs('保存目录', '文件名');\n}\n```\n\n## 获取 JSSDK 上传的高清语音\n\n```php\n$stream = $app->media->getJssdkMedia($mediaId);\n$stream->saveAs('保存目录', 'custom-name.speex');\n```\n"
  },
  {
    "path": "docs/src/5.x/basic-services/qrcode.md",
    "content": "# 二维码\n\n目前有 2 种类型的二维码：\n\n1. 临时二维码，是有过期时间的，最长可以设置为在二维码生成后的 **30天**后过期，但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景\n2. 永久二维码，是无过期时间的，但数量较少（目前为最多10万个）。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。\n\n## 创建临时二维码\n\n```php\n$result = $app->qrcode->temporary('foo', 6 * 24 * 3600);\n\n// Array\n// (\n//     [ticket] => gQFD8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTmFjVTRWU3ViUE8xR1N4ajFwMWsAAgS2uItZAwQA6QcA\n//     [expire_seconds] => 518400\n//     [url] => http://weixin.qq.com/q/02NacU4VSubPO1GSxj1p1k\n// )\n```\n\n## 创建永久二维码\n\n```php\n$result = $app->qrcode->forever(56);// 或者 $app->qrcode->forever(\"foo\");\n// Array\n// (\n//     [ticket] => gQFD8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTmFjVTRWU3ViUE8xR1N4ajFwMWsAAgS2uItZAwQA6QcA\n//     [url] => http://weixin.qq.com/q/02NacU4VSubPO1GSxj1p1k\n// )\n```\n\n## 获取二维码网址\n\n```php\n$url = $app->qrcode->url($ticket);\n// https://api.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET\n```\n\n## 获取二维码内容\n\n```php\n$url = $app->qrcode->url($ticket);\n\n$content = file_get_contents($url); // 得到二进制图片内容\n\nfile_put_contents(__DIR__ . '/code.jpg', $content); // 写入文件\n```\n"
  },
  {
    "path": "docs/src/5.x/basic-services/url.md",
    "content": "# 短网址服务\n\n主要使用场景： 开发者用于生成二维码的原链接（商品、支付二维码等）太长导致扫码速度和成功率下降，将原长链接通过此接口转成短链接再生成二维码将大大提升扫码速度和成功率。\n\n## 长链接转短链接\n\n```php\n$shortUrl = $app->url->shorten('https://easywechat.com');\n//\n(\n    [errcode] => 0\n    [errmsg] => ok\n    [short_url] => https://w.url.cn/s/Aq7jWrd\n)\n```"
  },
  {
    "path": "docs/src/5.x/contributing.md",
    "content": "# 贡献代码\n\n## 开发\n\n我们欢迎广大开发者贡献大家的智慧，让我们共同让它变得更完美.\n\n### 开始之前\n\n请严格遵循以下代码标准:\n\n> - [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md).\n> - 使用 4 个空格作为缩进。\n\n### 流程\n\n1. Fork [overtrue/wechat](https://github.com/overtrue/wechat) 到本地.\n2. 创建新的分支：\n\n```shell\n    $ git checkout -b new_feature\n```\n\n3. 编写代码。\n4. Push 到你的分支:\n\n```shell\n    $ git push origin new_feature\n```\n\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n> 注意：注释请使用英文\n\n## 更新文档\n\n我们的文档也是开源的，源代码在 [w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/master/docs)。\n\n### 流程\n\n1. Fork [w7corp/EasyWeChat](https://github.com/w7corp/easywechat)\n2. Clone 到你的电脑：\n\n```shell\n    $ git clone https://github.com/<username>/site.git\n    $ cd docs\n```\n\n3. 创建新的分支，编辑文档\n4. Push 到你的分支。\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n## 报告 Bug\n\n当你在使用过程中遇到问题，请查阅 [疑难解答](troubleshooting.html) 或者在这里提问 [GitHub](https://github.com/overtrue/wechat/issues). 如果还是不能解决你的问题，请到 GitHub 联系我们。\n\n[overtrue/wechat]: https://github.com/overtrue/wechat\n"
  },
  {
    "path": "docs/src/5.x/customize/access_token.md",
    "content": "# Access Token\n\n\n我们一个 SDK 应用在初始化以后，你可以在任何时机从应用中拿到该配置下的 Access Token 实例：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    //...\n];\n\n$app = Factory::officialAccount($config);\n\n// 获取 access token 实例\n$accessToken = $app->access_token;\n$token = $accessToken->getToken(); // token 数组  token['access_token'] 字符串\n$token = $accessToken->getToken(true); // 强制重新从微信服务器获取 token.\n```\n\n## 修改 `$app` 的 Access Token\n\n```php\n$app['access_token']->setToken($newAccessToken, 7200);\n```\n\n例如：\n\n```php\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675');\n// 或者指定过期时间\n$app['access_token']->setToken('ccfdec35bd7ba359f6101c2da321d675', 3600);  // 单位：秒\n```\n"
  },
  {
    "path": "docs/src/5.x/customize/cache.md",
    "content": "# 缓存\n\n\n本项目使用 [symfony/cache](https://github.com/symfony/cache) 来完成缓存工作，它支持基本目前所有的缓存引擎。\n\n在我们的 SDK 中的所有缓存默认使用文件缓存，缓存路径取决于 PHP 的临时目录，如果你需要自定义缓存，那么你需要做如下的事情：\n\n你可以参考[symfony/cache官方文档](https://symfony.com/doc/current/components/cache.html) 来替换掉应用中默认的缓存配置：\n\n\n## 以 redis 为例\n\n\n### Symfony 4.3 + \n\n> 请先安装 redis 拓展：`composer require predis/predis`\n\n```php\n\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\n\n// 创建 redis 实例\n$client = new \\Predis\\Client('tcp://10.0.0.1:6379');\n\n// 创建缓存实例\n$cache = new RedisAdapter($client);\n\n// 替换应用中的缓存\n$app->rebind('cache', $cache);\n```\n\n### Symfony 3.4 + \n\n> 请先安装 redis 拓展：https://github.com/phpredis/phpredis\n\n```php\n\nuse Symfony\\Component\\Cache\\Simple\\RedisCache;\n\n// 创建 redis 实例\n$redis = new Redis();\n$redis->connect('redis_host', 6379);\n\n// 创建缓存实例\n$cache = new RedisCache($redis);\n\n// 替换应用中的缓存\n$app->rebind('cache', $cache);\n```\n\n\n### Laravel 中使用\n\n在 Laravel 中框架使用 [predis/predis](https://github.com/nrk/predis)：\n\n### Symfony 4.3 + \n\n> 请先安装 redis 拓展：`composer require predis/predis`\n\n```php\n\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\n\n// 创建缓存实例\n$cache = new RedisAdapter(app('redis')->connection()->client());\n$app->rebind('cache', $cache);\n\n```\n\n### Symfony 3.4 + \n\n```php\n\nuse Symfony\\Component\\Cache\\Simple\\RedisCache;\n\n$predis = app('redis')->connection()->client(); // connection($name), $name 默认为 `default`\n$cache = new RedisCache($predis);\n\n$app->rebind('cache', $cache);\n```\n\n> 上面提到的 `app('redis')->connection($name)`, 这里的 `$name` 是 laravel 项目中配置文件 `database.php` 中 `redis` 配置名 `default`：https://github.com/laravel/laravel/blob/master/config/database.php#L118\n> 如果你使用的其它连接，对应传名称就好了。\n\n## 使用自定义的缓存方式\n\n如果你发现 symfony 提供的十几种缓存方式都满足不了你的需求的话，那么你可以自己建立一个类来完成缓存操作，前提这个类得实现接口：[PSR-16](http://www.php-fig.org/psr/psr-16/)\n\n该接口有以下方法需要实现：\n\n```php\n   public function get($key, $default = null);\n   public function set($key, $value, $ttl = null);\n   public function delete($key);\n   public function clear();\n   public function getMultiple($keys, $default = null);\n   public function setMultiple($values, $ttl = null);\n   public function deleteMultiple($keys);\n   public function has($key);\n```\n\n下面为一个示例：\n\n```php\n<?php\n\nuse Psr\\SimpleCache\\CacheInterface;\n\nclass MyCustomCache implements CacheInterface\n{\n    public function get($key, $default = null)\n    {\n        // your code\n    }\n\n    public function set($key, $value, $ttl = null)\n    {\n        // your code\n    }\n\n    public function delete($key)\n    {\n        // your code\n    }\n\n    public function clear()\n    {\n        // your code\n    }\n\n    public function getMultiple($keys, $default = null)\n    {\n        // your code\n    }\n\n    public function setMultiple($values, $ttl = null)\n    {\n        // your code\n    }\n\n    public function deleteMultiple($keys)\n    {\n        // your code\n    }\n\n    public function has($key)\n    {\n        // your code\n    }\n}\n```\n\n然后实例化你的缓存类并在 EasyWeChat 里使用它：\n\n```php\n$app->rebind('cache', new MyCustomCache());\n```\n\nOK，这样就完成了自定义缓存的操作。\n"
  },
  {
    "path": "docs/src/5.x/customize/replace-service.md",
    "content": "# 自定义服务模块\n\n由于使用了容器模式来组织各模块的实例，意味着你可以比较容易的替换掉已经有的服务，以公众号服务为例：\n\n```php\n\n<...>\n\n$app = Factory::officialAccount($config);\n\n$app->rebind('request', new MyCustomRequest(...)); \n```\n\n这里的 `request` 为 SDK 内部服务名称。\n"
  },
  {
    "path": "docs/src/5.x/index.md",
    "content": "> 👋🏼 您当前浏览的文档为 5.x，其它版本的文档请参考：[6.x](/6.x/)、[4.x](/4.x/)、[3.x](/3.x/)\n\n# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n## 环境要求\n\n> - PHP >= 7.4\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\nLaravel 5 拓展包: [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n# 参与贡献\n\n1. fork 当前库到你的名下\n2. 选择你想要修改的语言版本，`zh-CN` 或者 `en`\n3. 在你的本地修改完成审阅过后提交到你的仓库\n4. 提交 PR 并描述你的修改，等待合并\n\n# License\n\nMIT\n"
  },
  {
    "path": "docs/src/5.x/installation.md",
    "content": "# 安装\n\n\n## 环境要求\n\n> - PHP >= 7.4\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\nLaravel 5 拓展包: [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n## 安装\n\n使用 [composer](http://getcomposer.org/):\n\n```shell\n$ composer require overtrue/wechat:~5.0 -vvv\n```"
  },
  {
    "path": "docs/src/5.x/integration.md",
    "content": "# 在框架中使用\n\nEasyWeChat 是一个通用的 Composer 包，所以不需要对框架单独做修改，只要支持 Composer 就能直接使用，当然了，为了更方便的使用，我们收集了以下框架单独提供的拓展包：\n\n## Laravel\n\n>  - [overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)\n\n\n## Symfony\n\n>  - [lilocon/WechatBundle](https://github.com/lilocon/WechatBundle)\n\n## Yii\n\n> - [jianyan74/yii2-easy-wechat](https://github.com/jianyan74/yii2-easy-wechat) 适用于 EasyWeChat 4.x \n> - [max-wen/yii2-easy-wechat](https://github.com/max-wen/yii2-easy-wechat) 适用于 EasyWeChat 3.x \n\n## ThinkPHP\n\n>  - [naixiaoxin/think-wechat](https://github.com/qiqizjl/think-wechat) 适用于 EasyWeChat 4.x\n>  - [zyan/think-wechat](https://github.com/aa24615/think-wechat) 适用于 EasyWeChat 4.x/5.x\n\n## CI\n\nTODO\n\n## Phalcon\n\nTODO\n\n... more\n\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/certficates.md",
    "content": "# 获取平台证书\n调用获取平台证书接口之前，请前往微信支付商户平台升级API证书，升级后才可成功调用本接口。\n\n```php\n// 获取到证书后可以做缓存处理，无需每次重新获取\n$response = $app->certficates->get(bool $returnRaw = false);\n\n// 获取到平台证书后，可以直接使用 setCertificate 方法把证书配置追加到配置项里面去\n$app->setCertificate(string $certificate, string $serialNo);\n```\n> $returnRaw 不填默认为false时，请确保你的PHP已安装了sodium扩展    \n> 返回值：固定array格式的解密后的证书信息\n\n> $returnRaw 传入true时     \n> 返回值：Response对象`$response->getBody()->getContents();`获取到微信返回xml原始数据\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/index.md",
    "content": "# 小微商户\n\n你在阅读本文之前确认你已经仔细阅读了：[微信小微商户专属接口文档](https://pay.weixin.qq.com/wiki/doc/api/xiaowei.php?chapter=19_2)。\n\n## 配置\n\n小微商户整体接口调用方式相对于其他微信接口略有不同，配置时请勿填错，相关配置如下：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'mch_id'           => 'your-mch-id', // 服务商的商户号\n    'key'              => 'key-for-signature', // API 密钥\n    'apiv3_key'        => 'APIv3-key-for-signature', // APIv3 密钥\n    // API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'        => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'         => 'path/to/your/key', // XXX: 绝对路径！！！！\n    // 以下两项配置在获取证书接口时可为空，在调用入驻接口前请先调用获取证书接口获取以下两项配置,如果获取过证书可以直接在这里配置，也可参照本文档获取平台证书章节中示例\n    // 'serial_no'     => '获取证书接口获取到的平台证书序列号',\n    // 'certificate'   => '获取证书接口获取到的证书内容'\n    \n    // 以下为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array'\n    'appid'            => 'wx931386123456789e' // 服务商的公众账号 ID\n];\n\n$app = Factory::microMerchant($config);\n\n```\n\n\n`$app` 在所有相关小微商户的文档都是指 `Factory::microMerchant` 得到的实例，就不在每个页面单独写了。\n\n## 使用时值得注意的地方：\n1、小微商户所有接口中以下列出参数 `version`, `mch_id`, `nonce_str`, `sign`, `sign_type`, `cert_sn` 可不用传入。\n\n2、所有敏感信息无需手动加密，sdk会在调用接口前自动完成加密\n\n3、在调用入驻等需要敏感信息加密的接口前请先调用获取证书接口然后把配置填入配置项\n\n4、入驻成功获取到子商户号后需帮助子商户调用配置修改等接口可以先调用以下方法，方便调用修改等接口时无需再次传入子商户号\n```php\n// $subMchId 为子商户号\n// $appid    服务商的公众账号 ID\n$app->setSubMchId(string $subMchId, string $appId = '');\n```\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/material.md",
    "content": "# 商户信息修改\n## 修改结算银行卡\n\n```php\n$response = $app->material->setSettlementCard([\n    // 'sub_mch_id' => '1230000109',\n    'account_number' => '银行卡号',\n    'bank_name' => '开户银行全称（含支行）',\n    'account_bank' => '开户银行',\n    'bank_address_code' => '开户银行省市编码',\n]);\n```\n## 修改联系信息\n\n```php\n$response = $app->material->updateContact([\n    // 'sub_mch_id' => '1230000109',\n    'mobile_phone' => '手机号',\n    'email' => '邮箱',\n    'merchant_name' => '商户简称',\n]);\n```\n\n> 以上接口调用过 `setSubMchId` 方法则无需传入 `sub_mch_id` 参数"
  },
  {
    "path": "docs/src/5.x/micro-merchant/media.md",
    "content": "# 图片上传\n上传证件照片。支持 jpeg、jpg、bmp、png 格式，图片大小不超过2M。\n\n```php\n// $path string 图片路径\n$response = $app->media->upload($path);\n```\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/merchant-config.md",
    "content": "# 小微商户配置\n\n## 关注功能配置\n\n```php\n$response = $app->merchantConfig->setFollowConfig(string $subAppId, string $subscribeAppId, string $receiptAppId = '', string $subMchId = '');\n```\n> 注意：`subscribe_appid`，`receipt_appid` 两个参数二选一，两个都填的话SDK默认选第一个，具体请参考小微商户专属文档\n\n## 开发配置新增支付目录\n\n```php\n$response = $app->merchantConfig->addPath(string $jsapiPath, string $appId = '', string $subMchId = '');\n```\n\n## 新增对应APPID关联\n\n```php\n$response = $app->merchantConfig->bindAppId(string $subAppId, string $appId = '', string $subMchId = '');\n```\n\n## 开发配置查询\n\n```php\n$response = $app->merchantConfig->getConfig(string $subMchId = '', string $appId = '');\n```\n\n> 以上接口调用过 `setSubMchId` 方法并且两个参数都传入过 则无需传入 `sub_mch_id` 和 `appid` 参数"
  },
  {
    "path": "docs/src/5.x/micro-merchant/submit-application.md",
    "content": "# 商户入驻\n## 申请入驻\n\n使用申请入驻接口提交你的小微商户资料。\n\n```php\n$result = $app->submitApplication([\n    'business_code' => '123456', // 业务申请编号\n    'id_card_copy'  => 'media_id', // 身份证人像面照片\n    // ...\n    // 参数太多就不一一列出，自行根据 (小微商户专属文档 -> 申请入驻api) 填写\n]);\n```\n\n## 查询申请状态\n\n使用申请入驻接口提交小微商户资料后，一般5分钟左右可以通过该查询接口查询具体的申请结果。\n\n```php\n$applymentId = '商户申请单号(applyment_id 申请入驻接口返回)';\n$businessCode = '业务申请编号(business_code)';\n$app->getStatus(string $applymentId, string $businessCode = '');\n```\n> 商户申请单号和业务申请编号填写一个就行了，当 `applyment_id` 已填写时，`business_code` 字段无效。\n\n当查询申请状态为待签约，接口会一并返回签约二维码，服务商需引导商户使用本人微信扫码完成协议签署。\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/upgrade.md",
    "content": "# 商户升级\n## 提交升级申请单\n\n使用“提交升级申请单”接口为小微商户发起升级流程，根据商户实际情况可升级为个体户、企业、党政、机关及事业单位、其他组织。。\n\n```php\n$result = $app->upgrade([\n    'organization_type' => '2', // 主体类型\n    'business_license_copy'  => 'media_id', // 营业执照扫描件\n    // ...\n    // 参数太多就不一一列出，自行根据 (小微商户专属文档 -> 提交升级申请单API) 填写\n]);\n```\n\n## 查询升级申请单状态\n使用“提交升级申请单”接口后，可不定期调用此接口查询申请单状态（建议提交申请后1分钟查询），直至申请单为“完成”状态。\n\n1)若申请状态为待账户验证，请按接口中的指引完成账户验证\n\n2)若申请状态为审核中，微信支付会在2个工作日内完成资料审核\n\n3)若申请状态为待签约，接口会返回签约二维码\n\n```php\n$app->getUpgradeStatus(string $subMchId = '');\n```\n> 调用该接口前调用过 `setSubMchId` 方法则无需传入 `$subMchId` 参数\n"
  },
  {
    "path": "docs/src/5.x/micro-merchant/withdraw.md",
    "content": "# 提现相关\n\n## 查询提现状态\n\n```php\n$response = $app->withdraw->queryWithdrawalStatus($date, $subMchId = '');\n```\n## 重新发起提现\n\n```php\n$response = $app->withdraw->requestWithdraw($date, $subMchId = '');\n```\n\n> 以上接口调用过 `setSubMchId` 方法则无需传入 `sub_mch_id` 参数"
  },
  {
    "path": "docs/src/5.x/mini-program/activity_message.md",
    "content": "# 动态消息\n\n小程序动态消息功能允许开发者创建可变更的消息内容，在特定条件下更新消息显示内容。\n\n## 获取实例\n\n```php\n$activityMessage = $app->activity_message;\n```\n\n## 基础功能\n\n### 创建动态消息活动ID\n\n创建用于发送动态消息的活动ID：\n\n```php\n$result = $activityMessage->createActivityId();\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"activity_id\": \"xxx\",\n    \"expiration_time\": 1635724800\n}\n```\n\n**参数说明：**\n- `activity_id` string 活动ID，用于后续消息更新\n- `expiration_time` int 活动过期时间戳\n\n### 更新动态消息\n\n更新已发送的动态消息内容：\n\n```php\n$params = [\n    'member_count' => 2,      // 参与人数\n    'room_limit' => 4,        // 房间人数上限\n    'path' => 'pages/room?room_id=123',  // 页面路径\n    'version_type' => 'develop'  // 版本类型：develop, trial, release\n];\n\n$result = $activityMessage->updateMessage(\n    'activity_id_xxx',  // 活动ID\n    1,                  // 目标状态：0-参与前 1-参与后\n    $params             // 更新参数\n);\n```\n\n**参数说明：**\n- `activityId` string 活动ID\n- `state` int 目标状态：0-参与前状态 1-参与后状态\n- `params` array 消息参数\n  - `member_count` int 参与人数\n  - `room_limit` int 房间人数上限  \n  - `path` string 小程序页面路径\n  - `version_type` string 版本类型\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\"\n}\n```\n\n## 使用场景\n\n### 游戏房间动态消息\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$activityMessage = $app->activity_message;\n\n// 1. 创建活动ID\n$activity = $activityMessage->createActivityId();\n\nif ($activity['errcode'] === 0) {\n    $activityId = $activity['activity_id'];\n    echo \"活动ID创建成功: {$activityId}\\n\";\n    \n    // 2. 用户发送消息时附带活动ID\n    // 在发送消息接口中使用 activity_id\n    \n    // 3. 当房间状态变化时更新消息\n    // 例如：有新用户加入房间\n    $updateParams = [\n        'member_count' => 3,  // 当前3人\n        'room_limit' => 4,    // 最多4人\n        'path' => 'pages/game/room?id=room_123'\n    ];\n    \n    $updateResult = $activityMessage->updateMessage($activityId, 1, $updateParams);\n    \n    if ($updateResult['errcode'] === 0) {\n        echo \"消息更新成功，房间现在有3人\\n\";\n    }\n}\n```\n\n### 拼团活动动态消息\n\n```php\n// 创建拼团活动的动态消息\n$activity = $activityMessage->createActivityId();\n\nif ($activity['errcode'] === 0) {\n    $activityId = $activity['activity_id'];\n    \n    // 模拟拼团过程中的状态更新\n    $groupBuyingStates = [\n        ['member_count' => 1, 'room_limit' => 5, 'state' => 0], // 发起拼团\n        ['member_count' => 3, 'room_limit' => 5, 'state' => 0], // 3人参与\n        ['member_count' => 5, 'room_limit' => 5, 'state' => 1], // 拼团成功\n    ];\n    \n    foreach ($groupBuyingStates as $index => $stateData) {\n        $params = [\n            'member_count' => $stateData['member_count'],\n            'room_limit' => $stateData['room_limit'],\n            'path' => 'pages/group-buy/detail?group_id=gb_123'\n        ];\n        \n        $result = $activityMessage->updateMessage($activityId, $stateData['state'], $params);\n        \n        if ($result['errcode'] === 0) {\n            echo \"拼团状态更新: {$stateData['member_count']}/{$stateData['room_limit']}人\\n\";\n        }\n        \n        // 模拟时间间隔\n        sleep(1);\n    }\n}\n```\n\n### 实时竞赛动态消息\n\n```php\n// 创建竞赛活动的动态消息\n$activity = $activityMessage->createActivityId();\n\nif ($activity['errcode'] === 0) {\n    $activityId = $activity['activity_id'];\n    \n    // 竞赛报名阶段\n    $registrationParams = [\n        'member_count' => 8,   // 已报名8人\n        'room_limit' => 20,    // 最多20人\n        'path' => 'pages/contest/detail?contest_id=c_123'\n    ];\n    \n    $activityMessage->updateMessage($activityId, 0, $registrationParams);\n    echo \"竞赛报名中: 8/20人\\n\";\n    \n    // 竞赛开始阶段\n    $startParams = [\n        'member_count' => 20,  // 满员开始\n        'room_limit' => 20,\n        'path' => 'pages/contest/live?contest_id=c_123'\n    ];\n    \n    $activityMessage->updateMessage($activityId, 1, $startParams);\n    echo \"竞赛开始: 20/20人 已开始\\n\";\n}\n```\n\n### 直播间动态消息\n\n```php\n// 直播间观众数量变化\n$activity = $activityMessage->createActivityId();\n\nif ($activity['errcode'] === 0) {\n    $activityId = $activity['activity_id'];\n    \n    // 模拟直播间观众数量变化\n    $viewerCounts = [10, 25, 50, 100, 250];\n    \n    foreach ($viewerCounts as $count) {\n        $params = [\n            'member_count' => $count,\n            'room_limit' => 1000,  // 直播间容量\n            'path' => 'pages/live/room?live_id=live_123'\n        ];\n        \n        // 根据观众数量决定状态\n        $state = $count >= 100 ? 1 : 0;  // 超过100人为热门状态\n        \n        $result = $activityMessage->updateMessage($activityId, $state, $params);\n        \n        if ($result['errcode'] === 0) {\n            $status = $state ? '🔥热门' : '📺直播中';\n            echo \"直播间更新: {$status} {$count}人观看\\n\";\n        }\n        \n        sleep(2); // 模拟时间间隔\n    }\n}\n```\n\n### 队伍组建动态消息\n\n```php\nfunction updateTeamMessage($activityMessage, $activityId, $currentMembers, $maxMembers, $teamId) {\n    $params = [\n        'member_count' => $currentMembers,\n        'room_limit' => $maxMembers,\n        'path' => \"pages/team/detail?team_id={$teamId}\"\n    ];\n    \n    // 队伍满员时切换到完成状态\n    $state = ($currentMembers >= $maxMembers) ? 1 : 0;\n    \n    $result = $activityMessage->updateMessage($activityId, $state, $params);\n    \n    if ($result['errcode'] === 0) {\n        $status = $state ? '✅已满员' : '🚀招募中';\n        echo \"队伍状态: {$status} {$currentMembers}/{$maxMembers}人\\n\";\n        return true;\n    }\n    \n    return false;\n}\n\n// 使用示例\n$activity = $activityMessage->createActivityId();\nif ($activity['errcode'] === 0) {\n    $activityId = $activity['activity_id'];\n    \n    // 模拟队伍成员逐渐加入\n    for ($i = 1; $i <= 5; $i++) {\n        updateTeamMessage($activityMessage, $activityId, $i, 5, 'team_abc123');\n        sleep(1);\n    }\n}\n```\n\n## 注意事项\n\n1. **活动ID有效期**：活动ID有过期时间，过期后无法更新消息\n2. **更新频率限制**：消息更新有频率限制，不要过于频繁调用\n3. **状态一致性**：确保传递的参数与实际业务状态一致\n4. **页面路径有效性**：确保path参数指向的页面存在且可访问\n5. **版本类型**：根据小程序发布状态选择正确的version_type\n\n## 最佳实践\n\n1. **合理使用场景**：动态消息适用于多人协作、实时状态变化的场景\n2. **状态管理**：清晰定义参与前后的状态差异\n3. **用户体验**：确保消息更新能够提供有价值的信息\n4. **错误处理**：做好活动ID过期和更新失败的处理\n5. **数据同步**：保持消息内容与实际业务数据同步\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| -1 | 系统繁忙，此时请开发者稍候再试 |\n| 40001 | 获取access_token时AppSecret错误 |\n| 40013 | 不合法的AppID |\n| 41001 | 缺少access_token参数 |\n| 45009 | 接口调用超过限额 |\n| 47001 | 参数错误 |\n| 47501 | 一天只能创建100个活动ID |\n| 47502 | 活动ID已过期 |\n| 47503 | 状态值错误 |"
  },
  {
    "path": "docs/src/5.x/mini-program/app_code.md",
    "content": "# 小程序码\n\n## 获取小程序码\n\n### 接口A: 适用于需要的码数量较少的业务场景\n\nAPI:\n\n```\n$app->app_code->get(string $path, array $optional = []);\n```\n\n其中 `$optional` 为以下可选参数：\n\n>  - **width** Int - 默认 430 二维码的宽度\n>  - **auto_color**  默认 false  自动配置线条颜色，如果颜色依然是黑色，则说明不建议配置主色调\n>  - **line_color**  数组，`auto_color` 为 `false` 时生效，使用 rgb 设置颜色 例如 ，示例：`[\"r\" => 0,\"g\" => 0,\"b\" => 0]`。\n\n示例代码：\n\n```php\n$response = $app->app_code->get('path/to/page');\n// 或者\n$response = $app->app_code->get('path/to/page', [\n    'width' => 600,\n    //...\n]);\n\n// 或者指定颜色\n$response = $app->app_code->get('path/to/page', [\n    'width' => 600,\n    'line_color' => [\n        'r' => 105,\n        'g' => 166,\n        'b' => 134,\n    ],\n]);\n\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败时为数组或者你指定的 API 返回格式\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n### 接口B：适用于需要的码数量极多，或仅临时使用的业务场景\n\nAPI:\n\n```\n$app->app_code->getUnlimit(string $scene, array $optional = []);\n```\n\n> 其中 $scene 必填，$optinal 与 get 方法一致，多一个 page 参数。\n\n示例代码：\n\n```php\n$response = $app->app_code->getUnlimit('scene-value', [\n    'page'  => 'path/to/page',\n    'width' => 600,\n]);\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败为数组或你指定的 API 返回类型\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n## 获取小程序二维码\n\nAPI:\n\n```\n$app->app_code->getQrCode(string $path, int $width = null);\n```\n\n> 其中 $path 必填，其余参数可留空。\n\n示例代码：\n\n```php\n$response = $app->app_code->getQrCode('/path/to/page');\n\n// $response 成功时为 EasyWeChat\\Kernel\\Http\\StreamResponse 实例，失败为数组或你指定的 API 返回类型\n\n// 保存小程序码到文件\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->save('/path/to/directory');\n}\n\n// 或\nif ($response instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    $filename = $response->saveAs('/path/to/directory', 'appcode.png');\n}\n```\n\n##\n"
  },
  {
    "path": "docs/src/5.x/mini-program/auth.md",
    "content": "# 微信登录\n\n## 根据 jsCode 获取用户 session 信息\n\nAPI:\n\n```php\n$app->auth->session(string $code);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/business.md",
    "content": "# 商户功能\n\n小程序商户功能允许小程序接入商户能力，为小程序提供商户服务相关的接口。\n\n## 获取实例\n\n```php\n$business = $app->business;\n```\n\n## 商户注册\n\n注册商户账号：\n\n```php\n$result = $business->register('商户账号名', '商户昵称', '头像媒体ID');\n```\n\n**参数说明：**\n- `accountName` string 商户账号名\n- `nickname` string 商户昵称\n- `iconMediaId` string 头像媒体ID\n\n## 获取商户信息\n\n可以通过商户ID或账号名获取商户信息：\n\n```php\n// 通过商户ID获取\n$result = $business->getBusiness($businessId);\n\n// 通过账号名获取\n$result = $business->getBusiness(0, 'account_name');\n```\n\n**参数说明：**\n- `businessId` int 商户ID\n- `accountName` string 商户账号名\n\n## 获取商户列表\n\n获取商户列表，支持分页：\n\n```php\n$result = $business->list($offset, $count);\n```\n\n**参数说明：**\n- `offset` int 偏移量，默认为0\n- `count` int 返回数量，默认为10\n\n## 更新商户信息\n\n更新商户昵称和头像：\n\n```php\n$result = $business->update($businessId, '新昵称', '新头像媒体ID');\n```\n\n**参数说明：**\n- `businessId` int 商户ID\n- `nickname` string 新昵称（可选）\n- `iconMediaId` string 新头像媒体ID（可选）\n\n## 发送消息\n\n### 构建消息\n\n```php\n$message = $business->message('消息内容');\n```\n\n### 发送消息\n\n```php\n$message = [\n    'touser' => 'openid',\n    'msgtype' => 'text',\n    'text' => [\n        'content' => '消息内容'\n    ],\n    'business_id' => $businessId\n];\n\n$result = $business->send($message);\n```\n\n## 设置输入状态\n\n显示\"正在输入\"状态：\n\n```php\n$result = $business->typing($businessId, $toUser);\n```\n\n**参数说明：**\n- `businessId` int 商户ID\n- `toUser` string 用户openid\n\n## 完整示例\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$business = $app->business;\n\n// 注册商户\n$result = $business->register('test_merchant', '测试商户', 'media_id_123');\n\nif ($result['errcode'] === 0) {\n    $businessId = $result['business_id'];\n    \n    // 发送消息\n    $message = [\n        'touser' => 'user_openid',\n        'msgtype' => 'text',\n        'text' => [\n            'content' => '欢迎使用商户服务！'\n        ],\n        'business_id' => $businessId\n    ];\n    \n    $business->send($message);\n}\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/customer_service.md",
    "content": "# 客服消息\n\n## 获取实例\n\n```php\n$service = $app->customer_service;\n```\n\n> 使用方法详看公众号-客服消息章节。\n\n"
  },
  {
    "path": "docs/src/5.x/mini-program/data_cube.md",
    "content": "# 数据统计与分析\n\n获取小程序概况趋势：\n\n```php\n$app->data_cube->summaryTrend('20170313', '20170313')\n```\n开始日期与结束日期的格式为 yyyymmdd。\n\n## API\n\n>  - `summaryTrend(string $from, string $to);` 概况趋势\n>  - `dailyVisitTrend(string $from, string $to);` 访问日趋势\n>  - `weeklyVisitTrend(string $from, string $to);` 访问周趋势\n>  - `monthlyVisitTrend(string $from, string $to);` 访问月趋势\n>  - `visitDistribution(string $from, string $to);` 访问分布\n>  - `dailyRetainInfo(string $from, string $to);` 访问日留存\n>  - `weeklyRetainInfo(string $from, string $to);` 访问周留存\n>  - `monthlyRetainInfo(string $from, string $to);` 访问月留存\n>  - `visitPage(string $from, string $to);` 访问页面\n>  - `userPortrait(string $from, string $to);` 用户画像分布数据\n\n"
  },
  {
    "path": "docs/src/5.x/mini-program/decrypt.md",
    "content": "# 微信小程序消息解密\n\n## 比如获取电话等功能，信息是加密的，需要解密。\n\nAPI:\n\n```php\n$decryptedData = $app->encryptor->decryptData($session, $iv, $encryptedData);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/express.md",
    "content": "# 物流助手 电子面单\n\n## 获取支持的快递公司列表\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getAllDelivery.html\n\n```php\n\n$app->express->listProviders();\n\n{\n  \"count\": 8,\n  \"data\": [\n    {\n      \"delivery_id\": \"BEST\",\n      \"delivery_name\": \"百世快递\"\n    },\n    ...\n  ]\n}\n\n```\n\n## 生成运单\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.addOrder.html\n\n```php\n\n$app->express->createWaybill($data);\n\n\n// 成功返回\n\n{\n  \"order_id\": \"01234567890123456789\",\n  \"waybill_id\": \"123456789\",\n  \"waybill_data\": [\n    {\n      \"key\": \"SF_bagAddr\",\n      \"value\": \"广州\"\n    },\n    {\n      \"key\": \"SF_mark\",\n      \"value\": \"101- 07-03 509\"\n    }\n  ]\n}\n\n// 失败返回\n\n{\n  \"errcode\": 9300501,\n  \"errmsg\": \"delivery logic fail\",\n  \"delivery_resultcode\": 10002,\n  \"delivery_resultmsg\": \"客户密码不正确\"\n}\n\n```\n\n## 取消运单\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.cancelOrder.html\n\n```php\n$app->express->deleteWaybill($data);\n\n```\n\n## 获取运单数据\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getOrder.html\n\n```php\n$app->express->getWaybill($data);\n\n```\n\n## 查询运单轨迹\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getPath.html\n\n```php\n$app->express->getWaybillTrack($data);\n\n```\n\n## 获取电子面单余额。\n\n仅在使用加盟类快递公司时，才可以调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.getQuota.html\n\n```php\n\n$app->express->getBalance($deliveryId, $bizId);\n\n// 例如：\n\n$app->express->getBalance('YTO', 'xyz');\n```\n\n## 绑定打印员\n\n若需要使用微信打单 PC 软件，才需要调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.updatePrinter.html\n\n```php\n$app->express->bindPrinter($openid);\n```\n\n## 解绑打印员\n\n若需要使用微信打单 PC 软件，才需要调用。\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/express/by-business/logistics.updatePrinter.html\n\n```php\n$app->express->unbindPrinter($openid);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/index.md",
    "content": "# 小程序\n\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 下面为可选项\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::miniProgram($config);\n```\n\n`$app` 在所有相关小程序的文档都是指 `Factory::miniProgram` 得到的实例，就不在每个页面单独写了。\n"
  },
  {
    "path": "docs/src/5.x/mini-program/live.md",
    "content": "# 订阅消息\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html\n\n> tips:微信规定以下两个接口调用限制共享 **500次/天** 建议开发者自己做缓存，合理分配调用频次。\n\n## 获取直播房间列表\n\n```php\n$app->live->getRooms();\n```\n\n## 获取回放源视频\n\n```php\n$roomId = 1;    //直播房间id\n\n$app->live->getPlaybacks($roomId);\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/mall.md",
    "content": "# 微信小商店\n\n微信小商店是微信官方提供的电商能力，小程序可以通过相关接口管理商品、订单等。\n\n## 获取实例\n\n```php\n$mall = $app->mall;\n```\n\n## 商品管理\n\n### 导入或更新商品\n\n批量导入或更新商品信息：\n\n```php\n$products = [\n    [\n        'product_id' => 'product_001',\n        'title' => '商品标题',\n        'sub_title' => '商品副标题',\n        'head_imgs' => ['图片URL1', '图片URL2'],\n        'category_id' => 1234,\n        'brand_id' => 5678,\n        'model' => '型号',\n        'third_cat_id' => 9012,\n        'product_type' => 1,\n        'qualification_pics' => ['资质图片URL'],\n        'src_wxapp_path' => 'pages/product/detail?id=123',\n        'skus' => [\n            [\n                'sku_id' => 'sku_001',\n                'price' => 9900, // 以分为单位\n                'original_price' => 12900,\n                'status' => 1, // 1:上架 0:下架\n                'stock_num' => 100,\n                'sku_attrs' => [\n                    ['attr_key' => '颜色', 'attr_value' => '红色'],\n                    ['attr_key' => '尺寸', 'attr_value' => 'L']\n                ]\n            ]\n        ]\n    ]\n];\n\n$result = $mall->product->import($products, false); // false表示正式环境\n```\n\n### 查询商品信息\n\n查询商品详细信息：\n\n```php\n$params = [\n    'product_id' => 'product_001',\n    'need_edit_spu' => 1\n];\n\n$result = $mall->product->query($params);\n```\n\n### 获取商品状态\n\n```php\n$result = $mall->product->getStatus(['product_001', 'product_002']);\n```\n\n### 更新商品状态\n\n```php\n$result = $mall->product->updateStatus([\n    ['product_id' => 'product_001', 'status' => 1], // 1:上架 0:下架\n    ['product_id' => 'product_002', 'status' => 0]\n]);\n```\n\n## 购物车管理\n\n### 添加商品到购物车\n\n```php\n$params = [\n    'user_open_id' => 'user_openid',\n    'sku_product_id' => 'product_001',\n    'sku_id' => 'sku_001',\n    'num' => 2\n];\n\n$result = $mall->cart->add($params);\n```\n\n### 获取购物车商品\n\n```php\n$params = [\n    'user_open_id' => 'user_openid'\n];\n\n$result = $mall->cart->get($params);\n```\n\n### 删除购物车商品\n\n```php\n$params = [\n    'user_open_id' => 'user_openid',\n    'sku_product_id' => 'product_001',\n    'sku_id' => 'sku_001'\n];\n\n$result = $mall->cart->delete($params);\n```\n\n## 订单管理\n\n### 生成订单\n\n```php\n$orderData = [\n    'create_time' => time(),\n    'type' => 1,\n    'order_id' => 'order_' . time(),\n    'openid' => 'user_openid',\n    'union_id' => 'user_unionid',\n    'product_infos' => [\n        [\n            'product_id' => 'product_001',\n            'sku_id' => 'sku_001',\n            'product_cnt' => 2,\n            'sale_price' => 9900,\n            'head_img' => '商品图片URL',\n            'title' => '商品标题',\n            'path' => 'pages/product/detail?id=123'\n        ]\n    ],\n    'pay_info' => [\n        'pay_method' => '微信支付',\n        'pay_method_type' => 1,\n        'prepay_id' => 'prepay_id_xxx',\n        'prepay_time' => time()\n    ],\n    'price_info' => [\n        'order_price' => 19800,\n        'freight' => 1000,\n        'discounted_price' => 0,\n        'additional_price' => 0,\n        'additional_remarks' => ''\n    ],\n    'delivery_info' => [\n        'delivery_type' => 1,\n        'receiver_name' => '张三',\n        'detailed_address' => '详细地址',\n        'tel_number' => '13800138000',\n        'country' => '中国',\n        'province' => '北京市',\n        'city' => '北京市',\n        'town' => '朝阳区'\n    ]\n];\n\n$result = $mall->order->add($orderData);\n```\n\n### 更新订单状态\n\n```php\n$params = [\n    'order_id' => 'order_123',\n    'status' => 2, // 订单状态\n    'action_type' => 1, // 操作类型\n    'action_remark' => '操作备注'\n];\n\n$result = $mall->order->updateStatus($params);\n```\n\n### 批量获取订单\n\n```php\n$params = [\n    'start_create_time' => strtotime('-30 days'),\n    'end_create_time' => time(),\n    'last_index' => '', // 分页标识\n    'page_size' => 10\n];\n\n$result = $mall->order->list($params);\n```\n\n## 媒体文件管理\n\n### 上传图片\n\n```php\n$result = $mall->media->uploadImg('/path/to/image.jpg');\n```\n\n### 获取图片\n\n```php\n$result = $mall->media->getImg('media_id');\n```\n\n## 完整示例\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$mall = $app->mall;\n\n// 导入商品\n$products = [\n    [\n        'product_id' => 'test_product_001',\n        'title' => '测试商品',\n        'sub_title' => '这是一个测试商品',\n        'head_imgs' => ['https://example.com/img1.jpg'],\n        'category_id' => 1234,\n        'skus' => [\n            [\n                'sku_id' => 'sku_001',\n                'price' => 9900,\n                'original_price' => 12900,\n                'status' => 1,\n                'stock_num' => 100\n            ]\n        ]\n    ]\n];\n\n$result = $mall->product->import($products);\n\nif ($result['errcode'] === 0) {\n    echo \"商品导入成功\\n\";\n    \n    // 查询商品信息\n    $productInfo = $mall->product->query(['product_id' => 'test_product_001']);\n    print_r($productInfo);\n}\n```\n\n## 注意事项\n\n1. 商品价格以分为单位\n2. 图片需要先上传到微信服务器获取media_id或使用HTTPS URL\n3. 商品分类ID需要从微信官方获取\n4. 订单状态变更需要按照微信规范进行\n5. API调用频率有限制，请合理控制调用频次"
  },
  {
    "path": "docs/src/5.x/mini-program/nearby_poi.md",
    "content": "# 附近的小程序\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/nearby-poi/nearbyPoi.add.html\n\n## 添加地点\n\n```php\n$params = [\n    'kf_info' => '{\"open_kf\":true,\"kf_headimg\":\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"kf_name\":\"Harden\"}',\n    'pic_list' => '{\"list\":[\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITRneE5FS9uYruXGMmrtmhsBySwddEWUGOibG8Ze2NT5E3Dyt79I0htNg/0?wx_fmt=jpeg\"]}',\n    'service_infos' => '{\"service_infos\":[{\"id\":2,\"type\":1,\"name\":\"快递\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"},{\"id\":0,\"type\":2,\"name\":\"自定义\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"}]}',\n    'store_name' => '羊村小马烧烤',\n    'contract_phone' => '111111111',\n    'hour' => '00:00-11:11',\n    'company_name' => '深圳市腾讯计算机系统有限公司',\n    'credential' => '156718193518281',\n    'address' => '新疆维吾尔自治区克拉玛依市克拉玛依区碧水路15-1-8号(碧水云天广场)',\n    'qualification_list' => '3LaLzqiTrQcD20DlX_o-OV1-nlYMu7sdVAL7SV2PrxVyjZFZZmB3O6LPGaYXlZWq',\n];\n\n$app->nearby_poi->add($params);\n```\n\n## 更新地点\n\n```php\n$poiId = 'xxxxxxxx';\n\n$params = [\n    'kf_info' => '{\"open_kf\":true,\"kf_headimg\":\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"kf_name\":\"Harden\"}',\n    'pic_list' => '{\"list\":[\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITqmP914zSwhajIEJzUPpx40P7R8fRe1QmicneQMhFzpZNhSLjrvU1pIA/0?wx_fmt=jpeg\",\"http://mmbiz.qpic.cn/mmbiz_jpg/kKMgNtnEfQzDKpLXYhgo3W3Gndl34gITRneE5FS9uYruXGMmrtmhsBySwddEWUGOibG8Ze2NT5E3Dyt79I0htNg/0?wx_fmt=jpeg\"]}',\n    'service_infos' => '{\"service_infos\":[{\"id\":2,\"type\":1,\"name\":\"快递\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"},{\"id\":0,\"type\":2,\"name\":\"自定义\",\"appid\":\"wx1373169e494e0c39\",\"path\":\"index\"}]}',\n    'contract_phone' => '111111111',\n    'hour' => '00:00-11:11',\n    'company_name' => '深圳市腾讯计算机系统有限公司',\n    'credential' => '156718193518281',\n    'address' => '新疆维吾尔自治区克拉玛依市克拉玛依区碧水路15-1-8号(碧水云天广场)',\n    'qualification_list' => '3LaLzqiTrQcD20DlX_o-OV1-nlYMu7sdVAL7SV2PrxVyjZFZZmB3O6LPGaYXlZWq',\n];\n\n$app->nearby_poi->update($poiId, $params);\n```\n\n## 删除地点\n\n```php\n$poiId = 'xxxxxxxx';\n\n$app->nearby_poi->delete($poiId);\n```\n\n## 地点列表\n\n```php\n$page = 1;\n$pageRows = 10;\n\n$app->nearby_poi->list($page, $pageRows);\n```\n\n## 设置地点展示状态\n\n```php\n$poiId = 'xxxxxxxx';\n$status = 0; // 0: 不展示，1：展示\n\n$app->nearby_poi->setVisibility($poiId, $status);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/ocr.md",
    "content": "# OCR 文字识别\n\n小程序OCR功能提供各种文字识别能力，包括身份证、银行卡、驾驶证等证件识别以及通用文字识别。\n\n## 获取实例\n\n```php\n$ocr = $app->ocr;\n```\n\n## 身份证识别\n\n识别身份证正反面信息：\n\n```php\n// 身份证正面\n$result = $ocr->idcard($mediaId, 'photo');\n\n// 身份证反面  \n$result = $ocr->idcard($mediaId, 'reverse');\n```\n\n**参数说明：**\n- `mediaId` string 图片的媒体ID\n- `type` string 识别类型：'photo'(正面) 或 'reverse'(反面)\n\n**返回结果（正面）：**\n```json\n{\n    \"type\": \"身份证正面\",\n    \"name\": \"张三\",\n    \"id\": \"110101199001011234\",\n    \"addr\": \"北京市东城区...\",\n    \"gender\": \"男\",\n    \"nationality\": \"汉\"\n}\n```\n\n## 银行卡识别\n\n识别银行卡信息：\n\n```php\n$result = $ocr->bankcard($mediaId);\n```\n\n**返回结果：**\n```json\n{\n    \"number\": \"6225881234567890\"\n}\n```\n\n## 驾驶证识别\n\n识别驾驶证信息：\n\n```php\n$result = $ocr->driving($mediaId);\n```\n\n**返回结果：**\n```json\n{\n    \"id_num\": \"110101199001011234\",\n    \"name\": \"张三\",\n    \"nationality\": \"中国\",\n    \"sex\": \"男\",\n    \"address\": \"北京市...\",\n    \"birth_date\": \"1990-01-01\",\n    \"issue_date\": \"2020-01-01\",\n    \"car_class\": \"C1\",\n    \"valid_from\": \"2020-01-01\",\n    \"valid_to\": \"2026-01-01\"\n}\n```\n\n## 行驶证识别\n\n识别行驶证信息：\n\n```php\n$result = $ocr->drivingLicense($mediaId);\n```\n\n**返回结果：**\n```json\n{\n    \"vehicle_type\": \"小型汽车\",\n    \"owner\": \"张三\",\n    \"addr\": \"北京市...\",\n    \"use_character\": \"非营运\",\n    \"model\": \"长安牌SC1019...\",\n    \"plate_num\": \"京A12345\",\n    \"vin\": \"LDC613P23A1050312\",\n    \"engine_num\": \"0123456\",\n    \"register_date\": \"2020-01-01\",\n    \"issue_date\": \"2020-01-01\"\n}\n```\n\n## 营业执照识别\n\n识别营业执照信息：\n\n```php\n$result = $ocr->businessLicense($mediaId);\n```\n\n**返回结果：**\n```json\n{\n    \"reg_num\": \"91110101MA01A1B2C3\",\n    \"serial\": \"12345678\",\n    \"legal_representative\": \"张三\",\n    \"enterprise_name\": \"北京测试公司\",\n    \"type_of_organization\": \"有限责任公司\",\n    \"address\": \"北京市...\",\n    \"type_of_enterprise\": \"私营\",\n    \"business_scope\": \"技术开发...\",\n    \"registered_capital\": \"100万人民币\",\n    \"paid_in_capital\": \"100万人民币\",\n    \"valid_period\": \"2020-01-01至长期\",\n    \"registered_date\": \"2020-01-01\",\n    \"cert_position\": {\n        \"pos\": {\n            \"left_top\": {\"x\": 155, \"y\": 191},\n            \"right_top\": {\"x\": 725, \"y\": 157},\n            \"right_bottom\": {\"x\": 743, \"y\": 512},\n            \"left_bottom\": {\"x\": 164, \"y\": 539}\n        }\n    },\n    \"img_size\": {\"w\": 966, \"h\": 728}\n}\n```\n\n## 通用印刷体识别\n\n识别图片中的印刷体文字：\n\n```php\n$result = $ocr->printedText($mediaId);\n```\n\n**返回结果：**\n```json\n{\n    \"items\": [\n        {\n            \"text\": \"腾讯\",\n            \"pos\": {\n                \"left_top\": {\"x\": 575, \"y\": 519},\n                \"right_top\": {\"x\": 744, \"y\": 519},\n                \"right_bottom\": {\"x\": 744, \"y\": 532},\n                \"left_bottom\": {\"x\": 575, \"y\": 532}\n            }\n        }\n    ],\n    \"img_size\": {\"w\": 1280, \"h\": 720}\n}\n```\n\n## 完整示例\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$ocr = $app->ocr;\n\n// 识别身份证正面\n$result = $ocr->idcard('media_id_123', 'photo');\n\nif ($result['errcode'] === 0) {\n    $name = $result['name'];\n    $idNumber = $result['id'];\n    $address = $result['addr'];\n    \n    echo \"姓名：{$name}\\n\";\n    echo \"身份证号：{$idNumber}\\n\";\n    echo \"地址：{$address}\\n\";\n}\n\n// 识别银行卡\n$result = $ocr->bankcard('media_id_456');\nif ($result['errcode'] === 0) {\n    $cardNumber = $result['number'];\n    echo \"银行卡号：{$cardNumber}\\n\";\n}\n```\n\n## 注意事项\n\n1. 图片要求：格式支持PNG、JPG、JPEG、BMP，大小不超过1M\n2. 证件图片要求清晰，避免模糊、反光、阴影等情况\n3. 每个小程序每天有一定的免费识别额度，超出部分按量收费\n4. 建议在客户端先对图片进行预处理，如裁剪、旋转等，以提高识别准确率"
  },
  {
    "path": "docs/src/5.x/mini-program/phone_number.md",
    "content": "# 手机号\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html\n\n\n## 获取手机号\n\n\n```php\n$app->phone_number->getUserPhoneNumber(string $code);\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/plugin.md",
    "content": "# 插件管理\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/plugin-management/pluginManager.applyPlugin.html\n\n## 申请使用插件\n\n```php\n$pluginAppId = 'xxxxxxxxx';\n\n$app->plugin->apply($pluginAppId);\n```\n\n## 删除已添加的插件\n\n```php\n$pluginAppId = 'xxxxxxxxx';\n\n$app->plugin->unbind($pluginAppId);\n```\n\n## 查询已添加的插件\n\n```php\n$app->plugin->list();\n```\n\n## 获取当前所有插件使用方\n\n```php\n$page = 1;\n$size = 10;\n\n$app->plugin_dev->getUsers($page, $size);\n```\n\n## 同意插件使用申请\n\n```php\n$appId = 'wxxxxxxxxxxxxxx';\n\n$app->plugin_dev->agree($appId);\n```\n\n## 拒绝插件使用申请\n\n```php\n$app->plugin_dev->refuse('拒绝理由');\n```\n\n## 删除已拒绝的申请者\n\n```php\n$app->plugin_dev->delete();\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/realtime_log.md",
    "content": "# 实时日志\n\n小程序实时日志功能允许开发者查询小程序的实时运行日志，帮助定位和解决线上问题。\n\n## 获取实例\n\n```php\n$realtimeLog = $app->realtime_log;\n```\n\n## 查询实时日志\n\n查询指定时间段内的实时日志：\n\n```php\n$result = $realtimeLog->search(\n    '20230601',      // 日期，格式：yyyymmdd\n    1685577600,      // 开始时间戳（秒）\n    1685581200,      // 结束时间戳（秒）\n    [\n        'module' => 'default',     // 模块名，可选\n        'keyword' => 'error',      // 关键词过滤，可选  \n        'level' => 'error',        // 日志级别过滤，可选\n        'page_size' => 20,         // 每页数量，默认20，最大100\n        'page_num' => 1            // 页码，从1开始\n    ]\n);\n```\n\n**参数说明：**\n- `date` string 查询日期，格式：yyyymmdd\n- `beginTime` int 开始时间戳（秒）\n- `endTime` int 结束时间戳（秒）\n- `options` array 可选参数\n  - `module` string 模块名过滤\n  - `keyword` string 关键词过滤\n  - `level` string 日志级别过滤（info、warn、error）\n  - `page_size` int 每页数量，默认20，最大100\n  - `page_num` int 页码，从1开始\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"data\": [\n        {\n            \"timestamp\": 1685577660,\n            \"level\": \"error\",\n            \"module\": \"default\",\n            \"message\": \"网络请求失败\",\n            \"stack\": \"Error: request timeout\\n  at ...\",\n            \"page\": \"pages/index/index\",\n            \"function\": \"onLoad\",\n            \"line\": 25\n        }\n    ],\n    \"total\": 156,\n    \"page_num\": 1,\n    \"page_size\": 20\n}\n```\n\n## 使用示例\n\n### 查询错误日志\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$realtimeLog = $app->realtime_log;\n\n// 查询最近1小时的错误日志\n$endTime = time();\n$beginTime = $endTime - 3600; // 1小时前\n$date = date('Ymd', $endTime);\n\n$result = $realtimeLog->search($date, $beginTime, $endTime, [\n    'level' => 'error',\n    'page_size' => 50\n]);\n\nif ($result['errcode'] === 0) {\n    echo \"总共找到 {$result['total']} 条错误日志\\n\";\n    \n    foreach ($result['data'] as $log) {\n        echo \"时间：\" . date('Y-m-d H:i:s', $log['timestamp']) . \"\\n\";\n        echo \"页面：{$log['page']}\\n\";\n        echo \"消息：{$log['message']}\\n\";\n        echo \"堆栈：{$log['stack']}\\n\";\n        echo \"---\\n\";\n    }\n}\n```\n\n### 查询特定关键词日志\n\n```php\n// 查询包含\"支付\"关键词的日志\n$result = $realtimeLog->search($date, $beginTime, $endTime, [\n    'keyword' => '支付',\n    'page_size' => 30\n]);\n\nif ($result['errcode'] === 0) {\n    foreach ($result['data'] as $log) {\n        echo \"级别：{$log['level']}\\n\";\n        echo \"消息：{$log['message']}\\n\";\n        echo \"页面：{$log['page']}\\n\";\n        echo \"---\\n\";\n    }\n}\n```\n\n### 分页查询日志\n\n```php\n$pageNum = 1;\n$pageSize = 20;\n\ndo {\n    $result = $realtimeLog->search($date, $beginTime, $endTime, [\n        'page_num' => $pageNum,\n        'page_size' => $pageSize\n    ]);\n    \n    if ($result['errcode'] === 0 && !empty($result['data'])) {\n        echo \"第 {$pageNum} 页：\\n\";\n        \n        foreach ($result['data'] as $log) {\n            echo \"- {$log['message']}\\n\";\n        }\n        \n        $pageNum++;\n        \n        // 检查是否还有更多页\n        $hasMore = ($pageNum - 1) * $pageSize < $result['total'];\n    } else {\n        $hasMore = false;\n    }\n} while ($hasMore);\n```\n\n### 查询特定模块日志\n\n```php\n// 查询自定义模块的日志\n$result = $realtimeLog->search($date, $beginTime, $endTime, [\n    'module' => 'payment',  // 假设你在小程序中定义了payment模块\n    'level' => 'warn'\n]);\n\nif ($result['errcode'] === 0) {\n    echo \"支付模块警告日志：\\n\";\n    foreach ($result['data'] as $log) {\n        echo \"时间：\" . date('Y-m-d H:i:s', $log['timestamp']) . \"\\n\";\n        echo \"函数：{$log['function']}\\n\";\n        echo \"行号：{$log['line']}\\n\";\n        echo \"消息：{$log['message']}\\n\\n\";\n    }\n}\n```\n\n## 小程序端配置\n\n要使用实时日志功能，需要在小程序端进行相应配置：\n\n### 1. 开启实时日志\n\n在小程序的 `app.js` 中：\n\n```javascript\nApp({\n  onLaunch() {\n    // 开启实时日志\n    const logger = wx.getRealtimeLogManager();\n    \n    // 设置日志级别\n    logger.setFilterMsg('test');\n    \n    // 记录日志\n    logger.info('应用启动');\n  }\n});\n```\n\n### 2. 记录日志\n\n在需要记录日志的地方：\n\n```javascript\nconst logger = wx.getRealtimeLogManager();\n\n// 记录信息日志\nlogger.info('用户操作', { action: 'click', button: 'submit' });\n\n// 记录警告日志\nlogger.warn('网络慢', { latency: 2000 });\n\n// 记录错误日志\nlogger.error('请求失败', error);\n```\n\n## 注意事项\n\n1. **时间范围限制**：单次查询时间范围不能超过1天\n2. **查询频率限制**：API调用有频率限制，请合理控制调用频次\n3. **日志保留期**：实时日志通常保留7天\n4. **数据量限制**：单次查询最多返回100条日志\n5. **权限要求**：需要小程序管理员权限才能查询日志\n6. **模块名规范**：模块名建议使用英文，避免特殊字符\n\n## 最佳实践\n\n1. **合理设置查询时间范围**：避免查询过长时间段的日志\n2. **使用关键词过滤**：通过关键词快速定位问题日志\n3. **分级查询**：先查询error级别，再查询warn和info\n4. **结合监控告警**：可以定期查询错误日志，实现简单的监控告警\n5. **日志结构化**：在小程序端记录日志时，使用结构化的数据格式"
  },
  {
    "path": "docs/src/5.x/mini-program/risk_control.md",
    "content": "# 风险控制\n\n小程序风险控制功能提供用户安全等级评估能力，帮助开发者识别和防范风险用户。\n\n## 获取实例\n\n```php\n$riskControl = $app->risk_control;\n```\n\n## 获取用户安全等级\n\n评估用户的安全风险等级：\n\n```php\n$params = [\n    'openid' => 'user_openid',           // 用户的openid\n    'scene' => 1,                        // 场景值\n    'mobile_no' => '13800138000',        // 手机号（可选）\n    'client_ip' => '192.168.1.1',       // 客户端IP（可选）\n    'email_address' => 'user@test.com',  // 邮箱地址（可选）\n    'extended_info' => '{\"key\":\"value\"}' // 扩展信息（可选）\n];\n\n$result = $riskControl->getUserRiskRank($params);\n```\n\n**参数说明：**\n- `openid` string 用户的openid，必填\n- `scene` int 场景值，必填\n  - `1` 注册\n  - `2` 营销活动\n  - `3` 发布信息\n  - `4` 支付行为\n  - `5` 其他高风险行为\n- `mobile_no` string 用户手机号，可选，有助于提高评估准确性\n- `client_ip` string 用户客户端IP，可选\n- `email_address` string 用户邮箱，可选\n- `extended_info` string 扩展信息，JSON字符串，可选\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"risk_rank\": 1,\n    \"unoin_id\": \"user_union_id\"\n}\n```\n\n**风险等级说明：**\n- `0` 风险等级未知\n- `1` 风险等级较低\n- `2` 风险等级中等\n- `3` 风险等级较高\n- `4` 风险等级高\n\n## 使用示例\n\n### 注册场景风险评估\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$riskControl = $app->risk_control;\n\n// 用户注册时进行风险评估\n$params = [\n    'openid' => 'oABC123DEF456GHI789',\n    'scene' => 1, // 注册场景\n    'mobile_no' => '13800138000',\n    'client_ip' => '192.168.1.100'\n];\n\n$result = $riskControl->getUserRiskRank($params);\n\nif ($result['errcode'] === 0) {\n    $riskRank = $result['risk_rank'];\n    \n    switch ($riskRank) {\n        case 1:\n            echo \"用户风险等级较低，允许注册\\n\";\n            // 正常注册流程\n            break;\n        case 2:\n            echo \"用户风险等级中等，需要额外验证\\n\";\n            // 要求手机号验证\n            break;\n        case 3:\n        case 4:\n            echo \"用户风险等级较高，拒绝注册\\n\";\n            // 拒绝注册或要求人工审核\n            break;\n        default:\n            echo \"风险等级未知，采用默认策略\\n\";\n            break;\n    }\n} else {\n    echo \"风险评估失败：{$result['errmsg']}\\n\";\n    // 采用默认安全策略\n}\n```\n\n### 营销活动风险评估\n\n```php\n// 营销活动参与时进行风险评估\n$params = [\n    'openid' => 'user_openid',\n    'scene' => 2, // 营销活动场景\n    'client_ip' => $userIp,\n    'extended_info' => json_encode([\n        'activity_id' => 'activity_123',\n        'prize_type' => 'coupon',\n        'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''\n    ])\n];\n\n$result = $riskControl->getUserRiskRank($params);\n\nif ($result['errcode'] === 0 && $result['risk_rank'] <= 2) {\n    // 风险等级可接受，允许参与活动\n    echo \"允许参与营销活动\\n\";\n} else {\n    // 风险等级过高，禁止参与\n    echo \"风险等级过高，禁止参与活动\\n\";\n}\n```\n\n### 支付行为风险评估\n\n```php\n// 支付前进行风险评估\n$params = [\n    'openid' => 'user_openid',\n    'scene' => 4, // 支付行为场景\n    'mobile_no' => $userMobile,\n    'client_ip' => $userIp,\n    'extended_info' => json_encode([\n        'order_amount' => 10000, // 订单金额（分）\n        'order_id' => 'order_123456',\n        'payment_method' => 'wechat_pay'\n    ])\n];\n\n$result = $riskControl->getUserRiskRank($params);\n\nif ($result['errcode'] === 0) {\n    $riskRank = $result['risk_rank'];\n    \n    if ($riskRank <= 1) {\n        // 低风险，正常支付流程\n        echo \"支付风险低，进入正常支付流程\\n\";\n    } elseif ($riskRank == 2) {\n        // 中等风险，要求额外验证\n        echo \"支付风险中等，要求短信验证\\n\";\n    } else {\n        // 高风险，拒绝支付或人工审核\n        echo \"支付风险高，需要人工审核\\n\";\n    }\n}\n```\n\n### 发布信息风险评估\n\n```php\n// 用户发布内容时进行风险评估\n$params = [\n    'openid' => 'user_openid',\n    'scene' => 3, // 发布信息场景\n    'client_ip' => $userIp,\n    'extended_info' => json_encode([\n        'content_type' => 'text',\n        'content_length' => 500,\n        'has_image' => true,\n        'publish_frequency' => 5 // 今日发布次数\n    ])\n];\n\n$result = $riskControl->getUserRiskRank($params);\n\nif ($result['errcode'] === 0) {\n    if ($result['risk_rank'] <= 2) {\n        echo \"允许发布内容\\n\";\n        // 正常发布流程\n    } else {\n        echo \"发布风险较高，内容需要审核\\n\";\n        // 内容进入审核队列\n    }\n}\n```\n\n## 最佳实践\n\n### 1. 合理选择场景值\n\n根据实际业务场景选择合适的场景值，不同场景的风险评估策略可能不同。\n\n### 2. 提供充足的用户信息\n\n提供更多的用户信息（如手机号、IP地址等）有助于提高风险评估的准确性。\n\n### 3. 建立分级处理策略\n\n```php\nfunction handleUserRisk($riskRank, $scene) {\n    switch ($scene) {\n        case 1: // 注册\n            return handleRegistrationRisk($riskRank);\n        case 2: // 营销活动\n            return handleMarketingRisk($riskRank);\n        case 4: // 支付\n            return handlePaymentRisk($riskRank);\n        default:\n            return handleDefaultRisk($riskRank);\n    }\n}\n\nfunction handleRegistrationRisk($riskRank) {\n    if ($riskRank <= 1) {\n        return ['action' => 'allow', 'message' => '允许注册'];\n    } elseif ($riskRank == 2) {\n        return ['action' => 'verify', 'message' => '需要手机验证'];\n    } else {\n        return ['action' => 'deny', 'message' => '拒绝注册'];\n    }\n}\n```\n\n### 4. 异常处理\n\n```php\ntry {\n    $result = $riskControl->getUserRiskRank($params);\n    \n    if ($result['errcode'] === 0) {\n        // 处理正常响应\n        handleUserRisk($result['risk_rank'], $params['scene']);\n    } else {\n        // API调用失败，使用默认策略\n        logger()->warning('风险评估API调用失败', [\n            'errcode' => $result['errcode'],\n            'errmsg' => $result['errmsg']\n        ]);\n        // 采用保守的默认策略\n    }\n} catch (Exception $e) {\n    // 网络异常等，使用默认策略\n    logger()->error('风险评估异常', ['exception' => $e->getMessage()]);\n    // 采用保守的默认策略\n}\n```\n\n## 注意事项\n\n1. **API调用频率限制**：请合理控制API调用频率，避免超出限制\n2. **用户隐私保护**：确保用户数据的安全传输和存储\n3. **结果缓存**：可以对评估结果进行短期缓存，避免重复调用\n4. **降级策略**：当API不可用时，应有合理的降级策略\n5. **业务适配**：根据具体业务需求调整风险处理策略\n6. **监控告警**：建议对高风险用户行为进行监控和告警\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| -1 | 系统繁忙，此时请开发者稍候再试 |\n| 40003 | touser字段openid为空或者不正确 |\n| 45009 | 接口调用超过限额 |\n| 47001 | 参数错误 |"
  },
  {
    "path": "docs/src/5.x/mini-program/safety_control.md",
    "content": "# 安全风控\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html\n\n> tips: 根据提交的用户信息数据获取用户的安全等级 risk_rank，无需用户授权。\n\n## 获取用户的安全等级\n\n```php\n$app->risk_control->getUserRiskRank([\n\t'appid' => 'wx311232323',\n\t'openid' => 'oahdg535ON6vtkUXLdaLVKvzJdmM',\n\t'scene' => 1,\n\t'client_ip' => '12.234.134.2',\n]);\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/search.md",
    "content": "# 搜索功能\n\n小程序搜索功能允许开发者向微信提交小程序页面信息，提升小程序在微信搜索中的展现效果。\n\n## 获取实例\n\n```php\n$search = $app->search;\n```\n\n## 提交页面信息\n\n### 提交页面URL\n\n向微信提交小程序页面URL和参数信息，用于搜索收录：\n\n```php\n$pages = [\n    [\n        'path' => 'pages/product/detail',\n        'query' => 'id=123&category=electronics'\n    ],\n    [\n        'path' => 'pages/article/view', \n        'query' => 'article_id=456'\n    ],\n    [\n        'path' => 'pages/user/profile',\n        'query' => 'uid=789'\n    ]\n];\n\n$result = $search->submitPage($pages);\n```\n\n**参数说明：**\n- `path` string 页面路径（不包含参数）\n- `query` string 页面参数（URL查询字符串格式）\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\"\n}\n```\n\n## 使用场景\n\n### 电商小程序商品页面提交\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$search = $app->search;\n\n// 批量提交商品页面\nfunction submitProductPages($search, $products) {\n    $pages = [];\n    \n    foreach ($products as $product) {\n        $pages[] = [\n            'path' => 'pages/product/detail',\n            'query' => http_build_query([\n                'id' => $product['id'],\n                'category' => $product['category'],\n                'brand' => $product['brand']\n            ])\n        ];\n    }\n    \n    // 微信建议每次提交不超过1000个页面\n    $chunks = array_chunk($pages, 1000);\n    \n    foreach ($chunks as $chunk) {\n        $result = $search->submitPage($chunk);\n        \n        if ($result['errcode'] === 0) {\n            echo \"成功提交 \" . count($chunk) . \" 个商品页面\\n\";\n        } else {\n            echo \"提交失败: {$result['errmsg']}\\n\";\n        }\n        \n        // 避免频率限制\n        sleep(1);\n    }\n}\n\n// 示例商品数据\n$products = [\n    [\n        'id' => 'prod_001',\n        'category' => 'electronics',\n        'brand' => 'apple'\n    ],\n    [\n        'id' => 'prod_002', \n        'category' => 'clothing',\n        'brand' => 'nike'\n    ],\n    [\n        'id' => 'prod_003',\n        'category' => 'books',\n        'brand' => 'penguin'\n    ]\n];\n\nsubmitProductPages($search, $products);\n```\n\n### 内容平台页面提交\n\n```php\n// 提交文章和视频页面\nfunction submitContentPages($search) {\n    // 文章页面\n    $articlePages = [\n        [\n            'path' => 'pages/article/detail',\n            'query' => 'id=1001&category=technology'\n        ],\n        [\n            'path' => 'pages/article/detail',\n            'query' => 'id=1002&category=lifestyle'\n        ]\n    ];\n    \n    // 视频页面\n    $videoPages = [\n        [\n            'path' => 'pages/video/player',\n            'query' => 'vid=v001&playlist=tech'\n        ],\n        [\n            'path' => 'pages/video/player', \n            'query' => 'vid=v002&playlist=entertainment'\n        ]\n    ];\n    \n    // 用户页面\n    $userPages = [\n        [\n            'path' => 'pages/user/profile',\n            'query' => 'uid=user001'\n        ],\n        [\n            'path' => 'pages/user/profile',\n            'query' => 'uid=user002'\n        ]\n    ];\n    \n    // 合并所有页面\n    $allPages = array_merge($articlePages, $videoPages, $userPages);\n    \n    $result = $search->submitPage($allPages);\n    \n    if ($result['errcode'] === 0) {\n        echo \"内容页面提交成功，共提交 \" . count($allPages) . \" 个页面\\n\";\n    } else {\n        echo \"提交失败: {$result['errmsg']}\\n\";\n    }\n}\n\nsubmitContentPages($search);\n```\n\n### 服务类小程序页面提交\n\n```php\n// 餐厅预订小程序\nfunction submitRestaurantPages($search) {\n    $pages = [\n        // 餐厅详情页\n        [\n            'path' => 'pages/restaurant/detail',\n            'query' => 'restaurant_id=rest001&city=beijing'\n        ],\n        [\n            'path' => 'pages/restaurant/detail',\n            'query' => 'restaurant_id=rest002&city=shanghai'\n        ],\n        \n        // 菜品页面\n        [\n            'path' => 'pages/menu/dish',\n            'query' => 'dish_id=dish001&restaurant_id=rest001'\n        ],\n        \n        // 预订页面\n        [\n            'path' => 'pages/booking/form',\n            'query' => 'restaurant_id=rest001&date=2023-12-01'\n        ],\n        \n        // 活动页面\n        [\n            'path' => 'pages/promotion/detail',\n            'query' => 'promo_id=promo001&type=discount'\n        ]\n    ];\n    \n    $result = $search->submitPage($pages);\n    \n    if ($result['errcode'] === 0) {\n        echo \"餐厅页面提交成功\\n\";\n    }\n}\n\nsubmitRestaurantPages($search);\n```\n\n### 动态页面提交\n\n```php\n// 根据业务数据动态生成页面提交\nfunction submitDynamicPages($search, $database) {\n    $pages = [];\n    \n    // 从数据库获取最新商品\n    $latestProducts = $database->getLatestProducts(100);\n    foreach ($latestProducts as $product) {\n        $pages[] = [\n            'path' => 'pages/product/detail',\n            'query' => http_build_query([\n                'id' => $product['id'],\n                'category' => $product['category'],\n                'keywords' => $product['keywords'],\n                'price_range' => $product['price_range']\n            ])\n        ];\n    }\n    \n    // 获取热门分类页面\n    $popularCategories = $database->getPopularCategories(20);\n    foreach ($popularCategories as $category) {\n        $pages[] = [\n            'path' => 'pages/category/list',\n            'query' => http_build_query([\n                'category_id' => $category['id'],\n                'sort' => 'popular',\n                'filter' => json_encode($category['filters'])\n            ])\n        ];\n    }\n    \n    // 获取活动页面\n    $activePromotions = $database->getActivePromotions();\n    foreach ($activePromotions as $promo) {\n        $pages[] = [\n            'path' => 'pages/promotion/detail',\n            'query' => http_build_query([\n                'promo_id' => $promo['id'],\n                'type' => $promo['type'],\n                'start_time' => $promo['start_time'],\n                'end_time' => $promo['end_time']\n            ])\n        ];\n    }\n    \n    // 分批提交\n    $batches = array_chunk($pages, 500);\n    \n    foreach ($batches as $index => $batch) {\n        $result = $search->submitPage($batch);\n        \n        if ($result['errcode'] === 0) {\n            echo \"批次 \" . ($index + 1) . \" 提交成功，包含 \" . count($batch) . \" 个页面\\n\";\n        } else {\n            echo \"批次 \" . ($index + 1) . \" 提交失败: {$result['errmsg']}\\n\";\n        }\n        \n        // 控制频率\n        sleep(2);\n    }\n}\n\n// 模拟数据库类\nclass MockDatabase {\n    public function getLatestProducts($limit) {\n        // 返回模拟商品数据\n        return array_map(function($i) {\n            return [\n                'id' => \"prod_{$i}\",\n                'category' => ['electronics', 'clothing', 'books'][rand(0, 2)],\n                'keywords' => \"product,item,buy\",\n                'price_range' => rand(1, 5) * 100\n            ];\n        }, range(1, $limit));\n    }\n    \n    public function getPopularCategories($limit) {\n        return array_map(function($i) {\n            return [\n                'id' => \"cat_{$i}\",\n                'filters' => ['brand' => 'all', 'price' => 'any']\n            ];\n        }, range(1, $limit));\n    }\n    \n    public function getActivePromotions() {\n        return [\n            [\n                'id' => 'promo_001',\n                'type' => 'discount',\n                'start_time' => strtotime('-1 day'),\n                'end_time' => strtotime('+7 days')\n            ]\n        ];\n    }\n}\n\n$database = new MockDatabase();\nsubmitDynamicPages($search, $database);\n```\n\n### 定期更新页面信息\n\n```php\n// 定期提交页面信息的任务\nfunction schedulePageSubmission($search) {\n    // 每日提交新增页面\n    $dailyNewPages = [\n        // 今日新增商品\n        [\n            'path' => 'pages/product/detail',\n            'query' => 'id=new_prod_' . date('Ymd') . '&new=true'\n        ],\n        \n        // 今日文章\n        [\n            'path' => 'pages/article/detail',\n            'query' => 'id=article_' . date('Ymd') . '&date=' . date('Y-m-d')\n        ],\n        \n        // 今日活动\n        [\n            'path' => 'pages/daily/activity',\n            'query' => 'date=' . date('Y-m-d') . '&type=daily'\n        ]\n    ];\n    \n    $result = $search->submitPage($dailyNewPages);\n    \n    if ($result['errcode'] === 0) {\n        echo date('Y-m-d H:i:s') . \" - 每日页面提交成功\\n\";\n    } else {\n        echo date('Y-m-d H:i:s') . \" - 提交失败: {$result['errmsg']}\\n\";\n    }\n}\n\n// 可以放在定时任务中执行\nschedulePageSubmission($search);\n```\n\n## 注意事项\n\n1. **提交频率限制**：避免短时间内大量调用接口\n2. **页面有效性**：确保提交的页面路径真实存在且可访问\n3. **参数准确性**：确保query参数与实际页面逻辑匹配\n4. **数量限制**：单次提交页面数量有限制\n5. **权限要求**：需要小程序管理员权限\n\n## 最佳实践\n\n1. **合理规划提交**：根据业务重要性确定页面提交优先级\n2. **参数优化**：在query中包含有助于搜索的关键参数\n3. **定期更新**：定期提交新增和更新的页面信息\n4. **监控效果**：关注提交后的搜索展现效果\n5. **批量处理**：合理分批提交大量页面\n\n## 搜索优化建议\n\n1. **页面标题优化**：确保页面有清晰的标题\n2. **关键词布局**：在参数中包含相关关键词\n3. **分类标识**：通过参数明确页面分类\n4. **时效性信息**：包含时间相关参数提升时效性\n5. **用户体验**：确保搜索进入的页面体验良好\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| -1 | 系统繁忙，此时请开发者稍候再试 |\n| 40001 | 获取access_token时AppSecret错误 |\n| 40013 | 不合法的AppID |\n| 41001 | 缺少access_token参数 |\n| 45009 | 接口调用超过限额 |\n| 47001 | 参数错误 |\n| 85064 | pages参数错误 |\n| 85065 | 单次提交页面数超过限制 |"
  },
  {
    "path": "docs/src/5.x/mini-program/shipping.md",
    "content": "# 小程序发货信息管理\n\n> 微信文档 https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html\n\n\n## 发货信息录入接口\n\n```php \n$data = [\n    'order_key' => [\n        'order_number_type' => 1,\n        'mchid' => '',\n        'out_trade_no' => ''\n    ],\n    'logistics_type' => 4,\n    'delivery_mode' => 1,\n    'shipping_list' => [\n      [\n          'tracking_no' => '323244567777',\n          'express_company' => 'DHL',\n          'item_desc' => '微信红包抱枕*1个',\n          'contact' => [\n              'consignor_contact' => '189****1234',\n              'receiver_contact' => '189****1234'\n          ],\n      ],\n    ],\n    'upload_time' => '2022-12-15T13:29:35.120+08:00',\n    'payer' => [\n        'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'\n    ]\n];\n\n$app->shipping->uploadShippingInfo($data);\n```\n\n## 发货信息合单录入接口\n\n```php\n $data = [\n    'order_key' => [\n        'order_number_type' => 1,\n        'mchid' => '',\n        'out_trade_no' => ''\n    ],\n    'sub_orders' => [\n        'order_key' => [\n            'order_number_type' => 1,\n            'transaction_id' => '',\n            'mchid' => '',\n            'out_trade_no' => ''\n        ],\n        'logistics_type' => 4,\n        'delivery_mode' => 1,\n        'shipping_list' => [\n            [\n                'tracking_no' => '323244567777',\n                'express_company' => 'DHL',\n                'item_desc' => '微信红包抱枕*1个',\n                'contact' => [\n                    'consignor_contact' => '189****1234',\n                    'receiver_contact' => '189****1234'\n                ],\n            ],\n        ],\n    ],\n    'upload_time' => '2022-12-15T13:29:35.120+08:00',\n    'payer' => [\n        'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'\n    ]\n];\n\n$app->shipping->uploadCombineShippingInfo($data);\n```\n\n## 查询订单发货状态\n\n```php\n$data = $app->shipping->getOrder([\n    'transaction_id' => 'xxx'\n]);\n```\n\n## 查询订单列表\n\n```php\n$data = $app->shipping->getOrderList();\n```\n\n## 确认收货提醒接口\n\n```php\n$data = [\n    'transaction_id' => '42000020212023112332159214xx',\n    'received_time' => ''\n];\n\n$app->shipping->notifyConfirmReceive($data);\n```\n\n## 消息跳转路径设置接口\n\n```php\n$data = [\n    'path' => 'pages/goods/order_detail?id=xxxx',\n];\n\n$app->shipping->setMsgJumpPath($data);\n```\n\n## 查询小程序是否已开通发货信息管理服务\n\n```php\n$app->shipping->isTradeManaged();\n```\n\n## 查询小程序是否已完成交易结算管理确认\n\n```php\n$app->shipping->isTradeCompleted();\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/short_link.md",
    "content": "# Short Link\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/short-link/shortlink.generate.html\n\n\n## 获取小程序 Short Link\n\n```php\n$app->short_link->getShortLink(string $pageUrl, string $pageTitle, bool $isPermanent = false);\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/soter.md",
    "content": "# 生物认证\n\n## 生物认证秘钥签名验证\n\n> https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/soter/soter.verifySignature.html\n\n```php\n$app->soter->verifySignature($openid, $json, $signature);\n```\n\n返回值示例:\n```json\n{\n    \"is_ok\": true\n}\n```\n\n参数说明:\n\n> - string $openid - 用户 openid\n> - string $json - 通过 [wx.startSoterAuthentication](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/soter/wx.startSoterAuthentication.html) 成功回调获得的 resultJSON 字段\n> - string $signature - 通过 [wx.startSoterAuthentication](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/soter/wx.startSoterAuthentication.html) 成功回调获得的 resultJSONSignature 字段"
  },
  {
    "path": "docs/src/5.x/mini-program/subscribe_message.md",
    "content": "# 订阅消息\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html\n\n## 组合模板并添加至帐号下的个人模板库\n\n```php\n$tid = 563;     // 模板标题 id，可通过接口获取，也可登录小程序后台查看获取\n$kidList = [1, 2];      // 开发者自行组合好的模板关键词列表，可以通过 `getTemplateKeywords` 方法获取\n$sceneDesc = '提示用户图书到期';    // 服务场景描述，非必填\n\n$app->subscribe_message->addTemplate($tid, $kidList, $sceneDesc);\n```\n\n## 删除帐号下的个人模板\n\n```php\n$templateId = 'bDmywsp2oEHjwAadTGKkUHpC0RgBVPvfAM7Cu1s03z8';\n\n$app->subscribe_message->deleteTemplate($templateId);\n```\n\n## 获取小程序账号的类目\n\n```php\n$app->subscribe_message->getCategory();\n```\n\n## 获取模板标题的关键词列表\n\n```php\n$tid = 563;     // 模板标题 id，可通过接口获取，也可登录小程序后台查看获取\n\n$app->subscribe_message->getTemplateKeywords($tid);\n```\n\n## 获取帐号所属类目下的公共模板标题\n\n```php\n$ids = [612, 613];  // 类目 id\n$start = 0;         // 用于分页，表示从 start 开始。从 0 开始计数。  \n$limit = 30;        // 用于分页，表示拉取 limit 条记录。最大为 30。\n\n$app->subscribe_message->getTemplateTitles($ids, $start, $limit);\n```\n\n## 获取当前帐号下的个人模板列表\n\n```php\n$app->subscribe_message->getTemplates();\n```\n\n## 发送订阅消息\n\n```php\n$data = [\n    'template_id' => 'bDmywsp2oEHjwAadTGKkUJ-eJEiMiOf7H-dZ7wjdw80', // 所需下发的订阅模板id\n    'touser' => 'oSyZp5OBNPBRhG-7BVgWxbiNZm',     // 接收者（用户）的 openid\n    'page' => '',       // 点击模板卡片后的跳转页面，仅限本小程序内的页面。支持带参数,（示例index?foo=bar）。该字段不填则模板无跳转。\n    'data' => [         // 模板内容，格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }\n        'date01' => [\n            'value' => '2019-12-01',\n        ],\n        'number01' => [\n            'value' => 10,\n        ],\n    ],\n];\n\n$app->subscribe_message->send($data);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/template_message.md",
    "content": "# 模板消息\n\n## 获取小程序模板库标题列表\n\n```\n$app->template_message->list($offset, $count);\n```\n\n## 获取模板库某个模板标题下关键词库\n\n```\n$app->template_message->get($id);\n```\n\n## 组合模板并添加至帐号下的个人模板库\n\n```\n$app->template_message->add($id, $keywordIdList);\n```\n\n## 获取帐号下已存在的模板列表\n\n```\n$app->template_message->getTemplates($offset, $count);\n```\n\n## 删除帐号下的某个模板\n\n```\n$app->template_message->delete($templateId);\n```\n\n## 发送模板消息\n\n```php\n$app->template_message->send([\n    'touser' => 'user-openid',\n    'template_id' => 'template-id',\n    'page' => 'index',\n    'form_id' => 'form-id',\n    'data' => [\n        'keyword1' => 'VALUE',\n        'keyword2' => 'VALUE2',\n        // ...\n    ],\n]);\n```\n"
  },
  {
    "path": "docs/src/5.x/mini-program/union.md",
    "content": "# 联盟推广\n\n小程序联盟推广功能允许开发者管理推广计划，通过推广获得佣金收益。\n\n## 获取实例\n\n```php\n$union = $app->union;\n```\n\n## 推广计划管理\n\n### 创建推广计划\n\n创建新的推广计划：\n\n```php\n$result = $union->createPromotion('推广计划名称');\n```\n\n**参数说明：**\n- `promotionSourceName` string 推广计划名称\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"promotionSourcePid\": \"10000123\",\n    \"promotionSourceName\": \"推广计划名称\"\n}\n```\n\n### 删除推广计划\n\n删除指定的推广计划：\n\n```php\n$result = $union->deletePromotion('10000123', '推广计划名称');\n```\n\n**参数说明：**\n- `promotionSourcePid` string 推广计划ID\n- `promotionSourceName` string 推广计划名称\n\n### 更新推广计划\n\n更新推广计划信息：\n\n```php\n$result = $union->updatePromotion('10000123', '新的推广计划名称');\n```\n\n**参数说明：**\n- `promotionSourcePid` string 推广计划ID\n- `promotionSourceName` string 新的推广计划名称\n\n### 获取推广计划列表\n\n获取所有推广计划：\n\n```php\n$result = $union->getPromotions();\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"promotionList\": [\n        {\n            \"promotionSourcePid\": \"10000123\",\n            \"promotionSourceName\": \"推广计划1\",\n            \"createTime\": 1635724800,\n            \"status\": 1\n        }\n    ]\n}\n```\n\n## 推广商品管理\n\n### 添加推广商品\n\n将商品添加到推广计划：\n\n```php\n$result = $union->addProduct('10000123', [\n    'productId' => 'product_001',\n    'commissionRate' => 1500 // 佣金比例，单位为万分之一，1500表示15%\n]);\n```\n\n### 移除推广商品\n\n从推广计划中移除商品：\n\n```php\n$result = $union->removeProduct('10000123', 'product_001');\n```\n\n### 获取推广商品列表\n\n获取推广计划下的所有商品：\n\n```php\n$result = $union->getProducts('10000123', $page, $pageSize);\n```\n\n**参数说明：**\n- `promotionSourcePid` string 推广计划ID\n- `page` int 页码，从1开始\n- `pageSize` int 每页数量，最大50\n\n## 订单与佣金\n\n### 获取推广订单\n\n查询推广产生的订单：\n\n```php\n$result = $union->getOrders([\n    'promotionSourcePid' => '10000123',\n    'startTime' => strtotime('-30 days'),\n    'endTime' => time(),\n    'page' => 1,\n    'pageSize' => 20\n]);\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"orderList\": [\n        {\n            \"orderId\": \"order_123456\",\n            \"productId\": \"product_001\",\n            \"productName\": \"商品名称\",\n            \"orderAmount\": 10000,\n            \"commissionAmount\": 1500,\n            \"orderTime\": 1635724800,\n            \"status\": 2,\n            \"buyerOpenid\": \"buyer_openid\"\n        }\n    ],\n    \"totalCount\": 156\n}\n```\n\n### 获取佣金明细\n\n查询佣金收益明细：\n\n```php\n$result = $union->getCommissions([\n    'promotionSourcePid' => '10000123',\n    'startTime' => strtotime('-30 days'),\n    'endTime' => time(),\n    'page' => 1,\n    'pageSize' => 20\n]);\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"commissionList\": [\n        {\n            \"orderId\": \"order_123456\",\n            \"commissionAmount\": 1500,\n            \"commissionTime\": 1635724800,\n            \"status\": 1,\n            \"settleTime\": 1635811200\n        }\n    ],\n    \"totalAmount\": 15000,\n    \"totalCount\": 10\n}\n```\n\n## 推广数据统计\n\n### 获取推广数据概览\n\n```php\n$result = $union->getOverview('10000123', [\n    'startTime' => strtotime('-30 days'),\n    'endTime' => time()\n]);\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"data\": {\n        \"clickCount\": 1520,\n        \"orderCount\": 156,\n        \"orderAmount\": 1560000,\n        \"commissionAmount\": 234000,\n        \"conversionRate\": 10.26\n    }\n}\n```\n\n### 获取推广趋势数据\n\n```php\n$result = $union->getTrend('10000123', [\n    'startTime' => strtotime('-7 days'),\n    'endTime' => time(),\n    'granularity' => 'day' // day, hour\n]);\n```\n\n## 使用示例\n\n### 完整推广流程示例\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n];\n\n$app = Factory::miniProgram($config);\n$union = $app->union;\n\n// 1. 创建推广计划\n$promotion = $union->createPromotion('春季促销推广');\n\nif ($promotion['errcode'] === 0) {\n    $pid = $promotion['promotionSourcePid'];\n    echo \"推广计划创建成功，ID: {$pid}\\n\";\n    \n    // 2. 添加推广商品\n    $addProduct = $union->addProduct($pid, [\n        'productId' => 'spring_product_001',\n        'commissionRate' => 2000 // 20%佣金\n    ]);\n    \n    if ($addProduct['errcode'] === 0) {\n        echo \"商品添加成功\\n\";\n        \n        // 3. 获取推广链接\n        $promotionUrl = \"https://your-mini-program.com?pid={$pid}&product_id=spring_product_001\";\n        echo \"推广链接: {$promotionUrl}\\n\";\n        \n        // 4. 查询推广数据\n        sleep(1); // 模拟等待一段时间后查询\n        $overview = $union->getOverview($pid, [\n            'startTime' => strtotime('-1 day'),\n            'endTime' => time()\n        ]);\n        \n        if ($overview['errcode'] === 0) {\n            $data = $overview['data'];\n            echo \"点击数: {$data['clickCount']}\\n\";\n            echo \"订单数: {$data['orderCount']}\\n\";\n            echo \"订单金额: \" . ($data['orderAmount'] / 100) . \"元\\n\";\n            echo \"佣金收益: \" . ($data['commissionAmount'] / 100) . \"元\\n\";\n            echo \"转化率: {$data['conversionRate']}%\\n\";\n        }\n    }\n}\n```\n\n### 佣金结算示例\n\n```php\n// 查询待结算佣金\n$commissions = $union->getCommissions([\n    'promotionSourcePid' => $pid,\n    'status' => 0, // 0:待结算 1:已结算\n    'page' => 1,\n    'pageSize' => 50\n]);\n\nif ($commissions['errcode'] === 0) {\n    $totalPendingCommission = 0;\n    \n    foreach ($commissions['commissionList'] as $commission) {\n        $totalPendingCommission += $commission['commissionAmount'];\n    }\n    \n    echo \"待结算佣金总额: \" . ($totalPendingCommission / 100) . \"元\\n\";\n    echo \"待结算订单数: \" . count($commissions['commissionList']) . \"\\n\";\n}\n```\n\n### 推广效果分析\n\n```php\n// 获取最近7天的推广趋势\n$trend = $union->getTrend($pid, [\n    'startTime' => strtotime('-7 days'),\n    'endTime' => time(),\n    'granularity' => 'day'\n]);\n\nif ($trend['errcode'] === 0) {\n    echo \"最近7天推广趋势:\\n\";\n    foreach ($trend['data'] as $dayData) {\n        $date = date('Y-m-d', $dayData['timestamp']);\n        echo \"{$date}: 点击{$dayData['clickCount']}次, 订单{$dayData['orderCount']}个\\n\";\n    }\n}\n\n// 计算推广ROI\n$overview = $union->getOverview($pid, [\n    'startTime' => strtotime('-30 days'),\n    'endTime' => time()\n]);\n\nif ($overview['errcode'] === 0) {\n    $data = $overview['data'];\n    $roi = $data['orderAmount'] > 0 ? ($data['commissionAmount'] / $data['orderAmount']) * 100 : 0;\n    echo \"推广ROI: {$roi}%\\n\";\n}\n```\n\n## 注意事项\n\n1. **推广计划限制**：每个小程序的推广计划数量有限制\n2. **佣金结算周期**：佣金通常有一定的结算周期，不是实时到账\n3. **商品资质要求**：推广的商品需要符合平台规范\n4. **数据统计延迟**：推广数据可能有一定延迟\n5. **API调用限制**：注意API调用频率限制\n\n## 最佳实践\n\n1. **合理设置佣金比例**：根据商品利润合理设置佣金，既要有吸引力又要保证盈利\n2. **数据监控**：定期监控推广数据，及时调整推广策略\n3. **推广渠道多样化**：通过多个推广计划覆盖不同的推广渠道\n4. **效果追踪**：建立完善的推广效果追踪机制\n5. **合规经营**：确保推广活动符合相关法律法规\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| -1 | 系统繁忙 |\n| 40001 | 获取access_token时AppSecret错误 |\n| 40013 | 不合法的AppID |\n| 41001 | 缺少access_token参数 |\n| 45009 | 接口调用超过限额 |\n| 48001 | api功能未授权 |"
  },
  {
    "path": "docs/src/5.x/mini-program/url_link.md",
    "content": "# URL Link\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html\n\n> tips: 目前仅针对国内非个人主体的小程序开放.\n\n## 获取小程序 URL Link\n\n```php\n$app->url_link->generate(array $params);\n```"
  },
  {
    "path": "docs/src/5.x/mini-program/url_scheme.md",
    "content": "# URL Scheme\n\n> 微信文档：https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html\n\n> tips: 目前仅针对国内非个人主体的小程序开放.\n\n## 获取小程序scheme码\n\n```php\n$app->url_scheme->generate();\n```"
  },
  {
    "path": "docs/src/5.x/miscellaneous.md",
    "content": "# 其它\n\n\n### 其它"
  },
  {
    "path": "docs/src/5.x/official-account/accounts.md",
    "content": "# 多账号接入\n\n如果你想使用本项目接入多个公众号，在本程序中，您可以为每个帐号都设置一个 id，此 id 对应了该帐号的 appid、token 等信息。\n如下表\n\n| id   | appId                | secret                             | 其它... |\n| ---- | -------------------- | ---------------------------------- | ----- |\n| 1    | `wx3cf0f39249eb0e60` | `f28f735d4f1c242f4687abb469072a29` | ...   |\n| 2    | `wx49eb0e63cf0f39s2` | `8f735d4687abb469f1c2422a29f4f207` | ...   |\n| N    | `wx5cfeb0e60f392490` | `35f8f27d46f1c242f487a9072a29bb46` | ...   |\n\n在微信公众平台的设置中，您可以将您帐号中平台的 `url` 设置为 `您的网址/?id=xxx`，如:\n\n```\nhttp://easywechat.com/wechat?id=1\n```\n\n而在程序入口处，根据 `id` 查找对应帐号的 `appid` 和 其它信息来创建配置数组创建实例即可。\n"
  },
  {
    "path": "docs/src/5.x/official-account/base.md",
    "content": "# 基础接口\n\n## 清理接口调用次数\n\n> 此接口官方有每月调用限制，不可随意调用\n\n```php\n$app->base->clearQuota();\n```\n\n## 获取微信服务器 IP (或IP段)\n\n```php\n$app->base->getValidIps();\n```"
  },
  {
    "path": "docs/src/5.x/official-account/broadcasting.md",
    "content": "# 群发\n\n微信的群发消息接口有各种乱七八糟的注意事项及限制，具体请阅读微信官方文档。\n\n## 发送消息\n\n以下所有方法均有第二个参数 `$to` 用于指定接收对象：\n\n>  - 当 `$to` 为整型时为标签 id\n>  - 当 `$to` 为数组时为用户的 openid 列表（至少两个用户的 openid）\n>  - 当 `$to` 为 `null` 时表示全部用户\n\n```php\n$app->broadcasting->sendMessage(Message $message, array | int $to = null);\n```\n\n下面的别名方法 `sendXXX` 都是基于上面 `sendMessage` 方法的封装。\n\n### 文本消息\n\n```php\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\");\n\n// 指定目标用户\n// 至少两个用户的 openid，必须是数组。\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\", [$openid1, $openid2]);\n\n// 指定标签组用户\n$app->broadcasting->sendText(\"大家好！欢迎使用 EasyWeChat。\", $tagId); // $tagId 必须是整型数字\n```\n\n### 图文消息\n\n```php\n$app->broadcasting->sendNews($mediaId);\n$app->broadcasting->sendNews($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendNews($mediaId, $tagId);\n```\n\n### 图片消息\n\n```php\n$app->broadcasting->sendImage($mediaId);\n$app->broadcasting->sendImage($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendImage($mediaId, $tagId);\n```\n\n### 语音消息\n\n```php\n$app->broadcasting->sendVoice($mediaId);\n$app->broadcasting->sendVoice($mediaId, [$openid1, $openid2]);\n$app->broadcasting->sendVoice($mediaId, $tagId);\n```\n\n### 视频消息\n\n用于群发的视频消息，需要先创建消息对象，\n\n```php\n// 1. 先上传视频素材用于群发：\n$video = '/path/to/video.mp4';\n$videoMedia = $app->media->uploadVideoForBroadcasting($video, '视频标题', '视频描述');\n\n// 结果如下：\n//{\n//  \"type\":\"video\",\n//  \"media_id\":\"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc\",\n//  \"created_at\":1398848981\n//}\n\n// 2. 使用上面得到的 media_id 群发视频消息\n$app->broadcasting->sendVideo($videoMedia['media_id']);\n```\n\n### 卡券消息\n\n```php\n$app->broadcasting->sendCard($cardId);\n$app->broadcasting->sendCard($cardId, [$openid1, $openid2]);\n$app->broadcasting->sendCard($cardId, $tagId);\n```\n\n### 发送预览群发消息给指定的 `openId` 用户\n\n```php\n$app->broadcasting->previewText($text, $openId);\n$app->broadcasting->previewNews($mediaId, $openId);\n$app->broadcasting->previewVoice($mediaId, $openId);\n$app->broadcasting->previewImage($mediaId, $openId);\n$app->broadcasting->previewVideo($message, $openId);\n$app->broadcasting->previewCard($cardId, $openId);\n```\n\n### 发送预览群发消息给指定的微信号用户\n\n> $wxanme 是用户的微信号，比如：notovertrue\n\n```php\n$app->broadcasting->previewTextByName($text, $wxname);\n$app->broadcasting->previewNewsByName($mediaId, $wxname);\n$app->broadcasting->previewVoiceByName($mediaId, $wxname);\n$app->broadcasting->previewImageByName($mediaId, $wxname);\n$app->broadcasting->previewVideoByName($message, $wxname);\n$app->broadcasting->previewCardByName($cardId, $wxname);\n```\n\n### 删除群发消息\n\n```php\n$app->broadcasting->delete($msgId);\n```\n\n### 查询群发消息发送状态\n\n```php\n$app->broadcasting->status($msgId);\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/card.md",
    "content": "# 卡券\n\n-\n\n## 获取实例\n\n```php\n$card = $app->card;\n```\n\n## 通用功能\n\n### 获取卡券颜色\n\n```php\n$card->colors();\n```\n\n### 卡券开放类目查询\n\n```php\n$card->categories();\n```\n\n### 创建卡券\n\n创建卡券接口是微信卡券的基础接口，用于创建一类新的卡券，获取 card_id，创建成功并通过审核后，商家可以通过文档提供的其他接口将卡券下发给用户，每次成功领取，库存数量相应扣除。\n\n```php\n$card->create($cardType = 'member_card', array $attributes);\n```\n\n> - `attributes` array 卡券信息\n\n示例：\n\n```php\n<?php\n\n $cardType = 'GROUPON';\n\n    $attributes = [\n      'base_info' => [\n          'brand_name' => '微信餐厅',\n          'code_type' => 'CODE_TYPE_TEXT',\n          'title' => '132元双人火锅套餐',\n          //...\n      ],\n      'advanced_info' => [\n          'use_condition' => [\n              'accept_category' => '鞋类',\n              'reject_category' => '阿迪达斯',\n              'can_use_with_other_discount' => true,\n          ],\n          //...\n      ],\n    ];\n\n$result = $card->create($cardType, $attributes);\n```\n\n### 获取卡券详情\n\n```php\n$cardInfo = $card->get($cardId);\n```\n\n### 批量查询卡列表\n\n```php\n$card->list($offset = 0, $count = 10, $statusList = 'CARD_STATUS_VERIFY_OK');\n```\n\n> - `offset` int - 查询卡列表的起始偏移量，从 0 开始\n> - `count` int - 需要查询的卡片的数量\n> - `statusList` - 支持开发者拉出指定状态的卡券列表，详见 example\n\n示例：\n\n```php\n// CARD_STATUS_NOT_VERIFY, 待审核；\n// CARD_STATUS_VERIFY_FAIL, 审核失败；\n// CARD_STATUS_VERIFY_OK， 通过审核；\n// CARD_STATUS_USER_DELETE，卡券被商户删除；\n// CARD_STATUS_DISPATCH，在公众平台投放过的卡券；\n\n$result = $card->list($offset, $count, 'CARD_STATUS_NOT_VERIFY');\n```\n\n### 更改卡券信息接口\n\n支持更新所有卡券类型的部分通用字段及特殊卡券中特定字段的信息。\n\n```php\n$card->update($cardId, $type, $attributes = []);\n```\n\n> - `type` string - 卡券类型\n\n示例：\n\n```php\n$cardId = 'pdkJ9uCzKWebwgNjxosee0ZuO3Os';\n\n$type = 'groupon';\n\n$attributes = [\n  'base_info' => [\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/2aJY6aCPatSeibYAyy7yct9zJXL9WsNVL4JdkTbBr184gNWS6nibcA75Hia9CqxicsqjYiaw2xuxYZiaibkmORS2oovdg/0',\n    'center_title' => '顶部居中按钮',\n    'center_sub_title' => '按钮下方的wording',\n    'center_url' => 'http://easywechat.com',\n    'custom_url_name' => '立即使用',\n    'custom_url' => 'http://www.qq.com',\n    'custom_url_sub_title' => '6个汉字tips',\n    'promotion_url_name' => '更多优惠',\n    'promotion_url' => 'http://www.qq.com',\n  ],\n  //...\n];\n\n$result = $card->update($cardId, $type, $attributes);\n```\n\n### 删除卡券\n\n```php\n$card->delete($cardId);\n```\n\n### 创建二维码\n\n开发者可调用该接口生成一张卡券二维码供用户扫码后添加卡券到卡包。\n\n自定义 Code 码的卡券调用接口时，POST 数据中需指定 code，非自定义 code 不需指定，指定 openid 同理。指定后的二维码只能被用户扫描领取一次。\n\n```php\n$card->createQrCode($cards);\n```\n\n> - `cards` array - 卡券相关信息\n\n示例：\n\n```php\n// 领取单张卡券\n$cards = [\n    'action_name' => 'QR_CARD',\n    'expire_seconds' => 1800,\n    'action_info' => [\n      'card' => [\n        'card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY',\n        'is_unique_code' => false,\n        'outer_id' => 1,\n      ],\n    ],\n  ];\n\n$result = $card->createQrCode($cards);\n```\n\n```php\n// 领取多张卡券\n$cards = [\n    'action_name' => 'QR_MULTIPLE_CARD',\n    'action_info' => [\n      'multiple_card' => [\n        'card_list' => [\n          ['card_id' => 'pdkJ9uFS2WWCFfbbEfsAzrzizVyY'],\n        ],\n      ],\n    ],\n  ];\n\n$result = $card->createQrCode($cards);\n```\n\n请求成功返回值示例：\n\n```json\n{\n  \"errcode\": 0,\n  \"errmsg\": \"ok\",\n  \"ticket\": \"gQHB8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0JIV3lhX3psZmlvSDZmWGVMMTZvAAIEsNnKVQMEIAMAAA==\", //获取ticket后需调用换取二维码接口获取二维码图片，详情见字段说明。\n  \"expire_seconds\": 1800,\n  \"url\": \"http://weixin.qq.com/q/BHWya_zlfioH6fXeL16o \",\n  \"show_qrcode_url\": \"https://mp.weixin.qq.com/cgi-bin/showqrcode?  ticket=gQH98DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL0czVzRlSWpsamlyM2plWTNKVktvAAIE6SfgVQMEgDPhAQ%3D%3D\"\n}\n```\n\n### ticket 换取二维码图片\n\n获取二维码 ticket 后，开发者可用 ticket 换取二维码图片。\n\n```php\n$card->getQrCode($ticket);\n```\n\n> - `ticket` string> - 获取的二维码 ticket，凭借此 ticket 可以在有效时间内换取二维码。\n\n示例：\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$result = $card->getQrCode($ticket);\n```\n\n### ticket 换取二维码链接\n\n```php\n$card->getQrCodeUrl($ticket);\n```\n\n示例：\n\n```php\n$ticket = 'gQFF8DoAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL01VTzN0T0hsS1BwUlBBYUszbVN5AAIEughxVwMEAKd2AA==';\n$card->getQrCodeUrl($ticket);\n```\n\n### 创建货架接口\n\n开发者需调用该接口创建货架链接，用于卡券投放。创建货架时需填写投放路径的场景字段。\n\n```php\n$card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cards);\n```\n\n> - `banner` string -页面的 banner 图;\n> - `pageTitle` string - 页面的 title\n> - `canShare` bool - 页面是不是可以分享，true 或 false\n> - `scene` string - 投放页面的场景值，具体值请参考下面的 example\n> - `cards` array - 卡券列表，每个元素有两个字段\n\n示例：\n\n```php\n$banner = 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFN';\n$pageTitle = '惠城优惠大派送';\n$canShare  = true;\n\n//SCENE_NEAR_BY          附近\n//SCENE_MENU             自定义菜单\n//SCENE_QRCODE             二维码\n//SCENE_ARTICLE             公众号文章\n//SCENE_H5                 h5页面\n//SCENE_IVR                 自动回复\n//SCENE_CARD_CUSTOM_CELL 卡券自定义cell\n$scene = 'SCENE_NEAR_BY';\n\n$cardList = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/test.png'],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'thumb_url' => 'http://test.digilinx.cn/wxApi/Uploads/aa.jpg'],\n];\n\n$result = $card->createLandingPage($banner, $pageTitle, $canShare, $scene, $cardList);\n```\n\n### 图文消息群发卡券\n\n> 特别注意：目前该接口仅支持填入非自定义 code 的卡券,自定义 code 的卡券需先进行 code 导入后调用。\n\n```php\n$card->getHtml($cardId);\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->getHtml($cardId);\n```\n\n### 设置测试白名单\n\n同时支持“openid”、“username”两种字段设置白名单，总数上限为 10 个。\n\n```php\n$card->setTestWhitelist($openids); // 使用 openid\n$card->setTestWhitelistByName($usernames); // 使用 username\n```\n\n> - `openids` array - 测试的 openid 列表\n> - `usernames` array> - 测试的微信号列表\n\n示例：\n\n```php\n// by openid\n$openids   = [$openId, $openId2, $openid3...];\n$result = $card->setTestWhitelist($openids);\n\n// by username\n$usernames = ['tianye0327', 'iovertrue'];\n$result = $card->setTestWhitelistByName($usernames);\n```\n\n### 获取用户已领取卡券接口\n\n用于获取用户卡包里的，属于该 appid 下所有**可用卡券，包括正常状态和未生效状态**。\n\n```php\n$card->getUserCards($openid, $cardId);\n```\n\n示例：\n\n```php\n$openid  = 'odkJ9uDUz26RY-7DN1mxkznfo9xU';\n$cardId = ''; // 卡券ID。不填写时默认查询当前 appid 下的卡券。\n\n$result = $card->getUserCards($openid, $cardId);\n```\n\n### 设置微信买单接口\n\n```php\n$card->setPayCell($cardId, $isOpen = true);\n```\n\n> - `isOpen` string - 是否开启买单功能，填 true/false，不填默认 true\n\n示例：\n\n```php\n$cardId = 'pdkJ9uH7u11R-Tu1kilbaW_zDFow';\n\n$result = $card->setPayCell($cardId); // isOpen = true\n$result = $card->setPayCell($cardId, $isOpen);\n```\n\n### 修改库存接口\n\n```php\n$card->increaseStock($cardId, $amount); // 增加库存\n$card->reductStock($cardId, $amount); // 减少库存\n```\n\n> - `cardId` string - 卡券 ID\n> - `amount` int - 修改多少库存\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY';\n\n$result = $card->increaseStock($cardId, 100);\n```\n\n## 卡券 Code\n\n### 导入 code 接口\n\n在自定义 code 卡券成功创建并且通过审核后，必须将自定义 code 按照与发券方的约定数量调用导入 code 接口导入微信后台。\n\n```php\n$card->code->deposit($cardId, $codes);\n```\n\n> - `cardId` string - 要导入 code 的卡券 ID\n> - `codes` array - 要导入微信卡券后台的自定义 code，最多 100 个\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n$codes    = ['11111', '22222', '33333'];\n\n$result = $card->code->deposit($cardId, $codes);\n```\n\n### 查询导入 code 数目\n\n```php\n$card->code->getDepositedCount($cardId);  // 要导入 code 的卡券 ID\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$result = $card->code->getDepositedCount($cardId);\n```\n\n### 核查 code 接口\n\n为了避免出现导入差错，强烈建议开发者在查询完 code 数目的时候核查 code 接口校验 code 导入微信后台的情况。\n\n```php\n$card->code->check($cardId, $codes);\n```\n\n示例：\n\n```php\n$cardId = 'pdkJ9uLCEF_HSKO7JdQOUcZ-PUzo';\n\n$codes = ['807732265476', '22222', '33333'];\n\n$result = $card->code->check($cardId, $codes);\n```\n\n### 查询 Code 接口\n\n```php\n$card->code->get($code, $cardId, $checkConsume = true);\n```\n\n> - checkConsume 是否校验 code 核销状态，true 和 false\n\n示例：\n\n```php\n$code = '736052543512';\n$cardId = 'pdkJ9uDgnm0pKfrTb1yV0dFMO_Gk';\n\n$result = $card->code->get($code, $cardId);\n$result = $card->code->get($code, $cardId, false); // check_consume = false\n```\n\n### 核销 Code 接口\n\n```php\n$card->code->consume($code);\n// 或者指定 cardId\n$card->code->consume($code, $cardId);\n```\n\n示例：\n\n```php\n$code = '789248558333';\n$cardId = 'pdkJ9uDmhkLj6l5bm3cq9iteQBck';\n\n$result = $card->code->consume($code);\n// 或\n$result = $card->code->consume($code, $cardId);\n```\n\n### Code 解码接口\n\n```php\n$card->code->decrypt($encryptedCode);\n```\n\n示例：\n\n```php\n$encryptedCode = 'XXIzTtMqCxwOaawoE91+VJdsFmv7b8g0VZIZkqf4GWA60Fzpc8ksZ/5ZZ0DVkXdE';\n\n$result = $card->code->decrypt($encryptedCode);\n```\n\n### 更改 Code 接口\n\n```php\n$card->code->update($code, $newCode, $cardId);\n```\n\n> - `newCode` string - 变更后的有效 Code 码\n\n示例：\n\n```php\n$code = '148246271394';\n$newCode = '659266965266';\n$cardId = '';\n\n$result = $card->code->update($code, $newCode, $cardId);\n```\n\n### 设置卡券失效\n\n```php\n$card->code->disable($code, $cardId);\n```\n\n示例：\n\n```php\n$code    = '736052543512';\n$cardId = '';\n\n$result = $card->code->disable($code, $cardId);\n```\n\n## 通用卡券\n\n## 卡券激活\n\n```php\n$result = $card->general_card->activate($info);\n```\n\n## 撤销激活\n\n```php\n$result = $card->general_card->deactivate(string $cardId, string $code);\n```\n\n## 更新用户信息\n\n```php\n$result = $card->general_card->updateUser(array $info);\n```\n\n## 会员卡\n\n### 会员卡激活\n\n```php\n$result = $card->member_card->activate($info);\n```\n\n> - `info` - 需要激活的会员卡信息\n\n示例：\n\n```php\n$info = [\n      'membership_number'        => '357898858', //会员卡编号，由开发者填入，作为序列号显示在用户的卡包里。可与Code码保持等值。\n      'code'                     => '916679873278', //创建会员卡时获取的初始code。\n      'activate_begin_time'      => '1397577600', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式\n      'activate_end_time'        => '1422724261', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。\n      'init_bonus'               => '持白金会员卡到店消费，可享8折优惠。', //初始积分，不填为0。\n      'init_balance'             => '持白金会员卡到店消费，可享8折优惠。', //初始余额，不填为0。\n      'init_custom_field_value1' => '白银', //创建时字段custom_field1定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value2' => '9折', //创建时字段custom_field2定义类型的初始值，限制为4个汉字，12字节。\n      'init_custom_field_value3' => '200', //创建时字段custom_field3定义类型的初始值，限制为4个汉字，12字节。\n];\n\n$result = $card->member_card->activate($info);\n```\n\n### 设置开卡字段\n\n```php\n$card->member_card->setActivationForm($cardId, $settings);\n```\n\n> - `settings` array - 会员卡激活时的选项\n\n示例：\n\n```php\n$cardId = 'pdkJ9uJYAyfLXsUCwI2LdH2Pn1AU';\n\n$settings = [\n    'required_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_MOBILE',\n            'USER_FORM_INFO_FLAG_LOCATION',\n            'USER_FORM_INFO_FLAG_BIRTHDAY',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n    'optional_form' => [\n        'common_field_id_list' => [\n            'USER_FORM_INFO_FLAG_EMAIL',\n        ],\n        'custom_field_list' => [\n            '喜欢的食物',\n        ],\n    ],\n];\n\n$result = $card->member_card->setActivationForm($cardId, $settings);\n```\n\n### 拉取会员信息\n\n```php\n$card->member_card->getUser($cardId, $code);\n```\n\n示例：\n\n```php\n$cardId = 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8';\n$code    = '916679873278';\n\n$result = $card->member_card->getUser($cardId, $code);\n```\n\n### 更新会员信息\n\n```php\n$card->member_card->updateUser($info);\n```\n\n> - `info` array - 可以更新的会员信息\n\n示例：\n\n```php\n$info = [\n    'code'                => '916679873278', //卡券Code码。\n    'card_id'             => 'pbLatjtZ7v1BG_ZnTjbW85GYc_E8', //卡券ID。\n    'record_bonus'        => '消费30元，获得3积分', //商家自定义积分消耗记录，不超过14个汉字。\n    'bonus'               => '100', //需要设置的积分全量值，传入的数值会直接显示，如果同时传入add_bonus和bonus,则前者无效。\n    'balance'             => '持白金会员卡到店消费，可享8折优惠。', //需要设置的余额全量值，传入的数值会直接显示，如果同时传入add_balance和balance,则前者无效。\n    'record_balance'      => '持白金会员卡到店消费，可享8折优惠。', //商家自定义金额消耗记录，不超过14个汉字。\n    'custom_field_value1' => '100', //创建时字段custom_field1定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value2' => '200', //创建时字段custom_field2定义类型的最新数值，限制为4个汉字，12字节。\n    'custom_field_value3' => '300', //创建时字段custom_field3定义类型的最新数值，限制为4个汉字，12字节。\n];\n\n$result = $card->member_card->updateUser($info);\n```\n\n## 子商户\n\n### 添加子商户\n\n```php\n$card->sub_merchant->create(array $attributes); \n```\n\n示例：\n\n```php\n$attributes = [\n    'brand_name' => 'overtrue',\n    'logo_url' => 'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0',\n    'protocol' => 'qIqwTfzAdJ_1-VJFT0fIV53DSY4sZY2WyhkzZzbV498Qgdp-K5HJtZihbHLS0Ys0',\n    'end_time' => '1438990559',\n    'primary_category_id' => 1,\n    'secondary_category_id' => 101,\n    'agreement_media_id' => '',\n    'operator_media_id' => '',\n    'app_id' => '',\n];\n\n$result = $card->sub_merchant->create($attributes);\n```\n\n### 更新子商户\n\n```php\n$card->sub_merchant->update(int $merchantId, array $info);\n```\n\n> - `$merchantId` int - 子商户 ID\n> - `$info` array - 参数与创建子商户参数一样\n\n示例：\n\n```php\n$info = [\n  //...\n];\n$result = $card->sub_merchant->update('12', $info);\n```\n\n## 特殊票券\n\n### 机票值机\n\n```php\n$card->boarding_pass->checkin(array $params);\n```\n\n### 更新会议门票 - 更新用户\n\n```php\n$card->meeting_ticket->updateUser(array $params);\n```\n\n### 更新电影门票 - 更新用户\n\n```php\n$card->movie_ticket->updateUser(array $params);\n```\n\n## JSAPI\n\n### 卡券批量下发到用户\n\n```php\n$cards = [\n    ['card_id' => 'pdkJ9uLRSbnB3UFEjZAgUxAJrjeY', 'outer_id' => 2],\n    ['card_id' => 'pdkJ9uJ37aU-tyRj4_grs8S45k1c', 'outer_id' => 3],\n];\n$json = $card->jssdk->assign($cards); // 返回 json 格式\n```\n\n返回 json，在模板里的用法：\n\n```html\nwx.addCard({ cardList:\n<?= $json ?>, // 需要打开的卡券列表 success: function (res) { var cardList = res.cardList; // 添加的卡券列表信息 } });\n```\n\n### 获取 Ticket\n\n```php\n$card->jssdk->getTicket();\n// 强制刷新\n$card->jssdk->getTicket(true);\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/comment.md",
    "content": "# 评论数据管理\n\n\n\n## 打开已群发文章评论\n\n```php\n$app->comment->open($msgId, $index = null);\n```\n\n## 关闭已群发文章评论\n\n```php\n$app->comment->close($msgId, $index = null);\n```\n\n## 查看指定文章的评论数据\n\n```php\n$app->comment->list(string $msgId, int $index, int $begin, int $count, int $type = 0);\n```\n\n## 将评论标记精选\n\n```php\n$app->comment->markElect(string $msgId, int $index, int $commentId);\n```\n\n## 将评论取消精选\n\n```php\n$app->comment->unmarkElect(string $msgId, int $index, int $commentId);\n```\n\n## 删除评论\n\n```php\n$app->comment->delete(string $msgId, int $index, int $commentId);\n```\n\n## 回复评论\n\n```php\n$app->comment->reply(string $msgId, int $index, int $commentId, string $content);\n```\n\n## 删除回复\n\n```php\n$app->comment->deleteReply(string $msgId, int $index, int $commentId);\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/configuration.md",
    "content": "# 配置\n\n常用的配置参数会比较少，因为除非你有特别的定制，否则基本上默认值就可以了：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    //...\n];\n\n$app = Factory::officialAccount($config);\n```\n\n下面是一个完整的配置样例：\n\n> 不建议你在配置的时候弄这么多，用到啥就配置啥才是最好的，因为大部分用默认值即可。\n\n```php\n<?php\n\nreturn [\n    /**\n     * 账号基本信息，请从微信公众平台/开放平台获取\n     */\n    'app_id'  => 'your-app-id',         // AppID\n    'secret'  => 'your-app-secret',     // AppSecret\n    'token'   => 'your-token',          // Token\n    'aes_key' => '',                    // EncodingAESKey，兼容与安全模式下请一定要填写！！！\n\n     /**\n      * 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n      * 使用自定义类名时，构造函数将会接收一个 `EasyWeChat\\Kernel\\Http\\Response` 实例\n      */\n    'response_type' => 'array',\n\n    /**\n     * 日志配置\n     *\n     * level: 日志级别, 可选为：\n     *         debug/info/notice/warning/error/critical/alert/emergency\n     * path：日志文件位置(绝对路径!!!)，要求可写权限\n     */\n    'log' => [\n        'default' => 'dev', // 默认使用的 channel，生产环境可以改为下面的 prod\n        'channels' => [\n            // 测试环境\n            'dev' => [\n                'driver' => 'single',\n                'path' => '/tmp/easywechat.log',\n                'level' => 'debug',\n            ],\n            // 生产环境\n            'prod' => [\n                'driver' => 'daily',\n                'path' => '/tmp/easywechat.log',\n                'level' => 'info',\n            ],\n        ],\n    ],\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * http://docs.guzzlephp.org/en/stable/request-config.html\n     *\n     * - retries: 重试次数，默认 1，指定当 http 请求失败时重试的次数。\n     * - retry_delay: 重试延迟间隔（单位：ms），默认 500\n     * - log_template: 指定 HTTP 日志模板，请参考：https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php\n     */\n    'http' => [\n        'max_retries' => 1,\n        'retry_delay' => 500,\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n    ],\n\n    /**\n     * OAuth 配置\n     *\n     * scopes：公众平台（snsapi_userinfo / snsapi_base），开放平台：snsapi_login\n     * callback：OAuth授权完成后的回调页地址\n     */\n    'oauth' => [\n        'scopes'   => ['snsapi_userinfo'],\n        'callback' => '/examples/oauth_callback.php',\n    ],\n];\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n\n## 日志配置\n\n你可以配置多个日志的 channel，每个 channel 里的 `driver` 对应不同的日志驱动，内置可用的 `driver` 如下表：\n\n名称 | 描述\n------------- | -------------\n`stack` | 复合型，可以包含下面多种驱动的混合模式\n`single` | 基于 `StreamHandler` 的单一文件日志，参数有 `path`，`level`\n`daily` | 基于 `RotatingFileHandler` 按日期生成日志文件，参数有 `path`，`level`，`days`(默认 7 天)\n`slack` | 基于 `SlackWebhookHandler` 的 Slack 组件，参数请参考源码：[LogManager.php](https://github.com/overtrue/wechat/blob/master/src/Kernel/Log/LogManager.php#L247)\n`syslog` | 基于 `SyslogHandler` Monolog 驱动，参数有 `facility` 默认为 `LOG_USER`，`level`\n`errorlog` | 记录日志到系统错误日志，基于 `ErrorLogHandler`，参数有 `type`，默认为 `ErrorLogHandler::OPERATING_SYSTEM`\n\n### 自定义日志驱动\n\n由于日志使用的是 [Monolog](https://github.com/Seldaek/monolog)，所以，除了默认的文件式日志外，你可以自定义日志处理器：\n\n```php\nuse Monolog\\Logger;\nuse Monolog\\Handler\\RotatingFileHandler;\n\n\n// 注册自定义日志\n$app->logger->extend('mylog', function($app, $config){\n    return new Logger($this->parseChannel($config), [\n        $this->prepareHandler(new RotatingFileHandler(\n            $config['path'], $config['days'], $this->level($config)\n        )),\n    ]);\n});\n```\n\n>  在你自定义的闭包函数中，可以使用 `EasyWeChat\\Kernel\\Log\\LogManager` 中的方法，具体请查看 SDK 源代码。\n\n配置文件中在 `driver` 部分即可使用你自定义的驱动了：\n\n```php\n'log' => [\n    'default' => 'dev', // 默认使用的 channel，生产环境可以改为下面的 prod\n    'channels' => [\n        // 测试环境\n        'dev' => [\n            'driver' => 'mylog',\n            'path' => '/tmp/easywechat.log',\n            'level' => 'debug',\n            'days' => 5,\n        ],\n\n        //...\n    ],\n],\n```\n\n"
  },
  {
    "path": "docs/src/5.x/official-account/customer_service.md",
    "content": "# 客服\n\n使用客服系统可以向用户发送消息以及群发消息，客服的管理等功能。\n\n## 客服管理\n\n### 获取所有客服\n\n```php\n$app->customer_service->list();\n```\n\n### 获取所有在线的客服\n\n```php\n$app->customer_service->online();\n```\n\n### 添加客服\n\n```php\n$app->customer_service->create('foo@test', '客服1');\n```\n\n### 修改客服\n\n```php\n$app->customer_service->update('foo@test', '客服1');\n```\n\n### 删除账号\n\n```php\n$app->customer_service->delete('foo@test');\n```\n\n### 设置客服头像\n\n```php\n$app->customer_service->setAvatar('foo@test', $avatarPath); // $avatarPath 为本地图片路径，非 URL\n```\n\n### 获取客服与客户聊天记录\n\n```php\n$app->customer_service->messages($startTime, $endTime, $msgId = 1, $number = 10000);\n```\n\n示例:\n\n```php\n$records = $app->customer_service->messages('2015-06-07', '2015-06-21', 1, 20000);\n```\n\n### 主动发送消息给用户\n\n```php\n$app->customer_service->message($message)->to($openId)->send();\n```\n\n> `$message` 为消息对象或文本，请参考：[消息](messages)\n\n示例：\n\n```php\n$app->customer_service->message('hello')\n                  >  ->to('oV-gpwdOIwSI958m9osAhGBFxxxx')\n                  >  ->send();\n```\n\n### 指定客服发送消息\n\n```php\n$app->customer_service->message($message)\n                      >  ->from('account@test')\n                      >  ->to($openId)\n                      >  ->send();\n```\n> `$message` 为消息对象或文本，请参考：[消息](messages.html)\n\n示例：\n\n```php\n$app->customer_service->message('hello')\n                  >  ->from('kf2001@gh_176331xxxx')\n                  >  ->to('oV-gpwdOIwSI958m9osAhGBFxxxx')\n                  >  ->send();\n```\n\n### 邀请微信用户加入客服\n\n以账号 `foo@test` 邀请 微信号 为 `xxxx` 的微信用户加入客服。\n\n```php\n$app->customer_service->invite('foo@test', 'xxxx');\n```\n\n## 客服会话控制\n\n## 创建会话\n\n```php\n$app->customer_service_session->create('test1@test', 'OPENID');\n```\n\n### 关闭会话\n\n```php\n$app->customer_service_session->close('test1@test', 'OPENID');\n```\n\n### 获取客户会话状态\n\n```php\n$app->customer_service_session->get('OPENID');\n```\n\n### 获取客服会话列表\n\n```php\n$app->customer_service_session->list('test1@test');\n```\n\n### 获取未接入会话列表\n\n```php\n$app->customer_service_session->waiting();\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/data_cube.md",
    "content": "# 数据统计与分析\n\n通过数据接口，开发者可以获取与公众平台官网统计模块类似但更灵活的数据，还可根据需要进行高级处理。\n\n>\n> 1. 接口侧的公众号数据的数据库中仅存储了 **2014年12月1日之后**的数据，将查询不到在此之前的日期，即使有查到，也是不可信的脏数据；\n> 2. 请开发者在调用接口获取数据后，将数据保存在自身数据库中，即加快下次用户的访问速度，也降低了微信侧接口调用的不必要损耗。\n> 3. 额外注意，获取图文群发每日数据接口的结果中，只有**中间页阅读人数+原文页阅读人数+分享转发人数+分享转发次数+收藏次数 >=3** 的结果才会得到统计，过小的阅读量的图文消息无法统计。\n\n## 示例\n\n```php\n$userSummary = $app->data_cube->userSummary('2014-12-07', '2014-12-08');\n\nvar_dump($userSummary);\n//\n//[\n//    {\n//        \"ref_date\": \"2014-12-07\",\n//        \"user_source\": 0,\n//        \"new_user\": 0,\n//        \"cancel_user\": 0\n//    }\n//    //后续还有ref_date在begin_date和end_date之间的数据\n// ]\n\n```\n\n## API\n\n    $from   示例： `2014-02-13` 获取数据的起始日期\n    $to     示例： `2014-02-18` 获取数据的结束日期，`$to`允许设置的最大值为昨日\n\n    `$from` 和 `$to` 的差值需小于 “最大时间跨度”（比如最大时间跨度为 1 时，`$from` 和 `$to` 的差值只能为 0，才能小于 1 ），否则会报错\n\n+ `array userSummary(string $from, string $to)` 获取用户增减数据, 最大时间跨度：**7**;\n+ `array userCumulate(string $from, string $to)` 获取累计用户数据, 最大时间跨度：**7**;\n+ `array articleSummary(string $from, string $to)` 获取图文群发每日数据, 最大时间跨度：**1**;\n+ `array articleTotal(string $from, string $to)` 获取图文群发总数据, 最大时间跨度：**1**;\n+ `array userReadSummary(string $from, string $to)` 获取图文统计数据, 最大时间跨度：**3**;\n+ `array userReadHourly(string $from, string $to)` 获取图文统计分时数据, 最大时间跨度：**1**;\n+ `array userShareSummary(string $from, string $to)` 获取图文分享转发数据, 最大时间跨度：**7**;\n+ `array userShareHourly(string $from, string $to)` 获取图文分享转发分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageSummary(string $from, string $to)` 获取消息发送概况数据, 最大时间跨度：**7**;\n+ `array upstreamMessageHourly(string $from, string $to)` 获取消息发送分时数据, 最大时间跨度：**1**;\n+ `array upstreamMessageWeekly(string $from, string $to)` 获取消息发送周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageMonthly(string $from, string $to)` 获取消息发送月数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistSummary(string $from, string $to)` 获取消息发送分布数据, 最大时间跨度：**15**;\n+ `array upstreamMessageDistWeekly(string $from, string $to)` 获取消息发送分布周数据, 最大时间跨度：**30**;\n+ `array upstreamMessageDistMonthly(string $from, string $to)` 获取消息发送分布月数据, 最大时间跨度：**30**;\n+ `array interfaceSummary(string $from, string $to)` 获取接口分析数据, 最大时间跨度：**30**;\n+ `array interfaceSummaryHourly(string $from, string $to)` 获取接口分析分时数据, 最大时间跨度：**1**;\n+ `array cardSummary(string $from, string $to, int $condSource = 0)` 获取普通卡券分析分时数据, 最大时间跨度：**1**;\n+ `array freeCardSummary(string $from, string $to, int $condSource = 0, string $cardId = '')` 获取免费券分析分时数据, 最大时间跨度：**1**;\n+ `array memberCardSummary(string $from, string $to, int $condSource = 0)` 获取会员卡分析分时数据, 最大时间跨度：**1**;\n"
  },
  {
    "path": "docs/src/5.x/official-account/draft.md",
    "content": "# 草稿箱\n\n草稿箱提供了公众号文章的草稿管理功能，你可以通过草稿箱 API 添加、获取或删除草稿。\n\n### 添加草稿\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Article;\n\n// 添加单篇图文草稿\n$article = new Article([\n    'title' => 'xxx',\n    'thumb_media_id' => $mediaId,\n    'author' => 'overtrue',\n    'show_cover' => 1,\n    'digest' => '文章摘要',\n    'content' => '文章内容',\n    'source_url' => 'https://www.easywechat.com',\n    //...\n]);\n$app->draft->add($article);\n\n// 添加多篇图文草稿\n$app->draft->add([$article, $article2, ...]);\n```\n\n### 获取草稿\n\n```php\n$app->draft->get($mediaId);\n```\n\n### 删除草稿\n\n```php\n$app->draft->delete($mediaId);\n```\n\n### 更新草稿\n\n有三个参数：\n\n> - `$mediaId` 要更新的草稿的 `mediaId`\n> - `$article` 文章内容，`Article` 实例或者全字段数组\n> - `$index` 要更新的文章在图文消息中的位置（多图文消息时，此字段才有意义，单图片忽略此参数），第一篇为 0\n\n```php\n$result = $app->draft->update($mediaId, new Article([\n    'title' => 'EasyWeChat 5.x 发布了！',\n    'thumb_media_id' => 'qQFxUQGO21Li4YrSn3MhnrqtRp9Zi3cbM9uBsepvDmE', // 封面图片 mediaId\n    'author' => 'overtrue', // 作者\n    'show_cover' => 1, // 是否在文章内容显示封面图片\n    'digest' => '这里是文章摘要',\n    'content' => '这里是文章内容，你可以放很长的内容',\n    'source_url' => 'https://easywechat.com',\n]));\n\n// 指定更新多图文中的第 2 篇\n$result = $app->draft->update($mediaId, new Article([...]), 1); // 第 2 篇\n```\n\n### 获取草稿总数\n\n```php\n$app->draft->count();\n```\n\n### 获取草稿列表\n\n```php\n$app->draft->batchGet($offset, $count, $noContent = 0);\n```\n\n> - `$offset` - 从全部素材的该偏移位置开始返回，可选，默认 `0`，0 表示从第一个素材返回\n> - `$count` - 返回素材的数量，可选，默认 `20`，取值在 1 到 20 之间\n> - `$noContent` - 1 表示不返回 content 字段，0 表示正常返回，默认为 0"
  },
  {
    "path": "docs/src/5.x/official-account/events.md",
    "content": "# 事件\n\n\n\n更多请参考：[服务端](server.html)\n\n关于事件类型请参考微信官方文档：http://mp.weixin.qq.com/wiki/\n"
  },
  {
    "path": "docs/src/5.x/official-account/goods.md",
    "content": "# 返佣商品\n\n> 微信文档：https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&key=11533749572M9ODP&version=1&lang=zh_CN&platform=2\n\n## 导入商品\n\n每次调用支持批量导入不超过1000条的商品信息。每分钟单个商户全局调用次数不得超过200次。每天调用次数不得超过100万次。每次请求包大小不超过2M。\n\n```php\n$data = [\n    [\n        'pid' => 'pid001',\n        'image_info' => [\n            'main_image_list' => [\n                [\n                    'url' => 'http://www.google.com/a.jpg',\n                ],\n                [\n                    'url' => 'http://www.google.com/b.jpg',\n                ],\n            ],\n        ],\n        \n        //...\n    ],\n    \n    //...\n];\n\n$result = $app->goods->add($data);\n\n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"status_ticket\": \"115141102647330200\"\n//}\n```\n\n`status_ticket` 用于获取此次导入的详细结果。\n\n## 更新商品\n\n更新时，字段不填代表不更新该字段（此处的字段不填，代表无此字段，而不是把字段的值设为空，设为空即代表更新该字段为空）。\n\n对于字符串类型的选填字段，如副标题，若清空不展示，则可设置为空；对于数字类型的选填字段，如原价，若清空不展示，则需设置为0。\n\n> 基本字段更新中 `pid` 为必填字段，且无法修改\n\n```php\n$data = [\n    [\n        'pid' => 'pid001',\n        'image_info' => [\n            'main_image_list' => [\n                [\n                    'url' => 'http://www.baidu.com/c.jpg',\n                ],\n                [\n                    'url' => 'http://www.baidu.com/d.jpg',\n                ],\n            ],\n        ],\n        \n        //...\n    ],\n    \n    //...\n];\n \n$result = $app->goods->update($data);\n \n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"status_ticket\": \"115141102647330200\"\n//}\n```\n\n> 说明：导入商品和更新商品使用的是同一个接口。\n \n## 查询导入/更新商品状态\n \n用于查询导入或更新商品的结果，当导入或更新商品失败时，若为系统错误可进行重试；若为其他错误，请排查解决后进行重试。\n\n```php\n$status_ticket = '115141102647330200';\n\n$result = $app->goods->status($status_ticket);\n\n// $result:\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"result\": {\n//        \"succ_cnt\": 2,\n//        \"fail_cnt\": 0,\n//        \"total_cnt\": 2,\n//        \"progress\": \"100.00%\",\n//        \"statuses\": [\n//            {\n//                \"pid\": \"pid001\",\n//                \"ret\": 0,\n//                \"err_msg\": \"success\",\n//                \"err_msg_zh_cn\": \"成功\"\n//            },\n//            {\n//                \"pid\": \"pid002\",\n//                \"ret\": 0,\n//                \"err_msg\": \"success\",\n//                \"err_msg_zh_cn\": \"成功\"\n//            }\n//        ]\n//    }\n//}\n```\n\n## 获取单个商品信息\n\n使用该接口获取已导入的商品信息，供验证信息及抽查导入情况使用。\n\n```php\n$pid = 'pid001';\n\n$app->goods->get($pid);\n```\n\n> 返回结果中的 `product` 字段内容与 `导入商品接口` 字段一致，导入时未设置的值有可能获取时仍会返回，但显示为空\n\n## 分页获取商品信息\n\n使用该接口可获取已导入的全量商品信息，供全量验证信息使用。\n\n```php\n$context = '';  // page 为 1 时传空即可。当 page 大于 1 时必填，填入上一次访问本接口返回的 page_context。\n$page = 1;      // 页码\n$size = 10;     // 每页数据大小，目前限制为100以内，注意一次全量验证过程中该参数的值需保持不变\n\n$app->goods->list($context, $page, $size);\n```\n\n> 返回结果中的 `product` 字段内容与 `导入商品接口` 字段一致，导入时未设置的值有可能获取时仍会返回，但显示为空。\n> `page_context` 字段用于获取下一页数据时使用。\n"
  },
  {
    "path": "docs/src/5.x/official-account/index.md",
    "content": "## 公众号\n\n公众号的各模块相对比较统一，用法如下：\n\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n    \n    //...\n];\n\n$app = Factory::officialAccount($config);\n```\n\n`$app` 在所有相关公众号的文档都是指 `Factory::officialAccount` 得到的实例，就不在每个页面单独写了。\n"
  },
  {
    "path": "docs/src/5.x/official-account/material.md",
    "content": "# 素材管理\n\n在微信里的图片，音乐，视频等等都需要先上传到微信服务器作为素材才可以在消息中使用。\n\n### 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$result = $app->material->uploadImage(\"/path/to/your/image.jpg\");\n// {\n//    \"media_id\":MEDIA_ID,\n//    \"url\":URL\n// }\n```\n\n> `url` 只有上传图片素材有返回值。\n\n### 上传语音\n\n语音 **大小不超过 5M**，**长度不超过 60 秒**，支持 `mp3/wma/wav/amr` 格式。\n\n```php\n$result = $app->material->uploadVoice(\"/path/to/your/voice.mp3\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传视频\n\n```php\n$result = $app->material->uploadVideo(\"/path/to/your/video.mp4\", \"视频标题\", \"视频描述\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传缩略图\n\n用于视频封面或者音乐封面。\n\n```php\n$result = $app->material->uploadThumb(\"/path/to/your/thumb.jpg\");\n// {\n//    \"media_id\":MEDIA_ID,\n// }\n```\n\n### 上传图文消息\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Article;\n\n// 上传单篇图文\n$article = new Article([\n    'title' => 'xxx',\n    'thumb_media_id' => $mediaId,\n    //...\n  ]);\n$app->material->uploadArticle($article);\n\n// 或者多篇图文\n$app->material->uploadArticle([$article, $article2, ...]);\n```\n\n### 修改图文消息\n\n有三个参数：\n\n> - `$mediaId` 要更新的文章的 `mediaId`\n> - `$article` 文章内容，`Article` 实例或者 全字段数组\n> - `$index` 要更新的文章在图文消息中的位置（多图文消息时，此字段才有意义，单图片忽略此参数），第一篇为 0；\n\n```php\n$result = $app->material->updateArticle($mediaId, new Article(...));\n\n// or\n\n$result = $app->material->updateArticle($mediaId, [\n   'title' => 'EasyWeChat 4.0 发布了！',\n    'thumb_media_id' => 'qQFxUQGO21Li4YrSn3MhnrqtRp9Zi3cbM9uBsepvDmE', // 封面图片 mediaId\n    'author' => 'overtrue', // 作者\n    'show_cover' => 1, // 是否在文章内容显示封面图片\n    'digest' => '这里是文章摘要',\n    'content' => '这里是文章内容，你可以放很长的内容',\n    'source_url' => 'https://easywechat.com',\n  ]);\n\n// 指定更新多图文中的第 2 篇\n$result = $app->material->updateArticle($mediaId, new Article(...), 1); // 第 2 篇\n```\n\n### 上传图文消息图片\n\n返回值中 url 就是上传图片的 URL，可用于后续群发中，放置到图文消息中。\n\n```php\n$result = $app->material->uploadArticleImage($path);\n//{\n//    \"url\":  \"http://mmbiz.qpic.cn/mmbiz/gLO17UPS6FS2xsypf378iaNhWacZ1G1UplZYWEYfwvuU6Ont96b1roYsCNFwaRrSaKTPCUdBK9DgEHicsKwWCBRQ/0\"\n//}\n```\n\n### 获取永久素材\n\n```php\n$resource = $app->material->get($mediaId);\n```\n\n如果请求的素材为图文消息，则响应如下：\n\n```json\n{\n \"news_item\": [\n       {\n       \"title\":TITLE,\n       \"thumb_media_id\"::THUMB_MEDIA_ID,\n       \"show_cover_pic\":SHOW_COVER_PIC(0/1),\n       \"author\":AUTHOR,\n       \"digest\":DIGEST,\n       \"content\":CONTENT,\n       \"url\":URL,\n       \"content_source_url\":CONTENT_SOURCE_URL\n       },\n       //多图文消息有多篇文章\n    ]\n  }\n```\n\n如果返回的是视频消息素材，则内容如下：\n\n```json\n{\n  \"title\": TITLE,\n  \"description\": DESCRIPTION,\n  \"down_url\": DOWN_URL\n}\n```\n\n其他类型的素材消息，则响应为 `EasyWeChat\\Kernel\\Http\\StreamResponse` 实例，开发者可以自行保存为文件。例如\n\n```php\n$stream = $app->material->get($mediaId);\n\nif ($stream instanceof \\EasyWeChat\\Kernel\\Http\\StreamResponse) {\n    // 以内容 md5 为文件名\n    $stream->save('保存目录');\n\n    // 自定义文件名，不需要带后缀\n    $stream->saveAs('保存目录', '文件名');\n}\n```\n\n### 获取永久素材列表\n\n> - `$type` 素材的类型，图片（`image`）、视频（`video`）、语音 （`voice`）、图文（`news`）\n> - `$offset` 从全部素材的该偏移位置开始返回，可选，默认 `0`，0 表示从第一个素材 返回\n> - `$count` 返回素材的数量，可选，默认 `20`, 取值在 1 到 20 之间\n\n```php\n$app->material->list($type, $offset, $count);\n```\n\n示例：\n\n```php\n$list = $app->material->list('image', 0, 10);\n```\n\n图片、语音、视频 等类型的返回如下\n\n```json\n{\n  \"total_count\": TOTAL_COUNT,\n  \"item_count\": ITEM_COUNT,\n  \"item\": [\n    {\n      \"media_id\": MEDIA_ID,\n      \"name\": NAME,\n      \"update_time\": UPDATE_TIME,\n      \"url\": URL\n    }\n    //可能会有多个素材\n  ]\n}\n```\n\n永久图文消息素材列表的响应如下：\n\n```json\n{\n  \"total_count\": TOTAL_COUNT,\n  \"item_count\": ITEM_COUNT,\n  \"item\": [\n    {\n      \"media_id\": MEDIA_ID,\n      \"content\": {\n        \"news_item\": [\n          {\n            \"title\": TITLE,\n            \"thumb_media_id\": THUMB_MEDIA_ID,\n            \"show_cover_pic\": SHOW_COVER_PIC(0 / 1),\n            \"author\": AUTHOR,\n            \"digest\": DIGEST,\n            \"content\": CONTENT,\n            \"url\": URL,\n            \"content_source_url\": CONTETN_SOURCE_URL\n          }\n          //多图文消息会在此处有多篇文章\n        ]\n      },\n      \"update_time\": UPDATE_TIME\n    }\n    //可能有多个图文消息item结构\n  ]\n}\n```\n\n### 获取素材计数\n\n```php\n$stats = $app->material->stats();\n\n// {\n//   \"voice_count\":COUNT,\n//   \"video_count\":COUNT,\n//   \"image_count\":COUNT,\n//   \"news_count\":COUNT\n// }\n```\n\n### 删除永久素材\n\n```php\n$app->material->delete($mediaId);\n```\n\n### 文章预览\n\n文章预览请参阅 “消息群发” 章节。\n"
  },
  {
    "path": "docs/src/5.x/official-account/menu.md",
    "content": "# 自定义菜单\n\n## 读取（查询）已设置菜单\n\n\n```php\n$list = $app->menu->list();\n```\n\n## 获取当前菜单\n\n```php\n$current = $app->menu->current();\n```\n\n## 添加菜单\n\n### 添加普通菜单\n\n```php\n$buttons = [\n    [\n        \"type\" => \"click\",\n        \"name\" => \"今日歌曲\",\n        \"key\"  => \"V1001_TODAY_MUSIC\"\n    ],\n    [\n        \"name\"       => \"菜单\",\n        \"sub_button\" => [\n            [\n                \"type\" => \"view\",\n                \"name\" => \"搜索\",\n                \"url\"  => \"http://www.soso.com/\"\n            ],\n            [\n                \"type\" => \"view\",\n                \"name\" => \"视频\",\n                \"url\"  => \"http://v.qq.com/\"\n            ],\n            [\n                \"type\" => \"click\",\n                \"name\" => \"赞一下我们\",\n                \"key\" => \"V1001_GOOD\"\n            ],\n        ],\n    ],\n];\n$app->menu->create($buttons);\n```\n\n以上将会创建一个普通菜单。\n\n### 添加个性化菜单\n\n与创建普通菜单不同的是，需要在 `create()` 方法中将个性化匹配规则作为第二个参数传进去：\n\n```php\n$buttons = [\n    // ...\n];\n$matchRule = [\n    \"tag_id\" => \"2\",\n    \"sex\" => \"1\",\n    \"country\" => \"中国\",\n    \"province\" => \"广东\",\n    \"city\" => \"广州\",\n    \"client_platform_type\" => \"2\",\n    \"language\" => \"zh_CN\"\n];\n$app->menu->create($buttons, $matchRule);\n```\n\n## 删除菜单\n\n有两种删除方式，一种是**全部删除**，另外一种是**根据菜单 ID 来删除**(删除个性化菜单时用，ID 从查询接口获取)：\n\n```php\n$app->menu->delete(); // 全部\n$app->menu->delete($menuId);\n```\n\n## 测试个性化菜单\n\n```php\n$app->menu->match($userId);\n```\n\n> `$userId` 可以是粉丝的 OpenID，也可以是粉丝的微信号。\n\n返回 `$menu` 与指定的 `$userId` 匹配的菜单项。\n"
  },
  {
    "path": "docs/src/5.x/official-account/message-transfer.md",
    "content": "# 多客服消息转发\n\n多客服的消息转发绝对是超级的简单，转发的消息类型为 `transfer`：\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Transfer;\n\n// 转发收到的消息给客服\n$app->server->push(function($message) {\n  return new Transfer();\n});\n\n$response = $app->server->serve();\n```\n\n当然，你也可以指定转发给某一个客服：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Transfer;\n\n$app->server->push(function($message) {\n    return new Transfer($account);\n});\n```"
  },
  {
    "path": "docs/src/5.x/official-account/messages.md",
    "content": "# 消息\n\n我把微信的 API 里的所有“消息”都按类型抽象出来了，也就是说，你不用区分它是回复消息还是主动推送消息，免去了你去手动拼装微信的 XML 以及乱七八糟命名不统一的 JSON 了。\n\n在阅读以下内容时请忽略是 **接收消息** 还是 **回复消息**，后面我会给你讲它们的区别。\n\n## 消息类型\n\n消息分为以下几种：`文本`、`图片`、`视频`、`声音`、`链接`、`坐标`、`图文`、`文章` 和一种特殊的 `原始消息`。\n\n另外还有一种特殊的消息类型：**素材消息**，用于群发或者客服时发送已有素材用。\n\n> 注意：回复消息与客服消息里的图文类型为：**图文**，群发与素材中的图文为**文章**\n\n所有的消息类都在 `EasyWeChat\\Kernel\\Messages` 这个命名空间下， 下面我们来分开讲解：\n\n### 文本消息\n\n属性列表：\n\n> - `content` 文本内容\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Text;\n\n$text = new Text('您好！overtrue。');\n\n// or\n$text = new Text();\n$text->content = '您好！overtrue。';\n\n// or\n$text = new Text();\n$text->setAttribute('content', '您好！overtrue。');\n```\n\n### 图片消息\n\n属性列表：\n\n```\n- media_id 媒体资源 ID\n```\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Image;\n\n$image = new Image($mediaId);\n```\n\n### 视频消息\n\n属性列表：\n\n> - `title` 标题\n> - `description` 描述\n> - `media_id` 媒体资源 ID\n> - `thumb_media_id` 封面资源 ID\n\n```php\n\nuse EasyWeChat\\Kernel\\Messages\\Video;\n\n$video = new Video($mediaId, [\n        'title' => $title,\n        'description' => '...',\n    ]);\n```\n\n### 声音消息\n\n属性列表：\n\n> - `media_id` 媒体资源 ID\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Voice;\n\n$voice = new Voice($mediaId);\n```\n\n### 链接消息\n\n> 复链接消息\n\n### 坐标消息\n\n> 复坐标消息\n\n### 图文消息\n\n图文消息分为 `NewsItem` 与 `News`，`NewsItem` 为图文内容条目。\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n`NewsItem` 属性：\n\n> - `title` 标题\n> - `description` 描述\n> - `image` 图片链接\n> - `url` 链接 URL\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\News;\nuse EasyWeChat\\Kernel\\Messages\\NewsItem;\n\n$items = [\n    new NewsItem([\n        'title'       => $title,\n        'description' => '...',\n        'url'         => $url,\n        'image'       => $image,\n        // ...\n    ]),\n];\n$news = new News($items);\n```\n\n### 文章\n\n属性列表：\n\n> - `title` 标题\n> - `author` 作者\n> - `content` 具体内容\n> - `thumb_media_id` 图文消息的封面图片素材 id（必须是永久 mediaID）\n> - `digest` 图文消息的摘要，仅有单图文消息才有摘要，多图文此处为空\n> - `source_url` 来源 URL\n> - `show_cover` 是否显示封面，0 为 false，即不显示，1 为 true，即显示\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Article;\n\n$article = new Article([\n        'title'   => 'EasyWeChat',\n        'author'  => 'overtrue',\n        'content' => 'EasyWeChat 是一个开源的微信 SDK，它... ...',\n        // ...\n    ]);\n\n// or\n$article = new Article();\n$article->title   = 'EasyWeChat';\n$article->author  = 'overtrue';\n$article->content = '微信 SDK ...';\n// ...\n```\n\n### 素材消息\n\n素材消息用于群发与客服消息时使用。\n\n> 素材消息不支持被动回复，如需被动回复素材消息，首先组装后，再 News 方法返回。\n\n属性就一个：`media_id`。\n\n在构造时有两个参数：\n\n> - `$type` 素材类型，目前只支持：`mpnews`、 `mpvideo`、`voice`、`image` 等。\n> - `$mediaId` 素材 ID，从接口查询或者上传后得到。\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Media;\n\n$media = new Media($mediaId, 'mpnews');\n```\n\n以上呢，是所有微信支持的基本消息类型。\n\n> 需要注意的是，你不需要关心微信的消息字段叫啥，因为这里我们使用了更标准的命名，然后最终在中间做了转换，所以你不需要关注。\n\n### 原始消息\n\n原始消息是一种特殊的消息，它的场景是：**你不想使用其它消息类型，你想自己手动拼消息**。比如，回复消息时，你想自己拼 XML，那么你就直接用它就可以了：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Raw;\n\n$message = new Raw('<xml>\n<ToUserName><![CDATA[toUser]]></ToUserName>\n<FromUserName><![CDATA[fromUser]]></FromUserName>\n<CreateTime>12345678</CreateTime>\n<MsgType><![CDATA[image]]></MsgType>\n<Image>\n<MediaId><![CDATA[media_id]]></MediaId>\n</Image>\n</xml>');\n```\n\n比如，你要用于客服消息(客服消息是 JSON 结构)：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Raw;\n\n$message = new Raw('{\n    \"touser\":\"OPENID\",\n    \"msgtype\":\"text\",\n    \"text\":\n    {\n         \"content\":\"Hello World\"\n    }\n}');\n```\n\n总之，就是直接写微信接口要求的格式内容就好，此类型消息在 SDK 中不存在转换行为，所以请注意不要写错格式。\n\n## 在 SDK 中使用消息\n\n### 在服务端回复消息\n\n在 [服务端](server) 一节中，我们讲了回复消息的写法：\n\n```php\n// ... 前面部分省略\n$app->server->push(function ($message) {\n    return \"您好！欢迎关注我!\";\n});\n\n$response = $server->serve();\n```\n\n上面 `return` 了一句普通的文本内容，这里只是为了方便大家，实际上最后会有一个隐式转换为 `Text` 类型的动作。\n\n如果你要回复其它类型的消息，就需要返回一个具体的实例了，比如回复一个图片类型的消息：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Image;\n// ...\n$app->server->push(function ($message) {\n    return new Image('media-id');\n});\n// ...\n```\n\n#### 回复多图文消息\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n多图文消息其实就是单图文消息的一个数组而已了：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\News;\nuse EasyWeChat\\Kernel\\Messages\\NewsItem;\n\n// ...\n$app->server->push(function ($message) {\n   $news = new NewsItem(...);\n   return new News([$news]);\n});\n// ...\n```\n\n### 作为客服消息发送\n\n在客服消息里的使用也一样，都是直接传入消息实例即可：\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Text;\n\n$message = new Text('Hello world!');\n\n$result = $app->customer_service->message($message)->to($openId)->send();\n//...\n```\n\n#### 发送多图文消息\n\n> ，被动回复消息与客服消息接口的图文消息类型中图文数目只能为一条](https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=115383153198yAvN&version=&lang=zh_CN&token=)\n\n多图文消息其实就是单图文消息组成的一个 News 对象而已：\n\n```php\n$news1 = new NewsItem(...);\n$news = new News([$news1]);\n\n$app->customer_service->message($news)->to($openId)->send();\n```\n\n### 群发消息\n\n请参考：[群发消息](broadcasting)\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer)\n"
  },
  {
    "path": "docs/src/5.x/official-account/oauth.md",
    "content": "# 网页授权\n\n## 关于 OAuth2.0\n\nOAuth 是一个关于授权（authorization）的开放网络标准，在全世界得到广泛应用，目前的版本是 2.0 版。\n\n<img src=\"https://user-images.githubusercontent.com/1472352/29310178-5a7a91cc-81df-11e7-9468-b66e150bfba1.png\" alt=\"\" style=\"max-width: 500px\">\n\n> 摘自：[RFC 6749](https://datatracker.ietf.org/doc/rfc6749/?include_text=1)\n\n步骤解释：\n\n    （A）用户打开客户端以后，客户端要求用户给予授权。\n    （B）用户同意给予客户端授权。\n    （C）客户端使用上一步获得的授权，向认证服务器申请令牌。\n    （D）认证服务器对客户端进行认证以后，确认无误，同意发放令牌。\n    （E）客户端使用令牌，向资源服务器申请获取资源。\n    （F）资源服务器确认令牌无误，同意向客户端开放资源。\n\n关于 OAuth 协议我们就简单了解到这里，如果还有不熟悉的同学，请 [Google 相关资料](https://www.google.com.hk/?gws_rd=ssl#safe=strict&q=OAuth2)\n\n## 微信 OAuth\n\n在微信里的 OAuth 其实有两种：[公众平台网页授权获取用户信息](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html)、[开放平台网页登录](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN)。\n\n它们的区别有两处，授权地址不同，`scope` 不同。\n\n> - **公众平台网页授权获取用户信息**\n\n    **授权 URL**: `https://open.weixin.qq.com/connect/oauth2/authorize`\n    **Scopes**: `snsapi_base` 与 `snsapi_userinfo`\n\n> - **开放平台网页登录**\n\n    **授权 URL**: `https://open.weixin.qq.com/connect/qrconnect`\n    **Scopes**: `snsapi_login`\n\n他们的逻辑都一样：\n\n1. 用户尝试访问一个我们的业务页面，例如: `/user/profile`\n2. 如果用户已经登录，则正常显示该页面\n3. 系统检查当前访问的用户并未登录（从 session 或者其它方式检查），则跳转到**跳转到微信授权服务器**（上面的两种中一种**授权 URL** ），并告知微信授权服务器我的**回调 URL（redirect_uri=callback.php)**，此时用户看到蓝色的授权确认页面（`scope` 为 `snsapi_base` 时不显示）\n4. 用户点击确定完成授权，浏览器跳转到**回调 URL**: `callback.php` 并带上 `code`： `?code=CODE&state=STATE`。\n5. 在 `callback.php` 中得到 `code` 后，通过 `code` 再次向微信服务器请求得到 **网页授权 access_token** 与 `openid`\n6. 你可以选择拿 `openid` 去请求 API 得到用户信息（可选）\n7. 将用户信息写入 SESSION。\n8. 跳转到第 3 步写入的 `target_url` 页面（`/user/profile`）。\n\n> 看懵了？没事，使用 SDK，你不用管这么多。:smile:\n>\n> 注意，上面的第 3 步：redirect_uri=callback.php 实际上我们会在 `callback.php` 后面还会带上授权目标页面 `user/profile`，所以完整的 `redirect_uri` 应该是下面的这样的 PHP 去拼出来：`'redirect_uri='.urlencode('callback.php?target=user/profile')`\n> 结果：redirect_uri=callback.php%3Ftarget%3Duser%2Fprofile\n\n## 逻辑组成\n\n从上面我们所描述的授权流程来看，我们至少有 3 个页面：\n\n1. **业务页面**，也就是需要授权才能访问的页面。\n2. **发起授权页**，此页面其实可以省略，可以做成一个中间件，全局检查未登录就发起授权。\n3. **授权回调页**，接收用户授权后的状态，并获取用户信息，写入用户会话状态（SESSION）。\n\n## 开始之前\n\n在开始之前请一定要记住，先登录公众号后台，找到**边栏 “开发”** 模块下的 **“接口权限”**，点击 **“网页授权获取用户基本信息”** 后面的修改，添加你的网页授权域名。\n\n> 如果你的授权地址为：`http://www.abc.com/xxxxx`，那么请填写 `www.abc.com`，也就是说请填写与网址匹配的域名，前者如果填写 `abc.com` 是通过不了的。\n\n## SDK 中 OAuth 模块的 API\n\n在 SDK 中，我们使用名称为 `oauth` 的模块来完成授权服务，我们主要用到以下两个 API：\n\n### 发起授权\n\n```php\n// $redirectUrl 为跳转目标，请自行 302 跳转到目标地址\n$redirectUrl = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->redirect();\n```\n\n当然你也可以在发起授权的时候指定回调 URL，比如设置回调 URL 为当前页面：\n\n```php\n$redirectUrl = $app->oauth->scopes(['snsapi_userinfo'])\n                          ->redirect($request->fullUrl());\n```\n\n它的返回值 `$redirectUrl` 是一个字符串跳转地址，请自行使用框架的跳转方法实现跳转，PHP 原生写法：\n\n```php\nheader(\"Location: {$redirectUrl}\");\n```\n\n在 [Laravel](http://laravel.com) 框架中控制器方法是要求返回响应值的，那么你就直接:\n\n```php\nreturn \\redirect($redirectUrl);\n```\n\n### 获取已授权用户\n\n```php\n$code = \"微信回调URL携带的 code\";\n$user = $app->oauth->userFromCode($code);\n```\n\n返回的 `$user` 是 [Overtrue\\Socialite\\User](https://github.com/overtrue/socialite/blob/master/src/User.php) 对象，你可以从该对象拿到[更多的信息](https://github.com/overtrue/socialite#user-interface)。\n\n#### $user 可以用的方法:\n\n- `$user->getId(); ` 对应微信的 `openid`\n- `$user->getNickname(); ` 对应微信的 `nickname`\n- `$user->getName(); ` 对应微信的 `nickname`\n- `$user->getAvatar(); ` 头像地址\n- `$user->getRaw(); ` 原始 API 返回的结果\n- `$user->getAccessToken(); ` `access_token`\n- `$user->getRefreshToken(); ` `refresh_token`\n- `$user->getExpiresIn(); ` `expires_in`，Access Token 过期时间\n- `$user->getTokenResponse(); ` 返回 `access_token` 时的响应值\n\n> r`里没有`openid`， `$user->id` 便是 `openid`.\n> 如果你想拿微信返回给你的原样的全部信息，请使用：$user->getRaw();\n\n当 `scope` 为 `snsapi_base` 时 `$oauth->userFromCode($code);` 对象里只有 `id`，没有其它信息。\n\n## 网页授权实例\n\n我们这里来用原生 PHP 写法举个例子，`oauth_callback` 是我们的授权回调 URL (未 urlencode 编码的 URL), `user/profile` 是我们需要授权才能访问的页面，它的 PHP 代码如下：\n\n```php\n// http://easywechat.com/user/profile\n<?php\n\nuse EasyWeChat\\Factory;\n\n$config = [\n  // ...\n  'oauth' => [\n      'scopes'   => ['snsapi_userinfo'],\n      'callback' => '/oauth_callback',\n  ],\n  // ..\n];\n\n$app = Factory::officialAccount($config);\n$oauth = $app->oauth;\n\n// 未登录\nif (empty($_SESSION['wechat_user'])) {\n\n  $_SESSION['target_url'] = 'user/profile';\n\n  $redirectUrl = $oauth->redirect();\n\n  header(\"Location: {$redirectUrl}\");\n  exit;\n}\n\n// 已经登录过\n$user = $_SESSION['wechat_user'];\n\n// ...\n\n```\n\n授权回调页：\n\n```php\n// http://easywechat.com/oauth_callback\n<?php\n\nuse EasyWeChat\\Factory;\n\n$config = [\n  // ...\n];\n\n$app = Factory::officialAccount($config);\n$oauth = $app->oauth;\n// 获取 OAuth 授权结果用户信息\n$code = \"微信回调URL携带的 code\";\n// 不少用户这里的 code 是来源于静默授权 如果这里你的 $oauth 没有配置 scopes 为 snsapi_base 调用 $oauth->userFromCode($code); 默认会走 snsapi_userinfo; \n$oauth = $app->oauth->scopes(['snsapi_base'])\n$user = $oauth->userFromCode($code); \n\n$_SESSION['wechat_user'] = $user->toArray();\n\n$targetUrl = empty($_SESSION['target_url']) ? '/' : $_SESSION['target_url'];\n\nheader('Location:'. $targetUrl); // 跳转到 user/profile\n```\n\n上面的例子呢都是基于 `$_SESSION` 来保持会话的，在微信客户端中，你可以结合 Cookies 来存储，但是有效期平台不一样时间也不一样，好像 Android 的失效会快一些，不过基本也够用了。\n\n## 参考阅读\n\n- 本模块基于 [overtrue/socialite](https://github.com/overtrue/socialite/) 实现，更多的使用请阅读该扩展包文档。\n- state 参数的使用: [overtrue/socialite/#state](https://github.com/overtrue/socialite/#state)\n"
  },
  {
    "path": "docs/src/5.x/official-account/poi.md",
    "content": "# 门店\n\n## 创建门店\n\n用 POI 接口新建门店时所使用的图片 url 必须为微信自己域名的 url,因此需要先用上传图片接 口上传图片并获取 url,再创建门店。上传的图片限制文件大小限制 1MB,支持 JPG 格式，图片接口请参考：[临时素材](../basic-services/media.md)\n\n```php\n$app->poi->create($baseInfo);\n```\n\n> - `$baseInfo` 为门店的基本信息数组\n\n示例：\n\n```php\n<?php\n\n$info = array(\n         \"sid\"             => \"33788392\",\n         \"business_name\"   => \"麦当劳\",\n         \"branch_name\"     => \"艺苑路店\",\n         \"province\"        => \"广东省\",\n         \"city\"            => \"广州市\",\n         \"district\"        => \"海珠区\",\n         \"address\"         => \"艺苑路 11 号\",\n         \"telephone\"       => \"020-12345678\",\n         \"categories\"      => array(\"美食,快餐小吃\"),\n         \"offset_type\"     => 1,\n         \"longitude\"       => 115.32375,\n         \"latitude\"        => 25.097486,\n         \"photo_list\"      => array(\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                               array(\"photo_url\" => \"https://easywechat.com\"),\n                             ),\n         \"recommend\"       => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         \"special\"         => \"免费 wifi,外卖服务\",\n         \"introduction\"    => \"麦当劳是全球大型跨国连锁餐厅,1940 年创立于美国,在世界上大约拥有 3  万间分店。主要售卖汉堡包,以及薯条、炸鸡、汽水、冰品、沙拉、水果等 快餐食品\",\n         \"open_time\"       => \"8:00-20:00\",\n         \"avg_price\"       => 35,\n    );\n\n$result = $app->poi->create($info); // true or exception\n```\n\n> 注意：新创建的门店在审核通过后,会以事件形式推送给商户填写的回调 URL\n\n## 获取指定门店信息\n\n```php\n$app->poi->get($poiId);\n```\n\n> - `$poiId` 为门店 ID\n\n示例：\n\n```php\n$info = $app->poi->get(271262077);\n```\n\n## 获取门店列表\n\n```php\n$app->poi->list($begin, $limit);// begin:0, limit:10\n```\n\n> - `$begin` 就是查询起点，`MySQL` 里的 `offset`；\n> - `$limit` 查询条数，同 `MySQL` 里的 `limit`；\n\n> 两参数均可选\n\n示例：\n\n```php\n$pois = $app->poi->list(0, 2);// 取2条记录\n//\n//[\n//  {\n//    \"sid\": \"100\",\n//    \"poi_id\": \"271864249\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"艺苑路店\",\n//    \"address\": \"艺苑路 11 号\",\n//    \"available_state\": 3\n//  },\n//  {\n//    \"sid\": \"101\",\n//    \"business_name\": \"麦当劳\",\n//    \"branch_name\": \"赤岗路店\",\n//    \"address\": \"赤岗路 102 号\",\n//    \"available_state\": 4\n//  }\n//]\n```\n\n## 修改门店信息\n\n商户可以通过该接口,修改门店的服务信息,包括:图片列表、营业时间、推荐、特色服务、简 介、人均价格、电话 7 个字段。目前基础字段包括(名称、坐标、地址等不可修改)。\n\n```php\n$app->poi->update($poiId, $data);\n```\n\n> - `$poiId` 为门店 ID\n> - `$data` 需要更新的部分数据，**若有填写内容则为覆盖更新,若无内容则视为不 修改,维持原有内容。photo_list 字段为全列表覆盖,若需要增加图片,需将之前图片同样放入 list 中,在其后增加新增图片。如:已有 A、B、C 三张图片,又要增加 D、E 两张图,则需要调 用该接口,photo_list 传入 A、B、C、D、E 五张图片的链接。**\n\n示例：\n\n```php\n$data = array(\n         \"telephone\" => \"020-12345678\",\n         \"recommend\" => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n         //...\n        );\n\n$res = $app->poi->update(271262077, $data); //true or exception\n```\n\n## 删除门店\n\n```php\n$app->poi->delete($poiId);\n```\n\n示例：\n\n```php\n$app->poi->delete(271262077);// true or exception\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/reply.md",
    "content": "# 自动回复\n\n## 获取当前设置的回复规则\n\n```php\n$app->auto_reply->current();\n```"
  },
  {
    "path": "docs/src/5.x/official-account/semantic.md",
    "content": "# 语义理解\n\n> 貌似此接口已经下线，调用无正确返回值\n\n+ `query($keyword, $categories, $optional = [])` 语义理解:\n\n  + `$keyword` 为关键字\n  + `$categories` 需要使用的服务类型，多个用 “,” 隔开字符串，不能为空;\n  + `$optional` 为其它属性：\n    + `latitude`  `float`  纬度坐标，与经度同时传入；与城市二选一传入\n    + `longitude`  `float`  经度坐标，与纬度同时传入；与城市二选一传入\n    + `city`   `string`  城市名称，与经纬度二选一传入\n    + `region` `string`  区域名称，在城市存在的情况下可省；与经纬度二选一传入\n    + `uid`  `string` 用户唯一id（非开发者id），用户区分公众号下的不同用户（建议填入用户openid），如果为空，则无法使用上下文理解功能。appid和uid同时存在的情况下，才可以使用上下文理解功能。\n\n> 注：单类别意图比较明确，识别的覆盖率比较大，所以如果只要使用特定某个类别，建议将 category 只设置为该类别。\n\n示例：\n\n```php\n$result = $app->semantic->query('查一下明天从北京到上海的南航机票', \"flight,hotel\", array('city' => '北京', 'uid' => '123456'));\n// 查询参数：\n// {\n//    \"query\":\"查一下明天从北京到上海的南航机票\",\n//    \"city\":\"北京\",\n//    \"category\": \"flight,hotel\",\n//    \"appid\":\"wxaaaaaaaaaaaaaaaa\",\n//    \"uid\":\"123456\"\n// }\n```\n返回值示例：\n\n```json\n{\n    \"errcode\":0,\n    \"query\":\"查一下明天从北京到上海的南航机票\",\n    \"type\":\"flight\",\n    \"semantic\":{\n        \"details\":{\n            \"start_loc\":{\n                \"type\":\"LOC_CITY\",\n                \"city\":\"北京市\",\n                \"city_simple\":\"北京\",\n                \"loc_ori\":\"北京\"\n                },\n            \"end_loc\": {\n                \"type\":\"LOC_CITY\",\n                \"city\":\"上海市\",\n                \"city_simple\":\"上海\",\n                \"loc_ori\":\"上海\"\n              },\n            \"start_date\": {\n                \"type\":\"DT_ORI\",\n                \"date\":\"2014-03-05\",\n                \"date_ori\":\"明天\"\n              },\n           \"airline\":\"中国南方航空公司\"\n        },\n    \"intent\":\"SEARCH\"\n}\n```"
  },
  {
    "path": "docs/src/5.x/official-account/server.md",
    "content": "# 服务端\n\n我们在入门小教程一节以服务端为例讲解了一个基本的消息的处理，这里就不再讲服务器验证的流程了，请直接参考前面的入门实例即可。\n\n服务端的作用呢，在整个微信开发中主要是负责 **[接收用户发送过来的消息](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html)**，还有 **[用户触发的一系列事件](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html)**。\n\n首先我们得理清消息与事件的回复逻辑，当你收到用户消息后（消息由微信服务器推送到你的服务器），在你对消息进行一些处理后，不管是选择回复一个消息还是什么不都回给用户，你也应该给微信服务器一个 “答复”，如果是选择回复一条消息，就直接返回一个消息xml就好，如果选择不作任何回复，你也得回复一个空字符串或者字符串 `SUCCESS`（不然用户就会看到 `该公众号暂时无法提供服务`）。\n\n## 基本使用\n\n在 SDK 中使用 `$app->server->push(callable $callback)` 来设置消息处理器：\n\n```php\n$app->server->push(function ($message) {\n    // $message['FromUserName'] // 用户的 openid\n    // $message['MsgType'] // 消息类型：event, text....\n    return \"您好！欢迎使用 EasyWeChat\";\n});\n\n// 在 laravel 中：\n$response = $app->server->serve();\n\n// $response 为 `Symfony\\Component\\HttpFoundation\\Response` 实例\n// 对于需要直接输出响应的框架，或者原生 PHP 环境下\n$response->send();\n\n// 而 laravel 中直接返回即可：\n\nreturn $response;\n```\n\n这里我们使用 `push` 传入了一个 **闭包（[Closure](http://php.net/manual/en/class.closure.php)）**，该闭包接收一个参数 `$message` 为消息对象（类型取决于你的配置中 `response_type`），你可以在全局消息处理器中对消息类型进行筛选：\n\n```php\n$app->server->push(function ($message) {\n    switch ($message['MsgType']) {\n        case 'event':\n            return '收到事件消息';\n            break;\n        case 'text':\n            return '收到文字消息';\n            break;\n        case 'image':\n            return '收到图片消息';\n            break;\n        case 'voice':\n            return '收到语音消息';\n            break;\n        case 'video':\n            return '收到视频消息';\n            break;\n        case 'location':\n            return '收到坐标消息';\n            break;\n        case 'link':\n            return '收到链接消息';\n            break;\n        case 'file':\n            return '收到文件消息';\n        // ... 其它消息\n        default:\n            return '收到其它消息';\n            break;\n    }\n\n    // ...\n});\n```\n\n当然，因为这里 `push` 接收一个 [`callable`](http://php.net/manual/zh/language.types.callable.php) 的参数，所以你不一定要传入一个 Closure 闭包，你可以选择传入一个函数名，一个 `[$class, $method]` 或者 `Foo::bar` 这样的类型。\n\n某些情况，我们需要直接使用 `$message` 参数，那么怎么在 `push` 的闭包外调用呢？\n\n```php\n    $message = $app->server->getMessage();\n```\n>  注意：`$message` 的类型取决于你的配置中 `response_type`\n\n## 注册多个消息处理器\n\n有时候你可能需要对消息记日志，或者一系列的自定义操作，你可以注册多个 handler：\n\n```php\n$app->server->push(MessageLogHandler::class);\n$app->server->push(MessageReplyHandler::class);\n$app->server->push(OtherHandler::class);\n$app->server->push(...);\n```\n\n1. 最后一个非空返回值将作为最终应答给用户的消息内容，如果中间某一个 handler 返回值 false, 则将终止整个调用链，不会调用后续的 handlers。\n2. 传入的自定义 Handler 类需要实现 `\\EasyWeChat\\Kernel\\Contracts\\EventHandlerInterface`。\n\n## 注册指定消息类型的消息处理器\n\n我们想对特定类型的消息应用不同的处理器，可以在第二个参数传入类型筛选：\n\n> 注意，第二个参数必须是 `\\EasyWeChat\\Kernel\\Messages\\Message` 类的常量。\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\Message;\n\n$app->server->push(ImageMessageHandler::class, Message::IMAGE); // 图片消息\n$app->server->push(TextMessageHandler::class, Message::TEXT); // 文本消息\n\n// 同时处理多种类型的处理器\n$app->server->push(MediaMessageHandler::class, Message::VOICE|Message::VIDEO|Message::SHORT_VIDEO); // 当消息为 三种中任意一种都可触发\n```\n\n## 请求消息的属性\n\n当你接收到用户发来的消息时，可能会提取消息中的相关属性，参考：\n\n请求消息基本属性(以下所有消息都有的基本属性)：\n\n>>  - `ToUserName`    接收方帐号（该公众号 ID）\n>>  - `FromUserName`  发送方帐号（OpenID, 代表用户的唯一标识）\n>>  - `CreateTime`    消息创建时间（时间戳）\n>>  - `MsgId`        消息 ID（64位整型）\n\n### 文本：\n\n>  - `MsgType`  text\n>  - `Content`  文本消息内容\n\n### 图片：\n\n>  - `MsgType`  image\n>  - `MediaId`  图片消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `PicUrl`   图片链接\n\n### 语音：\n\n>  - `MsgType`        voice\n>  - `MediaId`        语音消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `Format`         语音格式，如 amr，speex 等\n>  - `Recognition`  * 开通语音识别后才有\n\n  > 识别后，用户每次发送语音给公众号时，微信会在推送的语音消息XML数据包中，增加一个 `Recongnition` 字段\n\n### 视频：\n\n>  - `MsgType`       video\n>  - `MediaId`       视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `ThumbMediaId`  视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 小视频：\n\n>  - `MsgType`     shortvideo\n>  - `MediaId`     视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n>  - `ThumbMediaId`    视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n\n### 事件：\n\n>  - `MsgType`     event\n>  - `Event`       事件类型 （如：subscribe(订阅)、unsubscribe(取消订阅) ...， CLICK 等）\n\n#### 扫描带参数二维码事件\n>  - `EventKey`    事件KEY值，比如：qrscene_123123，qrscene_为前缀，后面为二维码的参数值\n>  - `Ticket`      二维码的 ticket，可用来换取二维码图片\n\n#### 上报地理位置事件\n>  - `Latitude`    23.137466   地理位置纬度\n>  - `Longitude`   113.352425  地理位置经度\n>  - `Precision`   119.385040  地理位置精度\n\n#### 自定义菜单事件\n>  - `EventKey`    事件KEY值，与自定义菜单接口中KEY值对应，如：CUSTOM_KEY_001, www.qq.com\n\n### 地理位置：\n\n>  - `MsgType`     location\n>  - `Location_X`  地理位置纬度\n>  - `Location_Y`  地理位置经度\n>  - `Scale`       地图缩放大小\n>  - `Label`       地理位置信息\n\n### 链接：\n\n>  - `MsgType`      link\n>  - `Title`        消息标题\n>  - `Description`  消息描述\n>  - `Url`          消息链接\n\n### 文件：\n\n>  - `MsgType`      file\n>  - `Title`        文件名\n>  - `Description`  文件描述，可能为null\n>  - `FileKey`      文件KEY\n>  - `FileMd5`      文件MD5值\n>  - `FileTotalLen` 文件大小，单位字节\n\n## 回复消息\n\n回复的消息可以为 `null`，此时 SDK 会返回给微信一个 \"SUCCESS\"，你也可以回复一个普通字符串，比如：`欢迎关注 overtrue.`，此时 SDK 会对它进行一个封装，产生一个 [`EasyWeChat\\Kernel\\Messages\\Text`](https://github.com/EasyWeChat/message/blob/master/src/Kernel/Messages/Text.php) 类型的消息并在最后的 `$app->server->serve();` 时生成对应的消息 XML 格式。\n\n如果你想返回一个自己手动拼的原生 XML 格式消息，请返回一个 [`EasyWeChat\\Kernel\\Messages\\Raw`](https://github.com/EasyWeChat/message/blob/master/src/Kernel/Messages/Raw.php) 实例即可。\n\n## 消息转发给客服系统\n\n参见：[多客服消息转发](message-transfer)\n\n关于消息的使用，请参考 [`消息`](messages) 章节。\n"
  },
  {
    "path": "docs/src/5.x/official-account/shake-around.md",
    "content": "# 摇一摇周边\n\n\n摇一摇周边是微信在线下的全新功能, 为线下商户提供近距离连接用户的能力, 并支持线下商户向周边用户提供个性化营销、互动及信息推荐等服务。\n\n## 获取实例\n\n```php\n$shakearound = $app->shake_around;\n\n```\n\n## 说明\n\n> 特别提醒：\n1、下述所有的接口调用的方法参数都要严格按照方法参数前的类型传入相应类型的实参，否则可能会得到非预期的结果。\n2、涉及需要传入设备id（$deviceIdentifier）的参数时，该参数是一个以 `device_id` 或包含 `uuid` `major` `minor` 为key的关联数组。\n3、涉及需要传入设备id列表（$deviceIdentifiers）的参数时，该参数是一个二维数组，第一层为索引类型，第二层为关联类型（$deviceIdentifier）。\n\n```php\n// 参数 $deviceIdentifier 的实参形式：\n['device_id' => 10097]\n// 或\n[\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12102,\n]\n// 参数$deviceIdentifiers的实参形式：\n[\n    ['device_id' => 10097],\n    ['device_id' => 10098],\n]\n// 或\n[\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12102,\n    ],\n    [\n        'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n        'major' => 10001,\n        'minor' => 12103,\n    ]\n]\n```\n\n## 开通摇一摇周边\n\n> 提示：\n若不是做 [公众号第三方平台](https://open.weixin.qq.com/cgi-bin/frame?t=home/wx_plugin_tmpl&lang=zh_CN) 开发，建议直接在微信管理后台申请开通摇一摇周边功能。\n\n### 申请开通\n\n申请开通摇一摇周边功能。成功提交申请请求后，工作人员会在三个工作日内完成审核。若审核不通过，可以重新提交申请请求。若是审核中，请耐心等待工作人员审核，在审核中状态不能再提交申请请求。\n\n方法\n\n```php\n$shakearound->register($data)\n```\n\n>  注意：\n1、相关资质文件的图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material) 接口混淆。\n2、行业代码请务必传入**字符串**类型的实参，否则以数字0开头的行业代码将会被当成八进制数处理（将转换为十进制数），这可能不是期望的。\n\n### 查询审核状态\n\n查询已经提交的开通摇一摇周边功能申请的审核状态。在申请提交后，工作人员会在三个工作日内完成审核。\n\n方法\n\n```php\n$shakearound->status()\n```\n\n### 获取摇一摇的设备及用户信息\n\n获取设备信息，包括UUID、major、minor，以及距离、openID等信息。\n\n方法\n\n```php\n$shakearound->user($ticket);\n// 或者需要返回门店poi_id\n$shakearound->user($ticket, true);\n```\n\n## 设备管理\n\n### 申请设备 ID\n\n申请配置设备所需的UUID、Major、Minor。申请成功后返回批次ID，可用返回的批次ID通过“查询设备ID申请状态”接口查询目前申请的审核状态。\n一个公众账号最多可申请100000个设备ID，如需申请的设备ID数超过最大限额，请邮件至zhoubian@tencent.com，邮件格式如下：\n\n> 标题：申请提升设备ID额度\n内容：\n1、公众账号名称及appid（wx开头的字符串，在mp平台可查看）\n2、用途\n3、预估需要多少设备ID\n\n方法\n\n```php\n$shakearound->device->apply($data)\n```\n\n### 查询设备 ID 申请审核状态\n\n查询设备ID申请的审核状态。若单次申请的设备ID数量小于等于500个，系统会进行快速审核；若单次申请的设备ID数量大于500个，则在三个工作日内完成审核。\n\n方法\n\n```php\n$shakearound->device->status($applyId) // $applyId 批次ID，申请设备ID时所返回的批次ID\n```\n\n### 编辑设备信息\n\n> 仅能修改设备的备注信息。\n\n方法\n\n```php\n$shakearound->device->update(array $deviceIdentifier, string $comment)\n```\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$comment 设备的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->device->update(['device_id' => 10011], 'test');\n// 或\n$result = $shakearound->device->update(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                          'major' => 1002,\n                                          'minor' => 1223,\n], 'test');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result['errcode']) // 0\n```\n\n### 配置设备与门店/其他公众账号门店的关联关系\n\n关联本公众账号门店时，支持创建门店后直接关联在设备上，无需为审核通过状态，摇周边后台自动更新门店的最新信息和状态。\n关联其他公众账号门店时，支持设备关联其他公众账号的门店，门店需为审核通过状态。\n\n> 因为第三方门店不归属本公众账号，所以未保存到设备详情中，查询设备列表接口与获取摇周边的设备及用户信息接口不会返回第三方门店。\n\n方法\n\n```php\n$shakearound->device->bindPoi(array $deviceIdentifier, $poiId)\n\n//或者 绑定第三方\n$shakearound->device->bindThirdPoi(array $deviceIdentifier, $poiId, $poiAppId)\n```\n\n参数\n\n> $deviceIdentifier 设备 id，设备编号 device_id 或 UUID、major、minor 的关联数组，若二者都填，则以设备编号为优先\n$poiId 设备关联的门店 ID，关联门店后，在门店 1KM 的范围内有优先摇出信息的机会。当值为0时，将清除设备已关联的门店 ID\n$poiAppId 关联门店所归属的公众账号的 APP ID\n\n示例\n\n```php\n// 关联本公众账号门店\n$result = $shakearound->device->bindLocation(['device_id' => 10011], 1231);\n// 或\n$result = $shakearound->device->bindLocation([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n], 1231);\n\n// 关联其他公众账号门店\n// wxappid 为关联门店所归属的公众账号的 APP ID\n$result = $shakearound->device->bindThirdPoi(['device_id' => 10011], 1231, 'wxappid');\n\n// 或\n$result = $shakearound->device->bindThirdPoi([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n], 1231, 'wxappid');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 查询设备列表\n\n查询已有的设备 ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。\n\n### 根据设备id批量取回设备数据\n\n方法\n\n> $shakearound->device->listByIds(array $deviceIdentifiers)\n\n参数\n\n> $deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->device->listByIds([\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->device->listByIds([\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n   'major' => 10001,\n   'minor' => 12102,\n  ],\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n   'major' => 10001,\n   'minor' => 12103,\n  ]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"comment\": \"\",\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_id\": 0,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            },\n            {\n                \"comment\": \"\",\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"status\": 1,\n                \"last_active_time\":1437276018,\n                \"poi_appid\":\"wxe3813f5d8c546fc7\"\n                \"poi_id\": 123,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n            }\n        ],\n        \"total_count\": 151\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 分页批量取回设备数据\n\n方法\n\n```php\n$shakearound->device->list(int $lastId, int $count)\n```\n\n参数\n\n> $lastId 前一次查询列表末尾的设备编号 device_id，第一次查询 lastId 为 0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device->list(10097, 3);\n\n// 返回结果同上\n```\n\n### 根据申请时的批次 ID 分页批量取回设备数据\n\n方法\n\n> $shakearound->device->listByApplyId(int $applyId, int $lastId, int $count)\n\n参数\n\n> $applyId 批次ID，申请设备ID时所返回的批次ID\n$lastId 前一次查询列表末尾的设备编号device_id，第一次查询 lastId 为 0\n$count 待查询的设备数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->device->listByApplyId(1231, 10097, 3);\n\n// 返回结果同上\n```\n\n## 页面管理\n\n### 新增页面\n\n新增摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。其中，图片必须为用素材管理接口上传至微信侧服务器后返回的链接。\n\n>\n图片是使用本页面下方的素材管理的接口上传的，切勿和另一个 [素材管理](material) 接口混淆。\n\n方法\n\n```php\n$shakearound->page->create($data)\n```\n\n参数\n\n> $title 在摇一摇页面展示的主标题，不超过6个汉字或12个英文字母\n$description 在摇一摇页面展示的副标题，不超过7个汉字或14个英文字母\n$pageUrl 点击进去的超链接\n$iconUrl 在摇一摇页面展示的图片。图片需先上传至微信侧服务器，用“素材管理-上传图片素材”接口上传图片，返回的图片URL再配置在此处\n$comment 可选，页面的备注信息，不超过15个汉字或30个英文字母\n\n示例\n\n```php\n$result = $shakearound->page->create($data);\n\n/* 返回结果\n{\n   \"data\": {\n       \"page_id\": 28840\n   }\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 编辑页面信息\n\n编辑摇一摇出来的页面信息，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n方法\n\n```php\n$shakearound->page->update(int $pageId, array $data)\n```\n\n参数\n\n> $pageId 摇周边页面唯一ID\n$data 需要更新的信息\n\n示例\n\n```php\n$result = $shakearound->page->update(28840, [\n    'title' => '主标题',\n    'description' => '副标题',\n    //...\n]);\n```\n\n## 查询页面列表\n\n查询已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n### 根据页面id批量取回页面数据\n\n方法\n\n```php\n$shakearound->page->listByIds(array $pageIds)\n```\n\n参数\n\n> $pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->page->listByIds([28840, 28842]);\n\n/* 返回结果\n{\n   \"data\": {\n       \"pages\": [\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28840,\n               \"page_url\": \"http://xw.qq.com/testapi1\",\n               \"title\": \"测试1\"\n           },\n           {\n               \"comment\": \"just for test\",\n               \"description\": \"test\",\n               \"icon_url\": \"https://www.baidu.com/img/bd_logo1\",\n               \"page_id\": 28842,\n               \"page_url\": \"http://xw.qq.com/testapi2\",\n               \"title\": \"测试2\"\n           }\n       ],\n       \"total_count\": 2\n   },\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 分页批量取回页面数据\n\n方法\n\n```php\n$shakearound->page->list(int $begin, int $count)\n```\n\n参数\n\n> $begin 页面列表的起始索引值\n$count 待查询的页面数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->page->list(0,2);\n\n// 返回结果同上\n```\n\n### 删除页面\n\n删除已有的页面，包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。\n\n>\n只有页面与设备没有关联关系时，才可被删除。\n\n方法\n\n```php\n$shakearound->page->delete(int $pageId)\n```\n\n参数\n\n> $pageId 页面的id\n\n示例\n\n```php\n$result = $shakearound->page->delete(34567);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 素材管理\n\n上传在摇一摇功能中需使用到的图片素材，素材保存在微信侧服务器上。图片格式限定为：jpg,jpeg,png,gif。\n若图片为在摇一摇页面展示的图片，则其素材为 `icon` 类型的图片，图片大小建议 `120px*120 px` ，限制不超过 `200 px *200 px` ，图片需为 `正方形` 。\n若图片为申请开通摇一摇周边功能需要上传的资质文件图片，则其素材为 `license` 类型的图片，图片的文件大小不超过 `2MB` ，尺寸不限，形状不限。\n\n方法\n\n> $shakearound->material->uploadImage(string $path [, string $type = 'icon'])\n\n参数\n\n> $path 图片所在路径\n$type 可选，值为icon或license\n\n示例\n\n```php\n$result = $shakearound->material->uploadImage(__DIR__ . '/stubs/image.jpg');\n\n/* 返回结果\n{\n    \"data\": {\n        \"pic_url\": http://shp.qpic.cn/wechat_shakearound_pic/0/1428377032e9dd2797018cad79186e03e8c5aec8dc/120\"\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 管理设备与页面的关系\n\n通过接口申请的设备ID，需先配置页面，若未配置页面，则摇不出页面信息。\n\n### 配置设备与页面的关联关系\n\n配置完成后，在此设备的信号范围内，即可摇出关联的页面信息。\n若设备配置多个页面，则随机出现页面信息。一个设备最多可配置30个关联页面。\n\n>\n1、配置时传入该设备需要关联的页面的id列表，该设备原有的关联关系将被直接清除。\n2、页面的id列表允许为空（**传入空数组**），当页面的id列表为空时则会清除该设备的所有关联关系。\n\n方法\n\n> $shakearound->relation->bindPage(array $deviceIdentifier, array $pageIds)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$pageIds 页面的id列表，索引数组\n\n示例\n\n```php\n$result = $shakearound->relation->bindPage(['device_id' => 10011], [12345, 23456, 334567]);\n// 或\n$result = $shakearound->relation->bindPage(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                              'major' => 1002,\n                                              'minor' => 1223,\n], [12345, 23456, 334567]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\nvar_dump($result->errcode) // 0\n```\n\n### 查询设备与页面的关联关系\n\n#### 查询指定设备所关联的页面\n\n根据设备ID或完整的UUID、Major、Minor查询该设备所关联的所有页面信息\n\n方法\n\n> $shakearound->relation->listByDeviceId(array $deviceIdentifier [, boolean $raw = false])\n\n>\n该方法默认对返回的数据进行处理后返回一个包含页面id的索引数组。若要返回和 `getDeviceByPageId` 方法类似的数据，请传入 `true` 作为第二个参数。\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$raw 可选，当为true时，返回值和getDeviceByPageId方法类似，否则返回页面的id列表（索引数组，无关联时为空数组）\n\n示例\n\n```php\n$result = $shakearound->relation->listByDeviceId(['device_id' => 10011]);\n// 或\n$result = $shakearound->relation->listByDeviceId([\n  'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n  'major' => 1002,\n  'minor' => 1223,\n]);\n\n// 返回结果\nvar_dump($result) // [50054,50055]\n```\n\n##### 查询指定页面所关联的设备\n\n指定页面ID分页查询该页面所关联的所有的设备信息\n\n方法\n\n> $shakearound->relation->listByPageId(int $pageId, int $begin, int $count)\n\n参数\n\n> $pageId 指定的页面id\n$begin 关联关系列表的起始索引值\n$count 待查询的关联关系数量，不能超过50个\n\n示例\n\n```php\n$result = $shakearound->relation->listByPageId(50054, 0, 3);\n\n/* 返回结果\n{\n  \"data\": {\n      \"relations\": [\n          {\n              \"device_id\": 797994,\n              \"major\": 10001,\n              \"minor\": 10023,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          },\n          {\n              \"device_id\": 797995,\n              \"major\": 10001,\n              \"minor\": 10024,\n              \"page_id\": 50054,\n              \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n          }\n      ],\n      \"total_count\": 2\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 摇一摇数据统计\n\n> 此接口无法获取当天的数据，最早只能获取前一天的数据。\n由于系统在凌晨处理前一天的数据，太早调用此接口可能获取不到数据，建议在早上8：00之后调用此接口。\n\n### 以设备为维度的数据统计\n\n查询单个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n>\n查询的最长时间跨度为 30 天。只能查询最近 90 天的数据。\n\n方法\n\n> $shakearound->stats->deviceSummary(array $deviceIdentifier, int $beginDate, int $endDate)\n\n参数\n\n> $deviceIdentifier 设备id，设备编号device_id或UUID、major、minor的关联数组，若二者都填，则以设备编号为优先\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats->deviceSummary(['device_id' => 10011], 1425052800, 1425139200);\n// 或\n$result = $shakearound->stats->deviceSummary(['uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n                                                'major' => 1002,\n                                                'minor' => 1223,\n], 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 批量查询设备统计数据\n\n查询指定时间商家帐号下的每个设备进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n> 只能查询最近90天内的数据，且一次只能查询一天。\n\n>\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的设备，不在结果列表中返回。\n\n方法\n\n```php\n$shakearound->stats->devicesSummary(int $timestamp, int $pageIndex)\n```\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats->devicesSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"devices\": [\n            {\n                \"device_id\": 10097,\n                \"major\": 10001,\n                \"minor\": 12102,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            },\n            {\n                \"device_id\": 10098,\n                \"major\": 10001,\n                \"minor\": 12103,\n                \"uuid\": \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\"\n                \"shake_pv\": 1\n                \"shake_uv\": 2\n                \"click_pv\": 3\n                \"click_uv\": 4\n            }\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 以页面为维度的数据统计\n\n查询单个页面通过摇周边摇出来的人数、次数，点击摇周边页面的人数、次数\n\n> 注意：\n查询的最长时间跨度为30天。只能查询最近90天的数据。\n\n方法\n\n```php\n$shakearound->stats->pageSummary(int $pageId, int $beginDate, int $endDate);\n```\n\n参数\n\n> $pageId 指定页面的页面ID\n$beginDate 起始日期时间戳，最长时间跨度为30天，单位为秒\n$endDate 结束日期时间戳，最长时间跨度为30天，单位为秒\n\n示例\n\n```php\n$result = $shakearound->stats->pageSummary(12345, 1425052800, 1425139200);\n\n/* 返回结果\n{\n   \"data\": [\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425052800,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       },\n       {\n           \"click_pv\": 0,\n           \"click_uv\": 0,\n           \"ftime\": 1425139200,\n           \"shake_pv\": 0,\n           \"shake_uv\": 0\n       }\n   ],\n   \"errcode\": 0,\n   \"errmsg\": \"success.\"\n}\n*/\n```\n### 批量查询页面统计数据\n\n查询指定时间商家帐号下的每个页面进行摇周边操作的人数、次数，点击摇周边消息的人数、次数。\n\n>\n对于摇周边人数、摇周边次数、点击摇周边消息的人数、点击摇周边消息的次数都为0的页面，不在结果列表中返回。\n\n方法\n\n```php\n$shakearound->stats->pagesSummary(int $timestamp, int $pageIndex);\n```\n\n参数\n\n> $timestamp 指定查询日期时间戳，单位为秒\n$pageIndex 指定查询的结果页序号，返回结果按摇周边人数降序排序，每50条记录为一页\n\n示例\n\n```php\n$result = $shakearound->stats->pagesSummary(1435075200, 1);\n\n/* 返回结果\n{\n    \"data\": {\n        \"pages\": [\n            {\n                \"page_id\":1234\n                \"click_pv\": 1,\n                \"click_uv\": 3,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n            {\n                \"page_id\":5678\n                \"click_pv\": 1,\n                \"click_uv\": 2,\n                \"shake_pv\": 0,\n                \"shake_uv\": 0\n            },\n        ],\n    },\n    \"date\":1435075200\n    \"total_count\": 151\n    \"page_index\":1\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n## 设备分组管理\n\n调用H5页面获取设备信息 JS API接口，需要先把设备分组，微信客户端只会返回已在分组中的设备信息。\n\n### 新增分组\n\n新建设备分组，每个帐号下最多只有1000个分组。\n\n方法\n\n```php\n$shakearound->group->create(string $name)\n\n参数\n\n> $name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group->create('test');\n\n/* 返回结果\n{\n  \"data\": {\n      \"group_id\" : 123,\n      \"group_name\" : \"test\"\n  },\n  \"errcode\": 0,\n  \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 编辑分组信息\n\n编辑设备分组信息，目前只能修改分组名。\n\n方法\n\n> $shakearound->group->update(int $groupId, string $name)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$name 分组名称，不超过100汉字或200个英文字母\n\n示例\n\n```php\n$result = $shakearound->group->update(123, 'newName');\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 删除分组\n\n删除设备分组，若分组中还存在设备，则不能删除成功。需把设备移除以后，才能删除。\n\n> 在执行删除前，最好先使用 `get` 方法查询分组详情，若分组内有设备，先使用 `removeDevices` 方法移除。\n\n方法\n\n```php\n$shakearound->group->delete(int $groupId)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n\n示例\n\n```php\n$result = $shakearound->group->delete(123);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 查询分组列表\n\n查询账号下所有的分组。\n\n方法\n\n```php\n$shakearound->group->list(int $begin, int $count)\n```\n\n参数\n\n> $begin 分组列表的起始索引值\n$count 待查询的分组数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group->list(0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"groups\":[\n            {\n                \"group_id\" : 123,\n                \"group_name\" : \"test1\"\n            },\n            {\n                \"group_id\" : 124,\n                \"group_name\" : \"test2\"\n            }\n        ],\n        \"total_count\": 100\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 查询分组详情\n\n查询分组详情，包括分组名，分组id，分组里的设备列表。\n\n方法\n\n```php\n$shakearound->group->get(int $groupId, int $begin, int $count)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$begin 分组里设备的起始索引值\n$count 待查询的分组里设备的数量，不能超过1000个\n\n示例\n\n```php\n$result = $shakearound->group->get(123, 0, 2);\n\n/* 返回结果\n{\n    \"data\": {\n        \"group_id\" : 123,\n        \"group_name\" : \"test\",\n        \"total_count\": 100,\n        \"devices\" :[\n            {\n                \"device_id\" : 123456,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10001,\n                \"comment\" : \"test device1\",\n                \"poi_id\" : 12345,\n            },\n            {\n                \"device_id\" : 123457,\n                \"uuid\" : \"FDA50693-A4E2-4FB1-AFCF-C6EB07647825\",\n                \"major\" : 10001,\n                \"minor\" : 10002,\n                \"comment\" : \"test device2\",\n                \"poi_id\" : 12345,\n            }\n        ]\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 添加设备到分组\n\n添加设备到分组，每个分组能够持有的设备上限为10000，并且每次添加操作的添加上限为1000。\n\n> 只有在摇周边申请的设备才能添加到分组。\n\n方法\n\n> $shakearound->group->addDevices(int $groupId, array $deviceIdentifiers)\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group->addDevices(123, [\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n\n// 或\n$result = $shakearound->group->addDevices(123, [\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12102,\n  ],\n  [\n    'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n    'major' => 10001,\n    'minor' => 12103,\n  ]\n]);\n\n/* 返回结果\n{\n    \"data\": {\n    },\n    \"errcode\": 0,\n    \"errmsg\": \"success.\"\n}\n*/\n```\n\n### 从分组中移除设备\n\n从分组中移除设备，每次删除操作的上限为 1000。\n\n方法\n\n```php\n$shakearound->group->removeDevices(int $groupId, array $deviceIdentifiers)\n```\n\n参数\n\n> $groupId 分组唯一标识，全局唯一\n$deviceIdentifiers 设备id列表\n\n示例\n\n```php\n$result = $shakearound->group->removeDevices(123, [\n  ['device_id' => 10097],\n  ['device_id' => 10098],\n]);\n// 或\n$result = $shakearound->group->removeDevices(123, [\n    [\n      'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n      'major' => 10001,\n      'minor' => 12102,\n    ],\n    [\n      'uuid' => 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825',\n      'major' => 10001,\n      'minor' => 12103,\n    ]\n]);\n```\n\n## 摇一摇事件通知\n\n用户进入摇一摇界面，在“周边”页卡下摇一摇时，微信会把这个事件推送到开发者填写的URL（登录公众平台进入开发者中心设置）。推送内容包含摇一摇时“周边”页卡展示出来的页面所对应的设备信息，以及附近最多五个属于该公众账号的设备的信息。当摇出列表时，此事件不推送。\n\n> 摇一摇事件的事件类型：ShakearoundUserShake\n\n关于事件的处理请移步： [事件](events)\n\n"
  },
  {
    "path": "docs/src/5.x/official-account/store.md",
    "content": "# 门店小程序\n\n## 拉取门店小程序类目\n\n```php\n$app->store->categories();\n```\n\n## 创建门店小程序\n\n> 说明：创建门店小程序提交后需要公众号管理员确认通过后才可进行审核。如果主管理员 24 小时超时未确认，才能再次提交。\n\n```php\n$app->store->createMerchant($baseInfo);\n```\n\n> - `$baseInfo` 为门店小程序的基本信息数组，**`qualification_list` 字段为类目相关证件的临时素材 `mediaid` 如果 `second_catid` 对应的 `sensitive_type` 为 1 ，则 `qualification_list` 字段需要填 支持 0~5 个 `mediaid`，例如 `mediaid1`。`headimg_mediaid` 字段为头像 --- 临时素材 `mediaid`。`mediaid` 用现有的 `media/upload` 接口得到的,获取链接： [临时素材](../basic-services/media.md) ( 支持 PNG\\JPEG\\JPG\\GIF 格式的图片，后续加上其他格式)**\n\n示例：\n\n```php\n\n$info = [\n    \"first_catid\"        => 476, //categories 接口获取的一级类目id\n    \"second_catid\"       => 477, //categories 接口获取的二级类目id\n    \"qualification_list\" =>  \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"headimg_mediaid\"    => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"nickname\"           => \"hardenzhang308\",\n    \"intro\"              => \"hardenzhangtest\",\n    \"org_code\"           => \"\",\n    \"other_files\"        => \"\"\n];\n\n$result = $app->store->createMerchant($info);\n```\n\n> 注意：创建门店小程序的审核结果,会以事件形式推送给商户填写的回调 URL\n\n## 查询门店小程序审核结果\n\n```php\n$app->store->getStatus($baseInfo);\n```\n\n## 修改门店小程序信息\n\n```php\n$app->store->updateMerchant($data);\n```\n\n> - `$data` 需要更新的部分数据，目前仅支持门店头像和门店小程序介绍，**若有填写内容则为覆盖更新,若无内容则视为不修改,维持原有内容。`headimg_mediaid`、`intro` 字段参考创建门店小程序**\n\n示例：\n\n```php\n$data = [\n    \"headimg_mediaid\" => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\",\n    \"intro\"           => \"麦辣鸡腿堡套餐,麦乐鸡,全家桶\",\n];\n\n$result = $app->store->updateMerchant($data);\n```\n\n## 从腾讯地图拉取省市区信息\n\n```php\n$app->store->districts();\n```\n\n## 在腾讯地图中搜索门店\n\n```php\n$app->store->searchFromMap($districtId, $keyword);\n```\n\n> - `$districtId` 为从腾讯地图拉取的地区 `id`\n> - `$keyword` 为搜索的关键词\n\n## 在腾讯地图中创建门店\n\n```php\n$app->store->createFromMap($baseInfo);\n```\n\n示例：\n\n```php\n$baseInfo = [\n    \"name\"       => \"hardenzhang\",\n    \"longitude\"  => \"113.323753357\",\n    \"latitude\"   => \"23.0974903107\",\n    \"province\"   => \"广东省\",\n    \"city\"       => \"广州市\",\n    \"district\"   => \"海珠区\",\n    \"address\"    => \"TIT\",\n    \"category\"   => \"类目1:类目2\",\n    \"telephone\"  => \"12345678901\",\n    \"photo\"      => \"http://mmbiz.qpic.cn/mmbiz_png/tW66AWE2K6ECFPcyAcIZTG8RlcR0sAqBibOm8gao5xOoLfIic9ZJ6MADAktGPxZI7MZLcadZUT36b14NJ2cHRHA/0?wx_fmt=png\",\n    \"license\"    => \"http://mmbiz.qpic.cn/mmbiz_png/tW66AWE2K6ECFPcyAcIZTG8RlcR0sAqBibOm8gao5xOoLfIic9ZJ6MADAktGPxZI7MZLcadZUT36b14NJ2cHRHA/0?wx_fmt=png\",\n    \"introduct\"  => \"test\",\n    \"districtid\" => \"440105\",\n];\n```\n\n> - `$baseInfo`: 门店相关信息\n\n> 事件推送 --- 腾讯地图中创建门店的审核结果。腾讯地图审核周期为 3 个工作日，请在期间内留意审核结果事件推送。提交后未当即返回事件推送即为审核中，请耐心等待。\n\n## 添加门店\n\n```php\n$app->store->create($baseInfo);\n```\n\n示例：\n\n```php\n$baseInfo = [\n    \"poi_id\"             => \"\",\n    \"map_poi_id\"         => \"2880741500279549033\",\n    \"pic_list\"           => \"['list' => ['http://mmbiz.qpic.cn/mmbiz_jpg/tW66AWvE2K4EJxIYOVpiaGOkfg0iayibiaP2xHOChvbmKQD5uh8ymibbEKlTTPmjTdQ8ia43sULLeG1pT2psOfPic4kTw/0?wx_fmt=jpeg']]\",\n    \"contract_phone\"     => \"1111222222\",\n    \"credential\"         => \"22883878-0\",\n    \"qualification_list\" => \"RTZgKZ386yFn5kQSWLTxe4bqxwgzGBjs3OE02cg9CVQk1wRVE3c8fjUFX7jvpi-P\"\n];\n```\n\n> - `$baseInfo`: 门店相关信息。`pic_list` 门店图片，可传多张图片 `pic_list`\n\n> 事件推送 - 创建门店的审核结果\n\n## 更新门店信息\n\n```php\n$app->store->update($baseInfo);\n```\n\n> - `$baseInfo`: 门店相关信息。\n\n> 果要更新门店的图片，实际相当于走一次重新为门店添加图片的流程，之前的旧图片会全部废弃。并且如果重新添加的图片中有与之前旧图片相同的，此时这个图片不需要重新审核。\n"
  },
  {
    "path": "docs/src/5.x/official-account/template_message.md",
    "content": "# 模板消息\n\n模板消息仅用于公众号向用户发送重要的服务通知，只能用于符合其要求的服务场景中，如信用卡刷卡通知，商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。\n\n## 修改账号所属行业\n\n```php\n$app->template_message->setIndustry($industryId1, $industryId2);\n```\n\n## 获取支持的行业列表\n\n```php\n$app->template_message->getIndustry();\n```\n\n## 添加模板\n\n在公众号后台获取 `$shortId` 并添加到账户。\n\n```php\n$app->template_message->addTemplate($shortId);\n```\n\n## 获取所有模板列表\n\n```php\n$app->template_message->getPrivateTemplates();\n```\n\n## 删除模板\n\n```php\n$app->template_message->deletePrivateTemplate($templateId);\n```\n\n## 发送模板消息\n\n```php\n$app->template_message->send([\n        'touser' => 'user-openid',\n        'template_id' => 'template-id',\n        'url' => 'https://easywechat.com',\n        'miniprogram' => [\n                'appid' => 'xxxxxxx',\n                'pagepath' => 'pages/xxx',\n        ],\n        'data' => [\n            'key1' => 'VALUE',\n            'key2' => 'VALUE2',\n            ...\n        ],\n    ]);\n```\n\n> 如果 url 和 miniprogram 字段都传，会优先跳转小程序。\n\n## 发送一次性订阅消息\n\n```php\n$app->template_message->sendSubscription([\n        'touser' => 'user-openid',\n        'template_id' => 'template-id',\n        'url' => 'https://easywechat.com',\n        'scene' => 1000,\n        'data' => [\n            'key1' => 'VALUE',\n            'key2' => 'VALUE2',\n            ...\n        ],\n    ]);\n```\n\n> 如果你想为发送的内容字段指定颜色，你可以将 \"data\" 部分写成下面 4 种不同的样式，不写 `color` 将会是默认黑色：\n\n```php\n'data' => [\n    'foo' => '你好',  // 不需要指定颜色\n    'bar' => ['你好', '#F00'], // 指定为红色\n    'baz' => ['value' => '你好', 'color' => '#550038'], // 与第二种一样\n    'zoo' => ['value' => '你好'], // 与第一种一样\n]\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/tutorial.md",
    "content": "# 快速开始\n\n在我们已经安装完成后，即可很快的开始使用它了，当然你还是有必要明白 PHP 基本知识，如命名空间等，我这里就不赘述了。\n\n我们以完成服务器端验证与接收响应用户发送的消息为例来演示,首先你有必要了解一下微信交互的运行流程：\n\n```\n                                 +-----------------+                       +---------------+\n+----------+                     |                 |    POST/GET/PUT       |               |\n|          | ------------------> |                 | ------------------->  |               |\n|   user   |                     |  wechat server  |                       |  your server  |\n|          | < - - - - - - - - - |                 |                       |               |\n+----------+                     |                 | <- - - - - - - - - -  |               |\n                                 +-----------------+                       +---------------+\n\n```\n\n那么我们要做的就是图中 **微信服务器把用户消息转到我们的自有服务器（虚线返回部分）** 后的处理过程。\n\n## 服务端验证\n\n在微信接入开始有一个 “服务器验证” 的过程，这一步呢，其实就是微信服务器向我们服务器发起一个请求（上图实线部分），传了一个名称为 `echostr` 的字符串过来，我们只需要原样返回就好了。\n\n你也知道，微信后台只能填写一个服务器地址，所以 **服务器验证** 与 **消息的接收与回复**，都在这一个链接内完成交互。\n\n考虑到这些，我已经把验证这一步给封装到 SDK 里了，你可以完全忽略这一步。\n\n下面我们来配置一个基本的服务端，这里假设我们自己的服务器域名叫 `easywechat.com`，我们在服务器上准备这么一个文件`server.php`:\n\n// server.php\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0xxx',\n    'secret' => 'f1c242f4f28f735d4687abb469072xxx',\n    'token' => 'TestToken',\n    'response_type' => 'array',\n    //...\n];\n\n$app = Factory::officialAccount($config);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send();exit; // Laravel 里请使用：return $response;\n\n```\n\n> :heart: 安全模式下请一定要配置 `aes_key`\n\n一个服务端带验证功能的代码已经完成，当然没有对消息做处理，别着急，后面我们再讲。\n\n我们先来分析上面的代码：\n\n```php\n// 引入我们的主项目工厂类。\nuse EasyWeChat\\Factory;\n\n// 一些配置\n$config = [...];\n\n// 使用配置来初始化一个公众号应用实例。\n$app = Factory::officialAccount($config);\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); exit; // Laravel 里请使用：return $response;\n```\n\n最后这一行我有必要详细讲一下：\n\n> 1.  我们的 `$app->server->serve()` 就是执行服务端业务了，那么它的返回值是一个 `Symfony\\Component\\HttpFoundation\\Response` 实例。\n> 2.  我这里是直接调用了它的 `send()` 方法，它就是直接输出（echo）了，我们在一些框架就不能直接输出了，那你就直接拿到 Response 实例后做相应的操作即可，比如 Laravel 里你就可以直接 `return $app->server->serve();`\n\nOK, 有了上面的代码，那么请你按 **[微信官方的接入指引](http://mp.weixin.qq.com/wiki/)** 在公众号后台完成配置并启用，并相应修改上面的 `$config` 的相关配置。\n\n> URL 就是我们的 `http://easywechat.com/server.php`，这里我是举例哦，你可不要填写我的域名。\n\n这样，点击提交验证就 OK 了。\n\n> :heart: 请一定要将微信后台的开发者模式 “**启用**” ！！！！！！看到红色 “**停用**” 才真正的是启用了。\n> 最后，请不要用浏览器访问这个地址，它是给微信服务器访问的，不是给人访问的。\n\n## 接收 & 回复用户消息\n\n那服务端验证通过了，我们就来试一下接收消息吧。\n\n> 在刚刚上面代码最后一行 `$app->server->serve()->send();` 前面，我们调用 `$app->server` 的 `push()` 方法来注册一个消息处理器，这里用到了 **[PHP 闭包](http://php.net/manual/zh/functions.anonymous.php)** 的知识，如果你不熟悉赶紧补课去。\n\n```php\n// ...\n\n$app->server->push(function ($message) {\n    return \"您好！欢迎使用 EasyWeChat!\";\n});\n\n$response = $app->server->serve();\n\n// 将响应输出\n$response->send(); // Laravel 里请使用：return $response;\n\n```\n\n> 注意：send() 方法里已经包含 echo 了，请不要再加 echo 在前面。\n\n好吧，打开你的微信客户端，向你的公众号发送任意一条消息，你应该会收到回复：`您好！欢迎使用 EasyWeChat!`。\n\n> 到了“你的公众号暂时无法提供服务” ？， 好，那检查一下你的日志吧，日志在哪儿？我们的配置里写了日志路径了(`__DIR__.'/wechat.log'`)。 没有这个文件？看看权限哦。\n\n> avel 框架应用时，因 POST 请求默认会有 CSRF 验证，所以需要在 `App\\Http\\Middleware\\VerifyCsrfToken` 的 `except` 数组中添加微信请求，否则会提示“你的公众号暂时无法提供服务”。\n\n一个基本的服务端验证就完成了。\n\n## 总结\n\n1. 所有的应用服务都通过主入口 `EasyWeChat\\Factory` 类来创建：\n\n```php\n\n// 公众号\n$app = Factory::officialAccount($config);\n\n// 小程序\n$app = Factory::miniProgram($config);\n\n// 开放平台\n$app = Factory::openPlatform($config);\n\n// 企业微信\n$app = Factory::work($config);\n\n// 企业微信开放平台\n$app = Factory::openWork($config);\n\n// 微信支付\n$app = Factory::payment($config);\n\n```\n\n## 最后\n\n希望你在使用本 SDK 的时候如果你发现 SDK 的不足，欢迎提交 PR 或者给我[提建议 & 报告问题](https://github.com/overtrue/wechat/issues)。\n"
  },
  {
    "path": "docs/src/5.x/official-account/user-tag.md",
    "content": "# 用户标签\n\n## 获取所有标签\n\n```php\n$app->user_tag->list();\n```\n\n示例：\n\n```php\n$tags = $app->user_tag->list();\n\n// {\n//     \"tags\": [\n//         {\n//             \"id\": 0,\n//             \"name\": \"标签1\",\n//             \"count\": 72596\n//         },\n//         {\n//             \"id\": 1,\n//             \"name\": \"标签2\",\n//             \"count\": 36\n//         },\n//         ...\n//     ]\n// }\n```\n\n## 创建标签\n\n```php\n$app->user_tag->create($name);\n```\n\n示例：\n\n```php\n$app->user_tag->create('测试标签');\n```\n\n## 修改标签信息\n\n```php\n$app->user_tag->update($tagId, $name);\n```\n\n示例：\n\n```php\n$app->user_tag->update(12, \"新的名称\");\n```\n\n## 删除标签\n\n```php\n$app->user_tag->delete($tagId);\n```\n\n## 获取指定 openid 用户所属的标签\n\n```php\n$userTags = $app->user_tag->userTags($openId);\n//\n// {\n//     \"tagid_list\":[\"标签1\",\"标签2\"]\n// }\n```\n\n## 获取标签下用户列表\n\n```php\n$app->user_tag->usersOfTag($tagId, $nextOpenId = '');\n// $nextOpenId：第一个拉取的OPENID，不填默认从头开始拉取\n\n// {\n//   \"count\":2, // 这次获取的粉丝数量\n//   \"data\":{ // 粉丝列表\n//      \"openid\":[\n//          \"ocYxcuAEy30bX0NXmGn4ypqx3tI0\",\n//          \"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"\n//      ]\n//   },\n//   \"next_openid\":\"ocYxcuBt0mRugKZ7tGAHPnUaOW7Y\"//拉取列表最后一个用户的openid\n// }\n```\n\n## 批量为用户添加标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$app->user_tag->tagUsers($openIds, $tagId);\n```\n\n\n## 批量为用户移除标签\n\n```php\n$openIds = [$openId1, $openId2, ...];\n$app->user_tag->untagUsers($openIds, $tagId);\n```\n"
  },
  {
    "path": "docs/src/5.x/official-account/user.md",
    "content": "# 用户\n\n用户信息的获取是微信开发中比较常用的一个功能了，以下所有的用户信息的获取与更新，都是**基于微信的 `openid` 的，并且是已关注当前账号的**，其它情况可能无法正常使用。\n\n## 获取用户信息\n\n获取单个：\n\n```php\n$user = $app->user->get($openId);\n```\n\n获取多个：\n\n```php\n$users = $app->user->select([$openId1, $openId2, ...]);\n```\n\n## 获取用户列表\n\n```php\n$app->user->list($nextOpenId = null);  // $nextOpenId 可选\n```\n\n示例：\n\n```php\n $users = $app->user->list();\n\n// result\n {\n  \"total\": 2,\n  \"count\": 2,\n  \"data\": {\n    \"openid\": [\n      \"OPENID1\",\n      \"OPENID2\"\n    ]\n  },\n  \"next_openid\": \"NEXT_OPENID\"\n}\n```\n\n## 修改用户备注\n\n```php\n$app->user->remark($openId, $remark); // 成功返回boolean\n```\n\n示例：\n\n```php\n$app->user->remark($openId, \"僵尸粉\");\n```\n\n## 拉黑用户\n\n```php\n$app->user->block('openidxxxxx');\n// 或者多个用户\n$app->user->block(['openid1', 'openid2', 'openid3', ...]);\n```\n\n## 取消拉黑用户\n\n```php\n$app->user->unblock('openidxxxxx');\n// 或者多个用户\n$app->user->unblock(['openid1', 'openid2', 'openid3', ...]);\n```\n\n## 获取黑名单\n\n```php\n$app->user->blacklist($beginOpenid = null); // $beginOpenid 可选\n```\n\n## 账号迁移 openid 转换\n\n账号迁移请从这里了解：https://kf.qq.com/product/weixinmp.html#hid=2488\n\n微信用户关注不同的公众号，对应的 OpenID 是不一样的，迁移成功后，粉丝的 OpenID 以目标帐号（即新公众号）对应的 OpenID 为准。但开发者可以通过开发接口转换 OpenID，开发文档可以参考：\n提供一个 openid 转换的 API 接口，当帐号迁移后，可以通过该接口：\n\n1. 将原帐号粉丝的 openid 转换为新帐号的 openid。\n2. 将有授权关系用户的 openid 转换为新帐号的 openid。\n3. 将卡券关联用户的 openid 转换为新帐号的 openid。\n\n> - ◆ 原帐号：准备要迁移的帐号，当审核完成且管理员确认后即被回收。\n> - ◆ 新帐号：用来接纳粉丝的帐号。新帐号在整个流程中均能正常使用。\n\n一定要按照下面的步骤来操作。\n\n1. 一定要在原帐号被冻结之前，最好是准备提交审核前，获取原帐号的用户列表。如果没有原帐号的用户列表，用不了转换工具。如果原账号被回收，这时候也没办法调用接口获取用户列表。\n\n如何获取用户列表见这里：https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840\n\n2. 转换 openid 的 API 接口如下，可在帐号迁移审核完成后开始调用，并最多保留 15 天。若帐号迁移没完成，调用时无返回结果或报错。帐号迁移 15 天后，该转换接口将会失效、无法拉取到数据。\n\n```php\n$app->user->changeOpenid($oldAppId, $openidList);\n```\n\n返回值样例：\n\n```json\n{\n  \"errcode\": 0,\n  \"errmsg\": \"ok\",\n  \"result_list\": [\n    {\n      \"ori_openid\": \"oEmYbwN-n24jxvk4Sox81qedINkQ\",\n      \"new_openid\": \"o2FwqwI9xCsVadFah_HtpPfaR-X4\",\n      \"err_msg\": \"ok\"\n    },\n    {\n      \"ori_openid\": \"oEmYbwH9uVd4RKJk7ZZg6SzL6tTo\",\n      \"err_msg\": \"ori_openid error\"\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/src/5.x/open-platform/authorizer-delegate.md",
    "content": "# 代授权方实现业务\n\n> 授权方已经把公众号、小程序授权给你的开放平台第三方平台了，接下来的代授权方实现业务只需一行代码即可获得授权方实例。\n\n## 实例化\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // ...\n];\n\n$openPlatform = Factory::openPlatform($config);\n```\n\n### 获取授权方实例\n\n```php\n// 代公众号实现业务\n$officialAccount = $openPlatform->officialAccount(string $appId, string $refreshToken);\n// 代小程序实现业务\n$miniProgram = $openPlatform->miniProgram(string $appId, string $refreshToken);\n```\n\n> $appId 为授权方公众号 APPID，非开放平台第三方平台 APPID\n>\n> $refreshToken 为授权方的 refresh_token，可通过 [获取授权方授权信息](https://easywechat.com/docs/master/open-platform/index#heading-h2-2) 接口获得。\n\n### 帮助授权方管理开放平台账号\n\n```php\n// 代公众号实现业务\n$account = $officialAccount->account;\n// 代小程序实现业务\n$account = $miniProgram->account;\n\n// 创建开放平台账号\n// 并绑定公众号或小程序\n$result = $account->create();\n\n// 将公众号或小程序绑定到指定开放平台帐号下\n$result = $account->bindTo($openAppId);\n\n// 将公众号/小程序从开放平台帐号下解绑\n$result = $account->unbindFrom($openAppid);\n\n// 获取公众号/小程序所绑定的开放平台帐号\n$result = $account->getBinding();\n```\n\n> 授权第三方平台注册的开放平台帐号只可用于获取用户 unionid 实现用户身份打通。\n>\n> 第三方平台不可操作（包括绑定/解绑）通过 open.weixin.qq.com 线上流程注册的开放平台帐号。\n>\n> 公众号只可将此权限集授权给一个第三方平台，授权互斥。\n\n接下来的 API 调用等操作和公众号、小程序的开发一致，请移步到[公众号](https://easywechat.com/docs/master/official-account/index)或[小程序](https://easywechat.com/docs/master/mini-program/index)开发章节继续进行开发吧。\n\n### 代码示例\n\n```php\n// 假设你的公众号消息与事件接收 URL 为：https://easywechat.com/$APPID$/callback ...\n\nRoute::post('{appId}/callback', function ($appId) {\n    // ...\n    $officialAccount = $openPlatform->officialAccount($appId);\n    $server = $officialAccount->server; // ❗️❗️  这里的 server 为授权方的 server，而不是开放平台的 server，请注意！！！\n\n    $server->push(function () {\n        return 'Welcome!';\n    });\n\n    return $server->serve();\n});\n\n// 调用授权方业务例子\nRoute::get('how-to-use', function () {\n    $officialAccount = $openPlatform->officialAccount('已授权的公众号 APPID', 'Refresh-token');\n    // 获取用户列表：\n    $officialAccount->user->list();\n\n    $miniProgram = $openPlatform->miniProgram('已授权的小程序 APPID', 'Refresh-token');\n    // 根据 code 获取 session\n    $miniProgram->auth->session('js-code');\n    // 其他同理\n});\n```\n"
  },
  {
    "path": "docs/src/5.x/open-platform/index.md",
    "content": "# 微信开放平台第三方平台\n\n此页涉及接口信息与说明请参见：[授权流程技术说明 - 官方文档](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=)\n\n# 微信开放平台第三方平台\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Factory;\n\n$config = [\n  'app_id'   => '开放平台第三方平台 APPID',\n  'secret'   => '开放平台第三方平台 Secret',\n  'token'    => '开放平台第三方平台 Token',\n  'aes_key'  => '开放平台第三方平台 AES Key'\n];\n\n$openPlatform = Factory::openPlatform($config);\n```\n\n## 获取用户授权页 URL\n\n```php\n$openPlatform->getPreAuthorizationUrl('https://easywechat.com/callback'); // 传入回调URI即可\n```\n\n## 使用授权码换取接口调用凭据和授权信息\n\n在用户在授权页授权流程完成后，授权页会自动跳转进入回调URI，并在URL参数中返回授权码和过期时间，如：(https://easywechat.com/callback?auth_code=xxx&expires_in=600)\n\n```php\n$openPlatform->handleAuthorize(string $authCode = null);\n```\n\n> $authCode 不传的时候会获取 url 中的 auth_code 参数值\n\n## 获取授权方的帐号基本信息\n\n```php\n$openPlatform->getAuthorizer(string $appId);\n```\n\n## 获取授权方的选项设置信息\n\n```php\n$openPlatform->getAuthorizerOption(string $appId, string $name);\n```\n\n## 设置授权方的选项信息\n\n```php\n$openPlatform->setAuthorizerOption(string $appId, string $name, string $value);\n```\n\n> 该API用于获取授权方的公众号或小程序的选项设置信息，如：地理位置上报，语音识别开关，多客服开关。注意，获取各项选项设置信息，需要有授权方的授权，详见权限集说明。\n\n\n## 获取已授权的授权方列表\n\n```php\n$openPlatform->getAuthorizers(int $offset = 0, int $count = 500)\n```\n"
  },
  {
    "path": "docs/src/5.x/open-platform/server.md",
    "content": "# 服务端\n\n## 第三方平台推送事件\n\n公众号第三方平台推送的有四个事件：\n\n> 如已经授权的公众号、小程序再次进行授权，而未修改已授权的权限的话，是没有相关事件推送的。\n\n​\t授权成功 `authorized`\n\n​\t授权更新 `updateauthorized`\n\n​\t授权取消 `unauthorized`\n\n​\tVerifyTicket  `component_verify_ticket`\n\nSDK 默认会处理事件 `component_verify_ticket` ，并会缓存 `verify_ticket` 所以如果你暂时不需要处理其他事件，直接这样使用即可：\n\n```php\n$server = $openPlatform->server;\n\nreturn $server->serve();\n```\n\n## 自定义消息处理器\n\n> *消息处理器详细说明见公众号开发 - 服务端一节*\n\n```php\nuse EasyWeChat\\OpenPlatform\\Server\\Guard;\n\n$server = $openPlatform->server;\n\n// 处理授权成功事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_AUTHORIZED);\n\n// 处理授权更新事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_UPDATE_AUTHORIZED);\n\n// 处理授权取消事件\n$server->push(function ($message) {\n    // ...\n}, Guard::EVENT_UNAUTHORIZED);\n```\n\n### 示例（Laravel 框架）\n\n```php\n// 假设你的开放平台第三方平台设置的授权事件接收 URL 为: https://easywechat.com/open-platform （其他事件推送同样会推送到这个 URL）\nRoute::post('open-platform', function () { // 关闭 CSRF\n    // $openPlatform 为你实例化的开放平台对象，此处省略实例化步骤\n    return $openPlatform->server->serve(); // Done!\n});\n\n// 处理事件\nuse EasyWeChat\\OpenPlatform\\Server\\Guard;\nRoute::post('open-platform', function () {\n    $server = $openPlatform->server;\n    // 处理授权成功事件，其他事件同理\n    $server->push(function ($message) {\n        // $message 为微信推送的通知内容，不同事件不同内容，详看微信官方文档\n        // 获取授权公众号 AppId： $message['AuthorizerAppid']\n        // 获取 AuthCode：$message['AuthorizationCode']\n        // 然后进行业务处理，如存数据库等...\n    }, Guard::EVENT_AUTHORIZED);\n\n    return $server->serve();\n});\n```\n"
  },
  {
    "path": "docs/src/5.x/open-work/index.md",
    "content": "# 企业微信第三方服务商\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Factory;\n\n$config = [\n     'corp_id'              => '服务商的corpid',\n     'secret'               => '服务商的secret，在服务商管理后台可见',\n     'suite_id'             => '以ww或wx开头应用id',\n     'suite_secret'         => '应用secret',\n     'token'                => '应用的Token',\n     'aes_key'              => '应用的EncodingAESKey',\n     'reg_template_id'      => '注册定制化模板ID',\n     'redirect_uri_install' => '安装应用的回调url（可选）', \n     'redirect_uri_single'  => '单点登录回调url （可选）', \n     'redirect_uri_oauth'   => '网页授权第三方回调url （可选）', \n     \n];\n\n$app = Factory::openWork($config);\n```\n\n"
  },
  {
    "path": "docs/src/5.x/open-work/provider.md",
    "content": "# 服务商相关接口\n\n## 单点登录\n\n\n### 获取从第三方单点登录连接\n\n```php\n$app->provider->getLoginUrl(string $redirectUri = '', string $userType = 'admin', string $state = ''); //$redirectUri 回调地址  $userType支持登录的类型\n```\n\n### 获取登录用户信息\n\n```php\n$app->provider->getLoginInfo(string $authCode); //$authCode oauth2.0授权企业微信管理员登录产生的code，最长为512字节。只能使用一次，5分钟未被使用自动过期\n```\n\n## 注册定制化 \n\n### 获取注册码\n\n```php\n$app->provider->getRegisterCode(\n                        string $corpName = '', //企业名称\n                        string $adminName = '',//管理员姓名\n                        string $adminMobile = '',//管理员手机号\n                        string $state = ''//自定义的状态值\n                    ); \n```\n\n### 获取注册Uri\n\n```php\n$app->provider->getRegisterUri(string $registerCode = ''); //$registerCode 注册码\n```\n\n### 查询注册状态\n\n```php\n$app->provider->getRegisterInfo(string $registerCode); //$registerCode 注册码\n```\n\n### 设置授权应用可见范围\n\n```php\n$app->provider->setAgentScope(\n                        string $accessToken, //查询注册状态接口返回的access_token\n                        string $agentId, //\t授权方应用id\n                        array $allowUser = [], //应用可见范围（成员）若未填该字段，则清空可见范围中成员列表\n                        array $allowParty = [], //\t应用可见范围（部门）若未填该字段，则清空可见范围中部门列表\n                        array $allowTag = [] //应用可见范围（标签）若未填该字段，则清空可见范围中标签列表\n                    )\n```\n\n### 设置通讯录同步完成\n\n```php\n$app->provider->contactSyncSuccess(string $accessToken); //$accessToken //查询注册状态接口返回的access_token\n```\n\n### 通讯录单个搜索\n\n```php\n$app->provider->searchContact(\n                         string $corpId, //查询的企业corpid\n                         string $queryWord, //搜索关键词。当查询用户时应为用户名称、名称拼音或者英文名；当查询部门时应为部门名称或者部门名称拼音\n                         string $agentId, //授权方应用id\n                         int $offset = 0, //查询的偏移量，每次调用的offset在上一次offset基础上加上limit\n                         int $limit = 50, //查询返回的最大数量，默认为50，最多为200，查询返回的数量可能小于limit指定的值\n                         int $queryType = 0, //查询类型 1：查询用户，返回用户userid列表 2：查询部门，返回部门id列表。 不填该字段或者填0代表同时查询部门跟用户\n                         $fullMatchField = null //如果需要精确匹配用户名称或者部门名称或者英文名，不填则默认为模糊匹配；1：匹配用户名称或者部门名称 2：匹配用户英文名\n                     )\n```\n"
  },
  {
    "path": "docs/src/5.x/open-work/server.md",
    "content": "# 服务端\n\n## 企业微信第三方回调协议\n\nSDK 默认会处理事件 `suite_ticket` ，并会缓存 `suite_ticket`\n\n> 需要注意的是：授权成功、变更授权、取消授权通知时间的响应必须在 1000ms 内完成，以保证用户安装应用的体验。建议在接收到此事件时 立即回应企业微信，之后再做相关业务的处理。\n\n```php\n$server = $app->server;\n\n$server->push(function ($message) {\n    //指令回调\n    if (isset($message['InfoType'])) {\n        switch ($message['InfoType']) {\n            //推送suite_ticket\n            case 'suite_ticket':\n                break;\n            //授权成功通知\n            case 'create_auth':\n                break;\n            //变更授权通知\n            case 'cancel_auth':\n                break;\n            //通讯录事件通知\n            case 'change_contact':\n                switch ($message['ChangeType']) {\n                    case 'create_user':\n                        return '新增成员事件';\n                        break;\n                    case 'update_user':\n                        return '更新成员事件';\n                        break;\n                    case 'delete_user':\n                        return '删除成员事件';\n                        break;\n                    case 'create_party':\n                        return '新增部门事件';\n                        break;\n                    case 'update_party':\n                        return '更新部门事件';\n                        break;\n                    case 'delete_party':\n                        return '删除部门事件';\n                        break;\n                    case 'update_tag':\n                        return '标签成员变更事件';\n                        break;\n                }\n                break;\n            default:\n                return 'fail';\n                break;\n        }\n    }\n\n    //数据回调\n    if(isset($message['MsgType'])){\n        switch ($message['MsgType']) {\n            case 'event':\n                return '事件消息';//详情 https://work.weixin.qq.com/api/doc/90001/90143/90376#%E5%88%A0%E9%99%A4%E6%88%90%E5%91%98%E4%BA%8B%E4%BB%B6\n                break;\n            case 'text':\n                return '文本消息';//详情 https://work.weixin.qq.com/api/doc/90001/90143/90375#%E5%9B%BE%E7%89%87%E6%B6%88%E6%81%AF\n                break;\n            case 'image':\n                return '图片消息';\n                break;\n                //等等...不再一一举例\n            default:\n                return '其他消息';\n                break;\n        }\n    }\n\n});\n$response = $server->serve();\n$response->send();\n```\n"
  },
  {
    "path": "docs/src/5.x/open-work/service.md",
    "content": "# 第三方应用接口\n\n## 获取应用suite_access_token\n\n```php\n$app->suite_access_token->getToken()\n```\n\n## 获取预授权码\n\n```php\n$app->corp->getPreAuthCode();\n```\n\n## 设置授权配置\n\n```php\n$app->corp->setSession(string $preAuthCode, array $sessionInfo);\n```\n\n## 获取企业永久授权码\n\n```php\n$app->corp->getPermanentByCode(string $preAuthCode); //传入临时授权码\n```\n\n## 获取企业授权信息\n\n```php\n$app->corp->getAuthorization(string $authCorpId, string $permanentCode); //$authCorpId 授权的企业corp_id $permanentCode 授权的永久授权码\n```\n\n## 获取应用的管理员列表\n\n```php\n$app->corp->getManagers(string $authCorpId, string $agentId); //$authCorpId 授权的企业corp_id  $agentId 授权方安装的应用agentid\n```\n\n##  网页授权登录第三方\n\n### 构造第三方oauth2链接\n\n```php\n//$redirectUri 回调uri 这里可以覆盖 默认读取配置文件\n//$scope 应用授权作用域。\n//$state 自定义安全值\n$app->corp->getOAuthRedirectUrl(string $redirectUri = '', string $scope = 'snsapi_userinfo', string $state = null); \n```\n\n### 第三方根据code获取企业成员信息\n\n```php\n$app->corp->getUserByCode(string $code); \n```\n\n### 第三方使用user_ticket获取成员详情\n\n```php\n$app->corp->getUserByTicket(string $userTicket); \n```\n"
  },
  {
    "path": "docs/src/5.x/open-work/work.md",
    "content": "# 企业\n\n\n### 获取授权企业的相关信息\n\n\n```php\n\n$work = $app->work('授权企业的corp_id','授权企业的永久授权码');\n\n```\n\n然后就可以像企业微信一样 获取相关的数据信息 "
  },
  {
    "path": "docs/src/5.x/overview.md",
    "content": "# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。\n\nEasyWeChat 的安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n### 环境需求\n\n> - PHP >= 7.4\n> - [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n> - [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n> - [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n> - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\n### 加入我们\n\n[EasyWeChat SDK 交流群](http://shang.qq.com/wpa/qunwpa?idkey=b4dcf3ec51a7e8c3c3a746cf450ce59895e5c4ec4fbcb0f80c2cd97c3c6e63e9) ID: 319502940\n\n> 为了避免广告及不看文档用户，加群需要付费，所以请使用 能支持群费的客户端。\n> 另外：付费加群不代表我们有责任在群里回答你的问题，所以请认真阅读微信官方文档与 SDK 使用文档再使用，否则提的低级问题不会有人理你\n> 不喜勿加，谢谢！\n> 除非你发现了明确的 Bug，否则不要在群里 @ 我 :pray:\n\n你有以下两种方式加入到我们中来，为广大开发者提供更优质的免费开源的服务：\n\n> - **贡献代码**：我们的代码都在 [overtrue/wechat](https://github.com/overtrue/wechat) ，你可以提交 PR 到任何一个项目，当然，前提是代码质量必须是 OK 的。\n> - **翻译或补充文档**：我们的文档在：[w7corp/EasyWeChat](https://github.com/w7corp/easywechat/tree/master/docs)，你可以选择补充文档或者参与英文文档的翻译，目前有 `zh-cn` 与 `en` 两个分支，你可以提交对应的 PR 到目标分支参与翻译工作。\n\n### 开始之前\n\n我们提供了视频教程：<https://easywechat.com/tutorials> 当然，我还是建议你具备以下基础知识，否则可能没有那么快上手。\n\n本 SDK 不是一个全新再造的东西，所以我不会从 0 开始教会你开发微信，你完全有必要在使用本 SDK 前做好以下工作：\n\n> - 具备 PHP 基础知识，不要连闭包是啥都不明白，可以参考我在知乎的回答: [想要开发自己的 PHP 框架需要那些知识储备？](http://www.zhihu.com/question/26635323/answer/33812516)\n> - 熟悉 PHP 常见的知识：自动加载、composer 的使用、JSON 处理、Curl 的使用等；\n> - **仔细阅读并看懂**[微信官方文档](http://mp.weixin.qq.com/wiki/13/80a1a25adbc46faf2716774c423b3151.html) [微信开放平台文档](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318292&token=&lang=zh_CN)；\n> - 明白微信接口的组成，自有服务器、微信服务器、公众号（还有其它各种号）、测试号、以及通信原理（交互过程）；\n> - 了解基本的 HTTP 协议，Header 头、请求方式（GET\\POST\\PUT\\PATCH\\DELETE）等；\n> - 基本的 Debug 技能，查看 php 日志，nginx 日志等。\n\n如果你不具备这些知识，请不要使用，因为用起来会比较痛苦。\n\n另外你有必要看一下以下的链接：\n\n> - <https://learnku.com/laravel/t/535/assertion-people-who-do-not-understand-the-wisdom-of-asking-questions-will-not-graduate-from-junior-programmers>\n> - <http://laravel-china.github.io/php-the-right-way/>\n\n如果你在群里问以下类似的问题，这真的是你没有做好上面的工作：\n\n> - \"为啥我的不行啊，请问服务器日志怎么看啊？\"\n> - \"请问这是什么原因啊？[结果/报错截图]\"\n> - \"请问这个 SDK 怎么用啊？\"\n> - \"谁能告诉我这个 SDK 是怎么安装的啊？\"\n> - \"怎么接收用户发的消息啊？\"\n> - \"为啥我的报这个错啊：Class XXXX not found...\"\n> - ...\n\n我们专门针对一些容易出现的通用问题已经做了汇总： [疑难解答](troubleshooting) ，如果你在问题疑难解答没找到你出现的问题，那么可以在这里提问 [GitHub](https://github.com/overtrue/wechat/issues)，提问请描述清楚你用的版本，你的做法是什么，不然别人没法帮你。\n\n> 不要在 QQ 单独找我提问\\*\\*，除非你是发现了明显的 bug。有问题先审查代码，看文档, 再 google，然后 去群里发个问题，带上你的代码，重现流程，大家有空的会帮忙你解答。谢谢合作！:pray:\n\n### 打赏支持\n\n这是一个开源的项目，我们没有收费服务，你如果觉得你从中获益，简化了你的开发工作，你可以 [打赏](https://github.com/sponsors/overtrue) 来支持我们。\n"
  },
  {
    "path": "docs/src/5.x/payment/bill.md",
    "content": "# 对账单\n\n## 下载对账单\n\n> 调用参数正确会返回一个 `EasyWeChat\\Kernel\\Http\\StreamResponse` 对象，否则会返回相应错误信息\n\nExample:\n\n```php\n$bill = $app->bill->get('20140603'); // type: ALL\n// or\n$bill = $app->bill->get('20140603', 'SUCCESS'); // type: SUCCESS\n\n// 调用正确，`$bill` 为 csv 格式的内容，保存为文件：\n$bill->saveAs('your/path/to', 'file-20140603.csv');\n```\n\n第二个参数为账单类型，参考：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6 中 `bill_type`，默认为 `ALL`\n"
  },
  {
    "path": "docs/src/5.x/payment/contract.md",
    "content": "# 签约\n\n## 公众号签约\n\n> 参数 `appid`, `version`, `timestamp`, `sign` 可不用传入\n\n```php\n$result = $app->contract->web([\n    'mch_id' => '1200009811',\n    'plan_id' => '12535',\n    'contract_code' => '100000',\n    'contract_display_account' => '微信代扣',\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action',\n]);\n```\n\n## APP 签约\n\n```php\n$result = $app->contract->app(array $params);\n```\n\n## H5 签约\n\n```php\n$result = $app->contract->h5(array $params);\n```\n\n## 小程序签约\n\n```php\n$result = $app->jssdk->contractConfig(array $params);\n```\n\n## 申请扣款\n\n```php\n$result = $app->contract->apply(array $params);\n```\n\n## 申请解约\n\n```php\n$result = $app->contract->delete(array $params);\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/index.md",
    "content": "# 支付\n\n你在阅读本文之前确认你已经仔细阅读了：[微信支付 | 商户平台开发文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)。\n\n> 🚨 此版本仅支持微信支付 V2 版接口，V3 版接口请使用 6.x 版本或在支付模块独立使用 [wechatpay/wechatpay](https://packagist.org/packages/wechatpay/wechatpay) 来支持 v2+v3 支付接口。\n\n## 配置\n\n配置在前面的例子中已经提到过了，支付的相关配置如下：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',   // API v2 密钥 (注意: 是v2密钥 是v2密钥 是v2密钥)\n\n    // 如需使用敏感接口（如退款、发送红包等）需要配置 API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n];\n\n$app = Factory::payment($config);\n```\n\n### 服务商\n\n#### 设置子商户信息\n\n```php\n$app->setSubMerchant('sub-merchant-id', 'sub-app-id');  // 子商户 AppID 为可选项\n```\n\n### 刷卡支付\n\n[官方文档](https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10)\n\n```php\n$result = $app->pay([\n    'body' => 'image形象店-深圳腾大- QQ公仔',\n    'out_trade_no' => '1217752501201407033233368018',\n    'total_fee' => 888,\n    'auth_code' => '120061098828009406',\n]);\n```\n\n## 授权码查询 OPENID 接口\n\n```php\n$app->authCodeToOpenid($authCode);\n```\n\n## 沙箱模式\n\n微信支付沙箱环境，是提供给微信支付商户的开发者，用于模拟支付及回调通知。以验证商户是否理解回调通知、账单格式，以及是否对异常做了正确的处理。EasyWeChat SDK 对于这一功能进行了封装，开发者只需一步即可在沙箱模式和常规模式间切换，方便开发与最终的部署。\n\n```php\n// 在实例化的时候传入配置即可\n$app = Factory::payment([\n    // ...\n    'sandbox' => true, // 设置为 false 或注释则关闭沙箱模式\n]);\n\n// 判断当前是否为沙箱模式：\nbool $app->inSandbox();\n```\n\n> 特别注意，沙箱模式对于测试用例有严格要求，若使用的用例与规定不符，将导致测试失败。具体用例要求可关注公众号“微信支付商户接入验收助手”（WXPayAssist）查看。\n"
  },
  {
    "path": "docs/src/5.x/payment/jssdk.md",
    "content": "# JSSDK\n\nJSSDK 模块用于生成调起微信支付以及共享收货地址的调用所需的配置参数。\n\n## 配置\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 前面的appid什么的也得保留哦\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',\n    'cert_path'          => 'path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => 'path/to/your/key',      // XXX: 绝对路径！！！！\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n    // 'device_info'     => '013467007045764',\n    // 'sub_app_id'      => '',\n    // 'sub_merchant_id' => '',\n    // ...\n];\n\n$payment = Factory::payment($config);\n\n$jssdk = $payment->jssdk;\n```\n\n## 生成支付 JS 配置\n\n有三种发起支付的方式：[WeixinJSBridge](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6), [JSSDK](https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_1), [小程序](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7)\n\n1. WeixinJSBridge:\n\n    ```php\n    $json = $jssdk->bridgeConfig($prepayId); // 返回 json 字符串，如果想返回数组，传第二个参数 false\n    ```\n\n    javascript:\n\n    ```js\n    ...\n    WeixinJSBridge.invoke(\n           'getBrandWCPayRequest', <?= $json ?>,\n           function(res){\n               if(res.err_msg == \"get_brand_wcpay_request:ok\" ) {\n                    // 使用以上方式判断前端返回,微信团队郑重提示：\n                    // res.err_msg将在用户支付成功后返回\n                    // ok，但并不保证它绝对可靠。\n               }\n           }\n       );\n    ...\n    ```\n\n2. JSSDK:\n\n    ```php\n    $config = $jssdk->sdkConfig($prepayId); // 返回数组\n    ```\n\n    javascript:\n\n    ```js\n    wx.chooseWXPay({\n        timestamp: <?= $config['timestamp'] ?>,\n        nonceStr: '<?= $config['nonceStr'] ?>',\n        package: '<?= $config['package'] ?>',\n        signType: '<?= $config['signType'] ?>',\n        paySign: '<?= $config['paySign'] ?>', // 支付签名\n        success: function (res) {\n            // 支付成功后的回调函数\n        }\n    });\n    ```\n\n3. 小程序:\n\n    ```php\n    $config = $jssdk->bridgeConfig($prepayId, false); // 返回数组\n    ```\n\n    javascript:\n\n    ```js\n    wx.requestPayment({\n        timeStamp: <?= $config['timeStamp'] ?>, //注意 timeStamp 的格式\n        nonceStr: '<?= $config['nonceStr'] ?>',\n        package: '<?= $config['package'] ?>',\n        signType: '<?= $config['signType'] ?>',\n        paySign: '<?= $config['paySign'] ?>', // 支付签名\n        success: function (res) {\n            // 支付成功后的回调函数\n        }\n    });\n    ```\n\n## 生成共享收货地址 JS 配置\n\n1. 发起 OAuth 授权，获取用户 `$accessToken`,参考网页授权章节。\n\n2. 使用 `$accessToken` 获取配置\n\n```php\n$configForPickAddress = $jssdk->shareAddressConfig($token);\n\n// 拿着这个生成好的配置 $configForPickAddress 去订单页（或者直接显示订单页）写 js 调用了\n// ...\n```\n\n## 生成 APP 支付配置\n\n```php\n$config = $jssdk->appConfig($prepayId);\n```\n\n`$config` 为数组格式，你可以用 API 返回给客户端\n\n# 二维码生成工具推荐\n\n你也许需要生成二维码，那么以下这些供参考：\n\n>  - https://github.com/endroid/QrCode\n>  - https://github.com/Bacon/BaconQrCode\n>  - https://github.com/SimpleSoftwareIO/simple-qrcode (Bacon/BaconQrCode 的 Laravel 版本)\n>  - https://github.com/aferrandini/PHPQRCode\n"
  },
  {
    "path": "docs/src/5.x/payment/micropay.md",
    "content": "# 付款码支付\n\n## 配置\n\n> 请务必先熟悉流程：<https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_1>\n\n\n```php\n$result = $app->pay([\n    'body' => 'image形象店-深圳腾大- QQ公仔',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'auth_code' => '120061098828009406', // 扫码支付付款码，设备读取用户微信中的条码或者二维码信息\n]);\n```\n\n#### 支付结果\n\n付款码支付方式没有回调通知，支付结果直接返回，请参考：[微信付款码支付文档](https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=5_1) 更新您的订单状态。\n"
  },
  {
    "path": "docs/src/5.x/payment/notify.md",
    "content": "# 通知\n\n## 支付结果通知\n\n在用户成功支付后，微信服务器会向该 **订单中设置的回调 URL** 发起一个 POST 请求，请求的内容为一个 XML。里面包含了所有的详细信息，具体请参考：[支付结果通知](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7)\n\n而对于用户的退款操作，在退款成功之后也会有一个异步回调通知。\n\n本 SDK 内预置了相关方法，以方便开发者处理这些通知，具体用法如下：\n\n只需要在控制器中使用 `handlePaidNotify()` 方法，在其中对自己的业务进行处理并向微信服务器发送一个响应。\n\n```php\n$response = $app->handlePaidNotify(function ($message, $fail) {\n    // 你的逻辑\n    return true;\n    // 或者错误消息\n    $fail('Order not exists.');\n});\n\n$response->send(); // Laravel 里请使用：return $response;\n```\n\n这里需要注意的有几个点：\n\n0. 退款结果通知和扫码支付通知的使用方法均类似。\n1. `handlePaidNotify` 只接收一个 [`Closure`](http://php.net/manual/zh/class.closure.php) 匿名函数。\n2. 该匿名函数接收两个参数，这两个参数分别为：\n\n   > - `$message` 为微信推送过来的通知信息，为一个数组；\n   > - `$fail` 为一个函数，触发该函数可向微信服务器返回对应的错误信息，**微信会稍后重试再通知**。\n\n3. 该函数返回值就是告诉微信 **“我是否处理完成”**。如果你触发 `$fail` 函数，那么微信会在稍后再次继续通知你，直到你明确的告诉它：“我已经处理完成了”，**只有**在函数里 `return true;` 才代表处理完成。\n\n4. `handlePaidNotify` 返回值 `$response` 是一个 Response 对象，如果你要直接输出，使用 `$response->send()`, 在一些框架里（如 Laravel）不是输出而是返回：`return $response`。\n\n通常我们的处理逻辑大概是下面这样（**以下只是伪代码**）：\n\n```php\n$response = $app->handlePaidNotify(function($message, $fail){\n    // 使用通知里的 \"微信支付订单号\" 或者 \"商户订单号\" 去自己的数据库找到订单\n    $order = 查询订单($message['out_trade_no']);\n\n    if (!$order || $order->paid_at) { // 如果订单不存在 或者 订单已经支付过了\n        return true; // 告诉微信，我已经处理完了，订单没找到，别再通知我了\n    }\n\n    ///////////// <- 建议在这里调用微信的【订单查询】接口查一下该笔订单的情况，确认是已经支付 /////////////\n\n    if ($message['return_code'] === 'SUCCESS') { // return_code 表示通信状态，不代表支付状态\n        // 用户是否支付成功\n        if (array_get($message, 'result_code') === 'SUCCESS') {\n            $order->paid_at = time(); // 更新支付时间为当前时间\n            $order->status = 'paid';\n\n        // 用户支付失败\n        } elseif (array_get($message, 'result_code') === 'FAIL') {\n            $order->status = 'paid_fail';\n        }\n    } else {\n        return $fail('通信失败，请稍后再通知我');\n    }\n\n    $order->save(); // 保存订单\n\n    return true; // 返回处理完成\n});\n\n$response->send(); // return $response;\n```\n\n> 注意：请把 “支付成功与否” 与 “是否处理完成” 分开，它俩没有必然关系。\n> 比如：微信通知你用户支付完成，但是支付失败了(result_code 为 'FAIL')，你应该**更新你的订单为支付失败**，但是要**告诉微信处理完成**。\n\n## 退款结果通知\n\n使用示例：\n\n```php\n$response = $app->handleRefundedNotify(function ($message, $reqInfo, $fail) {\n    // 其中 $message['req_info'] 获取到的是加密信息\n    // $reqInfo 为 message['req_info'] 解密后的信息\n    // 你的业务逻辑...\n    return true; // 返回 true 告诉微信“我已处理完成”\n    // 或返回错误原因 $fail('参数格式校验错误');\n});\n\n$response->send();\n```\n\n## 扫码支付通知\n\n扫码支付【模式一】：https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4\n\n```php\n// 扫码支付通知接收第三个参数 `$alert`，如果触发该函数，会返回“业务错误”到微信服务器，触发 `$fail` 则返回“通信错误”\n$response = $app->handleScannedNotify(function ($message, $fail, $alert) use ($app) {\n    // 如：$alert('商品已售空');\n    // 如业务流程正常，则要调用“统一下单”接口，并返回 prepay_id 字符串，代码如下\n    $result = $app->order->unify([\n        'trade_type' => 'NATIVE',\n        'product_id' => $message['product_id'],\n        // ...\n    ]);\n\n    return $result['prepay_id'];\n});\n\n$response->send();\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/order.md",
    "content": "# 订单\n\n## 统一下单\n\n没错，什么 H5 支付，公众号支付，扫码支付，支付中签约，全部都是用这个接口下单。\n\n> 参数 `appid`, `mch_id`, `nonce_str`, `sign`, `sign_type` 可不用传入\n\n> 服务商模式下, 需使用 `sub_openid`, 并传入`sub_mch_id` 和`sub_appid`\n\n```php\n$result = $app->order->unify([\n    'body' => '腾讯充值中心-QQ会员充值',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型\n    'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o',\n]);\n\n// $result:\n//{\n//    \"return_code\": \"SUCCESS\",\n//    \"return_msg\": \"OK\",\n//    \"appid\": \"wx2421b1c4390ec4sb\",\n//    \"mch_id\": \"10000100\",\n//    \"nonce_str\": \"IITRi8Iabbblz1J\",\n//    \"openid\": \"oUpF8uMuAJO_M2pxb1Q9zNjWeSs6o\",\n//    \"sign\": \"7921E432F65EB8ED0CE9755F0E86D72F2\",\n//    \"result_code\": \"SUCCESS\",\n//    \"prepay_id\": \"wx201411102639507cbf6ffd8b0779950874\",\n//    \"trade_type\": \"JSAPI\"\n//}\n```\n\n**第二个参数**为是否[支付中签约](https://pay.weixin.qq.com/wiki/doc/api/pap.php?chapter=18_13&index=5)，默认 `false`\n\n> 支付中签约相关参数 `contract_mchid`, `contract_appid`, `request_serial` 可不用传入\n\n```php\n$isContract = true;\n\n$result = $app->order->unify([\n    'body' => '腾讯充值中心-QQ会员充值',\n    'out_trade_no' => '20150806125346',\n    'total_fee' => 88,\n    'spbill_create_ip' => '123.12.12.123', // 可选，如不传该参数，SDK 将会自动获取相应 IP 地址\n    'notify_url' => 'https://pay.weixin.qq.com/wxpay/pay.action', // 支付结果通知网址，如果不设置则会使用配置里的默认地址\n    'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型\n    'openid' => 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o',\n\n    'plan_id' => 123,// 协议模板id\n    'contract_code' => 100001256,// 签约协议号\n    'contract_display_account' => '腾讯充值中心',// 签约用户的名称\n    'contract_notify_url' => 'http://easywechat.com/contract_notify'\n], $isContract);\n\n//$result:\n//{\n//  \"return_code\": \"SUCCESS\",\n//  \"return_msg\": \"OK\",\n//  \"appid\": \"wx123456\",\n//  \"mch_id\": \"10000100\",\n//  \"nonce_str\": \"CfOcMkDFblzulYvI\",\n//  \"sign\": \"B53F4AFEE7FA6AD5739581486A5CB9C9\",\n//  \"result_code\": \"SUCCESS\",\n//  \"prepay_id\": \"wx08175759731015754a5c13791522969400\",\n//  \"trade_type\": \"JSAPI\",\n//  \"plan_id\": \"123\",\n//  \"request_serial\": \"1565258279\",\n//  \"contract_code\": \"100001256\",\n//  \"contract_display_account\": \"腾讯充值中心\",\n//  \"out_trade_no\": \"201908088195558331565258279\",\n//  \"contract_result_code\": \"SUCCESS\"\n//}\n```\n\n## 查询订单\n\n该接口提供所有微信支付订单的查询，商户可以通过该接口主动查询订单状态，完成下一步的业务逻辑。\n\n需要调用查询接口的情况：\n\n> - 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；\n> - 调用支付接口后，返回系统错误或未知交易状态情况；\n> - 调用被扫支付 API，返回 USERPAYING 的状态；\n> - 调用关单或撤销接口 API 之前，需确认支付状态；\n\n### 根据商户订单号查询\n\n```php\n$app->order->queryByOutTradeNumber(\"商户系统内部的订单号（out_trade_no）\");\n```\n\n### 根据微信订单号查询\n\n```php\n$app->order->queryByTransactionId(\"微信订单号（transaction_id）\");\n```\n\n## 关闭订单\n\n> 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为 5 分钟。\n\n```php\n$app->order->close(商户系统内部的订单号（out_trade_no）);\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/profit-sharing.md",
    "content": "# 分账\n> 官方文档 https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=27_1&index=1\n\n```php\nuse EasyWeChat\\Factory;\n$config = [\n\t'app_id'     => '***',\n\t\"secret\"     => \"***\",\n\t'mch_id'     => '***',\n\t'key'        => '***',\n\t'cert_path'  => 'cert.pem',\n\t'key_path'   => 'key.pem',\n\t'notify_url' => 'http://***.com/notify.php',\n];\n$payment = Factory::payment($config);\n```\n\n### 添加接收方\n\n> 商户发起添加分账接收方请求，后续可通过发起分账请求将结算后的钱分到该分账接收方。\n\n```php\n$receiver = [\n\t\"type\"          => \"PERSONAL_OPENID\",\n\t\"account\"       => \"…………\",//PERSONAL_OPENID：个人openid\n\t\"name\"          => \"张三\",//接收方真实姓名\n\t\"relation_type\" => \"PARTNER\"\n];\n$payment->profit_sharing->addReceiver($receiver);\n$receiver = [\n\t\"type\"          => \"MERCHANT_ID\",\n\t\"account\"       => \"132456798\",//MERCHANT_ID：商户ID\n\t\"name\"          => \"商户全称\",//商户全称\n\t\"relation_type\" => \"PARTNER\"\n];\n$payment->profit_sharing->addReceiver($receiver);\n```\n\n### 删除接收方\n\n```php\n$payment->profit_sharing->deleteReceiver($receiver);\n```\n\n### 单次分账\n\n```php\n$transaction_id = \"***\";\n$out_trade_no = \"***\";\n$receivers = [\n\t[\n\t\t\"type\"        => \"PERSONAL_OPENID\",\n\t\t\"account\"     => \"***\",\n\t\t\"amount\"      => 2,\n\t\t\"description\" => \"分到个人\"\n\t],\n\t[\n\t\t\"type\"        => \"MERCHANT_ID\",\n\t\t\"account\"     => \"***\",\n\t\t\"amount\"      => 1,\n\t\t\"description\" => \"分到商户\"\n\t]\n];\n$sharing = $payment->profit_sharing->share($transaction_id,$out_trade_no,$receivers);\n```\n\n### 多次分账\n\n```php\n$payment->profit_sharing->multiShare($transaction_id,$out_trade_no,$receivers);\n```\n\n### 多次分账完结\n\n```php\n$params = [\n\t\"transaction_id\" => \"\",\n\t\"out_order_no\"   => \"\",\n\t\"description\"    => \"\"\n];\n$payment->profit_sharing->markOrderAsFinished($params);\n```\n\n### 分账查询\n\n```php\n$res = $payment->profit_sharing->query($transaction_id,$out_trade_no);\n```\n\n> 查询结果\n\n```\nArray\n(\n    [return_code] => SUCCESS\n    [result_code] => SUCCESS\n    [mch_id] => ***\n    [nonce_str] => 38e92cbe2790642f\n    [sign] => 8904B6440C58785540950F2911500F55C9A94CAC75790B0721B9AA470E6BF9A8\n    [transaction_id] => 4200000589202007249764665257\n    [out_order_no] => 202007241544057945\n    [order_id] => 30000103702020072402011591464\n    [status] => FINISHED\n    [receivers] => [{\"type\":\"MERCHANT_ID\",\"account\":\"***\",\"amount\":7,\"description\":\"解冻给分账方\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172033\"},{\"type\":\"PERSONAL_OPENID\",\"account\":\"***\",\"amount\":2,\"description\":\"分到个人1\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172033\"},{\"type\":\"PERSONAL_OPENID\",\"account\":\"***-g4\",\"amount\":1,\"description\":\"分到郭\",\"result\":\"SUCCESS\",\"finish_time\":\"20200724172034\"}]\n)\n```\n\n### 分账退回\n\n```php\n$out_trade_no = \"***\";//退款订单号\n$out_return_no = \"***\";//系统内部退款单号\n$return_amount = 1;\n$return_account = \"***-g4\";\n$description = \"订单取消\";\n$payment->profit_sharing->returnShare($out_trade_no,$out_return_no,$return_amount,$return_account,$description);\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/redpack.md",
    "content": "# 红包\n\n\n在阅读本文之前确认你已经仔细阅读了：[微信支付 | 现金红包文档 ](https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_1)。\n\n## 配置\n\n与支付接口一样，红包接口也需要配置如下参数，需要特别注意的是，红包相关的全部接口**都需要使用 SSL 证书**，因此**cert_path 以及 cert_key 必须正确配置**。\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'app_id'    => 'you-app-id',\n    'mch_id'    => 'your-mch-id',\n    'key'       => 'key-for-signature',\n    'cert_path' => 'path/to/your/cert.pem',\n    'key_path'  => 'path/to/your/key',\n    // ...\n];\n\n$payment = Factory::payment($config);\n\n$redpack = $payment->redpack;\n```\n\n**服务商模式下需注意**\n\n```php\n// 不能传递第二个参数subAppID，否则微信将一直返回签名错误\n$payment->setSubMerchant($subMerchantID);\n```\n\n## 发送红包\n\n微信的现金红包分为**普通红包**和**裂变红包**两类。SDK 中对其分别进行了封装，同时也提供了一个统一的调用方法。\n\n**默认情况下，通过接口发送的红包金额应该在200元以内，但可以通过在调用发送接口时传递场景 ID (scene_id)来发送特定场景的红包，不同场景红包可以由商户自己登录商户平台设置最大金额。scene_id 的可选值及对应含义可参阅微信支付官方文档。**\n\n\n### 发送普通红包接口\n\n```php\n$redpackData = [\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 1,  //固定为1，可不传\n    'total_amount' => 100,  //单位为分，不小于100\n    'wishing'      => '祝福语',\n    'client_ip'    => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    // ...\n];\n\n$result = $redpack->sendNormal($redpackData);\n```\n\n### 发送裂变红包接口\n\n```php\n$redpackData = [\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 3,  //不小于3\n    'total_amount' => 300,  //单位为分，不小于300\n    'wishing'      => '祝福语',\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    'amt_type'     => 'ALL_RAND',  //可不传\n    // ...\n];\n\n$result = $redpack->sendGroup($redpackData);\n```\n\n### 发送小程序红包接口\n\n```php\n$redpackData = [\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_amount' => 100,  //单位为分，不小于100\n    'wishing'      => '祝福语',\n    'client_ip'    => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    // ...\n];\n\n$result = $redpack->sendMiniprogramNormal($redpackData);\n```\n\n## 红包预下单接口\n\n红包预下单接口是为摇一摇红包接口配合使用的，在开发摇一摇周边的摇红包相关功能时，需要调用本接口获取红包单号。详情参见[官方文档](http://mp.weixin.qq.com/wiki/7/0ddd50ed2421b99fedd071281c074aab.html#.E7.BA.A2.E5.8C.85.E9.A2.84.E4.B8.8B.E5.8D.95.E6.8E.A5.E5.8F.A3)\n\n\n```php\n$redpackData = [\n    'hb_type'      => 'NORMAL',  //NORMAL 或 GROUP\n    'mch_billno'   => 'xy123456',\n    'send_name'    => '测试红包',\n    're_openid'    => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'total_num'    => 1,  //普通红包固定为1，裂变红包不小于3\n    'total_amount' => 100,  //单位为分，普通红包不小于100，裂变红包不小于300\n    'wishing'      => '祝福语',\n    'client_ip'    => '192.168.0.1',  //可不传，不传则由 SDK 取当前客户端 IP\n    'act_name'     => '测试活动',\n    'remark'       => '测试备注',\n    'amt_type'     => 'ALL_RAND',\n    // ...\n];\n\n$result = $redpack->prepare($redpackData);\n```\n\n## 查询红包信息\n\n用于商户对已发放的红包进行查询红包的具体信息以及领取情况 ，普通红包和裂变包均使用这一接口进行查询。\n\n```php\n$mchBillNo = \"商户系统内部的订单号（mch_billno）\";\n$redpack->info($mchBillNo);\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/refund.md",
    "content": "# 退款\n\n## 申请退款\n\n当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，微信支付将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。\n\n注意：\n\n> 1、交易时间超过一年的订单无法提交退款；\n> 2、微信支付退款支持单笔交易分多次退款，多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。一笔退款失败后重新提交，要采用原来的退款单号。总退款金额不能超过用户实际支付金额。\n\n参考：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4\n\n### 根据微信订单号退款\n\n```php\n// 参数分别为：微信订单号、商户退款单号、订单金额、退款金额、其他参数\n$app->refund->byTransactionId(string $transactionId, string $refundNumber, int $totalFee, int $refundFee, array $config = []);\n\n// Example:\n$result = $app->refund->byTransactionId('transaction-id-xxx', 'refund-no-xxx', 10000, 10000, [\n    // 可在此处传入其他参数，详细参数见微信支付文档\n    'refund_desc' => '商品已售完',\n]);\n\n```\n### 根据商户订单号退款\n\n```php\n// 参数分别为：商户订单号、商户退款单号、订单金额、退款金额、其他参数\n$app->refund->byOutTradeNumber(string $number, string $refundNumber, int $totalFee, int $refundFee, array $config = []);\n\n// Example:\n$result = $app->refund->byOutTradeNumber('out-trade-no-xxx', 'refund-no-xxx', 20000, 1000, [\n    // 可在此处传入其他参数，详细参数见微信支付文档\n    'refund_desc' => '退运费',\n]);\n```\n\n> $refundNumber 为商户退款单号，自己生成用于自己识别即可。\n\n## 查询退款\n\n提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用零钱支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。\n\n可通过 4 种不同类型的单号查询：\n\n>  - 微信订单号 => `queryByTransactionId($transactionId)`\n>  - 商户订单号 => `queryByOutTradeNumber($outTradeNumber)`\n>  - 商户退款单号 => `queryByOutRefundNumber($outRefundNumber)`\n>  - 微信退款单号 => `queryByRefundId($refundId)`\n"
  },
  {
    "path": "docs/src/5.x/payment/reverse.md",
    "content": "# 撤销订单\n\n目前只有 **刷卡支付** 有此功能。\n\n> 调用支付接口后请勿立即调用撤销订单API，建议支付后至少15s后再调用撤销订单接口。\n\n## 通过内部订单号撤销订单\n\n```php\n$app->reverse->byOutTradeNumber(\"商户系统内部的订单号（out_trade_no）\");\n```\n\n## 通过微信订单号撤销订单\n\n```php\n$app->reverse->byTransactionId(\"微信的订单号（transaction_id）\");\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/scan-pay.md",
    "content": "## 扫码支付\n\n### 模式一：先生成产品二维码，扫码下单后支付\n\n> 请务必先熟悉流程：<https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4>\n\n#### 生成产品二维码内容\n\n```php\n$content = $app->scheme($productId); // $productId 为你的产品/商品ID，用于回调时带回，自己识别即可\n\n//结果示例：weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX\n```\n\n将 `$content` 生成二维码，SDK 并不内置二维码生成库，使用你熟悉的工具创建二维码即可，比如 PHP 部分有以下工具可以选择：\n\n> - <https://github.com/endroid/qr-code>\n> - <https://github.com/SimpleSoftwareIO/simple-qrcode>\n> - <https://github.com/aferrandini/PHPQRCode>\n\n#### 处理回调\n\n当用户扫码时，你的回调接口会收到一个通知，调用[统一下单接口](https://easywechat.com/5.x/payment/order)创建订单后返回 `prepay_id`，你可以使用下面的代码处理扫码通知：\n\n```php\n// 扫码支付通知接收第三个参数 `$alert`，如果触发该函数，会返回“业务错误”到微信服务器，触发 `$fail` 则返回“通信错误”\n$response = $app->handleScannedNotify(function ($message, $fail, $alert) use ($app) {\n    // 如：$alert('商品已售空');\n    // 如业务流程正常，则要调用“统一下单”接口，并返回 prepay_id 字符串，代码如下\n    $result = $app->order->unify([\n        'trade_type' => 'NATIVE',\n        'product_id' => $message['product_id'], // $message['product_id'] 则为生成二维码时的产品 ID\n        // ...\n    ]);\n\n    return $result['prepay_id'];\n});\n\n$response->send();\n```\n\n用户在手机上付完钱以后，你会再收到**付款结果通知**，这时候请参考：[处理微信支付通知](https://easywechat.com/5.x/payment/notify) 更新您的订单状态。\n\n### 模式二：先下单，生成订单后创建二维码\n\n> ：<https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5>\n\n#### 根据用户选购的商品生成订单\n\n调用[统一下单接口](https://easywechat.com/5.x/payment/order)创建订单：\n\n```php\n$result = $app->order->unify([\n      'trade_type' => 'NATIVE',\n      'product_id' => $message['product_id'], // $message['product_id'] 则为生成二维码时的产品 ID\n      // ...\n  ]);\n```\n\n#### 生成二维码\n\n> 版本 4.1.7+ 支持\n\n从上一步得到的 `$result['code_url']` 得到二维码内容：\n\n将 `$result['code_url']` 生成二维码图片向用户展示即可扫码，生成工具上面自己找一下即可。 SDK 不内置\n\n#### 支付通知\n\n这种方式的通知就只有**付款结果通知**了，这时候请参考：[处理微信支付通知](https://easywechat.com/5.x/payment/notify) 更新您的订单状态。\n"
  },
  {
    "path": "docs/src/5.x/payment/security.md",
    "content": "# 安全与风控\n\n> EasyWeChat 4.0.7+\n\n## 获取 RSA 公钥\n\n```php\n$result = $app->security->getPublicKey();\n\n// 存成文件\n\nfile_put_contents('./public.pem', $result);\n```\n\n将会得到 PKCS#1 格式密钥：\n\n```\n-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEArT82k67xybiJS9AD8nNAeuDYdrtCRaxkS6cgs8L9h83eqlDTlrdw\nzBVSv5V4imTq/URbXn4K0V/KJ1TwDrqOI8hamGB0fvU13WW1NcJuv41RnJVua0QA\nlS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+2kUWG94FccArNnBtBqqvFncXgQsm98JB\n3a62NbS1ePP/hMI7Kkz+JNMyYsWkrOUFDCXAbSZkWBJekY4nGZtK1erqGRve8Jbx\nTWirAm/s08rUrjOuZFA21/EI2nea3DidJMTVnXVPY2qcAjF+595shwUKyTjKB8v1\nREPB3hPF1Z75O6LwuLfyPiCrCTmVoyfqjwIDAQAB\n-----END RSA PUBLIC KEY-----\n```\n\n使用 OpenSSL 转换 PKCS#1 为 PKCS#8 格式密钥：\n\n```shell\nopenssl rsa -RSAPublicKey_in -in public.pem -out public.pem\n```\n\nPKCS#8 格式密钥：\n\n```\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT82k67xybiJS9AD8nNA\neuDYdrtCRaxkS6cgs8L9h83eqlDTlrdwzBVSv5V4imTq/URbXn4K0V/KJ1TwDrqO\nI8hamGB0fvU13WW1NcJuv41RnJVua0QAlS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+\nlkUWG94FccArNnBtBqqvFncXgQsm98JB3a42NbS1ePP/hMI7Kkz+JNMyYsWkrOUF\nDCXAbSZkWBJekY4nGZtK1erqGRve8JbxTWirAm/s08rUrjOuZFA21/EI2nea3Did\nJMTVnXVPY2qcAjF+595shwUKyTjKB8v1REPB3hPF1Z75O6LwuLfyPiCrCTmVoyfq\njwIDAQAB\n-----END PUBLIC KEY-----\n```\n"
  },
  {
    "path": "docs/src/5.x/payment/transfer.md",
    "content": "# 企业付款\n\n> EasyWeChat 4.0.7+\n\n该模块需要用到双向证书，请参考：https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=4_3\n\n## 企业付款到用户零钱\n\n```php\n$app->transfer->toBalance([\n    'partner_trade_no' => '1233455', // 商户订单号，需保持唯一性(只能是字母或者数字，不能包含有符号)\n    'openid' => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',\n    'check_name' => 'FORCE_CHECK', // NO_CHECK：不校验真实姓名, FORCE_CHECK：强校验真实姓名\n    're_user_name' => '王小帅', // 如果 check_name 设置为FORCE_CHECK，则必填用户真实姓名\n    'amount' => 10000, // 企业付款金额，单位为分\n    'desc' => '理赔', // 企业付款操作说明信息。必填\n]);\n```\n\n## 查询付款到零钱的订单\n\n```php\n$partnerTradeNo = 1233455;\n$app->transfer->queryBalanceOrder($partnerTradeNo);\n```\n\n\n## 企业付款到银行卡\n\n企业付款到银行卡需要对银行卡号与姓名进行 RSA 加密，所以这里需要先下载 RSA 公钥到本地（服务器），我们提供了一个命令行工具：[EasyWeChat/console](https://github.com/EasyWeChat/console)，请使用 composer 安装完成。\n\n```bash\n$ composer require easywechat/console -vvv\n```\n\n然后，在项目根目录执行以下命令下载公钥：\n\n```bash\n$ ./vendor/bin/easywechat payment:rsa_public_key \\\n  >  --mch_id=14339221228 \\\n  >  --api_key=36YTbDmLgyQ52noqdxgwGiYy \\\n  >  --cert_path=/Users/overtrue/www/demo/apiclient_cert.pem \\\n  >  --key_path=/Users/overtrue/www/demo/apiclient_key.pem\n```\n\n将会在当前目录生成一个 `./public-14339221228.pem` 文件，你可以将它移动到敏感目录，然后在支付配置文件中加如以下选项：\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    // 必要配置\n    'app_id'             => 'xxxx',\n    'mch_id'             => 'your-mch-id',\n    'key'                => 'key-for-signature',   // API 密钥\n\n    // 如需使用敏感接口（如退款、发送红包等）需要配置 API 证书路径(登录商户平台下载 API 证书)\n    'cert_path'          => '/path/to/your/cert.pem', // XXX: 绝对路径！！！！\n    'key_path'           => '/path/to/your/key',      // XXX: 绝对路径！！！！\n\n    // 将上面得到的公钥存放路径填写在这里\n    'rsa_public_key_path' => '/path/to/your/rsa/publick/key/public-14339221228.pem', // <<<------------------------\n\n    'notify_url'         => '默认的订单回调地址',     // 你也可以在下单时单独设置来想覆盖它\n];\n\n$app = Factory::payment($config);\n```\n\n```php\n$result = $app->transfer->toBankCard([\n    'partner_trade_no' => '1229222022',\n    'enc_bank_no' => '6214830901234564', // 银行卡号\n    'enc_true_name' => '安正超',   // 银行卡对应的用户真实姓名\n    'bank_code' => '1001', // 银行编号\n    'amount' => 100,  // 单位：分\n    'desc' => '测试',\n]);\n\n```\n\n## 查询付款到银行卡的订单\n\n```php\n$partnerTradeNo = 1233455;\n$app->transfer->queryBankCardOrder($partnerTradeNo);\n```\n\n"
  },
  {
    "path": "docs/src/5.x/sidebar.js",
    "content": "exports = module.exports = [\n  {\n    text: '开始使用',\n    collapsible: true,\n    items: [\n      { text: '概述', link: '/5.x/overview.html' },\n      { text: '安装', link: '/5.x/installation.html' },\n      { text: '在框架中使用', link: '/5.x/integration.html' },\n      { text: '常见问题汇总', link: '/5.x/troubleshooting.html' }\n    ]\n  },\n  {\n    text: '公众号',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/official-account/index.html' },\n      {\n        text: '快速开始',\n        link: '/5.x/official-account/tutorial.html'\n      },\n      {\n        text: '配置',\n        link: '/5.x/official-account/configuration.html'\n      },\n      { text: '基础接口', link: '/5.x/official-account/base.html' },\n      { text: '服务端', link: '/5.x/official-account/server.html' },\n      { text: '消息', link: '/5.x/official-account/messages.html' },\n      {\n        text: '多客服消息转发',\n        link: '/5.x/official-account/message-transfer'\n      },\n      {\n        text: '消息群发',\n        link: '/5.x/official-account/broadcasting.html'\n      },\n      {\n        text: '模板消息',\n        link: '/5.x/official-account/template_message.html'\n      },\n      { text: '用户', link: '/5.x/official-account/user.html' },\n      {\n        text: '用户标签',\n        link: '/5.x/official-account/user-tag.html'\n      },\n      { text: '网页授权', link: '/5.x/official-account/oauth.html' },\n      { text: 'JSSDK', link: '/5.x/basic-services/jssdk.html' },\n      { text: '临时素材', link: '/5.x/basic-services/media.html' },\n      { text: '二维码', link: '/5.x/basic-services/qrcode.html' },\n      { text: '短网址', link: '/5.x/basic-services/url.html' },\n      {\n        text: '素材管理',\n        link: '/5.x/official-account/material.html'\n      },\n      { text: '草稿箱',\n        link: '/5.x/official-account/draft.html'\n      },\n      { text: '菜单', link: '/5.x/official-account/menu.html' },\n      { text: '卡券', link: '/5.x/official-account/card.html' },\n      { text: '门店', link: '/5.x/official-account/poi.html' },\n      {\n        text: '客服',\n        link: '/5.x/official-account/customer_service.html'\n      },\n      {\n        text: '摇一摇周边',\n        link: '/5.x/official-account/shake-around.html'\n      },\n      {\n        text: '数据统计与分析',\n        link: '/5.x/official-account/data_cube.html'\n      },\n      {\n        text: '语义理解',\n        link: '/5.x/official-account/semantic.html'\n      },\n      { text: '自动回复', link: '/5.x/official-account/reply.html' },\n      {\n        text: '评论数据管理',\n        link: '/5.x/official-account/comment.html'\n      },\n      { text: '返佣商品', link: '/5.x/official-account/goods.html' }\n    ]\n  },\n  {\n    text: '微信支付',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/payment/index.html' },\n      { text: '订单', link: '/5.x/payment/order.html' },\n      { text: '退款', link: '/5.x/payment/refund.html' },\n      { text: '账单', link: '/5.x/payment/bill.html' },\n      { text: '通知', link: '/5.x/payment/notify.html' },\n      { text: '红包', link: '/5.x/payment/redpack.html' },\n      { text: '付款码支付', link: '/5.x/payment/micropay.html' },\n      { text: '扫码支付', link: '/5.x/payment/scan-pay.html' },\n      { text: 'JSSDK', link: '/5.x/payment/jssdk.html' },\n      { text: '企业付款', link: '/5.x/payment/transfer.html' },\n      { text: '撤销订单', link: '/5.x/payment/reverse.html' },\n      { text: '安全工具', link: '/5.x/payment/security.html' },\n      { text: '分账', link: '/5.x/payment/profit-sharing.html' }\n    ]\n  },\n  {\n    text: '小程序',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/mini-program/index.html' },\n      { text: '小程序码', link: '/5.x/mini-program/app_code.html' },\n      {\n        text: '客服消息',\n        link: '/5.x/mini-program/customer_service.html'\n      },\n      {\n        text: '数据统计与分析',\n        link: '/5.x/mini-program/data_cube.html'\n      },\n      { text: '微信登录', link: '/5.x/mini-program/auth.html' },\n      {\n        text: '模板消息',\n        link: '/5.x/mini-program/template_message.html'\n      },\n      { text: '消息解密', link: '/5.x/mini-program/decrypt.html' },\n      {\n        text: '内容安全',\n        link: '/5.x/basic-services/content_security.html'\n      },\n      { text: '物流助手', link: '/5.x/mini-program/express.html' },\n      { text: '生物认证', link: '/5.x/mini-program/soter.html' },\n      { text: '插件管理', link: '/5.x/mini-program/plugin.html' },\n      {\n        text: '附近的小程序',\n        link: '/5.x/mini-program/nearby_poi.html'\n      },\n      {\n        text: '订阅消息',\n        link: '/5.x/mini-program/subscribe_message.html'\n      },\n      { text: '直播', link: '/5.x/mini-program/live.html' },\n      {\n        text: '安全风控',\n        link: '/5.x/mini-program/safety_control.html'\n      },\n      {\n        text: 'URL Scheme',\n        link: '/5.x/mini-program/url_scheme.html'\n      },\n      { text: 'URL Link', link: '/5.x/mini-program/url_link.html' },\n      {\n        text: 'Short Link',\n        link: '/5.x/mini-program/short_link.html'\n      },\n      { text: '手机号', link: '/5.x/mini-program/phone_number.html' },\n      {\n        text: '小程序发货信息管理服务',\n        link: '/5.x/mini-program/shipping.html'\n      },\n      { text: '商户功能', link: '/5.x/mini-program/business.html' },\n      { text: 'OCR 文字识别', link: '/5.x/mini-program/ocr.html' },\n      { text: '微信小商店', link: '/5.x/mini-program/mall.html' },\n      { text: '实时日志', link: '/5.x/mini-program/realtime_log.html' },\n      { text: '风险控制', link: '/5.x/mini-program/risk_control.html' },\n      { text: '联盟推广', link: '/5.x/mini-program/union.html' },\n      { text: '动态消息', link: '/5.x/mini-program/activity_message.html' },\n      { text: '搜索功能', link: '/5.x/mini-program/search.html' }\n    ]\n  },\n  {\n    text: '开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/open-platform/index.html' },\n      { text: '服务端', link: '/5.x/open-platform/server.html' },\n      {\n        text: '代授权',\n        link: '/5.x/open-platform/authorizer-delegate.html'\n      }\n    ]\n  },\n  {\n    text: '企业微信',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/wework/index.html' },\n      { text: '服务端', link: '/5.x/wework/server.html' },\n      { text: '应用管理', link: '/5.x/wework/agents.html' },\n      { text: '消息发送', link: '/5.x/wework/message.html' },\n      { text: '通讯录', link: '/5.x/wework/contacts.html' },\n      { text: '网页授权', link: '/5.x/wework/oauth.html' },\n      { text: '客户联系', link: '/5.x/wework/external-contact.html' },\n      { text: '产品图册', link: '/5.x/wework/product.html' },\n      { text: '聊天敏感词', link: '/5.x/wework/intercept.html' },\n      { text: '微信客服', link: '/5.x/wework/kf.html' },\n      { text: '自定义菜单', link: '/5.x/wework/menu.html' },\n      { text: '素材管理', link: '/5.x/wework/media.html' },\n      { text: 'OA', link: '/5.x/wework/oa.html' },\n      { text: '企业互联', link: '/5.x/wework/corp-group.html' },\n      { text: '会话内容存档', link: '/5.x/wework/msg-audit.html' },\n      { text: '电子发票', link: '/5.x/wework/invoice.html' },\n      { text: '小程序', link: '/5.x/wework/mini-program.html' },\n      { text: 'JSSDK', link: '/5.x/wework/jssdk.html' },\n      { text: '群机器人', link: '/5.x/wework/group-robot.html' },\n      { text: '移动端', link: '/5.x/wework/mobile.html' },\n      { text: '企微帐号ID转换', link: '/5.x/wework/to-account.html' },\n      { text: '日程', link: '/5.x/wework/calendar.html' },\n      { text: '微盘', link: '/5.x/wework/wedrive.html' },\n      { text: '群聊管理', link: '/5.x/wework/chat.html' },\n      { text: '入群欢迎语', link: '/5.x/wework/group-welcome-template.html' }\n    ]\n  },\n  {\n    text: '企业微信开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/open-work/index.html' },\n      { text: '服务商接口', link: '/5.x/open-work/provider.html' },\n      { text: '服务商', link: '/5.x/open-work/server.html' },\n      { text: '第三方应用接口', link: '/5.x/open-work/service.html' },\n      { text: '企业相关', link: '/5.x/open-work/work.html' }\n    ]\n  },\n  {\n    text: '小微商户',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/5.x/micro-merchant/index.html' },\n      {\n        text: '获取平台证书',\n        link: '/5.x/micro-merchant/certficates.html'\n      },\n      { text: '图片上传', link: '/5.x/micro-merchant/media.html' },\n      {\n        text: '商户入驻',\n        link: '/5.x/micro-merchant/submit-application.html'\n      },\n      { text: '商户升级', link: '/5.x/micro-merchant/upgrade.html' },\n      {\n        text: '商户信息修改',\n        link: '/5.x/micro-merchant/material.html'\n      },\n      { text: '提现相关', link: '/5.x/micro-merchant/withdraw.html' },\n      {\n        text: '商户配置',\n        link: '/5.x/micro-merchant/merchant-config.html'\n      }\n    ]\n  },\n  {\n    text: '自定义',\n    collapsible: true,\n    items: [\n      {\n        text: 'Access Token',\n        link: '/5.x/customize/access_token.html'\n      },\n      { text: '缓存', link: '/5.x/customize/cache.html' },\n      { text: '模块替换', link: '/5.x/customize/replace-service.html' }\n    ]\n  },\n  {\n    text: '其他',\n    collapsible: true,\n    items: [\n      { text: '常见问题', link: '/5.x/troubleshooting.html' },\n      { text: '参与贡献', link: '/5.x/contributing.html' }\n    ]\n  }\n]\n"
  },
  {
    "path": "docs/src/5.x/troubleshooting.md",
    "content": "# 疑难解答\n\n在微信公众平台开发的道路上，遍布着各种大大小小的坑，有的人掉坑里，几经折腾又爬出来了，然后拍拍屁股走人。然而坑还在那里，还会继续有后来人掉进去……\n\n这，是我们不愿看到的。\n\n所以在这里，我们将陆续将微信开发中可能遇到的各种疑难问题进行汇总，并给出对应的解决办法。一般情况下，这些问题都可以对号入座，轻松地解决。但也不排除特殊情况，这时候你遇到的问题与文中某一个症状一致，但文中所给的解决方案并不凑效，这种情况下就需要发挥你自己的智慧，去……折腾了……\n\n我们期待这一版块为各位的开发带来便利，同时也希望各位本着开源、分享的精神对其进行补充和完善，将各种坑一一填小、填平，让微信开发变得不那么痛苦，甚至，变成一件快乐的事……\n\n## 时区不对\n\n使用命令 `date` 可以在服务器上查看当前时间，如果发现时区不对则需要修改时区：[Setting The Correct Timezone In CentOS And Ubuntu Servers With NTP](https://www.liberiangeek.net/2013/02/setting-the-correct-timezone-in-centos-and-ubuntu-servers-with-ntp/)\n\n## curl: (60) SSL certificate problem: unable to get local issuer certificate\n\n这是 SSL 证书问题所致，在使用 SDK 调用微信支付等相关的操作时可能会遇到报 “SSL certificate problem: unable to get local issuer certificate” 的错误。\n\n微信公众平台提供的文档中建议对部分较敏感的操作接口使用 https 协议进行访问，例如微信支付和红包等接口中涉及到操作商户资金的一些操作。\nwechat SDK 遵循了官方建议，所以在调用这些接口时，除了按照官方文档设置操作证书文件外，还需要保证服务器正确安装了 CA 证书。\n\n1. 下载 CA 证书\n\n你可以从 http://curl.haxx.se/ca/cacert.pem 下载 或者 使用[微信官方提供的证书](https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=4_3)中的 CA 证书 `rootca.pem` 也是同样的效果。\n\n2. 在 `php.ini` 中配置 CA 证书\n\n只需要将上面下载好的 CA 证书放置到您的服务器上某个位置，然后修改 `php.ini` 的 `curl.cainfo` 为该路径（**绝对路径！**），重启 `php-fpm` 服务即可。\n\n```ini\ncurl.cainfo = /path/to/downloaded/cacert.pem\n```\n\n> 注意证书文件**路径为绝对路径**！以自己实际情况为准。\n\n其它修改 HTTP 类源文件的方式是不允许的。\n\n## cURL error 56: SSLRead() return error -9806\n\n目前在 OSX 下，发现使用 HomeBrew 装的 PHP 7.0 有这个问题，解决方案是重新 brew 安装 PHP：\n\n```shell\n$ brew install homebrew/php/php70 --with-homebrew-openssl --with-homebrew-curl --without-snmp -vvv\n```\n\n验证：\n\n```shell\n$ php -i | grep 'OpenSSL support'\n\nOpenSSL support => enabled\nOpenSSL support => enabled\n```\n\n## 支付失败！当前页面的 URL 未注册\n\n这是由于微信支付授权目录未正确配置引起的。此时开发者应该登录微信公众平台，进入**【微信支付】->【开发设置】**进行设置。\n\n1. 公众号可添加 3 个支付授权目录，满足不同应用使用同一个公众号进行支付的业务需求。\n\n2. 正确的**【支付授权目录】**应以 `http://` 或 `https://` 开头，并以正斜杠 `/` 结尾，授权目录所包含的域名**必须经过 ICP 备案**。\n\n3. 支付授权目录需**细化至二级或三级目录**。\n\n4. 所有**实际调起微信支付请求的页面都必须要所配置的支付授权目录之下**。\n\n5. 在开发过程中，也可以使用测试授权目录进行开发测试，此时还**应该将参与测试的个人微信号添加到测试白名单中**，否则将出现对应的错误提示……\n\n> \\*页面**、**目录**、**URL **以及**域名\\*\\*等几个基本概念，并对自己所使用的框架的路由机制有一个大致了解。这样你才会知道自己正在配置的参数是个啥玩意儿，有什么卵用…… :smile:\n\n## redirect_url 参数错误\n\n这是由于程序使用了**网页授权**而公众号没有正确配置**【网页授权域名】**所致。此时你需要登录[微信公众平台](https://mp.weixin.qq.com/)，在【开发】->【接口权限】页面找到**网页授权获取用户基本信息**进行配置并保存。\n\n1. 网页授权域名应该为通过 ICP 备案的有效域名，否则保存时无法通过安全监测。\n\n2. 网页授权域名即程序完成授权获得授权 code 后跳转到的页面的域名，一般情况下为你的业务域名。\n\n3. 网页授权域名配置成功后会立即生效。\n\n4. 公众号的网页授权域名只可配置一个，请合理规划你的业务，否则你会发现……授权域名不够用哈。\n\n## [JSAPI] config: invalid url domain\n\n在使用 JS-SDK 进行开发时，每个页面都需要调用 wx.config() 方法配置 JSPAI 参数。如果没有正确配置 **JSAPI 安全域名**并且开启了调试模式，此时就报此错误。遇到这个问题时，开发者需要登录微信公众平台，进入【公众号设置】->【功能设置】页面，将项目所使用的域名添加至 **【JSAPI 安全域名】**列表中。\n\n1. 一个公众号同时最多可绑定**三个**安全域名，并且这些域名必须为通过 **ICP 备案**的**一级或一级以上**的有效域名。\n\n2. JSAPI 安全域名每个月**限修改三次**，修改任何一个都算，所以，请谨慎操作。\n\n3. 如果需要使用 JSAPI 调起支付功能，则支付目录必须也在所配置的**安全域名之下**，并且需要将支付目录添加至**支付授权目录**。\n\n## token 验证失败、向公众号发送消息无任何反应\n\n相信对接公众号一般是微信开发者进行开发过程中最先进行的工作，而在这看似简单的配置操作中，也可能会掉坑里。\n最常见的两种情况就如下：\n\n1. 确认你 “**启用**” 了开发模式， token 验证通过不代表启用，保存后也不代表启用。看到红色 “**停用**” 才真正的是启用了。\n\n2. 配置好 URL(服务器地址)以及 Token(令牌)后，点击保存时提示**token 验证失败**，出现这种情况的原因有多种，其中之一便是网络不稳定，所以**可尝试多次保存**，若始终无法通过再排查其它可能因素。\n\n3. 配置保存成功之后，向公众号发送消息无任何反应，自己的消息处理程序也没有被调用的记录（无对应日志）。这种情况下如果你尝试**反复停用和启用服务器配置**，可能突然间惊奇地了现，问题莫名其妙的解决了。\n\n4. 使用在线调试工具的消息接口，http://mp.weixin.qq.com/debug/， 只要返回绿色的“**请求成功**”，就代表你的代码没有问题，请**重复上面第 3 项**再测试。\n\n5. **如果你在用什么本地开发工具，或者什么 ngrok 代理到本机这样的开发方式，那么失败就很正常了，微信服务器到你机器的网络延迟太大（还是用服务器开发吧）。**\n\n> 器 TOKEN 验证原理（官方文档有说明）并谨记服务器验证时使用 GET 方式访问，而公众平台向你的服务器发送消息/数据则使用 POST 方式，所以服务器验证成功之后，在某些启用了 CSRF 验证的框架里，接收消息时可能还会遇到 CSRF 相关的问题，请根据自己项目实际情况进行排查。\n> 另外有的朋友的 Laravel 里使用了 laravel-debugbar，这个组件的原理是在页面输出时在后面添加 HTML 来实现的，所以它会改变我们返回给微信的内容，此时要么卸载，要么禁用掉它。\n\n## Maximum function nesting level of '100' reached, aborting!\n\n在使用了 Xdebug 的环境下可能出现这个问题。这是由于 Xdebug 限制函数嵌套的最大层级数（默认为 100），当嵌套次数达到该值便会触发 Xdebug 跳出嵌套并报此错误。\n\n为避免这个问题，**可以将 Xdebug 的 max_nesting_level 参数适当设置大一些**，通常设置为 200 就可以了（当然可根据自己实际情况设置为更大的值）。\n\n如下，修改 php.ini 配置文件后，重启 Apache 或 php-fpm 服务即可。\n\n```ini\nxdebug.max_nesting_level=200\n```\n\n## 扫码支付 获取商户订单信息超时或商户返回 httpcode 非 200!\n\n1.确定签名正确,使用 SDK 基本上不会出什么问题 2.微信调用扫码支付回调链接,使用 POST 方式,确定服务器回调方法是否取消 csrf 验证\n\n## Request access_token fail:{\"errcode\":61023,\"errmsg\":\"refresh_token is invalid hint: [zDNUIA07582974]\"}！\n\n在用户授权时会获得该 authorizer_refresh_token 刷新令牌，而当缓存或数据库存储的该 authorizer_refresh_token 刷新令牌丢失后，可能会出现该问题，微信文档中说明\n\n1.接口调用凭据刷新令牌（在授权的公众号具备 API 权限时，才有此返回值），刷新令牌主要用于第三方平台获取和刷新已授权用户的 access_token，只会在授权时刻提供，请妥善保存。\n\n2.一旦丢失，只能让用户重新授权，才能再次拿到新的刷新令牌(https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=)。\n\n3.为避免该问题，请将存储该刷新令牌的缓存有效期设置为 0(永久存储)，并尽量不要去将该缓存或数据库清空。\n\n如下：以 redis 为例。\n\n```php\n'expire'     => 0,\n```\n\n## 替换 Handler，让 easywechat 支持协程\n\n在 Swoft、IMI 等基于 Swoole 的协程框架中使用 easywechat 时，不免会有一个问题，就是 guzzle 客户端内置的 handler 不支持协程的问题。\n这里，提供一个办法主动替换容器内的 guzzle_handler.\n\n```php\n$app = Factory::miniProgram($config);\n$app['guzzle_handler'] = CoroutineHandler::class;\n```\n\n鉴于有些同学找不到可用的 `CoroutineHandler`，这里提供几个，供大家使用。\n\n- hyperf/guzzle\n- yurunsoft/guzzle-swoole\n- mix/guzzle-hook\n"
  },
  {
    "path": "docs/src/5.x/wework/agents.md",
    "content": "# 应用管理\n\n>  企业微信在17年11月对 API 进行了大量的改动，应用管理部分已经没啥用了\n\n应用管理是企业微信中比较特别的地方，因为它的使用是不基于应用的，或者说基于任何一个应用都能访问这些 API，所以在用法上是直接调用 work 实例的 `agent` 属性。\n\n```php\n$config = [\n    ...\n];\n\n$app = Factory::work($config);\n```\n\n## 应用列表\n\n```php\n$agents = $app->agent->list(); // 测试拿不到内容\n```\n\n## 应用详情\n\n```php\n$agents = $app->agent->get($agentId); // 只能传配置文件中的 id，API 改动所致\n```\n\n## 设置应用\n\n```php\n$agents = $app->agent->set($agentId, ['foo' => 'bar']);\n```\n\n## 设置工作台自定义展示\n\n### 模版类型数据结构\n\n可以通过接口配置展示类型。具体可设置:\n\n- 关键数据型\n- 图片型\n- 列表型\n- webview型\n\n> 官方文档\n> https://open.work.weixin.qq.com/api/doc/90000/90135/92535\n\n### 设置应用在工作台展示的模版\n\n```php\n$params = [\n    'agentid' => 1000005,\n    'type' => 'image', //展示类型\n    'image' => [\n        'url' => 'xxxx',\n        'jump_url' => 'http://www.qq.com',\n        'pagepath' => 'pages/index'\n    ],\n    'replace_user_data' => true\n];\n\n$agents->agent_workbench->setWorkbenchTemplate(array $params);\n```\n\n### 获取应用在工作台展示的模版\n\n```php\n$agentId = 100005;\n\n$agents->agent_workbench->getWorkbenchTemplate(int $agentId);\n```\n\n\n### 设置应用在用户工作台展示的数据\n\n```php\n$params = [\n    'agentid' => 1000005,\n    'userid' => 'test', //员工id\n    'type' => 'keydata', //展示类型\n    'keydata' => [\n        'items' => [\n            [\n                'key' => '待审批',\n                'data' => '2',\n                'jump_url' => 'http://www.qq.com',\n                'pagepath' => 'pages/index'\n            ],\n            [\n                'key' => '带批阅作业',\n                'data' => '4',\n                'jump_url' => 'http://www.qq.com',\n                'pagepath' => 'pages/index'\n            ],\n            [\n                'key' => '成绩录入',\n                'data' => '45',\n                'jump_url' => 'http://www.qq.com',\n                'pagepath' => 'pages/index'\n            ],\n            [\n                'key' => '综合评价',\n                'data' => '98',\n                'jump_url' => 'http://www.qq.com',\n                'pagepath' => 'pages/index'\n            ]\n        ]\n    ]\n];\n\n$agents->agent_workbench->setWorkbenchData(array $params);\n```"
  },
  {
    "path": "docs/src/5.x/wework/calendar.md",
    "content": "# 日程\n\n企业微信日程功能允许应用管理企业内的日程安排，包括创建、更新、删除日程等操作。\n\n## 获取实例\n\n```php\n$calendar = $app->calendar;\n```\n\n## 日程管理\n\n### 创建日程\n\n创建新的日程安排：\n\n```php\n$calendar = [\n    'organizer' => 'organizer_userid',\n    'summary' => '部门会议',\n    'color' => 1,\n    'description' => '讨论下季度工作计划',\n    'shares' => [\n        [\n            'userid' => 'participant1'\n        ],\n        [\n            'userid' => 'participant2'\n        ]\n    ],\n    'start_time' => 1635724800,  // 开始时间戳\n    'end_time' => 1635728400,    // 结束时间戳\n    'cal_id' => '日程ID',\n    'reminders' => [\n        [\n            'is_repeat' => 0,\n            'remind_before_event_secs' => 300,  // 提前5分钟提醒\n            'is_custom_repeat' => 0\n        ]\n    ],\n    'location' => [\n        'meetingroom' => '会议室A',\n        'address' => '公司A座10楼会议室A'\n    ]\n];\n\n$result = $calendar->add($calendar);\n```\n\n**参数说明：**\n- `organizer` string 组织者的userid\n- `summary` string 日程标题\n- `color` int 日程颜色，0-11对应不同颜色\n- `description` string 日程详情描述\n- `shares` array 参与人列表\n- `start_time` int 开始时间戳\n- `end_time` int 结束时间戳\n- `cal_id` string 日程ID（可选）\n- `reminders` array 提醒设置\n- `location` array 地点信息\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"cal_id\": \"wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA\"\n}\n```\n\n### 更新日程\n\n更新已存在的日程：\n\n```php\n$calendarData = [\n    'summary' => '更新后的会议标题',\n    'description' => '更新后的会议内容',\n    'start_time' => 1635731400,\n    'end_time' => 1635735000,\n    'location' => [\n        'meetingroom' => '会议室B',\n        'address' => '公司B座5楼会议室B'\n    ]\n];\n\n$result = $calendar->update('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA', $calendarData);\n```\n\n### 获取日程详情\n\n获取指定日程的详细信息：\n\n```php\n$result = $calendar->get('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"calendar\": {\n        \"cal_id\": \"wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA\",\n        \"organizer\": \"zhangsan\",\n        \"summary\": \"部门会议\",\n        \"color\": 1,\n        \"description\": \"讨论下季度工作计划\",\n        \"start_time\": 1635724800,\n        \"end_time\": 1635728400,\n        \"status\": 1,\n        \"location\": {\n            \"meetingroom\": \"会议室A\",\n            \"address\": \"公司A座10楼会议室A\"\n        },\n        \"shares\": [\n            {\n                \"userid\": \"participant1\"\n            }\n        ]\n    }\n}\n```\n\n### 删除日程\n\n删除指定的日程：\n\n```php\n$result = $calendar->delete('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA');\n```\n\n### 获取日程列表\n\n获取指定时间范围内的日程列表：\n\n```php\n$result = $calendar->list([\n    'offset' => 0,\n    'limit' => 10,\n    'start_time' => 1635724800,  // 可选，开始时间\n    'end_time' => 1635811200     // 可选，结束时间\n]);\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"calendar_list\": [\n        {\n            \"cal_id\": \"wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA\",\n            \"summary\": \"部门会议\",\n            \"start_time\": 1635724800,\n            \"end_time\": 1635728400,\n            \"status\": 1\n        }\n    ]\n}\n```\n\n## 日程操作\n\n### 接受日程邀请\n\n参与者接受日程邀请：\n\n```php\n$result = $calendar->accept('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA');\n```\n\n### 拒绝日程邀请\n\n参与者拒绝日程邀请：\n\n```php\n$result = $calendar->decline('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA');\n```\n\n### 设置日程状态\n\n设置日程的参与状态：\n\n```php\n$result = $calendar->setStatus('wcjgewCwAAqeJcPI1d8Pwbjt7nttzAAA', [\n    'status' => 1,  // 1:接受 2:拒绝 3:待定\n    'userid' => 'participant1'\n]);\n```\n\n## 使用示例\n\n### 创建部门例会\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'corp_id' => 'your-corp-id',\n    'agent_id' => 'your-agent-id',\n    'secret' => 'your-secret',\n    // ...\n];\n\n$app = Factory::work($config);\n$calendar = $app->calendar;\n\n// 创建每周例会\n$meetingData = [\n    'organizer' => 'manager_001',\n    'summary' => '技术部周例会',\n    'color' => 2,  // 蓝色\n    'description' => '本周工作总结和下周计划讨论',\n    'shares' => [\n        ['userid' => 'dev_001'],\n        ['userid' => 'dev_002'],\n        ['userid' => 'dev_003']\n    ],\n    'start_time' => strtotime('next Monday 14:00'),\n    'end_time' => strtotime('next Monday 15:30'),\n    'reminders' => [\n        [\n            'is_repeat' => 0,\n            'remind_before_event_secs' => 900,  // 提前15分钟提醒\n            'is_custom_repeat' => 0\n        ]\n    ],\n    'location' => [\n        'meetingroom' => '技术部会议室',\n        'address' => '研发中心2楼技术部会议室'\n    ]\n];\n\n$result = $calendar->add($meetingData);\n\nif ($result['errcode'] === 0) {\n    $calId = $result['cal_id'];\n    echo \"例会创建成功，日程ID: {$calId}\\n\";\n    \n    // 获取日程详情确认\n    $detail = $calendar->get($calId);\n    if ($detail['errcode'] === 0) {\n        $cal = $detail['calendar'];\n        echo \"会议标题: {$cal['summary']}\\n\";\n        echo \"开始时间: \" . date('Y-m-d H:i:s', $cal['start_time']) . \"\\n\";\n        echo \"结束时间: \" . date('Y-m-d H:i:s', $cal['end_time']) . \"\\n\";\n        echo \"参与人数: \" . count($cal['shares']) . \"\\n\";\n    }\n} else {\n    echo \"创建失败: {$result['errmsg']}\\n\";\n}\n```\n\n### 批量处理日程\n\n```php\n// 获取本周的所有日程\n$startOfWeek = strtotime('Monday this week');\n$endOfWeek = strtotime('Sunday this week 23:59:59');\n\n$calendarList = $calendar->list([\n    'start_time' => $startOfWeek,\n    'end_time' => $endOfWeek,\n    'limit' => 50\n]);\n\nif ($calendarList['errcode'] === 0) {\n    echo \"本周共有 \" . count($calendarList['calendar_list']) . \" 个日程\\n\";\n    \n    foreach ($calendarList['calendar_list'] as $cal) {\n        $startTime = date('m-d H:i', $cal['start_time']);\n        $endTime = date('H:i', $cal['end_time']);\n        \n        echo \"{$startTime}-{$endTime}: {$cal['summary']}\\n\";\n        \n        // 如果是会议室预订类型的会议，检查状态\n        if (strpos($cal['summary'], '会议室') !== false) {\n            $detail = $calendar->get($cal['cal_id']);\n            if ($detail['errcode'] === 0) {\n                $status = $detail['calendar']['status'] ?? 0;\n                echo \"  状态: \" . ($status == 1 ? '正常' : '可能有冲突') . \"\\n\";\n            }\n        }\n    }\n}\n```\n\n### 日程提醒管理\n\n```php\n// 创建有多种提醒的重要会议\n$importantMeeting = [\n    'organizer' => 'ceo_001',\n    'summary' => '董事会会议',\n    'color' => 0,  // 红色，表示重要\n    'description' => '季度业绩汇报和战略讨论',\n    'shares' => [\n        ['userid' => 'cto_001'],\n        ['userid' => 'cfo_001'],\n        ['userid' => 'director_001']\n    ],\n    'start_time' => strtotime('+3 days 09:00'),\n    'end_time' => strtotime('+3 days 12:00'),\n    'reminders' => [\n        [\n            'is_repeat' => 0,\n            'remind_before_event_secs' => 86400,  // 提前1天提醒\n            'is_custom_repeat' => 0\n        ],\n        [\n            'is_repeat' => 0,\n            'remind_before_event_secs' => 3600,   // 提前1小时提醒\n            'is_custom_repeat' => 0\n        ],\n        [\n            'is_repeat' => 0,\n            'remind_before_event_secs' => 300,    // 提前5分钟提醒\n            'is_custom_repeat' => 0\n        ]\n    ],\n    'location' => [\n        'meetingroom' => '董事会议室',\n        'address' => '总部大厦顶层董事会议室'\n    ]\n];\n\n$result = $calendar->add($importantMeeting);\n```\n\n## 注意事项\n\n1. **权限要求**：操作日程需要相应的应用权限\n2. **时间格式**：所有时间参数使用Unix时间戳\n3. **参与人限制**：单个日程的参与人数有上限\n4. **日程冲突**：系统会检测时间冲突但不会自动解决\n5. **提醒限制**：每个日程最多可设置5个提醒\n\n## 最佳实践\n\n1. **合理设置提醒**：根据会议重要性设置不同的提醒时间\n2. **明确描述**：在description中提供详细的会议议程\n3. **地点信息**：提供准确的会议地点信息\n4. **状态管理**：及时更新和处理日程状态变更\n5. **批量操作**：使用列表接口进行批量查询和管理\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| 40003 | 无效的UserID |\n| 40013 | 不合法的CorpID |\n| 60003 | 部门长度不符合限制 |\n| 60102 | UserID已存在 |\n| 85002 | 包含不合法字符 |\n| 85004 | 每企业日程数量超过上限 |"
  },
  {
    "path": "docs/src/5.x/wework/chat.md",
    "content": "# 群聊管理\n\n企业微信群聊管理功能允许应用管理企业内部群聊，包括创建群聊、管理群成员、发送群消息等。\n\n## 获取实例\n\n```php\n$chat = $app->chat;\n```\n\n## 群聊基础操作\n\n### 创建群聊\n\n创建新的企业微信群聊：\n\n```php\n$chatData = [\n    'chatid' => 'project_team_001',\n    'name' => '项目开发小组',\n    'owner' => 'project_manager',\n    'userlist' => ['dev001', 'dev002', 'test001', 'pm001']\n];\n\n$result = $chat->create($chatData);\n```\n\n**参数说明：**\n- `chatid` string 群聊ID，可自定义，不能与已有群聊重复\n- `name` string 群聊名称\n- `owner` string 群主的userid\n- `userlist` array 群成员userid列表\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\"\n}\n```\n\n### 获取群聊信息\n\n获取指定群聊的详细信息：\n\n```php\n$result = $chat->get('project_team_001');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"chat_info\": {\n        \"chatid\": \"project_team_001\",\n        \"name\": \"项目开发小组\",\n        \"owner\": \"project_manager\",\n        \"userlist\": [\n            \"dev001\",\n            \"dev002\", \n            \"test001\",\n            \"pm001\"\n        ]\n    }\n}\n```\n\n### 修改群聊信息\n\n更新群聊的基本信息：\n\n```php\n$updateData = [\n    'chatid' => 'project_team_001',\n    'name' => 'Alpha项目开发小组',  // 新群名称\n    'owner' => 'new_project_manager',  // 新群主\n    'add_user_list' => ['dev003', 'qa001'],  // 新增成员\n    'del_user_list' => ['test001']  // 移除成员\n];\n\n$result = $chat->update($updateData);\n```\n\n**参数说明：**\n- `chatid` string 群聊ID\n- `name` string 新群名称（可选）\n- `owner` string 新群主userid（可选）\n- `add_user_list` array 要添加的成员列表（可选）\n- `del_user_list` array 要移除的成员列表（可选）\n\n### 解散群聊\n\n解散指定的群聊：\n\n```php\n$result = $chat->quit('project_team_001');\n```\n\n## 群成员管理\n\n### 批量邀请成员\n\n向群聊中批量添加成员：\n\n```php\n$result = $chat->addMembers('project_team_001', ['new_dev001', 'new_qa001', 'new_pm001']);\n```\n\n### 批量移除成员\n\n从群聊中批量移除成员：\n\n```php\n$result = $chat->removeMembers('project_team_001', ['old_dev001', 'former_pm001']);\n```\n\n### 获取群成员列表\n\n获取群聊的所有成员信息：\n\n```php\n$result = $chat->getMembers('project_team_001');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"userlist\": [\n        {\n            \"userid\": \"dev001\",\n            \"status\": 1,\n            \"join_time\": 1635724800\n        },\n        {\n            \"userid\": \"pm001\", \n            \"status\": 1,\n            \"join_time\": 1635724800\n        }\n    ]\n}\n```\n\n## 群消息发送\n\n### 发送文本消息\n\n向群聊发送文本消息：\n\n```php\n$message = [\n    'chatid' => 'project_team_001',\n    'msgtype' => 'text',\n    'text' => [\n        'content' => '大家好，项目进入关键阶段，请及时沟通进展情况。'\n    ]\n];\n\n$result = $chat->sendMessage($message);\n```\n\n### 发送图片消息\n\n发送图片消息到群聊：\n\n```php\n$message = [\n    'chatid' => 'project_team_001',\n    'msgtype' => 'image',\n    'image' => [\n        'media_id' => 'image_media_id_123'\n    ]\n];\n\n$result = $chat->sendMessage($message);\n```\n\n### 发送文件消息\n\n发送文件到群聊：\n\n```php\n$message = [\n    'chatid' => 'project_team_001',\n    'msgtype' => 'file',\n    'file' => [\n        'media_id' => 'file_media_id_456'\n    ]\n];\n\n$result = $chat->sendMessage($message);\n```\n\n### 发送卡片消息\n\n发送图文卡片消息：\n\n```php\n$message = [\n    'chatid' => 'project_team_001',\n    'msgtype' => 'news',\n    'news' => [\n        'articles' => [\n            [\n                'title' => '项目进度报告',\n                'description' => '本周项目进展情况总结',\n                'url' => 'https://work.weixin.qq.com/report/123',\n                'picurl' => 'https://example.com/pic.jpg'\n            ]\n        ]\n    ]\n];\n\n$result = $chat->sendMessage($message);\n```\n\n## 使用示例\n\n### 项目群管理\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'corp_id' => 'your-corp-id',\n    'agent_id' => 'your-agent-id',\n    'secret' => 'your-secret',\n    // ...\n];\n\n$app = Factory::work($config);\n$chat = $app->chat;\n\n// 1. 创建项目群\n$projectChat = [\n    'chatid' => 'alpha_project_' . date('Ymd'),\n    'name' => 'Alpha项目组',\n    'owner' => 'project_manager_001',\n    'userlist' => [\n        'pm_001',      // 项目经理\n        'dev_001',     // 开发\n        'dev_002', \n        'qa_001',      // 测试\n        'ui_001',      // 设计\n        'ops_001'      // 运维\n    ]\n];\n\n$createResult = $chat->create($projectChat);\n\nif ($createResult['errcode'] === 0) {\n    $chatId = $projectChat['chatid'];\n    echo \"项目群创建成功: {$chatId}\\n\";\n    \n    // 2. 发送欢迎消息\n    $welcomeMsg = [\n        'chatid' => $chatId,\n        'msgtype' => 'text',\n        'text' => [\n            'content' => \"欢迎大家加入Alpha项目组！\\n\\n\" .\n                        \"项目目标：开发新版本产品功能\\n\" .\n                        \"预计周期：8周\\n\" .\n                        \"请大家及时在群内同步工作进展，有问题随时讨论。\"\n        ]\n    ];\n    \n    $sendResult = $chat->sendMessage($welcomeMsg);\n    \n    if ($sendResult['errcode'] === 0) {\n        echo \"欢迎消息发送成功\\n\";\n    }\n    \n    // 3. 定期发送项目进度提醒\n    $reminderMsg = [\n        'chatid' => $chatId,\n        'msgtype' => 'news',\n        'news' => [\n            'articles' => [\n                [\n                    'title' => '项目进度提醒',\n                    'description' => '请各位同事及时更新项目进度，确保按时完成任务',\n                    'url' => 'https://project.company.com/alpha/progress',\n                    'picurl' => 'https://cdn.company.com/project-icon.png'\n                ]\n            ]\n        ]\n    ];\n    \n    $chat->sendMessage($reminderMsg);\n}\n```\n\n### 动态群成员管理\n\n```php\n// 根据项目阶段动态调整群成员\nfunction updateProjectTeam($chat, $chatId, $phase) {\n    switch ($phase) {\n        case 'design':\n            // 设计阶段：加入设计师，移除运维\n            $chat->addMembers($chatId, ['ui_002', 'ux_001']);\n            $chat->removeMembers($chatId, ['ops_001']);\n            \n            $chat->sendMessage([\n                'chatid' => $chatId,\n                'msgtype' => 'text',\n                'text' => [\n                    'content' => '项目进入设计阶段，欢迎设计团队加入！'\n                ]\n            ]);\n            break;\n            \n        case 'development':\n            // 开发阶段：加入更多开发者\n            $chat->addMembers($chatId, ['dev_003', 'dev_004', 'backend_001']);\n            \n            $chat->sendMessage([\n                'chatid' => $chatId,\n                'msgtype' => 'text',\n                'text' => [\n                    'content' => '项目进入开发阶段，开发团队全员到位！'\n                ]\n            ]);\n            break;\n            \n        case 'testing':\n            // 测试阶段：加入测试团队\n            $chat->addMembers($chatId, ['qa_002', 'qa_003', 'automation_001']);\n            \n            $chat->sendMessage([\n                'chatid' => $chatId,\n                'msgtype' => 'text',\n                'text' => [\n                    'content' => '项目进入测试阶段，测试团队请开始工作！'\n                ]\n            ]);\n            break;\n            \n        case 'deployment':\n            // 部署阶段：重新加入运维\n            $chat->addMembers($chatId, ['ops_001', 'ops_002']);\n            \n            $chat->sendMessage([\n                'chatid' => $chatId,\n                'msgtype' => 'text',\n                'text' => [\n                    'content' => '项目准备部署，运维团队请准备！'\n                ]\n            ]);\n            break;\n    }\n}\n\n// 使用示例\nupdateProjectTeam($chat, $chatId, 'development');\n```\n\n### 群消息推送系统\n\n```php\n// 定时推送项目状态\nfunction sendProjectStatus($chat, $chatId) {\n    // 获取项目数据（示例）\n    $projectData = [\n        'completed_tasks' => 45,\n        'total_tasks' => 60,\n        'bugs_fixed' => 12,\n        'open_bugs' => 3,\n        'progress' => 75\n    ];\n    \n    $progressBar = str_repeat('█', intval($projectData['progress'] / 10)) . \n                   str_repeat('░', 10 - intval($projectData['progress'] / 10));\n    \n    $statusMessage = [\n        'chatid' => $chatId,\n        'msgtype' => 'text',\n        'text' => [\n            'content' => \"📊 项目进度日报\\n\\n\" .\n                        \"进度: {$progressBar} {$projectData['progress']}%\\n\" .\n                        \"任务完成: {$projectData['completed_tasks']}/{$projectData['total_tasks']}\\n\" .\n                        \"Bug修复: {$projectData['bugs_fixed']}\\n\" .\n                        \"待修复Bug: {$projectData['open_bugs']}\\n\\n\" .\n                        \"继续加油！💪\"\n        ]\n    ];\n    \n    return $chat->sendMessage($statusMessage);\n}\n\n// 每日定时发送\nsendProjectStatus($chat, $chatId);\n```\n\n### 群聊数据统计\n\n```php\n// 统计群聊活跃度\nfunction getChatStatistics($chat, $chatIds) {\n    $statistics = [];\n    \n    foreach ($chatIds as $chatId) {\n        $chatInfo = $chat->get($chatId);\n        \n        if ($chatInfo['errcode'] === 0) {\n            $info = $chatInfo['chat_info'];\n            $memberCount = count($info['userlist']);\n            \n            $statistics[] = [\n                'chatid' => $chatId,\n                'name' => $info['name'],\n                'member_count' => $memberCount,\n                'owner' => $info['owner']\n            ];\n        }\n    }\n    \n    return $statistics;\n}\n\n// 生成群聊报告\n$chatList = ['alpha_project_20231101', 'beta_project_20231015', 'gamma_project_20231020'];\n$stats = getChatStatistics($chat, $chatList);\n\nforeach ($stats as $stat) {\n    echo \"群聊: {$stat['name']}\\n\";\n    echo \"成员数: {$stat['member_count']}\\n\";\n    echo \"群主: {$stat['owner']}\\n\";\n    echo \"---\\n\";\n}\n```\n\n## 注意事项\n\n1. **群聊数量限制**：每个应用创建的群聊数量有限制\n2. **成员数量限制**：单个群聊的成员数量有上限\n3. **消息频率限制**：群消息发送有频率限制\n4. **权限要求**：操作群聊需要相应的应用权限\n5. **群主变更**：群主变更需要原群主或管理员权限\n\n## 最佳实践\n\n1. **群聊命名规范**：使用有意义的群聊ID和名称\n2. **成员管理**：及时添加或移除相关成员\n3. **消息内容**：发送有价值的群消息，避免垃圾信息\n4. **权限控制**：合理设置群主和管理员\n5. **定期清理**：定期清理不活跃的群聊\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| 40003 | 无效的UserID |\n| 40013 | 不合法的CorpID |\n| 86001 | 不合法的群聊ID |\n| 86002 | 群聊不存在 |\n| 86003 | 不合法的群聊名称 |\n| 86004 | 群聊成员数量超出限制 |\n| 86005 | 群聊创建数量超出限制 |"
  },
  {
    "path": "docs/src/5.x/wework/contacts.md",
    "content": "# 通讯录\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 通讯录的 secret\n    //...\n];\n\n$contacts = Factory::work($config);\n```\n\n## 成员管理\n### 创建成员\n\n```php\n$data = [\n    \"userid\" => \"overtrue\",\n    \"name\" => \"超哥\",\n    \"english_name\" => \"overtrue\"\n    \"mobile\" => \"1818888888\",\n];\n$contacts->user->create($data);\n```\n\n### 读取成员\n\n```php\n$contacts->user->get('overtrue');\n```\n\n### 更新成员\n\n```php\n$contacts->user->update('overtrue', [\n    \"isleader\" => 0,\n    'position' => 'PHP 酱油工程师',\n    //...\n]);\n```\n\n### 删除成员\n\n```php\n$contacts->user->delete('overtrue');\n// 或者删除多个\n$contacts->user->delete(['overtrue', 'zhangsan', 'wangwu']);\n```\n\n### 获取部门成员\n\n```php\n$contacts->user->getDepartmentUsers($departmentId);\n// 递归获取子部门下面的成员\n$contacts->user->getDepartmentUsers($departmentId, true);\n```\n\n### 获取部门成员详情\n\n```php\n$contacts->user->getDetailedDepartmentUsers($departmentId);\n// 递归获取子部门下面的成员\n$contacts->user->getDetailedDepartmentUsers($departmentId, true);\n```\n\n### 用户 ID 转为 openid\n\n```php\n$contacts->user->userIdToOpenid($userId);\n// 或者指定应用 ID\n$contacts->user->userIdToOpenid($userId, $agentId);\n```\n\n### openid 转为用户 ID\n\n```php\n$contacts->user->openidToUserId($openid);\n```\n\n### 手机号转为用户 ID\n\n```php\n$contacts->user->mobileToUserId($mobile);\n```\n\n### 二次验证\n\n企业在成员验证成功后，调用如下接口即可让成员加入成功\n\n```php\n$contacts->user->accept($userId);\n```\n\n### 邀请成员\n\n企业可通过接口批量邀请成员使用企业微信，邀请后将通过短信或邮件下发通知。\n\n```php\n$params = [\n    'user' => ['UserID1', 'UserID2', 'UserID3'],    // 成员ID列表, 最多支持1000个\n    'party' => ['PartyID1', 'PartyID2'],            // 部门ID列表，最多支持100个\n    'tag' => ['TagID1', 'TagID2'],                  // 标签ID列表，最多支持100个\n];\n\n$contacts->user->invite($params);\n```\n\n> `user`, `party`, `tag` 三者不能同时为空\n\n### 获取邀请二维码\n\n```php\n$sizeType = 1;  // qrcode尺寸类型，1: 171 x 171; 2: 399 x 399; 3: 741 x 741; 4: 2052 x 2052\n\n$contacts->user->getInvitationQrCode($sizeType);\n```\n\n## 部门管理\n\n### 创建部门\n\n```php\n$contacts->department->create([\n        'name' => '广州研发中心',\n        'parentid' => 1,\n        'order' => 1,\n        'id' => 2,\n    ]);\n```\n\n### 更新部门\n\n```php\n$contacts->department->update($id, [\n        'name' => '广州研发中心',\n        'parentid' => 1,\n        'order' => 1,\n    ]);\n```\n\n### 删除部门\n\n```php\n$contacts->department->delete($id);\n```\n\n### 获取部门列表\n\n```php\n$contacts->department->list();\n// 获取指定部门及其下的子部门\n$contacts->department->list($id);\n```\n\n## 标签管理\n\n### 创建标签\n\n```php\n$contacts->tag->create($tagName, $tagId);\n```\n\n### 更新标签名字\n\n```php\n$contacts->tag->update($tagId, $tagName);\n```\n\n### 删除标签\n\n```php\n$contacts->tag->delete($tagId);\n```\n\n### 获取标签列表\n\n```php\n$contacts->tag->list();\n```\n\n### 获取标签成员(标签详情)\n\n```php\n$contacts->tag->get($tagId);\n```\n\n### 增加标签成员\n\n```php\n$contacts->tag->tagUsers($tagId, [$userId1, $userId2, ...]);\n\n// 指定部门\n$contacts->tag->tagDepartments($tagId, [$departmentId1, $departmentId2, ...]);\n```\n\n\n### 删除标签成员\n\n```php\n$contacts->tag->untagUsers($tagId, [$userId1, $userId2, ...]);\n\n// 指定部门\n$contacts->tag->untagDepartments($tagId, [$departmentId1, $departmentId2, ...]);\n```\n\n## 异步批量接口\n\n> 注意: 【异步批量接口】需要使用“通讯录同步”secret所获取的accesstoken来调用。 \n> 传送: https://work.weixin.qq.com/wework_admin/frame#apps/contactsApi \n\n\n### 增量更新成员\n\n```php\n$params = [\n    'media_id' => 'mediaId',\n    'to_invite' => true,\n    'callback' => [\n        'url' => 'xxx',\n        'token' => 'xxx',\n        'encodingaeskey' => 'xxx'\n    ]\n];\n\n$contacts->batch_jobs->batchUpdateUsers(array $params);\n```\n\n### 全量覆盖成员\n\n```php\n$params = [\n    'media_id' => 'mediaId',\n    'to_invite' => true,\n    'callback' => [\n        'url' => 'xxx',\n        'token' => 'xxx',\n        'encodingaeskey' => 'xxx'\n    ]\n];\n\n$contacts->batch_jobs->batchReplaceUsers(array $params);\n```\n\n### 全量覆盖部门\n\n```php\n$params = [\n    'media_id' => 'mediaId',\n    'callback' => [\n        'url' => 'xxx',\n        'token' => 'xxx',\n        'encodingaeskey' => 'xxx'\n    ]\n];\n\n$contacts->batch_jobs->batchReplaceDepartments(array $params);\n```\n\n### 获取异步任务结果\n\n```php\n$jobId = '2322232';\n\n$contacts->batch_jobs->getJobStatus(array $jobId);\n```\n\n## 互联企业\n\n### 获取应用的可见范围\n\n```php\n$contacts->linked_corp->getAgentPermissions();\n```\n\n### 获取互联企业成员详细信息\n\n```php\n$userId = 'corpId/userId';\n\n$contacts->linked_corp->getUser(string $userId);\n```\n\n### 获取互联企业部门成员\n\n```php\n$departmentId = 'linkedId/departmentId';\n$fetchChild = true;\n\n$contacts->linked_corp->getUsers(string $departmentId, bool $fetchChild = true);\n```\n\n### 获取互联企业部门成员详情\n\n```php\n$departmentId = 'linkedId/departmentId';\n$fetchChild = true;\n\n$contacts->linked_corp->getDetailedUsers(string $departmentId, bool $fetchChild = true);\n```\n\n### 获取互联企业部门列表\n\n```php\n$departmentId = 'linkedId/departmentId';\n\n$contacts->linked_corp->getDepartments(string $departmentId);\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/corp-group.md",
    "content": "# 企业互联\n\n### 获取应用共享信息\n\n```php\n$agentId = 100001;\n\n$app->corp_group->getAppShareInfo(int $agentId);\n```\n\n### 获取下级企业的access_token\n\n```php\n$corpId = 'wwd216fa8c4c5c0e7x';\n$agentId = 100001;\n\n$app->corp_group->getToken(string $corpId, int $agentId)\n```\n\n### 获取下级企业的小程序session\n\n\n```php\n$userId = 'wmAoNVCwAAUrSqEqz7oQpEIEMVWDrPeg';\n$sessionKey = 'n8cnNEoyW1pxSRz6/Lwjwg==';\n\n$app->corp_group->getMiniProgramTransferSession(string $userId, string $sessionKey);\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/external-contact.md",
    "content": "# 客户联系\n\n## 获取实例\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    ...\n];\n\n$app = Factory::work($config);\n\n// 基础接口\n$app->external_contact;\n\n// 「联系我」\n$app->contact_way;\n\n// 消息管理\n$app->external_contact_message;\n\n// 数据统计\n$app->external_contact_statistics;\n```\n\n## 基础接口\n\n### 获取配置了客户联系功能的成员列表\n\n```php\n$app->external_contact->getFollowUsers();\n```\n\n### 获取外部联系人列表\n\n```php\n$userId = 'zhangsan';\n\n$app->external_contact->list($userId);\n```\n\n### 获取外部联系人详情\n\n```php\n$externalUserId = 'woAJ2GCAAAXtWyujaWJHDDGi0mACH71w';\n\n$app->external_contact->get($externalUserId);\n```\n\n### 批量获取客户详情\n\n```php\n$userId = 'zhangsai';\n$cursor = '';\n$limit = 100;\n\n$app->external_contact->batchGetByUser(string $userId, string $cursor, int $limit);\n```\n\n\n### 修改客户备注信息\n\n```php\n$data  = [\n    \"userid\"=>'员工id',\n    \"external_userid\"=>'客户id',\n    \"remark\"=> '新备注',\n    \"description\"=>'新描述',\n    \"remark_company\"=>'新公司',\n    \"remark_mobiles\"=>[ '电话1','电话2'],\n    \"remark_pic_mediaid\"=> \"MEDIAID\"\n];\n\n$app->external_contact->remark($data);\n```\n\n\n\n### 获取离职成员的客户列表\n\n```php\n$pageId = 0;\n$pageSize = 1000;\n$app->external_contact->getUnassigned($pageId, $pageSize);\n```\n\n### 分配成员的客户(离职或在职)\n\n```php\n$externalUserId = 'woAJ2GCAAAXtWyujaWJHDDGi0mACH71w';\n$handoverUserId = 'zhangsan';\n$takeoverUserId = 'lisi';\n$transferSuccessMessage = '您好，您的服务已升级，后续将由我的同事张三@腾讯接替我的工作，继续为您服务。'; //不填则使用默认文案\n\n$app->external_contact->transfer($externalUserId, $handoverUserId, $takeoverUserId, $transferSuccessMessage);\n```\n\n\n### 离职成员的群再分配\n\n```php\n$chatIds = ['群聊id1', '群聊id2'];\n$takeoverUserId = '接替群主userid';\n\n$app->external_contact->transferGroupChat($chatIds, $takeoverUserId);\n```\n\n\n\n### 查询客户接替结果\n\n```php\n$externalUserId = 'woAJ2GCAAAXtWyujaWJHDDGi0mACH71w';\n$handoverUserId = 'zhangsan';\n$takeoverUserId = 'lisi';\n\n$app->external_contact->getTransferResult($externalUserId, $handoverUserId, $takeoverUserId);\n```\n\n\n## 客户群管理\n\n### 获取客户群列表\n\n```php\n$params = [\n    \"status_filter\" => 0,\n    \"owner_filter\" => [\n        \"userid_list\" => [\"abel\"],\n        \"partyid_list\" => [7]\n    ],\n    \"offset\" => 0,\n    \"limit\" => 100\n];\n\n$app->external_contact->getGroupChats(array $params);\n```\n\n### 获取客户群详情\n\n```php\n$chatId = 'wrOgQhDgAAMYQiS5ol9G7gK9JVAAAA';\n\n$app->external_contact->getGroupChat(string $chatId);\n```\n## 客户朋友圈\n\n\n### 获取企业全部的发表列表\n```php\n$params = [\n    'start_time' => 1605000000,\n    'end_time' => 1605172726,\n    'creator' => 'zhangshan',\n    'filter_type' => 1,\n    'cursor' => 'CURSOR',\n    'limit' => 10\n];\n\n$app->external_contact_moment->list(array $params);\n```\n\n### 获取客户朋友圈企业发表的列表\n\n```php\n$momentId = 'momxxx';\n$cursor = 'CURSOR';\n$limit = 10;\n\n$app->external_contact_moment->getTasks(string $momentId, string $cursor, int $limit);\n```\n\n### 获取客户朋友圈发表时选择的可见范围\n\n```php\n$momentId = 'momxxx';\n$userId = 'xxx';\n$cursor = 'CURSOR';\n$limit = 10;\n\n$app->external_contact_moment->getCustomers(string $momentId, string $userId, string $cursor, int $limit);\n```\n\n### 获取客户朋友圈发表后的可见客户列表\n\n```php\n$momentId = 'momxxx';\n$userId = 'xxx';\n$cursor = 'CURSOR';\n$limit = 10;\n\n$app->external_contact_moment->getSendResult(string $momentId, string $userId, string $cursor, int $limit);\n```\n\n### 获取客户朋友圈的互动数据\n\n```php\n$momentId = 'momxxx';\n$userId = 'xxx';\n\n$app->external_contact_moment->getComments(string $momentId, string $userId);\n```\n\n## 客户标签管理\n\n> 注意: 对于添加/删除/编辑企业客户标签接口，目前仅支持使用“客户联系”secret所获取的accesstoken来调用。\n> 原文: https://work.weixin.qq.com/api/doc/90000/90135/92117\n\n### 获取企业标签库\n\n```php\n$tagIds = [\n    \"etXXXXXXXXXX\",\n    \"etYYYYYYYYYY\"\n];\n\n$app->external_contact->getCorpTags(array $tagIds=[]);\n```\n\n### 添加企业客户标签\n\n```php\n$params = [\n    \"group_id\" => \"GROUP_ID\",\n    \"group_name\" => \"GROUP_NAME\",\n    \"order\" => 1,\n    \"tag\" => [\n        [\n            \"name\" => \"TAG_NAME_1\",\n            \"order\" => 1\n        ],\n        [\n            \"name\" => \"TAG_NAME_2\",\n            \"order\" => 2\n        ]\n    ]\n];\n\n$app->external_contact->addCorpTag(array $params);\n```\n\n\n### 编辑企业客户标签\n\n```php\n$id = 'TAG_ID';\n$name = 'NEW_TAG_NAME';\n$order = 1;\n\n$app->external_contact->updateCorpTag(string $id, string $name, int $order = 1);\n```\n\n\n\n### 删除企业客户标签\n\n```php\n$tagId = [\n    'TAG_ID_1',\n    'TAG_ID_2'\n];\n$groupId = [\n    'GROUP_ID_1',\n    'GROUP_ID_2'\n];\n\n$app->external_contact->deleteCorpTag(array $tagId,array $groupId);\n```\n\n\n\n### 编辑客户企业标签\n\n```php\n$params = [\n    \"userid\" => \"zhangsan\",\n    \"external_userid\" => \"woAJ2GCAAAd1NPGHKSD4wKmE8Aabj9AAA\",\n    \"add_tag\" => [\"TAGID1\", \"TAGID2\"],\n    \"remove_tag\" => [\"TAGID3\", \"TAGID4\"]\n];\n\n$app->external_contact->markTags(array $params);\n```\n\n## 配置客户联系「联系我」方式\n\n>  注意：\n> 1. 通过API添加的「联系我」不会在管理端进行展示。\n> 2. 每个企业可通过API最多配置10万个「联系我」。\n> 3. 截止 2019-06-21 官方文档没有提供获取所有「联系我」列表的接口，请开发者注意自行保管处理 configId，避免无法溯源。\n\n### 增加「联系我」方式\n\n```php\n$type = 1;\n$scene = 1;\n$config = [\n   'style' => 1,\n   'remark' => '渠道客户',\n   'skip_verify' => true,\n   'state' => 'teststate',\n   'user' => ['UserID1', 'UserID2', 'UserID3'],\n];\n\n$app->contact_way->create($type, $scene, $config);\n\n// {\n//   \"errcode\": 0,\n//   \"errmsg\": \"ok\",\n//   \"config_id\":\"42b34949e138eb6e027c123cba77fad7\"　　\n// }\n```\n\n### 获取「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$app->contact_way->get($configId);\n```\n\n### 更新「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$config = [\n   'style' => 1,\n   'remark' => '渠道客户2',\n   'skip_verify' => true,\n   'state' => 'teststate2',\n   'user' => ['UserID4', 'UserID5', 'UserID6'],\n];\n\n$app->contact_way->update($configId, $config);\n```\n\n### 删除「联系我」方式\n\n```php\n$configId = '42b34949e138eb6e027c123cba77fad7';\n\n$app->contact_way->delete($configId);\n```\n\n## 消息管理\n\n### 添加企业群发消息模板\n\n```php\n$msg = [\n    'external_userid' => [\n        'woAJ2GCAAAXtWyujaWJHDDGi0mACas1w',\n        'wmqfasd1e1927831291723123109r712',\n    ],\n    'sender' => 'zhangsan',\n    'text' => [\n        'content' => '文本消息内容',\n    ],\n    'image' => [\n        'media_id' => 'MEDIA_ID',\n    ],\n    'link' => [\n        'title' => '消息标题',\n        'picurl' => 'https://example.pic.com/path',\n        'desc' => '消息描述',\n        'url' => 'https://example.link.com/path',\n    ],\n    'miniprogram' => [\n        'title' => '消息标题',\n        'pic_media_id' => 'MEDIA_ID',\n        'appid' => 'wx8bd80126147df384',\n        'page' => '/path/index',\n    ],\n];\n\n$app->external_contact_message->submit($msg);\n\n// {\n//     \"errcode\": 0,\n//     \"errmsg\": \"ok\",\n//     \"fail_list\":[\"wmqfasd1e19278asdasdasd\"],\n//     \"msgid\":\"msgGCAAAXtWyujaWJHDDGi0mACas1w\"\n// }\n```\n\n### 获取企业群发消息发送结果\n\n```php\n$msgId = 'msgGCAAAXtWyujaWJHDDGi0mACas1w';\n\n$app->external_contact_message->get($msgId);\n```\n\n### 发送新客户欢迎语\n\n```php\n$welcomeCode = 'WELCOMECODE';\n\n$msg = [\n    'text' => [\n        'content' => '文本消息内容',\n    ],\n    'image' => [\n        'media_id' => 'MEDIA_ID',\n    ],\n    'link' => [\n        'title' => '消息标题',\n        'picurl' => 'https://example.pic.com/path',\n        'desc' => '消息描述',\n        'url' => 'https://example.link.com/path',\n    ],\n    'miniprogram' => [\n        'title' => '消息标题',\n        'pic_media_id' => 'MEDIA_ID',\n        'appid' => 'wx8bd80126147df384',\n        'page' => '/path/index',\n    ],\n];\n\n$app->external_contact_message->sendWelcome($welcomeCode, $msg);\n```\n\n\n## 数据统计\n\n###  获取「联系客户统计」数据\n\n```php\n$userIds = [\n    'zhangsan',\n    'lisi'\n];\n$partyIds = [\n    'PARTY_ID_1',\n    'PARTY_ID_2'\n];\n$from = 1536508800;\n$to = 1536940800;\n\n$app->external_contact_statistics->userBehavior($userIds, $from, $to, $partyIds);\n```\n\n###  获取「群聊数据统计」数据.\n\n- 按群主聚合的方式\n\n```php\n$params = [\n    'day_begin_time' => 1600272000,\n    'day_end_time' => 1600444800,\n    'owner_filter' => [\n        'userid_list' => ['zhangsan']\n    ],\n    'order_by' => 2,\n    'order_asc' => 0,\n    'offset' => 0,\n    'limit' => 1000\n];\n\n$app->external_contact_statistics->groupChatStatistic(array $params);\n```\n\n- 按自然日聚合的方式\n\n```php\n$dayBeginTime = 1600272000;\n$dayEndTime = 1600444800;\n$userIds = ['userid1', 'userid2'];\n\n$app->external_contact_statistics->groupChatStatisticGroupByDay(int $dayBeginTime, int $dayEndTime, array $userIds);\n```"
  },
  {
    "path": "docs/src/5.x/wework/group-robot.md",
    "content": "# 群机器人\n\n## 使用说明\n\n使用前必须先在群组里面添加机器人，然后将 `Webhook 地址` 中的 `key` 取出来，作为示例中 `$groupKey` 的值。\n\n> Webhook 地址示例：<https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=`ab4f609a-3feb-427c-ae9d-b319ca712d36`>\n\n> 微信文档：<https://work.weixin.qq.com/api/doc#90000/90136/91770>\n\n## 发送文本类型消息\n\n快速发送文本消息\n\n```php\n// 获取 Messenger 实例\n$messenger = $app->group_robot_messenger;\n\n// 群组 key\n$groupKey = 'ab4f609a-3feb-427c-ae9d-b319ca712d36';\n\n$messenger->message('大家好，我是本群的\"喝水提醒小助手\"')->toGroup($groupKey)->send();\n// 或者写成\n$messenger->toGroup($groupKey)->send('大家好，我是本群的\"喝水提醒小助手\"');\n```\n\n使用 `Text` 发送文本消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Text;\n\n// 准备消息\n$text = new Text('hello');\n\n// 发送\n$messenger->message($text)->toGroup($groupKey)->send();\n```\n\n@某人：\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Text;\n\n// 通过构造函数传参\n$text = new Text('hello', 'her-cat', '18700000000');\n//$text = new Text('hello', ['her-cat', 'overtrue'], ['18700000000', '18700000001']);\n\n// 通过 userId\n$text->mention('her-cat');\n//$text->mention(['her-cat', 'overtrue']);\n\n// 通过手机号\n$text->mentionByMobile('18700000000');\n//$text->mentionByMobile(['18700000000', '18700000001']);\n\n// @所有人\n$text->mention('@all');\n//$text->mentionByMobile('@all');\n\n$messenger->message($text)->toGroup($groupKey)->send();\n```\n\n## 发送 Markdown 类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Markdown;\n\n$content = '\n# 标题一\n## 标题二\n<font color=\"info\">绿色</font>\n<font color=\"comment\">灰色</font>\n<font color=\"warning\">橙红色</font>\n> 引用文字\n';\n\n$markdown = new Markdown($content);\n\n$messenger->message($markdown)->toGroup($groupKey)->send();\n```\n\n## 发送图片类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\Image;\n\n$img = file_get_contents('http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png');\n\n$image = new Image(base64_encode($img), md5($img));\n\n$result = $messenger->message($image)->toGroup($groupKey)->send();\n```\n\n## 发送图文类型消息\n\n```php\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\News;\nuse EasyWeChat\\Work\\GroupRobot\\Messages\\NewsItem;\n\n$items = [\n    new NewsItem([\n        'title' => '中秋节礼品领取',\n        'description' => '今年中秋节公司有豪礼相送',\n        'url' => 'https://easywechat.com',\n        'image' => 'http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png',\n    ]),\n\n    //...\n];\n\n$news = new News($items);\n\n$messenger->message($news)->toGroup($groupKey)->send();\n```\n\n## 其他方式\n\n使用 `group_robot` 发送消息。\n\n```php\n$app->group_robot->message('大家好，我是本群的\"喝水提醒小助手\"')->toGroup($groupKey)->send();\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/group-welcome-template.md",
    "content": "# 入群欢迎语\n\n企业微信入群欢迎语功能允许企业管理外部联系人入群时的欢迎消息模板，提升客户体验。\n\n## 获取实例\n\n```php\n$groupWelcomeTemplate = $app->group_welcome_template;\n```\n\n## 欢迎语模板管理\n\n### 添加入群欢迎语素材\n\n向企业的入群欢迎语素材库中添加新的素材：\n\n```php\n$templateData = [\n    'text' => [\n        'content' => '欢迎加入我们的产品交流群！\\n\\n我是您的专属客服小助手，有任何问题都可以随时咨询我。'\n    ],\n    'image' => [\n        'media_id' => 'image_media_id_123',\n        'pic_url' => 'https://example.com/welcome.jpg'\n    ],\n    'link' => [\n        'title' => '产品使用指南',\n        'picurl' => 'https://example.com/guide_thumb.jpg',\n        'desc' => '点击查看产品详细使用说明',\n        'url' => 'https://help.example.com/guide'\n    ],\n    'miniprogram' => [\n        'title' => '产品小程序',\n        'pic_media_id' => 'pic_media_id_456',\n        'appid' => 'wx1234567890abcdef',\n        'page' => 'pages/welcome/index'\n    ]\n];\n\n$result = $groupWelcomeTemplate->add($templateData);\n```\n\n**参数说明：**\n- `text` array 文本消息内容\n- `image` array 图片消息（可选）\n- `link` array 链接消息（可选）\n- `miniprogram` array 小程序消息（可选）\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"template_id\": \"msgtemplate4doGWjViuUW\"\n}\n```\n\n### 编辑入群欢迎语素材\n\n编辑已存在的入群欢迎语素材：\n\n```php\n$templateId = 'msgtemplate4doGWjViuUW';\n$updateData = [\n    'text' => [\n        'content' => '欢迎加入我们的VIP客户交流群！\\n\\n感谢您对我们产品的支持，我们将为您提供专属的优质服务。'\n    ],\n    'image' => [\n        'media_id' => 'new_image_media_id',\n        'pic_url' => 'https://example.com/vip_welcome.jpg'\n    ]\n];\n\n$result = $groupWelcomeTemplate->edit($templateId, $updateData);\n```\n\n### 获取入群欢迎语素材\n\n获取指定的入群欢迎语素材详情：\n\n```php\n$result = $groupWelcomeTemplate->get('msgtemplate4doGWjViuUW');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"text\": {\n        \"content\": \"欢迎加入我们的产品交流群！\"\n    },\n    \"image\": {\n        \"pic_url\": \"https://example.com/welcome.jpg\"\n    },\n    \"link\": {\n        \"title\": \"产品使用指南\",\n        \"picurl\": \"https://example.com/guide_thumb.jpg\",\n        \"desc\": \"点击查看产品详细使用说明\",\n        \"url\": \"https://help.example.com/guide\"\n    }\n}\n```\n\n### 删除入群欢迎语素材\n\n删除指定的入群欢迎语素材：\n\n```php\n$result = $groupWelcomeTemplate->delete('msgtemplate4doGWjViuUW');\n```\n\n## 使用示例\n\n### 创建多样化欢迎语模板\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'corp_id' => 'your-corp-id',\n    'agent_id' => 'your-agent-id',\n    'secret' => 'your-secret',\n    // ...\n];\n\n$app = Factory::work($config);\n$groupWelcomeTemplate = $app->group_welcome_template;\n\n// 1. 基础文本欢迎语\n$basicTemplate = [\n    'text' => [\n        'content' => '👋 欢迎加入我们的官方客户群！\\n\\n' .\n                    '🎯 群功能介绍：\\n' .\n                    '• 产品使用答疑\\n' .\n                    '• 新功能抢先体验\\n' .\n                    '• 专享优惠活动\\n\\n' .\n                    '💡 有问题随时 @我，1对1为您解答'\n    ]\n];\n\n$basicResult = $groupWelcomeTemplate->add($basicTemplate);\n\nif ($basicResult['errcode'] === 0) {\n    echo \"基础欢迎语创建成功: {$basicResult['template_id']}\\n\";\n}\n\n// 2. 富媒体欢迎语（图片+链接）\n$richTemplate = [\n    'text' => [\n        'content' => '🎉 欢迎加入VIP专享群！'\n    ],\n    'image' => [\n        'pic_url' => 'https://cdn.example.com/vip_welcome.png'\n    ],\n    'link' => [\n        'title' => 'VIP专享权益说明',\n        'picurl' => 'https://cdn.example.com/vip_benefits_thumb.jpg',\n        'desc' => '点击了解VIP会员专享服务内容',\n        'url' => 'https://vip.example.com/benefits'\n    ]\n];\n\n$richResult = $groupWelcomeTemplate->add($richTemplate);\n\nif ($richResult['errcode'] === 0) {\n    echo \"富媒体欢迎语创建成功: {$richResult['template_id']}\\n\";\n}\n\n// 3. 小程序欢迎语\n$miniprogramTemplate = [\n    'text' => [\n        'content' => '欢迎使用我们的小程序服务！'\n    ],\n    'miniprogram' => [\n        'title' => '立即体验产品',\n        'appid' => 'wx1234567890abcdef',\n        'page' => 'pages/newuser/welcome?from=group',\n        'pic_media_id' => 'miniprogram_pic_media_id'\n    ]\n];\n\n$miniprogramResult = $groupWelcomeTemplate->add($miniprogramTemplate);\n\nif ($miniprogramResult['errcode'] === 0) {\n    echo \"小程序欢迎语创建成功: {$miniprogramResult['template_id']}\\n\";\n}\n```\n\n### 分类管理欢迎语模板\n\n```php\n// 根据不同群类型创建不同的欢迎语\nfunction createWelcomeTemplatesByType($groupWelcomeTemplate) {\n    $templates = [\n        'product_support' => [\n            'text' => [\n                'content' => '🔧 欢迎加入产品技术支持群！\\n\\n' .\n                           '📋 群内服务：\\n' .\n                           '• 技术问题快速解答\\n' .\n                           '• 产品使用教程分享\\n' .\n                           '• 故障排除指导\\n\\n' .\n                           '⏰ 服务时间：工作日 9:00-18:00'\n            ],\n            'link' => [\n                'title' => '技术文档中心',\n                'desc' => '查看完整的产品技术文档',\n                'url' => 'https://docs.example.com',\n                'picurl' => 'https://cdn.example.com/docs_thumb.jpg'\n            ]\n        ],\n        \n        'sales_consultation' => [\n            'text' => [\n                'content' => '💼 欢迎加入销售咨询群！\\n\\n' .\n                           '🎯 我们可以为您提供：\\n' .\n                           '• 产品价格咨询\\n' .\n                           '• 定制化方案设计\\n' .\n                           '• 合作政策解答\\n\\n' .\n                           '📞 急需帮助请直接联系：400-123-4567'\n            ],\n            'image' => [\n                'pic_url' => 'https://cdn.example.com/sales_banner.jpg'\n            ]\n        ],\n        \n        'community_discussion' => [\n            'text' => [\n                'content' => '🌟 欢迎加入用户交流社区！\\n\\n' .\n                           '💬 在这里您可以：\\n' .\n                           '• 分享使用心得\\n' .\n                           '• 参与产品讨论\\n' .\n                           '• 结识志同道合的朋友\\n\\n' .\n                           '🏆 活跃用户将有机会获得专属礼品'\n            ],\n            'miniprogram' => [\n                'title' => '社区积分商城',\n                'appid' => 'wx1234567890abcdef',\n                'page' => 'pages/community/points',\n                'pic_media_id' => 'community_pic_media_id'\n            ]\n        ]\n    ];\n    \n    $createdTemplates = [];\n    \n    foreach ($templates as $type => $template) {\n        $result = $groupWelcomeTemplate->add($template);\n        \n        if ($result['errcode'] === 0) {\n            $createdTemplates[$type] = $result['template_id'];\n            echo \"创建 {$type} 欢迎语成功: {$result['template_id']}\\n\";\n        } else {\n            echo \"创建 {$type} 欢迎语失败: {$result['errmsg']}\\n\";\n        }\n        \n        sleep(1); // 避免频率限制\n    }\n    \n    return $createdTemplates;\n}\n\n$templateIds = createWelcomeTemplatesByType($groupWelcomeTemplate);\n```\n\n### 定期更新欢迎语内容\n\n```php\n// 根据节日或活动更新欢迎语\nfunction updateSeasonalWelcome($groupWelcomeTemplate, $templateId) {\n    $currentMonth = date('n');\n    $seasonalContent = '';\n    \n    switch ($currentMonth) {\n        case 12:\n        case 1:\n        case 2:\n            $seasonalContent = '❄️ 冬日暖心服务，温暖每一位客户\\n';\n            break;\n        case 3:\n        case 4:\n        case 5:\n            $seasonalContent = '🌸 春暖花开，与您共享美好时光\\n';\n            break;\n        case 6:\n        case 7:\n        case 8:\n            $seasonalContent = '☀️ 夏日清凉，为您提供贴心服务\\n';\n            break;\n        case 9:\n        case 10:\n        case 11:\n            $seasonalContent = '🍂 秋高气爽，收获满满的服务体验\\n';\n            break;\n    }\n    \n    $updateData = [\n        'text' => [\n            'content' => $seasonalContent . \n                        '欢迎加入我们的客户服务群！\\n\\n' .\n                        '我们将为您提供专业的服务支持。'\n        ]\n    ];\n    \n    $result = $groupWelcomeTemplate->edit($templateId, $updateData);\n    \n    if ($result['errcode'] === 0) {\n        echo \"季节性欢迎语更新成功\\n\";\n    } else {\n        echo \"更新失败: {$result['errmsg']}\\n\";\n    }\n}\n\n// 使用示例\nif (!empty($templateIds['product_support'])) {\n    updateSeasonalWelcome($groupWelcomeTemplate, $templateIds['product_support']);\n}\n```\n\n### 欢迎语模板管理\n\n```php\n// 获取和管理所有欢迎语模板\nfunction manageWelcomeTemplates($groupWelcomeTemplate, $templateIds) {\n    foreach ($templateIds as $type => $templateId) {\n        // 获取模板详情\n        $detail = $groupWelcomeTemplate->get($templateId);\n        \n        if ($detail['errcode'] === 0) {\n            echo \"\\n=== {$type} 模板 ({$templateId}) ===\\n\";\n            echo \"文本内容: \" . (isset($detail['text']['content']) ? \n                substr($detail['text']['content'], 0, 50) . '...' : '无') . \"\\n\";\n            echo \"是否包含图片: \" . (isset($detail['image']) ? '是' : '否') . \"\\n\";\n            echo \"是否包含链接: \" . (isset($detail['link']) ? '是' : '否') . \"\\n\";\n            echo \"是否包含小程序: \" . (isset($detail['miniprogram']) ? '是' : '否') . \"\\n\";\n        }\n    }\n    \n    // 清理不再使用的模板\n    $unusedTemplates = ['old_template_id_1', 'old_template_id_2'];\n    \n    foreach ($unusedTemplates as $templateId) {\n        $deleteResult = $groupWelcomeTemplate->delete($templateId);\n        \n        if ($deleteResult['errcode'] === 0) {\n            echo \"已删除废弃模板: {$templateId}\\n\";\n        }\n    }\n}\n\nmanageWelcomeTemplates($groupWelcomeTemplate, $templateIds);\n```\n\n## 注意事项\n\n1. **模板数量限制**：每个企业最多可创建100个入群欢迎语素材\n2. **媒体资源**：图片和小程序封面需要先上传获取media_id\n3. **内容审核**：欢迎语内容需要符合企业微信规范\n4. **权限要求**：需要企业微信管理员权限\n5. **更新频率**：避免频繁修改同一模板\n\n## 最佳实践\n\n1. **内容个性化**：根据不同群组类型设计不同的欢迎语\n2. **信息丰富**：合理使用文本、图片、链接等多种形式\n3. **定期更新**：根据季节、活动等适时更新内容\n4. **简洁明了**：避免内容过长，突出重点信息\n5. **引导行为**：通过欢迎语引导用户进行下一步操作\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| 40003 | 无效的UserID |\n| 40013 | 不合法的CorpID |\n| 41001 | 缺少access_token参数 |\n| 84061 | 不合法的模板ID |\n| 84062 | 模板已达到数量上限 |\n| 84063 | 模板内容过长 |\n| 84064 | 不合法的媒体文件 |"
  },
  {
    "path": "docs/src/5.x/wework/index.md",
    "content": "## 企业微信\n\n企业微信的使用与公众号以及其它几个应用的使用方式都是一致的，使用 `\\EasyWeChat\\Factory::work($config)` 来初始化：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n\n    'agent_id' => 100020, // 如果有 agend_id 则填写\n    'secret'   => 'xxxxxxxxxx',\n\n    // 指定 API 调用返回结果的类型：array(default)/collection/object/raw/自定义类名\n    'response_type' => 'array',\n\n    'log' => [\n        'level' => 'debug',\n        'file' => __DIR__.'/wechat.log',\n    ],\n];\n\n$app = Factory::work($config);\n```\n\n然后你就可以用 `$app` 来调用企业微信的服务了。"
  },
  {
    "path": "docs/src/5.x/wework/intercept.md",
    "content": "# 聊天敏感词\n\n\n### 新建敏感词规则\n\n```php\n$params = [\n    'rule_name' => 'rulename',\n    'word_list' => [\n        '敏感词1', '敏感词2'\n    ],\n    'semantics_list' => [1, 2, 3],\n    'intercept_type' => 1,\n    'applicable_range' => [\n        'user_list' => ['zhangshan'],\n        'department_list' => [2, 3]\n    ]\n];\n\n$app->product->createInterceptRule($params);\n```\n\n### 获取敏感词规则详情\n\n```php\n$ruleId = 'ruleId';\n\n$app->product->getInterceptRuleDetails($ruleId);\n```\n\n### 删除敏感词规则\n\n```php\n$ruleId = 'ruleId';\n\n$app->product->deleteInterceptRule($ruleId);\n```\n\n\n### 编辑敏感词规则\n\n```php\n$params = [\n    'rule_id' => 'xxxx',\n    'rule_name' => 'rulename',\n    'word_list' => [\n        '敏感词1', '敏感词2'\n    ],\n    'semantics_list' => [1, 2, 3],\n    'intercept_type' => 1,\n    'applicable_range' => [\n        'user_list' => ['zhangshan'],\n        'department_list' => [2, 3]\n    ]\n];\n\n$app->product->updateInterceptRule($params);\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/invoice.md",
    "content": "# 电子发票\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n## 查询电子发票\n\nhttps://work.weixin.qq.com/api/doc#11631\n\nAPI:\n\n```php\nmixed get(string $cardId, string $encryptCode)\n```\n\nexample:\n\n```php\n$app->invoice->get('CARDID', 'ENCRYPTCODE');\n```\n\n## 批量查询电子发票\n\nhttps://work.weixin.qq.com/api/doc#11974\n\nAPI:\n\n```php\nmixed select(array $invoices)\n```\n\n> $invoices: 发票参数列表\n\nexample:\n\n```php\n$invoices = [\n    [\"card_id\" => \"CARDID1\", \"encrypt_code\" => \"ENCRYPTCODE1\"],\n    [\"card_id\" => \"CARDID2\", \"encrypt_code\" => \"ENCRYPTCODE2\"]\n];\n\n$app->invoice->select($invoices);\n```\n\n## 更新发票状态\n\nhttps://work.weixin.qq.com/api/doc#11633\n\nAPI:\n\n```php\nmixed update(string $cardId, string $encryptCode, string $status)\n```\n\n> $status: 发报销状态\n>\n> > - INVOICE_REIMBURSE_INIT：发票初始状态，未锁定；\n> > - INVOICE_REIMBURSE_LOCK：发票已锁定，无法重复提交报销;\n> > - INVOICE_REIMBURSE_CLOSURE:发票已核销，从用户卡包中移除\n\n## 批量更新发票状态\n\nhttps://work.weixin.qq.com/api/doc#11633\n\nAPI:\n\n```php\nmixed batchUpdate(array $invoices, string $openid, string $status)\n```\n\nexample:\n\n```php\n$invoices = [\n    [\"card_id\" => \"CARDID1\", \"encrypt_code\" => \"ENCRYPTCODE1\"],\n    [\"card_id\" => \"CARDID2\", \"encrypt_code\" => \"ENCRYPTCODE2\"]\n];\n$openid = 'oV-gpwSU3xlMXbq0PqqRp1xHu9O4';\n\n$status = 'INVOICE_REIMBURSE_CLOSURE';\n\n$app->invoice->batchUpdate($invoices, $openid, $status)\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/jssdk.md",
    "content": "# JSSDK\n\n企业微信 JSSDK 官方文档：https://open.work.weixin.qq.com/api/doc/90000/90136/90514\n\n## API\n\n### 获取config接口配置\n\n```php\n$app->jssdk->buildConfig(array $APIs, $debug = false, $beta = false, $json = true, array $openTagList = []);\n```\n\n默认返回 JSON 字符串，当 `$json` 为 `false` 时返回数组，你可以直接使用到网页中。\n\n- 设置当前URL\n\n```php\n$app->jssdk->setUrl($url);\n$app->jssdk->buildConfig(array $APIs, $debug = false, $beta = false, $json = true, array $openTagList = []);\n```\n如果不想用默认读取的URL，可以使用此方法手动设置，通常不需要。\n\n\n- 示例\n\n我们可以生成js配置文件：\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\n    wx.config(<?php echo $app->jssdk->buildConfig(array('updateAppMessageShareData', 'updateTimelineShareData'), true) ?>);\n</script>\n```\n结果如下：\n\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\nwx.config({\n    debug: true, // 请在上线前删除它\n    appId: 'wx3cf0f39249eb0e60',\n    timestamp: 1430009304,\n    nonceStr: 'qey94m021ik',\n    signature: '4F76593A4245644FAE4E1BC940F6422A0C3EC03E',\n    jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']\n});\n</script>\n```\n\n### 获取agentConfig接口配置\n\n调用wx.agentConfig之前，必须确保先成功调用wx.config. 注意：从企业微信3.0.24及以后版本（可通过企业微信UA判断版本号），无须先调用wx.config，可直接wx.agentConfig.\n\n```php\n$app->jssdk->buildAgentConfig(\n        array $jsApiList, // 需要检测的JS接口列表\n        $agentId, //应用id\n        bool $debug = false,\n        bool $beta = false,\n        bool $json = true,\n        array $openTagList = [],\n        string $url = null //设置当前URL\n    );\n```\n\n- 前端示例\n\n```js\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script src=\"https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\nwx.config({\n    debug: true, // 请在上线前删除它\n    appId: 'wx3cf0f39249eb0e60',\n    timestamp: 1430009304,\n    nonceStr: 'qey94m021ik',\n    signature: '4F76593A4245644FAE4E1BC940F6422A0C3EC03E',\n    jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']\n});\nwx.ready(function(){\n    wx.agentConfig({ //调用agentConfig\n        corpid: '', \n        agentid: '', \n        timestamp: '', \n        nonceStr: '', \n        signature: '',\n        jsApiList: ['selectExternalContact'],\n        success: function(res) {\n            // 回调\n        },\n        fail: function(res) {\n            if(res.errMsg.indexOf('function not exist') > -1){\n                alert('版本过低请升级')\n            }\n        }\n    });\n});\nwx.error(function(res){\n    console.log('失败');  \n});\n</script>\n```\n\n"
  },
  {
    "path": "docs/src/5.x/wework/kf.md",
    "content": "# 微信客服\n\n## 服务端(接收消息)\n我们在企业微信 ”微信客服” 应用开启API接收消息的功能    \n将设置页面的 token 与 aes key 配置到 agents 下对应的应用内   \n> 注意: 需要使用“微信客服”secret所获取的accesstoken来调用\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    // server config\n    'token' => 'xxxxxxxxx',\n    'aes_key' => 'xxxxxxxxxxxxxxxxxx',\n\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n接着配置服务端与公众号的服务端用法一样：\n\n请参考微信客服文档 https://open.work.weixin.qq.com/api/doc/90000/90135/94670\n\n```php\n$app->server->push(function($message){\n   // $message['FromUserName'] // 消息来源\n   // $message['MsgType'] // 消息类型：event ....\n    \n    return 'Hello easywechat.';\n});\n\n$response = $app->server->serve();\n\n$response->send();\n```\n\n`$response` 为 `Symfony\\Component\\HttpFoundation\\Response` 实例，根据你的框架情况来决定如何处理响应。\n\n## 客服帐号管理\n\n### 添加客服帐号\n\n```php\n$app->kf_account->add(string $name, string $mediaId);\n```\n\n### 删除客服帐号\n\n```php\n$app->kf_account->del(string $openKfId);\n```\n\n### 修改客服帐号\n\n```php\n$app->kf_account->update(string $openKfId, string $name, string $mediaId);\n```\n\n### 获取客服帐号列表\n\n```php\n$app->kf_account->list();\n```\n\n### 获取客服帐号链接\n\n```php\n$app->kf_account->getAccountLink(string $openKfId, string $scene);\n```\n\n## 接待人员管理\n\n### 添加接待人员\n\n```php\n$app->kf_servicer->add(string $openKfId, array $userIds);\n```\n\n### 删除接待人员\n\n```php\n$app->kf_servicer->del(string $openKfId, array $userIds);\n```\n\n### 获取接待人员列表\n\n```php\n$app->kf_servicer->list(string $openKfId);\n```\n\n## 会话分配与消息收发\n\n### 获取会话状态\n\n```php\n$app->kf_message->state(string $openKfId, string $externalUserId);\n```\n\n### 变更会话状态\n\n```php\n$app->kf_message->updateState(string $openKfId, string $externalUserId, int $serviceState, string $serviceUserId);\n```\n\n### 读取消息\n\n```php\n$app->kf_message->sync(string $cursor, string $token, int $limit);\n```\n\n### 发送消息\n\n```php\n$app->kf_message->send(array $params);\n```\n\n### 发送事件响应消息\n\n```php\n$app->kf_message->event(array $params);\n```"
  },
  {
    "path": "docs/src/5.x/wework/media.md",
    "content": "# 临时素材\n\n它的使用是不基于应用的，或者说基于任何一个应用都能访问这些 API，所以在用法上是直接调用 work 实例的 `media` 属性：\n\n**上传的媒体文件限制：**\n\n所有文件size必须大于5个字节\n\n>  - 图片（image）：2MB，支持JPG,PNG格式\n>  - 语音（voice）：2MB，播放长度不超过60s，支持AMR格式\n>  - 视频（video）：10MB，支持MP4格式\n>  - 普通文件（file）：20MB\n\n## 上传图片\n\n> 注意：微信图片上传服务有敏感检测系统，图片内容如果含有敏感内容，如色情，商品推广，虚假信息等，上传可能失败。\n\n```php\n$app->media->uploadImage($path); // $path 为本地文件路径\n```\n\n## 上传声音\n\n```php\n$app->media->uploadVoice($path);\n```\n\n## 上传视频\n\n```php\n$app->media->uploadVideo($path);\n```\n\n## 上传普通文件\n\n```php\n$path = '/path/to/企业微信操作手册.pdf'\n\n$form = [ //可选 发送时,中文文件名不显示或被过虑可传此参数\n    'filename' => '企业微信操作手册.pdf'\n];\n\n$app->media->uploadFile(string $path, array $form = []);\n```\n\n## 获取素材\n\n```php\n$app->media->get($mediaId);\n```"
  },
  {
    "path": "docs/src/5.x/wework/menu.md",
    "content": "# 自定义菜单\n\n自定义菜单是指为单个应用设置自定义菜单功能，所以在使用时请注意调用正确的应用实例。\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n    //...\n];\n$app = Factory::work($config);\n```\n\n## 创建菜单\n\n```php\n$menus = [\n    'button' => [\n        [\n            'name' => '首页',\n            'type' => 'view',\n            'url' => 'https://easywechat.com'\n        ],\n        [\n            'name' => '关于我们',\n            'type' => 'view',\n            'url' => 'https://easywechat.com/about'\n        ],\n        //...\n    ],\n];\n\n$app->menu->create($menus);\n```\n\n## 获取菜单\n\n```php\n$app->menu->get();\n```\n\n## 删除菜单\n\n```php\n$app->menu->delete();\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/message.md",
    "content": "# 消息\n\n## 主动发送消息\n\n```php\nuse EasyWeChat\\Kernel\\Messages\\TextCard;\n\n\n// 获取 Messenger 实例\n$messenger = $app->messenger;\n\n// 准备消息\n$message = new TextCard([\n    'title' => '你的请假单审批通过', \n    'description' => '单号：1928373, ....', \n    'url' => 'http://easywechat.com/oa/....'\n]);\n\n// 发送\n$messenger->message($message)->toUser('overtrue')->send();\n\n```\n\n你也可以很方便的发送普通文本消息：\n\n```php\n$messenger->message('你的请假单（单号：1928373）已经审批通过！')->toUser('overtrue')->send();\n// 或者写成\n$messenger->toUser('overtrue')->send('你的请假单（单号：1928373）已经审批通过！');\n```\n\n## 接收消息\n\n被动接收消息，与回复消息，请参考：[服务端](server)\n\n\n## 更新任务卡片消息状态 \n\n```php\n$messenger->updateTaskcard(array $userids, int $agentId, string $taskId, string $replaceName = '已收到')\n```\n\n"
  },
  {
    "path": "docs/src/5.x/wework/mini-program.md",
    "content": "# 小程序\n\n## 登录获取用户信息\n\n> 注意：需要关联小程序，并且使用关联后的小程序AgentId与Secret。\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx', //企业id\n\n    'agent_id' => 100020, // 企业微信关联后的AgentId\n    'secret'   => 'xxxxxxxxxx', //企业微信关联后的Secret\n];\n\n$app = Factory::work($config);\n\n$miniProgram = $app->miniProgram();\n\n$res = $miniProgram->auth->session(\"js-code\");\n```\n\n"
  },
  {
    "path": "docs/src/5.x/wework/mobile.md",
    "content": "# 移动端\n\n## 通过code获取用户信息\n\n通过iOS或Android应用授权登录，获取一次性code，通过后端服务器换取用户的信息。\n\n```php\n$code = 'CODE';\n\n$app->mobile->getUser(string $code);\n```"
  },
  {
    "path": "docs/src/5.x/wework/msg-audit.md",
    "content": "# 会话内容存档\n\n> 企业需要使用会话内容存档应用secret所获取的accesstoken来调用。\n> 原文: https://work.weixin.qq.com/api/doc/90000/90135/91614\n\n\n### 会话存档相关SDK\n\n- [wework-msgaudit](https://github.com/aa24615/wework-msgaudit)\n\n\n### 获取会话内容存档开启成员列表\n```php\n$type = 1;\n\n$app->msg_audit->getPermitUsers(string $type);\n```\n\n### 获取会话同意情况\n\n- 单聊\n\n```php\n$info = [\n    [\n        \"userid\" => \"XuJinSheng1\",\n        \"exteranalopenid\" => \"wmeDKaCQAAGd9oGiQWxVsAKwV2HxNAAA1\"\n    ],\n    [\n        \"userid\" => \"XuJinSheng2\",\n        \"exteranalopenid\" => \"wmeDKaCQAAGd9oGiQWxVsAKwV2HxNAAA2\"\n    ],\n    [\n        \"userid\" => \"XuJinSheng3\",\n        \"exteranalopenid\" => \"wmeDKaCQAAGd9oGiQWxVsAKwV2HxNAAA3\"\n    ]\n];\n\n$app->msg_audit->getSingleAgreeStatus(array $info);\n```\n\n- 群聊\n\n```php\n$roomId = 'wrjc7bDwAASxc8tZvBErFE02BtPWyAAA';\n\n$app->msg_audit->getRoomAgreeStatus(string $roomId);\n```\n\n### 获取会话内容存档内部群信息\n\n```php\n$roomId = 'wrjc7bDwAASxc8tZvBErFE02BtPWyAAA';\n\n$app->msg_audit->getRoom(string $roomId);\n```\n\n\n\n"
  },
  {
    "path": "docs/src/5.x/wework/oa.md",
    "content": "# OA\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx',\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n## 打卡\n\n### 获取企业所有打卡规则\n\n```php\n$app->oa->corpCheckinRules();\n```\n\n### 获取员工打卡规则\n\n```php\n$app->oa->checkinRules(int $datetime, array $userList);\n```\n\n### 获取打卡记录数据\n\n> $type: 打卡类型 1：上下班打卡；2：外出打卡；3：全部打卡\n\n```php\n\n// 全部打卡数据\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"]);\n\n// 获取上下班打卡\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"], 1);\n\n// 获取外出打卡\n$app->oa->checkinRecords(1492617600, 1492790400, [\"james\",\"paul\"], 2);\n\n```\n\n### 获取打卡日报数据\n\n```php\n$app->oa->checkinDayData(int $startTime, int $endTime, array $userids);\n```\n\n### 获取打卡月报数据\n\n```php\n$app->oa->checkinMonthData(int $startTime, int $endTime, array $userids);\n```\n\n### 获取打卡人员排班信息\n\n```php\n $params = [\n            'groupid' => 226,\n            'items' => [\n                [\n                    'userid' => 'james',\n                    'day' => 5,\n                    'schedule_id' => 234\n                ]\n            ],\n            'yearmonth' => 202012\n        ];\n$app->oa->setCheckinSchedus(array $params);\n```\n\n### 为打卡人员排班\n\n```php\n$app->oa->checkinSchedus(int $startTime, int $endTime, array $userids);\n```\n\n### 录入打卡人员人脸信息\n\n```php\n$app->oa->addCheckinUserface(string $userid, string $userface)\n```\n\n## 获取审批数据\n\nAPI:\n\n```php\nmixed approvalRecords(int $startTime, int $endTime, int $nextNumber = null)\n```\n\n> $nextNumber: 第一个拉取的审批单号，不填从该时间段的第一个审批单拉取\n\n```php\n$app->oa->approvalRecords(1492617600, 1492790400);\n\n// 指定第一个拉取的审批单号，不填从该时间段的第一个审批单拉取\n$app->oa->approvalRecords(1492617600, 1492790400, '201704240001');\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/oauth.md",
    "content": "# OAuth\n\n> 此文档为企业微信内部应用开发的网页授权\n\n[企业微信官方文档](https://work.weixin.qq.com/api/doc#90000/90135/91020)\n\n创建实例：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n    'agent_id' => 100001,\n];\n\n$app = Factory::work($config);\n```\n\n## 跳转授权\n\n```php\n// $callbackUrl 为授权回调地址\n$callbackUrl = 'https://xxx.xxx'; // 需设置可信域名\n\n// 返回一个 redirect 实例\n$redirect = $app->oauth->redirect($callbackUrl);\n\n// 获取企业微信跳转目标地址\n$targetUrl = $redirect->getTargetUrl();\n\n// 直接跳转到企业微信授权\n$redirect->send();\n```\n\n## 获取授权用户信息\n\n在回调页面中，你可以使用以下方式获取授权者信息：\n\n```php\n$code = \"回调URL中的code\";\n$user = $app->oauth->detailed()->userFromCode($code);\n\n// 获取用户信息\n$user->getId(); // 对应企业微信英文名（userid）\n$user->getRaw(); // 获取企业微信接口返回的原始信息\n```\n\n获取用户其他信息需调用通讯录接口，参考：[企业微信通讯录 API](https://github.com/EasyWeChat/docs/blob/master/wework/contacts.md)\n\n## 参考阅读\n\n- 本模块基于 [overtrue/socialite](https://github.com/overtrue/socialite/) 实现，更多的使用请阅读该扩展包文档。\n- state 参数的使用: [overtrue/socialite/#state](https://github.com/overtrue/socialite/#state)\n"
  },
  {
    "path": "docs/src/5.x/wework/product.md",
    "content": "# 产品图册\n\n### 创建商品图册\n\n```php\n$params = [\n  'description'=>'世界上最好的商品',\n  'price'=>30000,\n  'product_sn'=>'xxxxxxxx',\n  'attachments'=>[\n      [\n          'type'=> 'image',\n          'image'=> [\n              'media_id'=> 'MEDIA_ID'\n          ]\n      ]\n  ]\n];\n\n$app->product->createProductAlbum($params);\n```\n\n### 获取商品图册列表\n\n```php\n$app->product->getProductAlbums(int $limit, string $cursor);\n```\n\n### 获取商品图册\n\n```php\n$productId = 'productId';\n\n$app->product->getProductAlbumDetails($productId);\n```\n\n### 删除商品图册\n\n```php\n$productId = 'productId';\n\n$app->product->deleteProductAlbum($productId);\n```\n"
  },
  {
    "path": "docs/src/5.x/wework/server.md",
    "content": "## 服务端\n\n我们在企业微信应用开启接收消息的功能，将设置页面的 token 与 aes key 配置到 agents 下对应的应用内：\n\n```php\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n\n    'agent_id' => 100022,\n    'secret'   => 'xxxxxxxxxx',\n\n    // server config\n    'token' => 'xxxxxxxxx',\n    'aes_key' => 'xxxxxxxxxxxxxxxxxx',\n\n    //...\n];\n\n$app = Factory::work($config);\n```\n\n接着配置服务端与公众号的服务端用法一样：\n\n```php\n$app->server->push(function($message){\n   // $message['FromUserName'] // 消息来源\n   // $message['MsgType'] // 消息类型：event ....\n    \n    return 'Hello easywechat.';\n});\n\n$response = $app->server->serve();\n\n$response->send();\n```\n\n`$response` 为 `Symfony\\Component\\HttpFoundation\\Response` 实例，根据你的框架情况来决定如何处理响应。\n"
  },
  {
    "path": "docs/src/5.x/wework/to-account.md",
    "content": "# 企微ID账号升级转换\n\n:book:   [官方文档 - 企业微信帐号ID安全性全面升级 说明文档](https://open.work.weixin.qq.com/api/doc/90001/90143/95327)\n\n> 注意: 以下接口仅限第三方服务商调用\n\n```php\n$config = [...];\n\n$app = Factory::openWork($config);\n$work = $app->work('授权企业的corp_id','授权企业的永久授权码');\n```\n\n### corpid转换\n\n```php\n$work->corp_group->getOpenCorpid(string $corpId);\n```\n\n### userid转换\n\n```php\n$work->corp_group->batchUseridToOpenUserid(array $useridList);\n```\n\n### external_userid转换\n\n```php\n$work->external_contact->getNewExternalUserid(array $externalUserIds);\n```\n\n### 设置迁移完成\n\n```php\n$work->external_contact->finishExternalUseridMigration(string $corpId);\n```\n\n### unionid查询external_userid\n\n```php\n$work->external_contact->unionidToexternalUserid3rd(string $unionid, string $openid, string $corpid = '');\n```"
  },
  {
    "path": "docs/src/5.x/wework/wedrive.md",
    "content": "# 微盘\n\n企业微信微盘功能提供企业文件存储和管理能力，支持空间管理、文件操作等功能。\n\n## 获取实例\n\n```php\n$wedrive = $app->wedrive;\n```\n\n## 空间管理\n\n### 新建空间\n\n创建新的微盘空间：\n\n```php\n$authInfo = [\n    [\n        'type' => 1,  // 权限类型：1-成员 2-部门\n        'userid' => 'zhangsan',\n        'auth' => 1   // 权限：1-可下载 2-可预览 3-可编辑 4-可管理\n    ],\n    [\n        'type' => 2,\n        'departmentid' => 2,\n        'auth' => 2\n    ]\n];\n\n$result = $wedrive->space->create(\n    'admin_userid',      // 操作者userid\n    '项目资料空间',       // 空间名称\n    $authInfo,           // 权限信息\n    0                    // 空间类型：0-普通 1-相册\n);\n```\n\n**参数说明：**\n- `userid` string 操作者的userid\n- `spaceName` string 空间名称\n- `authInfo` array 空间成员权限信息\n- `spaceSubType` int 空间类型，0:普通 1:相册\n\n**权限类型说明：**\n- `type` int 权限类型：1-成员 2-部门\n- `userid` string 成员userid（type=1时必填）\n- `departmentid` int 部门ID（type=2时必填）\n- `auth` int 权限级别：1-可下载 2-可预览 3-可编辑 4-可管理\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"spaceid\": \"s_3b5ca2b43e454b89a6b4c32e516e4e99\"\n}\n```\n\n### 获取空间信息\n\n获取指定空间的详细信息：\n\n```php\n$result = $wedrive->space->get('admin_userid', 's_3b5ca2b43e454b89a6b4c32e516e4e99');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"space_info\": {\n        \"spaceid\": \"s_3b5ca2b43e454b89a6b4c32e516e4e99\",\n        \"space_name\": \"项目资料空间\",\n        \"auth_info\": [\n            {\n                \"type\": 1,\n                \"userid\": \"zhangsan\",\n                \"auth\": 4,\n                \"create_time\": 1635724800\n            }\n        ],\n        \"space_sub_type\": 0,\n        \"space_capacity\": 1073741824,\n        \"space_used\": 268435456\n    }\n}\n```\n\n### 重命名空间\n\n修改空间名称：\n\n```php\n$result = $wedrive->space->rename(\n    'admin_userid',\n    's_3b5ca2b43e454b89a6b4c32e516e4e99',\n    '更新后的空间名称'\n);\n```\n\n### 解散空间\n\n删除指定的空间：\n\n```php\n$result = $wedrive->space->dismiss('admin_userid', 's_3b5ca2b43e454b89a6b4c32e516e4e99');\n```\n\n## 文件管理\n\n### 上传文件\n\n上传文件到指定空间：\n\n```php\n$result = $wedrive->file->upload([\n    'userid' => 'zhangsan',\n    'spaceid' => 's_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'fatherid' => 'parent_folder_id',  // 父文件夹ID，根目录为空\n    'file_name' => '项目文档.docx',\n    'file_base64_content' => base64_encode(file_get_contents('/path/to/file.docx'))\n]);\n```\n\n**参数说明：**\n- `userid` string 操作者userid\n- `spaceid` string 空间ID\n- `fatherid` string 父文件夹ID，根目录时为空\n- `file_name` string 文件名\n- `file_base64_content` string 文件内容的base64编码\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"fileid\": \"f_3b5ca2b43e454b89a6b4c32e516e4e99\"\n}\n```\n\n### 新建文件夹\n\n在指定位置创建文件夹：\n\n```php\n$result = $wedrive->file->createFolder([\n    'userid' => 'zhangsan',\n    'spaceid' => 's_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'fatherid' => '',  // 父目录ID，根目录为空\n    'folder_name' => '2023年度报告'\n]);\n```\n\n### 获取文件列表\n\n获取指定目录下的文件和文件夹列表：\n\n```php\n$result = $wedrive->file->list([\n    'userid' => 'zhangsan',\n    'spaceid' => 's_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'fatherid' => '',  // 目录ID，根目录为空\n    'sort_type' => 1,  // 排序方式：1-名称 2-修改时间 3-大小\n    'start' => 0,      // 起始位置\n    'limit' => 50      // 返回数量\n]);\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"has_more\": false,\n    \"next_start\": 0,\n    \"file_list\": [\n        {\n            \"fileid\": \"f_3b5ca2b43e454b89a6b4c32e516e4e99\",\n            \"file_name\": \"项目文档.docx\",\n            \"spaceid\": \"s_3b5ca2b43e454b89a6b4c32e516e4e99\",\n            \"fatherid\": \"\",\n            \"file_size\": 1024000,\n            \"ctime\": 1635724800,\n            \"mtime\": 1635724800,\n            \"file_type\": 1,\n            \"file_status\": 1\n        }\n    ]\n}\n```\n\n### 下载文件\n\n获取文件下载链接：\n\n```php\n$result = $wedrive->file->download('zhangsan', 'f_3b5ca2b43e454b89a6b4c32e516e4e99');\n```\n\n**返回结果：**\n```json\n{\n    \"errcode\": 0,\n    \"errmsg\": \"ok\",\n    \"download_url\": \"https://file.work.weixin.qq.com/xxx\",\n    \"cookie_name\": \"wedrive_ticket\",\n    \"cookie_value\": \"xxx\"\n}\n```\n\n### 移动文件\n\n移动文件或文件夹到指定位置：\n\n```php\n$result = $wedrive->file->move([\n    'userid' => 'zhangsan',\n    'spaceid' => 's_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'fileid' => 'f_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'replace' => false,  // 是否覆盖同名文件\n    'fatherid' => 'target_folder_id'  // 目标文件夹ID\n]);\n```\n\n### 删除文件\n\n删除指定的文件或文件夹：\n\n```php\n$result = $wedrive->file->delete('zhangsan', 'f_3b5ca2b43e454b89a6b4c32e516e4e99');\n```\n\n### 重命名文件\n\n修改文件或文件夹名称：\n\n```php\n$result = $wedrive->file->rename([\n    'userid' => 'zhangsan',\n    'fileid' => 'f_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'new_name' => '新文件名.docx'\n]);\n```\n\n## 权限管理\n\n### 设置文件权限\n\n设置文件的访问权限：\n\n```php\n$authInfo = [\n    [\n        'type' => 1,\n        'userid' => 'lisi',\n        'auth' => 2  // 可预览\n    ],\n    [\n        'type' => 2,\n        'departmentid' => 3,\n        'auth' => 1  // 可下载\n    ]\n];\n\n$result = $wedrive->file->setAuth([\n    'userid' => 'zhangsan',\n    'fileid' => 'f_3b5ca2b43e454b89a6b4c32e516e4e99',\n    'auth_info' => $authInfo\n]);\n```\n\n### 获取文件权限\n\n查询文件的权限设置：\n\n```php\n$result = $wedrive->file->getAuth('zhangsan', 'f_3b5ca2b43e454b89a6b4c32e516e4e99');\n```\n\n## 使用示例\n\n### 项目文档管理\n\n```php\nuse EasyWeChat\\Factory;\n\n$config = [\n    'corp_id' => 'your-corp-id',\n    'agent_id' => 'your-agent-id',\n    'secret' => 'your-secret',\n    // ...\n];\n\n$app = Factory::work($config);\n$wedrive = $app->wedrive;\n\n// 1. 创建项目空间\n$authInfo = [\n    [\n        'type' => 1,\n        'userid' => 'project_manager',\n        'auth' => 4  // 项目经理有管理权限\n    ],\n    [\n        'type' => 2,\n        'departmentid' => 10,  // 开发部门\n        'auth' => 3  // 可编辑\n    ],\n    [\n        'type' => 2,\n        'departmentid' => 20,  // 测试部门\n        'auth' => 2  // 可预览\n    ]\n];\n\n$space = $wedrive->space->create('admin', 'Alpha项目文档空间', $authInfo);\n\nif ($space['errcode'] === 0) {\n    $spaceId = $space['spaceid'];\n    echo \"空间创建成功: {$spaceId}\\n\";\n    \n    // 2. 创建文件夹结构\n    $folders = ['需求文档', '设计文档', '开发文档', '测试文档'];\n    \n    foreach ($folders as $folderName) {\n        $folder = $wedrive->file->createFolder([\n            'userid' => 'project_manager',\n            'spaceid' => $spaceId,\n            'fatherid' => '',\n            'folder_name' => $folderName\n        ]);\n        \n        if ($folder['errcode'] === 0) {\n            echo \"文件夹创建成功: {$folderName}\\n\";\n        }\n    }\n    \n    // 3. 上传项目文档\n    $docPath = '/path/to/project_requirements.docx';\n    if (file_exists($docPath)) {\n        $upload = $wedrive->file->upload([\n            'userid' => 'project_manager',\n            'spaceid' => $spaceId,\n            'fatherid' => '',  // 先上传到根目录\n            'file_name' => '项目需求文档.docx',\n            'file_base64_content' => base64_encode(file_get_contents($docPath))\n        ]);\n        \n        if ($upload['errcode'] === 0) {\n            echo \"文档上传成功: {$upload['fileid']}\\n\";\n        }\n    }\n}\n```\n\n### 批量文件操作\n\n```php\n// 获取空间中的所有文件\n$fileList = $wedrive->file->list([\n    'userid' => 'admin',\n    'spaceid' => $spaceId,\n    'fatherid' => '',\n    'limit' => 100\n]);\n\nif ($fileList['errcode'] === 0) {\n    foreach ($fileList['file_list'] as $file) {\n        echo \"文件: {$file['file_name']} \";\n        echo \"大小: \" . round($file['file_size'] / 1024, 2) . \"KB \";\n        echo \"修改时间: \" . date('Y-m-d H:i:s', $file['mtime']) . \"\\n\";\n        \n        // 如果是旧文件（超过30天），移动到归档文件夹\n        if ($file['mtime'] < strtotime('-30 days')) {\n            $move = $wedrive->file->move([\n                'userid' => 'admin',\n                'spaceid' => $spaceId,\n                'fileid' => $file['fileid'],\n                'fatherid' => 'archive_folder_id',\n                'replace' => false\n            ]);\n            \n            if ($move['errcode'] === 0) {\n                echo \"  -> 已移动到归档文件夹\\n\";\n            }\n        }\n    }\n}\n```\n\n### 文件分享与权限控制\n\n```php\n// 为外部合作伙伴设置特定文件的预览权限\n$shareAuth = [\n    [\n        'type' => 1,\n        'userid' => 'partner_001',\n        'auth' => 2  // 只能预览，不能下载\n    ]\n];\n\n$setAuth = $wedrive->file->setAuth([\n    'userid' => 'project_manager',\n    'fileid' => $fileId,\n    'auth_info' => $shareAuth\n]);\n\nif ($setAuth['errcode'] === 0) {\n    // 获取下载链接供分享\n    $download = $wedrive->file->download('partner_001', $fileId);\n    \n    if ($download['errcode'] === 0) {\n        echo \"分享链接: {$download['download_url']}\\n\";\n        echo \"访问凭证: {$download['cookie_value']}\\n\";\n    }\n}\n```\n\n## 注意事项\n\n1. **存储限制**：每个企业的微盘空间有总容量限制\n2. **文件大小**：单个文件上传大小有限制，大文件建议分片上传\n3. **权限继承**：子文件夹会继承父文件夹的权限设置\n4. **删除恢复**：删除的文件会进入回收站，可以恢复\n5. **并发操作**：避免同时对同一文件进行多个操作\n\n## 最佳实践\n\n1. **合理规划空间结构**：按项目或部门创建独立空间\n2. **权限最小化原则**：只给必要的访问权限\n3. **定期清理**：定期清理不需要的文件，释放存储空间\n4. **版本管理**：对重要文档进行版本控制\n5. **备份策略**：重要文件建议进行额外备份\n\n## 错误码说明\n\n| 错误码 | 说明 |\n|--------|------|\n| 0 | 成功 |\n| 40003 | 无效的UserID |\n| 41006 | 缺少spaceid参数 |\n| 41008 | 缺少fileid参数 |\n| 85005 | 文件不存在 |\n| 85006 | 空间不存在 |\n| 85007 | 没有权限 |\n| 85008 | 空间容量不足 |"
  },
  {
    "path": "docs/src/6.x/cache.md",
    "content": "# 缓存\n\nEasyWeChat 6.x 使用 [symfony/cache](https://github.com/symfony/cache) 组件来处理缓存，它支持目前几乎所有主流的缓存引擎。\n\n在 SDK 中，所有缓存默认使用文件系统缓存，缓存路径取决于 PHP 的临时目录。如果你需要自定义缓存配置，可以通过简单的几个步骤来实现。\n\n## 默认缓存行为\n\nEasyWeChat 6.x 中的应用实例（如 `OfficialAccount\\Application`、`MiniApp\\Application` 等）都通过 `InteractWithCache` trait 提供了统一的缓存接口：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$app = new Application([\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // ...\n]);\n\n// 获取默认缓存实例\n$cache = $app->getCache();\n\n// 缓存默认配置\necho $app->getCacheLifetime(); // 1500 秒\necho $app->getCacheNamespace(); // 'easywechat'\n```\n\n## 缓存配置调整\n\n你可以调整缓存的生命周期和命名空间：\n\n```php\n// 设置缓存生命周期为 3600 秒（1小时）\n$app->setCacheLifetime(3600);\n\n// 设置缓存命名空间\n$app->setCacheNamespace('my_wechat_app');\n```\n\n## 使用 Redis 缓存\n\n### 基础 Redis 配置\n\n使用 Redis 作为缓存引擎是最常见的需求。首先安装 Redis 相关扩展：\n\n```bash\ncomposer require predis/predis\n```\n\n然后配置 Redis 缓存：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n// 创建应用实例\n$app = new Application([\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n]);\n\n// 创建 Redis 连接\n$redis = new \\Predis\\Client([\n    'scheme' => 'tcp',\n    'host'   => '127.0.0.1',\n    'port'   => 6379,\n]);\n\n// 创建 Redis 缓存适配器\n$cache = new Psr16Cache(\n    new RedisAdapter($redis, 'easywechat', 1500)\n);\n\n// 设置自定义缓存\n$app->setCache($cache);\n```\n\n### 使用 Redis 集群\n\n对于 Redis 集群环境：\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n// Redis 集群配置\n$redis = new \\Predis\\Client([\n    [\n        'scheme' => 'tcp',\n        'host'   => '10.0.0.1',\n        'port'   => 6379,\n    ],\n    [\n        'scheme' => 'tcp', \n        'host'   => '10.0.0.2',\n        'port'   => 6379,\n    ],\n], [\n    'cluster' => 'redis',\n]);\n\n$cache = new Psr16Cache(\n    new RedisAdapter($redis, 'easywechat_cluster', 1500)\n);\n\n$app->setCache($cache);\n```\n\n### 使用 phpredis 扩展\n\n如果你使用 phpredis 扩展：\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n// 使用 phpredis\n$redis = new \\Redis();\n$redis->connect('127.0.0.1', 6379);\n$redis->select(1); // 选择数据库\n\n$cache = new Psr16Cache(\n    new RedisAdapter($redis, 'easywechat', 1500)\n);\n\n$app->setCache($cache);\n```\n\n## 使用 Memcached 缓存\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\MemcachedAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n// 创建 Memcached 连接\n$memcached = new \\Memcached();\n$memcached->addServer('127.0.0.1', 11211);\n\n// 创建缓存实例\n$cache = new Psr16Cache(\n    new MemcachedAdapter($memcached, 'easywechat', 1500)\n);\n\n$app->setCache($cache);\n```\n\n## 在 Laravel 中使用\n\nLaravel 框架提供了便捷的缓存管理，你可以直接使用 Laravel 的缓存驱动：\n\n### 使用 Laravel Cache\n\n```php\nuse Symfony\\Component\\Cache\\Adapter\\Psr16Adapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\n// 将 Laravel Cache 转换为 PSR-16 缓存\n$cache = new Psr16Cache(\n    new Psr16Adapter(\n        app('cache')->store() // 使用默认缓存驱动\n    )\n);\n\n$app->setCache($cache);\n```\n\n### 使用指定的 Laravel Cache Store\n\n```php\n// 使用 Redis 作为缓存驱动\n$cache = new Psr16Cache(\n    new Psr16Adapter(\n        app('cache')->store('redis')\n    )\n);\n\n$app->setCache($cache);\n```\n\n### Laravel 服务提供者中配置\n\n在 Laravel 服务提供者中统一配置：\n\n```php\n// app/Providers/WeChatServiceProvider.php\nuse EasyWeChat\\OfficialAccount\\Application;\nuse Illuminate\\Support\\ServiceProvider;\nuse Symfony\\Component\\Cache\\Adapter\\Psr16Adapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\nclass WeChatServiceProvider extends ServiceProvider\n{\n    public function register()\n    {\n        $this->app->singleton('wechat.official_account', function ($app) {\n            $application = new Application(config('wechat.official_account'));\n            \n            // 使用 Laravel 缓存\n            $cache = new Psr16Cache(\n                new Psr16Adapter(\n                    $app['cache']->store(config('wechat.cache_store', 'redis'))\n                )\n            );\n            \n            $application->setCache($cache);\n            \n            return $application;\n        });\n    }\n}\n```\n\n## 自定义缓存驱动\n\n如果现有的缓存驱动无法满足你的需求，你可以实现自己的缓存类，只需实现 [PSR-16](https://www.php-fig.org/psr/psr-16/) 规范即可。\n\n### 实现 PSR-16 接口\n\nPSR-16 `CacheInterface` 接口包含以下方法：\n\n```php\ninterface CacheInterface\n{\n    public function get($key, $default = null);\n    public function set($key, $value, $ttl = null);\n    public function delete($key);\n    public function clear();\n    public function getMultiple($keys, $default = null);\n    public function setMultiple($values, $ttl = null);\n    public function deleteMultiple($keys);\n    public function has($key);\n}\n```\n\n### 自定义缓存实现示例\n\n```php\nuse Psr\\SimpleCache\\CacheInterface;\n\nclass MyCustomCache implements CacheInterface\n{\n    private array $data = [];\n\n    public function get($key, $default = null)\n    {\n        return $this->data[$key] ?? $default;\n    }\n\n    public function set($key, $value, $ttl = null)\n    {\n        // 这里可以实现你的存储逻辑\n        // 比如存储到数据库、文件或其他存储系统\n        $this->data[$key] = $value;\n        \n        return true;\n    }\n\n    public function delete($key)\n    {\n        unset($this->data[$key]);\n        return true;\n    }\n\n    public function clear()\n    {\n        $this->data = [];\n        return true;\n    }\n\n    public function getMultiple($keys, $default = null)\n    {\n        $result = [];\n        foreach ($keys as $key) {\n            $result[$key] = $this->get($key, $default);\n        }\n        return $result;\n    }\n\n    public function setMultiple($values, $ttl = null)\n    {\n        foreach ($values as $key => $value) {\n            $this->set($key, $value, $ttl);\n        }\n        return true;\n    }\n\n    public function deleteMultiple($keys)\n    {\n        foreach ($keys as $key) {\n            $this->delete($key);\n        }\n        return true;\n    }\n\n    public function has($key)\n    {\n        $stmt = $this->pdo->prepare(\"\n            SELECT 1 FROM cache_items \n            WHERE cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())\n        \");\n        $stmt->execute([$key]);\n        return $stmt->fetchColumn() !== false;\n    }\n}\n```\n\n### 使用自定义缓存\n\n```php\n// 实例化自定义缓存\n$myCache = new MyCustomCache();\n\n// 应用到 EasyWeChat\n$app->setCache($myCache);\n```\n\n## 数据库缓存实现\n\n下面是一个使用数据库作为缓存存储的完整示例：\n\n```php\nuse Psr\\SimpleCache\\CacheInterface;\n\nclass DatabaseCache implements CacheInterface\n{\n    private \\PDO $pdo;\n    \n    public function __construct(\\PDO $pdo)\n    {\n        $this->pdo = $pdo;\n        $this->createTable();\n    }\n    \n    private function createTable()\n    {\n        $sql = \"\n            CREATE TABLE IF NOT EXISTS cache_items (\n                cache_key VARCHAR(255) PRIMARY KEY,\n                cache_value TEXT,\n                expires_at TIMESTAMP NULL\n            )\n        \";\n        $this->pdo->exec($sql);\n    }\n\n    public function get($key, $default = null)\n    {\n        $stmt = $this->pdo->prepare(\"\n            SELECT cache_value FROM cache_items \n            WHERE cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())\n        \");\n        $stmt->execute([$key]);\n        \n        $result = $stmt->fetchColumn();\n        \n        if ($result === false) {\n            return $default;\n        }\n        \n        return unserialize($result);\n    }\n\n    public function set($key, $value, $ttl = null)\n    {\n        $expiresAt = $ttl ? date('Y-m-d H:i:s', time() + $ttl) : null;\n        \n        $stmt = $this->pdo->prepare(\"\n            REPLACE INTO cache_items (cache_key, cache_value, expires_at) \n            VALUES (?, ?, ?)\n        \");\n        \n        return $stmt->execute([\n            $key, \n            serialize($value), \n            $expiresAt\n        ]);\n    }\n\n    public function delete($key)\n    {\n        $stmt = $this->pdo->prepare(\"DELETE FROM cache_items WHERE cache_key = ?\");\n        return $stmt->execute([$key]);\n    }\n\n    public function clear()\n    {\n        return $this->pdo->exec(\"DELETE FROM cache_items\") !== false;\n    }\n\n    public function getMultiple($keys, $default = null)\n    {\n        $result = [];\n        foreach ($keys as $key) {\n            $result[$key] = $this->get($key, $default);\n        }\n        return $result;\n    }\n\n    public function setMultiple($values, $ttl = null)\n    {\n        foreach ($values as $key => $value) {\n            $this->set($key, $value, $ttl);\n        }\n        return true;\n    }\n\n    public function deleteMultiple($keys)\n    {\n        foreach ($keys as $key) {\n            $this->delete($key);\n        }\n        return true;\n    }\n\n    public function has($key)\n    {\n        $stmt = $this->pdo->prepare(\"\n            SELECT 1 FROM cache_items \n            WHERE cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())\n        \");\n        $stmt->execute([$key]);\n        return $stmt->fetchColumn() !== false;\n    }\n}\n\n// 使用示例\n$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');\n$cache = new DatabaseCache($pdo);\n\n$app->setCache($cache);\n```\n\n## 缓存性能优化建议\n\n### 1. 选择合适的缓存驱动\n\n- **Redis**: 适用于分布式环境，支持丰富的数据结构\n- **Memcached**: 简单高效，适用于纯缓存场景\n- **文件缓存**: 适用于单机环境或小型应用\n- **数据库缓存**: 适用于需要持久化的场景\n\n### 2. 合理设置缓存时间\n\n```php\n// 根据数据更新频率设置不同的缓存时间\n$app->setCacheLifetime(7200); // Access Token 缓存 2 小时\n```\n\n### 3. 使用缓存前缀避免冲突\n\n```php\n// 为不同环境设置不同的命名空间\n$app->setCacheNamespace('easywechat_prod'); // 生产环境\n$app->setCacheNamespace('easywechat_dev');  // 开发环境\n```\n\n### 4. 监控缓存命中率\n\n```php\n// 在自定义缓存中添加统计功能\nclass MonitoredCache implements CacheInterface \n{\n    private $hits = 0;\n    private $misses = 0;\n    \n    public function get($key, $default = null) \n    {\n        $value = $this->actualCache->get($key, $default);\n        \n        if ($value === $default) {\n            $this->misses++;\n        } else {\n            $this->hits++;\n        }\n        \n        return $value;\n    }\n    \n    public function getHitRate(): float\n    {\n        $total = $this->hits + $this->misses;\n        return $total > 0 ? $this->hits / $total : 0;\n    }\n}\n```\n\n## 常见问题\n\n### Q: 如何清空 EasyWeChat 的缓存？\n\n```php\n// 清空当前应用的所有缓存\n$app->getCache()->clear();\n\n// 删除特定缓存项\n$app->getCache()->delete('specific_key');\n```\n\n### Q: 如何在不同的应用间共享缓存？\n\n```php\n// 使用相同的缓存实例\n$sharedCache = new Psr16Cache(new RedisAdapter($redis));\n\n$officialAccount->setCache($sharedCache);\n$miniApp->setCache($sharedCache);\n```\n\n### Q: 缓存键冲突怎么办？\n\n```php\n// 为不同应用设置不同的命名空间\n$officialAccount->setCacheNamespace('wechat_oa');\n$miniApp->setCacheNamespace('wechat_mini');\n```\n\n通过合理配置缓存，你可以显著提升 EasyWeChat 应用的性能和响应速度。选择适合你业务场景的缓存方案，并根据实际使用情况进行优化调整。\n"
  },
  {
    "path": "docs/src/6.x/client.md",
    "content": "# API 调用\n\n与以往版本不同的是，SDK 不再内置具体 API 的逻辑，所有的 API 均交由开发者自行调用，以更新用户备注为例：\n\n```php\n$api = $app->getClient();\n\n$response = $api->post('/cgi-bin/user/info/updateremark', [\n    'json' => [\n            \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n            \"remark\" => \"pangzi\"\n        ]\n    ]);\n\n// or\n// 如果参数中存在query之类的关键字建议使用上面的方法，不建议以下调用方式\n$response = $api->postJson('/cgi-bin/user/info/updateremark', [\n    \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n    \"remark\" => \"pangzi\"\n]);\n```\n\n## 语法说明\n\n```php\nget(string $uri, array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\npost(string $uri, array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\npatch(string $uri, array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\nput(string $uri, array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\ndelete(string $uri, array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\n```\n\n同时还内置了一些便捷方法：\n\n```php\npostJson(string $url, array $data = [], array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\npatchJson(string $url, array $data = [], array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\npostXml(string $url, array $data = [], array $options = []): Symfony\\Contracts\\HttpClient\\ResponseInterface\n```\n\n`$options` 为请求参数，可以指定 `query`/`body`/`json`/`xml`/`headers` 等等，具体请参考：[HttpClientInterface::OPTIONS_DEFAULTS](https://github.com/symfony/symfony/blob/6.1/src/Symfony/Contracts/HttpClient/HttpClientInterface.php)\n\n---\n\n## 请求参数\n\n### GET\n\n```php\n$response = $api->get('/cgi-bin/user/list', [\n    'next_openid' => 'OPENID1',\n]);\n```\n\n### POST\n\n```php\n$response = $api->post('/cgi-bin/user/info/updateremark', [\n    'body' => \\json_encode([\n            \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n            \"remark\" => \"pangzi\"\n        ])\n    ]);\n```\n\n或者可以简写为：\n\n```php\n$response = $api->postJson('/cgi-bin/user/info/updateremark', [\n    \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n    \"remark\" => \"pangzi\"\n]);\n```\n\n或者指定 xml 格式：\n\n```php\n$response = $api->postXml('/mmpaymkttransfers/promotion/transfers', [\n    'mch_appid' => $app->getConfig()['app_id'],\n    'mchid' => $app->getConfig()['mch_id'],\n    'partner_trade_no' => '202203081646729819743',\n    'openid' => 'ogn1H45HCRxVRiEMLbLLuABbxxxx',\n    'check_name' => 'FORCE_CHECK',\n    're_user_name'=> 'overtrue',\n    'amount' => 100,\n    'desc' => '理赔',\n ]);\n```\n\n### 请求证书\n\n你可以在请求支付时指定证书，以微信支付 V2 为例：\n\n```php\n$response = $api->post('/mmpaymkttransfers/promotion/transfers', [\n    'xml' => [\n        'mch_appid' => $app->getConfig()['app_id'],\n        'mchid' => $app->getConfig()['mch_id'],\n        'partner_trade_no' => '202203081646729819743',\n        'openid' => 'ogn1H45HCRxVRiEMLbLLuABbxxxx',\n        'check_name' => 'FORCE_CHECK',\n        're_user_name'=> 'overtrue',\n        'amount' => 100,\n        'desc' => '理赔',\n    ],\n    'local_cert' => $app->getConfig()['cert_path'],\n    'local_pk' => $app->getConfig()['key_path'],\n    ]);\n```\n\n> 参考：[symfony/http-client#options](https://symfony.com/doc/current/reference/configuration/framework.html#local-cert)\n\n### 文件上传\n\n你有两种上传文件的方式可以选择：\n\n#### 从指定路径上传\n\n```php\nuse EasyWeChat\\Kernel\\Form\\File;\nuse EasyWeChat\\Kernel\\Form\\Form;\n\n$options = Form::create(\n    [\n        'media' => File::fromPath('/path/to/image.jpg'),\n    ]\n)->toArray();\n\n$response = $api->post('cgi-bin/media/upload?type=image', $options);\n```\n\n#### 从二进制内容上传\n\n```php\nuse EasyWeChat\\Kernel\\Form\\File;\nuse EasyWeChat\\Kernel\\Form\\Form;\n\n$options = Form::create(\n    [\n        'media' => File::withContents($contents, 'image.jpg'), // 注意：请指定文件名\n    ]\n)->toArray();\n\n$response = $api->post('cgi-bin/media/upload?type=image', $options);\n```\n\n#### 简化写法 <version-tag>6.4.0+</version-tag>\n\n上面的两种传法都可以简写为下面的方式：\n\n```php\n// withFile(string $localPath, string $formName = 'file', string $filename = null)\n$media = $client->withFile($path, 'media')->post('cgi-bin/media/upload?type=image');\n\n// withFileContents(string $contents, string $formName = 'file', string $filename = null)\n$media = $client->withFileContents($contents, 'media', 'filename.png')->post('cgi-bin/media/upload?type=image');\n```\n\n## 自定义 access_token\n\n自定义 Access Token 需要实现接口 `EasyWeChat\\Kernel\\Contracts\\AccessToken`：\n\n```php\nclass MyAccessToken implements EasyWeChat\\Kernel\\Contracts\\AccessToken\n{\n    public function getToken(): string\n    {\n        // 你的逻辑\n        return 'your token';\n    }\n\n    public function toQuery(): array\n    {\n        return ['access_token' => $this->getToken()];\n    }\n}\n```\n\n```php\n$client->withAccessToken(new MyAccessToken());\n$client->get('xxxx');\n$client->post('xxxx');\n//...\n```\n\n## 预置参数的传递 <version-tag>6.4.0+</version-tag>\n\n在调用 API 的时候难免有的需要传递账号的一些信息，尤其是支付相关的 API，例如[查询订单](https://pay.weixin.qq.com/doc/v3/merchant/4012791858)：\n\n```php\n$client->get('v3/pay/transactions/id/1217752501201407033233368018', [\n    'mchid' => $app->getAccount()->getMchid(),\n]);\n```\n\n不得不把商户号这种基础信息再读取传递一遍，比较麻烦，设计了如下的简化方案：\n\n```php\n$client->withMchId()->get('endpoint');\n// 结果：endpoint?mchid=xxxxx\n```\n\n注意：`withMchId` 默认产生的结果为：`mch_id`，因为微信支付官方的各种不统一， 可能有的场景你需要的不是 `mch_id` 而是 `mchid`, 可以通过 `withMchIdAs('mchid')` 来指定，比如：\n\n```php\n$client->withMchIdAs('mchid')->get('v3/pay/transactions/id/1217752501201407033233368018');\n\n// 结果：v3/pay/transactions/id/1217752501201407033233368018?mchid=xxxxx\n```\n\n原理就是 `with` + `配置 key`：\n\n> 注意: 如果配置key含有下划线的，如 `app_id` 应该转换为大写 `withAppId`\n\n```php\n$client->withAppId()->post('/path/to/resources', [...]);\n$client->withAppId()->withMchid()->post('/path/to/resources', [...]);\n```\n\n也可以自定义值：\n\n```php\n$client->withAppId('12345678')->post('/path/to/resources', [...]);\n// or\n$client->with('appid', '123456')->post('/path/to/resources', [...]);\n```\n\n还可以设置别名：把 `appid` 作为参数 `mch_appid` 值使用：\n\n```php\n$client->withAppIdAs('mch_appid')->post('/path/to/resources', [...]);\n```\n\n其它通用方法：\n\n```php\n$client->with('appid')->post(...)\n$client->with(['appid', 'mchid'])->post(...)\n$client->with(['appid' => '1234565', 'mchid'])->post(...)\n```\n\n---\n\n## 处理响应\n\nAPI Client 基于 [symfony/http-client](https://github.com/symfony/http-client) 实现，你可以通过以下方式对响应值进行访问：\n\n### 异常处理 <version-tag>6.3.0+</version-tag>\n\n当请求失败，例如状态码不为 200 时，默认访问响应内容都会抛出异常：\n\n```php\n$response->getContent(); // 这里会抛出异常\n```\n\n如果你不希望默认抛出异常，而希望自己处理，可以在配置文件指定 `http.throw` 参数为 `false`：\n\n```php\n$config = [\n  //...\n  'http' => [\n    'throw' => false,\n    //...\n  ],\n];\n```\n\n这样，你就可以在调用 API 时，自己处理异常：\n\n```php\n$options = [\n    'query' => [\n        'openid' => 'oDF3iY9ffA-hqb2vVvbr7qxf6A0Q',\n    ]\n];\n$response = $api->get('/cgi-bin/user/get', $options);\n\nif ($response->isFailed()) {\n    // 出错了，处理异常\n}\n\nreturn $response;\n```\n\n或者不改变默认配置的情况下，在调用请求时单独设置`throw(false)`，若该请求失败，也可以自己处理异常。\n\n```php\n// $options 同上文，这里省略\n$response = $api->get('/cgi-bin/user/get', $options)->throw(false);\n\nif ($response->isFailed()) {\n    // 出错了，处理异常\n}\n\nreturn $response;\n```\n\n### 数组式访问\n\nEasyWeChat 增强了 API 响应对象，比如增加了数组式访问，你可以不用每次 `toArray` 后再取值，更加便捷美观：\n\n```php\n$response = $api->get('/foo/bar');\n\n$response['foo']; // \"bar\"\nisset($response['foo']); // true\n```\n\n### 获取状态码\n\n```php\n$response->getStatusCode();\n// 200\n```\n\n### 判断业务是否成功/失败 <version-tag>6.3.0+</version-tag>\n\n比如状态码是 200，但是公众号接口返回 40029 code 错误：\n\n```php\n$response->isSuccessful();  // false\n$response->isFailed();      // true\n```\n\n### 获取响应头\n\n```php\n$response->getHeaders();\n// ['content-type' => ['application/json;encoding=utf-8'], '...']\n\n$response->getHeader('content-type');\n// ['application/json;encoding=utf-8']\n\n$response->getHeaderLine('content-type');\n// 'application/json;encoding=utf-8'\n```\n\n### 获取响应内容\n\n```php\n$response->getContent();\n$response->getContent(false); // 失败不抛出异常\n// {\"foo\":\"bar\"}\n\n// 获取 json 转换后的数组格式\n$response->toArray();\n$response->toArray(false); // 失败不抛出异常\n// [\"foo\" => \"bar\"]\n\n// 获取 json\n$response->toJson();\n$response->toJson(false);\n// {\"foo\":\"bar\"}\n\n// 将内容转换成流返回\n$response->toStream();\n$response->toStream(false); // 失败不抛出异常\n```\n\n### 转换为 PSR-7 Response <version-tag>6.6.0+</version-tag>\n\n如果你希望直接将 API 响应转换成 [PSR-7 规范](https://www.php-fig.org/psr/psr-7/) Response，可以使用 `toPsrResponse` 方法：\n\n```php\n$psrResponse = $response->toPsrResponse();\n```\n\n比如在 Laravel 中就可以这样使用：\n\n```php\nreturn $response->toPsrResponse();\n```\n\n### 保存到文件 <version-tag>6.3.0+</version-tag>\n\n你可以方便的将内容直接存储到文件：\n\n```php\n$path = $response->saveAs('/path/to/file.jpg');\n// /path/to/file.jpg\n```\n\n### 转换为 Data URLs <version-tag>6.3.0+</version-tag>\n\n你可以将内容转换为[Data URLs](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)\n\n```php\n$dataUrl = $response->toDataUrl();\n// data:image/png,%89PNG%0D%0A...\n```\n\n### 获取其他上下文信息\n\n如：\"response_headers\", \"redirect_count\", \"start_time\", \"redirect_url\" 等：\n\n```php\n$httpInfo = $response->getInfo();\n\n// 获取指定信息\n$startTime = $response->getInfo('start_time');\n\n// 获取请求日志\n$httpLogs = $response->getInfo('debug');\n```\n\n:book: 更多使用请参考： [HTTP client: Processing Responses](https://symfony.com/doc/current/http_client.html#processing-responses)\n\n---\n\n## 异步请求\n\n所有的请求都是异步的，当你第一次访问 `$response` 时才会真正的请求，比如：\n\n```php\n// 这段代码会立即执行，并不会发起网络请求\n$response = $api->postJson('/cgi-bin/user/info/updateremark', [\n    \"openid\" => \"oDF3iY9ffA-hqb2vVvbr7qxf6A0Q\",\n    \"remark\" => \"pangzi\"\n]);\n\n// 当你尝试访问 $response 的信息时，才会发起请求并等待返回\n$contentType = $response->getHeaders()['content-type'][0];\n\n// 尝试获取响应内容将阻塞执行，直到接收到完整的响应内容\n$content = $response->getContent();\n```\n\n## 并行请求\n\n由于请求天然是异步的，那么你可以很简单实现并行请求：\n\n```php\n$responses = [\n    $api->get('/cgi-bin/user/get'),\n    $api->post('/cgi-bin/user/info/updateremark', ['body' => ...]),\n    $api->post('/cgi-bin/user/message/custom/send', ['body' => ...]),\n];\n\n// 访问任意一个 $response 时将执行并发请求：\nforeach ($responses as $response) {\n    $content = $response->getContent();\n    // ...\n}\n```\n\n当然你也可以给每个请求分配名字独立访问：\n\n```php\n$responses = [\n    'users' => $api->get('/cgi-bin/user/get'),\n    'remark' => $api->post('/cgi-bin/user/info/updateremark', ['body' => ...]),\n    'message' => $api->post('/cgi-bin/user/message/custom/send', ['body' => ...]),\n];\n\n// 访问任意一个 $response 时将执行并发请求：\n$responses['users']->toArray();\n```\n\n## 失败重试 <version-tag>6.1.0+</version-tag>\n\n默认在公众号、小程序开启了重试机制，你可以通过全局配置或者手动开启重试特性。\n\n> 🚨 不建议在支付模块使用重试功能，因为一旦重试导致支付数据异常，可能造成无法挽回的损失。\n\n### 方式一：全局配置\n\n在支持重试的模块里增加如下配置可以完成重试机制的全局启用：\n\n```php\n    'http' => [\n        //...\n        'retry' => true, // 使用默认配置\n        // 'retry' => [\n        //     // 仅以下状态码重试\n        //     'status_codes' => [429, 500]\n        //     'max_retries' => 3\n        //     // 请求间隔 (毫秒)\n        //     'delay' => 1000,\n        //     // 如果设置，每次重试的等待时间都会增加这个系数\n        //     // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n        //     'multiplier' => 0.1\n        // ],\n    ],\n```\n\n### 方式二：手动开启\n\n如果你不想使用基于配置的全局重试机制，你可以使用 `HttpClient::retry()` 方法来开启失败重试能力：\n\n```php\n$app->getClient()->retry()->get('/foo/bar');\n```\n\n当然，你可以在 `retry` 配置中自定义重试的配置，如下所示：\n\n```php\n$app->getClient()->retry([\n    'max_retries' => 2,\n    //...\n])->get('/foo/bar');\n```\n\n### 自定义重试策略\n\n如果觉得参数不能满足需求，你还可以自己实现 [`Symfony\\Component\\HttpClient\\RetryStrategyInterface`](https://github.com/symfony/symfony/blob/6.1/src/Symfony/Component/HttpClient/Retry/RetryStrategyInterface.php) 接口来自定义重试策略，然后调用 `retryUsing` 方法来使用它。\n\n> 💡 建议继承基类来拓展，以实现默认重试类的基础功能。\n\n```php\nclass MyRetryStrategy extends \\Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy\n{\n    public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool\n    {\n        // 你的自定义逻辑\n        // if (...) {\n        //     return false;\n        // }\n\n        return parent::shouldRetry($context, $responseContent, $exception);\n    }\n}\n```\n\n使用自定义重试策略：\n\n```php\n$app->getClient()->retryUsing(new MyRetryStrategy())->get('/foo/bar');\n```\n\n## 更多使用方法\n\n:book: 更多使用请参考：[symfony/http-client](https://github.com/symfony/http-client)\n"
  },
  {
    "path": "docs/src/6.x/contributing.md",
    "content": "# 贡献代码\n\n## 开发\n\n我们欢迎广大开发者贡献大家的智慧，让我们共同让它变得更完美.\n\n### 开始之前\n\n请严格遵循以下代码标准:\n\n> - [PSR-12](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md).\n> - 使用 4 个空格作为缩进。\n\n### 流程\n\n1. Fork [w7corp/easywechat](https://github.com/w7corp/easywechat) 并 Clone 到本地.\n2. 创建新的分支：\n\n```shell:no-line-numbers\ngit checkout -b new_feature\n```\n\n3. 编写代码。\n4. Push 到你的分支:\n\n```shell:no-line-numbers\ngit push origin new_feature\n```\n\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n> 注意：注释请使用英文\n\n## 更新文档\n\n我们的文档也是开源的，源代码在 [w7corp/EasyWeChat/docs](https://github.com/w7corp/easywechat/tree/6.x/docs)\n\n### 流程\n\n1. Fork [w7corp/EasyWeChat](https://github.com/w7corp/easywechat)\n2. Clone 到你的电脑：\n\n```shell:no-line-numbers\ngit clone https://github.com/<username>/site.git\ncd docs\n```\n\n3. 创建新的分支，编辑文档\n4. Push 到你的分支。\n5. 创建 Pull Request 并描述你完成的功能或者做出的修改。\n\n## 报告 Bug\n\n当你在使用过程中遇到问题，请查阅 [疑难解答](troubleshooting.md) 或者在这里提问 [GitHub](https://github.com/w7corp/easywechat/issues). 如果还是不能解决你的问题，请到 GitHub 联系我们。\n\n[w7corp/easywechat]: https://github.com/w7corp/easywechat\n"
  },
  {
    "path": "docs/src/6.x/index.md",
    "content": "> 👋🏼 您当前浏览的文档为 6.x，其它版本的文档请参考：[5.x](/5.x/)、[4.x](/4.x/)、[3.x](/3.x/)\n\n\n# EasyWeChat\n\nEasyWeChat 是一个开源的 [微信](http://www.wechat.com) 非官方 SDK。安装非常简单，因为它是一个标准的 [Composer](https://getcomposer.org/) 包，这意味着任何满足下列安装条件的 PHP 项目支持 Composer 都可以使用它。\n\n## 环境需求\n\n- PHP >= 8.0\n- [PHP cURL 扩展](http://php.net/manual/en/book.curl.php)\n- [PHP OpenSSL 扩展](http://php.net/manual/en/book.openssl.php)\n- [PHP SimpleXML 扩展](http://php.net/manual/en/book.simplexml.php)\n- [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php)\n\n## 安装\n\n::: warning\n阿里云的 Composer 源已经停止维护，请停止使用，或更换官方源，或者[腾讯软件源](https://mirrors.tencent.com/help/composer.html)\n:::\n\n\n```shell:no-line-numbers\ncomposer require w7corp/easywechat:^6.17\n```\n\n## 使用\n\n从 6.x 起，EasyWeChat 依然保持了它开箱即用的特性，同样只需要传入一个配置，初始化一个模块实例即可：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n    'token' => 'easywechat',\n    'aes_key' => '' // 明文模式请勿填写 EncodingAESKey\n    //...\n];\n\n$app = new Application($config);\n```\n\n在创建实例后，所有的方法都几乎可以有 IDE 自动补全，当然，建议先阅读各模块的文档了解一下它们的区别，这里我们以调用公众号获取用户资料为例：\n\n```php\n$response = $app->getClient()->get(\"/cgi-bin/user/info?openid={$openid}&lang=zh_CN\");\n\n# 查看返回结果\nvar_dump($response->toArray());\n```\n\n## 开始之前\n\n在你动手写代码之前，建议您首先阅读以下内容：\n\n- [关于 6.x](./introduction.md)\n- [API 调用](./client.md)\n\n## 参与贡献\n\n我们欢迎广大开发者贡献大家的智慧，让我们共同让它变得更完美。您可以在 GitHub 上提交 Pull Request，我们会尽快审核并公布。更多信息请参考 [贡献指南](contributing.md)。\n\n## 开发者交流群\n\n[EasyWeChat SDK 交流群](http://shang.qq.com/wpa/qunwpa?idkey=b4dcf3ec51a7e8c3c3a746cf450ce59895e5c4ec4fbcb0f80c2cd97c3c6e63e9) ID: 319502940\n"
  },
  {
    "path": "docs/src/6.x/introduction.md",
    "content": "---\naside: false\nprev:\n  text: 参与贡献\n  link: ./contributing\n---\n\n# 版本说明\n\n如果您是之前的老版本用户，你就会发现，6.x 版本是没办法从旧版轻松升级的，当然如果非必要，我也不建议你去动已有的项目升级。\n\n## 关于 6.x\n\n6.x 和之前的版本设计风格完全不一样，可以说是一个完全重写的版本，其中最大的变化点是：\n\n### 不再封装业务接口\n\n从 6.x 起 SDK 中不再内置具体业务的接口，仅封装底层基础部分，如认证、授权和 API 客户端。为什么不再封装业务接口，主要有以下几个原因：\n\n##### 二次封装的学习成本\n    \n从使用者角度，首先需要了解微信的接口，然后再到 SDK 去寻找对应二次封装的新名称和新的用法，为啥会是新的名称呢？因为微信的接口起名太随意，很多名字无法表意或者有歧义，所以在以前的版本中我们精心设计了新的名字来掩盖这个问题，但是起名这件事情，相信写过代码的同学都知道，它是非常困难的，尤其微信这种杂乱的接口系统，越到后面越词穷。所以二次封装，不仅增加了用户的使用学习成本，也同时增加了 SDK 的封装成本。\n\n##### 更新时效性不足 \n    \n当有新接口，或者接口变化的时候，SDK 很难做到及时的更新，毕竟用爱发电的人也要工作和生活，大部分都需要好久才会有空把它同步更新过来，即使我已经很努力去尽可能做到快速更新，但还是无法满足使用者的诉求。虽然 SDK 在设计之初就已经考虑过这个问题提供了底层直接调用接口地址的能力，但很多人不知道。\n\n### 去容器化设计\n\n从 3.x 起我们重写了整个 SDK 的架构，引入了 [silexphp/Pimple](https://github.com/silexphp/Pimple) 作为服务容器，将各模块装载到服务容器，随取随用，一直沿用到 5.x。\n\n6.x 开始，因为也不需要再二次封装业务接口，容器存在的必要性就不大了。最终去掉了容器这一层，将各个模块类以纯粹的独立类存在，使用依赖注入的模式传入需要的依赖对象，方便有自定义需求或者特殊运行环境的同学使用自己的类做替换和更方便的生命周期管理。为了方便大部分自定义需求少的使用者场景，在各生态模块外层，提供了工厂类以便于最小成本的完成开发对接。\n\n### 几乎 100% 的自定义能力支持\n\n在继续保持开箱即用的能力下，在 6.x 中你几乎可以做到任何模块的替换，每一个模块都以接口形式依赖，所以你可以轻松的替换任何一个你不满意的模块。当然也包括底层的 Http Client。\n\n## 感谢\n\n感谢所有 EasyWeChat 用户的热爱和支持，更要感谢所有的贡献者，因为爱好产生了 EasyWeChat，因为大家的热爱它才成为了今天拥有无数使用者的 EasyWeChat。希望在未来的路上我们能保持初心，一起努力，一起成长。\n\n- [@overtrue](https://github.com/overtrue) 2022/02/25\n\n### 感谢可爱的贡献者们\n\n<a href=\"https://github.com/w7corp/easywechat/graphs/contributors\"><img src=\"https://opencollective.com/wechat/contributors.svg?width=890\" /></a>\n"
  },
  {
    "path": "docs/src/6.x/logging.md",
    "content": "# 自定义 HTTP 客户端日志\n\n在 6.x 版本中，虽然移除了默认的日志功能，但用户仍然可以通过自定义 HTTP 客户端来实现请求和响应的日志记录。\n\n## 基本用法\n\n所有的 Application 类都实现了 `LoggerAwareInterface`，你可以设置一个日志记录器，然后创建一个支持日志的 HTTP 客户端：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\nuse Monolog\\Logger;\nuse Monolog\\Handler\\StreamHandler;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Psr\\Log\\LoggerAwareInterface;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n    // 其他配置...\n];\n\n$app = new Application($config);\n\n// 创建一个日志记录器\n$logger = new Logger('easywechat');\n$logger->pushHandler(new StreamHandler('/path/to/your/logfile.log', Logger::DEBUG));\n\n// 设置日志记录器到应用实例\n$app->setLogger($logger);\n\n// 创建支持日志的 HTTP 客户端\n$httpClient = HttpClient::create();\n\n// 如果 HTTP 客户端支持 LoggerAwareInterface，将自动设置日志记录器\nif ($httpClient instanceof LoggerAwareInterface) {\n    $httpClient->setLogger($logger);\n}\n\n// 设置自定义 HTTP 客户端\n$app->setHttpClient($httpClient);\n```\n\n## 使用装饰器模式的日志客户端\n\n你也可以创建一个装饰器来包装现有的 HTTP 客户端，添加日志功能：\n\n```php\nuse Psr\\Log\\LoggerInterface;\nuse Psr\\Log\\LoggerAwareInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass LoggingHttpClient implements HttpClientInterface, LoggerAwareInterface\n{\n    private HttpClientInterface $client;\n    private ?LoggerInterface $logger = null;\n\n    public function __construct(HttpClientInterface $client)\n    {\n        $this->client = $client;\n    }\n\n    public function setLogger(LoggerInterface $logger): void\n    {\n        $this->logger = $logger;\n        \n        if ($this->client instanceof LoggerAwareInterface) {\n            $this->client->setLogger($logger);\n        }\n    }\n\n    public function request(string $method, string $url, array $options = []): ResponseInterface\n    {\n        // 记录请求日志\n        if ($this->logger) {\n            $this->logger->info('HTTP Request', [\n                'method' => $method,\n                'url' => $url,\n                'options' => $this->sanitizeOptions($options),\n            ]);\n        }\n\n        $startTime = microtime(true);\n        \n        try {\n            $response = $this->client->request($method, $url, $options);\n            \n            // 记录响应日志\n            if ($this->logger) {\n                $duration = microtime(true) - $startTime;\n                $this->logger->info('HTTP Response', [\n                    'method' => $method,\n                    'url' => $url,\n                    'status_code' => $response->getStatusCode(),\n                    'duration' => round($duration * 1000, 2) . 'ms',\n                ]);\n            }\n            \n            return $response;\n        } catch (\\Throwable $e) {\n            // 记录错误日志\n            if ($this->logger) {\n                $duration = microtime(true) - $startTime;\n                $this->logger->error('HTTP Request Failed', [\n                    'method' => $method,\n                    'url' => $url,\n                    'error' => $e->getMessage(),\n                    'duration' => round($duration * 1000, 2) . 'ms',\n                ]);\n            }\n            \n            throw $e;\n        }\n    }\n\n    public function stream($responses, float $timeout = null): iterable\n    {\n        return $this->client->stream($responses, $timeout);\n    }\n\n    private function sanitizeOptions(array $options): array\n    {\n        // 移除敏感信息，如密码、密钥等\n        if (isset($options['auth'])) {\n            $options['auth'] = '[HIDDEN]';\n        }\n        \n        if (isset($options['headers']['Authorization'])) {\n            $options['headers']['Authorization'] = '[HIDDEN]';\n        }\n        \n        return $options;\n    }\n}\n```\n\n## 使用自定义日志客户端\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\nuse Monolog\\Logger;\nuse Monolog\\Handler\\StreamHandler;\nuse Symfony\\Component\\HttpClient\\HttpClient;\n\n$config = [\n    'app_id' => 'your-app-id',\n    'secret' => 'your-app-secret',\n];\n\n$app = new Application($config);\n\n// 创建日志记录器\n$logger = new Logger('easywechat-http');\n$logger->pushHandler(new StreamHandler('/var/log/easywechat-http.log', Logger::DEBUG));\n\n// 创建装饰后的 HTTP 客户端\n$baseClient = HttpClient::create();\n$loggingClient = new LoggingHttpClient($baseClient);\n$loggingClient->setLogger($logger);\n\n// 设置到应用实例\n$app->setHttpClient($loggingClient);\n\n// 现在所有的 HTTP 请求都会被记录\n$accessToken = $app->getAccessToken()->getToken();\n```\n\n## 日志输出示例\n\n使用上述配置后，你的日志文件将包含类似以下的内容：\n\n```\n[2024-01-01 10:00:00] easywechat-http.INFO: HTTP Request {\"method\":\"GET\",\"url\":\"https://api.weixin.qq.com/cgi-bin/token\",\"options\":{\"query\":{\"grant_type\":\"client_credential\",\"appid\":\"your-app-id\",\"secret\":\"[HIDDEN]\"}}}\n[2024-01-01 10:00:01] easywechat-http.INFO: HTTP Response {\"method\":\"GET\",\"url\":\"https://api.weixin.qq.com/cgi-bin/token\",\"status_code\":200,\"duration\":\"156.75ms\"}\n```\n\n## 注意事项\n\n1. **敏感信息处理**：在记录日志时，请务必过滤掉敏感信息，如 `secret`、`access_token`、`password` 等。\n\n2. **性能影响**：启用详细的 HTTP 日志记录可能会对性能产生影响，特别是在高并发场景下。建议在生产环境中适当调整日志级别。\n\n3. **日志轮转**：确保配置适当的日志轮转策略，避免日志文件过大。\n\n4. **自动设置**：当你在应用实例上设置日志记录器后，如果 HTTP 客户端实现了 `LoggerAwareInterface`，框架会自动将日志记录器设置到 HTTP 客户端上。\n"
  },
  {
    "path": "docs/src/6.x/mini-app/config.md",
    "content": "---\ntitle: 配置 | 微信小程序开发\naside: false\n---\n\n# 配置\n\n下面是一个完整的配置样例：\n\n> 不建议你在配置的时候弄这么多，用到啥就配置啥才是最好的，因为大部分用默认值即可。\n\n```php\n[\n    /**\n     * 账号基本信息，请从微信公众平台/开放平台获取\n     */\n    'app_id'  => 'your-app-id',         // AppID\n    'secret'  => 'your-app-secret',     // AppSecret\n    'token'   => 'your-token',          // Token\n    'aes_key' => '',                    // EncodingAESKey，兼容与安全模式下请一定要填写！！！\n\n    /**\n     * 是否使用 Stable Access Token\n     * 默认 false\n     * https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html\n     * true 使用 false 不使用\n     */\n    'use_stable_access_token' => false,\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n     */\n    'http' => [\n        'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n        'retry' => true, // 使用默认重试配置\n        //  'retry' => [\n        //      // 仅以下状态码重试\n        //      'status_codes' => [429, 500]\n        //       // 最大重试次数\n        //      'max_retries' => 3,\n        //      // 请求间隔 (毫秒)\n        //      'delay' => 1000,\n        //      // 如果设置，每次重试的等待时间都会增加这个系数\n        //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n        //      'multiplier' => 3\n        //  ],\n    ],\n]\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n"
  },
  {
    "path": "docs/src/6.x/mini-app/examples.md",
    "content": "---\naside: false\ntitle: 微信小程序使用代码示例\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n<details open>\n    <summary>生成小程序码（wxacode.getUnlimited）</summary>\n\n[官方文档：wxacode.getUnlimited](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html)\n\n```php\ntry {\n    $response = $app->getClient()->postJson('/wxa/getwxacodeunlimit', [\n        'scene' => '123',\n        'page' => 'pages/index/index',\n        'width' => 430,\n        'check_path' => false,\n    ]);\n    \n    $path = $response->saveAs('/tmp/wxacode-123.png');\n} catch (\\Throwable $e) {\n    // 失败\n    echo $e->getMessage();\n}\n```\n</details>\n\n<details>\n    <summary>获取手机号（phonenumber.getPhoneNumber）</summary>\n\n[官方文档：phonenumber.getPhoneNumber](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html)\n\n```php\n// routes/api.php\nuse EasyWeChat\\MiniApp\\Application;\nRoute::post('getPhoneNumber', function () {\n    // $app 实例化步骤这里省略 \n    $data = [\n      'code' => (string) request()->get('code'),\n    ];\n\n    return $app->getClient()->postJson('wxa/business/getuserphonenumber', $data);\n  }\n}\n```\n</details>\n\n<!--\n<details>\n    <summary>标题</summary>\n内容\n</details>\n-->\n"
  },
  {
    "path": "docs/src/6.x/mini-app/index.md",
    "content": "# 小程序\n\n> 🚨 使用前建议熟读 [微信官方文档: 小程序](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)\n\n常用的配置参数会比较少，因为除非你有特别的定制，否则基本上默认值就可以了：\n\n```php\nuse EasyWeChat\\MiniApp\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n    'token' => 'easywechat',\n    'aes_key' => '......',\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n     */\n    'http' => [\n        'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n        'retry' => true, // 使用默认重试配置\n        //  'retry' => [\n        //      // 仅以下状态码重试\n        //      'status_codes' => [429, 500]\n        //       // 最大重试次数\n        //      'max_retries' => 3,\n        //      // 请求间隔 (毫秒)\n        //      'delay' => 1000,\n        //      // 如果设置，每次重试的等待时间都会增加这个系数\n        //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n        //      'multiplier' => 3\n        //  ],\n    ],\n];\n\n$app = new Application($config);\n```\n\n:book: 更多配置项请参考：[配置](config.md)\n\n## API\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### 服务端\n\n服务端模块封装了服务端相关的便捷操作，隐藏了部分复杂的细节，基于中间件模式可以更方便的处理消息推送和服务端验证。\n\n```php\n$app->getServer();\n```\n\n:book: 更多说明请参阅：[服务端使用文档](server.md)\n\n### API Client\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用小程序任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 配置\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->get($key, $default)` 读取配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### AccessToken\n\naccess_token 是小程序 API 调用的必备条件，如果你想获取它的值，你可以通过以下方式拿到当前的 access_token：\n\n```php\n$accessToken = $app->getAccessToken();\n$accessToken->getToken(); // string\n```\n\n当然你也可以使用自己的 AccessToken 类：\n\n```php\n$accessToken = new MyCustomAccessToken();\n$app->setAccessToken($accessToken)\n```\n\n### 小程序账户\n\n小程序账号类，提供一系列 API 获取小程序的基本信息：\n\n```php\n$account = $app->getAccount();\n\n$account->getAppId();\n$account->getSecret();\n$account->getToken();\n$account->getAesKey();\n```\n\n### 工具类\n\n对于一些高频接口，我们提供了一个简单的工具类来简化大家的开发工作，可以通过下面的方法获取：\n\n```php\n$utils = $app->getUtils();\n```\n\n:book: 更多说明请参阅：[工具类](utils.md)\n"
  },
  {
    "path": "docs/src/6.x/mini-app/server.md",
    "content": "---\ntitle: 服务端 | 微信小程序开发\naside: false\n---\n\n# 服务端\n\n小程序的服务端推送和公众号一样，请参考：[公众号：服务端](../official-account/server.md)"
  },
  {
    "path": "docs/src/6.x/mini-app/utils.md",
    "content": "# 工具类 <version-tag>6.3.0+</version-tag>\n\n为了简化大家对于一些高频基础的操作，我们提供了一些简单的工具类。你可以通过下面的方法获取工具类：\n\n```php\n$utils = $app->getUtils();\n```\n\n## 工具方法\n\n### code2session\n\n```php\n$response = $utils->codeToSession($code);\n\n// {\n//     \"openid\": \"o6_bmjrPTlm6_2sgVt7hMZOPxxxx\",\n//     \"session_key\": \"tiihtNczf5v6AKRyjwExxxx=\",\n//     \"unionid\": \"o6_bmasdasdsad6_2sgVt7hMZOxxxx\",\n//     \"errcode\": 0,\n//     \"errmsg\": \"ok\"\n//}\n```\n\n### 解密会话信息\n\n```php\n$session = $utils->decryptSession($sessionKey, $iv, $encryptedData);\n\n//{\n//    \"openId\": \"oGZUI0egBJY1zhBYw2KhdUfwVJJE\",\n//    \"nickName\": \"Band\",\n//    \"gender\": 1,\n//    \"language\": \"zh_CN\",\n//    \"city\": \"Guangzhou\",\n//    \"province\": \"Guangdong\",\n//    \"country\": \"CN\",\n//    \"avatarUrl\": \"http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0\",\n//    \"unionId\": \"ocMvos6NjeKLIBqg5Mr9QjxrP1FA\",\n//    \"watermark\": {\n//        \"timestamp\": 1477314187,\n//        \"appid\": \"wx4f4bc4dec97d474b\"\n//    }\n//}\n```\n\n### 获取手机号\n\n获取用户绑定的手机号，需先调用 `wx.login` 接口，然后通过 `getPhoneNumber` 方法获取用户手机号。\n\n参考文档：https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html\n\n```php\n$response = $utils->getPhoneNumber($code);\n\n//{\n//    \"errcode\": 0,\n//    \"errmsg\": \"ok\",\n//    \"phone_info\": {\n//        \"phoneNumber\": \"13800138000\",\n//        \"purePhoneNumber\": \"13800138000\",\n//        \"countryCode\": \"86\",\n//        \"watermark\": {\n//            \"timestamp\": 1637744274,\n//            \"appid\": \"xxxx\"\n//        }\n//    }\n//}\n```\n"
  },
  {
    "path": "docs/src/6.x/oauth.md",
    "content": "# 网页授权\n\n## 关于 OAuth2.0\n\nOAuth 是一个关于授权（authorization）的开放网络标准，在全世界得到广泛应用，目前的版本是 2.0 版。\n\n<img src=\"https://user-images.githubusercontent.com/1472352/29310178-5a7a91cc-81df-11e7-9468-b66e150bfba1.png\" alt=\"\" style=\"max-width: 500px\">\n\n> 摘自：[RFC 6749](https://datatracker.ietf.org/doc/rfc6749/?include_text=1)\n\n步骤解释：\n\n    （A）用户打开客户端以后，客户端要求用户给予授权。\n    （B）用户同意给予客户端授权。\n    （C）客户端使用上一步获得的授权，向认证服务器申请令牌。\n    （D）认证服务器对客户端进行认证以后，确认无误，同意发放令牌。\n    （E）客户端使用令牌，向资源服务器申请获取资源。\n    （F）资源服务器确认令牌无误，同意向客户端开放资源。\n\n关于 OAuth 协议我们就简单了解到这里，如果还有不熟悉的同学，请 [Google 相关资料](https://www.google.com.hk/?gws_rd=ssl#safe=strict&q=OAuth2)\n\n## 微信 OAuth\n\n在微信里的 OAuth 其实有两种：[公众平台网页授权获取用户信息](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html)、[开放平台网页登录](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN)。\n\n它们的区别有两处，授权地址不同，`scope` 不同。\n\n> - **公众平台网页授权获取用户信息**\n\n**授权 URL**: `https://open.weixin.qq.com/connect/oauth2/authorize`\n**Scopes**: `snsapi_base` 与 `snsapi_userinfo`\n\n> - **开放平台网页登录**\n\n**授权 URL**: `https://open.weixin.qq.com/connect/qrconnect`\n**Scopes**: `snsapi_login`\n\n他们的逻辑都一样：\n\n1. 用户尝试访问一个我们的业务页面，例如: `/user/profile`\n2. 如果用户已经登录，则正常显示该页面\n3. 系统检查当前访问的用户并未登录（从 session 或者其它方式检查），则跳转到**跳转到微信授权服务器**（上面的两种中一种**授权 URL** ），并告知微信授权服务器我的**回调 URL（redirect_uri=callback.php)**，此时用户看到蓝色的授权确认页面（`scope` 为 `snsapi_base` 时不显示）\n4. 用户点击确定完成授权，浏览器跳转到**回调 URL**: `callback.php` 并带上 `code`： `?code=CODE&state=STATE`。\n5. 在 `callback.php` 中得到 `code` 后，通过 `code` 再次向微信服务器请求得到 **网页授权 access_token** 与 `openid`\n6. 你可以选择拿 `openid` 去请求 API 得到用户信息（可选）\n7. 将用户信息写入 SESSION。\n8. 跳转到第 3 步写入的 `intend_url` 页面（`/user/profile`）。\n\n> 看懵了？没事，使用 SDK，你不用管这么多。:smile:\n>\n> 注意，上面的第 3 步：redirect_uri=callback.php 实际上我们会在 `callback.php` 后面还会带上授权目标页面 `user/profile`，所以完整的 `redirect_uri` 应该是下面的这样的 PHP 去拼出来：`'redirect_uri='.urlencode('callback.php?target=user/profile')`\n> 结果：redirect_uri=callback.php%3Ftarget%3Duser%2Fprofile\n\n## 逻辑组成\n\n从上面我们所描述的授权流程来看，我们至少有 3 个页面：\n\n1. **业务页面**，也就是需要授权才能访问的页面。\n2. **发起授权页**，此页面其实可以省略，可以做成一个中间件，全局检查未登录就发起授权。\n3. **授权回调页**，接收用户授权后的状态，并获取用户信息，写入用户会话状态（SESSION）。\n\n## 开始之前\n\n在开始之前请一定要记住，先登录公众号后台，找到**边栏 “开发”** 模块下的 **“接口权限”**，点击 **“网页授权获取用户基本信息”** 后面的修改，添加你的网页授权域名。\n\n> 如果你的授权地址为：`http://www.abc.com/xxxxx`，那么请填写 `www.abc.com`，也就是说请填写与网址匹配的域名，前者如果填写 `abc.com` 是通过不了的。\n\n### 发起授权\n\n```php\n$redirectUrl = $oauth->scopes(['snsapi_userinfo'])->redirect();\n\n// 指定回调 URL，比如设置回调 URL 为当前页面\n$redirectUrl = $oauth->scopes(['snsapi_userinfo'])->redirect($request->fullUrl());\n```\n\n返回值 `$redirectUrl` 是一个字符串 URL，请自行使用框架的跳转方法实现跳转，PHP 原生写法：\n\n```php\nheader(\"Location: {$redirectUrl}\");\n```\n\n在 [Laravel](http://laravel.com) 框架中控制器方法是要求返回响应值:\n\n```php\nreturn \\redirect($redirectUrl);\n```\n\n### 处理授权回调\n\n```php\n$code = \"微信回调URL携带的 code\"; // $_GET['code']\n$user = $oauth->userFromCode($code);\n```\n\n返回的 `$user` 是 [Overtrue\\Socialite\\User](https://github.com/overtrue/socialite/blob/master/src/User.php) 对象，你可以从该对象拿到[更多的信息](https://github.com/overtrue/socialite#user-interface)。\n\n**$user 可以用的方法**\n\n- `$user->getId(); ` 对应微信的 `openid`\n- `$user->getNickname(); ` 对应微信的 `nickname`\n- `$user->getName(); ` 对应微信的 `nickname`\n- `$user->getAvatar(); ` 头像地址\n- `$user->getRaw(); ` 原始 API 返回的结果\n- `$user->getAccessToken(); ` `access_token`\n- `$user->getRefreshToken(); ` `refresh_token`\n- `$user->getExpiresIn(); ` `expires_in`，Access Token 过期时间\n- `$user->getTokenResponse(); ` 返回 `access_token` 时的响应值\n\n> `$user` 里没有`openid`， `$user->id` 便是 `openid`.\n> 如果你想拿微信返回给你的原样的全部信息，请使用：$user->getRaw();\n\n当 `scope` 为 `snsapi_base` 时 `$oauth->userFromCode($code);` 对象里只有 `id`，没有其它信息。\n\n## 网页授权实例\n\n我们这里来用公众号原生 PHP 写法举个例子，`oauth_callback` 是我们的授权回调 URL (未 urlencode 编码的 URL), `user/profile` 是我们需要授权才能访问的页面，它的 PHP 代码如下：\n\n```php\n// http://easywechat.com/user/profile\n<?php\n\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n    'token' => 'easywechat',\n    'aes_key' => '......'\n  //...\n];\n\n\n$app = new Application($config);\n\n$oauth = $app->getOauth();\n\n// 未登录\n//callback_url 是授权回调的URL\nif (empty($_SESSION['wechat_user'])) {\n\n  $_SESSION['intend_url'] = 'user/profile';\n  //生成完整的授权URL\n  $redirectUrl = $oauth->redirect('callback_url');\n\n  header(\"Location: {$redirectUrl}\");\n  exit;\n} else {\n  // 已经登录过，则从 session 中取授权者信息\n  $user = $_SESSION['wechat_user'];\n\n  // ...\n}\n```\n\n**授权回调页**\n\n用户授权完成后浏览器调回的 URL 逻辑：\n\n```php\n// http://easywechat.com/oauth_callback\n<?php\n\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n    'token' => 'easywechat',\n    'aes_key' => '......'\n  //...\n];\n\n\n$app = new Application($config);\n\n$oauth = $app->getOauth();\n\n// 获取 OAuth 授权用户信息\n$user = $oauth->userFromCode($_GET['code']);\n\n$_SESSION['wechat_user'] = $user->toArray();\n\n$targetUrl = empty($_SESSION['intend_url']) ? '/' : $_SESSION['intend_url'];\n\nheader('Location:'. $targetUrl); // 跳转回授权前的目标页面：user/profile\n```\n\n上面的例子呢都是基于 `$_SESSION` 来保持会话的，在微信客户端中，你也可以结合 Cookies 来存储，但是有效期平台不一样时间也不一样，好像 Android 的失效会快一些，不过基本也够用了。\n\n## 参考阅读\n\n- 本模块基于 [overtrue/socialite](https://github.com/overtrue/socialite/) 实现，更多的使用请阅读该扩展包文档。\n- state 参数的使用: [overtrue/socialite/#state](https://github.com/overtrue/socialite/#state)\n"
  },
  {
    "path": "docs/src/6.x/official-account/config.md",
    "content": "---\ntitle: 配置 | 微信公众号开发\naside: false\n---\n\n# 配置\n\n下面是一个完整的配置样例：\n\n> 不建议你在配置的时候弄这么多，用到啥就配置啥才是最好的，因为大部分用默认值即可。\n\n```php\n[\n    /**\n     * 账号基本信息，请从微信公众平台/开放平台获取\n     */\n    'app_id'  => 'your-app-id',         // AppID\n    'secret'  => 'your-app-secret',     // AppSecret\n    'token'   => 'your-token',          // Token\n    'aes_key' => '',                    // EncodingAESKey，兼容与安全模式下请一定要填写！！！\n\n    /**\n     * 是否使用 Stable Access Token\n     * 默认 false\n     * https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/getStableAccessToken.html\n     * true 使用 false 不使用\n     */\n    'use_stable_access_token' => false,\n\n    /**\n     * OAuth 配置\n     *\n     * scopes：公众平台（snsapi_userinfo / snsapi_base），开放平台：snsapi_login\n     * redirect_url：OAuth授权完成后的回调页地址\n     */\n    'oauth' => [\n        'scopes'   => ['snsapi_userinfo'],\n        'redirect_url' => '/examples/oauth_callback.php',\n    ],\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n     */\n    'http' => [\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n        'retry' => true, // 使用默认重试配置\n        //  'retry' => [\n        //      // 仅以下状态码重试\n        //      'status_codes' => [429, 500]\n        //       // 最大重试次数\n        //      'max_retries' => 3,\n        //      // 请求间隔 (毫秒)\n        //      'delay' => 1000,\n        //      // 如果设置，每次重试的等待时间都会增加这个系数\n        //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n        //      'multiplier' => 3\n        //  ],\n    ],\n]\n```\n\n> :heart: 安全模式下请一定要填写 `aes_key`\n"
  },
  {
    "path": "docs/src/6.x/official-account/examples.md",
    "content": "---\naside: false\ntitle: 微信公众号使用代码示例\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n<details open>\n    <summary>webman 服务端验证消息</summary>\n\n```php\n<?php\n\nnamespace app\\controller;\n\nuse EasyWeChat\\OfficialAccount\\Application;\nuse support\\Request;\nuse support\\Redis;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\Cache\\Adapter\\RedisAdapter;\nuse Symfony\\Component\\HttpFoundation\\HeaderBag;\nuse Symfony\\Component\\HttpFoundation\\Request as SymfonyRequest;\n\n// 授权事件回调地址：http://easywechat.com/OfficialAccount/server\n\nclass OfficialAccount\n{\n    public function server(Request $request)\n    {\n        $config = config('wechatv6.official_account');\n        $app = new Application($config);\n        $symfony_request = new SymfonyRequest($request->get(), $request->post(), [], $request->cookie(), [], [], $request->rawBody());\n        $symfony_request->headers = new HeaderBag($request->header());\n        $app->setRequestFromSymfonyRequest($symfony_request);//必须替换服务端请求\n        //$app->setCache(new Psr16Cache(new RedisAdapter(Redis::connection()->client())));//根据需要替换缓存，access_token公众号的全局唯一接口调用凭据会使用该缓存存储\n        $server = $app->getServer();\n        $response = $server->serve();\n\n        return response($response->getBody()->getContents(), $response->getStatusCode(), $response->getHeaders());\n    }\n}\n```\n\n</details>\n\n\n<details open>\n    <summary>Hyperf 服务端验证消息</summary>\n  \n  ##### 方法一：\n  * 安装包: `composer require limingxinleo/easywechat-classmap`\n  * 在授权回调地址中使用：\n\n```php\n<?php\n\nnamespace App\\Controller;\n\nuse EasyWeChat\\OfficialAccount\\Application;\nuse Hyperf\\HttpServer\\Contract\\RequestInterface;\nuse Hyperf\\HttpServer\\Contract\\ResponseInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Hyperf\\Context\\ApplicationContext;\n\n// 授权事件回调地址：http://easywechat.com/OfficialAccount/server\n\nclass OfficialAccountController\n{\n    public function server(RequestInterface $request, ResponseInterface $response)\n    {\n        $app = new Application(config('wechat.defaults'));\n        \n        if (method_exists($app, 'setRequest')) {\n            $app->setRequest($request);  //必须替换服务端请求\n        }\n\n        if (method_exists($app, 'setCache')) {\n            $app->setCache(ApplicationContext::getContainer()->get(CacheInterface::class));  //可选，根据实际需求替换缓存\n        }\n\n        $server = $app->getServer();\n        \n        $server->with(function ($message, \\Closure $next) {\n            return '谢谢关注！';\n            \n            // 你的自定义逻辑\n            // return $next($message);\n        });\n        \n        return $server->serve();\n    }\n}\n  ```\n\n##### 方法二：\n* 安装包: `composer require pengxuxu/hyperf-easywechat6`\n  > 包里已替换了服务端请求和缓存，并封装了公众号、微信支付、小程序等外观。\n* 参照文档在授权回调地址和其他场景中直接使用。\n</details>\n\n"
  },
  {
    "path": "docs/src/6.x/official-account/index.md",
    "content": "# 公众号\n\n> 🚨 使用前建议熟读 [微信官方文档: 公众号](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)\n\n常用的配置参数会比较少，因为除非你有特别的定制，否则基本上默认值就可以了：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [\n    'app_id' => 'wx3cf0f39249eb0exx',\n    'secret' => 'f1c242f4f28f735d4687abb469072axx',\n    'token' => 'easywechat',\n    'aes_key' => '', // 明文模式请勿填写 EncodingAESKey\n\n    /**\n     * OAuth 配置\n     *\n     * scopes：公众平台（snsapi_userinfo / snsapi_base），开放平台：snsapi_login\n     * redirect_url：OAuth授权完成后的回调页地址\n     */\n    'oauth' => [\n        'scopes'   => ['snsapi_userinfo'],\n        'redirect_url' => '/examples/oauth_callback.php',\n    ],\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n     */\n    'http' => [\n        'timeout' => 5.0,\n        // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n        'retry' => true, // 使用默认重试配置\n        //  'retry' => [\n        //      // 仅以下状态码重试\n        //      'status_codes' => [429, 500]\n        //       // 最大重试次数\n        //      'max_retries' => 3,\n        //      // 请求间隔 (毫秒)\n        //      'delay' => 1000,\n        //      // 如果设置，每次重试的等待时间都会增加这个系数\n        //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n        //      'multiplier' => 3\n        //  ],\n    ],\n];\n\n$app = new Application($config);\n```\n\n:book: 更多配置项请参考：[配置](config.md)\n\n## API\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### 服务端\n\n服务端模块封装了服务端相关的便捷操作，隐藏了部分复杂的细节，基于中间件模式可以更方便的处理消息推送和服务端验证。\n\n```php\n$app->getServer();\n```\n\n:book: 更多说明请参阅：[服务端使用文档](server.md)\n\n### API Client\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用公众号任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 配置\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->get($key, $default)` 读取配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### AccessToken\n\naccess_token 是公众号 API 调用的必备条件，如果你想获取它的值，你可以通过以下方式拿到当前的 access_token：\n\n```php\n$accessToken = $app->getAccessToken();\n$accessToken->getToken(); // string\n```\n\n当然你也可以使用自己的 AccessToken 类：\n\n```php\n$accessToken = new MyCustomAccessToken();\n$app->setAccessToken($accessToken)\n```\n\n### 网页授权\n\n```php\n$oauth = $app->getOAuth();\n```\n\n:book: 详情请参考：[网页授权](../oauth.md)\n\n### 公众号账户\n\n公众号账号类，提供一系列 API 获取公众号的基本信息：\n\n```php\n$account = $app->getAccount();\n\n$account->getAppId();\n$account->getSecret();\n$account->getToken();\n$account->getAesKey();\n```\n"
  },
  {
    "path": "docs/src/6.x/official-account/message.md",
    "content": "# 消息\n\n公众号消息分为 [**服务端被动回复消息**](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html) 和 [**客服消息**](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html) 两个场景。\n\n需要注意的是两个场景的消息虽然类似，但是结构却有些差异，比如服务端使用 XML 结构，而客服消息使用 JSON 结构，且同样类似的消息类型，结构和名称都有些许差异，在使用时请勿混淆。\n\n## 服务端消息结构\n\n当你接收到用户发来的消息时，可能会提取消息中的相关属性，参考：\n\n请求消息基本属性(以下所有消息都有的基本属性)：\n\n```\n  - `ToUserName`    接收方帐号（该公众号 ID）\n  - `FromUserName`  发送方帐号（OpenID, 代表用户的唯一标识）\n  - `CreateTime`    消息创建时间（时间戳）\n  - `MsgId`        消息 ID（64位整型）\n```\n\n### 文本\n\n```\n  - `MsgType`  text\n  - `Content`  文本消息内容\n```\n\n### 图片\n\n```\n  - `MsgType`  image\n  - `MediaId`  图片消息媒体id，可以调用多媒体文件下载接口拉取数据。\n  - `PicUrl`   图片链接\n```\n\n### 语音\n\n```\n  - `MsgType`        voice\n  - `MediaId`        语音消息媒体id，可以调用多媒体文件下载接口拉取数据。\n  - `Format`         语音格式，如 amr，speex 等\n  - `Recognition`  * 开通语音识别后才有\n```\n\n> 请注意，开通语音识别后，用户每次发送语音给公众号时，微信会在推送的语音消息 XML 数据包中，增加一个 `Recongnition` 字段\n\n### 视频\n\n```\n  - `MsgType`       video\n  - `MediaId`       视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n  - `ThumbMediaId`  视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n```\n\n### 小视频\n\n```\n  - `MsgType`     shortvideo\n  - `MediaId`     视频消息媒体id，可以调用多媒体文件下载接口拉取数据。\n  - `ThumbMediaId`    视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。\n```\n\n### 事件消息\n\n```\n  - `MsgType`     event\n  - `Event`       事件类型 （如：subscribe(订阅)、unsubscribe(取消订阅) ...， CLICK 等）\n```\n\n#### 扫描带参数二维码事件\n\n```\n  - `EventKey`    事件KEY值，比如：qrscene_123123，qrscene_为前缀，后面为二维码的参数值\n  - `Ticket`      二维码的 ticket，可用来换取二维码图片\n```\n\n#### 上报地理位置事件\n\n```\n  - `Latitude`    23.137466   地理位置纬度\n  - `Longitude`   113.352425  地理位置经度\n  - `Precision`   119.385040  地理位置精度\n```\n\n#### 自定义菜单事件\n\n```\n  - `EventKey`    事件KEY值，与自定义菜单接口中KEY值对应，如：CUSTOM_KEY_001, www.qq.com\n```\n\n### 地理位置\n\n```\n  - `MsgType`     location\n  - `Location_X`  地理位置纬度\n  - `Location_Y`  地理位置经度\n  - `Scale`       地图缩放大小\n  - `Label`       地理位置信息\n```\n\n### 链接\n\n```\n  - `MsgType`      link\n  - `Title`        消息标题\n  - `Description`  消息描述\n  - `Url`          消息链接\n```\n\n### 文件\n\n```\n  - `MsgType`      file\n  - `Title`        文件名\n  - `Description`  文件描述，可能为null\n  - `FileKey`      文件KEY\n  - `FileMd5`      文件MD5值\n  - `FileTotalLen` 文件大小，单位字节\n```\n\n## 客服消息结构\n\n### 发送文本消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"text\",\n  \"text\": {\n    \"content\": \"Hello World\"\n  }\n}\n```\n\n### 图片消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"image\",\n  \"image\": {\n    \"media_id\": \"MEDIA_ID\"\n  }\n}\n```\n\n### 语音消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"voice\",\n  \"voice\": {\n    \"media_id\": \"MEDIA_ID\"\n  }\n}\n```\n\n### 视频消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"video\",\n  \"video\": {\n    \"media_id\": \"MEDIA_ID\",\n    \"thumb_media_id\": \"MEDIA_ID\",\n    \"title\": \"TITLE\",\n    \"description\": \"DESCRIPTION\"\n  }\n}\n```\n\n### 音乐消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"music\",\n  \"music\": {\n    \"title\": \"MUSIC_TITLE\",\n    \"description\": \"MUSIC_DESCRIPTION\",\n    \"musicurl\": \"MUSIC_URL\",\n    \"hqmusicurl\": \"HQ_MUSIC_URL\",\n    \"thumb_media_id\": \"THUMB_MEDIA_ID\"\n  }\n}\n```\n\n### 图文消息（点击跳转到外链）\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"news\",\n  \"news\": {\n    \"articles\": [\n      {\n        \"title\": \"Happy Day\",\n        \"description\": \"Is Really A Happy Day\",\n        \"url\": \"URL\",\n        \"picurl\": \"PIC_URL\"\n      }\n    ]\n  }\n}\n```\n\n### 图文消息（点击跳转到图文消息页面）\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"mpnews\",\n  \"mpnews\": {\n    \"media_id\": \"MEDIA_ID\"\n  }\n}\n```\n\n### 菜单消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"msgmenu\",\n  \"msgmenu\": {\n    \"head_content\": \"您对本次服务是否满意呢? \"\n    \"list\": [\n      {\n        \"id\": \"101\",\n        \"content\": \"满意\"\n      },\n      {\n        \"id\": \"102\",\n        \"content\": \"不满意\"\n      }\n    ],\n    \"tail_content\": \"欢迎再次光临\"\n  }\n}\n```\n\n### 卡券消息\n\n```json\n{\n  \"touser\": \"OPENID\",\n  \"msgtype\": \"wxcard\",\n  \"wxcard\": {\n    \"card_id\": \"123dsdajkasd231jhksad\"\n  }\n}\n```\n\n> 请以官方文档为准。\n"
  },
  {
    "path": "docs/src/6.x/official-account/server.md",
    "content": "# 服务端\n\n你可以通过 `$app->getServer()` 获取服务端模块，**服务端模块默认处理了服务端验证的逻辑**：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [...];\n$app = new Application($config);\n\n$server = $app->getServer();\n```\n\n## 服务端验证\n\nSDK 已经内置了服务端验证的实现，你不需要自己再去关心 `echostr` 怎么返回，直接像下面这样就可以完成服务端验证：\n\n```php\nreturn $server->serve();\n```\n\n> 🚨 注意\n> `$response` 是一个 [Psr\\Http\\Message\\ResponseInterface](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php) 实现，所以请自己决定如何适配您的框架。\n>  如果使用了 `thinkphp`、`workman` 等框架，需要先把框架请求转换成 Symfony 请求，再通过 `$app->setRequestFromSymfonyRequest($symfonyRequest)` 进行 request 对象替换，然后再调用 `getServer()`；\n\n\n## 自助处理推送消息\n\n> 🚨 注意：不要在返回 `$server->serve()` 前输出任何内容。\n\n你可以通过下面的方式获取来自微信服务器的推送消息：\n\n```php\n$message = $server->getRequestMessage(); // 原始消息\n```\n\n你也可以获取解密后的消息 <version-tag>6.5.0+</version-tag>\n\n```php\n$message = $server->getDecryptedMessage();\n```\n\n`$message` 为一个 `EasyWeChat\\OfficialAccount\\Message` 实例。\n\n你可以在处理完逻辑后自行创建一个响应，当然，在不同的框架里，响应写法也不一样，请自行实现，我建议使用下面的中间件模式来完成会更简单方便。\n\n## 中间件模式\n\n与 5.x 的设计类似，服务端使用中间件模式来依次调用开发者注册的中间件：\n\n```php\n$server->with(function($message, \\Closure $next) {\n    // 你的自定义逻辑\n    return $next($message);\n});\n\n$response = $server->serve();\n```\n\n你可以注册多个中间件来处理不同的情况：\n\n```php\n$server\n    ->with(function($message, \\Closure $next) {\n        // 你的自定义逻辑1\n        return $next($message);\n    })\n    ->with(function($message, \\Closure $next) {\n        // 你的自定义逻辑2\n        return $next($message);\n    })\n    ->with(function($message, \\Closure $next) {\n        // 你的自定义逻辑3\n        return $next($message);\n    });\n\n$response = $server->serve();\n```\n\n### 回复消息\n\n当你在中间件里不回复消息时，你将要传递消息给下一个中间件：\n\n```php\nfunction($message, \\Closure $next) {\n    // 你的自定义逻辑3\n    return $next($message);\n}\n```\n\n如果此时你需要返回消息给用户，你可以直接像下面这样回复消息内容：\n\n```php\nfunction($message, \\Closure $next) {\n    return '感谢你使用 EasyWeChat';\n}\n```\n\n> 注意：回复消息后其他没运行的中间件将不再执行，所以请你将全局都需要执行的中间件优先提前注册。\n\n其他类型的消息时，请直接参考 **[官方文档消息的 XML 结构](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html)** 以数组形式返回即可。\n\n需要省略 `ToUserName`、`FromUserName` 和 `CreateTime`，以回复图片消息为例:\n\n```php\nfunction($message, \\Closure $next) {\n    return [\n        'MsgType' => 'image',\n        'Image' => [\n            'MediaId' => 'media_id',\n        ],\n    ];\n}\n```\n\n### 怎么发送多条消息？\n\n服务端只能回复一条消息，如果你想在接收到消息时向用户发送多条消息，你可以调用 **[客服消息](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html)** 接口来发送多条。\n\n### 使用独立的中间件类\n\n当然，中间件也支持多种类型，比如你可以使用一个独立的类作为中间件：\n\n```php\nclass MyCustomHandler\n{\n    public function __invoke($message, \\Closure $next)\n    {\n        if ($message->MsgType === 'text') {\n            //...\n        }\n\n        return $next($message);\n    }\n}\n```\n\n注册中间件：\n\n```php\n$server->with(MyCustomHandler::class);\n\n// 或者\n\n$server->with(new MyCustomHandler());\n```\n\n### 使用 callable 类型中间件\n\n中间件支持 **[`callable`](http://php.net/manual/zh/language.types.callable.php)** 类型的参数，所以你不一定要传入一个闭包（Closure），你可以选择传入一个函数名，一个 `[$class, $method]` 或者 `Foo::bar` 这样的类型。\n\n```php\n$server->with([$object, 'method']);\n$server->with('ClassName::method');\n```\n\n## 注册指定消息类型的消息处理器\n\n为了方便开发者处理消息推送，server 类内置了两个便捷方法：\n\n### 处理普通消息\n\n当普通微信用户向公众账号发消息时被调用，且匹配对应的事件类型：\n\n```php\n$server->addMessageListener('text', function() { ... });\n```\n\n**参数**\n\n- 参数 1 为消息类型，也就是 message 中的 `MsgType` 字段，例如：`image`;\n- 参数 2 是中间件，也就是上面讲到的多种类型的中间件。\n\n### 处理事件消息\n\n事件消息中间件仅在推送事件消息时被调用，且匹配对应的事件类型：\n\n```php\n$server->addEventListener('subscribe', function() { ... });\n```\n\n**参数**\n\n- 参数 1 为事件类型，也就是 message 中的 `Event` 字段，例如：`subscribe`;\n- 参数 2 是中间件，也就是上面讲到的多种类型的中间件。\n\n关于回复消息的结构，可以查阅 **[消息](message.md)** 章节了解更多。\n\n## 完整示例\n\n以下示例完成了服务端验证，自定义中间件回复等逻辑：\n\n```php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [...];\n$app = new Application($config);\n\n$server = $app->getServer();\n\n$server->addEventListener('subscribe', function($message, \\Closure $next) {\n    return '感谢您关注 EasyWeChat!';\n});\n\n$response = $server->serve();\n\nreturn $response;\n```\n\n> `$response` 是一个 [Psr\\Http\\Message\\ResponseInterface](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php) 实现，所以请自己决定如何适配您的框架。\n>  如果使用了 `thinkphp`、`workman` 等框架，需要先把框架请求转换成 Symfony 请求，再通过 `$app->setRequestFromSymfonyRequest($symfonyRequest)` 进行 request 对象替换，然后再调用 `getServer()`；\n"
  },
  {
    "path": "docs/src/6.x/official-account/utils.md",
    "content": "# 工具\n\n提供微信网页开发 JS-SDK 相关方法\n\n## 配置\n\n```php\n<?php\nuse EasyWeChat\\OfficialAccount\\Application;\n\n$config = [...];\n\n$app = new Application($config);\n\n$utils = $app->getUtils();\n```\n\n## 生成 JS-SDK 签名\n\n:book: [官方文档 - JS-SDK说明文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html)\n\n```php\n$config = $utils->buildJsSdkConfig(\n    url: $url, \n    jsApiList: [],\n    openTagList: [], \n    debug: false, \n);\n\n// print\n[\n    \"appId\" => \"wx...\",\n    \"jsApiList\" => [],\n    \"nonceStr\" => \"string\",\n    \"openTagList\" => [],\n    \"signature\" =>  \"sign\",\n    \"timestamp\" =>  \"timestamp\"\n];\n\n```\n\n## 示例\n页面生成js配置（1.4和1.6版本任选一即可）：\n```html\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<!-- <script src=\"https://res.wx.qq.com/open/js/jweixin-1.6.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script> -->\n<script type=\"text/javascript\" charset=\"utf-8\">\n  wx.config(<?php echo json_encode($app->getUtils()->buildJsSdkConfig('当前页面url', ['updateAppMessageShareData', 'updateTimelineShareData'], [], false)); ?>);\n</script>\n```\n结果如下：\n```html\n<script src=\"https://res.wx.qq.com/open/js/jweixin-1.4.0.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n<script type=\"text/javascript\" charset=\"utf-8\">\n  wx.config({\n    \"jsApiList\":[\"updateAppMessageShareData\",\"updateTimelineShareData\"],\n    \"openTagList\":[],\n    \"debug\":false,\n    \"url\":\"当前页面url\",\n    \"nonceStr\":\"mYEeh068LPuWp06u\",\n    \"timestamp\":1710381708,\n    \"appId\":\"wxcb0f*****f5f6c2\",\n    \"signature\":\"9147682d4f77f7f03162915446f90288cafbda93\"\n  });\n</script>\n```\n"
  },
  {
    "path": "docs/src/6.x/open-platform/examples.md",
    "content": "---\naside: false\ntitle: 微信开放平台代码使用示例\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n<details open>\n  <summary>Laravel 开放平台处理推送消息</summary>\n\n> 注意：对应路由需要[排除 csrf 验证](https://laravel.com/docs/12.x/csrf#csrf-excluding-uris)。\n\n假设你的开放平台第三方平台设置的授权事件接收 URL 为: `https://easywechat.com/open-platform` （其他事件推送同样会推送到这个 URL）\n\n```php\n// routes/web.php\nRoute::post('open-platform', function () {\n    // $app 为你实例化的开放平台对象，此处省略实例化步骤\n    return $app->server->serve(); // Done!\n});\n\n// 处理授权事件\nRoute::post('open-platform', function () {\n    $server = $app->getServer();\n\n    // 处理授权成功事件，其他事件同理\n    $server->handleAuthorized(function ($message) {\n        // $message 为微信推送的通知内容，不同事件不同内容，详看微信官方文档\n        // 获取授权公众号 AppId： $message['AuthorizerAppid']\n        // 获取 AuthCode：$message['AuthorizationCode']\n        // 然后进行业务处理，如存数据库等...\n    });\n\n    return $server->serve();\n});\n```\n\n</details>\n\n<details>\n    <summary>Laravel Octane(swoole) 开放平台处理推送消息</summary>\n\n```php\n// routes/web.php\n\nuse EasyWeChat\\OpenPlatform\\Application;\n\n// 授权事件回调地址：http://easywechat.com/open-platform/server\nRoute::post('open-platform/server', function () {\n        $config = config('wechatv6.open_platform');\n        $app = new Application($config);\n\n        // 兼容octane\n        $app->setRequestFromSymfonyRequest(request());\n\n        $server = $app->getServer();\n        return $server->serve();\n});\n```\n\n</details>\n\n<details>\n    <summary>webman 开放平台处理推送消息</summary>\n\n```php\nnamespace app\\controller;\n\nuse EasyWeChat\\OpenPlatform\\Application;\nuse support\\Request;\nuse Symfony\\Component\\HttpFoundation\\HeaderBag;\nuse Symfony\\Component\\HttpFoundation\\Request as SymfonyRequest;\n\n// 授权事件回调地址：http://easywechat.com/openPlatform/server\n\nclass OpenPlatform\n{\n    public function server(Request $request)\n    {\n        $config = config('wechatv6.open_platform');\n        $app = new Application($config);\n        $symfony_request = new SymfonyRequest($request->get(), $request->post(), [], $request->cookie(), [], [], $request->rawBody());\n        $symfony_request->headers = new HeaderBag($request->header());\n        $app->setRequestFromSymfonyRequest($symfony_request);\n        $server = $app->getServer();\n        $response = $server->serve();\n        return $response->getBody()->getContents();\n    }\n}\n```\n\n</details>\n\n<details>\n  <summary>Laravel 开放平台PC版预授权<version-tag>6.3.0+</version-tag></summary>\n\n> [官方文档](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html)\n\n用例：\n\n```php\n// routes/web.php\n\n// 授权落地页\nRoute::any('open-platform/auth', function(){\n        $auth_code = request()->get('auth_code');\n        // 完成授权写入数据库的逻辑省略。。。\n})->name('open_platform.auth');\n\n// 授权跳转页\nRoute::any('open-platform/preauth', function(){\n      // $app 为你实例化的开放平台对象，此处省略实例化步骤\n      $options=[\n            //1 表示手机端仅展示公众号；2 表示仅展示小程序，3 表示公众号和小程序都展示。如果为未指定，则默认小程序和公众号都展示。\n            // 'auth_type' => '',\n\n            // 指定的权限集id列表，如果不指定，则默认拉取当前第三方账号已经全网发布的权限集列表。\n            // 'category_id_list' => '',\n      ];\n\n      $url = $app->createPreAuthorizationUrl(route('open_platform.auth'), $options);\n\n      return response(\"<script>window.location.href='$url';</script>\")->header('Content-Type', 'text/html');\n});\n```\n\n</details>\n\n<details>\n  <summary>Laravel 开放平台代公众号/小程序代调用示例<version-tag>6.3.0+</version-tag></summary>\n\n路由配置：\n\n```php\n// routes/web.php\n// 例如：https://easywechat.com/open-platform/miniapp/get-phone-number/wx123212312313abc\n\nRoute::any('open-platform/miniapp/get-phone-number/{appid}', 'OpenPlatformController@getPhoneNumber');\nRoute::any('open-platform/officialAccount/get-user-list/{appid}', 'OpenPlatformController@getUsers');\n```\n\n对应控制器：`app/Http/Controllers/OpenPlatformController`：\n\n```php\nuse App\\Http\\Controllers\\Controller;\n\nclass OpenPlatformController extends Controller\n{\n    public function mini(string $appid): \\EasyWeChat\\MiniApp\\Application\n    {\n        $refreshToken = '授权后在缓存或数据库获取';\n\n        // $app 为你实例化的开放平台对象，此处省略实例化步骤\n        return $app->getMiniAppWithRefreshToken($appid, $refreshToken);\n    }\n\n    public function officialAccount(string $appid): \\EasyWeChat\\OfficialAccount\\Application\n    {\n        $refreshToken = '授权后在缓存或数据库获取';\n\n        // $app 为你实例化的开放平台对象，此处省略实例化步骤\n        return $app->getOfficialAccountWithRefreshToken($appid, $refreshToken);\n    }\n\n    public function getUsers(string $appid)\n    {\n        return $this->officialAccount($appid)\n                    ->getClient()\n                    ->get('cgi-bin/users/list')\n                    ->toArray();\n    }\n\n    public function getPhoneNumber(string $appid)\n    {\n        $data = [\n          'code' => (string) request()->get('code'),\n        ];\n\n        return $this->mini($appid)\n                    ->getClient()\n                    ->postJson('wxa/business/getuserphonenumber', $data)\n                    ->toArray();\n    }\n}\n```\n\n</details>\n\n<details>\n  <summary>Laravel 开放平台代公众号处理回调事件</summary>\n\n```php\n// 代公众号处理回调事件\nRoute::any('callback/{appid}', function ($appId) {\n    // $app 为你实例化的开放平台对象，此处省略实例化步骤\n    // $refreshToken 为授权后你缓存或数据库中的 authorizer_refresh_token，此处省略获取步骤\n\n    $refreshToken = '你已缓存或数据库中的 authorizer_refresh_token';\n\n    $server = $app->getOfficialAccountWithRefreshToken($appId, $refreshToken)->getServer();\n\n    $server->addMessageListener('text', function ($message) {\n        return sprintf(\"你对 overtrue 说：“%s”\", $message->Content);\n    });\n\n    return $server->serve();\n});\n```\n\n</details>\n\n<!--\n<details>\n    <summary>标题</summary>\n内容\n</details>\n-->\n"
  },
  {
    "path": "docs/src/6.x/open-platform/index.md",
    "content": "# 微信开放平台第三方平台\n\n请仔细阅读并理解：[微信官方文档 - 开放平台 - 第三方平台](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/Third_party_platform_appid.html)\n\n## 实例化\n\n请按如下格式配置你的开放平台账号信息，并实例化一个开放平台对象：\n\n```php\n<?php\nuse EasyWeChat\\OpenPlatform\\Application;\n\n$config = [\n  'app_id' => 'wx3cf0f39249eb0exx', // 开放平台账号的 appid\n  'secret' => 'f1c242f4f28f735d4687abb469072axx',   // 开放平台账号的 secret\n  'token' => 'easywechat',  // 开放平台账号的 token\n  'aes_key' => '',   // 明文模式请勿填写 EncodingAESKey\n\n  /**\n   * 接口请求相关配置，超时时间等，具体可用参数请参考：\n   * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n   */\n  'http' => [\n      'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n      'timeout' => 5.0,\n      // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n      'retry' => true, // 使用默认重试配置\n      //  'retry' => [\n      //      // 仅以下状态码重试\n      //      'status_codes' => [429, 500]\n      //       // 最大重试次数\n      //      'max_retries' => 3,\n      //      // 请求间隔 (毫秒)\n      //      'delay' => 1000,\n      //      // 如果设置，每次重试的等待时间都会增加这个系数\n      //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n      //      'multiplier' => 3\n      //  ],\n  ],\n];\n\n$app = new Application($config);\n```\n\n> 💡 请不要把公众号/小程序的配置信息用于初始化开放平台。\n\n## API\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### 服务端\n\n服务端模块封装了服务端相关的便捷操作，隐藏了部分复杂的细节，基于中间件模式可以更方便的处理消息推送和服务端验证。\n\n```php\n$app->getServer();\n```\n\n:book: 更多说明请参阅：[服务端使用文档](server.md)\n\n### API Client\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用开放平台任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 配置\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->all()` 获取整个配置的数组。\n\n还可以使用 `$config->get($key, $default)` 读取单个配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### ComponentAccessToken\n\naccess_token 是开放平台 API 调用的必备条件，如果你想获取它的值，你可以通过以下方式拿到当前的 access_token：\n\n```php\n$componentAccessToken = $app->getComponentAccessToken();\n$componentAccessToken->getToken(); // string\n```\n\n当然你也可以使用自己的 ComponentAccessToken 类：\n\n```php\n$componentAccessToken = new MyCustomComponentAccessToken();\n$app->setComponentAccessToken($componentAccessToken)\n```\n\n### VerifyTicket\n\n你可以通过以下方式拿到当前 verify_ticket 类：\n\n```php\n$verifyTicket = $app->getVerifyTicket();\n\n$verifyTicket->getTicket(); // strval\n```\n\n### 开放平台账户\n\n开放平台账号类，提供一系列 API 获取开放平台的基本信息：\n\n```php\n$account = $app->getAccount();\n\n$account->getAppId();\n$account->getSecret();\n$account->getToken();\n$account->getAesKey();\n```\n\n## 第三方应用或网站网页授权\n\n> 注意：不是代公众号/小程序授权。\n\n第三方应用或者网站网页授权的逻辑和公众号的网页授权基本一样：\n\n```php\n$oauth = $app->getOAuth();\n```\n\n:book: 详情请参考：[网页授权](../oauth.md)\n\n## 使用授权码获取授权信息\n\n在用户在授权页授权流程完成后，授权页会自动跳转进入回调 URI，并在 URL 参数中返回授权码和过期时间，如：(`https://easywechat.com/callback?auth_code=xxx&expires_in=600`)\n\n```php\n$authorizationCode = '授权成功时返回给第三方平台的授权码';\n\n$authorization = $app->getAuthorization($authorizationCode);\n\n$authorization->getAppId(); // authorizer_appid\n$authorization->getAccessToken(); // EasyWeChat\\OpenPlatform\\AuthorizerAccessToken\n$authorization->getRefreshToken(); // authorizer_access_token\n$authorization->toArray();\n$authorization->toJson();\n\n// {\n//   \"authorization_info\": {\n//     \"authorizer_appid\": \"wxf8b4f85f3a79...\",\n//     \"authorizer_access_token\": \"QXjUqNqfYVH0yBE1iI_7vuN_9gQbpjfK7M...\",\n//     \"expires_in\": 7200,\n//     \"authorizer_refresh_token\": \"dTo-YCXPL4llX-u1W1pPpnp8Hgm4wpJt...\",\n//     \"func_info\": [\n//       {\n//         \"funcscope_category\": {\n//           \"id\": 1\n//         }\n//       },\n//       //...\n//     ]\n//   }\n// }\n\n```\n\n## 创建预授权码 <version-tag>6.3.0+</version-tag>\n\n你可以通过下面的方式创建预授权码：\n\n```php\n$reponse = $app->createPreAuthorizationCode();\n// {\n//   \"pre_auth_code\": \"Cx_Dk6qiBE0Dmx4eKM-2SuzA...\",\n//   \"expires_in\": 600\n// }\n```\n\n## 生成授权页地址 <version-tag>6.3.0+</version-tag>\n\n你可以通过下面方法生成一个授权页地址，引导用户进行授权：\n\n```php\n// 自动获取预授权码模式\n$url = $app->createPreAuthorizationUrl('http://easywechat.com/callback');\n\n// 或者指定预授权码\n$preAuthCode = 'createPreAuthorizationCode 得到的预授权码 pre_auth_code';\n$url = $app->createPreAuthorizationUrl('http://easywechat.com/callback', $preAuthCode);\n```\n\n## 获取/刷新接口调用令牌\n\n在公众号/小程序接口调用令牌 `authorizer_access_token` 失效时，可以使用刷新令牌 `authorizer_refresh_token` 获取新的接口调用令牌。\n\n> `authorizer_access_token` 有效期为 2 小时，开发者需要缓存 `authorizer_access_token`，避免获取/刷新接口调用令牌的 API 调用触发每日限额。\n\n```php\n$authorizerAppId = '授权方 appid';\n$authorizerRefreshToken = '上一步得到的 authorizer_refresh_token';\n\n$app->refreshAuthorizerToken($authorizerAppId, $authorizerRefreshToken)\n\n// {\n//   \"authorizer_access_token\": \"some-access-token\",\n//   \"expires_in\": 7200,\n//   \"authorizer_refresh_token\": \"refresh_token_value\"\n// }\n```\n\n---\n\n## 代替公众号/小程序请求 API\n\n代替公众号/小程序请求，需要首先拿到 `EasyWeChat\\OpenPlatform\\AuthorizerAccessToken`，用以代替公众号的 Access Token，官方流程说明：[开发前必读 /Token 生成介绍](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/creat_token.html) 。\n\n### 获取 AuthorizerAccessToken\n\n你可以使用开放 **平台永久授权码** 换取授权者信息，然后换取 Authorizer Access Token：\n\n```php\n$authorizationCode = '授权成功时返回给第三方平台的授权码';\n$authorization = $app->getAuthorization($authorizationCode);\n$authorizerAccessToken = $authorization->getAccessToken();\n```\n\n> 🚨 Authorizer Access Token 只有 2 小时有效期，不建议将它存储到数据库，当然如果你不得不这么做，请记得参考上面 「**获取/刷新接口调用令牌**」章节刷新。\n\n### 代公众号调用\n\n**方式一：使用 authorizer_refresh_token** <version-tag>6.3.0+</version-tag>\n\n此方式适用于大部分场景，将授权信息存储到数据库中，代替调用时取出对应公众号的 authorizer_refresh_token 即可。\n\n```php\n$authorizerRefreshToken = '公众号授权时得到的 authorizer_refresh_token';\n$officialAccount = $app->getOfficialAccountWithRefreshToken($appId, $authorizerRefreshToken);\n```\n\n**方式二：使用 authorizer_access_token** <version-tag>6.3.0+</version-tag>\n\n此方案适用于使用独立的中央授权服务单独维护授权信息的方式。\n\n```php\n$authorizerAccessToken = '公众号授权时得到的 authorizer_access_token';\n$officialAccount = $app->getOfficialAccountWithAccessToken($appId, $authorizerAccessToken);\n```\n\n**方式三：使用 AuthorizerAccessToken 类**\n\n不推荐，请使用方式一或者二，此方法由于设计之初没有充分考虑到使用场景，导致使用很麻烦。\n\n```php\n// $token 为你存到数据库的授权码 authorizer_access_token\n$authorizerAccessToken = new AuthorizerAccessToken($authorizerAppId, $token);\n$officialAccount = $app->getOfficialAccount($authorizerAccessToken);\n\n\n使用以上方式初始化公众号对象后，可以直接调用公众号的 API 方法，如：\n\n// 调用公众号接口\n$response = $officialAccount->getClient()->get('cgi-bin/users/list');\n```\n\n> `$officialAccount` 为 `EasyWeChat\\OfficialAccount\\Application` 实例\n\n:book: 更多公众号用法请参考：[公众号](../official-account/index.md)\n\n### 代小程序调用\n\n小程序和公众号使用方式一样，同样有三种方式：\n\n```php\n// 方式一：使用 authorizer_refresh_token\n$authorizerRefreshToken = '小程序授权时得到的 authorizer_refresh_token';\n$officialAccount = $app->getMiniAppWithRefreshToken($appId, $authorizerRefreshToken);\n\n// 方式二：使用 authorizer_access_token\n$authorizerAccessToken = '小程序授权时得到的 authorizer_access_token';\n$officialAccount = $app->getMiniAppWithAccessToken($appId, $authorizerAccessToken);\n\n// 方式三：不推荐\n// $token 为你存到数据库的授权码 authorizer_access_token\n$authorizerAccessToken = new AuthorizerAccessToken($authorizerAppId, $token);\n$miniApp = $app->getMiniApp($authorizerAccessToken);\n\n// 调用小程序接口\n$response = $miniApp->getClient()->get('cgi-bin/users/list');\n```\n\n- [微信官方文档 - 开放平台代小程序实现小程序登录接口](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/others/WeChat_login.html#请求地址)\n\n:book: 更多小程序用法请参考：[小程序](../mini-app/index.md)\n"
  },
  {
    "path": "docs/src/6.x/open-platform/server.md",
    "content": "# 服务端\n\n第三方平台的服务端推送和公众号一样，请参考：[公众号：服务端](../official-account/server.md)\n\n## 第三方平台推送事件处理\n\n公众号第三方平台推送的有四个事件：\n\n> 如已经授权的公众号、小程序再次进行授权，而未修改已授权的权限的话，是没有相关事件推送的。\n\n- 授权成功 `authorized`\n- 授权更新 `updateauthorized`\n- 授权取消 `unauthorized`\n- VerifyTicket `component_verify_ticket`\n\nSDK 默认会处理事件 `component_verify_ticket` ，并会缓存 `verify_ticket` 所以如果你暂时不需要处理其他事件，直接这样使用即可：\n\n```php\n$server = $app->getServer();\n\nreturn $server->serve();\n```\n\n## 内置消息处理器\n\n> _消息处理器详细说明见公众号开发 - 服务端一节_\n\n### 处理授权成功事件\n\n```php\n$server->handleAuthorized(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n### 处理授权更新事件\n\n```php\n$server->handleAuthorizeUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n### 处理授权取消事件\n\n```php\n$server->handleUnauthorized(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 处理 VerifyTicket 推送事件（已默认处理）\n\n此推送已经默认处理（使用缓存存储和刷新），可以直接忽略。\n\n> 注意：如果你自行处理了 VerifyTicket 推送，你必须同时设置 ComponentAccessToken 类，因为 ComponentAccessToken 依赖它。\n\n```php\n$server->handleVerifyTicketRefreshed(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n## 其它事件处理\n\n以上便捷方法都只处理了特定事件，其它状态，可以通过自定义事件处理中间件的形式处理：\n\n```php\n$server->with(function($message, \\Closure $next) {\n    // $message->event_type 事件类型\n    return $next($message);\n});\n```\n\n## 自助处理推送消息\n\n你可以通过下面的方式获取来自微信服务器的推送消息：\n\n\n```php\n$message = $server->getRequestMessage(); // 原始消息\n```\n\n你也可以获取解密后的消息 <version-tag>6.5.0+</version-tag>\n\n```php\n$message = $server->getDecryptedMessage();\n```\n\n`$message` 为一个 `EasyWeChat\\OpenPlatform\\Message` 实例。\n\n你可以在处理完逻辑后自行创建一个响应，当然，在不同的框架里，响应写法也不一样，请自行实现。\n"
  },
  {
    "path": "docs/src/6.x/open-work/examples.md",
    "content": "---\naside: false\ntitle: 企业微信开放平台使用代码示例\nnext:\n  text: 关于6.x\n  link: ../introduction\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n\n\n<!--\n<details>\n    <summary>标题</summary>\n内容\n</details>\n-->\n"
  },
  {
    "path": "docs/src/6.x/open-work/index.md",
    "content": "# 企业微信服务商\n\n请仔细阅读并理解：[企业微信 API - 第三方应用开发](https://open.work.weixin.qq.com/api/doc/90001/90142/90594)\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\OpenWork\\Application;\n\n$config = [\n  'corp_id' => 'wx3cf0f39249eb0exx',\n  'provider_secret' => 'f1c242f4f28f735d4687abb469072axx',\n  'token' => 'easywechat',\n  'aes_key' => '', // 明文模式请勿填写 EncodingAESKey\n\n  /**\n   * 接口请求相关配置，超时时间等，具体可用参数请参考：\n   * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n   */\n  'http' => [\n      'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n      'timeout' => 5.0,\n      // 'base_uri' => 'https://qyapi.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n      'retry' => true, // 使用默认重试配置\n      //  'retry' => [\n      //      // 仅以下状态码重试\n      //      'status_codes' => [429, 500]\n      //       // 最大重试次数\n      //      'max_retries' => 3,\n      //      // 请求间隔 (毫秒)\n      //      'delay' => 1000,\n      //      // 如果设置，每次重试的等待时间都会增加这个系数\n      //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n      //      'multiplier' => 3\n      //  ],\n  ],\n];\n\n$app = new Application($config);\n```\n\n## API\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### 服务端\n\n服务端模块封装了服务端相关的便捷操作，隐藏了部分复杂的细节，基于中间件模式可以更方便的处理消息推送和服务端验证。\n\n```php\n$app->getServer();\n```\n\n:book: 更多说明请参阅：[服务端使用文档](server.md)\n\n### API Client\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用开放平台任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 配置\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->get($key, $default)` 读取配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### ProviderAccessToken\n\nprovider_access_token 是开放平台 API 调用的必备条件，如果你想获取它的值，你可以通过以下方式拿到当前的 provider_access_token：\n\n```php\n$providerAccessToken = $app->getProviderAccessToken();\n$providerAccessToken->getToken(); // string\n```\n\n当然你也可以使用自己的 ProviderAccessToken 类：\n\n```php\n$providerAccessToken = new MyCustomProviderAccessToken();\n$app->setProviderAccessToken($providerAccessToken)\n```\n\n### SuiteTicket\n\n你可以通过以下方式拿到当前 suite_ticket 类：\n\n```php\n$suiteTicket = $app->getSuiteTicket();\n\n$suiteTicket->getTicket(); // string\n```\n\n### 开放平台账户\n\n开放平台账号类，提供一系列 API 获取开放平台的基本信息：\n\n```php\n$account = $app->getAccount();\n\n$account->getCorpId();\n$account->getProviderSecret();\n$account->getToken();\n$account->getAesKey();\n```\n\n## 第三方应用需要在打开的网页里面携带用户的身份信息\n\n> [点此查看官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/91120#%E6%9E%84%E9%80%A0%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%94%E7%94%A8oauth2%E9%93%BE%E6%8E%A5)\n\n第三方应用或者网站网页授权的逻辑和公众号的网页授权基本一样：\n\n```php\n$oauth = $app->getOAuth(string $suiteId, AccessTokenInterface $suiteAccessToken);\n```\n\n:book: 详情请参考：[网页授权](./oauth.md)\n\n## 企业网页授权\n\n> [点此查看官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/91120#%E6%9E%84%E9%80%A0%E4%BC%81%E4%B8%9Aoauth2%E9%93%BE%E6%8E%A5)\n\n```php\n$oauth = $app->getCorpOAuth(string $corpId, AccessTokenInterface $suiteAccessToken);\n```\n\n:book: 详情请参考：[网页授权](./oauth.md)\n\n## 使用授权码获取授权信息\n\n在用户在授权页授权流程完成后，授权页会自动跳转进入回调 URI，并在 URL 参数中返回授权码和过期时间，如：(`https://easywechat.com/callback?auth_code=xxx&expires_in=600`)\n\n```php\n$permanentCode = '企业永久授权码';\n$suiteAccessToken = new SuiteAccessToken($suiteId, $suiteSecret);\n\n$authorization = $app->getAuthorization($corpId, $authorizatpermanentCodeionCode, $suiteAccessToken);\n\n$authorization->getCorpId(); // auth_corp_info.corpid\n$authorization->toArray();\n$authorization->toJson();\n\n// {\n//     \"errcode\":0 ,\n//     \"errmsg\":\"ok\" ,\n//     \"dealer_corp_info\":\n//     {\n//         \"corpid\": \"xxxx\",\n//         \"corp_name\": \"name\"\n//     },\n//     \"auth_corp_info\":\n//     {\n//         \"corpid\": \"xxxx\",\n//         \"corp_name\": \"name\",\n//         \"corp_type\": \"verified\",\n//         \"corp_square_logo_url\": \"yyyyy\",\n//         \"corp_user_max\": 50,\n//         \"corp_agent_max\": 30,\n//         \"corp_full_name\":\"full_name\",\n//         \"verified_end_time\":1431775834,\n//         \"subject_type\": 1,\n//         \"corp_wxqrcode\": \"zzzzz\",\n//         \"corp_scale\": \"1-50人\",\n//         \"corp_industry\": \"IT服务\",\n//         \"corp_sub_industry\": \"计算机软件/硬件/信息服务\",\n//         \"location\":\"广东省广州市\"\n//     },\n//     \"auth_info\":\n//     {\n//         \"agent\" :\n//         [\n//             {\n//                 \"agentid\":1,\n//                 \"name\":\"NAME\",\n//                 \"round_logo_url\":\"xxxxxx\",\n//                 \"square_logo_url\":\"yyyyyy\",\n//                 \"appid\":1,\n//                 \"auth_mode\":1,\n//                 \"privilege\":\n//                 {\n//                     \"level\":1,\n//                     \"allow_party\":[1,2,3],\n//                     \"allow_user\":[\"zhansan\",\"lisi\"],\n//                     \"allow_tag\":[1,2,3],\n//                     \"extra_party\":[4,5,6],\n//                     \"extra_user\":[\"wangwu\"],\n//                     \"extra_tag\":[4,5,6]\n//                 },\n//                 \"shared_from\":\n//                 {\n//                     \"corpid\":\"wwyyyyy\"\n//                 }\n//             },\n//             {\n//                 \"agentid\":2,\n//                 \"name\":\"NAME2\",\n//                 \"round_logo_url\":\"xxxxxx\",\n//                 \"square_logo_url\":\"yyyyyy\",\n//                 \"appid\":5,\n//                 \"shared_from\":\n//                 {\n//                     \"corpid\":\"wwyyyyy\"\n//                 }\n//             }\n//         ]\n//     }\n// }\n\n```\n\n## 获取企业凭证\n\n在公众号/小程序接口调用令牌（`authorizer_access_token`）失效时，可以使用刷新令牌（authorizer_refresh_token）获取新的接口调用令牌。\n\n> 注意： `authorizer_access_token` 有效期为 2 小时，开发者需要缓存 `authorizer_access_token`，避免获取/刷新接口调用令牌的 API 调用触发每日限额。缓存方法可以参考：<https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html>\n\n```php\n$permanentCode = '企业永久授权码';\n$suiteAccessToken = new SuiteAccessToken($suiteId, $suiteSecret);\n\n$authorizerAccessToken = $app->getAuthorizerAccessToken($corpId, $permanentCode, $suiteAccessToken)\n\n// {\n//     \"errcode\":0 ,\n//     \"errmsg\":\"ok\" ,\n//     \"access_token\": \"xxxxxx\",\n//     \"expires_in\": 7200\n// }\n\n\n$authorizerAccessToken->getToken(); // string\n```\n"
  },
  {
    "path": "docs/src/6.x/open-work/oauth.md",
    "content": "# OAuth\n\n第三方服务商网页授权有两种：\n\n- [第三方应用网页授权](https://open.work.weixin.qq.com/api/doc/90001/90143/91120#%E6%9E%84%E9%80%A0%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%94%E7%94%A8oauth2%E9%93%BE%E6%8E%A5)\n- [企业网页授权](https://open.work.weixin.qq.com/api/doc/90001/90143/91120#%E6%9E%84%E9%80%A0%E4%BC%81%E4%B8%9Aoauth2%E9%93%BE%E6%8E%A5)\n\n创建实例：\n\n```php\nuse EasyWeChat\\Work\\Application;\n\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n];\n\n$app = new Application($config);\n```\n\n## 获取 OAuth 模块实例\n\n请根据你的场景选择对应的方法获取 OAuth 实例：\n\n```php\n// 第三方应用网页授权\n$oauth = $app->getOAuth(string $suiteId, AccessTokenInterface $suiteAccessToken);\n\n// 企业网页授权\n$oauth = $app->getCorpOAuth(string $corpId, AccessTokenInterface $suiteAccessToken);\n// 如需指定应用ID\n$oauth = $oauth->withAgentId($agentId);\n```\n\n## 跳转授权\n\n```php\n// $callbackUrl 为授权回调地址\n$callbackUrl = 'https://xxx.xxx'; // 需设置可信域名\n\n// 返回授权跳转链接\n$redirectUrl = $app->getOAuth()->redirect($callbackUrl);\n```\n\n## 获取授权用户信息\n\n在回调页面中，你可以使用以下方式获取授权者信息：\n\n```php\n$code = \"回调URL中的code\";\n$user = $app->getOAuth()->detailed()->userFromCode($code);\n\n// 获取用户信息\n$user->getId(); // 对应企业微信英文名（userid）\n$user->getRaw(); // 获取企业微信接口返回的原始信息\n```\n\n:book: OAuth 详情请参考：[网页授权](../oauth.md)\n\n获取用户其他信息需调用通讯录接口，参考：[企业微信通讯录 API](https://github.com/EasyWeChat/docs/blob/master/wework/contacts.md)\n\n## 参考阅读\n\n- 本模块基于 [overtrue/socialite](https://github.com/overtrue/socialite/) 实现，更多的使用请阅读该扩展包文档。\n- state 参数的使用: [overtrue/socialite/#state](https://github.com/overtrue/socialite/#state)\n"
  },
  {
    "path": "docs/src/6.x/open-work/server.md",
    "content": "# 服务端\n\n企业微信第三方服务端推送和公众号一样，请参考：[公众号：服务端](../official-account/server.md)\n\n## 第三方平台推送事件处理\n\n企业微信第三方数据推送的有以下事件：\n\n- suite_ticket 推送 `suite_ticket`\n- 授权成功 `create_auth`\n- 授权变更 `change_auth`\n- 授权取消 `cancel_auth`\n- 通讯录变更（InfoType） `change_contact`\n  - ChangeType\n    - 成员变更\n      - 新增成员 `create_user`\n      - 更新成员 `update_user`\n      - 删除成员 `delete_user`\n    - 部门变更\n      - 新增部门 `create_party`\n      - 更新部门 `update_party`\n      - 删除部门 `delete_party`\n    - 标签变更\n      - 成员标签变更 `update_tag`\n- 共享应用事件回调 `share_agent_change`\n- 重置永久授权码通知 `reset_permanent_code`\n- 应用管理员变更通知 `change_app_admin`\n\n## 内置消息处理器\n\n> _消息处理器详细说明见：公众号开发 - 服务端一节_\n\n### 授权成功事件\n\n```php\n$server->handleAuthCreated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 授权变更事件\n\n```php\n$server->handleAuthChanged(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 授权取消事件\n\n```php\n$server->handleAuthCancelled(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 通讯录变更事件\n\n```php\n$server->handleContactChanged(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 成员变更事件\n\n```php\n// 新增成员\n$server->handleUserCreated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 更新成员\n$server->handleUserUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 删除成员\n$server->handleUserDeleted(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 部门变更事件\n\n```php\n// 新增部门\n$server->handlePartyCreated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 更新部门\n$server->handlePartyUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 删除部门\n$server->handlePartyDeleted(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 成员标签变更事件\n\n```php\n$server->handleUserTagUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 共享应用事件\n\n```php\n$server->handleShareAgentChanged(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n### 重置永久授权码通知\n\n```php\n$server->handleResetPermanentCode(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n### 应用管理员变更通知\n\n```php\n$server->handleChangeAppAdmin(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### suite_ticket 推送事件\n\n此推送已经默认处理（使用缓存存储和刷新），可以直接忽略。\n\n> 注意：如果你自行处理了 SuiteTicket 推送，你必须同时设置 ProviderAccessToken 类，因为 ProviderAccessToken 依赖它。\n\n```php\n$server->handleSuiteTicketRefreshed(callable | string $handler);\n```\n\n## 其它事件处理\n\n以上便捷方法都只处理了特定事件，其它状态，可以通过自定义事件处理中间件的形式处理：\n\n```php\n$server->with(function($message, \\Closure $next) {\n    // $message->event_type 事件类型\n    return $next($message);\n});\n```\n\n## 自助处理推送消息\n\n你可以通过下面的方式获取来自微信服务器的推送消息：\n\n```php\n$message = $server->getRequestMessage(); // 原始消息\n```\n\n你也可以获取解密后的消息 <version-tag>6.5.0+</version-tag>\n\n```php\n$message = $server->getDecryptedMessage();\n```\n\n`$message` 为一个 `EasyWeChat\\OpenWork\\Message` 实例。\n\n你可以在处理完逻辑后自行创建一个响应，当然，在不同的框架里，响应写法也不一样，请自行实现。\n"
  },
  {
    "path": "docs/src/6.x/pay/examples.md",
    "content": "---\naside: false\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n<details open>\n    <summary>JSAPI 下单</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791856)\n\n```php\n$response = $app->getClient()->postJson(\"v3/pay/transactions/jsapi\", [\n   \"mchid\" => \"1518700000\", // <---- 请修改为您的商户号\n   \"out_trade_no\" => \"native12177525012012070352333'.rand(1,1000).'\",\n   \"appid\" => \"wx6222e9f48a0xxxxx\", // <---- 请修改为服务号的 appid\n   \"description\" => \"Image形象店-深圳腾大-QQ公仔\",\n   \"notify_url\" => \"https://weixin.qq.com/\",\n   \"amount\" => [\n        \"total\" => 1,\n        \"currency\" => \"CNY\"\n    ],\n    \"payer\" => [\n        \"openid\" => \"o4GgauInH_RCEdvrrNGrnxxxxxx\" // <---- 请修改为服务号下单用户的 openid\n    ]\n]);\n\n\\dd($response->toArray(false));\n```\n\n</details>\n\n<details>\n    <summary>Native 下单</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791877)\n\n```php\n$response = $app->getClient()->postJson('v3/pay/transactions/native', [\n    'mchid' => (string)$app->getMerchant()->getMerchantId(),\n    'out_trade_no' => 'native20210720xxx',\n    'appid' => 'wxe2fb06xxxxxxxxxx6',\n    'description' => 'Image形象店-深圳腾大-QQ公仔',\n    'notify_url' => 'https://weixin.qq.com/',\n    'amount' => [\n        'total' => 1,\n        'currency' => 'CNY',\n    ]\n]);\n\nprint_r($response->toArray(false));\n```\n\n</details>\n\n<details>\n    <summary>查询订单（商户订单号）</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791859)\n\n```php\n$outTradeNo = 'native20210720xxx';\n$response = $app->getClient()->get(\"v3/pay/transactions/out-trade-no/{$outTradeNo}\", [\n    'query'=>[\n        'mchid' =>  $app->getMerchant()->getMerchantId()\n    ]\n]);\n\nprint_r($response->toArray());\n```\n\n</details>\n\n<details>\n    <summary>查询订单（微信订单号）</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791858)\n\n```php\n$transactionId = '217752501201407033233368018';\n$response = $app->getClient()->get(\"v3/pay/transactions/id/{$transactionId}\", [\n    'query'=>[\n        'mchid' =>  $app->getMerchant()->getMerchantId()\n    ]\n]);\n\nprint_r($response->toArray());\n```\n\n</details>\n\n<details>\n    <summary>Laravel 中处理微信支付回调</summary>\n\n> 记得需要将此类路由 [排除 csrf 验证](https://laravel.com/docs/12.x/csrf#csrf-excluding-uris)。\n\n```php\n// 假设你设置的通知地址notify_url为: https://easywechat.com/payment_notify\n\n// 注意：通知地址notify_url必须为https协议\n\nRoute::post('payment_notify', function () {\n    // $app 为你实例化的支付对象，此处省略实例化步骤\n    $server = $app->getServer();\n\n    // 处理支付结果事件\n    $server->handlePaid(function ($message) {\n        // $message 为微信推送的通知结果，详看微信官方文档\n\n        // 微信支付订单号 $message['transaction_id']\n        // 商户订单号 $message['out_trade_no']\n        // 商户号 $message['mchid']\n        // 具体看微信官方文档...\n        // 进行业务处理，如存数据库等...\n    });\n\n    // 处理退款结果事件\n    $server->handleRefunded(function ($message) {\n        // 同上，$message 详看微信官方文档\n        // 进行业务处理，如存数据库等...\n    });\n\n    return $server->serve();\n});\n```\n\n</details>\n\n<details>\n   <summary>付款（V2）</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v2/merchant/4011989673)\n\n```php\n$response = $api->post('/mmpaymkttransfers/promotion/transfers', [\n    'xml' => [\n        'mch_appid' => $app->getConfig()['app_id'],     //注意在配置文件中加上app_id\n        'mchid' => $app->getConfig()['mch_id'],         //商户号\n        'partner_trade_no' => '202203081646729819743',  // 商户订单号，需保持唯一性(只能是字母或者数字，不能包含有符号)\n        'openid' => 'ogn1H45HCRxVRiEMLbLLuABbxxxx',     //用户openid\n        'check_name' => 'FORCE_CHECK',                  // NO_CHECK：不校验真实姓名, FORCE_CHECK：强校验真实姓名\n        're_user_name'=> '用户真实姓名',                  // 如果 check_name 设置为 FORCE_CHECK 则必填用户真实姓名\n        'amount' => '100',                              //金额\n        'desc' => '理赔',                                // 企业付款操作说明信息。必填\n    ],\n    'local_cert' => $app->getConfig()['certificate'], //v2证书绝对路径\n    'local_pk' => $app->getConfig()['private_key'],   //v2证书密钥绝对路径\n]);\n\nprint_r($response->toArray());\n```\n\n</details>\n\n<details>\n   <summary>JSAPI下单（服务商）</summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/partner/4012738519)\n\n```php\n$response = $app->getClient()->postJson(\"v3/pay/partner/transactions/jsapi\", [\n    \"sp_appid\" => $appId, // 服务商应用ID\n    \"sp_mchid\" => '********', // 服务商户号\n    'sub_mchid' => '*********', // 子商户号/二级商户号\n    \"sub_appid\" => '********', // 子商户/二级商户应用ID(选填)\n    \"description\" => $this->payDesc($from), // 商品描述\n    \"out_trade_no\" => $order['pay_sn'], // 商户订单号\n    \"notify_url\" => $this->config['notify_url'], // 通知地址\n    \"amount\" => [\n        \"total\" => intval($order['order_amount'] * 100), // 总金额\n    ], // 订单金额信息\n    \"payer\" => [\n        \"sp_openid\" => $this->auth['openid'], // 用户服务标识，户在服务商AppID下的唯一标识\n        \"sub_openid\" => $this->auth['openid'] // 用户子标识，用户在子商户AppID下的唯一标识。若传sub_openid，那sub_appid必填。下单前需获取到用户的OpenID\n    ], // 支付者,(sp_openid 和 sub_openid 二选一)\n    'attach' => $from\n]);\n\nprint_r($response->toArray());\n```\n\n</details>\n\n<details open>\n    <summary>敏感信息加密  <version-tag>6.17.0+</version-tag> </summary>\n\n> [官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4013053257)\n> 使用默认公钥 ID\n\n```php\n$utils = $app->getUtils();\n$response = $app->getClient()->withSerialHeader()->postJson(\"v3/applyment4sub/applyment/\", [\n   \"business_code\" => \"12345678\",\n   'contact_info'  => [\n        'contact_name' => $utils->encryptWithRsaPublicKey('张三'),\n        //...\n    ],\n    //...\n]);\n\nprint_r($response->toArray());\n```\n\n或指定平台证书序列号/微信支付公钥 ID (必须在配置项`platform_certs`内)\n\n```php\n$utils = $app->getUtils();\n$response = $app->getClient()->withSerialHeader(\"PUB_KEY_ID_123456\")->postJson(\"v3/applyment4sub/applyment/\", [\n   \"business_code\" => \"12345678\",\n   'contact_info'  => [\n        'contact_name' => $utils->encryptWithRsaPublicKey(\"张三\",\"PUB_KEY_ID_123456\"),\n        //...\n    ],\n    //...\n]);\n\nprint_r($response->toArray());\n```\n\n</details>\n  \n<!--\n<details>\n    <summary>标题</summary>\n内容\n</details>\n-->\n"
  },
  {
    "path": "docs/src/6.x/pay/index.md",
    "content": "# 微信支付\n\n请仔细阅读并理解：[微信官方文档 - 微信支付](https://pay.weixin.qq.com/doc/v3/merchant/4012062524)\n\n> [!NOTE]\n> 2024 年 Q3，微信支付官方开启了「微信支付公钥」平替「平台证书」方案，初始化所需的参数仅需配置上 **微信支付公钥 ID** 及 **微信支付公钥** 即完全兼容支持，CLI/API 下载 **平台证书** 已不是一个必要步骤，可略过。\n> **微信支付公钥 ID** 及 **微信支付公钥** 均可在 [微信支付商户平台](https://pay.weixin.qq.com/) -> 账户中心 -> API 安全 查看及/或下载。\n\n## 实例化 {#init}\n\n```php\n<?php\nuse EasyWeChat\\Pay\\Application;\n\n$config = [\n    'mch_id' => 1360649000,\n\n    // 商户证书\n    'private_key' => __DIR__ . '/certs/apiclient_key.pem',\n    'certificate' => __DIR__ . '/certs/apiclient_cert.pem',\n\n     // v3 API 秘钥\n    'secret_key' => '43A03299A3C3FED3D8CE7B820Fxxxxx',\n\n    // v2 API 秘钥\n    'v2_secret_key' => '26db3e15cfedb44abfbb5fe94fxxxxx',\n\n    // 平台证书：微信支付 APIv3 平台证书，需要使用工具下载\n    // 下载工具：https://github.com/wechatpay-apiv3/CertificateDownloader\n    'platform_certs' => [\n        // 如果是「平台证书」模式\n        // 使用 Key/Value 结构， key 为 平台证书的序列号，value 为微信支付平台证书的绝对路径\n        // \"{SerialNo}\" => '/path/to/wechatpay/cert.pem'\n\n        // 如果是「微信支付公钥」模式\n        // 使用 Key/Value 结构， key 为微信支付公钥 ID(PUB_KEY_ID 开头)，value 为微信支付公钥文件绝对路径\n        // \"{$pubKeyId}\" => '/path/to/wechatpay/pubkey.pem',\n    ],\n\n    /**\n     * 接口请求相关配置，超时时间等，具体可用参数请参考：\n     * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n     */\n    'http' => [\n        'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n        'timeout' => 5.0,\n        // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 base_uri\n        // 'base_uri' => 'https://api.mch.weixin.qq.com/',\n    ],\n];\n\n$app = new Application($config);\n```\n\n## API {#api}\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### API Client {#client}\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用开放平台任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 工具 {#tools}\n\n为了方便开发者生成各种调起支付所需配置，你可以使用工具类：\n\n```php\n$app->getUtils();\n```\n\n:book: 更多说明请参阅：[工具](utils.md)\n\n### 配置 {#config}\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->get($key, $default)` 读取配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### 支付账户 {#merchant}\n\n支付账户类，提供一系列 API 获取支付的基本信息：\n\n```php\n$account = $app->getMerchant();\n\n$account->getMerchantId();\n$account->getPrivateKey();\n$account->getCertificate();\n$account->getSecretKey();\n$account->getV2SecretKey();\n$account->getPlatformCert($serial);\n$account->getPlatformCerts();\n```\n\n### 一些可能会用到的 {#others}\n\n#### 签名验证 {#validation}\n\n按官方说法，建议在拿到**微信接口响应**和**接收到微信支付的回调通知**时，对通知的签名进行验证，以确保通知是微信支付发送的。\n\n你可以通过以下方式获取签名验证器：\n\n```php\n$app->getValidator();\n```\n\n##### 推送消息的签名验证 {#webhook}\n\n```php\n$server = $app->getServer();\n\n$server->handlePaid(function (Message $message, \\Closure $next) use ($app) {\n    // $message->out_trade_no 获取商户订单号\n    // $message->payer['openid'] 获取支付者 openid\n\n    try{\n        $app->getValidator()->validate($app->getRequest());\n       // 验证通过，业务处理\n    } catch(Exception $e){\n      // 验证失败\n    }\n\n    return $next($message);\n});\n\n// 默认返回 ['code' => 'SUCCESS', 'message' => '成功']\nreturn $server->serve();\n```\n\n##### API 返回值的签名验证 {#verify-response}\n\n```php\n// API 请求示例\n$response = $app->getClient()->postJson(\"v3/pay/transactions/jsapi\", [...]);\n\ntry{\n    $app->getValidator()->validate($response->toPsrResponse());\n   // 验证通过\n} catch(Exception $e){\n  // 验证失败\n}\n```\n\n#### 获取证书序列号 {#x509-serial-no}\n\n```bash\nopenssl x509 -in /path/to/merchant/apiclient_cert.pem -noout -serial | awk -F= '{print $2}'\n```\n"
  },
  {
    "path": "docs/src/6.x/pay/media.md",
    "content": "## 文件上传  <version-tag>6.10.0+</version-tag>\n\n由于微信 v3 对文件类上传使用特殊的签名机制，参见：[微信支付 - 图片上传API](https://pay.weixin.qq.com/doc/v3/merchant/4012557233)。\n\n因此，我们提供了一个媒体上传方法，方便开发者使用。\n\n```php\n$path = '/path/to/your/files/demo.jpg';\n\n$api->uploadMedia('/v3/marketing/favor/media/image-upload', $path);\n```\n\n## 自定义 meta 信息\n\n部分接口使用的签名 meta 不一致，所以可以自行传入：\n\n```php\n$url = '/v3/...';\n$path = '/path/to/your/files/demo.jpg';\n$meta = [\n  'bank_type' => 'CFT',\n  'filename' => 'demo.jpg',\n  'sha256' => 'xxxxxxxxxxx',\n];\n\n$api->uploadMedia($url, $path, $meta);\n```\n\n## 关于 sha256\n\n- 文件，用 `hash_file('sha256', $path)` 计算\n- 字符串，用 `hash('sha256', $string)` 计算\n"
  },
  {
    "path": "docs/src/6.x/pay/server.md",
    "content": "# 服务端\n\n支付推送和公众号几乎一样，请参考：[公众号：服务端](../official-account/server.md)。\n\n## 官方文档\n\n- [基础下单支付结果通知文档](https://pay.weixin.qq.com/doc/v3/merchant/4012284311)\n- [合单支付结果通知文档](https://pay.weixin.qq.com/doc/v3/partner/4012237246)\n- [退款结果通知文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791865)\n\n## 内置事件处理器\n\nSDK 内置了两个便捷方法以便于开发者快速处理支付推送事件：\n\n> `$message` 属性已经默认解密，可直接访问解密后的属性；\n> \n> 成功状态 SDK 默认会返回 success, 你可以不用返回任何东西；\n\n### 支付成功事件\n\n🚨 切记：推送信息不一定靠谱，可能是伪造的，所以拿到推送通知，只取订单号等必要信息，其它信息忽略，拿订单号重新查询微信支付订单的最新状态再做处理。\n\n> :book: [官方文档：支付结果通知](https://pay.weixin.qq.com/doc/v3/merchant/4012284311)\n\n```php\n$server = $app->getServer();\n$server->handlePaid(function (Message $message, \\Closure $next) {\n    // $message->out_trade_no 获取商户订单号\n    // $message->payer['openid'] 获取支付者 openid\n    // 🚨🚨🚨 注意：推送信息不一定靠谱哈，请务必验证\n    // 建议是拿订单号调用微信支付查询接口，以查询到的订单状态为准\n    return $next($message);\n});\n\n// 默认返回 ['code' => 'SUCCESS', 'message' => '成功']\nreturn $server->serve();\n```\n\n### 退款成功事件\n\n> :book: [官方文档：退款结果通知](https://pay.weixin.qq.com/doc/v3/merchant/4012791865)\n\n```php\n$server = $app->getServer();\n$server->handleRefunded(function (Message $message, \\Closure $next) {\n    // $message->out_trade_no 获取商户订单号\n    // $message->payer['openid'] 获取支付者 openid\n    return $next($message);\n});\n\n// 默认返回 ['code' => 'SUCCESS', 'message' => '成功']\nreturn $server->serve();\n```\n\n🚨 注意：经网友发现官方仍存在[使用 v2 模式的退款推送](https://pay.weixin.qq.com/doc/v2/merchant/4011985425)，所以如果你的退款逻辑有异常，请参考以下方式实现（需要配置 v2 API key）：\n\n> 网友反馈的[问题：#2737](https://github.com/w7corp/easywechat/issues/2737)\n> 目前已知的情况是：微信商户平台填写的 API 回调地址，然后在商户平台手动处理退款的。\n\n```php\n// 建议使用单独的路由处理退款！\n$server = $app->getServer();\n\n// 推送消息，已解密\n// 结构参考：https://pay.weixin.qq.com/doc/v2/merchant/4011985425\n$message = $server->getRequestMessage();\n\n// 你的逻辑...\n\n// 返回 SUCCESS 或者 FAIL 等其他状态\nreturn new \\Nyholm\\Psr7\\Response(\n        200, [],\n      \\EasyWeChat\\Kernel\\Support\\Xml::build([\n        'return_code' => 'SUCCESS',\n        'return_msg' => 'OK'\n      ])\n);\n```\n\n## 其它事件处理\n\n以上便捷方法都只处理了**成功状态**，其它状态，可以通过自定义事件处理中间件的形式处理：\n\n```php\n$server->with(function($message, \\Closure $next) {\n    // $message->event_type 事件类型\n    return $next($message);\n});\n```\n\n## 自助处理推送消息\n\n你可以通过下面的方式获取来自微信服务器的推送消息：\n\n```php\n$message = $server->getRequestMessage(); \n```\n\n`$message` 为一个 `EasyWeChat\\Pay\\Message` 实例。\n\n你可以在处理完逻辑后自行创建一个响应，当然，在不同的框架里，响应写法也不一样，请自行实现。\n\n\n## 回调消息\n\n微信推送的回调消息是默认密文的，可参考[文档](https://pay.weixin.qq.com/doc/v3/merchant/4012071382)，但是 SDK 已经帮你解密好了，所以以上例子中的 `$message` 默认访问的属性都是明文的，例如：\n\n```json\n{\n    \"transaction_id\":\"1217752501201407033233368018\",\n    \"amount\":{\n        \"payer_total\":100,\n        \"total\":100,\n        \"currency\":\"CNY\",\n        \"payer_currency\":\"CNY\"\n    },\n    \"mchid\":\"1230000109\",\n    \"trade_state\":\"SUCCESS\",\n    \"bank_type\":\"CMC\",\n    \"promotion_detail\":[...],\n    \"success_time\":\"2018-06-08T10:34:56+08:00\",\n    \"payer\":{\n        \"openid\":\"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\"\n    },\n    \"out_trade_no\":\"1217752501201407033233368018\",\n    \"appid\":\"wxd678efh567hg6787\",\n    \"trade_state_desc\":\"支付成功\",\n    \"trade_type\":\"MICROPAY\",\n    \"attach\":\"自定义数据\",\n    \"scene_info\":{\n        \"device_id\":\"013467007045764\"\n    }\n}\n```\n\n所以你可以直接使用 `$message->transaction_id` 或者 `$message['transaction_id']` 来访问以上属性。\n\n#### 怎么获取密文属性呢？\n\n`$message` 对象提供了 `$message->getOriginalAttributes()` 来获取加密前的数据：\n\n```json\n{\n    \"id\": \"EV-2018022511223320873\",\n    \"create_time\": \"2015-05-20T13:29:35+08:00\",\n    \"resource_type\": \"encrypt-resource\",\n    \"event_type\": \"TRANSACTION.SUCCESS\",\n    \"summary\": \"支付成功\",\n    \"resource\": {\n        \"original_type\": \"transaction\",\n        \"algorithm\": \"AEAD_AES_256_GCM\",\n        \"ciphertext\": \"\",\n        \"associated_data\": \"\",\n        \"nonce\": \"\"\n    }\n}\n```\n\n当然我们还特别封装了用于获取事件类型的方法：\n\n```php\n$message->getEventType(); // TRANSACTION.SUCCESS\n```\n"
  },
  {
    "path": "docs/src/6.x/pay/utils.md",
    "content": "---\ntitle: 工具 | 微信支付\naside: false\n---\n\n# 工具\n\n提供各种支付需要的配置生成方法。\n\n## 配置\n\n```php\n<?php\nuse EasyWeChat\\Pay\\Application;\n\n$config = [...];\n\n$app = new Application($config);\n\n$utils = $app->getUtils();\n```\n\n> 注意\n\n## 生成支付 JS 配置\n\n有四种发起支付的方式：WeixinJSBridge, JSSDK, 小程序支付, APP\n\n### WeixinJSBridge 调起支付 API\n\n:book: [官方文档 - WeixinJSBridge 调起支付](https://pay.weixin.qq.com/doc/v3/merchant/4012791857)\n\n```php\n$appId = '商户申请的公众号对应的 appid，由微信支付生成，可在公众号后台查看';\n$signType = 'RSA'; // 默认RSA，v2要传MD5\n$config = $utils->buildBridgeConfig($prepayId, $appId, $signType); // 返回数组\n```\n\n调用示例\n\n```js\nWeixinJSBridge.invoke(\n  'getBrandWCPayRequest',\n  {\n    timeStamp: \"<?= $config['timeStamp'] ?>\", //注意 timeStamp 的格式\n    nonceStr: \"<?= $config['nonceStr'] ?>\",\n    package: \"<?= $config['package'] ?>\",\n    signType: \"<?= $config['signType'] ?>\",\n    paySign: \"<?= $config['paySign'] ?>\" // 支付签名\n  },\n  function (res) {\n    if (res.err_msg == 'get_brand_wcpay_request:ok') {\n      // 使用以上方式判断前端返回,微信团队郑重提示：\n      // res.err_msg将在用户支付成功后返回\n      // ok，但并不保证它绝对可靠。\n    }\n  }\n)\n```\n\n### JSSDK 调起支付 API\n\n:book: [官方文档 - wx.chooseWXPay 调起支付](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58)\n\n```php\n$appId = '商户申请的公众号对应的 appid，由微信支付生成，可在公众号后台查看';\n$signType = 'RSA'; // 默认RSA，v2要传MD5\n$config = $utils->buildSdkConfig($prepayId, $appId, $signType); // 返回数组\n```\n\n调用实例:\n\n```js\nwx.chooseWXPay({\n  timestamp: \"<?= $config['timestamp'] ?>\",\n  nonceStr: \"<?= $config['nonceStr'] ?>\",\n  package: \"<?= $config['package'] ?>\",\n  signType: \"<?= $config['signType'] ?>\",\n  paySign: \"<?= $config['paySign'] ?>\",\n  success: function (res) {\n    // 支付成功后的回调函数\n  }\n})\n```\n\n### 小程序调起支付 API\n\n:book: [官方文档 - 小程序调起支付 API](https://pay.weixin.qq.com/doc/v3/merchant/4012791898)\n\n```php\n$appId = '商户申请的小程序对应的appid，由微信支付生成，可在小程序后台查看';\n$signType = 'RSA'; // 默认RSA，v2要传MD5\n$config = $utils->buildMiniAppConfig($prepayId, $appId, $signType); // 返回数组\n```\n\n调用示例：\n\n```js\nwx.requestPayment({\n  timeStamp: \"<?= $config['timeStamp'] ?>\",\n  nonceStr: \"<?= $config['nonceStr'] ?>\",\n  package: \"<?= $config['package'] ?>\",\n  signType: \"<?= $config['signType'] ?>\",\n  paySign: \"<?= $config['paySign'] ?>\",\n  success: function (res) {\n    // 支付成功后的回调函数\n  }\n})\n```\n\n### APP 调起支付 API\n\n:book: [官方文档 - APP 调起支付 API](https://pay.weixin.qq.com/doc/v3/merchant/4013070351)\n\n```php\n$appId = '商户申请的公众号对应的appid，由微信支付生成，可在公众号后台查看';\n$config = $utils->buildAppConfig($prepayId, $appId); // 返回数组\n```\n\n调用示例：[官方文档 - APP 调起支付 API](https://pay.weixin.qq.com/doc/v3/merchant/4013070351)\n\n### 使用微信支付公钥加密敏感字段 <version-tag>6.17.0+</version-tag>\n\n:book: [官方文档 - 如何使用微信支付公钥加密敏感字段](https://pay.weixin.qq.com/doc/v3/merchant/4012153196)\n\n```php\n$config = [\n   'platform_certs' => [\n       // 如果是「平台证书」模式\n       //    可简写使用平台证书文件绝对路径\n       // '/path/to/wechatpay/cert.pem',\n\n       // 如果是「平台公钥」模式\n       //    使用Key/Value结构， key为平台公钥ID，value为平台公钥文件绝对路径\n       // \"{$pubKeyId}\" => '/path/to/wechatpay/pubkey.pem',\n   ],\n];\n//使用微信支付公钥加密敏感字段可传入$serial(即 $pubKeyId)，或不传默认取第一个证书\n$encrypted = $utils->encryptWithRsaPublicKey($plaintext, $serial); // 返回加密后数据\n```\n\n调用示例：[官方文档 - 如何使用微信支付公钥加密敏感字段](https://pay.weixin.qq.com/doc/v3/merchant/4013053257)\n\n# 二维码生成工具推荐\n\n> :heart: 建议由前端生成二维码\n\n确实需要用 PHP 生成二维码，那么以下这些供参考：\n\n- [endroid/QrCode](https://github.com/endroid/QrCode)\n- [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode)\n- [SimpleSoftwareIO/simple-qrcode](https://github.com/SimpleSoftwareIO/simple-qrcode) Bacon/BaconQrCode 的 Laravel 版本\n- [aferrandini/PHPQRCode](https://github.com/aferrandini/PHPQRCode)\n"
  },
  {
    "path": "docs/src/6.x/sidebar.js",
    "content": "exports = module.exports = [\n  {\n    text: '开始使用',\n    collapsible: true,\n    items: [\n      { text: '关于6.x', link: '/6.x/introduction.html' },\n      { text: '立即开始', link: '/6.x/index.html' },\n      { text: '常见问题汇总', link: '/6.x/troubleshooting.html' },\n      { text: '参与贡献', link: '/6.x/contributing.html' }\n    ]\n  },\n  {\n    text: '公众号',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/official-account/index.html' },\n      { text: '配置', link: '/6.x/official-account/config.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '服务端', link: '/6.x/official-account/server.html' },\n      { text: '消息', link: '/6.x/official-account/message.html' },\n      { text: '网页授权', link: '/6.x/oauth.html' },\n      { text: '工具', link: '/6.x/official-account/utils.html' },\n      { text: '代码示例', link: '/6.x/official-account/examples.html' }\n    ]\n  },\n  {\n    text: '微信支付',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/pay/index.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '回调通知', link: '/6.x/pay/server.html' },\n      { text: '工具', link: '/6.x/pay/utils.html' },\n      { text: '文件上传', link: '/6.x/pay/media.html' },\n      { text: '代码示例', link: '/6.x/pay/examples.html' }\n    ]\n  },\n  {\n    text: '小程序',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/mini-app/index.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '配置', link: '/6.x/mini-app/config.html' },\n      { text: '服务端', link: '/6.x/mini-app/server.html' },\n      { text: '工具', link: '/6.x/mini-app/utils.html' },\n      { text: '代码示例', link: '/6.x/mini-app/examples.html' }\n    ]\n  },\n  {\n    text: '开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/open-platform/index.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '服务端', link: '/6.x/open-platform/server.html' },\n      { text: '代码示例', link: '/6.x/open-platform/examples.html' }\n    ]\n  },\n  {\n    text: '企业微信',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/work/index.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '服务端', link: '/6.x/work/server.html' },\n      { text: '网页授权', link: '/6.x/work/oauth.html' },\n      { text: '工具', link: '/6.x/work/utils.html' },\n      { text: '代码示例', link: '/6.x/work/examples.html' }\n    ]\n  },\n  {\n    text: '企业微信开放平台',\n    collapsible: true,\n    items: [\n      { text: '入门', link: '/6.x/open-work/index.html' },\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '服务端', link: '/6.x/open-work/server.html' },\n      { text: '网页授权', link: '/6.x/open-work/oauth.html' },\n      { text: '代码示例', link: '/6.x/open-work/examples.html' }\n    ]\n  },\n  {\n    text: '通用',\n    collapsible: true,\n    items: [\n      { text: 'API 调用', link: '/6.x/client.html' },\n      { text: '网页授权', link: '/6.x/oauth.html' },\n      { text: '自定义缓存', link: '/6.x/cache.html' },\n      { text: '日志', link: '/6.x/logging.html' }\n    ]\n  }\n]\n"
  },
  {
    "path": "docs/src/6.x/troubleshooting.md",
    "content": "# 疑难解答\n\n在微信公众平台开发的道路上，遍布着各种大大小小的坑，有的人掉坑里，几经折腾又爬出来了，然后拍拍屁股走人。然而坑还在那里，还会继续有后来人掉进去……\n\n这，是我们不愿看到的。\n\n所以在这里，我们将陆续将微信开发中可能遇到的各种疑难问题进行汇总，并给出对应的解决办法。一般情况下，这些问题都可以对号入座，轻松地解决。但也不排除特殊情况，这时候你遇到的问题与文中某一个症状一致，但文中所给的解决方案并不奏效，这种情况下就需要发挥你自己的智慧，去……折腾了……\n\n我们期待这一版块为各位的开发带来便利，同时也希望各位本着开源、分享的精神对其进行补充和完善，将各种坑一一填小、填平，让微信开发变得不那么痛苦，甚至，变成一件快乐的事……\n\n## 时区不对\n\n使用命令 `date` 可以在服务器上查看当前时间，如果发现时区不对则需要修改时区：[Setting The Correct Timezone In CentOS And Ubuntu Servers With NTP](https://www.liberiangeek.net/2013/02/setting-the-correct-timezone-in-centos-and-ubuntu-servers-with-ntp/)\n\n## curl: (60) SSL certificate problem: unable to get local issuer certificate\n\n这是 SSL 证书问题所致，在使用 SDK 调用微信支付等相关的操作时可能会遇到报 “SSL certificate problem: unable to get local issuer certificate” 的错误。\n\n微信公众平台提供的文档中建议对部分较敏感的操作接口使用 https 协议进行访问，例如微信支付和红包等接口中涉及到操作商户资金的一些操作。\nwechat SDK 遵循了官方建议，所以在调用这些接口时，除了按照官方文档设置操作证书文件外，还需要保证服务器正确安装了 CA 证书。\n\n1. 下载 CA 证书\n\n你可以从 http://curl.haxx.se/ca/cacert.pem 下载，或者使用操作系统提供的包管理器比如`brew install ca-certificates`安装或者更新CA根证书。\n\n2. 在 `php.ini` 中配置 CA 证书\n\n只需要将上面下载好的 CA 证书放置到您的服务器上某个位置，然后修改 `php.ini` 的 `curl.cainfo` 为该路径（**绝对路径！**），重启 `php-fpm` 服务即可。\n\n```ini\ncurl.cainfo = /path/to/downloaded/cacert.pem\n```\n\n> 注意证书文件**路径为绝对路径**！以自己实际情况为准。\n\n其它修改 HTTP 类源文件的方式是不允许的。\n\n## cURL error 56: SSLRead() return error -9806\n\n目前在 OSX 下，发现使用 HomeBrew 装的 PHP 7.0 有这个问题，解决方案是重新 brew 安装 PHP：\n\n```shell\n$ brew install homebrew/php/php70 --with-homebrew-openssl --with-homebrew-curl --without-snmp -vvv\n```\n\n验证：\n\n```shell\n$ php -i | grep 'OpenSSL support'\n\nOpenSSL support => enabled\nOpenSSL support => enabled\n```\n\n## 支付失败！当前页面的 URL 未注册\n\n这是由于微信支付授权目录未正确配置引起的。此时开发者应该登录微信公众平台，进入**【微信支付】->【开发设置】**进行设置。\n\n1. 公众号可添加 3 个支付授权目录，满足不同应用使用同一个公众号进行支付的业务需求。\n\n2. 正确的**【支付授权目录】**应以 `http://` 或 `https://` 开头，并以正斜杠 `/` 结尾，授权目录所包含的域名**必须经过 ICP 备案**。\n\n3. 支付授权目录需**细化至二级或三级目录**。\n\n4. 所有**实际调起微信支付请求的页面都必须要所配置的支付授权目录之下**。\n\n5. 在开发过程中，也可以使用测试授权目录进行开发测试，此时还**应该将参与测试的个人微信号添加到测试白名单中**，否则将出现对应的错误提示……\n\n> 配置前请先理解**页面**、**目录**、**URL **以及**域名**等几个基本概念，并对自己所使用的框架的路由机制有一个大致了解。这样你才会知道自己正在配置的参数是个啥玩意儿，有什么卵用…… :smile:\n\n## redirect_url 参数错误\n\n这是由于程序使用了**网页授权**而公众号没有正确配置**【网页授权域名】**所致。此时你需要登录[微信公众平台](https://mp.weixin.qq.com/)，在【开发】->【接口权限】页面找到**网页授权获取用户基本信息**进行配置并保存。\n\n1. 网页授权域名应该为通过 ICP 备案的有效域名，否则保存时无法通过安全监测。\n\n2. 网页授权域名即程序完成授权获得授权 code 后跳转到的页面的域名，一般情况下为你的业务域名。\n\n3. 网页授权域名配置成功后会立即生效。\n\n4. 公众号的网页授权域名只可配置一个，请合理规划你的业务，否则你会发现……授权域名不够用哈。\n\n## [JSAPI] config: invalid url domain\n\n在使用 JS-SDK 进行开发时，每个页面都需要调用 wx.config() 方法配置 JSPAI 参数。如果没有正确配置 **JSAPI 安全域名**并且开启了调试模式，此时就报此错误。遇到这个问题时，开发者需要登录微信公众平台，进入【公众号设置】->【功能设置】页面，将项目所使用的域名添加至 **【JSAPI 安全域名】**列表中。\n\n1. 一个公众号同时最多可绑定**三个**安全域名，并且这些域名必须为通过 **ICP 备案**的**一级或一级以上**的有效域名。\n\n2. JSAPI 安全域名每个月**限修改三次**，修改任何一个都算，所以，请谨慎操作。\n\n3. 如果需要使用 JSAPI 调起支付功能，则支付目录必须也在所配置的**安全域名之下**，并且需要将支付目录添加至**支付授权目录**。\n\n## token 验证失败、向公众号发送消息无任何反应\n\n相信对接公众号一般是微信开发者进行开发过程中最先进行的工作，而在这看似简单的配置操作中，也可能会掉坑里。\n最常见的两种情况就如下：\n\n1. 确认你 “**启用**” 了开发模式， token 验证通过不代表启用，保存后也不代表启用。看到红色 “**停用**” 才真正的是启用了。\n\n2. 配置好 URL(服务器地址)以及 Token(令牌)后，点击保存时提示**token 验证失败**，出现这种情况的原因有多种，其中之一便是网络不稳定，所以**可尝试多次保存**，若始终无法通过再排查其它可能因素。\n\n3. 配置保存成功之后，向公众号发送消息无任何反应，自己的消息处理程序也没有被调用的记录（无对应日志）。这种情况下如果你尝试**反复停用和启用服务器配置**，可能突然间惊奇地了现，问题莫名其妙的解决了。\n\n4. 使用在线调试工具的消息接口，[https://mp.weixin.qq.com/debug/](https://mp.weixin.qq.com/debug/)，只要返回绿色的“**请求成功**”，就代表你的代码没有问题，请**重复上面第 3 项**再测试。\n\n5. **如果你在用什么本地开发工具，或者什么 ngrok 代理到本机这样的开发方式，那么失败就很正常了，微信服务器到你机器的网络延迟太大（还是用服务器开发吧）。**\n\n> 请开发者理解服务器 TOKEN 验证原理（官方文档有说明）并谨记服务器验证时使用 GET 方式访问，而公众平台向你的服务器发送消息/数据则使用 POST 方式，所以服务器验证成功之后，在某些启用了 CSRF 验证的框架里，接收消息时可能还会遇到 CSRF 相关的问题，请根据自己项目实际情况进行排查。\n> 另外有的朋友的 Laravel 里使用了 laravel-debugbar，这个组件的原理是在页面输出时在后面添加 HTML 来实现的，所以它会改变我们返回给微信的内容，此时要么卸载，要么禁用掉它。\n\n## Maximum function nesting level of '100' reached, aborting!\n\n在使用了 Xdebug 的环境下可能出现这个问题。这是由于 Xdebug 限制函数嵌套的最大层级数（默认为 100），当嵌套次数达到该值便会触发 Xdebug 跳出嵌套并报此错误。\n\n为避免这个问题，**可以将 Xdebug 的 max_nesting_level 参数适当设置大一些**，通常设置为 200 就可以了（当然可根据自己实际情况设置为更大的值）。\n\n如下，修改 php.ini 配置文件后，重启 Apache 或 php-fpm 服务即可。\n\n```ini\nxdebug.max_nesting_level=200\n```\n\n## 扫码支付 获取商户订单信息超时或商户返回 httpcode 非 200!\n\n1.确定签名正确,使用 SDK 基本上不会出什么问题 2.微信调用扫码支付回调链接,使用 POST 方式,确定服务器回调方法是否取消 csrf 验证\n\n## Request access_token fail:{\"errcode\":61023,\"errmsg\":\"refresh_token is invalid hint: [zDNUIA07582974]\"}！\n\n在用户授权时会获得该 authorizer_refresh_token 刷新令牌，而当缓存或数据库存储的该 authorizer_refresh_token 刷新令牌丢失后，可能会出现该问题，微信文档中说明\n\n1.接口调用凭据刷新令牌（在授权的公众号具备 API 权限时，才有此返回值），刷新令牌主要用于第三方平台获取和刷新已授权用户的 access_token，只会在授权时刻提供，请妥善保存。\n\n2.一旦丢失，只能让用户重新授权，才能再次拿到新的刷新令牌(https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=)。\n\n3.为避免该问题，请将存储该刷新令牌的缓存有效期设置为 0(永久存储)，并尽量不要去将该缓存或数据库清空。\n\n如下：以 redis 为例。\n\n```php\n'expire'     => 0,\n```\n"
  },
  {
    "path": "docs/src/6.x/work/examples.md",
    "content": "---\naside: false\ntitle: 企业微信使用代码示例\n---\n\n# 示例\n\n> 👏🏻 欢迎点击本页下方 \"帮助我们改善此页面！\" 链接参与贡献更多的使用示例！\n\n<details open>\n    <summary>被动回复一个图片信息</summary>\n\n> [官方文档](https://developer.work.weixin.qq.com/document/path/90241)\n\n```php\n$server->with(function ($message) {\n    return [\n        'MsgType' => 'image',\n        'Image' => [\n            'MediaId' => $message['MediaId'],\n        ],\n    ]);\n};\n```\n\n`$server` 见[这里](server)，`media_id` 需提前由 [企业微信>素材管理](https://developer.work.weixin.qq.com/document/path/91054) 接口产生。\n\n</details>\n\n<!--\n<details>\n    <summary>标题</summary>\n内容\n</details>\n-->\n"
  },
  {
    "path": "docs/src/6.x/work/index.md",
    "content": "# 企业微信\n\n请仔细阅读并理解：[企业微信 API - 企业内部开发](https://open.work.weixin.qq.com/api/doc/90000/90135/90664)\n\n## 实例化\n\n```php\n<?php\nuse EasyWeChat\\Work\\Application;\n\n$config = [\n  'corp_id' => 'wx3cf0f39249eb0exx',\n  'secret' => 'f1c242f4f28f735d4687abb469072axx',\n  'token' => 'easywechat',\n  'aes_key' => '35d4687abb469072a29f1c242xxxxxx',\n  // 记得配置suite_id，不然suite_ticket不能自动存储\n  'suite_id' => 'ww9f1388bf664xxxxx',\n  'suite_secret' => 'reuXvCX_5FhDVm_sOslJEHRVxxxxxxx'\n\n  /**\n   * 接口请求相关配置，超时时间等，具体可用参数请参考：\n   * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php\n   */\n  'http' => [\n      'throw'  => true, // 状态码非 200、300 时是否抛出异常，默认为开启\n      'timeout' => 5.0,\n      // 'base_uri' => 'https://qyapi.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri\n\n      'retry' => true, // 使用默认重试配置\n      //  'retry' => [\n      //      // 仅以下状态码重试\n      //      'status_codes' => [429, 500]\n      //       // 最大重试次数\n      //      'max_retries' => 3,\n      //      // 请求间隔 (毫秒)\n      //      'delay' => 1000,\n      //      // 如果设置，每次重试的等待时间都会增加这个系数\n      //      // (例如. 首次:1000ms; 第二次: 3 * 1000ms; etc.)\n      //      'multiplier' => 3\n      //  ],\n  ],\n];\n\n$app = new Application($config);\n```\n\n## API\n\nApplication 就是一个工厂类，所有的模块都是从 `$app` 中访问，并且几乎都提供了协议和 setter 可自定义修改。\n\n### 服务端\n\n服务端模块封装了服务端相关的便捷操作，隐藏了部分复杂的细节，基于中间件模式可以更方便的处理消息推送和服务端验证。\n\n```php\n$app->getServer();\n```\n\n:book: 更多说明请参阅：[服务端使用文档](server.md)\n\n### API Client\n\n封装了多种模式的 API 调用类，你可以选择自己喜欢的方式调用开放平台任意 API，默认自动处理了 access_token 相关的逻辑。\n\n```php\n$app->getClient();\n```\n\n:book: 更多说明请参阅：[API 调用](../client.md)\n\n### 配置\n\n```php\n$config = $app->getConfig();\n```\n\n你可以轻松使用 `$config->get($key, $default)` 读取配置，或使用 `$config->set($key, $value)` 在调用前修改配置项。\n\n### getAccessToken\n\naccess_token 是 API 调用的必备条件，如果你想获取它的值，你可以通过以下方式拿到当前的 access_token：\n\n```php\n$accessToken = $app->getAccessToken();\n$accessToken->getToken(); // string\n```\n\n当然你也可以使用自己的 getAccessToken 类：\n\n```php\n$accessToken = new MyCustomAccessToken();\n$app->getAccessToken($accessToken)\n```\n\n### 企业账户\n\n企业账号类，提供一系列 API 获取企业的基本信息：\n\n```php\n$account = $app->getAccount();\n\n$account->getCorpId();\n$account->getSecret();\n$account->getToken();\n$account->getAesKey();\n```\n\n## 企业网页授权\n\n> [点此查看官方文档](https://open.work.weixin.qq.com/api/doc/90000/90135/91020)\n\n```php\n$oauth = $app->getOAuth();\n```\n\n:book: 详情请参考：[网页授权](./oauth.md)\n"
  },
  {
    "path": "docs/src/6.x/work/oauth.md",
    "content": "# OAuth\n\n> 此文档为企业微信内部应用开发的网页授权,非第三方应用网页授权\n\n[企业微信官方文档](https://work.weixin.qq.com/api/doc#90000/90135/91020)\n\n创建实例：\n\n```php\nuse EasyWeChat\\Work\\Application;\n\n$config = [\n    'corp_id' => 'xxxxxxxxxxxxxxxxx',\n    'secret'   => 'xxxxxxxxxx', // 应用的 secret\n];\n\n$app = new Application($config);\n```\n\n## 跳转授权\n\n```php\n// $callbackUrl 为授权回调地址\n$callbackUrl = 'https://xxx.xxx'; // 需设置可信域名\n\n// 返回授权跳转链接\n$redirectUrl = $app->getOAuth()->redirect($callbackUrl);\n```\n\n## 获取授权用户信息\n\n在回调页面中，你可以使用以下方式获取授权者信息：\n\n```php\n$code = \"回调URL中的code\";\n$user = $app->getOAuth()->detailed()->userFromCode($code);\n\n// 获取用户信息\n$user->getId(); // 对应企业微信英文名（userid）\n$user->getRaw(); // 获取企业微信接口返回的原始信息\n```\n\n:book: OAuth 详情请参考：[网页授权](../oauth.md)\n\n获取用户其他信息需调用通讯录接口，参考：[企业微信通讯录 API](https://github.com/EasyWeChat/docs/blob/master/wework/contacts.md)\n\n## 参考阅读\n\n- 本模块基于 [overtrue/socialite](https://github.com/overtrue/socialite/) 实现，更多的使用请阅读该扩展包文档。\n- state 参数的使用: [overtrue/socialite/#state](https://github.com/overtrue/socialite/#state)\n"
  },
  {
    "path": "docs/src/6.x/work/server.md",
    "content": "# 服务端\n\n企业微信服务端推送和公众号一样，请参考：[公众号：服务端](../official-account/server.md)\n\n## 第三方平台推送事件\n\n企业微信数据推送的有以下事件：\n\n- 通讯录变更（Event） `change_contact`\n  - ChangeType\n    - 成员变更\n      - 新增成员 `create_user`\n      - 更新成员 `update_user`\n      - 删除成员 `delete_user`\n    - 部门变更\n      - 新增部门 `create_party`\n      - 更新部门 `update_party`\n      - 删除部门 `delete_party`\n    - 标签变更\n      - 成员标签变更 `update_tag`\n- 批量任务执行完成 `batch_job_result`\n\n## 内置消息处理器\n\n### 处理通讯录变更事件（包括成员变更、部门变更、成员标签变更）\n\n```php\n$server->handleContactChanged(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 处理任务执行完成事件\n\n```php\n$server->handleBatchJobsFinished(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 成员变更事件\n\n```php\n// 新增成员\n$server->handleUserCreated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 更新成员\n$server->handleUserUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 删除成员\n$server->handleUserDeleted(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 部门变更事件\n\n```php\n// 新增部门\n$server->handlePartyCreated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 更新部门\n$server->handlePartyUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n\n// 删除部门\n$server->handlePartyDeleted(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 成员标签变更事件\n\n```php\n$server->handleUserTagUpdated(function($message, \\Closure $next) {\n    // ...\n    return $next($message);\n});\n```\n\n### 智能机器人事件\n因智能机器人消息变更为JSON格式，需要在获取 `server` 对象时指定消息格式为json：\n```php\n// 指定消息格式 JSON\n$server = $app->getServer(messageType: 'json');\n\n// 获取解密后的机器人消息\n$message = $server->getDecryptedMessage();\n\n// 回复消息\n$server->with(function($message, \\Closure $next) {\n    return [\n        'msgtype' => 'stream',\n        'stream' => [\n            'id' => 'id00001',\n            'finish' => true,\n            'content' => '信息已收到',\n        ],\n    ];\n});\n```\n\n回复消息具体格式请参考官方文档：[企业微信智能机器人文档](https://developer.work.weixin.qq.com/document/path/101039)\n\n## 其它事件处理\n\n以上便捷方法都只处理了特定事件，其它状态，可以通过自定义事件处理中间件的形式处理：\n\n```php\n$server->with(function($message, \\Closure $next) {\n    // $message->event_type 事件类型\n    return $next($message);\n});\n```\n\n## 自助处理推送消息\n\n你可以通过下面的方式获取来自微信服务器的推送消息：\n\n```php\n$message = $server->getRequestMessage(); // 原始消息\n```\n\n你也可以获取解密后的消息 <version-tag>6.5.0+</version-tag>\n\n```php\n$message = $server->getDecryptedMessage();\n```\n\n`$message` 为一个 `EasyWeChat\\Work\\Message` 实例。\n\n你可以在处理完逻辑后自行创建一个响应，当然，在不同的框架里，响应写法也不一样，请自行实现。\n"
  },
  {
    "path": "docs/src/6.x/work/utils.md",
    "content": "# 工具<version-tag>6.7.1+</version-tag>\n\n提供企业微信网页开发 JS-SDK 相关方法\n\n## 配置\n\n```php\n<?php\nuse EasyWeChat\\Work\\Application;\n\n$config = [...];\n\n$app = new Application($config);\n\n$utils = $app->getUtils();\n```\n\n## 生成 config 接口配置\n\n:book: [官方文档 - config 接口配置 说明文档](https://open.work.weixin.qq.com/api/doc/90001/90144/90547)\n\n```php\n$config = $utils->buildJsSdkConfig(\n    string $url,\n    array $jsApiList,\n    array $openTagList = [],\n    bool $debug = false,\n    bool $beta = true\n);\n\n// print\n[\n    'jsApiList' => ['api1','api2'],\n    'openTagList' => ['openTag1','openTag2'],\n    'debug' => false,\n    'beta' => true,\n    'url' => 'https://easywechat.com/',\n    'nonceStr' => 'mock-nonce',\n    'timestamp' => 1601234567,\n    'appId' => 'mock-appid',\n    'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767'\n];\n```\n\n## 生成 agentConfig 接口配置\n\n:book: [官方文档 - agentConfig 接口配置 说明文档](https://open.work.weixin.qq.com/api/doc/90001/90144/94325)\n\n```php\n$config = $utils->buildJsSdkAgentConfig(\n    int $agentId,\n    string $url,\n    array $jsApiList,\n    array $openTagList = [],\n    bool $debug = false\n);\n\n// print\n[\n    'jsApiList' => ['api1','api2'],\n    'openTagList' => ['openTag1','openTag2'],\n    'debug' => false,\n    'url' => 'https://easywechat.com/',\n    'nonceStr' => 'mock-nonce',\n    'timestamp' => 1601234567,\n    'corpid' => 'mock-corpid',\n    'agentid' => 100001,\n    'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767'\n];\n```\n"
  },
  {
    "path": "docs/src/index.md",
    "content": "---\nlayout: page\npageClass: im-home\ntitle: EasyWeChat - 一个 PHP 微信开发 SDK\n---\n\n<script lang=\"ts\" setup>\nimport versions from '@theme/../versions'\nimport {withBase} from 'vitepress'\n\nlet latestVersion = versions[0]\n\n</script>\n\n<section class=\"text-center flex flex-col flex-1 px-4 md:px-12\">\n  <div class=\"flex-1 flex flex-col items-center justify-center space-y-8\">\n    <div class=\"flex justify-center\">\n      <h1\n        class=\"tagline md:py-12 text-center text-4xl md:text-7xl xl:text-8xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-green-400 dark:from-green-400 dark:to-yellow-500\"\n      >\n        微信开发，可以更简单\n      </h1>\n    </div>\n    <p class=\"w-56 md:w-auto py-4 md:py-3 md:text-2xl\">\n      每一个细节，都经过精心打磨，只为了提供更好的开发体验。\n    </p>\n    <div\n      class=\"space-y-2 md:space-y-0 xl:flex justify-center\"\n    >\n      <div\n        class=\"hidden xl:block mr-4 items-center space-around text-gray-700 bg-gray-100 border-0 py-2 px-6 focus:outline-none hover:bg-gray-200 rounded lg:text-lg\"\n      >\n        <code\n          class=\"bash-composer text-gray-700 bg-transparent flex items-center\"\n        >\n          composer require w7corp/easywechat\n        </code>\n      </div>\n      <a\n        :href=\"withBase(`/${latestVersion}/`)\"\n        class=\"inline-flex items-center space-around text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded text-lg\"\n      >\n        <span>立即开始</span>\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          class=\"h-6 w-6 ml-2\"\n          fill=\"none\"\n          viewBox=\"0 0 24 24\"\n          stroke=\"currentColor\"\n        >\n          <path\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            stroke-width=\"2\"\n            d=\"M17 8l4 4m0 0l-4 4m4-4H3\"\n          />\n        </svg>\n      </a>\n    </div>\n  </div>\n  <div\n    class=\"py-4 md:py-12 md:flex items-center md:space-x-6 xl:space-x-12 xl:flex justify-center\"\n  >\n    <a\n      href=\"https://github.com/overtrue\"\n      target=\"_blank\"\n      class=\"flex items-center group hover:bg-gray-200 dark:hover:bg-indigo-500 xl:w-64 px-6 py-2 rounded-lg justify-center flex-col md:flex-row\"\n    >\n      <div\n        class=\"h-12 w-12 rounded-full border-2 border-blue-200 group-hover:border-blue-400 dark:group-hover:border-gray-300 p-0.5\"\n      >\n        <img src=\"/overtrue.jpg\" alt=\"overtrue\" class=\"rounded-full\"/>\n      </div>\n      <div class=\"px-2 md:text-left text-gray-400 dark:group-hover:text-gray-300\">\n        <div class=\"text-gray-700 dark:text-gray-300 dark:group-hover:text-white font-semibold\">overtrue</div>\n        <small>核心开发者</small>\n      </div>\n    </a>\n    <a\n      href=\"https://www.w7.cc/\"\n      target=\"_blank\"\n      class=\"flex items-center group hover:bg-gray-200 dark:hover:bg-indigo-500 xl:w-64 px-6 py-2 rounded-lg justify-center flex-col md:flex-row\"\n    >\n      <div\n        class=\"h-12 w-12 rounded-full border-2 border-blue-200 group-hover:border-blue-400 dark:group-hover:border-gray-300 p-0.5\"\n      >\n        <img src=\"/w7team.jpg\" alt=\"overtrue\" class=\"rounded-full\"/>\n      </div>\n      <div class=\"px-2 md:text-left text-gray-400 dark:group-hover:text-gray-300\">\n        <div class=\"text-gray-700  dark:text-gray-300 dark:group-hover:text-white font-semibold\">微擎</div>\n        <small>开源 Saas 平台提供商</small>\n      </div>\n    </a>\n  </div>\n</section>\n"
  },
  {
    "path": "docs/tailwind.config.js",
    "content": "module.exports = {\n  darkMode: 'class',\n  theme: {\n    extend: {}\n  },\n  variants: {},\n  plugins: [],\n  content: [\n    './src/**/*.md',\n    './.vitepress/theme/components/*.vue',\n    './.vitepress/theme/styles/*.scss',\n  ],\n}\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"target\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"allowJs\": true,\n    \"strict\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"/@theme/*\": [\".vitepress/theme/*\"]\n    }\n  },\n  \"include\": [\"env.d.ts\", \"src/**/*\", \".vitepress/**/*\"]\n}\n"
  },
  {
    "path": "phpstan.neon",
    "content": "parameters:\n    level: 9\n    paths:\n        - src\n    inferPrivatePropertyTypeFromConstructor: true\n    ignoreErrors:\n        -\n            identifier: missingType.iterableValue\n        -\n            message: '#Property EasyWeChat\\\\Kernel\\\\Config::\\$items \\(array<string, mixed>\\) does not accept array#'\n            path: src/Kernel/Config.php\n        -\n            message: '#\\$client .*? does not accept#'\n            path: src/Kernel/HttpClient/AccessTokenAwareClient.php\n        -\n            message: '#ServerResponse::with.*? should return static\\(EasyWeChat\\\\Kernel\\\\ServerResponse\\) but returns Psr\\\\Http\\\\Message\\\\ResponseInterface#'\n            path: src/Kernel/ServerResponse.php\n        -\n            message: '#Parameter \\#1 \\$object of function spl_object_hash expects object, callable given#'\n            path: src/Kernel/Traits/InteractWithHandlers.php\n        -\n            message: '#Call to function is_callable\\(\\) with callable\\(\\): mixed will always evaluate to true#'\n            path: src/Kernel/Traits/InteractWithHandlers.php\n        -\n            message: '#Parameter \\$stable of class EasyWeChat\\\\MiniApp\\\\AccessToken constructor expects bool\\|null, mixed given#'\n            path: src/MiniApp/Application.php\n        -\n            message: '#Parameter \\#1 \\$options of static method EasyWeChat\\\\Kernel\\\\HttpClient\\\\RequestUtil::mergeDefaultRetryOptions\\(\\) expects array<string, mixed>#'\n            path: src/MiniApp/Application.php\n        -\n            message: '#Method EasyWeChat\\\\MiniApp\\\\Decryptor::decrypt\\(\\) should return array<string, mixed> but returns array#'\n            path: src/MiniApp/Decryptor.php\n        -\n            message: '#Parameter \\$stable of class EasyWeChat\\\\OfficialAccount\\\\(AccessToken|JsApiTicket) constructor expects bool\\|null, mixed given#'\n            path: src/OfficialAccount/Application.php\n        -\n            message: '#Parameter \\#1 \\$options of static method EasyWeChat\\\\Kernel\\\\HttpClient\\\\RequestUtil::mergeDefaultRetryOptions\\(\\) expects array<string, mixed>#'\n            path: src/OfficialAccount/Application.php\n        -\n            message: '#Parameter \\#1 \\$scopes of method Overtrue\\\\Socialite\\\\Providers\\\\Base::scopes\\(\\) expects array<string>#'\n            path: src/OfficialAccount/Application.php\n        -\n            message: '#Parameter \\#1 \\$scopes of method Overtrue\\\\Socialite\\\\Providers\\\\Base::scopes\\(\\) expects array<string>#'\n            path: src/OpenPlatform/Application.php\n        -\n            message: '#Parameter \\#1 \\$scopes of method Overtrue\\\\Socialite\\\\Providers\\\\Base::scopes\\(\\) expects array<string>#'\n            path: src/OpenWork/Application.php\n        -\n            message: '#Parameter \\#3 \\$defaultOptions of class EasyWeChat\\\\Pay\\\\Client constructor expects array<string, mixed>#'\n            path: src/Pay/Application.php\n        -\n            message: '#Property .*?\\$client \\(Symfony\\\\Contracts\\\\HttpClient\\\\HttpClientInterface\\) does not accept Mockery\\\\Mock\\|Symfony\\\\Contracts\\\\HttpClient\\\\HttpClientInterface#'\n            path: src/Pay/Client.php\n        -\n            message: '#Method EasyWeChat\\\\Pay\\\\Client::createMockClient\\(\\) should return Mockery\\\\Mock\\|Symfony\\\\Contracts\\\\HttpClient\\\\HttpClientInterface but returns Mockery\\\\LegacyMockInterface#'\n            path: src/Pay/Client.php\n        -\n            message: '#Parameter \\#1 \\$scopes of method Overtrue\\\\Socialite\\\\Providers\\\\Base::scopes\\(\\) expects array<string>#'\n            path: src/Work/Application.php\n        -\n            message: '#Trait .*? is used zero times and is not analysed#'\n            path: src/Kernel/Traits/DecryptJsonMessage.php\n        -\n            message: '#Trait .*? is used zero times and is not analysed#'\n            path: src/Kernel/Traits/DecryptXmlMessage.php\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         bootstrap=\"tests/bootstrap.php\"\n         colors=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\">\n    <testsuites>\n        <testsuite name=\"Application Test Suite\">\n            <directory>./tests/</directory>\n        </testsuite>\n    </testsuites>\n    <coverage processUncoveredFiles=\"true\">\n        <include>\n            <directory suffix=\".php\">src/</directory>\n            <directory suffix=\"ServiceProvider.php\">src/</directory>\n            <directory suffix=\"Exception.php\">src/</directory>\n            <directory suffix=\"Helpers.php\">src/Kernel/Support</directory>\n            <directory>src/Encryption</directory>\n            <directory>src/Support</directory>\n        </include>\n    </coverage>\n    <php>\n        <const name=\"PHPUNIT_RUNNING\" value=\"true\" />\n    </php>\n</phpunit>\n"
  },
  {
    "path": "pint.json",
    "content": "{\n    \"rules\": {\n        \"single_line_empty_body\": false\n    }\n}"
  },
  {
    "path": "src/Kernel/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel;\n\nuse ArrayAccess;\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Support\\Arr;\n\nuse function strval;\n\n/**\n * @implements ArrayAccess<mixed, mixed>\n */\nclass Config implements ArrayAccess, ConfigInterface\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [];\n\n    /**\n     * @param  array<string, mixed>  $items\n     *\n     * @throws InvalidArgumentException\n     */\n    public function __construct(\n        protected array $items = [],\n    ) {\n        $this->checkMissingKeys();\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function has(string $key): bool\n    {\n        return Arr::has($this->items, $key);\n    }\n\n    /**\n     * @param  array<string>|string  $key\n     */\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function get(array|string $key, mixed $default = null): mixed\n    {\n        if (is_array($key)) {\n            return $this->getMany($key);\n        }\n\n        return Arr::get($this->items, $key, $default);\n    }\n\n    /**\n     * @param  array<string>  $keys\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function getMany(array $keys): array\n    {\n        $config = [];\n\n        foreach ($keys as $key => $default) {\n            if (is_numeric($key)) {\n                [$key, $default] = [$default, null];\n            }\n\n            $config[$key] = Arr::get($this->items, $key, $default);\n        }\n\n        return $config;\n    }\n\n    public function set(string $key, mixed $value = null): void\n    {\n        Arr::set($this->items, $key, $value);\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    public function all(): array\n    {\n        return $this->items;\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function offsetExists(mixed $offset): bool\n    {\n        return $this->has(strval($offset));\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function offsetGet(mixed $offset): mixed\n    {\n        return $this->get(strval($offset));\n    }\n\n    public function offsetSet(mixed $offset, mixed $value): void\n    {\n        $this->set(strval($offset), $value);\n    }\n\n    public function offsetUnset(mixed $offset): void\n    {\n        $this->set(strval($offset), null);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function checkMissingKeys(): bool\n    {\n        if (empty($this->requiredKeys)) {\n            return true;\n        }\n\n        $missingKeys = [];\n\n        foreach ($this->requiredKeys as $key) {\n            if (! $this->has($key)) {\n                $missingKeys[] = $key;\n            }\n        }\n\n        if (! empty($missingKeys)) {\n            throw new InvalidArgumentException(sprintf(\"\\\"%s\\\" cannot be empty.\\r\\n\", implode(',', $missingKeys)));\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/AccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface AccessToken\n{\n    public function getToken(): string;\n\n    /**\n     * @return array<string,string>\n     */\n    public function toQuery(): array;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/AccessTokenAwareHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface AccessTokenAwareHttpClient extends HttpClientInterface\n{\n    public function withAccessToken(AccessTokenInterface $accessToken): static;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/Aes.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface Aes\n{\n    public static function encrypt(string $plaintext, string $key, ?string $iv = null): string;\n\n    public static function decrypt(string $ciphertext, string $key, ?string $iv = null): string;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/Arrayable.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface Arrayable\n{\n    /**\n     * @return array<int|string, mixed>\n     */\n    public function toArray(): array;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\nuse ArrayAccess;\n\n/**\n * @extends ArrayAccess<string, mixed>\n */\ninterface Config extends ArrayAccess\n{\n    /**\n     * @return array<string,mixed>\n     */\n    public function all(): array;\n\n    public function has(string $key): bool;\n\n    public function set(string $key, mixed $value = null): void;\n\n    /**\n     * @param  array<string>|string  $key\n     */\n    public function get(array|string $key, mixed $default = null): mixed;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/JsApiTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface JsApiTicket\n{\n    public function getTicket(): string;\n\n    /**\n     * @return array<string,mixed>\n     */\n    public function configSignature(string $url, string $nonce, int $timestamp): array;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/Jsonable.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface Jsonable\n{\n    public function toJson(): string|false;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/RefreshableAccessToken.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface RefreshableAccessToken extends AccessToken\n{\n    public function refresh(): string;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/RefreshableJsApiTicket.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\ninterface RefreshableJsApiTicket extends JsApiTicket\n{\n    public function refreshTicket(): string;\n}\n"
  },
  {
    "path": "src/Kernel/Contracts/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Contracts;\n\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * @method mixed withDefaultSuiteTicketHandler(callable $handler) used in \\EasyWeChat\\OpenWork\\Server\n */\ninterface Server\n{\n    public function serve(): ResponseInterface;\n}\n"
  },
  {
    "path": "src/Kernel/Encryptor.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel;\n\nuse const OPENSSL_NO_PADDING;\nuse const SORT_STRING;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\Support\\Pkcs7;\nuse EasyWeChat\\Kernel\\Support\\Str;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse Exception;\nuse Throwable;\n\nuse function base64_decode;\nuse function base64_encode;\nuse function implode;\nuse function openssl_decrypt;\nuse function openssl_encrypt;\nuse function pack;\nuse function random_bytes;\nuse function sha1;\nuse function sort;\nuse function strlen;\nuse function substr;\nuse function time;\nuse function trim;\nuse function unpack;\n\n/**\n * @link https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Message_encryption_and_decryption_instructions.html\n * @link https://developer.work.weixin.qq.com/document/path/96211\n */\nclass Encryptor\n{\n    public const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed\n\n    public const ERROR_PARSE_XML = -40002; // Parse XML failed\n\n    public const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed\n\n    public const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey\n\n    public const ERROR_INVALID_APP_ID = -40005; // Check AppID failed\n\n    public const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed\n\n    public const ERROR_DECRYPT_AES = -40007; // AES decryption failed\n\n    public const ERROR_INVALID_XML = -40008; // Invalid XML\n\n    public const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed\n\n    public const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed\n\n    public const ERROR_XML_BUILD = -40011; // XML build failed\n\n    public const ERROR_JSON_BUILD = -40012; // JOSN build failed\n\n    public const ILLEGAL_BUFFER = -41003; // Illegal buffer\n\n    /** AES block size in bytes */\n    private const BLOCK_SIZE = 16;\n\n    protected string $appId;\n\n    protected string $token;\n\n    protected string $aesKey;\n\n    protected ?string $receiveId = null;\n\n    public function __construct(string $appId, string $token, string $aesKey, ?string $receiveId = null)\n    {\n        $this->appId = $appId;\n        $this->token = $token;\n        $this->receiveId = $receiveId;\n        $this->aesKey = base64_decode($aesKey.'=', true) ?: '';\n    }\n\n    public function getToken(): string\n    {\n        return $this->token;\n    }\n\n    /**\n     * @throws RuntimeException\n     * @throws Exception\n     */\n    public function encrypt(string $plaintext, ?string $nonce = null, int|string|null $timestamp = null, string $messageType = 'xml'): string\n    {\n        return $messageType === 'xml' ?\n            $this->encryptAsXml($plaintext, $nonce, $timestamp) :\n            $this->encryptAsJson($plaintext, $nonce, $timestamp);\n    }\n\n    public function encryptAsXml(string $plaintext, ?string $nonce = null, int|string|null $timestamp = null): string\n    {\n        $encrypted = $this->encryptAsArray($plaintext, $nonce, $timestamp);\n\n        $response = [\n            'Encrypt' => $encrypted['ciphertext'],\n            'MsgSignature' => $encrypted['signature'],\n            'TimeStamp' => $encrypted['timestamp'],\n            'Nonce' => $encrypted['nonce'],\n        ];\n\n        return Xml::build($response);\n    }\n\n    public function encryptAsJson(string $plaintext, ?string $nonce = null, int|string|null $timestamp = null): string\n    {\n        $encrypted = $this->encryptAsArray($plaintext, $nonce, $timestamp);\n\n        $response = [\n            'encrypt' => $encrypted['ciphertext'],\n            'msgsignature' => $encrypted['signature'],\n            'timestamp' => $encrypted['timestamp'],\n            'nonce' => $encrypted['nonce'],\n        ];\n\n        $jsonStr = json_encode($response, JSON_UNESCAPED_UNICODE);\n\n        if ($jsonStr === false) {\n            throw new RuntimeException('Invalid json data.', self::ERROR_JSON_BUILD);\n        }\n\n        return $jsonStr;\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function encryptAsArray(string $plaintext, ?string $nonce = null, int|string|null $timestamp = null): array\n    {\n        try {\n            $plaintext = Pkcs7::padding(\n                random_bytes(self::BLOCK_SIZE).pack('N', strlen($plaintext)).$plaintext.$this->appId,\n                blockSize: strlen($this->aesKey)\n            );\n            $ciphertext = base64_encode(\n                openssl_encrypt(\n                    $plaintext,\n                    'aes-256-cbc',\n                    $this->aesKey,\n                    OPENSSL_NO_PADDING,\n                    iv: substr($this->aesKey, 0, self::BLOCK_SIZE)\n                ) ?: ''\n            );\n        } catch (Throwable $e) {\n            throw new RuntimeException($e->getMessage(), self::ERROR_ENCRYPT_AES);\n        }\n\n        $nonce ??= Str::random();\n        $timestamp ??= time();\n\n        return [\n            'ciphertext' => $ciphertext,\n            'signature' => $this->createSignature($this->token, $timestamp, $nonce, $ciphertext),\n            'timestamp' => $timestamp,\n            'nonce' => $nonce,\n        ];\n    }\n\n    public function createSignature(mixed ...$attributes): string\n    {\n        sort($attributes, SORT_STRING);\n\n        return sha1(implode($attributes));\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function decrypt(string $ciphertext, string $msgSignature, string $nonce, int|string $timestamp): string\n    {\n        $signature = $this->createSignature($this->token, $timestamp, $nonce, $ciphertext);\n\n        if ($signature !== $msgSignature) {\n            throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE);\n        }\n\n        $plaintext = Pkcs7::unpadding(\n            openssl_decrypt(\n                base64_decode($ciphertext, true) ?: '',\n                'aes-256-cbc',\n                $this->aesKey,\n                OPENSSL_NO_PADDING,\n                iv: substr($this->aesKey, 0, self::BLOCK_SIZE)\n            ) ?: '',\n            blockSize: strlen($this->aesKey)\n        );\n        $plaintext = substr($plaintext, self::BLOCK_SIZE);\n        $contentLength = (unpack('N', substr($plaintext, 0, 4)) ?: [])[1];\n\n        if ($this->receiveId && trim(substr($plaintext, $contentLength + 4)) !== $this->receiveId) {\n            throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID);\n        }\n\n        return substr($plaintext, 4, $contentLength);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/BadMethodCallException.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass BadMethodCallException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/BadRequestException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass BadRequestException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/BadResponseException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass BadResponseException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/DecryptException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass DecryptException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/Exception.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nuse Exception as BaseException;\n\nclass Exception extends BaseException\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/HttpException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass HttpException extends Exception\n{\n    public ?ResponseInterface $response;\n\n    /**\n     * HttpException constructor.\n     */\n    public function __construct(string $message, ?ResponseInterface $response = null, int $code = 0)\n    {\n        parent::__construct($message, $code);\n\n        $this->response = $response;\n\n        if ($response) {\n            $response->getBody()->rewind();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/InvalidArgumentException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass InvalidArgumentException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/InvalidConfigException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass InvalidConfigException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/RuntimeException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass RuntimeException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Exceptions/ServiceNotFoundException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Exceptions;\n\nclass ServiceNotFoundException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Kernel/Form/File.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Form;\n\nuse const PATHINFO_EXTENSION;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse Symfony\\Component\\Mime\\MimeTypes;\nuse Symfony\\Component\\Mime\\Part\\DataPart;\n\nuse function file_put_contents;\nuse function md5;\nuse function pathinfo;\nuse function strtolower;\nuse function sys_get_temp_dir;\nuse function tempnam;\n\nclass File extends DataPart\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public static function from(\n        string $pathOrContents,\n        ?string $filename = null,\n        ?string $contentType = null,\n        ?string $encoding = null\n    ): DataPart {\n        if (file_exists($pathOrContents)) {\n            return static::fromPath($pathOrContents, $filename, $contentType);\n        }\n\n        return static::fromContents($pathOrContents, $filename, $contentType, $encoding);\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public static function fromContents(\n        string $contents,\n        ?string $filename = null,\n        ?string $contentType = null,\n        ?string $encoding = null\n    ): DataPart {\n        if ($contentType === null) {\n            $mimeTypes = new MimeTypes;\n\n            if ($filename) {\n                $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));\n                $contentType = $mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream';\n            } else {\n                $tmp = tempnam(sys_get_temp_dir(), 'easywechat');\n                if (! $tmp) {\n                    throw new RuntimeException('Failed to create temporary file.');\n                }\n\n                file_put_contents($tmp, $contents);\n                $contentType = $mimeTypes->guessMimeType($tmp) ?? 'application/octet-stream';\n                $filename = md5($contents).'.'.($mimeTypes->getExtensions($contentType)[0] ?? null);\n            }\n        }\n\n        return new self($contents, $filename, $contentType, $encoding);\n    }\n\n    /**\n     * @throws RuntimeException\n     *\n     * @deprecated since EasyWeChat 7.0, use fromContents() instead\n     */\n    public static function withContents(\n        string $contents,\n        ?string $filename = null,\n        ?string $contentType = null,\n        ?string $encoding = null\n    ): DataPart {\n        return self::fromContents($contents, $filename, $contentType, $encoding);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Form/Form.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Form;\n\nuse Symfony\\Component\\Mime\\Part\\DataPart;\nuse Symfony\\Component\\Mime\\Part\\Multipart\\FormDataPart;\n\nclass Form\n{\n    /**\n     * @param  array<string|array|DataPart>  $fields\n     */\n    public function __construct(protected array $fields)\n    {\n    }\n\n    /**\n     * @param  array<string|array|DataPart>  $fields\n     */\n    public static function create(array $fields): Form\n    {\n        return new self($fields);\n    }\n\n    /**\n     * @return array{headers:array<string,string|string[]>,body:string}\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['headers' => 'array', 'body' => 'string'])]\n    public function toArray(): array\n    {\n        return $this->toOptions();\n    }\n\n    /**\n     * @return array{headers:array<string,string|string[]>,body:string}\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['headers' => 'array', 'body' => 'string'])]\n    public function toOptions(): array\n    {\n        $formData = new FormDataPart($this->fields);\n\n        return [\n            'headers' => $formData->getPreparedHeaders()->toArray(),\n            'body' => $formData->bodyToString(),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/AccessTokenAwareClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\AccessTokenAwareHttpClient as AccessTokenAwareHttpClientInterface;\nuse EasyWeChat\\Kernel\\Traits\\MockableHttpClient;\nuse Symfony\\Component\\HttpClient\\AsyncDecoratorTrait;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function array_merge;\n\n/**\n * Class AccessTokenAwareClient.\n *\n *\n * @method HttpClientInterface withAppIdAs(string $name = null) 自定义 app_id 参数名\n * @method HttpClientInterface withAppId(string $value = null)\n */\nclass AccessTokenAwareClient implements AccessTokenAwareHttpClientInterface\n{\n    use AsyncDecoratorTrait;\n    use HttpClientMethods;\n    use MockableHttpClient;\n    use RequestWithPresets;\n    use RetryableClient;\n\n    public function __construct(\n        ?HttpClientInterface $client = null,\n        protected ?AccessTokenInterface $accessToken = null,\n        protected ?Closure $failureJudge = null,\n        protected bool $throw = true\n    ) {\n        $this->client = $client ?? HttpClient::create();\n    }\n\n    public function withAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     */\n    public function request(string $method, string $url, array $options = []): Response\n    {\n        if ($this->accessToken) {\n            $options['query'] = array_merge((array) ($options['query'] ?? []), $this->accessToken->toQuery());\n        }\n\n        $options = RequestUtil::formatBody($this->mergeThenResetPrepends($options));\n\n        return new Response(\n            response: $this->client->request($method, ltrim($url, '/'), $options),\n            failureJudge: $this->failureJudge,\n            throw: $this->throw\n        );\n    }\n\n    /**\n     * @param  array<int, mixed>  $arguments\n     */\n    public function __call(string $name, array $arguments): mixed\n    {\n        if (\\str_starts_with($name, 'with')) {\n            return $this->handleMagicWithCall($name, $arguments[0] ?? null);\n        }\n\n        return $this->client->$name(...$arguments);\n    }\n\n    public static function createMockClient(MockHttpClient $mockHttpClient): HttpClientInterface\n    {\n        return new self($mockHttpClient);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/AccessTokenExpiredRetryStrategy.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse Symfony\\Component\\HttpClient\\Response\\AsyncContext;\nuse Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy;\nuse Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface;\n\nclass AccessTokenExpiredRetryStrategy extends GenericRetryStrategy\n{\n    protected AccessTokenInterface $accessToken;\n\n    protected ?Closure $decider = null;\n\n    public function withAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    public function decideUsing(Closure $decider): static\n    {\n        $this->decider = $decider;\n\n        return $this;\n    }\n\n    public function shouldRetry(\n        AsyncContext $context,\n        ?string $responseContent,\n        ?TransportExceptionInterface $exception\n    ): ?bool {\n        if ($responseContent && $this->decider && ($this->decider)($context, $responseContent, $exception)) {\n            if ($this->accessToken instanceof RefreshableAccessTokenInterface) {\n                return (bool) $this->accessToken->refresh();\n            }\n\n            return false;\n        }\n\n        return parent::shouldRetry($context, $responseContent, $exception);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/HttpClientMethods.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface as ResponseInterfaceAlias;\n\ntrait HttpClientMethods\n{\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws TransportExceptionInterface\n     */\n    public function get(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        return $this->request('GET', $url, RequestUtil::formatOptions($options, 'GET'));\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws TransportExceptionInterface\n     */\n    public function post(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        return $this->request('POST', $url, RequestUtil::formatOptions($options, 'POST'));\n    }\n\n    /**\n     * @throws TransportExceptionInterface\n     */\n    public function postJson(string $url, array $data = [], array $options = []): Response|ResponseInterfaceAlias\n    {\n        $options['headers']['Content-Type'] = 'application/json';\n\n        $options['json'] = $data;\n\n        return $this->request('POST', $url, RequestUtil::formatOptions($options, 'POST'));\n    }\n\n    /**\n     * @throws TransportExceptionInterface\n     */\n    public function postXml(string $url, array $data = [], array $options = []): Response|ResponseInterfaceAlias\n    {\n        $options['headers']['Content-Type'] = 'text/xml';\n\n        if (array_key_exists('xml', $data)) {\n            $data = $data['xml'];\n        }\n\n        $options['xml'] = $data;\n\n        return $this->request('POST', $url, RequestUtil::formatOptions($options, 'POST'));\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws TransportExceptionInterface\n     */\n    public function patch(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        return $this->request('PATCH', $url, RequestUtil::formatOptions($options, 'PATCH'));\n    }\n\n    /**\n     * @throws TransportExceptionInterface\n     */\n    public function patchJson(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        $options['headers']['Content-Type'] = 'application/json';\n\n        return $this->request('PATCH', $url, RequestUtil::formatOptions($options, 'PATCH'));\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws TransportExceptionInterface\n     */\n    public function put(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        return $this->request('PUT', $url, RequestUtil::formatOptions($options, 'PUT'));\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws TransportExceptionInterface\n     */\n    public function delete(string $url, array $options = []): Response|ResponseInterfaceAlias\n    {\n        return $this->request('DELETE', $url, RequestUtil::formatOptions($options, 'DELETE'));\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/RequestUtil.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse const ARRAY_FILTER_USE_KEY;\nuse const JSON_FORCE_OBJECT;\nuse const JSON_UNESCAPED_UNICODE;\n\nuse EasyWeChat\\Kernel\\Support\\UserAgent;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse InvalidArgumentException;\nuse Nyholm\\Psr7\\Factory\\Psr17Factory;\nuse Nyholm\\Psr7Server\\ServerRequestCreator;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function array_key_exists;\nuse function in_array;\nuse function is_array;\nuse function is_string;\nuse function json_encode;\n\nclass RequestUtil\n{\n    /**\n     * @param  array<string, mixed>  $options\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'status_codes' => 'array',\n        'delay' => 'int',\n        'max_delay' => 'int',\n        'max_retries' => 'int',\n        'multiplier' => 'float',\n        'jitter' => 'float',\n    ])]\n    public static function mergeDefaultRetryOptions(array $options): array\n    {\n        return \\array_merge([\n            'status_codes' => GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES,\n            'delay' => 1000,\n            'max_delay' => 0,\n            'max_retries' => 3,\n            'multiplier' => 2.0,\n            'jitter' => 0.1,\n        ], $options);\n    }\n\n    /**\n     * @param  array<string, array|mixed>  $options\n     * @return array<string, array|mixed>\n     */\n    public static function formatDefaultOptions(array $options): array\n    {\n        $defaultOptions = \\array_filter(\n            array: $options,\n            callback: fn ($key) => array_key_exists($key, HttpClientInterface::OPTIONS_DEFAULTS),\n            mode: ARRAY_FILTER_USE_KEY\n        );\n\n        /** @phpstan-ignore-next-line */\n        if (! isset($options['headers']['User-Agent']) && ! isset($options['headers']['user-agent'])) {\n            /** @phpstan-ignore-next-line */\n            $defaultOptions['headers']['User-Agent'] = UserAgent::create();\n        }\n\n        return $defaultOptions;\n    }\n\n    public static function formatOptions(array $options, string $method): array\n    {\n        if (array_key_exists('query', $options) && is_array($options['query']) && empty($options['query'])) {\n            return $options;\n        }\n\n        if (array_key_exists('body', $options)\n            || array_key_exists('json', $options)\n            || array_key_exists('xml', $options)\n        ) {\n            return $options;\n        }\n\n        $contentType = $options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null;\n        $name = in_array($method, ['GET', 'HEAD', 'DELETE']) ? 'query' : 'body';\n\n        if ($contentType === 'application/json') {\n            $name = 'json';\n        }\n\n        if ($contentType === 'text/xml') {\n            $name = 'xml';\n        }\n\n        foreach ($options as $key => $value) {\n            if (! array_key_exists($key, HttpClientInterface::OPTIONS_DEFAULTS)) {\n                $options[$name][trim($key, '\"')] = $value;\n                unset($options[$key]);\n            }\n        }\n\n        return $options;\n    }\n\n    /**\n     * @param  array{headers?:array<string, string>, xml?:mixed, body?:array|string, json?:mixed}  $options\n     * @return array{headers?:array<string, string|array<string, string>|array<string>>, xml?:array|string, body?:array|string}\n     *\n     * @throws InvalidArgumentException\n     */\n    public static function formatBody(array $options): array\n    {\n        $contentType = $options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null;\n\n        if (array_key_exists('xml', $options)) {\n            if (is_array($options['xml'])) {\n                $options['xml'] = Xml::build($options['xml']);\n            }\n\n            if (! is_string($options['xml'])) {\n                throw new InvalidArgumentException('The type of `xml` must be string or array.');\n            }\n\n            if (! $contentType) {\n                $options['headers']['Content-Type'] = 'text/xml';\n            }\n\n            $options['body'] = $options['xml'];\n            unset($options['xml']);\n        }\n\n        if (array_key_exists('json', $options)) {\n            if (is_array($options['json'])) {\n                /** XXX: 微信的 JSON 是比较奇葩的，比如菜单不能把中文 encode 为 unicode */\n                $options['json'] = json_encode(\n                    $options['json'],\n                    empty($options['json']) ? JSON_FORCE_OBJECT : JSON_UNESCAPED_UNICODE\n                );\n            }\n\n            if (! is_string($options['json'])) {\n                throw new InvalidArgumentException('The type of `json` must be string or array.');\n            }\n\n            if (! $contentType) {\n                $options['headers']['Content-Type'] = 'application/json';\n            }\n\n            $options['body'] = $options['json'];\n            unset($options['json']);\n        }\n\n        return $options;\n    }\n\n    public static function createDefaultServerRequest(): ServerRequestInterface\n    {\n        $psr17Factory = new Psr17Factory;\n\n        $creator = new ServerRequestCreator(\n            serverRequestFactory: $psr17Factory,\n            uriFactory: $psr17Factory,\n            uploadedFileFactory: $psr17Factory,\n            streamFactory: $psr17Factory\n        );\n\n        return $creator->fromGlobals();\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/RequestWithPresets.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\Form\\File;\nuse EasyWeChat\\Kernel\\Form\\Form;\nuse EasyWeChat\\Kernel\\Support\\Str;\n\nuse function array_merge;\nuse function in_array;\nuse function is_file;\nuse function is_string;\nuse function str_ends_with;\nuse function str_starts_with;\nuse function strtoupper;\nuse function substr;\n\ntrait RequestWithPresets\n{\n    /**\n     * @var array<string, string>\n     */\n    protected array $prependHeaders = [];\n\n    /**\n     * @var array<string, mixed>\n     */\n    protected array $prependParts = [];\n\n    /**\n     * @var array<string, mixed>\n     */\n    protected array $presets = [];\n\n    /**\n     * @param  array<string, mixed>  $presets\n     */\n    public function setPresets(array $presets): static\n    {\n        $this->presets = $presets;\n\n        return $this;\n    }\n\n    public function withHeader(string $key, string $value): static\n    {\n        $this->prependHeaders[$key] = $value;\n\n        return $this;\n    }\n\n    public function withHeaders(array $headers): static\n    {\n        foreach ($headers as $key => $value) {\n            $this->withHeader($key, $value);\n        }\n\n        return $this;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function with(string|array $key, mixed $value = null): static\n    {\n        if (\\is_array($key)) {\n            // $client->with(['appid', 'mchid'])\n            // $client->with(['appid' => 'wx1234567', 'mchid'])\n            foreach ($key as $k => $v) {\n                if (\\is_int($k) && is_string($v)) {\n                    [$k, $v] = [$v, null];\n                }\n\n                $this->with($k, $v ?? $this->presets[$k] ?? null);\n            }\n\n            return $this;\n        }\n\n        $this->prependParts[$key] = $value ?? $this->presets[$key] ?? null;\n\n        return $this;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function withFile(string $pathOrContents, string $formName = 'file', ?string $filename = null): static\n    {\n        $file = is_file($pathOrContents) ? File::fromPath(\n            $pathOrContents,\n            $filename\n        ) : File::fromContents($pathOrContents, $filename);\n\n        /**\n         * @var array{headers: array<string, string>, body: string} $options\n         */\n        $options = Form::create([$formName => $file])->toOptions();\n\n        $this->withHeaders($options['headers']);\n\n        return $this->withOptions([\n            'body' => $options['body'],\n        ]);\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function withFileContents(string $contents, string $formName = 'file', ?string $filename = null): static\n    {\n        return $this->withFile($contents, $formName, $filename);\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function withFiles(array $files): static\n    {\n        foreach ($files as $key => $value) {\n            $this->withFile($value, $key);\n        }\n\n        return $this;\n    }\n\n    /**\n     * @return array{xml?:array<string,mixed>|string,json?:array<string,mixed>|string,body?:array<string,mixed>|string,query?:array<string,mixed>,headers?:array<string,string>}\n     */\n    public function mergeThenResetPrepends(array $options, string $method = 'GET'): array\n    {\n        $name = in_array(strtoupper($method), ['GET', 'HEAD', 'DELETE']) ? 'query' : 'body';\n\n        if (($options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null) === 'application/json' || ! empty($options['json'])) {\n            $name = 'json';\n        }\n\n        if (($options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null) === 'text/xml' || ! empty($options['xml'])) {\n            $name = 'xml';\n        }\n\n        if (! empty($this->prependParts)) {\n            $options[$name] = array_merge($this->prependParts, $options[$name] ?? []);\n        }\n\n        if (! empty($this->prependHeaders)) {\n            $options['headers'] = array_merge($this->prependHeaders, $options['headers'] ?? []);\n        }\n\n        $this->prependParts = [];\n        $this->prependHeaders = [];\n\n        return $options;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function handleMagicWithCall(string $method, mixed $value = null): static\n    {\n        // $client->withAppid();\n        // $client->withAppid('wxf8b4f85f3a794e77');\n        // $client->withAppidAs('sub_appid');\n        if (! str_starts_with($method, 'with')) {\n            throw new InvalidArgumentException(sprintf('The method \"%s\" is not supported.', $method));\n        }\n\n        $key = Str::snakeCase(substr($method, 4));\n\n        // $client->withAppidAs('sub_appid');\n        if (str_ends_with($key, '_as')) {\n            $key = substr($key, 0, -3);\n\n            [$key, $value] = [is_string($value) ? $value : $key, $this->presets[$key] ?? null];\n        }\n\n        return $this->with($key, $value);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/Response.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse const JSON_UNESCAPED_UNICODE;\n\nuse ArrayAccess;\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Arrayable;\nuse EasyWeChat\\Kernel\\Contracts\\Jsonable;\nuse EasyWeChat\\Kernel\\Exceptions\\BadMethodCallException;\nuse EasyWeChat\\Kernel\\Exceptions\\BadResponseException;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse Http\\Discovery\\Exception\\NotFoundException;\nuse Http\\Discovery\\Psr17FactoryDiscovery;\nuse Nyholm\\Psr7\\Factory\\Psr17Factory;\nuse Psr\\Http\\Message\\ResponseFactoryInterface;\nuse Psr\\Http\\Message\\StreamFactoryInterface;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Component\\HttpClient\\Response\\StreamableInterface;\nuse Symfony\\Component\\HttpClient\\Response\\StreamWrapper;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\nuse Throwable;\n\nuse function array_key_exists;\nuse function base64_encode;\nuse function file_put_contents;\nuse function json_encode;\nuse function sprintf;\nuse function str_contains;\nuse function str_starts_with;\nuse function strtolower;\n\n/**\n * @implements \\ArrayAccess<array-key, mixed>\n *\n * @see \\Symfony\\Contracts\\HttpClient\\ResponseInterface\n */\nclass Response implements Arrayable, ArrayAccess, Jsonable, ResponseInterface, StreamableInterface\n{\n    public function __construct(\n        protected ResponseInterface $response,\n        protected ?Closure $failureJudge = null,\n        protected bool $throw = true\n    ) {\n    }\n\n    public function throw(bool $throw = true): static\n    {\n        $this->throw = $throw;\n\n        return $this;\n    }\n\n    public function throwOnFailure(): static\n    {\n        return $this->throw(true);\n    }\n\n    public function quietly(): static\n    {\n        return $this->throw(false);\n    }\n\n    public function judgeFailureUsing(callable $callback): static\n    {\n        $this->failureJudge = $callback instanceof Closure ? $callback : fn (Response $response) => $callback($response);\n\n        return $this;\n    }\n\n    public function isSuccessful(): bool\n    {\n        return ! $this->isFailed();\n    }\n\n    public function isFailed(): bool\n    {\n        if ($this->is('text') && $this->failureJudge) {\n            return (bool) ($this->failureJudge)($this);\n        }\n\n        try {\n            return $this->getStatusCode() >= 400;\n        } catch (Throwable $e) {\n            return true;\n        }\n    }\n\n    /**\n     * @throws BadResponseException\n     */\n    public function toArray(?bool $throw = null): array\n    {\n        $throw ??= $this->throw;\n\n        if ('' === $content = $this->response->getContent($throw)) {\n            throw new BadResponseException('Response body is empty.');\n        }\n\n        $contentType = $this->getHeaderLine('content-type', $throw);\n\n        if (str_contains($contentType, 'text/xml')\n            || str_contains($contentType, 'application/xml')\n            || str_starts_with($content, '<xml>')) {\n            try {\n                return Xml::parse($content) ?? [];\n            } catch (Throwable $e) {\n                throw new BadResponseException('Response body is not valid xml.', 400, $e);\n            }\n        }\n\n        return $this->response->toArray($throw);\n    }\n\n    public function toJson(?bool $throw = null): string|false\n    {\n        return json_encode($this->toArray($throw), JSON_UNESCAPED_UNICODE);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @throws BadMethodCallException\n     */\n    public function toStream(?bool $throw = null)\n    {\n        if ($this->response instanceof StreamableInterface) {\n            return $this->response->toStream($throw ?? $this->throw);\n        }\n\n        if ($throw) {\n            throw new BadMethodCallException(sprintf('%s does\\'t implements %s', \\get_class($this->response), StreamableInterface::class));\n        }\n\n        return StreamWrapper::createResource(new MockResponse);\n    }\n\n    /**\n     * @throws \\LogicException\n     */\n    public function toDataUrl(): string\n    {\n        return 'data:'.$this->getHeaderLine('content-type').';base64,'.base64_encode($this->getContent());\n    }\n\n    public function toPsrResponse(?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null): \\Psr\\Http\\Message\\ResponseInterface\n    {\n        $streamFactory ??= $responseFactory instanceof StreamFactoryInterface ? $responseFactory : null;\n\n        if ($responseFactory === null || $streamFactory === null) {\n            if (! class_exists(Psr17Factory::class) && ! class_exists(Psr17FactoryDiscovery::class)) {\n                throw new \\LogicException('You cannot use the \"Symfony\\Component\\HttpClient\\Psr18Client\" as no PSR-17 factories have been provided. Try running \"composer require nyholm/psr7\".');\n            }\n\n            try {\n                $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory : null;\n                $responseFactory ??= $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory(); /** @phpstan-ignore-line */\n                $streamFactory ??= $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory(); /** @phpstan-ignore-line */\n\n                /** @phpstan-ignore-next-line */\n            } catch (NotFoundException $e) {\n                throw new \\LogicException('You cannot use the \"Symfony\\Component\\HttpClient\\HttplugClient\" as no PSR-17 factories have been found. Try running \"composer require nyholm/psr7\".', 0, $e);\n            }\n        }\n\n        $psrResponse = $responseFactory->createResponse($this->getStatusCode());\n\n        foreach ($this->getHeaders(false) as $name => $values) {\n            foreach ($values as $value) {\n                $psrResponse = $psrResponse->withAddedHeader($name, $value);\n            }\n        }\n\n        $body = $this->response instanceof StreamableInterface ? $this->toStream(false) : StreamWrapper::createResource($this->response);\n        $body = $streamFactory->createStreamFromResource($body);\n\n        if ($body->isSeekable()) {\n            $body->seek(0);\n        }\n\n        return $psrResponse->withBody($body);\n    }\n\n    /**\n     * @throws BadResponseException\n     */\n    public function saveAs(string $filename): string\n    {\n        try {\n            file_put_contents($filename, $this->response->getContent(true));\n        } catch (Throwable $e) {\n            throw new BadResponseException(sprintf(\n                'Cannot save response to %s: %s',\n                $filename,\n                $this->response->getContent(false)\n            ), $e->getCode(), $e);\n        }\n\n        return '';\n    }\n\n    public function offsetExists(mixed $offset): bool\n    {\n        return array_key_exists($offset, $this->toArray());\n    }\n\n    public function offsetGet(mixed $offset): mixed\n    {\n        return $this->toArray()[$offset] ?? null;\n    }\n\n    /**\n     * @throws BadMethodCallException\n     */\n    public function offsetSet(mixed $offset, mixed $value): void\n    {\n        throw new BadMethodCallException('Response is immutable.');\n    }\n\n    /**\n     * @throws BadMethodCallException\n     */\n    public function offsetUnset(mixed $offset): void\n    {\n        throw new BadMethodCallException('Response is immutable.');\n    }\n\n    /**\n     * @param  array<array-key, mixed>  $arguments\n     */\n    public function __call(string $name, array $arguments): mixed\n    {\n        return $this->response->{$name}(...$arguments);\n    }\n\n    public function getStatusCode(): int\n    {\n        return $this->response->getStatusCode();\n    }\n\n    public function getHeaders(?bool $throw = null): array\n    {\n        return $this->response->getHeaders($throw ?? $this->throw);\n    }\n\n    public function getContent(?bool $throw = null): string\n    {\n        return $this->response->getContent($throw ?? $this->throw);\n    }\n\n    public function cancel(): void\n    {\n        $this->response->cancel();\n    }\n\n    public function getInfo(?string $type = null): mixed\n    {\n        return $this->response->getInfo($type);\n    }\n\n    public function __toString(): string\n    {\n        return $this->toJson() ?: '';\n    }\n\n    public function hasHeader(string $name, ?bool $throw = null): bool\n    {\n        return isset($this->getHeaders($throw)[$name]);\n    }\n\n    /**\n     * @return array<array-key, mixed>\n     */\n    public function getHeader(string $name, ?bool $throw = null): array\n    {\n        $name = strtolower($name);\n        $throw ??= $this->throw;\n\n        return $this->hasHeader($name, $throw) ? $this->getHeaders($throw)[$name] : [];\n    }\n\n    public function getHeaderLine(string $name, ?bool $throw = null): string\n    {\n        $name = strtolower($name);\n        $throw ??= $this->throw;\n\n        return $this->hasHeader($name, $throw) ? implode(',', $this->getHeader($name, $throw)) : '';\n    }\n\n    public function is(string $type): bool\n    {\n        $contentType = $this->getHeaderLine('content-type');\n\n        return match (strtolower($type)) {\n            'json' => str_contains($contentType, '/json'),\n            'xml' => str_contains($contentType, '/xml'),\n            'html' => str_contains($contentType, '/html'),\n            'image' => str_contains($contentType, 'image/'),\n            'audio' => str_contains($contentType, 'audio/'),\n            'video' => str_contains($contentType, 'video/'),\n            'text' => str_contains($contentType, 'text/')\n                || str_contains($contentType, '/json')\n                || str_contains($contentType, '/xml'),\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/RetryableClient.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy;\nuse Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface;\nuse Symfony\\Component\\HttpClient\\RetryableHttpClient;\n\ntrait RetryableClient\n{\n    /**\n     * @param  array<string, mixed>  $config\n     */\n    public function retry(array $config = []): static\n    {\n        $config = RequestUtil::mergeDefaultRetryOptions($config);\n\n        $strategy = new GenericRetryStrategy(\n            // @phpstan-ignore-next-line\n            (array) $config['status_codes'],\n            // @phpstan-ignore-next-line\n            (int) $config['delay'],\n            // @phpstan-ignore-next-line\n            (float) $config['multiplier'],\n            // @phpstan-ignore-next-line\n            (int) $config['max_delay'],\n            // @phpstan-ignore-next-line\n            (float) $config['jitter']\n        );\n\n        /** @phpstan-ignore-next-line */\n        return $this->retryUsing($strategy, (int) $config['max_retries']);\n    }\n\n    public function retryUsing(\n        RetryStrategyInterface $strategy,\n        int $maxRetries = 3,\n        ?LoggerInterface $logger = null\n    ): static {\n        $this->client = new RetryableHttpClient($this->client, $strategy, $maxRetries, $logger);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/HttpClient/ScopingHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\HttpClient;\n\nuse Psr\\Log\\LoggerAwareInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpClient\\HttpClientTrait;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseStreamInterface;\nuse Symfony\\Contracts\\Service\\ResetInterface;\n\nclass ScopingHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface\n{\n    use HttpClientTrait;\n\n    private HttpClientInterface $client;\n\n    private array $defaultOptionsByRegexp;\n\n    public function __construct(HttpClientInterface $client, array $defaultOptionsByRegexp)\n    {\n        $this->client = $client;\n        $this->defaultOptionsByRegexp = $defaultOptionsByRegexp;\n    }\n\n    public function request(string $method, string $url, array $options = []): ResponseInterface\n    {\n        foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {\n            if (preg_match($regexp, $url)) {\n                $options = self::mergeDefaultOptions($options, $defaultOptions, true);\n                break;\n            }\n        }\n\n        return $this->client->request($method, $url, $options);\n    }\n\n    public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface\n    {\n        return $this->client->stream($responses, $timeout);\n    }\n\n    /**\n     * @return void\n     */\n    public function reset()\n    {\n        if ($this->client instanceof ResetInterface) {\n            $this->client->reset();\n        }\n    }\n\n    public function setLogger(LoggerInterface $logger): void\n    {\n        if ($this->client instanceof LoggerAwareInterface) {\n            $this->client->setLogger($logger);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Message.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel;\n\nuse ArrayAccess;\nuse EasyWeChat\\Kernel\\Contracts\\Jsonable;\nuse EasyWeChat\\Kernel\\Exceptions\\BadRequestException;\nuse EasyWeChat\\Kernel\\Support\\MessageParser;\nuse EasyWeChat\\Kernel\\Traits\\HasAttributes;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\n/**\n * @property string $FromUserName\n * @property string $ToUserName\n * @property string $Encrypt\n * @property string $encrypt\n *\n * @implements ArrayAccess<array-key, mixed>\n */\nabstract class Message implements \\JsonSerializable, ArrayAccess, Jsonable\n{\n    use HasAttributes;\n\n    /**\n     * @param  array<string, mixed>  $attributes\n     */\n    final public function __construct(array $attributes = [], protected ?string $originContent = '')\n    {\n        $this->attributes = $attributes;\n    }\n\n    /**\n     * @throws BadRequestException\n     */\n    public static function createFromRequest(ServerRequestInterface $request): Message\n    {\n        return static::createFromStringContent(strval($request->getBody()));\n    }\n\n    /**\n     * @throws BadRequestException\n     */\n    public static function createFromStringContent(string $originContent): Message\n    {\n        $attributes = MessageParser::parse($originContent);\n\n        return new static($attributes, $originContent);\n    }\n\n    public function getOriginalContents(): string\n    {\n        return $this->originContent ?? '';\n    }\n\n    public function __toString()\n    {\n        return $this->toJson() ?: '';\n    }\n}\n"
  },
  {
    "path": "src/Kernel/ServerResponse.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel;\n\nuse const PHP_OUTPUT_HANDLER_CLEANABLE;\nuse const PHP_OUTPUT_HANDLER_FLUSHABLE;\nuse const PHP_OUTPUT_HANDLER_REMOVABLE;\n\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\StreamInterface;\n\nuse function array_keys;\nuse function array_map;\nuse function count;\nuse function header;\nuse function max;\nuse function sprintf;\nuse function ucwords;\n\nclass ServerResponse implements ResponseInterface\n{\n    public function __construct(protected ResponseInterface $response)\n    {\n        $this->response->getBody()->rewind();\n    }\n\n    public static function make(ResponseInterface $response): ServerResponse\n    {\n        if ($response instanceof ServerResponse) {\n            return $response;\n        }\n\n        return new self($response);\n    }\n\n    public function getProtocolVersion(): string\n    {\n        return $this->response->getProtocolVersion();\n    }\n\n    public function withProtocolVersion($version): ServerResponse|ResponseInterface\n    {\n        return $this->response->withProtocolVersion($version);\n    }\n\n    public function getHeaders(): array\n    {\n        return $this->response->getHeaders();\n    }\n\n    public function hasHeader($name): bool\n    {\n        return $this->response->hasHeader($name);\n    }\n\n    public function getHeader($name): array\n    {\n        return $this->response->getHeader($name);\n    }\n\n    public function getHeaderLine($name): string\n    {\n        return $this->response->getHeaderLine($name);\n    }\n\n    public function withHeader($name, $value): ServerResponse|ResponseInterface\n    {\n        return $this->response->withHeader($name, $value);\n    }\n\n    public function withAddedHeader($name, $value): ServerResponse|ResponseInterface\n    {\n        return $this->response->withAddedHeader($name, $value);\n    }\n\n    public function withoutHeader($name): ServerResponse|ResponseInterface\n    {\n        return $this->response->withoutHeader($name);\n    }\n\n    public function getBody(): StreamInterface\n    {\n        return $this->response->getBody();\n    }\n\n    public function withBody(StreamInterface $body): ServerResponse|ResponseInterface\n    {\n        return $this->response->withBody($body);\n    }\n\n    public function getStatusCode(): int\n    {\n        return $this->response->getStatusCode();\n    }\n\n    public function withStatus($code, $reasonPhrase = ''): ServerResponse|ResponseInterface\n    {\n        $this->response->withStatus($code, $reasonPhrase);\n\n        return $this;\n    }\n\n    public function getReasonPhrase(): string\n    {\n        return $this->response->getReasonPhrase();\n    }\n\n    /**\n     * @link https://github.com/symfony/http-foundation/blob/6.1/Response.php\n     */\n    public function send(): static\n    {\n        $this->sendHeaders();\n        $this->sendContent();\n\n        if (\\function_exists('fastcgi_finish_request')) {\n            \\fastcgi_finish_request();\n        } elseif (\\function_exists('litespeed_finish_request')) {\n            \\litespeed_finish_request();\n        } elseif (! \\in_array(\\PHP_SAPI, ['cli', 'phpdbg'], true)) {\n            static::closeOutputBuffers(0, true);\n        }\n\n        return $this;\n    }\n\n    public function sendHeaders(): static\n    {\n        // headers have already been sent by the developer\n        if (\\headers_sent()) {\n            return $this;\n        }\n\n        foreach ($this->getHeaders() as $name => $values) {\n            $replace = \\strcasecmp($name, 'Content-Type') === 0;\n\n            foreach ($values as $value) {\n                header($name.': '.$value, $replace, $this->getStatusCode());\n            }\n        }\n\n        header(\n            header: sprintf(\n                'HTTP/%s %s %s',\n                $this->getProtocolVersion(),\n                $this->getStatusCode(),\n                $this->getReasonPhrase()\n            ),\n            replace: true,\n            response_code: $this->getStatusCode()\n        );\n\n        return $this;\n    }\n\n    public function sendContent(): static\n    {\n        echo (string) $this->getBody();\n\n        return $this;\n    }\n\n    /**\n     * Cleans or flushes output buffers up to target level.\n     *\n     * Resulting level can be greater than target level if a non-removable buffer has been encountered.\n     *\n     * @link https://github.com/symfony/http-foundation/blob/6.1/Response.php\n     *\n     * @final\n     */\n    public static function closeOutputBuffers(int $targetLevel, bool $flush): void\n    {\n        $status = ob_get_status(true);\n        $level = count($status);\n        $flags = PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE);\n\n        while ($level-- > $targetLevel && ($s = $status[$level]) && (! isset($s['del']) ? ! isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {\n            if ($flush) {\n                ob_end_flush();\n            } else {\n                ob_end_clean();\n            }\n        }\n    }\n\n    public function __toString(): string\n    {\n        $headers = $this->getHeaders();\n        $headersString = '';\n\n        if (! empty($headers)) {\n            ksort($headers);\n\n            $max = max(array_map('strlen', array_keys($headers))) + 1;\n\n            foreach ($headers as $name => $values) {\n                $name = ucwords($name, '-');\n                foreach ($values as $value) {\n                    $headersString .= sprintf(\"%-{$max}s %s\\r\\n\", $name.':', $value);\n                }\n            }\n        }\n\n        return sprintf(\n            'HTTP/%s %s %s',\n            $this->getProtocolVersion(),\n            $this->getStatusCode(),\n            $this->getReasonPhrase()\n        ).\"\\r\\n\".\n            $headersString.\"\\r\\n\".\n            $this->getBody();\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/AesCbc.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse const OPENSSL_RAW_DATA;\n\nuse EasyWeChat\\Kernel\\Contracts\\Aes;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\n\nuse function base64_decode;\nuse function openssl_decrypt;\nuse function openssl_error_string;\n\nclass AesCbc implements Aes\n{\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function encrypt(string $plaintext, string $key, ?string $iv = null): string\n    {\n        $ciphertext = \\openssl_encrypt($plaintext, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, (string) $iv);\n\n        if ($ciphertext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Encrypt AES CBC error.');\n        }\n\n        return base64_encode($ciphertext);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function decrypt(string $ciphertext, string $key, ?string $iv = null): string\n    {\n        $plaintext = openssl_decrypt(\n            base64_decode($ciphertext),\n            'aes-128-cbc',\n            $key,\n            OPENSSL_RAW_DATA,\n            (string) $iv\n        );\n\n        if ($plaintext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Decrypt AES CBC error.');\n        }\n\n        return $plaintext;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/AesEcb.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse const OPENSSL_RAW_DATA;\n\nuse EasyWeChat\\Kernel\\Contracts\\Aes;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\n\nuse function base64_decode;\nuse function openssl_decrypt;\nuse function openssl_error_string;\n\nclass AesEcb implements Aes\n{\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function encrypt(string $plaintext, string $key, ?string $iv = null): string\n    {\n        $ciphertext = \\openssl_encrypt($plaintext, 'aes-256-ecb', $key, OPENSSL_RAW_DATA, (string) $iv);\n\n        if ($ciphertext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Encrypt AES ECB failed.');\n        }\n\n        return \\base64_encode($ciphertext);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function decrypt(string $ciphertext, string $key, ?string $iv = null): string\n    {\n        $plaintext = openssl_decrypt(\n            base64_decode($ciphertext, true) ?: '',\n            'aes-256-ecb',\n            $key,\n            OPENSSL_RAW_DATA,\n            (string) $iv\n        );\n\n        if ($plaintext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Decrypt AES ECB failed.');\n        }\n\n        return $plaintext;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/AesGcm.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse const OPENSSL_RAW_DATA;\n\nuse EasyWeChat\\Kernel\\Contracts\\Aes;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\n\nuse function base64_decode;\nuse function base64_encode;\nuse function openssl_decrypt;\nuse function openssl_encrypt;\nuse function openssl_error_string;\n\nclass AesGcm implements Aes\n{\n    public const BLOCK_SIZE = 16;\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function encrypt(string $plaintext, string $key, ?string $iv = null, string $aad = ''): string\n    {\n        $ciphertext = openssl_encrypt(\n            $plaintext,\n            'aes-256-gcm',\n            $key,\n            OPENSSL_RAW_DATA,\n            (string) $iv,\n            $tag,\n            $aad,\n            self::BLOCK_SIZE\n        );\n\n        if ($ciphertext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Encrypt failed');\n        }\n\n        return base64_encode($ciphertext.$tag);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function decrypt(string $ciphertext, string $key, ?string $iv = null, string $aad = ''): string\n    {\n        $ciphertext = base64_decode($ciphertext);\n\n        $tag = substr($ciphertext, -self::BLOCK_SIZE);\n\n        $ciphertext = substr($ciphertext, 0, -self::BLOCK_SIZE);\n\n        $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, (string) $iv, $tag, $aad);\n\n        if ($plaintext === false) {\n            throw new InvalidArgumentException(openssl_error_string() ?: 'Decrypt failed');\n        }\n\n        return $plaintext;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/Arr.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse function is_string;\n\nclass Arr\n{\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public static function get(mixed $array, string|int|null $key, mixed $default = null): mixed\n    {\n        if (! is_array($array)) {\n            return $default;\n        }\n\n        if (is_null($key)) {\n            return $array;\n        }\n\n        if (static::exists($array, $key)) {\n            return $array[$key];\n        }\n\n        foreach (explode('.', (string) $key) as $segment) {\n            if (is_array($array) && static::exists($array, $segment)) {\n                $array = $array[$segment];\n            } else {\n                return $default;\n            }\n        }\n\n        return $array;\n    }\n\n    /**\n     * @param  array<int|string, mixed>  $array\n     */\n    public static function exists(array $array, string|int $key): bool\n    {\n        return array_key_exists($key, $array);\n    }\n\n    /**\n     * @param  array<string|int, mixed>  $array\n     * @return array<string, mixed>\n     */\n    public static function set(array &$array, string|int|null $key, mixed $value): array\n    {\n        if (! is_string($key)) {\n            $key = (string) $key;\n        }\n\n        $keys = explode('.', $key);\n\n        while (count($keys) > 1) {\n            $key = array_shift($keys);\n\n            // If the key doesn't exist at this depth, we will just create an empty array\n            // to hold the next value, allowing us to create the arrays to hold final\n            // values at the correct depth. Then we'll keep digging into the array.\n            if (! isset($array[$key]) || ! is_array($array[$key])) {\n                $array[$key] = [];\n            }\n\n            $array = &$array[$key];\n        }\n\n        $array[array_shift($keys)] = $value;\n\n        return $array;\n    }\n\n    /**\n     * @param  array<string|int, mixed>  $array\n     * @return array<string|int, mixed>\n     */\n    public static function dot(array $array, string $prepend = ''): array\n    {\n        $results = [];\n\n        foreach ($array as $key => $value) {\n            if (is_array($value) && ! empty($value)) {\n                $results = array_merge($results, static::dot($value, $prepend.$key.'.'));\n            } else {\n                $results[$prepend.$key] = $value;\n            }\n        }\n\n        return $results;\n    }\n\n    /**\n     * @param  array<string|int, mixed>  $array\n     * @param  string|int|array<string|int, mixed>|null  $keys\n     */\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public static function has(array $array, string|int|array|null $keys): bool\n    {\n        if (is_null($keys)) {\n            return false;\n        }\n\n        $keys = (array) $keys;\n\n        if (empty($array)) {\n            return false;\n        }\n\n        if ($keys === []) {\n            return false;\n        }\n\n        foreach ($keys as $key) {\n            $subKeyArray = $array;\n\n            /** @phpstan-ignore-next-line */\n            if (static::exists($array, $key)) {\n                continue;\n            }\n\n            /** @phpstan-ignore-next-line */\n            foreach (explode('.', (string) $key) as $segment) {\n                /** @phpstan-ignore-next-line */\n                if (static::exists($subKeyArray, $segment)) {\n                    /** @phpstan-ignore-next-line */\n                    $subKeyArray = $subKeyArray[$segment];\n                } else {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/MessageParser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Exceptions\\BadRequestException;\n\nclass MessageParser\n{\n    /**\n     * Parse message content from string (XML or JSON).\n     * This is a pure parser that automatically detects and parses XML or JSON format.\n     *\n     * @return array<string, mixed>\n     *\n     * @throws BadRequestException\n     */\n    public static function parse(string $content): array\n    {\n        $content = trim($content);\n\n        // Try JSON format first\n        $parsed = json_decode($content, true);\n\n        if (json_last_error() === JSON_ERROR_NONE && is_array($parsed) && ! empty($parsed)) {\n            /** @var array<string, mixed> $parsed */\n            return $parsed;\n        }\n\n        // If JSON decode failed or result is not an array, try XML format\n        $parsed = Xml::parse($content);\n\n        if (is_array($parsed) && ! empty($parsed)) {\n            /** @var array<string, mixed> $parsed */\n            return $parsed;\n        }\n\n        throw new BadRequestException('Failed to decode content. Content must be valid XML or JSON.');\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/Pkcs7.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\n\nclass Pkcs7\n{\n    /**\n     * @throws InvalidArgumentException\n     */\n    public static function padding(string $contents, int $blockSize): string\n    {\n        if ($blockSize > 32) {\n            throw new InvalidArgumentException('$blockSize may not be more than 32 bytes(256 bits)');\n        }\n        $padding = $blockSize - (strlen($contents) % $blockSize);\n        $pattern = chr($padding);\n\n        return $contents.str_repeat($pattern, $padding);\n    }\n\n    public static function unpadding(string $contents, int $blockSize): string\n    {\n        $pad = ord(substr($contents, -1));\n        if ($pad < 1 || $pad > $blockSize) {\n            $pad = 0;\n        }\n\n        return substr($contents, 0, (strlen($contents) - $pad));\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/PrivateKey.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse function file_exists;\nuse function file_get_contents;\nuse function str_starts_with;\n\nclass PrivateKey\n{\n    public function __construct(protected string $key, protected ?string $passphrase = null)\n    {\n        if (file_exists($key)) {\n            $this->key = \"file://{$key}\";\n        }\n    }\n\n    public function getKey(): string\n    {\n        if (str_starts_with($this->key, 'file://')) {\n            return file_get_contents($this->key) ?: '';\n        }\n\n        return $this->key;\n    }\n\n    public function getPassphrase(): ?string\n    {\n        return $this->passphrase;\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function __toString(): string\n    {\n        return $this->getKey();\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/PublicKey.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\n\nuse function file_exists;\nuse function file_get_contents;\nuse function openssl_x509_parse;\nuse function str_starts_with;\nuse function strtoupper;\n\nclass PublicKey\n{\n    public function __construct(public string $certificate)\n    {\n        if (file_exists($certificate)) {\n            $this->certificate = \"file://{$certificate}\";\n        }\n    }\n\n    /**\n     * @throws InvalidConfigException\n     */\n    public function getSerialNo(): string\n    {\n        $info = openssl_x509_parse($this->certificate);\n\n        if ($info === false) {\n            throw new InvalidConfigException('Read the $certificate failed, please check it whether or nor correct');\n        }\n\n        return strtoupper($info['serialNumberHex']);\n    }\n\n    public function __toString(): string\n    {\n        if (str_starts_with($this->certificate, 'file://')) {\n            return file_get_contents($this->certificate) ?: '';\n        }\n\n        return $this->certificate;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/Str.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse Exception;\n\nuse function base64_encode;\nuse function preg_replace;\nuse function random_bytes;\nuse function str_replace;\nuse function strlen;\nuse function strtolower;\nuse function substr;\nuse function trim;\n\nclass Str\n{\n    /**\n     * From https://github.com/laravel/framework/blob/9.x/src/Illuminate/Support/Str.php#L632-L644\n     *\n     * @throws Exception\n     */\n    public static function random(int $length = 16): string\n    {\n        $string = '';\n\n        while (($len = strlen($string)) < $length) {\n            $size = $length - $len;\n\n            /** @phpstan-ignore-next-line */\n            $bytes = random_bytes($size);\n\n            $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);\n        }\n\n        return $string;\n    }\n\n    public static function snakeCase(string $string): string\n    {\n        return trim(strtolower((string) preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $string)), '_');\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/UserAgent.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse Composer\\InstalledVersions;\n\nuse function array_map;\nuse function array_unshift;\nuse function class_exists;\nuse function constant;\nuse function curl_version;\nuse function defined;\nuse function explode;\nuse function extension_loaded;\nuse function function_exists;\nuse function ini_get;\n\nclass UserAgent\n{\n    /**\n     * @param  array<string>  $appends\n     */\n    public static function create(array $appends = []): string\n    {\n        $value = array_map('strval', $appends);\n\n        if (defined('HHVM_VERSION')) {\n            array_unshift($value, 'HHVM/'.constant('HHVM_VERSION'));\n        }\n\n        $disabledFunctions = explode(',', ini_get('disable_functions') ?: '');\n\n        if (extension_loaded('curl') && function_exists('curl_version')) {\n            array_unshift($value, 'curl/'.(curl_version() ?: ['version' => 'unknown'])['version']);\n        }\n\n        if (! ini_get('safe_mode')\n            && function_exists('php_uname')\n            && ! in_array('php_uname', $disabledFunctions, true)\n        ) {\n            $osName = 'OS/'.php_uname('s').'/'.php_uname('r');\n            array_unshift($value, $osName);\n        }\n\n        if (class_exists(InstalledVersions::class)) {\n            array_unshift($value, 'easywechat-sdk/'.((string) InstalledVersions::getVersion('w7corp/easywechat')));\n        }\n\n        return trim(implode(' ', $value));\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Support/Xml.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Support;\n\nuse TheNorthMemory\\Xml\\Transformer;\n\nclass Xml\n{\n    public static function parse(string $xml): ?array\n    {\n        return Transformer::toArray($xml);\n    }\n\n    public static function build(array $data): string\n    {\n        return Transformer::toXml($data);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/DecryptJsonMessage.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\n/**\n * @deprecated Use DecryptMessage trait instead. This trait will be removed in a future version.\n */\ntrait DecryptJsonMessage\n{\n    use DecryptMessage {\n        decryptMessage as decryptJsonMessage;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/DecryptMessage.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\BadRequestException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\Message;\nuse EasyWeChat\\Kernel\\Support\\MessageParser;\n\ntrait DecryptMessage\n{\n    /**\n     * Decrypt message (automatically detects XML or JSON format).\n     *\n     * @throws RuntimeException\n     * @throws BadRequestException\n     */\n    public function decryptMessage(\n        Message $message,\n        Encryptor $encryptor,\n        string $signature,\n        int|string $timestamp,\n        string $nonce\n    ): Message {\n        $ciphertext = $message->Encrypt ?? $message->encrypt ?? null;\n\n        if (! is_string($ciphertext) || $ciphertext === '') {\n            throw new BadRequestException('Request ciphertext must not be empty.');\n        }\n\n        $this->validateSignature($encryptor->getToken(), $ciphertext, $signature, $timestamp, $nonce);\n\n        $plaintext = $encryptor->decrypt(\n            ciphertext: $ciphertext,\n            msgSignature: $signature,\n            nonce: $nonce,\n            timestamp: $timestamp\n        );\n\n        $attributes = MessageParser::parse($plaintext);\n\n        $message->merge($attributes);\n\n        return $message;\n    }\n\n    /**\n     * Validate the request signature.\n     *\n     * @throws BadRequestException\n     */\n    protected function validateSignature(\n        string $token,\n        string $ciphertext,\n        string $signature,\n        int|string $timestamp,\n        string $nonce\n    ): void {\n        if (empty($signature)) {\n            throw new BadRequestException('Request signature must not be empty.');\n        }\n\n        $params = [$token, $timestamp, $nonce, $ciphertext];\n\n        sort($params, SORT_STRING);\n\n        if ($signature !== sha1(implode($params))) {\n            throw new BadRequestException('Invalid request signature.');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/DecryptXmlMessage.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\n/**\n * @deprecated Use DecryptMessage trait instead. This trait will be removed in a future version.\n */\ntrait DecryptXmlMessage\n{\n    use DecryptMessage {\n        decryptMessage as public;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/HasAttributes.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse function array_key_exists;\nuse function array_merge;\nuse function json_encode;\n\ntrait HasAttributes\n{\n    /**\n     * @var array<int|string,mixed>\n     */\n    protected array $attributes = [];\n\n    /**\n     * @param  array<int|string,mixed>  $attributes\n     */\n    public function __construct(array $attributes)\n    {\n        $this->attributes = $attributes;\n    }\n\n    /**\n     * @return array<int|string,mixed>\n     */\n    public function toArray(): array\n    {\n        return $this->attributes;\n    }\n\n    public function toJson(): string|false\n    {\n        return json_encode($this->attributes);\n    }\n\n    public function has(string $key): bool\n    {\n        return array_key_exists($key, $this->attributes);\n    }\n\n    /**\n     * @param  array<int|string,mixed>  $attributes\n     */\n    public function merge(array $attributes): static\n    {\n        $this->attributes = array_merge($this->attributes, $attributes);\n\n        return $this;\n    }\n\n    /**\n     * @return array<int|string,mixed> $attributes\n     */\n    public function jsonSerialize(): array\n    {\n        return $this->attributes;\n    }\n\n    public function __set(string $attribute, mixed $value): void\n    {\n        $this->attributes[$attribute] = $value;\n    }\n\n    public function __get(string $attribute): mixed\n    {\n        return $this->attributes[$attribute] ?? null;\n    }\n\n    public function offsetExists(mixed $offset): bool\n    {\n        /** @phpstan-ignore-next-line */\n        return array_key_exists($offset, $this->attributes);\n    }\n\n    public function offsetGet(mixed $offset): mixed\n    {\n        return $this->attributes[$offset];\n    }\n\n    public function offsetSet(mixed $offset, mixed $value): void\n    {\n        if ($offset === null) {\n            $this->attributes[] = $value;\n        } else {\n            $this->attributes[$offset] = $value;\n        }\n    }\n\n    public function offsetUnset(mixed $offset): void\n    {\n        unset($this->attributes[$offset]);\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithCache.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\ntrait InteractWithCache\n{\n    protected ?CacheInterface $cache = null;\n\n    protected int $cacheLifetime = 1500;\n\n    protected string $cacheNamespace = 'easywechat';\n\n    public function getCacheLifetime(): int\n    {\n        return $this->cacheLifetime;\n    }\n\n    public function setCacheLifetime(int $cacheLifetime): void\n    {\n        $this->cacheLifetime = $cacheLifetime;\n    }\n\n    public function getCacheNamespace(): string\n    {\n        return $this->cacheNamespace;\n    }\n\n    public function setCacheNamespace(string $cacheNamespace): void\n    {\n        $this->cacheNamespace = $cacheNamespace;\n    }\n\n    public function setCache(CacheInterface $cache): static\n    {\n        $this->cache = $cache;\n\n        return $this;\n    }\n\n    public function getCache(): CacheInterface\n    {\n        if (! $this->cache) {\n            $this->cache = new Psr16Cache(new FilesystemAdapter($this->cacheNamespace, $this->cacheLifetime));\n        }\n\n        return $this->cache;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\n\ntrait InteractWithClient\n{\n    protected ?AccessTokenAwareClient $client = null;\n\n    public function getClient(): AccessTokenAwareClient\n    {\n        if (! $this->client) {\n            $this->client = $this->createClient();\n        }\n\n        return $this->client;\n    }\n\n    public function setClient(AccessTokenAwareClient $client): static\n    {\n        $this->client = $client;\n\n        return $this;\n    }\n\n    abstract public function createClient(): AccessTokenAwareClient;\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithConfig.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\n\nuse function is_array;\n\ntrait InteractWithConfig\n{\n    protected ConfigInterface $config;\n\n    /**\n     * @param  array<string,mixed>|ConfigInterface  $config\n     */\n    public function __construct(array|ConfigInterface $config)\n    {\n        $this->config = is_array($config) ? new Config($config) : $config;\n    }\n\n    public function getConfig(): ConfigInterface\n    {\n        return $this->config;\n    }\n\n    public function setConfig(ConfigInterface $config): static\n    {\n        $this->config = $config;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithHandlers.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\n\nuse function array_reverse;\nuse function array_unshift;\nuse function call_user_func;\nuse function func_get_args;\nuse function gettype;\nuse function is_array;\nuse function is_callable;\nuse function is_string;\nuse function method_exists;\nuse function spl_object_hash;\n\ntrait InteractWithHandlers\n{\n    /**\n     * @var array<int, array{hash: string, handler: callable}>\n     */\n    protected array $handlers = [];\n\n    /**\n     * @return array<int, array{hash: string, handler: callable}>\n     */\n    public function getHandlers(): array\n    {\n        return $this->handlers;\n    }\n\n    public function with(callable|string $handler): static\n    {\n        return $this->withHandler($handler);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function withHandler(callable|string $handler): static\n    {\n        $this->handlers[] = $this->createHandlerItem($handler);\n\n        return $this;\n    }\n\n    /**\n     * @return array{hash: string, handler: callable}\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['hash' => 'string', 'handler' => 'callable'])]\n    public function createHandlerItem(callable|string $handler): array\n    {\n        return [\n            'hash' => $this->getHandlerHash($handler),\n            'handler' => $this->makeClosure($handler),\n        ];\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    protected function getHandlerHash(callable|array|string $handler): string\n    {\n        return match (true) {\n            is_string($handler) => $handler,\n            is_array($handler) => is_string($handler[0])\n                ? $handler[0].'::'.$handler[1]\n                : get_class($handler[0]).$handler[1],\n            $handler instanceof Closure => spl_object_hash($handler),\n            is_callable($handler) => spl_object_hash($handler),\n            default => throw new InvalidArgumentException('Invalid handler: '.gettype($handler)),\n        };\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    protected function makeClosure(callable|string $handler): callable\n    {\n        if (is_callable($handler)) {\n            return $handler;\n        }\n\n        if (class_exists($handler) && method_exists($handler, '__invoke')) {\n            /**\n             * @psalm-suppress InvalidFunctionCall\n             *\n             * @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/5867\n             */\n            return fn (): mixed => (new $handler)(...func_get_args());\n        }\n\n        throw new InvalidArgumentException(sprintf('Invalid handler: %s.', $handler));\n    }\n\n    public function prepend(callable|string $handler): static\n    {\n        return $this->prependHandler($handler);\n    }\n\n    public function prependHandler(callable|string $handler): static\n    {\n        array_unshift($this->handlers, $this->createHandlerItem($handler));\n\n        return $this;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function without(callable|string $handler): static\n    {\n        return $this->withoutHandler($handler);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function withoutHandler(callable|string $handler): static\n    {\n        $index = $this->indexOf($handler);\n\n        if ($index > -1) {\n            unset($this->handlers[$index]);\n        }\n\n        return $this;\n    }\n\n    public function indexOf(callable|string $handler): int\n    {\n        foreach ($this->handlers as $index => $item) {\n            if ($item['hash'] === $this->getHandlerHash($handler)) {\n                return $index;\n            }\n        }\n\n        return -1;\n    }\n\n    public function when(mixed $value, callable|string $handler): static\n    {\n        if (is_callable($value)) {\n            $value = call_user_func($value, $this);\n        }\n\n        if ($value) {\n            return $this->withHandler($handler);\n        }\n\n        return $this;\n    }\n\n    public function handle(mixed $result, mixed $payload = null): mixed\n    {\n        $next = $result = is_callable($result) ? $result : fn (mixed $p): mixed => $result;\n\n        foreach (array_reverse($this->handlers) as $item) {\n            $next = fn (mixed $p): mixed => $item['handler']($p, $next) ?? $result($p);\n        }\n\n        return $next($payload);\n    }\n\n    public function has(callable|string $handler): bool\n    {\n        return $this->indexOf($handler) > -1;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse EasyWeChat\\Kernel\\HttpClient\\ScopingHttpClient;\nuse EasyWeChat\\Kernel\\Support\\Arr;\nuse Psr\\Log\\LoggerAwareInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function is_array;\n\ntrait InteractWithHttpClient\n{\n    protected ?HttpClientInterface $httpClient = null;\n\n    public function getHttpClient(): HttpClientInterface\n    {\n        if (! $this->httpClient) {\n            $this->httpClient = $this->createHttpClient();\n        }\n\n        return $this->httpClient;\n    }\n\n    public function setHttpClient(HttpClientInterface $httpClient): static\n    {\n        $this->httpClient = $httpClient;\n\n        if ($this instanceof LoggerAwareInterface && $httpClient instanceof LoggerAwareInterface\n            && $this->logger instanceof LoggerInterface) {\n            $httpClient->setLogger($this->logger);\n        }\n\n        return $this;\n    }\n\n    protected function createHttpClient(): HttpClientInterface\n    {\n        $options = $this->getHttpClientDefaultOptions();\n\n        $optionsByRegexp = Arr::get($options, 'options_by_regexp', []);\n        unset($options['options_by_regexp']);\n\n        $client = HttpClient::create(RequestUtil::formatDefaultOptions($options));\n\n        if (is_array($optionsByRegexp) && ! empty($optionsByRegexp)) {\n            $client = new ScopingHttpClient($client, $optionsByRegexp);\n        }\n\n        return $client;\n    }\n\n    /**\n     * @return array<string,mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/InteractWithServerRequest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse Nyholm\\Psr7\\Factory\\Psr17Factory;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Symfony\\Bridge\\PsrHttpMessage\\Factory\\PsrHttpFactory;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\ntrait InteractWithServerRequest\n{\n    protected ?ServerRequestInterface $request = null;\n\n    public function getRequest(): ServerRequestInterface\n    {\n        if (! $this->request) {\n            $this->request = RequestUtil::createDefaultServerRequest();\n        }\n\n        return $this->request;\n    }\n\n    public function setRequest(ServerRequestInterface $request): static\n    {\n        $this->request = $request;\n\n        return $this;\n    }\n\n    public function setRequestFromSymfonyRequest(Request $symfonyRequest): static\n    {\n        $psr17Factory = new Psr17Factory;\n        $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);\n\n        $this->request = $psrHttpFactory->createRequest($symfonyRequest);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/MockableHttpClient.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse Mockery\\Mock;\nuse Symfony\\Component\\HttpClient\\DecoratorTrait;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ntrait MockableHttpClient\n{\n    public static function createMockClient(MockHttpClient $mockHttpClient): HttpClientInterface\n    {\n        return new self($mockHttpClient);\n    }\n\n    /**\n     * @param  array<string,mixed>  $headers\n     */\n    public static function mock(\n        string $response = '',\n        ?int $status = 200,\n        array $headers = [],\n        string $baseUri = 'https://example.com'\n    ): object {\n        $mockResponse = new MockResponse(\n            $response,\n            array_merge([\n                'http_code' => $status,\n                'content_type' => 'application/json',\n            ], $headers)\n        );\n\n        $client = self::createMockClient(new MockHttpClient($mockResponse, $baseUri));\n\n        // @phpstan-ignore-next-line\n        return new class($client, $mockResponse)\n        {\n            use DecoratorTrait;\n\n            public function __construct(Mock|HttpClientInterface $client, public MockResponse $mockResponse)\n            {\n                $this->client = $client;\n            }\n\n            /**\n             * @param  array<string,mixed>  $arguments\n             */\n            public function __call(string $name, array $arguments): mixed\n            {\n                return $this->client->$name(...$arguments);\n            }\n\n            #[\\JetBrains\\PhpStorm\\Pure]\n            public function getRequestMethod(): string\n            {\n                return $this->mockResponse->getRequestMethod();\n            }\n\n            #[\\JetBrains\\PhpStorm\\Pure]\n            public function getRequestUrl(): string\n            {\n                return $this->mockResponse->getRequestUrl();\n            }\n\n            /**\n             * @return array<string, mixed>\n             */\n            #[\\JetBrains\\PhpStorm\\Pure]\n            public function getRequestOptions(): array\n            {\n                return $this->mockResponse->getRequestOptions();\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/RespondJsonMessage.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Message;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\n\ntrait RespondJsonMessage\n{\n    public function transformJsonToReply(mixed $response, Message $message, ?Encryptor $encryptor = null): ResponseInterface\n    {\n        if (empty($response)) {\n            return new Response(200, [], 'success');\n        }\n\n        return $this->createJsonResponse(\n            attributes: $this->normalizeJsonResponse($response),\n            encryptor: $encryptor\n        );\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    protected function normalizeJsonResponse(mixed $response): array\n    {\n        if (! is_string($response) && is_callable($response)) {\n            $response = $response();\n        }\n\n        if (is_array($response)) {\n            if (! isset($response['msgtype'])) {\n                throw new InvalidArgumentException('msgtype cannot be empty.');\n            }\n\n            return $response;\n        }\n\n        throw new InvalidArgumentException(\n            sprintf('Invalid Response type \"%s\".', gettype($response))\n        );\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    protected function createJsonResponse(array $attributes, ?Encryptor $encryptor = null): ResponseInterface\n    {\n        $jsonStr = json_encode($attributes, JSON_UNESCAPED_UNICODE);\n\n        if (is_string($jsonStr)) {\n            return new Response(200, ['Content-Type' => 'application/json'], $encryptor ? $encryptor->encrypt($jsonStr, messageType: $this->messageType) : $jsonStr);\n        }\n\n        throw new InvalidArgumentException(\n            sprintf('Invalid Response content \"%s\".', implode(',', $attributes))\n        );\n    }\n}\n"
  },
  {
    "path": "src/Kernel/Traits/RespondXmlMessage.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\Message;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nuse function array_merge;\nuse function is_array;\nuse function is_callable;\nuse function is_string;\nuse function time;\n\ntrait RespondXmlMessage\n{\n    /**\n     * @throws RuntimeException\n     * @throws InvalidArgumentException\n     */\n    public function transformToReply(mixed $response, Message $message, ?Encryptor $encryptor = null): ResponseInterface\n    {\n        if (empty($response)) {\n            return new Response(200, [], 'success');\n        }\n\n        return $this->createXmlResponse(\n            attributes: array_filter(\n                array_merge(\n                    [\n                        'ToUserName' => $message->FromUserName,\n                        'FromUserName' => $message->ToUserName,\n                        'CreateTime' => time(),\n                    ],\n                    $this->normalizeResponse($response),\n                )\n            ),\n            encryptor: $encryptor\n        );\n    }\n\n    /**\n     * @return array<string, mixed>\n     *\n     * @throws InvalidArgumentException\n     */\n    protected function normalizeResponse(mixed $response): array\n    {\n        if (! is_string($response) && is_callable($response)) {\n            $response = $response();\n        }\n\n        if (is_array($response)) {\n            if (! isset($response['MsgType'])) {\n                throw new InvalidArgumentException('MsgType cannot be empty.');\n            }\n\n            return $response;\n        }\n\n        if (is_string($response) || is_numeric($response)) {\n            return [\n                'MsgType' => 'text',\n                'Content' => $response,\n            ];\n        }\n\n        throw new InvalidArgumentException(\n            sprintf('Invalid Response type \"%s\".', gettype($response))\n        );\n    }\n\n    /**\n     * @param  array<string, mixed>  $attributes\n     *\n     * @throws RuntimeException\n     */\n    protected function createXmlResponse(array $attributes, ?Encryptor $encryptor = null): ResponseInterface\n    {\n        $xml = Xml::build($attributes);\n\n        return new Response(200, ['Content-Type' => 'application/xml'], $encryptor ? $encryptor->encrypt($xml) : $xml);\n    }\n}\n"
  },
  {
    "path": "src/MiniApp/AccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp;\n\nclass AccessToken extends \\EasyWeChat\\OfficialAccount\\AccessToken\n{\n    const CACHE_KEY_PREFIX = 'mini_app';\n}\n"
  },
  {
    "path": "src/MiniApp/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp;\n\nuse EasyWeChat\\MiniApp\\Contracts\\Account as AccountInterface;\n\nclass Account extends \\EasyWeChat\\OfficialAccount\\Account implements AccountInterface\n{\n    //\n}\n"
  },
  {
    "path": "src/MiniApp/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenExpiredRetryStrategy;\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\MiniApp\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\MiniApp\\Contracts\\Application as ApplicationInterface;\nuse Psr\\Log\\LoggerAwareTrait;\nuse Symfony\\Component\\HttpClient\\Response\\AsyncContext;\nuse Symfony\\Component\\HttpClient\\RetryableHttpClient;\n\nuse function array_merge;\nuse function is_null;\nuse function str_contains;\n\n/**\n * @psalm-suppress PropertyNotSetInConstructor\n */\nclass Application implements ApplicationInterface\n{\n    use InteractWithCache;\n    use InteractWithClient;\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?Encryptor $encryptor = null;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?AccountInterface $account = null;\n\n    protected ?AccessTokenInterface $accessToken = null;\n\n    public function getAccount(): AccountInterface\n    {\n        if (! $this->account) {\n            $this->account = new Account(\n                appId: (string) $this->config->get('app_id'), /** @phpstan-ignore-line */\n                secret: (string) $this->config->get('secret'), /** @phpstan-ignore-line */\n                token: (string) $this->config->get('token'), /** @phpstan-ignore-line */\n                aesKey: (string) $this->config->get('aes_key'),/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->account;\n    }\n\n    public function setAccount(AccountInterface $account): static\n    {\n        $this->account = $account;\n\n        return $this;\n    }\n\n    /**\n     * @throws InvalidConfigException\n     */\n    public function getEncryptor(): Encryptor\n    {\n        if (! $this->encryptor) {\n            $token = $this->getAccount()->getToken();\n            $aesKey = $this->getAccount()->getAesKey();\n\n            if (empty($token) || empty($aesKey)) {\n                throw new InvalidConfigException('token or aes_key cannot be empty.');\n            }\n\n            $this->encryptor = new Encryptor(\n                appId: $this->getAccount()->getAppId(),\n                token: $token,\n                aesKey: $aesKey,\n                receiveId: $this->getAccount()->getAppId()\n            );\n        }\n\n        return $this->encryptor;\n    }\n\n    public function setEncryptor(Encryptor $encryptor): static\n    {\n        $this->encryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getServer(): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                request: $this->getRequest(),\n                encryptor: $this->getAccount()->getAesKey() ? $this->getEncryptor() : null\n            );\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function getAccessToken(): AccessTokenInterface\n    {\n        if (! $this->accessToken) {\n            $this->accessToken = new AccessToken(\n                appId: $this->getAccount()->getAppId(),\n                secret: $this->getAccount()->getSecret(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n                stable: $this->config->get('use_stable_access_token', false)\n            );\n        }\n\n        return $this->accessToken;\n    }\n\n    public function setAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function getUtils(): Utils\n    {\n        return new Utils($this);\n    }\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        $httpClient = $this->getHttpClient();\n\n        if ($this->config->get('http.retry', false)) {\n            $httpClient = new RetryableHttpClient(\n                $httpClient,\n                $this->getRetryStrategy(),\n                (int) $this->config->get('http.max_retries', 2) // @phpstan-ignore-line\n            );\n        }\n\n        return (new AccessTokenAwareClient(\n            client: $httpClient,\n            accessToken: $this->getAccessToken(),\n            failureJudge: fn (\n                Response $response\n            ) => ($response->toArray()['errcode'] ?? 0) || ! is_null($response->toArray()['error'] ?? null),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getRetryStrategy(): AccessTokenExpiredRetryStrategy\n    {\n        $retryConfig = RequestUtil::mergeDefaultRetryOptions((array) $this->config->get('http.retry', []));\n\n        return (new AccessTokenExpiredRetryStrategy($retryConfig))\n            ->decideUsing(function (AsyncContext $context, ?string $responseContent): bool {\n                return ! empty($responseContent)\n                    && str_contains($responseContent, '42001')\n                    && str_contains($responseContent, 'access_token expired');\n            });\n    }\n\n    /**\n     * @return array<string,mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return array_merge(\n            ['base_uri' => 'https://api.weixin.qq.com/'],\n            (array) $this->config->get('http', [])\n        );\n    }\n}\n"
  },
  {
    "path": "src/MiniApp/Contracts/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp\\Contracts;\n\ninterface Account\n{\n    public function getAppId(): string;\n\n    public function getSecret(): string;\n\n    public function getToken(): ?string;\n\n    public function getAesKey(): ?string;\n}\n"
  },
  {
    "path": "src/MiniApp/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Server;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getAccount(): Account;\n\n    public function getEncryptor(): Encryptor;\n\n    public function getServer(): Server;\n\n    public function getRequest(): ServerRequestInterface;\n\n    public function getClient(): AccessTokenAwareClient;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getConfig(): Config;\n\n    public function getAccessToken(): AccessToken;\n\n    public function getCache(): CacheInterface;\n}\n"
  },
  {
    "path": "src/MiniApp/Decryptor.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp;\n\nuse EasyWeChat\\Kernel\\Exceptions\\DecryptException;\nuse EasyWeChat\\Kernel\\Support\\AesCbc;\nuse Throwable;\n\nuse function base64_decode;\nuse function is_array;\nuse function json_decode;\nuse function sprintf;\n\nclass Decryptor\n{\n    /**\n     * @return array<string, mixed>\n     *\n     * @throws DecryptException\n     */\n    public static function decrypt(string $sessionKey, string $iv, string $ciphertext): array\n    {\n        try {\n            $decrypted = AesCbc::decrypt(\n                $ciphertext,\n                base64_decode($sessionKey, false),\n                base64_decode($iv, false)\n            );\n\n            $decrypted = json_decode($decrypted, true);\n\n            if (! $decrypted || ! is_array($decrypted)) {\n                throw new DecryptException('The given payload is invalid.');\n            }\n        } catch (Throwable $e) {\n            throw new DecryptException(sprintf('The given payload is invalid: %s', $e->getMessage()));\n        }\n\n        return $decrypted;\n    }\n}\n"
  },
  {
    "path": "src/MiniApp/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\MiniApp;\n\nclass Server extends \\EasyWeChat\\OfficialAccount\\Server\n{\n}\n"
  },
  {
    "path": "src/MiniApp/Utils.php",
    "content": "<?php\n\nnamespace EasyWeChat\\MiniApp;\n\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\n\nclass Utils\n{\n    public function __construct(protected Application $app)\n    {\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function codeToSession(string $code): array\n    {\n        $response = $this->app->getHttpClient()->request('GET', '/sns/jscode2session', [\n            'query' => [\n                'appid' => $this->app->getAccount()->getAppId(),\n                'secret' => $this->app->getAccount()->getSecret(),\n                'js_code' => $code,\n                'grant_type' => 'authorization_code',\n            ],\n        ])->toArray(false);\n\n        if (empty($response['openid'])) {\n            throw new HttpException('code2Session error: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        return $response;\n    }\n\n    public function decryptSession(string $sessionKey, string $iv, string $ciphertext): array\n    {\n        return Decryptor::decrypt($sessionKey, $iv, $ciphertext);\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getPhoneNumber(string $code): array\n    {\n        $response = $this->app->createClient()->request('POST', '/wxa/business/getuserphonenumber', [\n            'json' => [\n                'code' => $code,\n            ],\n        ])->toArray(false);\n\n        if (isset($response['errcode']) && $response['errcode'] !== 0) {\n            throw new HttpException('getPhoneNumber error: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        if (empty($response['phone_info'])) {\n            throw new HttpException('getPhoneNumber error: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/AccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function intval;\nuse function is_string;\nuse function json_encode;\nuse function sprintf;\n\nclass AccessToken implements RefreshableAccessTokenInterface\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    const CACHE_KEY_PREFIX = 'official_account';\n\n    public function __construct(\n        protected string $appId,\n        protected string $secret,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null,\n        protected ?bool $stable = false\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://api.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('%s.access_token.%s.%s.%s', static::CACHE_KEY_PREFIX, $this->appId, $this->secret, (int) $this->stable);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getToken(): string\n    {\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    /**\n     * @return array{access_token:string}\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['access_token' => $this->getToken()];\n    }\n\n    public function refresh(): string\n    {\n        return $this->stable ? $this->getStableAccessToken() : $this->getAccessToken();\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getStableAccessToken(bool $force_refresh = false): string\n    {\n        $response = $this->httpClient->request(\n            'POST',\n            'https://api.weixin.qq.com/cgi-bin/stable_token',\n            [\n                'json' => [\n                    'grant_type' => 'client_credential',\n                    'appid' => $this->appId,\n                    'secret' => $this->secret,\n                    'force_refresh' => $force_refresh,\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['access_token'])) {\n            throw new HttpException('Failed to get stable access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));\n\n        return $response['access_token'];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAccessToken(): string\n    {\n        $response = $this->httpClient->request(\n            'GET',\n            'cgi-bin/token',\n            [\n                'query' => [\n                    'grant_type' => 'client_credential',\n                    'appid' => $this->appId,\n                    'secret' => $this->secret,\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['access_token'])) {\n            throw new HttpException('Failed to get access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));\n\n        return $response['access_token'];\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse EasyWeChat\\OfficialAccount\\Contracts\\Account as AccountInterface;\nuse RuntimeException;\n\nclass Account implements AccountInterface\n{\n    public function __construct(\n        protected string $appId,\n        protected ?string $secret,\n        protected ?string $token = null,\n        protected ?string $aesKey = null\n    ) {\n    }\n\n    public function getAppId(): string\n    {\n        return $this->appId;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function getSecret(): string\n    {\n        if ($this->secret === null) {\n            throw new RuntimeException('No secret configured.');\n        }\n\n        return $this->secret;\n    }\n\n    public function getToken(): ?string\n    {\n        return $this->token;\n    }\n\n    public function getAesKey(): ?string\n    {\n        return $this->aesKey;\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\JsApiTicket as JsApiTicketInterface;\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableJsApiTicket as RefreshableJsApiTicketInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenExpiredRetryStrategy;\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\OfficialAccount\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\OfficialAccount\\Contracts\\Application as ApplicationInterface;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface as SocialiteProviderInterface;\nuse Overtrue\\Socialite\\Providers\\WeChat;\nuse Psr\\Log\\LoggerAwareTrait;\nuse Symfony\\Component\\HttpClient\\Response\\AsyncContext;\nuse Symfony\\Component\\HttpClient\\RetryableHttpClient;\n\nuse function array_merge;\nuse function call_user_func;\nuse function sprintf;\nuse function str_contains;\n\nclass Application implements ApplicationInterface\n{\n    use InteractWithCache;\n    use InteractWithClient;\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?Encryptor $encryptor = null;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?AccountInterface $account = null;\n\n    protected AccessTokenInterface|RefreshableAccessTokenInterface|null $accessToken = null;\n\n    protected ?JsApiTicketInterface $ticket = null;\n\n    protected ?\\Closure $oauthFactory = null;\n\n    public function getAccount(): AccountInterface\n    {\n        if (! $this->account) {\n            $this->account = new Account(\n                appId: (string) $this->config->get('app_id'), /** @phpstan-ignore-line */\n                secret: (string) $this->config->get('secret'), /** @phpstan-ignore-line */\n                token: (string) $this->config->get('token'), /** @phpstan-ignore-line */\n                aesKey: (string) $this->config->get('aes_key'),/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->account;\n    }\n\n    public function setAccount(AccountInterface $account): static\n    {\n        $this->account = $account;\n\n        return $this;\n    }\n\n    /**\n     * @throws InvalidConfigException\n     */\n    public function getEncryptor(): Encryptor\n    {\n        if (! $this->encryptor) {\n            $token = $this->getAccount()->getToken();\n            $aesKey = $this->getAccount()->getAesKey();\n\n            if (empty($token) || empty($aesKey)) {\n                throw new InvalidConfigException('token or aes_key cannot be empty.');\n            }\n\n            $this->encryptor = new Encryptor(\n                appId: $this->getAccount()->getAppId(),\n                token: $token,\n                aesKey: $aesKey,\n                receiveId: $this->getAccount()->getAppId()\n            );\n        }\n\n        return $this->encryptor;\n    }\n\n    public function setEncryptor(Encryptor $encryptor): static\n    {\n        $this->encryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getServer(): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                request: $this->getRequest(),\n                encryptor: $this->getAccount()->getAesKey() ? $this->getEncryptor() : null\n            );\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function getAccessToken(): AccessTokenInterface|RefreshableAccessTokenInterface\n    {\n        if (! $this->accessToken) {\n            $this->accessToken = new AccessToken(\n                appId: $this->getAccount()->getAppId(),\n                secret: $this->getAccount()->getSecret(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n                stable: $this->config->get('use_stable_access_token', false),\n            );\n        }\n\n        return $this->accessToken;\n    }\n\n    public function setAccessToken(AccessTokenInterface|RefreshableAccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    public function setOAuthFactory(callable $factory): static\n    {\n        $this->oauthFactory = fn (Application $app): WeChat => $factory($app);\n\n        return $this;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function getOAuth(): SocialiteProviderInterface\n    {\n        if (! $this->oauthFactory) {\n            $this->oauthFactory = fn (self $app): SocialiteProviderInterface => (new WeChat(\n                [\n                    'client_id' => $this->getAccount()->getAppId(),\n                    'client_secret' => $this->getAccount()->getSecret(),\n                    'redirect_url' => $this->config->get('oauth.redirect_url'),\n                ]\n            ))->scopes((array) $this->config->get('oauth.scopes', ['snsapi_userinfo']));\n        }\n\n        $provider = call_user_func($this->oauthFactory, $this);\n\n        if (! $provider instanceof SocialiteProviderInterface) {\n            throw new InvalidArgumentException(sprintf(\n                'The factory must return a %s instance.',\n                SocialiteProviderInterface::class\n            ));\n        }\n\n        return $provider;\n    }\n\n    public function getTicket(): JsApiTicketInterface|RefreshableJsApiTicketInterface\n    {\n        if (! $this->ticket) {\n            $this->ticket = new JsApiTicket(\n                appId: $this->getAccount()->getAppId(),\n                secret: $this->getAccount()->getSecret(),\n                cache: $this->getCache(),\n                httpClient: $this->getClient(),\n                stable: $this->config->get('use_stable_access_token', false),\n            );\n        }\n\n        return $this->ticket;\n    }\n\n    public function setTicket(JsApiTicketInterface|RefreshableJsApiTicketInterface $ticket): static\n    {\n        $this->ticket = $ticket;\n\n        return $this;\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function getUtils(): Utils\n    {\n        return new Utils($this);\n    }\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        $httpClient = $this->getHttpClient();\n\n        if ((bool) $this->config->get('http.retry', false)) {\n            $httpClient = new RetryableHttpClient(\n                $httpClient,\n                $this->getRetryStrategy(),\n                (int) $this->config->get('http.max_retries', 2) // @phpstan-ignore-line\n            );\n        }\n\n        return (new AccessTokenAwareClient(\n            client: $httpClient,\n            accessToken: $this->getAccessToken(),\n            failureJudge: fn (Response $response) => (bool) ($response->toArray()['errcode'] ?? 0),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getRetryStrategy(): AccessTokenExpiredRetryStrategy\n    {\n        $retryConfig = RequestUtil::mergeDefaultRetryOptions((array) $this->config->get('http.retry', []));\n\n        return (new AccessTokenExpiredRetryStrategy($retryConfig))\n            ->decideUsing(function (AsyncContext $context, ?string $responseContent): bool {\n                return ! empty($responseContent)\n                    && str_contains($responseContent, '42001')\n                    && str_contains($responseContent, 'access_token expired');\n            });\n    }\n\n    /**\n     * @return array<string,mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return array_merge(\n            ['base_uri' => 'https://api.weixin.qq.com/'],\n            (array) $this->config->get('http', [])\n        );\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nclass Config extends \\EasyWeChat\\Kernel\\Config\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [\n        'app_id',\n    ];\n}\n"
  },
  {
    "path": "src/OfficialAccount/Contracts/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount\\Contracts;\n\ninterface Account\n{\n    public function getAppId(): string;\n\n    public function getSecret(): string;\n\n    public function getToken(): ?string;\n\n    public function getAesKey(): ?string;\n}\n"
  },
  {
    "path": "src/OfficialAccount/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Server;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getAccount(): Account;\n\n    public function getEncryptor(): Encryptor;\n\n    public function getServer(): Server;\n\n    public function getRequest(): ServerRequestInterface;\n\n    public function getClient(): AccessTokenAwareClient;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getConfig(): Config;\n\n    public function getAccessToken(): AccessToken;\n\n    public function getCache(): CacheInterface;\n\n    public function getOAuth(): ProviderInterface;\n\n    public function setOAuthFactory(callable $factory): static;\n}\n"
  },
  {
    "path": "src/OfficialAccount/JsApiTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableJsApiTicket as RefreshableJsApiTicketInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\n\nuse function sprintf;\n\nclass JsApiTicket extends AccessToken implements RefreshableJsApiTicketInterface\n{\n    public function getTicket(): string\n    {\n        $key = $this->getKey();\n        $ticket = $this->cache->get($key);\n\n        if ($ticket && \\is_string($ticket)) {\n            return $ticket;\n        }\n\n        return $this->refreshTicket();\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refreshTicket(): string\n    {\n        $response = $this->httpClient->request('GET', '/cgi-bin/ticket/getticket', ['query' => ['type' => 'jsapi']])\n            ->toArray(false);\n\n        if (empty($response['ticket'])) {\n            throw new HttpException('Failed to get jssdk ticket: '.\\json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['ticket'], \\intval($response['expires_in']));\n\n        return $response['ticket'];\n    }\n\n    /**\n     * @return array<string,mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'url' => 'string',\n        'nonceStr' => 'string',\n        'timestamp' => 'int',\n        'appId' => 'string',\n        'signature' => 'string',\n    ])]\n    public function configSignature(string $url, string $nonce, int $timestamp): array\n    {\n        return [\n            'url' => $url,\n            'nonceStr' => $nonce,\n            'timestamp' => $timestamp,\n            'appId' => $this->appId,\n            'signature' => sha1(sprintf(\n                'jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s',\n                $this->getTicket(),\n                $nonce,\n                $timestamp,\n                $url\n            )),\n        ];\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('official_account.jsapi_ticket.%s', $this->appId);\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/Message.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\n/**\n * @property string $Event\n * @property string $MsgType\n */\nclass Message extends \\EasyWeChat\\Kernel\\Message\n{\n    //\n}\n"
  },
  {
    "path": "src/OfficialAccount/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Kernel\\Traits\\RespondXmlMessage;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Throwable;\n\nclass Server implements ServerInterface\n{\n    use DecryptMessage;\n    use InteractWithHandlers;\n    use InteractWithServerRequest;\n    use RespondXmlMessage;\n\n    public function __construct(\n        ?ServerRequestInterface $request = null,\n        protected ?Encryptor $encryptor = null,\n    ) {\n        $this->request = $request;\n    }\n\n    public function serve(): ResponseInterface\n    {\n        if ($str = $this->getRequest()->getQueryParams()['echostr'] ?? '') {\n            return new Response(200, [], $str);\n        }\n\n        $message = $this->getRequestMessage($this->getRequest());\n        $query = $this->getRequest()->getQueryParams();\n\n        if ($this->encryptor && ! empty($query['msg_signature'])) {\n            $this->prepend($this->decryptRequestMessage($query));\n        }\n\n        $response = $this->handle(new Response(200, [], 'success'), $message);\n\n        if (! ($response instanceof ResponseInterface)) {\n            $response = $this->transformToReply($response, $message, $this->encryptor);\n        }\n\n        return ServerResponse::make($response);\n    }\n\n    /**\n     * @throws Throwable\n     */\n    public function addMessageListener(string $type, callable|string $handler): static\n    {\n        $handler = $this->makeClosure($handler);\n        $this->withHandler(\n            function (Message $message, Closure $next) use ($type, $handler): mixed {\n                return $message->MsgType === $type ? $handler($message, $next) : $next($message);\n            }\n        );\n\n        return $this;\n    }\n\n    public function addEventListener(string $event, callable|string $handler): static\n    {\n        $handler = $this->makeClosure($handler);\n        $this->withHandler(\n            function (Message $message, Closure $next) use ($event, $handler): mixed {\n                return $message->Event === $event ? $handler($message, $next) : $next($message);\n            }\n        );\n\n        return $this;\n    }\n\n    /**\n     * @param  array<string,string>  $query\n     *\n     * @psalm-suppress PossiblyNullArgument\n     */\n    protected function decryptRequestMessage(array $query): Closure\n    {\n        return function (Message $message, Closure $next) use ($query): mixed {\n            if (! $this->encryptor) {\n                return null;\n            }\n\n            $this->decryptIncomingMessage($message, $query);\n\n            return $next($message);\n        };\n    }\n\n    public function getRequestMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        return Message::createFromRequest($request ?? $this->getRequest());\n    }\n\n    public function getDecryptedMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        $request = $request ?? $this->getRequest();\n        $message = $this->getRequestMessage($request);\n        $query = $request->getQueryParams();\n\n        if (! $this->encryptor || empty($query['msg_signature'])) {\n            return $message;\n        }\n\n        return $this->decryptIncomingMessage($message, $query);\n    }\n\n    /**\n     * @param  array<string,string>  $query\n     */\n    protected function decryptIncomingMessage(\\EasyWeChat\\Kernel\\Message $message, array $query): \\EasyWeChat\\Kernel\\Message\n    {\n        if (! $this->encryptor) {\n            return $message;\n        }\n\n        $signature = $query['msg_signature'] ?? '';\n        $timestamp = $query['timestamp'] ?? '';\n        $nonce = $query['nonce'] ?? '';\n\n        return $this->decryptMessage(\n            message: $message,\n            encryptor: $this->encryptor,\n            signature: $signature,\n            timestamp: $timestamp,\n            nonce: $nonce\n        );\n    }\n}\n"
  },
  {
    "path": "src/OfficialAccount/Utils.php",
    "content": "<?php\n\nnamespace EasyWeChat\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Support\\Str;\n\nuse function time;\n\nclass Utils\n{\n    public function __construct(protected Application $app)\n    {\n    }\n\n    /**\n     * @param  array<string>  $jsApiList\n     * @param  array<string>  $openTagList\n     * @return array<string, mixed>\n     */\n    public function buildJsSdkConfig(\n        string $url,\n        array $jsApiList = [],\n        array $openTagList = [],\n        bool $debug = false\n    ): array {\n        return array_merge(\n            compact('jsApiList', 'openTagList', 'debug'),\n            $this->app->getTicket()->configSignature($url, Str::random(), time())\n        );\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse EasyWeChat\\OpenPlatform\\Contracts\\Account as AccountInterface;\n\nclass Account implements AccountInterface\n{\n    public function __construct(\n        protected string $appId,\n        protected string $secret,\n        protected string $token,\n        protected string $aesKey\n    ) {\n    }\n\n    public function getAppId(): string\n    {\n        return $this->appId;\n    }\n\n    public function getSecret(): string\n    {\n        return $this->secret;\n    }\n\n    public function getToken(): string\n    {\n        return $this->token;\n    }\n\n    public function getAesKey(): string\n    {\n        return $this->aesKey;\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Support\\Arr;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\MiniApp\\Application as MiniAppApplication;\nuse EasyWeChat\\OfficialAccount\\Application as OfficialAccountApplication;\nuse EasyWeChat\\OfficialAccount\\Config as OfficialAccountConfig;\nuse EasyWeChat\\OpenPlatform\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\OpenPlatform\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\OpenPlatform\\Contracts\\VerifyTicket as VerifyTicketInterface;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface as SocialiteProviderInterface;\nuse Overtrue\\Socialite\\Providers\\WeChat;\nuse Psr\\Log\\LoggerAwareTrait;\n\nuse function array_merge;\nuse function is_string;\nuse function md5;\nuse function sprintf;\n\nclass Application implements ApplicationInterface\n{\n    use InteractWithCache;\n    use InteractWithClient;\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?Encryptor $encryptor = null;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?AccountInterface $account = null;\n\n    protected ?AccessTokenInterface $componentAccessToken = null;\n\n    protected ?VerifyTicketInterface $verifyTicket = null;\n\n    public function getAccount(): AccountInterface\n    {\n        if (! $this->account) {\n            $this->account = new Account(\n                appId: (string) $this->config->get('app_id'), /** @phpstan-ignore-line */\n                secret: (string) $this->config->get('secret'), /** @phpstan-ignore-line */\n                token: (string) $this->config->get('token'), /** @phpstan-ignore-line */\n                aesKey: (string) $this->config->get('aes_key'),/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->account;\n    }\n\n    public function setAccount(AccountInterface $account): static\n    {\n        $this->account = $account;\n\n        return $this;\n    }\n\n    public function getVerifyTicket(): VerifyTicketInterface\n    {\n        if (! $this->verifyTicket) {\n            $this->verifyTicket = new VerifyTicket(\n                appId: $this->getAccount()->getAppId(),\n                cache: $this->getCache(),\n            );\n        }\n\n        return $this->verifyTicket;\n    }\n\n    public function setVerifyTicket(VerifyTicketInterface $verifyTicket): static\n    {\n        $this->verifyTicket = $verifyTicket;\n\n        return $this;\n    }\n\n    public function getEncryptor(): Encryptor\n    {\n        if (! $this->encryptor) {\n            $this->encryptor = new Encryptor(\n                appId: $this->getAccount()->getAppId(),\n                token: $this->getAccount()->getToken(),\n                aesKey: $this->getAccount()->getAesKey(),\n                receiveId: $this->getAccount()->getAppId(),\n            );\n        }\n\n        return $this->encryptor;\n    }\n\n    public function setEncryptor(Encryptor $encryptor): static\n    {\n        $this->encryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getServer(): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                encryptor: $this->getEncryptor(),\n                request: $this->getRequest()\n            );\n        }\n\n        if ($this->server instanceof Server) {\n            $this->server->withDefaultVerifyTicketHandler(\n                function (Message $message, Closure $next): mixed {\n                    $ticket = $this->getVerifyTicket();\n                    $ticket->setTicket($message->ComponentVerifyTicket);\n\n                    return $next($message);\n                }\n            );\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function getAccessToken(): AccessTokenInterface\n    {\n        return $this->getComponentAccessToken();\n    }\n\n    public function getComponentAccessToken(): AccessTokenInterface\n    {\n        if (! $this->componentAccessToken) {\n            $this->componentAccessToken = new ComponentAccessToken(\n                appId: $this->getAccount()->getAppId(),\n                secret: $this->getAccount()->getSecret(),\n                verifyTicket: $this->getVerifyTicket(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n            );\n        }\n\n        return $this->componentAccessToken;\n    }\n\n    public function setComponentAccessToken(AccessTokenInterface $componentAccessToken): static\n    {\n        $this->componentAccessToken = $componentAccessToken;\n\n        return $this;\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAuthorization(string $authorizationCode): Authorization\n    {\n        $response = $this->getClient()->request(\n            'POST',\n            'cgi-bin/component/api_query_auth',\n            [\n                'json' => [\n                    'component_appid' => $this->getAccount()->getAppId(),\n                    'authorization_code' => $authorizationCode,\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['authorization_info'])) {\n            throw new HttpException('Failed to get authorization_info: '.json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        return new Authorization($response);\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refreshAuthorizerToken(string $authorizerAppId, string $authorizerRefreshToken): array\n    {\n        $response = $this->getClient()->request(\n            'POST',\n            'cgi-bin/component/api_authorizer_token',\n            [\n                'json' => [\n                    'component_appid' => $this->getAccount()->getAppId(),\n                    'authorizer_appid' => $authorizerAppId,\n                    'authorizer_refresh_token' => $authorizerRefreshToken,\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['authorizer_access_token'])) {\n            throw new HttpException('Failed to get authorizer_access_token: '.json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        return $response;\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function createPreAuthorizationCode(): array\n    {\n        $response = $this->getClient()->request(\n            'POST',\n            'cgi-bin/component/api_create_preauthcode',\n            [\n                'json' => [\n                    'component_appid' => $this->getAccount()->getAppId(),\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['pre_auth_code'])) {\n            throw new HttpException('Failed to get authorizer_access_token: '.json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        return $response;\n    }\n\n    public function createPreAuthorizationUrl(string $callbackUrl, array|string $optional = []): string\n    {\n        // 兼容旧版 API 设计\n        if (is_string($optional)) {\n            $optional = [\n                'pre_auth_code' => $optional,\n            ];\n        } else {\n            $optional['pre_auth_code'] = Arr::get($this->createPreAuthorizationCode(), 'pre_auth_code');\n        }\n\n        $queries = array_merge(\n            $optional,\n            [\n                'component_appid' => $this->getAccount()->getAppId(),\n                'redirect_uri' => $callbackUrl,\n            ]\n        );\n\n        return 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?'.http_build_query($queries);\n    }\n\n    public function getOAuth(): SocialiteProviderInterface\n    {\n        return (new WeChat(\n            [\n                'client_id' => $this->getAccount()->getAppId(),\n                'client_secret' => $this->getAccount()->getSecret(),\n                'redirect_url' => $this->config->get('oauth.redirect_url'),\n            ]\n        ))->scopes((array) $this->config->get('oauth.scopes', ['snsapi_userinfo']));\n    }\n\n    public function getOfficialAccountWithRefreshToken(\n        string $appId,\n        string $refreshToken,\n        array $config = []\n    ): OfficialAccountApplication {\n        return $this->getOfficialAccountWithAccessToken(\n            $appId,\n            $this->getAuthorizerAccessToken($appId, $refreshToken),\n            $config\n        );\n    }\n\n    public function getOfficialAccountWithAccessToken(\n        string $appId,\n        string $accessToken,\n        array $config = []\n    ): OfficialAccountApplication {\n        return $this->getOfficialAccount(new AuthorizerAccessToken($appId, $accessToken), $config);\n    }\n\n    public function getOfficialAccount(\n        AuthorizerAccessToken $authorizerAccessToken,\n        array $config = []\n    ): OfficialAccountApplication {\n        $config = new OfficialAccountConfig(\n            array_merge(\n                [\n                    'app_id' => $authorizerAccessToken->getAppId(),\n                    'token' => $this->config->get('token'),\n                    'aes_key' => $this->config->get('aes_key'),\n                    'logging' => $this->config->get('logging'),\n                    'http' => $this->config->get('http', []),\n                ],\n                $config\n            )\n        );\n\n        $app = new OfficialAccountApplication($config);\n\n        $app->setAccessToken($authorizerAccessToken);\n        $app->setEncryptor($this->getEncryptor());\n        $app->setOAuthFactory($this->createAuthorizerOAuthFactory($authorizerAccessToken->getAppId(), $config));\n\n        return $app;\n    }\n\n    public function getMiniAppWithRefreshToken(\n        string $appId,\n        string $refreshToken,\n        array $config = []\n    ): MiniAppApplication {\n        return $this->getMiniAppWithAccessToken(\n            $appId,\n            $this->getAuthorizerAccessToken($appId, $refreshToken),\n            $config\n        );\n    }\n\n    public function getMiniAppWithAccessToken(\n        string $appId,\n        string $accessToken,\n        array $config = []\n    ): MiniAppApplication {\n        return $this->getMiniApp(new AuthorizerAccessToken($appId, $accessToken), $config);\n    }\n\n    public function getMiniApp(AuthorizerAccessToken $authorizerAccessToken, array $config = []): MiniAppApplication\n    {\n        $app = new MiniAppApplication(\n            array_merge(\n                [\n                    'app_id' => $authorizerAccessToken->getAppId(),\n                    'token' => $this->config->get('token'),\n                    'aes_key' => $this->config->get('aes_key'),\n                    'logging' => $this->config->get('logging'),\n                    'http' => $this->config->get('http'),\n                ],\n                $config\n            )\n        );\n\n        $app->setAccessToken($authorizerAccessToken);\n        $app->setEncryptor($this->getEncryptor());\n\n        return $app;\n    }\n\n    protected function createAuthorizerOAuthFactory(string $authorizerAppId, OfficialAccountConfig $config): Closure\n    {\n        return fn () => (new WeChat(\n            [\n                'client_id' => $authorizerAppId,\n\n                'component' => [\n                    'component_app_id' => $this->getAccount()->getAppId(),\n                    'component_access_token' => fn () => $this->getComponentAccessToken()->getToken(),\n                ],\n\n                'redirect_url' => $this->config->get('oauth.redirect_url'),\n            ]\n        ))->scopes((array) $config->get('oauth.scopes', ['snsapi_userinfo']));\n    }\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        return (new AccessTokenAwareClient(\n            client: $this->getHttpClient(),\n            accessToken: $this->getComponentAccessToken(),\n            failureJudge: fn (Response $response) => (bool) ($response->toArray()['errcode'] ?? 0),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getAuthorizerAccessToken(string $appId, string $refreshToken): string\n    {\n        $cacheKey = sprintf('open-platform.authorizer_access_token.%s.%s', $appId, md5($refreshToken));\n\n        /** @phpstan-ignore-next-line */\n        $authorizerAccessToken = (string) $this->getCache()->get($cacheKey);\n\n        if (! $authorizerAccessToken) {\n            $response = $this->refreshAuthorizerToken($appId, $refreshToken);\n            $authorizerAccessToken = (string) $response['authorizer_access_token'];\n            $this->getCache()->set($cacheKey, $authorizerAccessToken, intval($response['expires_in'] ?? 7200) - 500);\n        }\n\n        return $authorizerAccessToken;\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return array_merge(\n            ['base_uri' => 'https://api.weixin.qq.com/'],\n            (array) $this->config->get('http', [])\n        );\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/Authorization.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse ArrayAccess;\nuse EasyWeChat\\Kernel\\Contracts\\Arrayable;\nuse EasyWeChat\\Kernel\\Contracts\\Jsonable;\nuse EasyWeChat\\Kernel\\Traits\\HasAttributes;\n\n/**\n * @implements ArrayAccess<string, mixed>\n */\nclass Authorization implements Arrayable, ArrayAccess, Jsonable\n{\n    use HasAttributes;\n\n    public function getAppId(): string\n    {\n        /** @phpstan-ignore-next-line */\n        return (string) $this->attributes['authorization_info']['authorizer_appid'] ?? '';\n    }\n\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function getAccessToken(): AuthorizerAccessToken\n    {\n        return new AuthorizerAccessToken(\n            /** @phpstan-ignore-next-line */\n            $this->attributes['authorization_info']['authorizer_appid'] ?? '',\n\n            /** @phpstan-ignore-next-line */\n            $this->attributes['authorization_info']['authorizer_access_token'] ?? ''\n        );\n    }\n\n    public function getRefreshToken(): string\n    {\n        /** @phpstan-ignore-next-line */\n        return $this->attributes['authorization_info']['authorizer_refresh_token'] ?? '';\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/AuthorizerAccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse Stringable;\n\nclass AuthorizerAccessToken implements AccessToken, Stringable\n{\n    public function __construct(protected string $appId, protected string $accessToken)\n    {\n    }\n\n    public function getAppId(): string\n    {\n        return $this->appId;\n    }\n\n    public function getToken(): string\n    {\n        return $this->accessToken;\n    }\n\n    public function __toString()\n    {\n        return $this->accessToken;\n    }\n\n    /**\n     * @return array<string, string>\n     */\n    #[\\JetBrains\\PhpStorm\\Pure]\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['access_token' => $this->getToken()];\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/ComponentAccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse EasyWeChat\\OpenPlatform\\Contracts\\VerifyTicket as VerifyTicketInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function abs;\nuse function intval;\nuse function json_encode;\n\nclass ComponentAccessToken implements RefreshableAccessTokenInterface\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $appId,\n        protected string $secret,\n        protected VerifyTicketInterface $verifyTicket,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null,\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://api.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = \\sprintf('open_platform.component_access_token.%s', $this->appId);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getToken(): string\n    {\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && \\is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['component_access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['component_access_token' => $this->getToken()];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refresh(): string\n    {\n        $response = $this->httpClient->request(\n            'POST',\n            'cgi-bin/component/api_component_token',\n            [\n                'json' => [\n                    'component_appid' => $this->appId,\n                    'component_appsecret' => $this->secret,\n                    'component_verify_ticket' => $this->verifyTicket->getTicket(),\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['component_access_token'])) {\n            throw new HttpException('Failed to get component_access_token: '.json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        $this->cache->set(\n            $this->getKey(),\n            $response['component_access_token'],\n            abs(intval($response['expires_in']) - 100)\n        );\n\n        return $response['component_access_token'];\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nclass Config extends \\EasyWeChat\\Kernel\\Config\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [\n        'app_id',\n        'secret',\n        'aes_key',\n    ];\n}\n"
  },
  {
    "path": "src/OpenPlatform/Contracts/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform\\Contracts;\n\ninterface Account\n{\n    public function getAppId(): string;\n\n    public function getSecret(): string;\n\n    public function getToken(): string;\n\n    public function getAesKey(): string;\n}\n"
  },
  {
    "path": "src/OpenPlatform/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Server;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\MiniApp\\Application as MiniAppApplication;\nuse EasyWeChat\\OfficialAccount\\Application as OfficialAccountApplication;\nuse EasyWeChat\\OpenPlatform\\AuthorizerAccessToken;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getAccount(): Account;\n\n    public function getEncryptor(): Encryptor;\n\n    public function getServer(): Server;\n\n    public function getRequest(): ServerRequestInterface;\n\n    public function getClient(): AccessTokenAwareClient;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getConfig(): Config;\n\n    public function getComponentAccessToken(): AccessToken;\n\n    public function getCache(): CacheInterface;\n\n    public function getOAuth(): ProviderInterface;\n\n    /**\n     * @param  array<string, mixed>  $config\n     */\n    public function getMiniApp(AuthorizerAccessToken $authorizerAccessToken, array $config): MiniAppApplication;\n\n    /**\n     * @param  array<string, mixed>  $config\n     */\n    public function getOfficialAccount(\n        AuthorizerAccessToken $authorizerAccessToken,\n        array $config\n    ): OfficialAccountApplication;\n}\n"
  },
  {
    "path": "src/OpenPlatform/Contracts/VerifyTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform\\Contracts;\n\ninterface VerifyTicket\n{\n    public function getTicket(): string;\n\n    public function setTicket(string $ticket): static;\n}\n"
  },
  {
    "path": "src/OpenPlatform/Message.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\n/**\n * @property string $InfoType\n * @property string $ComponentVerifyTicket\n */\nclass Message extends \\EasyWeChat\\Kernel\\Message\n{\n    //\n}\n"
  },
  {
    "path": "src/OpenPlatform/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Kernel\\Traits\\RespondXmlMessage;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nuse function func_get_args;\n\nclass Server implements ServerInterface\n{\n    use DecryptMessage;\n    use InteractWithHandlers;\n    use InteractWithServerRequest;\n    use RespondXmlMessage;\n\n    protected ?Closure $defaultVerifyTicketHandler = null;\n\n    public function __construct(\n        protected Encryptor $encryptor,\n        ?ServerRequestInterface $request = null,\n    ) {\n        $this->request = $request;\n    }\n\n    public function serve(): ResponseInterface\n    {\n        if ($str = $this->getRequest()->getQueryParams()['echostr'] ?? '') {\n            return new Response(200, [], $str);\n        }\n\n        $message = $this->getRequestMessage($this->getRequest());\n\n        $this->prepend($this->decryptRequestMessage());\n\n        $response = $this->handle(new Response(200, [], 'success'), $message);\n\n        if (! ($response instanceof ResponseInterface)) {\n            $response = $this->transformToReply($response, $message, $this->encryptor);\n        }\n\n        return ServerResponse::make($response);\n    }\n\n    public function handleAuthorized(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'authorized' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUnauthorized(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'unauthorized' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleAuthorizeUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'updateauthorized' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function withDefaultVerifyTicketHandler(callable $handler): void\n    {\n        $this->defaultVerifyTicketHandler = fn (): mixed => $handler(...func_get_args());\n        $this->handleVerifyTicketRefreshed($this->defaultVerifyTicketHandler);\n    }\n\n    public function handleVerifyTicketRefreshed(callable $handler): static\n    {\n        if ($this->defaultVerifyTicketHandler) {\n            $this->withoutHandler($this->defaultVerifyTicketHandler);\n        }\n\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'component_verify_ticket' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    protected function decryptRequestMessage(): Closure\n    {\n        $query = $this->getRequest()->getQueryParams();\n\n        return function (Message $message, Closure $next) use ($query): mixed {\n            $message = $this->decryptMessage(\n                message: $message,\n                encryptor: $this->encryptor,\n                signature: $query['msg_signature'] ?? '',\n                timestamp: $query['timestamp'] ?? '',\n                nonce: $query['nonce'] ?? ''\n            );\n\n            return $next($message);\n        };\n    }\n\n    public function getRequestMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        return Message::createFromRequest($request ?? $this->getRequest());\n    }\n\n    public function getDecryptedMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        $request = $request ?? $this->getRequest();\n        $message = $this->getRequestMessage($request);\n        $query = $request->getQueryParams();\n\n        return $this->decryptMessage(\n            message: $message,\n            encryptor: $this->encryptor,\n            signature: $query['msg_signature'] ?? '',\n            timestamp: $query['timestamp'] ?? '',\n            nonce: $query['nonce'] ?? ''\n        );\n    }\n}\n"
  },
  {
    "path": "src/OpenPlatform/VerifyTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenPlatform;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\OpenPlatform\\Contracts\\VerifyTicket as VerifyTicketInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\nuse function is_string;\nuse function sprintf;\n\nclass VerifyTicket implements VerifyTicketInterface\n{\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $appId,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n    ) {\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('open_platform.verify_ticket.%s', $this->appId);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function setTicket(string $ticket): static\n    {\n        $this->cache->set($this->getKey(), $ticket, 6000);\n\n        return $this;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function getTicket(): string\n    {\n        $ticket = $this->cache->get($this->getKey());\n\n        if (! $ticket || ! is_string($ticket)) {\n            throw new RuntimeException('No component_verify_ticket found.');\n        }\n\n        return $ticket;\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\OpenWork\\Contracts\\Account as AccountInterface;\n\nclass Account implements AccountInterface\n{\n    public function __construct(\n        protected string $corpId,\n        protected string $providerSecret,\n        protected string $suiteId,\n        protected string $suiteSecret,\n        protected string $token,\n        protected string $aesKey\n    ) {\n    }\n\n    public function getCorpId(): string\n    {\n        return $this->corpId;\n    }\n\n    public function getProviderSecret(): string\n    {\n        return $this->providerSecret;\n    }\n\n    public function getSuiteId(): string\n    {\n        return $this->suiteId;\n    }\n\n    public function getSuiteSecret(): string\n    {\n        return $this->suiteSecret;\n    }\n\n    public function getToken(): string\n    {\n        return $this->token;\n    }\n\n    public function getAesKey(): string\n    {\n        return $this->aesKey;\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\OpenWork\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\OpenWork\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\OpenWork\\Contracts\\SuiteTicket as SuiteTicketInterface;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface as SocialiteProviderInterface;\nuse Overtrue\\Socialite\\Providers\\OpenWeWork;\nuse Psr\\Log\\LoggerAwareTrait;\n\nuse function array_merge;\n\nclass Application implements ApplicationInterface\n{\n    use InteractWithCache;\n    use InteractWithClient;\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?AccountInterface $account = null;\n\n    protected ?Encryptor $encryptor = null;\n\n    protected ?SuiteEncryptor $suiteEncryptor = null;\n\n    protected ?SuiteTicketInterface $suiteTicket = null;\n\n    protected ?AccessTokenInterface $accessToken = null;\n\n    protected ?AccessTokenInterface $suiteAccessToken = null;\n\n    protected ?AuthorizerAccessToken $authorizerAccessToken = null;\n\n    public function getAccount(): AccountInterface\n    {\n        if (! $this->account) {\n            $this->account = new Account(\n                corpId: (string) $this->config->get('corp_id'), /** @phpstan-ignore-line */\n                providerSecret: (string) $this->config->get('provider_secret'), /** @phpstan-ignore-line */\n                suiteId: (string) $this->config->get('suite_id'), /** @phpstan-ignore-line */\n                suiteSecret: (string) $this->config->get('suite_secret'), /** @phpstan-ignore-line */\n                token: (string) $this->config->get('token'), /** @phpstan-ignore-line */\n                aesKey: (string) $this->config->get('aes_key'),/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->account;\n    }\n\n    public function setAccount(AccountInterface $account): static\n    {\n        $this->account = $account;\n\n        return $this;\n    }\n\n    public function getEncryptor(): Encryptor\n    {\n        if (! $this->encryptor) {\n            $this->encryptor = new Encryptor(\n                corpId: $this->getAccount()->getCorpId(),\n                token: $this->getAccount()->getToken(),\n                aesKey: $this->getAccount()->getAesKey(),\n            );\n        }\n\n        return $this->encryptor;\n    }\n\n    public function setEncryptor(Encryptor $encryptor): static\n    {\n        $this->encryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getSuiteEncryptor(): SuiteEncryptor\n    {\n        if (! $this->suiteEncryptor) {\n            $this->suiteEncryptor = new SuiteEncryptor(\n                suiteId: $this->getAccount()->getSuiteId(),\n                token: $this->getAccount()->getToken(),\n                aesKey: $this->getAccount()->getAesKey(),\n            );\n        }\n\n        return $this->suiteEncryptor;\n    }\n\n    public function setSuiteEncryptor(SuiteEncryptor $encryptor): static\n    {\n        $this->suiteEncryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getServer(): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                encryptor: $this->getSuiteEncryptor(),\n                providerEncryptor: $this->getEncryptor(),\n                request: $this->getRequest(),\n            );\n\n            $this->server->withDefaultSuiteTicketHandler(function (Message $message, \\Closure $next): mixed {\n                if ($message->SuiteId === $this->getAccount()->getSuiteId()) {\n                    $this->getSuiteTicket()->setTicket($message->SuiteTicket);\n                }\n\n                return $next($message);\n            });\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function getProviderAccessToken(): AccessTokenInterface\n    {\n        if (! $this->accessToken) {\n            $this->accessToken = new ProviderAccessToken(\n                corpId: $this->getAccount()->getCorpId(),\n                providerSecret: $this->getAccount()->getProviderSecret(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n            );\n        }\n\n        return $this->accessToken;\n    }\n\n    public function setProviderAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    public function getSuiteAccessToken(): AccessTokenInterface\n    {\n        if (! $this->suiteAccessToken) {\n            $this->suiteAccessToken = new SuiteAccessToken(\n                suiteId: $this->getAccount()->getSuiteId(),\n                suiteSecret: $this->getAccount()->getSuiteSecret(),\n                suiteTicket: $this->getSuiteTicket(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n            );\n        }\n\n        return $this->suiteAccessToken;\n    }\n\n    public function setSuiteAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->suiteAccessToken = $accessToken;\n\n        return $this;\n    }\n\n    public function getSuiteTicket(): SuiteTicketInterface\n    {\n        if (! $this->suiteTicket) {\n            $this->suiteTicket = new SuiteTicket(\n                suiteId: $this->getAccount()->getSuiteId(),\n                cache: $this->getCache(),\n            );\n        }\n\n        return $this->suiteTicket;\n    }\n\n    public function setSuiteTicket(SuiteTicketInterface $suiteTicket): SuiteTicketInterface\n    {\n        $this->suiteTicket = $suiteTicket;\n\n        return $this->suiteTicket;\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAuthorization(\n        string $corpId,\n        string $permanentCode,\n        ?AccessTokenInterface $suiteAccessToken = null\n    ): Authorization {\n        $suiteAccessToken = $suiteAccessToken ?? $this->getSuiteAccessToken();\n\n        $response = $this->getHttpClient()->request('POST', 'cgi-bin/service/get_auth_info', [\n            'query' => [\n                'suite_access_token' => $suiteAccessToken->getToken(),\n            ],\n            'json' => [\n                'auth_corpid' => $corpId,\n                'permanent_code' => $permanentCode,\n            ],\n        ])->toArray(false);\n\n        if (empty($response['auth_corp_info'])) {\n            throw new HttpException('Failed to get auth_corp_info: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        return new Authorization($response);\n    }\n\n    public function getAuthorizerAccessToken(\n        string $corpId,\n        string $permanentCode,\n        ?AccessTokenInterface $suiteAccessToken = null\n    ): AuthorizerAccessToken {\n        $suiteAccessToken = $suiteAccessToken ?? $this->getSuiteAccessToken();\n\n        return new AuthorizerAccessToken(\n            corpId: $corpId,\n            permanentCodeOrAccessToken: $permanentCode,\n            suiteAccessToken: $suiteAccessToken,\n            cache: $this->getCache(),\n            httpClient: $this->getHttpClient(),\n        );\n    }\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        return (new AccessTokenAwareClient(\n            client: $this->getHttpClient(),\n            accessToken: $this->getProviderAccessToken(),\n            failureJudge: fn (Response $response) => (bool) ($response->toArray()['errcode'] ?? 0),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getAuthorizerClient(string $corpId, string $permanentCode, ?AccessTokenInterface $suiteAccessToken = null): AccessTokenAwareClient\n    {\n        return (new AccessTokenAwareClient(\n            client: $this->getHttpClient(),\n            accessToken: $this->getAuthorizerAccessToken($corpId, $permanentCode, $suiteAccessToken),\n            failureJudge: fn (Response $response) => (bool) ($response->toArray()['errcode'] ?? 0),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getJsApiTicket(string $corpId, string $permanentCode, ?AccessTokenInterface $suiteAccessToken = null): JsApiTicket\n    {\n        return new JsApiTicket(\n            corpId: $corpId,\n            cache: $this->getCache(),\n            httpClient: $this->getAuthorizerClient($corpId, $permanentCode, $suiteAccessToken),\n        );\n    }\n\n    public function getOAuth(\n        string $suiteId,\n        ?AccessTokenInterface $suiteAccessToken = null\n    ): SocialiteProviderInterface {\n        $suiteAccessToken = $suiteAccessToken ?? $this->getSuiteAccessToken();\n\n        return (new OpenWeWork(array_filter([\n            'client_id' => $suiteId,\n            'redirect_url' => $this->config->get('oauth.redirect_url'),\n            'base_url' => $this->config->get('http.base_uri'),\n        ])))->withSuiteTicket($this->getSuiteTicket()->getTicket())\n            ->withSuiteAccessToken($suiteAccessToken->getToken())\n            ->scopes((array) $this->config->get('oauth.scopes', ['snsapi_base']));\n    }\n\n    public function getCorpOAuth(\n        string $corpId,\n        ?AccessTokenInterface $suiteAccessToken = null\n    ): SocialiteProviderInterface {\n        $suiteAccessToken = $suiteAccessToken ?? $this->getSuiteAccessToken();\n\n        return (new OpenWeWork(array_filter([\n            'client_id' => $corpId,\n            'redirect_url' => $this->config->get('oauth.redirect_url'),\n            'base_url' => $this->config->get('http.base_uri'),\n        ])))->withSuiteTicket($this->getSuiteTicket()->getTicket())\n            ->withSuiteAccessToken($suiteAccessToken->getToken())\n            ->scopes((array) $this->config->get('oauth.scopes', ['snsapi_base']));\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return array_merge(\n            ['base_uri' => 'https://qyapi.weixin.qq.com/'],\n            (array) $this->config->get('http', [])\n        );\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Authorization.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse ArrayAccess;\nuse EasyWeChat\\Kernel\\Contracts\\Arrayable;\nuse EasyWeChat\\Kernel\\Contracts\\Jsonable;\nuse EasyWeChat\\Kernel\\Traits\\HasAttributes;\n\n/**\n * @implements ArrayAccess<string, mixed>\n */\nclass Authorization implements Arrayable, ArrayAccess, Jsonable\n{\n    use HasAttributes;\n\n    public function getAppId(): string\n    {\n        /** @phpstan-ignore-next-line */\n        return (string) $this->attributes['auth_corp_info']['corpid'];\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/AuthorizerAccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Stringable;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass AuthorizerAccessToken implements RefreshableAccessToken, Stringable\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $corpId,\n        protected string $permanentCodeOrAccessToken,\n        protected ?AccessTokenInterface $suiteAccessToken = null,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null,\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getCorpId(): string\n    {\n        return $this->corpId;\n    }\n\n    public function getToken(): string\n    {\n        if (! isset($this->suiteAccessToken)) {\n            return $this->permanentCodeOrAccessToken;\n        }\n\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    public function __toString()\n    {\n        return $this->getToken();\n    }\n\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['access_token' => $this->getToken()];\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('open_work.authorizer.access_token.%s.%s', $this->corpId, $this->permanentCodeOrAccessToken);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refresh(): string\n    {\n        if (! isset($this->suiteAccessToken)) {\n            return '';\n        }\n\n        $response = $this->httpClient->request('POST', 'cgi-bin/service/get_corp_token', [\n            'query' => [\n                'suite_access_token' => $this->suiteAccessToken->getToken(),\n            ],\n            'json' => [\n                'auth_corpid' => $this->corpId,\n                'permanent_code' => $this->permanentCodeOrAccessToken,\n            ],\n        ])->toArray(false);\n\n        if (empty($response['access_token'])) {\n            throw new HttpException('Failed to get access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));\n\n        return $response['access_token'];\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nclass Config extends \\EasyWeChat\\Kernel\\Config\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [\n        'corp_id',\n        'suite_id',\n        'provider_secret',\n        'suite_secret',\n        'token',\n        'aes_key',\n    ];\n}\n"
  },
  {
    "path": "src/OpenWork/Contracts/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork\\Contracts;\n\ninterface Account\n{\n    public function getCorpId(): string;\n\n    public function getProviderSecret(): string;\n\n    public function getSuiteId(): string;\n\n    public function getSuiteSecret(): string;\n\n    public function getToken(): string;\n\n    public function getAesKey(): string;\n}\n"
  },
  {
    "path": "src/OpenWork/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Server;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getAccount(): Account;\n\n    public function getEncryptor(): Encryptor;\n\n    public function getSuiteEncryptor(): Encryptor;\n\n    public function getServer(): Server;\n\n    public function getRequest(): ServerRequestInterface;\n\n    public function getClient(): AccessTokenAwareClient;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getConfig(): Config;\n\n    public function getProviderAccessToken(): AccessToken;\n\n    public function getCache(): CacheInterface;\n}\n"
  },
  {
    "path": "src/OpenWork/Contracts/SuiteTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork\\Contracts;\n\ninterface SuiteTicket\n{\n    public function getTicket(): string;\n\n    public function setTicket(string $ticket): static;\n}\n"
  },
  {
    "path": "src/OpenWork/Encryptor.php",
    "content": "<?php\n\nnamespace EasyWeChat\\OpenWork;\n\nclass Encryptor extends \\EasyWeChat\\Kernel\\Encryptor\n{\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function __construct(string $corpId, string $token, string $aesKey)\n    {\n        parent::__construct($corpId, $token, $aesKey, null);\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/JsApiTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function intval;\nuse function is_string;\nuse function sprintf;\n\nclass JsApiTicket\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $corpId,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    public function createConfigSignature(string $nonce, int $timestamp, string $url, array $jsApiList = [], bool $debug = false, bool $beta = true): array\n    {\n        return [\n            'appId' => $this->corpId,\n            'nonceStr' => $nonce,\n            'timestamp' => $timestamp,\n            'url' => $url,\n            'signature' => $this->getTicketSignature($this->getTicket(), $nonce, $timestamp, $url),\n            'jsApiList' => $jsApiList,\n            'debug' => $debug,\n            'beta' => $beta,\n        ];\n    }\n\n    public function getTicketSignature(string $ticket, string $nonce, int $timestamp, string $url): string\n    {\n        return sha1(sprintf('jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s', $ticket, $nonce, $timestamp, $url));\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getTicket(): string\n    {\n        $key = $this->getKey();\n        $ticket = $this->cache->get($key);\n\n        if ($ticket && is_string($ticket)) {\n            return $ticket;\n        }\n\n        $response = $this->httpClient->request('GET', '/cgi-bin/get_jsapi_ticket')->toArray(false);\n\n        if (empty($response['ticket'])) {\n            throw new HttpException('Failed to get jssdk ticket: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($key, $response['ticket'], intval($response['expires_in']));\n\n        return $response['ticket'];\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('open_work.jsapi_ticket.%s', $this->corpId);\n    }\n\n    public function createAgentConfigSignature(int $agentId, string $nonce, int $timestamp, string $url, array $jsApiList = []): array\n    {\n        return [\n            'corpid' => $this->corpId,\n            'agentid' => $agentId,\n            'nonceStr' => $nonce,\n            'timestamp' => $timestamp,\n            'signature' => $this->getTicketSignature($this->getAgentTicket($agentId), $nonce, $timestamp, $url),\n            'jsApiList' => $jsApiList,\n        ];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAgentTicket(int $agentId): string\n    {\n        $key = $this->getAgentKey($agentId);\n        $ticket = $this->cache->get($key);\n\n        if ($ticket && is_string($ticket)) {\n            return $ticket;\n        }\n\n        $response = $this->httpClient->request('GET', '/cgi-bin/ticket/get', ['query' => ['type' => 'agent_config']])->toArray(false);\n\n        if (empty($response['ticket'])) {\n            throw new HttpException('Failed to get jssdk agentTicket: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($key, $response['ticket'], intval($response['expires_in']));\n\n        return $response['ticket'];\n    }\n\n    public function getAgentKey(int $agentId): string\n    {\n        return sprintf('%s.%s', $this->getKey(), $agentId);\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Message.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\n/**\n * @property string $InfoType\n * @property string $ChangeType\n * @property string $SuiteTicket\n * @property string $SuiteId\n * @property string $MsgType\n * @property string $Event\n */\nclass Message extends \\EasyWeChat\\Kernel\\Message\n{\n    //\n}\n"
  },
  {
    "path": "src/OpenWork/ProviderAccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse const JSON_UNESCAPED_UNICODE;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function intval;\n\nclass ProviderAccessToken implements RefreshableAccessTokenInterface\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $corpId,\n        protected string $providerSecret,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null,\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = \\sprintf('open_work.access_token.%s.%s', $this->corpId, $this->providerSecret);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getToken(): string\n    {\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && \\is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    /**\n     * @return array<string, string>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['provider_access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['provider_access_token' => $this->getToken()];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refresh(): string\n    {\n        $response = $this->httpClient->request('POST', 'cgi-bin/service/get_provider_token', [\n            'json' => [\n                'corpid' => $this->corpId,\n                'provider_secret' => $this->providerSecret,\n            ],\n        ])->toArray(false);\n\n        if (empty($response['provider_access_token'])) {\n            throw new HttpException('Failed to get provider_access_token: '.\\json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        $this->cache->set($this->getKey(), $response['provider_access_token'], intval($response['expires_in']));\n\n        return $response['provider_access_token'];\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\BadRequestException;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Kernel\\Traits\\RespondXmlMessage;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nuse function func_get_args;\n\nclass Server implements ServerInterface\n{\n    use DecryptMessage;\n    use InteractWithHandlers;\n    use InteractWithServerRequest;\n    use RespondXmlMessage;\n\n    protected ?Closure $defaultSuiteTicketHandler = null;\n\n    public function __construct(\n        protected Encryptor $encryptor,\n        protected Encryptor $providerEncryptor,\n        ?ServerRequestInterface $request = null,\n    ) {\n        $this->request = $request;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     * @throws BadRequestException\n     * @throws RuntimeException\n     */\n    public function serve(): ResponseInterface\n    {\n        $query = $this->getRequest()->getQueryParams();\n\n        if ($str = $query['echostr'] ?? '') {\n            $response = $this->providerEncryptor->decrypt(\n                $str,\n                $query['msg_signature'] ?? '',\n                $query['nonce'] ?? '',\n                $query['timestamp'] ?? ''\n            );\n\n            return new Response(200, [], $response);\n        }\n\n        $message = $this->getRequestMessage($this->getRequest());\n\n        $this->prepend($this->decryptRequestMessage());\n\n        $response = $this->handle(new Response(200, [], 'success'), $message);\n\n        if (! ($response instanceof ResponseInterface)) {\n            $response = $this->transformToReply($response, $message, $this->encryptor);\n        }\n\n        return ServerResponse::make($response);\n    }\n\n    public function withDefaultSuiteTicketHandler(callable $handler): void\n    {\n        $this->defaultSuiteTicketHandler = fn (): mixed => $handler(...func_get_args());\n        $this->handleSuiteTicketRefreshed($this->defaultSuiteTicketHandler);\n    }\n\n    public function handleSuiteTicketRefreshed(callable $handler): static\n    {\n        if ($this->defaultSuiteTicketHandler) {\n            $this->withoutHandler($this->defaultSuiteTicketHandler);\n        }\n\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'suite_ticket' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleAuthCreated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'create_auth' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleAuthChanged(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_auth' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleAuthCancelled(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'cancel_auth' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserCreated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'create_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'update_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserDeleted(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'delete_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyCreated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'create_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'update_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyDeleted(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'delete_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserTagUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'change_contact' && $message->ChangeType === 'update_tag' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleShareAgentChanged(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'share_agent_change' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleResetPermanentCode(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->InfoType === 'reset_permanent_code' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleChangeAppAdmin(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->MsgType === 'event' && $message->Event === 'change_app_admin' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    protected function decryptRequestMessage(): Closure\n    {\n        $query = $this->getRequest()->getQueryParams();\n\n        return function (Message $message, Closure $next) use ($query): mixed {\n            $this->decryptMessage(\n                $message,\n                $this->encryptor,\n                $query['msg_signature'],\n                $query['timestamp'],\n                $query['nonce']\n            );\n\n            return $next($message);\n        };\n    }\n\n    /**\n     * @throws BadRequestException\n     */\n    public function getRequestMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        return Message::createFromRequest($request ?? $this->getRequest());\n    }\n\n    /**\n     * @throws BadRequestException\n     * @throws RuntimeException\n     */\n    public function getDecryptedMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        $request = $request ?? $this->getRequest();\n        $message = $this->getRequestMessage($request);\n        $query = $request->getQueryParams();\n\n        return $this->decryptMessage(\n            message: $message,\n            encryptor: $this->encryptor,\n            signature: $query['msg_signature'] ?? '',\n            timestamp: $query['timestamp'] ?? '',\n            nonce: $query['nonce'] ?? ''\n        );\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/SuiteAccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse const JSON_UNESCAPED_UNICODE;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken as RefreshableAccessTokenInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse EasyWeChat\\OpenWork\\Contracts\\SuiteTicket as SuiteTicketInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function abs;\nuse function intval;\nuse function json_encode;\n\nclass SuiteAccessToken implements RefreshableAccessTokenInterface\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $suiteId,\n        protected string $suiteSecret,\n        protected ?SuiteTicketInterface $suiteTicket = null,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null,\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n        $this->suiteTicket ??= new SuiteTicket($this->suiteId, $this->cache);\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = \\sprintf('open_work.suite_access_token.%s.%s', $this->suiteId, $this->suiteSecret);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getToken(): string\n    {\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && \\is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    /**\n     * @return array<string, string>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['suite_access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['suite_access_token' => $this->getToken()];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refresh(): string\n    {\n        $response = $this->httpClient->request('POST', 'cgi-bin/service/get_suite_token', [\n            'json' => [\n                'suite_id' => $this->suiteId,\n                'suite_secret' => $this->suiteSecret,\n                'suite_ticket' => $this->suiteTicket?->getTicket(),\n            ],\n        ])->toArray(false);\n\n        if (empty($response['suite_access_token'])) {\n            throw new HttpException('Failed to get suite_access_token: '.json_encode(\n                $response,\n                JSON_UNESCAPED_UNICODE\n            ));\n        }\n\n        $this->cache->set(\n            $this->getKey(),\n            $response['suite_access_token'],\n            abs(intval($response['expires_in']) - 100)\n        );\n\n        return $response['suite_access_token'];\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/SuiteEncryptor.php",
    "content": "<?php\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\Kernel\\Encryptor;\n\nclass SuiteEncryptor extends Encryptor\n{\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function __construct(string $suiteId, string $token, string $aesKey)\n    {\n        parent::__construct($suiteId, $token, $aesKey, null);\n    }\n}\n"
  },
  {
    "path": "src/OpenWork/SuiteTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\OpenWork;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\OpenWork\\Contracts\\SuiteTicket as SuiteTicketInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\nuse function is_string;\nuse function sprintf;\n\nclass SuiteTicket implements SuiteTicketInterface\n{\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $suiteId,\n        ?CacheInterface $cache = null,\n        protected ?string $key = null,\n    ) {\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('open_work.suite_ticket.%s', $this->suiteId);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function setTicket(string $ticket): static\n    {\n        $this->cache->set($this->getKey(), $ticket, 6000);\n\n        return $this;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function getTicket(): string\n    {\n        $ticket = $this->cache->get($this->getKey());\n\n        if (! $ticket || ! is_string($ticket)) {\n            throw new RuntimeException('No suite_ticket found.');\n        }\n\n        return $ticket;\n    }\n}\n"
  },
  {
    "path": "src/Pay/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Pay\\Contracts\\Validator as ValidatorInterface;\nuse Psr\\Log\\LoggerAwareTrait;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass Application implements \\EasyWeChat\\Pay\\Contracts\\Application\n{\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?ValidatorInterface $validator = null;\n\n    protected ?HttpClientInterface $client = null;\n\n    protected ?Merchant $merchant = null;\n\n    public function getUtils(): Utils\n    {\n        return new Utils($this->getMerchant());\n    }\n\n    public function getMerchant(): Merchant\n    {\n        if (! $this->merchant) {\n            $this->merchant = new Merchant(\n                mchId: $this->config['mch_id'], /** @phpstan-ignore-line */\n                privateKey: new PrivateKey((string) $this->config['private_key']), /** @phpstan-ignore-line */\n                certificate: new PublicKey((string) $this->config['certificate']), /** @phpstan-ignore-line */\n                secretKey: (string) $this->config['secret_key'], /** @phpstan-ignore-line */\n                v2SecretKey: (string) $this->config['v2_secret_key'], /** @phpstan-ignore-line */\n                platformCerts: $this->config->has('platform_certs') ? (array) $this->config['platform_certs'] : [],/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->merchant;\n    }\n\n    public function getValidator(): ValidatorInterface\n    {\n        if (! $this->validator) {\n            $this->validator = new Validator($this->getMerchant());\n        }\n\n        return $this->validator;\n    }\n\n    public function setValidator(ValidatorInterface $validator): static\n    {\n        $this->validator = $validator;\n\n        return $this;\n    }\n\n    public function getServer(): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                merchant: $this->getMerchant(),\n                request: $this->getRequest(),\n            );\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function setConfig(ConfigInterface $config): static\n    {\n        $this->config = $config;\n\n        return $this;\n    }\n\n    public function getConfig(): ConfigInterface\n    {\n        return $this->config;\n    }\n\n    public function getClient(): Client|HttpClientInterface\n    {\n        return $this->client ?? $this->client = (new Client(\n            $this->getMerchant(),\n            $this->getHttpClient(),\n            (array) $this->config->get('http', [])\n        ))->setPresets($this->config->all());\n    }\n\n    public function setClient(HttpClientInterface $client): static\n    {\n        $this->client = $client;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Pay/Client.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\Form\\File;\nuse EasyWeChat\\Kernel\\Form\\Form;\nuse EasyWeChat\\Kernel\\HttpClient\\HttpClientMethods;\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse EasyWeChat\\Kernel\\HttpClient\\RequestWithPresets;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Kernel\\Support\\UserAgent;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Kernel\\Traits\\MockableHttpClient;\nuse Exception;\nuse Mockery;\nuse Mockery\\Mock;\nuse Nyholm\\Psr7\\Uri;\nuse Symfony\\Component\\HttpClient\\DecoratorTrait;\nuse Symfony\\Component\\HttpClient\\HttpClient as SymfonyHttpClient;\nuse Symfony\\Component\\HttpClient\\HttpClientTrait;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\Mime\\Part\\DataPart;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nuse function array_key_exists;\nuse function is_array;\nuse function ltrim;\nuse function str_starts_with;\nuse function strcasecmp;\n\n/**\n * @method ResponseInterface get(string $uri, array $options = [])\n * @method ResponseInterface post(string $uri, array $options = [])\n * @method ResponseInterface put(string $uri, array $options = [])\n * @method ResponseInterface patch(string $uri, array $options = [])\n * @method ResponseInterface delete(string $uri, array $options = [])\n * @method HttpClientInterface withMchId(string $value = null)\n * @method HttpClientInterface withMchIdAs(string $key)\n */\nclass Client implements HttpClientInterface\n{\n    use DecoratorTrait {\n        DecoratorTrait::withOptions insteadof HttpClientTrait;\n    }\n    use HttpClientMethods;\n    use HttpClientTrait;\n    use MockableHttpClient;\n    use RequestWithPresets;\n\n    /**\n     * @var array{base_uri:string,headers:array{'Content-Type':string,Accept:string}}\n     */\n    protected array $defaultOptions = [\n        'base_uri' => 'https://api.mch.weixin.qq.com/',\n        'headers' => [\n            'Content-Type' => 'application/json',\n            'Accept' => 'application/json',\n        ],\n    ];\n\n    protected const V3_URI_PREFIXES = [\n        '/v3/',\n        '/hk/v3/',\n        '/global/v3/',\n    ];\n\n    /**\n     * Special absolute path string over `GET` method\n     */\n    protected const V2_URI_OVER_GETS = [\n        '/appauth/getaccesstoken', // secret API which's respond `JSON`, must keep in the first\n        '/papay/entrustweb',\n        '/papay/h5entrustweb',\n        '/papay/partner/entrustweb',\n        '/papay/partner/h5entrustweb',\n    ];\n\n    protected bool $throw = true;\n\n    /**\n     * @param  array<string, mixed>  $defaultOptions\n     */\n    public function __construct(\n        protected Merchant $merchant,\n        ?HttpClientInterface $client = null,\n        array $defaultOptions = []\n    ) {\n        $this->throw = (bool) ($defaultOptions['throw'] ?? true);\n\n        $this->defaultOptions = array_merge(self::OPTIONS_DEFAULTS, $this->defaultOptions);\n\n        if (! empty($defaultOptions)) {\n            $defaultOptions = RequestUtil::formatDefaultOptions($defaultOptions);\n            [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);\n        }\n\n        $this->client = ($client ?? SymfonyHttpClient::create())->withOptions($this->defaultOptions);\n    }\n\n    /**\n     * @param  array<string, array|mixed>  $options\n     *\n     * @throws InvalidArgumentException\n     */\n    public function request(string $method, string $url, array $options = []): ResponseInterface\n    {\n        if (empty($options['headers'])) {\n            $options['headers'] = [];\n        }\n\n        $options = $this->mergeThenResetPrepends($options);\n\n        $options['headers']['User-Agent'] = UserAgent::create();\n\n        if ($this->isV3Request($url)) {\n            [, $_options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);\n\n            // 部分签名算法需要使用到 body 中额外的部分，所以交由前置逻辑自行完成\n            if (empty($options['headers']['Authorization'])) {\n                $options['headers']['Authorization'] = $this->createSignature($method, $url, $_options);\n            }\n        } else {\n            if (! strcasecmp($method, 'POST') && ! empty($options['xml'])) {\n                if (is_array($options['xml'])) {\n                    $options['xml'] = Xml::build($this->attachLegacySignature($options['xml']));\n                }\n\n                $options['body'] = $options['xml'];\n                unset($options['xml']);\n            }\n\n            if (! empty($options['body']) && is_array($options['body'])) {\n                $options['body'] = Xml::build($this->attachLegacySignature($options['body']));\n            }\n\n            if (! strcasecmp($method, 'GET') && in_array($url, self::V2_URI_OVER_GETS) && is_array($options['query'] ?? null)) {\n                $options['query'] = $this->attachLegacySignature($options['query']);\n            }\n\n            if (! isset($options['headers']['Content-Type']) && ! isset($options['headers']['content-type'])) {\n                $options['headers']['Content-Type'] = 'text/xml';\n            }\n        }\n\n        // 合并通过 withHeader 和 withHeaders 设置的信息\n        if (! empty($this->prependHeaders)) {\n            $options['headers'] = array_merge($this->prependHeaders, $options['headers']);\n        }\n\n        return new Response(\n            $this->client->request($method, $url, $options),\n            failureJudge: $this->isV3Request($url) ? null : function (Response $response) use ($url): bool {\n                $arr = $response->toArray();\n\n                if ($url === self::V2_URI_OVER_GETS[0]) {\n                    return ! (array_key_exists('retcode', $arr) && $arr['retcode'] === 0);\n                }\n\n                return ! (\n                    // protocol code, most similar to the HTTP status code in APIv3\n                    array_key_exists('return_code', $arr) && $arr['return_code'] === 'SUCCESS'\n                ) || (\n                    // business code, most similar to the Response.JSON.code in APIv3\n                    array_key_exists('result_code', $arr) && $arr['result_code'] !== 'SUCCESS'\n                );\n            },\n            throw: $this->throw\n        );\n    }\n\n    protected function isV3Request(string $url): bool\n    {\n        $uri = '/'.ltrim((new Uri($url))->getPath(), '/');\n\n        foreach (self::V3_URI_PREFIXES as $prefix) {\n            if (str_starts_with($uri, $prefix)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     */\n    public function withSerialHeader(?string $serial = null): static\n    {\n        $platformCerts = $this->merchant->getPlatformCerts();\n        if (empty($platformCerts)) {\n            throw new InvalidConfigException('Missing platform certificate.');\n        }\n\n        $serial ??= array_key_first($platformCerts);\n        $this->withHeader('Wechatpay-Serial', $serial);\n\n        return $this;\n    }\n\n    /**\n     * @param  array<int, mixed>  $arguments\n     */\n    public function __call(string $name, array $arguments): mixed\n    {\n        if (\\str_starts_with($name, 'with')) {\n            return $this->handleMagicWithCall($name, $arguments[0] ?? null);\n        }\n\n        return $this->client->$name(...$arguments);\n    }\n\n    public function uploadMedia(string $uri, string $pathOrContents, ?array $meta = null, ?string $filename = null): ResponseInterface\n    {\n        $isFile = is_file($pathOrContents);\n\n        $meta = self::jsonEncode($meta ?? [\n            'filename' => $isFile ? basename($pathOrContents) : $filename ?? 'file',\n            'sha256' => $isFile ? hash_file('sha256', $pathOrContents) : hash('sha256', $pathOrContents),\n        ]);\n\n        $form = Form::create([\n            'file' => File::from($pathOrContents),\n            'meta' => new DataPart($meta, null, 'application/json'),\n        ]);\n\n        $options = $signatureOptions = $form->toOptions();\n\n        $signatureOptions['body'] = $meta;\n\n        $options['headers']['Authorization'] = $this->createSignature('POST', $uri, $signatureOptions);\n\n        return $this->request('POST', $uri, $options);\n    }\n\n    /**\n     * @param  array<string, mixed>  $options\n     *\n     * @throws Exception\n     */\n    protected function createSignature(string $method, string $url, array $options): string\n    {\n        return (new Signature($this->merchant))->createHeader($method, $url, $options);\n    }\n\n    /**\n     * @param  array<string, mixed>  $body\n     * @return array<string, mixed>\n     *\n     * @throws Exception\n     */\n    protected function attachLegacySignature(array $body): array\n    {\n        return (new LegacySignature($this->merchant))->sign($body);\n    }\n\n    public static function createMockClient(MockHttpClient $mockHttpClient): HttpClientInterface|Mock\n    {\n        $mockMerchant = new Merchant(\n            'mch_id',\n            /** @phpstan-ignore-next-line */\n            Mockery::mock(PrivateKey::class),\n            /** @phpstan-ignore-next-line */\n            Mockery::mock(PublicKey::class),\n            'mock-v3-key',\n            'mock-v2-key',\n            [\n                'PUB_KEY_ID_MOCK' => '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlReZ1YnfAohRIfUqIeyP\naO0PlkMw1RLPdZbEZmldbGrIrOh/0XqSzNZ+mtB6H0eB7TSaoGFtdp/AWy3tb67m\n1T62OrEhz6bnSKMcZkYVmODyxZvcwsCZ3zqCaFo7FrGmh1o9M0/Xfa5SOX4jVGni\n3iM7r7YD/NiW2RCYDtjMoLTmVgrzv45Mzu2XpJqtNbUJIRRhVSnjsAZRC6spWH+b\nQpYIkVd4qmYE0qdpIQBMYOV1w7v1pYn6Z5QdKG4keemADTn4QaZZHrryTcHNYVsZ\n2OZ3aybrevSV3wDGnYGk2nt2xtkdfaNfFn4dGW+p4an5M4fRK+CnYpeTgI6POABk\npwIDAQAB\n-----END PUBLIC KEY-----',\n            ]\n        );\n\n        return Mockery::mock(static::class, [$mockMerchant, $mockHttpClient])\n            ->shouldAllowMockingProtectedMethods()\n            ->makePartial();\n    }\n}\n"
  },
  {
    "path": "src/Pay/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nclass Config extends \\EasyWeChat\\Kernel\\Config\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [\n        'mch_id',\n        'secret_key',\n        'private_key',\n        'certificate',\n    ];\n}\n"
  },
  {
    "path": "src/Pay/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getMerchant(): Merchant;\n\n    public function getConfig(): Config;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getClient(): HttpClientInterface;\n}\n"
  },
  {
    "path": "src/Pay/Contracts/Merchant.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay\\Contracts;\n\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\n\ninterface Merchant\n{\n    public function getMerchantId(): int;\n\n    public function getPrivateKey(): PrivateKey;\n\n    public function getSecretKey(): string;\n\n    public function getV2SecretKey(): ?string;\n\n    public function getCertificate(): PublicKey;\n\n    public function getPlatformCert(string $serial): ?PublicKey;\n\n    /**\n     * @return array<string,PublicKey>\n     */\n    public function getPlatformCerts(): array;\n}\n"
  },
  {
    "path": "src/Pay/Contracts/ResponseValidator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay\\Contracts;\n\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\n\ninterface ResponseValidator\n{\n    public function validate(ResponseInterface|Response $response): void;\n}\n"
  },
  {
    "path": "src/Pay/Contracts/Validator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay\\Contracts;\n\nuse Psr\\Http\\Message\\MessageInterface;\n\ninterface Validator\n{\n    public function validate(MessageInterface $message): void;\n}\n"
  },
  {
    "path": "src/Pay/Exceptions/EncryptionFailureException.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Pay\\Exceptions;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\n\nclass EncryptionFailureException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Pay/Exceptions/InvalidSignatureException.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Pay\\Exceptions;\n\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\n\nclass InvalidSignatureException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Pay/LegacySignature.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\Support\\Str;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\n\nuse function call_user_func_array;\nuse function hash_hmac;\nuse function http_build_query;\nuse function is_string;\nuse function strtoupper;\nuse function urldecode;\n\nclass LegacySignature\n{\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @param  array<string, mixed>  $params\n     * @return array<string, mixed>\n     *\n     * @throws InvalidConfigException\n     * @throws RuntimeException\n     */\n    public function sign(array $params): array\n    {\n        $nonce = Str::random();\n\n        $params = $attributes = array_filter(\n            \\array_merge(\n                [\n                    'nonce_str' => $nonce,\n                    'sub_mch_id' => $params['sub_mch_id'] ?? null,\n                    'sub_appid' => $params['sub_appid'] ?? null,\n                ],\n                $params\n            ),\n            static fn ($value, $key) => ! ($key === 'sign' || $value === '' || is_null($value)),\n            ARRAY_FILTER_USE_BOTH\n        );\n\n        ksort($attributes);\n\n        $attributes['key'] = $this->merchant->getV2SecretKey();\n\n        if (empty($attributes['key'])) {\n            throw new InvalidConfigException('Missing V2 API key.');\n        }\n\n        if (! empty($params['sign_type']) && $params['sign_type'] === 'HMAC-SHA256') {\n            $signType = fn (string $message): string => hash_hmac('sha256', $message, $attributes['key']);\n        } else {\n            $signType = 'md5';\n        }\n\n        $sign = call_user_func_array($signType, [urldecode(http_build_query($attributes))]);\n\n        if (! is_string($sign)) {\n            throw new RuntimeException('Failed to sign the request.');\n        }\n\n        $params['sign'] = strtoupper($sign);\n\n        return $params;\n    }\n}\n"
  },
  {
    "path": "src/Pay/Merchant.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\n\nuse function array_is_list;\nuse function intval;\nuse function is_string;\n\nclass Merchant implements MerchantInterface\n{\n    /**\n     * @var array<string, PublicKey>\n     */\n    protected array $platformCerts = [];\n\n    /**\n     * @param  array<int|string, string|PublicKey>  $platformCerts\n     */\n    public function __construct(\n        protected int|string $mchId,\n        protected PrivateKey $privateKey,\n        protected PublicKey $certificate,\n        protected string $secretKey,\n        protected ?string $v2SecretKey = null,\n        array $platformCerts = [],\n    ) {\n        $this->platformCerts = $this->normalizePlatformCerts($platformCerts);\n    }\n\n    public function getMerchantId(): int\n    {\n        return intval($this->mchId);\n    }\n\n    public function getPrivateKey(): PrivateKey\n    {\n        return $this->privateKey;\n    }\n\n    public function getCertificate(): PublicKey\n    {\n        return $this->certificate;\n    }\n\n    public function getSecretKey(): string\n    {\n        return $this->secretKey;\n    }\n\n    public function getV2SecretKey(): ?string\n    {\n        return $this->v2SecretKey;\n    }\n\n    public function getPlatformCert(string $serial): ?PublicKey\n    {\n        return $this->platformCerts[$serial] ?? null;\n    }\n\n    public function getPlatformCerts(): array\n    {\n        return $this->platformCerts;\n    }\n\n    /**\n     * @param  array<array-key, mixed>  $platformCerts\n     * @return array<string, PublicKey>\n     *\n     * @throws InvalidArgumentException\n     */\n    protected function normalizePlatformCerts(array $platformCerts): array\n    {\n        $certs = [];\n        $isList = array_is_list($platformCerts);\n        foreach ($platformCerts as $index => $publicKey) {\n            if (is_string($publicKey)) {\n                $publicKey = new PublicKey($publicKey);\n            }\n\n            if (! $publicKey instanceof PublicKey) {\n                throw new InvalidArgumentException('Invalid platform certficate.');\n            }\n\n            $certs[$isList ? $publicKey->getSerialNo() : $index] = $publicKey;\n        }\n\n        return $certs;\n    }\n}\n"
  },
  {
    "path": "src/Pay/Message.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Pay;\n\nuse RuntimeException;\n\nuse function is_array;\nuse function is_string;\nuse function json_decode;\n\n/**\n * @property string $trade_state\n */\nclass Message extends \\EasyWeChat\\Kernel\\Message\n{\n    /**\n     * @return array<string, mixed>\n     */\n    public function getOriginalAttributes(): array\n    {\n        $attributes = json_decode($this->getOriginalContents(), true);\n\n        return is_array($attributes) ? $attributes : [];\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    public function getEventType(): ?string\n    {\n        $eventType = $this->getOriginalAttributes()['event_type'];\n\n        if (! is_string($eventType)) {\n            throw new RuntimeException('Invalid event type.');\n        }\n\n        return $eventType;\n    }\n}\n"
  },
  {
    "path": "src/Pay/ResponseValidator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\BadResponseException;\nuse EasyWeChat\\Kernel\\HttpClient\\Response as HttpClientResponse;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\nuse Psr\\Http\\Message\\ResponseInterface as PsrResponse;\n\nclass ResponseValidator implements \\EasyWeChat\\Pay\\Contracts\\ResponseValidator\n{\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @throws BadResponseException\n     */\n    public function validate(PsrResponse|HttpClientResponse $response): void\n    {\n        if ($response instanceof HttpClientResponse) {\n            $response = $response->toPsrResponse();\n        }\n\n        if ($response->getStatusCode() !== 200) {\n            throw new BadResponseException('Request Failed');\n        }\n\n        (new Validator($this->merchant))->validate($response);\n    }\n}\n"
  },
  {
    "path": "src/Pay/Server.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Pay;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Exceptions\\RuntimeException;\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse EasyWeChat\\Kernel\\Support\\AesEcb;\nuse EasyWeChat\\Kernel\\Support\\AesGcm;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\nuse Exception;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nuse function array_key_exists;\nuse function is_array;\nuse function is_string;\nuse function json_decode;\nuse function json_encode;\nuse function str_contains;\nuse function strval;\n\nclass Server implements ServerInterface\n{\n    use InteractWithHandlers;\n    use InteractWithServerRequest;\n\n    public function __construct(\n        protected MerchantInterface $merchant,\n        ?ServerRequestInterface $request,\n    ) {\n        $this->request = $request;\n    }\n\n    public function serve(): ResponseInterface\n    {\n        $message = $this->getRequestMessage();\n\n        try {\n            $defaultResponse = new Response(\n                200,\n                [],\n                strval(json_encode(['code' => 'SUCCESS', 'message' => '成功'], JSON_UNESCAPED_UNICODE))\n            );\n            $response = $this->handle($defaultResponse, $message);\n\n            if (! ($response instanceof ResponseInterface)) {\n                $response = $defaultResponse;\n            }\n\n            return ServerResponse::make($response);\n        } catch (Exception $e) {\n            return new Response(\n                500,\n                [],\n                strval(json_encode(['code' => 'ERROR', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE))\n            );\n        }\n    }\n\n    /**\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791861\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013070368\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791836\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791882\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791902\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012158598\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421143\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421231\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421336\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421407\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012587960\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012647435\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085146\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013080237\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085680\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085875\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085801\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012231898\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462105\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462175\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462250\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462574\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012155283\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012586136\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085573\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013194298\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012284311\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012285856\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012286313\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012595808\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012289459\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013392770\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012086059\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012090195\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012076414\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012159706\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011935221\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011937152\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011937248\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011938508\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011941607\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011985057\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011985480\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011936650\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011989262\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011941679\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011941306\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011984334\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011989906\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011988207\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011984263\n     */\n    public function handlePaid(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->getEventType() === 'TRANSACTION.SUCCESS' && $message->trade_state === 'SUCCESS'\n                ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    /**\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791865\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013070388\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012810605\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791886\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012791906\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012085921\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421172\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421273\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421356\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013421448\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013071196\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012268885\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012082022\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012587976\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012647469\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012083103\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012285869\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085298\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013080241\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085681\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085876\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012085802\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012231901\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462126\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462195\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462327\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013462586\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013080628\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012167494\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012650317\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012166857\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012886275\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012086319\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012124635\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012076419\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013163616\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4012586138\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011940955\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011935223\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011937201\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011939959\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011939475\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011941681\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011985425\n     * @link https://pay.weixin.qq.com/doc/v2/merchant/4011987569\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011941745\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011936652\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011989265\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011984153\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4012297550\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011984440\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011989912\n     * @link https://pay.weixin.qq.com/doc/v2/partner/4011988218\n     */\n    public function handleRefunded(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return in_array($message->getEventType(), [\n                'REFUND.SUCCESS',\n                'REFUND.ABNORMAL',\n                'REFUND.CLOSED',\n            ]) ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function getRequestMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message|Message\n    {\n        $originContent = (string) ($request ?? $this->getRequest())->getBody();\n\n        // 微信支付的回调数据回调，偶尔是 XML https://github.com/w7corp/easywechat/issues/2737\n        $contentType = ($request ?? $this->getRequest())->getHeaderLine('content-type');\n        $isXml = (str_contains($contentType, 'text/xml') || str_contains($contentType, 'application/xml')) && str_starts_with($originContent, '<xml');\n        $attributes = $isXml ? $this->decodeXmlMessage($originContent) : $this->decodeJsonMessage($originContent);\n\n        return new Message($attributes, $originContent);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     * @throws RuntimeException\n     */\n    protected function decodeXmlMessage(string $contents): array\n    {\n        $attributes = Xml::parse($contents);\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Invalid request body.');\n        }\n\n        if (! empty($attributes['req_info'])) {\n            $key = $this->merchant->getV2SecretKey();\n\n            if (empty($key)) {\n                throw new InvalidArgumentException('V2 secret key is required.');\n            }\n\n            $attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));\n        }\n\n        if (\n            is_array($attributes)\n            && array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])\n            && array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])\n            && array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])\n        ) {\n            $attributes += Xml::parse(AesGcm::decrypt(\n                $attributes['event_ciphertext'],\n                $this->merchant->getSecretKey(),\n                $attributes['event_nonce'],\n                $attributes['event_associated_data'] // maybe empty string\n            ));\n        }\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Failed to decrypt request message.');\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    protected function decodeJsonMessage(string $contents): array\n    {\n        $attributes = json_decode($contents, true);\n\n        if (! (is_array($attributes) && is_array($attributes['resource']))) {\n            throw new RuntimeException('Invalid request body.');\n        }\n\n        if (empty($attributes['resource']['ciphertext'] ?? null)) {\n            throw new RuntimeException('Invalid request.');\n        }\n\n        $attributes = json_decode(\n            AesGcm::decrypt(\n                $attributes['resource']['ciphertext'],\n                $this->merchant->getSecretKey(),\n                $attributes['resource']['nonce'],\n                $attributes['resource']['associated_data'],\n            ),\n            true\n        );\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Failed to decrypt request message.');\n        }\n\n        return $attributes;\n    }\n\n    public function getDecryptedMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message|Message\n    {\n        return $this->getRequestMessage($request);\n    }\n}\n"
  },
  {
    "path": "src/Pay/Signature.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Support\\Str;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\nuse Nyholm\\Psr7\\Uri;\nuse Stringable;\n\nuse function array_merge;\nuse function base64_encode;\nuse function http_build_query;\nuse function is_scalar;\nuse function ltrim;\nuse function openssl_sign;\nuse function parse_str;\nuse function strtoupper;\nuse function strval;\nuse function time;\n\nclass Signature\n{\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @param  array<string,mixed>  $options\n     */\n    public function createHeader(string $method, string $url, array $options): string\n    {\n        $uri = new Uri($url);\n\n        parse_str($uri->getQuery(), $query);\n        $uri = $uri->withQuery(http_build_query(array_merge($query, (array) ($options['query'] ?? []))));\n\n        $body = '';\n        $query = $uri->getQuery();\n        $timestamp = time();\n        $nonce = Str::random();\n        $path = '/'.ltrim($uri->getPath().(empty($query) ? '' : '?'.$query), '/');\n\n        if (! empty($options['body']) && (is_scalar($options['body']) || $options['body'] instanceof Stringable)) {\n            $body = strval($options['body']);\n        }\n\n        $message = strtoupper($method).\"\\n\".\n            $path.\"\\n\".\n            $timestamp.\"\\n\".\n            $nonce.\"\\n\".\n            $body.\"\\n\";\n\n        openssl_sign($message, $signature, $this->merchant->getPrivateKey()->getKey(), 'sha256WithRSAEncryption');\n\n        return sprintf(\n            'WECHATPAY2-SHA256-RSA2048 %s',\n            sprintf(\n                'mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"',\n                $this->merchant->getMerchantId(),\n                $nonce,\n                $timestamp,\n                $this->merchant->getCertificate()->getSerialNo(),\n                base64_encode($signature)\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/Pay/URLSchemeBuilder.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Support\\Str;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\n\nuse function sprintf;\n\nclass URLSchemeBuilder\n{\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    public function forProduct(string|int $productId, string $appId): string\n    {\n        $params = [\n            'appid' => $appId,\n            'mch_id' => $this->merchant->getMerchantId(),\n            'time_stamp' => time(),\n            'nonce_str' => Str::random(),\n            'product_id' => $productId,\n        ];\n\n        $params['sign'] = (new LegacySignature($this->merchant))->sign($params);\n\n        return 'weixin://wxpay/bizpayurl?'.http_build_query($params);\n    }\n\n    public function forCodeUrl(string $codeUrl): string\n    {\n        return sprintf('weixin://wxpay/bizpayurl?sr=%s', $codeUrl);\n    }\n}\n"
  },
  {
    "path": "src/Pay/Utils.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Pay;\n\nuse const OPENSSL_PKCS1_OAEP_PADDING;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\Support\\Str;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\nuse EasyWeChat\\Pay\\Exceptions\\EncryptionFailureException;\n\nuse function base64_encode;\nuse function call_user_func_array;\nuse function http_build_query;\nuse function openssl_sign;\nuse function strtoupper;\nuse function time;\nuse function urldecode;\n\nclass Utils\n{\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'appId' => 'string',\n        'timeStamp' => 'string',\n        'nonceStr' => 'string',\n        'package' => 'string',\n        'signType' => 'string',\n        'paySign' => 'string',\n    ])]\n    public function buildBridgeConfig(string $prepayId, string $appId, string $signType = 'RSA'): array\n    {\n        $params = [\n            'appId' => $appId,\n            'timeStamp' => strval(time()),\n            'nonceStr' => Str::random(),\n            'package' => \"prepay_id=$prepayId\",\n            'signType' => $signType,\n        ];\n\n        $message = $params['appId'].\"\\n\".\n            $params['timeStamp'].\"\\n\".\n            $params['nonceStr'].\"\\n\".\n            $params['package'].\"\\n\";\n\n        // v2\n        if ($signType != 'RSA') {\n            $params['paySign'] = $this->createV2Signature($params);\n        } else {\n            // v3\n            $params['paySign'] = $this->createSignature($message);\n        }\n\n        return $params;\n    }\n\n    /**\n     * @see https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58\n     *\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'appId' => 'string',\n        'nonceStr' => 'string',\n        'package' => 'string',\n        'signType' => 'string',\n        'paySign' => 'string',\n        'timestamp' => 'string',\n    ])]\n    public function buildSdkConfig(string $prepayId, string $appId, string $signType = 'RSA'): array\n    {\n        $params = $this->buildBridgeConfig($prepayId, $appId, $signType);\n\n        $params['timestamp'] = $params['timeStamp'];\n        unset($params['timeStamp']);\n\n        return $params;\n    }\n\n    /**\n     * @see https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html\n     *\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'appId' => 'string',\n        'timeStamp' => 'string',\n        'nonceStr' => 'string',\n        'package' => 'string',\n        'signType' => 'string',\n        'paySign' => 'string',\n    ])]\n    public function buildMiniAppConfig(string $prepayId, string $appId, string $signType = 'RSA'): array\n    {\n        return $this->buildBridgeConfig($prepayId, $appId, $signType);\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'appid' => 'string',\n        'partnerid' => 'int',\n        'prepayid' => 'string',\n        'noncestr' => 'string',\n        'timestamp' => 'int',\n        'package' => 'string',\n        'sign' => 'string',\n    ])]\n    public function buildAppConfig(string $prepayId, string $appId): array\n    {\n        $params = [\n            'appid' => $appId,\n            'partnerid' => $this->merchant->getMerchantId(),\n            'prepayid' => $prepayId,\n            'noncestr' => Str::random(),\n            'timestamp' => time(),\n            'package' => 'Sign=WXPay',\n        ];\n\n        $message = $params['appid'].\"\\n\".\n            $params['timestamp'].\"\\n\".\n            $params['noncestr'].\"\\n\".\n            $params['prepayid'].\"\\n\";\n\n        $params['sign'] = $this->createSignature($message);\n\n        return $params;\n    }\n\n    protected function createSignature(string $message): string\n    {\n        openssl_sign($message, $signature, $this->merchant->getPrivateKey(), 'sha256WithRSAEncryption');\n\n        return base64_encode($signature);\n    }\n\n    /**\n     * @link https://pay.weixin.qq.com/doc/v3/merchant/4013053257\n     * @link https://pay.weixin.qq.com/doc/v3/partner/4013059044\n     *\n     * @param  string  $plaintext  The text to be encrypted.\n     * @param  string|null  $serial  The serial number of the platform certificate to use for encryption. If null, the first available certificate will be used.\n     * @return string The base64-encoded encrypted text.\n     *\n     * @throws InvalidConfigException If no platform certificate is found.\n     * @throws EncryptionFailureException If the encryption process fails.\n     */\n    public function encryptWithRsaPublicKey(string $plaintext, ?string $serial = null): string\n    {\n        $platformCerts = $this->merchant->getPlatformCerts();\n        /** @var string $identifier - One of the serial number of the platform certificates OR the weixin pay's public key identifier. */\n        $identifier = $serial ?? array_key_first($platformCerts);\n        $platformCert = $this->merchant->getPlatformCert($identifier);\n\n        if (empty($platformCert)) {\n            throw new InvalidConfigException('Missing platform certificate.');\n        }\n\n        if (! openssl_public_encrypt($plaintext, $encrypted, $platformCert, OPENSSL_PKCS1_OAEP_PADDING)) {\n            throw new EncryptionFailureException('Encrypt failed.');\n        }\n\n        return base64_encode($encrypted);\n    }\n\n    /**\n     * @throws InvalidConfigException\n     */\n    public function createV2Signature(array $params): string\n    {\n        $method = 'md5';\n        $secretKey = $this->merchant->getV2SecretKey();\n\n        if (empty($secretKey)) {\n            throw new InvalidConfigException('Missing v2 secret key.');\n        }\n\n        if ($params['signType'] === 'HMAC-SHA256') {\n            $method = function ($str) use ($secretKey) {\n                return hash_hmac('sha256', $str, $secretKey);\n            };\n        }\n\n        ksort($params);\n\n        $params['key'] = $secretKey;\n\n        // @phpstan-ignore-next-line\n        return strtoupper((string) call_user_func_array($method, [urldecode(http_build_query($params))]));\n    }\n}\n"
  },
  {
    "path": "src/Pay/Validator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Pay\\Contracts\\Merchant as MerchantInterface;\nuse EasyWeChat\\Pay\\Exceptions\\InvalidSignatureException;\nuse Psr\\Http\\Message\\MessageInterface;\n\nclass Validator implements \\EasyWeChat\\Pay\\Contracts\\Validator\n{\n    public const MAX_ALLOWED_CLOCK_OFFSET = 300;\n\n    public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';\n\n    public const HEADER_NONCE = 'Wechatpay-Nonce';\n\n    public const HEADER_SERIAL = 'Wechatpay-Serial';\n\n    public const HEADER_SIGNATURE = 'Wechatpay-Signature';\n\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @throws InvalidConfigException\n     * @throws InvalidSignatureException\n     */\n    public function validate(MessageInterface $message): void\n    {\n        foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {\n            if (! $message->hasHeader($header)) {\n                throw new InvalidSignatureException(\"Missing Header: {$header}\");\n            }\n        }\n\n        [$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);\n        [$nonce] = $message->getHeader(self::HEADER_NONCE);\n        [$serial] = $message->getHeader(self::HEADER_SERIAL);\n        [$signature] = $message->getHeader(self::HEADER_SIGNATURE);\n\n        $body = (string) $message->getBody();\n\n        $message = \"{$timestamp}\\n{$nonce}\\n{$body}\\n\";\n\n        if (\\time() - \\intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {\n            throw new InvalidSignatureException('Clock Offset Exceeded');\n        }\n\n        $publicKey = $this->merchant->getPlatformCert($serial);\n\n        if (! $publicKey) {\n            throw new InvalidConfigException(\n                \"No platform certs found for serial: {$serial}, \n                please download from wechat pay and set it in merchant config with key `certs`.\"\n            );\n        }\n\n        if (\\openssl_verify(\n            $message,\n            base64_decode($signature),\n            strval($publicKey),\n            OPENSSL_ALGO_SHA256\n        ) !== 1) {\n            throw new InvalidSignatureException('Invalid Signature');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Work/AccessToken.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nuse const JSON_UNESCAPED_UNICODE;\n\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function intval;\nuse function is_string;\nuse function json_encode;\nuse function sprintf;\n\nclass AccessToken implements RefreshableAccessToken\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $corpId,\n        protected string $secret,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('work.access_token.%s.%s', $this->corpId, $this->secret);\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getToken(): string\n    {\n        $token = $this->cache->get($this->getKey());\n\n        if ($token && is_string($token)) {\n            return $token;\n        }\n\n        return $this->refresh();\n    }\n\n    /**\n     * @return array<string, string>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape(['access_token' => 'string'])]\n    public function toQuery(): array\n    {\n        return ['access_token' => $this->getToken()];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function refresh(): string\n    {\n        $response = $this->httpClient->request('GET', '/cgi-bin/gettoken', [\n            'query' => [\n                'corpid' => $this->corpId,\n                'corpsecret' => $this->secret,\n            ],\n        ])->toArray(false);\n\n        if (empty($response['access_token'])) {\n            throw new HttpException('Failed to get access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));\n\n        return $response['access_token'];\n    }\n}\n"
  },
  {
    "path": "src/Work/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nuse EasyWeChat\\Work\\Contracts\\Account as AccountInterface;\n\nclass Account implements AccountInterface\n{\n    public function __construct(\n        protected string $corpId,\n        protected string $secret,\n        protected string $token,\n        protected string $aesKey,\n    ) {\n    }\n\n    public function getCorpId(): string\n    {\n        return $this->corpId;\n    }\n\n    public function getSecret(): string\n    {\n        return $this->secret;\n    }\n\n    public function getToken(): string\n    {\n        return $this->token;\n    }\n\n    public function getAesKey(): string\n    {\n        return $this->aesKey;\n    }\n}\n"
  },
  {
    "path": "src/Work/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Work\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\Work\\Contracts\\Application as ApplicationInterface;\nuse Overtrue\\Socialite\\Contracts\\ProviderInterface as SocialiteProviderInterface;\nuse Overtrue\\Socialite\\Providers\\WeWork;\nuse Psr\\Log\\LoggerAwareTrait;\n\nuse function array_merge;\n\nclass Application implements ApplicationInterface\n{\n    use InteractWithCache;\n    use InteractWithClient;\n    use InteractWithConfig;\n    use InteractWithHttpClient;\n    use InteractWithServerRequest;\n    use LoggerAwareTrait;\n\n    protected ?Encryptor $encryptor = null;\n\n    protected ?ServerInterface $server = null;\n\n    protected ?AccountInterface $account = null;\n\n    protected ?JsApiTicket $ticket = null;\n\n    protected ?AccessTokenInterface $accessToken = null;\n\n    public function getAccount(): AccountInterface\n    {\n        if (! $this->account) {\n            $this->account = new Account(\n                corpId: (string) $this->config->get('corp_id'), /** @phpstan-ignore-line */\n                secret: (string) $this->config->get('secret'), /** @phpstan-ignore-line */\n                token: (string) $this->config->get('token'), /** @phpstan-ignore-line */\n                aesKey: (string) $this->config->get('aes_key'),/** @phpstan-ignore-line */\n            );\n        }\n\n        return $this->account;\n    }\n\n    public function setAccount(AccountInterface $account): static\n    {\n        $this->account = $account;\n\n        return $this;\n    }\n\n    public function getEncryptor(): Encryptor\n    {\n        if (! $this->encryptor) {\n            $this->encryptor = new Encryptor(\n                corpId: $this->getAccount()->getCorpId(),\n                token: $this->getAccount()->getToken(),\n                aesKey: $this->getAccount()->getAesKey(),\n            );\n        }\n\n        return $this->encryptor;\n    }\n\n    public function setEncryptor(Encryptor $encryptor): static\n    {\n        $this->encryptor = $encryptor;\n\n        return $this;\n    }\n\n    public function getServer(string $messageType = 'xml'): Server|ServerInterface\n    {\n        if (! $this->server) {\n            $this->server = new Server(\n                encryptor: $this->getEncryptor(),\n                request: $this->getRequest(),\n                messageType: $messageType,\n            );\n        }\n\n        return $this->server;\n    }\n\n    public function setServer(ServerInterface $server): static\n    {\n        $this->server = $server;\n\n        return $this;\n    }\n\n    public function getAccessToken(): AccessTokenInterface\n    {\n        if (! $this->accessToken) {\n            $this->accessToken = new AccessToken(\n                corpId: $this->getAccount()->getCorpId(),\n                secret: $this->getAccount()->getSecret(),\n                cache: $this->getCache(),\n                httpClient: $this->getHttpClient(),\n            );\n        }\n\n        return $this->accessToken;\n    }\n\n    public function setAccessToken(AccessTokenInterface $accessToken): static\n    {\n        $this->accessToken = $accessToken;\n\n        return $this;\n    }\n\n    public function getUtils(): Utils\n    {\n        return new Utils($this);\n    }\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        return (new AccessTokenAwareClient(\n            client: $this->getHttpClient(),\n            accessToken: $this->getAccessToken(),\n            failureJudge: fn (Response $response) => (bool) ($response->toArray()['errcode'] ?? 0),\n            throw: (bool) $this->config->get('http.throw', true),\n        ))->setPresets($this->config->all());\n    }\n\n    public function getOAuth(): SocialiteProviderInterface\n    {\n        $provider = new WeWork(\n            [\n                'client_id' => $this->getAccount()->getCorpId(),\n                'client_secret' => $this->getAccount()->getSecret(),\n                'redirect_url' => $this->config->get('oauth.redirect_url'),\n            ]\n        );\n\n        $provider->withApiAccessToken($this->getAccessToken()->getToken());\n        $provider->scopes((array) $this->config->get('oauth.scopes', ['snsapi_base']));\n\n        if ($this->config->has('agent_id') && \\is_numeric($this->config->get('agent_id'))) {\n            $provider->withAgentId((int) $this->config->get('agent_id'));\n        }\n\n        return $provider;\n    }\n\n    public function getTicket(): JsApiTicket\n    {\n        if (! $this->ticket) {\n            $this->ticket = new JsApiTicket(\n                corpId: $this->getAccount()->getCorpId(),\n                cache: $this->getCache(),\n                httpClient: $this->getClient(),\n            );\n        }\n\n        return $this->ticket;\n    }\n\n    public function setTicket(JsApiTicket $ticket): static\n    {\n        $this->ticket = $ticket;\n\n        return $this;\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    protected function getHttpClientDefaultOptions(): array\n    {\n        return array_merge(\n            ['base_uri' => 'https://qyapi.weixin.qq.com/'],\n            (array) $this->config->get('http', [])\n        );\n    }\n}\n"
  },
  {
    "path": "src/Work/Config.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nclass Config extends \\EasyWeChat\\Kernel\\Config\n{\n    /**\n     * @var array<string>\n     */\n    protected array $requiredKeys = [\n        'corp_id',\n        'secret',\n        'token',\n        'aes_key',\n    ];\n}\n"
  },
  {
    "path": "src/Work/Contracts/Account.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work\\Contracts;\n\ninterface Account\n{\n    public function getCorpId(): string;\n\n    public function getSecret(): string;\n\n    public function getToken(): string;\n\n    public function getAesKey(): string;\n}\n"
  },
  {
    "path": "src/Work/Contracts/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work\\Contracts;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\Server;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\ninterface Application\n{\n    public function getAccount(): Account;\n\n    public function getEncryptor(): Encryptor;\n\n    public function getServer(): Server;\n\n    public function getRequest(): ServerRequestInterface;\n\n    public function getClient(): AccessTokenAwareClient;\n\n    public function getHttpClient(): HttpClientInterface;\n\n    public function getConfig(): Config;\n\n    public function getAccessToken(): AccessToken;\n\n    public function getCache(): CacheInterface;\n}\n"
  },
  {
    "path": "src/Work/Encryptor.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Work;\n\nclass Encryptor extends \\EasyWeChat\\Kernel\\Encryptor\n{\n    #[\\JetBrains\\PhpStorm\\Pure]\n    public function __construct(string $corpId, string $token, string $aesKey)\n    {\n        parent::__construct($corpId, $token, $aesKey, null);\n    }\n}\n"
  },
  {
    "path": "src/Work/JsApiTicket.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Component\\HttpClient\\HttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nuse function intval;\nuse function is_string;\nuse function sprintf;\n\nclass JsApiTicket\n{\n    protected HttpClientInterface $httpClient;\n\n    protected CacheInterface $cache;\n\n    public function __construct(\n        protected string $corpId,\n        protected ?string $key = null,\n        ?CacheInterface $cache = null,\n        ?HttpClientInterface $httpClient = null\n    ) {\n        $this->httpClient = $httpClient ?? HttpClient::create(['base_uri' => 'https://qyapi.weixin.qq.com/']);\n        $this->cache = $cache ?? new Psr16Cache(new FilesystemAdapter(namespace: 'easywechat', defaultLifetime: 1500));\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'url' => 'string',\n        'nonceStr' => 'string',\n        'timestamp' => 'int',\n        'appId' => 'string',\n        'signature' => 'string',\n    ])]\n    public function createConfigSignature(string $url, string $nonce, int $timestamp): array\n    {\n        return [\n            'appId' => $this->corpId,\n            'nonceStr' => $nonce,\n            'timestamp' => $timestamp,\n            'url' => $url,\n            'signature' => $this->getTicketSignature($this->getTicket(), $nonce, $timestamp, $url),\n        ];\n    }\n\n    public function getTicketSignature(string $ticket, string $nonce, int $timestamp, string $url): string\n    {\n        return sha1(sprintf('jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s', $ticket, $nonce, $timestamp, $url));\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getTicket(): string\n    {\n        $key = $this->getKey();\n        $ticket = $this->cache->get($key);\n\n        if ($ticket && is_string($ticket)) {\n            return $ticket;\n        }\n\n        $response = $this->httpClient->request('GET', '/cgi-bin/get_jsapi_ticket')->toArray(false);\n\n        if (empty($response['ticket'])) {\n            throw new HttpException('Failed to get jssdk ticket: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($key, $response['ticket'], intval($response['expires_in']));\n\n        return $response['ticket'];\n    }\n\n    public function setKey(string $key): static\n    {\n        $this->key = $key;\n\n        return $this;\n    }\n\n    public function getKey(): string\n    {\n        return $this->key ?? $this->key = sprintf('work.jsapi_ticket.%s', $this->corpId);\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    #[\\JetBrains\\PhpStorm\\ArrayShape([\n        'corpid' => 'string',\n        'agentid' => 'int',\n        'nonceStr' => 'string',\n        'timestamp' => 'int',\n        'url' => 'string',\n        'signature' => 'string',\n    ])]\n    public function createAgentConfigSignature(int $agentId, string $url, string $nonce, int $timestamp): array\n    {\n        return [\n            'corpid' => $this->corpId,\n            'agentid' => $agentId,\n            'nonceStr' => $nonce,\n            'timestamp' => $timestamp,\n            'url' => $url,\n            'signature' => $this->getTicketSignature($this->getAgentTicket($agentId), $nonce, $timestamp, $url),\n        ];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAgentTicket(int $agentId): string\n    {\n        $key = $this->getAgentKey($agentId);\n        $ticket = $this->cache->get($key);\n\n        if ($ticket && is_string($ticket)) {\n            return $ticket;\n        }\n\n        $response = $this->httpClient->request('GET', '/cgi-bin/ticket/get', ['query' => ['type' => 'agent_config']])\n            ->toArray(false);\n\n        if (empty($response['ticket'])) {\n            throw new HttpException('Failed to get jssdk agentTicket: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($key, $response['ticket'], intval($response['expires_in']));\n\n        return $response['ticket'];\n    }\n\n    public function getAgentKey(int $agentId): string\n    {\n        return sprintf('%s.%s', $this->getKey(), $agentId);\n    }\n}\n"
  },
  {
    "path": "src/Work/Message.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\n/**\n * @property string $Event\n * @property string $InfoType\n * @property string $MsgType\n * @property string $ChangeType\n */\nclass Message extends \\EasyWeChat\\Kernel\\Message\n{\n    //\n}\n"
  },
  {
    "path": "src/Work/Server.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Work;\n\nuse Closure;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Kernel\\Traits\\RespondJsonMessage;\nuse EasyWeChat\\Kernel\\Traits\\RespondXmlMessage;\nuse Nyholm\\Psr7\\Response;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nclass Server implements ServerInterface\n{\n    use DecryptMessage;\n    use InteractWithHandlers;\n    use InteractWithServerRequest;\n    use RespondJsonMessage;\n    use RespondXmlMessage;\n\n    public function __construct(\n        protected Encryptor $encryptor,\n        ?ServerRequestInterface $request = null,\n        protected string $messageType = 'xml',\n    ) {\n        $this->request = $request;\n    }\n\n    public function serve(): ResponseInterface\n    {\n        $query = $this->getRequest()->getQueryParams();\n\n        if (! empty($query['echostr'])) {\n            $response = $this->encryptor->decrypt(\n                $query['echostr'],\n                $query['msg_signature'] ?? '',\n                $query['nonce'] ?? '',\n                $query['timestamp'] ?? ''\n            );\n\n            return new Response(200, [], $response);\n        }\n\n        $message = $this->getRequestMessage($this->getRequest());\n\n        $this->prepend($this->decryptRequestMessage());\n\n        $response = $this->handle(new Response(200, [], 'SUCCESS'), $message);\n\n        if (! ($response instanceof ResponseInterface)) {\n            $response = $this->messageType === 'xml' ?\n                $this->transformToReply($response, $message, $this->encryptor) :\n                $this->transformJsonToReply($response, $message, $this->encryptor);\n        }\n\n        return ServerResponse::make($response);\n    }\n\n    public function handleContactChanged(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserTagUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'update_tag' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserCreated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'create_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'update_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleUserDeleted(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'delete_user' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyCreated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'create_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyUpdated(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'update_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handlePartyDeleted(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'change_contact' && $message->ChangeType === 'delete_party' ? $handler(\n                $message,\n                $next\n            ) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function handleBatchJobsFinished(callable $handler): static\n    {\n        $this->with(function (Message $message, Closure $next) use ($handler): mixed {\n            return $message->Event === 'batch_job_result' ? $handler($message, $next) : $next($message);\n        });\n\n        return $this;\n    }\n\n    public function addMessageListener(string $type, callable $handler): static\n    {\n        $this->withHandler(\n            function (Message $message, Closure $next) use ($type, $handler): mixed {\n                return $message->MsgType === $type ? $handler($message, $next) : $next($message);\n            }\n        );\n\n        return $this;\n    }\n\n    public function addEventListener(string $event, callable $handler): static\n    {\n        $this->withHandler(\n            function (Message $message, Closure $next) use ($event, $handler): mixed {\n                return $message->Event === $event ? $handler($message, $next) : $next($message);\n            }\n        );\n\n        return $this;\n    }\n\n    protected function validateUrl(): Closure\n    {\n        return function (Message $message, Closure $next): Response {\n            $query = $this->getRequest()->getQueryParams();\n            $response = $this->encryptor->decrypt(\n                $query['echostr'],\n                $query['msg_signature'] ?? '',\n                $query['nonce'] ?? '',\n                $query['timestamp'] ?? ''\n            );\n\n            return new Response(200, [], $response);\n        };\n    }\n\n    protected function decryptRequestMessage(): Closure\n    {\n        return function (Message $message, Closure $next): mixed {\n            $query = $this->getRequest()->getQueryParams();\n\n            $params = [\n                $query['msg_signature'] ?? '',\n                $query['timestamp'] ?? '',\n                $query['nonce'] ?? '',\n            ];\n\n            $this->decryptMessage($message, $this->encryptor, ...$params);\n\n            return $next($message);\n        };\n    }\n\n    public function getRequestMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        return Message::createFromRequest($request ?? $this->getRequest());\n    }\n\n    public function getDecryptedMessage(?ServerRequestInterface $request = null): \\EasyWeChat\\Kernel\\Message\n    {\n        $request = $request ?? $this->getRequest();\n        $message = $this->getRequestMessage($request);\n        $query = $request->getQueryParams();\n\n        $params = [\n            $query['msg_signature'] ?? '',\n            $query['timestamp'] ?? '',\n            $query['nonce'] ?? '',\n        ];\n\n        return $this->decryptMessage(\n            $message,\n            $this->encryptor,\n            ...$params\n        );\n    }\n}\n"
  },
  {
    "path": "src/Work/Utils.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Work;\n\nuse EasyWeChat\\Kernel\\Support\\Str;\n\nuse function time;\n\nclass Utils\n{\n    public function __construct(protected Application $app)\n    {\n    }\n\n    /**\n     * @param  array<string>  $jsApiList\n     * @param  array<string>  $openTagList\n     * @return array<string, mixed>\n     */\n    public function buildJsSdkConfig(\n        string $url,\n        array $jsApiList,\n        array $openTagList = [],\n        bool $debug = false,\n        bool $beta = true,\n    ): array {\n        return array_merge(\n            compact('jsApiList', 'openTagList', 'debug', 'beta'),\n            $this->app->getTicket()->createConfigSignature($url, Str::random(), time())\n        );\n    }\n\n    /**\n     * @param  array<string>  $jsApiList\n     * @param  array<string>  $openTagList\n     * @return array<string, mixed>\n     */\n    public function buildJsSdkAgentConfig(\n        int $agentId,\n        string $url,\n        array $jsApiList,\n        array $openTagList = [],\n        bool $debug = false\n    ): array {\n        return array_merge(\n            compact('jsApiList', 'openTagList', 'debug'),\n            $this->app->getTicket()->createAgentConfigSignature($agentId, $url, Str::random(), time())\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/EncryptorTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass EncryptorTest extends TestCase\n{\n    public function getEncryptor()\n    {\n        return new Encryptor('wxb11529c136998cb6', 'pamtest', 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG');\n    }\n\n    public function test_decrypt()\n    {\n        $encrypted = \"<xml>\\n    <ToUserName><![CDATA[asdasdasd]]></ToUserName>\\n    <Encrypt><![CDATA[rTNFcsut4LfGuAFKEUVVpwcaCOTJzOd9twZdIW910jb3k+iicx2uvhttIZ3Qg9Qgty3BEF2xbOrz6boTfb30dMomcgrkTqdFPwnhqbk+kIQ7rZiwny9D7NUrTgA5kpX3KsZvrXzUZyP2x9YOlxbgm572lmxKvM7HAQQhIQ/p6HBmoY30bGXFK0BtIu1pW9TjhOYrLQoU18nWYjWqDA1ynkmOytpv7QRI1P1+0NoxL0q2zO1DgeSvnE8CZGo/o5Ap/WHK5W2RAsinpzN4/LjPnmB6U01I5XCoJoC0GK/yMZycd2Oh8Nq6+wBkC1U85oy0ktOY4nLvsQMLrourmMGdZHuTbqpeJ8Ao/5PRYJ+WBvRUwPfGKBL2+2IKZF49vAJqkcGWSHGE76ZN2erXeuNazf/o9o3lIE3q739o4c8t9QGPe31GT2Go/rOz1BsrASwvauNulCh+++yz+CQzBIuikA==]]></Encrypt>\\n</xml>\\n\";\n        $encrypt = Xml::parse($encrypted);\n        $decrypted = Xml::parse($this->getEncryptor()->decrypt($encrypt['Encrypt'], '4f3ad57b6989f09f4eb392acce4f9e93942ed890', '260774613', '1458300676'));\n\n        $this->assertSame('asdasdasd', $decrypted['ToUserName']);\n        $this->assertSame('asdasdasdsadasd', $decrypted['FromUserName']);\n        $this->assertSame('1234567898', $decrypted['CreateTime']);\n        $this->assertSame('hello', $decrypted['Content']);\n    }\n\n    public function test_decrypt_with_error_signature()\n    {\n        $this->expectExceptionMessage('Invalid Signature.');\n        $encrypted = \"<xml>\\n    <ToUserName><![CDATA[asdasdasd]]></ToUserName>\\n    <Encrypt><![CDATA[rTNFcsut4LfGuAFKEUVVpwcaCOTJzOd9twZdIW910jb3k+iicx2uvhttIZ3Qg9Qgty3BEF2xbOrz6boTfb30dMomcgrkTqdFPwnhqbk+kIQ7rZiwny9D7NUrTgA5kpX3KsZvrXzUZyP2x9YOlxbgm572lmxKvM7HAQQhIQ/p6HBmoY30bGXFK0BtIu1pW9TjhOYrLQoU18nWYjWqDA1ynkmOytpv7QRI1P1+0NoxL0q2zO1DgeSvnE8CZGo/o5Ap/WHK5W2RAsinpzN4/LjPnmB6U01I5XCoJoC0GK/yMZycd2Oh8Nq6+wBkC1U85oy0ktOY4nLvsQMLrourmMGdZHuTbqpeJ8Ao/5PRYJ+WBvRUwPfGKBL2+2IKZF49vAJqkcGWSHGE76ZN2erXeuNazf/o9o3lIE3q739o4c8t9QGPe31GT2Go/rOz1BsrASwvauNulCh+++yz+CQzBIuikA==]]></Encrypt>\\n</xml>\\n\";\n        $encrypt = Xml::parse($encrypted);\n        $this->getEncryptor()->decrypt($encrypt['Encrypt'], 'invalid-signature', '260774613', '1458300676');\n    }\n\n    public function test_decrypt_with_error_received_id()\n    {\n        $this->expectExceptionMessage('Invalid appId.');\n        $encrypted = \"<xml>\\n    <ToUserName><![CDATA[asdasdasd]]></ToUserName>\\n    <Encrypt><![CDATA[rTNFcsut4LfGuAFKEUVVpwcaCOTJzOd9twZdIW910jb3k+iicx2uvhttIZ3Qg9Qgty3BEF2xbOrz6boTfb30dMomcgrkTqdFPwnhqbk+kIQ7rZiwny9D7NUrTgA5kpX3KsZvrXzUZyP2x9YOlxbgm572lmxKvM7HAQQhIQ/p6HBmoY30bGXFK0BtIu1pW9TjhOYrLQoU18nWYjWqDA1ynkmOytpv7QRI1P1+0NoxL0q2zO1DgeSvnE8CZGo/o5Ap/WHK5W2RAsinpzN4/LjPnmB6U01I5XCoJoC0GK/yMZycd2Oh8Nq6+wBkC1U85oy0ktOY4nLvsQMLrourmMGdZHuTbqpeJ8Ao/5PRYJ+WBvRUwPfGKBL2+2IKZF49vAJqkcGWSHGE76ZN2erXeuNazf/o9o3lIE3q739o4c8t9QGPe31GT2Go/rOz1BsrASwvauNulCh+++yz+CQzBIuikA==]]></Encrypt>\\n</xml>\\n\";\n        $encryptor = new Encryptor('invalid appid', 'pamtest', 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG', 'invalid appid');\n        $encrypt = Xml::parse($encrypted);\n        $encryptor->decrypt($encrypt['Encrypt'], '4f3ad57b6989f09f4eb392acce4f9e93942ed890', '260774613', '1458300676');\n    }\n\n    public function test_encrypt_and_decrypt()\n    {\n        $raw = [\n            'ToUserName' => '测试中文',\n            'FromUserName' => 'gh_7f083739789a',\n            'CreateTime' => '1407743423',\n            'MsgType' => 'video',\n            'Video' => [\n                'MediaId' => 'eYJ1MbwPRJtOvIEabaxHs7TX2D-HV71s79GUxqdUkjm6Gs2Ed1KF3ulAOA9H1xG0',\n                'Title' => 'testCallBackReplyVideo',\n                'Description' => 'testCallBackReplyVideo',\n            ],\n        ];\n        $xml = Xml::build($raw);\n        $encrypted = $this->getEncryptor()->encrypt($xml, 'xxxxxx', '1407743423');\n\n        $array = Xml::parse($encrypted);\n        $this->assertSame('1407743423', $array['TimeStamp']);\n        $this->assertSame('xxxxxx', $array['Nonce']);\n        $this->assertNotEmpty($array['Encrypt']);\n        $this->assertNotEmpty($array['MsgSignature']);\n        $this->assertSame($raw, Xml::parse($this->getEncryptor()->decrypt($array['Encrypt'], $array['MsgSignature'], $array['Nonce'], $array['TimeStamp'])));\n    }\n\n    public function test_get_token()\n    {\n        $this->assertSame('pamtest', $this->getEncryptor()->getToken());\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/AccessTokenAwareClientTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AccessTokenAwareClientTest extends TestCase\n{\n    public function test_full_uri_call()\n    {\n        $client = AccessTokenAwareClient::mock();\n\n        $options = [\n            'headers' => [\n                'accept' => 'application/json',\n            ],\n        ];\n\n        $client->request('GET', 'https://api2.mch.weixin.qq.com/v3/certificates', $options);\n\n        $this->assertSame('GET', $client->getRequestMethod());\n        $this->assertSame('https://api2.mch.weixin.qq.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame(['accept: application/json'], $client->getRequestOptions()['headers']);\n    }\n\n    public function test_shortcuts_call()\n    {\n        $client = AccessTokenAwareClient::mock();\n\n        $client->get('v3/certificates', [\n            'headers' => [\n                'accept' => 'application/json',\n            ],\n        ]);\n\n        $this->assertSame('GET', $client->getRequestMethod());\n        $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame(['accept: application/json'], $client->getRequestOptions()['headers']);\n    }\n\n    public function test_it_will_auto_wrap_body()\n    {\n        $client = AccessTokenAwareClient::mock();\n\n        $client->post('v3/certificates', [\n            'body' => [\n                'foo' => 'bar',\n            ],\n        ]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame('foo=bar', $client->getRequestOptions()['body']);\n\n        // post without body key\n        $client = AccessTokenAwareClient::mock();\n        $client->post('v3/certificates', [\n            'foo' => 'bar',\n        ]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame('foo=bar', $client->getRequestOptions()['body']);\n\n        // patch without body key\n        $client = AccessTokenAwareClient::mock();\n        $client->patch('v3/certificates', [\n            'foo' => 'bar',\n        ]);\n\n        $this->assertSame('PATCH', $client->getRequestMethod());\n        $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame('foo=bar', $client->getRequestOptions()['body']);\n\n        // put without body key\n        $client = AccessTokenAwareClient::mock();\n        $client->put('v3/certificates', [\n            'foo' => 'bar',\n        ]);\n\n        $this->assertSame('PUT', $client->getRequestMethod());\n        $this->assertSame('https://example.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame('foo=bar', $client->getRequestOptions()['body']);\n    }\n\n    public function test_it_will_apply_access_token_to_query()\n    {\n        $client = AccessTokenAwareClient::mock();\n\n        $client->withAccessToken(new class implements AccessToken\n        {\n            public function getToken(): string\n            {\n                return 'mock-access-token';\n            }\n\n            public function toQuery(): array\n            {\n                return ['access_token' => 'mock-access-token'];\n            }\n        });\n\n        $client->get('v3/certificates', ['foo' => 'bar']);\n\n        $this->assertSame('https://example.com/v3/certificates?foo=bar&access_token=mock-access-token', $client->getRequestUrl());\n    }\n\n    public function test_it_will_merge_presets()\n    {\n        $client = AccessTokenAwareClient::mock();\n\n        $client->setPresets([\n            'mch_id' => 'mock-mch-id',\n        ]);\n\n        // raw name\n        $client->withMchId()->get('v3/certificates');\n        $this->assertSame('https://example.com/v3/certificates?mch_id=mock-mch-id', $client->getRequestUrl());\n\n        $client = AccessTokenAwareClient::mock();\n\n        $client->setPresets([\n            'mch_id' => 'mock-mch-id',\n        ]);\n\n        // alias\n        $client->withMchIdAs('mchid')->get('v3/certificates');\n        $this->assertSame('https://example.com/v3/certificates?mchid=mock-mch-id', $client->getRequestUrl());\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/AccessTokenExpiredRetryStrategyTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken;\nuse EasyWeChat\\Kernel\\Contracts\\RefreshableAccessToken;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenExpiredRetryStrategy;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\AsyncContext;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\n\nclass AccessTokenExpiredRetryStrategyTest extends TestCase\n{\n    public function test_it_will_passthru_to_parent_retry_strategy()\n    {\n        $strategy = new AccessTokenExpiredRetryStrategy;\n\n        // no decider: 200 should not be retried\n        $context = $this->getContext(2, 'GET', 'http://easywechat.com', 200);\n        $this->assertFalse($strategy->shouldRetry($context, 'mock-response', null));\n\n        // no decider: 429 should be retried (default)\n        $context = $this->getContext(2, 'GET', 'http://easywechat.com', 429);\n        $this->assertTrue($strategy->shouldRetry($context, 'mock-response', null));\n    }\n\n    public function test_it_will_refresh_access_token_when_token_is_refreshable()\n    {\n        $strategy = new AccessTokenExpiredRetryStrategy;\n\n        $notRefreshAbleAccessToken = \\Mockery::mock(AccessToken::class, function ($mock) {\n            $mock->shouldReceive('refresh')->never();\n        });\n\n        // no decider: 200 should not be retried\n        $context = $this->getContext(2, 'GET', 'http://easywechat.com', 200);\n        $this->assertFalse($strategy->shouldRetry($context, 'mock-response', null));\n\n        // with not refreshable access token: 200 should not be retried\n        $strategy->withAccessToken($notRefreshAbleAccessToken)\n            ->decideUsing(function () {\n                return true;\n            });\n\n        $context = $this->getContext(2, 'GET', 'http://easywechat.com', 200);\n        $this->assertFalse($strategy->shouldRetry($context, 'mock-response', null));\n        $this->assertFalse($strategy->shouldRetry($context, 'mock-response', null));\n\n        // with refreshable access token and token expired: should be retried first time\n        $refreshAbleAccessToken = \\Mockery::mock(RefreshableAccessToken::class, function ($mock) {\n            $mock->shouldReceive('refresh')->twice()->andReturns('mock-access-token', false);\n        });\n\n        $strategy->withAccessToken($refreshAbleAccessToken)\n            ->decideUsing(function () {\n                return true;\n            });\n\n        $context = $this->getContext(2, 'GET', 'http://easywechat.com', 200);\n\n        // first time should be retried\n        $this->assertTrue($strategy->shouldRetry($context, 'mock-response', null));\n\n        // refresh failed(no refresh result string returned): should not be retried\n        $this->assertFalse($strategy->shouldRetry($context, 'mock-response', null));\n    }\n\n    private function getContext($retryCount, $method, $url, $statusCode): AsyncContext\n    {\n        $passthru = null;\n        $info = [\n            'retry_count' => $retryCount,\n            'http_method' => $method,\n            'url' => $url,\n            'http_code' => $statusCode,\n        ];\n        $response = new MockResponse('', $info);\n\n        return new AsyncContext($passthru, new MockHttpClient, $response, $info, null, 0);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/HttpClientMethodsTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\HttpClient\\HttpClientMethods;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass HttpClientMethodsTest extends TestCase\n{\n    public function test_get()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->get('http://easywechat.com');\n\n        $this->assertInstanceOf(ResponseInterface::class, $response);\n        $this->assertSame('GET', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n    }\n\n    public function test_post()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->post('http://easywechat.com', ['foo' => 'bar']);\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['body']);\n\n        $response = $client->post('http://easywechat.com', ['json' => ['foo' => 'bar']]);\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['json' => ['foo' => 'bar']], $response->getRequestOptions());\n\n        $response = $client->post('http://easywechat.com', ['xml' => ['foo' => 'bar']]);\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['xml' => ['foo' => 'bar']], $response->getRequestOptions());\n    }\n\n    public function test_post_json()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->postJson('http://easywechat.com', ['foo' => 'bar']);\n\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['json']);\n        $this->assertSame('application/json', $response->getRequestOptions()['headers']['Content-Type']);\n\n        // with options keywords\n        $response = $client->postJson('http://easywechat.com', ['foo' => 'bar', 'query' => 'k1=v1&k2=v2']);\n\n        $this->assertSame(['foo' => 'bar', 'query' => 'k1=v1&k2=v2'], $response->getRequestOptions()['json']);\n    }\n\n    public function test_post_xml()\n    {\n        $client = new DummyHttpClient;\n\n        // no type\n        $response = $client->postXml('http://easywechat.com', ['foo' => 'bar'], [\n            'headers' => ['Accept' => 'application/xml'],\n        ]);\n\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['xml']);\n        $this->assertSame('text/xml', $response->getRequestOptions()['headers']['Content-Type']);\n        $this->assertSame('application/xml', $response->getRequestOptions()['headers']['Accept']);\n\n        // with type\n        $response = $client->postXml('http://easywechat.com', ['xml' => ['foo' => 'bar']]);\n\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['xml']);\n        $this->assertSame('text/xml', $response->getRequestOptions()['headers']['Content-Type']);\n\n        // with string\n        $response = $client->postXml('http://easywechat.com', ['xml' => Xml::build(['foo' => 'bar'])]);\n\n        $this->assertSame('POST', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(Xml::build(['foo' => 'bar']), $response->getRequestOptions()['xml']);\n        $this->assertSame('text/xml', $response->getRequestOptions()['headers']['Content-Type']);\n\n        // with options keywords\n        $response = $client->postXml('http://easywechat.com', ['xml' => ['foo' => 'bar', 'query' => 'k1=v1&k2=v2']]);\n\n        $this->assertSame(['foo' => 'bar', 'query' => 'k1=v1&k2=v2'], $response->getRequestOptions()['xml']);\n    }\n\n    public function test_put()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->put('http://easywechat.com', ['foo' => 'bar']);\n        $this->assertSame('PUT', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['body']);\n    }\n\n    public function test_patch()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->patch('http://easywechat.com', ['foo' => 'bar']);\n        $this->assertSame('PATCH', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['body']);\n    }\n\n    public function test_patch_json()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->patchJson('http://easywechat.com', ['foo' => 'bar']);\n        $this->assertSame('PATCH', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n        $this->assertSame(['foo' => 'bar'], $response->getRequestOptions()['json']);\n        $this->assertSame('application/json', $response->getRequestOptions()['headers']['Content-Type']);\n    }\n\n    public function test_delete()\n    {\n        $client = new DummyHttpClient;\n\n        $response = $client->delete('http://easywechat.com');\n        $this->assertSame('DELETE', $response->getRequestMethod());\n        $this->assertSame('http://easywechat.com', $response->getRequestUrl());\n    }\n}\n\nclass DummyHttpClient\n{\n    use HttpClientMethods;\n\n    public function request($method, $url, $options = []): ResponseInterface\n    {\n        $response = new MockResponse('');\n\n        $response->fromRequest($method, $url, $options, $response);\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/RequestUtilTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\HttpClient\\RequestUtil;\nuse EasyWeChat\\Kernel\\Support\\UserAgent;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy;\n\nclass RequestUtilTest extends TestCase\n{\n    public function test_merge_default_retry_options()\n    {\n        $defaultRetryOptions = [\n            'status_codes' => GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES,\n            'delay' => 1000,\n            'max_delay' => 0,\n            'max_retries' => 3,\n            'multiplier' => 2.0,\n            'jitter' => 0.1,\n        ];\n\n        $this->assertSame($defaultRetryOptions, RequestUtil::mergeDefaultRetryOptions([]));\n        $this->assertSame(\n            \\array_merge($defaultRetryOptions, ['max_retries' => 3, 'jitter' => 2]),\n            RequestUtil::mergeDefaultRetryOptions(['max_retries' => 3, 'jitter' => 2])\n        );\n    }\n\n    public function test_format_default_options()\n    {\n        $options = ['foo' => 'bar', 'headers' => ['User-Agent' => 'EasyWeChat']];\n\n        $formatted = RequestUtil::formatDefaultOptions($options);\n\n        $this->assertArrayNotHasKey('foo', $formatted);\n        $this->assertArrayHasKey('User-Agent', $formatted['headers']);\n        $this->assertSame('EasyWeChat', $formatted['headers']['User-Agent']);\n\n        // test default User-Agent\n        $options = ['foo' => 'bar', 'headers' => ['foo' => 'bar']];\n\n        $formatted = RequestUtil::formatDefaultOptions($options);\n\n        $this->assertArrayNotHasKey('foo', $formatted);\n        $this->assertArrayHasKey('User-Agent', $formatted['headers']);\n        $this->assertSame(UserAgent::create(), $formatted['headers']['User-Agent']);\n    }\n\n    public function test_format_options()\n    {\n        // GET\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat']];\n        $formatted = RequestUtil::formatOptions($options, 'GET');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('query', $formatted);\n        $this->assertSame('true', $formatted['query']['overtrue']);\n        $this->assertSame('world', $formatted['query']['hello']);\n\n        // DELETE\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat']];\n        $formatted = RequestUtil::formatOptions($options, 'DELETE');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('query', $formatted);\n        $this->assertSame('true', $formatted['query']['overtrue']);\n        $this->assertSame('world', $formatted['query']['hello']);\n\n        // POST\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat']];\n        $formatted = RequestUtil::formatOptions($options, 'POST');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('body', $formatted);\n        $this->assertSame('true', $formatted['body']['overtrue']);\n        $this->assertSame('world', $formatted['body']['hello']);\n\n        // POST with json\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat', 'content-type' => 'application/json']];\n        $formatted = RequestUtil::formatOptions($options, 'POST');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('json', $formatted);\n        $this->assertSame(['overtrue' => 'true', 'hello' => 'world'], $formatted['json']);\n\n        // POST with xml\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat', 'content-type' => 'text/xml']];\n        $formatted = RequestUtil::formatOptions($options, 'POST');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('xml', $formatted);\n        $this->assertSame(['overtrue' => 'true', 'hello' => 'world'], $formatted['xml']);\n\n        // PATCH\n        $options = ['overtrue' => 'true', 'hello' => 'world', 'headers' => ['User-Agent' => 'EasyWeChat']];\n        $formatted = RequestUtil::formatOptions($options, 'PATCH');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('body', $formatted);\n        $this->assertSame('true', $formatted['body']['overtrue']);\n        $this->assertSame('world', $formatted['body']['hello']);\n\n        // POST with `query`\n        $options = ['overtrue' => 'true', 'hello' => 'world', '\"query\"' => 'id=1'];\n        $formatted = RequestUtil::formatOptions($options, 'POST');\n\n        $this->assertArrayNotHasKey('overtrue', $formatted);\n        $this->assertArrayNotHasKey('hello', $formatted);\n        $this->assertArrayHasKey('body', $formatted);\n        $this->assertArrayNotHasKey('query', $formatted);\n        $this->assertArrayHasKey('query', $formatted['body']);\n        $this->assertSame('id=1', $formatted['body']['query']);\n        $this->assertSame('true', $formatted['body']['overtrue']);\n        $this->assertSame('world', $formatted['body']['hello']);\n    }\n\n    public function test_format_xml_body()\n    {\n        // xml string\n        $options = RequestUtil::formatBody(['xml' => '<xml><foo>bar</foo></xml>']);\n\n        $this->assertArrayNotHasKey('xml', $options);\n        $this->assertArrayHasKey('body', $options);\n        $this->assertSame('<xml><foo>bar</foo></xml>', $options['body']);\n        $this->assertSame('text/xml', $options['headers']['Content-Type']);\n\n        // xml array\n        $options = RequestUtil::formatBody(['xml' => ['foo' => 'bar']]);\n\n        $this->assertArrayNotHasKey('xml', $options);\n        $this->assertArrayHasKey('body', $options);\n        $this->assertSame('<xml><foo>bar</foo></xml>', $options['body']);\n        $this->assertSame('text/xml', $options['headers']['Content-Type']);\n\n        // invalid xml\n        $this->expectExceptionMessage('The type of `xml` must be string or array.');\n        RequestUtil::formatBody(['xml' => true]);\n    }\n\n    public function test_format_json_body()\n    {\n        // json string\n        $options = RequestUtil::formatBody(['json' => '{\"foo\":\"bar\"}']);\n\n        $this->assertArrayNotHasKey('json', $options);\n        $this->assertArrayHasKey('body', $options);\n        $this->assertSame('{\"foo\":\"bar\"}', $options['body']);\n        $this->assertSame('application/json', $options['headers']['Content-Type']);\n\n        // json array\n        $options = RequestUtil::formatBody(['json' => ['foo' => 'bar', 'chinese' => '中文']]);\n\n        $this->assertArrayNotHasKey('json', $options);\n        $this->assertArrayHasKey('body', $options);\n        $this->assertSame('{\"foo\":\"bar\",\"chinese\":\"中文\"}', $options['body']);\n\n        // json empty array\n        $options = RequestUtil::formatBody(['json' => []]);\n\n        $this->assertArrayNotHasKey('json', $options);\n        $this->assertArrayHasKey('body', $options);\n        $this->assertSame('{}', $options['body']);\n\n        // invalid json\n        $this->expectExceptionMessage('The type of `json` must be string or array.');\n        RequestUtil::formatBody(['json' => true]);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/RequestWithPresetsTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\HttpClient\\RequestWithPresets;\nuse PHPUnit\\Framework\\TestCase;\n\nclass RequestWithPresetsTest extends TestCase\n{\n    public function test_it_can_with_key_value()\n    {\n        $client = new DummyClassForRequestWithPresetsTest;\n\n        $client->with('foo', 'bar')\n            ->with('bar')\n            ->with(['appid' => 'wx123456', 'secret'])\n            ->with([\n                'age' => 12,\n                'name' => 'overtrue',\n            ])\n            ->with('items', ['foo', 'bar']);\n\n        $this->assertSame([\n            'foo' => 'bar',\n            'bar' => null,\n            'appid' => 'wx123456',\n            'secret' => null,\n            'age' => 12,\n            'name' => 'overtrue',\n            'items' => ['foo', 'bar'],\n        ], $client->getPrependsParts());\n\n        // update\n        $client->with('foo', 'baz');\n        $this->assertSame([\n            'foo' => 'baz',\n            'bar' => null,\n            'appid' => 'wx123456',\n            'secret' => null,\n            'age' => 12,\n            'name' => 'overtrue',\n            'items' => ['foo', 'bar'],\n        ], $client->getPrependsParts());\n    }\n\n    public function test_it_can_with_key_of_presets()\n    {\n        $client = new DummyClassForRequestWithPresetsTest;\n\n        $client->setPresets([\n            'appid' => 'wx123456',\n            'secret' => 'helloworld',\n            'bar' => 'baz',\n        ]);\n\n        $client->with('foo', 'bar')\n            ->with('bar')\n            ->with(['appid', 'secret']);\n\n        $this->assertSame([\n            'foo' => 'bar',\n            'bar' => 'baz',\n            'appid' => 'wx123456',\n            'secret' => 'helloworld',\n        ], $client->getPrependsParts());\n    }\n\n    public function test_it_can_with_use_magic_call()\n    {\n        $client = new DummyClassForRequestWithPresetsTest;\n\n        $client->setPresets([\n            'appid' => 'wx123456',\n            'secret' => 'helloworld',\n            'bar' => 'balabala',\n            'name' => 'w7corp',\n        ]);\n\n        $client->with('foo', 'bar')\n            ->with('bar')\n            ->withAppid()\n            ->withSecret()\n            ->withBarAs('baz')\n            ->withName('overtrue');\n\n        $this->assertSame([\n            'foo' => 'bar',\n            'bar' => 'balabala',\n            'appid' => 'wx123456',\n            'secret' => 'helloworld',\n            'baz' => 'balabala',\n            'name' => 'overtrue',\n        ], $client->getPrependsParts());\n    }\n\n    public function test_it_can_merge_to_options()\n    {\n        $client = new DummyClassForRequestWithPresetsTest;\n\n        $client->setPresets([\n            'appid' => 'wx123456',\n            'secret' => 'helloworld',\n            'bar' => 'balabala',\n            'name' => 'w7corp',\n        ]);\n\n        // empty\n        $this->assertSame([], $client->mergeThenResetPrepends([]));\n\n        // GET/HEAD/DELETE\n        $client->withAppid()->withSecret();\n        $this->assertSame(['query' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([]));\n        $client->withAppid()->withSecret();\n        $this->assertSame(['query' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'HEAD'));\n        $client->withAppid()->withSecret();\n        $this->assertSame(['query' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'DELETE'));\n\n        // POST/PUT/PATCH\n        $client->withAppid()->withSecret();\n        $this->assertSame(['body' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'POST'));\n        $client->withAppid()->withSecret();\n        $this->assertSame(['body' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'PUT'));\n        $client->withAppid()->withSecret();\n        $this->assertSame(['body' => ['appid' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'PATCH'));\n\n        // merge\n        $client->withAppid();\n        $this->assertSame(\n            ['query' => ['appid' => 'wx123456', 'name' => '1234']],\n            $client->mergeThenResetPrepends(['query' => ['name' => '1234']])\n        );\n\n        $client->withAppid();\n        $this->assertSame(\n            ['body' => ['appid' => 'wx123456', 'name' => '1234']],\n            $client->mergeThenResetPrepends(['body' => ['name' => '1234']], 'POST')\n        );\n\n        // XML\n        // 1. !empty xml\n        $client->withAppid();\n        $this->assertSame(\n            ['xml' => ['appid' => 'wx123456', 'name' => '1234']],\n            $client->mergeThenResetPrepends(['xml' => ['name' => '1234']], 'POST')\n        );\n        // 2. content-type is xml\n        $client->withAppid();\n        $this->assertSame(\n            ['headers' => ['content-type' => 'text/xml'], 'xml' => ['appid' => 'wx123456']],\n            $client->mergeThenResetPrepends(['headers' => ['content-type' => 'text/xml']], 'POST')\n        );\n\n        // JSON\n        // 1. !empty json\n        $client->withAppid();\n        $this->assertSame(\n            ['json' => ['appid' => 'wx123456', 'name' => '1234']],\n            $client->mergeThenResetPrepends(['json' => ['name' => '1234']], 'POST')\n        );\n        // 2. content-type is json\n        $client->withAppid();\n        $this->assertSame(\n            ['headers' => ['content-type' => 'application/json'], 'json' => ['appid' => 'wx123456']],\n            $client->mergeThenResetPrepends(['headers' => ['content-type' => 'application/json']], 'POST')\n        );\n\n        // HEADERS\n        $client->withAppid()->withHeader('X-foo', 'bar');\n        $this->assertSame(\n            ['headers' => ['X-foo' => 'bar', 'content-type' => 'application/json'], 'json' => ['appid' => 'wx123456']],\n            $client->mergeThenResetPrepends(['headers' => ['content-type' => 'application/json']], 'POST')\n        );\n\n        // 带下划线\n        $client->setPresets([\n            'app_id' => 'wx123456',\n            'secret' => 'helloworld',\n            'bar' => 'balabala',\n            'name' => 'w7corp',\n        ]);\n\n        // GET/HEAD/DELETE\n        $client->withAppId()->withSecret();\n        $this->assertSame(['query' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([]));\n        $client->withAppId()->withSecret();\n        $this->assertSame(['query' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'HEAD'));\n        $client->withAppId()->withSecret();\n        $this->assertSame(['query' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'DELETE'));\n\n        // POST/PUT/PATCH\n        $client->withAppId()->withSecret();\n        $this->assertSame(['body' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'POST'));\n        $client->withAppId()->withSecret();\n        $this->assertSame(['body' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'PUT'));\n        $client->withAppId()->withSecret();\n        $this->assertSame(['body' => ['app_id' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([], 'PATCH'));\n\n        // test As\n        $client->withAppIdAs('test')->withSecret();\n        $this->assertSame(['query' => ['test' => 'wx123456', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([]));\n\n        // test key/value\n        $client->withAppId('test')->withSecret();\n        $this->assertSame(['query' => ['app_id' => 'test', 'secret' => 'helloworld']], $client->mergeThenResetPrepends([]));\n    }\n\n    public function test_it_can_with_headers()\n    {\n        $client = new DummyClassForRequestWithPresetsTest;\n\n        $client->withHeaders(['content-type' => 'application/xml'])->withHeader('accept', 'application/json');\n\n        $this->assertSame(\n            ['content-type' => 'application/xml', 'accept' => 'application/json'],\n            $client->getPrependsHeaders()\n        );\n\n        // update\n        $client->withHeaders(['content-type' => 'text/xml']);\n        $this->assertSame(\n            ['content-type' => 'text/xml', 'accept' => 'application/json'],\n            $client->getPrependsHeaders()\n        );\n    }\n}\n\nclass DummyClassForRequestWithPresetsTest\n{\n    use RequestWithPresets;\n\n    public function getPrependsParts(): array\n    {\n        return $this->prependParts;\n    }\n\n    public function getPrependsHeaders(): array\n    {\n        return $this->prependHeaders;\n    }\n\n    public function __call(string $name, array $arguments)\n    {\n        if (\\str_starts_with($name, 'with')) {\n            return $this->handleMagicWithCall($name, $arguments[0] ?? null);\n        }\n\n        throw new \\BadMethodCallException('Call to undefined method '.__CLASS__.'::'.$name.'()');\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/ResponseTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\Exceptions\\BadResponseException;\nuse EasyWeChat\\Kernel\\HttpClient\\Response;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass ResponseTest extends TestCase\n{\n    public function test_it_will_throw_if_body_is_empty()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getContent')->andReturns('');\n        });\n\n        $this->expectException(BadResponseException::class);\n        (new Response($response))->toArray();\n    }\n\n    public function test_it_can_decode_xml()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns([], ['content-type' => ['text/xml']], ['content-type' => ['text/xml']]);\n            $mock->shouldReceive('getContent')->andReturns('<xml><foo>bar</foo></xml>', '<xml><foo>bar</foo></xml>', '<invalid xml>');\n        });\n\n        $this->assertSame(['foo' => 'bar'], (new Response($response))->toArray());\n        $this->assertSame(['foo' => 'bar'], (new Response($response))->toArray());\n\n        $response = (new Response($response))->toArray();\n        $this->assertIsArray($response);\n        $this->assertEmpty($response);\n    }\n\n    public function test_it_support_array_access()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns([]);\n            $mock->shouldReceive('getContent')->andReturns('{\"foo\":\"bar\"}');\n            $mock->shouldReceive('toArray')->andReturns(['foo' => 'bar']);\n        });\n\n        $response = (new Response($response));\n\n        $this->assertSame('bar', $response['foo']);\n        $this->assertNull($response['not-exist']);\n\n        $this->assertTrue(isset($response['foo']));\n        $this->assertFalse(isset($response['not-exist']));\n    }\n\n    public function test_it_support_to_json()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns([]);\n            $mock->shouldReceive('getContent')->andReturns('{\"foo\":\"bar\"}');\n            $mock->shouldReceive('toArray')->andReturns(['foo' => 'bar']);\n        });\n\n        $response = (new Response($response));\n\n        $this->assertSame('{\"foo\":\"bar\"}', $response->toJson());\n    }\n\n    public function test_it_can_get_headers()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns([\n                'content-type' => ['text/xml; encoding=utf-8'],\n                'cache-control' => ['max-age=3600', 'public'],\n            ]);\n        });\n\n        $response = (new Response($response));\n\n        $this->assertTrue($response->hasHeader('content-type'));\n        $this->assertSame(['text/xml; encoding=utf-8'], $response->getHeader('content-type'));\n        $this->assertSame('max-age=3600,public', $response->getHeaderLine('cache-control'));\n    }\n\n    public function test_it_can_save_content_to_files()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns([]);\n            $mock->shouldReceive('getContent')->andReturns('{\"foo\":\"bar\"}');\n            $mock->shouldReceive('toArray')->andReturns(['foo' => 'bar']);\n        });\n\n        $response = (new Response($response));\n        $tmpFile = \\sys_get_temp_dir().'/'.\\uniqid('', true);\n        $response->saveAs($tmpFile);\n\n        $this->assertSame('{\"foo\":\"bar\"}', \\file_get_contents($tmpFile));\n        @\\unlink($tmpFile);\n\n        // throw when response get content failed\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getContent')->with(true)->andThrow(new \\Exception('mock-exception'))->once();\n            $mock->shouldReceive('getContent')->with(false)->andReturns('{\"errcode\":40029, \"errmsg\":\"invalid code\"}')->once();\n        });\n        $response = (new Response($response));\n\n        $this->expectException(BadResponseException::class);\n        $this->expectExceptionMessageMatches('/Cannot save response to .*?: {\"errcode\":40029, \"errmsg\":\"invalid code\"}/');\n\n        $response->saveAs($tmpFile);\n    }\n\n    public function test_it_can_transform_to_data_url()\n    {\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns(['content-type' => ['application/json;encoding=utf-8']]);\n            $mock->shouldReceive('getContent')->andReturns('{\"foo\":\"bar\"}');\n            $mock->shouldReceive('toArray')->andReturns(['foo' => 'bar']);\n        });\n\n        $response = (new Response($response));\n\n        $this->assertSame('data:application/json;encoding=utf-8;base64,eyJmb28iOiJiYXIifQ==', $response->toDataUrl());\n    }\n\n    public function test_it_can_judge_failure_with_custom_callback()\n    {\n        // from http code 200\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getStatusCode')->andReturns(200)->times(2);\n            $mock->shouldReceive('getHeaders')->andReturns(['content-type' => ['application/json;encoding=utf-8']]);\n        });\n\n        $response = (new Response($response));\n\n        $this->assertFalse($response->isFailed());  // 200\n        $this->assertTrue($response->isSuccessful());\n\n        // from http code 400\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getHeaders')->andReturns(['content-type' => ['application/json;encoding=utf-8']]);\n            $mock->shouldReceive('getStatusCode')->andReturns(400)->times(2);\n        });\n\n        $response = (new Response($response));\n        $this->assertTrue($response->isFailed());   // 400\n        $this->assertFalse($response->isSuccessful());\n\n        // custom callback\n        $response = \\Mockery::mock(ResponseInterface::class, function ($mock) {\n            $mock->shouldReceive('getStatusCode')->never();\n            $mock->shouldReceive('getHeaders')->andReturns(['content-type' => ['application/json;encoding=utf-8']]);\n            $mock->shouldReceive('getContent')->andReturns(\\json_encode(['errcode' => 40029, 'errmsg' => 'invalid code']));\n            $mock->shouldReceive('toArray')->andReturns(['errcode' => 40029, 'errmsg' => 'invalid code']);\n        });\n\n        $response = (new Response($response));\n\n        $response->judgeFailureUsing(function ($response) {\n            return ! empty($response->toArray()['errcode'] ?? null);\n        });\n\n        $this->assertTrue($response->isFailed());\n        $this->assertFalse($response->isSuccessful());\n    }\n\n    public function test_it_can_has_global_throw_settings()\n    {\n        $httpClient = new MockHttpClient(new MockResponse('{\"foo\":\"bar\"}', ['http_code' => 403]));\n        $response = (new Response($httpClient->request('GET', '/foo'), throw: false));\n\n        // global throw setting is false\n        try {\n            $this->assertSame(['foo' => 'bar'], $response->toArray());\n            $this->assertSame('{\"foo\":\"bar\"}', $response->getContent());\n        } catch (\\Exception $e) {\n            $this->fail('should not throw exception');\n        }\n\n        // global throw setting is ignored\n        try {\n            $response->toArray(true);\n            $this->fail('should throw exception');\n        } catch (\\Exception $e) {\n            $this->assertSame('HTTP 403 returned for \"https://example.com/foo\".', $e->getMessage());\n        }\n\n        try {\n            $response->getContent(true);\n            $this->fail('should throw exception');\n        } catch (\\Exception $e) {\n            $this->assertSame('HTTP 403 returned for \"https://example.com/foo\".', $e->getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/HttpClient/RetryableClientTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\HttpClient;\n\nuse EasyWeChat\\Kernel\\HttpClient\\RetryableClient;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\DecoratorTrait;\nuse Symfony\\Component\\HttpClient\\Exception\\ServerException;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy;\nuse Symfony\\Component\\HttpClient\\RetryableHttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass RetryableClientTest extends TestCase\n{\n    public function test_it_can_retry_with_default_config()\n    {\n        $client = new DummyClientForRetryableClientTest([\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 200]),\n        ]);\n\n        $this->assertInstanceOf(HttpClientInterface::class, $client->getClient());\n        $this->assertNotInstanceOf(RetryableHttpClient::class, $client->getClient());\n\n        // No retry\n        $response = $client->request('GET', 'http://foo.com');\n        $this->assertSame(500, $response->getStatusCode());\n\n        // Retry\n        $client->retry(['delay' => 10]);\n        $this->assertInstanceOf(RetryableHttpClient::class, $client->getClient());\n\n        // default retry 3 times\n        $response = $client->request('GET', 'http://foo.com');\n        $this->assertSame(200, $response->getStatusCode());\n    }\n\n    public function test_it_can_retry_with_custom_strategy()\n    {\n        $client = new DummyClientForRetryableClientTest([\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 500]),\n            new MockResponse('', ['http_code' => 200]),\n        ]);\n\n        $response = $client->request('GET', 'https://easywechat.com');\n\n        $this->assertNull($response->getInfo('ret'));\n\n        $client->retryUsing(new class extends GenericRetryStrategy\n        {\n            public function __construct(\n                array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES,\n                int $delayMs = 1000,\n                float $multiplier = 2.0,\n                int $maxDelayMs = 0,\n                float $jitter = 0.1\n            ) {\n                parent::__construct($statusCodes, 10, $multiplier, $maxDelayMs, $jitter);\n            }\n        }, maxRetries: 2);\n\n        $this->expectException(ServerException::class);\n        $client->request('GET', 'http://foo.com');\n    }\n}\n\nclass DummyClientForRetryableClientTest implements HttpClientInterface\n{\n    use DecoratorTrait;\n    use RetryableClient;\n\n    public function __construct($response = null)\n    {\n        $this->client = new MockHttpClient($response);\n    }\n\n    public function getClient(): HttpClientInterface\n    {\n        return $this->client;\n    }\n\n    public function __call(string $name, array $arguments)\n    {\n        return $this->client->{$name}(...$arguments);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/MessageTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel;\n\nuse EasyWeChat\\Pay\\Message;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass MessageTest extends TestCase\n{\n    public function test_message()\n    {\n        $message = new Message(['one' => 1]);\n        $this->assertSame($message->one, 1);\n        $this->assertSame($message['one'], 1);\n    }\n\n    public function test_message_can_be_encode_as_json()\n    {\n        $message = new Message(['one' => 1]);\n        $this->assertSame(json_encode($message), '{\"one\":1}');\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/ServerResponseTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel;\n\nuse EasyWeChat\\Kernel\\ServerResponse;\nuse Nyholm\\Psr7\\Response;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ServerResponseTest extends TestCase\n{\n    public function test_to_string()\n    {\n        $response = ServerResponse::make(new Response(200, ['X-Foo' => 'bar'], 'foo'));\n\n        $responseLines = explode(\"\\r\\n\", $response);\n        $this->assertEquals('HTTP/1.1 200 OK', $responseLines[0]);\n        $this->assertEquals('X-Foo: bar', $responseLines[1]);\n        $this->assertEquals('foo', $responseLines[3]);\n    }\n\n    public function test_to_string_without_headers()\n    {\n        $response = ServerResponse::make(new Response(200, [], 'foo'));\n\n        $responseLines = explode(\"\\r\\n\", $response);\n        $this->assertEquals('HTTP/1.1 200 OK', $responseLines[0]);\n        $this->assertEquals('foo', $responseLines[2]);\n    }\n\n    public function test_it_can_send_response()\n    {\n        \\ob_start();\n        $response = ServerResponse::make(new Response(200, ['X-Foo' => 'bar'], 'foo'));\n        $response->sendContent();\n        $contents = \\ob_get_contents();\n        \\ob_end_clean();\n\n        $this->assertSame('foo', $contents);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/AesCbcTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Support\\AesCbc;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AesCbcTest extends TestCase\n{\n    public function test_it_can_encrypt_and_decrypt()\n    {\n        $iv = \\base64_decode('r7BXXKkLb8qrSNn05n0qiA==');\n        $key = \\base64_decode('tiihtNczf5v6AKRyjwEUhQ==');\n        $plaintext = '{\"openId\":\"oGZUI0egBJY1zhBYw2KhdUfwVJJE\",\"nickName\":\"Band\",\"gender\":1,\"language\":\"zh_CN\",\"city\":\"Guangzhou\",\"province\":\"Guangdong\",\"country\":\"CN\",\"avatarUrl\":\"http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0\",\"unionId\":\"ocMvos6NjeKLIBqg5Mr9QjxrP1FA\",\"watermark\":{\"timestamp\":1477314187,\"appid\":\"wx4f4bc4dec97d474b\"}}';\n        $ciphertext = 'CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==';\n\n        // encrypt\n        $this->assertSame($ciphertext, AesCbc::encrypt($plaintext, $key, $iv));\n\n        // decrypt\n        $json = \\json_decode(AesCbc::decrypt($ciphertext, $key, $iv), true);\n        $this->assertSame('oGZUI0egBJY1zhBYw2KhdUfwVJJE', $json['openId']);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/AesEcbTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Support\\AesEcb;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AesEcbTest extends TestCase\n{\n    public function test_it_can_encrypt_and_decrypt()\n    {\n        $key = md5('1234567890abcdef1234567890abcdef');\n        $plaintext = '<root><out_refund_no><![CDATA[123]]></out_refund_no><out_trade_no><![CDATA[abc]]></out_trade_no><refund_account><![CDATA[REFUND_SOURCE_UNSETTLED_FUNDS]]></refund_account><refund_fee><![CDATA[1]]></refund_fee><refund_id><![CDATA[50000303712017072701466990000]]></refund_id><refund_recv_accout><![CDATA[用户零钱]]></refund_recv_accout><refund_request_source><![CDATA[API]]></refund_request_source><refund_status><![CDATA[SUCCESS]]></refund_status><settlement_refund_fee><![CDATA[1]]></settlement_refund_fee><settlement_total_fee><![CDATA[1]]></settlement_total_fee><success_time><![CDATA[2017-07-27 00:33:09]]></success_time><total_fee><![CDATA[1]]></total_fee><transaction_id><![CDATA[4005302001201707272838220000]]></transaction_id></root>';\n        $ciphertext = 'm+cqC5OWNek/jGSeSFjMwxqoQmt9x/XQEPRosjYHR2a6kosdkuS2hFlt1QP/ykVOCm6PXnC0tOc7gsPzP3aG4Hhn7wJwJAvMhSUJhOAOlnXtHupbyiwb9vNgqAwcSr04U1yoI8UemDCz+TnIsbldurZh6UKtDqSutp/KiutgJ/9crss+fh9hy9UcBKO60JkJgka79q6lkQoOKZh3kIHrFEZGFKcvCOJzx/heVtz8AHGoB/IuNV4Mh280FZM1TTe8V54eXgqHNAOdJCoYQuKu34tepA+a4sjCcPOmNU5wLCjEFQ/+w7Ad8U2i3bfaA713DPk5qV8IVSB1cMGZj+zZBGPT4OWBg0vZD4ZJCydf93e95CbxV7FuSPiFnZwjvsHBCA7DNGoAfSx72p5ZBcyCTFV9y4O9xTukHUmJNI8XK+JhR5Imz9u5422lfN5FcM6g4WdLDTVO/DiN4chaTUk9uqEiMqD2Bn3+ZWe/R91YDW8koG3qd7m/9y7sckptNQWU9fi+zk/AbCLHETiUIj4dtFxsZRTBUFIEmSl2ebcPEdowOLzjUe2uW/Qr8dDwFuWGsSYawYnsbsxliNc5DthzhcdB9MDkOab6hckUIC7639s6DKFP44Olgjc+tt5EfDpNxK0rHh4rhlCz9h5ZhzI7CVqJRx5pCLEBaJKntPeF9IWfj92VYMY7o8TesmqhiDWnGVGSK8vXDsQMAWIhHi2STvdVAZkaOTzF+cxVJsUgy2zVlGhNzlqjQZmLoXZ/Kiid7cfUQvg/Bqw4We6KRrAfTHplwjbghjVzqWsgJv8KI3cYjFJEtTy19a0z3yBrcthtjszmBEUyUG/d4O0DzGEG+JNB4VsMz/jWUJ2d2uJmpyvngyt5RkafRH4mCHWkNTPz5UCBZtJbvFzQ/VB/X077Apwgk7lfgULu1uVF9N108kRKQcrqoGEH/oZ6lo0wIXbITx+7lr91eMvu4JgGPSQW4MKbluSa28iwkE5YcnQbYd0=';\n\n        // encrypt\n        $this->assertSame($ciphertext, AesEcb::encrypt($plaintext, $key));\n\n        // decrypt\n        $xml = Xml::parse(AesEcb::decrypt($ciphertext, $key));\n        $this->assertSame('123', $xml['out_refund_no']);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/AesGcmTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Support\\AesGcm;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AesGcmTest extends TestCase\n{\n    public function test_it_can_encrypt_and_decrypt()\n    {\n        // 感谢大佬 @TheNorthMemory 提供的测试数据\n        $key = '5YI2YwEdV56hCsNEoOGEeL17vRFcz3i6';\n        $nonce = 'katvtHDDPxkw';\n        $aad = 'transaction';\n        $plaintext = '{\"transaction_id\":\"1217752501201407033233368018\",\"amount\":{\"payer_total\":100,\"total\":100,\"currency\":\"CNY\",\"payer_currency\":\"CNY\"},\"mchid\":\"1230000109\",\"trade_state\":\"SUCCESS\",\"bank_type\":\"CMC\",\"promotion_detail\":[{\"amount\":100,\"wechatpay_contribute\":0,\"coupon_id\":\"109519\",\"scope\":\"GLOBAL\",\"merchant_contribute\":0,\"name\":\"单品惠-6\",\"other_contribute\":0,\"currency\":\"CNY\",\"stock_id\":\"931386\",\"goods_detail\":[{\"goods_remark\":\"商品备注信息\",\"quantity\":1,\"discount_amount\":1,\"goods_id\":\"M1006\",\"unit_price\":100},{\"goods_remark\":\"商品备注信息\",\"quantity\":1,\"discount_amount\":1,\"goods_id\":\"M1006\",\"unit_price\":100}]},{\"amount\":100,\"wechatpay_contribute\":0,\"coupon_id\":\"109519\",\"scope\":\"GLOBAL\",\"merchant_contribute\":0,\"name\":\"单品惠-6\",\"other_contribute\":0,\"currency\":\"CNY\",\"stock_id\":\"931386\",\"goods_detail\":[{\"goods_remark\":\"商品备注信息\",\"quantity\":1,\"discount_amount\":1,\"goods_id\":\"M1006\",\"unit_price\":100},{\"goods_remark\":\"商品备注信息\",\"quantity\":1,\"discount_amount\":1,\"goods_id\":\"M1006\",\"unit_price\":100}]}],\"success_time\":\"2018-06-08T10:34:56+08:00\",\"payer\":{\"openid\":\"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\"},\"out_trade_no\":\"1217752501201407033233368018\",\"appid\":\"wxd678efh567hg6787\",\"trade_state_desc\":\"支付成功\",\"trade_type\":\"MICROPAY\",\"attach\":\"自定义数据\",\"scene_info\":{\"device_id\":\"013467007045764\"}}';\n        $ciphertext = 'LsCtZf91SEdVjdVNHtl6cnmrMz6atFZbsF0cwQQmgrTSb4WOey3uaKPqKbVb3E+9bT65ND/vJDD7/kLBnqOf8j2niXJ3vtdx11dpAG9yevfWdD8My6k9moQ+uuoLq9D1LVM4/QFGUYU68mKZr5FsPjWlImj06vg010LWzNe24cInjPNfelvfsz1xDKVpMwC1dy4ANPt4ZOCmch/pfezLrg13f/aTs8Rs9v4v+B1jqme9oyUnSVFOOU19gCPvyvCLSVbvf3Ng0noWHKDh2IbA7tozFDkZr8q9Y9f9igfCUq4W4LCnyhIyidqYr17DM8O0I+JlAf+Awf0xFYRMDckQItWAsYp/a11Z3wZ4pEurRlL8Dvz1PsZU9X4ZmCtxfvYVND+b1Xf4zWJV4e9atODn9JIXg8ENmGASrxG64hOQI4al79DT2kIoWSy7SmdyJX0ZI6MILD+x4f5gi4GAT7lpw0ufXK73YXIRfvzEU3EsJDc72AHAsACTdWosSOmLejB2W0Saf27HqDxGOvb2IFaIlwVgL7g7hCItbFJ0MV3X7clwU0V35T+hF5lrrB9OSH0w0GWeRNZmWjeR+EQX2WJWB0JnG7ZKCtxkMhPcCThze92TkIJsCHj0c2uCOru37hMXvce3qJNZSKpMkperGia4HeqVRI+Zn+qc1PCfK+1m8wqdnROMzEa8U3Gw9Exx0CuYff6NGmIMQedr6IlBcJpM8loR1Q3b5ea2oI4lGy+3akXT70QkVRExIcubvPsCzf8r14+rgbonKCkGeAfSiIzRzW7u9HkTSzcZPDQqzMtaIsYVLx/b+eQ0tuGaYbLAfTO0yn+MJx+EQMAPi31qTAggmmSZOudvZHal2/azn12+dSF1Im1KZdddAFcTahKcu9Iu4qZo7q8XvOHuSLjSxh6aAWij6c4HbdxgxDw5zkk3GZtqG+u977HlPDgnJCDTvbTvQ7vrnptBzkXJ3QehJ8AlNLdW3GXXSF3ZkOPYNYnlrbEIpPu255nNj0aTURH4UXQgO+LCjy53bCEumlSoO91BQ1++WSBVnJK9xm5yuBDkjFSZjwQ27IM3V66db3766QUnUG8gnqASMzXQ+eNTAUMTNW8NC7ccQ1Z2YCsDVhEH7fDzbSuTExIV+/fgIE4Zpn7r+Ry+AnTRn92CQRW1ri8+hdL60uQDiBma1fovMNPX/cVrNwPF2r+b12Fp8KLF51572KCy487nj2+1OhzF8tbe8ZPLQRvv2bHKJnubQDKHP607KdbZMMPrl9BfpTQYVUKwo1GAjytizlQpVvWf4+PVgDjdys84tiw+iS9/HStC9NatcRZqXn4YR9c69ICaCqiHKEV9S7EPEI73nmb1o/Jl+Pnl/wul+eOtK5X3LY+n5Rh+WciCL4zZG64lsUGzRgFprxoJBqDJFFydVTzKktQT2aoHiUpk9j4Vc3fcmYru0CfUBi5qhi2bKr8+PwWWKgS1mPeU3o/cqkBUqSAXTjewq298351pQ7LVSadp+UXR5je+Nal5OfCVCAVyUB4PVhRTQLPcubRLwYRh/+1LLL/xizgUf6rCOcYNZQ3MnCUCdgVKE4IftSmFQwE91fdVoJQUbrsNuNchG26681XekJVh70iLhzmkYaSdE69WPfN01R8B4uQziY/VHLztg60F9vEIZs5DxPj2lafiwDGTk+cSH/VF2DC+A/8mJlqZCMEn2kv1SDg1i0aV74MJn6EDkI2NyahXAzvEEpdUDG9nRtRqSy9DA/E5Ae0HV0jqWXSGYSrXOT07PNqCxgzfgXE/oEGuGTb/30rD7dU=';\n\n        // encrypt\n        $this->assertSame($ciphertext, AesGcm::encrypt($plaintext, $key, $nonce, $aad));\n\n        // decrypt\n        $json = \\json_decode(AesGcm::decrypt($ciphertext, $key, $nonce, $aad), true);\n\n        $this->assertSame('1217752501201407033233368018', $json['transaction_id']);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/MessageParserTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Exceptions\\BadRequestException;\nuse EasyWeChat\\Kernel\\Support\\MessageParser;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass MessageParserTest extends TestCase\n{\n    public function test_it_can_parse_json_content()\n    {\n        $content = '{\"key\":\"value\",\"number\":123}';\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['key']);\n        $this->assertSame(123, $result['number']);\n    }\n\n    public function test_it_can_parse_json_with_whitespace()\n    {\n        $content = \"  \\n\\t  {\\\"key\\\":\\\"value\\\"}  \\n  \";\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['key']);\n    }\n\n    public function test_it_falls_back_to_xml_when_json_decode_fails()\n    {\n        $content = '<xml><key>value</key><number>123</number></xml>';\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['key']);\n        $this->assertSame('123', $result['number']);\n    }\n\n    public function test_it_falls_back_to_xml_when_json_is_not_array()\n    {\n        // JSON string (not an array) should fall back to XML\n        // Since it's not valid XML either, it should throw exception\n        $this->expectException(BadRequestException::class);\n        MessageParser::parse('\"just a string\"');\n    }\n\n    public function test_it_falls_back_to_xml_when_json_is_empty_array()\n    {\n        // Empty JSON array should fall back to XML\n        // Since empty array is not valid XML, it should throw exception\n        $this->expectException(BadRequestException::class);\n        MessageParser::parse('[]');\n    }\n\n    public function test_it_falls_back_to_xml_when_json_is_not_array_but_xml_is_valid()\n    {\n        // JSON that parses to a string (not array) should fall back to XML\n        // If the content is also valid XML, it should parse as XML\n        $content = '<xml><key>value</key></xml>';\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['key']);\n    }\n\n    public function test_it_throws_exception_when_both_json_and_xml_fail()\n    {\n        $this->expectException(BadRequestException::class);\n        $this->expectExceptionMessage('Failed to decode content. Content must be valid XML or JSON.');\n\n        MessageParser::parse('invalid content');\n    }\n\n    public function test_it_prioritizes_json_over_xml()\n    {\n        // Content that could be both valid JSON and valid XML\n        // JSON should be parsed first\n        $content = '{\"xml\":\"value\"}';\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['xml']);\n    }\n\n    public function test_it_can_parse_xml_with_whitespace()\n    {\n        $content = \"  \\n  <xml><key>value</key></xml>  \\n  \";\n        $result = MessageParser::parse($content);\n\n        $this->assertIsArray($result);\n        $this->assertSame('value', $result['key']);\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/PrivateKeyTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass PrivateKeyTest extends TestCase\n{\n    public function test_create_from_contents()\n    {\n        $contents = file_get_contents(__DIR__.'/../../fixtures/private.key') ?: '';\n        $key = new PrivateKey($contents, 'overtrue');\n\n        $this->assertSame($contents, $key->getKey());\n        $this->assertSame('overtrue', $key->getPassphrase());\n    }\n\n    public function test_create_from_path()\n    {\n        $path = __DIR__.'/../../fixtures/private.key';\n        $contents = file_get_contents($path) ?: '';\n        $key = new PrivateKey($path, 'overtrue');\n\n        $this->assertSame($contents, $key->getKey());\n        $this->assertSame('overtrue', $key->getPassphrase());\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/PublicKeyTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass PublicKeyTest extends TestCase\n{\n    public function test_create_from_contents()\n    {\n        $contents = file_get_contents(__DIR__.'/../../fixtures/cert.pem') ?: '';\n        $cert = new PublicKey($contents);\n\n        $this->assertSame($contents, \\strval($cert));\n    }\n\n    public function test_create_from_path()\n    {\n        $path = __DIR__.'/../../fixtures/cert.pem';\n        $contents = file_get_contents($path) ?: '';\n        $cert = new PublicKey($path);\n\n        $this->assertSame($contents, \\strval($cert));\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Support/UserAgentTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Support;\n\nuse Composer\\InstalledVersions;\nuse EasyWeChat\\Kernel\\Support\\UserAgent;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass UserAgentTest extends TestCase\n{\n    public function test_it_can_generate_user_agent()\n    {\n        $this->assertSame(\n            \\sprintf(\n                'easywechat-sdk/%s OS/%s curl/%s custom-part custom-part2',\n                InstalledVersions::getVersion('w7corp/easywechat'),\n                php_uname('s').'/'.php_uname('r'),\n                \\curl_version()['version']\n            ),\n            UserAgent::create(['custom-part', 'custom-part2'])\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/DecryptJsonMessageTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Message;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Tests\\TestCase;\nuse Nyholm\\Psr7\\ServerRequest;\n\nclass DecryptJsonMessageTest extends TestCase\n{\n    use DecryptMessage;\n\n    public function test_it_can_decrypt_json_message(): void\n    {\n        $plaintext = json_encode([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'FromUserName' => 'mycreate',\n            'CreateTime' => '1409659813',\n            'MsgType' => 'text',\n            'Content' => 'hello',\n            'MsgId' => '4561255354251345929',\n        ], JSON_UNESCAPED_UNICODE);\n\n        $this->assertIsString($plaintext);\n\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $encrypted = $encryptor->encryptAsArray(\n            plaintext: $plaintext,\n            nonce: '1372623149',\n            timestamp: '1409659813'\n        );\n\n        $body = json_encode([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'Encrypt' => $encrypted['ciphertext'],\n            'AgentID' => '218',\n        ], JSON_UNESCAPED_UNICODE);\n\n        $this->assertIsString($body);\n\n        $message = JsonDummyMessage::createFromRequest(new ServerRequest('POST', 'http://easywechat.com/server', [], $body));\n\n        $message = $this->decryptMessage(\n            message: $message,\n            encryptor: $encryptor,\n            signature: $encrypted['signature'],\n            timestamp: $encrypted['timestamp'],\n            nonce: $encrypted['nonce']\n        );\n\n        $this->assertSame([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'Encrypt' => $encrypted['ciphertext'],\n            'AgentID' => '218',\n            'FromUserName' => 'mycreate',\n            'CreateTime' => '1409659813',\n            'MsgType' => 'text',\n            'Content' => 'hello',\n            'MsgId' => '4561255354251345929',\n        ], $message->toArray());\n    }\n}\n\nclass JsonDummyMessage extends Message\n{\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/DecryptXmlMessageTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Message;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Tests\\TestCase;\nuse Nyholm\\Psr7\\ServerRequest;\n\nclass DecryptXmlMessageTest extends TestCase\n{\n    use DecryptMessage;\n\n    public function test_it_can_decrypt_message()\n    {\n        $body = '<xml>\n                <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>\n                <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>\n                <AgentID><![CDATA[218]]></AgentID>\n                </xml>';\n        $message = DummyMessage::createFromRequest(new ServerRequest('POST', 'http://easywechat.com/server', [], $body));\n\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $message = $this->decryptMessage($message, $encryptor, '477715d11cdb4164915debcba66cb864d751f3e6', '1409659813', '1372623149');\n\n        $this->assertSame([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'Encrypt' => 'RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==',\n            'AgentID' => '218',\n            'FromUserName' => 'mycreate',\n            'CreateTime' => '1409659813',\n            'MsgType' => 'text',\n            'Content' => 'hello',\n            'MsgId' => '4561255354251345929',\n        ], $message->toArray());\n    }\n}\n\nclass DummyMessage extends Message\n{\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithCacheTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Traits\\InteractWithCache;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Psr16Cache;\n\nclass InteractWithCacheTest extends TestCase\n{\n    public function test_get_and_set_cache()\n    {\n        $app = new DummyClassForInteractWithCacheTest;\n\n        $this->assertInstanceOf(CacheInterface::class, $app->getCache());\n        $this->assertSame($app->getCache(), $app->getCache());\n\n        // set\n        $cache = \\Mockery::mock(Psr16Cache::class);\n        $app->setCache($cache);\n        $this->assertSame($cache, $app->getCache());\n    }\n}\n\nclass DummyClassForInteractWithCacheTest\n{\n    use InteractWithCache;\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithClientTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithClient;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass InteractWithClientTest extends TestCase\n{\n    public function test_get_and_set_client()\n    {\n        $app = new DummyClassForInteractWithClientTest;\n\n        $this->assertInstanceOf(AccessTokenAwareClient::class, $app->getClient());\n        $this->assertSame($app->getClient(), $app->getClient());\n\n        // set\n        $client = new AccessTokenAwareClient;\n        $app->setClient($client);\n        $this->assertSame($client, $app->getClient());\n    }\n}\n\nclass DummyClassForInteractWithClientTest\n{\n    use InteractWithClient;\n\n    public function createClient(): AccessTokenAwareClient\n    {\n        return new AccessTokenAwareClient;\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithConfigTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Traits\\InteractWithConfig;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass InteractWithConfigTest extends TestCase\n{\n    public function test_get_and_set_config()\n    {\n        $app = new DummyClassForInteractWithConfigTest([]);\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n        $this->assertSame($app->getConfig(), $app->getConfig());\n\n        // set\n        $config = \\Mockery::mock(ConfigInterface::class);\n        $app->setConfig($config);\n        $this->assertSame($config, $app->getConfig());\n    }\n}\n\nclass DummyClassForInteractWithConfigTest\n{\n    use InteractWithConfig;\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithHandlersTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHandlers;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass InteractWithHandlersTest extends TestCase\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_has_callable_handlers()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $c = new class\n        {\n            public function hello()\n            {\n                return 'hello';\n            }\n        };\n        $m->with([$c, 'hello']);\n        $this->assertCount(1, $m->getHandlers());\n        $this->assertSame('hello', $m->handle('result'));\n\n        // remove\n        $m->withoutHandler([$c, 'hello']);\n\n        $ci = new class\n        {\n            public function __invoke()\n            {\n                return 'hello invoke';\n            }\n        };\n        $m->with($ci);\n        $this->assertCount(1, $m->getHandlers());\n        $this->assertSame('hello invoke', $m->handle('result'));\n\n        // remove\n        $m->withoutHandler($ci);\n\n        $this->assertCount(0, $m->getHandlers());\n\n        $this->assertSame('result', $m->handle('result'));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_has_closure_handlers()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h = fn () => 'hello';\n        $m->with($h);\n        $this->assertCount(1, $m->getHandlers());\n        $this->assertSame('hello', $m->handle('result'));\n\n        // remove\n        $m->withoutHandler($h);\n        $this->assertCount(0, $m->getHandlers());\n        $this->assertSame('result', $m->handle('result'));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_has_class_based_handlers()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $m->with(DummyClassBasedHandler::class);\n        $this->assertCount(1, $m->getHandlers());\n        $this->assertSame('hello', $m->handle('result'));\n\n        // remove\n        $m->withoutHandler(DummyClassBasedHandler::class);\n\n        $this->assertCount(0, $m->getHandlers());\n        $this->assertSame('result', $m->handle('result'));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_will_run_by_sort()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h1 = function ($payload, $next) {\n            return 'h1'.$next($payload);\n        };\n\n        $h2 = function ($payload, $next) {\n            return 'h2'.$next($payload);\n        };\n\n        $h3 = function ($payload, $next) {\n            return 'h3'.$next($payload);\n        };\n\n        $h4 = function ($payload, $next) {\n            return 'h4';\n        };\n\n        $m->with($h1);\n        $m->with($h2);\n        $m->with($h3);\n        $m->with($h4);\n        $this->assertSame('h1h2h3h4', $m->handle('success'));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_can_push_with_conditions()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h1 = fn () => 'handler1';\n        $h2 = fn () => 'handler2';\n        $h3 = fn () => 'handler3';\n        $h4 = fn () => 'handler4';\n\n        $m->when(fn () => false, $h1);\n        $m->when(fn () => true, $h2);\n        $m->when('bool-value-true', $h3);\n        $m->when(fn () => 0, $h4);\n\n        $this->assertCount(2, $m->getHandlers());\n        $this->assertFalse($m->has($h1));\n        $this->assertTrue($m->has($h2));\n        $this->assertTrue($m->has($h3));\n        $this->assertFalse($m->has($h4));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_can_handle_with_chain_handles()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h1 = function ($payload, $next) {\n            return $next($payload);\n        };\n\n        $h2 = function ($payload, $next) {\n            return $next($payload);\n        };\n\n        $h3 = function ($payload, $next) {\n            return 'final result';\n        };\n\n        $h4 = function ($payload, $next) {\n            return $next($payload);\n        };\n\n        // h4 will not run\n        $m->with($h1);\n        $m->with($h2);\n        $m->with($h3);\n        $m->with($h4);\n        $this->assertSame('final result', $m->handle('SUCCESS'));\n\n        $m->without($h3);\n        $this->assertSame('SUCCESS', $m->handle('SUCCESS'));\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     */\n    public function test_it_can_handle_with_default_value()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h1 = function ($payload, $next) {\n            return $next($payload);\n        };\n\n        $h2 = function ($payload, $next) {\n            return $next($payload);\n        };\n\n        $h3 = function ($payload, $next) {\n            return null;\n        };\n\n        $h4 = function ($payload, $next) {\n            return 'hello';\n        };\n\n        $m->with($h1);\n        $m->with($h2);\n        $m->with($h3);\n        $m->with($h4);\n\n        // null\n        $this->assertSame('default value', $m->handle('default value'));\n        // closure\n        $h5 = fn () => 'h5';\n        $this->assertSame('h5', $m->handle($h5));\n\n        // return $h4\n        $m->without($h3);\n        $this->assertSame('hello', $m->handle('default value'));\n    }\n\n    public function test_it_can_prepend_handlers()\n    {\n        $m = \\Mockery::mock(InteractWithHandlers::class);\n\n        $h1 = function ($payload, $next) {\n            return 'h1'.$next($payload);\n        };\n\n        $h2 = function ($payload, $next) {\n            return 'h2'.$next($payload);\n        };\n\n        $h3 = function ($payload, $next) {\n            return 'h3'.$next($payload);\n        };\n\n        $h4 = function ($payload, $next) {\n            return 'h4';\n        };\n\n        $m->with($h1);\n        $m->with($h4);\n        $this->assertSame('h1h4', $m->handle('success'));\n\n        $m->prepend($h2);\n        $this->assertSame('h2h1h4', $m->handle('success'));\n\n        $m->prepend($h3);\n        $this->assertSame('h3h2h1h4', $m->handle('success'));\n    }\n}\n\nclass DummyClassBasedHandler\n{\n    public function __invoke($payload, \\Closure $next)\n    {\n        return 'hello';\n    }\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithHttpClientTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Traits\\InteractWithHttpClient;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\CurlHttpClient;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass InteractWithHttpClientTest extends TestCase\n{\n    public function test_get_and_set_http_client()\n    {\n        $app = new DummyClassForInteractWithHttpClientTest;\n\n        $this->assertInstanceOf(HttpClientInterface::class, $app->getHttpClient());\n        $this->assertSame($app->getHttpClient(), $app->getHttpClient());\n\n        // set\n        $client = new CurlHttpClient;\n        $app->setHttpClient($client);\n        $this->assertSame($client, $app->getHttpClient());\n    }\n}\n\nclass DummyClassForInteractWithHttpClientTest\n{\n    use InteractWithHttpClient;\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/InteractWithServerRequestTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Traits\\InteractWithServerRequest;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nclass InteractWithServerRequestTest extends TestCase\n{\n    public function test_get_and_set_request()\n    {\n        $app = new DummyClassForInteractWithServerRequestTest;\n\n        $this->assertInstanceOf(ServerRequestInterface::class, $app->getRequest());\n        $this->assertSame($app->getRequest(), $app->getRequest());\n\n        // set\n        $request = \\Mockery::mock(ServerRequestInterface::class);\n        $app->setRequest($request);\n        $this->assertSame($request, $app->getRequest());\n    }\n\n    public function test_it_can_set_request_from_symfony_request()\n    {\n        $app = new DummyClassForInteractWithServerRequestTest;\n\n        $request = \\Symfony\\Component\\HttpFoundation\\Request::create('/foo', 'GET');\n\n        $app->setRequestFromSymfonyRequest($request);\n\n        $this->assertInstanceOf(ServerRequestInterface::class, $app->getRequest());\n        $this->assertSame($request->getUri(), \\strval($app->getRequest()->getUri()));\n        $this->assertSame($request->getMethod(), $app->getRequest()->getMethod());\n    }\n}\n\nclass DummyClassForInteractWithServerRequestTest\n{\n    use InteractWithServerRequest;\n}\n"
  },
  {
    "path": "tests/Kernel/Traits/RespondXmlMessageTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Kernel\\Traits;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Kernel\\Message;\nuse EasyWeChat\\Kernel\\Traits\\RespondXmlMessage;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass RespondXmlMessageTest extends TestCase\n{\n    use RespondXmlMessage;\n\n    public function test_it_will_return_success_response()\n    {\n        $response = $this->transformToReply('', \\Mockery::mock(Message::class));\n\n        $this->assertSame(200, $response->getStatusCode());\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_handle_array_response()\n    {\n        $response = $this->transformToReply([\n            'MsgType' => 'text',\n            'Content' => 'Hello',\n        ], \\Mockery::mock(Message::class));\n\n        $this->assertSame(200, $response->getStatusCode());\n        $this->assertMatchesRegularExpression(\n            '~<xml>'\n                    .'<CreateTime>\\d{10}</CreateTime>'\n                    .'<MsgType>text</MsgType>'\n                    .'<Content>Hello</Content>'\n                    .'</xml>~',\n            \\strval($response->getBody())\n        );\n    }\n\n    public function test_it_will_handle_string_response()\n    {\n        $response = $this->transformToReply('Hello', \\Mockery::mock(Message::class));\n\n        $this->assertSame(200, $response->getStatusCode());\n        $this->assertSame('application/xml', $response->getHeaderLine('content-type'));\n        $this->assertMatchesRegularExpression(\n            '~<xml>'\n            .'<CreateTime>\\d{10}</CreateTime>'\n            .'<MsgType>text</MsgType>'\n            .'<Content>Hello</Content>'\n            .'</xml>~',\n            \\strval($response->getBody())\n        );\n    }\n\n    public function test_it_will_throw_when_response_type_error()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Invalid Response type \"object\".');\n        $this->transformToReply(new \\stdClass, \\Mockery::mock(Message::class));\n\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage('Invalid Response type \"boolean\".');\n        $this->transformToReply(false, \\Mockery::mock(Message::class));\n    }\n}\n"
  },
  {
    "path": "tests/MiniApp/AccessTokenTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\MiniApp;\n\nuse EasyWeChat\\MiniApp\\AccessToken;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AccessTokenTest extends TestCase\n{\n    public function test_it_will_use_mini_app_cache_prefix()\n    {\n        $accessToken = new AccessToken('mock-app-id', 'mock-secret');\n\n        $this->assertStringStartsWith('mini_app.access_token', $accessToken->getKey());\n    }\n}\n"
  },
  {
    "path": "tests/MiniApp/ApplicationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\MiniApp;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\MiniApp\\AccessToken;\nuse EasyWeChat\\MiniApp\\Account;\nuse EasyWeChat\\MiniApp\\Account as AccountInterface;\nuse EasyWeChat\\MiniApp\\Application;\nuse EasyWeChat\\MiniApp\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\MiniApp\\Server;\nuse EasyWeChat\\MiniApp\\Utils;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nclass ApplicationTest extends TestCase\n{\n    public function test_get_and_set_account()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n            ]\n        );\n\n        $this->assertInstanceOf(ApplicationInterface::class, $app);\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($app->getAccount(), $app->getAccount());\n\n        // set\n        $account = new Account(appId: 'wx3cf0f39249000060', secret: 'mock-secret', token: 'mock-token');\n        $app->setAccount($account);\n        $this->assertSame($account, $app->getAccount());\n    }\n\n    public function test_get_and_set_encryptor()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(Encryptor::class, $app->getEncryptor());\n        $this->assertSame($app->getEncryptor(), $app->getEncryptor());\n\n        // set\n        $encryptor = new Encryptor(appId: 'wx3cf0f39249000060', token: 'mock-token', aesKey: 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG');\n        $app->setEncryptor($encryptor);\n        $this->assertSame($encryptor, $app->getEncryptor());\n    }\n\n    public function test_get_and_set_server()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ServerInterface::class, $app->getServer());\n        $this->assertSame($app->getServer(), $app->getServer());\n\n        // set\n        $server = new Server(\\Mockery::mock(ServerRequestInterface::class));\n        $app->setServer($server);\n        $this->assertSame($server, $app->getServer());\n    }\n\n    public function test_get_and_set_access_token()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(AccessTokenInterface::class, $app->getAccessToken());\n\n        // set\n        $accessToken = new AccessToken('wx3cf0f39249000060', 'mock-secret');\n        $app->setAccessToken($accessToken);\n        $this->assertSame($accessToken, $app->getAccessToken());\n    }\n\n    public function test_get_utils()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(Utils::class, $app->getUtils());\n    }\n}\n"
  },
  {
    "path": "tests/MiniApp/DecryptorTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\MiniApp;\n\nuse EasyWeChat\\Kernel\\Exceptions\\DecryptException;\nuse EasyWeChat\\MiniApp\\Decryptor;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass DecryptorTest extends TestCase\n{\n    public function test_it_can_decrypt_message()\n    {\n        $encryptedData = 'CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142d\n                NCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn\n                /Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns\n                /8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==';\n\n        $decrypted = Decryptor::decrypt('tiihtNczf5v6AKRyjwEUhQ==', 'r7BXXKkLb8qrSNn05n0qiA==', $encryptedData);\n\n        $this->assertSame([\n            'openId' => 'oGZUI0egBJY1zhBYw2KhdUfwVJJE',\n            'nickName' => 'Band',\n            'gender' => 1,\n            'language' => 'zh_CN',\n            'city' => 'Guangzhou',\n            'province' => 'Guangdong',\n            'country' => 'CN',\n            'avatarUrl' => 'http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0',\n            'unionId' => 'ocMvos6NjeKLIBqg5Mr9QjxrP1FA',\n            'watermark' => [\n                'timestamp' => 1477314187,\n                'appid' => 'wx4f4bc4dec97d474b',\n            ],\n        ], $decrypted);\n    }\n\n    public function test_it_will_throw_exception_when_payload_is_invalid()\n    {\n        $encryptedData = 'aaaaCiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142d\n                NCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn\n                /Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns\n                /8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==';\n\n        try {\n            Decryptor::decrypt('tiihtNczf5v6AKRyjwEUhQ==', 'r7BXXKkLb8qrSNn05n0qiA==', $encryptedData);\n        } catch (\\Throwable $e) {\n            $this->assertInstanceOf(DecryptException::class, $e);\n            $this->assertStringStartsWith('The given payload is invalid:', $e->getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/MiniApp/UtilsTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\MiniApp;\n\nuse EasyWeChat\\MiniApp\\Application;\nuse EasyWeChat\\MiniApp\\Utils;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\n\nclass UtilsTest extends TestCase\n{\n    public function test_code_to_session()\n    {\n        $response = [\n            'openid' => 'o6_bmjrPTlm6_2sgVt7hMZOPxxxx',\n            'session_key' => 'tiihtNczf5v6AKRyjwExxxx=',\n            'unionid' => 'o6_bmasdasdsad6_2sgVt7hMZOxxxx',\n            'errcode' => 0,\n            'errmsg' => 'ok',\n        ];\n\n        $httpClient = new MockHttpClient(new MockResponse(json_encode($response)));\n\n        $app = new Application([\n            'app_id' => 'mock-appid',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ]);\n        $app->setHttpClient($httpClient);\n\n        $utils = new Utils($app);\n\n        $result = $utils->codeToSession('mock-js-code');\n\n        $this->assertSame($response, $result);\n    }\n\n    public function test_decrypt_session()\n    {\n        $sessionKey = 'tiihtNczf5v6AKRyjwEUhQ==';\n\n        $encryptedData = 'CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM\n                QmRzooG2xrDcvSnxIMXFufNstNGTyaGS\n                9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+\n                3hVbJSRgv+4lGOETKUQz6OYStslQ142d\n                NCuabNPGBzlooOmB231qMM85d2/fV6Ch\n                evvXvQP8Hkue1poOFtnEtpyxVLW1zAo6\n                /1Xx1COxFvrc2d7UL/lmHInNlxuacJXw\n                u0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn\n                /Hz7saL8xz+W//FRAUid1OksQaQx4CMs\n                8LOddcQhULW4ucetDf96JcR3g0gfRK4P\n                C7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB\n                6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns\n                /8wR2SiRS7MNACwTyrGvt9ts8p12PKFd\n                lqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYV\n                oKlaRv85IfVunYzO0IKXsyl7JCUjCpoG\n                20f0a04COwfneQAGGwd5oa+T8yO5hzuy\n                Db/XcxxmK01EpqOyuxINew==';\n\n        $iv = 'r7BXXKkLb8qrSNn05n0qiA==';\n\n        $app = new Application([\n            'app_id' => 'mock-appid',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ]);\n\n        $utils = new Utils($app);\n\n        $this->assertSame([\n            'openId' => 'oGZUI0egBJY1zhBYw2KhdUfwVJJE',\n            'nickName' => 'Band',\n            'gender' => 1,\n            'language' => 'zh_CN',\n            'city' => 'Guangzhou',\n            'province' => 'Guangdong',\n            'country' => 'CN',\n            'avatarUrl' => 'http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0',\n            'unionId' => 'ocMvos6NjeKLIBqg5Mr9QjxrP1FA',\n            'watermark' => [\n                'timestamp' => 1477314187,\n                'appid' => 'wx4f4bc4dec97d474b',\n            ],\n        ], $utils->decryptSession($sessionKey, $iv, $encryptedData));\n    }\n\n    public function test_get_phone_number()\n    {\n        $response = [\n            'errcode' => 0,\n            'errmsg' => 'ok',\n            'phone_info' => [\n                'phoneNumber' => '13800138000',\n                'purePhoneNumber' => '13800138000',\n                'countryCode' => '86',\n                'watermark' => [\n                    'timestamp' => 1637744274,\n                    'appid' => 'xxxx',\n                ],\n            ],\n        ];\n\n        $httpClient = new MockHttpClient([\n            new MockResponse(json_encode($response)),\n        ]);\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->allows()->get(\\Mockery::any())->andReturn('mock-access-token');\n\n        $app = new Application([\n            'app_id' => 'mock-appid',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ]);\n        $app->setHttpClient($httpClient);\n        $app->setCache($cache);\n\n        $utils = new Utils($app);\n\n        $result = $utils->getPhoneNumber('mock-phone-code');\n\n        $this->assertSame($response, $result);\n    }\n\n    public function test_get_phone_number_with_error()\n    {\n        $this->expectException(\\EasyWeChat\\Kernel\\Exceptions\\HttpException::class);\n        $this->expectExceptionMessage('getPhoneNumber error:');\n\n        $errorResponse = [\n            'errcode' => 40029,\n            'errmsg' => 'invalid code',\n        ];\n\n        $httpClient = new MockHttpClient([\n            new MockResponse(json_encode($errorResponse)),\n        ]);\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->allows()->get(\\Mockery::any())->andReturn('mock-access-token');\n\n        $app = new Application([\n            'app_id' => 'mock-appid',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ]);\n        $app->setHttpClient($httpClient);\n        $app->setCache($cache);\n\n        $utils = new Utils($app);\n\n        $utils->getPhoneNumber('invalid-code');\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/AccessTokenTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\OfficialAccount\\AccessToken;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass AccessTokenTest extends TestCase\n{\n    public function test_get_token_from_http_request()\n    {\n        $httpClient = \\Mockery::mock(HttpClientInterface::class);\n        $response = \\Mockery::mock(ResponseInterface::class);\n\n        $result = [\n            'access_token' => 'mock_access_token',\n            'expires_in' => '1500',\n        ];\n\n        $response->allows()->toArray(false)->andReturn($result);\n\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $options = [\n            'query' => [\n                'grant_type' => 'client_credential',\n                'appid' => $config['app_id'],\n                'secret' => $config['secret'],\n            ],\n        ];\n\n        $httpClient->allows()->request('GET', 'cgi-bin/token', $options)->andReturn($response);\n\n        $accessToken = new AccessToken($config['app_id'], $config['secret'], null, null, $httpClient);\n\n        $this->assertSame($result['access_token'], $accessToken->getToken());\n    }\n\n    public function test_get_token_from_cache()\n    {\n        $cache = \\Mockery::mock(CacheInterface::class);\n\n        $key = 'mock-cache-key';\n\n        $result = [\n            'access_token' => 'mock_access_token',\n            'expires_in' => '1500',\n        ];\n\n        $cache->expects()->get($key)->andReturn($result['access_token']);\n\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $accessToken = new AccessToken($config['app_id'], $config['secret'], $key, $cache);\n\n        $this->assertSame($result['access_token'], $accessToken->getToken());\n    }\n\n    public function test_set_key()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $key = 'mock-cache-key';\n\n        $accessToken = new AccessToken($config['app_id'], $config['secret'], $key);\n\n        $this->assertSame($key, $accessToken->getKey());\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/AccountTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\OfficialAccount\\Account;\nuse EasyWeChat\\OfficialAccount\\Application;\nuse EasyWeChat\\OfficialAccount\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AccountTest extends TestCase\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_application_can_create_account_instance()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n            ]\n        );\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_set_account_to_application()\n    {\n        $accountConfig = [\n            'app_id' => 'wx3cf0f39249111111',\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $accountConfig['app_id'],\n            secret: $accountConfig['secret'],\n            token: $accountConfig['token'],\n            aesKey: $accountConfig['aes_key']\n        );\n\n        $applicationConfig = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n        ];\n\n        $app = new Application(\n            [\n                'app_id' => $applicationConfig['app_id'],\n                'secret' => $applicationConfig['secret'],\n                'token' => $applicationConfig['token'],\n            ]\n        );\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($applicationConfig['app_id'], $app->getAccount()->getAppId());\n\n        $app->setAccount($account);\n\n        $this->assertNotSame($applicationConfig['app_id'], $app->getAccount()->getAppId());\n        $this->assertSame($accountConfig['app_id'], $app->getAccount()->getAppId());\n    }\n\n    public function test_get_account_app_id()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['app_id'], $account->getAppId());\n    }\n\n    public function test_get_account_secret()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['secret'], $account->getSecret());\n    }\n\n    public function test_get_account_token()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['token'], $account->getToken());\n    }\n\n    public function test_get_account_aes_key()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['aes_key'], $account->getAesKey());\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/ApplicationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\OfficialAccount\\AccessToken;\nuse EasyWeChat\\OfficialAccount\\Account;\nuse EasyWeChat\\OfficialAccount\\Account as AccountInterface;\nuse EasyWeChat\\OfficialAccount\\Application;\nuse EasyWeChat\\OfficialAccount\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\OfficialAccount\\JsApiTicket;\nuse EasyWeChat\\OfficialAccount\\Server;\nuse EasyWeChat\\OfficialAccount\\Utils;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\nclass ApplicationTest extends TestCase\n{\n    public function test_get_and_set_account()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n            ]\n        );\n\n        $this->assertInstanceOf(ApplicationInterface::class, $app);\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($app->getAccount(), $app->getAccount());\n\n        // set\n        $account = new Account(appId: 'wx3cf0f39249000060', secret: 'mock-secret', token: 'mock-token');\n        $app->setAccount($account);\n        $this->assertSame($account, $app->getAccount());\n    }\n\n    public function test_get_and_set_encryptor()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(Encryptor::class, $app->getEncryptor());\n        $this->assertSame($app->getEncryptor(), $app->getEncryptor());\n\n        // set\n        $encryptor = new Encryptor(appId: 'wx3cf0f39249000060', token: 'mock-token', aesKey: 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG');\n        $app->setEncryptor($encryptor);\n        $this->assertSame($encryptor, $app->getEncryptor());\n    }\n\n    public function test_get_and_set_server()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ServerInterface::class, $app->getServer());\n        $this->assertSame($app->getServer(), $app->getServer());\n\n        // set\n        $server = new Server(\\Mockery::mock(ServerRequestInterface::class));\n        $app->setServer($server);\n        $this->assertSame($server, $app->getServer());\n    }\n\n    public function test_get_and_set_access_token()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(AccessTokenInterface::class, $app->getAccessToken());\n\n        // set\n        $accessToken = new AccessToken('wx3cf0f39249000060', 'mock-secret');\n        $app->setAccessToken($accessToken);\n        $this->assertSame($accessToken, $app->getAccessToken());\n    }\n\n    // https://github.com/w7corp/easywechat/issues/2743\n    public function test_get_client_without_http_config()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(AccessTokenAwareClient::class, $app->getClient());\n\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n                'http' => null,\n            ]\n        );\n\n        // no exception\n        $this->assertInstanceOf(AccessTokenAwareClient::class, $app->getClient());\n    }\n\n    public function test_get_and_set_ticket()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(JsApiTicket::class, $app->getTicket());\n\n        // set\n        $ticket = new JsApiTicket('wx3cf0f39249000060', 'mock-secret', 'mock-token', $app->getCache(), $app->getClient());\n        $app->setTicket($ticket);\n        $this->assertSame($ticket, $app->getTicket());\n    }\n\n    public function test_get_utils()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(Utils::class, $app->getUtils());\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/ConfigTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\OfficialAccount\\Application;\nuse EasyWeChat\\OfficialAccount\\Config;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass ConfigTest extends TestCase\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_application_created_can_get_config()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n            ]\n        );\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_set_config_to_application()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249111111',\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes-key',\n        ];\n\n        $config = new Config($config);\n\n        $applicationConfig = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n        ];\n\n        $app = new Application(\n            [\n                'app_id' => $applicationConfig['app_id'],\n                'secret' => $applicationConfig['secret'],\n                'token' => $applicationConfig['token'],\n            ]\n        );\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n        $this->assertSame($applicationConfig['app_id'], $app->getConfig()->get('app_id'));\n        $this->assertSame(false, $app->getConfig()->has('aes_key'));\n\n        $app->setConfig($config);\n\n        $this->assertNotSame($applicationConfig['app_id'], $app->getConfig()->get('app_id'));\n        $this->assertSame($config['app_id'], $app->getConfig()->get('app_id'));\n        $this->assertSame(true, $app->getConfig()->has('aes_key'));\n    }\n\n    public function test_init_config_can_check_missing_keys()\n    {\n        $config = [\n            'secret' => 'mock-account-secret',\n        ];\n\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage(\\sprintf('\"%s\" cannot be empty.', 'app_id'));\n\n        new Config($config);\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/JsApiTicketTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\OfficialAccount\\AccessToken;\nuse EasyWeChat\\OfficialAccount\\JsApiTicket;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass JsApiTicketTest extends TestCase\n{\n    public function test_get_key()\n    {\n        $ticket = new JsApiTicket('mock-appid', 'mock-secret');\n\n        $this->assertInstanceOf(AccessToken::class, $ticket);\n\n        $this->assertSame('official_account.jsapi_ticket.mock-appid', $ticket->getKey());\n\n        $ticket->setKey('mock-key');\n        $this->assertSame('mock-key', $ticket->getKey());\n\n        $ticket = new JsApiTicket('mock-appid', 'mock-secret', 'test-key');\n        $this->assertSame('test-key', $ticket->getKey());\n    }\n\n    public function test_get_ticket()\n    {\n        $cacheKey = 'official_account.jsapi_ticket.mock-appid';\n\n        $ticket = [\n            'ticket' => 'mock-ticket',\n            'expires_in' => 7200,\n        ];\n\n        $response = \\Mockery::mock(ResponseInterface::class);\n        $response->allows()->toArray(false)->andReturn($ticket);\n\n        $client = \\Mockery::mock(HttpClientInterface::class);\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn($ticket['ticket']);\n\n        $jsApiTicket = new JsApiTicket('mock-appid', 'mock-secret', null, $cache, $client);\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getTicket());\n\n        // 设为过期\n        $cache->expects()->get($cacheKey)->andReturn(false);\n        $cache->expects()->set($cacheKey, $ticket['ticket'], $ticket['expires_in'])->andReturn(true);\n\n        $client->allows()->request('GET', '/cgi-bin/ticket/getticket', ['query' => ['type' => 'jsapi']])\n            ->andReturn($response);\n\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getTicket());\n    }\n\n    public function test_config_signature()\n    {\n        $nonce = 'mock-nonce';\n        $timestamp = 1601234567;\n\n        $cacheKey = 'official_account.jsapi_ticket.mock-appid';\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn('mock-ticket');\n\n        $ticket = new JsApiTicket('mock-appid', 'mock-secret', null, $cache);\n\n        $result = $ticket->configSignature('https://www.easywechat.com/', $nonce, $timestamp);\n\n        $data = [\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'appId' => 'mock-appid',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $this->assertSame($data, $result);\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/ServerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\OfficialAccount\\Server;\nuse EasyWeChat\\Tests\\TestCase;\nuse Nyholm\\Psr7\\ServerRequest;\n\nclass ServerTest extends TestCase\n{\n    public function test_it_will_handle_validation_request()\n    {\n        $request = (new ServerRequest('GET', 'http://easywechat.com/?echostr=abcdefghijklmn'))->withQueryParams(['echostr' => 'abcdefghijklmn']);\n        $server = new Server($request);\n\n        $response = $server->serve();\n\n        $this->assertSame('abcdefghijklmn', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_response_success_without_handlers()\n    {\n        $body = '<xml>\n          <ToUserName><![CDATA[toUser]]></ToUserName>\n          <FromUserName><![CDATA[fromUser]]></FromUserName>\n          <CreateTime>1348831860</CreateTime>\n          <MsgType><![CDATA[text]]></MsgType>\n          <Content><![CDATA[this is a test]]></Content>\n          <MsgId>1234567890123456</MsgId>\n        </xml>';\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body));\n        $server = new Server($request);\n\n        $response = $server->serve();\n\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_respond_from_message_handlers()\n    {\n        $body = '<xml>\n          <ToUserName><![CDATA[toUser]]></ToUserName>\n          <FromUserName><![CDATA[fromUser]]></FromUserName>\n          <CreateTime>1348831860</CreateTime>\n          <MsgType><![CDATA[text]]></MsgType>\n          <Content><![CDATA[this is a test]]></Content>\n          <MsgId>1234567890123456</MsgId>\n        </xml>';\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body));\n        $server = new Server($request);\n\n        $response = $server\n            ->addMessageListener(\n                'text',\n                function ($message) {\n                    return 'hello';\n                }\n            )->addEventListener(\n                'subscribe',\n                function ($message) {\n                    return 'world';\n                }\n            )->serve();\n\n        $response = Xml::parse(\\strval($response->getBody()));\n\n        $this->assertSame('toUser', $response['FromUserName']);\n        $this->assertSame('fromUser', $response['ToUserName']);\n        $this->assertSame('text', $response['MsgType']);\n        $this->assertSame('hello', $response['Content']);\n    }\n\n    public function test_it_will_respond_from_event_handlers()\n    {\n        $body = '<xml>\n          <ToUserName><![CDATA[toUser]]></ToUserName>\n          <FromUserName><![CDATA[fromUser]]></FromUserName>\n          <CreateTime>123456789</CreateTime>\n          <MsgType><![CDATA[event]]></MsgType>\n          <Event><![CDATA[subscribe]]></Event>\n          <EventKey><![CDATA[qrscene_123123]]></EventKey>\n          <Ticket><![CDATA[TICKET]]></Ticket>\n        </xml>';\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body));\n        $server = new Server($request);\n\n        $response = $server\n            ->addMessageListener(\n                'text',\n                function ($message) {\n                    return 'hello';\n                }\n            )->addEventListener(\n                'subscribe',\n                function ($message) {\n                    return 'world';\n                }\n            )->serve();\n\n        $response = Xml::parse(\\strval($response->getBody()));\n\n        $this->assertSame('toUser', $response['FromUserName']);\n        $this->assertSame('fromUser', $response['ToUserName']);\n        $this->assertSame('text', $response['MsgType']);\n        $this->assertSame('world', $response['Content']);\n    }\n\n    public function test_it_can_decrypt_json_mode_messages()\n    {\n        $plaintext = json_encode([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'FromUserName' => 'mycreate',\n            'CreateTime' => '1409659813',\n            'MsgType' => 'text',\n            'Content' => 'hello',\n            'MsgId' => '4561255354251345929',\n        ], JSON_UNESCAPED_UNICODE);\n\n        $this->assertIsString($plaintext);\n\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $encrypted = $encryptor->encryptAsArray(\n            plaintext: $plaintext,\n            nonce: '1372623149',\n            timestamp: '1409659813'\n        );\n\n        $body = json_encode([\n            'ToUserName' => 'wx5823bf96d3bd56c7',\n            'Encrypt' => $encrypted['ciphertext'],\n        ], JSON_UNESCAPED_UNICODE);\n\n        $this->assertIsString($body);\n\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body))->withQueryParams([\n            'msg_signature' => $encrypted['signature'],\n            'timestamp' => $encrypted['timestamp'],\n            'nonce' => $encrypted['nonce'],\n        ]);\n\n        $server = new Server($request, $encryptor);\n\n        $message = $server->getDecryptedMessage();\n\n        $this->assertSame('hello', $message->Content);\n        $this->assertSame('mycreate', $message->FromUserName);\n    }\n}\n"
  },
  {
    "path": "tests/OfficialAccount/UtilsTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OfficialAccount;\n\nuse EasyWeChat\\OfficialAccount\\Application;\nuse EasyWeChat\\OfficialAccount\\JsApiTicket;\nuse EasyWeChat\\OfficialAccount\\Utils;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass UtilsTest extends TestCase\n{\n    public function test_build_js_sdk_config()\n    {\n        $data = [\n            'jsApiList' => ['api1', 'api2'],\n            'openTagList' => ['openTag1', 'openTag2'],\n            'debug' => true,\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'appId' => 'mock-appid',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $signatue = [\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'appId' => 'mock-appid',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $ticket = \\Mockery::mock(JsApiTicket::class);\n        $ticket->shouldReceive('configSignature')->andReturn($signatue);\n\n        $app = \\Mockery::mock(Application::class);\n        $app->allows()->getTicket()->andReturn($ticket);\n\n        $utils = new Utils($app);\n\n        $result = $utils->buildJsSdkConfig('https://www.easywechat.com/', ['api1', 'api2'], ['openTag1', 'openTag2'], true);\n\n        $this->assertSame($data, $result);\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/AccountTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\OpenPlatform\\Account;\nuse EasyWeChat\\OpenPlatform\\Application;\nuse EasyWeChat\\OpenPlatform\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AccountTest extends TestCase\n{\n    public function test_application_can_create_account_instance()\n    {\n        $app = new Application(\n            [\n                'app_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'mock-account-aes-key',\n            ]\n        );\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_set_account_to_application()\n    {\n        $accountConfig = [\n            'app_id' => 'wx3cf0f39249111111',\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $accountConfig['app_id'],\n            secret: $accountConfig['secret'],\n            token: $accountConfig['token'],\n            aesKey: $accountConfig['aes_key']\n        );\n\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ];\n\n        $app = new Application($config);\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($config['app_id'], $app->getAccount()->getAppId());\n\n        $app->setAccount($account);\n\n        $this->assertNotSame($config['app_id'], $app->getAccount()->getAppId());\n        $this->assertSame($accountConfig['app_id'], $app->getAccount()->getAppId());\n    }\n\n    public function test_get_account_app_id()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['app_id'], $account->getAppId());\n    }\n\n    public function test_get_account_secret()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['secret'], $account->getSecret());\n    }\n\n    public function test_get_account_token()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['token'], $account->getToken());\n    }\n\n    public function test_get_account_aes_key()\n    {\n        $config = [\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            appId: $config['app_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['aes_key'], $account->getAesKey());\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/ApplicationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Exceptions\\HttpException;\nuse EasyWeChat\\MiniApp\\Application as MiniAppApplication;\nuse EasyWeChat\\OfficialAccount\\Application as OfficialAccountApplication;\nuse EasyWeChat\\OpenPlatform\\Account;\nuse EasyWeChat\\OpenPlatform\\Account as AccountInterface;\nuse EasyWeChat\\OpenPlatform\\Application;\nuse EasyWeChat\\OpenPlatform\\Authorization;\nuse EasyWeChat\\OpenPlatform\\AuthorizerAccessToken;\nuse EasyWeChat\\OpenPlatform\\ComponentAccessToken;\nuse EasyWeChat\\OpenPlatform\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\OpenPlatform\\Contracts\\VerifyTicket as VerifyTicketInterface;\nuse EasyWeChat\\OpenPlatform\\Server;\nuse EasyWeChat\\OpenPlatform\\VerifyTicket;\nuse EasyWeChat\\Tests\\TestCase;\nuse Overtrue\\Socialite\\Providers\\WeChat;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\n\nclass ApplicationTest extends TestCase\n{\n    public function test_get_and_set_account()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ]);\n\n        $this->assertInstanceOf(ApplicationInterface::class, $app);\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($app->getAccount(), $app->getAccount());\n\n        // set\n        $account = new Account(appId: 'wx3cf0f39249000060', secret: 'mock-secret', token: 'mock-token', aesKey: 'mock-aes_key');\n        $app->setAccount($account);\n        $this->assertSame($account, $app->getAccount());\n    }\n\n    public function test_get_and_set_encryptor()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(Encryptor::class, $app->getEncryptor());\n        $this->assertSame($app->getEncryptor(), $app->getEncryptor());\n\n        // set\n        $encryptor = new Encryptor(\n            appId: 'wx3cf0f39249000060',\n            token: 'mock-token',\n            aesKey: 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG'\n        );\n        $app->setEncryptor($encryptor);\n        $this->assertSame($encryptor, $app->getEncryptor());\n    }\n\n    public function test_get_and_set_server()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(ServerInterface::class, $app->getServer());\n        $this->assertSame($app->getServer(), $app->getServer());\n\n        // set\n        $server = new Server(\n            encryptor: \\Mockery::mock(Encryptor::class),\n            request: \\Mockery::mock(ServerRequestInterface::class)\n        );\n        $app->setServer($server);\n        $this->assertSame($server, $app->getServer());\n    }\n\n    public function test_get_and_set_component_access_token()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(AccessTokenInterface::class, $app->getAccessToken());\n        // set\n        $accessToken = new ComponentAccessToken('wx3cf0f39249000060', 'mock-secret', $app->getVerifyTicket());\n        $app->setComponentAccessToken($accessToken);\n        $this->assertSame($accessToken, $app->getAccessToken());\n    }\n\n    public function test_get_and_set_verify_ticket()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(VerifyTicketInterface::class, $app->getVerifyTicket());\n\n        // set\n        $verifyTicket = new VerifyTicket('wx3cf0f39249000060');\n        $app->setVerifyTicket($verifyTicket);\n        $this->assertSame($verifyTicket, $app->getVerifyTicket());\n    }\n\n    public function test_get_authorization()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $verifyTicket = new VerifyTicket('wx3cf0f39249000060');\n        $verifyTicket->setTicket('mock-verify-ticket');\n        $app->setVerifyTicket($verifyTicket);\n        $tokenResponse = [\n            'component_access_token' => 'mock-access-token',\n            'expires_in' => 2700,\n        ];\n        // token http client\n        $mockTokenResponse = new MockResponse(\\json_encode($tokenResponse));\n        $tokenHttpClient = new MockHttpClient($mockTokenResponse, 'https://api.weixin.qq.com/');\n        $token = new ComponentAccessToken('wx3cf0f39249000060', 'mock-secret', $app->getVerifyTicket(), httpClient: $tokenHttpClient);\n        $app->setComponentAccessToken($token);\n        $mockResponse = new MockResponse(\n            \\json_encode([\n                'authorization_info' => ['authorizer_appid' => 'mock-appid'],\n            ]),\n            [\n                'http_code' => 201,\n                'response_headers' => ['Content-Type: application/json'],\n            ]\n        );\n        $httpClient = new MockHttpClient($mockResponse, 'https://api.weixin.qq.com/');\n        $app->setHttpClient($httpClient);\n        $this->assertInstanceOf(Authorization::class, $app->getAuthorization('mock-auth-code'));\n\n        $this->assertSame('POST', $mockResponse->getRequestMethod());\n        $this->assertSame(\n            'https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=mock-access-token',\n            $mockResponse->getRequestUrl()\n        );\n        $this->assertSame(\n            \\json_encode([\n                'component_appid' => 'wx3cf0f39249000060',\n                'authorization_code' => 'mock-auth-code',\n            ]),\n            $mockResponse->getRequestOptions()['body']\n        );\n    }\n\n    public function test_get_authorization_exception()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $verifyTicket = new VerifyTicket('wx3cf0f39249000060');\n        $verifyTicket->setTicket('mock-verify-ticket');\n        $app->setVerifyTicket($verifyTicket);\n        $tokenResponse = [\n            'component_access_token' => 'mock-access-token',\n            'expires_in' => 2700,\n        ];\n        // token http client\n        $mockTokenResponse = new MockResponse(\\json_encode($tokenResponse));\n        $tokenHttpClient = new MockHttpClient($mockTokenResponse, 'https://api.weixin.qq.com/');\n        $token = new ComponentAccessToken('wx3cf0f39249000060', 'mock-secret', $app->getVerifyTicket(), httpClient: $tokenHttpClient);\n        $app->setComponentAccessToken($token);\n\n        // exception\n        $mockResponse = new MockResponse(\n            \\json_encode([\n                'error_code' => 100029,\n            ]),\n            [\n                'http_code' => 201,\n                'response_headers' => ['Content-Type: application/json'],\n            ]\n        );\n        $httpClient = new MockHttpClient($mockResponse, 'https://api.weixin.qq.com/');\n        $app->setHttpClient($httpClient);\n\n        $this->expectException(HttpException::class);\n        $this->expectExceptionMessage('Failed to get authorization_info: {\"error_code\":100029}');\n        $app->getAuthorization('mock-auth-code');\n    }\n\n    public function test_refresh_authorizer_token()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $verifyTicket = new VerifyTicket('wx3cf0f39249000060');\n        $verifyTicket->setTicket('mock-verify-ticket');\n        $app->setVerifyTicket($verifyTicket);\n\n        $tokenResponse = [\n            'component_access_token' => 'mock-access-token',\n            'expires_in' => 2700,\n        ];\n        // token http client\n        $mockTokenResponse = new MockResponse(\\json_encode($tokenResponse));\n        $tokenHttpClient = new MockHttpClient($mockTokenResponse, 'https://api.weixin.qq.com/');\n        $token = new ComponentAccessToken('wx3cf0f39249000060', 'mock-secret', $app->getVerifyTicket(), httpClient: $tokenHttpClient);\n\n        $app->setComponentAccessToken($token);\n        $mockResponse = new MockResponse(\n            \\json_encode([\n                'authorizer_access_token' => 'mock-access-token',\n            ]),\n            [\n                'http_code' => 201,\n                'response_headers' => ['Content-Type: application/json'],\n            ]\n        );\n        $httpClient = new MockHttpClient($mockResponse, 'https://api.weixin.qq.com/');\n        $app->setHttpClient($httpClient);\n        $this->assertSame([\n            'authorizer_access_token' => 'mock-access-token',\n        ], $app->refreshAuthorizerToken('mock-authorizer-appid', 'mock-refresh-token'));\n\n        $this->assertSame('POST', $mockResponse->getRequestMethod());\n        $this->assertSame(\n            'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=mock-access-token',\n            $mockResponse->getRequestUrl()\n        );\n        $this->assertSame(\n            \\json_encode([\n                'component_appid' => 'wx3cf0f39249000060',\n                'authorizer_appid' => 'mock-authorizer-appid',\n                'authorizer_refresh_token' => 'mock-refresh-token',\n            ]),\n            $mockResponse->getRequestOptions()['body']\n        );\n    }\n\n    public function test_refresh_authorizer_token_exception()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $verifyTicket = new VerifyTicket('wx3cf0f39249000060');\n        $verifyTicket->setTicket('mock-verify-ticket');\n        $app->setVerifyTicket($verifyTicket);\n\n        $tokenResponse = [\n            'component_access_token' => 'mock-access-token',\n            'expires_in' => 2700,\n        ];\n        // token http client\n        $mockTokenResponse = new MockResponse(\\json_encode($tokenResponse));\n        $tokenHttpClient = new MockHttpClient($mockTokenResponse, 'https://api.weixin.qq.com/');\n\n        $token = new ComponentAccessToken('wx3cf0f39249000060', 'mock-secret', $app->getVerifyTicket(), httpClient: $tokenHttpClient);\n\n        $app->setComponentAccessToken($token);\n\n        // exception\n        $mockResponse = new MockResponse(\n            \\json_encode([\n                'error_code' => 100029,\n            ]),\n            [\n                'http_code' => 201,\n                'response_headers' => ['Content-Type: application/json'],\n            ]\n        );\n        $httpClient = new MockHttpClient($mockResponse, 'https://api.weixin.qq.com/');\n        $app->setHttpClient($httpClient);\n\n        $this->expectException(HttpException::class);\n        $this->expectExceptionMessage('Failed to get authorizer_access_token: {\"error_code\":100029}');\n        $app->refreshAuthorizerToken('mock-authorizer-appid', 'mock-refresh-token');\n    }\n\n    public function test_get_oauth()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(WeChat::class, $app->getOAuth());\n    }\n\n    public function test_get_official_account()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(\n            OfficialAccountApplication::class,\n            $app->getOfficialAccount(new AuthorizerAccessToken('mock-app-id', 'mock-access-token'), [\n                'secret' => 'mock-secret',\n            ])\n        );\n    }\n\n    public function test_get_mini_app()\n    {\n        $app = new Application([\n            'app_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n        ]);\n\n        $this->assertInstanceOf(\n            MiniAppApplication::class,\n            $app->getMiniApp(new AuthorizerAccessToken('mock-app-id', 'mock-access-token'), [\n                'secret' => 'mock-secret',\n            ])\n        );\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/AuthorizationTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\OpenPlatform\\Authorization;\nuse EasyWeChat\\OpenPlatform\\AuthorizerAccessToken;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AuthorizationTest extends TestCase\n{\n    public function test_get_app_id()\n    {\n        $authorization = new Authorization([\n            'authorization_info' => [\n                'authorizer_appid' => 'mock-app-id',\n            ],\n        ]);\n\n        $this->assertSame('mock-app-id', $authorization->getAppId());\n    }\n\n    public function test_get_access_token()\n    {\n        $authorization = new Authorization([\n            'authorization_info' => [\n                'authorizer_appid' => 'mock-app-id',\n                'authorizer_access_token' => 'mock-access-token',\n            ],\n        ]);\n\n        $this->assertInstanceOf(AuthorizerAccessToken::class, $authorization->getAccessToken());\n        $this->assertSame('mock-app-id', $authorization->getAccessToken()->getAppId());\n        $this->assertSame('mock-access-token', $authorization->getAccessToken()->getToken());\n    }\n\n    public function test_get_refresh_token()\n    {\n        $authorization = new Authorization([\n            'authorization_info' => [\n                'authorizer_appid' => 'mock-app-id',\n                'authorizer_refresh_token' => 'mock-refresh-token',\n            ],\n        ]);\n\n        $this->assertSame('mock-refresh-token', $authorization->getRefreshToken());\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/AuthorizerAccessTokenTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\OpenPlatform\\AuthorizerAccessToken;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass AuthorizerAccessTokenTest extends TestCase\n{\n    public function test_get_app_id_and_token()\n    {\n        $token = new AuthorizerAccessToken('mock-app-id', 'mock-access-token');\n\n        $this->assertSame('mock-app-id', $token->getAppId());\n        $this->assertSame('mock-access-token', $token->getToken());\n        $this->assertSame('mock-access-token', \\strval($token));\n        $this->assertSame([\n            'access_token' => 'mock-access-token',\n        ], $token->toQuery());\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/ComponentAccessTokenTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\OpenPlatform\\ComponentAccessToken;\nuse EasyWeChat\\OpenPlatform\\Contracts\\VerifyTicket;\nuse EasyWeChat\\Tests\\TestCase;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\n\nclass ComponentAccessTokenTest extends TestCase\n{\n    public function test_set_and_get_cache_key()\n    {\n        $token = new ComponentAccessToken('mock-app-id', 'mock-secret', \\Mockery::mock(VerifyTicket::class));\n\n        $this->assertSame('open_platform.component_access_token.mock-app-id', $token->getKey());\n\n        $token->setKey('custom-cache-key-for-app-id');\n        $this->assertSame('custom-cache-key-for-app-id', $token->getKey());\n    }\n\n    public function test_get_token_from_cache()\n    {\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get('open_platform.component_access_token.mock-app-id')->andReturns('mock-cached-access-token')->twice();\n        $token = new ComponentAccessToken('mock-app-id', 'mock-secret', \\Mockery::mock(VerifyTicket::class), cache: $cache);\n\n        $this->assertSame('mock-cached-access-token', $token->getToken());\n\n        // to query\n        $this->assertSame([\n            'component_access_token' => 'mock-cached-access-token',\n        ], $token->toQuery());\n    }\n\n    public function test_get_token_from_server()\n    {\n        $verifyTicket = \\Mockery::mock(VerifyTicket::class);\n        $verifyTicket->expects()->getTicket()->andReturns('mock-verify-ticket');\n        $response = [\n            'component_access_token' => 'mock-access-token',\n            'expires_in' => 2700,\n        ];\n\n        // cache client\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get('open_platform.component_access_token.mock-app-id')->andReturns(null)->once();\n        $cache->expects()->set('open_platform.component_access_token.mock-app-id', 'mock-access-token', 2700 - 100)->once();\n\n        // http client\n        $mockResponse = new MockResponse(\\json_encode($response));\n        $httpClient = new MockHttpClient($mockResponse, 'https://api.weixin.qq.com/');\n\n        $token = new ComponentAccessToken('mock-app-id', 'mock-secret', $verifyTicket, httpClient: $httpClient, cache: $cache);\n\n        $this->assertSame('mock-access-token', $token->getToken());\n\n        $this->assertSame('POST', $mockResponse->getRequestMethod());\n        $this->assertSame('https://api.weixin.qq.com/cgi-bin/component/api_component_token', $mockResponse->getRequestUrl());\n        $this->assertSame(\\json_encode([\n            'component_appid' => 'mock-app-id',\n            'component_appsecret' => 'mock-secret',\n            'component_verify_ticket' => 'mock-verify-ticket',\n        ]), $mockResponse->getRequestOptions()['body']);\n    }\n}\n"
  },
  {
    "path": "tests/OpenPlatform/ServerTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\OpenPlatform;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\OpenPlatform\\Server;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass ServerTest extends TestCase\n{\n    public function test_it_will_handle_authorized_event()\n    {\n        $body = '<xml>\n              <AppId>第三方平台appid</AppId>\n              <CreateTime>1413192760</CreateTime>\n              <InfoType>authorized</InfoType>\n              <AuthorizerAppid>公众号appid</AuthorizerAppid>\n              <AuthorizationCode>授权码</AuthorizationCode>\n              <AuthorizationCodeExpiredTime>过期时间</AuthorizationCodeExpiredTime>\n              <PreAuthCode>预授权码</PreAuthCode>\n            </xml>\n        ';\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $request = $this->createEncryptedXmlMessageRequest($body, $encryptor);\n\n        $server = new Server(encryptor: $encryptor, request: $request);\n\n        $handleResult = null;\n        $response = $server->handleAuthorized(function ($message) use (&$handleResult) {\n            $handleResult = 'authorized-event-handled';\n        })->serve();\n\n        $this->assertSame('authorized-event-handled', $handleResult);\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_handle_unauthorized_event()\n    {\n        $body = '<xml>\n              <AppId>第三方平台appid</AppId>\n              <CreateTime>1413192760</CreateTime>\n              <InfoType>unauthorized</InfoType>\n              <AuthorizerAppid>公众号appid</AuthorizerAppid>\n            </xml>\n        ';\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $request = $this->createEncryptedXmlMessageRequest($body, $encryptor);\n\n        $server = new Server(encryptor: $encryptor, request: $request);\n\n        $handleResult = null;\n        $response = $server->handleUnauthorized(function ($message) use (&$handleResult) {\n            $handleResult = 'unauthorized-event-handled';\n        })->serve();\n\n        $this->assertSame('unauthorized-event-handled', $handleResult);\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_handle_authorize_updated_event()\n    {\n        $body = '<xml>\n              <AppId>第三方平台appid</AppId>\n              <CreateTime>1413192760</CreateTime>\n              <InfoType>updateauthorized</InfoType>\n              <AuthorizerAppid>公众号appid</AuthorizerAppid>\n              <AuthorizationCode>授权码</AuthorizationCode>\n              <AuthorizationCodeExpiredTime>过期时间</AuthorizationCodeExpiredTime>\n              <PreAuthCode>预授权码</PreAuthCode>\n            </xml>\n        ';\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $request = $this->createEncryptedXmlMessageRequest($body, $encryptor);\n\n        $server = new Server(encryptor: $encryptor, request: $request);\n\n        $handleResult = null;\n        $response = $server->handleAuthorizeUpdated(function ($message) use (&$handleResult) {\n            $handleResult = 'authorized-updated-event-handled';\n        })->serve();\n\n        $this->assertSame('authorized-updated-event-handled', $handleResult);\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_handle_verify_ticket_refresh_event()\n    {\n        $body = '<xml>\n            <AppId>some_appid</AppId>\n            <CreateTime>1413192605</CreateTime>\n            <InfoType>component_verify_ticket</InfoType>\n            <ComponentVerifyTicket>some_verify_ticket</ComponentVerifyTicket>\n            </xml>\n        ';\n        $encryptor = new Encryptor('wx5823bf96d3bd56c7', 'QDG6eK', 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C');\n        $request = $this->createEncryptedXmlMessageRequest($body, $encryptor);\n\n        $server = new Server(\n            encryptor: $encryptor,\n            request: $request\n        );\n\n        $handleResult = null;\n        $response = $server->handleVerifyTicketRefreshed(function ($message) use (&$handleResult) {\n            $handleResult = 'verify-ticket-refreshed-event-handled';\n        })->serve();\n\n        $this->assertSame('verify-ticket-refreshed-event-handled', $handleResult);\n        $this->assertSame('success', \\strval($response->getBody()));\n    }\n}\n"
  },
  {
    "path": "tests/Pay/ApplicationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Pay;\n\nuse EasyWeChat\\Pay\\Application;\nuse EasyWeChat\\Pay\\Client;\nuse EasyWeChat\\Pay\\Contracts\\Merchant;\nuse EasyWeChat\\Pay\\Contracts\\Validator;\nuse EasyWeChat\\Pay\\Server;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass ApplicationTest extends TestCase\n{\n    public function test_get_merchant()\n    {\n        $app = new Application(\n            [\n                'mch_id' => 101111111,\n                'secret_key' => 'mock-secret-key',\n                'private_key' => 'mock-private-key',\n                'certificate' => '/path/to/certificate.cert',\n                'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO',\n            ]\n        );\n\n        $this->assertInstanceOf(Merchant::class, $app->getMerchant());\n        $this->assertSame($app->getMerchant(), $app->getMerchant());\n    }\n\n    public function test_get_client()\n    {\n        $app = new Application(\n            [\n                'mch_id' => 101111111,\n                'secret_key' => 'mock-secret-key',\n                'private_key' => 'mock-private-key',\n                'certificate' => '/path/to/certificate.cert',\n                'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO',\n            ]\n        );\n\n        $this->assertInstanceOf(Client::class, $app->getClient());\n        $this->assertSame($app->getHttpClient(), $app->getHttpClient());\n    }\n\n    public function test_get_server()\n    {\n        $app = new Application(\n            [\n                'mch_id' => 101111111,\n                'secret_key' => 'mock-secret-key',\n                'private_key' => 'mock-private-key',\n                'certificate' => '/path/to/certificate.cert',\n                'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO',\n            ]\n        );\n\n        $this->assertInstanceOf(Server::class, $app->getServer());\n        $this->assertSame($app->getServer(), $app->getServer());\n    }\n\n    public function test_get_and_set_validator()\n    {\n        $app = new Application(\n            [\n                'mch_id' => 101111111,\n                'secret_key' => 'mock-secret-key',\n                'private_key' => 'mock-private-key',\n                'certificate' => '/path/to/certificate.cert',\n                'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO',\n            ]\n        );\n\n        $this->assertInstanceOf(Validator::class, $app->getValidator());\n        $this->assertSame($app->getValidator(), $app->getValidator());\n\n        $validator = \\Mockery::mock(Validator::class);\n\n        $app->setValidator($validator);\n\n        $this->assertSame($validator, $app->getValidator());\n    }\n}\n"
  },
  {
    "path": "tests/Pay/ClientTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Pay;\n\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Pay\\Client;\nuse EasyWeChat\\Tests\\TestCase;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\n\nclass ClientTest extends TestCase\n{\n    public function test_v3_request()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->andReturn('mock-signature');\n\n        $options = [\n            'headers' => [\n                'accept' => 'application/json',\n            ],\n        ];\n\n        $client->request('GET', 'https://api2.mch.weixin.qq.com/v3/certificates', $options);\n        $this->assertSame('GET', $client->getRequestMethod());\n        $this->assertSame('https://api2.mch.weixin.qq.com/v3/certificates', $client->getRequestUrl());\n        $this->assertSame('Content-Type: application/json', $client->getRequestOptions()['headers'][3]);\n        $this->assertSame('accept: application/json', $client->getRequestOptions()['headers'][0]);\n    }\n\n    public function test_v2_request_with_array()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $client->post('certificates', [\n            'body' => [\n                'foo' => 'bar',\n            ],\n        ]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame(Xml::build(['foo' => 'bar', 'sign' => 'mock-signature']), $client->getRequestOptions()['body']);\n    }\n\n    public function test_v2_request_without_body()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $client->post('certificates', ['foo' => 'bar']);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame(Xml::build(['foo' => 'bar', 'sign' => 'mock-signature']), $client->getRequestOptions()['body']);\n    }\n\n    public function test_v2_request_with_xml_option()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $client->post('certificates', ['xml' => ['foo' => 'bar']]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame(Xml::build(['foo' => 'bar', 'sign' => 'mock-signature']), $client->getRequestOptions()['body']);\n    }\n\n    public function test_v2_request_with_xml_string()\n    {\n        // XML array will attach signature\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature'])->once();\n\n        $client->post('certificates', ['xml' => ['foo' => 'bar']]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame('<xml><foo>bar</foo><sign>mock-signature</sign></xml>', $client->getRequestOptions()['body']);\n\n        // XML string will not attach signature\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->never();\n\n        $client->post('certificates', ['xml' => Xml::build(['foo' => 'bar'])]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame(Xml::build(['foo' => 'bar']), $client->getRequestOptions()['body']);\n    }\n\n    public function test_v2_request_with_xml_string_as_body()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $client->post('certificates', ['body' => Xml::build(['foo' => 'bar'])]);\n\n        $this->assertSame('POST', $client->getRequestMethod());\n        $this->assertSame('https://api.mch.weixin.qq.com/certificates', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n        $this->assertSame(Xml::build(['foo' => 'bar']), $client->getRequestOptions()['body']);\n    }\n\n    public function test_v2_request_appauth_getaccesstoken()\n    {\n        $client = Client::mock('{\"retcode\":-1,\"access_token\":\"mock-token\"}', 200, ['Content-Type' => 'application/json']);\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('isV3Request')->andReturn(false);\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $response = $client->get('/appauth/getaccesstoken', ['query' => ['foo' => 'bar']]);\n\n        $this->assertSame('GET', $client->getRequestMethod());\n        $this->assertEquals(['foo' => 'bar', 'sign' => 'mock-signature'], $client->getRequestOptions()['query']);\n        $this->assertSame('https://api.mch.weixin.qq.com/appauth/getaccesstoken?foo=bar&sign=mock-signature', $client->getRequestUrl());\n        $this->assertContains('Content-Type: text/xml', $client->getRequestOptions()['headers']);\n    }\n\n    public function test_v3_upload_media()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->with(\n            'POST',\n            '/v3/merchant/media/upload',\n            \\Mockery::on(function ($options) {\n                return $options['body'] === json_encode([\n                    'filename' => 'image.jpg',\n                    'sha256' => hash('sha256', file_get_contents('./tests/fixtures/files/image.jpg')),\n                ]);\n            })\n        )->andReturn('mock-signature');\n\n        $response = new MockResponse('{\"media_id\":\"mock-media-id\"}');\n\n        $client->shouldReceive('request')->with(\n            'POST',\n            '/v3/merchant/media/upload',\n            \\Mockery::on(function ($options) {\n                return $options['body'] !== json_encode([\n                    'filename' => 'image.jpg',\n                    'sha256' => hash('sha256', file_get_contents('./tests/fixtures/files/image.jpg')),\n                ]);\n            })\n        )->andReturn($response);\n\n        $this->assertSame($response, $client->uploadMedia('/v3/merchant/media/upload', './tests/fixtures/files/image.jpg'));\n    }\n\n    public function test_v3_with_serial_header()\n    {\n        $client = Client::mock();\n        $client->shouldReceive('createSignature')->never();\n        $client->shouldReceive('attachLegacySignature')->with([\n            'foo' => 'bar',\n        ])->andReturn(['foo' => 'bar', 'sign' => 'mock-signature']);\n\n        $client->withSerialHeader()->post('certificates', ['body' => Xml::build(['foo' => 'bar'])]);\n        $this->assertSame('Wechatpay-Serial: PUB_KEY_ID_MOCK', $client->getRequestOptions()['headers'][0]);\n    }\n}\n"
  },
  {
    "path": "tests/Pay/MerchantTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Pay;\n\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Pay\\Merchant;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass MerchantTest extends TestCase\n{\n    public function test_construct()\n    {\n        $privateKey = new PrivateKey('mock-private-key');\n        $publicKey = \\Mockery::mock(PublicKey::class);\n\n        $merchant = new Merchant(\n            mchId: 100001,\n            privateKey: $privateKey,\n            certificate: $publicKey,\n            secretKey: 'v3SecretKey',\n            v2SecretKey: 'v2SecretKey',\n        );\n\n        $this->assertSame(100001, $merchant->getMerchantId());\n        $this->assertSame($privateKey, $merchant->getPrivateKey());\n        $this->assertSame('v3SecretKey', $merchant->getSecretKey());\n        $this->assertSame('v2SecretKey', $merchant->getV2SecretKey());\n        $this->assertSame($publicKey, $merchant->getCertificate());\n    }\n}\n"
  },
  {
    "path": "tests/Pay/ServerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Pay;\n\nuse EasyWeChat\\Kernel\\Support\\AesEcb;\nuse EasyWeChat\\Kernel\\Support\\AesGcm;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Pay\\Contracts\\Merchant;\nuse EasyWeChat\\Pay\\Message;\nuse EasyWeChat\\Pay\\Server;\nuse EasyWeChat\\Tests\\TestCase;\nuse Mockery\\LegacyMockInterface;\nuse Nyholm\\Psr7\\Response;\nuse Nyholm\\Psr7\\ServerRequest;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nuse function bin2hex;\nuse function fopen;\nuse function md5;\nuse function random_bytes;\n\nclass ServerTest extends TestCase\n{\n    public function test_it_will_handle_validation_request()\n    {\n        $request = (new ServerRequest(\n            'POST',\n            'http://easywechat.com/',\n            [\n                'Content-Type' => 'application/json',\n            ],\n            fopen(__DIR__.'/../fixtures/files/pay_demo.json', 'r')\n        ));\n\n        /** @var Merchant&LegacyMockInterface $merchant */\n        $merchant = \\Mockery::mock(Merchant::class);\n        $merchant->shouldReceive('getSecretKey')->andReturn('key');\n\n        $server = new Server($merchant, $request);\n\n        $response = $server->serve();\n        $this->assertSame('{\"code\":\"SUCCESS\",\"message\":\"成功\"}', \\strval($response->getBody()));\n    }\n\n    public function test_legacy_encryped_by_aesecb_refund_request()\n    {\n        /** @var Merchant&LegacyMockInterface $merchant */\n        $merchant = \\Mockery::mock(Merchant::class);\n        $merchant->shouldReceive(['getV2SecretKey' => random_bytes(32)]);\n        $symmtricKey = $merchant->getV2SecretKey();\n\n        $server = new Server($merchant, new ServerRequest(\n            'POST',\n            'http://easywechat.com/sample-webhook-handler',\n            [\n                'Content-Type' => 'text/xml',\n            ],\n            Xml::build([\n                'return_code' => 'SUCCESS',\n                'req_info' => AesEcb::encrypt(Xml::build([\n                    'refund_id' => '50000408942018111907145868882',\n                    'transaction_id' => '4200000215201811190261405420',\n                ]), md5($symmtricKey), ''),\n            ])\n        ));\n\n        $response = $server->with(function (Message $message): ResponseInterface {\n            $source = $message->getOriginalContents();\n            $parsed = $message->toArray();\n\n            $this->assertStringContainsString('<xml>', $source);\n            $this->assertStringContainsString('<req_info>', $source);\n            $this->assertStringNotContainsString('<refund_id>', $source);\n            $this->assertStringNotContainsString('<transaction_id>', $source);\n            $this->assertArrayNotHasKey('return_code', $parsed);\n            $this->assertArrayNotHasKey('req_info', $parsed);\n            $this->assertArrayHasKey('refund_id', $parsed);\n            $this->assertArrayHasKey('transaction_id', $parsed);\n\n            return new Response(\n                200,\n                ['Content-Type' => 'text/xml'],\n                '<xml><return_code>SUCCESS</return_code></xml>'\n            );\n        })->serve();\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertSame('<xml><return_code>SUCCESS</return_code></xml>', \\strval($response->getBody()));\n    }\n\n    public function test_legacy_encryped_by_aesgcm_notification_request()\n    {\n        /** @var Merchant&LegacyMockInterface $merchant */\n        $merchant = \\Mockery::mock(Merchant::class);\n        $merchant->shouldReceive(['getSecretKey' => random_bytes(32)]);\n        $symmtricKey = $merchant->getSecretKey();\n\n        $server = new Server($merchant, new ServerRequest(\n            'POST',\n            'http://easywechat.com/sample-webhook-handler',\n            [\n                'Content-Type' => 'text/xml',\n            ],\n            Xml::build([\n                'event_type' => 'TRANSACTION.SUCCESS',\n                'event_algorithm' => 'AEAD_AES_256_GCM',\n                'event_nonce' => $nonce = bin2hex(random_bytes(6)),\n                'event_associated_data' => $aad = '',\n                'event_ciphertext' => AesGcm::encrypt(Xml::build([\n                    'state' => 'USER_PAID',\n                    'service_id' => '1234352342',\n                ]), $symmtricKey, iv: $nonce, aad: $aad),\n            ])\n        ));\n\n        $response = $server->with(function (Message $message): ResponseInterface {\n            $source = $message->getOriginalContents();\n            $parsed = $message->toArray();\n\n            $this->assertStringContainsString('<xml>', $source);\n            $this->assertStringContainsString('<event_type>', $source);\n            $this->assertStringContainsString('<event_algorithm>', $source);\n            $this->assertStringContainsString('<event_nonce>', $source);\n            $this->assertStringContainsString('<event_associated_data>', $source);\n            $this->assertStringContainsString('<event_ciphertext>', $source);\n            $this->assertStringNotContainsString('<state>', $source);\n            $this->assertStringNotContainsString('<service_id>', $source);\n            $this->assertArrayHasKey('event_type', $parsed);\n            $this->assertArrayHasKey('event_algorithm', $parsed);\n            $this->assertArrayHasKey('event_nonce', $parsed);\n            $this->assertArrayHasKey('event_associated_data', $parsed);\n            $this->assertArrayHasKey('event_ciphertext', $parsed);\n            $this->assertArrayHasKey('state', $parsed);\n            $this->assertArrayHasKey('service_id', $parsed);\n\n            return new Response(\n                500,\n                ['Content-Type' => 'text/xml'],\n                '<xml><code>ERROR_NAME</code><message>ERROR_DESCRIPTION</message></xml>'\n            );\n        })->serve();\n\n        $this->assertEquals(500, $response->getStatusCode());\n        $this->assertSame('<xml><code>ERROR_NAME</code><message>ERROR_DESCRIPTION</message></xml>', \\strval($response->getBody()));\n    }\n}\n"
  },
  {
    "path": "tests/Pay/UtilsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Pay;\n\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidConfigException;\nuse EasyWeChat\\Kernel\\Support\\PrivateKey;\nuse EasyWeChat\\Kernel\\Support\\PublicKey;\nuse EasyWeChat\\Pay\\Exceptions\\EncryptionFailureException;\nuse EasyWeChat\\Pay\\Merchant;\nuse EasyWeChat\\Pay\\Utils;\nuse EasyWeChat\\Tests\\TestCase;\n\nclass UtilsTest extends TestCase\n{\n    /**\n     * @see https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=20_1\n     * MD5签名名方式\n     * 1.对参数按照key=value的格式，并按照参数名ASCII字典序排序生成字符串：\n     * appId=mock-appid&nonceStr=mock-nonce&package=prepay_id=mock-prepay-id&signType=MD5&timeStamp=1601234567\n     *\n     * 2.连接密钥key：\n     * appId=mock-appid&nonceStr=mock-nonce&package=prepay_id=mock-prepay-id&signType=MD5&timeStamp=1601234567&key=mock-v2-secret-key\n     *\n     * 3.生成sign并转成大写：\n     * sign=C52D6B09E8A039D6E8696A014BB37160\n     *\n     * HMAC-SHA256签名方式\n     * 1.对参数按照key=value的格式，并按照参数名ASCII字典序排序生成字符串：\n     * appId=mock-appid&nonceStr=mock-nonce&package=prepay_id=mock-prepay-id&signType=HMAC-SHA256&timeStamp=1601234567\n     *\n     * 2.连接密钥key：\n     * appId=mock-appid&nonceStr=mock-nonce&package=prepay_id=mock-prepay-id&signType=HMAC-SHA256&timeStamp=1601234567&key=mock-v2-secret-key\n     *\n     * 3.生成sign并转成大写：\n     * sign=BAC9240577E86EDC7753264E502196C61F78F24777E9E7CCE82A7BD97F906EED\n     */\n    public function test_create_v2_signature()\n    {\n        $params = [\n            'appId' => 'mock-appid',\n            'timeStamp' => 1601234567,\n            'nonceStr' => 'mock-nonce',\n            'package' => 'prepay_id=mock-prepay-id',\n            'signType' => 'MD5',\n        ];\n\n        $merchant = \\Mockery::mock(Merchant::class);\n        $merchant->allows()->getV2SecretKey()->andReturn('mock-v2-secret-key');\n\n        $utils = new Utils(merchant: $merchant);\n\n        $this->assertSame('C52D6B09E8A039D6E8696A014BB37160', $utils->createV2Signature($params));\n\n        $params['signType'] = 'HMAC-SHA256';\n        $this->assertSame('BAC9240577E86EDC7753264E502196C61F78F24777E9E7CCE82A7BD97F906EED', $utils->createV2Signature($params));\n    }\n\n    public function test_encrypt_with_rsa_public_key()\n    {\n        $privateKey = new PrivateKey('mock-private-key');\n        $publicKey = \\Mockery::mock(PublicKey::class);\n\n        $merchant = new Merchant(\n            mchId: 100001,\n            privateKey: $privateKey,\n            certificate: $publicKey,\n            secretKey: 'v3SecretKey',\n            v2SecretKey: 'v2SecretKey',\n            platformCerts: [\n                'PUB_KEY_ID_MOCK' => '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlReZ1YnfAohRIfUqIeyP\naO0PlkMw1RLPdZbEZmldbGrIrOh/0XqSzNZ+mtB6H0eB7TSaoGFtdp/AWy3tb67m\n1T62OrEhz6bnSKMcZkYVmODyxZvcwsCZ3zqCaFo7FrGmh1o9M0/Xfa5SOX4jVGni\n3iM7r7YD/NiW2RCYDtjMoLTmVgrzv45Mzu2XpJqtNbUJIRRhVSnjsAZRC6spWH+b\nQpYIkVd4qmYE0qdpIQBMYOV1w7v1pYn6Z5QdKG4keemADTn4QaZZHrryTcHNYVsZ\n2OZ3aybrevSV3wDGnYGk2nt2xtkdfaNfFn4dGW+p4an5M4fRK+CnYpeTgI6POABk\npwIDAQAB\n-----END PUBLIC KEY-----',\n            ]\n        );\n        $utils = new Utils(merchant: $merchant);\n        $this->assertIsString($utils->encryptWithRsaPublicKey('mock-plaintext', 'PUB_KEY_ID_MOCK'));\n        $this->assertIsString($utils->encryptWithRsaPublicKey('mock-plaintext'));\n\n        $this->expectException(InvalidConfigException::class);\n        $utils->encryptWithRsaPublicKey('mock-plaintext', 'PUB_KEY_ID_OTHER');\n\n        $this->expectException(EncryptionFailureException::class);\n        $utils->encryptWithRsaPublicKey('', 'PUB_KEY_ID_MOCK');\n\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests;\n\nuse EasyWeChat\\Kernel\\Encryptor;\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse Nyholm\\Psr7\\ServerRequest;\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nclass TestCase extends BaseTestCase\n{\n    /**\n     * Tear down the test case.\n     */\n    protected function tearDown(): void\n    {\n        parent::tearDown();\n        if ($container = \\Mockery::getContainer()) {\n            $this->addToAssertionCount($container->Mockery_getExpectationCount());\n        }\n        \\Mockery::close();\n    }\n\n    public function createEncryptedXmlMessageRequest($plainMessageXml, Encryptor $encryptor, array $query = []): ServerRequest\n    {\n        $body = $encryptor->encrypt($plainMessageXml);\n\n        $xml = Xml::parse($body);\n\n        return (new ServerRequest('POST', 'http://easywechat.com/server', [], $body))->withQueryParams([\n            'msg_signature' => $xml['MsgSignature'],\n            'timestamp' => $xml['TimeStamp'],\n            'nonce' => $xml['Nonce'],\n            ...$query,\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Work/AccessTokenTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\AccessToken;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass AccessTokenTest extends TestCase\n{\n    public function test_get_token_from_http_request()\n    {\n        $httpClient = \\Mockery::mock(HttpClientInterface::class);\n        $response = \\Mockery::mock(ResponseInterface::class);\n\n        $result = [\n            'access_token' => 'mock_access_token',\n            'expires_in' => '1500',\n        ];\n\n        $response->allows()->toArray(false)->andReturn($result);\n\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $options = [\n            'query' => [\n                'corpid' => $config['corp_id'],\n                'corpsecret' => $config['secret'],\n            ],\n        ];\n\n        $httpClient->allows()->request('GET', '/cgi-bin/gettoken', $options)->andReturn($response);\n\n        $accessToken = new AccessToken($config['corp_id'], $config['secret'], null, null, $httpClient);\n\n        $this->assertSame($result['access_token'], $accessToken->getToken());\n    }\n\n    public function test_get_token_from_cache()\n    {\n        $cache = \\Mockery::mock(CacheInterface::class);\n\n        $key = 'mock-cache-key';\n\n        $result = [\n            'access_token' => 'mock_access_token',\n            'expires_in' => '1500',\n        ];\n\n        $cache->expects()->get($key)->andReturn($result['access_token']);\n\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $accessToken = new AccessToken($config['corp_id'], $config['secret'], $key, $cache);\n\n        $this->assertSame($result['access_token'], $accessToken->getToken());\n    }\n\n    public function test_set_key()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n        ];\n\n        $key = 'mock-cache-key';\n\n        $accessToken = new AccessToken($config['corp_id'], $config['secret'], $key);\n\n        $this->assertSame($key, $accessToken->getKey());\n    }\n}\n"
  },
  {
    "path": "tests/Work/AccountTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\Account;\nuse EasyWeChat\\Work\\Application;\nuse EasyWeChat\\Work\\Contracts\\Account as AccountInterface;\n\nclass AccountTest extends TestCase\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_application_created_can_get_account()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'mock-aes_key',\n                'agent_id' => 1000001,\n            ]\n        );\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_set_account_to_application()\n    {\n        $accountConfig = [\n            'corp_id' => 'wx3cf0f39249111111',\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes-key',\n        ];\n\n        $account = new Account(\n            corpId: $accountConfig['corp_id'],\n            secret: $accountConfig['secret'],\n            token: $accountConfig['token'],\n            aesKey: $accountConfig['aes_key'],\n        );\n\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes_key',\n        ];\n\n        $app = new Application($config);\n\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($config['corp_id'], $app->getAccount()->getCorpId());\n\n        $app->setAccount($account);\n\n        $this->assertNotSame($config['corp_id'], $app->getAccount()->getCorpId());\n        $this->assertSame($accountConfig['corp_id'], $app->getAccount()->getCorpId());\n    }\n\n    public function test_get_account_corp_id()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            corpId: $config['corp_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['corp_id'], $account->getCorpId());\n    }\n\n    public function test_get_account_secret()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            corpId: $config['corp_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['secret'], $account->getSecret());\n    }\n\n    public function test_get_account_token()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            corpId: $config['corp_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['token'], $account->getToken());\n    }\n\n    public function test_get_account_aes_key()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n            'aes_key' => 'mock-aes-key',\n        ];\n\n        $account = new Account(\n            corpId: $config['corp_id'],\n            secret: $config['secret'],\n            token: $config['token'],\n            aesKey: $config['aes_key']\n        );\n\n        $this->assertSame($config['aes_key'], $account->getAesKey());\n    }\n}\n"
  },
  {
    "path": "tests/Work/ApplicationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Kernel\\Config;\nuse EasyWeChat\\Kernel\\Contracts\\AccessToken as AccessTokenInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Contracts\\Server as ServerInterface;\nuse EasyWeChat\\Kernel\\HttpClient\\AccessTokenAwareClient;\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\AccessToken;\nuse EasyWeChat\\Work\\Account;\nuse EasyWeChat\\Work\\Application;\nuse EasyWeChat\\Work\\Contracts\\Account as AccountInterface;\nuse EasyWeChat\\Work\\Contracts\\Application as ApplicationInterface;\nuse EasyWeChat\\Work\\Encryptor;\nuse EasyWeChat\\Work\\JsApiTicket;\nuse EasyWeChat\\Work\\Server;\nuse EasyWeChat\\Work\\Utils;\nuse Overtrue\\Socialite\\Providers\\WeWork;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Component\\Cache\\Psr16Cache;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass ApplicationTest extends TestCase\n{\n    public function test_get_and_set_account()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ApplicationInterface::class, $app);\n        $this->assertInstanceOf(AccountInterface::class, $app->getAccount());\n        $this->assertSame($app->getAccount(), $app->getAccount());\n\n        // set\n        $account = new Account(corpId: 'wx3cf0f39249000060', secret: 'mock-secret', token: 'mock-token', aesKey: 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG');\n        $app->setAccount($account);\n        $this->assertSame($account, $app->getAccount());\n    }\n\n    public function test_get_and_set_encryptor()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(Encryptor::class, $app->getEncryptor());\n        $this->assertSame($app->getEncryptor(), $app->getEncryptor());\n\n        // set\n        $encryptor = new Encryptor(corpId: 'wx3cf0f39249000060', token: 'mock-token', aesKey: 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG');\n        $app->setEncryptor($encryptor);\n        $this->assertSame($encryptor, $app->getEncryptor());\n    }\n\n    public function test_get_and_set_request()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ServerRequestInterface::class, $app->getRequest());\n        $this->assertSame($app->getRequest(), $app->getRequest());\n\n        // set\n        $request = \\Mockery::mock(ServerRequestInterface::class);\n        $app->setRequest($request);\n        $this->assertSame($request, $app->getRequest());\n    }\n\n    public function test_get_and_set_server()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ServerInterface::class, $app->getServer());\n        $this->assertSame($app->getServer(), $app->getServer());\n\n        // set\n        $server = new Server(\n            encryptor: $app->getEncryptor(),\n            request: \\Mockery::mock(ServerRequestInterface::class)\n        );\n        $app->setServer($server);\n        $this->assertSame($server, $app->getServer());\n    }\n\n    public function test_get_and_set_client()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(AccessTokenAwareClient::class, $app->getClient());\n        $this->assertSame($app->getClient(), $app->getClient());\n\n        // set\n        $client = new AccessTokenAwareClient;\n        $app->setClient($client);\n        $this->assertSame($client, $app->getClient());\n    }\n\n    public function test_get_and_set_http_client()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(HttpClientInterface::class, $app->getHttpClient());\n        $this->assertSame($app->getHttpClient(), $app->getHttpClient());\n\n        // set\n        $client = new AccessTokenAwareClient;\n        $app->setHttpClient($client);\n        $this->assertSame($client, $app->getHttpClient());\n    }\n\n    public function test_get_and_set_access_token()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(AccessTokenInterface::class, $app->getAccessToken());\n        $this->assertSame($app->getAccessToken(), $app->getAccessToken());\n\n        // set\n        $accessToken = new AccessToken('wx3cf0f39249000060', 'mock-secret');\n        $app->setAccessToken($accessToken);\n        $this->assertSame($accessToken, $app->getAccessToken());\n    }\n\n    public function test_get_and_set_cache()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(CacheInterface::class, $app->getCache());\n        $this->assertSame($app->getCache(), $app->getCache());\n\n        // set\n        $cache = \\Mockery::mock(Psr16Cache::class);\n        $app->setCache($cache);\n        $this->assertSame($cache, $app->getCache());\n    }\n\n    public function test_get_and_set_config()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n        $this->assertSame($app->getConfig(), $app->getConfig());\n\n        // set\n        $config = new Config(\n            [\n                'corp_id' => 'wx3cf0f39249000060-2',\n                'secret' => 'mock-secret-2',\n                'token' => 'mock-token-2',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n        $app->setConfig($config);\n        $this->assertSame($config, $app->getConfig());\n    }\n\n    public function test_get_and_set_ticket()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n                'agent_id' => 100001,\n            ]\n        );\n\n        $this->assertInstanceOf(JsApiTicket::class, $app->getTicket());\n\n        // set\n        $ticket = new JsApiTicket('wx3cf0f39249000060', 'mock-token', $app->getCache(), $app->getClient());\n        $app->setTicket($ticket);\n        $this->assertSame($ticket, $app->getTicket());\n    }\n\n    public function test_get_utils()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n                'agent_id' => 100001,\n            ]\n        );\n\n        $this->assertInstanceOf(Utils::class, $app->getUtils());\n    }\n\n    public function test_get_oauth()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n            ]\n        );\n\n        $oauth = $app->getOauth();\n        $this->assertInstanceOf(WeWork::class, $oauth);\n        $ref = new \\ReflectionProperty($oauth, 'agentId');\n        $this->assertNull($ref->getValue($oauth));\n\n        // with default agent id\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n                'aes_key' => 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',\n                'agent_id' => '100001',\n            ]\n        );\n\n        $oauth = $app->getOauth();\n        $this->assertInstanceOf(WeWork::class, $oauth);\n        $ref = new \\ReflectionProperty($oauth, 'agentId');\n        $this->assertSame(100001, $ref->getValue($oauth));\n    }\n}\n"
  },
  {
    "path": "tests/Work/ConfigTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Kernel\\Contracts\\Config as ConfigInterface;\nuse EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException;\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\Application;\nuse EasyWeChat\\Work\\Config;\n\nclass ConfigTest extends TestCase\n{\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_application_created_can_get_config()\n    {\n        $app = new Application(\n            [\n                'corp_id' => 'wx3cf0f39249000060',\n                'secret' => 'mock-secret',\n                'token' => 'mock-token',\n            ]\n        );\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n    }\n\n    /**\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\InvalidArgumentException\n     * @throws \\EasyWeChat\\Kernel\\Exceptions\\RuntimeException\n     */\n    public function test_set_config_to_application()\n    {\n        $config = [\n            'corp_id' => 'wx3cf0f39249111111',\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes-key',\n            'agent_id' => 1000001,\n        ];\n\n        $config = new Config($config);\n\n        $applicationConfig = [\n            'corp_id' => 'wx3cf0f39249000060',\n            'secret' => 'mock-secret',\n            'token' => 'mock-token',\n        ];\n\n        $app = new Application(\n            [\n                'corp_id' => $applicationConfig['corp_id'],\n                'secret' => $applicationConfig['secret'],\n                'token' => $applicationConfig['token'],\n            ]\n        );\n\n        $this->assertInstanceOf(ConfigInterface::class, $app->getConfig());\n        $this->assertSame($applicationConfig['corp_id'], $app->getConfig()->get('corp_id'));\n        $this->assertSame(false, $app->getConfig()->has('aes_key'));\n\n        $app->setConfig($config);\n\n        $this->assertNotSame($applicationConfig['corp_id'], $app->getConfig()->get('corp_id'));\n        $this->assertSame($config['corp_id'], $app->getConfig()->get('corp_id'));\n        $this->assertSame(true, $app->getConfig()->has('aes_key'));\n    }\n\n    public function test_init_config_can_check_missing_keys()\n    {\n        $config = [\n            'secret' => 'mock-account-secret',\n            'token' => 'mock-account-token',\n            'aes_key' => 'mock-account-aes_key',\n            'agent_id' => 1000001,\n        ];\n\n        $this->expectException(InvalidArgumentException::class);\n        $this->expectExceptionMessage(\\sprintf('\"%s\" cannot be empty.', 'corp_id'));\n\n        new Config($config);\n    }\n}\n"
  },
  {
    "path": "tests/Work/JsApiTicketTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\JsApiTicket;\nuse Psr\\SimpleCache\\CacheInterface;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass JsApiTicketTest extends TestCase\n{\n    public function test_get_key()\n    {\n        $ticket = new JsApiTicket('mock-corpid');\n\n        $this->assertInstanceOf(JsApiTicket::class, $ticket);\n\n        $this->assertSame('work.jsapi_ticket.mock-corpid', $ticket->getKey());\n\n        $ticket->setKey('mock-key');\n        $this->assertSame('mock-key', $ticket->getKey());\n\n        $ticket = new JsApiTicket('mock-corpid', 'test-key');\n        $this->assertSame('test-key', $ticket->getKey());\n    }\n\n    public function test_get_agent_key()\n    {\n        $ticket = new JsApiTicket('mock-corpid');\n\n        $this->assertInstanceOf(JsApiTicket::class, $ticket);\n\n        $this->assertSame('work.jsapi_ticket.mock-corpid.100001', $ticket->getAgentKey(100001));\n\n        $ticket->setKey('mock-key');\n        $this->assertSame('mock-key.100001', $ticket->getAgentKey(100001));\n\n        $ticket = new JsApiTicket('mock-corpid', 'test-key');\n        $this->assertSame('test-key.100001', $ticket->getAgentKey(100001));\n    }\n\n    public function test_get_ticket()\n    {\n        $cacheKey = 'work.jsapi_ticket.mock-corpid';\n\n        $ticket = [\n            'ticket' => 'mock-ticket',\n            'expires_in' => 7200,\n        ];\n\n        $response = \\Mockery::mock(ResponseInterface::class);\n        $response->allows()->toArray(false)->andReturn($ticket);\n\n        $client = \\Mockery::mock(HttpClientInterface::class);\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn($ticket['ticket']);\n\n        $jsApiTicket = new JsApiTicket('mock-corpid', cache: $cache, httpClient: $client);\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getTicket());\n\n        // 设为过期\n        $cache->expects()->get($cacheKey)->andReturn(false);\n        $cache->expects()->set($cacheKey, $ticket['ticket'], $ticket['expires_in'])->andReturn(true);\n\n        $client->allows()->request('GET', '/cgi-bin/get_jsapi_ticket')\n            ->andReturn($response);\n\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getTicket());\n    }\n\n    public function test_get_agent_ticket()\n    {\n        $cacheKey = 'work.jsapi_ticket.mock-corpid.100001';\n\n        $ticket = [\n            'ticket' => 'mock-ticket',\n            'expires_in' => 7200,\n        ];\n\n        $response = \\Mockery::mock(ResponseInterface::class);\n        $response->allows()->toArray(false)->andReturn($ticket);\n\n        $client = \\Mockery::mock(HttpClientInterface::class);\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn($ticket['ticket']);\n\n        $jsApiTicket = new JsApiTicket('mock-corpid', cache: $cache, httpClient: $client);\n\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getAgentTicket(100001));\n\n        // 设为过期\n        $cache->expects()->get($cacheKey)->andReturn(false);\n        $cache->expects()->set($cacheKey, $ticket['ticket'], $ticket['expires_in'])->andReturn(true);\n\n        $client->allows()->request('GET', '/cgi-bin/ticket/get', ['query' => ['type' => 'agent_config']])\n            ->andReturn($response);\n\n        $this->assertSame($ticket['ticket'], $jsApiTicket->getAgentTicket(100001));\n    }\n\n    public function test_config_signature()\n    {\n        $nonce = 'mock-nonce';\n        $timestamp = 1601234567;\n\n        $cacheKey = 'work.jsapi_ticket.mock-corpid';\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn('mock-ticket');\n\n        $ticket = new JsApiTicket('mock-corpid', cache: $cache);\n\n        $result = $ticket->createConfigSignature('https://www.easywechat.com/', $nonce, $timestamp);\n\n        $data = [\n            'appId' => 'mock-corpid',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'url' => 'https://www.easywechat.com/',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $this->assertSame($data, $result);\n    }\n\n    public function test_agent_config_signature()\n    {\n        $nonce = 'mock-nonce';\n        $timestamp = 1601234567;\n\n        $cacheKey = 'work.jsapi_ticket.mock-corpid.100001';\n\n        $cache = \\Mockery::mock(CacheInterface::class);\n        $cache->expects()->get($cacheKey)->andReturn('mock-ticket');\n\n        $ticket = new JsApiTicket('mock-corpid', cache: $cache);\n\n        $result = $ticket->createAgentConfigSignature(100001, 'https://www.easywechat.com/', $nonce, $timestamp);\n\n        $data = [\n            'corpid' => 'mock-corpid',\n            'agentid' => 100001,\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'url' => 'https://www.easywechat.com/',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $this->assertSame($data, $result);\n    }\n\n    public function test_get_ticket_signature()\n    {\n        $ticket = new JsApiTicket('mock-corpid', 'mock-secret');\n\n        $sign = $ticket->getTicketSignature('mock-ticket', 'mock-nonce', 1601234567, 'https://www.easywechat.com/');\n        $this->assertSame('22772d2fb393ab9f7f6a5a54168a566fbf1ab767', $sign);\n    }\n}\n"
  },
  {
    "path": "tests/Work/ServerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Kernel\\Support\\Xml;\nuse EasyWeChat\\Kernel\\Traits\\DecryptMessage;\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\Application;\nuse Nyholm\\Psr7\\ServerRequest;\n\nclass ServerTest extends TestCase\n{\n    use DecryptMessage;\n\n    public function test_it_will_handle_validation_request()\n    {\n        $request = (new ServerRequest('GET', 'http://easywechat.com/server'))\n            ->withQueryParams([\n                'msg_signature' => '5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3',\n                'timestamp' => '1409659589',\n                'nonce' => '263014780',\n                'echostr' => 'P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==',\n            ]);\n\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => 'secret',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n        $app->setRequest($request);\n\n        $response = $app->getServer()->serve();\n\n        $this->assertSame('1616140317555161061', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_validate_message()\n    {\n        $body = '<xml>\n                <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>\n                <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>\n                <AgentID><![CDATA[218]]></AgentID>\n                </xml>';\n\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body))->withQueryParams([\n            'msg_signature' => '477715d11cdb4164915debcba66cb864d751f3e6',\n            'timestamp' => '1409659813',\n            'nonce' => '1372623149',\n        ]);\n\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => '',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n\n        $app->setRequest($request);\n\n        $response = $app->getServer()->serve();\n\n        $this->assertSame('SUCCESS', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_response_success_without_handlers()\n    {\n        $body = '<xml>\n                <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>\n                <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>\n                <AgentID><![CDATA[218]]></AgentID>\n                </xml>';\n\n        $request = (new ServerRequest('POST', 'http://easywechat.com/server', [], $body))->withQueryParams([\n            'msg_signature' => '477715d11cdb4164915debcba66cb864d751f3e6',\n            'timestamp' => '1409659813',\n            'nonce' => '1372623149',\n        ]);\n\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => '',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n\n        $app->setRequest($request);\n\n        $response = $app->getServer()->serve();\n\n        $this->assertSame('SUCCESS', \\strval($response->getBody()));\n    }\n\n    public function test_it_will_respond_from_message_handlers()\n    {\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => '',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n\n        $request = $this->createEncryptedXmlMessageRequest('<xml>\n            <ToUserName><![CDATA[toUser]]></ToUserName>\n            <FromUserName><![CDATA[sys]]></FromUserName>\n            <CreateTime>1403610513</CreateTime>\n            <MsgType><![CDATA[event]]></MsgType>\n            <Event><![CDATA[change_contact]]></Event>\n            <ChangeType>change_contact</ChangeType>\n            <UserID><![CDATA[zhangsan]]></UserID>\n        </xml>', $app->getEncryptor());\n\n        $app->setRequest($request);\n\n        $response = $app->getServer()\n            ->addMessageListener(\n                'event',\n                function ($message) {\n                    return 'hello';\n                }\n            )\n            ->addEventListener(\n                'scancode_push',\n                function ($message) {\n                    return 'world';\n                }\n            )\n            ->serve();\n\n        $message = Xml::parse(\\strval($response->getBody()));\n\n        $response = Xml::parse($app->getEncryptor()->decrypt($message['Encrypt'], $message['MsgSignature'], $message['Nonce'], $message['TimeStamp']));\n\n        $this->assertSame('sys', $response['ToUserName']);\n        $this->assertSame('toUser', $response['FromUserName']);\n        $this->assertSame('text', $response['MsgType']);\n        $this->assertSame('hello', $response['Content']);\n    }\n\n    public function test_it_will_respond_from_event_handlers()\n    {\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => '',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n\n        $request = $this->createEncryptedXmlMessageRequest('<xml>\n            <ToUserName><![CDATA[toUser]]></ToUserName>\n            <FromUserName><![CDATA[sys]]></FromUserName>\n            <CreateTime>1403610513</CreateTime>\n            <MsgType><![CDATA[event]]></MsgType>\n            <Event><![CDATA[change_contact]]></Event>\n            <ChangeType>change_contact</ChangeType>\n            <UserID><![CDATA[zhangsan]]></UserID>\n        </xml>', $app->getEncryptor());\n\n        $app = new Application([\n            'corp_id' => 'wx5823bf96d3bd56c7',\n            'secret' => '',\n            'token' => 'QDG6eK',\n            'aes_key' => 'jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C',\n        ]);\n\n        $app->setRequest($request);\n\n        $response = $app->getServer()\n            ->addMessageListener(\n                'event',\n                function ($message) {\n                    return 'hello';\n                }\n            )\n            ->addEventListener(\n                'change_contact',\n                function ($message) {\n                    return 'world';\n                }\n            )\n            ->serve();\n\n        $message = Xml::parse(\\strval($response->getBody()));\n\n        $response = Xml::parse($app->getEncryptor()->decrypt($message['Encrypt'], $message['MsgSignature'], $message['Nonce'], $message['TimeStamp']));\n\n        $this->assertSame('sys', $response['ToUserName']);\n        $this->assertSame('toUser', $response['FromUserName']);\n        $this->assertSame('text', $response['MsgType']);\n        $this->assertSame('hello', $response['Content']);\n    }\n}\n"
  },
  {
    "path": "tests/Work/UtilsTest.php",
    "content": "<?php\n\nnamespace EasyWeChat\\Tests\\Work;\n\nuse EasyWeChat\\Tests\\TestCase;\nuse EasyWeChat\\Work\\Application;\nuse EasyWeChat\\Work\\JsApiTicket;\nuse EasyWeChat\\Work\\Utils;\n\nclass UtilsTest extends TestCase\n{\n    public function test_build_js_sdk_config()\n    {\n        $data = [\n            'jsApiList' => ['api1', 'api2'],\n            'openTagList' => ['openTag1', 'openTag2'],\n            'debug' => true,\n            'beta' => true,\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'appId' => 'mock-appid',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $signatue = [\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'appId' => 'mock-appid',\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $ticket = \\Mockery::mock(JsApiTicket::class);\n        $ticket->shouldReceive('createConfigSignature')->andReturn($signatue);\n\n        $app = \\Mockery::mock(Application::class);\n        $app->allows()->getTicket()->andReturn($ticket);\n\n        $utils = new Utils($app);\n\n        $result = $utils->buildJsSdkConfig('https://www.easywechat.com/', ['api1', 'api2'], ['openTag1', 'openTag2'], true, true);\n\n        $this->assertSame($data, $result);\n    }\n\n    public function test_build_js_sdk_agent_config()\n    {\n        $data = [\n            'jsApiList' => ['api1', 'api2'],\n            'openTagList' => ['openTag1', 'openTag2'],\n            'debug' => true,\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'corpid' => 'mock-corpid',\n            'agentid' => 100001,\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $signatue = [\n            'url' => 'https://www.easywechat.com/',\n            'nonceStr' => 'mock-nonce',\n            'timestamp' => 1601234567,\n            'corpid' => 'mock-corpid',\n            'agentid' => 100001,\n            'signature' => '22772d2fb393ab9f7f6a5a54168a566fbf1ab767',\n        ];\n\n        $ticket = \\Mockery::mock(JsApiTicket::class);\n        $ticket->shouldReceive('createAgentConfigSignature')->andReturn($signatue);\n\n        $app = \\Mockery::mock(Application::class);\n        $app->allows()->getTicket()->andReturn($ticket);\n\n        $utils = new Utils($app);\n\n        $result = $utils->buildJsSdkAgentConfig(100001, 'https://www.easywechat.com/', ['api1', 'api2'], ['openTag1', 'openTag2'], true);\n\n        $this->assertSame($data, $result);\n    }\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of the overtrue/wechat.\n *\n * (c) overtrue <i@overtrue.me>\n *\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\ndefine('TEST_ROOT', __DIR__);\ndefine('STUBS_ROOT', __DIR__.'/fixtures');\n\n$_SERVER['HTTP_HOST'] = 'localhost';\n\ninclude __DIR__.'/../vendor/autoload.php';\n"
  },
  {
    "path": "tests/fixtures/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDsjCCApqgAwIBAgIEDcDfgzANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJT\nQTERMA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxEzARBgNVBAoT\nCk15IENvbXBhbnkxEDAOBgNVBAsTB015IFRlYW0xGTAXBgNVBAMTEERlbW8gQ2Vy\ndGlmaWNhdGUwIBcNMjIwMzA0MTUxNzAxWhgPMjI5NTEyMTcxNTE3MDFaMHQxCzAJ\nBgNVBAYTAlNBMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTET\nMBEGA1UEChMKTXkgQ29tcGFueTEQMA4GA1UECxMHTXkgVGVhbTEZMBcGA1UEAxMQ\nRGVtbyBDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAPX8l0tL4xSuOqTxjdp560g8G+kt9YpRmhyYWdF6sztqYG1LdaBBxF1qFMU0/mwk\nv7G0YbcqEuwh+V0+veRYxPIpZJ5f9fqt6uY99BzRHbcur1XNlvKrJean2Zu0T+XH\nrFbGuDa+MvqPevwyymZkvxodh/RHkUCgjCmFMkH6KGa3SjpgBBn1/j6lM+PG4+Cr\nglear4YefgtfyqeGUcT9kmmyBQKl9jcM4392WsZEyfk17e9JYivH4Zcpzb2/47ZM\nTqXQymor4NFtSQ0FytMigoKzVSH5QWxiNZPe8/8Vn96sPiPmouy69dicexD2yti4\nThYuSp59Uu0iDgJgEjulQKcCAwEAAaNKMEgwJwYDVR0RBCAwHoILb3ZlcnRydWUu\nbWWCCWxvY2FsaG9zdIcEfwAAATAdBgNVHQ4EFgQUYYqZN/c2sBfiE8VFV09T+lBo\nKpQwDQYJKoZIhvcNAQELBQADggEBALsMXtmqvYVBSGPX1sP11K/Hw/oVdX+mOWEY\n5o73URfJ9iYTQ1zfUpwzK1RBWFhqYpUmFy4butt3RKPcmvu505/fZuyWFRD4tjKY\nMnazZ1n3/tqMePgjrx0UY9M4IAzd7JB8eIXAiyn/jf09uwkRrPwtKlAPxUDvaZVa\nNlZ5SogUanDS2vGSCpedvYXt2F/RxbodwO/DfkFdBfmafHWWqeUPjlLSa9uyser9\noKmO70LGHeT0wMnll7Z4nYUScFfrBMnqoA0eH8nnZsBvydc8SvSjIqVwiYdmjshP\nCdebaN9CH/9US2sqCunHCo1E+EKI90FOeKu2kinW3HUvmg3XCOI=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/fixtures/files/demo_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nVGhpcyBpcyBhIGRlbW8gY2VydCBjb250ZW50cy4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/fixtures/files/demo_key.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nVGhpcyBpcyBhIGRlbW8gcHVibGljIGtleSBjb250ZW50cy4=\n-----END PUBLIC KEY-----"
  },
  {
    "path": "tests/fixtures/files/empty.file",
    "content": ""
  },
  {
    "path": "tests/fixtures/files/pay_demo.json",
    "content": "{\"id\":\"EV-2018022511223320873\",\"create_time\":\"2015-05-20T13:29:35+08:00\",\"resource_type\":\"encrypt-resource\",\"event_type\":\"TRANSACTION.SUCCESS\",\"summary\":\"\\u652f\\u4ed8\\u6210\\u529f\",\"resource\":{\"nonce\":\"iv\",\"associated_data\":\"aad\",\"ciphertext\":\"gjxWKg2rMkwEcv\\/Hx9otXWwpEfyk\\/lltqRdgs1spC5vCj8VJn8lrmMuGArlZsXB4lhcJ5mBLo2mbQRlfZ0bobHV4DXspkyTec7\\/6E5W+MP1MI0CeO7phqbx6pYtrmlhEfKpcCaDUClbAuNv5YnO1buG9eKA+E7u+VbCTaVQTYkdWaM0VNQjI89m3ys+HUWl2s4cjAa3OSdrGo3AI9HJwIGoaX2F+gcBZTiheNNpaxKAdCqZVBP965tvew6qv2UHqwppKtgh91OTvKnbvH7lT0DFavgd+eqeB18E9cnCEh\\/vYAFyU2LMbi2pkp5P5Q7PGXJBqg1XtvtkkFOxGwh8r7eEKCReCrDnP8jr6YgJP7bC+3kBXpFV+QzGOVuOB9liUH1yHkzz8gR2bMA9l23gp393fgoPlkZdx7wqHKZ3qrrc8mBIVHzbbv95w7NfpWutToai\\/qOEfGYrawkZLACE+c\\/U+zPdcpuWWQFgR0q4o+8+LLvgg+rppJCqKsiZq229Ux1SRl1sq6CEocR5bP\\/7uWSuWDAb0VSy33Amg07Wtb1BHThwowOtuZNQVVJo3XucRi6W3ETzXsSw=\"}}"
  },
  {
    "path": "tests/fixtures/private.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD1/JdLS+MUrjqk\n8Y3aeetIPBvpLfWKUZocmFnRerM7amBtS3WgQcRdahTFNP5sJL+xtGG3KhLsIfld\nPr3kWMTyKWSeX/X6rermPfQc0R23Lq9VzZbyqyXmp9mbtE/lx6xWxrg2vjL6j3r8\nMspmZL8aHYf0R5FAoIwphTJB+ihmt0o6YAQZ9f4+pTPjxuPgq4JXmq+GHn4LX8qn\nhlHE/ZJpsgUCpfY3DON/dlrGRMn5Ne3vSWIrx+GXKc29v+O2TE6l0MpqK+DRbUkN\nBcrTIoKCs1Uh+UFsYjWT3vP/FZ/erD4j5qLsuvXYnHsQ9srYuE4WLkqefVLtIg4C\nYBI7pUCnAgMBAAECggEBAONzz5XBMtUxkWppLQiiiWs+oXRIme1vLPhZ61tmgIpk\n/4qwJrCrf139M/1x1fu7M0GnEF+kWODdPU36MO4PIvTkT3SeOL21ySliifGHb1mt\nolTNiFEU7jPMasutu981dWa70yRTVKIYv9ipQjvR1l7YhZjcADRo/5F45xLYtDjc\nXH8HnWb+OB2uPG72HDOtNkg55V1oCcTZqlRw1P8MpibIW/OiE5s70/USqR9v+Dua\nGUWKXEggD80GsyB8T5SYEG9KV0GqDDnJ2ebvrCniBkwFkjGlNy6VC7T5WzwMP7af\nNM1a5Rl4r5SgLkhU9Boug9xh78HNcpEyIoYRIgbYKiECgYEA/R6X0uugUqVnoStu\nU9dOcaeuxbqg8uVmmq4hRBsuwL1MT2mhQrjkpiGzmf8BFgZhPmdBPNbkiEFfWzJl\nG/aADemwycICyKEQJrsTv830HBB40icxDesnfGXrFSN1rPutyrvuk+O60QpfrCKV\nvAmvebfsOmI7rZUCjO4wm3FupncCgYEA+Mk30o7pkKnlbbmzIGnpnT0JJO2Ukg03\nTbbUJ9n39eR0Q4yU0qn9GkUD6j3JUzN35kLdorD8GtRECT+Dwe/MTJmXOmSa+pNv\nr2Oz6+VkIozK8Arb1UhXZvdxmEK+G8JbGb9TBdY+g5KEUzxiJVvGaohNEmUrdiU+\nmhUwJM4mU1ECgYEAjHRDbwjMx898t5Qv/loE71BKzPGIif/9dWuWjgI/aTghdhd5\nIlsSlZUx4gv8gEo9/7CwJKBh47HSxfoq3N9lo1K/hNUhUk79dWBtzwrrn/8L7CFP\nORf5qy+qyOoKTFjD0TGDa5D8gy6sdAok6Fi/hF0ouiT0VJfQ+H7bfgXtjnUCgYAL\nqGosPLQnKj7tAFmreCsRSi3mhBeFLIdd5eZVI7qh1B26mJjUslCA7aD8Vf7cHjpN\nGO8H06FMpIu3nzlOWi1JnZzfiJ1WhpgiMNraf/pMQCq2Stv4R5aI5al2CBKefGIG\nd9etG1f8lF4NBHRY6c8HaPGrngAGXkJrt/h0kRBssQKBgHj7HfSxz1prctAj/zSH\n6KuxvZuUy33vjazfIyTFuZpLypUqduA+FdpRy/4LOe1SvM7ocs6wUplVRVt0m6/N\n8mTtP+V5aKeT03wor8n9AU37nLJXp1LrYiLiBkv8xVqCao50nI5TmfGDScaowILc\nwv9NyxIRjUZ1EaLp16JDQaWN\n-----END PRIVATE KEY-----\n"
  }
]