1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-03 07:26:49 +08:00

Merge branch 'koogua/v1.5.4'

# Conflicts:
#	app/Library/AppInfo.php
This commit is contained in:
koogua 2022-06-16 09:24:01 +08:00
commit d1917781fb
51 changed files with 597 additions and 251 deletions

View File

@ -1,3 +1,16 @@
### [v1.5.4](https://gitee.com/koogua/course-tencent-cloud/releases/v1.5.4)(2022-06-15)
- 增加migration助手SettingTrait
- 增加积分兑换会员
- 增加ISP备案和电子执照配置
- 增加获取视频时长补偿机制
- 优化课程和套餐发货
- 优化验证码
- 优化视频点播回调处理任务
- 优化章节排序初始值和步长
- 优化后台视频上传和转码
- 修正获取子分类查询条件
### [v1.5.3](https://gitee.com/koogua/course-tencent-cloud/releases/v1.5.3)(2022-05-30)
- 优化章节排序初始值和步长

View File

@ -84,9 +84,9 @@ class CategoryTreeList extends Builder
{
$query = CategoryModel::query();
$query->where('published = 1');
$query->where('deleted = 0');
$query->andWhere('parent_id = :parent_id:', ['parent_id' => $parentId]);
$query->where('parent_id = :parent_id:', ['parent_id' => $parentId]);
$query->andWhere('published = 1');
$query->andWhere('deleted = 0');
$query->orderBy('priority ASC');
return $query->execute();

View File

@ -20,23 +20,23 @@ class CategoryList extends Cache
return $this->lifetime;
}
public function getKey($type = null)
public function getKey($id = null)
{
return "category_list:{$type}";
return "category_list:{$id}";
}
/**
* @param null $type
* @param null $id
* @return array
*/
public function getContent($type = null)
public function getContent($id = null)
{
/**
* @var Resultset $categories
*/
$categories = CategoryModel::query()
->columns(['id', 'parent_id', 'name', 'priority', 'level', 'path'])
->where('type = :type:', ['type' => $type])
->where('type = :type:', ['type' => $id])
->andWhere('published = 1')
->andWhere('deleted = 0')
->orderBy('level ASC, priority ASC')

View File

@ -19,16 +19,16 @@ class CategoryTreeList extends Cache
return $this->lifetime;
}
public function getKey($type = null)
public function getKey($id = null)
{
return "category_tree_list:{$type}";
return "category_tree_list:{$id}";
}
public function getContent($type = null)
public function getContent($id = null)
{
$builder = new CategoryTreeListBuilder();
$list = $builder->handle($type);
$list = $builder->handle($id);
return $list ?: [];
}

View File

