Browse Source

卓美马甲功能

yuhongqi 4 days ago
parent
commit
a3119ab693
30 changed files with 1677 additions and 2 deletions
  1. 1 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  2. 64 0
      fs-admin/src/main/java/com/fs/life/controller/FsLifeVestController.java
  3. 92 0
      fs-admin/src/main/java/com/fs/life/controller/FsLifeVideoController.java
  4. 57 0
      fs-company/src/main/java/com/fs/company/controller/life/FsLifeVestController.java
  5. 83 0
      fs-company/src/main/java/com/fs/company/controller/life/FsLifeVideoController.java
  6. 10 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  7. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  8. 50 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  9. 28 0
      fs-service/src/main/java/com/fs/life/domain/FsLifeVest.java
  10. 41 0
      fs-service/src/main/java/com/fs/life/domain/FsLifeVideo.java
  11. 18 0
      fs-service/src/main/java/com/fs/life/domain/FsLifeVideoProduct.java
  12. 22 0
      fs-service/src/main/java/com/fs/life/domain/FsLifeVideoUserAction.java
  13. 19 0
      fs-service/src/main/java/com/fs/life/mapper/FsLifeVestMapper.java
  14. 32 0
      fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoMapper.java
  15. 17 0
      fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoProductMapper.java
  16. 17 0
      fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoUserActionMapper.java
  17. 13 0
      fs-service/src/main/java/com/fs/life/param/LifeFeedParam.java
  18. 18 0
      fs-service/src/main/java/com/fs/life/service/IFsLifeVestService.java
  19. 43 0
      fs-service/src/main/java/com/fs/life/service/IFsLifeVideoService.java
  20. 80 0
      fs-service/src/main/java/com/fs/life/service/impl/FsLifeVestServiceImpl.java
  21. 325 0
      fs-service/src/main/java/com/fs/life/service/impl/FsLifeVideoServiceImpl.java
  22. 14 0
      fs-service/src/main/java/com/fs/life/vo/LifeFeedVO.java
  23. 32 0
      fs-service/src/main/java/com/fs/life/vo/LifeVideoCardVO.java
  24. 18 0
      fs-service/src/main/java/com/fs/life/vo/LifeVideoProductVO.java
  25. 98 0
      fs-service/src/main/resources/db/changelog/changes/20260722-life-account-video.sql
  26. 88 0
      fs-service/src/main/resources/mapper/life/FsLifeVestMapper.xml
  27. 203 0
      fs-service/src/main/resources/mapper/life/FsLifeVideoMapper.xml
  28. 38 0
      fs-service/src/main/resources/mapper/life/FsLifeVideoProductMapper.xml
  29. 30 0
      fs-service/src/main/resources/mapper/life/FsLifeVideoUserActionMapper.xml
  30. 121 0
      fs-user-app/src/main/java/com/fs/app/controller/life/LifeController.java

+ 1 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -855,7 +855,7 @@ public class FsStoreOrderScrmController extends BaseController {
     @PreAuthorize("@ss.hasPermi('store:storeOrder:editDeliveryId')")
     @PutMapping("/editDeliveryId")
     public AjaxResult editDeliveryId(@RequestBody FsStoreOrderScrm fsStoreOrder) {
-        return toAjax(fsStoreOrderService.updateFsStoreOrder(fsStoreOrder));
+        return toAjax(fsStoreOrderService.editDeliveryId(fsStoreOrder));
     }
 
     @Log(title = "同步管易物流单号", businessType = BusinessType.UPDATE)

+ 64 - 0
fs-admin/src/main/java/com/fs/life/controller/FsLifeVestController.java

@@ -0,0 +1,64 @@
+package com.fs.life.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.life.domain.FsLifeVest;
+import com.fs.life.service.IFsLifeVestService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 生活号马甲管理
+ */
+@Api(tags = "生活号马甲管理")
+@RestController
+@RequestMapping("/life/vest")
+public class FsLifeVestController extends BaseController {
+
+    @Autowired
+    private IFsLifeVestService fsLifeVestService;
+
+    @ApiOperation("马甲号列表")
+    @GetMapping("/list")
+    public TableDataInfo list(FsLifeVest query) {
+        startPage();
+        List<FsLifeVest> list = fsLifeVestService.selectFsLifeVestList(query);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("马甲号详情")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(fsLifeVestService.selectFsLifeVestById(id));
+    }
+
+    @ApiOperation("新增马甲号")
+    @Log(title = "生活号马甲", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsLifeVest vest) {
+        vest.setCreateBy(getUsername());
+        return toAjax(fsLifeVestService.insertFsLifeVest(vest));
+    }
+
+    @ApiOperation("修改马甲号")
+    @Log(title = "生活号马甲", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsLifeVest vest) {
+        vest.setUpdateBy(getUsername());
+        return toAjax(fsLifeVestService.updateFsLifeVest(vest));
+    }
+
+    @ApiOperation("删除马甲号")
+    @Log(title = "生活号马甲", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        return toAjax(fsLifeVestService.deleteFsLifeVestById(id));
+    }
+}

+ 92 - 0
fs-admin/src/main/java/com/fs/life/controller/FsLifeVideoController.java

@@ -0,0 +1,92 @@
+package com.fs.life.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.life.domain.FsLifeVideo;
+import com.fs.life.service.IFsLifeVideoService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 生活号视频内容管理
+ */
+@Api(tags = "生活号视频管理")
+@RestController
+@RequestMapping("/life/video")
+public class FsLifeVideoController extends BaseController {
+
+    @Autowired
+    private IFsLifeVideoService fsLifeVideoService;
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductScrmService;
+
+    @ApiOperation("视频列表(可按马甲号)")
+    @GetMapping("/list")
+    public TableDataInfo list(FsLifeVideo query) {
+        startPage();
+        List<FsLifeVideo> list = fsLifeVideoService.selectFsLifeVideoList(query);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("视频详情")
+    @GetMapping("/{videoId}")
+    public AjaxResult getInfo(@PathVariable Long videoId) {
+        return AjaxResult.success(fsLifeVideoService.selectFsLifeVideoById(videoId));
+    }
+
+    @ApiOperation("新增视频")
+    @Log(title = "生活号视频", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsLifeVideo video) {
+        video.setCreateBy(getUsername());
+        return toAjax(fsLifeVideoService.insertFsLifeVideo(video));
+    }
+
+    @ApiOperation("修改视频")
+    @Log(title = "生活号视频", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsLifeVideo video) {
+        video.setUpdateBy(getUsername());
+        return toAjax(fsLifeVideoService.updateFsLifeVideo(video));
+    }
+
+    @ApiOperation("删除视频")
+    @Log(title = "生活号视频", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{videoIds}")
+    public AjaxResult remove(@PathVariable Long[] videoIds) {
+        return toAjax(fsLifeVideoService.deleteFsLifeVideoByIds(videoIds));
+    }
+
+    @ApiOperation("上下架")
+    @Log(title = "生活号视频上下架", businessType = BusinessType.UPDATE)
+    @PutMapping("/shelf")
+    public AjaxResult shelf(@RequestBody FsLifeVideo video) {
+        if (video.getVideoId() == null || video.getIsShow() == null) {
+            return AjaxResult.error("videoId 与 isShow 不能为空");
+        }
+        FsLifeVideo update = new FsLifeVideo();
+        update.setVideoId(video.getVideoId());
+        update.setIsShow(video.getIsShow());
+        update.setUpdateBy(getUsername());
+        return toAjax(fsLifeVideoService.updateFsLifeVideo(update));
+    }
+
+    @ApiOperation("上架中商品搜索(关联商品弹窗)")
+    @GetMapping("/product/search")
+    public TableDataInfo productSearch(FsStoreProductScrm query) {
+        query.setIsShow(1);
+        query.setIsDel(0);
+        startPage();
+        List<FsStoreProductScrm> list = fsStoreProductScrmService.selectFsStoreProductList(query);
+        return getDataTable(list);
+    }
+}

+ 57 - 0
fs-company/src/main/java/com/fs/company/controller/life/FsLifeVestController.java

@@ -0,0 +1,57 @@
+package com.fs.company.controller.life;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.life.domain.FsLifeVest;
+import com.fs.life.service.IFsLifeVestService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "生活号马甲管理")
+@RestController
+@RequestMapping("/life/vest")
+public class FsLifeVestController extends BaseController {
+
+    @Autowired
+    private IFsLifeVestService fsLifeVestService;
+
+    @ApiOperation("马甲号列表")
+    @GetMapping("/list")
+    public TableDataInfo list(FsLifeVest query) {
+        startPage();
+        List<FsLifeVest> list = fsLifeVestService.selectFsLifeVestList(query);
+        return getDataTable(list);
+    }
+
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(fsLifeVestService.selectFsLifeVestById(id));
+    }
+
+    @Log(title = "生活号马甲", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsLifeVest vest) {
+        vest.setCreateBy(getUsername());
+        return toAjax(fsLifeVestService.insertFsLifeVest(vest));
+    }
+
+    @Log(title = "生活号马甲", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsLifeVest vest) {
+        vest.setUpdateBy(getUsername());
+        return toAjax(fsLifeVestService.updateFsLifeVest(vest));
+    }
+
+    @Log(title = "生活号马甲", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        return toAjax(fsLifeVestService.deleteFsLifeVestById(id));
+    }
+}

+ 83 - 0
fs-company/src/main/java/com/fs/company/controller/life/FsLifeVideoController.java

@@ -0,0 +1,83 @@
+package com.fs.company.controller.life;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.life.domain.FsLifeVideo;
+import com.fs.life.service.IFsLifeVideoService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "生活号视频管理")
+@RestController
+@RequestMapping("/life/video")
+public class FsLifeVideoController extends BaseController {
+
+    @Autowired
+    private IFsLifeVideoService fsLifeVideoService;
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductScrmService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(FsLifeVideo query) {
+        startPage();
+        List<FsLifeVideo> list = fsLifeVideoService.selectFsLifeVideoList(query);
+        return getDataTable(list);
+    }
+
+    @GetMapping("/{videoId}")
+    public AjaxResult getInfo(@PathVariable Long videoId) {
+        return AjaxResult.success(fsLifeVideoService.selectFsLifeVideoById(videoId));
+    }
+
+    @Log(title = "生活号视频", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsLifeVideo video) {
+        video.setCreateBy(getUsername());
+        return toAjax(fsLifeVideoService.insertFsLifeVideo(video));
+    }
+
+    @Log(title = "生活号视频", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsLifeVideo video) {
+        video.setUpdateBy(getUsername());
+        return toAjax(fsLifeVideoService.updateFsLifeVideo(video));
+    }
+
+    @Log(title = "生活号视频", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{videoIds}")
+    public AjaxResult remove(@PathVariable Long[] videoIds) {
+        return toAjax(fsLifeVideoService.deleteFsLifeVideoByIds(videoIds));
+    }
+
+    @Log(title = "生活号视频上下架", businessType = BusinessType.UPDATE)
+    @PutMapping("/shelf")
+    public AjaxResult shelf(@RequestBody FsLifeVideo video) {
+        if (video.getVideoId() == null || video.getIsShow() == null) {
+            return AjaxResult.error("videoId 与 isShow 不能为空");
+        }
+        FsLifeVideo update = new FsLifeVideo();
+        update.setVideoId(video.getVideoId());
+        update.setIsShow(video.getIsShow());
+        update.setUpdateBy(getUsername());
+        return toAjax(fsLifeVideoService.updateFsLifeVideo(update));
+    }
+
+    @ApiOperation("上架中商品搜索")
+    @GetMapping("/product/search")
+    public TableDataInfo productSearch(FsStoreProductScrm query) {
+        query.setIsShow(1);
+        query.setIsDel(0);
+        startPage();
+        List<FsStoreProductScrm> list = fsStoreProductScrmService.selectFsStoreProductList(query);
+        return getDataTable(list);
+    }
+}

+ 10 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -483,6 +483,16 @@ public class FsStoreOrderScrmController extends BaseController
         return toAjax(fsStoreOrderService.updateFsStoreOrder(fsStoreOrder));
     }
 
