This commit is contained in:
2025-08-13 18:31:52 +08:00
parent 17ad88608b
commit 275713f893
77 changed files with 10658 additions and 178 deletions

94
app/utils/AliUtils.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
namespace app\utils;
use app\exception\SysException;
use think\facade\Log;
class AliUtils
{
public static function verify($name, $idCard, $accountNo, $bankPreMobile)
{
Log::info("阿里云四要素认证请求参数: {$name}, {$idCard}, {$accountNo}, {$bankPreMobile}");
// 参数校验
if (empty($name)) {
throw new SysException("持卡人姓名不能为空");
}
if (empty($idCard)) {
throw new SysException("身份证号码不能为空");
}
if (empty($accountNo)) {
throw new SysException("银行卡卡号不能为空");
}
if (empty($bankPreMobile)) {
throw new SysException("银行预留手机号码不能为空");
}
if (!preg_match('/^[\x{4e00}-\x{9fa5}]{2,}$/u', $name)) {
throw new SysException("持卡人姓名不合法");
}
if (!self::isValidIdCard($idCard)) {
throw new SysException("身份证号码不合法");
}
if (!preg_match('/^1[3-9]\d{9}$/', $bankPreMobile)) {
throw new SysException("银行预留手机号码格式不正确");
}
$appcode = 'f98606b602564d209f37fc02b0bd590c';
$headers = [
"Authorization: APPCODE {$appcode}",
"Content-Type: application/x-www-form-urlencoded; charset=UTF-8"
];
$url = 'https://bkvip.market.alicloudapi.com/v3/bcheck';
$body = http_build_query([
'accountNo' => $accountNo,
'name' => $name,
'idCardCode' => $idCard,
'bankPreMobile' => $bankPreMobile
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15
]);
$response = curl_exec($ch);
curl_close($ch);
Log::info("阿里云四要素: {$idCard}, {$name}, {$accountNo}, {$bankPreMobile}, 结果: {$response}");
$ret = json_decode($response, true);
if (!isset($ret['error_code']) || !isset($ret['result'])) {
throw new SysException("阿里云接口返回格式错误".$response);
}
$errorCode = $ret['error_code'];
$result = $ret['result'];
$respCode = $result['respCode'] ?? '';
if ($errorCode == 0 && $respCode === "0") {
// 验证通过
} else {
throw new SysException($result['respMsg'] ?? '认证失败');
}
$bankName = $result['bancardInfor']['bankName'] ?? null;
return [
'bankName' => $bankName,
'respJson' => $response
];
}
private static function isValidIdCard($idCard)
{
// 简单校验身份证号
return preg_match('/^\d{17}[\dXx]$/', $idCard);
}
}

86
app/utils/JwtUtils.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace app\utils;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtUtils
{
private string $secret = "f4e2e52034348f86b67cde581c0f9eb5";
private int $expire = 604800; // 单位:秒
private string $header = "token";
public function __construct()
{
$this->secret = base64_decode($this->secret);
}
/**
* 生成 JWT Token
*/
public function generateToken($userId, string $type): string
{
$now = time();
$payload = [
'sub' => (string)$userId,
'type' => $type,
'iat' => $now,
'exp' => $now + $this->expire
];
return JWT::encode($payload, $this->secret, 'HS512');
}
/**
* 从 token 中解析出 Claims
*/
public function getClaimByToken(string $token): ?object
{
try {
return JWT::decode($token, new Key($this->secret, 'HS512'));
} catch (\Exception $e) {
error_log('Token 验证失败: ' . $e->getMessage());
return null;
}
}
/**
* 判断 token 是否过期(通过传入的 exp 字段)
*/
public function isTokenExpired(int $exp): bool
{
return $exp < time();
}
// Getter/Setter
public function getSecret(): string
{
return $this->secret;
}
public function getExpire(): int
{
return $this->expire;
}
public function getHeader(): string
{
return $this->header;
}
public function setSecret(string $secret): void
{
$this->secret = $secret;
}
public function setExpire(int $expire): void
{
$this->expire = $expire;
}
public function setHeader(string $header): void
{
$this->header = $header;
}
}