@ -145,6 +145,33 @@ class DeliverTask extends Task
$service = new VipDeliverService();
$service->handle($vip, $user);
/**
* 先下单购买课程,发现会员有优惠,于是购买会员,再回头购买课程
* 自动关闭未支付订单,让用户可以使用会员价再次下单
*/
$this->closePendingOrders($user->id);
}
protected function closePendingOrders($userId)
{
$orders = $this->findUserPendingOrders($userId);
if ($orders->count() == 0) return;
$itemTypes = [
OrderModel::ITEM_COURSE,
OrderModel::ITEM_PACKAGE,
];
foreach ($orders as $order) {
$case1 = in_array($order->item_type, $itemTypes);
$case2 = $order->promotion_type == 0;
if ($case1 && $case2) {
$order->status = OrderModel::STATUS_CLOSED;
$order->update();
}
}
}
protected function handleOrderConsumePoint(OrderModel $order)
@ -209,6 +236,20 @@ class DeliverTask extends Task
]);
}
/**
* @param int $userId
* @return ResultsetInterface|Resultset|OrderModel[]
*/
protected function findUserPendingOrders($userId)
{
$status = OrderModel::STATUS_PENDING;
return OrderModel::query()
->where('owner_id = :owner_id:', ['owner_id' => $userId])
->andWhere('status = :status:', ['status' => $status])
->execute();
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset|TaskModel[]

View File

@ -7,17 +7,16 @@
namespace App\Console\Tasks;
use App\Models\CourseUser as CourseUserModel;
use App\Models\ImGroupUser as ImGroupUserModel;
use App\Models\PointGift as PointGiftModel;
use App\Models\PointGiftRedeem as PointGiftRedeemModel;
use App\Models\Task as TaskModel;
use App\Repos\Course as CourseRepo;
use App\Repos\CourseUser as CourseUserRepo;
use App\Repos\ImGroup as ImGroupRepo;
use App\Repos\ImGroupUser as ImGroupUserRepo;
use App\Repos\PointGift as PointGiftRepo;
use App\Repos\PointGiftRedeem as PointGiftRedeemRepo;
use App\Repos\User as UserRepo;
use App\Repos\Vip as VipRepo;
use App\Services\Logic\Deliver\CourseDeliver as CourseDeliverService;
use App\Services\Logic\Deliver\VipDeliver as VipDeliverService;
use App\Services\Logic\Notice\DingTalk\PointGiftRedeem as PointGiftRedeemNotice;
use App\Services\Logic\Point\History\PointGiftRefund as PointGiftRefundPointHistory;
use Phalcon\Mvc\Model\Resultset;
@ -52,6 +51,9 @@ class PointGiftDeliverTask extends Task
case PointGiftModel::TYPE_COURSE:
$this->handleCourseRedeem($redeem);
break;
case PointGiftModel::TYPE_VIP:
$this->handleVipRedeem($redeem);
break;
case PointGiftModel::TYPE_GOODS:
$this->handleGoodsRedeem($redeem);
break;
@ -112,54 +114,52 @@ class PointGiftDeliverTask extends Task
throw new \RuntimeException('Course Not Found');
}
$groupRepo = new ImGroupRepo();
$redeem->status = PointGiftRedeemModel::STATUS_FINISHED;
$group = $groupRepo->findByCourseId($course->id);
if (!$group) {
throw new \RuntimeException('Im Group Not Found');
if ($redeem->update() === false) {
throw new \RuntimeException('Update Point Redeem Status Failed');
}
$courseUserRepo = new CourseUserRepo();
$userRepo = new UserRepo();
$courseUser = $courseUserRepo->findCourseUser($course->id, $redeem->user_id);
$user = $userRepo->findById($redeem->user_id);
if (!$courseUser) {
$deliverService = new CourseDeliverService();
$courseUser = new CourseUserModel();
$courseUser->user_id = $redeem->user_id;
$courseUser->course_id = $course->id;
$courseUser->expiry_time = strtotime("+{$course->study_expiry} months");
$courseUser->role_type = CourseUserModel::ROLE_STUDENT;
$courseUser->source_type = CourseUserModel::SOURCE_POINT_REDEEM;
if ($courseUser->create() === false) {
throw new \RuntimeException('Create Course User Failed');
}
$deliverService->handle($course, $user);
}
$groupUserRepo = new ImGroupUserRepo();
protected function handleVipRedeem(PointGiftRedeemModel $redeem)
{
$giftRepo = new PointGiftRepo();
$groupUser = $groupUserRepo->findGroupUser($group->id, $redeem->user_id);
$gift = $giftRepo->findById($redeem->gift_id);
if (!$groupUser) {
$groupUser = new ImGroupUserModel();
$groupUser->group_id = $group->id;
$groupUser->user_id = $redeem->user_id;
if ($groupUser->create() === false) {
throw new \RuntimeException('Create Group User Failed');
if (!$gift) {
throw new \RuntimeException('Gift Not Found');
}
$vipRepo = new VipRepo();
$vip = $vipRepo->findById($gift->attrs['id']);
if (!$vip) {
throw new \RuntimeException('Vip Not Found');
}
$redeem->status = PointGiftRedeemModel::STATUS_FINISHED;
if ($redeem->update() === false) {
throw new \RuntimeException('Update Redeem Status Failed');
throw new \RuntimeException('Update Point Redeem Status Failed');
}
$userRepo = new UserRepo();
$user = $userRepo->findById($redeem->user_id);
$deliverService = new VipDeliverService();
$deliverService->handle($vip, $user);
}
protected function handleGoodsRedeem(PointGiftRedeemModel $redeem)

View File

@ -25,19 +25,25 @@ class VodEventTask extends Task
foreach ($events as $event) {
$handles[] = $event['EventHandle'];
$result = true;
if ($event['EventType'] == 'NewFileUpload') {
$this->handleNewFileUploadEvent($event);
$result = $this->handleNewFileUploadEvent($event);
} elseif ($event['EventType'] == 'ProcedureStateChanged') {
$this->handleProcedureStateChangedEvent($event);
$result = $this->handleProcedureStateChangedEvent($event);
} elseif ($event['EventType'] == 'FileDeleted') {
$this->handleFileDeletedEvent($event);
$result = $this->handleFileDeletedEvent($event);
}
if ($result) {
$handles[] = $event['EventHandle'];
}
}
if (count($handles) > 0) {
$this->confirmEvents($handles);
}
}
protected function handleNewFileUploadEvent($event)
{
@ -46,68 +52,75 @@ class VodEventTask extends Task
$height = $event['FileUploadEvent']['MetaData']['Width'] ?? 0;
$duration = $event['FileUploadEvent']['MetaData']['Duration'] ?? 0;
if ($fileId == 0) return;
if ($fileId == 0) return false;
$chapterRepo = new ChapterRepo();
$chapter = $chapterRepo->findByFileId($fileId);
if (!$chapter) return;
if (!$chapter) return false;
$attrs = $chapter->attrs;
/**
* 获取不到时长视为失败
* 获取不到时长,尝试通过主动查询获取
*/
if ($duration == 0) {
$attrs['file']['status'] = ChapterModel::FS_FAILED;
$chapter->update(['attrs' => $attrs]);
return;
$duration = $this->getFileDuration($fileId);
}
$isVideo = $width > 0 && $height > 0;
$vodService = new VodService();
if ($width == 0 && $height == 0) {
$vodService->createTransAudioTask($fileId);
} else {
if ($duration > 0) {
if ($isVideo) {
$vodService->createTransVideoTask($fileId);
} else {
$vodService->createTransAudioTask($fileId);
}
$attrs['file']['status'] = ChapterModel::FS_TRANSLATING;
} else {
$attrs['file']['status'] = ChapterModel::FS_FAILED;
}
$attrs['file']['status'] = ChapterModel::FS_TRANSLATING;
$attrs['duration'] = (int)$duration;
$chapter->update(['attrs' => $attrs]);
$chapter->attrs = $attrs;
$this->updateVodAttrs($chapter);
$chapter->update();
$this->updateCourseVodAttrs($chapter->course_id);
return true;
}
protected function handleProcedureStateChangedEvent($event)
{
$fileId = $event['ProcedureStateChangeEvent']['FileId'] ?? 0;
if ($fileId == 0) return;
if ($fileId == 0) return false;
$chapterRepo = new ChapterRepo();
$chapter = $chapterRepo->findByFileId($fileId);
if (!$chapter) return;
if (!$chapter) return false;
$attrs = $chapter->attrs;
$processResult = $event['ProcedureStateChangeEvent']['MediaProcessResultSet'] ?? [];
/**
* 获取不到处理结果视为失败
* 获取不到时长,尝试通过接口获得
*/
if (empty($processResult)) {
$attrs['file']['status'] = ChapterModel::FS_FAILED;
$chapter->update(['attrs' => $attrs]);
return;
if ($attrs['duration'] == 0) {
$attrs['duration'] = $this->getFileDuration($fileId);
}
$failCount = $successCount = 0;
$processResult = $event['ProcedureStateChangeEvent']['MediaProcessResultSet'] ?? [];
if ($processResult) {
foreach ($processResult as $item) {
if ($item['Type'] == 'Transcode') {
if ($item['TranscodeTask']['Status'] == 'SUCCESS') {
@ -117,9 +130,14 @@ class VodEventTask extends Task
}
}
}
}
$fileStatus = ChapterModel::FS_TRANSLATING;
if (!$processResult) {
$fileStatus = ChapterModel::FS_FAILED;
}
/**
* 当有一个成功标记为成功
*/
@ -129,17 +147,21 @@ class VodEventTask extends Task
$fileStatus = ChapterModel::FS_FAILED;
}
if ($fileStatus == ChapterModel::FS_TRANSLATING) return;
$attrs['file']['id'] = $fileId;
$attrs['file']['status'] = $fileStatus;
$chapter->update(['attrs' => $attrs]);
$chapter->attrs = $attrs;
$chapter->update();
$this->updateCourseVodAttrs($chapter->course_id);
return true;
}
protected function handleFileDeletedEvent($event)
{
return true;
}
protected function pullEvents()
@ -156,11 +178,20 @@ class VodEventTask extends Task
return $vodService->confirmEvents($handles);
}
protected function updateVodAttrs(ChapterModel $chapter)
protected function updateCourseVodAttrs($courseId)
{
$courseStats = new CourseStatService();
$courseStats->updateVodAttrs($chapter->course_id);
$courseStats->updateVodAttrs($courseId);
}
protected function getFileDuration($fileId)
{
$service = new VodService();
$metaInfo = $service->getOriginVideoInfo($fileId);
return $metaInfo['duration'] ?? 0;
}
}

View File

@ -47,9 +47,11 @@ class PointGiftController extends Controller
$service = new PointGiftService();
$xmCourses = $service->getXmCourses();
$xmVips = $service->getXmVips();
$types = $service->getTypes();
$this->view->setVar('xm_courses', $xmCourses);
$this->view->setVar('xm_vips', $xmVips);
$this->view->setVar('types', $types);
}

