xiangpei
2025-05-19 ad2fc69d87a8ba4d2f4b248e571b7207bdd9261e
集成腾讯云cos、小程序视频发布功能
8个文件已修改
34个文件已添加
1 文件已重命名
2228 ■■■■■ 已修改文件
buyer-api/src/main/java/cn/lili/controller/lmk/VideoController.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
buyer-api/src/main/java/cn/lili/controller/lmk/VideoTagController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
buyer-api/src/test/java/cn/lili/buyer/test/cart/LmkFileTest.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
common-api/src/main/java/cn/lili/controller/lmk/LmkFileController.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/application.yml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/pom.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/common/exception/BaseException.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/common/exception/FileFormatNotSupport.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/common/security/context/UserContext.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/cos/COSConfigProperty.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/cos/CosSTS.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/entity/LmkFile.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/entity/Video.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/entity/VideoTag.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/entity/VideoTagRef.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/form/FileInfoForm.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/form/VideoForm.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/form/WxVideoTagForm.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/query/VideoQuery.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/query/WxVideoTagQuery.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/vo/LmkFileVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoVO.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/enums/general/FileTypeEnum.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/enums/general/VideoStatusEnum.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/mapper/LmkFileMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoTagRefMapper.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/LmkFileService.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/VideoService.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/VideoTagRefService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/VideoTagService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/impl/LmkFileServiceImpl.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoServiceImpl.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoTagRefServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoTagServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/utils/COSUtil.java 285 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/utils/FileUtil.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/LmkFileMapper.xml 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/VideoMapper.xml 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/VideoTagMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/VideoTagRefMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager-api/src/main/java/cn/lili/controller/lmk/VideoController.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
buyer-api/src/main/java/cn/lili/controller/lmk/VideoController.java
New file
@@ -0,0 +1,77 @@
package cn.lili.controller.lmk;
import cn.lili.group.Update;
import cn.lili.group.Add;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import cn.lili.modules.lmk.service.VideoService;
import cn.lili.base.Result;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
 * 视频内容 前端控制器
 *
 * @author xp
 * @since 2025-05-16
 */
@Validated
@RequiredArgsConstructor
@Api(value = "视频内容", tags = "视频内容管理")
@RestController
@RequestMapping("/buyer/lmk/video")
public class VideoController {
    private final VideoService videoService;
    @PostMapping("/publish")
    @ApiOperation(value = "发布视频", notes = "发布视频")
    public Result publish(@RequestBody @Validated({Add.class}) VideoForm form) {
        return videoService.publish(form);
    }
    @PutMapping
    @ApiOperation(value = "修改", notes = "修改")
    public Result update(@RequestBody @Validated(Update.class) VideoForm form) {
        return videoService.update(form);
    }
    @DeleteMapping("/{id}")
    @ApiOperation(value = "ID删除", notes = "ID删除")
    public Result removeById(@PathVariable("id") String id) {
        return videoService.removeById(id);
    }
    @DeleteMapping("/batch")
    @ApiOperation(value = "批量删除", notes = "批量删除")
    public Result remove(@RequestBody @NotEmpty(message = "请选择数据") List<String> ids) {
        return videoService.remove(ids);
    }
    @GetMapping("/page")
    @ApiOperation(value = "分页", notes = "分页")
    public Result page(VideoQuery query) {
        return videoService.page(query);
    }
    @GetMapping("/{id}")
    @ApiOperation(value = "详情", notes = "详情")
    public Result detail(@PathVariable("id") Integer id) {
        return videoService.detail(id);
    }
    @GetMapping("/list")
    @ApiOperation(value = "列表", notes = "列表")
    public Result list() {
        return videoService.all();
    }
}
buyer-api/src/main/java/cn/lili/controller/lmk/VideoTagController.java
New file
@@ -0,0 +1,40 @@
package cn.lili.controller.lmk;
import cn.lili.base.Result;
import cn.lili.group.Update;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import cn.lili.modules.lmk.domain.query.WxVideoTagQuery;
import cn.lili.modules.lmk.service.VideoService;
import cn.lili.modules.lmk.service.VideoTagService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
 * 视频标签 前端控制器
 *
 * @author xp
 * @since 2025-05-16
 */
