548 lines
18 KiB
PHP
548 lines
18 KiB
PHP
<?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;
|
||
}
|
||
} |