news 2026/9/24 19:28:31

Deployer Provision Recipe 详解:从裸机到可部署 Web 服务器的全自动配置指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Deployer Provision Recipe 详解:从裸机到可部署 Web 服务器的全自动配置指南

Deployer Provision Recipe 详解:从裸机到可部署 Web 服务器的全自动配置指南

【免费下载链接】deployerThe PHP deployment tool with support for popular frameworks out of the box项目地址: https://gitcode.com/gh_mirrors/de/deployer

Provision Recipe 是 Deployer 提供的服务器自动化配置方案,用于将一台全新的 Ubuntu 服务器一键配置成可直接运行 PHP 应用的生产环境——包括系统更新、PHP/FPM、Node.js、数据库、Caddy Web 服务器、防火墙、SSH 安全策略与部署用户等全部环节。本文以 docs/recipe/provision.md 为骨架,结合 recipe/provision.php 及各子任务源码,完整讲解该 Recipe 的配置参数、15 个编排任务的作用与执行顺序、以及每个子模块(PHP、数据库、Node.js、用户、网站)的实现细节,让你既能读懂源码,也能直接上手将一台新服务器纳入 Deployer 管理。

一、Recipe 概览与引入方式

Provision Recipe 不是一个独立的单一文件,而是由 5 个子 Recipe 组合而成的总入口。在 Deployer 部署脚本(deploy.php)中通过以下方式引入:

require 'recipe/provision.php';

引入后,recipe/provision.php 会自动再引入 5 个子模块:

子 Recipe文件负责内容
Databasesrecipe/provision/databases.phpMySQL / MariaDB / PostgreSQL 的安装与建库建用户
Nodejsrecipe/provision/nodejs.php通过 fnm 安装 Node.js(默认 LTS 版本)
Phprecipe/provision/php.phpPHP 与 PHP-FPM 的安装与调优、Composer
Userrecipe/provision/user.php创建部署用户deployer、配置 sudo 与 SSH
Websiterecipe/provision/website.phpCaddy 服务器配置与网站站点落盘

此外源码中还有一行add('recipes', ['provision']);(recipe/provision.php),将provision注册进 Deployer 的配方列表,便于dep recipe:list之类的命令检索。

二、配置参数(Configuration)

Provision Recipe 定义了两个顶层配置项,其余大量参数(如php_versiondb_typedomain等)由各子 Recipe 定义。它们共同构成了整个配置流程所需的全部输入。

2.1 lsb_release

// Name of lsb_release like: focal, bionic, etc. // As only Ubuntu 20.04 LTS is supported for provision should be the `focal`. set('lsb_release', function () { return run("lsb_release -s -c"); });
  • 含义:返回远端服务器的 LSB 发行版代号,例如focal(Ubuntu 20.04)、bionic(Ubuntu 18.04)。
  • 默认值run("lsb_release -s -c"),即动态执行远端命令获取。
  • 适用前提:文档明确说明 Provision 仅官方支持 Ubuntu 20.04 LTS,因此该值应为focal。它用于在后续流程中判断系统环境与选择匹配的软件源。
  • 源码位置:recipe/provision.php。

2.2 provision_user

// Default user to use for provisioning. set('provision_user', 'root');
  • 含义:执行整个 provisioning 流程时使用的远程账号。
  • 默认值'root'
  • 作用:所有provision:*任务在开始处都会执行set('remote_user', get('provision_user')),将当前任务的执行用户切换到该账号(通常是root),以保证有权限安装软件包、修改系统配置。
  • 源码位置:recipe/provision.php。

2.3 子 Recipe 中的交互式参数

这些参数大多不是写死在配置里,而是在访问时通过交互式提问自动收集(Deployer 的ask系列函数),保证一台新服务器在无任何预配置的情况下也能完成初始化。全部参数如下:

参数默认值 / 交互提示定义位置
sudo_passwordaskHiddenResponse(' Password for sudo: '),密码隐藏输入recipe/provision/user.php
domainask(' Domain: ', get('hostname')),默认取主机名recipe/provision/website.php
public_pathask(' Public path: ', 'public')recipe/provision/website.php
php_version自动探测composer.json中的require.php并归一化(如^8.28.2),探测失败默认8.4;可选5.6/7.4/8.2/8.3/8.4/8.5recipe/provision/php.php
db_typeaskChoice(' What DB to install? ', ['none','mysql','mariadb','postgresql'], 0),默认none(第 3 个参数为默认选项下标 0)recipe/provision/databases.php
db_nameask(' DB name: ', 'prod')recipe/provision/databases.php
db_userask(' DB user: ', 'deployer')recipe/provision/databases.php
db_passwordaskHiddenResponse(' DB password: ')recipe/provision/databases.php
node_version'--lts',即安装 Node.js 长期支持版recipe/provision/nodejs.php

关键机制——provision:configure的配置回显:当某些参数未被你在部署脚本中显式设置(即Context::get()->getConfig()->hasOwn($name)为 false)时,provision:configure会逐个调用get($name)触发交互式提问,并在结束时把收集到的值host(...)配置代码的形式打印出来(见 recipe/provision.php),形如:

====== Configuration Start ====== host('{{alias}}') ->set('sudo_password', 'xxx') ->set('domain', 'example.com') ->set('public_path', 'public') ->set('php_version', '8.4') ->set('db_type', 'mysql') ->set('db_user', 'deployer') ->set('db_name', 'prod') ->set('db_password', 'xxx'); ====== Configuration End ======

这是设计上的一等公民用法:首次 provisioning 采用交互问答,之后把回显的代码固化进deploy.php,后续即可全自动重跑。注意sudo_passworddb_password属于敏感信息,回显时请谨慎处理(实际生产中建议改用环境变量或密钥管理)。

三、核心任务:provision 编排与 15 个执行步骤

provision是 Recipe 的入口任务,源码 recipe/provision.php 中它被定义为一个分组任务(group task),按严格顺序依次执行 15 个子任务:

provision:check → provision:configure → provision:update → provision:upgrade → provision:install → provision:ssh → provision:firewall → provision:user → provision:php → provision:node → provision:databases → provision:composer → provision:server → provision:website → provision:verify

运行方式:dep provision(会应用到当前 host 配置中的所有主机)。

下面按执行顺序逐个解析每个子任务的目的与实现。

3.1 provision:check —— 检查前置系统状态

源码:recipe/provision.php

任务首先将remote_user切到provision_user,然后读取/etc/os-release并解析出NAMEVERSION_ID

  • 仅支持 Ubuntu:若非 Ubuntu,打印醒目的警告横幅并默认拒绝继续(askConfirmation('Do you want to continue? (Not recommended)', false),默认否);用户坚持继续才会放行,否则抛出RuntimeException
  • 版本约束version_compare($version, '20', '<'),即只接受 Ubuntu 20 及以上版本,低于 20 同样需要用户二次确认。
  • 任务标记了->oncePerNode():同一节点(主机)在一次执行周期内只运行一次,避免多主机并发时的重复检查。

3.2 provision:configure —— 收集必需参数

源码:recipe/provision.php

依次确保以下参数可用:sudo_passworddomainpublic_pathphp_versiondb_type;若db_type !== 'none',再额外确保db_userdb_namedb_password。凡是未显式配置的参数都会触发交互式提问,并把完整配置代码回显给用户(见上文 2.3 节)。该任务不标记oncePerNode,但通常由provision编排顺序保证只执行一次。

3.3 provision:update —— 添加软件源并更新

源码:recipe/provision.php

按顺序执行:

  1. apt-get update(全程设置DEBIAN_FRONTEND=noninteractive避免交互卡死);
  2. 安装前置工具curl gpg software-properties-common
  3. 添加 PHP PPA:apt-add-repository ppa:ondrej/php -y(设置LC_ALL=C.UTF-8防止 locale 问题);
  4. 添加Caddy Web 服务器官方稳定源:通过 Cloudsmith 下载 GPG 密钥(去装甲后存入/usr/share/keyrings/caddy-stable-archive-keyring.gpg),并把仓库描述写入/etc/apt/sources.list.d/caddy-stable.list
  5. 再次apt-get update刷新索引。

任务标记->oncePerNode()->verbose()(输出详细日志)。

3.4 provision:upgrade —— 升级全部软件包

源码:recipe/provision.php