@Validated
@RequiredArgsConstructor
@Api(value = "视频标签", tags = "视频标签")
@RestController
@RequestMapping("/buyer/lmk/video/tag")
public class VideoTagController {
    private final VideoTagService videoTagService;
    @GetMapping("/recommend")
    @ApiOperation(value = "推荐标签", notes = "推荐标签")
    public Result recommend(WxVideoTagQuery query) {
        return videoTagService.recommend(query);
    }
}
buyer-api/src/test/java/cn/lili/buyer/test/cart/LmkFileTest.java
File was renamed from buyer-api/src/test/java/cn/lili/buyer/test/cart/FileTest.java
@@ -23,7 +23,7 @@
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest
class FileTest {
class LmkFileTest {
    @Autowired
common-api/src/main/java/cn/lili/controller/lmk/LmkFileController.java
New file
@@ -0,0 +1,112 @@
package cn.lili.controller.lmk;
import cn.lili.base.Result;
import cn.lili.modules.lmk.service.LmkFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.annotations.Delete;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
 * 文件信息 前端控制器
 *
 * @author xp
 * @since 2025-05-19
 */
@Validated
@RequiredArgsConstructor
@Api(value = "文件管理", tags = "文件管理")
@RestController("lmkFileController")
@RequestMapping("/common/lmk/file")
public class LmkFileController {
    private final LmkFileService lmkFileService;
    /**
     * 单文件上传(返回有过期时间的链接)
     *
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation(value = "单文件上传(返回有过期时间的链接)")
    public Result upload(@RequestPart("file") @NotNull MultipartFile file) {
        return lmkFileService.uploadObject(file);
    }
    /**
     * 多文件上传
     *
     * @param files
     * @return
     */
    @PostMapping("/multi/upload")
    @ApiOperation(value = "多文件上传(返回有过期时间的链接)")
    public Result uploads(@RequestPart("files") @NotEmpty List<MultipartFile> files) {
        return lmkFileService.uploadObjects(files);
    }
    /**
     * 删除某个文件
     *
     * @param fileKey oss文件名
     * @return
     */
    @Delete("/delete/{file_key}")
    @ApiOperation(value = "删除某个文件")
    public Result deleteObject(@PathVariable(value = "file_key") String fileKey) {
        return lmkFileService.deleteObject(fileKey);
    }
    /**
     * 批量删除文件
     *
     * @param fileKeys
     * @return
     */
    @Delete("/delete/files")
    @ApiOperation(value = "批量删除文件")
    public Result deleteObjects(@RequestBody @NotEmpty List<String> fileKeys) {
        return lmkFileService.deleteObjects(fileKeys);
    }
    /**
     * 下载文件
     *
     * @param fileKey
     * @return
     */
    @GetMapping("/download/{file_key}")
    @ApiOperation(value = "下载文件")
    public void downloadFile(@PathVariable(value = "file_key") String fileKey, HttpServletResponse response) {
        lmkFileService.downloadObject(fileKey, response);
    }
    /**
     * 预览文件
     *
     * @param fileKey
     * @return
     */
    @GetMapping("/preview/{file_key}")
    @ApiOperation(value = "获取文件预览url(链接存在时效)")
    public Result getPreviewUrl(@PathVariable(value = "file_key") String fileKey) {
        return Result.ok().data(lmkFileService.getPreviewUrl(fileKey));
    }
    @GetMapping("/sts")
    @ApiOperation(value = "获取STS访问令牌", notes = "前端做直传")
    public Result getSTSToken() {
        return Result.ok().data(lmkFileService.getSTSToken());
    }
}
config/application.yml
@@ -314,3 +314,31 @@
      port: 8891
      logpath: ./xxl-job/executor
      logretentiondays: 7
# 腾讯cos-sts配置
cos:
  secretId: AKIDYyBCzb1FOPGx0fCXfdOwJVWM1TjqmW3N  # 腾讯ARM用户的secretId
  secretKey: DD1b1LWVIvPlusAOYjnfKm150jO0NYWH  # 腾讯ARM用户的secretKey
  durationSeconds: 1800  # STS临时访问凭证有效期,单位秒,默认1800s,主账号最长2小时,子账号(ARM用户)36小时
  bucket: lmk-1308069279
  region: ap-chengdu
  urlExpireMinute: 360  # 预签名url有效时间(分钟)
  actions:  # sts的权限
    - cos:ListMultipartUploads
    - cos:GetBucket
    - cos:GetObject
    - cos:GetObjectTagging
    - cos:GetSymlink
    - cos:HeadObject
    - cos:ListParts
    - cos:AbortMultipartUpload
    - cos:AppendObject
    - cos:CompleteMultipartUpload
    - cos:InitiateMultipartUpload
    - cos:PostObject
    - cos:PutObject
    - cos:UploadPart
    - cos:DeleteObject
    - cos:PutObjectTagging
  resources: # 能操作哪些资源
    - qcs::cos:ap-chengdu:uid/1308069279:lmk-1308069279/*
framework/pom.xml
@@ -15,6 +15,20 @@
    <packaging>jar</packaging>
    <dependencies>
        <!-- 腾讯cos对象存储服务 -->
        <dependency>
            <groupId>com.qcloud</groupId>
            <artifactId>cos_api</artifactId>
            <version>${tx-cos.version}</version>
        </dependency>
        <dependency>
            <groupId>com.qcloud</groupId>
            <artifactId>cos-sts_api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
@@ -471,4 +485,4 @@
    </dependencies>
</project>
</project>
framework/src/main/java/cn/lili/common/exception/BaseException.java
New file
@@ -0,0 +1,35 @@
package cn.lili.common.exception;
/**
 * 基础非检查异常
 * @author 29443
 * @version 1.0
 * @date 2022/4/25
 */
public class BaseException extends RuntimeException {
    private String msg;
    private Integer code;
    public BaseException(String msg, Throwable cause) {
        super(msg, cause);
    }
    public BaseException(String msg) {
        super(msg);
    }
    public BaseException(String msg, Integer code) {
        this.msg = msg;
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public Integer getCode() {
        return code;
    }
}
framework/src/main/java/cn/lili/common/exception/FileFormatNotSupport.java
New file
@@ -0,0 +1,32 @@
package cn.lili.common.exception;
/**
 * @author 29443
 * @version 1.0
 * @date 2022/4/25
 */
public class FileFormatNotSupport extends BaseException {
    private String suffix;
    public FileFormatNotSupport(String msg, Throwable cause) {
        super(msg, cause);
    }
    public FileFormatNotSupport(String msg) {
        super(msg);
    }
    public FileFormatNotSupport(String msg, String suffix) {
        super(msg);
        this.suffix = suffix;
    }
    public FileFormatNotSupport(String msg, Integer code) {
        super(msg, code);
    }
    public String getSuffix() {
        return suffix;
    }
}
framework/src/main/java/cn/lili/common/security/context/UserContext.java
@@ -26,6 +26,20 @@
public class UserContext {
    /**
     * 根据request获取用户id
     *
     * @return 授权用户
     */
    public static String getCurrentUserId() {
        if (RequestContextHolder.getRequestAttributes() != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String accessToken = request.getHeader(SecurityEnum.HEADER_TOKEN.getValue());
            return getAuthUser(accessToken).getId();
        }
        return null;
    }
    /**
     * 根据request获取用户信息
     *
     * @return 授权用户
framework/src/main/java/cn/lili/cos/COSConfigProperty.java
New file
@@ -0,0 +1,105 @@
package cn.lili.cos;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
 * 读取配置文件关于cos的配置
 *
 * @author:xp
 * @date:2025/5/16 16:23
 */
@Configuration
@ConfigurationProperties(prefix = "cos")
public class COSConfigProperty {
    /** 腾讯ARM用户的secretId */
    private String secretId;
    /** 腾讯ARM用户的secretKey */
    private String secretKey;
    /** STS临时访问凭证有效期,单位秒,默认1800s,主账号最长2小时,子账号(ARM用户)36小时 */
    private Integer durationSeconds;
    /** bucket所在的地域 */
    private String region;
    /** bucket名称 */
    private String bucket;
    /** 预签名url过期时间(分钟) */
    private Integer urlExpireMinute;
    /** sts的权限 */
    private String[] actions;
    /** sts能操作的资源 */
    private String[] resources;
    public String getSecretId() {
        return secretId;
    }
    public void setSecretId(String secretId) {
        this.secretId = secretId;
    }
    public String getSecretKey() {
        return secretKey;
    }
    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }
    public Integer getDurationSeconds() {
        return durationSeconds;
    }
    public void setDurationSeconds(Integer durationSeconds) {
        this.durationSeconds = durationSeconds;
    }
    public String getRegion() {
        return region;
    }
    public void setRegion(String region) {
        this.region = region;
    }
    public String getBucket() {
        return bucket;
    }
    public void setBucket(String bucket) {
        this.bucket = bucket;
    }
    public String[] getActions() {
        return actions;
    }
    public void setActions(String[] actions) {
        this.actions = actions;
    }
    public String[] getResources() {
        return resources;
    }
    public void setResources(String[] resources) {
        this.resources = resources;
    }
    public Integer getUrlExpireMinute() {
        return urlExpireMinute;
    }
    public void setUrlExpireMinute(Integer urlExpireMinute) {
        this.urlExpireMinute = urlExpireMinute;
    }
}
framework/src/main/java/cn/lili/cos/CosSTS.java
New file
@@ -0,0 +1,40 @@
package cn.lili.cos;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * sts临时访问凭证对象
 * @author:xp
 * @date:2025/5/16 16:33
 */
@Data
@ApiModel("STS临时访问凭证")
public class CosSTS {
    @ApiModelProperty("临时密钥id")
    private String tmpSecretId;
    @ApiModelProperty("临时密钥key")
    private String tmpSecretKey;
    @ApiModelProperty("sts-token")
    private String sessionToken;
    @ApiModelProperty("sts起效时间")
    private Long stsStartTime;
    @ApiModelProperty("sts失效时间")
    private Long stsEndTime;
    @ApiModelProperty("bucket")
    private String bucket;
    @ApiModelProperty("region")
    private String region;
}
framework/src/main/java/cn/lili/modules/lmk/domain/entity/LmkFile.java
New file
@@ -0,0 +1,38 @@
package cn.lili.modules.lmk.domain.entity;
import cn.lili.mybatis.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
/**
 * 文件信息
 *
 * @author xp
 * @since 2025-05-19
 */
@Data
@TableName("lmk_file")
public class LmkFile extends BaseEntity {
    private static final long serialVersionUID = 1L;
    @TableField("file_key")
    /** 文件唯一标识 */
    private String fileKey;
    @TableField("file_type")
    /** 文件类型 */
    private String fileType;
    @TableField("file_size")
    /** 文件大小 */
    private Long fileSize;
    @TableField("original_filename")
    /** 文件原始名 */
    private String originalFileName;
}
framework/src/main/java/cn/lili/modules/lmk/domain/entity/Video.java
New file
@@ -0,0 +1,83 @@
package cn.lili.modules.lmk.domain.entity;
import cn.lili.mybatis.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
/**
 * 视频内容
 *
 * @author xp
 * @since 2025-05-16
 */
@Data
@TableName("lmk_video")
public class Video extends BaseEntity {
    private static final long serialVersionUID = 1L;
    @TableField("author_id")
    /** 作者id */
    private String authorId;
    @TableField("cover_url")
    /** 图片封面 */
    private String coverUrl;
    @TableField("video_file_key")
    /** 视频地址 */
    private String videoFileKey;
    @TableField("video_fit")
    /** 视频填充模式 */
    private String videoFit;
    @TableField("title")
    /** 视频标题 */
    private String title;
    @TableField("goods_id")
    /** 商品id */
    private String goodsId;
    @TableField("goods_view_num")
    /** 商品查看次数 */
    private Long goodsViewNum;
    @TableField("goods_order_num")
    /** 商品下单次数 */
    private Long goodsOrderNum;
    @TableField("recommend")
    /** 是否推荐 */
    private Boolean recommend;
    @TableField("status")
    /** 状态 */
    private String status;
    @TableField("play_num")
    /** 播放量 */
    private Long playNum;
    @TableField("collect_num")
    /** 收藏数 */
    private Long collectNum;
    @TableField("comment_num")
    /** 评论数 */
    private Long commentNum;
    @TableField("weight")
    /** 权重 */
    private Double weight;
    @TableField("audit_pass_time")
    /** 审核通过时间 */
    private LocalDateTime auditPassTime;
}
framework/src/main/java/cn/lili/modules/lmk/domain/entity/VideoTag.java
@@ -26,5 +26,8 @@
    /** 创建方式 */
    private String createType;
    @TableField("use_num")
    /** 视频引用次数 */
    private Long useNum;
}
framework/src/main/java/cn/lili/modules/lmk/domain/entity/VideoTagRef.java
New file
@@ -0,0 +1,31 @@
package cn.lili.modules.lmk.domain.entity;
import cn.lili.mybatis.BaseEntity;
import cn.lili.mybatis.BaseIdEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
/**
 * 视频标签中间表
 *
 * @author xp
 * @since 2025-05-19
 */
@Data
@TableName("lmk_video_tag_ref")
public class VideoTagRef extends BaseIdEntity {
    private static final long serialVersionUID = 1L;
    @TableField("video_id")
    /** 视频id */
    private String videoId;
    @TableField("video_tag_id")
    /** 视频标签id */
    private String videoTagId;
}
framework/src/main/java/cn/lili/modules/lmk/domain/form/FileInfoForm.java
New file
@@ -0,0 +1,31 @@
package cn.lili.modules.lmk.domain.form;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
/**
 * 小程序直传,需要把上传的文件信息传到后端保存
 *
 * @author:xp
 * @date:2025/5/19 15:17
 */
@Data
@Validated
@ApiModel("小程序直传文件信息")
public class FileInfoForm {
    @ApiModelProperty("文件标识")
    private String fileKey;
    @ApiModelProperty("文件类型")
    private String fileType;
    @ApiModelProperty("文件大小")
    private Long fileSize;
    @ApiModelProperty("文件原始名")
    private String originalFileName;
}
framework/src/main/java/cn/lili/modules/lmk/domain/form/VideoForm.java
New file
@@ -0,0 +1,64 @@
package cn.lili.modules.lmk.domain.form;
import cn.lili.group.Update;
import cn.lili.group.Add;
import cn.lili.base.AbsForm;
import cn.lili.modules.lmk.domain.entity.Video;
import org.hibernate.validator.constraints.Length;
import org.springframework.beans.BeanUtils;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.springframework.lang.NonNull;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * 视频内容表单
 *
 * @author xp
 * @since 2025-05-16
 */
@Data
@ApiModel(value = "Video表单", description = "视频内容表单")
public class VideoForm extends AbsForm {
    @NotBlank(message = "视频不能为空", groups = {Add.class, Update.class})
    @ApiModelProperty("视频")
    private String videoFileKey;
    @NotBlank(message = "标题不能为空", groups = {Add.class, Update.class})
    @ApiModelProperty("标题")
    private String title;
    @ApiModelProperty("视频标签")
    @Length(max = 5, message = "最多只能添加五个标签")
    private List<WxVideoTagForm> tags = new ArrayList<>(2);
//    @NotBlank(message = "商品id不能为空", groups = {Add.class, Update.class})
    @ApiModelProperty("商品id")
    private String goodsId;
    @ApiModelProperty("视频填充模式")
    private String videoFit;
    @ApiModelProperty("文件信息")
    @Valid
    private FileInfoForm fileInfo;
    public static Video getEntityByForm(@NonNull VideoForm form, Video entity) {
        if(entity == null) {
          entity = new Video();
        }
        BeanUtils.copyProperties(form, entity);
        return entity;
    }
}
framework/src/main/java/cn/lili/modules/lmk/domain/form/WxVideoTagForm.java
New file
@@ -0,0 +1,30 @@
package cn.lili.modules.lmk.domain.form;
import cn.lili.base.AbsForm;
import cn.lili.group.Add;
import cn.lili.group.Update;
import cn.lili.modules.lmk.domain.entity.VideoTag;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.beans.BeanUtils;
import org.springframework.lang.NonNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
 * 视频标签表单
 *
 * @author xp
 * @since 2025-05-13
 */
@Data
@ApiModel(value = "VideoTag表单", description = "视频标签表单")
public class WxVideoTagForm extends AbsForm {
    @ApiModelProperty("标签名称")
    private String tagName;
}
framework/src/main/java/cn/lili/modules/lmk/domain/query/VideoQuery.java
New file
@@ -0,0 +1,22 @@
package cn.lili.modules.lmk.domain.query;
import cn.lili.base.AbsQuery;
import java.util.List;
import org.springframework.lang.NonNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 视频内容查询
 *
 * @author xp
 * @since 2025-05-16
 */
@Data
@ApiModel(value = "Video查询参数", description = "视频内容查询参数")
public class VideoQuery extends AbsQuery {
}
framework/src/main/java/cn/lili/modules/lmk/domain/query/WxVideoTagQuery.java
New file
@@ -0,0 +1,25 @@
package cn.lili.modules.lmk.domain.query;
import cn.lili.base.AbsQuery;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 视频标签查询
 *
 * @author xp
 * @since 2025-05-13
 */
@Data
@ApiModel(value = "VideoTag查询参数", description = "视频标签查询参数")
public class WxVideoTagQuery {
    @ApiModelProperty("标签名称")
    private String tagName;
    @ApiModelProperty("搜索类型:HOT(热门标签)、SEARCH(根据输入的标签名称搜索)")
    private String searchType = "HOT";
}
framework/src/main/java/cn/lili/modules/lmk/domain/vo/LmkFileVO.java
New file
@@ -0,0 +1,49 @@
package cn.lili.modules.lmk.domain.vo;
import cn.lili.base.AbsVo;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import lombok.experimental.Accessors;
import org.springframework.lang.NonNull;
import org.springframework.beans.BeanUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 文件信息展示
 *
 * @author xp
 * @since 2025-05-19
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "文件信息响应数据", description = "文件信息响应数据")
public class LmkFileVO extends AbsVo {
    /**
     * 临时访问链接
     */
    @ApiModelProperty("临时访问链接")
    private String url;
    /**
     * fileKey数据库保存的值
     */
    @ApiModelProperty("文件标识")
    private String fileKey;
    /**
     * 文件名
     */
    @ApiModelProperty("文件原始名")
    private String originalName;
    public static LmkFileVO getVoByEntity(@NonNull LmkFile entity, LmkFileVO vo) {
        if(vo == null) {
            vo = new LmkFileVO();
        }
        BeanUtils.copyProperties(entity, vo);
        return vo;
    }
}
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoVO.java
New file
@@ -0,0 +1,91 @@
package cn.lili.modules.lmk.domain.vo;
import cn.lili.base.AbsVo;
import cn.lili.modules.lmk.domain.entity.Video;
import java.util.List;
import org.springframework.lang.NonNull;
import org.springframework.beans.BeanUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * 视频内容展示
 *
 * @author xp
 * @since 2025-05-16
 */
@Data
@ApiModel(value = "视频内容响应数据", description = "视频内容响应数据")
public class VideoVO extends AbsVo {
    /** 作者id */
    @ApiModelProperty("作者id")
    private String authorId;
    /** 图片封面 */
    @ApiModelProperty("图片封面")
    private String coverUrl;
    /** 视频地址 */
    @ApiModelProperty("视频地址")
    private String videoFileKey;
    /** 视频填充模式 */
    @ApiModelProperty("视频填充模式")
    private String videoFit;
    /** 视频标题 */
    @ApiModelProperty("视频标题")
    private String title;
    /** 商品id */
    @ApiModelProperty("商品id")
    private String goodsId;
    /** 商品查看次数 */
    @ApiModelProperty("商品查看次数")
    private Long goodsViewNum;
    /** 商品下单次数 */
    @ApiModelProperty("商品下单次数")
    private Long goodsOrderNum;
    /** 是否推荐 */
    @ApiModelProperty("是否推荐")
    private Boolean recommend;
    /** 状态 */
    @ApiModelProperty("状态")
    private String status;
    /** 播放量 */
    @ApiModelProperty("播放量")
    private Long playNum;
    /** 收藏数 */
    @ApiModelProperty("收藏数")
    private Long collectNum;
    /** 评论数 */
    @ApiModelProperty("评论数")
    private Long commentNum;
    /** 权重 */
    @ApiModelProperty("权重")
    private double weight;
    /** 审核通过时间 */
    @ApiModelProperty("审核通过时间")
    private Date auditPassTime;
    public static VideoVO getVoByEntity(@NonNull Video entity, VideoVO vo) {
        if(vo == null) {
            vo = new VideoVO();
        }
        BeanUtils.copyProperties(entity, vo);
        return vo;
    }
}
framework/src/main/java/cn/lili/modules/lmk/enums/general/FileTypeEnum.java
New file
@@ -0,0 +1,54 @@
package cn.lili.modules.lmk.enums.general;
import java.util.Arrays;
import java.util.List;
/**
 * oss文件分类目录,文件类型白名单
 * @author 29443
 *
 */
public enum FileTypeEnum {
    IMAGE("image", "图片", Arrays.asList("jpg", "png", "jpeg", "gif", "bmp", "webp", "tiff", "svg", "ico", "psd", "raw")),
    VIDEO("video", "视频", Arrays.asList("mp4", "avi", "rmvb", "mov", "wmv", "flv", "mkv", "mpeg", "mpg", "m4v", "3gp", "webm", "vob", "swf")),
    RADIO("radio", "音频", Arrays.asList("mp3", "wma", "wav", "mpeg-4", "cd", "m4a", "aac", "flac", "ogg", "aiff", "ape", "midi", "amr", "ra")),
    TEXT("text", "文本文件", Arrays.asList("txt", "xls", "xlsx", "doc", "docx", "pdf", "ppt", "pptx", "csv", "rtf", "odt", "ods", "odp", "epub", "mobi", "pages", "numbers", "key")),
    ZIP("zip", "压缩文件", Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "iso", "dmg", "pkg", "cab", "z", "lz", "lzma", "lzo")),
    ;
    /**
     * 类型
     */
    private final String type;
    /**
     * 类型对应的后缀
     */
    private final List<String> suffixs;
    /**
     * 描述
     */
    private final String desc;
    FileTypeEnum(String type, String desc, List<String> suffixs) {
        this.type = type;
        this.suffixs = suffixs;
        this.desc = desc;
    }
    public String getType() {
        return type;
    }
    public List<String> getSuffixs() {
        return suffixs;
    }
}
framework/src/main/java/cn/lili/modules/lmk/enums/general/VideoStatusEnum.java
New file
@@ -0,0 +1,48 @@
package cn.lili.modules.lmk.enums.general;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/**
 * 视频标签来源
 *
 * @author:xp
 * @date:2025/5/14 10:30
 */