View File

@ -50,6 +50,7 @@ class Article extends Service
'type' => CategoryModel::TYPE_ARTICLE,
'level' => 1,
'published' => 1,
'deleted' => 0,
]);
}

View File

@ -133,14 +133,19 @@ class ChapterContent extends Service
$vod = $chapterRepo->findChapterVod($chapter->id);
$attrs = $chapter->attrs;
if ($fileId != $vod->file_id) {
$vod->file_id = $fileId;
$vod->file_transcode = [];
$vod->update();
$attrs = $chapter->attrs;
$attrs['duration'] = 0;
$attrs['file']['status'] = ChapterModel::FS_UPLOADED;
$attrs['duration'] = 0;
}
$chapter->attrs = $attrs;
$chapter->update();
$this->updateCourseVodAttrs($vod->course_id);

View File

@ -274,6 +274,7 @@ class Course extends Service
$allCategories = $categoryRepo->findAll([
'type' => CategoryModel::TYPE_COURSE,
'published' => 1,
'deleted' => 0,
]);
if ($allCategories->count() == 0) return [];

View File

@ -50,7 +50,11 @@ class FlashSale extends Service
{
$courseRepo = new CourseRepo();
$items = $courseRepo->findAll(['free' => 0, 'published' => 1]);
$items = $courseRepo->findAll([
'free' => 0,
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];
@ -70,7 +74,10 @@ class FlashSale extends Service
{
$packageRepo = new PackageRepo();
$items = $packageRepo->findAll(['published' => 1]);
$items = $packageRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];

View File

@ -11,6 +11,7 @@ use App\Library\Paginator\Query as PagerQuery;
use App\Models\PointGift as PointGiftModel;
use App\Repos\Course as CourseRepo;
use App\Repos\PointGift as PointGiftRepo;
use App\Repos\Vip as VipRepo;
use App\Validators\PointGift as PointGiftValidator;
class PointGift extends Service
@ -47,6 +48,29 @@ class PointGift extends Service
return $result;
}
public function getXmVips()
{
$vipRepo = new VipRepo();
$items = $vipRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];
$result = [];
foreach ($items as $item) {
$result[] = [
'name' => sprintf('%s¥%0.2f', $item->title, $item->price),
'value' => $item->id,
];
}
return $result;
}
public function getPointGifts()
{
$pagerQuery = new PagerQuery();
@ -83,6 +107,9 @@ class PointGift extends Service
case PointGiftModel::TYPE_COURSE:
$gift = $this->createCoursePointGift($post);
break;
case PointGiftModel::TYPE_VIP:
$gift = $this->createVipPointGift($post);
break;
case PointGiftModel::TYPE_GOODS:
$gift = $this->createGoodsPointGift($post);
break;
@ -168,7 +195,7 @@ class PointGift extends Service
$giftRepo = new PointGiftRepo();
$gift = $giftRepo->findByCourseId($course->id);
$gift = $giftRepo->findItemGift($course->id, PointGiftModel::TYPE_COURSE);
if ($gift) return $gift;
@ -188,6 +215,34 @@ class PointGift extends Service
return $gift;
}
protected function createVipPointGift($post)
{
$validator = new PointGiftValidator();
$vip = $validator->checkVip($post['xm_vip_id']);
$giftRepo = new PointGiftRepo();
$gift = $giftRepo->findItemGift($vip->id, PointGiftModel::TYPE_VIP);
if ($gift) return $gift;
$gift = new PointGiftModel();
$gift->type = PointGiftModel::TYPE_VIP;
$gift->name = sprintf('会员服务(%s个月', $vip->expiry);
$gift->cover = $vip->cover;
$gift->attrs = [
'id' => $vip->id,
'title' => $vip->title,
'price' => $vip->price,
];
$gift->create();
return $gift;
}
protected function createGoodsPointGift($post)
{
$validator = new PointGiftValidator();

View File

@ -49,6 +49,7 @@ class Question extends Service
'type' => CategoryModel::TYPE_ARTICLE,
'level' => 1,
'published' => 1,
'deleted' => 0,
]);
}