230
app/utils/RedisUtils.php Normal file
View File

@@ -0,0 +1,230 @@
<?php
namespace app\utils;
use app\exception\SysException;
use support\think\Cache;
class RedisUtils
{
const CAN_CREATE_ORDER = 'can_create_order:';
const CREATE_ORDER_LIMIT = 'createOrder:';
const CAN_CASH = 'cash:canCash:';
public static function checkCanCreateOrder($userId)
{
$val = cache(self::CAN_CREATE_ORDER . $userId);
return empty($val);
}
public static function setUserCanCreateOrder($userId, $seconds)
{
cache(self::CAN_CREATE_ORDER . $userId, 1, $seconds);
}
public static function setCreateOrderFlagAndCheckLimit($userId, $orderId): bool
{
// 构造 key
$key = "createOrder:{$userId}:{$orderId}";
// 设置 Redis key值为 orderId过期时间 60 秒
Cache::store('redis')->set($key, (string)$orderId, 60);
// 获取 Redis 原生实例
$redis = Cache::store('redis')->handler();
// 使用 scan 非阻塞统计 key 数量
$pattern = "createOrder:{$userId}:*";
$iterator = null;
$count = 0;
while ($keys = $redis->scan($iterator, $pattern, 100)) {
$count += count($keys);
}
// 超过 22 返回 true
return $count > 22;
}
/**
* 通用的“按周统计”计数器(自动按课程 ID、业务类型区分
*
* @param string $prefix 前缀标识,如 'course:week_play:'
* @param int|string $id 主体 ID如课程 ID
* @param int $expireSeconds 默认过期时间(秒),默认 7 天
* @return int 返回当前统计值
*/
public static function incrWeekCounter(string $prefix, $id, int $expireSeconds = 604800): int
{
$key = $prefix . date('oW') . ':' . $id; // oW 为当前“ISO 年 + 周数”,确保每周一个 key
$count = Cache::store('redis')->inc($key);
// 第一次设置时设定过期时间
if ($count === 1) {
Cache::store('redis')->expire($key, $expireSeconds);
}
return $count;
}
public static function getconfig()
{
return Cache::getConfig();
}
/**
* 获取指定“周统计”计数值
*
* @param string $prefix 前缀
* @param int|string $id 主体 ID
* @return int
*/
public static function getWeekCounter(string $prefix, $id): int
{
$key = $prefix . date('oW') . ':' . $id;
return (int)Cache::store('redis')->get($key, 0);
}
public static function isCanCash($userId)
{
$val = cache(self::CAN_CASH . $userId);
return !empty($val);
}
public static function checkRealCount(int $userId)
{
$key = 'updateAuthCertInfo:' . $userId;
$val = cache($key);
if (!empty($val)) {
throw new SysException("实名修改失败: 每月可修改次数已用完,请联系管理员");
}
}
public static function setRealCount(int $userId)
{
$key = 'updateAuthCertInfo:' . $userId;
cache($key, 2628000);
}
private static function getFreeWatchKey($userId, $permanently = false)
{
if ($permanently) {
return "free:watch:" . $userId;
}
return "free:watch:" . date('Y-m-d') . ':' . $userId;
}
public static function isExpiredSet($key)
{
$redis = Cache::store('redis')->handler();
// 获取 key 的剩余过期时间(单位:秒)
$ttl = $redis->ttl($key);
if ($ttl == -1) {
return false;
}
return $ttl !== -2;
}
public static function getFreeWatchTimeIsExpire($userId)
{
$freeWatchKey = self::getFreeWatchKey($userId, true);
$permanentlyFreeWatch = cache($freeWatchKey);
$watchKey = self::getFreeWatchKey($userId, false);
$payFreeWatchInfo = cache($watchKey);
$expireTime = -1;
$jsonObject = null;
if (!empty($payFreeWatchInfo)) {
$jsonObject = json_decode($payFreeWatchInfo, true);
$expireTime = isset($jsonObject['expireTime']) ? $jsonObject['expireTime'] : -1;
}
$now = time(); // 当前时间戳
if ((!empty($permanentlyFreeWatch) && RedisUtils::isExpiredSet($freeWatchKey)) ||
(!empty($permanentlyFreeWatch) && $now >= $expireTime)) {
if (empty($permanentlyFreeWatch)) {
return null;
}
if (!RedisUtils::isExpiredSet($freeWatchKey)) {
cache($freeWatchKey, intval($permanentlyFreeWatch));
}
return false;
} else {
if (empty($payFreeWatchInfo)) {
return null;
}
$second = isset($jsonObject['second']) ? intval($jsonObject['second']) : 0;
if ($expireTime == -1) {
$jsonObject['expireTime'] = $now + $second;
$tomorrowZero = strtotime(date('Y-m-d', strtotime('+1 day')));
$expire = $tomorrowZero - $now;
cache($watchKey, json_encode($jsonObject), $expire);
return false;
} else {
return $now > $expireTime;
}
}
}
public static function setFreeWatchTime($userId, $second, $isPermanently = false)
{
$now = time();
$tomorrow = strtotime(date('Y-m-d', strtotime('+1 day')));
$freeWatchKey = self::getFreeWatchKey($userId, $isPermanently);
if ($isPermanently) {
$data = cache($freeWatchKey);
if (empty($data)) {
// 永久的,但不设置过期时间(-1
cache($freeWatchKey, $second);
} else {
$expire = Cache::store('redis')->handler()->ttl($freeWatchKey);
if ($expire === -1 || $expire === false || $expire === null) {
$expire = -1;
} else {
$expire += intval($second);
}
$newValue = intval($data) + intval($second);
if ($expire > 0) {
cache($freeWatchKey, $newValue, $expire);
} else {
cache($freeWatchKey, $newValue);
}
}
} else {
// 非永久,临时有效期到明天零点
$expire = $tomorrow - $now;
$jsonObject = [
'expireTime' => -1,
'second' => intval($second)
];
// 如果不存在才设置
if (!Cache::store('redis')->has($freeWatchKey)) {
cache($freeWatchKey, json_encode($jsonObject), $expire);
}
}
}
public static function setCanCashFlag(mixed $userId, int $param)
{
cache("cash:canCash:".$userId, $param,300);
}
}