@Getter
public enum VideoStatusEnum {
    AUDITING("99", "审核中"),
    PUBLISHED("1", "已发布"),
    DISABLE("0", "已下架"),
    REJECT("-1", "审核未通过"),
    ;
    private final String value;
    private final String desc;
    VideoStatusEnum(String value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    /**
     * 获取含义
     *
     * @param value
     * @return
     */
    public static String getDescByValue(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        for (VideoStatusEnum e : VideoStatusEnum.values()){
            if (value.equals(e.getValue())) {
                return e.getDesc();
            }
        }
        return null;
    }
}
framework/src/main/java/cn/lili/modules/lmk/mapper/LmkFileMapper.java
New file
@@ -0,0 +1,26 @@
package cn.lili.modules.lmk.mapper;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import cn.lili.modules.lmk.domain.vo.LmkFileVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
 * 文件信息 Mapper 接口
 *
 * @author xp
 * @since 2025-05-19
 */
@Mapper
public interface LmkFileMapper extends BaseMapper<LmkFile> {
    /**
     * 查找文件信息
     *
     * @param fileKey
     * @return
     */
    LmkFile getByFileKey(String fileKey);
}
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoMapper.java
New file
@@ -0,0 +1,34 @@
package cn.lili.modules.lmk.mapper;
import cn.lili.modules.lmk.domain.entity.Video;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.lili.modules.lmk.domain.vo.VideoVO;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * 视频内容 Mapper 接口
 *
 * @author xp
 * @since 2025-05-16
 */
@Mapper
public interface VideoMapper extends BaseMapper<Video> {
    /**
     * id查找视频内容
     * @param id
     * @return
     */
    VideoVO getById(Integer id);
    /**
    *  分页
    */
    IPage getPage(IPage page, @Param("query") VideoQuery query);
}
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoTagRefMapper.java
New file
@@ -0,0 +1,17 @@
package cn.lili.modules.lmk.mapper;
import cn.lili.modules.lmk.domain.entity.VideoTagRef;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
 * 视频标签中间表 Mapper 接口
 *
 * @author xp
 * @since 2025-05-19
 */
@Mapper
public interface VideoTagRefMapper extends BaseMapper<VideoTagRef> {
}
framework/src/main/java/cn/lili/modules/lmk/service/LmkFileService.java
New file
@@ -0,0 +1,83 @@
package cn.lili.modules.lmk.service;
import cn.lili.base.Result;
import cn.lili.cos.CosSTS;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import cn.lili.modules.lmk.domain.form.FileInfoForm;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * 文件信息 服务类
 *
 * @author xp
 * @since 2025-05-19
 */
public interface LmkFileService extends IService<LmkFile> {
    /**
     * 上传单个文件
     *
     * @param file
     * @return
     */
    Result uploadObject(MultipartFile file);
    /**
     * 上传多个文件
     *
     * @param files
     * @return
     */
    Result uploadObjects(List<MultipartFile> files);
    /**
     * 删除单个文件
     *
     * @param fileKey
     * @return
     */
    Result deleteObject(String fileKey);
    /**
     * 删除多个文件
     *
     * @param fileKeys
     * @return
     */
    Result deleteObjects(List<String> fileKeys);
    /**
     * 下载文件
     *
     * @param fileKey
     * @param response
     * @return
     */
    void downloadObject(String fileKey, HttpServletResponse response);
    /**
     * 预览文件
     *
     * @param fileKey
     * @return
     */
    String getPreviewUrl(String fileKey);
    /**
     * 获取sts临时访问凭证
     *
     * @return
     */
    CosSTS getSTSToken();
    /**
     * 保存文件信息
     *
     * @param fileInfo
     */
    void addByForm(FileInfoForm fileInfo);
}
framework/src/main/java/cn/lili/modules/lmk/service/VideoService.java
New file
@@ -0,0 +1,73 @@
package cn.lili.modules.lmk.service;
import cn.lili.modules.lmk.domain.entity.Video;
import com.baomidou.mybatisplus.extension.service.IService;
import cn.lili.base.Result;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import java.util.List;
/**
 * 视频内容 服务类
 *
 * @author xp
 * @since 2025-05-16
 */
public interface VideoService extends IService<Video> {
    /**
     * 添加
     * @param form
     * @return
     */
    Result add(VideoForm form);
    /**
     * 修改
     * @param form
     * @return
     */
    Result update(VideoForm form);
    /**
     * 批量删除
     * @param ids
     * @return
     */
    Result remove(List<String> ids);
    /**
     * id删除
     * @param id
     * @return
     */
    Result removeById(String id);
    /**
     * 分页查询
     * @param query
     * @return
     */
    Result page(VideoQuery query);
    /**
     * 根据id查找
     * @param id
     * @return
     */
    Result detail(Integer id);
    /**
     * 列表
     * @return
     */
    Result all();
    /**
     * 发布视频
     *
     * @param form
     * @return
     */
    Result publish(VideoForm form);
}
framework/src/main/java/cn/lili/modules/lmk/service/VideoTagRefService.java
New file
@@ -0,0 +1,15 @@
package cn.lili.modules.lmk.service;
import cn.lili.modules.lmk.domain.entity.VideoTagRef;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * 视频标签中间表 服务类
 *
 * @author xp
 * @since 2025-05-19
 */
public interface VideoTagRefService extends IService<VideoTagRef> {
}
framework/src/main/java/cn/lili/modules/lmk/service/VideoTagService.java
@@ -1,6 +1,7 @@
package cn.lili.modules.lmk.service;
import cn.lili.modules.lmk.domain.entity.VideoTag;
import cn.lili.modules.lmk.domain.query.WxVideoTagQuery;
import com.baomidou.mybatisplus.extension.service.IService;
import cn.lili.base.Result;
import cn.lili.modules.lmk.domain.form.VideoTagForm;
@@ -62,4 +63,12 @@
     * @return
     */
    Result all();
    /**
     * 推荐标签
     *
     * @param query
     * @return
     */
    Result recommend(WxVideoTagQuery query);
}
framework/src/main/java/cn/lili/modules/lmk/service/impl/LmkFileServiceImpl.java
New file
@@ -0,0 +1,93 @@
package cn.lili.modules.lmk.service.impl;
import cn.lili.base.Result;
import cn.lili.common.exception.ServiceException;
import cn.lili.cos.CosSTS;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import cn.lili.modules.lmk.domain.form.FileInfoForm;
import cn.lili.modules.lmk.domain.vo.LmkFileVO;
import cn.lili.modules.lmk.mapper.LmkFileMapper;
import cn.lili.modules.lmk.service.LmkFileService;
import cn.lili.utils.COSUtil;
import cn.lili.utils.FileUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
 * 文件信息 服务实现类
 *
 * @author xp
 * @since 2025-05-19
 */
