来团智慧商业小程序零代码开发平台,多行业适配。无需代码,拖拽式设计,轻松打造订货商城、会员制商城、分销商城及小程序官网。不仅能满足通用需求,还支持定制化,从页面布局到功能模块,随心定制,助您快速搭建专属商业小程序,抢占市场先机。
短视频功能是来团科技SAAS多租户商城系统的重要营销工具,主要功能包括:
| 组件 | 技术栈 | 版本 |
|---|---|---|
| 后端框架 | ThinkPHP | ^8.0 |
| PHP版本 | PHP | >= 8.0.2 |
| ORM | topthink/think-orm | ^3.0 |
| 前端框架 | Layui | - |
| 小程序 | 微信小程序 | - |
| 缓存 | Redis | - |
┌─────────────────────────────────────────────────────────────────────┐
│ 短视频业务流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [商家后台] │
│ ↓ │
│ [上传短视频] ──→ [填写视频信息] ──→ [绑定商品] ──→ [发布视频] │
│ │
│ [用户端] │
│ ↓ │
│ [浏览短视频列表] ──→ [播放短视频] ──→ [点击左下角商品] ──→ [跳转商品详情]│
│ ↑ │
│ [商品详情页] ←── [浮动小窗] ←── [商品已绑定短视频] │
│ │
└─────────────────────────────────────────────────────────────────────┘
---
## 二、后端架构设计
### 2.1 系统分层架构
app/
├── store/ # 商家后台应用
│ ├── controller/content/
│ │ └── Video.php # 短视频控制器(接收请求)
│ ├── model/
│ │ └── Video.php # 短视频模型(store层,扩展业务方法)
│ └── view/content/video/
│ ├── index.php # 视频列表页视图
│ ├── add.php # 新增视频页视图
│ └── edit.php # 编辑视频页视图
│
├── api/ # 小程序API应用
│ ├── controller/
│ │ └── Video.php # 短视频API控制器
│ └── model/
│ └── Video.php # 短视频API模型
│
└── common/ # 公共模块
├── model/
│ ├── Video.php # 公共视频模型(核心数据操作)
│ └── VideoGoodsRel.php # 视频-商品关联模型
└── service/ # 服务层(待补充)
### 2.2 模块职责划分
| 模块位置 | 职责 |
|----------|------|
| `appcommonmodelVideo` | 公共模型:负责数据表关联、缓存管理、基础查询 |
| `appcommonmodelVideoGoodsRel` | 关联模型:负责视频与商品的多对多关系管理 |
| `appstoremodelVideo` | 商店模型:负责增删改业务逻辑、缓存清理 |
| `appstorecontrollercontentVideo` | 商店控制器:处理商家后台管理请求 |
| `apppicontrollerVideo` | API控制器:处理小程序端请求 |
| `apppimodelVideo` | API模型:负责小程序端数据查询与格式化 |
### 2.3 缓存设计
```php
// 缓存Key格式
$listCacheKey = 'video_list_' . $wxapp_id . '_' . $page . '_' . $pageSize; // 视频列表缓存
$detailCacheKey = 'video_detail_' . $video_id; // 视频详情缓存
// 缓存数据结构
// 列表缓存
[
'list' => [...], // 视频列表数组
'total' => 100 // 总数量
]
// 详情缓存
[
'video' => [...], // 视频基本信息
'goods' => [...] // 绑定的商品列表
]
// 缓存标签
Cache::tag('cache')->set($cacheKey, $data, 3600); // 缓存1小时
CREATE TABLE IF NOT EXISTS `ltshop_video` (
`video_id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '视频ID',
`video_title` varchar(255) NOT NULL DEFAULT '' COMMENT '视频标题',
`video_desc` varchar(500) NOT NULL DEFAULT '' COMMENT '视频描述',
`video_file_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '视频文件ID',
`cover_image_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '封面图ID',
`video_status` tinyint(3) unsigned NOT NULL DEFAULT '10' COMMENT '视频状态(10草稿 20已发布 30已下架)',
`video_sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '排序(数字越小越靠前)',
`view_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '观看次数',
`like_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '点赞次数',
`share_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '分享次数',
`supplier_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '供应商ID(0表示自营)',
`wxapp_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '小程序ID',
`is_delete` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`video_id`),
KEY `video_status` (`video_status`),
KEY `video_sort` (`video_sort`),
KEY `wxapp_id` (`wxapp_id`),
KEY `supplier_id` (`supplier_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短视频表';
CREATE TABLE IF NOT EXISTS `ltshop_video_goods_rel` (
`rel_id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '关联ID',
`video_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '视频ID',
`goods_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '商品ID',
`goods_sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '商品排序(数字越小越靠前)',
`wxapp_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '小程序ID',
`create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`rel_id`),
KEY `video_id` (`video_id`),
KEY `goods_id` (`goods_id`),
KEY `wxapp_id` (`wxapp_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频-商品关联表';
ltshop_video表字段:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| video_id | int(11) unsigned | 自增 | 视频ID,主键 |
| video_title | varchar(255) | '' | 视频标题 |
| video_desc | varchar(500) | '' | 视频描述 |
| video_file_id | int(11) unsigned | 0 | 视频文件ID,关联upload_file表 |
| cover_image_id | int(11) unsigned | 0 | 封面图ID,关联upload_file表 |
| video_status | tinyint(3) unsigned | 10 | 视频状态:10-草稿,20-已发布,30-已下架 |
| video_sort | int(11) unsigned | 100 | 排序值,数字越小越靠前 |
| view_count | int(11) unsigned | 0 | 观看次数 |
| like_count | int(11) unsigned | 0 | 点赞次数 |
| share_count | int(11) unsigned | 0 | 分享次数 |
| supplier_id | int(11) unsigned | 0 | 供应商ID,0表示自营 |
| wxapp_id | int(11) unsigned | 0 | 租户ID,用于多租户数据隔离 |
| is_delete | tinyint(3) unsigned | 0 | 是否删除:0-否,1-是 |
| create_time | int(11) unsigned | 0 | 创建时间戳 |
| update_time | int(11) unsigned | 0 | 更新时间戳 |
ltshop_video_goods_rel表字段:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| rel_id | int(11) unsigned | 自增 | 关联ID,主键 |
| video_id | int(11) unsigned | 0 | 视频ID,外键 |
| goods_id | int(11) unsigned | 0 | 商品ID,外键 |
| goods_sort | int(11) unsigned | 100 | 商品在视频中的排序 |
| wxapp_id | int(11) unsigned | 0 | 租户ID |
| create_time | int(11) unsigned | 0 | 创建时间戳 |
┌─────────────────────┐ ┌───────────────────────┐ ┌─────────────────────┐
│ ltshop_video │ │ ltshop_video_goods_rel│ │ ltshop_goods │
├─────────────────────┤ ├───────────────────────┤ ├─────────────────────┤
│ video_id (PK) │◄──────│ video_id (FK) │──────►│ goods_id (PK) │
│ video_title │ │ goods_id (FK) │ │ goods_name │
│ video_desc │ │ goods_sort │ │ goods_price │
│ video_file_id │ │ wxapp_id │ │ ... │
│ cover_image_id │ └───────────────────────┘ └─────────────────────┘
│ video_status │
│ video_sort │
│ view_count │
│ like_count │
│ share_count │
│ supplier_id │
│ wxapp_id │
│ create_time │
│ update_time │
└─────────────────────┘
│
│
▼
┌─────────────────────┐
│ ltshop_upload_file │
├─────────────────────┤
│ file_id (PK) │
│ file_path │
│ file_type │
└─────────────────────┘
文件位置: app/common/model/Video.php
<?php
namespace appcommonmodel;
use thinkacadeCache;
/**
* 短视频公共模型
* Class Video
* @package appcommonmodel
*/
class Video extends BaseModel
{
protected $name = 'video';
protected $pk = 'video_id';
const STATUS_DRAFT = 10; // 草稿
const STATUS_PUBLISHED = 20; // 已发布
const STATUS_OFFLINE = 30; // 已下架
/**
* 视频文件关联
* @return hinkmodel
elationHasOne
*/
public function videoFile()
{
return $this->hasOne('UploadFile', 'file_id', 'video_file_id');
}
/**
* 封面图关联
* @return hinkmodel
elationHasOne
*/
public function coverImage()
{
return $this->hasOne('UploadFile', 'file_id', 'cover_image_id');
}
/**
* 关联商品(多对多)
* @return hinkmodel
elationBelongsToMany
*/
public function goods()
{
return $this->belongsToMany('Goods', 'video_goods_rel', 'goods_id', 'video_id')
->order(['goods_sort' => 'asc']);
}
/**
* 获取视频状态获取器
* @param $value
* @return array
*/
public function getVideoStatusAttr($value)
{
$status = [
self::STATUS_DRAFT => ['text' => '草稿', 'value' => self::STATUS_DRAFT],
self::STATUS_PUBLISHED => ['text' => '已发布', 'value' => self::STATUS_PUBLISHED],
self::STATUS_OFFLINE => ['text' => '已下架', 'value' => self::STATUS_OFFLINE],
];
return $status[$value] ?? ['text' => '未知', 'value' => $value];
}
/**
* 获取已发布的视频列表(含缓存)
* @param int $page 页码
* @param int $pageSize 每页数量
* @return array
*/
public static function getPublishedList($page = 1, $pageSize = 10)
{
$model = new static;
$cacheKey = 'video_list_' . $model::$wxapp_id . '_' . $page . '_' . $pageSize;
if (!Cache::get($cacheKey)) {
$list = $model->with(['videoFile', 'coverImage', 'goods'])
->where('video_status', '=', self::STATUS_PUBLISHED)
->where('is_delete', '=', 0)
->order(['video_sort' => 'asc', 'create_time' => 'desc'])
->page($page, $pageSize)
->select();
$total = $model->where('video_status', '=', self::STATUS_PUBLISHED)
->where('is_delete', '=', 0)
->count();
$data = [
'list' => !empty($list) ? $list->toArray() : [],
'total' => $total
];
Cache::tag('cache')->set($cacheKey, $data, 3600); // 缓存1小时
}
return Cache::get($cacheKey);
}
/**
* 获取视频详情
* @param int $videoId 视频ID
* @param bool $useCache 是否使用缓存
* @return array|null
*/
public static function getDetail($videoId, $useCache = false)
{
if ($useCache) {
$cacheKey = 'video_detail_' . $videoId;
if (!Cache::get($cacheKey)) {
$video = static::with(['videoFile', 'coverImage', 'goods'])
->where('video_id', '=', $videoId)
->where('is_delete', '=', 0)
->find();
if ($video) {
Cache::tag('cache')->set($cacheKey, $video->toArray(), 3600);
}
}
return Cache::get($cacheKey);
} else {
$video = static::with(['videoFile', 'coverImage', 'goods'])
->where('video_id', '=', $videoId)
->where('is_delete', '=', 0)
->find();
return $video ? $video->toArray() : null;
}
}
/**
* 增加观看次数
* @param int $videoId 视频ID
* @return bool
*/
public static function incViewCount($videoId)
{
return static::where('video_id', '=', $videoId)->inc('view_count')->update();
}
/**
* 增加点赞次数
* @param int $videoId 视频ID
* @return bool
*/
public static function incLikeCount($videoId)
{
return static::where('video_id', '=', $videoId)->inc('like_count')->update();
}
/**
* 增加分享次数
* @param int $videoId 视频ID
* @return bool
*/
public static function incShareCount($videoId)
{
return static::where('video_id', '=', $videoId)->inc('share_count')->update();
}
/**
* 删除缓存
* @param int|null $videoId 视频ID(为空时删除所有相关缓存)
* @return bool
*/
public static function deleteCache($videoId = null)
{
try {
if ($videoId) {
Cache::delete('video_detail_' . $videoId);
}
Cache::tag('cache')->clear();
return true;
} catch (Throwable $e) {
return false;
}
}
}
文件位置: app/common/model/VideoGoodsRel.php
<?php
namespace appcommonmodel;
/**
* 视频-商品关联模型
* Class VideoGoodsRel
* @package appcommonmodel
*/
class VideoGoodsRel extends BaseModel
{
protected $name = 'video_goods_rel';
protected $pk = 'rel_id';
/**
* 获取视频关联的商品ID列表
* @param int $videoId 视频ID
* @return array
*/
public static function getGoodsIdsByVideoId($videoId)
{
$list = self::where('video_id', '=', $videoId)
->order(['goods_sort' => 'asc'])
->select();
return array_column($list->toArray(), 'goods_id');
}
/**
* 获取商品关联的视频ID列表
* @param int $goodsId 商品ID
* @return array
*/
public static function getVideoIdsByGoodsId($goodsId)
{
$self = new static;
$list = self::alias('rel')
->join('video v', 'v.video_id = rel.video_id')
->where('rel.goods_id', '=', $goodsId)
->where('v.video_status', '=', Video::STATUS_PUBLISHED)
->where('v.is_delete', '=', 0);
if ($self::$wxapp_id > 0) {
$list->where('v.wxapp_id', '=', $self::$wxapp_id);
}
$list = $list->order(['v.video_sort' => 'asc', 'v.create_time' => 'desc'])
->select();
return array_column($list->toArray(), 'video_id');
}
/**
* 获取商品关联的第一个视频(用于商品详情页浮动小窗)
* @param int $goodsId 商品ID
* @return array|null
*/
public static function getFirstVideoByGoodsId($goodsId)
{
$self = new static;
$videoId = self::alias('rel')
->join('video v', 'v.video_id = rel.video_id')
->where('rel.goods_id', '=', $goodsId)
->where('v.video_status', '=', Video::STATUS_PUBLISHED)
->where('v.is_delete', '=', 0);
if ($self::$wxapp_id > 0) {
$videoId->where('v.wxapp_id', '=', $self::$wxapp_id);
}
$videoId = $videoId->order(['v.video_sort' => 'asc', 'v.create_time' => 'desc'])
->value('v.video_id');
if ($videoId) {
return Video::getDetail($videoId, false);
}
return null;
}
/**
* 获取商品关联的第一个视频ID
* @param int $goodsId 商品ID
* @return int|null
*/
public static function getFirstVideoIdByGoodsId($goodsId)
{
$self = new static;
$query = self::where('goods_id', '=', $goodsId);
$rel = $query->order(['goods_sort' => 'asc', 'create_time' => 'desc'])
->find();
if ($rel) {
return $rel->video_id;
}
return null;
}
/**
* 批量绑定商品到视频
* @param int $videoId 视频ID
* @param array $goodsIds 商品ID数组
* @param int $wxappId 小程序ID
* @return bool
*/
public static function bindGoods($videoId, $goodsIds, $wxappId)
{
// 删除原有绑定
self::where('video_id', '=', $videoId)->delete();
// 批量新增绑定
$data = [];
$sort = 100;
foreach ($goodsIds as $goodsId) {
$data[] = [
'video_id' => $videoId,
'goods_id' => $goodsId,
'goods_sort' => $sort,
'wxapp_id' => $wxappId,
'create_time' => time()
];
$sort += 10;
}
if (!empty($data)) {
return (new self)->insertAll($data) > 0;
}
return true;
}
}
文件位置: app/store/model/Video.php
<?php
namespace appstoremodel;
use thinkacadeCache;
use appcommonmodelVideo as VideoModel;
use appcommonmodelVideoGoodsRel;
/**
* 短视频模型 - 商店层
* Class Video
* @package appstoremodel
*/
class Video extends VideoModel
{
protected $allowField = [
'video_title', 'video_desc', 'video_file_id', 'cover_image_id',
'video_status', 'video_sort', 'supplier_id', 'wxapp_id'
];
/**
* 添加新视频
* @param array $data 视频数据
* @param array $goodsIds 绑定的商品ID数组
* @return false|int
*/
public function add($data, $goodsIds = [])
{
$data['wxapp_id'] = self::$wxapp_id;
$data['create_time'] = time();
$data['update_time'] = time();
try {
$this->startTrans();
// 保存视频
$videoId = $this->insertGetId($data);
// 绑定商品
if (!empty($goodsIds)) {
VideoGoodsRel::bindGoods($videoId, $goodsIds, self::$wxapp_id);
}
// 删除缓存
self::deleteCache();
$this->commit();
return $videoId;
} catch (Throwable $e) {
$this->rollback();
$this->error = $e->getMessage();
return false;
}
}
/**
* 编辑视频
* @param array $data 视频数据
* @param array $goodsIds 绑定的商品ID数组
* @return bool
*/
public function edit($data, $goodsIds = [])
{
$data['update_time'] = time();
try {
$this->startTrans();
// 更新视频
$result = $this->save($data);
// 绑定商品
VideoGoodsRel::bindGoods($this['video_id'], $goodsIds, self::$wxapp_id);
// 删除缓存
self::deleteCache($this['video_id']);
$this->commit();
return $result !== false;
} catch (Throwable $e) {
$this->rollback();
$this->error = $e->getMessage();
return false;
}
}
/**
* 删除视频
* @param int $videoId 视频ID
* @return bool
*/
public function remove($videoId)
{
try {
$this->startTrans();
// 软删除
$result = $this->where('video_id', '=', $videoId)
->update(['is_delete' => 1, 'update_time' => time()]);
// 删除关联
VideoGoodsRel::where('video_id', '=', $videoId)->delete();
// 删除缓存
self::deleteCache($videoId);
$this->commit();
return $result !== false;
} catch (Throwable $e) {
$this->rollback();
$this->error = $e->getMessage();
return false;
}
}
/**
* 获取视频列表(商家后台)
* @param array $param 查询参数
* @return hinkPaginator
*/
public function getList($param = [])
{
$query = $this->with(['videoFile', 'coverImage'])
->where('is_delete', '=', 0);
// 状态筛选
if (isset($param['video_status']) && $param['video_status'] > 0) {
$query->where('video_status', '=', $param['video_status']);
}
// 关键词搜索
if (isset($param['keyword']) && !empty($param['keyword'])) {
$query->whereLike('video_title', '%' . $param['keyword'] . '%');
}
return $query->order(['video_sort' => 'asc', 'create_time' => 'desc'])
->paginate(15, false, [
'query' => request()->param()
]);
}
}
文件位置: app/store/controller/content/Video.php
<?php
namespace appstorecontrollercontent;
use appstorecontrollerController;
use appstoremodelVideo as VideoModel;
use appstoremodelGoods as GoodsModel;
use appcommonmodelVideoGoodsRel;
/**
* 短视频控制器
* Class Video
* @package appstorecontrollercontent
*/
class Video extends Controller
{
/**
* 视频列表页
* @return mixed
*/
public function index()
{
$model = new VideoModel;
$list = $model->getList($this->request->param());
return $this->fetch('index', compact('list'));
}
/**
* 新增视频
* @return array|mixed
*/
public function add()
{
$model = new VideoModel;
if (!$this->request->isAjax()) {
return $this->fetch('add');
}
$postData = $this->postData('video');
$goodsIds = isset($postData['goods_ids']) ? $postData['goods_ids'] : [];
if (empty($postData['video_title'])) {
return $this->renderError('视频标题不能为空');
}
if (empty($postData['video_file_id'])) {
return $this->renderError('请上传视频文件');
}
try {
$result = $model->add($postData, $goodsIds);
if ($result !== false) {
return $this->renderSuccess('添加成功', url('content.video/index'));
}
return $this->renderError($model->getError() ?: '添加失败');
} catch (Throwable $e) {
return $this->renderError('添加失败: ' . $e->getMessage());
}
}
/**
* 编辑视频
* @param int $video_id 视频ID
* @return array|mixed
*/
public function edit($video_id)
{
$model = VideoModel::find($video_id);
if (!$this->request->isAjax()) {
$goodsIds = VideoGoodsRel::getGoodsIdsByVideoId($video_id);
$goodsList = [];
if (!empty($goodsIds)) {
$goodsModel = new GoodsModel;
$goodsList = $goodsModel->getListByIds($goodsIds);
}
return $this->fetch('edit', compact('model', 'goodsIds', 'goodsList'));
}
$postData = $this->postData('video');
$goodsIds = isset($postData['goods_ids']) ? $postData['goods_ids'] : [];
if (empty($postData['video_title'])) {
return $this->renderError('视频标题不能为空');
}
try {
$result = $model->edit($postData, $goodsIds);
if ($result !== false) {
return $this->renderSuccess('更新成功', url('content.video/index'));
}
return $this->renderError($model->getError() ?: '更新失败');
} catch (Throwable $e) {
return $this->renderError('更新失败: ' . $e->getMessage());
}
}
/**
* 删除视频
* @param int|null $video_id 视频ID
* @return array|bool
*/
public function delete($video_id = null)
{
$video_id = $video_id ?? $this->request->param('video_id');
if (empty($video_id)) {
return $this->renderError('参数错误:缺少video_id');
}
$model = VideoModel::find($video_id);
if (!$model) {
return $this->renderError('视频不存在');
}
if (!$model->remove($video_id)) {
return $this->renderError($model->getError() ?: '删除失败');
}
return $this->renderSuccess('删除成功');
}
}
文件位置: app/api/model/Video.php
<?php
namespace apppimodel;
use appcommonmodelVideo as VideoModel;
class Video extends VideoModel
{
protected $hidden = [
'is_delete',
'wxapp_id',
'update_time'
];
/**
* 获取视频列表
* @param array $param 查询参数
* @return array
*/
public function getList($param)
{
$page = isset($param['page']) ? $param['page'] : 1;
$pageSize = isset($param['pageSize']) ? $param['pageSize'] : 10;
$data = parent::getPublishedList($page, $pageSize);
if (!empty($data['list'])) {
$data['list'] = $this->setVideoListDataFromApi($data['list']);
}
return $data;
}
/**
* 获取视频详情
* @param int $videoId 视频ID
* @return array|bool
*/
public function getDetail($videoId)
{
$detail = parent::getDetail($videoId);
if (empty($detail) || $detail['video_status']['value'] != 20) {
$this->error = '视频不存在或已下架';
return false;
}
$detail = $this->setVideoListDataFromApi($detail, false);
parent::incViewCount($videoId);
return $detail;
}
/**
* 格式化视频数据
* @param array $data 数据
* @param bool $isMultiple 是否多个
* @return array
*/
private function setVideoListDataFromApi($data, $isMultiple = true)
{
if (!$isMultiple) {
$dataSource = [&$data];
} else {
$dataSource = &$data;
}
foreach ($dataSource as &$video) {
if (isset($video['videoFile'])) {
$video['video_url'] = $video['videoFile']['file_path'];
}
if (isset($video['coverImage'])) {
$video['cover_url'] = $video['coverImage']['file_path'];
}
if (isset($video['create_time'])) {
$video['create_time'] = date('Y-m-d H:i', $video['create_time']);
}
}
return $data;
}
}
文件位置: app/api/controller/Video.php
<?php
namespace apppicontroller;
use apppimodelVideo as VideoModel;
class Video extends Controller
{
/**
* 视频列表
* @return hink
esponseJson
*/
public function lists()
{
$param = $this->request->param();
$model = new VideoModel;
$data = $model->getList($param);
return $this->renderSuccess($data);
}
/**
* 视频详情
* @param int $video_id 视频ID
* @return hink
esponseJson
*/
public function detail($video_id)
{
$model = new VideoModel;
$video = $model->getDetail($video_id);
if ($video === false) {
return $this->renderError($model->getError() ?: '视频信息不存在');
}
return $this->renderSuccess([
'detail' => $video
]);
}
/**
* 点赞
* @return hink
esponseJson
*/
public function like()
{
$videoId = $this->request->param('video_id');
$model = new VideoModel;
$video = $model->where('video_id', $videoId)->find();
if (!$video) {
return $this->renderError('视频不存在');
}
$model->incLikeCount($videoId);
return $this->renderSuccess([], '点赞成功');
}
/**
* 分享
* @return hink
esponseJson
*/
public function share()
{
$videoId = $this->request->param('video_id');
$model = new VideoModel;
$model->incShareCount($videoId);
return $this->renderSuccess([], '分享成功');
}
}
| 接口名称 | 请求方式 | 接口地址 | 说明 |
|---|---|---|---|
| 获取视频列表 | GET | /api/video/lists | 获取已发布的视频列表 |
| 获取视频详情 | GET | /api/video/detail | 获取单个视频的详细信息 |
| 点赞视频 | POST | /api/video/like | 增加视频点赞数 |
| 分享视频 | POST | /api/video/share | 增加视频分享数 |
接口地址: GET /api/video/lists
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int | 否 | 页码,默认1 |
| pageSize | int | 否 | 每页数量,默认10 |
返回数据:
{
"code": 1,
"msg": "success",
"data": {
"list": [
{
"video_id": 1,
"video_title": "视频标题",
"video_desc": "视频描述",
"video_url": "https://example.com/video.mp4",
"cover_url": "https://example.com/cover.jpg",
"video_status": {
"text": "已发布",
"value": 20
},
"view_count": 100,
"like_count": 50,
"share_count": 20,
"create_time": "2026-04-06 10:00",
"goods": [...]
}
],
"total": 10
}
}
接口地址: GET /api/video/detail
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_id | int | 是 | 视频ID |
返回数据:
{
"code": 1,
"msg": "success",
"data": {
"detail": {
"video_id": 1,
"video_title": "视频标题",
"video_desc": "视频描述",
"video_url": "https://example.com/video.mp4",
"cover_url": "https://example.com/cover.jpg",
"video_status": {
"text": "已发布",
"value": 20
},
"view_count": 100,
"like_count": 50,
"share_count": 20,
"create_time": "2026-04-06 10:00",
"goods": [
{
"goods_id": 1,
"goods_name": "商品名称",
"goods_image": "https://example.com/goods.jpg",
"goods_price": "99.00"
}
]
}
}
}
接口地址: POST /api/video/like
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_id | int | 是 | 视频ID |
返回数据:
{
"code": 1,
"msg": "点赞成功",
"data": []
}
接口地址: POST /api/video/share
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_id | int | 是 | 视频ID |
返回数据:
{
"code": 1,
"msg": "分享成功",
"data": []
}
文件位置: app/store/view/content/video/index.php
功能说明:
界面截图说明:
文件位置: app/store/view/content/video/add.php 和 edit.php
表单字段说明:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_title | 文本输入 | 是 | 视频标题 |
| video_desc | 文本域 | 否 | 视频描述 |
| video_file_id | 视频上传 | 是 | 视频文件 |
| cover_image_id | 图片上传 | 否 | 封面图 |
| video_status | 单选 | 是 | 视频状态:草稿/已发布/已下架 |
| video_sort | 数字输入 | 是 | 视频排序,数字越小越靠前 |
| goods_ids | 商品选择 | 否 | 绑定的商品列表 |
操作流程:
商家后台菜单配置在 app/store/config/menus.php 中,需要添加视频管理菜单项。
建议在小程序端创建以下页面:
wxapp/pages/
├── video/
│ ├── index/ # 视频列表页
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.wxml
│ │ └── index.wxss
│ └── detail/ # 视频详情页
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
文件位置: wxapp/pages/video/index.js
const App = getApp();
Page({
data: {
list: [],
page: 1,
pageSize: 10,
hasMore: true,
loading: false
},
onLoad() {
this.getVideoList();
},
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.getVideoList();
}
},
getVideoList() {
if (this.data.loading) return;
this.setData({ loading: true });
App._get('video/lists', {
page: this.data.page,
pageSize: this.data.pageSize
}, (result) => {
let list = this.data.list.concat(result.data.list);
this.setData({
list: list,
page: this.data.page + 1,
hasMore: list.length < result.data.total,
loading: false
});
}, null, () => {
this.setData({ loading: false });
});
},
onPlayVideo(e) {
const videoId = e.currentTarget.dataset.videoId;
wx.navigateTo({
url: '/pages/video/detail/index?video_id=' + videoId
});
}
});
文件位置: wxapp/pages/video/detail/index.js
const App = getApp();
Page({
data: {
videoId: 0,
detail: {},
goodsList: [],
showGoodsPanel: false
},
onLoad(e) {
this.setData({ videoId: e.video_id });
this.getVideoDetail();
},
onShow() {
if (this.data.videoId > 0) {
App._post('video/view', {
video_id: this.data.videoId
});
}
},
getVideoDetail() {
wx.showLoading({ title: '加载中' });
App._get('video/detail', {
video_id: this.data.videoId
}, (result) => {
this.setData({
detail: result.data.detail,
goodsList: result.data.detail.goods || []
});
wx.hideLoading();
}, null, () => {
wx.hideLoading();
});
},
onToggleGoodsPanel() {
this.setData({
showGoodsPanel: !this.data.showGoodsPanel
});
},
onGoToGoods(e) {
const goodsId = e.currentTarget.dataset.goodsId;
wx.navigateTo({
url: '/pages/goods/index?goods_id=' + goodsId
});
},
onLike() {
App._post('video/like', {
video_id: this.data.videoId
}, (result) => {
let detail = this.data.detail;
detail.like_count = (detail.like_count || 0) + 1;
this.setData({ detail: detail });
App.showSuccess('点赞成功');
});
},
onShare() {
App._post('video/share', {
video_id: this.data.videoId
});
},
onShareAppMessage() {
this.onShare();
return {
title: this.data.detail.video_title,
path: '/pages/video/detail/index?video_id=' + this.data.videoId
};
}
});
在商品详情页添加短视频浮动小窗功能:
文件位置: wxapp/pages/goods/index.js(在原有的基础上添加)
// 在 data 中添加
data: {
// ... 原有数据
hasVideo: false,
videoInfo: null
},
// 在 _initGoodsDetailData 方法中添加
_initGoodsDetailData(data) {
// ... 原有代码
// 检查是否有关联视频
if (data.video_info) {
this.setData({
hasVideo: true,
videoInfo: data.video_info
});
}
return data;
},
// 添加跳转视频方法
onGoToVideo() {
if (this.data.videoInfo) {
wx.navigateTo({
url: '/pages/video/detail/index?video_id=' + this.data.videoInfo.video_id
});
}
}
商品详情页视图(添加浮动小窗):
<!-- 浮动小窗 - 短视频入口 -->
<view wx:if="{{hasVideo}}" class="video-float-window" bindtap="onGoToVideo">
<image class="video-cover" src="{{videoInfo.cover_url}}" mode="aspectFill"></image>
<view class="video-play-icon">
<image src="/images/play-icon.png" mode="aspectFit"></image>
</view>
<view class="video-title">{{videoInfo.video_title}}</view>
</view>
<!-- 样式 -->
<style>
.video-float-window {
position: fixed;
right: 20rpx;
bottom: 200rpx;
width: 160rpx;
height: 220rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
z-index: 100;
}
.video-cover {
width: 100%;
height: 160rpx;
}
.video-play-icon {
position: absolute;
top: 40rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 80rpx;
}
.video-play-icon image {
width: 100%;
height: 100%;
}
.video-title {
padding: 8rpx 12rpx;
font-size: 24rpx;
color: #333;
background: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
| 模块 | 功能 | 状态 | 说明 |
|---|---|---|---|
| 数据库 | 数据表创建 | ✅ 完成 | ltshop_video和ltshop_video_goods_rel表已创建 |
| 后端公共层 | 公共模型 | ✅ 完成 | Video.php和VideoGoodsRel.php已实现 |
| 后端商店层 | 商店模型 | ✅ 完成 | Video.php商店层模型已实现 |
| 后端商店层 | 商店控制器 | ✅ 完成 | Video.php商店控制器已实现 |
| 后端API层 | API模型 | ✅ 完成 | Video.php API模型已实现 |
| 后端API层 | API控制器 | ✅ 完成 | Video.php API控制器已实现 |
| 商家后台 | 列表页视图 | ✅ 完成 | index.php视图已实现 |
| 商家后台 | 新增页视图 | ✅ 完成 | add.php视图已实现 |
| 商家后台 | 编辑页视图 | ✅ 完成 | edit.php视图已实现 |
| 商家后台 | 视频上传 | ✅ 完成 | 集成视频上传组件 |
| 商家后台 | 商品绑定 | ✅ 完成 | 支持选择商品绑定到视频 |
后端文件:
app/common/model/Video.php - 公共视频模型app/common/model/VideoGoodsRel.php - 视频-商品关联模型app/store/model/Video.php - 商店视频模型app/store/controller/content/Video.php - 商店视频控制器app/api/model/Video.php - API视频模型app/api/controller/Video.php - API视频控制器视图文件:
app/store/view/content/video/index.php - 视频列表页app/store/view/content/video/add.php - 新增视频页app/store/view/content/video/edit.php - 编辑视频页数据库文件:
sql/ltshop_video.sql - 视频相关表结构| 功能 | 说明 | 预计工作量 |
|---|---|---|
| 小程序视频列表页 | 瀑布流展示视频列表 | 2天 |
| 小程序视频详情页 | 全屏播放视频,展示绑定商品 | 3天 |
| 商品详情页视频入口 | 浮动小窗展示,点击跳转视频 | 1天 |
| 商品详情接口集成 | 在商品详情接口返回关联视频信息 | 0.5天 |
| DIY组件集成 | 将视频功能集成到DIY页面编辑器 | 2天 |
| 功能 | 说明 | 预计工作量 |
|---|---|---|
| 视频评论功能 | 用户可以对视频进行评论 | 3天 |
| 视频收藏功能 | 用户可以收藏喜欢的视频 | 2天 |
| 视频分类管理 | 支持视频分类 | 2天 |
| 视频数据统计 | 观看、点赞、分享等数据统计图表 | 2天 |
| 视频批量操作 | 批量上架、下架、删除视频 | 1天 |
| 功能 | 说明 | 预计工作量 |
|---|---|---|
| 视频话题功能 | 支持视频打标签/话题 | 3天 |
| 视频挑战赛 | 商家发起视频挑战赛活动 | 5天 |
| 视频直播联动 | 短视频与直播功能联动 | 5天 |
| 视频AI分析 | 视频内容分析、自动生成封面 | 7天 |
来团科技GEO优化&AI搜索优化系统,是通过大模型内容投喂+训练,将企业品牌及产品信息在多平台AI生成的答案中获取优先展现,更精准触达潜在目标客户,让企业品牌出现在AI搜索里。让客户一搜就看到你,实现一问就有你,一查就信你,一看就找你的营销效果。
来团智慧商业小程序零代码开发平台,多行业适配。无需代码,拖拽式设计,轻松打造订货商城、会员制商城、分销商城及小程序官网。不仅能满足通用需求,还支持定制化,从页面布局到功能模块,随心定制,助您快速搭建专属商业小程序,抢占市场先机。
来团科技微名通不止是电子名片,更是你的商业连接器。比起传统名片,它更像你的 “迷你商业工具”:信息多、好携带、能互动,还不浪费纸张。不管是跑业务、拓人脉,还是展示企业,一张「微名通」电子名片,就能帮你把商机揣在手机里。
来团科技CRM客户管理系统,帮你把 “线索→成交→回款” 全流程管明白。这就是一套 “让销售省心、老板放心” 的客户管理工具,从获客到回款,帮你把生意攥在手里。