WP笔记

WooCommerce 创建自定义邮件(2022)

除了更改WooCommerce邮件的样子外,还可以创建自定义的邮件。发件人、发送内容都可以定制。比如每个产品有不同的供货商,客户下单后要给每个供货商发送邮件,或者要发一些产品你使用说明给客户等等。

创建插件

wp-content/plugins目录下创建文件夹my-custom-wc-email来保存代码,插件的目录结构如下:

└─my-custom-wc-email
    │  class-wc-custom-email.php
    │  plugin.php
    │
    └─emails
            my-custom-email-plain.php
            my-custom-email.php

每个文件的用途如下:

  • plugin.php: 插件主文件,包含插件的header信息,以及核心功能:注册自定义邮件
  • class-wc-custom-email.php:实现自定义邮件类
  • emails/***.php:邮件的html模板和plain text模板

注册自定义邮件

plugin.php的内容如下:

<?php
/**
 * Plugin Name:       Custom WooCommerce Email Example
 * Plugin URI:        https://www.solagirl.net
 * Description:       Custom WooCommerce Email Example
 * Version:           1.0.0
 * Author:            Sola
 * Author URI:        https://www.solagirl.net
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Update URI:        https://example.com/my-plugin/
 * Text Domain:       cwe
 * Domain Path:       /languages
 */

// Exit if accessed directly
if (!defined('ABSPATH')) exit;

function sola_add_custom_woocommerce_email( $email_classes ) {

    // include our custom email class
    include( 'class-wc-custom-email.php' );

    // add the email class to the list of email classes that WooCommerce loads
    $email_classes['Sola_WC_Custom_Email'] = new Sola_WC_Custom_Email();

    return $email_classes;

}
add_filter( 'woocommerce_email_classes', 'sola_add_custom_woocommerce_email' );
  1. 定义插件header信息
  2. 通过filter: woocommerce_email_classes向WooCommerce注册一个新的邮件,具体实现email的class需要通过include文件的方式引入。因为该class要以WC_Email class为模板来创建,如果不在这里include,可能会遇到WC_Email未定义的错误。

以WC_Email为模板创建自定义邮件的class

class-wc-custom-email.php的内容如下:

<?php
// Exit if accessed directly
if (!defined('ABSPATH')) exit;