192
app/utils/WuYouPayUtils.php Normal file
View File

@@ -0,0 +1,192 @@
<?php
namespace app\utils;
use app\api\model\CommonInfo;
use app\exception\SysException;
use think\facade\Log;
class WuYouPayUtils
{
private static string $mchId;
private static string $payUrl;
private static string $notifyUrl;
private static string $h5BaseUrl;
private static string $secret;
private static string $extractUrl;
private static string $extractNotifyUrl;
private static string $queryUrl;
private static function boot(): void
{
static $booted = false;
if ($booted) {
return;
}
$booted = true;
self::$mchId = config('wuyou.merchant_id', '');
self::$payUrl = config('wuyou.url', '');
self::$notifyUrl = config('wuyou.notify_url', '');
self::$h5BaseUrl = config('wuyou.h5_base_url', '');
self::$secret = config('wuyou.secret', '');
self::$extractUrl = config('wuyou.extract_url', '');
self::$extractNotifyUrl = config('wuyou.extract_notify_url', '');
self::$queryUrl = config('wuyou.query_url', '');
if (hasEmpty(self::$mchId, self::$payUrl, self::$notifyUrl, self::$h5BaseUrl, self::$secret)) {
throw new SysException('缺少必要参数');
}
}
public static function getParamsSign(array $params): string
{
self::boot();
ksort($params);
$signStr = '';
foreach ($params as $key => $value) {
$signStr .= $key . '=' . $value . '&';
}
$signStr .= 'key=' . self::$secret;
return strtoupper(md5($signStr));
}
private static function getBaseParams()
{
self::boot();
return [
'mch_id' => self::$mchId,
'timestamp' => time(),
];
}
public static function payOrderTest($orderNo, $userId, $amount, $userAgent, $allId, $payType) {
return http_post('192.168.1.21:8080/api/delay', [
'callbacks' => 'CODE_SUCCESS',
'order_sn' => uuid(),
'out_trade_no' => "{$orderNo}-{$userId}",
'pay_time' => time(),
'sign' => '',
'total' => $amount
]);
}
public static function payOrder($orderNo, $userId, $amount, $userAgent, $allId, $payType)
{
$payConfig = (new CommonInfo())->getByCode(926)['value'] ?? '0';
if ($payConfig !== '1') {
throw new SysException('暂无支付渠道');
}
$params = self::getBaseParams();
$params['type'] = '6001';
$params['is_code'] = '1';
$params['out_trade_no'] = "{$orderNo}-{$userId}";
$params['total'] = $amount;
$params['notify_url'] = self::$notifyUrl;
$params['sign'] = self::getParamsSign($params);
if ($payType === 'h5') {
$params['return_url'] = self::$h5BaseUrl . $allId;
}
return self::requestPost(self::$payUrl, $params, $userAgent);
}
/**
* 发起 POST 请求
* @param string $url 请求地址
* @param array $params 请求参数
* @param string $userAgent User-Agent
* @return array|null
*/
private static function requestPost(string $url, array $params, string $userAgent): ?array
{
Log::info("[支付]请求地址:{$url},请求参数:".json_encode($params).",User-Agent:{$userAgent}");
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_HTTPHEADER => [
'User-Agent: ' . $userAgent,
'Content-Type: application/x-www-form-urlencoded',
// 'Content-Type: application/json',
]
]);
$response = curl_exec($curl);
curl_close($curl);
Log::info("[支付]返回结果:{$response}");
return json_decode($response, true);
}
public static function queryExtractOrder($outOrderNo, $userId, $isUser, $amount)
{
$params = self::getBaseParams();
$params['out_trade_no'] = sprintf('%s-%s:%s', $outOrderNo, $userId, $isUser ? 'us' : 'dl');
$params['total'] = $amount;
$params['sign'] = self::getParamsSign($params);
return self::requestPost(self::$extractUrl, $params, 'WuYouPay');
}
public static function extractOrder($isAliPay, $outOrderNo, $userId, $amount, $isUser, $account, $userName, $bankName, $province, $city, $bankBranch)
{
$params = self::getBaseParams();
$params['out_trade_no'] = sprintf('%s-%s:%s', $outOrderNo, $userId, $isUser ? 'us' : 'dl');
$params['total'] = $amount;
$params['bank_card'] = $account;
$params['bank_account_name'] = $userName;
if ($isAliPay) {
$params['bank_name'] = '1';
$params['bank_branch'] = '1';
$params['province'] = '1';
$params['city'] = '1';
} else {
$params['bank_name'] = $bankName;
$params['bank_branch'] = !empty($bankBranch) ? $bankBranch : '1';
$params['province'] = !empty($province) ? $province : '1';
$params['city'] = !empty($city) ? $city : '1';
}
$params['notify_url'] = self::$extractNotifyUrl;
$params['sign'] = self::getParamsSign($params);
$params['business_type'] = 0;
if ($isAliPay) {
$params['business_attr'] = 'alipay';
} else {
$params['business_attr'] = 'unionpay';
}
// 发送请求POST
return self::requestPost(self::$extractUrl, $params, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36');
}
public static function queryOrder($trade_no, $user_id, string $amount, string $string)
{
$params = self::getBaseParams();
$params['out_trade_no'] = "{$trade_no}-{$user_id}";
$params['total'] = $amount;
$params['sign'] = self::getParamsSign($params);
return self::requestPost(self::$queryUrl, $params, 'WuYouPay');
}
}