Parcourir la source

满减活动相关代码提交+销售制单+看课APP兼容

yjwang il y a 1 jour
Parent
commit
0e01d0c773
67 fichiers modifiés avec 1978 ajouts et 444 suppressions
  1. 14 0
      fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java
  2. 251 78
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  3. 29 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePromotionController.java
  4. 7 5
      fs-company-app/src/main/java/com/fs/app/controller/AppBaseController.java
  5. 3 5
      fs-company-app/src/main/java/com/fs/app/controller/UserController.java
  6. 10 22
      fs-company-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  7. 92 17
      fs-company-app/src/main/java/com/fs/app/utils/JwtUtils.java
  8. 9 3
      fs-company-app/src/main/java/com/fs/core/PermissionService.java
  9. 31 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStorePromotionScrmController.java
  10. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  11. 5 0
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  12. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  13. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCoursePlaySourceConfig.java
  14. 8 0
      fs-service/src/main/java/com/fs/course/mapper/FsCoursePlaySourceConfigMapper.java
  15. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigCreateParam.java
  16. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigEditParam.java
  17. 15 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java
  18. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCoursePlaySourceConfigService.java
  19. 17 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCoursePlaySourceConfigServiceImpl.java
  20. 6 0
      fs-service/src/main/java/com/fs/course/vo/FsCoursePlaySourceConfigVO.java
  21. 3 0
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  22. 37 0
      fs-service/src/main/java/com/fs/his/enums/FsUserBizTypeEnum.java
  23. 19 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  24. 10 1
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  25. 10 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  26. 1 1
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  27. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStorePreparedPriceChange.java
  28. 1 1
      fs-service/src/main/java/com/fs/hisStore/domain/FsStorePromotionActivity.java
  29. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreScrm.java
  30. 2 0
      fs-service/src/main/java/com/fs/hisStore/dto/FsStoreOrderComputeDTO.java
  31. 18 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionReportMapper.java
  32. 37 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStorePromotionReportParam.java
  33. 22 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStorePromotionService.java
  34. 291 101
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  35. 21 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePreparedPriceChangeServiceImpl.java
  36. 6 48
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionComputeServiceImpl.java
  37. 276 17
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionServiceImpl.java
  38. 7 24
      fs-service/src/main/java/com/fs/hisStore/support/FsStorePromotionTierCalculator.java
  39. 36 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderPromotionDetailVO.java
  40. 2 2
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionDetailVO.java
  41. 56 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionReportOrderVO.java
  42. 27 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionReportStatsVO.java
  43. 0 20
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionScopeCategoryVO.java
  44. 20 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionScopeProductTypeVO.java
  45. 24 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionTierDisplayVO.java
  46. 66 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionUserDisplayVO.java
  47. 8 4
      fs-service/src/main/resources/application-config-druid-yjb.yml
  48. 9 2
      fs-service/src/main/resources/application-dev-yjb.yml
  49. 3 0
      fs-service/src/main/resources/db/20250703-满减活动报表索引.sql
  50. 20 0
      fs-service/src/main/resources/db/20250703-点播播放源看课小程序字段.sql
  51. 18 0
      fs-service/src/main/resources/db/fs_user_biz_type.sql
  52. 8 0
      fs-service/src/main/resources/mapper/company/CompanyMapper.xml
  53. 3 0
      fs-service/src/main/resources/mapper/course/FsCoursePlaySourceConfigMapper.xml
  54. 43 6
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  55. 5 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  56. 8 1
      fs-service/src/main/resources/mapper/hisStore/FsStorePreparedPriceChangeMapper.xml
  57. 77 0
      fs-service/src/main/resources/mapper/hisStore/FsStorePromotionReportMapper.xml
  58. 12 8
      fs-service/src/main/resources/mapper/hisStore/FsStoreScrmMapper.xml
  59. 29 0
      fs-store/src/main/java/com/fs/hisStore/controller/store/FsStorePromotionScrmController.java
  60. 52 9
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java
  61. 9 10
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  62. 31 8
      fs-user-app/src/main/java/com/fs/app/controller/WxUserController.java
  63. 3 3
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java
  64. 13 5
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java
  65. 13 5
      fs-user-app/src/main/java/com/fs/app/controller/store/StoreScrmController.java
  66. 10 20
      fs-user-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  67. 72 17
      fs-user-app/src/main/java/com/fs/app/utils/JwtUtils.java

+ 14 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java

@@ -57,6 +57,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
     public TableDataInfo list(@RequestParam(required = false) String name,
                               @RequestParam(required = false) String appid,
                               @RequestParam(required = false) Integer isMall,
+                              @RequestParam(required = false) Integer isCourseMini,
                               @RequestParam(required = false, defaultValue = "1") Integer pageNum,
                               @RequestParam(required = false, defaultValue = "10") Integer pageSize,
                               @RequestParam(required = false) Integer status,
@@ -65,6 +66,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         params.put("name", name);
         params.put("appid", appid);
         params.put("isMall", isMall);
+        params.put("isCourseMini", isCourseMini);
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
@@ -98,6 +100,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         FsCoursePlaySourceConfigVO configVO = new FsCoursePlaySourceConfigVO();
         BeanUtils.copyProperties(config, configVO);
+        configVO.setIsCourseMini(normalizeIsCourseMini(config.getIsCourseMini()));
         return AjaxResult.success(configVO);
     }
 
@@ -120,6 +123,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         config.setCreateUserId(loginUser.getUserId());
         config.setCreateDeptId(loginUser.getDeptId());
         BeanUtils.copyProperties(param, config);
+        config.setIsCourseMini(normalizeIsCourseMini(param.getIsCourseMini()));
 
         config.setIsDel(0);
         config.setCreateTime(LocalDateTime.now());
@@ -156,6 +160,9 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
 
         BeanUtils.copyProperties(param, config);
+        if (param.getIsCourseMini() != null) {
+            config.setIsCourseMini(normalizeIsCourseMini(param.getIsCourseMini()));
+        }
         //加入缓存
         if(param.getViewPermissions() != null){
             addRedis(param.getAppid(), param.getViewPermissions());
@@ -299,6 +306,13 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
     }
 
+    /**
+     * 空值视为看课小程序
+     */
+    private Integer normalizeIsCourseMini(Integer isCourseMini) {
+        return isCourseMini == null ? 1 : isCourseMini;
+    }
+
     /**
      * 校验是否总公司部门
      * **/

+ 251 - 78
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -1,178 +1,351 @@
 package com.fs.course.controller;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import com.fs.common.constant.HttpStatus;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.entity.SysDept;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.service.ICompanyService;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCoursePeriodDaysService;
+import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
+import com.fs.framework.web.service.TokenService;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
-import org.springframework.security.access.prepost.PreAuthorize;
+import com.fs.system.mapper.SysDeptMapper;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.course.domain.FsCourseWatchLog;
-import com.fs.course.service.IFsCourseWatchLogService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
- * 短链课程看课记录Controller
- *
- * @author fs
- * @date 2024-10-24
+ * 短链课程看课记录Controller(总后台)
  */
 @RestController
 @RequestMapping("/course/courseWatchLog")