执行apt-get upgrade -y,并设置900 秒超时(软件包升级可能较慢),同样使用非交互前端、oncePerNode与 verbose。

3.5 provision:install —— 安装系统级软件包

源码:recipe/provision.php

一次性安装 24 个基础包,涵盖:

  • Web/服务caddysendmailredis
  • 构建工具链build-essentialgccmakepkg-configlibpcre3-devlibsqlite3-devlibmcrypt4
  • 运维/安全fail2banufw(防火墙)、whois(提供mkpasswd)、ncdu(磁盘分析)
  • 通用工具aclapt-transport-httpscurlgitunzipuuid-runtimedebian-keyringdebian-archive-keyringpython-is-python3sqlite3nodejs

其中nodejs只是系统基础包,真正的 Node 版本管理由后面的provision:node(fnm)负责;whois包中的mkpasswd会在provision:user中用于生成密码哈希。任务同样有 900 秒超时、oncePerNode与 verbose。

3.6 provision:ssh —— 配置 SSH 安全策略

源码:recipe/provision.php

  1. sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config——禁用密码登录,强制密钥认证;
  2. ssh-keygen -A—— 生成缺失的主机密钥;
  3. service ssh restart—— 重启 SSH 服务生效;
  4. /root/.ssh不存在则创建并初始化authorized_keys

⚠️安全提示:禁用密码登录前请务必确保你的公钥已部署(可先用dep provision:ssh_copy_id或手动写入authorized_keys),否则可能把自己锁在服务器外。

3.7 provision:firewall —— 配置防火墙

源码:recipe/provision.php

使用 UFW:

ufw allow 22 # SSH ufw allow 80 # HTTP ufw allow 443 # HTTPS ufw --force enable

--force保证无交互直接启用。默认只放行 22/80/443 三个端口,其余全部拒绝,符合 Web 服务器的最小暴露面原则。

3.8 provision:user —— 创建 deployer 部署用户

源码:recipe/provision/user.php

这是从"root 一次性配置"切换到"日常部署用户"的关键步骤:

  • id deployer已存在,直接提示deployer user already exist并跳过(源码中留有 TODO 注释,表明后续版本可能补充对既有用户的校验与密码同步)。
  • 否则依次执行:
    1. useradd deployer创建用户;
    2. 创建/home/deployer/.ssh/home/deployer/.deployer
    3. adduser deployer sudo加入 sudo 组;
    4. chsh -s /bin/bash deployer设置默认 shell;
    5. 从 root 复制.profile.bashrc,并开启彩色提示符(force_color_prompt=yes);
    6. mkpasswd -m sha-512sudo_password生成 SHA-512 哈希,通过usermod --password设置密码(使用 secrets 机制,避免密码明文出现在命令输出中);
    7. 把 root 的authorized_keys复制给 deployer(继承密钥登录能力),并为 deployer 生成 ed25519 密钥对;
    8. 设置目录/文件权限(chown deployer:deployer.ssh700、私钥与authorized_keys600 等,权限变更的异常会被捕获并降级为 warning);
    9. 将 deployer 追加到www-datacaddy组,使其能读写 Web 相关资源。

3.9 provision:ssh_copy_id —— 复制公钥到远端

源码:recipe/provision/user.php

独立于主流程的辅助任务(不包含在provision编排内,可单独运行dep provision:ssh_copy_id)。逻辑:

  1. 依次探测本机~/.ssh/id_rsa.pubid_ed25519.pubid_ecdsa.pubid_dsa.pub(通过parse_home_dir解析家目录);
  2. 都没找到则提示手动粘贴公钥;
  3. 若仍为空则跳过并提示;
  4. 否则通过环境变量PUBLIC_KEY追加写入/home/deployer/.ssh/authorized_keys(避免引号/特殊字符转义问题)。

3.10 provision:php —— 安装并调优 PHP / PHP-FPM

源码:recipe/provision/php.php

根据php_version(如8.4)安装 16 个扩展包:bcmathclicurldevfpmgdimapintlmbstringmysqlpgsqlreadlinesoapsqlite3xmlzip,一条命令安装完毕。

随后通过sed对 CLI 与 FPM 两份php.ini以及 FPM 池配置pool.d/www.conf做调优:

配置项设置值适用文件
error_reportingE_ALLCLI / FPM 的php.ini
display_errorsOnCLI / FPM 的php.ini
memory_limit512MCLI / FPM 的php.ini
upload_max_filesize128MCLI / FPM 的php.ini
date.timezoneUTCCLI / FPM 的php.ini
cgi.fix_pathinfo0FPM 的php.ini
request_terminate_timeout60FPM 的www.conf
catch_workers_outputyesFPM 的www.conf
php_flag[display_errors]yesFPM 的www.conf
php_admin_value[error_log]/var/log/fpm-php.www.logFPM 的www.conf
php_admin_flag[log_errors]onFPM 的www.conf

最后对会话目录/var/lib/php/sessions设置733权限并加上 sticky bit(+t),确保 PHP-FPM 与 CLI 都能写入 session。任务标记->limit(1)(限制并发为 1,避免多主机并行冲突)与 verbose。

该子模块还附带两个日志查看任务:

  • logs:php-fpm(recipe/provision/php.php):自动列出/var/log下含fpm的日志文件并tail -f;无匹配时抛出异常。
  • provision:composer(recipe/provision/php.php):从 getcomposer.org 下载安装脚本并用 PHP 执行,将composer.phar移到/usr/local/bin/composer,全局可用,oncePerNode

3.11 provision:node —— 通过 fnm 安装 Node.js

源码:recipe/provision/nodejs.php

  • 若检测到旧参数nodejs_version被设置,直接抛出异常提示改用node_version
  • 通过uname -m探测 CPU 架构,从 fnm(Fast Node Manager)GitHub Releases 下载对应二进制:arm/armv7*fnm-arm32aarch*/armv8*fnm-arm64,其余 →fnm-linux
  • 解压到/tmp,安装到/usr/local/bin/fnm,执行fnm install {{node_version}}(默认--lts即 LTS 版)。
  • eval "fnm env"写入/etc/profile.d/fnm.sh,使所有登录 shell 自动加载 fnm 环境。

3.12 provision:databases —— 安装并初始化数据库

源码:recipe/provision/databases.php

入口任务provision:databases(L27-L37)读取db_type,若为none直接跳过;否则invoke('provision:' . $dbType)动态分发到对应子任务,并标记->limit(1)

三个数据库子任务细节:

provision:mysql(L39-L48)

  • apt-get install -y mysql-server(900 秒超时);
  • 以 root 免密方式创建用户{{db_user}}(同时允许0.0.0.0%来源),密码通过secrets 参数注入%db_password%,避免出现在命令回显中;
  • 授予*.*全部权限(含GRANT OPTION),FLUSH PRIVILEGES
  • 创建数据库{{db_name}},字符集UTF8mb4、排序规则utf8mb4_bin

provision:mariadb(L50-L59)

  • 与 MySQL 流程完全一致,仅安装包改为mariadb-server

provision:postgresql(L61-L66)

  • 安装postgresql postgresql-contrib
  • sudo -u postgres psql依次执行:CREATE DATABASE {{db_name}};CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD '...';GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};

3.13 provision:server —— 配置服务器基础目录

源码:recipe/provision/website.php

  • caddy用户加入www-data组;
  • 创建/var/deployer目录,并把仓库内置的 404.html 写入其中,作为全局 404 兜底页面。

3.14 provision:website —— 生成并激活 Caddy 站点

源码:recipe/provision/website.php

这是把应用真正"挂"上 Web 服务器的步骤:

  1. 通过become('deployer')临时切换到 deployer 用户(执行完由$restoreBecome()还原);
  2. 创建部署目录{{deploy_path}}chown deployer:deployer,随后set('deploy_path', realpath(...))归一化路径;
  3. 创建log目录并chgrp caddy log
  4. 用模板引擎解析内置的 Caddyfile 模板(其中的{{domain}}{{deploy_path}}{{public_path}}{{php_version}}会被替换成实际值);
  5. 智能合并策略:若站点目录已存在 Caddyfile,则生成Caddyfile.new并与旧文件做diff;无差异则删除新文件,有差异则打印 diff 并让用户选择保留old还是newaskChoice默认保留旧版);不存在则直接写入;
  6. 回到 root 身份,若/etc/caddy/Caddyfile尚未import {{deploy_path}}/Caddyfile则追加该行,随后service caddy reload热加载;
  7. 输出Website {{domain}} configured!

