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