首页 > 学习中心 > 开发技术 > 技术随笔

文档大纲

    来团SAAS商城视频功能完整教学文档

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台,多行业适配。无需代码,拖拽式设计,轻松打造订货商城、会员制商城、分销商城及小程序官网。不仅能满足通用需求,还支持定制化,从页面布局到功能模块,随心定制,助您快速搭建专属商业小程序,抢占市场先机。

    一、项目概述

    1.1 功能简介

    短视频功能是来团科技SAAS多租户商城系统的重要营销工具,主要功能包括:

    • 短视频上传与管理(支持视频封面、标题、描述)
    • 短视频与商品绑定(支持一个视频绑定多个商品)
    • 短视频列表展示(瀑布流或全屏滑动)
    • 短视频播放页面(左下角显示绑定商品列表)
    • 商品详情页短视频入口(浮动小窗展示,点击跳转短视频)
    • 短视频分享与传播

    1.2 技术栈

    组件 技术栈 版本
    后端框架 ThinkPHP ^8.0
    PHP版本 PHP >= 8.0.2
    ORM topthink/think-orm ^3.0
    前端框架 Layui -
    小程序 微信小程序 -
    缓存 Redis -

    1.3 核心业务流程

    ┌─────────────────────────────────────────────────────────────────────┐
    │ 短视频业务流程 │
    ├─────────────────────────────────────────────────────────────────────┤
    │ │
    │ [商家后台] │
    │ ↓ │
    │ [上传短视频] ──→ [填写视频信息] ──→ [绑定商品] ──→ [发布视频] │
    │ │
    │ [用户端] │
    │ ↓ │
    │ [浏览短视频列表] ──→ [播放短视频] ──→ [点击左下角商品] ──→ [跳转商品详情]│
    │ ↑ │
    │ [商品详情页] ←── [浮动小窗] ←── [商品已绑定短视频] │
    │ │
    └─────────────────────────────────────────────────────────────────────┘

    ---
    
    ## 二、后端架构设计
    
    ### 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小时
    

    三、数据库表结构设计

    3.1 短视频表(ltshop_video)

    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='短视频表';
    

    3.2 视频-商品关联表(ltshop_video_goods_rel)

    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='视频-商品关联表';
    

    3.3 字段详细说明

    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 创建时间戳

    3.4 ER图

    ┌─────────────────────┐       ┌───────────────────────┐       ┌─────────────────────┐
    │   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           │
    └─────────────────────┘
    

    四、后端核心代码实现

    4.1 公共视频模型

    文件位置: app/common/model/Video.php

    <?php
    
    namespace appcommonmodel;
    
    use thinkacadeCache;
    
    /**
     * 短视频公共模型
     * 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;
            }
        }
    }
    

    4.2 视频-商品关联模型

    文件位置: 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;
        }
    }
    

    4.3 商店视频模型

    文件位置: app/store/model/Video.php

    <?php
    
    namespace appstoremodel;
    
    use thinkacadeCache;
    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()
                ]);
        }
    }
    

    4.4 商店视频控制器

    文件位置: 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('删除成功');
        }
    }
    

    4.5 API视频模型

    文件位置: 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;
        }
    }
    

    4.6 API视频控制器

    文件位置: 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([], '分享成功');
        }
    }
    

    五、API接口设计

    5.1 接口说明

    接口名称 请求方式 接口地址 说明
    获取视频列表 GET /api/video/lists 获取已发布的视频列表
    获取视频详情 GET /api/video/detail 获取单个视频的详细信息
    点赞视频 POST /api/video/like 增加视频点赞数
    分享视频 POST /api/video/share 增加视频分享数

    5.2 接口详细说明

    1. 获取视频列表

    接口地址: 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
      }
    }
    

    2. 获取视频详情

    接口地址: 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"
            }
          ]
        }
      }
    }
    

    3. 点赞视频

    接口地址: POST /api/video/like

    请求参数:

    参数名 类型 必填 说明
    video_id int 视频ID

    返回数据:

    {
      "code": 1,
      "msg": "点赞成功",
      "data": []
    }
    

    4. 分享视频

    接口地址: POST /api/video/share

    请求参数:

    参数名 类型 必填 说明
    video_id int 视频ID

    返回数据:

    {
      "code": 1,
      "msg": "分享成功",
      "data": []
    }
    

    六、商家后台管理系统

    6.1 视频列表页

    文件位置: app/store/view/content/video/index.php

    功能说明:

    • 展示所有视频列表
    • 支持按状态筛选(草稿/已发布/已下架)
    • 支持按标题搜索
    • 支持分页浏览
    • 提供编辑、删除操作

    界面截图说明:

    • 顶部:新增按钮、状态筛选下拉框、搜索框
    • 中间:视频列表表格,包含ID、封面、标题、状态、观看/点赞/分享数、排序、添加时间、操作
    • 底部:分页控件

    6.2 新增/编辑视频页

    文件位置: app/store/view/content/video/add.phpedit.php

    表单字段说明:

    字段名 类型 必填 说明
    video_title 文本输入 视频标题
    video_desc 文本域 视频描述
    video_file_id 视频上传 视频文件
    cover_image_id 图片上传 封面图
    video_status 单选 视频状态:草稿/已发布/已下架
    video_sort 数字输入 视频排序,数字越小越靠前
    goods_ids 商品选择 绑定的商品列表

    操作流程:

    1. 填写视频基本信息(标题、描述)
    2. 上传视频文件
    3. 上传封面图(可选)
    4. 选择视频状态
    5. 设置排序值
    6. 选择要绑定的商品(支持多选)
    7. 点击确认提交

    6.3 菜单配置

    商家后台菜单配置在 app/store/config/menus.php 中,需要添加视频管理菜单项。


    七、小程序端实现

    7.1 项目结构

    建议在小程序端创建以下页面:

    wxapp/pages/
    ├── video/
    │   ├── index/           # 视频列表页
    │   │   ├── index.js
    │   │   ├── index.json
    │   │   ├── index.wxml
    │   │   └── index.wxss
    │   └── detail/          # 视频详情页
    │       ├── index.js
    │       ├── index.json
    │       ├── index.wxml
    │       └── index.wxss
    

    7.2 视频列表页实现

    文件位置: 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
        });
      }
    });
    

    7.3 视频详情页实现

    文件位置: 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
        };
      }
    });
    

    7.4 商品详情页集成

    在商品详情页添加短视频浮动小窗功能:

    文件位置: 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>
    

    八、当前开发进度

    8.1 已完成功能

    模块 功能 状态 说明
    数据库 数据表创建 ✅ 完成 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视图已实现
    商家后台 视频上传 ✅ 完成 集成视频上传组件
    商家后台 商品绑定 ✅ 完成 支持选择商品绑定到视频

    8.2 涉及文件清单

    后端文件:

    • 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 - 视频相关表结构

    九、待开发功能清单

    9.1 高优先级

    功能 说明 预计工作量
    小程序视频列表页 瀑布流展示视频列表 2天
    小程序视频详情页 全屏播放视频,展示绑定商品 3天
    商品详情页视频入口 浮动小窗展示,点击跳转视频 1天
    商品详情接口集成 在商品详情接口返回关联视频信息 0.5天
    DIY组件集成 将视频功能集成到DIY页面编辑器 2天

    9.2 中优先级

    功能 说明 预计工作量
    视频评论功能 用户可以对视频进行评论 3天
    视频收藏功能 用户可以收藏喜欢的视频 2天
    视频分类管理 支持视频分类 2天
    视频数据统计 观看、点赞、分享等数据统计图表 2天
    视频批量操作 批量上架、下架、删除视频 1天

    9.3 低优先级

    功能 说明 预计工作量
    视频话题功能 支持视频打标签/话题 3天
    视频挑战赛 商家发起视频挑战赛活动 5天
    视频直播联动 短视频与直播功能联动 5天
    视频AI分析 视频内容分析、自动生成封面 7天

    十、开发注意

    推荐商品

    更多
    来团GEO-AI搜索优化系统 用AI打造企业品牌

    来团GEO-AI搜索优化系统 用AI打造企业品牌

    来团科技GEO优化&AI搜索优化系统,是通过大模型内容投喂+训练,将企业品牌及产品信息在多平台AI生成的答案中获取优先展现,更精准触达潜在目标客户,让企业品牌出现在AI搜索里。让客户一搜就看到你,实现一问就有你,一查就信你,一看就找你的营销效果。

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台,多行业适配。无需代码,拖拽式设计,轻松打造订货商城、会员制商城、分销商城及小程序官网。不仅能满足通用需求,还支持定制化,从页面布局到功能模块,随心定制,助您快速搭建专属商业小程序,抢占市场先机。

    微名通名片 VIP年卡会员 | SVIP永久会员

    微名通名片 VIP年卡会员 | SVIP永久会员

    来团科技微名通不止是电子名片,更是你的商业连接器。比起传统名片,它更像你的 “迷你商业工具”:信息多、好携带、能互动,还不浪费纸张。不管是跑业务、拓人脉,还是展示企业,一张「微名通」电子名片,就能帮你把商机揣在手机里。

    来团LTCRM客户管理系统 可独立部署

    来团LTCRM客户管理系统 可独立部署

    来团科技CRM客户管理系统,帮你把 “线索→成交→回款” 全流程管明白。这就是一套 “让销售省心、老板放心” 的客户管理工具,从获客到回款,帮你把生意攥在手里。

    大纲

    文档目录

      联系我们
      联系方式
      • 官方服务热线:17721141027
      • 邮箱:kf@ilaituan.com
      • QQ:20262336
      扫码添加客服
      微信