Przeglądaj źródła

小程序加购,收藏,评分功能优化

lk 4 dni temu
rodzic
commit
d805a10683
45 zmienionych plików z 2610 dodań i 8 usunięć
  1. 259 0
      java/fs-doctor-app/src/main/java/com/fs/app/controller/DoctorDashboardController.java
  2. 70 0
      java/fs-service/src/main/java/com/fs/his/mapper/DoctorDashboardMapper.java
  3. 10 0
      java/fs-service/src/main/java/com/fs/hisStore/constants/UserAppsLockConstant.java
  4. 3 0
      java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCartScrm.java
  5. 30 0
      java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreFavoriteScrm.java
  6. 15 0
      java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreScrm.java
  7. 2 0
      java/fs-service/src/main/java/com/fs/hisStore/dto/FsStoreOrderPriceDTO.java
  8. 45 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java
  9. 74 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreFavoriteScrmMapper.java
  10. 22 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  11. 3 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  12. 2 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartDelParam.java
  13. 2 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartNumParam.java
  14. 2 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartParam.java
  15. 60 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiStoreOrderCreateParam.java
  16. 17 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCartScrmService.java
  17. 35 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreFavoriteScrmService.java
  18. 15 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  19. 5 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  20. 84 0
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  21. 62 0
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreFavoriteScrmServiceImpl.java
  22. 254 0
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  23. 5 0
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  24. 26 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartGroupVO.java
  25. 22 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartStoreVO.java
  26. 46 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreFavoriteVO.java
  27. 44 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreOrderResultVO.java
  28. 3 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java
  29. 26 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/StoreScrmTopVo.java
  30. 141 0
      java/fs-service/src/main/resources/mapper/his/DoctorDashboardMapper.xml
  31. 6 1
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreCartScrmMapper.xml
  32. 69 0
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreFavoriteScrmMapper.xml
  33. 54 0
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  34. 10 2
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreScrmMapper.xml
  35. 91 1
      java/fs-user-app/src/main/java/com/fs/app/controller/StoreAfterSalesController.java
  36. 15 0
      java/fs-user-app/src/main/java/com/fs/app/controller/StoreOrderController.java
  37. 9 0
      java/fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  38. 161 3
      java/fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java
  39. 12 0
      java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreAfterSalesScrmController.java
  40. 87 0
      java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreFavoriteScrmController.java
  41. 9 0
      java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java
  42. 124 1
      java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreScrmController.java
  43. 248 0
      saasdoctorui/src/views/doctor/dashboard/index.vue
  44. 131 0
      saasdoctorui/src/views/doctor/myLive/index.vue
  45. 200 0
      saasdoctorui/src/views/doctor/performance/index.vue

+ 259 - 0
java/fs-doctor-app/src/main/java/com/fs/app/controller/DoctorDashboardController.java

@@ -0,0 +1,259 @@
+package com.fs.app.controller;
+
+import com.fs.app.annotation.Login;
+import com.fs.common.core.domain.R;
+import com.fs.his.mapper.DoctorDashboardMapper;
+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.math.BigDecimal;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 医生工作台Dashboard Controller
+ * 今日概览、问诊统计、业绩看板、我的直播
+ * 
+ * Redis缓存策略:
+ *   - 今日概览(overview): TTL=30s   — 实时性强,短TTL保证数据新鲜
+ *   - 趋势数据(trend):    TTL=300s  — 历史聚合数据,变化频率低
+ *   - 业绩汇总(performance): TTL=60s — 累计数据,中等实时性
+ *   - 直播列表(liveList):  TTL=30s   — 直播状态变化快,需要短TTL
+ */
+@Api("医生工作台Dashboard")
+@RestController
+@RequestMapping("/app/doctor/dashboard")
+public class DoctorDashboardController extends AppBaseController {
+
+    @Autowired
+    private DoctorDashboardMapper doctorDashboardMapper;
+
+    // ======================== Cache Key 常量 ========================
+    private static final String CK_OVERVIEW      = "doctor:dashboard:overview:";
+    private static final String CK_CONSULT_TREND = "doctor:dashboard:consultTrend:";
+    private static final String CK_PRESCRIBE_TREND = "doctor:dashboard:prescribeTrend:";
+    private static final String CK_REVENUE_TREND = "doctor:dashboard:revenueTrend:";
+    private static final String CK_PERFORMANCE   = "doctor:dashboard:performance:";
+    private static final String CK_LIVE_LIST     = "doctor:dashboard:liveList:";
+
+    // ======================== TTL 常量(秒) ========================
+    private static final int TTL_OVERVIEW    = 30;   // 今日数据,30秒
+    private static final int TTL_TREND       = 300;  // 历史趋势,5分钟
+    private static final int TTL_PERFORMANCE = 60;   // 累计业绩,1分钟
+    private static final int TTL_LIVE_LIST   = 30;   // 直播列表,30秒
+
+    // ======================== 缓存工具方法 ========================
+
+    /**
+     * 通用 Cache-Aside 读取:先查缓存,未命中则查询DB并回填缓存
+     */
+    @SuppressWarnings("unchecked")
+    private <T> T cacheGet(String key, int ttlSeconds, CacheLoader<T> loader) {
+        T cached = (T) redisCache.getCacheObject(key);
+        if (cached != null) {
+            return cached;
+        }
+        T data = loader.load();
+        if (data != null) {
+            redisCache.setCacheObject(key, data, ttlSeconds, TimeUnit.SECONDS);
+        }
+        return data;
+    }
+
+    /** 清除指定医生的所有Dashboard缓存 */
+    private void clearDashboardCache(Long doctorId) {
+        String[] keys = {
+            CK_OVERVIEW + doctorId,
+            CK_CONSULT_TREND + doctorId + ":7",
+            CK_CONSULT_TREND + doctorId + ":30",
+            CK_PRESCRIBE_TREND + doctorId + ":7",
+            CK_PRESCRIBE_TREND + doctorId + ":30",
+            CK_REVENUE_TREND + doctorId + ":7",
+            CK_REVENUE_TREND + doctorId + ":30",
+            CK_PERFORMANCE + doctorId,
+            CK_LIVE_LIST + doctorId
+        };
+        for (String k : keys) {
+            redisCache.deleteObject(k);
+        }
+    }
+
+    /** 功能性接口,用于延迟加载 */
+    @FunctionalInterface
+    private interface CacheLoader<T> {
+        T load();
+    }
+
+    // ======================== API 接口 ========================
+
+    /**
+     * 今日工作台概览(高频调用,DB查询5次合并为1次缓存读取)
+     * 缓存策略: TTL=30s
+     * 优化点: 将5次独立DB查询的结果整体缓存,命中时直接返回
+     */
+    @Login
+    @ApiOperation("今日工作台概览")
+    @GetMapping("/overview")
+    public R overview() {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_OVERVIEW + doctorId;
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> data = cacheGet(cacheKey, TTL_OVERVIEW, () -> {
+            Map<String, Object> d = new HashMap<>();
+
+            // 今日问诊概览(各状态数量)
+            Map<String, Object> consultOverview = doctorDashboardMapper.selectTodayConsultOverview(doctorId);
+            d.put("consultOverview", consultOverview != null ? consultOverview : new HashMap<>());
+
+            // 今日收入
+            BigDecimal todayIncome = doctorDashboardMapper.selectTodayIncome(doctorId);
+            d.put("todayIncome", todayIncome != null ? todayIncome : BigDecimal.ZERO);
+
+            // 待处理随访
+            Integer pendingFollow = doctorDashboardMapper.selectPendingFollowCount(doctorId);
+            d.put("pendingFollowCount", pendingFollow != null ? pendingFollow : 0);
+
+            // 待审核处方
+            Integer pendingPrescribe = doctorDashboardMapper.selectPendingPrescribeCount(doctorId);
+            d.put("pendingPrescribeCount", pendingPrescribe != null ? pendingPrescribe : 0);
+
+            // 今日排班
+            List<Map<String, Object>> todaySchedule = doctorDashboardMapper.selectTodaySchedule(doctorId);
+            d.put("todaySchedule", todaySchedule != null ? todaySchedule : new ArrayList<>());
+
+            return d;
+        });
+
+        return R.ok(data);
+    }
+
+    /**
+     * 问诊统计趋势(按天聚合)
+     * 缓存策略: TTL=300s(5分钟),按doctorId+days隔离
+     */
+    @Login
+    @ApiOperation("问诊量趋势")
+    @GetMapping("/consultTrend")
+    public R consultTrend(@RequestParam(value = "days", defaultValue = "7") int days) {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_CONSULT_TREND + doctorId + ":" + days;
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> trend = cacheGet(cacheKey, TTL_TREND, () -> {
+            List<Map<String, Object>> data = doctorDashboardMapper.selectConsultTrend(doctorId, days);
+            return data != null ? data : new ArrayList<>();
+        });
+
+        return R.ok().put("data", trend);
+    }
+
+    /**
+     * 处方统计趋势(按天聚合)
+     * 缓存策略: TTL=300s(5分钟)
+     */
+    @Login
+    @ApiOperation("处方量趋势")
+    @GetMapping("/prescribeTrend")
+    public R prescribeTrend(@RequestParam(value = "days", defaultValue = "7") int days) {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_PRESCRIBE_TREND + doctorId + ":" + days;
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> trend = cacheGet(cacheKey, TTL_TREND, () -> {
+            List<Map<String, Object>> data = doctorDashboardMapper.selectPrescribeTrend(doctorId, days);
+            return data != null ? data : new ArrayList<>();
+        });
+
+        return R.ok().put("data", trend);
+    }
+
+    /**
+     * 收入趋势(按天聚合)
+     * 缓存策略: TTL=300s(5分钟)
+     */
+    @Login
+    @ApiOperation("收入趋势")
+    @GetMapping("/revenueTrend")
+    public R revenueTrend(@RequestParam(value = "days", defaultValue = "30") int days) {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_REVENUE_TREND + doctorId + ":" + days;
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> trend = cacheGet(cacheKey, TTL_TREND, () -> {
+            List<Map<String, Object>> data = doctorDashboardMapper.selectRevenueTrend(doctorId, days);
+            return data != null ? data : new ArrayList<>();
+        });
+
+        return R.ok().put("data", trend);
+    }
+
+    /**
+     * 业绩看板汇总(含本月排名)
+     * 缓存策略: TTL=60s(1分钟)
+     * 优化: 合并 selectPerformanceSummary + selectConsultRank 到同一缓存
+     */
+    @Login
+    @ApiOperation("业绩看板汇总")
+    @GetMapping("/performance")
+    public R performance() {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_PERFORMANCE + doctorId;
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> summary = cacheGet(cacheKey, TTL_PERFORMANCE, () -> {
+            Map<String, Object> data = doctorDashboardMapper.selectPerformanceSummary(doctorId);
+            if (data == null) {
+                data = new HashMap<>();
+            }
+            // 附加上本月排名信息
+            try {
+                Map<String, Object> rankInfo = doctorDashboardMapper.selectConsultRank(doctorId);
+                if (rankInfo != null) {
+                    data.put("rankInfo", rankInfo);
+                }
+            } catch (Exception ignored) {
+                // 排名查询失败不影响主流程
+            }
+            return data;
+        });
+
+        return R.ok(summary);
+    }
+
+    /**
+     * 我的直播列表
+     * 缓存策略: TTL=30s — 直播状态变化频繁
+     * 注意: 列表查询不包含分页,一次性返回全部
+     */
+    @Login
+    @ApiOperation("我的直播列表")
+    @GetMapping("/myLiveList")
+    public R myLiveList() {
+        Long doctorId = Long.parseLong(getDoctorId());
+        String cacheKey = CK_LIVE_LIST + doctorId;
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> list = cacheGet(cacheKey, TTL_LIVE_LIST, () -> {
+            List<Map<String, Object>> data = doctorDashboardMapper.selectMyLiveList(doctorId);
+            return data != null ? data : new ArrayList<>();
+        });
+
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 清除当前医生的所有Dashboard缓存
+     * 用于问诊状态变更、直播状态变更等场景的主动失效
+     */
+    @Login
+    @ApiOperation("清除Dashboard缓存")
+    @DeleteMapping("/clearCache")
+    public R clearCache() {
+        Long doctorId = Long.parseLong(getDoctorId());
+        clearDashboardCache(doctorId);
+        return R.ok("缓存已清除");
+    }
+}

+ 70 - 0
java/fs-service/src/main/java/com/fs/his/mapper/DoctorDashboardMapper.java

@@ -0,0 +1,70 @@
+package com.fs.his.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 医生工作台Dashboard Mapper
+ * 今日概览、问诊统计、业绩看板
+ * SQL映射文件: resources/mapper/his/DoctorDashboardMapper.xml
+ */
+public interface DoctorDashboardMapper {
+
+    /**
+     * 今日问诊概览(各状态数量)
+     */
+    Map<String, Object> selectTodayConsultOverview(@Param("doctorId") Long doctorId);
+
+    /**
+     * 今日收入
+     */
+    BigDecimal selectTodayIncome(@Param("doctorId") Long doctorId);
+
+    /**
+     * 待处理随访提醒数
+     */
+    Integer selectPendingFollowCount(@Param("doctorId") Long doctorId);
+
+    /**
+     * 待审核处方数
+     */
+    Integer selectPendingPrescribeCount(@Param("doctorId") Long doctorId);
+
+    /**
+     * 问诊量趋势(按天统计)
+     */
+    List<Map<String, Object>> selectConsultTrend(@Param("doctorId") Long doctorId, @Param("days") int days);
+
+    /**
+     * 处方量趋势(按天统计)
+     */
+    List<Map<String, Object>> selectPrescribeTrend(@Param("doctorId") Long doctorId, @Param("days") int days);
+
+    /**
+     * 收入趋势(按天统计)
+     */
+    List<Map<String, Object>> selectRevenueTrend(@Param("doctorId") Long doctorId, @Param("days") int days);
+
+    /**
+     * 业绩汇总(累计数据)
+     */
+    Map<String, Object> selectPerformanceSummary(@Param("doctorId") Long doctorId);
+
+    /**
+     * 今日排班列表
+     */
+    List<Map<String, Object>> selectTodaySchedule(@Param("doctorId") Long doctorId);
+
+    /**
+     * 我的直播列表
+     */
+    List<Map<String, Object>> selectMyLiveList(@Param("doctorId") Long doctorId);
+
+    /**
+     * 本月问诊排行榜(医生本人排名信息)
+     */
+    Map<String, Object> selectConsultRank(@Param("doctorId") Long doctorId);
+}

+ 10 - 0
java/fs-service/src/main/java/com/fs/hisStore/constants/UserAppsLockConstant.java

@@ -21,4 +21,14 @@ public interface UserAppsLockConstant {
      * 修改支付类型
      */
     public static final String LOCK_KEY_EDIT_PAY_TYPE = "payment:paytype:lock:%d";
+
+    /**
+     * 合并多店铺下单 - 用户级锁,防止同一用户并发创建多店铺订单
+     */
+    public static final String LOCK_KEY_MULTI_STORE_CREATE = "order:multiStore:create:%d";
+
+    /**
+     * 整单退款 - 订单级锁,防止同一订单并发退款
+     */
+    public static final String LOCK_KEY_ORDER_REFUND = "order:refund:%d";
 }

+ 3 - 0
java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCartScrm.java

@@ -60,5 +60,8 @@ public class FsStoreCartScrm extends BaseEntity
     @Excel(name = "改价(商品单价)")
     private BigDecimal changePrice;
 
+    /** 店铺ID */
+    @Excel(name = "店铺ID")
+    private Long storeId;
 
 }