@Service
@RequiredArgsConstructor
public class LmkFileServiceImpl extends ServiceImpl<LmkFileMapper, LmkFile> implements LmkFileService {
    private final LmkFileMapper lmkFileMapper;
    private final COSUtil cosUtil;
    @Override
    public Result uploadObject(MultipartFile file) {
        LmkFile fileInfo = FileUtil.getFileInfo(file);
        try {
            cosUtil.upload(file.getInputStream(), fileInfo);
            lmkFileMapper.insert(fileInfo);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ServiceException("上传异常,请重试");
        }
        String url = cosUtil.getPreviewUrl(fileInfo.getFileKey());
        LmkFileVO fileVo = new LmkFileVO().setUrl(url).setFileKey(fileInfo.getFileKey());
        return Result.ok("上传成功").data(fileVo);
    }
    @Override
    public Result uploadObjects(List<MultipartFile> files) {
        return null;
    }
    @Override
    public Result deleteObject(String fileKey) {
        cosUtil.deleteFile(fileKey);
        return Result.ok("删除成功");
    }
    @Override
    public Result deleteObjects(List<String> fileKeys) {
        cosUtil.deleteFiles(fileKeys);
        return Result.ok("删除成功");
    }
    @Override
    public void downloadObject(String fileKey, HttpServletResponse response) {
        cosUtil.download(fileKey, response);
    }
    @Override
    public String getPreviewUrl(String fileKey) {
        return cosUtil.getPreviewUrl(fileKey);
    }
    @Override
    public CosSTS getSTSToken() {
        return cosUtil.getSTS();
    }
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addByForm(FileInfoForm fileInfo) {
        LmkFile file = new LmkFile();
        BeanUtils.copyProperties(fileInfo, file);
        baseMapper.insert(file);
    }
}
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoServiceImpl.java
New file
@@ -0,0 +1,171 @@
package cn.lili.modules.lmk.service.impl;
import cn.lili.common.security.context.UserContext;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import cn.lili.modules.lmk.domain.entity.VideoTag;
import cn.lili.modules.lmk.domain.entity.VideoTagRef;
import cn.lili.modules.lmk.enums.general.TagCreateTypeEnum;
import cn.lili.modules.lmk.enums.general.VideoStatusEnum;
import cn.lili.modules.lmk.service.LmkFileService;
import cn.lili.modules.lmk.service.VideoTagRefService;
import cn.lili.modules.lmk.service.VideoTagService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.lili.modules.lmk.domain.entity.Video;
import cn.lili.modules.lmk.mapper.VideoMapper;
import cn.lili.modules.lmk.service.VideoService;
import cn.lili.base.Result;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.vo.VideoVO;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import cn.lili.utils.PageUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * 视频内容 服务实现类
 *
 * @author xp
 * @since 2025-05-16
 */
