初始化项目:添加后端代码、ThinkPHP框架、前端资源
This commit is contained in:
@@ -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 "执行完毕";
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user