+ 30 - 0
java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreFavoriteScrm.java

@@ -0,0 +1,30 @@
+package com.fs.hisStore.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.*;
+
+/**
+ * 用户店铺收藏对象 fs_store_favorite_scrm
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class FsStoreFavoriteScrm extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 收藏ID */
+    private Long favoriteId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 店铺ID */
+    private Long storeId;
+
+}

+ 15 - 0
java/fs-service/src/main/java/com/fs/hisStore/domain/FsStoreScrm.java

@@ -5,6 +5,7 @@ import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import org.springframework.web.bind.annotation.PathVariable;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -194,4 +195,18 @@ public class FsStoreScrm extends BaseEntity
     /** 医疗机构执业许可证有效期结束 */
     private LocalDate medicalLicenseExpiryEnd;
 
+    /** 营业时间 */
+    @Excel(name = "营业时间")
+    private String openingHours;
+
+    /** 店铺评分 */
+    @Excel(name = "店铺评分")
+    private BigDecimal storeScore;
+
+    /**
+     * 是否被收藏
+     */
+    @TableField(exist = false)
+    private Integer isFavorite;
+
 }

+ 2 - 0
java/fs-service/src/main/java/com/fs/hisStore/dto/FsStoreOrderPriceDTO.java

@@ -18,5 +18,7 @@ import java.math.BigDecimal;
 
     private BigDecimal payIntegral;//积分
 
+    private BigDecimal couponPrice;//优惠券金额
+
 
 }

+ 45 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java

@@ -6,6 +6,7 @@ import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.hisStore.domain.FsStoreCartScrm;
 import com.fs.hisStore.param.FsStoreCartCountParam;
 import com.fs.hisStore.vo.FsStoreCartQueryVO;
+import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 import org.apache.ibatis.annotations.Delete;
 import org.apache.ibatis.annotations.Param;
@@ -116,4 +117,48 @@ public interface FsStoreCartScrmMapper
     Integer selectFsStoreCartCount(@Param("maps") FsStoreCartCountParam param);
 
     Integer selectCartProductTypeCountByUserId(long userId);
+
+    @Select({"<script>" +
+            "select c.*,p.store_id as storeId,p.product_type,p.product_name,p.image as product_image," +
+            "v.price,v.sku as product_attr_name,v.image as product_attr_image,v.stock," +
+            "fs.store_name as storeName " +
+            "from fs_store_cart_scrm c " +
+            "inner join fs_store_product_scrm p on p.product_id=c.product_id " +
+            "inner join fs_store_scrm fs on fs.store_id = p.store_id " +
+            "<if test='config.isAudit == \"1\"'>" +
+            "and fs.is_audit = 1 " +
+            "</if>" +
+            "inner join fs_store_product_attr_value_scrm v on v.id=c.product_attr_value_id " +
+            "where c.is_pay=0 and c.is_del=0 and c.is_buy=0 " +
+            "and p.is_del=0 and p.is_show=1 " +
+            "<if test='config.isAudit == \"1\"'>" +
+            "and p.is_audit = '1' " +
+            "</if>" +
+            "and c.user_id= #{uid} " +
+            "</script>"})
+    List<FsStoreCartStoreVO> selectFsStoreCartStoreListByUid(@Param("uid") long uid, @Param("config") MedicalMallConfig config);
+
+    /**
+     * 查询指定店铺的购物车列表(含店铺ID和名称)
+     */
+    @Select({"<script>" +
+            "select c.*,p.store_id as storeId,p.product_type,p.product_name,p.image as product_image," +
+            "v.price,v.sku as product_attr_name,v.image as product_attr_image,v.stock," +
+            "fs.store_name as storeName " +
+            "from fs_store_cart_scrm c " +
+            "inner join fs_store_product_scrm p on p.product_id=c.product_id " +
+            "inner join fs_store_scrm fs on fs.store_id = p.store_id " +
+            "<if test='config.isAudit == \"1\"'>" +
+            "and fs.is_audit = 1 " +
+            "</if>" +
+            "inner join fs_store_product_attr_value_scrm v on v.id=c.product_attr_value_id " +
+            "where c.is_pay=0 and c.is_del=0 and c.is_buy=0 " +
+            "and p.is_del=0 and p.is_show=1 " +
+            "<if test='config.isAudit == \"1\"'>" +
+            "and p.is_audit = '1' " +
+            "</if>" +
+            "and c.user_id = #{uid} " +
+            "and p.store_id = #{storeId} " +
+            "</script>"})
+    List<FsStoreCartStoreVO> selectFsStoreCartListByUidAndStoreId(@Param("uid") long uid, @Param("storeId") Long storeId, @Param("config") MedicalMallConfig config);
 }

+ 74 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreFavoriteScrmMapper.java

@@ -0,0 +1,74 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreFavoriteScrm;
+import com.fs.hisStore.vo.FsStoreFavoriteVO;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 用户店铺收藏Mapper接口
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+public interface FsStoreFavoriteScrmMapper
+{
+    /**
+     * 查询店铺收藏
+     */
+    public FsStoreFavoriteScrm selectFsStoreFavoriteById(Long favoriteId);
+
+    /**
+     * 查询店铺收藏列表
+     */
+    public List<FsStoreFavoriteScrm> selectFsStoreFavoriteList(FsStoreFavoriteScrm fsStoreFavorite);
+
+    /**
+     * 新增店铺收藏
+     */
+    public int insertFsStoreFavorite(FsStoreFavoriteScrm fsStoreFavorite);
+
+    /**
+     * 修改店铺收藏
+     */
+    public int updateFsStoreFavorite(FsStoreFavoriteScrm fsStoreFavorite);
+
+    /**
+     * 删除店铺收藏
+     */
+    public int deleteFsStoreFavoriteById(Long favoriteId);
+
+    /**
+     * 批量删除店铺收藏
+     */
+    public int deleteFsStoreFavoriteByIds(Long[] favoriteIds);
+
+    /**
+     * 检查是否已收藏
+     */
+    @Select("select ifnull(count(1),0) from fs_store_favorite_scrm where store_id = #{storeId} and user_id = #{userId}")
+    int checkFavorite(@Param("storeId") Long storeId, @Param("userId") Long userId);
+
+    /**
+     * 取消收藏
+     */
+    @Delete("delete from fs_store_favorite_scrm where store_id = #{storeId} and user_id = #{userId}")
+    int deleteFavorite(@Param("storeId") Long storeId, @Param("userId") Long userId);
+
+    /**
+     * 查询用户收藏的店铺列表(含店铺信息)
+     */
+    @Select({"<script>" +
+            "select f.favorite_id as favoriteId, f.user_id as userId, f.store_id as storeId, " +
+            "s.store_name as storeName, s.logo_url as logoUrl, s.address, s.phone, s.descs, " +
+            "s.sales_count as salesCount, f.create_time as createTime " +
+            "from fs_store_favorite_scrm f " +
+            "inner join fs_store_scrm s on s.store_id = f.store_id " +
+            "where f.user_id = #{userId} " +
+            "order by f.create_time desc " +
+            "</script>"})
+    List<FsStoreFavoriteVO> selectStoreFavoriteList(@Param("userId") Long userId);
+}

+ 22 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -23,6 +23,8 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 
+import java.util.Map;
+
 /**
  * 订单Mapper接口
  *
@@ -1458,4 +1460,24 @@ public interface FsStoreOrderScrmMapper
      */
     int batchUpdateAuditStatus(@Param("orderIds") List<Long> orderIds, @Param("isAudit") Integer isAudit);
 
+    // ==================== 合并多店铺下单 ====================
+
+    /**
+     * 根据购物车ID列表查询购物车信息(含店铺ID和店铺名称)
+     */
+    List<FsStoreCartStoreVO> selectCartByCartIds(@Param("cartIds") List<Long> cartIds);
+
+    /**
+     * 根据购物车ID列表按店铺分组查询聚合信息
+     */
+    List<Map<String, Object>> selectCartGroupByStore(@Param("cartIds") List<Long> cartIds);
+
+    /**
+     * 更新订单退款状态
+     */
+    int updateRefundStatus(@Param("orderId") Long orderId,
+                           @Param("status") Integer status,
+                           @Param("refundStatus") Integer refundStatus,
+                           @Param("refundPrice") java.math.BigDecimal refundPrice);
+
 }

+ 3 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -426,4 +426,7 @@ public interface FsStoreProductScrmMapper
     List<FsStoreProductListVO> liveList(@Param("maps") LiveGoods maps);
 
     List<FsStoreProductScrm> selectFsStoreProductByProductIds(@Param("productIds") List<Long> productIdsLong);
+
+    @Select("SELECT p.* FROM fs_store_product_scrm p WHERE p.store_id = #{storeId} AND p.is_hot = 1 AND p.is_display = 1 AND p.is_show = 1 AND p.is_del = 0 ORDER BY p.sort DESC LIMIT #{limit}")
+    List<FsStoreProductListQueryVO> selectHotProductsByStoreId(@Param("storeId") Long storeId, @Param("limit") int limit);
 }

+ 2 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartDelParam.java

@@ -14,4 +14,6 @@ public class FsStoreCartDelParam {
     @ApiModelProperty(value = "购物车ID,多个用,分隔开")
     Long[] ids;
 
+    @ApiModelProperty(value = "店铺ID")
+    private Long storeId;
 }

+ 2 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartNumParam.java

@@ -21,5 +21,7 @@ public class FsStoreCartNumParam {
     @ApiModelProperty(value = "购物车ID")
     private Long id;
 
+    @ApiModelProperty(value = "店铺ID")
+    private Long storeId;
 
 }

+ 2 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartParam.java

@@ -28,5 +28,7 @@ public class FsStoreCartParam {
     @ApiModelProperty(value = "是否购买")
     private Integer isBuy=0;
 
+    @ApiModelProperty(value = "店铺ID")
+    private Long storeId;
 
 }

+ 60 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiStoreOrderCreateParam.java

@@ -0,0 +1,60 @@
+package com.fs.hisStore.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并多店铺下单参数
+ *
+ * @author fs
+ */
+@Data
+public class FsStoreMultiStoreOrderCreateParam implements Serializable {
+
+    @ApiModelProperty(value = "地址ID")
+    private Long addressId;
+
+    @Size(max = 200, message = "长度超过了限制")
+    @ApiModelProperty(value = "备注")
+    private String mark;
+
+    @ApiModelProperty(value = "支付方式")
+    private String payType;
+
+    @ApiModelProperty(value = "使用积分 1使用")
+    private Integer useIntegral;
+
+    @ApiModelProperty(value = "优惠券用户ID")
+    private Long couponUserId;
+
+    @ApiModelProperty(value = "订单类型")
+    private Integer orderType;
+
+    @ApiModelProperty(value = "媒体来源")
+    private Integer orderMedium;
+
+    /** 各店铺购物车分组 */
+    @Valid
+    @NotEmpty(message = "请选择下单商品")
+    @ApiModelProperty(value = "各店铺购物车分组")
+    private List<StoreCartGroup> storeCarts;
+
+    @Data
+    public static class StoreCartGroup implements Serializable {
+        @NotNull(message = "店铺ID不能为空")
+        @ApiModelProperty(value = "店铺ID")
+        private Long storeId;
+
+        @NotEmpty(message = "购物车ID列表不能为空")
+        @ApiModelProperty(value = "购物车ID列表")
+        private List<Long> cartIds;
+    }
+}

+ 17 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCartScrmService.java

@@ -8,6 +8,7 @@ import com.fs.hisStore.param.FsStoreCartCountParam;
 import com.fs.hisStore.param.FsStoreCartDelParam;
 import com.fs.hisStore.param.FsStoreCartNumParam;
 import com.fs.hisStore.param.FsStoreCartParam;
+import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 
 /**
@@ -71,10 +72,26 @@ public interface IFsStoreCartScrmService
 
     List<FsStoreCartVO> selectFsStoreCartListByUid(long parseLong);
 
+    List<FsStoreCartStoreVO> selectFsStoreCartStoreListByUid(long parseLong);
+
+    /**
+     * 查询指定店铺的购物车列表(含店铺ID和名称)
+     */
+    List<FsStoreCartStoreVO> selectFsStoreCartListByUidAndStoreId(long uid, Long storeId);
+
     R delCart(long userId, FsStoreCartDelParam cartParam);
 
     R changeNum(long userId, FsStoreCartNumParam cartParam);
 
+    /** 多店铺添加购物车(含店铺ID) */
+    R addCartByStore(long userId, FsStoreCartParam cartParam);
+
+    /** 多店铺改变购物车数量 */
+    R changeNumByStore(long userId, FsStoreCartNumParam cartParam);
+
+    /** 多店铺删除购物车 */
+    R delCartByStore(long userId, FsStoreCartDelParam cartParam);
+
     void checkProductStock( Long productId, Long productAttrValueId, Integer quantity);
 
     Integer selectFsStoreCartCountByUserId(long userId);

+ 35 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreFavoriteScrmService.java

@@ -0,0 +1,35 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.vo.FsStoreFavoriteVO;
+
+/**
+ * 用户店铺收藏Service接口
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+public interface IFsStoreFavoriteScrmService
+{
+    /**
+     * 切换收藏状态(收藏/取消收藏)
+     */
+    R toggleFavorite(Long userId, Long storeId);
+
+    /**
+     * 检查是否已收藏
+     */
+    R checkFavorite(Long storeId, Long userId);
+
+    /**
+     * 检查是否已收藏(返回数量)
+     */
+    int checkFavoriteCount(Long storeId, Long userId);
+
+    /**
+     * 查询用户收藏的店铺列表
+     */
+    List<FsStoreFavoriteVO> selectStoreFavoriteList(Long userId);
+}

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

@@ -349,6 +349,21 @@ public interface IFsStoreOrderScrmService
      */
     R createPackageSalesOrder(CompanyUser companyUser, String packageId, Integer orderType, Integer orderMedium);
 