if (!class_exists('Sola_WC_Custom_Email')) :

  class Sola_WC_Custom_Email extends WC_Email {
    /**
     * Constructor.
     */
    public function __construct() {
      $this->id             = 'my_custom_email';
      $this->title          = __('My Custom Email', 'woocommerce');
      $this->description    = __('This is an example of WooCommerce Custom Email', 'woocommerce');

      // 将email templates存储在插件的目录下
      $this->template_base  = __DIR__ . '/emails/';
      $this->template_html  = 'my-custom-email.php';
      $this->template_plain = 'my-custom-email-plain.php';

      $this->placeholders   = array(
        '{order_date}'   => '',
        '{order_number}' => '',
      );

      // Triggers for this email.
      add_action('woocommerce_order_status_pending_to_processing_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_pending_to_completed_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_pending_to_on-hold_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_failed_to_processing_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_failed_to_completed_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_failed_to_on-hold_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_cancelled_to_processing_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_cancelled_to_completed_notification', array($this, 'trigger'), 10, 2);
      add_action('woocommerce_order_status_cancelled_to_on-hold_notification', array($this, 'trigger'), 10, 2);

      // Call parent constructor.
      parent::__construct();

      // Other settings.
      $this->recipient = $this->get_option('recipient', get_option('admin_email'));
    }

    /**
     * Get email subject.
     *
     * @since  3.1.0
     * @return string
     */
    public function get_default_subject() {
      return __('[{site_title}]: My Custom Email #{order_number}', 'woocommerce');
    }

    /**
     * Get email heading.
     *
     * @since  3.1.0
     * @return string
     */
    public function get_default_heading() {
      return __('My Custom Email: #{order_number}', 'woocommerce');
    }

    /**
     * Trigger the sending of this email.
     *
     * @param int            $order_id The order ID.
     * @param WC_Order|false $order Order object.
     */
    public function trigger($order_id, $order = false) {
      $this->setup_locale();

      if ($order_id && !is_a($order, 'WC_Order')) {
        $order = wc_get_order($order_id);
      }

      if (is_a($order, 'WC_Order')) {
        $this->object                         = $order;
        $this->placeholders['{order_date}']   = wc_format_datetime($this->object->get_date_created());
        $this->placeholders['{order_number}'] = $this->object->get_order_number();

        $email_already_sent = $order->get_meta('_new_order_email_sent');
      }

      /**
       * Controls if new order emails can be resend multiple times.
       *
       * @since 5.0.0
       * @param bool $allows Defaults to false.
       */
      if ('true' === $email_already_sent && !apply_filters('woocommerce_new_order_email_allows_resend', false)) {
        return;
      }

      if ($this->is_enabled() && $this->get_recipient()) {

        // 发送email
        $this->send($this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments());

        $order->update_meta_data('_new_order_email_sent', 'true');
        $order->save();
      }

      $this->restore_locale();
    }

    /**
     * Get content html.
     *
     * @return string
     */
    public function get_content_html() {
      return wc_get_template_html(
        $this->template_html,
        array(
          'order'              => $this->object,
          'email_heading'      => $this->get_heading(),
          'additional_content' => $this->get_additional_content(),
          'sent_to_admin'      => true,
          'plain_text'         => false,
          'email'              => $this,
        ),
        $this->template_base,
        $this->template_base
      );
    }

    /**
     * Get content plain.
     *
     * @return string
     */
    public function get_content_plain() {
      return wc_get_template_html(
        $this->template_plain,
        array(
          'order'              => $this->object,
          'email_heading'      => $this->get_heading(),
          'additional_content' => $this->get_additional_content(),
          'sent_to_admin'      => true,
          'plain_text'         => true,
          'email'              => $this,
        ),
        $this->template_base,
        $this->template_base
      );
    }

    /**
     * Default content to show below main email content.
     *
     * @since 3.7.0
     * @return string
     */
    public function get_default_additional_content() {
      return __('Congratulations on the sale.', 'woocommerce');
    }

    /**
     * Initialise settings form fields.
     */
    public function init_form_fields() {
      /* translators: %s: list of placeholders */
      $placeholder_text  = sprintf(__('Available placeholders: %s', 'woocommerce'), '<code>' . implode('</code>, <code>', array_keys($this->placeholders)) . '</code>');
      $this->form_fields = array(
        'enabled'            => array(
          'title'   => __('Enable/Disable', 'woocommerce'),
          'type'    => 'checkbox',
          'label'   => __('Enable this email notification', 'woocommerce'),
          'default' => 'yes',
        ),
        'recipient'          => array(
          'title'       => __('Recipient(s)', 'woocommerce'),
          'type'        => 'text',
          /* translators: %s: WP admin email */
          'description' => sprintf(__('Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce'), '<code>' . esc_attr(get_option('admin_email')) . '</code>'),
          'placeholder' => '',
          'default'     => '',
          'desc_tip'    => true,
        ),
        'subject'            => array(
          'title'       => __('Subject', 'woocommerce'),
          'type'        => 'text',
          'desc_tip'    => true,
          'description' => $placeholder_text,
          'placeholder' => $this->get_default_subject(),
          'default'     => '',
        ),
        'heading'            => array(
          'title'       => __('Email heading', 'woocommerce'),
          'type'        => 'text',
          'desc_tip'    => true,
          'description' => $placeholder_text,
          'placeholder' => $this->get_default_heading(),
          'default'     => '',
        ),
        'additional_content' => array(
          'title'       => __('Additional content', 'woocommerce'),
          'description' => __('Text to appear below the main email content.', 'woocommerce') . ' ' . $placeholder_text,
          'css'         => 'width:400px; height: 75px;',
          'placeholder' => __('N/A', 'woocommerce'),
          'type'        => 'textarea',
          'default'     => $this->get_default_additional_content(),
          'desc_tip'    => true,
        ),
        'email_type'         => array(
          'title'       => __('Email type', 'woocommerce'),
          'type'        => 'select',
          'description' => __('Choose which format of email to send.', 'woocommerce'),
          'default'     => 'html',
          'class'       => 'email_type wc-enhanced-select',
          'options'     => $this->get_email_type_options(),
          'desc_tip'    => true,
        ),
      );
    }
  }

endif;
  1. __construct()方法中定义邮件的ID、title、description等等。要想将email模板保存在自定义目录下,还要定义template_base,就是代码中的$this->template_base  = __DIR__ . '/emails/'
  2. $this->recipient必须是有效的邮件地址,或者逗号分隔的邮件列表。
  3. trigger()函数负责判断是否发送邮件和实际发送邮件,该函数被触发的条件定义在__construct()函数中,例如add_action('woocommerce_order_status_pending_to_processing_notification', array($this, 'trigger'), 10, 2);,当订单状态从pending变成processing时看情况发送邮件。
  4. get_content_html()get_content_plain()负责获取邮件模板内容,因为模板的位置被修改了,所以这里也要告知模板位置,方法是向wc_get_template_html()函数传入额外参数。