+    /**
+     * 修改物流单号/发货人手机号
+     */
+    @Log(title = "修改物流", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:editDeliveryId')")
+    @PutMapping("/editDeliveryId")
+    public AjaxResult editDeliveryId(@RequestBody FsStoreOrderScrm fsStoreOrder) {
+        return toAjax(fsStoreOrderService.editDeliveryId(fsStoreOrder));
+    }
+
     /**
      * 删除订单
      */

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -80,6 +80,11 @@ public interface IFsStoreOrderScrmService
      */
     public int updateFsStoreOrder(FsStoreOrderScrm fsStoreOrder);
 
+    /**
+     * 修改物流单号/发货人手机号,并尝试重新订阅物流
+     */
+    int editDeliveryId(FsStoreOrderScrm fsStoreOrder);
+
     /**
      * 待支付订单仅改运费:更新 pay_postage,并按差额同步 pay_price、total_postage;货到付款( pay_type=3 )时同步 pay_delivery
      */

+ 50 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -2410,10 +2410,59 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (order == null || StringUtils.isBlank(deliverySn) || StringUtils.isBlank(deliveryId)) {
             return;
         }
-        String senderPhones = resolveSenderPhonesByOrder(order);
+        // 优先使用订单已保存/手工填写的发货人手机号,否则回退商品配置
+        String senderPhones = StringUtils.isNotBlank(order.getSenderPhone())
+                ? order.getSenderPhone()
+                : resolveSenderPhonesByOrder(order);
         expressService.subscribeExpressSmart(order.getOrderCode(), deliverySn, deliveryId, lastFourNumber, senderPhones);
     }
 
+    /**
+     * 修改物流单号/发货人手机号后重新订阅物流
+     */
+    @Override
+    public int editDeliveryId(FsStoreOrderScrm fsStoreOrder) {
+        if (fsStoreOrder == null || fsStoreOrder.getId() == null) {
+            return 0;
+        }
+        int rows = updateFsStoreOrder(fsStoreOrder);
+        if (rows > 0) {
+            tryResubscribeExpress(fsStoreOrder.getId());
+        }
+        return rows;
+    }
+
+    private void tryResubscribeExpress(Long orderId) {
+        FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
+        if (order == null || StringUtils.isBlank(order.getDeliveryId())) {
+            return;
+        }
+        String deliverySn = order.getDeliverySn();
+        if (StringUtils.isBlank(deliverySn)) {
+            StringBuilder nameBuf = new StringBuilder();
+            FsExpressScrm express = tryResolveExpressByLogisticCode(order.getDeliveryId(), order.getOrderCode(), nameBuf);
+            if (express == null || StringUtils.isBlank(express.getCode())) {
+                return;
+            }
+            deliverySn = express.getCode();
+            order.setDeliverySn(deliverySn);
+            order.setDeliveryName(StringUtils.isNotBlank(nameBuf.toString()) ? nameBuf.toString() : express.getName());
+            fsStoreOrderMapper.updateFsStoreOrder(order);
+        }
+        String lastFourNumber = "";
+        if (ShipperCodeEnum.SF.getValue().equals(deliverySn) || ShipperCodeEnum.ZTO.getValue().equals(deliverySn)) {
+            lastFourNumber = order.getUserPhone();
+            if (StringUtils.isNotBlank(lastFourNumber) && lastFourNumber.length() == 11) {
+                lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+            }
+        }
+        try {
+            subscribeOrderExpress(order, deliverySn, order.getDeliveryId(), lastFourNumber);
+        } catch (Exception e) {
+            log.warn("修改物流后重新订阅失败: orderId={}, error={}", orderId, e.getMessage());
+        }
+    }
+
     @Override
     public void updateDeliveryOrder(Long id, String deliveryId, String deliverCode, String deliverName) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(id);