@Service
@RequiredArgsConstructor
public class VideoServiceImpl extends ServiceImpl<VideoMapper, Video> implements VideoService {
    private final VideoMapper videoMapper;
    private final VideoTagService videoTagService;
    private final VideoTagRefService videoTagRefService;
    private final LmkFileService lmkFileService;
    /**
     * 添加
     * @param form
     * @return
     */
    @Override
    public Result add(VideoForm form) {
        Video entity = VideoForm.getEntityByForm(form, null);
        baseMapper.insert(entity);
        return Result.ok("添加成功");
    }
    /**
     * 修改
     * @param form
     * @return
     */
    @Override
    public Result update(VideoForm form) {
        Video entity = baseMapper.selectById(form.getId());
        // 为空抛IllegalArgumentException,做全局异常处理
        Assert.notNull(entity, "记录不存在");
        BeanUtils.copyProperties(form, entity);
        baseMapper.updateById(entity);
        return Result.ok("修改成功");
    }
    /**
     * 批量删除
     * @param ids
     * @return
     */
    @Override
    public Result remove(List<String> ids) {
        baseMapper.deleteBatchIds(ids);
        return Result.ok("删除成功");
    }
    /**
     * id删除
     * @param id
     * @return
     */
    @Override
    public Result removeById(String id) {
        baseMapper.deleteById(id);
        return Result.ok("删除成功");
    }
    /**
     * 分页查询
     * @param query
     * @return
     */
    @Override
    public Result page(VideoQuery query) {
        IPage<VideoVO> page = PageUtil.getPage(query, VideoVO.class);
        baseMapper.getPage(page, query);
        return Result.ok().data(page.getRecords()).total(page.getTotal());
    }
    /**
     * 根据id查找
     * @param id
     * @return
     */
    @Override
    public Result detail(Integer id) {
        VideoVO vo = baseMapper.getById(id);
        Assert.notNull(vo, "记录不存在");
        return Result.ok().data(vo);
    }
    /**
     * 列表
     * @return
     */
    @Override
    public Result all() {
        List<Video> entities = baseMapper.selectList(null);
        List<VideoVO> vos = entities.stream()
                .map(entity -> VideoVO.getVoByEntity(entity, null))
                .collect(Collectors.toList());
        return Result.ok().data(vos);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result publish(VideoForm form) {
        // 1.保存视频
        Video video = VideoForm.getEntityByForm(form, null);
        video.setAuthorId(UserContext.getCurrentUserId());
        video.setStatus(VideoStatusEnum.AUDITING.getValue());
        baseMapper.insert(video);
        // 2.处理标签
        List<VideoTagRef> videoTagRefs = form.getTags().stream().map(tag -> {
            VideoTagRef videoTagRef = new VideoTagRef();
            videoTagRef.setVideoId(video.getId());
            if (StringUtils.isBlank(tag.getId())) {
                VideoTag videoTag = new LambdaQueryChainWrapper<>(videoTagService.getBaseMapper())
                        .eq(VideoTag::getTagName, tag.getTagName())
                        .one();
                if (Objects.nonNull(videoTag)) {
                    videoTagRef.setVideoTagId(videoTag.getId());
                } else {
                    videoTag = new VideoTag();
                    videoTag.setTagName(tag.getTagName());
                    videoTag.setCreateType(TagCreateTypeEnum.USER.getValue());
                    videoTagService.save(videoTag);
                    videoTagRef.setVideoTagId(videoTag.getId());
                }
            } else {
                videoTagRef.setVideoTagId(tag.getId());
            }
            return videoTagRef;
        }).collect(Collectors.toList());
        videoTagRefService.saveBatch(videoTagRefs);
        // 3.保存视频文件信息
        lmkFileService.addByForm(form.getFileInfo());
        return Result.ok("发布成功,视频审核中~");
    }
}
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoTagRefServiceImpl.java
New file
@@ -0,0 +1,23 @@
package cn.lili.modules.lmk.service.impl;
import cn.lili.modules.lmk.domain.entity.VideoTagRef;
import cn.lili.modules.lmk.mapper.VideoTagRefMapper;
import cn.lili.modules.lmk.service.VideoTagRefService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
 * 视频标签中间表 服务实现类
 *
 * @author xp
 * @since 2025-05-19
 */
@Service
@RequiredArgsConstructor
public class VideoTagRefServiceImpl extends ServiceImpl<VideoTagRefMapper, VideoTagRef> implements VideoTagRefService {
    private final VideoTagRefMapper videoTagRefMapper;
}
framework/src/main/java/cn/lili/modules/lmk/service/impl/VideoTagServiceImpl.java
@@ -1,11 +1,13 @@
package cn.lili.modules.lmk.service.impl;
import cn.lili.modules.lmk.domain.query.WxVideoTagQuery;
import cn.lili.modules.lmk.enums.general.TagCreateTypeEnum;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.lili.modules.lmk.domain.entity.VideoTag;
import cn.lili.modules.lmk.mapper.VideoTagMapper;
import cn.lili.modules.lmk.service.VideoTagService;
import cn.lili.base.Result;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.lili.modules.lmk.domain.form.VideoTagForm;
import cn.lili.modules.lmk.domain.vo.VideoTagVO;
@@ -16,6 +18,7 @@
import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -121,4 +124,29 @@
                .collect(Collectors.toList());
        return Result.ok().data(vos);
    }
    @Override
    public Result recommend(WxVideoTagQuery query) {
        List<VideoTagVO> tags = new ArrayList<>(3);
        switch (query.getSearchType()) {
            case "HOT":
                // TODO 热门标签通过定时任务统计表lmk_video_tag_ref数量到lmk_video_tag中
                tags = new LambdaQueryChainWrapper<>(baseMapper)
                        .orderByDesc(VideoTag::getUseNum)
                        .last("limit 3")
                        .list().stream().map(entity -> {
                            return VideoTagVO.getVoByEntity(entity, null);
                        }).collect(Collectors.toList());
            case "SEARCH":
                tags = new LambdaQueryChainWrapper<>(baseMapper)
                        .orderByDesc(VideoTag::getUseNum)
                        .like(VideoTag::getTagName, query.getTagName())
                        .last("limit 3")
                        .list().stream().map(entity -> {
                            return VideoTagVO.getVoByEntity(entity, null);
                        }).collect(Collectors.toList());
        }
        return Result.ok().data(tags);
    }
}
framework/src/main/java/cn/lili/utils/COSUtil.java
New file
@@ -0,0 +1,285 @@
package cn.lili.utils;
import cn.lili.cos.COSConfigProperty;
import cn.lili.cos.CosSTS;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicSessionCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.exception.CosServiceException;
import com.qcloud.cos.http.HttpMethodName;
import com.qcloud.cos.model.*;
import com.qcloud.cos.region.Region;
import com.tencent.cloud.CosStsClient;
import com.tencent.cloud.Policy;
import com.tencent.cloud.Response;
import com.tencent.cloud.Statement;
import com.tencent.cloud.cos.util.Jackson;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
/**
 * @author:xp
 * @date:2025/5/16 16:31
 */