-public class FsCourseWatchLogController extends BaseController
-{
+public class FsCourseWatchLogController extends BaseController {
+
+    private static final int EXPORT_MAX_DAYS = 31;
+    private static final int EXPORT_QEC_MAX_DAYS = 7;
+    private static final int LIST_QUERY_MAX_MONTHS = 3;
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
     @Autowired
     private IFsCourseWatchLogService fsCourseWatchLogService;
-
     @Autowired
     private IQwWatchLogService qwWatchLogService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private SysDeptMapper sysDeptMapper;
+    @Autowired
+    private IFsUserCoursePeriodService userCoursePeriodService;
+    @Autowired
+    private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
+    @Autowired
+    private ICompanyService companyService;
+
     /**
      * 查询短链课程看课记录列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:list')")
     @GetMapping("/list")
-    public TableDataInfo list(FsCourseWatchLogListParam param)
-    {
+    public TableDataInfo list(FsCourseWatchLogListParam param) {
+        applyDeptDataScope(param);
+        applyListThreeMonthsScope(param);
+        if (!resolveAllCompanyIds(param)) {
+            return getDataTable(new ArrayList<>());
+        }
+        if (!preparePeriodFilter(param)) {
+            return getDataTable(new ArrayList<>());
+        }
         startPage();
         List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
         return getDataTable(list);
     }
 
     @GetMapping("/qwWatchLogAllStatisticsList")
-    public TableDataInfo qwWatchLogAllStatisticsList(QwWatchLogStatisticsListParam param)
-    {
-        logger.info("会员课程数据汇总 参数: {}",param);
-
-        if(param.getCompanyId() == null){
+    public TableDataInfo qwWatchLogAllStatisticsList(QwWatchLogStatisticsListParam param) {
+        logger.info("会员课程数据汇总 参数: {}", param);
+        if (param.getCompanyId() == null) {
             throw new CustomException("必须选择公司!");
         }
-        if (param.getSTime()==null||param.getETime()==null){
+        if (param.getSTime() == null || param.getETime() == null) {
             return getDataTable(new ArrayList<>());
         }
         return qwWatchLogService.selectQwWatchLogAllStatisticsListVONew(param);
     }
+
     @GetMapping("/qwWatchLogStatisticsList")
-    public TableDataInfo qwWatchLogStatisticsList(QwWatchLogStatisticsListParam param)
-    {
-        if(param.getPageNum() == null){
+    public TableDataInfo qwWatchLogStatisticsList(QwWatchLogStatisticsListParam param) {
+        if (param.getPageNum() == null) {
             param.setPageNum(1L);
         }
-        if(param.getPageSize() == null){
+        if (param.getPageSize() == null) {
             param.setPageSize(10L);
         }
-        if (param.getSTime()==null||param.getETime()==null){
+        if (param.getSTime() == null || param.getETime() == null) {
             return getDataTable(new ArrayList<>());
         }
-        if(param.getCompanyId() == null){
+        if (param.getCompanyId() == null) {
             throw new CustomException("必须选择公司!");
         }
         return qwWatchLogService.selectQwWatchLogStatisticsListVONew(param);
     }
+
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:statisticsList')")
     @GetMapping("/statisticsList")
-    public TableDataInfo statisticsList(FsCourseWatchLogStatisticsListParam param)
-    {
-        // 如果看指定用户就不用设置公司
-        if(param.getCompanyId() == null){
-            if(param.getUserId() == null) {
+    public TableDataInfo statisticsList(FsCourseWatchLogStatisticsListParam param) {
+        if (param.getCompanyId() == null) {
+            if (param.getUserId() == null) {
                 throw new CustomException("查看公司或者用户必填!");
             }
         }
-        if (param.getSTime()==null||param.getETime()==null){
+        if (param.getSTime() == null || param.getETime() == null) {
             throw new CustomException("必须选择开始时间和结束时间!");
         }
         startPage();
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVONew(param);
-
         return getDataTable(list);
     }
 
     /**
-     * 导出短链课程看课记录列表
+     * 导出短链课程看课记录列表(须带筛选条件,禁止全量导出)
      */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:export')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(FsCourseWatchLogListParam param)
-    {
-        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
-        ExcelUtil<FsCourseWatchLogListVO> util = new ExcelUtil<FsCourseWatchLogListVO>(FsCourseWatchLogListVO.class);
+    public AjaxResult export(FsCourseWatchLogListParam param) {
+        applyDeptDataScope(param);
+        validateExportParams(param);
+        if (!resolveAllCompanyIds(param)) {
+            ExcelUtil<FsCourseWatchLogListVO> emptyUtil = new ExcelUtil<>(FsCourseWatchLogListVO.class);
+            return emptyUtil.exportExcel(new ArrayList<>(), "短链课程看课记录数据");
+        }
+        if (!preparePeriodFilter(param)) {
+            ExcelUtil<FsCourseWatchLogListVO> emptyUtil = new ExcelUtil<>(FsCourseWatchLogListVO.class);
+            return emptyUtil.exportExcel(new ArrayList<>(), "短链课程看课记录数据");
+        }
+        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVOexport(param);
+        ExcelUtil<FsCourseWatchLogListVO> util = new ExcelUtil<>(FsCourseWatchLogListVO.class);
         return util.exportExcel(list, "短链课程看课记录数据");
     }
 
-    /**
-     * 获取短链课程看课记录详细信息
-     */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
     @GetMapping(value = "/{logId}")
-    public AjaxResult getInfo(@PathVariable("logId") Long logId)
-    {
+    public AjaxResult getInfo(@PathVariable("logId") Long logId) {
         return AjaxResult.success(fsCourseWatchLogService.selectFsCourseWatchLogByLogId(logId));
     }
 
-    /**
-     * 新增短链课程看课记录
-     */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:add')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody FsCourseWatchLog fsCourseWatchLog)
-    {
+    public AjaxResult add(@RequestBody FsCourseWatchLog fsCourseWatchLog) {
         return toAjax(fsCourseWatchLogService.insertFsCourseWatchLog(fsCourseWatchLog));
     }
 
-    /**
-     * 修改短链课程看课记录
-     */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:edit')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody FsCourseWatchLog fsCourseWatchLog)
-    {
+    public AjaxResult edit(@RequestBody FsCourseWatchLog fsCourseWatchLog) {
         return toAjax(fsCourseWatchLogService.updateFsCourseWatchLog(fsCourseWatchLog));
     }
 
-    /**
-     * 删除短链课程看课记录
-     */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:remove')")
     @Log(title = "短链课程看课记录", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{logIds}")
-    public AjaxResult remove(@PathVariable Long[] logIds)
-    {
+    @DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds) {
         return toAjax(fsCourseWatchLogService.deleteFsCourseWatchLogByLogIds(logIds));
     }
 
     @GetMapping("/watchLogStatistics")
-    public TableDataInfo watchLogStatistics(FsCourseOverParam param)
-    {
+    public TableDataInfo watchLogStatistics(FsCourseOverParam param) {
         startPage();
-        if (param.getSTime()==null||param.getETime()==null){
+        if (param.getSTime() == null || param.getETime() == null) {
             return getDataTable(new ArrayList<>());
         }
         List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
         return getDataTable(list);
     }
+
+    /**
+     * 列表查询限定近三个月创建时间,避免全表扫描
+     */
+    private void applyListThreeMonthsScope(FsCourseWatchLogListParam param) {
+        if (param == null) {
+            return;
+        }
+        LocalDate today = LocalDate.now();
+        LocalDate minDate = today.minusMonths(LIST_QUERY_MAX_MONTHS);
+        String minStr = minDate.format(DATE_FMT);
+        String maxStr = today.format(DATE_FMT);
+        if (StringUtils.isEmpty(param.getSTime()) && StringUtils.isEmpty(param.getETime())) {
+            param.setSTime(minStr);
+            param.setETime(maxStr);
+            return;
+        }
+        LocalDate start = StringUtils.isNotEmpty(param.getSTime())
+                ? LocalDate.parse(param.getSTime(), DATE_FMT) : minDate;
+        LocalDate end = StringUtils.isNotEmpty(param.getETime())
+                ? LocalDate.parse(param.getETime(), DATE_FMT) : today;
+        if (start.isBefore(minDate)) {
+            start = minDate;
+        }
+        if (end.isAfter(today)) {
+            end = today;
+        }
+        if (start.isAfter(end)) {
+            start = minDate;
+            end = today;
+        }
+        param.setSTime(start.format(DATE_FMT));
+        param.setETime(end.format(DATE_FMT));
+    }
+
+    /**
+     * 选「全部」时:后端按数据权限查询销售公司ID并写入 companyIds,走 IN 查询
+     */
+    private boolean resolveAllCompanyIds(FsCourseWatchLogListParam param) {
+        if (param == null || !Integer.valueOf(1).equals(param.getAllCompany()) || param.getCompanyId() != null) {
+            return true;
+        }
+        List<Long> companyIds = companyService.selectCompanyIdsBySysDeptScope(param.getSysDeptId());
+        if (companyIds == null || companyIds.isEmpty()) {
+            param.setCompanyIds(new ArrayList<>());
+            return false;
+        }
+        param.setCompanyIds(companyIds);
+        param.setSysDeptId(null);
+        return true;
+    }
+
+    /**
+     * 数据隔离:admin 看全部;其它账号按登录用户所属系统部门过滤销售公司(company.dept_id)
+     */
+    private void applyDeptDataScope(FsCourseWatchLogListParam param) {
+        if (param == null) {
+            return;
+        }
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser == null || loginUser.isAdmin()) {
+                return;
+            }
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+            if (config.getDept() != null && config.getDept() && !checkDept(loginUser.getUser().getDeptId())) {
+                param.setSysDeptId(loginUser.getDeptId());
+            }
+        } catch (Exception ignore) {
+        }
+    }
+
+    private boolean preparePeriodFilter(FsCourseWatchLogListParam param) {
+        if (param == null || param.getSendType() == null || param.getSendType() != 1) {
+            return true;
+        }
+        if (param.getPeriodETime() == null || param.getPeriodSTime() == null) {
+            return true;
+        }
+        List<Long> periodIds = userCoursePeriodDaysService.selectFsUserCoursePeriodDaysByTime(
+                param.getPeriodSTime(), param.getPeriodETime());
+        if (periodIds.isEmpty()) {
+            return false;
+        }
+        List<Long> scopedPeriodIds = userCoursePeriodService.selectFsUserCoursePeriodListByPeriodId(
+                periodIds, param.getCompanyId());
+        if (scopedPeriodIds.isEmpty()) {
+            return false;
+        }
+        param.setPeriodIds(scopedPeriodIds);
+        return true;
+    }
+
+    /**
+     * 导出必须携带筛选条件,避免全量导出导致系统卡死
+     */
+    private void validateExportParams(FsCourseWatchLogListParam param) {
+        if (param == null) {
+            throw new CustomException("导出参数不能为空");
+        }
+        boolean hasCreateTime = StringUtils.isNotEmpty(param.getSTime()) && StringUtils.isNotEmpty(param.getETime());
+        boolean hasUpdateTime = StringUtils.isNotEmpty(param.getUpSTime()) && StringUtils.isNotEmpty(param.getUpETime());
+        boolean hasScheduleTime = StringUtils.isNotEmpty(param.getScheduleStartTime())
+                && StringUtils.isNotEmpty(param.getScheduleEndTime());
+        boolean hasQecTime = StringUtils.isNotEmpty(param.getQecSTime()) && StringUtils.isNotEmpty(param.getQecETime());
+        boolean hasOtherFilter = param.getCompanyId() != null
+                || Integer.valueOf(1).equals(param.getAllCompany())
+                || (param.getCompanyIds() != null && !param.getCompanyIds().isEmpty())
+                || param.getCourseId() != null
+                || param.getVideoId() != null
+                || param.getPeriodId() != null
+                || param.getUserId() != null
+                || param.getCompanyUserId() != null
+                || StringUtils.isNotEmpty(param.getNickName())
+                || StringUtils.isNotEmpty(param.getExternalUserName())
+                || StringUtils.isNotEmpty(param.getSopId());
+
+        if (!hasCreateTime && !hasUpdateTime && !hasScheduleTime && !hasQecTime && !hasOtherFilter) {
+            throw new CustomException("导出请至少选择创建时间、更新时间、营期时间、进线时间范围,或指定公司/课程/营期/销售等条件");
+        }
+        if (hasCreateTime) {
+            assertDateRange(param.getSTime(), param.getETime(), EXPORT_MAX_DAYS, "创建时间");
+        }
+        if (hasUpdateTime) {
+            assertDateRange(param.getUpSTime(), param.getUpETime(), EXPORT_MAX_DAYS, "更新时间");
+        }
+        if (hasScheduleTime) {
+            assertDateRange(param.getScheduleStartTime(), param.getScheduleEndTime(), EXPORT_MAX_DAYS, "营期时间");
+        }
+        if (hasQecTime) {
+            assertDateRange(param.getQecSTime(), param.getQecETime(), EXPORT_QEC_MAX_DAYS, "进线时间");
+        }
+    }
+
+    private void assertDateRange(String start, String end, int maxDays, String label) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        LocalDate startDate = LocalDate.parse(start, formatter);
+        LocalDate endDate = LocalDate.parse(end, formatter);
+        if (startDate.isAfter(endDate)) {
+            throw new CustomException(label + "开始日期不能晚于结束日期");
+        }
+        long days = ChronoUnit.DAYS.between(startDate, endDate);
+        if (days > maxDays - 1L) {
+            throw new CustomException(label + "导出范围不能超过" + maxDays + "天");
+        }
+    }
+
+    /**
+     * 判断是否总公司部门(deptId=1),总公司账号视为看全部
+     */
+    private boolean checkDept(Long deptId) {
+        if (deptId == null) {
+            return false;
+        }
+        SysDept dept = sysDeptMapper.selectDeptById(deptId);
+        return dept != null && dept.getDeptId().equals(1L);
+    }
 }

+ 29 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePromotionController.java

@@ -8,6 +8,7 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.dto.FsStorePromotionActivityDTO;
+import com.fs.hisStore.param.FsStorePromotionReportParam;
 import com.fs.hisStore.service.IFsStorePromotionService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -93,4 +94,32 @@ public class FsStorePromotionController extends BaseController {
         data.put("newDisplayStatus", 4);
         return AjaxResult.success(data);
     }
+
+    /**
+     * 查询订单关联的阶梯满减活动详情
+     */
+    @PreAuthorize("@ss.hasAnyPermi('store:storeOrder:query,store:storeAfterSales:query')")
+    @GetMapping("/order/{orderId}")
+    public AjaxResult getOrderPromotionDetail(@PathVariable Long orderId) {
+        return AjaxResult.success(promotionService.selectOrderPromotionDetailByOrderId(orderId));
+    }
+
+    /**
+     * 活动报表汇总
+     */
+    @PreAuthorize("@ss.hasPermi('store:storePromotion:query')")
+    @GetMapping("/report/stats")
+    public AjaxResult reportStats(FsStorePromotionReportParam param) {
+        return AjaxResult.success(promotionService.selectPromotionReportStats(param));
+    }
+
+    /**
+     * 活动报表明细订单(分页)
+     */
+    @PreAuthorize("@ss.hasPermi('store:storePromotion:query')")
+    @GetMapping("/report/orders")
+    public TableDataInfo reportOrders(FsStorePromotionReportParam param) {
+        startPage();
+        return getDataTable(promotionService.selectPromotionReportOrderList(param));
+    }
 }

+ 7 - 5
fs-company-app/src/main/java/com/fs/app/controller/AppBaseController.java

@@ -46,7 +46,7 @@ public class AppBaseController {
 		if (companyId==null){
 			CompanyUser companyUser = companyUserService.selectCompanyUserById(Long.parseLong(userId));
 			companyId = companyUser.getCompanyId();
-			redisCache.setCacheObject("companyId:" + companyUser.getUserId(), companyUser.getCompanyId(), 604800, TimeUnit.SECONDS);
+			redisCache.setCacheObject("companyId:" + companyUser.getUserId(), companyUser.getCompanyId(), jwtUtils.getExpireSeconds(), TimeUnit.SECONDS);
 		}
 		return companyId;
 	}
@@ -61,10 +61,12 @@ public class AppBaseController {
 		return userId;
 	}
 	public Long getCompanyUserId() {
-		String headValue =  ServletUtils.getRequest().getHeader("APPToken");
-		Claims claims=jwtUtils.getClaimByToken(headValue);
-		String userId = claims.getSubject();
-		return Long.parseLong(userId);
+		String headValue = ServletUtils.getRequest().getHeader("APPToken");
+		Claims claims = jwtUtils.getClaimByToken(headValue);
+		if (ObjectUtil.isEmpty(claims) || StringUtils.isEmpty(claims.getSubject())) {
+			throw new FSException("未授权,请先登录!");
+		}
+		return Long.parseLong(claims.getSubject());
 	}
 	//获取商城手机号
 	public Long getUserId(Long companyUserId)

+ 3 - 5
fs-company-app/src/main/java/com/fs/app/controller/UserController.java

@@ -192,11 +192,9 @@ public class UserController extends AppBaseController {
                 userService.updateUserProfile(companyUser);
             }
             //生成token
-            String token = jwtUtils.generateToken(companyUser.getUserId());
-            //调用REDIS生成用户的TOKEN
+            String token = jwtUtils.issueAppToken(redisCache, companyUser.getUserId());
 
-            redisCache.setCacheObject("token:" + companyUser.getUserId(), token, 604800, TimeUnit.SECONDS);
-            redisCache.setCacheObject("companyId:" + companyUser.getUserId(), companyUser.getCompanyId(), 604800, TimeUnit.SECONDS);
+            redisCache.setCacheObject("companyId:" + companyUser.getUserId(), companyUser.getCompanyId(), jwtUtils.getExpireSeconds(), TimeUnit.SECONDS);
 
             //获取权限
             Set<String> perms = new HashSet<String>();
@@ -208,7 +206,7 @@ public class UserController extends AppBaseController {
             }
 
             //redisCache.setCacheObject("perms:"+companyUser.getUserId(), JSONUtil.toJsonStr(perms),2592000, TimeUnit.SECONDS);
-            redisCache.setCacheObject("perms:" + companyUser.getUserId(), JSONUtil.toJsonStr(perms), 604800, TimeUnit.SECONDS);
+            redisCache.setCacheObject("perms:" + companyUser.getUserId(), JSONUtil.toJsonStr(perms), jwtUtils.getExpireSeconds(), TimeUnit.SECONDS);
 
             Map<String, Object> result = new HashMap<>();
             result.put("token", token);

+ 10 - 22
fs-company-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -6,7 +6,6 @@ import com.fs.app.exception.FSException;
 import com.fs.app.utils.JwtUtils;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
-import io.jsonwebtoken.Claims;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Component;
@@ -17,54 +16,43 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 /**
- * 权限(Token)验证
+ * Token 验证:/app/user/login 登录(Redis company:login:token)滑动续期;其他登录仍按 JWT 过期时间校验。
  */
 @Component
 public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
     @Autowired
     private JwtUtils jwtUtils;
     @Autowired
-    RedisCache redisCache;
+    private RedisCache redisCache;
     public static final String USER_KEY = "userId";
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         Login annotation;
-        if(handler instanceof HandlerMethod) {
+        if (handler instanceof HandlerMethod) {
             annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
-        }else{
+        } else {
             return true;
         }
 
-        if(annotation == null){
+        if (annotation == null) {
             return true;
         }
 
-        //获取用户凭证
         String token = request.getHeader(jwtUtils.getHeader());
-        if(StringUtils.isBlank(token)){
+        if (StringUtils.isBlank(token)) {
             token = request.getParameter(jwtUtils.getHeader());
         }
-
-        //凭证为空
-        if(StringUtils.isBlank(token)){
+        if (StringUtils.isBlank(token)) {
             throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
         }
 
-        Claims claims = jwtUtils.getClaimByToken(token);
-        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+        Long userId = jwtUtils.validateToken(redisCache, token, true);
+        if (userId == null) {
             throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
         }
 
-        //查询用户的TOKEN是否和REDIS中的一样
-//        String redisToken=redisCache.getCacheObject("AiChatToken:"+ Long.parseLong(claims.getSubject()));
-//        if(redisToken==null||!redisToken.equals(token)){
-//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
-//        }
-//        long l = Long.parseLong(claims.getSubject());
-        //设置userId到request里,后续根据userId,获取用户信息
-        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
-
+        request.setAttribute(USER_KEY, userId);
         return true;
     }
 }

+ 92 - 17
fs-company-app/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -1,6 +1,9 @@
 package com.fs.app.utils;
 
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 import org.slf4j.Logger;
@@ -9,57 +12,129 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
 import java.util.Date;
+import java.util.concurrent.TimeUnit;
 
 /**
- * jwt工具类
-
+ * 公司 App JWT;{@link #APP_REDIS_TOKEN_KEY_PREFIX} 仅 /app/user/login 登录写入,用于 Redis 滑动续期。
  */
 @ConfigurationProperties(prefix = "fs.jwt")
 @Component
 public class JwtUtils {
-    private Logger logger = LoggerFactory.getLogger(getClass());
 
+    /** 仅 UserController(/app/user/login)登录写入,与 H5 等 token: 区分 */
+    public static final String APP_REDIS_TOKEN_KEY_PREFIX = "company:login:token:";
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
 
     private String secret;
-    /** 默认7天(秒),防止配置未加载时 token 立即过期 */
+    /** 会话滑动时长(秒),默认 7 天 */
     private long expire = 604800L;
     private String header;
 
-    /**
-     * 生成jwt token
-     */
     public String generateToken(long userId) {
         Date nowDate = new Date();
-        //过期时间
         Date expireDate = new Date(nowDate.getTime() + expire * 1000);
-//        System.out.println("==============================="+secret);
         return Jwts.builder()
                 .setHeaderParam("typ", "JWT")
-                .setSubject(userId+"")
+                .setSubject(String.valueOf(userId))
                 .setIssuedAt(nowDate)
                 .setExpiration(expireDate)
                 .signWith(SignatureAlgorithm.HS512, secret)
                 .compact();
     }
 
+    /** /app/user/login 登录:签发 JWT 并写入 Redis 会话 */
+    public String issueAppToken(RedisCache redisCache, long userId) {
+        String token = generateToken(userId);
+        cacheAppToken(redisCache, userId, token);
+        return token;
+    }
+
+    public void cacheAppToken(RedisCache redisCache, long userId, String token) {
+        redisCache.setCacheObject(appRedisTokenKey(userId), token, getExpireSeconds(), TimeUnit.SECONDS);
+    }
+
+    /** 公司 App 会话滑动续期(客户端 token 字符串不变) */
+    public void renewAppSession(RedisCache redisCache, long userId, String token) {
+        cacheAppToken(redisCache, userId, token);
+        renewLoginAuxSession(redisCache, userId);
+    }
+
+    /** 同步续期登录时写入的 companyId、perms 缓存 */
+    public void renewLoginAuxSession(RedisCache redisCache, long userId) {
+        int ttl = getExpireSeconds();
+        String companyIdKey = "companyId:" + userId;
+        String permsKey = "perms:" + userId;
+        if (redisCache.getCacheObject(companyIdKey) != null) {
+            redisCache.expire(companyIdKey, ttl, TimeUnit.SECONDS);
+        }
+        if (redisCache.getCacheObject(permsKey) != null) {
+            redisCache.expire(permsKey, ttl, TimeUnit.SECONDS);
+        }
+    }
+
+    public String appRedisTokenKey(long userId) {
+        return APP_REDIS_TOKEN_KEY_PREFIX + userId;
+    }
+
+    /**
+     * 解析 token(签名非法返回 null;JWT 过期仍返回 claims,供 Redis 会话续期使用)
+     */
     public Claims getClaimByToken(String token) {
         try {
             return Jwts.parser()
                     .setSigningKey(secret)
                     .parseClaimsJws(token)
                     .getBody();
-        }catch (Exception e){
+        } catch (ExpiredJwtException e) {
+            return e.getClaims();
+        } catch (Exception e) {
             logger.debug("validate is token error ", e);
             return null;
         }
     }
 
-    /**
-     * token是否过期
-     * @return  true:过期
-     */
     public boolean isTokenExpired(Date expiration) {
-        return expiration.before(new Date());
+        return expiration == null || expiration.before(new Date());
+    }
+
+    public boolean isAccessTokenValid(RedisCache redisCache, String token) {
+        Claims claims = getClaimByToken(token);
+        if (claims == null || StringUtils.isEmpty(claims.getSubject())) {
+            return false;
+        }
+        long userId = Long.parseLong(claims.getSubject());
+        String appSessionToken = redisCache.getCacheObject(appRedisTokenKey(userId));
+        if (appSessionToken != null) {
+            return appSessionToken.equals(token);
+        }
+        return !isTokenExpired(claims.getExpiration());
+    }
+
+    public Long validateToken(RedisCache redisCache, String token, boolean renewSession) {
+        Claims claims = getClaimByToken(token);
+        if (claims == null || StringUtils.isEmpty(claims.getSubject())) {
+            return null;
+        }
+        long userId = Long.parseLong(claims.getSubject());
+        String appSessionToken = redisCache.getCacheObject(appRedisTokenKey(userId));
+        if (appSessionToken != null) {
+            if (!appSessionToken.equals(token)) {
+                return null;
+            }
+            if (renewSession) {
+                renewAppSession(redisCache, userId, token);
+            }
+            return userId;
+        }
+        if (isTokenExpired(claims.getExpiration())) {
+            return null;
+        }
+        return userId;
+    }
+
+    public int getExpireSeconds() {
+        return expire > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) expire;
     }
 
     public String getSecret() {
@@ -79,7 +154,7 @@ public class JwtUtils {
     }
 
     public String getHeader() {
-        return header;
+        return StringUtils.isEmpty(header) ? "AppToken" : header;
     }
 
     public void setHeader(String header) {

+ 9 - 3
fs-company-app/src/main/java/com/fs/core/PermissionService.java

@@ -36,9 +36,15 @@ public class PermissionService
     public boolean hasPermi(String permission)
     {
 
-        String headValue = ServletUtils.getRequest().getHeader("APPToken");
-        Claims claims=jwtUtils.getClaimByToken(headValue);
-        String userId = claims.getSubject().toString();
+        String headValue = ServletUtils.getRequest().getHeader(jwtUtils.getHeader());
+        if (StringUtils.isEmpty(headValue) || !jwtUtils.isAccessTokenValid(redisCache, headValue)) {
+            return false;
+        }
+        Claims claims = jwtUtils.getClaimByToken(headValue);
+        if (claims == null || StringUtils.isEmpty(claims.getSubject())) {
+            return false;
+        }
+        String userId = claims.getSubject();
         //获取用户所有的权限
         String permss=redisCache.getCacheObject("perms:"+userId);
         if(StringUtils.isNotEmpty(permss)){

+ 31 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStorePromotionScrmController.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.hisStore.service.IFsStorePromotionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 公司端-订单阶梯满减活动
+ */
+@RestController
+@RequestMapping("/store/store/storePromotion")
+public class FsStorePromotionScrmController extends BaseController {
+
+    @Autowired
+    private IFsStorePromotionService promotionService;
+
+    /**
+     * 查询订单关联的阶梯满减活动详情
+     */
+    @PreAuthorize("@ss.hasAnyPermi('store:storeOrder:query,store:storeAfterSales:query')")
+    @GetMapping("/order/{orderId}")
+    public AjaxResult getOrderPromotionDetail(@PathVariable Long orderId) {
+        return AjaxResult.success(promotionService.selectOrderPromotionDetailByOrderId(orderId));
+    }
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -180,6 +180,8 @@ public interface CompanyMapper
 
     List<OptionsVO> selectAllCompanyList(@Param("deptId") Long deptId);
 
+    List<Long> selectCompanyIdsBySysDeptScope(@Param("sysDeptId") Long sysDeptId);
+
     @Select("select company_id from company")
     List<Long> selectCompanyIds();
 

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -32,6 +32,11 @@ public interface ICompanyService
 {
      List<OptionsVO> selectAllCompanyList(Long deptId) ;
 
+    /**
+     * 按系统部门数据权限查询销售公司ID(含下级部门,sysDeptId 为空则查全部)
+     */
+    List<Long> selectCompanyIdsBySysDeptScope(Long sysDeptId);
+
     /**
      * 查询企业
      *

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -127,6 +127,11 @@ public class CompanyServiceImpl implements ICompanyService
         return companyMapper.selectAllCompanyList(deptId);
     }
 
+    @Override
+    public List<Long> selectCompanyIdsBySysDeptScope(Long sysDeptId) {
+        return companyMapper.selectCompanyIdsBySysDeptScope(sysDeptId);
+    }
+
     private final String IPAD_DEACTIVATE = "ipadDeactivate:";
 
     /**

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsCoursePlaySourceConfig.java

@@ -96,6 +96,11 @@ public class FsCoursePlaySourceConfig {
      */
     private Integer isMall;
 
+    /**
+     * 是否看课小程序:1是 0否,空视为是
+     */
+    private Integer isCourseMini;
+
     /**
      * 小程序状态:0正常,1半封禁,2封禁
      */

+ 8 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCoursePlaySourceConfigMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.vo.FsCoursePlaySourceConfigVO;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
 import java.util.Map;
@@ -14,4 +15,11 @@ public interface FsCoursePlaySourceConfigMapper extends BaseMapper<FsCoursePlayS
      * 查询点播配置列表
      */
     List<FsCoursePlaySourceConfigVO> selectCoursePlaySourceConfigVOListByMap(@Param("params") Map<String, Object> params);
+
+    /**
+     * 获取小程序
+     * **/
+    @Select("select * from fs_course_play_source_config where appid = #{appId}")
+    FsCoursePlaySourceConfigVO selectInfoByAppId(@Param("appId") String appId);
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigCreateParam.java

@@ -50,6 +50,10 @@ public class FsCoursePlaySourceConfigCreateParam {
 
     @ApiModelProperty("是否是互医/商城小程序")
     private Integer isMall;
+
+    @ApiModelProperty("是否看课小程序:1是 0否,空视为是")
+    private Integer isCourseMini;
+
     private Long createDeptId;
 
 

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigEditParam.java

@@ -45,6 +45,10 @@ public class FsCoursePlaySourceConfigEditParam {
 
     @ApiModelProperty("是否是互医/商城小程序")
     private Integer isMall;
+
+    @ApiModelProperty("是否看课小程序:1是 0否,空视为是")
+    private Integer isCourseMini;
+
     private Long createDeptId;
 
     @ApiModelProperty("小程序状态:0正常,1半封禁,2封禁")

+ 15 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java

@@ -16,6 +16,16 @@ public class FsCourseWatchLogListParam implements Serializable {
 
     private Long companyId;
 
+    /**
+     * 多销售公司筛选(选「全部」时由后端按数据权限组装)
+     */
+    private List<Long> companyIds;
+
+    /**
+     * 是否查询权限内全部销售公司:1是(后端拼接 companyIds)
+     */
+    private Integer allCompany;
+
     private Long companyUserId;
 
     private String companyUserName;
@@ -111,4 +121,9 @@ public class FsCourseWatchLogListParam implements Serializable {
      */
     private String qwUserName;
     private String ids;
+
+    /**
+     * 总后台数据隔离:按销售公司所属系统部门(company.dept_id)过滤
+     */
+    private Long sysDeptId;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/course/service/IFsCoursePlaySourceConfigService.java

@@ -22,4 +22,11 @@ public interface IFsCoursePlaySourceConfigService extends IService<FsCoursePlayS
      * @param key 缓存KEY
      * **/
     String selectConfigByAppId(String appId,String key);
+
+    /**
+     * 根据小程序 appId 解析用户业务类型
+     *
+     * @return 1看课 2商城,未配置时默认看课
+     */
+    Integer resolveBizTypeByAppId(String appId);
 }

+ 17 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCoursePlaySourceConfigServiceImpl.java

@@ -61,4 +61,21 @@ public class FsCoursePlaySourceConfigServiceImpl extends ServiceImpl<FsCoursePla
         }
         return null;
     }
+
+    @Override
+    public Integer resolveBizTypeByAppId(String appId) {
+        if (StringUtils.isBlank(appId)) {
+            return 1;
+        }
+        FsCoursePlaySourceConfig config = baseMapper.selectOne(new LambdaQueryWrapper<FsCoursePlaySourceConfig>()
+                .select(FsCoursePlaySourceConfig::getAppid, FsCoursePlaySourceConfig::getIsCourseMini)
+                .eq(FsCoursePlaySourceConfig::getAppid, appId)
+                .eq(FsCoursePlaySourceConfig::getIsDel, 0)
+                .last("limit 1"));
+        if (config == null) {
+            return null;
+        }
+        Integer isCourseMini = config.getIsCourseMini();
+        return isCourseMini == null?null:isCourseMini == 1 ? 1 : 2;
+    }
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/vo/FsCoursePlaySourceConfigVO.java

@@ -54,6 +54,12 @@ public class FsCoursePlaySourceConfigVO {
      * 是否是互医/商城小程序
      */
     private Integer isMall;
+
+    /**
+     * 是否看课小程序:1是 0否,空视为是
+     */
+    private Integer isCourseMini;
+
     private Long createDeptId;
 
     @ApiModelProperty("小程序状态:0正常,1半封禁,2封禁")

+ 3 - 0
fs-service/src/main/java/com/fs/his/domain/FsUser.java

@@ -166,6 +166,9 @@ public class FsUser extends BaseEntity
      * 小程序appId,多个用逗号分隔
      */
     private String appId;
+
+    /** 业务类型:1看课 2商城 */
+    private Integer bizType;
     public void setNickName(String nickname)
     {
         if(StringUtils.isNotEmpty(nickname)){

+ 37 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserBizTypeEnum.java

@@ -0,0 +1,37 @@
+package com.fs.his.enums;
+
+/**
+ * 用户业务类型
+ */
+public enum FsUserBizTypeEnum {
+
+    COURSE(1, "看课"),
+    MALL(2, "商城");
+
+    private final Integer code;
+    private final String label;
+
+    FsUserBizTypeEnum(Integer code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * 空 bizType 视为看课
+     */
+    public static Integer normalize(Integer bizType) {
+        return bizType == null ? COURSE.code : bizType;
+    }
+
+    public static boolean isSameBizType(Integer userBizType, Integer expectedBizType) {
+        return normalize(userBizType).equals(normalize(expectedBizType));
+    }
+}

+ 19 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -122,6 +122,16 @@ public interface FsUserMapper
     @Select("select * from fs_user where is_del = 0 and phone=#{phone}")
     FsUser selectFsUserByPhone(String phone);
 
+    @Select({"<script>",
+            "select * from fs_user where is_del = 0 and phone = #{phone}",
+            "<choose>",
+            "  <when test='bizType != null and bizType == 2'> and biz_type = 2 </when>",
+            "  <otherwise> and (biz_type = 1 or biz_type is null) </otherwise>",
+            "</choose>",
+            " limit 1",
+            "</script>"})
+    FsUser selectFsUserByPhoneAndBizType(@Param("phone") String phone, @Param("bizType") Integer bizType);
+
     @Select("select * from fs_user where phone=#{phone} limit 1")
     FsUser selectFsUserByPhoneLimitOne(String phone);
 
@@ -454,6 +464,15 @@ public interface FsUserMapper
 
     List<FsUser> selectFsUserListByPhone(@Param("phone") String phone, @Param("appid") String appid);
 
+    @Select({"<script>",
+            "select * from fs_user where phone = #{phone}",
+            "<choose>",
+            "  <when test='bizType != null and bizType == 2'> and biz_type = 2 </when>",
+            "  <otherwise> and (biz_type = 1 or biz_type is null) </otherwise>",
+            "</choose>",
+            "</script>"})
+    List<FsUser> selectFsUserListByPhoneAndBizType(@Param("phone") String phone, @Param("appid") String appid, @Param("bizType") Integer bizType);
+
     void updatePasswordByPhone(@Param("password")String password, @Param("encryptPhone")String encryptPhone);
 
     @Select("select * from fs_user where phone=#{phone} AND app_id LIKE CONCAT('%',#{appid},'%')")

+ 10 - 1
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -109,6 +109,11 @@ public interface IFsUserService
 
     FsUser selectFsUserByPhone(String phone);
 
+    /**
+     * 按手机号和业务类型查询用户(看课与商城互斥)
+     */
+    FsUser selectFsUserByPhoneAndBizType(String phone, Integer bizType);
+
     List<FsUserAddress> selectFsUserAddressByUserId(Long userId);
 
     List<UserVo> selectUserVOList(String name);
@@ -251,6 +256,11 @@ public interface IFsUserService
 
     List<FsUser> selectFsUserListByPhone(String phone,String appid);
 
+    /**
+     * 按手机号、appId、业务类型查询用户(看课与商城互斥)
+     */
+    List<FsUser> selectFsUserListByPhoneAndBizType(String phone, String appid, Integer bizType);
+
     R updatePasswordByPhone(String password, String encryptPhone);
 
     /**
@@ -266,5 +276,4 @@ public interface IFsUserService
      * @return R
      * **/
     R batchAddUserAndSaleFriend(List<FsUser> userList,Long compayUserId);
-
 }

+ 10 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -317,6 +317,11 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.selectFsUserByPhone(phone);
     }
 
+    @Override
+    public FsUser selectFsUserByPhoneAndBizType(String phone, Integer bizType) {
+        return fsUserMapper.selectFsUserByPhoneAndBizType(phone, bizType);
+    }
+
     @Override
     public List<FsUserAddress> selectFsUserAddressByUserId(Long userId) {
         FsUserAddress fsUserAddress = new FsUserAddress();
@@ -1464,6 +1469,11 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.selectFsUserListByPhone(phone,appid);
     }
 
+    @Override
+    public List<FsUser> selectFsUserListByPhoneAndBizType(String phone, String appid, Integer bizType) {
+        return fsUserMapper.selectFsUserListByPhoneAndBizType(phone, appid, bizType);
+    }
+
     @Override
     public R updatePasswordByPhone(String password, String encryptPhone) {
         fsUserMapper.updatePasswordByPhone(password, encryptPhone);

+ 1 - 1
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -100,6 +100,6 @@ public class PhoneUtil {
     }
 
     public static void main(String[] args) {
-        System.out.println(encryptPhone("13121679182"));
+        System.out.println(encryptPhone("13309215228"));
     }
 }

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStorePreparedPriceChange.java

@@ -46,6 +46,10 @@ public class FsStorePreparedPriceChange extends BaseEntity{
     @Excel(name = "销售公司ID")
     private Long companyId;
 
+    /** 满减活动ID */
+    @Excel(name = "满减活动ID")
+    private Long promotionActivityId;
+
     /** 销售ID */
     @Excel(name = "销售ID")
     private Long companyUserId;

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/domain/FsStorePromotionActivity.java

@@ -39,7 +39,7 @@ public class FsStorePromotionActivity extends BaseEntity {
     @JsonProperty("endTime")
     private Date activityEndTime;
 
-    /** 1全场 2指定分类 3指定商品 */
+    /** 1全场 2指定分类(存商品类型) 3指定商品 */
     private Integer scopeType;
 
     /** 阶梯类型:1金额 2折扣 */

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

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.his.vo.FsDoctorVO;
+import com.fs.hisStore.vo.FsStorePromotionUserDisplayVO;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
@@ -557,4 +558,15 @@ public class FsStoreScrm extends BaseEntity {
     @TableField(exist = false)
     private Integer pharmacyCodeFilter;
 
+    /**
+     * 活动类型 1金额 2折扣
+     * **/
+    @TableField(exist = false)
+    private Integer tierType;
+
+    /**
+     * 进行中的阶梯满减/折扣活动(用户端展示)
+     */
+    @TableField(exist = false)
+    private List<FsStorePromotionUserDisplayVO> promotionActivities;
 }

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

@@ -29,5 +29,7 @@ public class FsStoreOrderComputeDTO implements Serializable
 
     private BigDecimal deductionPrice;
 
+    /** 活动优惠金额(满减/折扣) */
+    private BigDecimal promotionDiscountAmount;
 
 }

+ 18 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePromotionReportMapper.java

@@ -0,0 +1,18 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.param.FsStorePromotionReportParam;
+import com.fs.hisStore.vo.FsStorePromotionReportOrderVO;
+import com.fs.hisStore.vo.FsStorePromotionReportStatsVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 满减活动报表
+ */
+public interface FsStorePromotionReportMapper {
+
+    FsStorePromotionReportStatsVO selectPromotionReportStats(@Param("param") FsStorePromotionReportParam param);
+
+    List<FsStorePromotionReportOrderVO> selectPromotionReportOrderList(@Param("param") FsStorePromotionReportParam param);
+}

+ 37 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStorePromotionReportParam.java

@@ -0,0 +1,37 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 满减活动报表查询参数
+ */
+@Data
+public class FsStorePromotionReportParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 订单号 */
+    private String orderCode;
+
+    /** 所属公司 */
+    private Long companyId;
+
+    /** 所属销售 */
+    private Long companyUserId;
+
+    /** 用户昵称 */
+    private String nickname;
+
+    /** 订单状态 */
+    private Integer status;
+
+    /**
+     * 订单来源:null/空全部,1销售订单(有company_user_id),2商城订单(无company_user_id)
+     */
+    private Integer orderSourceType;
+}

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

@@ -2,7 +2,12 @@ package com.fs.hisStore.service;
 
 import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.dto.FsStorePromotionActivityDTO;
+import com.fs.hisStore.param.FsStorePromotionReportParam;
+import com.fs.hisStore.vo.FsStoreOrderPromotionDetailVO;
 import com.fs.hisStore.vo.FsStorePromotionDetailVO;
+import com.fs.hisStore.vo.FsStorePromotionReportOrderVO;
+import com.fs.hisStore.vo.FsStorePromotionReportStatsVO;
+import com.fs.hisStore.vo.FsStorePromotionUserDisplayVO;
 
 import java.util.List;
 
@@ -12,6 +17,11 @@ public interface IFsStorePromotionService {
 
     List<FsStorePromotionActivity> selectFsStorePromotionList(FsStorePromotionActivity query);
 
+    /**
+     * 查询店铺当前进行中的活动(用户端展示)
+     */
+    List<FsStorePromotionUserDisplayVO> listActivePromotionDisplayByStoreId(Long storeId);
+
     int insertFsStorePromotion(FsStorePromotionActivityDTO dto);
 
     int updateFsStorePromotion(FsStorePromotionActivityDTO dto);
@@ -25,4 +35,16 @@ public interface IFsStorePromotionService {
     void fillDisplayLabels(List<FsStorePromotionActivity> list);
 
     void fillDisplayLabels(FsStorePromotionActivity activity);
+
+    /**
+     * 查询订单关联的阶梯满减活动详情
+     *
+     * @param orderId 订单ID
+     * @return 活动详情,未参与活动时返回 null
+     */
+    FsStoreOrderPromotionDetailVO selectOrderPromotionDetailByOrderId(Long orderId);
+
+    FsStorePromotionReportStatsVO selectPromotionReportStats(FsStorePromotionReportParam param);
+
+    List<FsStorePromotionReportOrderVO> selectPromotionReportOrderList(FsStorePromotionReportParam param);
 }

+ 291 - 101
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -758,7 +758,16 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             payPrice = BigDecimal.ZERO;
         }
 
-        payPrice = subtractAutoPromotionDiscount(uid, carts, null, param.getCouponUserId(), payPrice);
+        // 制单改过价的跳过满减计算,避免重复优惠
+        boolean skipPromotion = false;
+        if (StringUtils.isNotEmpty(param.getCreateOrderKey())) {
+            Boolean isAdjusted = redisCache.getCacheObject("createOrderIsAdjusted:" + param.getCreateOrderKey());
+            skipPromotion = (isAdjusted != null && isAdjusted);
+        }
+        PromotionApplyResult promotionResult = skipPromotion
+                ? new PromotionApplyResult(payPrice, BigDecimal.ZERO)
+                : applyAutoPromotionDiscount(uid, carts, null, param.getCouponUserId(), payPrice);
+        payPrice = promotionResult.getPayPrice();
 
         //优惠券
         if (param.getCouponUserId() != null) {
@@ -778,6 +787,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 .usedIntegral(usedIntegral)
                 .payIntegral(priceGroup.getPayIntegral())
                 .serviceFee(serviceFee)
+                .promotionDiscountAmount(promotionResult.getPromotionDiscountAmount())
                 .build();
     }
 
@@ -906,7 +916,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 storeOrder.setServiceFee(config.getServiceFee());
             }
 
-            applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
+            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
 
             //后台制单处理
             if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
@@ -971,8 +981,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if (flag == 0) {
                 return R.error("订单创建失败");
             }
-            promotionComputeService.recordPendingUsage(storeOrder,
-                    promotionComputeService.autoApplyBestPromotion(userId, carts, param.getStoreId(), param.getCouponUserId()));
+            promotionComputeService.recordPendingUsage(storeOrder, promotionResult);
             if (!isPay && storeOrder.getCompanyId() != null) {
                 // 添加订单审核
                 addOrderAudit(storeOrder);
@@ -1091,6 +1100,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     redisCache.deleteObject("createOrderKey:" + createOrderKey);
                     redisCache.deleteObject("orderCarts:" + createOrderKey);
                     redisCache.deleteObject("createOrderMoney:" + createOrderKey);
+                    redisCache.deleteObject("createOrderIsAdjusted:" + createOrderKey);
                 }
 
                 //货到付款自定义金额 key改为id存储
@@ -1457,8 +1467,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 try {
                     JSONObject js = JSON.parseObject(itemList.get(0).getJsonInfo());
                     String productName = js.getString("productName");
-                    if (StringUtils.isNotEmpty(productName)) {
+                    String commonName = js.getString("commonName");
+                    if (StringUtils.isNotEmpty(productName) && !productName.equals("-")) {
                         itemDesc = productName;
+                    }else {
+                        itemDesc = commonName;
                     }
                 } catch (Exception ignore) {
                 }
@@ -3017,6 +3030,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
         redisCache.setCacheObject("orderKey:" + uuid, carts.stream().map(c->c.getId().toString()).collect(Collectors.joining(",")) , 24, TimeUnit.HOURS);//处理订单过期问题
         redisCache.setCacheObject("createOrderMoney:" + uuid, totalMoney, 24, TimeUnit.HOURS);
+        redisCache.setCacheObject("createOrderIsAdjusted:" + uuid, false, 24, TimeUnit.HOURS);
         return R.ok().put("orderKey", uuid).put("carts", carts);
     }
 
@@ -3033,7 +3047,21 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (StringUtils.isEmpty(key)) {
             throw new CustomException("订单已过期", 501);
         }
-        return R.ok().put("carts", carts).put("totalMoney", money).put("payType", payType);
+        // 计算满减活动优惠(改过价则跳过,避免重复优惠)
+        BigDecimal promotionDiscountAmount = BigDecimal.ZERO;
+        BigDecimal payPrice = money != null ? money : BigDecimal.ZERO;
+        Boolean isAdjusted = redisCache.getCacheObject("createOrderIsAdjusted:" + createOrderKey);
+        if ((isAdjusted == null || !isAdjusted) && CollectionUtils.isNotEmpty(carts) && key.contains("-")) {
+            Long userId = Long.parseLong(key.substring(key.indexOf("-") + 1));
+            PromotionApplyResult promotionResult = this.applyAutoPromotionDiscount(userId, carts, null, null, payPrice);
+            payPrice = promotionResult.getPayPrice();
+            promotionDiscountAmount = promotionResult.getPromotionDiscountAmount();
+        }
+        return R.ok()
+                .put("carts", carts)
+                .put("totalMoney", payPrice)
+                .put("payType", payType)
+                .put("promotionDiscountAmount", promotionDiscountAmount);
     }
 
     @Override
@@ -3200,16 +3228,26 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             preparedPriceChange.setCreateBy(userId.toString());//创建人
             preparedPriceChange.setCreateOrderKey(createOrderKey);//唯一key
             preparedPriceChange.setCartIds(carts.stream().map(c -> c.getId().toString()).collect(Collectors.joining(",")));//购物车IDs
+            // 捕获满减活动ID,用于后续分享时校验活动是否进行中
+            if (key.contains("-")) {
+                Long cartUserId = Long.parseLong(key.substring(key.indexOf("-") + 1));
+                FsStorePromotionComputeResultVO promotion = promotionComputeService.autoApplyBestPromotion(cartUserId, carts, null, null);
+                if (promotion != null && promotion.getPromotionActivityId() != null) {
+                    preparedPriceChange.setPromotionActivityId(promotion.getPromotionActivityId());
+                }
+            }
             fsStorePreparedPriceChangeService.insertFsStorePreparedPriceChange(preparedPriceChange);
 
             //缓存半小时
             redisCache.setCacheObject("createOrderMoney:" + createOrderKey, money, 30, TimeUnit.MINUTES);
             redisCache.setCacheObject("createOrderAmount:" + createOrderKey, payAmount, 30, TimeUnit.MINUTES);//物流代收自定义金额
             redisCache.setCacheObject("createOrderPayType:" + createOrderKey, payType, 30, TimeUnit.MINUTES);//支付类型 1-全付款 3-物流代收(货到付款)
+            redisCache.setCacheObject("createOrderIsAdjusted:" + createOrderKey, true, 30, TimeUnit.MINUTES);//改价标记,避免重复计算活动优惠
         } else {
             redisCache.setCacheObject("createOrderMoney:" + createOrderKey, money, 24, TimeUnit.HOURS);
             redisCache.setCacheObject("createOrderAmount:" + createOrderKey, payAmount, 24, TimeUnit.HOURS);//物流代收自定义金额
             redisCache.setCacheObject("createOrderPayType:" + createOrderKey, payType, 24, TimeUnit.HOURS);//支付类型 1-全付款 3-物流代收(货到付款)
+            redisCache.setCacheObject("createOrderIsAdjusted:" + createOrderKey, true, 24, TimeUnit.HOURS);//改价标记,避免重复计算活动优惠
         }
         return R.ok().put("disabled", config.getIsPreparedPriceChange() != null && config.getIsPreparedPriceChange());
     }
@@ -5289,45 +5327,106 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Transactional
     @Synchronized
     public String payConfirmMultiStore(Integer type, Long orderId, String payCode, String tradeNo, String bankTransactionId, String bankSerialNo) {
-        //支付订单
-//        FsStoreOrder order = null;
         try {
             List<FsStoreOrderScrm> orders = new ArrayList<>();
             if (type.equals(1)) {
                 List<FsStorePaymentScrm> storePayments = paymentService.selectFsStorePaymentByPaymentCode(payCode);
-                if (storePayments != null && !storePayments.isEmpty()) {
-                    for (FsStorePaymentScrm storePayment : storePayments) {
-                        if (storePayment == null || !storePayment.getStatus().equals(0)) {
-                            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                            return "";
-                        }
-                        storePayment.setStatus(1);
-                        storePayment.setPayTime(new Date());
-                        storePayment.setTradeNo(tradeNo);
-                        storePayment.setBankSerialNo(bankSerialNo);
-                        storePayment.setBankTransactionId(bankTransactionId);
-                        paymentService.updateFsStorePayment(storePayment);
-                        if (storePayment.getBusinessOrderId() != null && storePayment.getBusinessOrderId().length() > 30) {
-                            orders.addAll(fsStoreOrderMapper.getStoreOrderByCombinationId(storePayment.getBusinessOrderId()));
-                        } else {
-                            orders.add(fsStoreOrderMapper.selectFsStoreOrderById(storePayment.getOrderId()));
+                if (storePayments == null || storePayments.isEmpty()) {
+                    logger.warn("payConfirmMultiStore: 未找到支付记录, payCode={}", payCode);
+                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+                    return "";
+                }
 
+                // 【关键修复】第一步:先加载并校验所有订单状态,避免 payment 已更新但 order 更新失败
+                boolean allPaymentAlreadyPaid = true;
+                for (FsStorePaymentScrm storePayment : storePayments) {
+                    if (storePayment == null) {
+                        continue;
+                    }
+                    // 检查 payment 是否已处理(幂等判断)
+                    if (storePayment.getStatus() != null && !storePayment.getStatus().equals(0)) {
+                        // payment 已经是非待支付状态,不再更新 payment,但仍需校验订单
+                        logger.info("payConfirmMultiStore: payment已处理, paymentId={}, status={}, payCode={}",
+                                storePayment.getPaymentId(), storePayment.getStatus(), payCode);
+                        allPaymentAlreadyPaid = allPaymentAlreadyPaid && true;
+                    } else {
+                        allPaymentAlreadyPaid = false;
+                    }
+                    // 加载关联订单
+                    if (storePayment.getBusinessOrderId() != null && storePayment.getBusinessOrderId().length() > 30) {
+                        orders.addAll(fsStoreOrderMapper.getStoreOrderByCombinationId(storePayment.getBusinessOrderId()));
+                    } else {
+                        FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(storePayment.getOrderId());
+                        if (order != null) {
+                            orders.add(order);
                         }
                     }
                 }
 
+                if (orders.isEmpty()) {
+                    logger.warn("payConfirmMultiStore: 未找到关联订单, payCode={}", payCode);
+                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+                    return "";
+                }
 
+                // 【关键修复】第二步:校验订单状态,跳过已支付的订单
                 for (FsStoreOrderScrm order : orders) {
-                    if (order != null && !order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
-                        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                        return "";
+                    if (order == null) {
+                        continue;
                     }
-                    if (order != null && !order.getPaid().equals(0)) {
+                    // 如果订单已经支付完成,跳过(幂等)
+                    if (order.getPaid() != null && order.getPaid().equals(OrderInfoEnum.PAY_STATUS_1.getValue())
+                            && order.getStatus() != null && order.getStatus().equals(OrderInfoEnum.STATUS_1.getValue())) {
+                        logger.info("payConfirmMultiStore: 订单已支付,跳过, orderId={}, orderCode={}", order.getId(), order.getOrderCode());
+                        continue;
+                    }
+                    // 订单状态不是待支付且也不是已支付,说明订单状态异常
+                    if (order.getStatus() != null && !order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())
+                            && !order.getStatus().equals(OrderInfoEnum.STATUS_1.getValue())) {
+                        logger.error("payConfirmMultiStore: 订单状态异常, orderId={}, status={}, orderCode={}",
+                                order.getId(), order.getStatus(), order.getOrderCode());
                         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                         return "";
                     }
                 }
 
+                // 【关键修复】第三步:先更新订单状态,再更新 payment(保证订单优先落库)
+                for (FsStoreOrderScrm order : orders) {
+                    if (order == null) {
+                        continue;
+                    }
+                    // 跳过的已支付订单
+                    if (order.getPaid() != null && order.getPaid().equals(OrderInfoEnum.PAY_STATUS_1.getValue())) {
+                        continue;
+                    }
+
+                    FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+                    storeOrder.setId(order.getId());
+                    storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
+                    storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+                    storeOrder.setPayTime(new Date());
+                    fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+
+                    // 增加状态日志
+                    orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
+                            OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
+                }
+
+                // 【关键修复】第四步:更新 payment 状态(订单已落库,payment 更新即使异常也有 orders 兜底)
+                for (FsStorePaymentScrm storePayment : storePayments) {
+                    if (storePayment == null) {
+                        continue;
+                    }
+                    if (storePayment.getStatus() != null && storePayment.getStatus().equals(0)) {
+                        storePayment.setStatus(1);
+                        storePayment.setPayTime(new Date());
+                        storePayment.setTradeNo(tradeNo);
+                        storePayment.setBankSerialNo(bankSerialNo);
+                        storePayment.setBankTransactionId(bankTransactionId);
+                        paymentService.updateFsStorePayment(storePayment);
+                    }
+                }
+
             } else if (type.equals(2)) {
                 //货到付款
                 FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
@@ -5336,55 +5435,58 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     return "";
                 }
                 orders.add(order);
+
+                FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+                storeOrder.setId(order.getId());
+                storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
+                storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+                storeOrder.setPayTime(new Date());
+                fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+
+                orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
+                        OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
             }
-            //写入公司佣金
+
+            //第五步:OMS推送、佣金等后置处理(非关键路径,异常不影响订单支付状态)
             if (orders.size() > 0) {
                 for (FsStoreOrderScrm order : orders) {
-                    if (order.getCompanyId() != null && order.getCompanyId() > 0) {
-                        companyService.addCompanyTuiMoney(order);
+                    if (order == null) {
+                        continue;
+                    }
+                    try {
+                        if (order.getCompanyId() != null && order.getCompanyId() > 0) {
+                            companyService.addCompanyTuiMoney(order);
+                        }
+                    } catch (Exception e) {
+                        logger.error("payConfirmMultiStore 公司佣金处理失败, orderId={}", order.getId(), e);
+                    }
+                    try {
+                        userService.incPayCount(order.getUserId());
+                    } catch (Exception e) {
+                        logger.error("payConfirmMultiStore 用户购买次数增加失败, orderId={}", order.getId(), e);
                     }
-                    //增加用户购买次数
-                    userService.incPayCount(order.getUserId());
-                    //增加状态
-                    orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
-                            OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
-                    //        FsUser user=userService.selectFsUserById(order.getUserId());
-                    //        //增加流水
-                    //        String payTypeMsg = PayTypeEnum.WEIXIN.getDesc();
-                    //        billService.addBill(user.getUserId(),
-                    //                BillDetailEnum.CATEGORY_1.getValue(),
-                    //                0,
-                    //                BillDetailEnum.TYPE_1.getDesc(),
-                    //                order.getPayPrice().doubleValue(), user.getNowMoney().doubleValue(),
-                    //                payTypeMsg + order.getPayPrice() + "元购买商品",order.getId().toString());
-
-                    FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
-                    storeOrder.setId(order.getId());
-                    storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
-                    storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
-                    storeOrder.setPayTime(new Date());
-                    fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
                     try {
                         //非处方直接提交OMS
                         if (order.getIsPrescribe() != null && order.getIsPrescribe().equals(0)) {
                             createOmsOrder(order.getId());
                         } else if (order.getIsPrescribe() != null && order.getIsPrescribe().equals(1)) {
-                            //是否已开方
                             FsPrescribeScrm prescribe = prescribeService.selectFsPrescribeByOrderId(order.getId());
                             if (prescribe != null && prescribe.getStatus() == 1) {
                                 createOmsOrder(order.getId());
                             }
                         }
                     } catch (Exception e) {
-                        // 预防支付成功但上传聚水潭失败导致支付状态未改变
                         logger.error("payConfirmMultiStore 创建ERP订单失败, orderId={}, orderCode={}", order.getId(), order.getOrderCode(), e);
                     }
-                    //处理佣金 套餐不分佣金
-                    if (order.getIsPackage() != 1 && order.getTuiUserId() != null && order.getTuiUserId() > 0) {
-                        FsStoreOrderItemScrm orderItemMap = new FsStoreOrderItemScrm();
-                        orderItemMap.setOrderId(order.getId());
-                        List<FsStoreOrderItemScrm> items = storeOrderItemService.selectFsStoreOrderItemList(orderItemMap);
-                        userService.addTuiMoney(order, items);
+                    try {
+                        if (order.getIsPackage() != 1 && order.getTuiUserId() != null && order.getTuiUserId() > 0) {
+                            FsStoreOrderItemScrm orderItemMap = new FsStoreOrderItemScrm();
+                            orderItemMap.setOrderId(order.getId());
+                            List<FsStoreOrderItemScrm> items = storeOrderItemService.selectFsStoreOrderItemList(orderItemMap);
+                            userService.addTuiMoney(order, items);
+                        }
+                    } catch (Exception e) {
+                        logger.error("payConfirmMultiStore 佣金处理失败, orderId={}", order.getId(), e);
                     }
                 }
             }
@@ -5398,35 +5500,48 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     @Override
     public String payRemainConfirmMultiStore(String payCode, String tradeNo, String bankTransactionId, String bankSerialNo) {
-        //支付订单
-//        FsStoreOrder order = null;
         String orderIds = "";
         List<FsStorePaymentScrm> storePayments = paymentService.selectFsStorePaymentByPaymentCode(payCode);
-        if (storePayments != null && !storePayments.isEmpty()) {
-            for (int i = 0; i < storePayments.size(); i++) {
-                if (storePayments.get(i) == null || !storePayments.get(i).getStatus().equals(0)) {
-                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                    return "";
-                }
-                storePayments.get(i).setStatus(1);
-                storePayments.get(i).setPayTime(new Date());
-                storePayments.get(i).setTradeNo(tradeNo);
-                storePayments.get(i).setBankSerialNo(bankSerialNo);
-                storePayments.get(i).setBankTransactionId(bankTransactionId);
-                storePayments.get(i).setIsPayRemain(1);
-                paymentService.updateFsStorePayment(storePayments.get(i));
-                if (i == 0) {
-                    orderIds = orderIds + (storePayments.get(i).getOrderId());
-                } else {
-                    orderIds = orderIds + "," + storePayments.get(i).getOrderId();
-                }
+        if (storePayments == null || storePayments.isEmpty()) {
+            logger.warn("payRemainConfirmMultiStore: 未找到支付记录, payCode={}", payCode);
+            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+            return "";
+        }
+
+        // 【修复】先收集 orderIds 并校验订单
+        for (int i = 0; i < storePayments.size(); i++) {
+            FsStorePaymentScrm sp = storePayments.get(i);
+            if (sp == null) {
+                continue;
+            }
+            // 幂等:payment已经处理过则跳过payment更新,但订单仍需校验
+            if (sp.getStatus() != null && !sp.getStatus().equals(0) && sp.getIsPayRemain() != null && sp.getIsPayRemain().equals(1)) {
+                logger.info("payRemainConfirmMultiStore: payment已处理, paymentId={}, payCode={}", sp.getPaymentId(), payCode);
+            }
+            if (i == 0) {
+                orderIds = orderIds + (sp.getOrderId());
+            } else {
+                orderIds = orderIds + "," + sp.getOrderId();
             }
         }
 
-//        order = fsStoreOrderMapper.selectFsStoreOrderById(storePayment.getOrderId());
+        if (StringUtils.isBlank(orderIds)) {
+            logger.warn("payRemainConfirmMultiStore: orderIds为空, payCode={}", payCode);
+            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+            return "";
+        }
+
+        // 【修复】先更新订单状态
         List<FsStoreOrderScrm> orders = fsStoreOrderMapper.selectFsStoreOrderByOrderIds(orderIds);
         for (FsStoreOrderScrm order : orders) {
-            //增加状态
+            if (order == null) {
+                continue;
+            }
+            // 幂等:已支付尾款的订单跳过
+            if (order.getIsPayRemain() != null && order.getIsPayRemain().equals(1)) {
+                logger.info("payRemainConfirmMultiStore: 订单已支付尾款,跳过, orderId={}", order.getId());
+                continue;
+            }
             orderStatusService.create(order.getId(), OrderLogEnum.PAY_REMAIN_ORDER_SUCCESS.getValue(),
                     OrderLogEnum.PAY_REMAIN_ORDER_SUCCESS.getDesc());
 
@@ -5436,6 +5551,21 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
         }
 
+        // 【修复】订单已落库后再更新 payment
+        for (FsStorePaymentScrm storePayment : storePayments) {
+            if (storePayment == null) {
+                continue;
+            }
+            if (storePayment.getStatus() != null && storePayment.getStatus().equals(0)) {
+                storePayment.setStatus(1);
+                storePayment.setPayTime(new Date());
+                storePayment.setTradeNo(tradeNo);
+                storePayment.setBankSerialNo(bankSerialNo);
+                storePayment.setBankTransactionId(bankTransactionId);
+                storePayment.setIsPayRemain(1);
+                paymentService.updateFsStorePayment(storePayment);
+            }
+        }
 
         return "success";
     }
@@ -5603,7 +5733,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setShippingType(1);
             storeOrder.setCreateTime(new Date());
             storeOrder.setStatus(0);
-            applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
+            FsStorePromotionComputeResultVO promotionResult = applyPromotionFields(storeOrder, carts, userId, param.getCouponUserId(), param.getStoreId());
             //后台制单处理
             if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
                 storeOrder.setPayPrice(param.getPayPrice());
@@ -5635,8 +5765,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if (flag == 0) {
                 return R.error("订单创建失败");
             }
-            promotionComputeService.recordPendingUsage(storeOrder,
-                    promotionComputeService.autoApplyBestPromotion(userId, carts, param.getStoreId(), param.getCouponUserId()));
+            promotionComputeService.recordPendingUsage(storeOrder, promotionResult);
             //收款单更新
 //            if(param.getPaymentId()!=null&&param.getPaymentId()>0){
 //                FsStorePayment payment=new FsStorePayment();
@@ -5769,6 +5898,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 redisCache.deleteObject("createOrderKey:" + param.getCreateOrderKey());
                 redisCache.deleteObject("orderCarts:" + param.getCreateOrderKey());
                 redisCache.deleteObject("createOrderMoney:" + param.getCreateOrderKey());
+                redisCache.deleteObject("createOrderIsAdjusted:" + param.getCreateOrderKey());
                 redisCache.deleteObject("createOrderMoneyDetails:" + param.getCreateOrderKey());
             }
             redisCache.setCacheObject("orderAmount:" + storeOrder.getId(), storeOrder.getPayMoney(), 24, TimeUnit.HOURS);//物流代收自定义金额
@@ -5791,6 +5921,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         BigDecimal payIntegralTotal = BigDecimal.ZERO;
         BigDecimal payPostageTotal = BigDecimal.ZERO;
         BigDecimal deductionPriceTotal = BigDecimal.ZERO;
+        BigDecimal promotionDiscountTotal = BigDecimal.ZERO;
 
 
         List<String> createOrderKeys = null;
@@ -5816,6 +5947,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             payIntegralTotal = NumberUtil.add(payIntegralTotal, dto.getPayIntegral());
             payPostageTotal = NumberUtil.add(payPostageTotal, dto.getPayPostage());
             deductionPriceTotal = NumberUtil.add(deductionPriceTotal, dto.getDeductionPrice());
+            if (dto.getPromotionDiscountAmount() != null) {
+                promotionDiscountTotal = NumberUtil.add(promotionDiscountTotal, dto.getPromotionDiscountAmount());
+            }
             dtos.add(dto);
         }
 //
@@ -5826,6 +5960,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 .deductionPrice(deductionPriceTotal)
                 .usedIntegral(usedIntegralTotal)
                 .payIntegral(payIntegralTotal)
+                .promotionDiscountAmount(promotionDiscountTotal)
                 .build());
         return dtos;
     }
@@ -6157,7 +6292,20 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             payPrice = BigDecimal.ZERO;
         }
 
-        payPrice = subtractAutoPromotionDiscount(uid, carts, null, param.getCouponUserId(), payPrice);
+        // 制单改过价的只算活动优惠金额供展示,不参与 payPrice 扣减
+        boolean skipPromotion = false;
+        if (StringUtils.isNotEmpty(param.getCreateOrderKey())) {
+            Boolean isAdjusted = redisCache.getCacheObject("createOrderIsAdjusted:" + param.getCreateOrderKey());
+            skipPromotion = (isAdjusted != null && isAdjusted);
+        }
+        PromotionApplyResult promotionResult;
+        if (skipPromotion) {
+            BigDecimal displayDiscountAmount = getPromotionDiscountAmount(uid, carts, null, param.getCouponUserId());
+            promotionResult = new PromotionApplyResult(payPrice, displayDiscountAmount);
+        } else {
+            promotionResult = applyAutoPromotionDiscount(uid, carts, null, param.getCouponUserId(), payPrice);
+        }
+        payPrice = promotionResult.getPayPrice();
 
         //优惠券
         if (param.getCouponUserId() != null) {
@@ -6176,41 +6324,83 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 .deductionPrice(deductionPrice)
                 .usedIntegral(usedIntegral)
                 .payIntegral(priceGroup.getPayIntegral())
+                .promotionDiscountAmount(promotionResult.getPromotionDiscountAmount())
                 .build();
     }
 
     /** 积分之后、优惠券之前:自动匹配满减/折扣并扣减 */
-    private BigDecimal subtractAutoPromotionDiscount(long uid,
-                                                     List<FsStoreCartQueryVO> carts,
-                                                     Long storeId,
-                                                     Long couponUserId,
-                                                     BigDecimal payPrice) {
+    private PromotionApplyResult applyAutoPromotionDiscount(long uid,
+                                                            List<FsStoreCartQueryVO> carts,
+                                                            Long storeId,
+                                                            Long couponUserId,
+                                                            BigDecimal payPrice) {
+        FsStorePromotionComputeResultVO promotion = promotionComputeService.autoApplyBestPromotion(
+                uid, carts, storeId, couponUserId);
+        BigDecimal promotionDiscountAmount = BigDecimal.ZERO;
+        if (promotion != null && promotion.getPromotionDiscountAmount() != null
+                && promotion.getPromotionDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
+            promotionDiscountAmount = promotion.getPromotionDiscountAmount();
+            payPrice = NumberUtil.sub(payPrice, promotionDiscountAmount);
+            if (payPrice.compareTo(BigDecimal.ZERO) < 0) {
+                payPrice = BigDecimal.ZERO;
+            }
+        }
+        return new PromotionApplyResult(payPrice, promotionDiscountAmount);
+    }
+
+    /** 仅获取活动优惠金额(不参与 payPrice 扣减),用于制单改价后 computedMultiStore 前端展示 */
+    private BigDecimal getPromotionDiscountAmount(long uid, List<FsStoreCartQueryVO> carts,
+                                                   Long storeId, Long couponUserId) {
         FsStorePromotionComputeResultVO promotion = promotionComputeService.autoApplyBestPromotion(
                 uid, carts, storeId, couponUserId);
-        if (promotion == null || promotion.getPromotionDiscountAmount() == null) {
+        if (promotion != null && promotion.getPromotionDiscountAmount() != null
+                && promotion.getPromotionDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
+            return promotion.getPromotionDiscountAmount();
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static class PromotionApplyResult {
+        private final BigDecimal payPrice;
+        private final BigDecimal promotionDiscountAmount;
+
+        private PromotionApplyResult(BigDecimal payPrice, BigDecimal promotionDiscountAmount) {
+            this.payPrice = payPrice;
+            this.promotionDiscountAmount = promotionDiscountAmount;
+        }
+
+        private BigDecimal getPayPrice() {
             return payPrice;
         }
-        BigDecimal newPayPrice = NumberUtil.sub(payPrice, promotion.getPromotionDiscountAmount());
-        return newPayPrice.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : newPayPrice;
+
+        private BigDecimal getPromotionDiscountAmount() {
+            return promotionDiscountAmount;
+        }
     }
 
-    private void applyPromotionFields(FsStoreOrderScrm storeOrder,
-                                      List<FsStoreCartQueryVO> carts,
-                                      long userId,
-                                      Long couponUserId,
-                                      Long storeId) {
+    /**
+     * 计算并应用满减活动优惠到订单对象(不持久化),返回计算结果供后续复用
+     *
+     * @return 活动计算结果,无活动或无优惠时返回 null
+     */
+    private FsStorePromotionComputeResultVO applyPromotionFields(FsStoreOrderScrm storeOrder,
+                                                                  List<FsStoreCartQueryVO> carts,
+                                                                  long userId,
+                                                                  Long couponUserId,
+                                                                  Long storeId) {
         if (storeOrder == null) {
-            return;
+            return null;
         }
         FsStorePromotionComputeResultVO promotion = promotionComputeService.autoApplyBestPromotion(
                 userId, carts, storeId, couponUserId);
         if (promotion == null || promotion.getPromotionDiscountAmount() == null
                 || promotion.getPromotionDiscountAmount().compareTo(BigDecimal.ZERO) <= 0) {
-            return;
+            return null;
         }
         storeOrder.setPromotionActivityId(promotion.getPromotionActivityId());
         storeOrder.setPromotionTierId(promotion.getPromotionTierId());
         storeOrder.setPromotionDiscountAmount(promotion.getPromotionDiscountAmount());
+        return promotion;
     }
 
     /**

+ 21 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePreparedPriceChangeServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.service.impl;
 
 import java.math.BigDecimal;
+import java.util.Date;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -13,8 +14,10 @@ import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.hisStore.domain.FsStoreCartScrm;
 import com.fs.hisStore.domain.FsStorePreparedPriceChange;
+import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.mapper.FsStoreCartScrmMapper;
 import com.fs.hisStore.mapper.FsStorePreparedPriceChangeMapper;
+import com.fs.hisStore.mapper.FsStorePromotionActivityMapper;
 import com.fs.hisStore.param.FsStorePreparedPriceChangeParam;
 import com.fs.hisStore.service.IFsStorePreparedPriceChangeService;
 import com.fs.hisStore.vo.FsStoreCartQueryVO;
@@ -41,6 +44,9 @@ public class FsStorePreparedPriceChangeServiceImpl extends ServiceImpl<FsStorePr
     @Autowired
     private FsStoreCartScrmMapper cartScrmMapper;
 
+    @Autowired
+    private FsStorePromotionActivityMapper promotionActivityMapper;
+
     /**
      * 查询销售制单改价审核记录
      *
@@ -176,6 +182,21 @@ public class FsStorePreparedPriceChangeServiceImpl extends ServiceImpl<FsStorePr
             return R.error("操作失败,销售信息不一致!");
         }
 
+        // 检查关联的满减活动是否仍在进行中
+        if (fsStorePreparedPriceChange.getPromotionActivityId() != null) {
+            FsStorePromotionActivity activity = promotionActivityMapper.selectFsStorePromotionActivityById(fsStorePreparedPriceChange.getPromotionActivityId());
+            if (activity == null) {
+                return R.error("活动不存在,无法继续分享");
+            }
+            Date now = new Date();
+            if (activity.getStartTime() != null && now.before(activity.getStartTime())) {
+                return R.error("活动尚未开始,无法继续分享");
+            }
+            if (activity.getActivityEndTime() != null && now.after(activity.getActivityEndTime())) {
+                return R.error("活动已结束,无法继续分享");
+            }
+        }
+
         // 配置写入REDIS
         String keyPrefix = "createOrder";
         String orderKey = fsStorePreparedPriceChange.getCreateOrderKey();

+ 6 - 48
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionComputeServiceImpl.java

@@ -5,7 +5,6 @@ import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.exception.CustomException;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
-import com.fs.hisStore.domain.FsStoreProductCategoryScrm;
 import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.domain.FsStorePromotionScope;
 import com.fs.hisStore.domain.FsStorePromotionTier;
@@ -18,7 +17,6 @@ import com.fs.hisStore.dto.FsStorePromotionUsageCountDTO;
 import com.fs.hisStore.param.FsStorePromotionComputeParam;
 import com.fs.hisStore.param.FsStorePromotionListMultiStoreParam;
 import com.fs.hisStore.param.FsStorePromotionListParam;
-import com.fs.hisStore.service.IFsStoreProductCategoryScrmService;
 import com.fs.hisStore.service.IFsStorePromotionComputeService;
 import com.fs.hisStore.support.FsStorePromotionTierCalculator;
 import com.fs.hisStore.support.FsStorePromotionTierCalculator.EligibleSummary;
@@ -38,7 +36,6 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -69,8 +66,6 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
     private FsStorePromotionScopeMapper scopeMapper;
     @Autowired
     private FsStorePromotionUsageMapper usageMapper;
-    @Autowired
-    private IFsStoreProductCategoryScrmService categoryService;
 
     @Override
     public FsStorePromotionListVO listApplicablePromotions(Long userId, FsStorePromotionListParam param) {
@@ -133,10 +128,9 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
         List<FsStorePromotionTier> tiers = tierMapper.selectByActivityId(activity.getId());
         List<FsStorePromotionScope> scopes = loadScopesIfNeeded(activity);
         Set<Long> scopeTargetIds = scopes.stream().map(FsStorePromotionScope::getTargetId).collect(Collectors.toSet());
-        Map<Long, FsStoreProductCategoryScrm> categoryMap = buildCategoryMap(storeCarts, scopes);
 
         EligibleSummary summary = FsStorePromotionTierCalculator.computeEligibleSummary(
-                storeCarts, activity.getScopeType(), scopeTargetIds, categoryMap);
+                storeCarts, activity.getScopeType(), scopeTargetIds);
         int usedCount = usageMapper.countEffectiveByActivityAndUser(activity.getId(), userId);
         FsStorePromotionActivityItemVO item = buildActivityItem(
                 activity, tiers, summary, couponUserId, usedCount);
@@ -190,8 +184,6 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
         }
 
         PromotionBatchData batchData = loadPromotionBatchData(activities, userId);
-        Map<Long, FsStoreProductCategoryScrm> categoryMap = buildCategoryMap(
-                storeCarts, flattenScopes(batchData.getScopeMap()));
         EligibleSummary storeSummary = summarizeStoreCarts(storeCarts);
 
         List<FsStorePromotionActivityItemVO> activityItems = new ArrayList<>();
@@ -202,7 +194,7 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
             }
             Set<Long> scopeTargetIds = resolveScopeTargetIds(batchData.getScopeMap(), activity.getId());
             EligibleSummary summary = FsStorePromotionTierCalculator.computeEligibleSummary(
-                    storeCarts, activity.getScopeType(), scopeTargetIds, categoryMap);
+                    storeCarts, activity.getScopeType(), scopeTargetIds);
             int usedCount = batchData.getUsageCountMap().getOrDefault(activity.getId(), 0);
             activityItems.add(buildActivityItem(activity, tiers, summary, couponUserId, usedCount));
         }
@@ -340,10 +332,10 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
                                          List<FsStorePromotionTier> tiers,
                                          Long couponUserId,
                                          int usedCount) {
-        if (activity.getLimitPerUser() != null && activity.getLimitPerUser() > 0
-                && usedCount >= activity.getLimitPerUser()) {
-            return "您已超过该活动参与次数限制";
-        }
+        // if (activity.getLimitPerUser() != null && activity.getLimitPerUser() > 0
+        //         && usedCount >= activity.getLimitPerUser()) {
+        //     return "您已超过该活动参与次数限制";
+        // }
         if (couponUserId != null && activity.getIsStackable() != null && activity.getIsStackable() == 0) {
             return "该活动不可与优惠券叠加使用";
         }
@@ -495,40 +487,6 @@ public class FsStorePromotionComputeServiceImpl implements IFsStorePromotionComp
         return scopeMap;
     }
 
-    private List<FsStorePromotionScope> flattenScopes(Map<Long, List<FsStorePromotionScope>> scopeMap) {
-        List<FsStorePromotionScope> scopes = new ArrayList<>();
-        for (List<FsStorePromotionScope> list : scopeMap.values()) {
-            scopes.addAll(list);
-        }
-        return scopes;
-    }
-
-    private Map<Long, FsStoreProductCategoryScrm> buildCategoryMap(List<FsStoreCartQueryVO> carts,
-                                                                   List<FsStorePromotionScope> scopes) {
-        Set<Long> cateIds = new HashSet<>();
-        for (FsStoreCartQueryVO cart : carts) {
-            if (cart.getCateId() != null) {
-                cateIds.add(cart.getCateId());
-            }
-        }
-        if (!CollectionUtils.isEmpty(scopes)) {
-            for (FsStorePromotionScope scope : scopes) {
-                if (scope.getScopeType() != null && scope.getScopeType() == 2 && scope.getTargetId() != null) {
-                    cateIds.add(scope.getTargetId());
-                }
-            }
-        }
-        if (cateIds.isEmpty()) {
-            return Collections.emptyMap();
-        }
-        List<FsStoreProductCategoryScrm> categories = categoryService.selectByCateIds(new ArrayList<>(cateIds));
-        if (CollectionUtils.isEmpty(categories)) {
-            return Collections.emptyMap();
-        }
-        return categories.stream()
-                .collect(Collectors.toMap(FsStoreProductCategoryScrm::getCateId, item -> item, (a, b) -> a));
-    }
-
     private EligibleSummary summarizeStoreCarts(List<FsStoreCartQueryVO> storeCarts) {
         EligibleSummary summary = new EligibleSummary();
         summary.setAmount(calculateStoreTotalAmount(storeCarts));

+ 276 - 17
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePromotionServiceImpl.java

@@ -2,22 +2,33 @@ package com.fs.hisStore.service.impl;
 
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.DictUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.domain.FsStorePromotionScope;
 import com.fs.hisStore.domain.FsStorePromotionTier;
-import com.fs.hisStore.domain.FsStoreProductCategoryScrm;
 import com.fs.hisStore.dto.FsStorePromotionActivityDTO;
 import com.fs.hisStore.dto.FsStorePromotionTierDTO;
 import com.fs.hisStore.mapper.FsStorePromotionActivityMapper;
+import com.fs.hisStore.mapper.FsStorePromotionReportMapper;
 import com.fs.hisStore.mapper.FsStorePromotionScopeMapper;
 import com.fs.hisStore.mapper.FsStorePromotionTierMapper;
-import com.fs.hisStore.service.IFsStoreProductCategoryScrmService;
+import com.fs.hisStore.param.FsStorePromotionReportParam;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
 import com.fs.hisStore.service.IFsStorePromotionService;
+import com.fs.hisStore.vo.FsStoreOrderPromotionDetailVO;
 import com.fs.hisStore.vo.FsStorePromotionDetailVO;
-import com.fs.hisStore.vo.FsStorePromotionScopeCategoryVO;
+import com.fs.hisStore.vo.FsStorePromotionReportOrderVO;
+import com.fs.hisStore.vo.FsStorePromotionReportStatsVO;
+import com.fs.hisStore.vo.FsStorePromotionScopeProductTypeVO;
+import com.fs.hisStore.vo.FsStorePromotionTierDisplayVO;
+import com.fs.hisStore.vo.FsStorePromotionUserDisplayVO;
+import com.fs.hisStore.vo.FsStoreProductActivityListVO;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -25,6 +36,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -40,6 +52,7 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
     private static final Map<Integer, String> DISPLAY_STATUS_LABELS = new HashMap<>();
     private static final Map<Integer, String> SCOPE_TYPE_LABELS = new HashMap<>();
     private static final Map<Integer, String> TIER_TYPE_LABELS = new HashMap<>();
+    private static final Map<Integer, String> USER_TIER_TYPE_LABELS = new HashMap<>();
 
     static {
         DISPLAY_STATUS_LABELS.put(0, "草稿");
@@ -52,6 +65,8 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
         SCOPE_TYPE_LABELS.put(3, "指定商品");
         TIER_TYPE_LABELS.put(1, "金额");
         TIER_TYPE_LABELS.put(2, "折扣");
+        USER_TIER_TYPE_LABELS.put(1, "金额满减");
+        USER_TIER_TYPE_LABELS.put(2, "件数折扣");
     }
 
     @Autowired
@@ -61,9 +76,11 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
     @Autowired
     private FsStorePromotionScopeMapper scopeMapper;
     @Autowired
-    private IFsStoreProductCategoryScrmService categoryService;
+    private FsStorePromotionReportMapper reportMapper;
     @Autowired
     private IFsStoreProductScrmService productService;
+    @Autowired
+    private IFsStoreOrderScrmService orderService;
 
     @Override
     public FsStorePromotionDetailVO selectFsStorePromotionById(Long id) {
@@ -87,22 +104,17 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
             return;
         }
         if (Integer.valueOf(2).equals(vo.getScopeType())) {
-            List<FsStoreProductCategoryScrm> categoryList = categoryService.selectByCateIds(scopeIds);
-            Map<Long, FsStoreProductCategoryScrm> categoryMap = categoryList.stream()
-                    .collect(Collectors.toMap(FsStoreProductCategoryScrm::getCateId, c -> c, (a, b) -> a));
-            List<FsStorePromotionScopeCategoryVO> categories = new ArrayList<>();
-            for (Long cateId : scopeIds) {
-                FsStoreProductCategoryScrm category = categoryMap.get(cateId);
-                if (category == null) {
+            List<FsStorePromotionScopeProductTypeVO> productTypes = new ArrayList<>();
+            for (Long targetId : scopeIds) {
+                if (targetId == null) {
                     continue;
                 }
-                FsStorePromotionScopeCategoryVO item = new FsStorePromotionScopeCategoryVO();
-                item.setCateId(category.getCateId());
-                item.setCateName(category.getCateName());
-                item.setPic(category.getPic());
-                categories.add(item);
+                FsStorePromotionScopeProductTypeVO item = new FsStorePromotionScopeProductTypeVO();
+                item.setProductType(targetId.intValue());
+                item.setProductTypeLabel(DictUtils.getDictLabel("store_product_type", String.valueOf(targetId)));
+                productTypes.add(item);
             }
-            vo.setScopeCategories(categories);
+            vo.setScopeProductTypes(productTypes);
             return;
         }
         if (Integer.valueOf(3).equals(vo.getScopeType())) {
@@ -111,6 +123,164 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
         }
     }
 
+    @Override
+    public List<FsStorePromotionUserDisplayVO> listActivePromotionDisplayByStoreId(Long storeId) {
+        if (storeId == null) {
+            return Collections.emptyList();
+        }
+        activityMapper.expireActivities();
+        List<FsStorePromotionActivity> activities = activityMapper.selectActivePromotionsByStoreId(storeId);
+        if (CollectionUtils.isEmpty(activities)) {
+            return Collections.emptyList();
+        }
+        List<FsStorePromotionUserDisplayVO> result = new ArrayList<>(activities.size());
+        for (FsStorePromotionActivity activity : activities) {
+            result.add(buildUserDisplayVO(activity));
+        }
+        return result;
+    }
+
+    private FsStorePromotionUserDisplayVO buildUserDisplayVO(FsStorePromotionActivity activity) {
+        FsStorePromotionUserDisplayVO vo = new FsStorePromotionUserDisplayVO();
+        vo.setActivityId(activity.getId());
+        vo.setTitle(activity.getTitle());
+        vo.setStartTime(activity.getStartTime());
+        vo.setEndTime(activity.getActivityEndTime());
+        int tierType = activity.getTierType() == null ? 1 : activity.getTierType();
+        vo.setTierType(tierType);
+        vo.setTierTypeLabel(getUserTierTypeLabel(tierType));
+        int scopeType = activity.getScopeType() == null ? 1 : activity.getScopeType();
+        vo.setScopeType(scopeType);
+        vo.setScopeTypeLabel(getScopeTypeLabel(scopeType));
+        vo.setStackable(activity.getIsStackable() != null && activity.getIsStackable() == 1);
+        vo.setCapped(activity.getIsCapped() != null && activity.getIsCapped() == 1);
+        int limitPerUser = activity.getLimitPerUser() == null ? 0 : activity.getLimitPerUser();
+        vo.setLimitPerUser(limitPerUser);
+        vo.setLimitPerUserText(limitPerUser > 0 ? "每人限享" + limitPerUser + "次" : "不限次数");
+        vo.setStackableText(Boolean.TRUE.equals(vo.getStackable()) ? "可叠加优惠券" : "不可叠加优惠券");
+        vo.setRemark(activity.getActivityRemark());
+
+        List<FsStorePromotionTier> tiers = tierMapper.selectByActivityId(activity.getId());
+        List<FsStorePromotionTierDisplayVO> tierDisplays = buildTierDisplayList(tierType, tiers);
+        vo.setTiers(tierDisplays);
+        vo.setRuleSummary(buildRuleSummary(tierType, vo.getCapped(), tierDisplays));
+
+        List<FsStorePromotionScope> scopes = scopeMapper.selectByActivityId(activity.getId());
+        List<Long> scopeIds = scopes.stream().map(FsStorePromotionScope::getTargetId).collect(Collectors.toList());
+        fillUserDisplayScope(vo, scopeIds);
+        vo.setScopeSummary(buildScopeSummary(vo));
+        return vo;
+    }
+
+    private List<FsStorePromotionTierDisplayVO> buildTierDisplayList(int tierType, List<FsStorePromotionTier> tiers) {
+        if (CollectionUtils.isEmpty(tiers)) {
+            return Collections.emptyList();
+        }
+        List<FsStorePromotionTierDisplayVO> list = new ArrayList<>(tiers.size());
+        for (FsStorePromotionTier tier : tiers) {
+            FsStorePromotionTierDisplayVO item = new FsStorePromotionTierDisplayVO();
+            item.setSortOrder(tier.getSortOrder());
+            item.setThresholdAmount(tier.getThresholdAmount());
+            item.setDiscountAmount(tier.getDiscountAmount());
+            item.setTierDesc(buildTierDesc(tierType, tier));
+            list.add(item);
+        }
+        return list;
+    }
+
+    private String buildTierDesc(int tierType, FsStorePromotionTier tier) {
+        if (tier == null || tier.getThresholdAmount() == null || tier.getDiscountAmount() == null) {
+            return "";
+        }
+        if (tierType == 2) {
+            int pieces = tier.getThresholdAmount().setScale(0, RoundingMode.UP).intValue();
+            return "满" + pieces + "件" + stripTrailingZero(tier.getDiscountAmount()) + "折";
+        }
+        return "满" + stripTrailingZero(tier.getThresholdAmount()) + "元减"
+                + stripTrailingZero(tier.getDiscountAmount()) + "元";
+    }
+
+    private String buildRuleSummary(int tierType, Boolean capped, List<FsStorePromotionTierDisplayVO> tiers) {
+        if (CollectionUtils.isEmpty(tiers)) {
+            return "";
+        }
+        String summary = tiers.stream()
+                .map(FsStorePromotionTierDisplayVO::getTierDesc)
+                .filter(StringUtils::isNotEmpty)
+                .collect(Collectors.joining(";"));
+        if (tierType == 1 && Boolean.TRUE.equals(capped) && StringUtils.isNotEmpty(summary)) {
+            summary = summary + "(上不封顶)";
+        }
+        return summary;
+    }
+
+    private void fillUserDisplayScope(FsStorePromotionUserDisplayVO vo, List<Long> scopeIds) {
+        if (vo.getScopeType() == null || CollectionUtils.isEmpty(scopeIds)) {
+            return;
+        }
+        if (Integer.valueOf(2).equals(vo.getScopeType())) {
+            List<FsStorePromotionScopeProductTypeVO> productTypes = new ArrayList<>();
+            for (Long targetId : scopeIds) {
+                if (targetId == null) {
+                    continue;
+                }
+                FsStorePromotionScopeProductTypeVO item = new FsStorePromotionScopeProductTypeVO();
+                item.setProductType(targetId.intValue());
+                item.setProductTypeLabel(DictUtils.getDictLabel("store_product_type", String.valueOf(targetId)));
+                productTypes.add(item);
+            }
+            vo.setScopeProductTypes(productTypes);
+            return;
+        }
+        if (Integer.valueOf(3).equals(vo.getScopeType())) {
+            String productIds = scopeIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+            vo.setScopeProducts(productService.selectFsStoreProductByIds(productIds));
+        }
+    }
+
+    private String buildScopeSummary(FsStorePromotionUserDisplayVO vo) {
+        Integer scopeType = vo.getScopeType();
+        if (scopeType == null || scopeType == 1) {
+            return "全场通用";
+        }
+        if (Integer.valueOf(2).equals(scopeType)) {
+            if (CollectionUtils.isEmpty(vo.getScopeProductTypes())) {
+                return getScopeTypeLabel(2);
+            }
+            String labels = vo.getScopeProductTypes().stream()
+                    .map(item -> StringUtils.isNotEmpty(item.getProductTypeLabel())
+                            ? item.getProductTypeLabel()
+                            : String.valueOf(item.getProductType()))
+                    .collect(Collectors.joining("、"));
+            return "指定分类:" + labels;
+        }
+        if (Integer.valueOf(3).equals(scopeType)) {
+            if (CollectionUtils.isEmpty(vo.getScopeProducts())) {
+                return getScopeTypeLabel(3);
+            }
+            String names = vo.getScopeProducts().stream()
+                    .map(FsStoreProductActivityListVO::getProductName)
+                    .filter(StringUtils::isNotEmpty)
+                    .collect(Collectors.joining("、"));
+            return StringUtils.isNotEmpty(names) ? "指定商品:" + names : getScopeTypeLabel(3);
+        }
+        return getScopeTypeLabel(scopeType);
+    }
+
+    private String getUserTierTypeLabel(Integer tierType) {
+        if (tierType == null) {
+            return USER_TIER_TYPE_LABELS.getOrDefault(1, "金额满减");
+        }
+        return USER_TIER_TYPE_LABELS.getOrDefault(tierType, "未知");
+    }
+
+    private String stripTrailingZero(BigDecimal value) {
+        if (value == null) {
+            return "0";
+        }
+        return value.stripTrailingZeros().toPlainString();
+    }
+
     @Override
     public List<FsStorePromotionActivity> selectFsStorePromotionList(FsStorePromotionActivity query) {
         activityMapper.expireActivities();
@@ -448,4 +618,93 @@ public class FsStorePromotionServiceImpl implements IFsStorePromotionService {
         }
         return TIER_TYPE_LABELS.getOrDefault(tierType, "未知");
     }
+
+    private String getOrderPromotionTierTypeLabel(Integer tierType) {
+        if (tierType != null && tierType == 2) {
+            return "满件折扣";
+        }
+        return "金额满减";
+    }
+
+    @Override
+    public FsStoreOrderPromotionDetailVO selectOrderPromotionDetailByOrderId(Long orderId) {
+        if (orderId == null) {
+            return null;
+        }
+        FsStoreOrderScrm order = orderService.selectFsStoreOrderById(orderId);
+        if (order == null || order.getPromotionActivityId() == null) {
+            return null;
+        }
+        BigDecimal discountAmount = order.getPromotionDiscountAmount();
+        if (discountAmount == null || discountAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+
+        FsStoreOrderPromotionDetailVO vo = new FsStoreOrderPromotionDetailVO();
+        vo.setActivityId(order.getPromotionActivityId());
+        vo.setMatchedTierId(order.getPromotionTierId());
+        vo.setPromotionDiscountAmount(discountAmount);
+
+        FsStorePromotionActivity activity = activityMapper.selectFsStorePromotionActivityById(order.getPromotionActivityId());
+        if (activity != null) {
+            vo.setActivityTitle(activity.getTitle());
+            int tierType = activity.getTierType() == null ? 1 : activity.getTierType();
+            vo.setTierType(tierType);
+            vo.setTierTypeLabel(getOrderPromotionTierTypeLabel(tierType));
+            vo.setMatchedTierDesc(resolveMatchedTierDesc(tierType, order.getPromotionTierId(), activity.getId()));
+        } else {
+            vo.setActivityTitle("活动已删除");
+            vo.setTierTypeLabel("-");
+            vo.setMatchedTierDesc("-");
+        }
+        return vo;
+    }
+
+    private String resolveMatchedTierDesc(int tierType, Long tierId, Long activityId) {
+        if (tierId == null || activityId == null) {
+            return "-";
+        }
+        List<FsStorePromotionTier> tiers = tierMapper.selectByActivityId(activityId);
+        if (CollectionUtils.isEmpty(tiers)) {
+            return "-";
+        }
+        for (FsStorePromotionTier tier : tiers) {
+            if (tierId.equals(tier.getId())) {
+                return buildTierDesc(tierType, tier);
+            }
+        }
+        return "-";
+    }
+
+    @Override
+    public FsStorePromotionReportStatsVO selectPromotionReportStats(FsStorePromotionReportParam param) {
+        if (param == null || param.getActivityId() == null) {
+            throw new ServiceException("活动ID不能为空");
+        }
+        FsStorePromotionActivity activity = activityMapper.selectFsStorePromotionActivityById(param.getActivityId());
+        if (activity == null) {
+            throw new ServiceException("活动不存在");
+        }
+        FsStorePromotionReportStatsVO stats = reportMapper.selectPromotionReportStats(param);
+        if (stats == null) {
+            stats = new FsStorePromotionReportStatsVO();
+            stats.setParticipantCount(0L);
+            stats.setOrderCount(0L);
+            stats.setDiscountAmount(BigDecimal.ZERO);
+            stats.setTransactionAmount(BigDecimal.ZERO);
+        }
+        return stats;
+    }
+
+    @Override
+    public List<FsStorePromotionReportOrderVO> selectPromotionReportOrderList(FsStorePromotionReportParam param) {
+        if (param == null || param.getActivityId() == null) {
+            throw new ServiceException("活动ID不能为空");
+        }
+        FsStorePromotionActivity activity = activityMapper.selectFsStorePromotionActivityById(param.getActivityId());
+        if (activity == null) {
+            throw new ServiceException("活动不存在");
+        }
+        return reportMapper.selectPromotionReportOrderList(param);
+    }
 }

+ 7 - 24
fs-service/src/main/java/com/fs/hisStore/support/FsStorePromotionTierCalculator.java

@@ -3,7 +3,6 @@ package com.fs.hisStore.support;
 import cn.hutool.core.util.NumberUtil;
 import com.fs.hisStore.domain.FsStorePromotionActivity;
 import com.fs.hisStore.domain.FsStorePromotionTier;
-import com.fs.hisStore.domain.FsStoreProductCategoryScrm;
 import com.fs.hisStore.vo.FsStoreCartQueryVO;
 import lombok.Data;
 
@@ -13,7 +12,6 @@ import java.math.RoundingMode;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 /**
@@ -35,8 +33,7 @@ public final class FsStorePromotionTierCalculator {
 
     public static EligibleSummary computeEligibleSummary(List<FsStoreCartQueryVO> storeCarts,
                                                          Integer scopeType,
-                                                         Set<Long> scopeTargetIds,
-                                                         Map<Long, FsStoreProductCategoryScrm> categoryMap) {
+                                                         Set<Long> scopeTargetIds) {
         EligibleSummary summary = new EligibleSummary();
         if (storeCarts == null || storeCarts.isEmpty()) {
             return summary;
@@ -46,7 +43,7 @@ public final class FsStorePromotionTierCalculator {
             if (cart == null || cart.getCartNum() == null || cart.getCartNum() <= 0) {
                 continue;
             }
-            if (!isCartInScope(cart, scope, scopeTargetIds, categoryMap)) {
+            if (!isCartInScope(cart, scope, scopeTargetIds)) {
                 continue;
             }
             BigDecimal price = cart.getPrice() == null ? BigDecimal.ZERO : cart.getPrice();
@@ -59,8 +56,7 @@ public final class FsStorePromotionTierCalculator {
 
     public static boolean isCartInScope(FsStoreCartQueryVO cart,
                                         int scopeType,
-                                        Set<Long> scopeTargetIds,
-                                        Map<Long, FsStoreProductCategoryScrm> categoryMap) {
+                                        Set<Long> scopeTargetIds) {
         if (scopeType == 1) {
             return true;
         }
@@ -71,29 +67,16 @@ public final class FsStorePromotionTierCalculator {
             return cart.getProductId() != null && scopeTargetIds.contains(cart.getProductId());
         }
         if (scopeType == 2) {
-            return isCategoryInScope(cart.getCateId(), scopeTargetIds, categoryMap);
+            return isProductTypeInScope(cart.getProductType(), scopeTargetIds);
         }
         return false;
     }
 
-    public static boolean isCategoryInScope(Long cateId, Set<Long> scopeTargetIds,
-                                            Map<Long, FsStoreProductCategoryScrm> categoryMap) {
-        if (cateId == null || scopeTargetIds == null || scopeTargetIds.isEmpty()) {
+    public static boolean isProductTypeInScope(Integer productType, Set<Long> scopeTargetIds) {
+        if (productType == null || scopeTargetIds == null || scopeTargetIds.isEmpty()) {
             return false;
         }
-        Long current = cateId;
-        int guard = 0;
-        while (current != null && current > 0 && guard++ < 20) {
-            if (scopeTargetIds.contains(current)) {
-                return true;
-            }
-            FsStoreProductCategoryScrm category = categoryMap == null ? null : categoryMap.get(current);
-            if (category == null || category.getPid() == null || category.getPid() <= 0) {
-                break;
-            }
-            current = category.getPid();
-        }
-        return false;
+        return scopeTargetIds.contains(productType.longValue());
     }
 
     public static FsStorePromotionTier matchTier(FsStorePromotionActivity activity,

+ 36 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderPromotionDetailVO.java

@@ -0,0 +1,36 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 订单关联的阶梯满减活动详情
+ */
+@Data
+public class FsStoreOrderPromotionDetailVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 活动名称 */
+    private String activityTitle;
+
+    /** 阶梯类型:1金额满减 2件数折扣 */
+    private Integer tierType;
+
+    /** 阶梯类型文案 */
+    private String tierTypeLabel;
+
+    /** 命中档位ID */
+    private Long matchedTierId;
+
+    /** 命中档位说明,如:满199元减30元 */
+    private String matchedTierDesc;
+
+    /** 活动优惠金额 */
+    private BigDecimal promotionDiscountAmount;
+}

+ 2 - 2
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionDetailVO.java

@@ -15,8 +15,8 @@ public class FsStorePromotionDetailVO extends FsStorePromotionActivity {
 
     private List<Long> scopeIds;
 
-    /** 指定分类详情(scopeType=2) */
-    private List<FsStorePromotionScopeCategoryVO> scopeCategories;
+    /** 指定分类详情(scopeType=2,值为商品类型) */
+    private List<FsStorePromotionScopeProductTypeVO> scopeProductTypes;
 
     /** 指定商品详情(scopeType=3) */
     private List<FsStoreProductActivityListVO> scopeProducts;

+ 56 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionReportOrderVO.java

@@ -0,0 +1,56 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 满减活动报表明细订单
+ */
+@Data
+public class FsStorePromotionReportOrderVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    private String orderCode;
+
+    private String storeName;
+
+    private String merchantId;
+
+    private String storeSeq;
+
+    private Long storeId;
+
+    private String companyName;
+
+    private Long companyId;
+
+    private String companyUserNickName;
+
+    private Long companyUserId;
+
+    private String nickname;
+
+    private String realName;
+
+    private BigDecimal totalPrice;
+
+    private BigDecimal payPrice;
+
+    private BigDecimal promotionDiscountAmount;
+
+    /** 订单状态 */
+    private Integer status;
+
+    /** 1销售订单 2商城订单 */
+    private Integer orderSourceType;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionReportStatsVO.java

@@ -0,0 +1,27 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 满减活动报表汇总
+ */
+@Data
+public class FsStorePromotionReportStatsVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 参与人数 */
+    private Long participantCount;
+
+    /** 订单量 */
+    private Long orderCount;
+
+    /** 优惠金额 */
+    private BigDecimal discountAmount;
+
+    /** 成交额(实付) */
+    private BigDecimal transactionAmount;
+}

+ 0 - 20
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionScopeCategoryVO.java

@@ -1,20 +0,0 @@
-package com.fs.hisStore.vo;
-
-import lombok.Data;
-
-import java.io.Serializable;
-
-/**
- * 满减活动详情-指定分类展示项
- */
-@Data
-public class FsStorePromotionScopeCategoryVO implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    private Long cateId;
-
-    private String cateName;
-
-    private String pic;
-}

+ 20 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionScopeProductTypeVO.java

@@ -0,0 +1,20 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 满减活动详情-指定分类展示项(scopeType=2,存储商品类型字典值)
+ */
+@Data
+public class FsStorePromotionScopeProductTypeVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 商品类型,对应字典 store_product_type */
+    private Integer productType;
+
+    /** 商品类型名称 */
+    private String productTypeLabel;
+}

+ 24 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionTierDisplayVO.java

@@ -0,0 +1,24 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 用户端展示-活动阶梯档位
+ */
+@Data
+public class FsStorePromotionTierDisplayVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer sortOrder;
+
+    private BigDecimal thresholdAmount;
+
+    private BigDecimal discountAmount;
+
+    /** 档位说明,如:满199元减30元、满3件9.5折 */
+    private String tierDesc;
+}

+ 66 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStorePromotionUserDisplayVO.java

@@ -0,0 +1,66 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 用户端(小程序)店铺活动展示
+ */
+@Data
+public class FsStorePromotionUserDisplayVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long activityId;
+
+    private String title;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 1金额满减 2件数折扣 */
+    private Integer tierType;
+
+    private String tierTypeLabel;
+
+    /** 1全场 2指定分类(商品类型) 3指定商品 */
+    private Integer scopeType;
+
+    private String scopeTypeLabel;
+
+    /** 是否可叠加优惠券 */
+    private Boolean stackable;
+
+    /** 金额满减是否上不封顶 */
+    private Boolean capped;
+
+    /** 每人限享次数,0 表示不限 */
+    private Integer limitPerUser;
+
+    private String limitPerUserText;
+
+    private String stackableText;
+
+    private String remark;
+
+    /** 阶梯规则汇总,如:满199元减30元;满299元减50元(上不封顶) */
+    private String ruleSummary;
+
+    /** 适用范围说明,如:全场通用 / 指定分类:非处方、处方 */
+    private String scopeSummary;
+
+    private List<FsStorePromotionTierDisplayVO> tiers;
+
+    /** scopeType=2 指定分类(商品类型) */
+    private List<FsStorePromotionScopeProductTypeVO> scopeProductTypes;
+
+    /** scopeType=3 指定商品 */
+    private List<FsStoreProductActivityListVO> scopeProducts;
+}

+ 8 - 4
fs-service/src/main/resources/application-config-druid-yjb.yml

@@ -8,12 +8,16 @@ logging:
     org.springframework.web: INFO
     com.github.binarywang.demo.wx.cp: DEBUG
     me.chanjar.weixin: DEBUG
+    com:
+      fs:
+        course:
+          mapper:  debug
 wx:
   open:
-#      appId: wx7796a33a71912e32 #互易享(三方入驻APP)
-#      secret: 51601ec1e3247fe1615ef7b55baf95c7 #互易享(三方入驻APP)
-      appId: wx98abee0aa8ccdd3c #鸿良(看课APP)
-      secret: 6c269d8ee939d1a241721d0dea834568 #鸿良(看课APP)
+      appId: wx7796a33a71912e32 #互易享(三方入驻APP)
+      secret: 51601ec1e3247fe1615ef7b55baf95c7 #互易享(三方入驻APP)
+#      appId: wx98abee0aa8ccdd3c #鸿良(看课APP)
+#      secret: 6c269d8ee939d1a241721d0dea834568 #鸿良(看课APP)
   miniapp:
     configs:
       - appid: wx9e61312fe7ac85c4   #医健宝

+ 9 - 2
fs-service/src/main/resources/application-dev-yjb.yml

@@ -144,14 +144,21 @@ rocketmq:
         group: test-group
         access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
         secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
-# token配置
+# token配置(公司Web后台 fs-company 使用,App端不走此配置)
 token:
     # 令牌自定义标识
     header: Authorization
     # 令牌密钥
     secret: feeb79c778c1274dd0e4a709cd948718
-    # 令牌有效期(默认30分钟)
+    # 令牌有效期(分钟),Web后台会话;App端见 fs.jwt.expire
     expireTime: 180
+# App端 JWT 配置(fs-company-app / fs-user-app 使用)
+fs:
+    jwt:
+        secret: 7af48cb753c96945816b5fdaaa01e879
+        # 7天,单位秒
+        expire: 604800
+        header: AppToken
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: false
 openIM:

+ 3 - 0
fs-service/src/main/resources/db/20250703-满减活动报表索引.sql

@@ -0,0 +1,3 @@
+-- 满减活动报表查询性能索引
+ALTER TABLE `fs_store_order_scrm`
+    ADD INDEX `idx_promotion_activity_del` (`promotion_activity_id`, `is_del`);

+ 20 - 0
fs-service/src/main/resources/db/20250703-点播播放源看课小程序字段.sql

@@ -0,0 +1,20 @@
+-- 点播播放源配置:是否看课小程序
+ALTER TABLE `fs_course_play_source_config`
+    ADD COLUMN `is_course_mini` TINYINT(1) NULL DEFAULT 1 COMMENT '是否看课小程序:1是 0否,空视为是' AFTER `is_mall`;
+
+-- 存量数据空值回填为看课小程序
+UPDATE `fs_course_play_source_config`
+SET `is_course_mini` = 1
+WHERE `is_course_mini` IS NULL;
+
+-- 按钮权限:看课小程序标识(仅管理员勾选后可见/可配置)
+SET @parent_id = (SELECT `menu_id` FROM `sys_menu` WHERE `perms` = 'course:playSourceConfig:list' LIMIT 1);
+
+INSERT INTO `sys_menu`
+    (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT
+    '看课小程序标识', @parent_id, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'course:playSourceConfig:courseMini', '#', 'admin', NOW(), '', NULL, '配置是否看课小程序,用于区分APP与小程序用户'
+WHERE @parent_id IS NOT NULL
+  AND NOT EXISTS (
+      SELECT 1 FROM `sys_menu` WHERE `perms` = 'course:playSourceConfig:courseMini'
+  );

+ 18 - 0
fs-service/src/main/resources/db/fs_user_biz_type.sql

@@ -0,0 +1,18 @@
+-- fs_user 增加业务类型字段:区分看课、商城
+-- 1=看课(wx98abee0aa8ccdd3c)  2=商城(wx7796a33a71912e32)
+
+ALTER TABLE `fs_user`
+    ADD COLUMN `biz_type` TINYINT(1) NULL DEFAULT NULL COMMENT '业务类型:1看课 2商城' AFTER `app_id`;
+
+CREATE INDEX `idx_fs_user_phone_is_del_biz_type` ON `fs_user` (`phone`, `is_del`, `biz_type`);
+
+-- 存量数据按 app_id 回填(按需执行)
+UPDATE `fs_user`
+SET `biz_type` = 1
+WHERE `biz_type` IS NULL
+  AND `app_id` LIKE '%wx98abee0aa8ccdd3c%';
+
+UPDATE `fs_user`
+SET `biz_type` = 2
+WHERE `biz_type` IS NULL
+  AND `app_id` LIKE '%wx7796a33a71912e32%';

+ 8 - 0
fs-service/src/main/resources/mapper/company/CompanyMapper.xml

@@ -341,6 +341,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </if>
     </select>
 
+    <select id="selectCompanyIdsBySysDeptScope" resultType="java.lang.Long">
+        select company_id from company where is_del = 0
+        <if test="sysDeptId != null">
+            and (dept_id = #{sysDeptId}
+            or dept_id in (select t.dept_id from sys_dept t where find_in_set(#{sysDeptId}, ancestors)))
+        </if>
+    </select>
+
     <select id="getCompanyListByCorpId" resultType="com.fs.his.vo.OptionsVO">
         select c.company_id dictValue, c.company_name dictLabel
         from company c

+ 3 - 0
fs-service/src/main/resources/mapper/course/FsCoursePlaySourceConfigMapper.xml

@@ -30,6 +30,9 @@
         <if test="params.isMall != null">
             and fcpsc.is_mall = #{params.isMall}
         </if>
+        <if test="params.isCourseMini != null">
+            and ifnull(fcpsc.is_course_mini, 1) = #{params.isCourseMini}
+        </if>
         <if test="params.status != null">
             and fcpsc.status = #{params.status}
         </if>

+ 43 - 6
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -55,6 +55,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where log_id = #{logId}
     </select>
 
+    <sql id="watchLogCompanyIdFilter">
+        <choose>
+            <when test="maps.companyId != null">
+                and l.company_id = #{maps.companyId}
+            </when>
+            <when test="maps.companyIds != null and maps.companyIds.size() > 0">
+                and l.company_id in
+                <foreach collection="maps.companyIds" item="cid" open="(" separator="," close=")">
+                    #{cid}
+                </foreach>
+            </when>
+        </choose>
+    </sql>
+
     <select id="selectFsCourseWatchLogListVO" resultType="com.fs.course.vo.FsCourseWatchLogListVO">
         select l.log_id,l.project,l.period_id,l.user_id,uc.course_name,v.title as video_name,qec.avatar as external_user_avatar,
         l.log_type,SEC_TO_TIME(l.duration) as duration,c.company_name,l.camp_period_time,l.finish_time,
@@ -106,8 +120,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test ='maps.logType !=null'>
                 and l.log_type = #{maps.logType}
             </if>
-            <if test ='maps.companyId !=null'>
-                and l.company_id = #{maps.companyId}
+            <include refid="watchLogCompanyIdFilter"/>
+            <if test="maps.sysDeptId != null">
+                AND (c.dept_id = #{maps.sysDeptId}
+                OR c.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{maps.sysDeptId}, ancestors) ))
             </if>
             <if test ='maps.companyUserId !=null'>
                 and l.company_user_id = #{maps.companyUserId}
@@ -128,10 +144,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 and DATE(qec.create_time) &lt;= DATE(#{maps.qecETime})
             </if>
             <if test= 'maps.sTime != null '>
-                and DATE(l.create_time) &gt;= #{maps.sTime}
+                and l.create_time &gt;= CONCAT(#{maps.sTime},' 00:00:00')
             </if>
             <if test='maps.eTime != null '>
-                and DATE(l.create_time) &lt;= #{maps.eTime}
+                and l.create_time &lt;= CONCAT(#{maps.eTime},' 00:00:00')
             </if>
             <if test= 'maps.scheduleStartTime != null '>
                 and DATE(l.camp_period_time) &gt;= DATE(#{maps.scheduleStartTime})
@@ -801,9 +817,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         fs_course_watch_log l LEFT JOIN qw_external_contact qec on l.qw_external_contact_id = qec.id
         left join fs_user u on u.user_id = l.user_id
         left join company_user cu on cu.user_id = l.company_user_id
+        left join company c on c.company_id = l.company_id
         <where>
+            <if test ='maps.isVip != null and maps.isVip == 0'>
+                and (l.user_id = 0 or l.user_id is null)
+            </if>
+            <if test ='maps.isVip != null and maps.isVip == 1'>
+                and l.user_id != 0 and l.user_id is not null
+            </if>
             <if test ='maps.sendType !=null'>
                 and l.send_type = #{maps.sendType}
+                <if test ='maps.sendType == 2'>
+                    and l.qw_user_id IS NOT NULL
+                </if>
             </if>
             <if test ='maps.userId !=null'>
                 and l.user_id = #{maps.userId}
@@ -823,8 +849,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test ='maps.logType !=null'>
                 and l.log_type = #{maps.logType}
             </if>
-            <if test ='maps.companyId !=null'>
-                and l.company_id = #{maps.companyId}
+            <include refid="watchLogCompanyIdFilter"/>
+            <if test="maps.sysDeptId != null">
+                AND (c.dept_id = #{maps.sysDeptId}
+                OR c.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{maps.sysDeptId}, ancestors) ))
             </if>
             <if test ='maps.companyUserId !=null'>
                 and l.company_user_id = #{maps.companyUserId}
@@ -874,6 +902,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                     #{periodId}
                 </foreach>
             </if>
+            <if test="maps.periodId != null">
+                and l.period_id = #{maps.periodId}
+            </if>
+            <if test="maps.sopId != null and maps.sopId != ''">
+                and l.sop_id = #{maps.sopId}
+            </if>
+            <if test = 'maps.deptId != null'>
+                and cu.dept_id = #{maps.deptId}
+            </if>
 
         </where>
         order by l.finish_time desc,l.update_time desc,l.create_time desc

+ 5 - 1
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -49,11 +49,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="qwExtId"    column="qw_ext_id"    />
         <result property="qwUserId"    column="qw_user_id"    />
         <result property="appId"    column="app_id"    />
+        <result property="bizType"    column="biz_type"    />
         <result property="historyApp"    column="history_app"    />
     </resultMap>
 
     <sql id="selectFsUserVo">
-        select user_id,qw_ext_id,sex,is_buy,course_ma_open_id,is_push,is_add_qw,source,history_app,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id,app_id from fs_user
+        select user_id,qw_ext_id,sex,is_buy,course_ma_open_id,is_push,is_add_qw,source,history_app,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id,app_id,biz_type from fs_user
     </sql>
 
     <select id="selectFsUserList" parameterType="FsUser" resultMap="FsUserResult">
@@ -566,6 +567,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">company_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
             <if test="appId != null">app_id,</if>
+            <if test="bizType != null">biz_type,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="nickName != null">#{nickName},</if>
@@ -610,6 +612,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">#{companyId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="appId != null">#{appId},</if>
+            <if test="bizType != null">#{bizType},</if>
          </trim>
     </insert>
 
@@ -659,6 +662,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="orderCount != null">order_count = #{orderCount},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="appId != null">app_id = #{appId},</if>
+            <if test="bizType != null">biz_type = #{bizType},</if>
             <if test="historyApp != null">history_app = #{historyApp},</if>
         </trim>
         where user_id = #{userId}

+ 8 - 1
fs-service/src/main/resources/mapper/hisStore/FsStorePreparedPriceChangeMapper.xml

@@ -18,6 +18,7 @@
         <result property="auditStatus" column="audit_status"/>
         <result property="totalAmount" column="total_amount"/>
         <result property="companyId" column="company_id"/>
+        <result property="promotionActivityId" column="promotion_activity_id"/>
     </resultMap>
 
     <sql id="selectFsStorePreparedPriceChangeVo">
@@ -33,7 +34,10 @@
                create_by,
                update_by,
                audit_status,
-               total_amount
+               audit_remark,
+               pay_type,
+               total_amount,
+               promotion_activity_id
         from fs_store_prepared_price_change
     </sql>
 
@@ -75,6 +79,7 @@
             <if test="auditRemark != null">audit_remark ,</if>
             <if test="totalAmount != null">total_amount ,</if>
             <if test="companyId != null">company_id ,</if>
+            <if test="promotionActivityId != null">promotion_activity_id,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="cartIds != null and cartIds != ''">#{cartIds},</if>
@@ -91,6 +96,7 @@
             <if test="auditRemark != null">#{auditRemark} ,</if>
             <if test="totalAmount != null">#{totalAmount} ,</if>
             <if test="companyId != null">#{companyId} ,</if>
+            <if test="promotionActivityId != null">#{promotionActivityId},</if>
         </trim>
     </insert>
 
@@ -111,6 +117,7 @@
             <if test="auditRemark != null">audit_remark = #{auditRemark} ,</if>
             <if test="totalAmount != null">total_amount = #{totalAmount} ,</if>
             <if test="companyId != null">company_id = #{companyId} ,</if>
+            <if test="promotionActivityId != null">promotion_activity_id = #{promotionActivityId},</if>
         </trim>
         where id = #{id}
     </update>

+ 77 - 0
fs-service/src/main/resources/mapper/hisStore/FsStorePromotionReportMapper.xml

@@ -0,0 +1,77 @@
+<?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.FsStorePromotionReportMapper">
+
+    <sql id="promotionReportWhere">
+        o.promotion_activity_id = #{param.activityId}
+        AND o.is_del = 0
+        <if test="param.orderCode != null and param.orderCode != ''">
+            AND o.order_code LIKE CONCAT('%', #{param.orderCode}, '%')
+        </if>
+        <if test="param.companyId != null">
+            AND o.company_id = #{param.companyId}
+        </if>
+        <if test="param.companyUserId != null">
+            AND o.company_user_id = #{param.companyUserId}
+        </if>
+        <if test="param.nickname != null and param.nickname != ''">
+            AND (u.nickname LIKE CONCAT('%', #{param.nickname}, '%')
+            OR u.nick_name LIKE CONCAT('%', #{param.nickname}, '%'))
+        </if>
+        <if test="param.status != null">
+            AND o.status = #{param.status}
+        </if>
+        <if test="param.orderSourceType != null and param.orderSourceType == 1">
+            AND o.company_user_id IS NOT NULL
+        </if>
+        <if test="param.orderSourceType != null and param.orderSourceType == 2">
+            AND o.company_user_id IS NULL
+        </if>
+    </sql>
+
+    <select id="selectPromotionReportStats" resultType="com.fs.hisStore.vo.FsStorePromotionReportStatsVO">
+        SELECT
+            COUNT(DISTINCT o.user_id) AS participantCount,
+            COUNT(1) AS orderCount,
+            IFNULL(SUM(o.promotion_discount_amount), 0) AS discountAmount,
+            IFNULL(SUM(IFNULL(o.pay_money, 0)), 0) AS transactionAmount
+        FROM fs_store_order_scrm o
+        <if test="param.nickname != null and param.nickname != ''">
+            LEFT JOIN fs_user u ON o.user_id = u.user_id
+        </if>
+        <where>
+            <include refid="promotionReportWhere"/>
+        </where>
+    </select>
+
+    <select id="selectPromotionReportOrderList" resultType="com.fs.hisStore.vo.FsStorePromotionReportOrderVO">
+        SELECT
+            o.id,
+            o.order_code AS orderCode,
+            o.total_price AS totalPrice,
+            o.pay_price AS payPrice,
+            o.promotion_discount_amount AS promotionDiscountAmount,
+            o.status,
+            o.real_name AS realName,
+            o.company_id AS companyId,
+            o.company_user_id AS companyUserId,
+            o.store_id AS storeId,
+            o.create_time AS createTime,
+            IFNULL(u.nickname, u.nick_name) AS nickname,
+            c.company_name AS companyName,
+            cu.nick_name AS companyUserNickName,
+            ssc.store_name AS storeName,
+            ssc.store_seq AS storeSeq,
+            ssc.merchant_id AS merchantId,
+            CASE WHEN o.company_user_id IS NOT NULL THEN 1 ELSE 2 END AS orderSourceType
+        FROM fs_store_order_scrm o
+        LEFT JOIN fs_user u ON o.user_id = u.user_id
+        LEFT JOIN company c ON c.company_id = o.company_id
+        LEFT JOIN company_user cu ON cu.user_id = o.company_user_id
+        LEFT JOIN fs_store_scrm ssc ON o.store_id = ssc.store_id
+        <where>
+            <include refid="promotionReportWhere"/>
+        </where>
+        ORDER BY o.id DESC
+    </select>
+</mapper>

+ 12 - 8
fs-service/src/main/resources/mapper/hisStore/FsStoreScrmMapper.xml

@@ -512,14 +512,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </select>
     <select id="selectFsStoreListByProduct" resultType="com.fs.hisStore.domain.FsStoreScrm">
-        select s.*
-        from fs_store_scrm s
---         JOIN (
---         SELECT store_id
---         FROM fs_store_product_category_scrm
---         GROUP BY store_id
---         HAVING COUNT(*) > 0
---         ) c ON s.store_id = c.store_id
+        SELECT
+        s.*,
+        pa.tier_type
+        FROM
+        fs_store_scrm s
+        LEFT JOIN fs_store_promotion_activity pa
+        ON s.store_id = pa.store_id
+        AND pa.is_del = 0
+        AND pa.manual_status = 1
+        AND pa.status = 1
+        AND pa.start_time &lt;= NOW()
+        AND pa.end_time &gt;= NOW()
         <where>
             <if test="storeName != null  and storeName != ''"> and s.store_name like concat('%', #{storeName}, '%')</if>
             <if test="address != null  and address != ''"> and s.address like concat('%', #{address}, '%')</if>

+ 29 - 0
fs-store/src/main/java/com/fs/hisStore/controller/store/FsStorePromotionScrmController.java

@@ -0,0 +1,29 @@
+package com.fs.hisStore.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.hisStore.service.IFsStorePromotionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 店铺端-订单阶梯满减活动
+ */
+@RestController
+@RequestMapping("/store/store/storePromotion")
+public class FsStorePromotionScrmController extends BaseController {
+
+    @Autowired
+    private IFsStorePromotionService promotionService;
+
+    /**
+     * 查询订单关联的阶梯满减活动详情
+     */
+    @GetMapping("/order/{orderId}")
+    public AjaxResult getOrderPromotionDetail(@PathVariable Long orderId) {
+        return AjaxResult.success(promotionService.selectOrderPromotionDetailByOrderId(orderId));
+    }
+}

+ 52 - 9
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -16,6 +16,7 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.sign.Md5Utils;
 import com.fs.core.config.WxOpenProperties;
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserNewTask;
@@ -68,6 +69,9 @@ public class AppLoginController extends AppBaseController{
     @Autowired
     private ISmsService smsService;
 
+    @Autowired
+    private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
+
     @ApiOperation("注册app用户")
     @PostMapping("/register")
     @RepeatSubmit
@@ -189,6 +193,11 @@ public class AppLoginController extends AppBaseController{
                 }
                 user.setUnionId(unionid);
                 user.setAppId(openProperties.getAppId());
+                if(user.getAppId().contains("wx98abee0aa8ccdd3c")){
+                    user.setBizType(1);
+                }else {
+                    user.setBizType(2);
+                }
                 user.setCreateTime(new Date());
                 user.setStatus(1);
                 if (StringUtils.isNotEmpty(param.getJpushId())) {
@@ -205,6 +214,11 @@ public class AppLoginController extends AppBaseController{
                     FsUser userMap = new FsUser();
                     userMap.setUserId(user.getUserId());
                     userMap.setAppId(updatedAppId);
+                    if(userMap.getAppId().contains("wx98abee0aa8ccdd3c")){
+                        userMap.setBizType(1);
+                    }else {
+                        userMap.setBizType(2);
+                    }
                     userService.updateFsUser(userMap);
                 }else if(user.getMaOpenId() == null || "".equals(user.getMaOpenId())){
                     FsUser userUpdate = new FsUser();
@@ -223,8 +237,7 @@ public class AppLoginController extends AppBaseController{
                 }
             }
             int isFirstLogin = userNewTaskService.performTaskOne(user.getUserId());
-            String token = jwtUtils.generateToken(user.getUserId());
-            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+            String token = jwtUtils.issueAppToken(redisCache, user.getUserId());
             map.put("token", token);
             map.put("user", user);
             map.put("isFirst",isFirstLogin);
@@ -313,8 +326,7 @@ public class AppLoginController extends AppBaseController{
 
     private R generateTokenAndReturn(FsUser user) {
         try {
-            String token = jwtUtils.generateToken(user.getUserId());
-            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+            String token = jwtUtils.issueAppToken(redisCache, user.getUserId());
             int isFirstLogin = userNewTaskService.performTaskOne(user.getUserId());
             Map<String, Object> map = new HashMap<>();
             map.put("token", token);
@@ -564,8 +576,7 @@ public class AppLoginController extends AppBaseController{
     public R loginTest(@RequestBody String userId) {
         FsUser user = userMapper.selectFsUserByUserId(Long.parseLong(userId));
         if (user!=null){
-            String token = jwtUtils.generateToken(user.getUserId());
-            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+            String token = jwtUtils.issueAppToken(redisCache, user.getUserId());
             Map<String, Object> map = new HashMap<>();
             map.put("token", token);
             map.put("user", user);
@@ -754,6 +765,11 @@ public class AppLoginController extends AppBaseController{
         user.setPassword(Md5Utils.hash(password));
         user.setCreateTime(new Date());
         user.setAppId(appid);
+        if(appid.contains("wx98abee0aa8ccdd3c")){
+            user.setBizType(1);
+        }else {
+            user.setBizType(2);
+        }
         if (userService.insertFsUser(user) > 0) {
             return R.ok("注册成功");
         } else {
@@ -767,9 +783,20 @@ public class AppLoginController extends AppBaseController{
         String code = map.get("code");
         String encryptPhone = encryptPhone(phone);
         String appid = openProperties.getAppId();
-        List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone,appid);
+        Integer bizType = fsCoursePlaySourceConfigService.resolveBizTypeByAppId(appid);
+        logger.info("打印数据--------------》1:{}",bizType);
+        if(bizType == null){
+            if(appid.equals("wx7796a33a71912e32")){
+                bizType = 2;
+            }else {
+                bizType = 1;
+            }
+        }
+        List<FsUser> user = userService.selectFsUserListByPhoneAndBizType(encryptPhone, appid, bizType);
+        logger.info("打印数据--------------》2:{}",encryptPhone+"-"+appid+"-"+bizType);
         if (CollectionUtil.isEmpty(user)){
-            user = userService.selectFsUserListByPhone(encryptPhoneOldKey(phone),appid);
+            user = userService.selectFsUserListByPhoneAndBizType(encryptPhoneOldKey(phone), appid, bizType);
+            logger.info("打印数据--------------》3:{}",encryptPhone+"-"+appid+"-"+bizType);
         }
 
         if (CollectionUtil.isEmpty(user)){
@@ -777,7 +804,9 @@ public class AppLoginController extends AppBaseController{
             FsUserLoginParam userLoginParam = new FsUserLoginParam();
             userLoginParam.setPhone(phone);
             userLoginParam.setAppId(appid);
-            user.add(createNewUser(userLoginParam));//创建用户
+            FsUser newUser = createNewUser(userLoginParam);
+            fillUserBizType(newUser, bizType);
+            user.add(newUser);
 //            return R.error("此电话号码未绑定用户");
         }
 
@@ -880,4 +909,18 @@ public class AppLoginController extends AppBaseController{
         return fsUsers;
     }
 
+    /**
+     * 补全用户业务类型
+     */
+    private void fillUserBizType(FsUser user, Integer bizType) {
+        if (user == null || user.getUserId() == null || bizType == null || user.getBizType() != null) {
+            return;
+        }
+        FsUser update = new FsUser();
+        update.setUserId(user.getUserId());
+        update.setBizType(bizType);
+        userService.updateFsUser(update);
+        user.setBizType(bizType);
+    }
+
 }

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

@@ -42,6 +42,7 @@ import com.fs.system.oss.OSSFactory;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.google.zxing.WriterException;
+import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.lang.Assert;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -197,18 +198,16 @@ public class UserController extends  AppBaseController {
 
     @ApiOperation("检测是否登录")
     @GetMapping("/checkLogin")
-    public R checkLogin(HttpServletRequest request){
-        if(StringUtils.isEmpty(getUserId())){
-            //未登录
-            return R.error("未登录");
+    public R checkLogin(HttpServletRequest request) {
+        String token = request.getHeader(jwtUtils.getHeader());
+        if (StringUtils.isBlank(token)) {
+            token = request.getParameter(jwtUtils.getHeader());
         }
-        else{
-            //登录
-            String token = jwtUtils.generateToken(Long.parseLong(getUserId()));
-            Map<String,Object> map=new HashMap<>();
-            map.put("token",token);
-            return R.ok("认证成功").put("userId",getUserId()).put("token",token);
+        if (StringUtils.isBlank(token) || !jwtUtils.isAccessTokenValid(redisCache, token)) {
+            return R.error("未登录");
         }
+        Claims claims = jwtUtils.getClaimByToken(token);
+        return R.ok("认证成功").put("userId", claims.getSubject());
     }
 
     @Login

+ 31 - 8
fs-user-app/src/main/java/com/fs/app/controller/WxUserController.java

@@ -20,8 +20,10 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.config.WxMpConfiguration;
 import com.fs.course.config.CourseMaConfig;
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
+import com.fs.his.enums.FsUserBizTypeEnum;
 import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.his.mapper.FsUserLoginLogMapper;
 import com.fs.his.service.IFsUserService;
@@ -89,10 +91,10 @@ public class WxUserController extends AppBaseController{
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
+
 
-    /**
-     * 登陆接口
-     */
     /**
      * 登陆接口
      */
@@ -110,6 +112,9 @@ public class WxUserController extends AppBaseController{
             appId = con.getAppid();
         }
 
+        // 根据小程序配置解析业务类型:1看课 2商城
+        Integer bizType = fsCoursePlaySourceConfigService.resolveBizTypeByAppId(appId);
+
         final WxMaService wxService = WxMaConfiguration.getMaService(appId);
         try {
             WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
@@ -127,21 +132,23 @@ public class WxUserController extends AppBaseController{
             }else {
                 user = userService.selectFsUserByOpenId(session.getOpenid());
             }
+            if (user != null && !FsUserBizTypeEnum.isSameBizType(user.getBizType(), bizType)) {
+                user = null;
+            }
 
             if(user==null){
                 //新用户
                 String phoneNumber = phoneNoInfo.getPhoneNumber();
-                //查询手机号是否存在,如果存在,更新
-                FsUser checkPhone=userService.selectFsUserByPhone(encryptPhone(phoneNumber));
-                if (checkPhone==null){
-                    checkPhone=userService.selectFsUserByPhone(phoneNumber);
-                }
+                // 按业务类型查询手机号,看课与商城互斥
+                FsUser checkPhone = findUserByPhoneAndBizType(phoneNumber, bizType);
                 if(checkPhone!=null){
                     user=checkPhone;
+                    user.setBizType(bizType);
                     FsUser userMap=new FsUser();
                     userMap.setMaOpenId(session.getOpenid());
                     userMap.setUserId(checkPhone.getUserId());
                     userMap.setUpdateTime(new DateTime());
+                    userMap.setBizType(bizType);
                     if(session.getUnionid()!=null){
                         userMap.setUnionId(session.getUnionid());
                     }
@@ -157,6 +164,7 @@ public class WxUserController extends AppBaseController{
                     user.setStatus(1);
                     user.setMaOpenId(session.getOpenid());
                     user.setAppId(appId);
+                    user.setBizType(bizType);
                     user.setCreateTime(new Date());
                     if(session.getUnionid()!=null){
                         user.setUnionId(session.getUnionid());
@@ -177,6 +185,10 @@ public class WxUserController extends AppBaseController{
                 userMap.setMaOpenId(session.getOpenid());
                 userMap.setPhone(phoneNoInfo.getPhoneNumber());
                 userMap.setUpdateTime(new DateTime());
+                if (user.getBizType() == null) {
+                    userMap.setBizType(bizType);
+                    user.setBizType(bizType);
+                }
                 // 老用户 - 检查并添加appId(不重复添加)
                 String updatedAppId = addAppIdIfNotExists(user.getAppId(), appId);
                 if (!updatedAppId.equals(user.getAppId())) {
@@ -458,4 +470,15 @@ public class WxUserController extends AppBaseController{
         return currentAppIds + "," + newAppId;
     }
 
+    /**
+     * 按手机号和业务类型查询用户,兼容加密/明文手机号
+     */
+    private FsUser findUserByPhoneAndBizType(String phoneNumber, Integer bizType) {
+        FsUser user = userService.selectFsUserByPhoneAndBizType(encryptPhone(phoneNumber), bizType);
+        if (user == null) {
+            user = userService.selectFsUserByPhoneAndBizType(phoneNumber, bizType);
+        }
+        return user;
+    }
+
 }

+ 3 - 3
fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java

@@ -455,9 +455,9 @@ public class CourseQwController extends AppBaseController {
             if (StringUtils.isBlank(token)) {
                 return R.error(4001, "登录凭证不能为空,重新授权");
             }
-            Claims claims=jwtUtils.getClaimByToken(token);
-            if(claims==null || jwtUtils.isTokenExpired(claims.getExpiration())){
-                return R.error(4004,"回传token校验失败,重新授权");
+            Claims claims = jwtUtils.getClaimByToken(token);
+            if (claims == null || !jwtUtils.isAccessTokenValid(redisCache, token)) {
+                return R.error(4004, "回传token校验失败,重新授权");
             }
             return R.ok();
 

+ 13 - 5
fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

@@ -268,17 +268,25 @@ public class PayScrmController {
                         if ("success".equals(result) || "SUCCESS".equals(result)) {
                             return "SUCCESS";
                         }
-                        // 返回空字符串表示重复回调或处理失败,回写SUCCESS避免重试,但记录日志
-                        logger.warn("易宝回调payConfirmMultiStore返回非success:{},payCode={}", result, payCode);
-                        return "SUCCESS";
+                        // 处理失败返回FAIL,让易宝重试(注意:空字符串表示已处理过/幂等,返回SUCCESS避免重复回调)
+                        if (StringUtils.isBlank(result)) {
+                            logger.warn("易宝回调payConfirmMultiStore返回空(payment已处理或状态异常),payCode={}", payCode);
+                            return "SUCCESS";
+                        }
+                        logger.error("易宝回调payConfirmMultiStore处理失败:result={},payCode={},返回FAIL触发重试", result, payCode);
+                        return "FAIL";
                     }
                     case "store_remain": {
                         String result = orderService.payRemainConfirmMultiStore(payCode, uniqueOrderNo, channelTrxId, bankOrderId);
                         if ("success".equals(result) || "SUCCESS".equals(result)) {
                             return "SUCCESS";
                         }
-                        logger.warn("易宝回调payRemainConfirmMultiStore返回非success:{},payCode={}", result, payCode);
-                        return "SUCCESS";
+                        if (StringUtils.isBlank(result)) {
+                            logger.warn("易宝回调payRemainConfirmMultiStore返回空,payCode={}", payCode);
+                            return "SUCCESS";
+                        }
+                        logger.error("易宝回调payRemainConfirmMultiStore处理失败:result={},payCode={},返回FAIL触发重试", result, payCode);
+                        return "FAIL";
                     }
 //                    case "payment": {
 //                        String result = storePaymentService.payConfirmMultiStore(payCode, uniqueOrderNo, channelTrxId, bankOrderId);

+ 13 - 5
fs-user-app/src/main/java/com/fs/app/controller/store/StoreScrmController.java

@@ -8,18 +8,15 @@ 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.StringUtils;
-import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.param.FsDoctorParam;
-import com.fs.his.param.FsStoreAuditParam;
 import com.fs.his.service.IFsDoctorService;
 import com.fs.hisStore.domain.FsStoreScrm;
+import com.fs.hisStore.service.IFsStorePromotionService;
 import com.fs.hisStore.service.IFsStoreScrmService;
 import com.fs.hisStore.utils.StoreAuditLogUtil;
 import com.fs.hisStore.vo.FsStoreRecommendListVO;
 import com.fs.hisStore.vo.FsStoreScrmVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.*;
 
@@ -45,6 +42,9 @@ public class StoreScrmController extends BaseController
     @Autowired
     private IFsDoctorService fsDoctorService;
 
+    @Autowired
+    private IFsStorePromotionService fsStorePromotionService;
+
     /**
      * 查询店铺管理列表
      */
@@ -67,14 +67,22 @@ public class StoreScrmController extends BaseController
     public AjaxResult getInfo(@PathVariable("storeId") Long storeId)
     {
         FsStoreScrm fsStore = fsStoreService.selectFsStoreByStoreId(storeId);
+        if (fsStore == null) {
+            return AjaxResult.error("店铺不存在");
+        }
 
         //获取店铺下绑定的药师职业证书
         FsDoctorParam param=new FsDoctorParam();
         param.setStoreId(fsStore.getStoreId());
         param.setDoctorType(2);
         fsStore.setDoctorList(fsDoctorService.getDoctorInfoByStoreId(param));
-        if(fsStore != null && fsStore.getPhone() != null)
+        if(fsStore.getPhone() != null) {
             fsStore.setPhone(ParseUtils.parsePhone(fsStore.getPhone()));
+        }
+
+        //获取活动详情
+        fsStore.setPromotionActivities(fsStorePromotionService.listActivePromotionDisplayByStoreId(storeId));
+
         return AjaxResult.success(fsStore);
     }
 

+ 10 - 20
fs-user-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -6,7 +6,6 @@ import com.fs.app.exception.FSException;
 import com.fs.app.utils.JwtUtils;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
-import io.jsonwebtoken.Claims;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Component;
@@ -17,52 +16,43 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 /**
- * 权限(Token)验证
+ * Token 验证:/app/app/* 登录(Redis app:login:token)滑动续期;其他登录仍按 JWT 过期时间校验。
  */
 @Component
 public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
     @Autowired
     private JwtUtils jwtUtils;
     @Autowired
-    RedisCache redisCache;
+    private RedisCache redisCache;
     public static final String USER_KEY = "userId";
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         Login annotation;
-        if(handler instanceof HandlerMethod) {
+        if (handler instanceof HandlerMethod) {
             annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
-        }else{
+        } else {
             return true;
         }
 
-        if(annotation == null){
+        if (annotation == null) {
             return true;
         }
 
-        //获取用户凭证
         String token = request.getHeader(jwtUtils.getHeader());
-        if(StringUtils.isBlank(token)){
+        if (StringUtils.isBlank(token)) {
             token = request.getParameter(jwtUtils.getHeader());
         }
-
-        //凭证为空
-        if(StringUtils.isBlank(token)){
+        if (StringUtils.isBlank(token)) {
             throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
         }
 
-        Claims claims = jwtUtils.getClaimByToken(token);
-        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+        Long userId = jwtUtils.validateToken(redisCache, token, true);
+        if (userId == null) {
             throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
         }
-//        //查询用户的TOKEN是否和REDIS中的一样
-//        String redisToken=redisCache.getCacheObject("token:"+ Long.parseLong(claims.getSubject()));
-//        if(redisToken==null||!redisToken.equals(token)){
-//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
-//        }
-        //设置userId到request里,后续根据userId,获取用户信息
-        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
 
+        request.setAttribute(USER_KEY, userId);
         return true;
     }
 }

+ 72 - 17
fs-user-app/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -1,6 +1,9 @@
 package com.fs.app.utils;
 
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 import org.slf4j.Logger;
@@ -9,58 +12,110 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
 import java.util.Date;
+import java.util.concurrent.TimeUnit;
 
 /**
- * jwt工具类
-
+ * App JWT 工具;{@link #APP_REDIS_TOKEN_KEY_PREFIX} 仅 /app/app/* 登录写入,用于 Redis 滑动续期。
  */
 @ConfigurationProperties(prefix = "fs.jwt")
 @Component
 public class JwtUtils {
-    private Logger logger = LoggerFactory.getLogger(getClass());
 
+    /** 仅 AppLoginController(/app/app/*)登录写入,与 SCRM 等 userToken: 区分 */
+    public static final String APP_REDIS_TOKEN_KEY_PREFIX = "app:login:token:";
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
 
     private String secret;
-    /** 默认7天(秒),防止配置未加载时 token 立即过期 */
+    /** 会话滑动时长(秒),默认 7 天 */
     private long expire = 604800L;
     private String header;
 
-    /**
-     * 生成jwt token
-     */
     public String generateToken(long userId) {
         Date nowDate = new Date();
-        //过期时间
         Date expireDate = new Date(nowDate.getTime() + expire * 1000);
-//        System.out.println("==============================="+secret);
-//        logger.info("secret:{}",secret);
         return Jwts.builder()
                 .setHeaderParam("typ", "JWT")
-                .setSubject(userId+"")
+                .setSubject(String.valueOf(userId))
                 .setIssuedAt(nowDate)
                 .setExpiration(expireDate)
                 .signWith(SignatureAlgorithm.HS512, secret)
                 .compact();
     }
 
+    /** /app/app/* 登录:签发 JWT 并写入 Redis 会话 */
+    public String issueAppToken(RedisCache redisCache, long userId) {
+        String token = generateToken(userId);
+        cacheAppToken(redisCache, userId, token);
+        return token;
+    }
+
+    public void cacheAppToken(RedisCache redisCache, long userId, String token) {
+        redisCache.setCacheObject(appRedisTokenKey(userId), token, getExpireSeconds(), TimeUnit.SECONDS);
+    }
+
+    /** App 会话滑动续期(客户端 token 字符串不变) */
+    public void renewAppSession(RedisCache redisCache, long userId, String token) {
+        cacheAppToken(redisCache, userId, token);
+    }
+
+    public String appRedisTokenKey(long userId) {
+        return APP_REDIS_TOKEN_KEY_PREFIX + userId;
+    }
+
+    /**
+     * 解析 token(签名非法返回 null;JWT 过期仍返回 claims,供 App Redis 会话续期使用)
+     */
     public Claims getClaimByToken(String token) {
         try {
             return Jwts.parser()
                     .setSigningKey(secret)
                     .parseClaimsJws(token)
                     .getBody();
-        }catch (Exception e){
+        } catch (ExpiredJwtException e) {
+            return e.getClaims();
+        } catch (Exception e) {
             logger.error("验证令牌错误", e);
             return null;
         }
     }
 
+    public boolean isTokenExpired(Date expiration) {
+        return expiration == null || expiration.before(new Date());
+    }
+
     /**
-     * token是否过期
-     * @return  true:过期
+     * 与 AuthorizationInterceptor 一致的校验(不续期)
      */
-    public boolean isTokenExpired(Date expiration) {
-        return expiration.before(new Date());
+    public boolean isAccessTokenValid(RedisCache redisCache, String token) {
+        return validateToken(redisCache, token, false) != null;
+    }
+
+    /**
+     * 校验 token;App 登录会话命中时可选滑动续期。无效返回 null。
+     * App 会话(Redis)与 wx 等纯 JWT 登录并存:仅 token 与 Redis 一致时走 App 滑动续期;否则按 JWT 是否过期校验。
+     */
+    public Long validateToken(RedisCache redisCache, String token, boolean renewSession) {
+        Claims claims = getClaimByToken(token);
+        if (claims == null || StringUtils.isEmpty(claims.getSubject())) {
+            return null;
+        }
+        long userId = Long.parseLong(claims.getSubject());
+        String appSessionToken = redisCache.getCacheObject(appRedisTokenKey(userId));
+        if (appSessionToken != null && appSessionToken.equals(token)) {
+            if (renewSession) {
+                renewAppSession(redisCache, userId, token);
+            }
+            return userId;
+        }
+        if (isTokenExpired(claims.getExpiration())) {
+            return null;
+        }
+        return userId;
+    }
+
+    public int getExpireSeconds() {
+        return expire > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) expire;
     }
 
     public String getSecret() {
@@ -80,7 +135,7 @@ public class JwtUtils {
     }
 
     public String getHeader() {
-        return header;
+        return StringUtils.isEmpty(header) ? "AppToken" : header;
     }
 
     public void setHeader(String header) {