View File

@ -43,7 +43,7 @@ class Session extends Service
$validator = new CaptchaValidator();
$validator->checkCode($post['ticket'], $post['rand']);
$validator->checkCode($post['captcha']['ticket'], $post['captcha']['rand']);
}
$this->auth->saveAuthInfo($user);

View File

@ -48,8 +48,15 @@
</fieldset>
<div class="layui-form-item" id="upload-block">
<label class="layui-form-label">视频文件</label>
<div class="layui-input-block">
<div class="layui-input-inline">
<input class="layui-input" type="text" name="file_id" value="{{ file_id }}" readonly="readonly" lay-verify="required">
</div>
<div class="layui-inline">
{% if vod.file_id > 0 %}
<span class="layui-btn" id="upload-btn">重新上传</span>
{% else %}
<span class="layui-btn" id="upload-btn">选择视频</span>
{% endif %}
<input class="layui-hide" type="file" name="file" accept="video/*,audio/*">
</div>
</div>
@ -61,16 +68,10 @@
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">文件编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="file_id" value="{{ file_id }}" readonly="readonly" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button id="vod-submit" class="layui-btn layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>

View File

@ -4,7 +4,7 @@
{% elseif value == 2 %}
商品
{% elseif value == 3 %}
现金
会员
{% endif %}
{%- endmacro %}

View File