@Component
@RequiredArgsConstructor
public class COSUtil {
    private final COSConfigProperty cosConfigProperty;
    /**
     * 获取sts临时访问凭证
     *
     * @return
     */
    public CosSTS getSTS() {
        TreeMap<String, Object> config = new TreeMap<String, Object>();
        try {
            config.put("secretId", cosConfigProperty.getSecretId());
            config.put("secretKey", cosConfigProperty.getSecretKey());
            // 初始化 policy
            Policy policy = new Policy();
            // 设置域名:
            // 如果您使用了腾讯云 cvm,可以设置内部域名
            //config.put("host", "sts.internal.tencentcloudapi.com");
            // 临时密钥有效时长,单位是秒,默认 1800 秒,目前主账号最长 2 小时(即 7200 秒),子账号最长 36 小时(即 129600)秒
            config.put("durationSeconds", cosConfigProperty.getDurationSeconds());
            // 换成您的 bucket
            config.put("bucket", cosConfigProperty.getBucket());
            // 换成 bucket 所在地区
            config.put("region", cosConfigProperty.getRegion());
            // 开始构建一条 statement
            Statement statement = new Statement();
            // 声明设置的结果是允许操作
            statement.setEffect("allow");
            /**
             * 密钥的权限列表。必须在这里指定本次临时密钥所需要的权限。
             * 权限列表请参见 https://cloud.tencent.com/document/product/436/31923
             * 规则为 {project}:{interfaceName}
             * project : 产品缩写  cos相关授权为值为cos,数据万象(数据处理)相关授权值为ci
             * 授权所有接口用*表示,例如 cos:*,ci:*
             * 添加一批操作权限 :
             */
            statement.addActions(cosConfigProperty.getActions());
            /**
             * 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
             * 资源表达式规则分对象存储(cos)和数据万象(ci)两种
             * 数据处理、审核相关接口需要授予ci资源权限
             *  cos : qcs::cos:{region}:uid/{appid}:{bucket}/{path}
             *  ci  : qcs::ci:{region}:uid/{appid}:bucket/{bucket}/{path}
             * 列举几种典型的{path}授权场景:
             * 1、允许访问所有对象:"*"
             * 2、允许访问指定的对象:"a/a1.txt", "b/b1.txt"
             * 3、允许访问指定前缀的对象:"a*", "a/*", "b/*"
             *  如果填写了“*”,将允许用户访问所有资源;除非业务需要,否则请按照最小权限原则授予用户相应的访问权限范围。
             *
             * 示例:授权examplebucket-1250000000 bucket目录下的所有资源给cos和ci 授权两条Resource
             */
            statement.addResources(cosConfigProperty.getResources());
            // 把一条 statement 添加到 policy
            // 可以添加多条
            policy.addStatement(statement);
            // 将 Policy 示例转化成 String,可以使用任何 json 转化方式,这里是本 SDK 自带的推荐方式
            config.put("policy", Jackson.toJsonPrettyString(policy));
            Response response = CosStsClient.getCredential(config);
            System.out.println(response.credentials.tmpSecretId);
            System.out.println(response.credentials.tmpSecretKey);
            System.out.println(response.credentials.sessionToken);
            CosSTS cosSTS = new CosSTS();
            cosSTS.setTmpSecretId(response.credentials.tmpSecretId);
            cosSTS.setTmpSecretKey(response.credentials.tmpSecretKey);
            cosSTS.setSessionToken(response.credentials.sessionToken);
            Date now = new Date();
            cosSTS.setStsStartTime(now.getTime() / 1000);
            // 预留30s的请求时间,防止给小程序的结束时间超过实际的结束时间
            cosSTS.setStsEndTime(cosSTS.getStsStartTime() + cosConfigProperty.getDurationSeconds() - 30);
            cosSTS.setBucket(cosConfigProperty.getBucket());
            cosSTS.setRegion(cosConfigProperty.getRegion());
            return cosSTS;
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("get sts error");
        }
    }
    /**
     * 初始化cos客户端
     *
     * @return COSClient cos客户端
     */
    public COSClient initClient() {
        CosSTS sts = this.getSTS();
        BasicSessionCredentials cred = new BasicSessionCredentials(sts.getTmpSecretId(), sts.getTmpSecretKey(), sts.getSessionToken());
        // 2 设置 bucket 的地域
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分
        Region region = new Region(cosConfigProperty.getRegion()); //COS_REGION 参数:配置成存储桶 bucket 的实际地域,例如 ap-beijing,更多 COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        ClientConfig clientConfig = new ClientConfig(region);
        // 3 生成 cos 客户端
        COSClient cosClient = new COSClient(cred, clientConfig);
        return cosClient;
    }
    /**
     * 最简单的上传文件
     *
     * @param fileInput 文件
     * @param fileInfo 文件信息
     * @return fileKey 文件路径,或者叫唯一标识
     */
    public void upload(InputStream fileInput, LmkFile fileInfo) {
        COSClient cosClient = this.initClient();
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(fileInfo.getFileType());
        objectMetadata.setContentLength(fileInfo.getFileSize());
//        objectMetadata.setContentDisposition("attachment;filename=" + URLEncoder.encode(fileInfo.getOriginalFilename(), "UTF-8"));
        // 指定文件上传到 COS 上的路径,即对象键。例如对象键为 folder/picture.jpg,则表示将文件 picture.jpg 上传到 folder 路径下
        PutObjectRequest putObjectRequest = new PutObjectRequest(cosConfigProperty.getBucket(), fileInfo.getFileKey(), fileInput, objectMetadata);
        PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
        System.out.println(putObjectResult);
        cosClient.shutdown();
    }
    /**
     * 下载文件
     *
     * @param fileKey 文件路径,或者叫唯一标识
     * @param response
     */
    public void download(String fileKey, HttpServletResponse response) {
        COSClient cosClient = this.initClient();
        GetObjectRequest getObjectRequest = new GetObjectRequest(cosConfigProperty.getBucket(), fileKey);
        try {
            // 获取COS对象
            COSObject cosObject = cosClient.getObject(getObjectRequest);
            // 获取对象元数据
            ObjectMetadata metadata = cosObject.getObjectMetadata();
            String contentType = metadata.getContentType();
            long contentLength = metadata.getContentLength();
            String filename = fileKey.substring(fileKey.lastIndexOf('/') + 1);
            // 设置响应头
            response.setContentType(contentType != null ? contentType : "application/octet-stream");
            response.setContentLengthLong(contentLength);
            response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(filename, "UTF-8") + "\"");
            // 获取输入流和输出流
            try (InputStream cosObjectInput = cosObject.getObjectContent();
                 OutputStream responseOutputStream = response.getOutputStream()) {
                // 使用缓冲区传输数据
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = cosObjectInput.read(buffer)) != -1) {
                    responseOutputStream.write(buffer, 0, bytesRead);
                }
                responseOutputStream.flush();
            }
        } catch (CosServiceException e) {
            // COS服务异常
            e.printStackTrace();
            throw new RuntimeException("存储服务异常");
        } catch (CosClientException e) {
            // COS客户端异常
            e.printStackTrace();
            throw new RuntimeException("存储服务客户端异常");
        } catch (IOException e) {
            // IO异常
            e.printStackTrace();
            throw new RuntimeException("文件读取异常");
        } finally {
            // 确保COS客户端关闭
            if (cosClient != null) {
                cosClient.shutdown();
            }
        }
    }
    /**
     * 获取在线访问文件地址
     *
     * @param fileKey
     * @return
     */
    public String getPreviewUrl(String fileKey) {
        COSClient cosClient = this.initClient();
        // 设置签名过期时间(可选), 若未进行设置则默认使用 ClientConfig 中的签名过期时间(1小时)
        // 这里设置签名在半个小时后过期
        Date expirationDate = new Date(System.currentTimeMillis() + cosConfigProperty.getUrlExpireMinute() * 60 * 1000);
        // 填写本次请求的参数,需与实际请求相同,能够防止用户篡改此签名的 HTTP 请求的参数
        Map<String, String> params = new HashMap<String, String>();
        // 填写本次请求的头部,需与实际请求相同,能够防止用户篡改此签名的 HTTP 请求的头部
        Map<String, String> headers = new HashMap<String, String>();
        // 请求的 HTTP 方法,上传请求用 PUT,下载请求用 GET,删除请求用 DELETE
        HttpMethodName method = HttpMethodName.GET;
        URL url = cosClient.generatePresignedUrl(cosConfigProperty.getBucket(), fileKey, expirationDate, method, headers, params);
        System.out.println(url.toString());
        // 确认本进程不再使用 cosClient 实例之后,关闭即可
        cosClient.shutdown();
        return url.toString();
    }
    /**
     * 删除单个文件
     *
     * @param fileKey
     */
    public void deleteFile(String fileKey) {
        COSClient cosClient = this.initClient();
        try {
            cosClient.deleteObject(cosConfigProperty.getBucket(), fileKey);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("文件删除失败");
        } finally {
            cosClient.shutdown();
        }
    }
    /**
     * 删除多个文件
     *
     * @param fileKeys
     */
    public void deleteFiles(List<String> fileKeys) {
        if (CollectionUtils.isEmpty(fileKeys)) {
            return;
        }
        List<DeleteObjectsRequest.KeyVersion> keys = fileKeys.stream().map(key -> new DeleteObjectsRequest.KeyVersion(key)).collect(Collectors.toList());
        COSClient cosClient = this.initClient();
        try {
            DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(cosConfigProperty.getBucket());
            deleteObjectsRequest.setKeys(keys);
            cosClient.deleteObjects(deleteObjectsRequest);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("文件删除失败");
        } finally {
            cosClient.shutdown();
        }
    }
}
framework/src/main/java/cn/lili/utils/FileUtil.java
New file
@@ -0,0 +1,89 @@
package cn.lili.utils;
import cn.lili.common.exception.FileFormatNotSupport;
import cn.lili.modules.lmk.domain.entity.LmkFile;
import cn.lili.modules.lmk.enums.general.FileTypeEnum;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
 * @author 29443
 * @version 1.0
 * @date 2022/4/25
 */
