提交代码

This commit is contained in:
youda 2025-04-17 15:17:22 +08:00
parent 87a37460bd
commit 529ea5a636
6 changed files with 239 additions and 378 deletions

View File

@ -214,7 +214,7 @@ class Index extends Base
/**
* 生成带用户推广二维码的海报图片
* 生成带用户推广二维码的海报图片兼容旧版API
* @return \think\response\Json|void
*/
public function generate_urllinks()
@ -231,123 +231,27 @@ class Index extends Base
return $this->renderError('用户不存在');
}
// 获取配置信息
$config = getConfig('base');
$posterPath = $config['poster_template'] ?? '/img_poster.jpg';
$cacheExpire = $config['poster_cache_expire'] ?? 86400; // 默认缓存1天
// 创建缓存目录结构用户ID分组避免单目录文件过多
$userGroup = floor($userId / 1000); // 每1000个用户一个目录
$cacheDir = runtime_path() . 'poster/' . $userGroup . '/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
// 缓存文件名基于用户ID和模板文件的哈希
$templateFile = getcwd() . $posterPath;
$templateHash = md5_file($templateFile); // 使用文件内容哈希而不是修改时间
$cacheFile = $cacheDir . 'user_' . $userId . '_' . $templateHash . '.png';
// 清理该用户的旧缓存文件(保留当前有效的)
$this->cleanUserPosterCache($cacheDir, $userId, $templateHash);
// 定期清理过期缓存每100次请求执行一次避免频繁IO
if (mt_rand(1, 100) === 1) {
$this->cleanExpiredPosterCache($cacheExpire);
}
// 如果缓存存在且未过期,直接使用缓存
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheExpire)) {
$imageData = file_get_contents($cacheFile);
} else {
// 生成URL链接
$wxServer = new \app\common\server\Wx($this->app);
$user_base = $wxServer->generateUrlLinks($userId);
// 生成海报
$autoload = new \app\common\server\autoload();
$imageData = $autoload->generatePosterWithQR($templateFile, $user_base);
// 保存到缓存
if ($imageData) {
file_put_contents($cacheFile, $imageData);
// 更新最后访问时间文件(用于清理判断)
touch($cacheFile);
}
}
// 使用新的PosterService生成海报
$posterService = new \app\common\service\PosterService();
$result = $posterService->getUserPoster($userId);
// 检查是否需要直接输出图片
$outputImage = request()->param('output/d', 1);
$outputImage = request()->param('output/d', 0);
if ($imageData && $outputImage) {
header('Content-Type: image/png');
header('Content-Length: ' . strlen($imageData));
echo $imageData;
exit();
} elseif ($imageData) {
// 生成可访问的URL使用相对路径更安全且灵活
$relativePath = 'runtime/poster/' . $userGroup . '/user_' . $userId . '_' . $templateHash . '.png';
return $this->renderSuccess('海报生成成功', [
'image_url' => request()->domain() . '/' . $relativePath
]);
if ($result['status']) {
if ($outputImage) {
// 重定向到COS上的图片
header("Location: " . $result['data']['image_url']);
exit();
} else {
// 返回图片URL
return $this->renderSuccess('海报生成成功', [
'image_url' => $result['data']['image_url']
]);
}
} else {
// 生成失败,返回错误信息
return $this->renderError('海报生成失败');
}
}
/**
* 清理指定用户的旧海报缓存
* @param string $cacheDir 缓存目录
* @param int $userId 用户ID
* @param string $currentHash 当前模板哈希
*/
private function cleanUserPosterCache($cacheDir, $userId, $currentHash)
{
// 查找该用户的所有缓存文件
$pattern = $cacheDir . 'user_' . $userId . '_*.png';
$files = glob($pattern);
foreach ($files as $file) {
// 如果不是当前使用的缓存文件,则删除
if (strpos($file, 'user_' . $userId . '_' . $currentHash . '.png') === false) {
@unlink($file);
}
}
}
/**
* 清理所有过期的海报缓存
* @param int $expireTime 过期时间(秒)
*/
private function cleanExpiredPosterCache($expireTime)
{
// 获取缓存根目录
$rootCacheDir = runtime_path() . 'poster/';
if (!is_dir($rootCacheDir)) {
return;
}
// 当前时间
$now = time();
// 遍历所有用户组目录
$groupDirs = glob($rootCacheDir . '*/');
foreach ($groupDirs as $groupDir) {
// 获取组目录中的所有PNG文件
$files = glob($groupDir . '*.png');
foreach ($files as $file) {
// 如果文件过期(最后修改时间超过过期时间)
if ($now - filemtime($file) > $expireTime) {
@unlink($file);
}
}
// 如果目录为空,删除目录
$remainingFiles = glob($groupDir . '*');
if (empty($remainingFiles)) {
@rmdir($groupDir);
}
return $this->renderError($result['message']);
}
}

