同步 WooCommerce 会员与 Discourse 群组

此插件允许您将 WooCommerce 会员资格与 Discourse 群组同步。

先决条件

要使其正常工作,您必须启用 DiscourseConnect,并将 WordPress 或 Discourse 设置为 DiscourseConnect 提供商。

步骤

  1. 安装此 WordPress 插件:https://github.com/paviliondev/discourse-woocommerce。

  2. 使用 WordPress 主题编辑器更新以下数字,将其更改为您想要同步的 WooCommerce plan_id、Discourse group_id 和 group_name:

    $member_group_map[] = (object) array('plan_id' => 51, 'group_id' => 43, 'group_name' => 'group1');
    $member_group_map[] = (object) array('plan_id' => 62, 'group_id' => 44, 'group_name' => 'group2');
    

    您可以添加任意数量的条目(或移除现有条目)。

可选步骤

  1. 安装 WP Crontrol – WordPress 插件 | WordPress.org 并安排 run_full_wc_membership_sync 每 24 小时运行一次。这将执行完整同步以确保一致性。

技术说明

捕获 WooCommerce 中会员资格创建和状态变更的唯一可靠方法是使用 wc_memberships_user_membership_saved 管理钩子。您可以将其与 wc_memberships_user_membership_status_changed 钩子结合使用,但通常没有必要。

此代码使用 WooCommerce 日志记录器将信息记录到 /wp-content/uploads/wc-logs/{log_file}。如果无法正常工作,请检查该位置的输出。您将看到如下所示的日志,展示逻辑的各个阶段:

2019-05-23T07:01:57+00:00 INFO Running handle_wc_membership_saved 1, 92, 1
2019-05-23T07:01:57+00:00 INFO angus@email.com membership of VIP Membership changed to wcm-active
2019-05-23T07:01:57+00:00 INFO Sending PUT request to http://localhost:3000/groups/41/members with angus@email.com
2019-05-23T07:01:57+00:00 INFO Response from Discourse: 200 OK

使用说明

:point_right: 如果您在 WordPress 管理面板中点击“删除用户会员资格”,同步将不会生效。WooCommerce 中没有在此操作被点击时触发的钩子。请改为更改会员资格的状态。

19 个赞

Thanks, @angus. Funny that it seems we were both doing this at the same time.

You managed to figure out what was in those $args. I couldn’t find the user_id in it, I think.

1 个赞

It looks correct to me. Since the site is using WordPress as the SSO provider, you could use the static add_user_to_discourse_group and remove_user_from_discourse_group functions to handle changing the group status on Discourse. That would let you do something like:

	if ( in_array( $status, ACTIVE_STATUSES ) ) {
		DiscourseUtilities::add_user_to_discourse_group( $user_id, 'your_discourse_group_name' );
	} else {
		DiscourseUtilities::remove_user_from_discourse_group( $user_id, 'your_discourse_group_name' );
	}

Both of those functions take the user’s WordPress ID as the first argument, and a comma separated list of group names (with no spaces between the names) as the second argument. Those functions will take care of all the API credentials for you, so using them would simplify the code a bit. There are more details about those functions here: Manage group membership in Discourse with WP Discourse SSO.

4 个赞

In this case Discourse is the SSO Provider :slight_smile:

2 个赞

Just an update here.

The updated version of this integration has the following structure:

  1. Sync on every WooCommerce Membership status change

  2. Batch sync every relevant WooCommerce Member every 24 hours to ensure consistency.

  3. In terms of matching wordpress users with discourse users, the code first checks if the wordpress user has a discourse account associated with it. If so, it uses the discourse user id. If not it attempts the sync using the wordpress user’s email. The sync can fail if

    3.1 the user has never associated their wordpress and discourse accounts; and
    3.2 they have used different emails on wordpress and discourse.

We’re successfully running this in production on a client’s site. So far we’ve had only 2 users who have failed to sync, i.e. where both 3.1 and 3.2 were true.

Implementation

The updated implementation is

  1. Functions.php methods (update MEMBERSHIP_PLAN_ID and DISCOURSE_GROUP_ID)
Functions.php

use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_ID = 61128;
const DISCOURSE_GROUP_ID = 62;
const ACTIVE_STATUSES = array(‘wcm-active’);

