初始化项目:添加后端代码、ThinkPHP框架、前端资源

This commit is contained in:
amb
2026-09-03 12:42:39 +08:00
commit 482bece22e
3726 changed files with 416708 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace app\callback\controller;
/**
* 自动任务
*/
class Auto
{
public function handle(){
$token = input('token');
$data = \app\common\model\app\Config::getConfig('crontab');
if($data['token'] != $token){
exit("token错误");
}
//公众号 - 消息推送
(new \app\admin\library\app\msgpush\Msg)->handle();
//订单超时关闭
(new \app\callback\controller\Order)->ordertimeoutclose();
//实物商品自动确认收货
(new \app\callback\controller\app\physical\Physical)->autoConfirmReceive();
if(\app\admin\library\project\App::isInstall('test')){
//发布考试
(new \app\admin\model\app\test\Test)->timePush();
//自动交卷
(new \app\admin\model\app\test\Worklog)->timeSubmitWorklog();
}
//同步学习数据缓存到数据库
(new \app\common\model\user\Study())->batchSyncStudyCache();
//同步流量数据到数据库
(new \app\api\model\data\Traffic)->cacheToDatabses();
//清理已删除课程对应的阿里云互动消息群组
\app\common\library\live\message\Manager::cleanupOrphanGroups(50, false);
echo "执行完毕";
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace app\callback\controller;
use app\common\model\live\Room;
/**
* 直播回调
*/
class Live
{
/**
* 阿里云视频点播HTTP回调回调鉴权
* @return bool
*/
public function checkVodCallbackAuth(){
$timestamp = request()->header('X-VOD-TIMESTAMP');
$signature = request()->header('X-VOD-SIGNATURE');
$callbackUrl = \app\common\library\live\Url::getCallbackUrl();
$vodConfig = \app\common\model\config\System::getConfig('alivod');
$authKey = $vodConfig['auth_key'];
if(md5($callbackUrl."|".$timestamp."|".$authKey) != $signature){
return false;
}
return true;
}
/**
* 直播回放录制文件回调接口
* 这里回调分为两种
* 一个是阿里云直播自动录制中在OSS录制中设置的回调接口 用$backData接收
* 另一种是在视频点播中设置的回调接口 用@$postData 参数接受
* @return void
*/
public function playback(){
$uniacid = input('i');
$postData = input('post.');
\think\Log::info('playback data:'.json_encode($postData));
if(isset($postData['oss_bucket'])){
return 'error_no_vod_data';
}
if(isset($postData['EventType']) && isset($postData['VideoId'])){
$sourceType = 'alivod';
}else{
$sourceType = 'alioss';
}
if($sourceType == 'alioss' && (!isset($postData['push_args']) || !isset($postData['push_args']['callback_record_key']))){
return 'error_no_data';
}
if($sourceType == 'alivod'){
if(!$this->checkVodCallbackAuth()){
return 'false_fail_auth';
}
if($postData['EventType'] != 'AddLiveRecordVideoComplete'){
return 'error_event_type';
}
$roomCondition = [
'stream_name'=>$postData['StreamName'],
'uniacid'=>$uniacid
];
}else{
$roomCondition = [
'live_record_key'=>$postData['push_args']['callback_record_key'],
'uniacid'=>$uniacid
];
}
$roomData = \app\common\model\live\Room::where($roomCondition)->find();
if(!$roomData){
return 'error_no_room';
}
//写入回放视频列表
if($sourceType == 'alivod'){
$playbackCondition = [
'course_id'=>$roomData['course_id'],
'file_path'=>'<alivod>'.$postData['VideoId'].'</alivod>'
];
}else{
$playbackCondition = [
'file_path'=>'/'.$postData['uri'],
'course_id'=>$roomData['course_id'],
];
}
if(\app\common\model\live\Playback::where($playbackCondition)->find()){
return 'error_repeat';
}
$data = [
'uniacid'=>$roomData['uniacid'],
'course_id'=>$roomData['course_id'],
'status'=>1,
'createtime'=>time()
];
if($sourceType == 'alivod'){
$data['start_time'] = strtotime($postData['RecordStartTime']);
$data['end_time'] = strtotime($postData['RecordEndTime']);
}else{
$data['start_time'] = $postData['start_time'];
$data['end_time'] = $postData['stop_time'];
}
$data['file_name'] = date('h:i',$data['start_time']) . '~' . date('h:i',$data['end_time'])."录制";
$data['source'] = $sourceType;
if($sourceType == 'alioss'){
$data['file_path'] = '/'.$postData['uri'];
}else{
$data['file_path'] = '<alivod>'.$postData['VideoId'].'</alivod>';
$vodConfig = \app\common\model\config\System::getConfig('alivod');
if($vodConfig && $vodConfig['status'] == 'open'){
try{
\addons\alivod\library\Alivod::submitTranscodeJobs($postData['VideoId'],$vodConfig['template_group_id']);
}catch (\Exception $e){}
}
}
\app\common\model\live\Playback::insert($data);
return 'success';
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace app\callback\controller;
/**
* 订单
*/
class Order
{
/**
* 订单超时关闭
* @return bool
*/
public function ordertimeoutclose()
{
$orderConfig = \app\common\model\app\Config::getConfig('order');
$orderModel = new \app\admin\model\order\Order;
$list = $orderModel->where([
'status'=>\app\common\constant\order\Status::STATUS_UNPAID,
'createtime'=>['<',time() - $orderConfig['order_timeout_close']]
])->field(['id','order_no'])->select();
foreach ($list as $item) {
$orderModel->where([
'id'=>$item['id']
])->update([
'status'=>\app\common\constant\order\Status::STATUS_CLOSE
]);
$orderNo = $item['order_no'];
//作废分佣金记录
\think\Hook::exec('app\\common\\behavior\\app\\agent\\Commission','cancelOrder',$orderNo);
\app\common\model\order\Log::set($item['id'],"订单超时关闭");
}
return 'ok';
}
}
@@ -0,0 +1,221 @@
<?php
namespace app\callback\controller\app;
use think\Log;
use TuzhiEasyWeChat\Kernel\Support\XML;
/**
* 微信小程序消息推送回调
* 处理虚拟支付相关的消息推送(道具发货、代币支付、退款、投诉)
*/
class Wxapp
{
/**
* 消息推送入口
* GET请求:验证URL有效性(echostr验证)
* POST请求:接收微信推送的事件消息
* @return string
*/
public function notify()
{
$uniacid = input('uniacid', 0);
if ($uniacid) {
define('UNIACID', intval($uniacid));
}
$wxConfig = \app\common\model\config\System::getConfig('wxMiniProgram');
$msgPushConfig = \app\common\model\config\System::getConfig('wxMiniProgramMsgPush');
if (!$msgPushConfig || empty($msgPushConfig['msg_push_token'])) {
Log::error('[WxappNotify] 消息推送配置不完整');
return 'config error';
}
$token = $msgPushConfig['msg_push_token'];
$appId = $wxConfig['app_id'];
try {
if (request()->isGet()) {
return $this->verifyUrl($token);
}
return $this->handleMessage();
} catch (\Exception $e) {
Log::error('[WxappNotify] 处理异常: ' . $e->getMessage());
return $this->buildResponse(0, 'error');
}
}
/**
* URL验证(GET请求)
* 微信服务器发送验证请求时,校验signature并返回echostr
* @param string $token 验证Token
* @return string
*/
private function verifyUrl($token)
{
$signature = input('signature');
$timestamp = input('timestamp');
$nonce = input('nonce');
$echostr = input('echostr', '');
$tmpArr = [$token, $timestamp, $nonce];
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr === $signature) {
echo $echostr;
exit;
}
echo 'Invalid signature';
exit;
}
/**
* 处理推送消息(POST请求)
* 明文模式:先校验消息签名,再解析消息内容,根据事件类型分发到对应处理器
* @return string
*/
private function handleMessage()
{
$content = file_get_contents('php://input');
if (empty($content)) {
Log::error('[WxappNotify] 未收到消息内容');
return $this->buildResponse(0, 'no content');
}
$message = $this->parseMessage($content);
if (empty($message)) {
Log::error('[WxappNotify] 解析消息失败');
return $this->buildResponse(0, 'parse error');
}
if (!is_array($message)) {
Log::error('[WxappNotify] 消息格式错误: 期望数组类型, 实际类型:' . gettype($message) . ', content=' . $content);
return $this->buildResponse(0, 'invalid message type');
}
if (!isset($message['Event'])) {
Log::error('[WxappNotify] 消息格式错误或缺少Event字段: ' . json_encode($message, JSON_UNESCAPED_UNICODE));
return $this->buildResponse(0, 'invalid message');
}
if (!$this->verifyPushSignature($message)) {
Log::error('[WxappNotify] 消息签名校验失败');
return $this->buildResponse(0, 'invalid signature');
}
$event = $message['Event'];
Log::info('[WxappNotify] 收到事件: ' . $event . ', message=' . json_encode($message, JSON_UNESCAPED_UNICODE));
try {
$handler = new \app\callback\library\app\wxapp\Notify();
$result = $handler->handle($event, $message);
if ($result !== null && is_array($result)) {
return json_encode($result, JSON_UNESCAPED_UNICODE);
}
return $this->buildSuccessResponse();
} catch (\Exception $e) {
Log::error('[WxappNotify] 事件处理失败: ' . $e->getMessage());
return $this->buildResponse(0, $e->getMessage());
}
}
/**
* 校验微信推送消息签名
* 使用 msg_push_token 对消息内容进行签名校验,防止伪造推送
* @param array $message 解析后的消息数组
* @return bool
*/
private function verifyPushSignature($message)
{
$msgPushConfig = \app\common\model\config\System::getConfig('wxMiniProgramMsgPush');
$token = $msgPushConfig['msg_push_token'] ?? '';
if (empty($token)) {
Log::error('[WxappNotify] 消息推送Token未配置,无法校验签名');
return false;
}
$signature = input('signature');
$timestamp = input('timestamp');
$nonce = input('nonce');
if (empty($signature) || empty($timestamp) || empty($nonce)) {
Log::error('[WxappNotify] 缺少签名参数');
return false;
}
$tmpArr = [$token, $timestamp, $nonce];
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$expectedSignature = sha1($tmpStr);
if (!hash_equals($expectedSignature, $signature)) {
Log::error('[WxappNotify] 签名不匹配: expected=' . $expectedSignature . ', received=' . $signature);
return false;
}
$timestampInt = intval($timestamp);
$now = time();
if (abs($now - $timestampInt) > 300) {
Log::error('[WxappNotify] 消息时间戳已过期: timestamp=' . $timestampInt . ', now=' . $now . ', diff=' . abs($now - $timestampInt));
return false;
}
return true;
}
/**
* 解析消息内容(支持XML和JSON格式)
* @param string $content 原始消息内容
* @return array|null
*/
private function parseMessage($content)
{
if (empty($content)) {
return null;
}
if (0 === stripos($content, '<')) {
$result = XML::parse($content);
if (is_array($result)) {
return $result;
}
Log::warning('[WxappNotify] XML解析结果非数组类型: ' . gettype($result));
return null;
}
$data = json_decode($content, true);
if ($data && is_array($data) && json_last_error() === JSON_ERROR_NONE) {
return $data;
}
return null;
}
/**
* 构建成功响应(明文模式)
* @return string
*/
private function buildSuccessResponse()
{
return json_encode(['ErrCode' => 0, 'ErrMsg' => 'success'], JSON_UNESCAPED_UNICODE);
}
/**
* 构建错误响应
* @param int $errCode 错误码
* @param string $errMsg 错误信息
* @return string
*/
private function buildResponse($errCode, $errMsg = '')
{
return json_encode(['ErrCode' => $errCode, 'ErrMsg' => $errMsg], JSON_UNESCAPED_UNICODE);
}
}
@@ -0,0 +1,17 @@
<?php
namespace app\callback\controller\app\msgpush;
/**
* 直播回调
*/
class Notice
{
/**
* @return bool
*/
public function handle()
{
(new \app\admin\library\app\msgpush\Msg)->handle();
}
}
@@ -0,0 +1,40 @@
<?php
namespace app\callback\controller\app\physical;
/**
* 实物商品订单
*/
class Physical
{
/**
* 实物商品自动确认收货
* @return bool
*/
public function autoConfirmReceive()
{
$physicalConfig = \app\common\model\app\Config::getConfig('physical');
$autoConfirmDays = $physicalConfig['auto_confirm_days'] ?? 7;
$showSales = $physicalConfig['show_sales'] ?? 1;
$autoConfirmTime = $autoConfirmDays * 86400;
$orderModel = new \app\admin\model\order\Order;
$list = $orderModel->where([
'order_type' => 'physical',
'status' => \app\common\constant\order\Status::STATUS_UNRECEIVE,
'createtime' => ['<', time() - $autoConfirmTime]
])->field(['id', 'order_no'])->select();
foreach ($list as $item) {
$orderModel->where([
'id' => $item['id']
])->update([
'status' => \app\common\constant\order\Status::STATUS_SUCCESS
]);
\app\common\model\order\Log::set($item['id'], "系统自动确认收货");
}
return 'ok';
}
}