+    /**
+     * 合并多店铺下单
+     * @param userId 用户ID
+     * @param param  多店铺下单参数
+     * @return 多店铺下单结果
+     */
+    R createMultiStoreOrder(long userId, FsStoreMultiStoreOrderCreateParam param);
+
+    /**
+     * 整单退款(用户端)
+     * @param orderId 订单ID
+     * @return 退款结果
+     */
+    R fullOrderRefund(Long orderId);
+
     void cancelOrderByCode(String outerPayId);
 
     void updateFsStoreOrderDb(FsStoreOrderScrm order);

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

@@ -151,4 +151,9 @@ public interface IFsStoreProductScrmService
     R copyStoreProduct(Long productId);
 
     void sendMiniAppFsStoreProductScrm(Long sendId, String uuid, QwUser user, FastGptChatSession fastGptChatSession, FsStoreProductScrm fsStoreProductScrm, String qwExtId);
+
+    /**
+     * 按店铺ID获取TopN热门商品
+     */
+    List<FsStoreProductListQueryVO> selectHotProductsByStoreId(Long storeId, int limit);
 }

+ 84 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java

@@ -29,6 +29,7 @@ import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
 import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -224,6 +225,16 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         return fsStoreCartMapper.selectFsStoreCartListByUid(uid, medicalMallConfig);
     }
 
+    @Override
+    public List<FsStoreCartStoreVO> selectFsStoreCartStoreListByUid(long uid) {
+        return fsStoreCartMapper.selectFsStoreCartStoreListByUid(uid, medicalMallConfig);
+    }
+
+    @Override
+    public List<FsStoreCartStoreVO> selectFsStoreCartListByUidAndStoreId(long uid, Long storeId) {
+        return fsStoreCartMapper.selectFsStoreCartListByUidAndStoreId(uid, storeId, medicalMallConfig);
+    }
+
     @Override
     public R delCart(long userId, FsStoreCartDelParam cartParam) {
         fsStoreCartMapper.delCart(cartParam.getIds());
@@ -251,6 +262,79 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         return R.ok();
     }
 
+    @Override
+    public R addCartByStore(long uid, FsStoreCartParam cartParam) {
+        // 检查并调整限购数量
+        Integer adjustedNum = adjustPurchaseLimit(uid, cartParam.getProductId(), cartParam.getCartNum());
+        cartParam.setCartNum(adjustedNum);
+
+        if(cartParam.getIsBuy()==1){
+            FsStoreCartScrm storeCart = FsStoreCartScrm.builder()
+                    .cartNum(cartParam.getCartNum())
+                    .productAttrValueId(cartParam.getAttrValueId())
+                    .productId(cartParam.getProductId())
+                    .userId(uid)
+                    .cartNum(cartParam.getCartNum())
+                    .isPay(0)
+                    .isDel(0)
+                    .isBuy(cartParam.getIsBuy())
+                    .storeId(cartParam.getStoreId())
+                    .build();
+            storeCart.setCreateTime(new Date());
+            checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId(),cartParam.getCartNum());
+            fsStoreCartMapper.insertFsStoreCart(storeCart);
+            return R.ok().put("id",storeCart.getId());
+        }
+        else{
+            FsStoreCartScrm cartMap=new FsStoreCartScrm();
+            cartMap.setUserId(uid);
+            cartMap.setIsPay(0);
+            cartMap.setIsBuy(0);
+            cartMap.setProductId(cartParam.getProductId());
+            cartMap.setProductAttrValueId(cartParam.getAttrValueId());
+            cartMap.setStoreId(cartParam.getStoreId());
+            List<FsStoreCartScrm> cart = fsStoreCartMapper.selectFsStoreCartList(cartMap);
+            FsStoreCartScrm storeCart;
+            if(cart==null||cart.size()==0){
+                storeCart = FsStoreCartScrm.builder()
+                        .cartNum(cartParam.getCartNum())
+                        .productAttrValueId(cartMap.getProductAttrValueId())
+                        .productId(cartMap.getProductId())
+                        .userId(uid)
+                        .cartNum(cartParam.getCartNum())
+                        .isPay(0)
+                        .isDel(0)
+                        .isBuy(0)
+                        .storeId(cartParam.getStoreId())
+                        .build();
+                storeCart.setCreateTime(new Date());
+                checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId(),cartParam.getCartNum());
+                fsStoreCartMapper.insertFsStoreCart(storeCart);
+                return R.ok().put("id",storeCart.getId());
+            }
+            else{
+                storeCart=cart.get(0);
+                int newCartNum = cartParam.getCartNum() + cart.get(0).getCartNum();
+                Integer adjustedNewNum = adjustPurchaseLimit(uid, cartParam.getProductId(), newCartNum);
+                storeCart.setCartNum(adjustedNewNum);
+                storeCart.setUpdateTime(new Date());
+                checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId(),adjustedNewNum);
+                fsStoreCartMapper.updateFsStoreCart(storeCart);
+                return R.ok().put("id",storeCart.getId());
+            }
+        }
+    }
+
+    @Override
+    public R changeNumByStore(long userId, FsStoreCartNumParam cartParam) {
+        return changeNum(userId, cartParam);
+    }
+
+    @Override
+    public R delCartByStore(long userId, FsStoreCartDelParam cartParam) {
+        return delCart(userId, cartParam);
+    }
+
     /**
      * 检查限购(用于修改数量时,超过限购则抛出异常)
      * @param userId 用户ID

+ 62 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreFavoriteScrmServiceImpl.java

@@ -0,0 +1,62 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.Date;
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.domain.FsStoreFavoriteScrm;
+import com.fs.hisStore.mapper.FsStoreFavoriteScrmMapper;
+import com.fs.hisStore.service.IFsStoreFavoriteScrmService;
+import com.fs.hisStore.vo.FsStoreFavoriteVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 用户店铺收藏Service实现
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+@Service
+public class FsStoreFavoriteScrmServiceImpl implements IFsStoreFavoriteScrmService
+{
+    @Autowired
+    private FsStoreFavoriteScrmMapper favoriteMapper;
+
+    @Override
+    public R checkFavorite(Long storeId, Long userId) {
+        int count = favoriteMapper.checkFavorite(storeId, userId);
+        return count > 0 ? R.ok().put("isFavorite", 1) : R.ok().put("isFavorite", 0);
+    }
+
+    @Override
+    public int checkFavoriteCount(Long storeId, Long userId) {
+        return favoriteMapper.checkFavorite(storeId, userId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R toggleFavorite(Long userId, Long storeId) {
+        int count = favoriteMapper.checkFavorite(storeId, userId);
+        if (count > 0) {
+            // 已收藏,取消收藏
+            favoriteMapper.deleteFavorite(storeId, userId);
+            return R.ok("取消收藏成功").put("isFavorite", 0);
+        } else {
+            // 未收藏,添加收藏
+            FsStoreFavoriteScrm favorite = FsStoreFavoriteScrm.builder()
+                    .userId(userId)
+                    .storeId(storeId)
+                    .build();
+            favorite.setCreateTime(new Date());
+            favoriteMapper.insertFsStoreFavorite(favorite);
+            return R.ok("收藏成功").put("isFavorite", 1);
+        }
+    }
+
+    @Override
+    public List<FsStoreFavoriteVO> selectStoreFavoriteList(Long userId) {
+        return favoriteMapper.selectStoreFavoriteList(userId);
+    }
+}

+ 254 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -136,6 +136,9 @@ import me.chanjar.weixin.common.error.WxErrorException;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.http.util.Asserts;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.aop.framework.AopContext;
@@ -166,6 +169,8 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+
+import static com.fs.hisStore.constants.UserAppsLockConstant.*;
 import java.util.stream.Collectors;
 
 import static com.fs.his.utils.PhoneUtil.decryptPhone;
@@ -410,6 +415,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private FsUserCompanyPackageScrmMapper fsUserCompanyPackageScrmMapper;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     /**
      * 租户编码请求头名称。
      * 前端需在每次请求中携带该头,才能启用多租户能力。
@@ -5660,6 +5668,252 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return fsStoreOrderMapper.updateFsStoreOrder(order);
     }
 
+    // ==================== 合并多店铺下单 ====================
+
+    /**
+     * 构建多租户隔离的分布式锁 Key
+     * 多租户: 1:order:multiStore:create:10001
+     * 单租户: order:multiStore:create:10001
+     */
+    private String buildTenantLockKey(String keyFormat, Object... args) {
+        Long tenantId = DynamicDataSourceContextHolder.getCurrentTenantId();
+        String prefix = tenantId != null ? tenantId + ":" : "";
+        return prefix + String.format(keyFormat, args);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R createMultiStoreOrder(long userId, FsStoreMultiStoreOrderCreateParam param) {
+        RLock lock = redissonClient.getLock(buildTenantLockKey(LOCK_KEY_MULTI_STORE_CREATE, userId));
+        try {
+            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("合并多店铺下单失败: 用户{}正在创建订单中,获取锁失败", userId);
+                return R.error("您有订单正在处理中,请稍后再试");
+            }
+
+            // 1. 收集所有 cartIds 并查询购物车信息(含店铺ID)
+            List<Long> allCartIds = new ArrayList<>();
+            for (FsStoreMultiStoreOrderCreateParam.StoreCartGroup group : param.getStoreCarts()) {
+                allCartIds.addAll(group.getCartIds());
+            }
+            if (allCartIds.isEmpty()) {
+                return R.error("购物车ID列表不能为空");
+            }
+
+            List<FsStoreCartStoreVO> allCarts = fsStoreOrderMapper.selectCartByCartIds(allCartIds);
+            if (allCarts == null || allCarts.isEmpty()) {
+                return R.error("购物车数据不存在或已过期");
+            }
+
+            // 2. 按店铺分组
+            Map<Long, List<FsStoreCartStoreVO>> storeCartMap = new HashMap<>();
+            for (FsStoreCartStoreVO cart : allCarts) {
+                storeCartMap.computeIfAbsent(cart.getStoreId(), k -> new ArrayList<>()).add(cart);
+            }
+
+            // 3. 获取用户地址
+            FsUserAddressScrm address = null;
+            if (param.getAddressId() != null) {
+                address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
+            }
+
+            // 4. 获取用户信息
+            FsUserScrm user = userService.selectFsUserById(userId);
+
+            // 5. 按店铺逐个创建订单
+            FsStoreMultiStoreOrderResultVO result = new FsStoreMultiStoreOrderResultVO();
+            List<FsStoreMultiStoreOrderResultVO.StoreOrderEntry> orderEntries = new ArrayList<>();
+            BigDecimal totalAmount = BigDecimal.ZERO;
+
+            for (Map.Entry<Long, List<FsStoreCartStoreVO>> entry : storeCartMap.entrySet()) {
+                Long storeId = entry.getKey();
+                List<FsStoreCartStoreVO> storeCarts = entry.getValue();
+                String storeName = storeCarts.get(0).getStoreName();
+
+                // 生成订单号
+                String orderSn = OrderCodeUtils.getOrderSn();
+
+                // 组装成 FsStoreCartQueryVO 以复用计价逻辑
+                List<FsStoreCartQueryVO> cartQueryVOs = new ArrayList<>();
+                StringBuilder cartIdSb = new StringBuilder();
+                for (FsStoreCartStoreVO cart : storeCarts) {
+                    FsStoreCartQueryVO vo = new FsStoreCartQueryVO();
+                    vo.setId(cart.getId());
+                    vo.setProductId(cart.getProductId());
+                    vo.setCartNum(cart.getCartNum());
+                    vo.setPrice(cart.getPrice());
+                    vo.setProductName(cart.getProductName());
+                    vo.setProductImage(cart.getProductImage());
+                    cartQueryVOs.add(vo);
+                    if (cartIdSb.length() > 0) cartIdSb.append(",");
+                    cartIdSb.append(cart.getId());
+                }
+
+                // 计算价格
+                BigDecimal payPrice = getOrderSumPrice(cartQueryVOs, "truePrice");
+                FsStoreOrderPriceDTO priceGroup = this.getOrderPriceGroup(cartQueryVOs, address);
+                BigDecimal payPostage = priceGroup.getStorePostage();
+                if (payPostage.compareTo(BigDecimal.valueOf(-1)) == 0) {
+                    throw new ServiceException("店铺[" + storeName + "]偏远地区暂不可购买");
+                }
+                payPrice = NumberUtil.add(payPrice, payPostage);
+
+                // 构建订单实体
+                FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+                storeOrder.setStoreId(storeId);
+                storeOrder.setStoreHouseCode("CK01");
+                storeOrder.setUserId(userId);
+                storeOrder.setOrderCode(orderSn);
+                storeOrder.setCartId(cartIdSb.toString());
+                storeOrder.setTotalNum((long) storeCarts.size());
+                storeOrder.setTotalPrice(payPrice);
+                storeOrder.setTotalPostage(payPostage);
+                storeOrder.setPayPostage(payPostage);
+                storeOrder.setDeductionPrice(priceGroup.getCouponPrice());
+                storeOrder.setPaid(0);
+                storeOrder.setPayType(param.getPayType());
+                storeOrder.setStatus(0);
+                storeOrder.setRefundStatus(0);
+                storeOrder.setBackIntegral(BigDecimal.ZERO);
+                storeOrder.setGainIntegral(BigDecimal.ZERO);
+                storeOrder.setMark(param.getMark());
+                storeOrder.setIsChannel(1);
+                storeOrder.setIsDel(0);
+                storeOrder.setIsSysDel(0);
+                storeOrder.setOrderType(param.getOrderType());
+                storeOrder.setOrderMedium(param.getOrderMedium());
+
+                // 成本价
+                BigDecimal costPrice = this.getOrderSumPrice(cartQueryVOs, "costPrice");
+                storeOrder.setCost(costPrice);
+
+                // 地址信息
+                if (address != null) {
+                    storeOrder.setRealName(address.getRealName());
+                    storeOrder.setUserPhone(address.getPhone());
+                    storeOrder.setUserAddress(address.getProvince() + " " + address.getCity()
+                            + " " + address.getDistrict() + " " + address.getDetail().trim());
+                }
+
+                // 插入订单
+                fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
+                Long orderId = storeOrder.getId();
+
+                // 插入订单明细和扣减库存
+                for (FsStoreCartQueryVO cartVO : cartQueryVOs) {
+                    FsStoreOrderItemScrm item = new FsStoreOrderItemScrm();
+                    item.setOrderId(orderId);
+                    item.setOrderCode(orderSn);
+                    item.setProductId(cartVO.getProductId());
+                    item.setCartId(cartVO.getId());
+                    item.setNum(cartVO.getCartNum());
+                    item.setStoreId(storeId);
+
+                    JSONObject itemJson = new JSONObject();
+                    itemJson.put("productName", cartVO.getProductName());
+                    itemJson.put("productImage", cartVO.getProductImage());
+                    itemJson.put("price", cartVO.getPrice());
+                    itemJson.put("num", cartVO.getCartNum());
+                    item.setJsonInfo(itemJson.toJSONString());
+                    fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+
+                    // 扣减库存
+                    FsStoreProductScrm product = productService.selectFsStoreProductById(cartVO.getProductId());
+                    if (product != null) {
+                        if (product.getStock() >= cartVO.getCartNum()) {
+                            product.setStock(product.getStock() - cartVO.getCartNum());
+                            productService.updateFsStoreProduct(product);
+                        }
+                    }
+                }
+
+                // 清理购物车
+                for (FsStoreCartQueryVO cartVO : cartQueryVOs) {
+                    FsStoreCartScrm deleteCart = new FsStoreCartScrm();
+                    deleteCart.setId(cartVO.getId());
+                    cartMapper.deleteFsStoreCartByIds(new Long[]{cartVO.getId()});
+                }
+
+                // 组装返回结果
+                FsStoreMultiStoreOrderResultVO.StoreOrderEntry storeOrderEntry = new FsStoreMultiStoreOrderResultVO.StoreOrderEntry();
+                storeOrderEntry.setStoreId(storeId);
+                storeOrderEntry.setStoreName(storeName);
+                storeOrderEntry.setOrderId(orderId);
+                storeOrderEntry.setOrderCode(orderSn);
+                storeOrderEntry.setPayPrice(payPrice);
+                orderEntries.add(storeOrderEntry);
+
+                totalAmount = totalAmount.add(payPrice);
+            }
+
+            result.setOrderCount(orderEntries.size());
+            result.setTotalAmount(totalAmount);
+            result.setOrders(orderEntries);
+            return R.ok().put("map",result);
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("合并多店铺下单被中断, userId: {}", userId, e);
+            return R.error("下单处理被中断,请稍后重试");
+        } catch (ServiceException e) {
+            // 运费模板等业务异常直接返回
+            return R.error(e.getMessage());
+        } catch (Exception e) {
+            log.error("合并多店铺下单异常, userId: {}", userId, e);
+            return R.error("下单失败: " + e.getMessage());
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    // ==================== 整单退款 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R fullOrderRefund(Long orderId) {
+        RLock lock = redissonClient.getLock(buildTenantLockKey(LOCK_KEY_ORDER_REFUND, orderId));
+        try {
+            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("整单退款失败: 订单{}正在退款中,获取锁失败", orderId);
+                return R.error("订单正在退款处理中,请勿重复提交");
+            }
+
+            FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
+            if (order == null) {
+                return R.error("订单不存在");
+            }
+
+            // 已退款订单不可重复退款
+            if (order.getStatus() == -2) {
+                return R.error("该订单已退款");
+            }
+
+            // 仅已支付或已发货的订单可退款
+            if (order.getStatus() != 1 && order.getStatus() != 2) {
+                return R.error("当前订单状态不支持退款");
+            }
+
+            // 调用已有的退款逻辑
+            return refundOrderMoney(orderId);
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("整单退款被中断, orderId: {}", orderId, e);
+            return R.error("退款处理被中断,请稍后重试");
+        } catch (Exception e) {
+            log.error("整单退款异常, orderId: {}", orderId, e);
+            return R.error("退款失败: " + e.getMessage());
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
     private static final DateTimeFormatter CST_FORMATTER = DateTimeFormatter
             .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US)
             .withZone(ZoneId.of("Asia/Shanghai"));

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

@@ -1843,4 +1843,9 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         courseMaConfigs.add(hisConfigNew);
         return PubFun.listToMapByGroupObject(courseMaConfigs, FsCoursePlaySourceConfigVO::getAppid);
     }
