Files
2026-09-15 10:26:34 +08:00

611 lines
24 KiB
PHP

<?php
/**
* 2025 D1m Group
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0) that is available
* through the world-wide-web at this URL: http://www.opensource.org/licenses/OSL-3.0
*
* @copyright 2025 D1m Group
* @license http://www.opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
namespace Stemcell\Commerce\Model;
use Magento\Authorization\Model\UserContextInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Api\Data\CustomerInterfaceFactory;
use Magento\Customer\Model\CustomerFactory;
use Magento\Customer\Model\CustomerRegistry;
use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Math\Random;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\UrlInterface;
use Magento\Framework\Webapi\Exception;
use Magento\Integration\Api\CustomerTokenServiceInterface;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;
use Stemcell\Commerce\Api\CustomerManagementInterface;
use Stemcell\Commerce\Model\Wechat\Client;
use Stemcell\Commerce\Model\Wechat\WechatAvatarService;
/**
* 微信小程序用户管理实现类
*/
class CustomerManagement implements CustomerManagementInterface
{
private const CUSTOMER_EXPIRATION_CONFIG_PATH = 'webapi/jwtauth/customer_expiration';
/**
* CustomerManagement constructor.
*
* @param CustomerFactory $customerFactory
* @param CustomerRepositoryInterface $customerRepository
* @param CustomerInterfaceFactory $customerDataFactory
* @param Client $wechatClient
* @param Random $random
* @param CustomerTokenServiceInterface $customerTokenService
* @param EncryptorInterface $encryptor
* @param CustomerRegistry $customerRegistry
* @param LoggerInterface $logger
* @param CustomerCollectionFactory $customerCollectionFactory
* @param UserContextInterface $userContext
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
*/
public function __construct(
protected CustomerFactory $customerFactory,
protected CustomerRepositoryInterface $customerRepository,
protected CustomerInterfaceFactory $customerDataFactory,
protected Client $wechatClient,
protected Random $random,
protected CustomerTokenServiceInterface $customerTokenService,
protected EncryptorInterface $encryptor,
protected CustomerRegistry $customerRegistry,
protected LoggerInterface $logger,
protected CustomerCollectionFactory $customerCollectionFactory,
protected UserContextInterface $userContext,
protected \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
protected \Magento\Framework\App\ResourceConnection $resourceConnection,
protected TimezoneInterface $timezone,
protected WechatAvatarService $wechatAvatarService,
protected StoreManagerInterface $storeManager,
protected UrlInterface $urlInterface
) {
// 使用PHP 7.4+构造函数属性提升,无需单独赋值
}
/**
* {@inheritdoc}
*/
public function getWechatOpenid($code)
{
try {
$this->logger->info('Wechat Integration: Attempting to get openid', ['code' => $code]);
// 获取微信session_key和openid
$sessionData = $this->wechatClient->getSessionKey($code);
if (!isset($sessionData['openid'])) {
$this->logger->error('Wechat Integration: Failed to get openid');
throw new LocalizedException(__('获取微信openid失败'));
}
$this->logger->info('Wechat Integration: Openid obtained successfully', ['openid' => $sessionData['openid']]);
$result = [
'openid' => $sessionData['openid']
];
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception when getting openid', ['exception' => $e->getMessage()]);
$result = [
'error' => "获取openid失败,请稍后重试",
];
}
return WebapiResponse::json($result);
}
/**
* {@inheritdoc}
*/
public function loginByMobile($code, $openid)
{
try {
$this->logger->info('Wechat Integration: Attempting to create user by WeChat', ['openid' => $openid]);
// 1. 使用微信API获取手机号
$phoneNumber = $this->wechatClient->getPhoneNumberByApi($code);
$this->logger->info('Wechat Integration: Phone number obtained', ['phone_number_masked' => substr($phoneNumber, 0, 3) . '****' . substr($phoneNumber, -4)]);
// 2. 检查手机号是否已存在
$existingCustomer = $this->getCustomerByMobile($phoneNumber);
if ($existingCustomer) {
// 如果用户已存在,更新其openid
$this->logger->info('Wechat Integration: Customer already exists, updating openid', ['customer_id' => $existingCustomer->getId()]);
$existingCustomer->setCustomAttribute('wechat_openid', $openid);
$customer = $this->customerRepository->save($existingCustomer);
} else {
// 3. 创建新用户
$this->logger->info('Wechat Integration: Creating new customer', ['phone_number_masked' => substr($phoneNumber, 0, 3) . '****' . substr($phoneNumber, -4)]);
$customer = $this->customerDataFactory->create();
$customer->setWebsiteId(1); // 默认网站ID
$customer->setStoreId(1); // 默认店铺ID
$customer->setFirstname('wx_' . $phoneNumber);
$customer->setLastname('');
// 生成临时邮箱
$tempEmail = $this->generateTempEmail($phoneNumber);
$customer->setEmail($tempEmail);
// 设置手机号
$customer->setCustomAttribute('mobile', $phoneNumber);
// 设置微信openid
$this->logger->info('Wechat Integration: Setting wechat_openid for new customer', ['openid' => $openid]);
$customer->setCustomAttribute('wechat_openid', $openid);
// 设置随机密码
$password = $this->random->getRandomString(10);
// 保存用户
$customer = $this->customerRepository->save($customer);
// 使用CustomerRepository的setPasswordHash方法设置密码
$customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
$hashedPassword = $this->encryptor->getHash($password, true);
$customerSecure->setPasswordHash($hashedPassword);
}
$result = $this->generateCustomerToken($customer);
$this->logger->info('Wechat Integration: Login successful, token generated', ['customer_id' => $customer->getId(), 'token' => $result]);
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception creating user by WeChat', ['exception' => $e->getMessage()]);
$result = [
'error' => $e->getMessage(),
];
}
return WebapiResponse::json($result);
}
/**
* {@inheritdoc}
*/
public function updateCustomerInfo($name, $email, $mobile, $company, $wechatAvatar = null)
{
try {
// 1. 加载用户
$customerId = $this->userContext->getUserId();
$customer = $this->customerRepository->getById($customerId);
// 2. 更新用户信息
if (!empty($name)) {
$customer->setFirstname($name);
}
if (!empty($email)) {
// 检查邮箱是否已被使用
try {
$existingCustomer = $this->customerRepository->get($email);
$existingCustomerId = $existingCustomer->getId();
if (!empty($existingCustomerId) && $existingCustomerId != $customerId) {
throw new LocalizedException(__('该邮箱已被使用'));
}
} catch (\Exception $e) {
// 邮箱未被使用,可以设置
}
$customer->setEmail($email);
}
if (!empty($mobile)) {
// 检查手机号是否已被使用
// 手机号不可修改,此逻辑可根据需求调整
/**
$existingCustomer = $this->getCustomerByMobile($mobile);
if ($existingCustomer && $existingCustomer->getId() != $customerId) {
throw new LocalizedException(__('该手机号已被使用'));
}
$customer->setCustomAttribute('mobile', $mobile);**/
}
if (!empty($company)) {
// 设置公司属性
$customer->setCustomAttribute('company', $company);
}
// 处理微信头像
$this->processWechatAvatar($customer, $wechatAvatar, $this->getCustomerAttribute($customer, 'wechat_openid'));
// 3. 保存更新
$customer = $this->customerRepository->save($customer);
// 使用 Magento2 时区处理类格式化更新时间
$formattedUpdatedAt = $this->timezone->formatDateTime(
$customer->getUpdatedAt(),
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::MEDIUM
);
$result = [
'customer_id' => $customer->getId(),
'customer_updated' => $formattedUpdatedAt,
];
} catch (\Exception $e) {
$result = ['error' => "更新用户信息失败,请稍后重试, " . $e->getMessage()];
}
return WebapiResponse::json($result);
}
/**
* {@inheritdoc}
*/
public function wechatLogin($code)
{
try {
$this->logger->info('Wechat Integration: WeChat login attempt', ['code' => $code]);
// 1. 获取微信session_key和openid
$sessionData = $this->wechatClient->getSessionKey($code);
if (!isset($sessionData['openid'])) {
$this->logger->error('Wechat Integration: Failed to get openid during login');
throw new LocalizedException(__('获取微信openid失败'));
}
$openid = $sessionData['openid'];
$this->logger->info('Wechat Integration: Openid obtained for login', ['openid' => $openid]);
// 2. 检查是否有绑定该openid的用户
$customer = $this->getCustomerByWechatOpenid($openid);
if ($customer) {
$this->logger->info('Wechat Integration: Customer found with openid', ['customer_id' => $customer->getId()]);
// 已存在用户,生成access token
$result = $this->generateCustomerToken($customer);
$this->logger->info('Wechat Integration: Login successful, token generated', ['customer_id' => $customer->getId(), 'token' => $result]);
} else {
// 3. 新用户,返回需要绑定手机号的提示
$this->logger->info('Wechat Integration: New user detected, needs phone binding', ['openid' => $openid]);
$result = [
'error' => '请先绑定手机号',
];
}
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception during WeChat login', ['exception' => $e->getMessage()]);
$result = [
'error' => $e->getMessage(),
];
}
return WebapiResponse::json($result);
}
/**
* 根据手机号获取用户
*
* @param string $mobile
* @return CustomerInterface|null
*/
protected function getCustomerByMobile($mobile)
{
try {
$this->logger->info('Wechat Integration: Attempting to find customer by mobile', ['mobile' => $mobile]);
// 使用集合方式查找用户
$customerCollection = $this->customerCollectionFactory->create();
$customerCollection->addAttributeToSelect('*')
->addAttributeToFilter('mobile', $mobile)
->addAttributeToFilter('website_id', 1)
->setPageSize(1);
$customer = $customerCollection->getFirstItem();
if ($customer->getId()) {
$this->logger->info('Wechat Integration: Customer found by mobile', ['customer_id' => $customer->getId(), 'mobile' => $mobile]);
return $this->customerRepository->getById($customer->getId());
} else {
$this->logger->info('Wechat Integration: No customer found with mobile', ['mobile' => $mobile]);
}
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Error finding customer by mobile', ['mobile' => $mobile, 'exception' => $e->getMessage()]);
}
return null;
}
/**
* 根据微信openid获取用户
*
* @param string $openid
* @return CustomerInterface|null
*/
protected function getCustomerByWechatOpenid($openid)
{
try {
$this->logger->info('Wechat Integration: Attempting to find customer by openid', ['openid' => $openid]);
// 使用集合方式查找用户
$customerCollection = $this->customerCollectionFactory->create();
$customerCollection->addAttributeToSelect('*')
->addAttributeToFilter('wechat_openid', $openid)
->addAttributeToFilter('website_id', 1)
->setPageSize(1);
$customer = $customerCollection->getFirstItem();
if ($customer->getId()) {
$this->logger->info('Wechat Integration: Customer found by openid', ['customer_id' => $customer->getId(), 'openid' => $openid]);
return $this->customerRepository->getById($customer->getId());
} else {
$this->logger->info('Wechat Integration: No customer found with openid', ['openid' => $openid]);
}
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Error finding customer by openid', ['openid' => $openid, 'exception' => $e->getMessage()]);
}
return null;
}
/**
* 生成临时邮箱
*
* @param string $phoneNumber
* @return string
*/
protected function generateTempEmail($phoneNumber)
{
$randomString = $this->random->getRandomString(6);
return 'wx_' . $phoneNumber . '_' . $randomString . '@stemcell.cn';
}
/**
* 生成用户访问令牌
*
* @param CustomerInterface $customer
* @return array
* @throws \Exception
*/
protected function generateCustomerToken(CustomerInterface $customer)
{
try {
$this->logger->info('Wechat Integration: Attempting to generate token for customer', ['customer_id' => $customer->getId()]);
// 获取客户实体以访问密码哈希
$customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
$originalPasswordHash = $customerSecure->getPasswordHash();
// 生成临时密码
$tempPassword = $this->random->getRandomString(20);
try {
// 设置临时密码
$customerSecure->setPasswordHash($this->encryptor->getHash($tempPassword, true));
// 使用临时密码生成令牌
$token = $this->customerTokenService->createCustomerAccessToken(
$customer->getEmail(),
$tempPassword
);
$this->logger->info('Wechat Integration: Token generated successfully', ['customer_id' => $customer->getId()]);
$expiresIn = $this->scopeConfig->getValue(self::CUSTOMER_EXPIRATION_CONFIG_PATH, \Magento\Store\Model\ScopeInterface::SCOPE_STORE);
return [
'token_type' => 'Bearer',
'token' => $token,
'expires_in' => (int)$expiresIn * 60
];
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Error generating token', [
'customer_id' => $customer->getId(),
'error' => $e->getMessage()
]);
throw $e;
} finally {
// 恢复原始密码哈希,确保用户密码不变
$customerSecure->setPasswordHash($originalPasswordHash);
$this->logger->info('Wechat Integration: Restored original password hash', ['customer_id' => $customer->getId()]);
}
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Failed to generate customer token', ['error' => $e->getMessage()]);
throw new \Exception('Failed to generate customer token: ' . $e->getMessage(), $e->getCode(), $e);
}
}
/**
* 用户登出
*
* @return bool
* @throws Exception
*/
public function logout()
{
try {
$this->logger->info('Wechat Integration: User logout attempt');
// 获取当前登录的用户信息
$customerId = $this->getCurrentCustomerId();
$this->logger->info('Wechat Integration: User logout processing', ['customer_id' => $customerId]);
// 记录用户登出行为到数据库
$this->customerTokenService->revokeCustomerAccessToken($customerId);
$this->logger->info('Wechat Integration: User logout successful', ['customer_id' => $customerId]);
$result = ['logout' => 'success'];
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception during user logout', ['exception' => $e->getMessage()]);
$result = ['error' => 'Logout failed: ' . $e->getMessage()];
}
return WebapiResponse::json($result);
}
/**
* {@inheritdoc}
*/
public function getCustomerInfo()
{
try {
$this->logger->info('Wechat Integration: Getting customer info');
// 获取当前登录的用户ID
$customerId = $this->userContext->getUserId();
$this->logger->info('Wechat Integration: Processing customer info request', ['customer_id' => $customerId]);
// 获取用户信息
$customer = $this->customerRepository->getById($customerId);
// 构建返回数据
$mobile = $this->getCustomerAttribute($customer, 'mobile');
$result = [
'name' => str_contains($customer->getFirstname(), 'wx_' . $mobile) ? '' : $customer->getFirstname(),
'email' => str_contains($customer->getEmail(), 'wx_' . $mobile) ? '' : $customer->getEmail(),
'company' => $this->getCustomerAttribute($customer, 'company'),
'mobile' => $mobile,
'wechatAvatar' => $this->getAbsoluteAvatarUrl($this->getCustomerAttribute($customer, 'wechat_avatar'))
];
$this->logger->info('Wechat Integration: Customer info retrieved successfully', ['customer_id' => $customerId]);
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception getting customer info', ['exception' => $e->getMessage()]);
$result = ['error' => 'Failed to get customer info: ' . $e->getMessage()];
}
return WebapiResponse::json($result);
}
/**
* 获取用户自定义属性值
*
* @param CustomerInterface $customer
* @param string $attributeCode
* @return string|null
*/
protected function getCustomerAttribute(CustomerInterface $customer, string $attributeCode)
{
try {
return $customer->getCustomAttribute($attributeCode)?->getValue();
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Error getting customer attribute', ['attribute' => $attributeCode, 'exception' => $e->getMessage()]);
return null;
}
}
/**
* 获取头像的绝对URL
*
* @param string|null $relativePath 相对路径
* @return string|null 返回绝对URL,如果输入为空则返回null
*/
private function getAbsoluteAvatarUrl(?string $relativePath): ?string
{
if (empty($relativePath)) {
return null;
}
try {
// 获取媒体目录的绝对URL
$mediaUrl = $this->urlInterface->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]);
// 拼接完整的绝对URL
$absoluteUrl = rtrim($mediaUrl, '/') . '/' . ltrim($relativePath, '/');
$this->logger->info('Wechat Integration: Generated absolute avatar URL', [
'relative_path' => $relativePath,
'absolute_url' => $absoluteUrl
]);
return $absoluteUrl . '?r=' . time();
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Error generating absolute avatar URL', [
'relative_path' => $relativePath,
'exception' => $e->getMessage()
]);
return null;
}
}
/**
* 处理微信头像
*
* @param CustomerInterface $customer 用户对象
* @param string|null $wechatAvatar 微信头像URL或base64数据
* @param string $openid 用户openid
* @return void
*/
private function processWechatAvatar(CustomerInterface $customer, ?string $wechatAvatar, string $openid): void
{
// 如果提供了有效的头像数据,则更新用户头像
if (!empty($wechatAvatar)) {
$this->logger->info('Wechat Integration: Processing wechat avatar', [
'customer_id' => $customer->getId(),
'data_type' => $this->isBase64Data($wechatAvatar) ? 'base64' : 'url'
]);
// 判断是URL还是base64数据
if ($this->isBase64Data($wechatAvatar)) {
// 处理base64数据
$localAvatarUrl = $this->wechatAvatarService->saveBase64Avatar($wechatAvatar, $openid);
} else {
// 处理URL
$localAvatarUrl = $this->wechatAvatarService->downloadAndSaveAvatar($wechatAvatar, $openid);
}
if ($localAvatarUrl) {
$customer->setCustomAttribute('wechat_avatar', $localAvatarUrl);
$this->logger->info('Wechat Integration: Wechat avatar saved', [
'customer_id' => $customer->getId(),
'local_url' => $localAvatarUrl
]);
} else {
$this->logger->warning('Wechat Integration: Failed to save wechat avatar', [
'customer_id' => $customer->getId(),
'data_type' => $this->isBase64Data($wechatAvatar) ? 'base64' : 'url'
]);
}
}
}
/**
* 判断是否为base64数据
*
* @param string $data
* @return bool
*/
private function isBase64Data(string $data): bool
{
// 检查是否包含data:前缀(数据URL格式)
if (strpos($data, 'data:') === 0) {
return true;
}
// 检查是否为有效的base64编码
// base64编码通常只包含字母、数字、+、/、=字符
$base64Pattern = '/^[A-Za-z0-9+\/]*={0,2}$/';
if (preg_match($base64Pattern, $data)) {
// 进一步验证解码是否成功
$decoded = base64_decode($data, true);
return $decoded !== false;
}
return false;
}
/**
* 获取当前登录用户的ID
*
* @return int
* @throws LocalizedException
*/
protected function getCurrentCustomerId()
{
try {
$this->logger->info('Wechat Integration: Getting current customer ID');
$customerId = $this->userContext->getUserId();
$this->logger->info('Wechat Integration: Current customer ID retrieved', ['customer_id' => $customerId]);
return $customerId;
} catch (\Exception $e) {
$this->logger->error('Wechat Integration: Exception getting current customer ID', ['exception' => $e->getMessage()]);
throw new LocalizedException(__('获取当前用户ID失败'));
}
}
}