提交其他文件

This commit is contained in:
2026-03-14 16:20:49 +08:00
parent a227deaecd
commit 0a19b334f8
1385 changed files with 73568 additions and 0 deletions

View File

@ -0,0 +1,478 @@
<?php
namespace app\common\service;
use think\facade\Db;
use think\facade\Cache;
use think\Exception;
use app\common\model\teastore\TeaStore;
use app\common\model\teastore\TeaStoreGroup;
use app\common\model\user\UserGroup;
use app\common\model\verify\VerifyRecords;
/**
* 抖音核销服务类
*/
class DouyinVerifyService
{
// 抖音配置
protected $appId = 'awyjvrmyjwss6d71';
protected $appSecret = '1a9208f7742e83a91a2a27ccb558849c';
/**
* 核销券码
*/
public function verify($userId, $storeId, $code, $type = 2)
{
try {
// 1. 频率限制
$this->checkRateLimit($userId);
// 2. 获取分布式锁
if (!$this->getLock($code)) {
throw new Exception('系统正在处理该券码,请稍候');
}
// 3. 获取抖音Token
$token = $this->getToken();
// 4. 准备核销凭证
$prepared = $this->prepare($token, $code, $type);
// 5. 检查是否已核销
$this->checkUsed($prepared['order_id']);
// 6. 获取门店pro ID
$poiId = $this->getStorePoiId($storeId);
// 7. 执行抖音核销
$this->executeVerify($prepared['verify_token'], $token, $poiId, [$prepared['encrypted_code']]);
// 8. 保存核销记录
$this->saveRecord($userId, $storeId, $code, $prepared, 1);
// 9. 标记为已使用
$this->markUsed($prepared['order_id'], $storeId, $prepared['third_sku_id'], $userId);
return [
'success' => true,
'message' => '核销成功',
'order_id' => $prepared['order_id']
];
} catch (Exception $e) {
// 记录错误日志
$this->saveRecord(
$userId,
$storeId,
$code,
$prepared ?? [],
2,
$e->getMessage()
);
return [
'success' => false,
'message' => $e->getMessage()
];
} finally {
// 释放锁
if (isset($code)) {
$this->releaseLock($code);
}
}
}
/**
* 频率限制检查
*/
protected function checkRateLimit($userId)
{
$key = 'verify:rate:' . $userId . ':' . date('Hi');
try {
// 使用Cache门面的store方法指定使用redis驱动
$cache = Cache::store('redis');
$count = $cache->inc($key);
$cache->set($key, $count, 60);
if ($count > 20) {
throw new Exception('操作过于频繁,请稍后再试');
}
} catch (\Exception $e) {
// Redis不可用时跳过频率限制
return;
}
}
/**
* 获取分布式锁
*/
protected function getLock($code)
{
$lockKey = 'verify:lock:' . md5($code);
$lockValue = uniqid();
try {
// 使用Cache门面获取Redis实例
$redis = Cache::store('redis')->handler();
// 使用SET NX EX实现分布式锁
$result = $redis->set($lockKey, $lockValue, ['nx', 'ex' => 10]);
return (bool)$result;
} catch (\Exception $e) {
// Redis不可用时不使用锁
return true;
}
}
/**
* 释放锁
*/
protected function releaseLock($code)
{
$lockKey = 'verify:lock:' . md5($code);
try {
$redis = Cache::store('redis')->handler();
$redis->del($lockKey);
} catch (\Exception $e) {
// 忽略释放锁错误
}
}
/**
* 获取抖音Token
*/
protected function getToken()
{
$cacheKey = 'douyin:token';
// try {
// // 从Redis缓存获取
// $token = Cache::store('redis')->get($cacheKey);
// if ($token) {
// return $token;
// }
// } catch (\Exception $e) {
// // Redis不可用时使用文件缓存
// $token = Cache::get($cacheKey);
// if ($token) {
// return $token;
// }
// }
$url = 'https://open.douyin.com/oauth/client_token/';
$params = [
'client_key' => $this->appId,
'client_secret' => $this->appSecret,
'grant_type' => 'client_credential'
];
$response = $this->httpPost($url, $params, false);
$result = json_decode($response, true);
if (isset($result['data']['error_code']) && $result['data']['error_code'] === 0) {
$token = $result['data']['access_token'];
// // 存入Redis缓存
// try {
// Cache::store('redis')->set($cacheKey, $token, $expire - 300);
// } catch (\Exception $e) {
// // Redis不可用时使用文件缓存
// Cache::set($cacheKey, $token, $expire - 300);
// }
return $token;
}
throw new Exception('获取抖音Token失败');
}
/**
* 准备核销凭证
*/
protected function prepare($token, $code, $type)
{
$url = 'https://open.douyin.com/goodlife/v1/fulfilment/certificate/prepare/';
if ($type == 1) {
$params = ['code' => $code];
} else {
$longUrl = $this->followRedirect($code);
$objectId = $this->parseObjectId($longUrl);
$params = ['encrypted_data' => $objectId];
}
$response = $this->httpGet($url . '?' . http_build_query($params), $token);
$result = json_decode($response, true);
if (!isset($result['data']['error_code']) ||
$result['data']['error_code'] !== 0 ||
!isset($result['data']['certificates'][0])) {
throw new Exception('券码无效或已使用');
}
return [
'order_id' => $result['data']['order_id'] ?? '',
'encrypted_code' => $result['data']['certificates'][0]['encrypted_code'] ?? '',
'verify_token' => $result['data']['verify_token'] ?? '',
'third_sku_id' => $result['data']['certificates'][0]['sku']['third_sku_id'] ?? ''
];
}
/**
* 检查是否已核销
*/
protected function checkUsed($orderId)
{
$cacheKey = 'verify:used:' . $orderId;
// 先检查Redis缓存
try {
if (Cache::store('redis')->has($cacheKey)) {
throw new Exception('该券码已被核销');
}
} catch (\Exception $e) {
// Redis不可用跳过缓存检查
}
// 检查数据库
$exists = UserGroup::where('code', $orderId)->find();
if ($exists) {
// 更新Redis缓存
try {
Cache::store('redis')->set($cacheKey, 1, 86400);
} catch (\Exception $e) {
// Redis不可用时忽略
}
throw new Exception('该券码已被核销');
}
}
/**
* 获取门店POI ID
*/
protected function getStorePoiId($storeId)
{
$store = TeaStore::where('id', $storeId)->field('pro_id')->find();
if (!$store) {
throw new Exception('门店不存在');
}
if ($store['pro_id'] == 0) {
throw new Exception('门店未配置核销权限');
}
return $store['pro_id'];
}
/**
* 执行核销
*/
protected function executeVerify($verifyToken, $accessToken, $poiId, $codes)
{
$url = 'https://open.douyin.com/goodlife/v1/fulfilment/certificate/verify/';
$data = [
'verify_token' => $verifyToken,
'poi_id' => $poiId,
'encrypted_codes' => $codes
];
$response = $this->httpPost($url, $data, $accessToken);
$result = json_decode($response, true);
if (!isset($result['data']['error_code']) || $result['data']['error_code'] !== 0) {
throw new Exception('抖音核销失败');
}
return $result;
}
/**
* 保存核销记录
*/
protected function saveRecord($userId, $storeId, $code, $data, $status = 1, $error = '')
{
$record = [
'user_id' => $userId,
'dy_order_id' => $data['order_id'] ?? '',
'third_sku_id' => $data['third_sku_id'] ?? '',
'qr_code' => $code,
'encrypted_code' => $data['encrypted_code'] ?? '',
'verify_result' => json_encode($data, JSON_UNESCAPED_UNICODE),
'error_log' => $error,
'verify_time' => date('Y-m-d H:i:s'),
'status' => $status
];
VerifyRecords::insert($record);
}
/**
* 标记为已使用
*/
protected function markUsed($orderId, $storeId, $skuId, $userId)
{
// 获取商品ID
$store_group = TeaStoreGroup::where([
'store_id' => $storeId,
'sku_id' => $skuId
])->field('id')->find();
if ($store_group) {
// 保存到UserGroup表
$usergroupData = [
'group_id' => $store_group['id'],
'user_id' => $userId,
'store_id'=>$storeId,
'type' => 2,
'dtime' => date('Y-m-d H:i:s'),
'code' => $orderId
];
// 保存到UserGroup表
UserGroup::insert($usergroupData);
}
Db::startTrans();
try {
// 更新Redis缓存
try {
Cache::store('redis')->set('verify:used:' . $orderId, 1, 86400);
} catch (\Exception $e) {
// Redis不可用时忽略
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new Exception('保存核销记录失败');
}
}
/**
* 跟踪重定向
*/
protected function followRedirect($url)
{
// 先尝试解码Base64
$url = $this->autoDecodeBase64($url);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception('获取重定向失败: ' . $error);
}
$redirectUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
return $redirectUrl;
}
/**
* 自动解码Base64
*/
protected function autoDecodeBase64($str)
{
// 检查是否是有效的Base64字符串
if (preg_match('/^[A-Za-z0-9+\/=]+$/', $str)) {
$decoded = @base64_decode($str, true);
if ($decoded !== false && base64_encode($decoded) === $str) {
// 再次检查解码后是否是URL
if (filter_var($decoded, FILTER_VALIDATE_URL)) {
\think\facade\Log::info('自动解码Base64', [
'original' => $str,
'decoded' => $decoded
]);
return $decoded;
}
}
}
return $str;
}
/**
* 解析object_id
*/
protected function parseObjectId($url)
{
parse_str(parse_url($url, PHP_URL_QUERY), $params);
return $params['object_id'] ?? '';
}
/**
* HTTP GET请求
*/
protected function httpGet($url, $token = '')
{
$ch = curl_init();
$headers = ['Content-Type: application/json'];
if ($token) {
$headers[] = 'access-token: ' . $token;
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
/**
* HTTP POST请求
*/
protected function httpPost($url, $data, $token = '')
{
$ch = curl_init();
$headers = ['Content-Type: application/json'];
if ($token) {
$headers[] = 'access-token: ' . $token;
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}