public class FileUtil {
    /**
     * 获取文件后缀
     *
     * @param fileName
     * @return
     */
    public static String getSuffix(String fileName) {
        String suffix = fileName.substring(fileName.lastIndexOf('.') + 1);
        return suffix;
    }
    /**
     * 获取该文件上传到oss哪个目录
     *
     * @param suffix 文件后缀
     * @return
     */
    public static String getFileType(String suffix) {
        String fileType = "";
        for (FileTypeEnum type : FileTypeEnum.values()) {
            if (type.getSuffixs().contains(suffix)) {
                fileType = type.getType();
                return fileType;
            }
        }
        if (StringUtils.isBlank(fileType)) {
            throw new FileFormatNotSupport("文件格式" + suffix + "不支持", 500);
        }
        return fileType;
    }
    /**
     * 获取文件的信息,调用此方法默认普通上传
     *
     * @param file
     * @return
     */
    public static LmkFile getFileInfo(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        String random = generateFileKey();
        String suffix = getSuffix(originalFilename);
        String fileType = getFileType(suffix);
        String fileKey = String.format("%s/%s.%s", fileType, random, suffix);
        LmkFile fileInfo = new LmkFile();
        fileInfo.setFileKey(fileKey);
        fileInfo.setOriginalFileName(originalFilename);
        fileInfo.setFileType(file.getContentType());
        fileInfo.setFileSize(file.getSize());
        return fileInfo;
    }
    /**
     * 获取fileKey
     *
     * @return
     */
    public static String generateFileKey() {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        String no = format.format(new Date());
        Random random = new Random();
        for (int i = 0; i < 5; i++) {
            no += random.nextInt(10);
        }
        return no;
    }
}
framework/src/main/resources/mapper/lmk/LmkFileMapper.xml
New file
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lili.modules.lmk.mapper.LmkFileMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.lili.modules.lmk.domain.entity.LmkFile">
        <id column="id" property="id"/>
        <result column="file_key" property="fileKey" />
        <result column="file_type" property="fileType" />
        <result column="file_size" property="fileSize" />
        <result column="original_filename" property="originalFileName" />
    </resultMap>
    <select id="getByFileKey" resultMap="BaseResultMap">
        SELECT
            LF.file_key,
            LF.file_type,
            LF.file_size,
            LF.original_filename,
            LF.deleted_flag,
            LF.id
        FROM
            lmk_file LF
        WHERE
            LF.file_key = #{flieKey} AND LF.delete_flag = 0
    </select>