+ 28 - 0
fs-service/src/main/java/com/fs/life/domain/FsLifeVest.java

@@ -0,0 +1,28 @@
+package com.fs.life.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 生活号马甲账号 fs_life_vest
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsLifeVest extends BaseEntity {
+
+    private Long id;
+    /** 马甲号昵称 */
+    private String nickName;
+    /** 头像 */
+    private String avatar;
+    /** 简介 */
+    private String intro;
+    /** 1启用 0禁用 */
+    private Integer status;
+    /** 0未删除 1已删除 */
+    private Integer isDel;
+
+    /** 查询关键字:昵称或ID */
+    private String keyword;
+}

+ 41 - 0
fs-service/src/main/java/com/fs/life/domain/FsLifeVideo.java

@@ -0,0 +1,41 @@
+package com.fs.life.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 生活号短视频 fs_life_video
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsLifeVideo extends BaseEntity {
+
+    private Long videoId;
+    private Long vestId;
+    private String title;
+    private String fileName;
+    private String coverUrl;
+    private String videoUrl;
+    private Integer duration;
+    /** 是否上架 1是 0否 */
+    private Integer isShow;
+    /** 是否今日主推 1是 0否 */
+    private Integer isHot;
+    private Long views;
+    private Long likes;
+    private Long favoriteNum;
+    private Long shareNum;
+    private Integer isDel;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date shelfTime;
+
+    /** 关联商品ID列表(非表字段) */
+    private List<Long> productIds;
+    /** 查询:标题关键字 */
+    private String keyword;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/life/domain/FsLifeVideoProduct.java

@@ -0,0 +1,18 @@
+package com.fs.life.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 生活号视频商品关联 fs_life_video_product
+ */
+@Data
+public class FsLifeVideoProduct {
+
+    private Long id;
+    private Long videoId;
+    private Long productId;
+    private Integer sort;
+    private Date createTime;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/life/domain/FsLifeVideoUserAction.java

@@ -0,0 +1,22 @@
+package com.fs.life.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 生活号视频用户点赞/收藏 fs_life_video_user_action
+ */
+@Data
+public class FsLifeVideoUserAction {
+
+    public static final int ACTION_LIKE = 1;
+    public static final int ACTION_FAVORITE = 2;
+
+    private Long id;
+    private Long userId;
+    private Long videoId;
+    /** 1点赞 2收藏 */
+    private Integer actionType;
+    private Date createTime;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/life/mapper/FsLifeVestMapper.java

@@ -0,0 +1,19 @@
+package com.fs.life.mapper;
+
+import com.fs.life.domain.FsLifeVest;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface FsLifeVestMapper {
+
+    FsLifeVest selectFsLifeVestById(Long id);
+
+    List<FsLifeVest> selectFsLifeVestList(FsLifeVest query);
+
+    int insertFsLifeVest(FsLifeVest vest);
+
+    int updateFsLifeVest(FsLifeVest vest);
+
+    int countByNickName(@Param("nickName") String nickName, @Param("excludeId") Long excludeId);
+}

+ 32 - 0
fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoMapper.java

@@ -0,0 +1,32 @@
+package com.fs.life.mapper;
+
+import com.fs.life.domain.FsLifeVideo;
+import com.fs.life.vo.LifeVideoCardVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface FsLifeVideoMapper {
+
+    FsLifeVideo selectFsLifeVideoById(Long videoId);
+
+    List<FsLifeVideo> selectFsLifeVideoList(FsLifeVideo query);
+
+    List<LifeVideoCardVO> selectFeedVideoList(@Param("tab") String tab,
+                                              @Param("keyword") String keyword,
+                                              @Param("userId") Long userId);
+
+    LifeVideoCardVO selectFeedVideoDetail(@Param("videoId") Long videoId, @Param("userId") Long userId);
+
+    int insertFsLifeVideo(FsLifeVideo video);
+
+    int updateFsLifeVideo(FsLifeVideo video);
+
+    int incrViews(@Param("videoId") Long videoId);
+
+    int incrShareNum(@Param("videoId") Long videoId);
+
+    int incrLikes(@Param("videoId") Long videoId, @Param("delta") int delta);
+
+    int incrFavoriteNum(@Param("videoId") Long videoId, @Param("delta") int delta);
+}

+ 17 - 0
fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoProductMapper.java

@@ -0,0 +1,17 @@
+package com.fs.life.mapper;
+
+import com.fs.life.domain.FsLifeVideoProduct;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface FsLifeVideoProductMapper {
+
+    List<FsLifeVideoProduct> selectByVideoId(Long videoId);
+
+    List<Long> selectProductIdsByVideoId(Long videoId);
+
+    int deleteByVideoId(Long videoId);
+
+    int batchInsert(@Param("list") List<FsLifeVideoProduct> list);
+}

+ 17 - 0
fs-service/src/main/java/com/fs/life/mapper/FsLifeVideoUserActionMapper.java

@@ -0,0 +1,17 @@
+package com.fs.life.mapper;
+
+import com.fs.life.domain.FsLifeVideoUserAction;
+import org.apache.ibatis.annotations.Param;
+
+public interface FsLifeVideoUserActionMapper {
+
+    FsLifeVideoUserAction selectOne(@Param("userId") Long userId,
+                                    @Param("videoId") Long videoId,
+                                    @Param("actionType") Integer actionType);
+
+    int insert(FsLifeVideoUserAction action);
+
+    int delete(@Param("userId") Long userId,
+               @Param("videoId") Long videoId,
+               @Param("actionType") Integer actionType);
+}

+ 13 - 0
fs-service/src/main/java/com/fs/life/param/LifeFeedParam.java

@@ -0,0 +1,13 @@
+package com.fs.life.param;
+
+import lombok.Data;
+
+@Data
+public class LifeFeedParam {
+    /** all | hot */
+    private String tab = "all";
+    private String keyword;
+    private Integer pageNum = 1;
+    private Integer pageSize = 10;
+    private Long userId;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/life/service/IFsLifeVestService.java

@@ -0,0 +1,18 @@
+package com.fs.life.service;
+
+import com.fs.life.domain.FsLifeVest;
+
+import java.util.List;
+
+public interface IFsLifeVestService {
+
+    FsLifeVest selectFsLifeVestById(Long id);
+
+    List<FsLifeVest> selectFsLifeVestList(FsLifeVest query);
+
+    int insertFsLifeVest(FsLifeVest vest);
+
+    int updateFsLifeVest(FsLifeVest vest);
+
+    int deleteFsLifeVestById(Long id);
+}

+ 43 - 0
fs-service/src/main/java/com/fs/life/service/IFsLifeVideoService.java

@@ -0,0 +1,43 @@
+package com.fs.life.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.life.domain.FsLifeVideo;
+import com.fs.life.param.LifeFeedParam;
+import com.fs.life.vo.LifeFeedVO;
+import com.fs.life.vo.LifeVideoCardVO;
+import com.fs.life.vo.LifeVideoProductVO;
+
+import java.util.List;
+
+public interface IFsLifeVideoService {
+
+    FsLifeVideo selectFsLifeVideoById(Long videoId);
+
+    List<FsLifeVideo> selectFsLifeVideoList(FsLifeVideo query);
+
+    int insertFsLifeVideo(FsLifeVideo video);
+
+    int updateFsLifeVideo(FsLifeVideo video);
+
+    int deleteFsLifeVideoById(Long videoId);
+
+    int deleteFsLifeVideoByIds(Long[] videoIds);
+
+    LifeFeedVO feed(LifeFeedParam param);
+
+    List<LifeVideoCardVO> selectFeedVideos(LifeFeedParam param);
+
+    List<com.fs.live.domain.Live> selectPublicLivingList();
+
+    LifeVideoCardVO detail(Long videoId, Long userId);
+
+    List<LifeVideoProductVO> listProducts(Long videoId);
+
+    R toggleLike(Long userId, Long videoId);
+
+    R toggleFavorite(Long userId, Long videoId);
+
+    R share(Long videoId);
+
+    R view(Long videoId);
+}

+ 80 - 0
fs-service/src/main/java/com/fs/life/service/impl/FsLifeVestServiceImpl.java

@@ -0,0 +1,80 @@
+package com.fs.life.service.impl;
+
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.life.domain.FsLifeVest;
+import com.fs.life.mapper.FsLifeVestMapper;
+import com.fs.life.service.IFsLifeVestService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class FsLifeVestServiceImpl implements IFsLifeVestService {
+
+    @Autowired
+    private FsLifeVestMapper fsLifeVestMapper;
+
+    @Override
+    public FsLifeVest selectFsLifeVestById(Long id) {
+        return fsLifeVestMapper.selectFsLifeVestById(id);
+    }
+
+    @Override
+    public List<FsLifeVest> selectFsLifeVestList(FsLifeVest query) {
+        return fsLifeVestMapper.selectFsLifeVestList(query);
+    }
+
+    @Override
+    public int insertFsLifeVest(FsLifeVest vest) {
+        validateVest(vest, null);
+        if (vest.getStatus() == null) {
+            vest.setStatus(1);
+        }
+        vest.setIsDel(0);
+        vest.setCreateTime(DateUtils.getNowDate());
+        return fsLifeVestMapper.insertFsLifeVest(vest);
+    }
+
+    @Override
+    public int updateFsLifeVest(FsLifeVest vest) {
+        if (vest.getId() == null) {
+            throw new CustomException("马甲号ID不能为空");
+        }
+        FsLifeVest exist = fsLifeVestMapper.selectFsLifeVestById(vest.getId());
+        if (exist == null) {
+            throw new CustomException("马甲号不存在");
+        }
+        validateVest(vest, vest.getId());
+        vest.setUpdateTime(DateUtils.getNowDate());
+        return fsLifeVestMapper.updateFsLifeVest(vest);
+    }
+
+    @Override
+    public int deleteFsLifeVestById(Long id) {
+        FsLifeVest vest = new FsLifeVest();
+        vest.setId(id);
+        vest.setIsDel(1);
+        vest.setUpdateTime(DateUtils.getNowDate());
+        return fsLifeVestMapper.updateFsLifeVest(vest);
+    }
+
+    private void validateVest(FsLifeVest vest, Long excludeId) {
+        if (StringUtils.isEmpty(vest.getNickName())) {
+            throw new CustomException("马甲号昵称不能为空");
+        }
+        String nick = vest.getNickName().trim();
+        if (nick.length() < 2 || nick.length() > 10) {
+            throw new CustomException("马甲号昵称长度需为2-10字");
+        }
+        vest.setNickName(nick);
+        if (StringUtils.isEmpty(vest.getAvatar())) {
+            throw new CustomException("马甲号头像不能为空");
+        }
+        if (fsLifeVestMapper.countByNickName(nick, excludeId) > 0) {
+            throw new CustomException("马甲号昵称已存在");
+        }
+    }
+}

+ 325 - 0
fs-service/src/main/java/com/fs/life/service/impl/FsLifeVideoServiceImpl.java

@@ -0,0 +1,325 @@
+package com.fs.life.service.impl;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
+import com.fs.life.domain.FsLifeVest;
+import com.fs.life.domain.FsLifeVideo;
+import com.fs.life.domain.FsLifeVideoProduct;
+import com.fs.life.domain.FsLifeVideoUserAction;
+import com.fs.life.mapper.FsLifeVestMapper;
+import com.fs.life.mapper.FsLifeVideoMapper;
+import com.fs.life.mapper.FsLifeVideoProductMapper;
+import com.fs.life.mapper.FsLifeVideoUserActionMapper;
+import com.fs.life.param.LifeFeedParam;
+import com.fs.life.service.IFsLifeVideoService;
+import com.fs.life.vo.LifeFeedVO;
+import com.fs.life.vo.LifeVideoCardVO;
+import com.fs.life.vo.LifeVideoProductVO;
+import com.fs.live.domain.Live;
+import com.fs.live.mapper.LiveMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FsLifeVideoServiceImpl implements IFsLifeVideoService {
+
+    @Autowired
+    private FsLifeVideoMapper fsLifeVideoMapper;
+    @Autowired
+    private FsLifeVideoProductMapper fsLifeVideoProductMapper;
+    @Autowired
+    private FsLifeVideoUserActionMapper fsLifeVideoUserActionMapper;
+    @Autowired
+    private FsLifeVestMapper fsLifeVestMapper;
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+    @Autowired
+    private LiveMapper liveMapper;
+
+    @Override
+    public FsLifeVideo selectFsLifeVideoById(Long videoId) {
+        FsLifeVideo video = fsLifeVideoMapper.selectFsLifeVideoById(videoId);
+        if (video != null) {
+            video.setProductIds(fsLifeVideoProductMapper.selectProductIdsByVideoId(videoId));
+        }
+        return video;
+    }
+
+    @Override
+    public List<FsLifeVideo> selectFsLifeVideoList(FsLifeVideo query) {
+        return fsLifeVideoMapper.selectFsLifeVideoList(query);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertFsLifeVideo(FsLifeVideo video) {
+        validateVideo(video, true);
+        FsLifeVest vest = fsLifeVestMapper.selectFsLifeVestById(video.getVestId());
+        if (vest == null) {
+            throw new CustomException("马甲号不存在");
+        }
+        if (video.getIsShow() == null) {
+            video.setIsShow(1);
+        }
+        if (video.getIsHot() == null) {
+            video.setIsHot(0);
+        }
+        video.setViews(0L);
+        video.setLikes(0L);
+        video.setFavoriteNum(0L);
+        video.setShareNum(0L);
+        video.setIsDel(0);
+        video.setCreateTime(DateUtils.getNowDate());
+        if (Integer.valueOf(1).equals(video.getIsShow())) {
+            video.setShelfTime(DateUtils.getNowDate());
+        }
+        int rows = fsLifeVideoMapper.insertFsLifeVideo(video);
+        saveProducts(video.getVideoId(), video.getProductIds(), video.getIsShow());
+        return rows;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateFsLifeVideo(FsLifeVideo video) {
+        if (video.getVideoId() == null) {
+            throw new CustomException("视频ID不能为空");
+        }
+        FsLifeVideo exist = fsLifeVideoMapper.selectFsLifeVideoById(video.getVideoId());
+        if (exist == null) {
+            throw new CustomException("视频不存在");
+        }
+        validateVideo(video, false);
+        if (video.getVestId() != null) {
+            FsLifeVest vest = fsLifeVestMapper.selectFsLifeVestById(video.getVestId());
+            if (vest == null) {
+                throw new CustomException("马甲号不存在");
+            }
+        }
+        video.setUpdateTime(DateUtils.getNowDate());
+        if (video.getIsShow() != null && Integer.valueOf(1).equals(video.getIsShow())
+                && !Integer.valueOf(1).equals(exist.getIsShow())) {
+            video.setShelfTime(DateUtils.getNowDate());
+        }
+        int rows = fsLifeVideoMapper.updateFsLifeVideo(video);
+        if (video.getProductIds() != null) {
+            Integer bindShow = video.getIsShow() != null ? video.getIsShow() : exist.getIsShow();
+            // 是否关联商品:传空列表表示不关联
+            boolean needProduct = !video.getProductIds().isEmpty();
+            if (needProduct) {
+                saveProducts(video.getVideoId(), video.getProductIds(), bindShow);
+            } else {
+                fsLifeVideoProductMapper.deleteByVideoId(video.getVideoId());
+            }
+        }
+        return rows;
+    }
+
+    @Override
+    public int deleteFsLifeVideoById(Long videoId) {
+        FsLifeVideo video = new FsLifeVideo();
+        video.setVideoId(videoId);
+        video.setIsDel(1);
+        video.setUpdateTime(DateUtils.getNowDate());
+        return fsLifeVideoMapper.updateFsLifeVideo(video);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteFsLifeVideoByIds(Long[] videoIds) {
+        int total = 0;
+        if (videoIds == null) {
+            return 0;
+        }
+        for (Long id : videoIds) {
+            total += deleteFsLifeVideoById(id);
+        }
+        return total;
+    }
+
+    @Override
+    public LifeFeedVO feed(LifeFeedParam param) {
+        LifeFeedVO vo = new LifeFeedVO();
+        String tab = StringUtils.isEmpty(param.getTab()) ? "all" : param.getTab();
+        if ("all".equalsIgnoreCase(tab)) {
+            vo.setLives(selectPublicLivingList());
+        } else {
+            vo.setLives(new ArrayList<>());
+        }
+        vo.setVideos(selectFeedVideos(param));
+        return vo;
+    }
+
+    @Override
+    public List<LifeVideoCardVO> selectFeedVideos(LifeFeedParam param) {
+        String tab = StringUtils.isEmpty(param.getTab()) ? "all" : param.getTab().toLowerCase();
+        return fsLifeVideoMapper.selectFeedVideoList(tab, param.getKeyword(), param.getUserId());
+    }
+
+    @Override
+    public List<Live> selectPublicLivingList() {
+        return liveMapper.selectPublicLivingList();
+    }
+
+    @Override
+    public LifeVideoCardVO detail(Long videoId, Long userId) {
+        LifeVideoCardVO card = fsLifeVideoMapper.selectFeedVideoDetail(videoId, userId);
+        if (card == null || (card.getVideoId() == null)) {
+            throw new CustomException("视频不存在或已下架");
+        }
+        card.setProducts(listProducts(videoId));
+        return card;
+    }
+
+    @Override
+    public List<LifeVideoProductVO> listProducts(Long videoId) {
+        List<Long> productIds = fsLifeVideoProductMapper.selectProductIdsByVideoId(videoId);
+        List<LifeVideoProductVO> list = new ArrayList<>();
+        if (productIds == null || productIds.isEmpty()) {
+            return list;
+        }
+        for (Long productId : productIds) {
+            FsStoreProductScrm product = fsStoreProductScrmMapper.selectFsStoreProductById(productId);
+            if (product == null || Integer.valueOf(1).equals(product.getIsDel())) {
+                continue;
+            }
+            LifeVideoProductVO item = new LifeVideoProductVO();
+            item.setProductId(product.getProductId());
+            item.setProductName(product.getProductName());
+            item.setImage(product.getImage());
+            item.setBarCode(product.getBarCode());
+            item.setPrice(product.getPrice());
+            int stock = product.getStock() == null ? 0 : product.getStock().intValue();
+            item.setStock(stock);
+            item.setIsShow(product.getIsShow());
+            boolean soldOut = stock <= 0 || !Integer.valueOf(1).equals(product.getIsShow());
+            item.setSoldOut(soldOut);
+            list.add(item);
+        }
+        return list;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R toggleLike(Long userId, Long videoId) {
+        return toggleAction(userId, videoId, FsLifeVideoUserAction.ACTION_LIKE, true);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R toggleFavorite(Long userId, Long videoId) {
+        return toggleAction(userId, videoId, FsLifeVideoUserAction.ACTION_FAVORITE, false);
+    }
+
+    @Override
+    public R share(Long videoId) {
+        ensureVideoOnShelf(videoId);
+        fsLifeVideoMapper.incrShareNum(videoId);
+        return R.ok("分享成功");
+    }
+
+    @Override
+    public R view(Long videoId) {
+        ensureVideoOnShelf(videoId);
+        fsLifeVideoMapper.incrViews(videoId);
+        return R.ok("ok");
+    }
+
+    private R toggleAction(Long userId, Long videoId, int actionType, boolean like) {
+        if (userId == null) {
+            return R.error("请先登录");
+        }
+        ensureVideoOnShelf(videoId);
+        FsLifeVideoUserAction exist = fsLifeVideoUserActionMapper.selectOne(userId, videoId, actionType);
+        if (exist == null) {
+            FsLifeVideoUserAction action = new FsLifeVideoUserAction();
+            action.setUserId(userId);
+            action.setVideoId(videoId);
+            action.setActionType(actionType);
+            action.setCreateTime(new Date());
+            fsLifeVideoUserActionMapper.insert(action);
+            if (like) {
+                fsLifeVideoMapper.incrLikes(videoId, 1);
+            } else {
+                fsLifeVideoMapper.incrFavoriteNum(videoId, 1);
+            }
+            return R.ok(like ? "点赞成功" : "收藏成功").put("active", true);
+        }
+        fsLifeVideoUserActionMapper.delete(userId, videoId, actionType);
+        if (like) {
+            fsLifeVideoMapper.incrLikes(videoId, -1);
+        } else {
+            fsLifeVideoMapper.incrFavoriteNum(videoId, -1);
+        }
+        return R.ok(like ? "已取消点赞" : "已取消收藏").put("active", false);
+    }
+
+    private void ensureVideoOnShelf(Long videoId) {
+        FsLifeVideo video = fsLifeVideoMapper.selectFsLifeVideoById(videoId);
+        if (video == null || !Integer.valueOf(1).equals(video.getIsShow())) {
+            throw new CustomException("视频不存在或已下架");
+        }
+    }
+
+    private void validateVideo(FsLifeVideo video, boolean create) {
+        if (create && video.getVestId() == null) {
+            throw new CustomException("马甲号不能为空");
+        }
+        if (StringUtils.isEmpty(video.getTitle())) {
+            throw new CustomException("视频标题不能为空");
+        }
+        if (video.getTitle().trim().length() > 20) {
+            throw new CustomException("视频标题不能超过20字");
+        }
+        video.setTitle(video.getTitle().trim());
+        if (create || video.getCoverUrl() != null) {
+            if (StringUtils.isEmpty(video.getCoverUrl())) {
+                throw new CustomException("视频封面不能为空");
+            }
+        }
+        if (create || video.getVideoUrl() != null) {
+            if (StringUtils.isEmpty(video.getVideoUrl())) {
+                throw new CustomException("视频文件不能为空");
+            }
+        }
+        // 关联商品:若传入非空列表则校验商品
+        if (video.getProductIds() != null && !video.getProductIds().isEmpty()) {
+            for (Long productId : video.getProductIds()) {
+                FsStoreProductScrm product = fsStoreProductScrmMapper.selectFsStoreProductById(productId);
+                if (product == null || Integer.valueOf(1).equals(product.getIsDel())) {
+                    throw new CustomException("商品不存在:" + productId);
+                }
+                if (!Integer.valueOf(1).equals(product.getIsShow())) {
+                    throw new CustomException("仅可关联上架中的商品:" + product.getProductName());
+                }
+            }
+        }
+    }
+
+    private void saveProducts(Long videoId, List<Long> productIds, Integer isShow) {
+        fsLifeVideoProductMapper.deleteByVideoId(videoId);
+        if (productIds == null || productIds.isEmpty()) {
+            return;
+        }
+        List<FsLifeVideoProduct> list = new ArrayList<>();
+        int sort = 0;
+        Date now = DateUtils.getNowDate();
+        for (Long productId : productIds) {
+            FsLifeVideoProduct item = new FsLifeVideoProduct();
+            item.setVideoId(videoId);
+            item.setProductId(productId);
+            item.setSort(sort++);
+            item.setCreateTime(now);
+            list.add(item);
+        }
+        fsLifeVideoProductMapper.batchInsert(list);
+    }
+}

+ 14 - 0
fs-service/src/main/java/com/fs/life/vo/LifeFeedVO.java

@@ -0,0 +1,14 @@
+package com.fs.life.vo;
+
+import com.fs.live.domain.Live;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class LifeFeedVO {
+    /** 公域直播中列表(仅 tab=all 时填充) */
+    private List<Live> lives;
+    /** 短视频列表 */
+    private List<LifeVideoCardVO> videos;
+}

+ 32 - 0
fs-service/src/main/java/com/fs/life/vo/LifeVideoCardVO.java

@@ -0,0 +1,32 @@
+package com.fs.life.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class LifeVideoCardVO {
+    private Long videoId;
+    private Long vestId;
+    private String title;
+    private String coverUrl;
+    private String videoUrl;
+    private Integer duration;
+    private Long views;
+    private Long likes;
+    private Long favoriteNum;
+    private Long shareNum;
+    private Integer isHot;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date shelfTime;
+    private String vestNickName;
+    private String vestAvatar;
+    private Boolean liked;
+    private Boolean favorited;
+    private Integer productCount;
+    private BigDecimal minPrice;
+    private List<LifeVideoProductVO> products;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/life/vo/LifeVideoProductVO.java

@@ -0,0 +1,18 @@
+package com.fs.life.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class LifeVideoProductVO {
+    private Long productId;
+    private String productName;
+    private String image;
+    private String barCode;
+    private BigDecimal price;
+    private Integer stock;
+    /** 商品是否售罄(总库存为0或未上架) */
+    private Boolean soldOut;
+    private Integer isShow;
+}

+ 98 - 0
fs-service/src/main/resources/db/changelog/changes/20260722-life-account-video.sql

@@ -0,0 +1,98 @@
+--liquibase formatted sql
+
+--changeset yhq:20260722-fs-life-vest
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_life_vest'
+CREATE TABLE `fs_life_vest` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '马甲号ID',
+    `nick_name` varchar(32) NOT NULL COMMENT '马甲号昵称',
+    `avatar` varchar(512) NOT NULL COMMENT '头像URL',
+    `intro` varchar(512) DEFAULT NULL COMMENT '简介',
+    `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0禁用',
+    `is_del` tinyint NOT NULL DEFAULT 0 COMMENT '0未删除 1已删除',
+    `create_by` varchar(64) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+    `remark` varchar(500) DEFAULT NULL,
+    PRIMARY KEY (`id`),
+    KEY `idx_nick_name` (`nick_name`),
+    KEY `idx_is_del_status` (`is_del`,`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生活号马甲账号';
+--rollback DROP TABLE fs_life_vest;
+
+--changeset yhq:20260722-fs-life-video
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_life_video'
+CREATE TABLE `fs_life_video` (
+    `video_id` bigint NOT NULL AUTO_INCREMENT COMMENT '视频ID',
+    `vest_id` bigint NOT NULL COMMENT '马甲号ID',
+    `title` varchar(64) NOT NULL COMMENT '视频标题',
+    `file_name` varchar(256) DEFAULT NULL COMMENT '视频文件名称',
+    `cover_url` varchar(512) NOT NULL COMMENT '封面图',
+    `video_url` varchar(1024) NOT NULL COMMENT '视频地址',
+    `duration` int DEFAULT 0 COMMENT '时长秒',
+    `is_show` tinyint NOT NULL DEFAULT 1 COMMENT '是否上架 1是 0否',
+    `is_hot` tinyint NOT NULL DEFAULT 0 COMMENT '是否今日主推 1是 0否',
+    `views` bigint NOT NULL DEFAULT 0 COMMENT '播放量',
+    `likes` bigint NOT NULL DEFAULT 0 COMMENT '点赞数',
+    `favorite_num` bigint NOT NULL DEFAULT 0 COMMENT '收藏数',
+    `share_num` bigint NOT NULL DEFAULT 0 COMMENT '分享数',
+    `is_del` tinyint NOT NULL DEFAULT 0 COMMENT '0未删除 1已删除',
+    `shelf_time` datetime DEFAULT NULL COMMENT '上架时间',
+    `create_by` varchar(64) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+    `remark` varchar(500) DEFAULT NULL,
+    PRIMARY KEY (`video_id`),
+    KEY `idx_vest` (`vest_id`),
+    KEY `idx_show_hot` (`is_del`,`is_show`,`is_hot`,`shelf_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生活号短视频';
+--rollback DROP TABLE fs_life_video;
+
+--changeset yhq:20260722-fs-life-video-product
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_life_video_product'
+CREATE TABLE `fs_life_video_product` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `video_id` bigint NOT NULL COMMENT '视频ID',
+    `product_id` bigint NOT NULL COMMENT '商城商品ID fs_store_product_scrm',
+    `sort` int NOT NULL DEFAULT 0 COMMENT '排序,越小越前',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_video_product` (`video_id`,`product_id`),
+    KEY `idx_product` (`product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生活号视频商品关联';
+--rollback DROP TABLE fs_life_video_product;
+
+--changeset yhq:20260722-fs-life-video-user-action
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_life_video_user_action'
+CREATE TABLE `fs_life_video_user_action` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `user_id` bigint NOT NULL COMMENT '用户ID',
+    `video_id` bigint NOT NULL COMMENT '视频ID',
+    `action_type` tinyint NOT NULL COMMENT '1点赞 2收藏',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_user_video_action` (`user_id`,`video_id`,`action_type`),
+    KEY `idx_video_action` (`video_id`,`action_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生活号视频用户点赞收藏';
+--rollback DROP TABLE fs_life_video_user_action;
+
+--changeset yhq:20260722-live-add-vest-id
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'live'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'live' AND column_name = 'vest_id'
+ALTER TABLE `live`
+    ADD COLUMN `vest_id` bigint DEFAULT NULL COMMENT '生活号马甲号ID' AFTER `live_group_type`;
+--rollback ALTER TABLE live DROP COLUMN vest_id;
+
+--changeset yhq:20260722-live-add-visible-scope
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'live'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'live' AND column_name = 'visible_scope'
+ALTER TABLE `live`
+    ADD COLUMN `visible_scope` tinyint NOT NULL DEFAULT 0 COMMENT '可见范围 0私域 1公域' AFTER `vest_id`;
+--rollback ALTER TABLE live DROP COLUMN visible_scope;

+ 88 - 0
fs-service/src/main/resources/mapper/life/FsLifeVestMapper.xml

@@ -0,0 +1,88 @@
+<?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="com.fs.life.mapper.FsLifeVestMapper">
+
+    <resultMap id="FsLifeVestResult" type="com.fs.life.domain.FsLifeVest">
+        <result property="id" column="id"/>
+        <result property="nickName" column="nick_name"/>
+        <result property="avatar" column="avatar"/>
+        <result property="intro" column="intro"/>
+        <result property="status" column="status"/>
+        <result property="isDel" column="is_del"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <sql id="selectVo">
+        select id, nick_name, avatar, intro, status, is_del, create_by, create_time, update_by, update_time, remark
+        from fs_life_vest
+    </sql>
+
+    <select id="selectFsLifeVestById" resultMap="FsLifeVestResult">
+        <include refid="selectVo"/>
+        where id = #{id} and is_del = 0
+    </select>
+
+    <select id="selectFsLifeVestList" resultMap="FsLifeVestResult">
+        <include refid="selectVo"/>
+        where is_del = 0
+        <if test="status != null">and status = #{status}</if>
+        <if test="keyword != null and keyword != ''">
+            and (nick_name like concat('%', #{keyword}, '%')
+            or cast(id as char) like concat('%', #{keyword}, '%'))
+        </if>
+        <if test="nickName != null and nickName != ''">
+            and nick_name like concat('%', #{nickName}, '%')
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="countByNickName" resultType="int">
+        select count(1) from fs_life_vest
+        where is_del = 0 and nick_name = #{nickName}
+        <if test="excludeId != null">and id != #{excludeId}</if>
+    </select>
+
+    <insert id="insertFsLifeVest" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_life_vest
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="nickName != null">nick_name,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="intro != null">intro,</if>
+            <if test="status != null">status,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="nickName != null">#{nickName},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="intro != null">#{intro},</if>
+            <if test="status != null">#{status},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsLifeVest">
+        update fs_life_vest
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="intro != null">intro = #{intro},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+</mapper>

+ 203 - 0
fs-service/src/main/resources/mapper/life/FsLifeVideoMapper.xml

@@ -0,0 +1,203 @@
+<?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="com.fs.life.mapper.FsLifeVideoMapper">
+
+    <resultMap id="FsLifeVideoResult" type="com.fs.life.domain.FsLifeVideo">
+        <result property="videoId" column="video_id"/>
+        <result property="vestId" column="vest_id"/>
+        <result property="title" column="title"/>
+        <result property="fileName" column="file_name"/>
+        <result property="coverUrl" column="cover_url"/>
+        <result property="videoUrl" column="video_url"/>
+        <result property="duration" column="duration"/>
+        <result property="isShow" column="is_show"/>
+        <result property="isHot" column="is_hot"/>
+        <result property="views" column="views"/>
+        <result property="likes" column="likes"/>
+        <result property="favoriteNum" column="favorite_num"/>
+        <result property="shareNum" column="share_num"/>
+        <result property="isDel" column="is_del"/>
+        <result property="shelfTime" column="shelf_time"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <resultMap id="LifeVideoCardResult" type="com.fs.life.vo.LifeVideoCardVO">
+        <result property="videoId" column="video_id"/>
+        <result property="vestId" column="vest_id"/>
+        <result property="title" column="title"/>
+        <result property="coverUrl" column="cover_url"/>
+        <result property="videoUrl" column="video_url"/>
+        <result property="duration" column="duration"/>
+        <result property="views" column="views"/>
+        <result property="likes" column="likes"/>
+        <result property="favoriteNum" column="favorite_num"/>
+        <result property="shareNum" column="share_num"/>
+        <result property="isHot" column="is_hot"/>
+        <result property="shelfTime" column="shelf_time"/>
+        <result property="vestNickName" column="vest_nick_name"/>
+        <result property="vestAvatar" column="vest_avatar"/>
+        <result property="liked" column="liked"/>
+        <result property="favorited" column="favorited"/>
+        <result property="productCount" column="product_count"/>
+        <result property="minPrice" column="min_price"/>
+    </resultMap>
+
+    <sql id="selectVo">
+        select video_id, vest_id, title, file_name, cover_url, video_url, duration, is_show, is_hot,
+               views, likes, favorite_num, share_num, is_del, shelf_time,
+               create_by, create_time, update_by, update_time, remark
+        from fs_life_video
+    </sql>
+
+    <select id="selectFsLifeVideoById" resultMap="FsLifeVideoResult">
+        <include refid="selectVo"/>
+        where video_id = #{videoId} and is_del = 0
+    </select>
+
+    <select id="selectFsLifeVideoList" resultMap="FsLifeVideoResult">
+        <include refid="selectVo"/>
+        where is_del = 0
+        <if test="vestId != null">and vest_id = #{vestId}</if>
+        <if test="isShow != null">and is_show = #{isShow}</if>
+        <if test="isHot != null">and is_hot = #{isHot}</if>
+        <if test="keyword != null and keyword != ''">
+            and (title like concat('%', #{keyword}, '%')
+            or cast(video_id as char) like concat('%', #{keyword}, '%')
+            or file_name like concat('%', #{keyword}, '%'))
+        </if>
+        <if test="title != null and title != ''">
+            and title like concat('%', #{title}, '%')
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="selectFeedVideoList" resultMap="LifeVideoCardResult">
+        select v.video_id, v.vest_id, v.title, v.cover_url, v.video_url, v.duration,
+               v.views, v.likes, v.favorite_num, v.share_num, v.is_hot, v.shelf_time,
+               t.nick_name as vest_nick_name, t.avatar as vest_avatar,
+               case when like_a.id is null then 0 else 1 end as liked,
+               case when fav_a.id is null then 0 else 1 end as favorited,
+               (select count(1) from fs_life_video_product p where p.video_id = v.video_id) as product_count,
+               (select min(sp.price) from fs_life_video_product p
+                   left join fs_store_product_scrm sp on sp.product_id = p.product_id and sp.is_del = 0
+                where p.video_id = v.video_id) as min_price
+        from fs_life_video v
+        left join fs_life_vest t on t.id = v.vest_id and t.is_del = 0
+        left join fs_life_video_user_action like_a
+            on like_a.video_id = v.video_id and like_a.action_type = 1
+            and like_a.user_id = #{userId}
+        left join fs_life_video_user_action fav_a
+            on fav_a.video_id = v.video_id and fav_a.action_type = 2
+            and fav_a.user_id = #{userId}
+        where v.is_del = 0 and v.is_show = 1
+        <if test="tab != null and tab == 'hot'">and v.is_hot = 1</if>
+        <if test="keyword != null and keyword != ''">
+            and v.title like concat('%', #{keyword}, '%')
+        </if>
+        order by v.shelf_time desc, v.video_id desc
+    </select>
+
+    <select id="selectFeedVideoDetail" resultMap="LifeVideoCardResult">
+        select v.video_id, v.vest_id, v.title, v.cover_url, v.video_url, v.duration,
+               v.views, v.likes, v.favorite_num, v.share_num, v.is_hot, v.shelf_time,
+               t.nick_name as vest_nick_name, t.avatar as vest_avatar,
+               case when like_a.id is null then 0 else 1 end as liked,
+               case when fav_a.id is null then 0 else 1 end as favorited,
+               (select count(1) from fs_life_video_product p where p.video_id = v.video_id) as product_count,
+               (select min(sp.price) from fs_life_video_product p
+                   left join fs_store_product_scrm sp on sp.product_id = p.product_id and sp.is_del = 0
+                where p.video_id = v.video_id) as min_price
+        from fs_life_video v
+        left join fs_life_vest t on t.id = v.vest_id and t.is_del = 0
+        left join fs_life_video_user_action like_a
+            on like_a.video_id = v.video_id and like_a.action_type = 1
+            and like_a.user_id = #{userId}
+        left join fs_life_video_user_action fav_a
+            on fav_a.video_id = v.video_id and fav_a.action_type = 2
+            and fav_a.user_id = #{userId}
+        where v.video_id = #{videoId} and v.is_del = 0
+    </select>
+
+    <insert id="insertFsLifeVideo" useGeneratedKeys="true" keyProperty="videoId">
+        insert into fs_life_video
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="vestId != null">vest_id,</if>
+            <if test="title != null">title,</if>
+            <if test="fileName != null">file_name,</if>
+            <if test="coverUrl != null">cover_url,</if>
+            <if test="videoUrl != null">video_url,</if>
+            <if test="duration != null">duration,</if>
+            <if test="isShow != null">is_show,</if>
+            <if test="isHot != null">is_hot,</if>
+            <if test="views != null">views,</if>
+            <if test="likes != null">likes,</if>
+            <if test="favoriteNum != null">favorite_num,</if>
+            <if test="shareNum != null">share_num,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="shelfTime != null">shelf_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="vestId != null">#{vestId},</if>
+            <if test="title != null">#{title},</if>
+            <if test="fileName != null">#{fileName},</if>
+            <if test="coverUrl != null">#{coverUrl},</if>
+            <if test="videoUrl != null">#{videoUrl},</if>
+            <if test="duration != null">#{duration},</if>
+            <if test="isShow != null">#{isShow},</if>
+            <if test="isHot != null">#{isHot},</if>
+            <if test="views != null">#{views},</if>
+            <if test="likes != null">#{likes},</if>
+            <if test="favoriteNum != null">#{favoriteNum},</if>
+            <if test="shareNum != null">#{shareNum},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="shelfTime != null">#{shelfTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsLifeVideo">
+        update fs_life_video
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="vestId != null">vest_id = #{vestId},</if>
+            <if test="title != null">title = #{title},</if>
+            <if test="fileName != null">file_name = #{fileName},</if>
+            <if test="coverUrl != null">cover_url = #{coverUrl},</if>
+            <if test="videoUrl != null">video_url = #{videoUrl},</if>
+            <if test="duration != null">duration = #{duration},</if>
+            <if test="isShow != null">is_show = #{isShow},</if>
+            <if test="isHot != null">is_hot = #{isHot},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="shelfTime != null">shelf_time = #{shelfTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where video_id = #{videoId}
+    </update>
+
+    <update id="incrViews">
+        update fs_life_video set views = ifnull(views,0) + 1 where video_id = #{videoId} and is_del = 0
+    </update>
+
+    <update id="incrShareNum">
+        update fs_life_video set share_num = ifnull(share_num,0) + 1 where video_id = #{videoId} and is_del = 0
+    </update>
+
+    <update id="incrLikes">
+        update fs_life_video set likes = greatest(ifnull(likes,0) + #{delta}, 0) where video_id = #{videoId} and is_del = 0
+    </update>
+
+    <update id="incrFavoriteNum">
+        update fs_life_video set favorite_num = greatest(ifnull(favorite_num,0) + #{delta}, 0) where video_id = #{videoId} and is_del = 0
+    </update>
+</mapper>

+ 38 - 0
fs-service/src/main/resources/mapper/life/FsLifeVideoProductMapper.xml

@@ -0,0 +1,38 @@
+<?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="com.fs.life.mapper.FsLifeVideoProductMapper">
+
+    <resultMap id="Result" type="com.fs.life.domain.FsLifeVideoProduct">
+        <result property="id" column="id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="productId" column="product_id"/>
+        <result property="sort" column="sort"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <select id="selectByVideoId" resultMap="Result">
+        select id, video_id, product_id, sort, create_time
+        from fs_life_video_product
+        where video_id = #{videoId}
+        order by sort asc, id asc
+    </select>
+
+    <select id="selectProductIdsByVideoId" resultType="long">
+        select product_id from fs_life_video_product
+        where video_id = #{videoId}
+        order by sort asc, id asc
+    </select>
+
+    <delete id="deleteByVideoId">
+        delete from fs_life_video_product where video_id = #{videoId}
+    </delete>
+
+    <insert id="batchInsert">
+        insert into fs_life_video_product (video_id, product_id, sort, create_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.videoId}, #{item.productId}, #{item.sort}, #{item.createTime})
+        </foreach>
+    </insert>
+</mapper>

+ 30 - 0
fs-service/src/main/resources/mapper/life/FsLifeVideoUserActionMapper.xml

@@ -0,0 +1,30 @@
+<?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="com.fs.life.mapper.FsLifeVideoUserActionMapper">
+
+    <resultMap id="Result" type="com.fs.life.domain.FsLifeVideoUserAction">
+        <result property="id" column="id"/>
+        <result property="userId" column="user_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="actionType" column="action_type"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <select id="selectOne" resultMap="Result">
+        select id, user_id, video_id, action_type, create_time
+        from fs_life_video_user_action
+        where user_id = #{userId} and video_id = #{videoId} and action_type = #{actionType}
+        limit 1
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_life_video_user_action (user_id, video_id, action_type, create_time)
+        values (#{userId}, #{videoId}, #{actionType}, #{createTime})
+    </insert>
+
+    <delete id="delete">
+        delete from fs_life_video_user_action
+        where user_id = #{userId} and video_id = #{videoId} and action_type = #{actionType}
+    </delete>
+</mapper>

+ 121 - 0
fs-user-app/src/main/java/com/fs/app/controller/life/LifeController.java

@@ -0,0 +1,121 @@
+package com.fs.app.controller.life;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.life.param.LifeFeedParam;
+import com.fs.life.service.IFsLifeVideoService;
+import com.fs.life.vo.LifeVideoCardVO;
+import com.fs.live.domain.Live;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 生活号 C 端接口
+ */
+@Api(tags = "生活号")
+@RestController
+@RequestMapping("/app/life")
+public class LifeController extends AppBaseController {
+
+    @Autowired
+    private IFsLifeVideoService fsLifeVideoService;
+
+    @ApiOperation("生活号首页 Feed(公域直播置顶 + 短视频)")
+    @GetMapping("/feed")
+    public R feed(LifeFeedParam param) {
+        if (param.getPageNum() == null || param.getPageNum() < 1) {
+            param.setPageNum(1);
+        }
+        if (param.getPageSize() == null || param.getPageSize() < 1) {
+            param.setPageSize(10);
+        }
+        if (StringUtils.isNotEmpty(getUserId())) {
+            param.setUserId(Long.parseLong(getUserId()));
+        }
+
+        String tab = StringUtils.isEmpty(param.getTab()) ? "all" : param.getTab();
+        List<Live> lives = new ArrayList<>();
+        if ("all".equalsIgnoreCase(tab)) {
+            lives = fsLifeVideoService.selectPublicLivingList();
+        }
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<LifeVideoCardVO> videos = fsLifeVideoService.selectFeedVideos(param);
+        PageInfo<LifeVideoCardVO> pageInfo = new PageInfo<>(videos);
+
+        Map<String, Object> data = new HashMap<>(4);
+        data.put("lives", lives);
+        data.put("videos", pageInfo);
+        return R.ok().put("data", data);
+    }
+
+    @ApiOperation("视频详情")
+    @GetMapping("/video/{id}")
+    public R detail(@PathVariable("id") Long id) {
+        Long userId = null;
+        if (StringUtils.isNotEmpty(getUserId())) {
+            userId = Long.parseLong(getUserId());
+        }
+        LifeVideoCardVO detail = fsLifeVideoService.detail(id, userId);
+        return R.ok().put("data", detail);
+    }
+
+    @Login
+    @ApiOperation("点赞/取消点赞")
+    @PostMapping("/video/like")
+    public R like(@RequestBody Map<String, Long> body) {
+        Long videoId = body.get("videoId");
+        if (videoId == null) {
+            return R.error("videoId不能为空");
+        }
+        return fsLifeVideoService.toggleLike(Long.parseLong(getUserId()), videoId);
+    }
+
+    @Login
+    @ApiOperation("收藏/取消收藏")
+    @PostMapping("/video/favorite")
+    public R favorite(@RequestBody Map<String, Long> body) {
+        Long videoId = body.get("videoId");
+        if (videoId == null) {
+            return R.error("videoId不能为空");
+        }
+        return fsLifeVideoService.toggleFavorite(Long.parseLong(getUserId()), videoId);
+    }
+
+    @ApiOperation("分享计数+1")
+    @PostMapping("/video/share")
+    public R share(@RequestBody Map<String, Long> body) {
+        Long videoId = body.get("videoId");
+        if (videoId == null) {
+            return R.error("videoId不能为空");
+        }
+        return fsLifeVideoService.share(videoId);
+    }
+
+    @ApiOperation("播放计数+1")
+    @PostMapping("/video/view")
+    public R view(@RequestBody Map<String, Long> body) {
+        Long videoId = body.get("videoId");
+        if (videoId == null) {
+            return R.error("videoId不能为空");
+        }
+        return fsLifeVideoService.view(videoId);
+    }
+
+    @ApiOperation("视频关联商品列表")
+    @GetMapping("/video/{id}/products")
+    public R products(@PathVariable("id") Long id) {
+        return R.ok().put("data", fsLifeVideoService.listProducts(id));
+    }
+}