function update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status) {
$options = DiscourseUtilities::get_options();
$base_url = $options[‘url’];
$api_key = $options[‘api-key’];
$api_username = $options[‘publish-username’];

if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
  return new \WP_Error( 'discourse_configuration_error', 'The WP Discourse plugin has not been properly configured.' );
}

$discourse_user_id = get_user_meta($user_id, 'discourse_sso_user_id', true);
$user_info = get_userdata($user_id);
$user_email = $user_info->user_email;
$logger = wc_get_logger();

$logger->info( sprintf('%s membership of %s is %s' , $user_email, $membership_plan_name, $status ) );

if (in_array($status, ACTIVE_STATUSES)) {
	$action = 'PUT';
} else {
	$action = 'DELETE';
}

$external_url = esc_url_raw( $base_url . "/groups/". DISCOURSE_GROUP_ID ."/members" );

$args = array(
‘api_key’ => $api_key,
‘api_username’ => $api_username
);

if ($discourse_user_id) {
$args[‘user_id’] = $discourse_user_id;
} else {
$args[‘user_emails’] = $user_email;
}

$logger->info( sprintf(‘Sending %s request to %s with %s’, $action, $external_url, http_build_query($args)) );

$external_url = add_query_arg($args, $external_url);

$response = wp_remote_request($external_url,
 array(
	 'method' => $action
 )
);

$logger->info( sprintf( 'Response from Discourse: %s %s' ,
    wp_remote_retrieve_response_code($response),
    wp_remote_retrieve_response_message($response) ) );

if ( ! DiscourseUtilities::validate( $response ) ) {

	return new \WP_Error( 'discourse_response_error', 'There has been an error in retrieving the user data from Discourse.' );
}

};