+
+    @Override
+    public List<FsStoreProductListQueryVO> selectHotProductsByStoreId(Long storeId, int limit) {
+        return fsStoreProductMapper.selectHotProductsByStoreId(storeId, limit);
+    }
 }

+ 26 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartGroupVO.java

@@ -0,0 +1,26 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 多店铺购物车分组视图对象
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+@Data
+public class FsStoreCartGroupVO implements Serializable {
+
+    /** 店铺ID */
+    private Long storeId;
+
+    /** 店铺名称 */
+    private String storeName;
+
+    /** 该店铺下的购物车列表 */
+    private List<FsStoreCartVO> carts;
+
+}

+ 22 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartStoreVO.java

@@ -0,0 +1,22 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 多店铺购物车视图对象(含店铺ID和店铺名称)
+ *
+ * @author fs
+ * @date 2026-08-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsStoreCartStoreVO extends FsStoreCartVO {
+
+    /** 店铺ID */
+    private Long storeId;
+
+    /** 店铺名称 */
+    private String storeName;
+
+}

+ 46 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreFavoriteVO.java

@@ -0,0 +1,46 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 用户店铺收藏列表VO
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+@Data
+public class FsStoreFavoriteVO implements Serializable
+{
+    /** 收藏ID */
+    private Long favoriteId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 店铺ID */
+    private Long storeId;
+
+    /** 店铺名称 */
+    private String storeName;
+
+    /** 店铺Logo */
+    private String logoUrl;
+
+    /** 店铺地址 */
+    private String address;
+
+    /** 店铺电话 */
+    private String phone;
+
+    /** 店铺简介 */
+    private String descs;
+
+    /** 销量 */
+    private Long salesCount;
+
+    /** 收藏时间 */
+    private Date createTime;
+}

+ 44 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreOrderResultVO.java

@@ -0,0 +1,44 @@
+package com.fs.hisStore.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并多店铺下单结果
+ *
+ * @author fs
+ */
+@Data
+public class FsStoreMultiStoreOrderResultVO implements Serializable {
+
+    @ApiModelProperty(value = "订单总数")
+    private int orderCount;
+
+    @ApiModelProperty(value = "订单总金额")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "各店铺订单列表")
+    private List<StoreOrderEntry> orders;
+
+    @Data
+    public static class StoreOrderEntry implements Serializable {
+        @ApiModelProperty(value = "店铺ID")
+        private Long storeId;
+
+        @ApiModelProperty(value = "店铺名称")
+        private String storeName;
+
+        @ApiModelProperty(value = "订单ID")
+        private Long orderId;
+
+        @ApiModelProperty(value = "订单号")
+        private String orderCode;
+
+        @ApiModelProperty(value = "订单金额")
+        private BigDecimal payPrice;
+    }
+}

+ 3 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java

@@ -59,6 +59,9 @@ public class FsStoreProductListQueryVO implements Serializable
     //商品存在的店铺ids
     private String storeIds;
 
+    /** 分类名称 */
+    private String cateName;
+
     private Long storeId;
     private Long companyId;
     private Long companyUserId;

+ 26 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/StoreScrmTopVo.java

@@ -0,0 +1,26 @@
+package com.fs.hisStore.vo;
+
+import com.fs.hisStore.domain.FsStoreScrm;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 店铺+热门商品Top视图对象(多店铺管理)
+ *
+ * @author fs
+ * @date 2026-08-03
+ */
+@Data
+public class StoreScrmTopVo {
+
+    /** 店铺信息 */
+    private FsStoreScrm store;
+
+    /** Top热门商品列表(默认3个) */
+    private List<FsStoreProductListQueryVO> topProducts;
+
+    /** 是否已收藏(0=未收藏 1=已收藏) */
+    private Integer isFavorite;
+
+}

+ 141 - 0
java/fs-service/src/main/resources/mapper/his/DoctorDashboardMapper.xml