模板 recipe/provision/Caddyfile 的核心内容如下,它同时定义了反向代理、FastCGI 与访问日志:

{{domain}} { root * {{deploy_path}}/current/{{public_path}} encode zstd gzip file_server php_fastcgi * unix//run/php/php{{php_version}}-fpm.sock { resolve_root_symlink } log { output file {{deploy_path}}/log/access.log { mode 0644 } } handle_errors { @404 { expression {http.error.status_code} == 404 } rewrite @404 /404.html encode zstd gzip file_server { root /var/deployer } } }

要点解读:

  • root指向{{deploy_path}}/current/{{public_path}}——这与 Deployer 的"符号链接 release 机制"天然契合,current即当前版本软链接,配合resolve_root_symlink让 PHP-FPM 正确解析真实路径;
  • encode zstd gzip启用 zstd 与 gzip 双重压缩;
  • php_fastcgi直连php{{php_version}}-fpm.sock(Unix socket),即前面安装的 FPM 版本;
  • 访问日志写入{{deploy_path}}/log/access.log
  • 404 请求被重写到/var/deployer/404.html的统一错误页。

3.15 provision:verify —— 验证 provisioning 是否成功

源码:recipe/provision.php

使用fetch('{{domain}}', 'get', [], null, $info, true)对配置好的域名发起 HTTP GET 请求,并检查返回的$info['http_code']:由于此时尚未部署任何应用(current目录还不存在),预期返回404,命中即输出provisioned successfully!

验证逻辑的合理之处:404 说明 Caddy 已正常监听该域名并返回了内置错误页(而非连接失败/超时/500),恰好证明"Web 服务器就绪、只等部署代码"这一中间态,与"部署应用后再验证 200"的场景形成闭环。

3.16 辅助日志任务

除编排任务外,Provision 还提供了两个实时日志任务:

  • logs:access(recipe/provision/website.php):tail -f {{deploy_path}}/log/access.log跟踪站点访问日志;
  • logs:caddy(recipe/provision/website.php):sudo journalctl -u caddy -f跟踪 Caddy 服务日志。

四、推荐使用流程

把 Provision Recipe 纳入 Deployer 工作流的推荐路径:

  1. 准备:一台全新的 Ubuntu 20.04(或更新版本)服务器,确保能从本机免密 SSH 登录 root;在部署脚本中引入require 'recipe/provision.php';并定义 host。
  2. 预置公钥(可选但强烈推荐):先执行dep provision:ssh_copy_id把本机公钥写入远端。
  3. 执行配置dep provision,按提示回答 sudo 密码、域名、公开目录、PHP 版本、数据库类型/名称/用户/密码等提问。
  4. 固化配置:把provision:configure回显的host(...)->set(...)代码写入deploy.php,此后即可无交互重跑;或直接为所有参数提供显式配置,例如:
host('example.com') ->set('provision_user', 'root') ->set('domain', 'example.com') ->set('public_path', 'public') ->set('php_version', '8.4') ->set('db_type', 'mysql') ->set('db_user', 'deployer') ->set('db_name', 'prod') ->set('db_password', 'your-strong-password') ->set('sudo_password', 'your-sudo-password') ->set('deploy_path', '/var/www/example.com');
  1. 部署应用:provision 完成后,即可用常规的dep deploy流程发布代码(current符号链接由 Caddyfile 的resolve_root_symlink平滑衔接)。
  2. 日常运维:使用logs:accesslogs:caddylogs:php-fpm观察运行日志。

五、重要限制与注意事项

  • 操作系统强约束:源码在 provision:check 中明确只支持 Ubuntu(且建议 20+);文档声明 "As only Ubuntu 20.04 LTS is supported for provision",其他发行版会收到警告并需人工确认才能继续,非 Ubuntu 上不建议执行。
  • 全程以 root 运行provision_user默认root,请保证 root 的 SSH 公钥已部署;provision:ssh禁用 SSH 密码登录,务必先确认密钥可用。
  • 交互式提问不可避免:首次运行时多数参数(sudo_passworddomaindb_password等)通过ask/askHiddenResponse/askChoice收集,自动化 CI 场景请务必预先在脚本中->set()全部参数(可参考provision:configure的回显代码)。
  • 敏感信息处理:数据库密码与 sudo 密码通过 Deployer 的secrets参数传入远端命令,避免明文出现在进程列表中;回显的配置代码含明文密码,注意保管deploy.php
  • 并发限制provision:phpprovision:databasesprovision:website标记了->limit(1),多数任务标记->oncePerNode(),多主机场景下请理解这些约束以避免资源竞争。
  • 不可逆操作provision:upgrade会升级系统全部软件包(900 秒超时),provision:firewall会立即启用 UFW;执行前请评估对现有环境的影响。

六、小结

Provision Recipe 是 Deployer 体系中"从零到可部署"的自动化方案:provision一个命令即可串起系统检查、软件源、系统升级、基础软件、SSH 加固、防火墙、部署用户、PHP 与 FPM、Node.js、数据库、Caddy 站点直至最终验证的完整链路,并且通过与current符号链接机制和resolve_root_symlink的配合,与 Deployer 常规部署流程无缝衔接。理解 recipe/provision.php 及其 5 个子模块的实现,既能让你安全地把它用在新服务器初始化上,也能为你按需裁剪或扩展属于自己的 provisioning 流程提供参考。

【免费下载链接】deployerThe PHP deployment tool with support for popular frameworks out of the box项目地址: https://gitcode.com/gh_mirrors/de/deployer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 19:27:54

用Playground脚本快速搭建Hadoop三节点完全分布式集群

先把话说在前面&#xff1a;干大数据这行&#xff0c;自己手动搭过一套Hadoop集群的人&#xff0c;十有八九都被配置文件和进程日志折磨过。网上那些动辄几十步的教程&#xff0c;你照着敲到凌晨两点&#xff0c;最后发现是hosts没配对&#xff0c;真的很泄气。所以当我第一次用…

作者头像 李华
网站建设 2026/9/24 19:27:08

api-ms-win-core-profile-l1-1-0.dll缺失修复:Windows系统DLL报错完整指南

平时帮朋友修电脑&#xff0c;最常遇到的一类弹窗错误就是“无法启动此程序&#xff0c;因为计算机中丢失 api-ms-win-core-profile-l1-1-0.dll”&#xff0c;或者是“找不到 api-ms-win-core-profile-l1-1-0.dll&#xff0c;无法继续执行代码”。很多人第一次看到这个提示都会…

作者头像 李华
网站建设 2026/9/24 19:27:08

Netty粘包拆包源码解析:ByteToMessageDecoder与LengthFieldBasedFrameDecoder深度剖析

Netty源码分析写了好几篇了&#xff0c;后台不断有朋友催更“认真系列”第二篇。上一篇我们把 Netty 的整体脉络、NioEventLoop 线程模型和启动流程啃了一遍&#xff0c;这次我想换个角度&#xff0c;挑一个实际工作中几乎每天都会碰到、面试也高频被问的方向来拆——就是粘包拆…

作者头像 李华
网站建设 2026/9/24 19:27:07

时间序列预测Python实战:三个经典数据集跑通ARIMA与SARIMA

简介&#xff1a;面向 Python 时间序列预测初学者与分析人员&#xff0c;这份代码资源覆盖金融、气象、销售等常见时序场景&#xff0c;系统演示 Pandas 预处理、ARIMA/SARIMA 建模、状态空间方法、Prophet 以及机器学习模型的应用。压缩包共 214 个文件&#xff0c;含 181 个可…

作者头像 李华
网站建设 2026/9/24 19:26:56

手机卡顿真相:存储空间与运行内存的区别与清理指南

1. 为什么“清理手机”成了当代人的日常仪式&#xff1f;你有没有过这种体验&#xff1a;刚换的新机用半年&#xff0c;微信一开就转圈&#xff0c;拍照要等三秒才出预览&#xff0c;刷短视频卡成PPT&#xff0c;连扫码付款都要多扫两次&#xff1f;不是手机老了&#xff0c;是…

作者头像 李华