创建邮件模板

邮件模板可以参考现有的邮件,这些文件位于woocommerce/templates/emails下,写一个简单的模板来测试,以下是my-custom-email.php的内容

<?php
// Exit if accessed directly
if (!defined('ABSPATH')) exit;

/*
 * @hooked WC_Emails::email_header() Output the email header
 */
do_action( 'woocommerce_email_header', $email_heading, $email ); ?>

<h2>这是一封自定义邮件</h2>

<?php
/*
 * @hooked WC_Emails::email_footer() Output the email footer
 */
do_action( 'woocommerce_email_footer', $email );

测试效果

首先,可以在WooCommerce > Settings >Emails下的邮件列表里找到”My Custom Email“,打开编辑界面,如下图所示,可以看到模板位置是位于插件下面的。

solablog:WooCommerce自定义邮件的设置界面

WooCommerce付款测试插件下一个订单,并安装邮件log插件,例如Mail logging – WP Mail Catcher,查看下单后的邮件,可以找到我们的自定义邮件。

自定义邮件

使邮件模板易于编辑

虽然可以通过邮件模板来编辑邮件,但还有更好的方式,比如所见即所得的page builder——yaymail。由于内容较长,感兴趣请移步Yaymail WooCommerce邮件模板生成器,支持自定义模版