function handle_wc_membership_saved($membership_plan, $args) {
$logger = wc_get_logger();

$logger->info( sprintf('Running handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

$user_id = $args['user_id'];
$membership = wc_memberships_get_user_membership($args['user_membership_id']);
$membership_plan_id = $membership->plan->id;

if ($membership && $membership_plan_id == MEMBERSHIP_PLAN_ID) {
	$membership_plan_name = $membership_plan->name;
	$status = $membership->status;
	update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);
}

};

add_action(‘wc_memberships_user_membership_saved’, ‘handle_wc_membership_saved’, 10, 2);

function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Running full_wc_membership_sync') );

foreach ( $allusers as $user ) {
	$user_id = $user->id;
	$membership = wc_memberships_get_user_membership($user_id, MEMBERSHIP_PLAN_ID);
	$membership_plan_id = $membership->plan->id;

	$logger->info( sprintf('Checking membership of %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Updating group access of %s', $user->user_login) );

		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

		$logger->info( sprintf('Sleeping for 5 seconds') );
		sleep(5);
	}
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

  1. Install WP Crontrol – WordPress plugin | WordPress.org and schedule “run_full_wc_membership_sync” to run every 24 hours.

You can review the logs of the sync in WooCommerce > Status > Logs.

@DNSTARS @pfaffman

8 个赞

What a baller, sending some magical internet money your way in thanks. :+1:

6 个赞

Much appreciated! Thank you @DNSTARS

1 个赞

在我的情况下应该做哪些修改?我的情况略有不同,以下是差异点:

  1. 我使用 WordPress 作为 SSO 客户端,用户通过 Discourse 登录。
  2. 我安装了 WordPress 插件“WooCommerce Groups”:https://docs.woocommerce.com/documentation/plugins/woocommerce/woocommerce-extensions/groups-woocommerce/。它与 WooCommerce Memberships 类似,但功能较少。我认为核心功能是相同的。
  3. 我在 Discourse 中有多个群组(超过 3 个),因此在 WordPress 中对应设置了 3 个访问群组。

您能为我提供这段代码的修正建议或一些指导吗?谢谢!
也许您能帮忙?@pfaffman @simon

啊,我在那里没有写清楚问题……
所以我需要激活 WordPress 和 Discourse 之间的这项集成。

目前正常工作的部分:

  1. WordPress 作为 SSO 客户端运行正常。
  2. 会员资格通过 WP Groups 插件进行销售,到期后会将用户添加到相应的群组。
  3. 会员资格到期后,用户会从群组中移除。

我需要实现:

  • 当用户在 WordPress 中被添加到某个群组时,将该用户添加到 Discourse 中的特定群组
  • 当用户在 WordPress 中从该群组被移除时,在 Discourse 中也将其移除

WordPress 和 Discourse 中均存在多个群组

我注意到 update_discourse_group_access 函数将 API 认证凭据添加到了 URL 中。这在任何较新版本的 Discourse 中都无法工作。您是否有使用基于头部(header)认证的更新版代码?如果没有,我可以编辑代码,但我无法测试我的修改,因此存在一定的风险。

我拥有干净的 Discourse 和 WordPress 站点,可以无风险地测试您的代码。

2 个赞

这是 Angus 的代码,已更新为使用基于头的身份验证。我没有对其做任何其他更改,也尚未测试该代码。在有人有机会测试之前,我不会更新原帖。\n\nphp\nuse WPDiscourse\\Utilities\\Utilities as DiscourseUtilities;\n\nconst MEMBERSHIP_PLAN_ID = 35;\nconst DISCOURSE_GROUP_ID = 41;\nconst ACTIVE_STATUSES = array( 'wcm-active' );\n\nfunction update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status ) {\n\t$options = DiscourseUtilities::get_options();\n\t$base_url = $options['url'];\n\t$api_key = $options['api-key'];\n\t$api_username = $options['publish-username'];\n\n\tif ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {\n\t\treturn new \\WP_Error( 'discourse_configuration_error', 'WP Discourse 插件未正确配置。' );\n\t}\n\n\t$user_info = get_userdata( $user_id );\n\t$user_email = $user_info->user_email;\n\t$logger = wc_get_logger();\n\n\t$logger->info( sprintf( '%s 的 %s 会员资格已更改为 %s', $user_email, $membership_plan_name, $status ) );\n\n\tif ( in_array( $status, ACTIVE_STATUSES ) ) {\n\t\t$action = 'PUT';\n\t} else {\n\t\t$action = 'DELETE';\n\t}\n\n\t$external_url = esc_url_raw( $base_url . "/groups/" . DISCOURSE_GROUP_ID . "/members" );\n\n\t$logger->info( sprintf( '正在向 %s 发送 %s 请求,用户邮箱为 %s', $action, $external_url, $user_email ) );\n\n\t$response = wp_remote_request( $external_url,\n\t\tarray(\n\t\t\t'method' => $action,\n\t\t\t'headers' => array(\n\t\t\t\t'Api-Key' => sanitize_key( $api_key ),\n\t\t\t\t'Api-Username' => sanitize_text_field( $api_username ),\n\t\t\t),\n\t\t\t'body' => array( 'user_emails' => $user_email ),\n\t\t)\n\t);\n\n\t$logger->info( sprintf( 'Discourse 响应:%s %s',\n\t\twp_remote_retrieve_response_code( $response ),\n\t\twp_remote_retrieve_response_message( $response ) ) );\n\n\tif ( ! DiscourseUtilities::validate( $response ) ) {\n\n\t\treturn new \\WP_Error( 'discourse_response_error', '从 Discourse 获取用户数据时发生错误。' );\n\t}\n}\n\nfunction handle_wc_membership_saved( $membership_plan, $args ) {\n\t$logger = wc_get_logger();\n\n\t$logger->info( sprintf( '正在执行 handle_wc_membership_saved,用户 ID: %s,会员 ID: %s,是否为更新: %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );\n\n\t$user_id = $args['user_id'];\n\t$membership = wc_memberships_get_user_membership( $args['user_membership_id'] );\n\t$membership_plan_id = $membership->plan->id;\n\n\tif ( $membership && $membership_plan_id == MEMBERSHIP_PLAN_ID ) {\n\t\t$membership_plan_name = $membership_plan->name;\n\t\t$status = $membership->status;\n\t\tupdate_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status );\n\t}\n}\nadd_action( 'wc_memberships_user_membership_saved', 'handle_wc_membership_saved', 10, 2 );\n\n\n代码中设置了 $discourse_user_id 变量,但该变量并未在任何地方使用。可能可以从代码中将其移除。\n\n[quote=“Ed_Bobkov, post:8, topic:118485”]\n我安装了一个 WP 插件“WooCommerce Groups”:Groups WooCommerce Archives - WooCommerce Docs 。它与 WooCommerce Memberships 类似,但功能较少。我认为所有核心功能都是相同的。\n[/quote]\n\n我对此不太确定。该代码挂钩到 wc_memberships_user_membership_saved 动作钩子。看起来该钩子很可能由 WooCommerce Memberships 插件添加。WooCommerce Groups 插件中可能也有类似的钩子,但不太可能具有相同的名称和参数。

3 个赞

‘Groups for Woocommerce’ 插件使用 ‘WP Group’ 插件来运行群组功能,其 API 如下:Actions | Itthinx Documentation

至于 Groups WooCommerce

动作

该扩展当前不会触发任何特定动作。

为了简化,我切换到了 WooCommerce Memberships,并使用了原始代码加上 @simon 所做的修改。

我将 ID 更改为我的数据:

const MEMBERSHIP_PLAN_ID = 347;
const DISCOURSE_GROUP_ID = 42;

目前我无法同步,但在日志中获取到了一些信息:

> 2020-05-14T13:12:34+00:00 INFO test@gmail.com 的会员状态从 PAID1 变更为 wcm-active
> 2020-05-14T13:12:34+00:00 INFO 正在向 https://site/groups/42/members 发送 PUT 请求,用户为 test@gmail.com
> 2020-05-14T13:12:34+00:00 INFO Discourse 响应:400 错误请求

Discourse 组的 ID 我在此处找到:
site/groups/PAID1.json

> {"group":{"id":42,"automatic":false,"name":"PAID1","user_count":0,"mentionable_level":0,"messageable_level":0,"visibility_level":0,"automatic_membership_email_domains":"","primary_group":false,"title":"","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"bio_excerpt":null,"public_admission":false,"public_exit":false,"allow_membership_requests":false,"full_name":"France-PAID1","default_notification_level":3,"membership_request_template":"","is_group_user":false,"is_group_owner":true,"members_visibility_level":0,"can_see_members":true,"publish_read_state":false,"is_group_owner_display":false,"mentionable":true,"messageable":false},"extras":{"visible_group_names":["admins","moderators","PAID1","PAID2","PAID3","staff","trust_level_0","trust_level_1","trust_level_2","trust_level_3","trust_level_4"]}}
2 个赞

所以有两个问题待解决:

  1. 如何无错误地同步?
  2. 如果我在 WordPress 中有超过 3 种会员类型,而在 Discourse 中有对应的群组怎么办?这段代码仅适用于一种类型。

这看起来是我对代码更新中引入的一个 bug。我稍晚些时候会查看一下。

2 个赞

我已在此更新了代码示例:https://meta.discourse.org/t/syncing-woocommerce-memberships-with-discourse-groups-when-discourse-is-the-sso-provider/118485/11。昨天我发布的代码中存在编码错误。我已测试代码中的 API 调用可以正常工作,但尚未用 WooCommerce Memberships 插件测试该代码。

这应该是可以实现的。或许有人能在这里帮助你,但你也可以考虑雇佣专业人士来完成这项工作。你可以尝试在我们的 Marketplace 分类中创建一个主题,以寻找能够完成这项工作的开发者。

2 个赞

@simon,搞定了!谢谢大家!:vulcan_salute:

1 个赞

@simon,在你的代码中,我没有找到 @angus 所使用的那段用于批量同步的代码。

/*run_full_wc_membership_sync*/
function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Running full_wc_membership_sync') );

foreach ( $allusers as $user ) {
	$user_id = $user->id;
	$membership = wc_memberships_get_user_membership($user_id, MEMBERSHIP_PLAN_ID);
	$membership_plan_id = $membership->plan->id;

	$logger->info( sprintf('Checking membership of %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Updating group access of %s', $user->user_login) );

		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

		$logger->info( sprintf('Sleeping for 5 seconds') );
		sleep(5);
	}
}

}

add_action('run_full_wc_membership_sync', 'full_wc_membership_sync');  

如果我想启用每日同步,是否应该将这段代码添加到你代码的末尾?

当我尝试在 WordPress Crontrol 插件中通过钩子名称添加此定时任务时:

run_full_wc_membership_sync

我收到了一个错误:

这可能是什么原因导致的?

已解决。或许能帮到某人。我需要在“下次运行”字段中选择“在”,并填入类似 2020-05-10 10:00:00 的日期。

测试了同步功能——现在可以正常工作了。

如果您在 Discourse 中拥有多个 WooCommerce 会员计划和群组,以下是可行的解决方案。您需要将会员 ID 和 Discourse 群组 ID 替换为您自己的 ID。

  1. WooCommerce 会员计划 ID 可在 WordPress 中编辑计划时的网址中找到:https://site/wp-admin/post.php?post=**441**&action=edit
  2. Discourse 群组 ID 可在此处找到:https://site/groups/group_name.json

可包含在 functions.php 中的可用代码:

摘要

//wp+discourse
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_DISCOURSE_GROUP = [
“347” => “42”,
“357” => “43”,
“441” => “44”
];

//const ACTIVE_STATUSES = array( ‘wcm-active’ );
const ACTIVE_STATUSES = array( ‘wcm-active’, ‘wcm-free_trial’ );

function update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status ) {
$options = DiscourseUtilities::get_options();
$base_url = $options[‘url’];
$api_key = $options[‘api-key’];
$api_username = $options[‘publish-username’];

if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
	return new \WP_Error( 'discourse_configuration_error', 'WP Discourse 插件尚未正确配置。' );
}

$user_info         = get_userdata( $user_id );
$user_email        = $user_info->user_email;
$logger            = wc_get_logger();

$logger->info( sprintf( '%s 的 %s 会员状态已更改为 %s', $user_email, $membership_plan_name, $status ) );

if ( in_array( $status, ACTIVE_STATUSES ) ) {
	$action = 'PUT';
} else {
	$action = 'DELETE';
}

$external_url = esc_url_raw( $base_url . "/groups/" . MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id] . "/members" );

$logger->info( sprintf( '正在向 %s 发送 %s 请求,用户邮箱为 %s', $action, $external_url, $user_email ) );

$response = wp_remote_request( $external_url,
	array(
		'method'  => $action,
		'headers' => array(
			'Api-Key'      => sanitize_key( $api_key ),
			'Api-Username' => sanitize_text_field( $api_username ),
		),
		'body'    => array( 'user_emails' => $user_email ),
	)
);

$logger->info( sprintf( '来自 Discourse 的响应:%s %s',
	wp_remote_retrieve_response_code( $response ),
	wp_remote_retrieve_response_message( $response ) ) );

if ( ! DiscourseUtilities::validate( $response ) ) {

	return new \WP_Error( 'discourse_response_error', '从 Discourse 获取用户数据时发生错误。' );
}

}