@ -22,6 +22,14 @@
</div>
</div>
</div>
<div id="block-3" class="block" style="display:none;">
<div class="layui-form-item">
<label class="layui-form-label">会员选择</label>
<div class="layui-input-block">
<div id="xm-vip-id"></div>
</div>
</div>
</div>
<div id="block-2" class="block" style="display:none;">
<div class="layui-form-item">
<label class="layui-form-label">商品名称</label>
@ -67,6 +75,13 @@
data: {{ xm_courses|json_encode }}
});
xmSelect.render({
el: '#xm-vip-id',
name: 'xm_vip_id',
radio: true,
data: {{ xm_vips|json_encode }}
});
form.on('radio(type)', function (data) {
$('.block').hide();
$('#block-' + data.value).show();

View File

@ -6,6 +6,8 @@
{% if gift.type == 1 %}
{{ partial('point_gift/edit_course') }}
{% elseif gift.type == 3 %}
{{ partial('point_gift/edit_vip') }}
{% elseif gift.type == 2 %}
{{ partial('point_gift/edit_goods') }}
{% endif %}

View File

@ -0,0 +1,41 @@
<form class="layui-form kg-form" method="POST" action="{{ update_url }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>编辑礼品</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">礼品封面</label>
<div class="layui-input-inline">
<img id="img-cover" class="kg-cover" src="{{ gift.cover }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">礼品名称</label>
<div class="layui-form-mid layui-word-aux">{{ gift.name }}{{ '¥%0.2f'|format(gift.attrs['price']) }}</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">所需积分</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="point" value="{{ gift.point }}" lay-verify="number">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">库存数量</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="stock" value="{{ gift.stock }}" lay-verify="number">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是" {% if gift.published == 1 %}checked="checked"{% endif %}>
<input type="radio" name="published" value="0" title="否" {% if gift.published == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>

View File

@ -12,7 +12,7 @@
<form class="layui-form kg-login-form" method="POST" action="{{ url({'for':'admin.login'}) }}">
<div class="layui-form-item">
<label class="layui-icon layui-icon-username"></label>
<input class="layui-input" type="text" name="account" autocomplete="off" placeholder="手机 / 邮箱" lay-verify="required">
<input id="cl-account" class="layui-input" type="text" name="account" autocomplete="off" placeholder="手机 / 邮箱" lay-verify="required">
</div>
<div class="layui-form-item">
<label class="layui-icon layui-icon-password"></label>
@ -21,15 +21,17 @@
{% if captcha.enabled == 1 %}
<div id="captcha-block" class="layui-form-item">
<div class="layui-input-block">
<button id="captcha-btn" class="layui-btn layui-btn-fluid" type="button" data-app-id="{{ captcha.app_id }}">点击完成验证</button>
<button id="cl-emit-btn" class="layui-btn layui-btn-fluid" type="button">点击完成验证</button>
</div>
</div>
{% endif %}
<div class="layui-form-item">
<div class="layui-input-block">
<button id="submit-btn" class="layui-btn layui-btn-fluid {{ disabled_class }}" {{ disabled_submit }} lay-submit="true" lay-filter="go">立即登录</button>
<input type="hidden" name="ticket">
<input type="hidden" name="rand">
<button id="cl-submit-btn" class="layui-btn layui-btn-fluid {{ disabled_class }}" {{ disabled_submit }} lay-submit="true" lay-filter="go">立即登录</button>
<input id="cl-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cl-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cl-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cl-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>
@ -72,13 +74,8 @@
{{ js_include('lib/jquery.min.js') }}
{{ js_include('lib/jquery.buoyant.min.js') }}
{% if captcha.enabled == 1 %}
{{ js_include('https://ssl.captcha.qq.com/TCaptcha.js', false) }}
{% endif %}
{% endblock %}
{% block inline_js %}
@ -98,24 +95,27 @@
});
</script>
{% if captcha.enabled == 1 %}
<script>
layui.use(['jquery', 'form'], function () {
layui.use(['jquery'], function () {
var $ = layui.jquery;
new TencentCaptcha(
$('#captcha-btn')[0],
$('#captcha-btn').data('app-id'),
if ($('#cl-captcha-enabled').val() === '1') {
var captcha = new TencentCaptcha(
$('#cl-emit-btn')[0],
$('#cl-captcha-appId').val(),
function (res) {
if (res.ret === 0) {
$('input[name=ticket]').val(res.ticket);
$('input[name=rand]').val(res.randstr);
$('#cl-captcha-ticket').val(res.ticket);
$('#cl-captcha-rand').val(res.randstr);
$('#cl-submit-btn').removeClass('layui-btn-disabled').removeAttr('disabled');
$('#captcha-block').hide();
$('#submit-btn').removeClass('layui-btn-disabled').removeAttr('disabled');
}
}
);
}
});
</script>
{% endif %}
{% endblock %}

View File

@ -93,6 +93,20 @@
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">ISP备案号</label>
<div class="kg-input-inline">
<input class="layui-input" type="text" name="isp_sn" value="{{ site.isp_sn }}">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">备案链接</label>
<div class="kg-input-inline" style="width:500px;">
<input class="layui-input" type="text" name="isp_link" value="{{ site.isp_link }}">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">公安备案号</label>
@ -107,6 +121,20 @@
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">企业信用代码</label>
<div class="kg-input-inline">
<input class="layui-input" type="text" name="company_sn" value="{{ site.company_sn }}">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">电子执照链接</label>
<div class="kg-input-inline" style="width:500px;">
<input class="layui-input" type="text" name="company_sn_link" value="{{ site.company_sn_link }}">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">开启统计</label>
<div class="layui-input-block">

View File

@ -62,7 +62,7 @@ class Account extends Service
$validator = new CaptchaValidator();
$validator->checkCode($post['ticket'], $post['rand']);
$validator->checkCode($post['captcha']['ticket'], $post['captcha']['rand']);
}
$this->auth->saveAuthInfo($user);

View File

@ -29,10 +29,10 @@
<div class="layui-form-item">
<div class="layui-input-block">
<button id="cv-submit-btn" class="layui-btn layui-btn-fluid layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">重置密码</button>
<input id="cv-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-app-id" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-ticket" type="hidden" name="ticket">
<input id="cv-rand" type="hidden" name="rand">
<input id="cv-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cv-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -4,7 +4,7 @@
<form class="layui-form account-form" method="POST" action="{{ url({'for':'home.account.pwd_login'}) }}">
<div class="layui-form-item">
<label class="layui-icon layui-icon-username"></label>
<input class="layui-input" type="text" name="account" autocomplete="off" placeholder="手机 / 邮箱" lay-verify="required">
<input id="cl-account" class="layui-input" type="text" name="account" autocomplete="off" placeholder="手机 / 邮箱" lay-verify="required">
</div>
<div class="layui-form-item">
<label class="layui-icon layui-icon-password"></label>
@ -13,16 +13,18 @@
{% if captcha.enabled == 1 %}
<div id="captcha-block" class="layui-form-item">
<div class="layui-input-block">
<button id="captcha-btn" class="layui-btn layui-btn-fluid" type="button" data-app-id="{{ captcha.app_id }}">点击完成验证</button>
<button id="cl-emit-btn" class="layui-btn layui-btn-fluid" type="button">点击完成验证</button>
</div>
</div>
{% endif %}
<div class="layui-form-item">
<div class="layui-input-block">
<button id="submit-btn" class="layui-btn layui-btn-fluid {{ disabled_class }}" {{ disabled_submit }} lay-submit="true" lay-filter="go">立即登录</button>
<button id="cl-submit-btn" class="layui-btn layui-btn-fluid {{ disabled_class }}" {{ disabled_submit }} lay-submit="true" lay-filter="go">立即登录</button>
<input type="hidden" name="return_url" value="{{ return_url }}">
<input id="ticket" type="hidden" name="ticket">
<input id="rand" type="hidden" name="rand">
<input id="cl-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cl-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cl-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cl-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -16,10 +16,10 @@
<div class="layui-input-block">
<button id="cv-submit-btn" class="layui-btn layui-btn-fluid layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">立即登录</button>
<input type="hidden" name="return_url" value="{{ return_url }}">
<input id="cv-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-app-id" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-ticket" type="hidden" name="ticket">
<input id="cv-rand" type="hidden" name="rand">
<input id="cv-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cv-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -45,10 +45,10 @@
<div class="layui-input-block">
<button id="cv-submit-btn" class="layui-btn layui-btn-fluid layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">注册帐号</button>
<input type="hidden" name="return_url" value="{{ return_url }}">
<input id="cv-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-app-id" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-ticket" type="hidden" name="ticket">
<input id="cv-rand" type="hidden" name="rand">
<input id="cv-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cv-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -4,7 +4,7 @@
{% elseif value == 2 %}
商品
{% elseif value == 3 %}
现金
会员
{% endif %}
{%- endmacro %}

View File

@ -12,9 +12,15 @@
{% if site_info.icp_sn %}
<a href="{{ site_info.icp_link }}" target="_blank">{{ site_info.icp_sn }}</a>
{% endif %}
{% if site_info.isp_sn %}
<a href="{{ site_info.isp_link }}" target="_blank">{{ site_info.isp_sn }}</a>
{% endif %}
{% if site_info.police_sn %}
<a href="{{ site_info.police_link }}" target="_blank">{{ site_info.police_sn }}</a>
{% endif %}
{% if site_info.company_sn %}
<a href="{{ site_info.company_sn_link }}" title="企业信用代码:{{ site_info.company_sn }}" target="_blank">工商网监电子标识</a>
{% endif %}
</div>
{% if contact_info.enabled == 1 %}
<div class="row contact">

View File

@ -34,10 +34,10 @@
<div class="layui-form-item">
<div class="layui-input-block">
<button id="cv-submit-btn" class="layui-btn layui-btn-fluid layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">提交修改</button>
<input id="cv-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-app-id" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-ticket" type="hidden" name="ticket">
<input id="cv-rand" type="hidden" name="rand">
<input id="cv-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cv-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -34,10 +34,10 @@
<div class="layui-form-item">
<div class="layui-input-block">
<button id="cv-submit-btn" class="layui-btn layui-btn-fluid layui-btn-disabled" disabled="disabled" lay-submit="true" lay-filter="go">提交修改</button>
<input id="cv-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-app-id" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-ticket" type="hidden" name="ticket">
<input id="cv-rand" type="hidden" name="rand">
<input id="cv-captcha-enabled" type="hidden" value="{{ captcha.enabled }}">
<input id="cv-captcha-appId" type="hidden" value="{{ captcha.app_id }}">
<input id="cv-captcha-ticket" type="hidden" name="captcha[ticket]">
<input id="cv-captcha-rand" type="hidden" name="captcha[rand]">
</div>
</div>
</form>

View File

@ -16,7 +16,7 @@ class AppInfo
protected $link = 'https://koogua.com';
protected $version = '1.5.3';
protected $version = '1.5.4';
public function __get($name)
{

View File

@ -100,7 +100,7 @@ class Account extends Model
$user = new User();
$user->id = $this->id;
$user->name = "user:{$this->id}";
$user->name = "user_{$this->id}";
if ($user->create() === false) {
throw new \RuntimeException('Create User Failed');

View File

@ -19,6 +19,7 @@ class PointGift extends Model
*/
const TYPE_COURSE = 1; // 课程
const TYPE_GOODS = 2; // 商品
const TYPE_VIP = 3; // 会员
/**
* 课程扩展属性
@ -31,6 +32,17 @@ class PointGift extends Model
'price' => 0,
];
/**
* 会员扩展属性
*
* @var array
*/
protected $_vip_attrs = [
'id' => 0,
'title' => '',
'price' => 0,
];
/**
* 商品扩展属性
*
@ -162,6 +174,8 @@ class PointGift extends Model
if (empty($this->attrs)) {
if ($this->type == self::TYPE_COURSE) {
$this->attrs = $this->_course_attrs;
} elseif ($this->type == self::TYPE_VIP) {
$this->attrs = $this->_vip_attrs;
} elseif ($this->type == self::TYPE_GOODS) {
$this->attrs = $this->_goods_attrs;
}
@ -224,6 +238,7 @@ class PointGift extends Model
return [
self::TYPE_COURSE => '课程',
self::TYPE_GOODS => '商品',
self::TYPE_VIP => '会员',
];
}

View File

@ -163,4 +163,17 @@ class CourseUser extends Repository
->execute();
}
/**
* @param int $courseId
* @param int $userId
* @return ResultsetInterface|Resultset|CourseUserModel[]
*/
public function findByCourseAndUserId($courseId, $userId)
{
return CourseUserModel::query()
->where('course_id = :course_id:', ['course_id' => $courseId])
->andWhere('user_id = :user_id:', ['user_id' => $userId])
->execute();
}
}

View File

@ -77,10 +77,11 @@ class PointGift extends Repository
}
/**
* @param int $courseId
* @param int $itemId
* @param int $itemType
* @return PointGiftModel|Model|bool
*/
public function findByCourseId($courseId)
public function findItemGift($itemId, $itemType)
{
/**
* @todo 重新设计表结构
@ -88,13 +89,13 @@ class PointGift extends Repository
* 没有预留独立的条目编号,先这么将就实现吧
*/
$records = PointGiftModel::query()
->where('type = :type:', ['type' => PointGiftModel::TYPE_COURSE])
->where('type = :type:', ['type' => $itemType])
->execute();
if ($records->count() == 0) return false;
foreach ($records as $record) {
if ($record->attrs['id'] == $courseId) {
if ($record->attrs['id'] == $itemId) {
return $record;
}
}

View File

@ -19,7 +19,10 @@ class XmTagList extends LogicService
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1]);
$allTags = $tagRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($allTags->count() == 0) return [];

View File

@ -19,7 +19,10 @@ class XmTagList extends LogicService
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1]);
$allTags = $tagRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($allTags->count() == 0) return [];

View File

@ -11,6 +11,7 @@ use App\Models\Course as CourseModel;
use App\Models\CourseUser as CourseUserModel;
use App\Models\ImGroupUser as ImGroupUserModel;
use App\Models\User as UserModel;
use App\Repos\CourseUser as CourseUserRepo;
use App\Repos\ImGroup as ImGroupRepo;
use App\Repos\ImGroupUser as ImGroupUserRepo;
use App\Repos\ImUser as ImUserRepo;
@ -20,6 +21,13 @@ class CourseDeliver extends LogicService
{
public function handle(CourseModel $course, UserModel $user)
{
$this->revokeCourseUser($course, $user);
$this->handleCourseUser($course, $user);
$this->handleImGroupUser($course, $user);
}
protected function handleCourseUser(CourseModel $course, UserModel $user)
{
if ($course->model == CourseModel::MODEL_OFFLINE) {
$expiryTime = strtotime($course->attrs['end_date']);
@ -28,7 +36,6 @@ class CourseDeliver extends LogicService
}
$courseUser = new CourseUserModel();
$courseUser->user_id = $user->id;
$courseUser->course_id = $course->id;
$courseUser->expiry_time = $expiryTime;
@ -39,6 +46,12 @@ class CourseDeliver extends LogicService
$course->user_count += 1;
$course->update();
$user->course_count += 1;
$user->update();
}
protected function handleImGroupUser(CourseModel $course, UserModel $user)
{
$groupRepo = new ImGroupRepo();
$group = $groupRepo->findByCourseId($course->id);
@ -52,9 +65,7 @@ class CourseDeliver extends LogicService
$groupUser = $groupUserRepo->findGroupUser($group->id, $user->id);
if (!$groupUser) {
$groupUser = new ImGroupUserModel();
$groupUser->group_id = $group->id;
$groupUser->user_id = $user->id;
$groupUser->create();
@ -67,4 +78,20 @@ class CourseDeliver extends LogicService
}
}
protected function revokeCourseUser(CourseModel $course, UserModel $user)
{
$courseUserRepo = new CourseUserRepo();
$relations = $courseUserRepo->findByCourseAndUserId($course->id, $user->id);
if ($relations->count() == 0) return;
foreach ($relations as $relation) {
if ($relation->deleted == 0) {
$relation->deleted = 1;
$relation->update();
}
}
}
}

View File

@ -7,13 +7,8 @@
namespace App\Services\Logic\Deliver;
use App\Models\CourseUser as CourseUserModel;
use App\Models\ImGroupUser as ImGroupUserModel;
use App\Models\Package as PackageModel;
use App\Models\User as UserModel;
use App\Repos\ImGroup as ImGroupRepo;
use App\Repos\ImGroupUser as ImGroupUserRepo;
use App\Repos\ImUser as ImUserRepo;
use App\Repos\Package as PackageRepo;
use App\Services\Logic\Service as LogicService;
@ -27,43 +22,8 @@ class PackageDeliver extends LogicService
$courses = $packageRepo->findCourses($package->id);
foreach ($courses as $course) {
$courseUser = new CourseUserModel();
$courseUser->user_id = $user->id;
$courseUser->course_id = $course->id;
$courseUser->expiry_time = strtotime("+{$course->study_expiry} months");
$courseUser->role_type = CourseUserModel::ROLE_STUDENT;
$courseUser->source_type = CourseUserModel::SOURCE_CHARGE;
$courseUser->create();
$course->user_count += 1;
$course->update();
$imUserRepo = new ImUserRepo();
$imUser = $imUserRepo->findById($user->id);
$groupRepo = new ImGroupRepo();
$group = $groupRepo->findByCourseId($course->id);
$groupUserRepo = new ImGroupUserRepo();
$groupUser = $groupUserRepo->findGroupUser($group->id, $user->id);
if (!$groupUser) {
$groupUser = new ImGroupUserModel();
$groupUser->group_id = $group->id;
$groupUser->user_id = $user->id;
$groupUser->create();
$imUser->group_count += 1;
$imUser->update();
$group->user_count += 1;
$group->update();
}
$deliver = new CourseDeliver();
$deliver->handle($course, $user);
}
}