@@ -0,0 +1,141 @@
+<?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.his.mapper.DoctorDashboardMapper">
+
+    <!-- 今日问诊概览(各状态数量) -->
+    <select id="selectTodayConsultOverview" resultType="java.util.Map">
+        SELECT
+            COUNT(CASE WHEN o.status = 0 THEN 1 END) AS waitingCount,
+            COUNT(CASE WHEN o.status = 1 THEN 1 END) AS inProgressCount,
+            COUNT(CASE WHEN o.status = 2 THEN 1 END) AS finishedCount,
+            COUNT(*) AS totalCount
+        FROM fs_inquiry_order o
+        WHERE o.doctor_id = #{doctorId}
+          AND DATE(o.create_time) = CURDATE()
+    </select>
+
+    <!-- 今日收入 -->
+    <select id="selectTodayIncome" resultType="java.math.BigDecimal">
+        SELECT COALESCE(SUM(b.money), 0) AS todayIncome
+        FROM fs_doctor_bill b
+        WHERE b.doctor_id = #{doctorId}
+          AND b.bill_type = 1
+          AND b.money > 0
+          AND DATE(b.create_time) = CURDATE()
+    </select>
+
+    <!-- 待处理随访提醒数 -->
+    <select id="selectPendingFollowCount" resultType="java.lang.Integer">
+        SELECT COUNT(1) FROM fs_follow f
+        WHERE f.doctor_id = #{doctorId}
+          AND f.follow_status = 0
+    </select>
+
+    <!-- 待审核处方数 -->
+    <select id="selectPendingPrescribeCount" resultType="java.lang.Integer">
+        SELECT COUNT(1) FROM fs_prescribe p
+        WHERE p.doctor_id = #{doctorId}
+          AND p.status = 1
+    </select>
+
+    <!-- 问诊量趋势(按天统计) -->
+    <select id="selectConsultTrend" resultType="java.util.Map">
+        SELECT DATE(o.create_time) AS date,
+               COUNT(1) AS count
+        FROM fs_inquiry_order o
+        WHERE o.doctor_id = #{doctorId}
+          AND o.create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
+        GROUP BY DATE(o.create_time)
+        ORDER BY date ASC
+    </select>
+
+    <!-- 处方量趋势(按天统计) -->
+    <select id="selectPrescribeTrend" resultType="java.util.Map">
+        SELECT DATE(p.create_time) AS date,
+               COUNT(1) AS count
+        FROM fs_prescribe p
+        WHERE p.doctor_id = #{doctorId}
+          AND p.create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
+        GROUP BY DATE(p.create_time)
+        ORDER BY date ASC
+    </select>
+
+    <!-- 收入趋势(按天统计) -->
+    <select id="selectRevenueTrend" resultType="java.util.Map">
+        SELECT DATE(b.create_time) AS date,
+               COALESCE(SUM(b.money), 0) AS amount
+        FROM fs_doctor_bill b
+        WHERE b.doctor_id = #{doctorId}
+          AND b.bill_type = 1
+          AND b.money > 0
+          AND b.create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
+        GROUP BY DATE(b.create_time)
+        ORDER BY date ASC
+    </select>
+
+    <!-- 业绩汇总(累计数据) -->
+    <select id="selectPerformanceSummary" resultType="java.util.Map">
+        SELECT
+            COALESCE(d.balance, 0) AS balance,
+            COALESCE(d.total_income, 0) AS totalIncome,
+            (SELECT COUNT(1) FROM fs_inquiry_order WHERE doctor_id = #{doctorId}) AS totalConsults,
+            (SELECT COUNT(1) FROM fs_inquiry_order WHERE doctor_id = #{doctorId} AND status = 2) AS finishedConsults,
+            (SELECT COUNT(1) FROM fs_prescribe WHERE doctor_id = #{doctorId}) AS totalPrescribes,
+            (SELECT COUNT(1) FROM fs_follow WHERE doctor_id = #{doctorId}) AS totalFollows
+        FROM fs_doctor d
+        WHERE d.doctor_id = #{doctorId}
+    </select>
+
+    <!-- 今日排班列表 -->
+    <select id="selectTodaySchedule" resultType="java.util.Map">
+        SELECT ds.id,
+               ds.doctor_name AS doctorName,
+               ds.schedule_date AS scheduleDate,
+               ds.shift,
+               CASE ds.shift WHEN 0 THEN '上午' WHEN 1 THEN '下午' WHEN 2 THEN '全天' ELSE '未知' END AS shiftName,
+               ds.max_patients AS maxPatients,
+               ds.current_patients AS currentPatients,
+               ds.status,
+               CASE ds.status WHEN 0 THEN '可挂号' WHEN 1 THEN '已满' WHEN 2 THEN '停诊' ELSE '未知' END AS statusName,
+               ds.department,
+               ds.remark
+        FROM doctor_schedule ds
+        WHERE ds.doctor_id = #{doctorId}
+          AND ds.schedule_date = CURDATE()
+        ORDER BY ds.shift ASC
+    </select>
+
+    <!-- 我的直播列表 -->
+    <select id="selectMyLiveList" resultType="java.util.Map">
+        SELECT l.id,
+               l.name AS title,
+               l.cover_img AS coverImg,
+               l.status,
+               l.start_time AS startTime,
+               l.end_time AS endTime,
+               l.view_count AS viewCount,
+               l.like_count AS likeCount,
+               CASE WHEN l.status = 0 THEN '未开始'
+                    WHEN l.status = 1 THEN '直播中'
+                    WHEN l.status = 2 THEN '已结束' ELSE '未知' END AS statusName
+        FROM fs_live l
+        WHERE l.doctor_id = #{doctorId}
+        ORDER BY l.create_time DESC
+    </select>
+
+    <!-- 本月问诊排行榜(医生本人排名信息) -->
+    <select id="selectConsultRank" resultType="java.util.Map">
+        SELECT COUNT(DISTINCT t.doctor_id) AS totalDoctors,
+               (SELECT COUNT(1) + 1 FROM fs_inquiry_order WHERE doctor_id != #{doctorId}
+                 AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+                 GROUP BY doctor_id HAVING COUNT(1) > (
+                  SELECT COUNT(1) FROM fs_inquiry_order WHERE doctor_id = #{doctorId}
+                   AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+                 ) LIMIT 1) AS rank
+        FROM fs_inquiry_order
+        WHERE DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+    </select>
+
+</mapper>

+ 6 - 1
java/fs-service/src/main/resources/mapper/hisStore/FsStoreCartScrmMapper.xml

@@ -17,11 +17,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="isDel"    column="is_del"    />
         <result property="isBuy"    column="is_buy"    />
         <result property="changePrice"    column="change_price"    />
+        <result property="storeId"    column="store_id"    />
     </resultMap>
 
     <sql id="selectFsStoreCartVo">
         select id, user_id, type, product_id, product_attr_value_id, cart_num, create_time, update_time, is_pay, is_del,
-               is_buy,change_price from fs_store_cart_scrm
+               is_buy,change_price,store_id from fs_store_cart_scrm
     </sql>
 
     <select id="selectFsStoreCartList" resultMap="FsStoreCartResult">
@@ -36,6 +37,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null "> and is_del = #{isDel}</if>
             <if test="isBuy != null "> and is_buy = #{isBuy}</if>
             <if test="changePrice != null "> and change_price = #{changePrice}</if>
+            <if test="storeId != null "> and store_id = #{storeId}</if>
         </where>
     </select>
 
@@ -58,6 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null">is_del,</if>
             <if test="isBuy != null">is_buy,</if>
             <if test="changePrice != null">change_price,</if>
+            <if test="storeId != null">store_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null">#{userId},</if>
@@ -71,6 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null">#{isDel},</if>
             <if test="isBuy != null">#{isBuy},</if>
             <if test="changePrice != null">#{changePrice},</if>
+            <if test="storeId != null">#{storeId},</if>
          </trim>
     </insert>
 
@@ -88,6 +92,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null">is_del = #{isDel},</if>
             <if test="isBuy != null">is_buy = #{isBuy},</if>
             <if test="changePrice != null">change_price = #{changePrice},</if>
+            <if test="storeId != null">store_id = #{storeId},</if>
         </trim>
         where id = #{id}
     </update>

+ 69 - 0
java/fs-service/src/main/resources/mapper/hisStore/FsStoreFavoriteScrmMapper.xml

@@ -0,0 +1,69 @@
+<?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.hisStore.mapper.FsStoreFavoriteScrmMapper">
+
+    <resultMap type="FsStoreFavoriteScrm" id="FsStoreFavoriteResult">
+        <result property="favoriteId"    column="favorite_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="storeId"    column="store_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsStoreFavoriteVo">
+        select favorite_id, user_id, store_id, create_time, update_time from fs_store_favorite_scrm
+    </sql>
+
+    <select id="selectFsStoreFavoriteList" resultMap="FsStoreFavoriteResult">
+        <include refid="selectFsStoreFavoriteVo"/>
+        <where>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="storeId != null "> and store_id = #{storeId}</if>
+        </where>
+    </select>
+
+    <select id="selectFsStoreFavoriteById" resultMap="FsStoreFavoriteResult">
+        <include refid="selectFsStoreFavoriteVo"/>
+        where favorite_id = #{favoriteId}
+    </select>
+
+    <insert id="insertFsStoreFavorite" useGeneratedKeys="true" keyProperty="favoriteId">
+        insert into fs_store_favorite_scrm
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="storeId != null">store_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="storeId != null">#{storeId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsStoreFavorite">
+        update fs_store_favorite_scrm
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="storeId != null">store_id = #{storeId},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where favorite_id = #{favoriteId}
+    </update>
+
+    <delete id="deleteFsStoreFavoriteById">
+        delete from fs_store_favorite_scrm where favorite_id = #{favoriteId}
+    </delete>
+
+    <delete id="deleteFsStoreFavoriteByIds">
+        delete from fs_store_favorite_scrm where favorite_id in
+        <foreach item="favoriteId" collection="array" open="(" separator="," close=")">
+            #{favoriteId}
+        </foreach>
+    </delete>
+
+</mapper>

+ 54 - 0
java/fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -2279,4 +2279,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <!-- ==================== 合并多店铺下单 ==================== -->
+
+    <!-- 根据购物车ID列表查询购物车信息(含店铺ID和店铺名称) -->
+    <select id="selectCartByCartIds" resultType="com.fs.hisStore.vo.FsStoreCartStoreVO">
+        SELECT
+            c.id,
+            c.product_id           AS productId,
+            c.cart_num             AS cartNum,
+            p.store_id             AS storeId,
+            s.store_name           AS storeName,
+            p.product_name         AS productName,
+            p.image                AS productImage,
+            p.price                AS price,
+            p.stock                AS stock,
+            p.product_attr_name    AS productAttrName
+        FROM fs_store_cart_scrm c
+        INNER JOIN fs_store_product_scrm p ON c.product_id = p.product_id
+        LEFT JOIN fs_store_scrm s ON p.store_id = s.store_id
+        WHERE c.id IN
+        <foreach collection="cartIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        AND c.is_del = 0
+    </select>
+
+    <!-- 根据购物车ID列表按店铺分组查询(带店铺名的聚合视图) -->
+    <resultMap id="StoreCartGroupResult" type="java.util.HashMap"/>
+    <select id="selectCartGroupByStore" resultType="java.util.HashMap">
+        SELECT
+            p.store_id              AS storeId,
+            s.store_name            AS storeName,
+            COUNT(1)                AS itemCount,
+            SUM(c.cart_num * p.price) AS totalAmount
+        FROM fs_store_cart_scrm c
+        INNER JOIN fs_store_product_scrm p ON c.product_id = p.product_id
+        LEFT JOIN fs_store_scrm s ON p.store_id = s.store_id
+        WHERE c.id IN
+        <foreach collection="cartIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        AND c.is_del = 0
+        GROUP BY p.store_id, s.store_name
+    </select>
+
+    <!-- 根据订单ID更新订单退款状态 -->
+    <update id="updateRefundStatus">
+        UPDATE fs_store_order_scrm
+        SET status          = #{status},
+            refund_status   = #{refundStatus},
+            refund_price    = #{refundPrice},
+            refund_time     = NOW()
+        WHERE id = #{orderId}
+    </update>
+
 </mapper>

+ 10 - 2
java/fs-service/src/main/resources/mapper/hisStore/FsStoreScrmMapper.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" ?>
+<?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">
@@ -59,6 +59,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="medicalLicense"	column="medical_license" />
         <result property="medicalLicenseExpiryStart"	column="medical_license_expiry_start" />
         <result property="medicalLicenseExpiryEnd"	column="medical_license_expiry_end" />
+        <result property="openingHours"	column="business_hours" />
+        <result property="storeScore"	column="rating" />
     </resultMap>
 
     <sql id="selectFsStoreVo">
@@ -70,7 +72,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                drug_license_expiry_start,drug_license_expiry_end,medical_device1,medical_device1_expiry_start,medical_device1_expiry_end,
                medicalDevice2,medical_device2_expiry_start,medical_device2_expiry_end,medicalDevice3,medical_device3_expiry_start,
                medical_device3_expiry_end,food_license,food_license_expiry_start,food_license_expiry_end,medical_license,
-               medical_license_expiry_start,medical_license_expiry_end
+               medical_license_expiry_start,medical_license_expiry_end,opening_hours,store_score
         from fs_store_scrm
     </sql>
 
@@ -148,6 +150,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="medicalLicense !=null">medical_license , </if>
             <if test="medicalLicenseExpiryStart !=null ">medical_license_expiry_start , </if>
             <if test="medicalLicenseExpiryEnd !=null ">medical_license_expiry_end , </if>
+            <if test="openingHours !=null and openingHours !=''">business_hours , </if>
+            <if test="storeScore !=null">rating , </if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="cityIds != null">#{cityIds},</if>
@@ -203,6 +207,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="medicalLicense !=null">#{medicalLicense}, </if>
             <if test="medicalLicenseExpiryStart !=null ">#{medicalLicenseExpiryStart}, </if>
             <if test="medicalLicenseExpiryEnd !=null ">#{medicalLicenseExpiryEnd}, </if>
+            <if test="openingHours !=null and openingHours !=''">#{openingHours}, </if>
+            <if test="storeScore !=null">#{storeScore}, </if>
         </trim>
     </insert>
 
@@ -262,6 +268,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="medicalLicense !=null and medicalLicense !=''"> medical_license = #{medicalLicense} ,</if>
             <if test="medicalLicenseExpiryStart !=null "> medical_license_expiry_start = #{medicalLicenseExpiryStart} ,</if>
             <if test="medicalLicenseExpiryEnd !=null "> medical_license_expiry_end = #{medicalLicenseExpiryEnd} ,</if>
+            <if test="openingHours !=null"> business_hours = #{openingHours} ,</if>
+            <if test="storeScore !=null"> rating = #{storeScore} ,</if>
         </trim>
         where store_id = #{storeId}
     </update>

+ 91 - 1
java/fs-user-app/src/main/java/com/fs/app/controller/StoreAfterSalesController.java

@@ -15,7 +15,17 @@ import com.fs.his.service.IFsStoreOrderItemService;
 import com.fs.his.service.IFsStoreOrderService;
 import com.fs.his.vo.FsStoreAfterSalesListUVO;
 import com.fs.his.vo.FsStoreOrderItemListUVO;
-import com.fs.his.vo.FsStoreOrderItemVO;
+import com.fs.hisStore.domain.FsStoreAfterSalesItemScrm;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.param.FsStoreAfterSalesParam;
+import com.fs.hisStore.param.FsStoreAfterSalesQueryParam;
+import com.fs.hisStore.service.IFsStoreAfterSalesItemScrmService;
+import com.fs.hisStore.service.IFsStoreAfterSalesScrmService;
+import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.vo.FsStoreAfterSalesQueryVO;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -43,6 +53,18 @@ public class StoreAfterSalesController extends  AppBaseController {
     private IFsStoreOrderItemService itemService;
     @Autowired
     private IFsStoreOrderService orderService;
+
+    @Autowired
+    private IFsStoreAfterSalesScrmService storeAfterSalesScrmService;
+
+    @Autowired
+    private IFsStoreAfterSalesItemScrmService salesItemScrmService;
+
+    @Autowired
+    private IFsStoreOrderScrmService orderScrmService;
+
+    @Autowired
+    private IFsStoreOrderItemScrmService orderItemScrmService;
     @Login
     @ApiOperation("获取订单项列表")
     @GetMapping("/getStoreOrderItems")
@@ -54,6 +76,17 @@ public class StoreAfterSalesController extends  AppBaseController {
         return R.ok().put("order", order).put("items",list);
     }
 
+    @Login
+    @ApiOperation("获取订单项列表(SCRM)")
+    @GetMapping("/getMyStoreOrderItemByOrderId")
+    public R getMyStoreOrderItemByOrderId(@RequestParam("orderId") Long orderId, HttpServletRequest request){
+        List<FsStoreOrderItemVO> list = orderItemScrmService.selectFsStoreOrderItemListByOrderId(orderId);
+        FsStoreOrderScrm order = orderScrmService.selectFsStoreOrderById(orderId);
+        order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
+        order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
+        return R.ok().put("order", order).put("items", list);
+    }
+
 
     @Login
     @PostMapping("/applyAfterSales")
@@ -62,6 +95,14 @@ public class StoreAfterSalesController extends  AppBaseController {
     public R applyAfterSales(@RequestBody FsStoreAfterSalesApplyParam param) {
         return storeAfterSalesService.applyAfterSales(Long.parseLong(getUserId()), param);
     }
+
+    @Login
+    @PostMapping("/applyAfterSalesScrm")
+    @ApiOperation(value = "申请售后(SCRM订单)", notes = "针对fs_store_order_scrm订单的售后申请")
+    @RepeatSubmit
+    public R applyAfterSalesScrm(@RequestBody FsStoreAfterSalesParam param) {
+        return storeAfterSalesScrmService.applyForAfterSales(Long.parseLong(getUserId()), param);
+    }
     @Login
     @PostMapping("/revoke")
     @ApiOperation(value = "撤销售后", notes = "撤销售后")
@@ -77,6 +118,22 @@ public class StoreAfterSalesController extends  AppBaseController {
         return storeAfterSalesService.addDelivery(param);
     }
 
+    @Login
+    @PostMapping("/revokeScrm")
+    @ApiOperation(value = "撤销售后(SCRM)", notes = "针对fs_store_after_sales_scrm的撤销售后")
+    @RepeatSubmit
+    public R revokeScrm(@RequestBody com.fs.hisStore.param.FsStoreAfterSalesRevokeParam param) throws ParseException {
+        return storeAfterSalesScrmService.revoke(Long.parseLong(getUserId()), param.getSalesId());
+    }
+
+    @Login
+    @PostMapping("/addDeliveryScrm")
+    @ApiOperation(value = "提交物流信息(SCRM)", notes = "针对fs_store_after_sales_scrm的物流提交")
+    public R addDeliveryScrm(@Validated @RequestBody com.fs.hisStore.param.FsStoreAfterSalesDeliveryParam param) {
+        param.setUserId(Long.parseLong(getUserId()));
+        return storeAfterSalesScrmService.addDelivery(param);
+    }
+
 
     @Login
     @GetMapping("/getStoreAfterSalesList")
@@ -88,6 +145,17 @@ public class StoreAfterSalesController extends  AppBaseController {
         PageInfo<FsStoreAfterSalesListUVO> listPageInfo=new PageInfo<>(list);
         return R.ok().put("data",listPageInfo);
     }
+
+    @Login
+    @GetMapping("/getStoreAfterSalesScrmList")
+    @ApiOperation(value = "获取售后列表(SCRM)", notes = "查fs_store_after_sales_scrm表")
+    public R getStoreAfterSalesScrmList(FsStoreAfterSalesQueryParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setUserId(Long.parseLong(getUserId()));
+        List<FsStoreAfterSalesQueryVO> list = storeAfterSalesScrmService.selectFsStoreAfterSalesListQuery(param);
+        PageInfo<FsStoreAfterSalesQueryVO> listPageInfo = new PageInfo<>(list);
+        return R.ok().put("data", listPageInfo);
+    }
     @Login
     @GetMapping("/getStoreAfterSalesById")
     @ApiOperation(value = "获取售后详情", notes = "获取售后详情")
@@ -100,4 +168,26 @@ public class StoreAfterSalesController extends  AppBaseController {
         return R.ok().put("sales",sales).put("items", items).put("order",order);
     }
 
+    @Login
+    @GetMapping("/getAfterSalesDetails")
+    @ApiOperation(value = "获取售后详情(含订单)", notes = "查fs_store_after_sales表")
+    public R getAfterSalesDetails(@RequestParam("salesId") Long id) {
+        FsStoreAfterSales sales = storeAfterSalesService.selectFsStoreAfterSalesById(id);
+        FsStoreAfterSalesItem map = new FsStoreAfterSalesItem();
+        map.setAfterSalesId(id);
+        List<FsStoreAfterSalesItem> items = salesItemService.selectFsStoreAfterSalesItemList(map);
+        return R.ok().put("sales", sales).put("items", items);
+    }
+
+    @Login
+    @GetMapping("/getAfterSalesDetailsScrm")
+    @ApiOperation(value = "获取售后详情(SCRM)", notes = "查fs_store_after_sales_scrm表")
+    public R getAfterSalesDetailsScrm(@RequestParam("salesId") Long id) {
+        FsStoreAfterSalesScrm sales = storeAfterSalesScrmService.selectFsStoreAfterSalesById(id);
+        FsStoreAfterSalesItemScrm map = new FsStoreAfterSalesItemScrm();
+        map.setStoreAfterSalesId(id);
+        List<FsStoreAfterSalesItemScrm> items = salesItemScrmService.selectFsStoreAfterSalesItemList(map);
+        return R.ok().put("sales", sales).put("items", items);
+    }
+
 }

+ 15 - 0
java/fs-user-app/src/main/java/com/fs/app/controller/StoreOrderController.java

@@ -16,6 +16,7 @@ import com.fs.hisStore.param.FsStoreOrderCancelParam;
 import com.fs.hisStore.param.FsStoreOrderComputedParam;
 import com.fs.hisStore.param.FsStoreOrderCreateParam;
 import com.fs.hisStore.param.FsStoreOrderPayParam;
+import com.fs.hisStore.service.IFsStoreAfterSalesScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStoreProductPackageScrmService;
 import com.fs.hisStore.vo.FsMyStoreOrderListQueryVO;
@@ -64,6 +65,9 @@ public class StoreOrderController extends AppBaseController {
     @Autowired
     private IFsStoreProductPackageScrmService productPackageService;
 
+    @Autowired
+    private IFsStoreAfterSalesScrmService afterSalesService;
+
     @Autowired
     private ISysConfigService configService;
 
@@ -222,4 +226,15 @@ public class StoreOrderController extends AppBaseController {
         }
         return result;
     }
+
+    @Login
+    @ApiOperation("获取订单数量统计")
+    @GetMapping("/getOrderCount")
+    public R getOrderCount(HttpServletRequest request) {
+        Integer count0 = orderService.selectFsStoreOrderCount(Long.parseLong(getUserId()), 0);
+        Integer count1 = orderService.selectFsStoreOrderCount(Long.parseLong(getUserId()), 1);
+        Integer count2 = orderService.selectFsStoreOrderCount(Long.parseLong(getUserId()), 2);
+        Integer afterSalesCount = afterSalesService.selectFsStoreAfterSalesCount(Long.parseLong(getUserId()), 0);
+        return R.ok().put("count0", count0).put("count1", count1).put("count2", count2).put("afterSalesCount", afterSalesCount);
+    }
 }

+ 9 - 0
java/fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -22,6 +22,7 @@ import com.fs.his.service.IFsDoctorService;
 import com.fs.his.service.IFsPackageService;
 import com.fs.his.service.IFsUserCouponService;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.param.FsStoreProductFootDelParam;
 import com.fs.hisStore.param.FsStoreProductRelationQueryParam;
 import com.fs.hisStore.service.IFsStoreProductRelationScrmService;
 import com.fs.hisStore.vo.FsStoreProductRelationQueryVO;
@@ -382,4 +383,12 @@ public class UserController extends  AppBaseController {
         PageInfo<FsStoreProductRelationQueryVO> listPageInfo = new PageInfo<>(list);
         return R.ok().put("data", listPageInfo);
     }
+
+    @Login
+    @ApiOperation("删除商品足迹")
+    @PostMapping("/delProductFoots")
+    public R delProductFoots(@RequestBody FsStoreProductFootDelParam param, HttpServletRequest request) {
+        productRelationScrmService.deleteFsStoreProductRelationById(param.getId());
+        return R.ok("操作成功");
+    }
 }

+ 161 - 3
java/fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -10,12 +10,15 @@ import com.fs.erp.service.IErpGoodsService;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.hisStore.vo.FsStoreCartGroupVO;
+import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 import com.fs.hisStore.vo.FsStoreProductAttrValueQueryVO;
 import com.fs.hisStore.vo.FsStoreProductListQueryVO;
 import com.fs.hisStore.vo.FsStoreProductQueryVO;
 import com.fs.hisStore.param.CartPurchaseLimitCheckParam;
 import com.fs.hisStore.domain.FsStoreOrderItemScrm;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
@@ -30,6 +33,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletRequest;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 
 @Api("商品中心")
@@ -65,6 +69,46 @@ public class ProductScrmController extends AppBaseController {
     
     @Autowired
     private IFsStoreOrderItemScrmService orderItemService;
+
+    // ======================== 购物车缓存(多租户隔离) ========================
+    private static final int CART_CACHE_TTL = 300;  // 缓存 5 分钟
+
+    /**
+     * 获取当前租户ID(从动态数据源上下文解析)
+     * 多租户模式下返回 "tenantId:",单租户模式返回空串
+     */
+    private String getTenantKeyPrefix() {
+        Long tenantId = DynamicDataSourceContextHolder.getCurrentTenantId();
+        return tenantId != null ? tenantId + ":" : "";
+    }
+
+    /** 构建多店铺缓存 key: cart:{tenantId}:store:all:{userId} */
+    private String buildAllCartKey(Long userId) {
+        return "cart:" + getTenantKeyPrefix() + "store:all:" + userId;
+    }
+
+    /** 构建单店铺缓存 key: cart:{tenantId}:store:{storeId}:{userId} */
+    private String buildStoreCartKey(Long storeId, Long userId) {
+        return "cart:" + getTenantKeyPrefix() + "store:" + storeId + ":" + userId;
+    }
+
+    /** 清除指定用户的所有购物车缓存 */
+    private void clearCartCache(Long userId) {
+        redisCache.deleteObject(buildAllCartKey(userId));
+    }
+
+    /** 清除指定用户+店铺的购物车缓存 */
+    private void clearCartCacheByStore(Long userId, Long storeId) {
+        redisCache.deleteObject(buildStoreCartKey(storeId, userId));
+    }
+
+    /** 清除用户全量 + 指定店铺缓存 */
+    private void clearCartCacheFull(Long userId, Long storeId) {
+        clearCartCache(userId);
+        if (storeId != null) {
+            clearCartCacheByStore(userId, storeId);
+        }
+    }
     /**
      * 获取用户信息
      * @param storeId
@@ -108,6 +152,33 @@ public class ProductScrmController extends AppBaseController {
         PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(productList);
         return R.ok().put("data",listPageInfo);
     }
+    @ApiOperation("获取商品列表(含分类名称)")
+    @GetMapping("/getProductsWithCate")
+    public R getProductsWithCate(FsStoreProductQueryParam param, HttpServletRequest request){
+        PageHelper.startPage(param.getPage(), param.getPageSize());
+        param.setIsDisplay(1);
+        List<FsStoreProductListQueryVO> productList=productService.selectFsStoreProductListQuery(param);
+        PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(productList);
+        // 收集所有 cateId,批量查询分类名称
+        java.util.Set<Long> cateIds = productList.stream()
+                .map(FsStoreProductListQueryVO::getCateId)
+                .filter(id -> id != null)
+                .collect(java.util.stream.Collectors.toSet());
+        java.util.Map<Long, String> cateNameMap = new HashMap<>();
+        for (Long cateId : cateIds) {
+            FsStoreProductCategoryScrm category = categoryService.selectFsStoreProductCategoryById(cateId);
+            if (category != null && StringUtils.isNotEmpty(category.getCateName())) {
+                cateNameMap.put(cateId, category.getCateName());
+            }
+        }
+        // 回填分类名称到每个商品
+        for (FsStoreProductListQueryVO vo : productList) {
+            if (vo.getCateId() != null) {
+                vo.setCateName(cateNameMap.get(vo.getCateId()));
+            }
+        }
+        return R.ok().put("data",listPageInfo);
+    }
     @ApiOperation("获取商品详情")
     @GetMapping("/getProductDetails")
     public R getProductDetails(@RequestParam(value="productId") Long productId,String storeId){
@@ -295,7 +366,26 @@ public class ProductScrmController extends AppBaseController {
     @ApiOperation("添加购物车")
     @PostMapping("/addCart")
     public R addCart(@Validated @RequestBody FsStoreCartParam cartParam, HttpServletRequest request){
-        return cartService.addCart(Long.parseLong(getUserId()),cartParam);
+        R result = cartService.addCart(Long.parseLong(getUserId()), cartParam);
+        // 清理缓存:根据 productId 查 storeId 做精准清除
+        Long storeId = null;
+        try {
+            FsStoreProductScrm product = productService.selectFsStoreProductById(cartParam.getProductId());
+            if (product != null) {
+                storeId = product.getStoreId();
+            }
+        } catch (Exception ignored) {}
+        clearCartCacheFull(Long.parseLong(getUserId()), storeId);
+        return result;
+    }
+    @Login
+    @ApiOperation("多店铺添加购物车")
+    @PostMapping("/addCartByStore")
+    public R addCartByStore(@Validated @RequestBody FsStoreCartParam cartParam, HttpServletRequest request){
+        R result = cartService.addCartByStore(Long.parseLong(getUserId()), cartParam);
+        // 使用请求参数中的 storeId 清理缓存
+        clearCartCacheFull(Long.parseLong(getUserId()), cartParam.getStoreId());
+        return result;
     }
     @Login
     @ApiOperation("获取购物车列表")
@@ -305,16 +395,84 @@ public class ProductScrmController extends AppBaseController {
         return R.ok().put("carts",carts);
     }
     @Login
+    @ApiOperation("获取多店铺购物车列表(按店铺分组)")
+    @GetMapping("/getCartsWithStore")
+    public R getCartsWithStore(HttpServletRequest request){
+        Long userId = Long.parseLong(getUserId());
+        String cacheKey = buildAllCartKey(userId);
+        @SuppressWarnings("unchecked")
+        List<FsStoreCartStoreVO> carts = redisCache.getCacheObject(cacheKey);
+        if (carts == null) {
+            carts = cartService.selectFsStoreCartStoreListByUid(userId);
+            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
+        }
+        // 按 storeId 分组
+        Map<Long, List<FsStoreCartStoreVO>> groupedMap = carts.stream()
+                .collect(java.util.stream.Collectors.groupingBy(
+                        c -> c.getStoreId() != null ? c.getStoreId() : 0L,
+                        java.util.LinkedHashMap::new,
+                        java.util.stream.Collectors.toList()
+                ));
+        List<FsStoreCartGroupVO> result = new ArrayList<>();
+        for (Map.Entry<Long, List<FsStoreCartStoreVO>> entry : groupedMap.entrySet()) {
+            FsStoreCartGroupVO group = new FsStoreCartGroupVO();
+            group.setStoreId(entry.getKey());
+            List<FsStoreCartStoreVO> storeCarts = entry.getValue();
+            group.setStoreName(storeCarts.get(0).getStoreName());
+            group.setCarts(new ArrayList<>(storeCarts));
+            result.add(group);
+        }
+        return R.ok().put("carts", result);
+    }
+    @Login
+    @ApiOperation("查询指定店铺的购物车")
+    @GetMapping("/getCartsByStore")
+    public R getCartsByStore(@RequestParam("storeId") Long storeId, HttpServletRequest request){
+        Long userId = Long.parseLong(getUserId());
+        String cacheKey = buildStoreCartKey(storeId, userId);
+        @SuppressWarnings("unchecked")
+        List<FsStoreCartStoreVO> carts = redisCache.getCacheObject(cacheKey);
+        if (carts == null) {
+            carts = cartService.selectFsStoreCartListByUidAndStoreId(userId, storeId);
+            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
+        }
+        return R.ok().put("carts", carts);
+    }
+    @Login
     @ApiOperation("改变购物车数量")
     @PostMapping("/cartNum")
     public R cartNum(@Validated @RequestBody FsStoreCartNumParam cartParam, HttpServletRequest request){
-        return cartService.changeNum(Long.parseLong(getUserId()),cartParam);
+        R result = cartService.changeNum(Long.parseLong(getUserId()), cartParam);
+        // 清理多店铺缓存(单店铺缓存 TTL 自然过期)
+        clearCartCache(Long.parseLong(getUserId()));
+        return result;
+    }
+    @Login
+    @ApiOperation("多店铺改变购物车数量")
+    @PostMapping("/cartNumByStore")
+    public R cartNumByStore(@Validated @RequestBody FsStoreCartNumParam cartParam, HttpServletRequest request){
+        R result = cartService.changeNumByStore(Long.parseLong(getUserId()), cartParam);
+        // 使用请求参数中的 storeId 清理缓存
+        clearCartCacheFull(Long.parseLong(getUserId()), cartParam.getStoreId());
+        return result;
     }
     @Login
     @ApiOperation("删除购物车")
     @PostMapping("/delCart")
     public R delCart(@Validated @RequestBody FsStoreCartDelParam cartParam, HttpServletRequest request){
-        return cartService.delCart(Long.parseLong(getUserId()),cartParam);
+        R result = cartService.delCart(Long.parseLong(getUserId()), cartParam);
+        // 清理多店铺缓存(单店铺缓存 TTL 自然过期)
+        clearCartCache(Long.parseLong(getUserId()));
+        return result;
+    }
+    @Login
+    @ApiOperation("多店铺删除购物车")
+    @PostMapping("/delCartByStore")
+    public R delCartByStore(@Validated @RequestBody FsStoreCartDelParam cartParam, HttpServletRequest request){
+        R result = cartService.delCartByStore(Long.parseLong(getUserId()), cartParam);
+        // 使用请求参数中的 storeId 清理缓存
+        clearCartCacheFull(Long.parseLong(getUserId()), cartParam.getStoreId());
+        return result;
     }
 
 

+ 12 - 0
java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreAfterSalesScrmController.java

@@ -36,6 +36,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletRequest;
 import java.text.ParseException;
 import java.util.List;
+import java.util.Map;
 
 
 @Api("售后接口")
@@ -121,4 +122,15 @@ public class StoreAfterSalesScrmController extends AppBaseController {
         return R.ok().put("sales",sales).put("items", items).put("order",order);
     }
 
+    @Login
+    @PostMapping("/fullRefund")
+    @ApiOperation(value = "整单退款", notes = "对指定订单进行整单退款")
+    public R fullRefund(@RequestBody Map<String, Long> param) {
+        Long orderId = param.get("orderId");
+        if (orderId == null) {
+            return R.error("订单ID不能为空");
+        }
+        return orderService.fullOrderRefund(orderId);
+    }
+
 }

+ 87 - 0
java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreFavoriteScrmController.java

@@ -0,0 +1,87 @@
+package com.fs.app.controller.store;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.hisStore.service.IFsStoreFavoriteScrmService;
+import com.fs.hisStore.vo.FsStoreFavoriteVO;
+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 javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 用户店铺收藏Controller
+ *
+ * @author fs
+ * @date 2026-08-06
+ */
+@Api("店铺收藏")
+@RestController
+@RequestMapping(value = "/store/app/storeFavorite")
+public class StoreFavoriteScrmController extends AppBaseController
+{
+    @Autowired
+    private IFsStoreFavoriteScrmService favoriteService;
+
+    // ======================== 收藏列表缓存(多租户隔离) ========================
+    private static final int FAVORITE_CACHE_TTL = 300;
+
+    private String getTenantKeyPrefix() {
+        Long tenantId = DynamicDataSourceContextHolder.getCurrentTenantId();
+        return tenantId != null ? tenantId + ":" : "";
+    }
+
+    private String buildFavoriteListKey(Long userId) {
+        return "storeFavorite:" + getTenantKeyPrefix() + "list:" + userId;
+    }
+
+    private void clearFavoriteCache(Long userId) {
+        redisCache.deleteObject(buildFavoriteListKey(userId));
+    }
+
+    @Login
+    @ApiOperation("收藏/取消收藏店铺")
+    @PostMapping("/toggle")
+    public R toggleFavorite(@RequestParam("storeId") Long storeId, HttpServletRequest request) {
+        Long userId = Long.parseLong(getUserId());
+        R result = favoriteService.toggleFavorite(userId, storeId);
+        clearFavoriteCache(userId);
+        return result;
+    }
+
+    @Login
+    @ApiOperation("检查是否已收藏店铺")
+    @GetMapping("/check")
+    public R checkFavorite(@RequestParam("storeId") Long storeId, HttpServletRequest request) {
+        Long userId = Long.parseLong(getUserId());
+        return favoriteService.checkFavorite(storeId, userId);
+    }
+
+    @Login
+    @ApiOperation("获取收藏店铺列表")
+    @GetMapping("/list")
+    public R getFavoriteList(@RequestParam(value = "page", defaultValue = "1") Integer page,
+                             @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
+                             HttpServletRequest request) {
+        Long userId = Long.parseLong(getUserId());
+        String cacheKey = buildFavoriteListKey(userId);
+        // 分页数据不适合直接缓存 PageInfo,缓存全量列表
+        @SuppressWarnings("unchecked")
+        List<FsStoreFavoriteVO> list = redisCache.getCacheObject(cacheKey);
+        if (list == null) {
+            PageHelper.startPage(page, pageSize);
+            list = favoriteService.selectStoreFavoriteList(userId);
+            redisCache.setCacheObject(cacheKey, list, FAVORITE_CACHE_TTL, TimeUnit.SECONDS);
+        }
+        PageInfo<FsStoreFavoriteVO> pageInfo = new PageInfo<>(list);
+        return R.ok().put("data", pageInfo);
+    }
+}

+ 9 - 0
java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -216,6 +216,15 @@ public class StoreOrderScrmController extends AppBaseController {
         return orderService.createOrder(Long.parseLong(getUserId()),param);
     }
 
+    @Login
+    @ApiOperation("合并多店铺下单")
+    @PostMapping("/createMultiStore")
+    public R createMultiStore(@Validated @RequestBody FsStoreMultiStoreOrderCreateParam param, HttpServletRequest request){
+        String userId = getUserId();
+        log.info("开始合并多店铺下单, 用户ID: {}, 店铺数: {}", userId, param.getStoreCarts().size());
+        return orderService.createMultiStoreOrder(Long.parseLong(userId), param);
+    }
+
     @Login
     @ApiOperation("支付")
     @PostMapping("/pay")

+ 124 - 1
java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreScrmController.java

@@ -1,6 +1,7 @@
 package com.fs.app.controller.store;
 
 import com.fs.app.annotation.Login;
+import com.fs.app.utils.JwtUtils;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -8,17 +9,25 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.param.FsStoreAuditParam;
 import com.fs.hisStore.domain.FsStoreScrm;
+import com.fs.hisStore.service.IFsStoreFavoriteScrmService;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
 import com.fs.hisStore.service.IFsStoreScrmService;
 import com.fs.hisStore.utils.StoreAuditLogUtil;
+import com.fs.hisStore.vo.FsStoreProductListQueryVO;
 import com.fs.hisStore.vo.FsStoreRecommendListVO;
+import com.fs.hisStore.vo.FsStoreScrmVO;
+import com.fs.hisStore.vo.StoreScrmTopVo;
+import io.jsonwebtoken.Claims;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
-
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -34,9 +43,30 @@ public class StoreScrmController extends BaseController
     @Autowired
     private IFsStoreScrmService fsStoreService;
 
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductService;
+
     @Autowired
     private StoreAuditLogUtil storeAuditLogUtil;
 
+    @Autowired
+    private IFsStoreFavoriteScrmService favoriteService;
+
+    @Autowired
+    private JwtUtils jwtUtils;
+
+    /** 获取当前登录用户ID(未登录返回null) */
+    private Long getCurrentUserId() {
+        String headValue = ServletUtils.getRequest().getHeader("APPToken");
+        if (StringUtils.isNotEmpty(headValue)) {
+            Claims claims = jwtUtils.getClaimByToken(headValue);
+            if (claims != null) {
+                return Long.parseLong(claims.getSubject());
+            }
+        }
+        return null;
+    }
+
 
     /**
      * 查询店铺管理列表
@@ -52,6 +82,34 @@ public class StoreScrmController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询店铺列表(每个店铺携带Top3热门商品)
+     */
+    @GetMapping("/listWithTopProducts")
+    public TableDataInfo listWithTopProducts(FsStoreScrm fsStore)
+    {
+        startPage();
+        List<FsStoreScrm> storeList = fsStoreService.selectFsStoreList(fsStore);
+        Long userId = getCurrentUserId();
+        List<StoreScrmTopVo> voList = new ArrayList<>();
+        for (FsStoreScrm store : storeList) {
+            store.setPhone(ParseUtils.parsePhone(store.getPhone()));
+            StoreScrmTopVo vo = new StoreScrmTopVo();
+            vo.setStore(store);
+            List<FsStoreProductListQueryVO> hotProducts = fsStoreProductService.selectHotProductsByStoreId(store.getStoreId(), 3);
+            vo.setTopProducts(hotProducts);
+            // 检查是否已收藏
+            if (userId != null) {
+                int count = favoriteService.checkFavoriteCount(store.getStoreId(), userId);
+                vo.setIsFavorite(count > 0 ? 1 : 0);
+            } else {
+                vo.setIsFavorite(0);
+            }
+            voList.add(vo);
+        }
+        return getDataTable(voList);
+    }
+
     /**
      * 获取店铺管理详细信息
      */
@@ -62,6 +120,13 @@ public class StoreScrmController extends BaseController
         FsStoreScrm fsStore = fsStoreService.selectFsStoreByStoreId(storeId);
         if(fsStore != null && fsStore.getPhone() != null)
             fsStore.setPhone(ParseUtils.parsePhone(fsStore.getPhone()));
+        // 检查是否已收藏
+        Integer isFavorite = 0;
+        Long userId = getCurrentUserId();
+        if (userId != null) {
+            int count = favoriteService.checkFavoriteCount(storeId, userId);
+            fsStore.setIsFavorite(count > 0 ? 1 : 0);
+        }
         return AjaxResult.success(fsStore);
     }
 
@@ -123,4 +188,62 @@ public class StoreScrmController extends BaseController
         List<FsStoreRecommendListVO> fsStoreRecommendListVO = fsStoreService.storeRecommendList();
         return getDataTable(fsStoreRecommendListVO);
     }
+
+    /**
+     * 导出店铺管理列表
+     */
+    @Log(title = "店铺管理", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreScrm fsStore  )
+    {
+        List<FsStoreScrm> list = fsStoreService.selectFsStoreList(fsStore);
+        ExcelUtil<FsStoreScrm> util = new ExcelUtil<FsStoreScrm>(FsStoreScrm.class);
+        return util.exportExcel( list, "店铺管理数据");
+    }
+
+    /**
+     * 修改店铺Logo
+     */
+    @Login
+    @Log(title = "店铺管理", businessType = BusinessType.UPDATE, logParam = {"店铺", "修改店铺Logo"}, isStoreLog = true)
+    @PutMapping("/editLogo")
+    public AjaxResult editLogo(@RequestBody FsStoreScrm fsStore)
+    {
+        return toAjax(fsStoreService.updateFsStoreLogo(fsStore));
+    }
+
+    /**
+     * 修改店铺基础信息(多店铺管理)
+     */
+    @Login
+    @Log(title = "修改店铺基础信息", businessType = BusinessType.UPDATE, logParam = {"店铺", "修改店铺基础信息"}, isStoreLog = true)
+    @PutMapping("/editData")
+    public AjaxResult editData(@RequestBody FsStoreScrm fsStore)
+    {
+        if (fsStore.getPhone() != null && fsStore.getPhone().contains("*")) {
+            fsStore.setPhone(null);
+        }
+        return toAjax(fsStoreService.updateEditData(fsStore));
+    }
+
+    /**
+     * 获取所有店铺列表(多店铺下拉选择)
+     */
+    @GetMapping("/all")
+    public AjaxResult selectAllStore()
+    {
+        List<FsStoreScrmVO> list = fsStoreService.selectAllStore();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 店铺审核
+     */
+    @Login
+    @Log(title = "店铺管理", businessType = BusinessType.UPDATE, logParam = {"店铺", "店铺审核"}, isStoreLog = true)
+    @PutMapping("/audit")
+    public AjaxResult audit(@RequestBody FsStoreAuditParam fsStore)
+    {
+        return toAjax(fsStoreService.updateFsStoreAudit(fsStore));
+    }
 }

+ 248 - 0
saasdoctorui/src/views/doctor/dashboard/index.vue

@@ -0,0 +1,248 @@
+<template>
+  <div class="app-container doctor-dashboard">
+    <!-- 指标卡片行 -->
+    <el-row :gutter="16" class="stat-cards">
+      <el-col :span="6">
+        <div class="stat-card stat-waiting" @click="$router.push('/doctor/consultWorkbench')">
+          <div class="stat-icon"><i class="el-icon-s-order" /></div>
+          <div class="stat-body">
+            <div class="stat-value">{{ overview.consultOverview ? overview.consultOverview.waitingCount || 0 : 0 }}</div>
+            <div class="stat-label">待接诊</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card stat-progress" @click="$router.push('/doctor/consultWorkbench')">
+          <div class="stat-icon"><i class="el-icon-s-data" /></div>
+          <div class="stat-body">
+            <div class="stat-value">{{ overview.consultOverview ? overview.consultOverview.inProgressCount || 0 : 0 }}</div>
+            <div class="stat-label">进行中</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card stat-done">
+          <div class="stat-icon"><i class="el-icon-circle-check" /></div>
+          <div class="stat-body">
+            <div class="stat-value">{{ overview.consultOverview ? overview.consultOverview.finishedCount || 0 : 0 }}</div>
+            <div class="stat-label">今日完成</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card stat-income">
+          <div class="stat-icon"><i class="el-icon-money" /></div>
+          <div class="stat-body">
+            <div class="stat-value">¥{{ fmtMoney(overview.todayIncome) }}</div>
+            <div class="stat-label">今日收入</div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="16">
+      <!-- 左侧:待处理事项 + 快捷入口 + 排班 -->
+      <el-col :span="12">
+        <!-- 待处理事项 -->
+        <el-card shadow="never" class="todo-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-bell" /> 待处理事项</span>
+          </div>
+          <el-row :gutter="12">
+            <el-col :span="12">
+              <div class="todo-item" @click="$router.push('/doctor/prescribeCreate')">
+                <el-badge :value="overview.pendingPrescribeCount" :hidden="!overview.pendingPrescribeCount">
+                  <el-button type="warning" size="small" icon="el-icon-document">待审处方</el-button>
+                </el-badge>
+                <span class="todo-desc">{{ overview.pendingPrescribeCount || 0 }} 张处方待审核</span>
+              </div>
+            </el-col>
+            <el-col :span="12">
+              <div class="todo-item" @click="$router.push('/doctor/followUp')">
+                <el-badge :value="overview.pendingFollowCount" :hidden="!overview.pendingFollowCount">
+                  <el-button type="primary" size="small" icon="el-icon-phone-outline">待随访</el-button>
+                </el-badge>
+                <span class="todo-desc">{{ overview.pendingFollowCount || 0 }} 位患者待随访</span>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 快捷入口 -->
+        <el-card shadow="never" class="shortcut-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-s-home" /> 快捷入口</span>
+          </div>
+          <el-row :gutter="12">
+            <el-col :span="6">
+              <div class="shortcut-item" @click="$router.push('/doctor/consultWorkbench')">
+                <i class="el-icon-chat-dot-round shortcut-icon" style="background:#409eff" />
+                <span>在线问诊</span>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="shortcut-item" @click="$router.push('/doctor/prescribeCreate')">
+                <i class="el-icon-document shortcut-icon" style="background:#67c23a" />
+                <span>开具处方</span>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="shortcut-item" @click="$router.push('/doctor/patientRecord')">
+                <i class="el-icon-user shortcut-icon" style="background:#e6a23c" />
+                <span>患者管理</span>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="shortcut-item" @click="$router.push('/doctor/myLive')">
+                <i class="el-icon-video-camera shortcut-icon" style="background:#f56c6c" />
+                <span>我的直播</span>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 今日排班 -->
+        <el-card shadow="never" class="schedule-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-date" /> 今日排班</span>
+            <el-button type="text" size="mini" style="float:right" @click="$router.push('/doctor/schedule')">管理排班</el-button>
+          </div>
+          <div v-if="overview.todaySchedule && overview.todaySchedule.length > 0">
+            <el-tag v-for="s in overview.todaySchedule" :key="s.id"
+              :type="s.status == 0 ? 'success' : s.status == 1 ? 'warning' : 'danger'"
+              size="small" class="schedule-tag">
+              {{ s.shiftName }} ({{ s.currentPatients || 0 }}/{{ s.maxPatients || 0 }}) - {{ s.department || '全科' }}
+            </el-tag>
+          </div>
+          <el-empty v-else description="今日暂无排班" :image-size="40" />
+        </el-card>
+      </el-col>
+
+      <!-- 右侧:问诊趋势图 -->
+      <el-col :span="12">
+        <el-card shadow="never" class="chart-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-s-marketing" /> 近7天问诊量</span>
+            <el-radio-group v-model="trendDays" size="mini" style="float:right" @change="loadTrend">
+              <el-radio-button :label="7">7天</el-radio-button>
+              <el-radio-button :label="30">30天</el-radio-button>
+            </el-radio-group>
+          </div>
+          <div ref="chartDom" style="height:260px" v-loading="chartLoading" />
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { getOverview, getConsultTrend } from '@/api/doctor/dashboard'
+import * as echarts from 'echarts'
+
+export default {
+  name: 'DoctorDashboard',
+  data() {
+    return {
+      overview: {
+        consultOverview: {},
+        todayIncome: 0,
+        pendingFollowCount: 0,
+        pendingPrescribeCount: 0,
+        todaySchedule: []
+      },
+      trendDays: 7,
+      chartLoading: false,
+      chartInstance: null
+    }
+  },
+  mounted() {
+    this.loadOverview()
+    this.loadTrend()
+  },
+  beforeDestroy() {
+    if (this.chartInstance) {
+      this.chartInstance.dispose()
+    }
+  },
+  methods: {
+    fmtMoney(val) {
+      if (val === null || val === undefined) return '0.00'
+      return Number(val).toFixed(2)
+    },
+    loadOverview() {
+      getOverview().then(res => {
+        if (res.code === 200) {
+          this.overview = res
+        }
+      })
+    },
+    loadTrend() {
+      this.chartLoading = true
+      getConsultTrend(this.trendDays).then(res => {
+        this.chartLoading = false
+        const list = (res.code === 200 && res.data) ? res.data : []
+        this.renderChart(list)
+      }).catch(() => {
+        this.chartLoading = false
+      })
+    },
+    renderChart(list) {
+      const dom = this.$refs.chartDom
+      if (!dom) return
+      if (!this.chartInstance) {
+        this.chartInstance = echarts.init(dom)
+      }
+      const dates = list.map(d => d.date)
+      const counts = list.map(d => d.count)
+      this.chartInstance.setOption({
+        tooltip: { trigger: 'axis' },
+        grid: { top: 10, left: 40, right: 20, bottom: 30 },
+        xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30, fontSize: 10 } },
+        yAxis: { type: 'value', minInterval: 1 },
+        series: [{
+          type: 'bar',
+          data: counts,
+          itemStyle: { color: '#409eff', borderRadius: [4, 4, 0, 0] }
+        }]
+      }, true)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.doctor-dashboard { padding: 0; }
+
+.stat-cards { margin-bottom: 16px; }
+.stat-card {
+  display: flex; align-items: center; padding: 20px; border-radius: 8px;
+  cursor: pointer; transition: transform .2s;
+}
+.stat-card:hover { transform: translateY(-2px); }
+.stat-waiting { background: linear-gradient(135deg, #ffecd2, #fcb69f); }
+.stat-progress { background: linear-gradient(135deg, #a1c4fd, #c2e9fb); }
+.stat-done { background: linear-gradient(135deg, #d4fc79, #96e6a1); }
+.stat-income { background: linear-gradient(135deg, #fbc2eb, #a6c1ee); }
+.stat-icon { font-size: 36px; margin-right: 16px; opacity: .6; }
+.stat-value { font-size: 24px; font-weight: bold; color: #303133; }
+.stat-label { font-size: 12px; color: #606266; margin-top: 4px; }
+
+.card-header { display: flex; align-items: center; font-weight: bold; }
+.card-header i { margin-right: 6px; }
+
+.todo-card { margin-bottom: 12px; }
+.todo-item { cursor: pointer; padding: 8px 0; display: flex; align-items: center; gap: 8px; }
+.todo-desc { font-size: 12px; color: #909399; }
+
+.shortcut-card { margin-bottom: 12px; }
+.shortcut-item { text-align: center; cursor: pointer; padding: 8px 0; }
+.shortcut-item:hover { opacity: .8; }
+.shortcut-icon { display: block; width: 36px; height: 36px; border-radius: 50%; color: #fff;
+  font-size: 20px; line-height: 36px; margin: 0 auto 4px; text-align: center; }
+.shortcut-item span { font-size: 12px; color: #606266; }
+
+.schedule-card { margin-bottom: 12px; }
+.schedule-tag { margin: 4px 8px 4px 0; }
+
+.chart-card { min-height: 340px; }
+</style>

+ 131 - 0
saasdoctorui/src/views/doctor/myLive/index.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="$router.push('/live/liveConsole')">创建直播</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table v-loading="loading" :data="dataList" border stripe>
+      <el-table-column label="封面" width="120">
+        <template slot-scope="scope">
+          <el-image v-if="scope.row.coverImg" :src="scope.row.coverImg" fit="cover"
+            style="width:80px;height:60px;border-radius:4px" :preview-src-list="[scope.row.coverImg]" />
+          <span v-else style="color:#c0c4cc">暂无封面</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="直播标题" prop="title" min-width="180" show-overflow-tooltip />
+      <el-table-column label="状态" width="90">
+        <template slot-scope="scope">
+          <el-tag :type="statusTagType(scope.row.status)" size="small">{{ scope.row.statusName }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="开始时间" prop="startTime" width="160" />
+      <el-table-column label="结束时间" prop="endTime" width="160" />
+      <el-table-column label="观看量" prop="viewCount" width="90" />
+      <el-table-column label="点赞量" prop="likeCount" width="90" />
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="scope">
+          <el-button v-if="scope.row.status === 0" type="text" size="mini" icon="el-icon-video-play"
+            style="color:#67c23a" @click="handleStartLive(scope.row)">开始直播</el-button>
+          <el-button v-if="scope.row.status === 1" type="text" size="mini" icon="el-icon-video-pause"
+            style="color:#f56c6c" @click="handleStopLive(scope.row)">结束直播</el-button>
+          <el-button v-if="scope.row.status === 1" type="text" size="mini" icon="el-icon-monitor"
+            @click="handleEnterConsole(scope.row)">控制台</el-button>
+          <el-button type="text" size="mini" icon="el-icon-view"
+            @click="handleViewDetail(scope.row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-empty v-if="!loading && dataList.length === 0" description="暂无直播记录" style="margin-top:60px" />
+
+    <!-- 直播详情弹窗 -->
+    <el-dialog title="直播详情" :visible.sync="detailOpen" width="600px" append-to-body>
+      <el-descriptions v-if="detailRow" :column="2" border size="small">
+        <el-descriptions-item label="直播标题">{{ detailRow.title }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusTagType(detailRow.status)" size="mini">{{ detailRow.statusName }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ detailRow.startTime }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ detailRow.endTime || '未结束' }}</el-descriptions-item>
+        <el-descriptions-item label="观看量">{{ detailRow.viewCount || 0 }}</el-descriptions-item>
+        <el-descriptions-item label="点赞量">{{ detailRow.likeCount || 0 }}</el-descriptions-item>
+      </el-descriptions>
+      <span slot="footer">
+        <el-button @click="detailOpen = false">关 闭</el-button>
+        <el-button v-if="detailRow && detailRow.status === 0" type="primary" @click="handleStartLive(detailRow); detailOpen = false">开始直播</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getMyLiveList, startLive, stopLive, clearDashboardCache } from '@/api/doctor/myLive'
+
+export default {
+  name: 'DoctorMyLive',
+  data() {
+    return {
+      loading: false,
+      dataList: [],
+      detailOpen: false,
+      detailRow: null
+    }
+  },
+  mounted() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      getMyLiveList().then(res => {
+        this.dataList = (res.code === 200 && res.data) ? res.data : []
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    statusTagType(status) {
+      const map = { 0: 'info', 1: 'success', 2: 'default' }
+      return map[status] || 'info'
+    },
+    handleStartLive(row) {
+      this.$confirm('确认开始直播「' + row.title + '」吗?', '提示', { type: 'info' }).then(() => {
+        startLive(row.id).then(res => {
+          if (res.code === 200) {
+            this.$message.success('直播已开始')
+            clearDashboardCache().finally(() => this.getList())
+          } else {
+            this.$message.error(res.msg || '操作失败')
+          }
+        })
+      }).catch(() => {})
+    },
+    handleStopLive(row) {
+      this.$confirm('确认结束直播「' + row.title + '」吗?', '提示', { type: 'warning' }).then(() => {
+        stopLive(row.id).then(res => {
+          if (res.code === 200) {
+            this.$message.success('直播已结束')
+            clearDashboardCache().finally(() => this.getList())
+          } else {
+            this.$message.error(res.msg || '操作失败')
+          }
+        })
+      }).catch(() => {})
+    },
+    handleEnterConsole(row) {
+      this.$router.push('/live/liveConsole?id=' + row.id)
+    },
+    handleViewDetail(row) {
+      this.detailRow = row
+      this.detailOpen = true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+</style>

+ 200 - 0
saasdoctorui/src/views/doctor/performance/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="app-container doctor-performance">
+    <!-- 汇总卡片 -->
+    <el-row :gutter="16" class="summary-row">
+      <el-col :span="4">
+        <div class="pf-card pf-blue">
+          <div class="pf-title">账户余额</div>
+          <div class="pf-value">¥{{ fmtMoney(summary.balance) }}</div>
+        </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="pf-card pf-green">
+          <div class="pf-title">累计收入</div>
+          <div class="pf-value">¥{{ fmtMoney(summary.totalIncome) }}</div>
+        </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="pf-card pf-orange">
+          <div class="pf-title">累计问诊</div>
+          <div class="pf-value">{{ summary.totalConsults || 0 }} <small>次</small></div>
+        </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="pf-card pf-purple">
+          <div class="pf-title">累计处方</div>
+          <div class="pf-value">{{ summary.totalPrescribes || 0 }} <small>张</small></div>
+        </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="pf-card pf-rank">
+          <div class="pf-title">累计随访</div>
+          <div class="pf-value">{{ summary.totalFollows || 0 }} <small>次</small></div>
+        </div>
+      </el-col>
+      <el-col :span="4">
+        <div class="pf-card pf-rank" v-if="summary.rankInfo">
+          <div class="pf-title">本月排名</div>
+          <div class="pf-value">
+            <template v-if="summary.rankInfo.rank">#{{ summary.rankInfo.rank }}</template>
+            <template v-else>-</template>
+            <small>/ {{ summary.rankInfo.totalDoctors || 0 }}人</small>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 趋势图行 -->
+    <el-row :gutter="16">
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header">
+            <span>问诊量趋势</span>
+            <el-radio-group v-model="consultDays" size="mini" style="float:right" @change="loadConsultChart">
+              <el-radio-button :label="7">7天</el-radio-button>
+              <el-radio-button :label="30">30天</el-radio-button>
+            </el-radio-group>
+          </div>
+          <div ref="consultChart" style="height:240px" v-loading="consultLoading" />
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header">
+            <span>收入趋势</span>
+            <el-radio-group v-model="revenueDays" size="mini" style="float:right" @change="loadRevenueChart">
+              <el-radio-button :label="7">7天</el-radio-button>
+              <el-radio-button :label="30">30天</el-radio-button>
+            </el-radio-group>
+          </div>
+          <div ref="revenueChart" style="height:240px" v-loading="revenueLoading" />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 账单记录 -->
+    <el-card shadow="never" style="margin-top:16px">
+      <div slot="header">
+        <span>账单记录</span>
+      </div>
+      <el-table v-loading="billLoading" :data="billList" border stripe size="small">
+        <el-table-column label="类型" prop="title" min-width="120" />
+        <el-table-column label="金额" width="120">
+          <template slot-scope="scope">
+            <span :style="{ color: scope.row.money >= 0 ? '#67c23a' : '#f56c6c' }">
+              {{ scope.row.money >= 0 ? '+' : '' }}{{ fmtMoney(scope.row.money) }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="余额" width="120">
+          <template slot-scope="scope">{{ fmtMoney(scope.row.balance) }}</template>
+        </el-table-column>
+        <el-table-column label="时间" prop="createTime" width="160" />
+      </el-table>
+      <pagination v-show="billTotal > 0" :total="billTotal" :page.sync="billQuery.pageNum"
+        :limit.sync="billQuery.pageSize" @pagination="loadBills" />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getPerformance, getConsultTrend, getRevenueTrend } from '@/api/doctor/performance'
+import { getBillList } from '@/api/doctor/performance'
+import * as echarts from 'echarts'
+
+export default {
+  name: 'DoctorPerformance',
+  data() {
+    return {
+      summary: {},
+      consultDays: 7,
+      revenueDays: 30,
+      consultLoading: false,
+      revenueLoading: false,
+      consultChartInst: null,
+      revenueChartInst: null,
+      billLoading: false,
+      billList: [],
+      billTotal: 0,
+      billQuery: { pageNum: 1, pageSize: 10 }
+    }
+  },
+  mounted() {
+    this.loadSummary()
+    this.loadConsultChart()
+    this.loadRevenueChart()
+    this.loadBills()
+  },
+  beforeDestroy() {
+    if (this.consultChartInst) this.consultChartInst.dispose()
+    if (this.revenueChartInst) this.revenueChartInst.dispose()
+  },
+  methods: {
+    fmtMoney(val) {
+      if (val === null || val === undefined) return '0.00'
+      return Number(val).toFixed(2)
+    },
+    loadSummary() {
+      getPerformance().then(res => {
+        if (res.code === 200) this.summary = res
+      })
+    },
+    loadConsultChart() {
+      this.consultLoading = true
+      getConsultTrend(this.consultDays).then(res => {
+        this.consultLoading = false
+        const list = (res.code === 200 && res.data) ? res.data : []
+        this.renderLineChart(this.$refs.consultChart, 'consultChartInst', list, '#409eff')
+      }).catch(() => { this.consultLoading = false })
+    },
+    loadRevenueChart() {
+      this.revenueLoading = true
+      getRevenueTrend(this.revenueDays).then(res => {
+        this.revenueLoading = false
+        const list = (res.code === 200 && res.data) ? res.data : []
+        this.renderLineChart(this.$refs.revenueChart, 'revenueChartInst', list, '#67c23a')
+      }).catch(() => { this.revenueLoading = false })
+    },
+    renderLineChart(dom, instName, list, color) {
+      if (!dom) return
+      if (!this[instName]) this[instName] = echarts.init(dom)
+      this[instName].setOption({
+        tooltip: { trigger: 'axis' },
+        grid: { top: 10, left: 50, right: 20, bottom: 30 },
+        xAxis: { type: 'category', data: list.map(d => d.date), axisLabel: { rotate: 30, fontSize: 10 } },
+        yAxis: { type: 'value', minInterval: 1 },
+        series: [{
+          type: 'line', smooth: true,
+          data: list.map(d => d.count || d.amount || 0),
+          itemStyle: { color }, areaStyle: { color: color + '20' }
+        }]
+      }, true)
+    },
+    loadBills() {
+      this.billLoading = true
+      getBillList(this.billQuery).then(res => {
+        this.billLoading = false
+        if (res.code === 200 && res.data) {
+          this.billList = res.data.list || []
+          this.billTotal = res.data.total || 0
+        }
+      }).catch(() => { this.billLoading = false })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.summary-row { margin-bottom: 16px; }
+.pf-card {
+  padding: 20px; border-radius: 8px; color: #fff; text-align: center;
+}
+.pf-blue { background: linear-gradient(135deg, #409eff, #66b1ff); }
+.pf-green { background: linear-gradient(135deg, #67c23a, #85ce61); }
+.pf-orange { background: linear-gradient(135deg, #e6a23c, #ebb563); }
+.pf-purple { background: linear-gradient(135deg, #9b59b6, #a569bd); }
+.pf-rank { background: linear-gradient(135deg, #f56c6c, #f89898); }
+.pf-title { font-size: 12px; opacity: .85; margin-bottom: 6px; }
+.pf-value { font-size: 24px; font-weight: bold; }
+.pf-value small { font-size: 12px; font-weight: normal; }
+</style>