View File

@ -1,261 +0,0 @@
<?php
namespace app\api\controller;
use app\common\server\Upload as uploadss;
use \think\facade\Db;
use OSS\Core\OssException;
use OSS\OssClient;
require_once '../extend/oss/autoload.php';
class Upload extends Base
{
public function picture()
{
#获取表单上传文件
$files = request()->file('file', '');
if (empty($files)) {
return $this->renderError("请上传图片");
}
$ext = ['jpg', 'png', 'jpeg', 'JPG', 'PNG', 'JPEG', 'gif', 'apk', 'mp3'];
$type = substr($_FILES['file']['name'], strrpos($_FILES['file']['name'], '.') + 1);
if (!in_array($type, $ext)) {
return $this->renderError("文件格式错误");
}
if ($_FILES['file']['size'] > 20971520) {
return $this->renderError("上传文件不能超过20M");
}
#判断是否上传过
$hash = $files->hash('sha1');
$info = Db::name('picture')->where('token', $hash)->find();
if ($info) {
$data['path'] = imageUrl($info['imgurl']);
$data['imgurl'] = $info['imgurl'];
return $this->renderSuccess('上传成功', $data);
} else {
$object_file = 'storage/topic';
$accessKeyId = '';
$accessKeySecret = '';
$endpoint = '';
$bucket = '';
// 设置文件名称。
$object = $object_file . '/' . date('Ymd') . '/' . sha1(date('YmdHis', time()) . uniqid()) . '.' . $type;
// <yourLocalFile>由本地文件路径加文件名包括后缀组成,例如/users/local/myfile.txt。
$filePath = $_FILES['file']['tmp_name'];
try {
$ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint);
$ossClient->uploadFile($bucket, $object, $filePath);
} catch (OssException $e) {
return $this->renderError("上传失败");
}
#新增数据
$object = '/' . $object;
$save_data['token'] = $hash;
$save_data['imgurl'] = $object;
$save_data['addtime'] = time();
$save_data['status'] = 1;
$res = Db::name('picture')->insertGetId($save_data);
if ($res) {
$data['path'] = imageUrl($object);
$data['imgurl'] = $object;
return $this->renderSuccess('上传成功', $data);
} else {
return $this->renderError('上传失败');
}
}
}
public function picture1()
{
#获取表单上传文件
$files = request()->file('file', '');
if (empty($files)) {
return $this->renderError("请上传图片");
}
$ext = ['jpg', 'png', 'jpeg', 'JPG', 'PNG', 'JPEG', 'gif'];
$type = substr($_FILES['file']['name'], strrpos($_FILES['file']['name'], '.') + 1);
if (!in_array($type, $ext)) {
return $this->renderError("文件格式错误");
}
if ($_FILES['file']['size'] > 5242880) {
return $this->renderError("上传文件不能超过5M");
}
#判断是否上传过
$hash = $files->hash('sha1');
$info = Db::name('picture')->where('token', $hash)->find();
// if ($info) {
// $data['id'] = $info['id'];
// $data['path'] = imageUrl($info['imgurl']);
// $data['imgurl'] = $info['imgurl'];
// return $this->renderSuccess('上传成功', $data);
// } else {
// 保存图片
$date = date('Ymd');
$uniqueFileName = md5(uniqid(rand(), true)) . '.' . $type;
$saveDir = './storage/topic/' . $date;
if (!is_dir($saveDir)) {
mkdir($saveDir, 0777, true);
}
$savename = $saveDir . '/' . $uniqueFileName;
// 移动文件到目标目录
if (move_uploaded_file($_FILES['file']['tmp_name'], $savename)) {
$savename = str_replace('\\', '/', $savename);
$savename = substr($savename, 1); // 去掉开头的 “.”
// 新增数据
$save_data['token'] = $hash;
$save_data['imgurl'] = $savename;
$save_data['addtime'] = time();
$res = Db::name('picture')->insertGetId($save_data);
if ($res) {
$data['id'] = $res;
$data['path'] = imageUrl($savename);
$data['imgurl'] = $savename;
return $this->renderSuccess('上传成功', $data);
} else {
return $this->renderError('上传失败');
}
} else {
return $this->renderError('文件保存失败');
}
// #保存图片
// $savename = \think\facade\Filesystem::disk('public')->putFile('topic', $files);
// $hash = $files->hash('sha1');
// $savename = '/storage/' . $savename;
// $savename = str_replace('\\', '/', $savename);
// #新增数据
// $save_data['token'] = $hash;
// $save_data['imgurl'] = $savename;
// $save_data['addtime'] = time();
// $res = Db::name('picture')->insertGetId($save_data);
// if ($res) {
// $data['id'] = $res;
// $data['path'] = imageUrl($savename);
// $data['imgurl'] = $savename;
// return $this->renderSuccess('上传成功', $data);
// } else {
// return $this->renderError('上传失败');
// }
// }
}
/**
* 上传图片
*/
public function picture_old()
{
// 获取表单上传文件
$files = request()->file();
if (empty($files['file'])) {
return $this->renderError("请上传图片");
}
try {
$ext = ['jpg', 'png', 'jpeg', 'JPG', 'PNG', 'JPEG'];
$type = substr($_FILES['file']['name'], strrpos($_FILES['file']['name'], '.') + 1);
if (!in_array($type, $ext)) {
return $this->renderError("文件格式错误");
}
$omgpath = $_FILES['file']['tmp_name'];
if ($this->checkMuma($omgpath) == 1) {
return $this->renderError('您上传的文件为可疑木马,请自重!');
}
validate(['image' => 'filesize:10240|fileExt:jpg|image:200,200,jpg'])
->check($files);
$file = $files['file'];
$savename = \think\facade\Filesystem::disk('public')->putFile('topic', $file);
$hash = $file->hash('sha1');
} catch (\think\exception\ValidateException $e) {
return $this->renderError($e->getMessage());
}
$savename = '/storage/' . $savename;
$savename = str_replace('\\', '/', $savename);
$pic = Db::name('picture')->where('token', $hash)->find();
if ($pic) {
$return['id'] = $pic['id'];
$return['path'] = imageUrl($pic['imgurl']);
$return['imgurl'] = $pic['imgurl'];
unlink('.' . $savename);
return $this->renderSuccess('上传成功', $return);
}
//判断是否开始阿里云存储
$oss_path = $hash . '.jpg';
$path = $this->aliyunupload($oss_path, '.' . $savename);
unlink('.' . $savename);
$savename = $path;
//新增数据
$save_data['imgurl'] = $savename;
$save_data['token'] = $hash;
$save_data['addtime'] = time();
$save_data['status'] = 1;
$resultId = Db::name('picture')->insertGetId($save_data);
if ($resultId) {
$return['id'] = $resultId;
$return['path'] = imageUrl($savename);
$return['imgurl'] = $savename;
return $this->renderSuccess('上传成功', $return);
} else {
return $this->renderError('上传失败');
}
}
/**
* 检测文件是否包含木马
*
* @param $filepath 文件路径
* @return $status 0为正常 1为可疑木马文件 -1为文件没有上传
*/
public function checkMuma($filepath)
{
$status = 0;
$tips = array(
"0" => "文件没问题",
"1" => "文件为可疑木马文件",
"-1" => "文件没有上传"
);
if (file_exists($filepath)) {
$resource = fopen($filepath, 'rb');
$fileSize = filesize($filepath);
fseek($resource, 0);
if ($fileSize > 512) { // 取头和尾
$hexCode = bin2hex(fread($resource, 512));
fseek($resource, $fileSize - 512);
$hexCode .= bin2hex(fread($resource, 512));
} else { // 取全部
if ($fileSize > 0) {
$hexCode = bin2hex(fread($resource, $fileSize));
} else {
return $status = -1;
}
}
fclose($resource);
/* 通过匹配十六进制代码检测是否存在木马脚本*/
/* 匹配16进制中的 <% ( ) %> */
/* 匹配16进制中的 <? ( ) ?> */
/* 匹配16进制中的 <script | /script> 大小写亦可 */
if (preg_match("/(3c25.*?28.*?29.*?253e)|(3c3f.*?28.*?29.*?3f3e)|(3C534352495054)|(2F5343524950543E)|(3C736372697074)|(2F7363726970743E)/is", $hexCode)) {
$status = 1;
}
} else {
$status = -1;
}
return $status;
}
//图片上传云存储(阿里云)
public function aliyunupload($filename, $file_path)
{
$upload = new Uploadss;
$data = $upload->uploadFile($filename, $file_path);
return $data;
}
}