function handle_wc_membership_saved( $membership_plan, $args ) {
$logger = wc_get_logger();

$logger->info( sprintf( '正在运行 handle_wc_membership_saved,用户 ID: %s,会员 ID: %s,是否为更新: %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

$user_id            = $args['user_id'];

$membership         = wc_memberships_get_user_membership( $args['user_membership_id'] );

$membership_plan_id = $membership->plan->id;

if ( $membership && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id])) {
	$membership_plan_name = $membership_plan->name;
	$status               = $membership->status;
	update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status );
}

}
add_action( ‘wc_memberships_user_membership_saved’, ‘handle_wc_membership_saved’, 10, 2 );

/run_full_wc_membership_sync/
function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('正在运行 full_wc_membership_sync') );

foreach ( $allusers as $user ) {

   $user_id = $user->id;

   $membership = wc_memberships_get_user_membership($user_id);

   $membership_plan_id = $membership->plan->id;

   $logger->info( sprintf('正在检查 %s 的会员状态', $user->user_login) );

   if ($membership  && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id])) {
       
	  $membership_plan_name = $membership->plan->name;
      
	  $status = $membership->status;
      
	  $logger->info( sprintf('正在更新 %s 的群组访问权限', $user->user_login) );

	  update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

	  $logger->info( sprintf('休眠 5 秒') );
      
	  sleep(5);
   }
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

您需要修改第一列中的会员计划 ID,以及第二列中的 Discourse 群组 ID。

摘要
                                    "347"  => "42",
                                    "357"  => "43",
                                    "441"  => "44"
7 个赞

你好 @Ed_Bobkov,希望你能帮我解决这个问题。以下是我目前的情况说明:

  1. 我已将 WordPress 配置为 SSO 客户端,用户可通过 Discourse 或 WordPress 登录。
  2. 我目前在 WordPress 上尚未使用任何“会员资格”插件。
  3. 我计划在 Discourse 中为每种会员类型设置数十个不同的群组。
  4. 我原本打算每当有新的会员群组可供用户选择时,手动在 WordPress 中创建一个新的会员群组,并在 Discourse 中创建对应的群组。
  5. 正如你所说,“我需要激活 WordPress 与 Discourse 之间的集成。”
  6. 我猜测我可能需要安装“WooCommerce Memberships”插件?

接下来我该怎么做?