28条评论

  1. 发送自定义邮件的按钮是否可以直接提到 WC 的「订单」这个页面来呢?那样会方便很多,不用每次点进每个订单详情中去。也就是在「订单」页面增加一个自定义的栏目?这样也可以把一些自定义字段直接显示,会不会更方便些?

    1. 发送邮件的按钮应该说原本就在订单页面里,你看看order actions里,不是有所有邮件的发送选项吗,如下图所示

      1. 谢谢…我的意思是,它的上一层级的那个订单列表的页面。因为左侧菜单中那个页面叫「订单」页面,你截图的这个叫订单详情页…那个列表是否能增加一个栏目,显示自定义字段呢,以及增加一个栏目,可以直接发邮件的…

        1. 好吧,「订单」这个页面就是订单列表页,我记住了。

          你说的当然可以,custom post type列表页显示自定义字段,谷歌搜,文章很多。

          直接发送邮件,把woocommerce订单详情页面发送邮件的代码读懂,移植到订单列表页面就行了,用ajax+php实现。你可以搜索下有没有相关插件。

  2. 你好,我前几个小时的那个评论,是问「能否在用户购买后发送一封邮件,此邮件包含两个变量,手动输入」。

    后来我想到,用户订单详情中有一个「订单 提示」(Order Notes)模块,能否将这个模块复制一下,修改生成另一个「发送账号信息」的模块。每次在此输入不同的账号,发送出去后,别的内容是固定的,比如包含「如何使用此账号」的说明链接等?

    因为你的模板发送出去的数据,都是固定的,只是自己修改邮件主题什么的。

    或者,通过添加「自定义项目」(Custom Fields)的方式发送?
    比如 Custom Fields「账号」「密码」,然后执行一个「自定义模板」的动作,发送新邮件呢?如何在模板中只显示这两个元数据,订单详情不显示呢?

    1. 你说的是WooCommerce吗?如果是的话,你可以创建一封新的邮件模板,包含你要在QQ企业邮箱HTML模式下编辑的信息,在客户结账后发送给你自己,然后手动输入对方的账号和密码,转发给对方。

      或者这封邮件不自动发送给你,而是在订单产生后,到订单详情里填写账号密码,再用order actions发送这个邮件,当然也要你手动操作,除非你能让填写账号密码这步自动化。

      给WooCommerce创建新邮件不难

      1. 谢谢!第一种方法很机智。

        已在「订单」类型中添加 4 个自定义字段,能在订单管理页呈现一个填写字段值的模块。希望在填写后执行一个动作发送信息邮件。
        1、现在不知如何在邮件中 PHP 呈现当前 POST 的指定字段值;
        2、选用了 processing 模板,导致一下单就触发了邮件,如何修改触发机制呢?
        3、准备在「新订单」邮件中加一个指向当前订单管理页的地址,发现是 …/wp-admin/post.php?post=1991&action=edit
        动态修改的话是这样吗:
        ……/wp-admin/post.php?post=get_order_number(); ?>&action=edit

        1. 第一点,如果是自定义字段的话,保存订单后直接从数据库读取就行了,应该跟POST没什么关系。
          二三点我没有试过,我想控制触发条件的代码就在email class的construct里

          // Triggers for this email
          		add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ) );
          		add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ) );
          

          模板里$order变量记录了订单的信息,通过这个变量可以获取订单号。

        2. 1、删除了两行 add_action,还是会触发发送。如果一同删除下方的 parent::__construct(); 就不会自动触发了,但是执行动作也不能发送邮件了。

          2、PHP 如何将字段值显示出来呢?搜了好久都没找到。

        3. var_dump和get_post_meta没什么关系,而且var_dump仅在调试时使用,前者是php基本函数,后者是WordPress基于php定义的函数。

          很抱歉这样说,但如果你弄不清这两个东西,改动代码是非常困难的,建议先学习php的基础知识。

          你说的border是1的问题没那么简单,邮件大部分样式都不是在模板里定义的,而是通过css和一个将css转换为内联样式的php库来写的。改动这个需要你懂php和css。

        4. 哈哈,其实你说的我都懂。get_post_meta 很明显 post 就是 WordPress 定义的概念,本身不属于 php 语法中的函数。CSS 我懂。只是改了这么久有点累了,还得找找在哪里- –

    2. 去读一下parent::__construct()的代码可能就明白了,这个class不是extend的一个class吗
      php显示字段值种类太多了,如果你是说custom field,可以用get_post_meta()

      1. 如果要打印在屏幕上,是不是这样:

        username 是字段名。

        后来我用了 invoice 收据的模板,是必须手动发送的。只是它有支付和未支付,感觉有点累赘,但不太敢乱改。

        1. 你确定删除trigger代码不管用吗?parent constructor里并没有发送邮件的代码,我本地用admin-new-order模板试了下,删除trigger就不会发送了。

        2. 我也觉得,至少理论上是不会的。可能当时由于缓存之类的问题导致的吧,我只试了一次。

          刚才我发送了一个 php 代码,但是没有显示出来,可能作为代码执行了,而你这里没有那个字段,因此是空的。
          我在前后加了几个星号,看看这回能否显示~

        3. 用我那条成功了~ var_dump 和 get_post_meta 是一样的效果?

          然后就是发现我模板做出来的表格,右侧和下放有较深的细边线,但是其他的邮件是正常的,灰色的粗线。border 没动过,值是默认的”1″,我改成 solid 之后还是一样的。

    3. 确实,给woocommerce加新邮件很简单,难度全在定制化那个模板上,邮件模板本来就很罗嗦,如果再需要显示与默认数据不同的东西更麻烦。
      其实这篇文章记录的东西正是我给一个客户写的插件,但我只写了最简单的部分。如果要写后面咋改的模板,估计我就要跟你一样的心情了。
      要自己改的话恐怕必须把WooCommerce模板原理搞明白。

      1. 恩,辛苦。
        我已经搞定了,主要是表格中的字段值是动态的,别的部分都是固定的。
        现在就差那个框线了,虽然可以忽略,但是看着怪怪的。我去找找相关的 CSS 在哪里好了。
        疑点是表格的样式竟然没动也会产生差异。不知道是不是我删了 tbody 和 tfoot 标记的缘故。

        1. 关于修改woocommerce样式,可以看这篇文章https://www.solagirl.net/woocommerce-2-3-email-customization.html

      2. 还有一个问题就是,要保护商店的经营数据,避免被回头客知晓他回访期间我们的销量,需要将订单号变一种样式。比如在前面加上 年份+月份。看起来就像是这个月的第几单,而不是从一开始的第几单。或者其他的算法,来保护这个数值不被联想。

        1. 这个没改过,不过order就是custom post type,搜下custom post type permalink之类的应该有解决方案。

      3. 因为发的货是帐号,有使用期限的。有一个字段就是时间,yymmdd。

        在这一天需要发送一封邮件,说到期了。

        那么再新建一个邮件模板,能否设置一个 trigger,在那天的 15:00 触发,自动发送提醒邮件呢?

        如果不行,记得有个定时任务的插件叫 WP-Cron,如果要在那时候执行任务,「让 WC 发送某封邮件」的函数是什么?

        1. 每个class里的trigger()函数就是发送邮件的,至于定时发送,自己写吧。
          如果不需要显示订单详情之类的,不需要用WooCommerce的邮件写法。

  3. sola, 你现在仍然用的是 Hostgator的主机吗? 最近有看到一个帖子,说HG支持成人/赌博站,影响SEO之类的。你觉得有这回事吗?