View File

@ -19,7 +19,10 @@ class XmTagList extends LogicService
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1]);
$allTags = $tagRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($allTags->count() == 0) return [];

View File

@ -23,6 +23,7 @@ class TagList extends LogicService
$params = $pagerQuery->getParams();
$params['published'] = 1;
$params['deleted'] = 0;
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();

View File

@ -29,7 +29,7 @@ class MailCode extends LogicService
$validator = new CaptchaValidator();
$validator->checkCode($post['ticket'], $post['rand']);
$validator->checkCode($post['captcha']['ticket'], $post['captcha']['rand']);
}
$service = new MailVerifyService();

View File

@ -29,7 +29,7 @@ class SmsCode extends LogicService
$validator = new CaptchaValidator();
$validator->checkCode($post['ticket'], $post['rand']);
$validator->checkCode($post['captcha']['ticket'], $post['captcha']['rand']);
}
$service = new SmsVerifyService();

View File

@ -1,27 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 深圳市酷瓜软件有限公司
* @license https://opensource.org/licenses/GPL-2.0
* @link https://www.koogua.com
*/
namespace App\Services\Logic\Verify;
use App\Services\Logic\Service as LogicService;
use App\Validators\Verify as VerifyValidator;
class Ticket extends LogicService
{
public function handle()
{
$rand = $this->request->getPost('rand', ['trim', 'string']);
$validator = new VerifyValidator();
$rand = $validator->checkRand($rand);
return $this->crypt->encryptBase64($rand);
}
}