View File

@ -921,10 +921,19 @@ class User extends Base
->sum('change_money');
#配置
$rule = getConfig('base');
// $share_image = $this->product_img();
// 获取用户推广海报
$posterService = new \app\common\service\PosterService();
$posterResult = $posterService->getUserPoster($user_id);
$share_image = '';
if ($posterResult['status']) {
$share_image = $posterResult['data']['image_url'];
}
$new_data = [
'share_title' => $rule['share_title'],
'share_image' => '',
'share_image' => $share_image,
'count' => $data->total(),
'money' => $money,
'data' => $data->items(),

View File

@ -27,8 +27,8 @@ Route::any('getAccessTokenOffiaccountSign', 'Login/getAccessTokenOffiaccountSign
#============================
#Upload.php图片上传
#============================
Route::any('picture', 'Upload/picture');
Route::any('picture1', 'Upload/picture1');
// Route::any('picture', 'Upload/picture');
// Route::any('picture1', 'Upload/picture1');
#============================
#Index.php首页
#============================

View File

@ -0,0 +1,82 @@
<?php
namespace app\common\model;
class UserPosterCache extends Base
{
// 设置当前模型对应的完整数据表名称
protected $table = 'user_poster_cache';
// 设置字段信息
protected $schema = [
'id' => 'int',
'user_id' => 'int',
'app_id' => 'string',
'template_hash' => 'string',
'cos_url' => 'string',
'file_size' => 'int',
'mime_type' => 'string',
'status' => 'int',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'expires_at' => 'datetime',
];
/**
* 查找用户的有效海报缓存
*
* @param int $userId 用户ID
* @param string $templateHash 模板哈希
* @param string $appId 小程序APPID
* @return array|null 缓存记录
*/
public static function findValidCache($userId, $templateHash, $appId)
{
return self::where([
'user_id' => $userId,
'template_hash' => $templateHash,
'app_id' => $appId,
'status' => 1
])
->where('expires_at', '>', date('Y-m-d H:i:s'))
->find();
}
/**
* 失效指定用户的所有其他模板海报
*
* @param int $userId 用户ID
* @param string $currentTemplateHash 当前模板哈希
* @return bool 操作结果
*/
public static function invalidateOldCaches($userId, $currentTemplateHash)
{
return self::where('user_id', $userId)
->where('template_hash', '<>', $currentTemplateHash)
->update(['status' => 0]);
}
/**
* 清理过期的海报缓存记录
*
* @param int $limit 最大处理记录数
* @return int 清理的记录数
*/
public static function cleanExpiredCaches($limit = 100)
{
// 查询过期的缓存记录
$expiredCaches = self::where('expires_at', '<', date('Y-m-d H:i:s'))
->where('status', 1)
->limit($limit)
->select();
$cleanCount = 0;
foreach ($expiredCaches as $cache) {
$cache->status = 0;
$cache->save();
$cleanCount++;
}
return $cleanCount;
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace app\common\service;
use app\common\model\UserPosterCache;
use app\common\server\TencentCosUploader;
use app\common\server\Wx;
use app\common\server\autoload;
use think\facade\Log;
class PosterService
{
/**
* 获取或生成用户推广海报
*
* @param int $userId 用户ID
* @return array 海报信息 ['status' => bool, 'message' => string, 'data' => ['image_url' => string]]
*/
public function getUserPoster($userId)
{
try {
// 1. 验证用户ID
if ($userId <= 0) {
return ['status' => false, 'message' => '无效的用户ID'];
}
// 2. 获取系统配置
$config = getConfig('base');
$posterPath = $config['poster_template'] ?? '/img_poster.jpg';
$cacheExpire = $config['poster_cache_expire'] ?? (86400 * 100); // 默认缓存1天
// 3. 获取模板文件信息
$templateFile = getcwd() . $posterPath;
if (!file_exists($templateFile)) {
return ['status' => false, 'message' => '海报模板文件不存在'];
}
$templateHash = md5_file($templateFile);
// 4. 获取小程序配置
// 使用新的MiniprogramHelper获取当前域名对应的小程序配置
$miniprogram = \app\common\helper\MiniprogramHelper::getMiniprogramConfig();
$appId = $miniprogram['appid'] ?? '';
if (empty($appId)) {
$wechat_setting = getConfig("wechat_setting");
$appId = $wechat_setting['appid'] ?? '';
}
// 5. 查询缓存记录
$cacheRecord = UserPosterCache::findValidCache($userId, $templateHash, $appId);
// 6. 如果存在有效缓存,直接返回
if ($cacheRecord) {
return [
'status' => 1,
'message' => '海报获取成功',
'data' => ['image_url' => $cacheRecord['cos_url']]
];
}
// 7. 生成推广链接
$wxServer = new Wx(app());
$urlLink = $wxServer->generateUrlLinks($userId);
if (empty($urlLink)) {
return ['status' => false, 'message' => '生成推广链接失败'];
}
// 8. 生成海报图片
$autoload = new autoload();
$imageData = $autoload->generatePosterWithQR($templateFile, $urlLink);
if (!$imageData) {
return ['status' => false, 'message' => '海报生成失败'];
}
// 9. 上传到腾讯云COS
$cosConfig = getConfig('uploads');
$tencentUploader = new TencentCosUploader($cosConfig);
// 创建文件存储路径
$filePath = 'poster/' . date('Ymd') . '/' . $userId . '_' . substr($templateHash, 0, 8) . '_' . uniqid() . '.png';
try {
$uploadResult = $tencentUploader->uploadFile($imageData, $filePath);
$cosUrl = $uploadResult['full_url'];
$fileSize = strlen($imageData);
// 10. 存储缓存记录
$expiresAt = date('Y-m-d H:i:s', time() + $cacheExpire);
$cacheData = [
'user_id' => $userId,
'app_id' => $appId,
'template_hash' => $templateHash,
'cos_url' => $cosUrl,
'file_size' => $fileSize,
'mime_type' => 'image/png',
'status' => 1,
'expires_at' => $expiresAt
];
// 11. 失效该用户的其他海报缓存
UserPosterCache::invalidateOldCaches($userId, $templateHash);
// 12. 保存新缓存记录
$posterCache = new UserPosterCache();
$posterCache->save($cacheData);
// 13. 随机清理过期缓存
if (mt_rand(1, 100) <= 5) {
UserPosterCache::cleanExpiredCaches();
}
return [
'status' => true,
'message' => '海报生成成功',
'data' => ['image_url' => $cosUrl]
];
} catch (\Exception $e) {
Log::error('用户海报上传COS失败' . $e->getMessage());
return ['status' => false, 'message' => '海报上传失败'];
}
} catch (\Exception $e) {
Log::error('获取用户海报异常:' . $e->getMessage());
return ['status' => false, 'message' => '系统异常,请稍后重试'];
}
}
}