</mapper>
framework/src/main/resources/mapper/lmk/VideoMapper.xml
New file
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lili.modules.lmk.mapper.VideoMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.lili.modules.lmk.domain.vo.VideoVO">
        <id column="id" property="id"/>
        <result column="author_id" property="authorId" />
        <result column="cover_url" property="coverUrl" />
        <result column="video_file_key" property="videoFileKey" />
        <result column="video_fit" property="videoFit" />
        <result column="title" property="title" />
        <result column="goods_id" property="goodsId" />
        <result column="goods_view_num" property="goodsViewNum" />
        <result column="goods_order_num" property="goodsOrderNum" />
        <result column="recommend" property="recommend" />
        <result column="status" property="status" />
        <result column="play_num" property="playNum" />
        <result column="collect_num" property="collectNum" />
        <result column="comment_num" property="commentNum" />
        <result column="weight" property="weight" />
        <result column="audit_pass_time" property="auditPassTime" />
    </resultMap>
    <select id="getById" resultMap="BaseResultMap">
        SELECT
            LV.author_id,
            LV.cover_url,
            LV.video_fit,
            LV.video_file_key,
            LV.title,
            LV.goods_id,
            LV.goods_view_num,
            LV.goods_order_num,
            LV.recommend,
            LV.status,
            LV.play_num,
            LV.collect_num,
            LV.comment_num,
            LV.weight,
            LV.audit_pass_time,
            LV.id
        FROM
            lmk_video LV
        WHERE
            LV.id = #{id} AND LV.delete_flag = 0
    </select>
    <select id="getPage" resultMap="BaseResultMap">
        SELECT
            LV.author_id,
            LV.cover_url,
            LV.video_fit,
            LV.video_file_key,
            LV.title,
            LV.goods_id,
            LV.goods_view_num,
            LV.goods_order_num,
            LV.recommend,
            LV.status,
            LV.play_num,
            LV.collect_num,
            LV.comment_num,
            LV.weight,
            LV.audit_pass_time,
            LV.id
        FROM
            lmk_video LV
        WHERE
            LV.delete_flag = 0
    </select>
</mapper>
framework/src/main/resources/mapper/lmk/VideoTagMapper.xml
@@ -6,6 +6,7 @@
    <resultMap id="BaseResultMap" type="cn.lili.modules.lmk.domain.vo.VideoTagVO">
        <id column="id" property="id"/>
        <result column="tag_name" property="tagName" />
        <result column="use_num" property="useNum" />
        <result column="create_type" property="createType"/>
        <result column="update_time" property="updateTime" />
    </resultMap>
@@ -19,6 +20,7 @@
    <select id="getById" resultMap="BaseResultMap">
        SELECT
            LVT.tag_name,
            LVT.use_num,
            LVT.create_type,
            LVT.update_time,
            LVT.id
@@ -32,6 +34,7 @@
    <select id="getPage" resultMap="BaseResultMap">
        SELECT
            LVT.tag_name,
            LVT.use_num,
            LVT.create_type,
            LVT.update_time,
            LVT.id
framework/src/main/resources/mapper/lmk/VideoTagRefMapper.xml
New file
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lili.modules.lmk.mapper.VideoTagRefMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.lili.modules.lmk.domain.entity.VideoTagRef">
        <id column="id" property="id"/>
        <result column="video_id" property="videoId" />
        <result column="video_tag_id" property="videoTagId" />
    </resultMap>
</mapper>
manager-api/src/main/java/cn/lili/controller/lmk/VideoController.java
New file
@@ -0,0 +1,76 @@
package cn.lili.controller.lmk;
import cn.lili.group.Update;
import cn.lili.group.Add;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import cn.lili.modules.lmk.service.VideoService;
import cn.lili.base.Result;
import cn.lili.modules.lmk.domain.form.VideoForm;
import cn.lili.modules.lmk.domain.query.VideoQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
 * 视频内容 前端控制器
 *
 * @author xp
 * @since 2025-05-16
 */
@Validated
@RequiredArgsConstructor
@Api(value = "视频内容", tags = "视频内容管理")
@RestController
@RequestMapping("/manager/lmk/video")
public class VideoController {
    private final VideoService videoService;
    @PostMapping
    @ApiOperation(value = "添加", notes = "添加")
    public Result add(@RequestBody @Validated(Add.class) VideoForm form) {
        return videoService.add(form);
    }
    @PutMapping
    @ApiOperation(value = "修改", notes = "修改")
    public Result update(@RequestBody @Validated(Update.class) VideoForm form) {
        return videoService.update(form);
    }
    @DeleteMapping("/{id}")
    @ApiOperation(value = "ID删除", notes = "ID删除")
    public Result removeById(@PathVariable("id") String id) {
        return videoService.removeById(id);
    }
    @DeleteMapping("/batch")
    @ApiOperation(value = "批量删除", notes = "批量删除")
    public Result remove(@RequestBody @NotEmpty(message = "请选择数据") List<String> ids) {
        return videoService.remove(ids);
    }
    @GetMapping("/page")
    @ApiOperation(value = "分页", notes = "分页")
    public Result page(VideoQuery query) {
        return videoService.page(query);
    }
    @GetMapping("/{id}")
    @ApiOperation(value = "详情", notes = "详情")
    public Result detail(@PathVariable("id") Integer id) {
        return videoService.detail(id);
    }
    @GetMapping("/list")
    @ApiOperation(value = "列表", notes = "列表")
    public Result list() {
        return videoService.all();
    }
}
pom.xml
@@ -65,6 +65,7 @@
        <cos.version>5.6.97</cos.version>
        <tencentcloud.version>3.1.693</tencentcloud.version>
        <kuaidi100-api.version>1.0.11</kuaidi100-api.version>
        <tx-cos.version>5.6.227</tx-cos.version>
    </properties>
    <modules>
@@ -138,4 +139,4 @@
            </snapshots>
        </repository>
    </repositories>
</project>
</project>