View File

@ -177,4 +177,11 @@ class PointGift extends Validator
return $validator->checkCourse($id);
}
public function checkVip($id)
{
$validator = new Vip();
return $validator->checkVip($id);
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2022 深圳市酷瓜软件有限公司
* @license https://opensource.org/licenses/GPL-2.0
* @link https://www.koogua.com
*/
require_once 'SettingTrait.php';
use Phinx\Migration\AbstractMigration;
final class V20220607014823 extends AbstractMigration
{
use SettingTrait;
public function up()
{
$this->handleSiteSettings();
}
protected function handleSiteSettings()
{
$rows =
[
[
'section' => 'site',
'item_key' => 'isp_sn',
'item_value' => '',
],
[
'section' => 'site',
'item_key' => 'isp_link',
'item_value' => 'https://dxzhgl.miit.gov.cn',
],
[
'section' => 'site',
'item_key' => 'company_sn',
'item_value' => '',
],
[
'section' => 'site',
'item_key' => 'company_sn_link',
'item_value' => '',
],
];
$this->insertSettings($rows);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @copyright Copyright (c) 2022 深圳市酷瓜软件有限公司
* @license https://opensource.org/licenses/GPL-2.0
* @link https://www.koogua.com
*/
trait SettingTrait
{
protected function insertSettings(array $rows)
{
foreach ($rows as $key => $row) {
$exists = $this->settingExits($row['section'], $row['item_key']);
if ($exists) unset($rows[$key]);
}
if (count($rows) == 0) return;
$this->table('kg_setting')->insert($rows)->save();
}
protected function settingExits($section, $itemKey)
{
$row = $this->getQueryBuilder()
->select('*')
->from('kg_setting')
->where(['section' => $section, 'item_key' => $itemKey])
->execute()->fetch();
return $row ? true : false;
}
}

View File

@ -46,6 +46,7 @@ layui.use(['jquery', 'element'], function () {
uploader.done().then(function (result) {
$('input[name=file_id]').val(result.fileId);
$('#vod-submit').removeAttr('disabled').removeClass('layui-btn-disabled');
$.ajax({
type: 'POST',
url: $('#vod-form').attr('action'),

View File

@ -2,16 +2,16 @@ layui.use(['jquery'], function () {
var $ = layui.jquery;
if ($('#captcha-btn').length > 0) {
if ($('#cl-captcha-enabled').val() === '1') {
var captcha = new TencentCaptcha(
$('#captcha-btn')[0],
$('#captcha-btn').data('app-id'),
$('#cl-emit-btn')[0],
$('#cl-captcha-appId').val(),
function (res) {
if (res.ret === 0) {
$('#ticket').val(res.ticket);
$('#rand').val(res.randstr);
$('#cl-captcha-ticket').val(res.ticket);
$('#cl-captcha-rand').val(res.randstr);
$('#cl-submit-btn').removeClass('layui-btn-disabled').removeAttr('disabled');
$('#captcha-block').hide();
$('#submit-btn').removeClass('layui-btn-disabled').removeAttr('disabled');
}
}
);

View File

@ -9,14 +9,14 @@ layui.use(['jquery', 'layer', 'util'], function () {
var $emit = $('#cv-emit-btn');
var $submit = $('#cv-submit-btn');
if ($('#cv-enabled').val() === '1') {
if ($('#cv-captcha-enabled').val() === '1') {
var captcha = new TencentCaptcha(
$emit[0],
$('#cv-app-id').val(),
$('#cv-captcha-appId').val(),
function (res) {
if (res.ret === 0) {
$('#cv-ticket').val(res.ticket);
$('#cv-rand').val(res.randstr);
$('#cv-captcha-ticket').val(res.ticket);
$('#cv-captcha-rand').val(res.randstr);
sendVerifyCode();
}
}
@ -49,8 +49,10 @@ layui.use(['jquery', 'layer', 'util'], function () {
if (isEmail($account.val()) || isPhone($account.val())) {
var postUrl;
var postData = {
ticket: $('#cv-ticket').val(),
rand: $('#cv-rand').val(),
captcha: {
ticket: $('#cv-captcha-ticket').val(),
rand: $('#cv-captcha-rand').val(),
}
};
if (isPhone($account.val())) {
postData.phone = $account.val();