初始化项目:添加后端代码、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
@@ -0,0 +1,548 @@
<?php
namespace app\common\model\user;
use think\Model;
use app\common\model\order\Item as OrderItem;
use app\api\model\course\ColumnBind;
use app\admin\model\course\Course as CourseModel;
use think\Cache;
/**
* 用户已购课程
*/
class Subscription extends Model
{
protected $uniacid = false;
// 表名
protected $name = 'subscription';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
/**
* 获取订阅权限
* 说明:
* - 历史逻辑会对免费课程或价格为0的课程直接返回 true,导致用户无需任何动作即可观看。
* - 现调整为:免费课程也必须由用户主动点击【加入学习】生成订阅记录(get_type=free)后才视为已订阅。
* - 付费会员卡免费权益命中(include 全部课程作用域 / 指定课程作用域)同样要求用户点击【加入学习】,
* 生成 get_type=vip 的订阅记录后才视为已订阅;当 VIP 权益失效时,该记录会被 getUserCourse 自动忽略。
* - 当课程从免费变更为收费/其他模式时,get_type=free 的订阅记录会被 getUserCourse 自动忽略,从而失效。
* @return bool
*/
public static function getSubscriptionAuth($userId, $courseId)
{
//判断课程是否存在
$buyData = \app\common\model\course\Course::getPrice($courseId);
if (!$buyData) {
return false;
}
$userGotCourse = self::getUserCourse($userId, $courseId);
return $userGotCourse;
}
/**
* 用户在当前是否拥有「通过付费会员卡免费权益免费观看该课程」的资格
* 说明:仅作为「加入学习」资格判断,不直接代表已订阅
* @param int $userId
* @param int $courseId
* @return bool
*/
public static function getVipFreeAccess($userId, $courseId)
{
$vipConfig = \app\common\model\app\Config::getConfig('vip');
if (!$vipConfig || $vipConfig['status'] != 1) {
return false;
}
$userVips = \app\common\model\app\vip\CardUser::getUserAllVipInfo($userId);
if (!$userVips) {
return false;
}
foreach ($userVips as $userVip) {
//会员卡未开启免费权益时跳过
if (empty($userVip['card']) || $userVip['card']['privilege_free'] != 1) {
continue;
}
$freeScope = isset($userVip['card']['privilege_free_scope'])
? $userVip['card']['privilege_free_scope']
: 'specify';
if ($freeScope == 'all') {
//全部课程作用域:仅 article/video/audio/live/column 类型的课程享受
if (\app\common\model\app\vip\CardPrivilege::isCourseInScopeAll($courseId)) {
return true;
}
continue;
}
//指定课程作用域:保持原逻辑
$privilegeGoodsIds = \app\common\model\app\vip\CardPrivilege::getPrivilegeGoodsIds($userVip['card_id'], 'free');
if ($privilegeGoodsIds && in_array($courseId, $privilegeGoodsIds)) {
return true;
}
}
return false;
}
/**
* 判断课程当前是否为免费
* pay_type=free 或 pay_type=pay 且价格为 0 视为免费
* @param $courseId
* @return bool
*/
public static function isFreeCourse($courseId)
{
$buyData = \app\common\model\course\Course::getPrice($courseId);
if (!$buyData) {
return false;
}
if ($buyData['pay_type'] == 'free') {
return true;
}
if ($buyData['pay_type'] == 'pay' && floatval($buyData['price']) == 0) {
return true;
}
return false;
}
/**
* 获取用户购买的课程
* 未购买 或 超过有效期 返回false
* @param $userId
* @param $courseId
* @return bool
*/
public static function getUserCourse($userId, $courseId)
{
$userInfo = \app\common\model\User::getUserInfo($userId);
if (!$userInfo) {
return false;
}
//当前课程是否仍为免费(pay_type=free 或 pay_type=pay 且价格为 0
//仅当课程仍为免费时,get_type=free 的订阅记录才有效
$isFreeCourse = self::isFreeCourse($courseId);
$mustCondition = [
'course_id' => $courseId,
'uniacid' => UNIACID,
'validity_time' => ['>', time()]
];
if (!$isFreeCourse) {
//课程当前非免费时,过滤掉通过【加入学习】获得的 get_type=free 的记录
$mustCondition['get_type'] = ['<>', 'free'];
}
//判断是否订阅(同时返回所有可能的命中记录,以便对 get_type=vip 做权益失效判断)
$list = self::whereOr(function ($query) use ($mustCondition, $userInfo) {
if( $userInfo['mobile']){
$query->where($mustCondition)->where('mobile', $userInfo['mobile']);
}else{
$query->where($mustCondition)->where('mobile', '<>', null);
}
})->whereOr(function ($query) use ($mustCondition, $userInfo) {
$query->where($mustCondition)->where(['user_id' => $userInfo['id']]);
})->select();
if ($list) {
//缓存 VIP 免费权益命中结果,避免多条记录重复计算
$vipFreeAccessChecked = false;
$vipFreeAccess = false;
foreach ($list as $item) {
if ($item['get_type'] == 'vip') {
//get_type=vip 的记录需当前用户 VIP 免费权益仍命中该课程才视为有效
if (!$vipFreeAccessChecked) {
$vipFreeAccess = self::getVipFreeAccess($userId, $courseId);
$vipFreeAccessChecked = true;
}
if (!$vipFreeAccess) {
continue;
}
}
return true;
}
}
//判断是否已订阅该课程所属的专栏
$columnBindIds = ColumnBind::where([
'uniacid' => UNIACID,
'course_id' => $courseId,
'status'=>1,
'type'=>1 //类型是课程才可用,目录不算
])->field("column_id")->select();
$data = self::where([
'user_id' => $userId,
'uniacid' => UNIACID,
'course_id' => ['in', array_column($columnBindIds, 'column_id')],
'validity_time' => ['>', time()]
])->find();
if ($data) {
return true;
}
foreach ($columnBindIds as $column) {
if (self::getSubscriptionAuth($userId, $column['column_id'])) {
return true;
}
//判断是否在专栏内开启了试看
$isTry = ColumnBind::where([
'uniacid' => UNIACID,
'course_id' => $courseId,
'column_id' => $column['column_id'],
'try' => 1
])->field("try")->find();
if ($isTry) {
return true;
}
}
return false;
}
public function course()
{
return $this->belongsTo('\app\common\model\course\Course', 'course_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
public function user()
{
return $this->belongsTo('\app\admin\model\User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
public function mobileuser()
{
return $this->belongsTo('\app\admin\model\User', 'mobile', 'mobile', [], 'LEFT')->setEagerlyType(0);
}
/**
* 构建手机号搜索过滤参数
* 将 user.mobile 过滤条件从 filter/op 中提取并移除,
* 以便在控制器中扩展为同时匹配 subscription.mobile 或 user.mobile
* @param array $filter 过滤条件数组
* @param array $op 操作符数组
* @return array [手机号关键词, 修改后的filter, 修改后的op]
*/
public static function buildMobileFilterParams(array $filter, array $op)
{
$mobile = '';
if (isset($filter['user.mobile']) && $filter['user.mobile'] !== '') {
$mobile = trim($filter['user.mobile']);
unset($filter['user.mobile'], $op['user.mobile']);
}
return [$mobile, $filter, $op];
}
/**
* 获取课程列表
* @param $userId 分组ID
* @param $params 分页信息 page页码 limit获取数量
* @return array
*/
public function getMyCourse($userId = 0, $params = [])
{
$userInfo = \app\common\model\User::getUserInfo($userId);
if (!$userInfo) {
return [];
}
$rows = self::with(['course']);
$mustCondition = [
'subscription.uniacid' => UNIACID,
'course.id'=>['<>', 'null']
];
if ($params['type'] != 'all') {
$mustCondition['course.type'] = $params['type'];
}
//判断是否订阅
//要同时查UserId与Mobile
$list = $rows->whereOr(function ($query) use ($mustCondition, $userInfo) {
if( $userInfo['mobile']){
$query->where($mustCondition)->where('mobile', $userInfo['mobile']);
}else{
$query->where($mustCondition)->where('mobile', '<>', null);
}
// $query->where($mustCondition)->where('mobile', $userInfo['mobile'])->where('mobile', '<>', null);
})->whereOr(function ($query) use ($mustCondition, $userInfo) {
$query->where($mustCondition)->where(['user_id' => $userInfo['id']]);
})
->limit($params['limit'])->page($params['page'])
->order('createtime', 'desc')
->select();
$data = [];
if (!empty($list)) {
foreach ($list as $item) {
//课程从免费变为收费/其他时,通过【加入学习】获得的订阅记录视为失效,不展示
if ($item['get_type'] == 'free' && !self::isFreeCourse($item['course_id'])) {
continue;
}
//VIP 免费权益失效(卡过期/取消权益/范围调整)时,对应订阅记录视为失效,不展示
if ($item['get_type'] == 'vip' && !self::getVipFreeAccess($item['user_id'], $item['course_id'])) {
continue;
}
$item->getRelation('course')->visible(['name', 'cover', 'type', 'id']);
$data[] = $item;
}
}
return $data;
}
/**
* 获取有效时间
* @param array $courseInfo 课程信息
* @param int $historyTime 历史订阅的时间,如果是指定天数,那么在原有时长的基础上累加
* @param array|null $validityOverride 有效期覆盖配置(来自组合商品规格项),格式:['validity_type'=>int, 'validity_value'=>string]
* @return int
*/
public function getValidityTime($courseInfo, $historyTime = 0, $validityOverride = null)
{
// 如果有覆盖配置(组合商品自定义有效期),优先使用
if ($validityOverride && !empty($validityOverride['validity_type'])) {
$overrideType = intval($validityOverride['validity_type']);
$overrideValue = $validityOverride['validity_value'] ?? '';
switch ($overrideType) {
case 1:
// 与商品原有效期一致,走课程自身逻辑
break;
case 2:
// 长期有效
return \app\common\constant\course\Subscription::VALIDITY_ETERNITY;
case 3:
// 固定天数
$days = intval($overrideValue);
if ($historyTime && $historyTime > time()) {
return $historyTime + ($days * 86400);
}
return time() + ($days * 86400);
case 4:
// 固定截止日期
return is_numeric($overrideValue) ? intval($overrideValue) : strtotime($overrideValue);
}
}
// 默认使用课程自身的有效期配置
if ($courseInfo['validity_type'] == 'long') {
$validityTime = \app\common\constant\course\Subscription::VALIDITY_ETERNITY;
} elseif ($courseInfo['validity_type'] == 'fix') {
$validityTime = $courseInfo['validity_fix_time'];
} else {
if ($historyTime && $historyTime > time()) {
$validityTime = $historyTime + ($courseInfo['validity_diy_time'] * 86400);
} else {
$validityTime = time() + ($courseInfo['validity_diy_time'] * 86400);
}
}
return $validityTime;
}
/**
* 增加用户购买课程
* @param int $userId 用户ID
* @param int $courseId 课程ID
* @param string $getType 获取方式
* @param string $mobile 手机号
* @param array|null $validityOverride 有效期覆盖配置(来自组合商品规格项),格式:['validity_type'=>int, 'validity_value'=>string]
* @return void
*/
public function setUserCourse($userId, $courseId, $getType = '',$mobile='', $validityOverride = null)
{
$courseInfo = CourseModel::where([
'id' => $courseId,
'status' => 1,
'uniacid' => UNIACID
])->find();
if (!$courseInfo) {
return false;
}
if (!$getType) {
$getType = $courseInfo['pay_type'];
}
$row = [
'user_id' => $userId,
'mobile'=>$mobile,
'course_id' => $courseId,
'uniacid' => UNIACID,
'get_type' => $getType,
'createtime' => time()
];
$subInfo = $this->getSubscriptionInfo($userId,$mobile,$courseId);
if ($subInfo) {
$row['validity_time'] = $this->getValidityTime($courseInfo, $subInfo['validity_time'], $validityOverride);
$where = [
'course_id' => $courseId
];
if($userId){
$where['user_id'] = $userId;
}else{
$where['mobile'] = $mobile;
}
self::where($where)->update($row);
} else {
$row['validity_time'] = $this->getValidityTime($courseInfo, 0, $validityOverride);
self::insert($row);
}
if ($row['validity_time'] < time()) {
return false;
}
return true;
}
/**
* 获取订阅信息
* @param $userId
* @param $mobile
* @param $courseId
* @return mixed
*/
protected function getSubscriptionInfo($userId,$mobile,$courseId){
$subInfo = false;
if($mobile){
//手机号订阅
//判断有没有订阅过
$subInfo = self::where([
'mobile' => $mobile,
'course_id' => $courseId
])->find();
if(!$subInfo){
$userInfo = \app\common\model\User::where([
'mobile'=>$mobile
])->find();
if($userInfo){
$userId = $userInfo['id'];
//判断有没有订阅过
$subInfo = self::where([
'user_id' => $userId,
'course_id' => $courseId
])->find();
}
}
}else{
//判断有没有订阅过
$subInfo = self::where([
'user_id' => $userId,
'course_id' => $courseId
])->find();
}
return $subInfo;
}
/**
* 取消用户订阅项目
* @param $userId
* @param $goodsId
* @param $goodsType
* @return void
*/
public static function cancelUserSubscription($userId, $goodsId, $goodsType,$extendData='')
{
switch ($goodsType) {
case 'activity':
\app\api\model\app\activity\UserTicket::where([
'uniacid'=>UNIACID,
'user_id'=>$userId,
'activity_id'=>$goodsId,
'ticket_no'=>$extendData
])->update([
'status'=>2
]);
break;
case 'course':
case 'column':
self::where([
'user_id'=>$userId,
'course_id'=>$goodsId,
'uniacid'=>UNIACID
])->update([
'validity_time'=>time()
]);
break;
case 'exercises':
\app\common\model\app\exam\ExercisesSubscribe::where([
'user_id'=>$userId,
'exercises_id'=>$goodsId,
'uniacid'=>UNIACID
])->delete();
break;
case 'vipcard':
$extendData = explode("_",$extendData);
if(!$extendData){
return false;
}
$cardUserData = \app\common\model\app\vip\CardUser::where([
'user_id'=>$userId,
'card_id'=>$goodsId,
'uniacid'=>UNIACID
])->find();
if(!$cardUserData){
return false;
}
$vipTime = $extendData[1] * 86400;
$endtime = ($cardUserData['end_time'] - $vipTime) > time() ? time() + ($cardUserData['end_time'] - $vipTime) : time();
\app\common\model\app\vip\CardUser::where([
'id'=>$cardUserData['id'],
'uniacid'=>UNIACID
])->update([
'updatetime'=>time(),
'end_time'=>$endtime
]);
break;
}
return true;
}
}