Prechádzať zdrojové kódy

医健宝,积分商品相关代码提交

yjwang 2 dní pred
rodič
commit
e5b5f59327
30 zmenil súbory, kde vykonal 2644 pridanie a 71 odobranie
  1. 305 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralAfterSalesController.java
  2. 0 21
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java
  3. 22 5
      fs-company-app/src/main/java/com/fs/app/controller/CompanyTagController.java
  4. 14 0
      fs-service/src/main/java/com/fs/company/domain/CompanyTag.java
  5. 170 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralAfterSales.java
  6. 9 1
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java
  7. 2 0
      fs-service/src/main/java/com/fs/his/domain/FsPayConfig.java
  8. 106 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralAfterSalesMapper.java
  9. 41 2
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java
  10. 70 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralAfterSalesApplyParam.java
  11. 43 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralAfterSalesQueryParam.java
  12. 132 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralAfterSalesService.java
  13. 20 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java
  14. 2 2
      fs-service/src/main/java/com/fs/his/service/impl/FsExpressServiceImpl.java
  15. 392 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralAfterSalesServiceImpl.java
  16. 196 19
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  17. 7 2
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  18. 72 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralAfterSalesVO.java
  19. 6 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderListUVO.java
  20. 6 3
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  21. 2 2
      fs-service/src/main/resources/application-dev-yjb.yml
  22. 12 11
      fs-service/src/main/resources/mapper/company/CompanyTagMapper.xml
  23. 328 0
      fs-service/src/main/resources/mapper/his/FsIntegralAfterSalesMapper.xml
  24. 23 2
      fs-service/src/main/resources/mapper/his/FsIntegralOrderMapper.xml
  25. 26 0
      fs-user-app/src/main/java/com/fs/app/controller/InquiryOrderController.java
  26. 146 0
      fs-user-app/src/main/java/com/fs/app/controller/IntegralAfterSalesController.java
  27. 220 1
      fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java
  28. 118 0
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java
  29. 114 0
      积分售后表_数据库变更.sql
  30. 40 0
      积分订单状态重构_数据库变更.sql

+ 305 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralAfterSalesController.java

@@ -0,0 +1,305 @@
+package com.fs.his.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+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.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.domain.FsIntegralOrder;
+import com.fs.his.dto.ExpressInfoDTO;
+import com.fs.his.enums.ShipperCodeEnum;
+import com.fs.his.service.IFsExpressService;
+import com.fs.his.service.IFsIntegralAfterSalesService;
+import com.fs.his.service.IFsIntegralOrderService;
+import com.fs.his.vo.FsIntegralAfterSalesVO;
+import com.fs.framework.web.service.TokenService;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static com.fs.his.utils.PhoneUtil.decryptAutoPhoneMk;
+
+/**
+ * 积分订单售后单Controller
+ * <p>负责积分订单售后单的查询、详情、审核、撤销、退款等管理操作。</p>
+ * <p>售后流程(退款退货 serviceType=1):用户提交(0) → 平台审核(1) → 用户发货(2) → 仓库审核(3) → 财务审核(4) → 退款成功(5)</p>
+ * <p>售后流程(仅退款 serviceType=0):用户提交(0) → 平台审核(1) → 财务审核(4) → 退款成功(5)</p>
+ *
+ * @author fs
+ * @date 2026-07-22
+ */
+@RestController
+@RequestMapping("/his/integralAfterSales")
+public class FsIntegralAfterSalesController extends BaseController
+{
+    @Autowired
+    private IFsIntegralAfterSalesService fsIntegralAfterSalesService;
+
+    @Autowired
+    private IFsIntegralOrderService fsIntegralOrderService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IFsExpressService expressService;
+
+    /**
+     * 查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        startPage();
+        List<FsIntegralAfterSalesVO> list = fsIntegralAfterSalesService.selectFsIntegralAfterSalesVOList(fsIntegralAfterSales);
+        for (FsIntegralAfterSalesVO vo : list) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出积分订单售后单列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:export')")
+    @Log(title = "积分订单售后单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        List<FsIntegralAfterSalesVO> list = fsIntegralAfterSalesService.selectFsIntegralAfterSalesVOList(fsIntegralAfterSales);
+        for (FsIntegralAfterSalesVO vo : list) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+        }
+        ExcelUtil<FsIntegralAfterSalesVO> util = new ExcelUtil<>(FsIntegralAfterSalesVO.class);
+        return util.exportExcel(list, "积分订单售后单");
+    }
+
+    /**
+     * 获取积分订单售后单详情(含关联用户、企业、积分订单信息)
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        FsIntegralAfterSalesVO vo = fsIntegralAfterSalesService.selectFsIntegralAfterSalesVOById(id);
+        if (vo != null) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+        }
+        return AjaxResult.success(vo);
+    }
+
+    /**
+     * 根据订单编号查询售后单详情
+     * <p>用于订单列表跳转售后详情场景。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:query')")
+    @GetMapping(value = "/byOrderCode/{orderCode}")
+    public AjaxResult getByOrderCode(@PathVariable("orderCode") String orderCode)
+    {
+        List<FsIntegralAfterSalesVO> list = fsIntegralAfterSalesService.selectFsIntegralAfterSalesVOList(
+                new FsIntegralAfterSales() {{ setOrderCode(orderCode); setSalesStatus(0); }});
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.success();
+        }
+        // 取最新一条售后单(按创建时间倒序)
+        FsIntegralAfterSalesVO vo = list.get(0);
+        if (vo != null) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+        }
+        return AjaxResult.success(vo);
+    }
+
+    /**
+     * 平台审核
+     * <p>仅售后状态正常(salesStatus=0)且已提交(status=0)的售后单可审核。</p>
+     * <p>仅退款(serviceType=0):status 0 → 1(等待财务退款)。</p>
+     * <p>退款退货(serviceType=1):
+     * <ul>
+     *   <li>用户已填写寄回物流(deliverySn 非空):status 0 → 2(直接进入仓库审核)</li>
+     *   <li>用户未填写寄回物流(deliverySn 为空):status 0 → 1(等待平台编辑物流后进入仓库审核)</li>
+     * </ul></p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:audit')")
+    @Log(title = "积分售后-平台审核", businessType = BusinessType.UPDATE)
+    @PutMapping("/audit")
+    public AjaxResult audit(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        FsIntegralAfterSales db = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(fsIntegralAfterSales.getId());
+        if (db == null) {
+            return AjaxResult.error("售后单不存在");
+        }
+        if (db.getSalesStatus() != null && db.getSalesStatus() != 0) {
+            return AjaxResult.error("售后单已撤销或拒绝,不可审核");
+        }
+        if (db.getStatus() == null || db.getStatus() != 0) {
+            return AjaxResult.error("售后单状态不允许平台审核");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        // 退货退款且用户已填写寄回物流,审核后直接进入仓库审核节点;否则按正常流程进入待发货节点
+        Integer serviceType = db.getServiceType();
+        if (serviceType != null && serviceType == 1 && StrUtil.isNotEmpty(db.getDeliverySn())) {
+            fsIntegralAfterSales.setStatus(2);
+        } else {
+            fsIntegralAfterSales.setStatus(1);
+        }
+        fsIntegralAfterSales.setOperator(loginUser.getUser().getNickName());
+        return toAjax(fsIntegralAfterSalesService.updateFsIntegralAfterSales(fsIntegralAfterSales));
+    }
+
+    /**
+     * 撤销售后单(salesStatus: 0 → 1)
+     * <p>仅正常状态(salesStatus=0)且未进入退款流程(status < 4)的售后单可撤销。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:cancel')")
+    @Log(title = "积分售后-撤销", businessType = BusinessType.UPDATE)
+    @PutMapping("/cancel")
+    public AjaxResult cancel(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        FsIntegralAfterSales db = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(fsIntegralAfterSales.getId());
+        if (db == null) {
+            return AjaxResult.error("售后单不存在");
+        }
+        if (db.getSalesStatus() != null && db.getSalesStatus() != 0) {
+            return AjaxResult.error("售后单已被撤销或拒绝,不可重复操作");
+        }
+        if (db.getStatus() != null && db.getStatus() >= 4) {
+            return AjaxResult.error("售后单已进入退款流程,不可撤销");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsIntegralAfterSales.setSalesStatus(1);
+        fsIntegralAfterSales.setOperator(loginUser.getUser().getNickName());
+        return toAjax(fsIntegralAfterSalesService.updateFsIntegralAfterSales(fsIntegralAfterSales));
+    }
+
+    /**
+     * 仓库审核(status: 2 → 3)
+     * <p>仅退款退货(serviceType=1)流程中使用,用户发货后由仓库确认收货。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:audit')")
+    @Log(title = "积分售后-仓库审核", businessType = BusinessType.UPDATE)
+    @PutMapping("/warehouseAudit")
+    public AjaxResult warehouseAudit(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        FsIntegralAfterSales db = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(fsIntegralAfterSales.getId());
+        if (db == null) {
+            return AjaxResult.error("售后单不存在");
+        }
+        if (db.getSalesStatus() != null && db.getSalesStatus() != 0) {
+            return AjaxResult.error("售后单已撤销或拒绝,不可审核");
+        }
+        if (db.getStatus() == null || db.getStatus() != 2) {
+            return AjaxResult.error("售后单状态不允许仓库审核");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsIntegralAfterSales.setStatus(3);
+        fsIntegralAfterSales.setOperator(loginUser.getUser().getNickName());
+        return toAjax(fsIntegralAfterSalesService.updateFsIntegralAfterSales(fsIntegralAfterSales));
+    }
+
+    /**
+     * 财务审核退款(status: 1/3 → 4)
+     * <p>仅退款(serviceType=0):平台审核后(status=1)直接财务退款。</p>
+     * <p>退款退货(serviceType=1):仓库审核后(status=3)财务退款。</p>
+     * <p>触发积分订单退款流程(资金原路退回 + 积分退回),回调确认后变为退款成功(5)。</p>
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:refund')")
+    @Log(title = "积分售后-财务退款", businessType = BusinessType.UPDATE)
+    @PutMapping("/refund")
+    public AjaxResult refund(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        FsIntegralAfterSales db = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(fsIntegralAfterSales.getId());
+        if (db == null) {
+            return AjaxResult.error("售后单不存在");
+        }
+        if (db.getSalesStatus() != null && db.getSalesStatus() != 0) {
+            return AjaxResult.error("售后单已撤销或拒绝,不可退款");
+        }
+        if (db.getStatus() == null || db.getStatus() >= 4) {
+            return AjaxResult.error("售后单状态不允许退款");
+        }
+        // 仅退款(serviceType=0):status=1 可退款;退款退货(serviceType=1):status=3 可退款
+        Integer serviceType = db.getServiceType();
+        Integer status = db.getStatus();
+        if (serviceType != null && serviceType == 0) {
+            if (status != 1) {
+                return AjaxResult.error("仅退款类型需平台审核后方可退款");
+            }
+        } else {
+            if (status != 3) {
+                return AjaxResult.error("退款退货类型需仓库审核后方可退款");
+            }
+        }
+        // 1. 调用积分订单退款(资金+积分,内部幂等)
+        try {
+            fsIntegralOrderService.mandatoryRefunds(db.getOrderCode());
+        } catch (Exception e) {
+            logger.error("积分售后退款异常,售后单ID={},订单编号={}", db.getId(), db.getOrderCode(), e);
+            return AjaxResult.error("退款处理异常:" + e.getMessage());
+        }
+        // 2. 更新售后单状态为退款中(status=4),等待回调确认后变为退款成功(5)
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        FsIntegralAfterSales update = new FsIntegralAfterSales();
+        update.setId(db.getId());
+        update.setStatus(4);
+        update.setOperator(loginUser.getUser().getNickName());
+        fsIntegralAfterSalesService.updateFsIntegralAfterSales(update);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 编辑用户寄回物流(status: 1 → 2)
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:edit')")
+    @Log(title = "积分售后-编辑物流", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        FsIntegralAfterSales db = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(fsIntegralAfterSales.getId());
+        if (db == null) {
+            return AjaxResult.error("售后单不存在");
+        }
+        fsIntegralAfterSales.setStatus(2);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsIntegralAfterSales.setOperator(loginUser.getUser().getNickName());
+        return toAjax(fsIntegralAfterSalesService.updateFsIntegralAfterSales(fsIntegralAfterSales));
+    }
+
+    /**
+     * 查询用户寄回物流信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralAfterSales:query')")
+    @GetMapping(value = "/getExpress/{id}")
+    public R getExpress(@PathVariable("id") Long id)
+    {
+        FsIntegralAfterSales afterSales = fsIntegralAfterSalesService.selectFsIntegralAfterSalesById(id);
+        if (afterSales == null || StrUtil.isEmpty(afterSales.getDeliverySn())) {
+            return R.ok();
+        }
+        String lastFourNumber = "";
+        if (afterSales.getDeliveryCode() != null && afterSales.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
+            // 顺丰快递需要收件人手机号后四位
+            FsIntegralOrder order = fsIntegralOrderService.selectFsIntegralOrderByOrderCode(afterSales.getOrderCode());
+            if (order != null && StrUtil.isNotEmpty(order.getUserPhone())) {
+                String phone = order.getUserPhone();
+                if (phone.length() == 11) {
+                    lastFourNumber = StrUtil.sub(phone, phone.length(), -4);
+                }
+            }
+        }
+        ExpressInfoDTO expressInfoDTO = expressService.getExpressInfo(
+                afterSales.getOrderCode(),
+                afterSales.getShipperCode(),
+                afterSales.getDeliverySn(),
+                lastFourNumber);
+        return R.ok().put("data", expressInfoDTO);
+    }
+}

+ 0 - 21
fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java

@@ -171,27 +171,6 @@ public class FsIntegralOrderController extends BaseController
     {
         return toAjax(fsIntegralOrderService.deleteFsIntegralOrderByOrderIds(orderIds));
     }
-    @PreAuthorize("@ss.hasPermi('his:integralOrder:cancel')")
-    @Log(title = "积分商品订单", businessType = BusinessType.UPDATE)
-    @PostMapping("/cancelOrder")
-    public AjaxResult cancelOrder(@RequestBody Map<String, String> requestBody){
-        String orderCode = requestBody.get("orderCode");
-        return toAjax(fsIntegralOrderService.cancelOrder(orderCode));
-    }
-
-    /**
-     * 管理员强制退款(不校验是否发货)
-     * <p>流程:取消订单 → 还原库存 → 易宝原路退款 → 退回积分(logType=27)。</p>
-     * <p>幂等:通过 logType=27 存在性判断,重复退款不会重复退积分。</p>
-     */
-    @PreAuthorize("@ss.hasPermi('his:integralOrder:cancel')")
-    @Log(title = "积分商品订单", businessType = BusinessType.UPDATE)
-    @PostMapping("/mandatoryRefunds")
-    public AjaxResult mandatoryRefunds(@RequestBody Map<String, String> requestBody){
-        String orderCode = requestBody.get("orderCode");
-        return toAjax(fsIntegralOrderService.mandatoryRefunds(orderCode));
-    }
-
     /**
      * 确认收货(status: 2 待收货 → 3 已完成)
      */

+ 22 - 5
fs-company-app/src/main/java/com/fs/app/controller/CompanyTagController.java

@@ -9,8 +9,10 @@ import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.CompanyTag;
 import com.fs.company.domain.CompanyTagUser;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyTagService;
 import com.fs.company.service.ICompanyTagUserService;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.company.vo.CompanyTagUserVO;
 import com.fs.store.vo.h5.CompanyUserTagListVO;
 import com.github.pagehelper.PageHelper;
@@ -35,9 +37,15 @@ public class CompanyTagController extends AppBaseController {
 
     private final ICompanyTagService companyTagService;
     private final ICompanyTagUserService companyTagUserService;
+    private final ICompanyUserService companyUserService;
 
     /**
      * 查询公司标签列表
+     * <p>
+     * 权限规则:
+     * - 管理员(userType=00/02):可查看公司下所有标签,支持按 companyUserId 过滤指定员工的标签
+     * - 非管理员:仅能查看自己打过的标签,前端传入的 companyUserId 会被忽略以防越权
+     * </p>
      */
     @Login
     @GetMapping("/list")
@@ -46,7 +54,12 @@ public class CompanyTagController extends AppBaseController {
                   @RequestParam(required = false, defaultValue = "1") Integer pageNum,
                   @RequestParam(required = false, defaultValue = "10") Integer pageSize,
                   @RequestParam(required = false) Long companyUserId) {
-        log.debug("查询公司标签列表 keyword: {}, pageNum: {}, pageSize: {}", keyword, pageNum, pageSize, companyUserId);
+        log.debug("查询公司标签列表 keyword: {}, pageNum: {}, pageSize: {}, companyUserId: {}", keyword, pageNum, pageSize, companyUserId);
+
+        // 获取当前登录企业员工信息,判断是否为管理员
+        Long currentUserId = getCompanyUserId();
+        CompanyUser currentUser = companyUserService.selectCompanyUserByIdForRedis(currentUserId);
+        boolean isAdmin = currentUser != null && currentUser.isAdmin();
 
         Map<String, Object> params = new HashMap<>();
         params.put("companyId", getCompanyId());
@@ -55,18 +68,21 @@ public class CompanyTagController extends AppBaseController {
         }
 
         PageHelper.startPage(pageNum, pageSize);
-        if (ObjectUtil.isNotEmpty(companyUserId)) {
-            params.put("companyUserId", companyUserId);
-            List<CompanyTag> list = companyTagService.selectCompanyTagByList(params);
+        if (isAdmin) {
+            // 管理员:可查看所有标签;若指定 companyUserId 则按该员工过滤
+            List<CompanyTag> list = companyTagService.selectCompanyTagListByMap(params);
             return R.ok().put("data", new PageInfo<>(list));
         } else {
-            List<CompanyTag> list = companyTagService.selectCompanyTagListByMap(params);
+            // 非管理员:强制按当前用户过滤,仅返回自己打过的标签(忽略前端传入的 companyUserId 防越权)
+            params.put("companyUserId", currentUserId);
+            List<CompanyTag> list = companyTagService.selectCompanyTagByList(params);
             return R.ok().put("data", new PageInfo<>(list));
         }
     }
 
         /**
          * 新增公司标签
+         * <p>记录创建人(当前登录销售)company_user_id,用于非管理员数据隔离查询。</p>
          */
     @Login
     @PostMapping("/add")
@@ -74,6 +90,7 @@ public class CompanyTagController extends AppBaseController {
     public R add(@RequestBody CompanyTag companyTag) throws CustomException {
         log.debug("新增公司标签 companyTag: {}", JSON.toJSONString(companyTag));
         companyTag.setCompanyId(getCompanyId());
+        companyTag.setCompanyUserId(getCompanyUserId());
         companyTagService.insertCompanyTag(companyTag);
         return R.ok();
     }

+ 14 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyTag.java

@@ -28,6 +28,10 @@ public class CompanyTag extends BaseEntity
     @Excel(name = "标签")
     private String tag;
 
+    /** 创建人(销售)企业员工ID */
+    @Excel(name = "创建人企业员工ID")
+    private Long companyUserId;
+
     public void setTagId(Long tagId)
     {
         this.tagId = tagId;
@@ -55,6 +59,15 @@ public class CompanyTag extends BaseEntity
     {
         return tag;
     }
+    public void setCompanyUserId(Long companyUserId)
+    {
+        this.companyUserId = companyUserId;
+    }
+
+    public Long getCompanyUserId()
+    {
+        return companyUserId;
+    }
 
     @Override
     public String toString() {
@@ -62,6 +75,7 @@ public class CompanyTag extends BaseEntity
             .append("tagId", getTagId())
             .append("companyId", getCompanyId())
             .append("tag", getTag())
+            .append("companyUserId", getCompanyUserId())
             .append("createTime", getCreateTime())
             .toString();
     }

+ 170 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralAfterSales.java

@@ -0,0 +1,170 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 积分订单售后单对象 fs_integral_after_sales
+ *
+ * @author fs
+ * @date 2026-07-21
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FsIntegralAfterSales extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    private Long id;
+
+    /** 订单ID */
+    @Excel(name = "订单ID")
+    private Long orderId;
+
+    /** 订单编号 */
+    @Excel(name = "订单编号")
+    private String orderCode;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 企业ID */
+    private Long companyId;
+
+    /** 企业员工ID */
+    private Long companyUserId;
+
+    /** 退款金额 */
+    @Excel(name = "退款金额")
+    private BigDecimal refundAmount;
+
+    /** 退回积分 */
+    @Excel(name = "退回积分")
+    private Long refundIntegral;
+
+    /** 服务类型 0仅退款 1退货退款 */
+    @Excel(name = "服务类型")
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @Excel(name = "申请原因")
+    private String reasons;
+
+    /** 说明 */
+    @Excel(name = "说明")
+    private String explains;
+
+    /** 说明图片,多个用逗号分割 */
+    @Excel(name = "说明图片")
+    private String explainImg;
+
+    /** 物流公司编码 */
+    private String shipperCode;
+
+    /** 物流单号 */
+    @Excel(name = "物流单号")
+    private String deliverySn;
+
+    /** 物流名称 */
+    @Excel(name = "物流名称")
+    private String deliveryName;
+
+    /** 物流公司编码(兼容字段) */
+    private String deliveryCode;
+
+    /**
+     * 审核状态
+     * 0 用户提交(待平台审核)
+     * 1 平台已审核
+     * 2 用户已发货(待仓库审核)
+     * 3 仓库已审核(待财务审核)
+     * 4 退款中(财务已发起退款,等待回调确认)
+     * 5 退款成功(回调确认完成)
+     */
+    @Excel(name = "审核状态")
+    private Integer status;
+
+    /**
+     * 售后状态
+     * 0 正常
+     * 1 用户取消
+     * 2 商家拒绝
+     * 3 已完成
+     */
+    @Excel(name = "售后状态")
+    private Integer salesStatus;
+
+    /** 订单当前状态 */
+    @Excel(name = "订单当前状态")
+    private Integer orderStatus;
+
+    /** 商家收货人 */
+    private String consignee;
+
+    /** 商家手机号 */
+    private String phoneNumber;
+
+    /** 商家地址 */
+    private String address;
+
+    /** 商家收货电话 */
+    private String consigneePhone;
+
+    /** 部门ID */
+    private Long deptId;
+
+    /** 操作人 */
+    private String operator;
+
+    /** 备注 */
+    private String remark;
+
+    /** 逻辑删除 0正常 1已删除 */
+    private Integer isDel;
+
+    /** 开始时间(查询用) */
+    @JsonIgnore
+    private String beginTime;
+
+    /** 结束时间(查询用) */
+    @JsonIgnore
+    private String endTime;
+
+    /** 创建时间区间(查询用) */
+    @TableField(exist = false)
+    private String createTimeRange;
+
+    /** 创建时间区间数组(查询用) */
+    @TableField(exist = false)
+    private String[] createTimeList;
+
+    // ========== 关联查询字段(非表字段) ==========
+
+    /** 用户昵称(关联查询) */
+    @TableField(exist = false)
+    private String nickName;
+
+    /** 用户电话(关联查询) */
+    @TableField(exist = false)
+    private String userPhone;
+
+    /** 企业名称(关联查询) */
+    @TableField(exist = false)
+    private String companyName;
+
+    /** 企业员工昵称(关联查询) */
+    @TableField(exist = false)
+    private String companyUserNickName;
+
+    /** 商品名称(关联查询) */
+    @TableField(exist = false)
+    private String goodsName;
+}

+ 9 - 1
fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java

@@ -1,5 +1,6 @@
 package com.fs.his.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
@@ -99,6 +100,11 @@ public class FsIntegralOrder extends BaseEntity
     @Excel(name = "发货时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date deliveryTime;
 
+    /** 完成时间(确认收货时间) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
 
     /**
      * 企业微信ID
@@ -118,6 +124,8 @@ public class FsIntegralOrder extends BaseEntity
     @Excel(name = "销售公司ID")
     private Long companyId;
 
-
+    /** 是否可申请售后 0不可 1可(非表字段,由 Controller 按订单状态+售后时效计算填充) */
+    @TableField(exist = false)
+    private Integer isAfterSales;
 
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/domain/FsPayConfig.java

@@ -23,6 +23,8 @@ public class FsPayConfig {
     private String ybPayNotifyUrl;
     /** 易宝退款回调地址 */
     private String ybRefundNotifyUrl;
+    /** 易宝积分订单退款回调地址(与普通商品退款回调隔离,独立处理积分订单退款状态) */
+    private String ybIntegralRefundNotifyUrl;
     /** 易宝分账回调地址 */
     private String ybDivideNotifyUrl;
     /** 易宝清算回调地址 */

+ 106 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralAfterSalesMapper.java

@@ -0,0 +1,106 @@
+package com.fs.his.mapper;
+
+import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.param.FsIntegralAfterSalesQueryParam;
+import com.fs.his.vo.FsIntegralAfterSalesVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 积分订单售后单Mapper接口
+ *
+ * @author fs
+ * @date 2026-07-21
+ */
+public interface FsIntegralAfterSalesMapper
+{
+    /**
+     * 查询积分订单售后单
+     *
+     * @param id 积分订单售后单主键
+     * @return 积分订单售后单
+     */
+    public FsIntegralAfterSales selectFsIntegralAfterSalesById(Long id);
+
+    /**
+     * 查询积分订单售后单列表
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 积分订单售后单集合
+     */
+    public List<FsIntegralAfterSales> selectFsIntegralAfterSalesList(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 新增积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    public int insertFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 修改积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    public int updateFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 删除积分订单售后单
+     *
+     * @param id 积分订单售后单主键
+     * @return 结果
+     */
+    public int deleteFsIntegralAfterSalesById(Long id);
+
+    /**
+     * 批量删除积分订单售后单
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteFsIntegralAfterSalesByIds(Long[] ids);
+
+    /**
+     * 根据订单编号查询售后单
+     *
+     * @param orderCode 订单编号
+     * @return 售后单列表
+     */
+    public List<FsIntegralAfterSales> selectFsIntegralAfterSalesByOrderCode(@Param("orderCode") String orderCode);
+
+    /**
+     * 根据订单编号统计未完成售后单数量
+     *
+     * @param orderCode 订单编号
+     * @return 数量
+     */
+    public int countUnfinishedByOrderCode(@Param("orderCode") String orderCode);
+
+    /**
+     * 查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     *
+     * @param fsIntegralAfterSales 查询条件
+     * @return 售后单VO集合
+     */
+    public List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOList(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 查询积分订单售后单详情(含关联用户、企业、积分订单信息)
+     *
+     * @param id 售后单主键
+     * @return 售后单VO
+     */
+    public FsIntegralAfterSalesVO selectFsIntegralAfterSalesVOById(Long id);
+
+    /**
+     * App 端查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     * <p>强制按 userId 过滤,支持 status/serviceType/salesStatus/orderCode 筛选。</p>
+     *
+     * @param param 查询参数(含分页字段)
+     * @return 售后单VO集合
+     */
+    public List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOListByParam(FsIntegralAfterSalesQueryParam param);
+}

+ 41 - 2
fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java

@@ -107,8 +107,47 @@ public interface FsIntegralOrderMapper
     @Select("select fio.*,fu.nick_name,fu.phone from fs_integral_order fio left join fs_user fu on fu.user_id = fio.user_id where order_id=#{orderId} ")
     FsIntegralOrderPVO selectFsIntegralOrderPVO(Long orderId);
 
+    /**
+     * 取消订单(status → -3 已取消)
+     * <p>用户主动取消未支付订单,或管理员主动取消未发货订单。</p>
+     *
+     * @param orderId 订单ID
+     * @return 影响行数
+     */
     int cancelOrder(@Param("orderId") Long orderId);
 
+    /**
+     * 标记订单为退款中(status → -1 退款中)
+     * <p>用户提交售后申请后调用,订单进入退款处理流程。</p>
+     *
+     * @param orderId 订单ID
+     * @return 影响行数
+     */
+    int markAfterSalesStatus(@Param("orderId") Long orderId);
+
+    /**
+     * 标记订单为已退款(status → -2 已退款)
+     * <p>退款成功后调用(mandatoryRefunds 资金+积分退款完成)。</p>
+     *
+     * @param orderId 订单ID
+     * @return 影响行数
+     */
+    int refundOrder(@Param("orderId") Long orderId);
+
+    /**
+     * 恢复订单状态(撤销售后时调用)
+     * <p>将订单状态从 -1 退款中恢复为售后申请前冗余的原始状态,
+     * 带旧状态=-1 校验的乐观更新,避免并发场景下状态被覆盖。</p>
+     *
+     * @param orderId    订单ID
+     * @param oldStatus  期望的旧状态(必须为 -1 退款中,防止误恢复)
+     * @param newStatus  恢复后的新状态
+     * @return 影响行数
+     */
+    int restoreOrderStatus(@Param("orderId") Long orderId,
+                           @Param("oldStatus") Integer oldStatus,
+                           @Param("newStatus") Integer newStatus);
+
     /**
      * 确认收货(status: 2 待收货 → 3 已完成)
      * <p>带旧状态校验的乐观更新,避免并发场景下状态被覆盖。</p>
@@ -121,7 +160,7 @@ public interface FsIntegralOrderMapper
 
     /**
      * 统计用户已购买指定积分商品的有效订单数(限购校验用)
-     * <p>排除已取消(status=5)和已删除(status=-1)的订单;通过 JSON 函数精确匹配 item_json 中的 goodsId。</p>
+     * <p>排除无效订单:5已取消、-1退款中、-2已退款、-3已取消;通过 JSON 函数精确匹配 item_json 中的 goodsId。</p>
      *
      * @param userId  用户ID
      * @param goodsId 积分商品ID
@@ -129,7 +168,7 @@ public interface FsIntegralOrderMapper
      */
     @Select("SELECT COUNT(1) FROM fs_integral_order " +
             "WHERE user_id = #{userId} " +
-            "AND status NOT IN (5, -1) " +
+            "AND status NOT IN (5, -1, -2, -3) " +
             "AND JSON_CONTAINS(JSON_EXTRACT(item_json, '$[*].goodsId'), CAST(#{goodsId} AS JSON))")
     int selectUserPurchasedCount(@Param("userId") Long userId, @Param("goodsId") Long goodsId);
 }

+ 70 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralAfterSalesApplyParam.java

@@ -0,0 +1,70 @@
+package com.fs.his.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 积分订单售后申请参数
+ * <p>积分订单商品信息统一存在 fs_integral_order.item_json 中,无需像普通商品售后那样传 productList。</p>
+ *
+ * @author fs
+ * @date 2026-07-22
+ */
+@Data
+public class FsIntegralAfterSalesApplyParam implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 订单编号 */
+    @NotBlank(message = "订单编号不能为空")
+    private String orderCode;
+
+    /** 服务类型 0仅退款 1退货退款 */
+    @NotNull(message = "服务类型不能为空")
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @NotBlank(message = "申请原因不能为空")
+    private String reasons;
+
+    /** 申请说明 */
+    private String explains;
+
+    /** 申请说明图片,多个用逗号分割 */
+    private String explainImg;
+
+    /** 退款金额(现金部分,仅积分+现金支付时填写) */
+    private BigDecimal refundAmount;
+
+    /** 退回积分(不传则默认退回订单全部积分) */
+    private Long refundIntegral;
+
+    // ========== 以下为"一步提交"可选字段(同时传商家收货信息+物流信息时使用) ==========
+
+    /** 商家收货人 */
+    private String consignee;
+
+    /** 商家手机号 */
+    @JsonProperty("phone_number")
+    private String phoneNumber;
+
+    /** 商家地址 */
+    private String address;
+
+    /** 物流公司编码 */
+    @JsonProperty("shipper_code")
+    private String shipperCode;
+
+    /** 物流单号(有值则视为退货退款,serviceType 强制为 1) */
+    @JsonProperty("delivery_sn")
+    private String deliverySn;
+
+    /** 物流名称 */
+    @JsonProperty("delivery_name")
+    private String deliveryName;
+}

+ 43 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralAfterSalesQueryParam.java

@@ -0,0 +1,43 @@
+package com.fs.his.param;
+
+import com.fs.common.param.BaseQueryParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 积分订单售后单 App 端查询参数
+ * <p>参考 {@link com.fs.hisStore.param.FsStoreAfterSalesQueryParam} 设计,
+ * 继承 {@link BaseQueryParam} 获取分页字段(page/pageSize)。</p>
+ * <p>App 端查询强制按当前登录用户 userId 过滤,防止越权查看他人售后单。</p>
+ *
+ * @author fs
+ * @date 2026-07-23
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FsIntegralAfterSalesQueryParam extends BaseQueryParam implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 用户ID(App端由 Controller 强制设置为当前登录用户,前端传值无效) */
+    private Long userId;
+
+    /**
+     * 查询类型(前端传入,非售后单 status 字段)
+     * 0:全部
+     * 1:售后中(审核中/待发货/退款中等进行中的状态)
+     * 2:已完成(退款成功/已撤销/已拒绝)
+     */
+    private Integer status;
+
+    /** 售后状态 0正常 1用户取消 2商家拒绝 */
+    private Integer salesStatus;
+
+    /** 服务类型 0仅退款 1退货退款 */
+    private Integer serviceType;
+
+    /** 订单编号(模糊查询) */
+    private String orderCode;
+}

+ 132 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralAfterSalesService.java

@@ -0,0 +1,132 @@
+package com.fs.his.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.param.FsIntegralAfterSalesApplyParam;
+import com.fs.his.param.FsIntegralAfterSalesQueryParam;
+import com.fs.his.vo.FsIntegralAfterSalesVO;
+
+import java.util.List;
+
+/**
+ * 积分订单售后单Service接口
+ *
+ * @author fs
+ * @date 2026-07-21
+ */
+public interface IFsIntegralAfterSalesService
+{
+    /**
+     * 查询积分订单售后单
+     *
+     * @param id 积分订单售后单主键
+     * @return 积分订单售后单
+     */
+    FsIntegralAfterSales selectFsIntegralAfterSalesById(Long id);
+
+    /**
+     * 查询积分订单售后单列表
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 积分订单售后单集合
+     */
+    List<FsIntegralAfterSales> selectFsIntegralAfterSalesList(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 新增积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    int insertFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 修改积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    int updateFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 删除积分订单售后单(逻辑删除)
+     *
+     * @param id 积分订单售后单主键
+     * @return 结果
+     */
+    int deleteFsIntegralAfterSalesById(Long id);
+
+    /**
+     * 批量删除积分订单售后单(逻辑删除)
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    int deleteFsIntegralAfterSalesByIds(Long[] ids);
+
+    /**
+     * 根据订单编号查询售后单
+     *
+     * @param orderCode 订单编号
+     * @return 售后单列表
+     */
+    List<FsIntegralAfterSales> selectFsIntegralAfterSalesByOrderCode(String orderCode);
+
+    /**
+     * 根据订单编号统计未完成售后单数量
+     *
+     * @param orderCode 订单编号
+     * @return 数量
+     */
+    int countUnfinishedByOrderCode(String orderCode);
+
+    /**
+     * 查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     *
+     * @param fsIntegralAfterSales 查询条件
+     * @return 售后单VO集合
+     */
+    List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOList(FsIntegralAfterSales fsIntegralAfterSales);
+
+    /**
+     * 查询积分订单售后单详情(含关联用户、企业、积分订单信息)
+     *
+     * @param id 售后单主键
+     * @return 售后单VO
+     */
+    FsIntegralAfterSalesVO selectFsIntegralAfterSalesVOById(Long id);
+
+    /**
+     * App 端查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     * <p>强制按 userId 过滤,支持 status/serviceType/salesStatus/orderCode 筛选。</p>
+     *
+     * @param param 查询参数(含分页字段、userId)
+     * @return 售后单VO集合
+     */
+    List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOListByParam(FsIntegralAfterSalesQueryParam param);
+
+    /**
+     * 用户申请积分订单售后
+     * <p>参考普通商品售后流程,针对积分订单特性简化:
+     * 无独立商品详情表(商品信息在 fs_integral_order.item_json),无操作流水表。</p>
+     * <p>校验:订单归属、订单状态、重复申请。生成售后单并冗余订单状态。</p>
+     *
+     * @param userId 当前登录用户ID
+     * @param param  售后申请参数
+     * @return 操作结果
+     */
+    R applyForAfterSales(Long userId, FsIntegralAfterSalesApplyParam param);
+
+    /**
+     * 用户撤销售后申请
+     * <p>参考普通商品售后 {@link com.fs.hisStore.service.impl.FsStoreAfterSalesScrmServiceImpl#revoke} 实现,
+     * 针对积分订单特性调整:撤销后通过 {@code restoreOrderStatus} 恢复订单原始状态。</p>
+     * <p>校验:售后单归属、售后单状态(已取消/已拒绝不可撤销)、
+     * 审核状态(用户已发货/退款成功不可撤销)。</p>
+     *
+     * @param userId  当前登录用户ID
+     * @param salesId 售后单ID
+     * @return 操作结果
+     */
+    R revoke(Long userId, Long salesId);
+}

+ 20 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java

@@ -28,6 +28,14 @@ public interface IFsIntegralOrderService
      */
     public FsIntegralOrder selectFsIntegralOrderByOrderId(Long orderId);
 
+    /**
+     * 根据订单编号查询积分商品订单
+     *
+     * @param orderCode 订单编号
+     * @return 积分商品订单
+     */
+    public FsIntegralOrder selectFsIntegralOrderByOrderCode(String orderCode);
+
     /**
      * 查询积分商品订单列表
      *
@@ -117,6 +125,18 @@ public interface IFsIntegralOrderService
      */
     String payConfirmByYop(String payCode, String uniqueOrderNo, String channelTrxId, String bankOrderId);
 
+    /**
+     * 易宝积分订单退款回调确认(更新支付记录退款状态)
+     * <p>易宝退款成功后异步回调,将支付记录从"退款中(-2)"更新为"已退款(-1)"。</p>
+     * <p>幂等:已退款(status=-1)直接跳过。</p>
+     * <p>注意:积分退还在发起退款时已同步完成(refundIntegralSafely),本方法仅更新资金退款状态。</p>
+     *
+     * @param paymentId     支付记录主键(从退款请求号 refund-{paymentId} 拆分得到)
+     * @param refundAmount  退款金额(易宝回调返回,可能为 null)
+     * @return "success"=处理成功 / "FAIL"=处理失败需重试 / ""=未找到支付记录或异常
+     */
+    String refundConfirmByYop(String paymentId, String refundAmount);
+
     AjaxResult export(FsIntegralOrder fsIntegralOrder);
 
     int cancelOrder(String orderCode);

+ 2 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsExpressServiceImpl.java

@@ -129,9 +129,9 @@ public class FsExpressServiceImpl implements IFsExpressService
     @Override
     public ExpressInfoDTO getExpressInfo(String OrderCode, String ShipperCode, String LogisticCode, String lastFourNumber)     {
 
-        //处理顺丰查询轨迹需手机号码后4位
+        //处理顺丰查询轨迹需手机号码后4位(常量在左侧避免 ShipperCode 为 null 时 NPE)
         String requestData;
-        if (ShipperCode.equals(ShipperCodeEnum.SF.getValue())) {
+        if (ShipperCodeEnum.SF.getValue().equals(ShipperCode)) {
             requestData = "{'OrderCode':'" + OrderCode + "','ShipperCode':'" + ShipperCode + "','LogisticCode':'" + LogisticCode + "','CustomerName':'" + lastFourNumber + "'}";
         } else {
             requestData = "{'OrderCode':'" + OrderCode + "','ShipperCode':'" + ShipperCode + "','LogisticCode':'" + LogisticCode + "'}";

+ 392 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralAfterSalesServiceImpl.java

@@ -0,0 +1,392 @@
+package com.fs.his.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsIntegralAfterSales;
+import com.fs.his.domain.FsIntegralOrder;
+import com.fs.his.mapper.FsIntegralAfterSalesMapper;
+import com.fs.his.mapper.FsIntegralOrderMapper;
+import com.fs.his.param.FsIntegralAfterSalesApplyParam;
+import com.fs.his.param.FsIntegralAfterSalesQueryParam;
+import com.fs.his.service.IFsIntegralAfterSalesService;
+import com.fs.his.vo.FsIntegralAfterSalesVO;
+import com.fs.hisStore.config.StoreConfig;
+import com.fs.system.service.ISysConfigService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+
+/**
+ * 积分订单售后单Service实现
+ *
+ * @author fs
+ * @date 2026-07-21
+ */
+@Service
+public class FsIntegralAfterSalesServiceImpl implements IFsIntegralAfterSalesService
+{
+    private static final Logger log = LoggerFactory.getLogger(FsIntegralAfterSalesServiceImpl.class);
+
+    @Autowired
+    private FsIntegralAfterSalesMapper fsIntegralAfterSalesMapper;
+
+    @Autowired
+    private FsIntegralOrderMapper fsIntegralOrderMapper;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 查询积分订单售后单
+     *
+     * @param id 积分订单售后单主键
+     * @return 积分订单售后单
+     */
+    @Override
+    public FsIntegralAfterSales selectFsIntegralAfterSalesById(Long id)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesById(id);
+    }
+
+    /**
+     * 查询积分订单售后单列表
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 积分订单售后单集合
+     */
+    @Override
+    public List<FsIntegralAfterSales> selectFsIntegralAfterSalesList(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesList(fsIntegralAfterSales);
+    }
+
+    /**
+     * 新增积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    @Override
+    public int insertFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        return fsIntegralAfterSalesMapper.insertFsIntegralAfterSales(fsIntegralAfterSales);
+    }
+
+    /**
+     * 修改积分订单售后单
+     *
+     * @param fsIntegralAfterSales 积分订单售后单
+     * @return 结果
+     */
+    @Override
+    public int updateFsIntegralAfterSales(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        return fsIntegralAfterSalesMapper.updateFsIntegralAfterSales(fsIntegralAfterSales);
+    }
+
+    /**
+     * 删除积分订单售后单(逻辑删除)
+     *
+     * @param id 积分订单售后单主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsIntegralAfterSalesById(Long id)
+    {
+        return fsIntegralAfterSalesMapper.deleteFsIntegralAfterSalesById(id);
+    }
+
+    /**
+     * 批量删除积分订单售后单(逻辑删除)
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsIntegralAfterSalesByIds(Long[] ids)
+    {
+        return fsIntegralAfterSalesMapper.deleteFsIntegralAfterSalesByIds(ids);
+    }
+
+    /**
+     * 根据订单编号查询售后单
+     *
+     * @param orderCode 订单编号
+     * @return 售后单列表
+     */
+    @Override
+    public List<FsIntegralAfterSales> selectFsIntegralAfterSalesByOrderCode(String orderCode)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesByOrderCode(orderCode);
+    }
+
+    /**
+     * 根据订单编号统计未完成售后单数量
+     *
+     * @param orderCode 订单编号
+     * @return 数量
+     */
+    @Override
+    public int countUnfinishedByOrderCode(String orderCode)
+    {
+        return fsIntegralAfterSalesMapper.countUnfinishedByOrderCode(orderCode);
+    }
+
+    /**
+     * 查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     *
+     * @param fsIntegralAfterSales 查询条件
+     * @return 售后单VO集合
+     */
+    @Override
+    public List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOList(FsIntegralAfterSales fsIntegralAfterSales)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesVOList(fsIntegralAfterSales);
+    }
+
+    /**
+     * 查询积分订单售后单详情(含关联用户、企业、积分订单信息)
+     *
+     * @param id 售后单主键
+     * @return 售后单VO
+     */
+    @Override
+    public FsIntegralAfterSalesVO selectFsIntegralAfterSalesVOById(Long id)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesVOById(id);
+    }
+
+    /**
+     * App 端查询积分订单售后单列表(含关联用户、企业、积分订单信息)
+     * <p>强制按 userId 过滤,支持 status/serviceType/salesStatus/orderCode 筛选。</p>
+     *
+     * @param param 查询参数(含分页字段、userId)
+     * @return 售后单VO集合
+     */
+    @Override
+    public List<FsIntegralAfterSalesVO> selectFsIntegralAfterSalesVOListByParam(FsIntegralAfterSalesQueryParam param)
+    {
+        return fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesVOListByParam(param);
+    }
+
+    /**
+     * 用户申请积分订单售后
+     * <p>参考普通商品售后流程,针对积分订单特性简化:
+     * 无独立商品详情表(商品信息在 fs_integral_order.item_json),无操作流水表。</p>
+     * <p>校验:订单归属、订单状态、重复申请、售后时效。生成售后单并将订单状态置为退款中(-1)。</p>
+     *
+     * @param userId 当前登录用户ID
+     * @param param  售后申请参数
+     * @return 操作结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R applyForAfterSales(Long userId, FsIntegralAfterSalesApplyParam param)
+    {
+        log.info("积分订单售后申请 userId:{}, param:{}", userId, com.alibaba.fastjson.JSON.toJSONString(param));
+
+        // 1. 查询订单
+        FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderCode(param.getOrderCode());
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        // 2. 校验订单归属
+        if (!userId.equals(order.getUserId())) {
+            throw new CustomException("非法操作");
+        }
+        // 3. 校验订单状态(4待支付/5已取消/-1售后中/-2已退款 不可申请)
+        Integer orderStatus = order.getStatus();
+        if (orderStatus == null || orderStatus == 4) {
+            return R.error("未支付订单不能申请售后");
+        }
+        if (orderStatus == 5) {
+            return R.error("已取消订单不能申请售后");
+        }
+        if (orderStatus == -1) {
+            return R.error("该订单已提交售后申请,等待处理");
+        }
+        if (orderStatus == -2) {
+            return R.error("该订单已退款,不能重复申请");
+        }
+        // 4. 售后时效校验(已完成订单 status=3,校验 finishTime + storeAfterSalesDay)
+        if (orderStatus == 3 && !checkAfterSalesTimeValid(order)) {
+            return R.error("此订单已超过售后时间");
+        }
+        // 5. 幂等校验:存在未完成的售后单则不允许重复申请
+        int unfinishedCount = fsIntegralAfterSalesMapper.countUnfinishedByOrderCode(param.getOrderCode());
+        if (unfinishedCount > 0) {
+            return R.error("该订单已提交售后申请,等待处理");
+        }
+        // 6. 退款金额校验(仅积分+现金支付时)
+        if (param.getRefundAmount() != null && order.getPayMoney() != null
+                && param.getRefundAmount().compareTo(order.getPayMoney()) > 0) {
+            return R.error("退款金额不能大于支付金额");
+        }
+        // 7. 构建售后单
+        FsIntegralAfterSales afterSales = buildAfterSales(userId, param, order);
+        fsIntegralAfterSalesMapper.insertFsIntegralAfterSales(afterSales);
+        // 8. 更新订单状态为售后中(status=-1)
+        fsIntegralOrderMapper.markAfterSalesStatus(order.getOrderId());
+        log.info("积分订单售后单生成成功,售后单ID={}, orderCode={}, 订单状态已置为售后中(-1)",
+                afterSales.getId(), param.getOrderCode());
+        return R.ok();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>撤销流程:
+     * <ol>
+     *   <li>校验售后单存在</li>
+     *   <li>校验售后单归属(防越权)</li>
+     *   <li>校验售后单状态:已取消/已拒绝不可撤销</li>
+     *   <li>校验审核状态:用户已发货(2)/退款成功(3)不可撤销</li>
+     *   <li>更新售后单 salesStatus=1(用户取消)</li>
+     *   <li>恢复订单状态:从 -1 退款中恢复为 orderStatus 冗余的原始状态(带乐观锁校验)</li>
+     * </ol>
+     * 事务保护:售后单更新与订单状态恢复在同一事务内,保证数据一致性。</p>
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R revoke(Long userId, Long salesId)
+    {
+        FsIntegralAfterSales afterSales = fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesById(salesId);
+        if (afterSales == null) {
+            throw new CustomException("未查询到售后单信息");
+        }
+        // 安全校验:仅允许撤销本人的售后单
+        if (!userId.equals(afterSales.getUserId())) {
+            throw new CustomException("非法操作");
+        }
+        // 售后单状态校验:已取消(1)/已拒绝(2) 不可撤销
+        if (afterSales.getSalesStatus() == null || afterSales.getSalesStatus() != 0) {
+            throw new CustomException("当前售后单状态不可撤销");
+        }
+        // 审核状态校验:退款中(4)/退款成功(5)不可撤销
+        Integer status = afterSales.getStatus();
+        if (status != null && status >= 4) {
+            throw new CustomException("售后单已进入退款流程,不可撤销");
+        }
+
+        // 更新售后单状态为用户取消
+        afterSales.setSalesStatus(1);
+        fsIntegralAfterSalesMapper.updateFsIntegralAfterSales(afterSales);
+
+        // 恢复订单状态:从 -1 退款中 恢复为售后申请前冗余的原始状态
+        // orderStatus 为申请售后时冗余的订单原始状态(1待发货/2待收货/3已完成)
+        Integer originalOrderStatus = afterSales.getOrderStatus();
+        if (originalOrderStatus != null) {
+            int affected = fsIntegralOrderMapper.restoreOrderStatus(
+                    afterSales.getOrderId(), -1, originalOrderStatus);
+            if (affected == 0) {
+                log.warn("撤销售后恢复订单状态失败(订单状态已非退款中),售后单ID={}, orderId={}, 期望恢复状态={}",
+                        salesId, afterSales.getOrderId(), originalOrderStatus);
+            }
+        }
+
+        log.info("积分订单售后单撤销成功,售后单ID={}, orderId={}, 订单状态已恢复为{}",
+                salesId, afterSales.getOrderId(), originalOrderStatus);
+        return R.ok();
+    }
+
+    /**
+     * 售后时效校验
+     * <p>复用 store.config.storeAfterSalesDay 配置:已完成订单的完成时间 + 售后有效天数 必须 >= 当前时间。</p>
+     * <p>与普通商品售后逻辑保持一致(参考 FsStoreAfterSalesScrmServiceImpl)。</p>
+     *
+     * @param order 积分订单
+     * @return true=可申请售后,false=已超时
+     */
+    private boolean checkAfterSalesTimeValid(FsIntegralOrder order)
+    {
+        if (order.getFinishTime() == null) {
+            // 完成时间缺失(历史数据),不限制
+            return true;
+        }
+        String json = configService.selectConfigByKey("store.config");
+        if (StringUtils.isEmpty(json)) {
+            return true;
+        }
+        StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+        if (config.getStoreAfterSalesDay() == null || config.getStoreAfterSalesDay() <= 0) {
+            return true;
+        }
+        Calendar calendarAfterSales = new GregorianCalendar();
+        calendarAfterSales.setTime(order.getFinishTime());
+        calendarAfterSales.add(Calendar.DATE, config.getStoreAfterSalesDay());
+        return calendarAfterSales.getTime().getTime() >= System.currentTimeMillis();
+    }
+
+    /**
+     * 构建积分售后单
+     * <p>填充订单冗余字段(orderId/userId/companyId/companyUserId/orderStatus),
+     * 处理退款积分默认值,处理"一步提交"物流信息。</p>
+     *
+     * @param userId 当前用户ID
+     * @param param  申请参数
+     * @param order  积分订单
+     * @return 售后单实体
+     */
+    private FsIntegralAfterSales buildAfterSales(Long userId, FsIntegralAfterSalesApplyParam param, FsIntegralOrder order)
+    {
+        FsIntegralAfterSales afterSales = new FsIntegralAfterSales();
+        afterSales.setOrderId(order.getOrderId());
+        afterSales.setOrderCode(order.getOrderCode());
+        afterSales.setUserId(userId);
+        afterSales.setCompanyId(order.getCompanyId());
+        afterSales.setCompanyUserId(order.getCompanyUserId());
+        afterSales.setRefundAmount(param.getRefundAmount());
+        // 退回积分:未传则默认退回订单全部积分
+        Long refundIntegral = param.getRefundIntegral();
+        if (refundIntegral == null && StringUtils.isNotEmpty(order.getIntegral())) {
+            try {
+                refundIntegral = Long.parseLong(order.getIntegral());
+            } catch (NumberFormatException e) {
+                log.warn("积分订单 integral 字段解析失败, orderCode:{}, integral:{}", order.getOrderCode(), order.getIntegral());
+            }
+        }
+        afterSales.setRefundIntegral(refundIntegral);
+        afterSales.setServiceType(param.getServiceType());
+        afterSales.setReasons(param.getReasons());
+        afterSales.setExplains(param.getExplains());
+        afterSales.setExplainImg(param.getExplainImg());
+        afterSales.setStatus(0);
+        afterSales.setSalesStatus(0);
+        afterSales.setOrderStatus(order.getStatus());
+        afterSales.setIsDel(0);
+        afterSales.setCreateTime(new Date());
+
+        // "一步提交"兼容:有寄回物流单号则视为退货退款,serviceType 强制为 1
+        if (StringUtils.isNotBlank(param.getDeliverySn())) {
+            afterSales.setServiceType(1);
+        }
+        if (StringUtils.isNotBlank(param.getConsignee())) {
+            afterSales.setConsignee(param.getConsignee());
+        }
+        if (StringUtils.isNotBlank(param.getPhoneNumber())) {
+            afterSales.setPhoneNumber(param.getPhoneNumber());
+        }
+        if (StringUtils.isNotBlank(param.getAddress())) {
+            afterSales.setAddress(param.getAddress());
+        }
+        if (StringUtils.isNotBlank(param.getShipperCode())) {
+            afterSales.setShipperCode(param.getShipperCode());
+        }
+        if (StringUtils.isNotBlank(param.getDeliverySn())) {
+            afterSales.setDeliverySn(param.getDeliverySn());
+        }
+        if (StringUtils.isNotBlank(param.getDeliveryName())) {
+            afterSales.setDeliveryName(param.getDeliveryName());
+        }
+        return afterSales;
+    }
+}

+ 196 - 19
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -55,6 +55,7 @@ import com.fs.ybPay.dto.YopRefundRequestDTO;
 import com.fs.ybPay.dto.YopRefundResponseDTO;
 import com.fs.ybPay.service.IPayService;
 import com.fs.ybPay.service.IYopPayService;
+import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.springframework.beans.BeanUtils;
@@ -151,6 +152,13 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
      */
     @Autowired
     private IFsUserService userService;
+    @Autowired
+    private ISysConfigService configService;
+    /**
+     * 积分售后单Mapper(退款回调时更新售后单状态)
+     */
+    @Autowired
+    private FsIntegralAfterSalesMapper fsIntegralAfterSalesMapper;
 
     /**
      * 查询积分商品订单
@@ -164,6 +172,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         return fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(orderId);
     }
 
+    /**
+     * 根据订单编号查询积分商品订单
+     */
+    @Override
+    public FsIntegralOrder selectFsIntegralOrderByOrderCode(String orderCode)
+    {
+        return fsIntegralOrderMapper.selectFsIntegralOrderByOrderCode(orderCode);
+    }
+
     /**
      * 查询积分商品订单列表
      *
@@ -854,10 +871,11 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
      * 管理员强制退款(不校验是否发货)
      * <p>流程:
      * <ol>
-     *   <li>更新订单状态为已取消(status=-1)</li>
+     *   <li>更新订单状态为售后中(status=-1)</li>
      *   <li>还原商品库存</li>
      *   <li>易宝原路退款(Yop SDK,退款处理中也算成功,等回调最终确认)</li>
      *   <li>原子退回用户积分(incIntegral)+ 独立事务写积分日志(logType=27,REQUIRES_NEW)</li>
+     *   <li>无需等待回调的场景(纯积分订单/退款立即成功)直接置为已退款(-2)</li>
      * </ol>
      * </p>
      * <p>幂等:通过 logType=27 存在性判断,重复退款不会重复退积分。</p>
@@ -871,22 +889,25 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             throw new ServiceException("订单不存在");
         }
 
-        // 1. 修改订单状态为已取消
-        int rows = fsIntegralOrderMapper.cancelOrder(order.getOrderId());
-        if (rows <= 0) {
-            throw new ServiceException("订单状态更新失败,可能已被处理");
-        }
+        // 1. 标记订单为售后中(status=-1),等待回调确认后变为已退款(-2)
+        //    纯积分订单/退款立即成功 无回调,步骤5后会直接设为已退款
+        fsIntegralOrderMapper.markAfterSalesStatus(order.getOrderId());
 
         // 2. 还原库存
         restoreStock(order);
 
-        // 3. 易宝原路退款(仅纯现金/积分+现金订单需要退资金)
-        refundPaymentIfNeeded(order);
+        // 3. 易宝原路退款(仅纯现金/积分+现金订单需要退资金),返回是否需要等待回调
+        boolean needCallback = refundPaymentIfNeeded(order);
 
         // 4. 退回积分(独立事务,异常不回滚主事务)
         refundIntegralSafely(order);
 
-        return rows;
+        // 5. 无需等待回调的场景(纯积分订单无支付记录、或易宝退款立即成功),直接标记为已退款(-2)
+        if (!needCallback) {
+            fsIntegralOrderMapper.refundOrder(order.getOrderId());
+        }
+
+        return 1;
     }
 
     /**
@@ -932,15 +953,17 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
      * 易宝原路退款(仅当订单有支付记录时执行)
      * <p>退款成功(status=SUCCESS)或处理中(status=PROCESSING)都更新支付记录,
      * 退款失败抛异常回滚主事务(订单状态还原)。</p>
+     *
+     * @return true 表示退款处理中,需等待回调确认;false 表示无需等待回调(纯积分订单、已退款、或退款立即成功)
      */
-    private void refundPaymentIfNeeded(FsIntegralOrder order) {
+    private boolean refundPaymentIfNeeded(FsIntegralOrder order) {
         // 查询订单关联的支付记录(businessType=6 积分订单)
         List<FsStorePayment> payments = fsStorePaymentMapper.selectFsStorePaymentByPay(
                 BusinessTypeEnum.INTEGRAL_ORDER.getCode(), order.getOrderId());
         if (payments == null || payments.isEmpty()) {
             // 纯积分订单无支付记录,跳过资金退款
             log.info("积分订单强制退款:无支付记录,跳过资金退款 orderId: {}", order.getOrderId());
-            return;
+            return false;
         }
         if (payments.size() > 1) {
             log.warn("积分订单强制退款:存在多笔支付记录,仅处理第一笔 orderId: {}, 支付记录数: {}",
@@ -951,16 +974,26 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         // 幂等校验:支付记录已退款则跳过
         if (payment.getStatus() != null && payment.getStatus() < 0) {
             log.info("积分订单强制退款:支付记录已退款,跳过 paymentId: {}, status: {}", payment.getPaymentId(), payment.getStatus());
-            return;
+            return false;
+        }
+
+        // 读取支付配置,获取积分订单专属退款回调地址(与普通商品退款回调隔离)
+        String configJson = configService.selectConfigByKey("his.pay");
+        FsPayConfig payConfig = JSONUtil.toBean(configJson, FsPayConfig.class);
+        if (payConfig == null || StringUtils.isEmpty(payConfig.getYbIntegralRefundNotifyUrl())) {
+            throw new ServiceException("积分订单退款回调地址未配置,请先在系统配置中设置 ybIntegralRefundNotifyUrl");
         }
 
-        // 构建易宝退款请求(对齐 ylrz 商城订单 FsStoreAfterSalesScrmServiceImpl 退款逻辑)
         YopRefundRequestDTO yopRefundRequest = new YopRefundRequestDTO();
         yopRefundRequest.setOrderId(BusinessTypeEnum.INTEGRAL_ORDER.getPrefix() + "-" + payment.getPayCode());
         yopRefundRequest.setUniqueOrderNo(payment.getTradeNo());
-        yopRefundRequest.setRefundRequestId("refund-" + payment.getPayCode());
+        // refundRequestId 后缀使用 paymentId(与普通商品退款一致),回调时通过 selectFsStorePaymentById 查询
+        yopRefundRequest.setRefundRequestId("refund-" + payment.getPaymentId());
         yopRefundRequest.setRefundAmount(payment.getPayMoney());
-        yopRefundRequest.setDescription("积分订单管理员强制退款");
+        yopRefundRequest.setDescription("积分订单退款!");
+        // 指定积分订单专属退款回调地址,由 PayScrmController.ybIntegralRefundNotify 接收处理
+        yopRefundRequest.setNotifyUrl(payConfig.getYbIntegralRefundNotifyUrl());
+        // 积分订单不走分账(支付时未传companyId),退款默认从未结算资金(渠道在途账户)退,与普通商品一致
 
         YopRefundResponseDTO yopRefundResult = yopPayService.refund(yopRefundRequest);
         log.info("易宝SDK退款返回结果: 积分订单orderId: {}, code: {}, status: {}",
@@ -970,22 +1003,25 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             throw new CustomException("易宝退款请求失败:" + yopRefundResult.getMessage());
         }
 
-        // 更新支付记录(最小更新对象,避免覆盖其他字段)
+        // 更新支付记录
         FsStorePayment paymentUpdate = new FsStorePayment();
         paymentUpdate.setPaymentId(payment.getPaymentId());
         paymentUpdate.setRefundMoney(payment.getPayMoney());
+        boolean needCallback;
         if (yopRefundResult.isRefundSuccess()) {
-            // 退款立即成功 → 标记已退款
+            // 退款立即成功 → 标记已退款,无需等待回调
             paymentUpdate.setStatus(-1);
             paymentUpdate.setRefundTime(new Date());
+            needCallback = false;
         } else if (yopRefundResult.isProcessing()) {
             // 退款处理中 → 标记退款中(status=-2),等待退款回调最终确认
             paymentUpdate.setStatus(-2);
+            needCallback = true;
         } else {
-            // 其他状态(CANCEL/SUSPEND等)视为退款失败
             throw new CustomException("易宝退款异常,状态:" + yopRefundResult.getStatus());
         }
         storePaymentService.updateFsStorePayment(paymentUpdate);
+        return needCallback;
     }
 
     /**
@@ -1028,7 +1064,7 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             logs.setIntegral(refundIntegral);
             logs.setUserId(order.getUserId());
             logs.setBalance(userAfter.getIntegral());
-            // logType=27 退款订单退回积分(区别于 logType=26 取消订单退回积分)
+            // logType=27 退款订单退回积分
             logs.setLogType(FsUserIntegralLogTypeEnum.TYPE_27.getValue());
             logs.setBusinessId(businessId);
             logs.setBusinessType(2);
@@ -1044,6 +1080,147 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         }
     }
 
+    /**
+     * 易宝积分订单退款回调确认(更新支付记录、订单、售后单状态)
+     * <p>易宝退款成功后异步回调,将支付记录从"退款中(-2)"更新为"已退款(-1)",
+     * 同时更新积分订单状态为已退款(-2),售后单状态为退款成功(3)。</p>
+     * <p>幂等:已退款(status=-1)直接返回 success,重复回调不会重复处理。</p>
+     * <p>注意:积分退还在发起退款时已同步完成(refundIntegralSafely),本方法仅更新资金退款状态,
+     * 不涉及积分操作,避免回调链路过长导致不可控。</p>
+     *
+     * @param paymentId     支付记录主键(从退款请求号 refund-{paymentId} 拆分得到)
+     * @param refundAmount  退款金额(易宝回调返回,可能为 null 或空串)
+     * @return "success"=处理成功 / "FAIL"=处理失败需重试 / ""=未找到支付记录或状态异常
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public String refundConfirmByYop(String paymentId, String refundAmount) {
+        log.info("进入积分订单退款回调确认(Yop) paymentId: {}, refundAmount: {}", paymentId, refundAmount);
+
+        if (StringUtils.isEmpty(paymentId)) {
+            log.error("积分订单退款回调(Yop) paymentId 为空");
+            return "";
+        }
+
+        // 1. 查询支付记录(与普通商品退款回调一致,使用 selectFsStorePaymentById 按 paymentId 查询)
+        Long payId;
+        try {
+            payId = Long.parseLong(paymentId);
+        } catch (NumberFormatException e) {
+            log.error("积分订单退款回调(Yop) paymentId 格式非法 paymentId: {}", paymentId);
+            return "";
+        }
+        FsStorePayment storePayment = storePaymentService.selectFsStorePaymentByPaymentId(payId);
+        if (Objects.isNull(storePayment)) {
+            log.error("积分订单退款回调(Yop) 支付记录不存在 paymentId: {}", paymentId);
+            return "";
+        }
+
+        Integer status = storePayment.getStatus();
+        // 2. 幂等校验:已退款(status=-1)直接返回成功(易宝重试场景)
+        if (status != null && status == -1) {
+            log.info("积分订单退款回调(Yop) 支付记录已退款,幂等返回 success paymentId: {}", paymentId);
+            return "success";
+        }
+
+        // 3. 状态校验:仅退款中(status=-2)才允许更新为已退款
+        if (status == null || status != -2) {
+            log.warn("积分订单退款回调(Yop) 支付记录状态非退款中,跳过 paymentId: {}, status: {}", paymentId, status);
+            return "";
+        }
+
+        // 4. 更新支付记录:退款中(-2) → 已退款(-1)
+        FsStorePayment paymentUpdate = new FsStorePayment();
+        paymentUpdate.setPaymentId(storePayment.getPaymentId());
+        paymentUpdate.setStatus(-1);
+        paymentUpdate.setRefundTime(new Date());
+        if (StringUtils.isNotEmpty(refundAmount)) {
+            try {
+                paymentUpdate.setRefundMoney(new BigDecimal(refundAmount));
+            } catch (NumberFormatException e) {
+                log.warn("积分订单退款回调(Yop) refundAmount 格式非法,使用原支付金额 paymentId: {}, refundAmount: {}", paymentId, refundAmount);
+                paymentUpdate.setRefundMoney(storePayment.getPayMoney());
+            }
+        } else {
+            // 易宝未返回退款金额时,回退使用原支付金额
+            paymentUpdate.setRefundMoney(storePayment.getPayMoney());
+        }
+        storePaymentService.updateFsStorePayment(paymentUpdate);
+        log.info("积分订单退款回调(Yop) 更新支付记录完成 paymentId: {}, status: -2->-1", storePayment.getPaymentId());
+
+        // 5. 查询积分订单并更新状态为已退款
+        Long orderId = null;
+        try {
+            orderId = Long.parseLong(storePayment.getBusinessId());
+        } catch (NumberFormatException e) {
+            log.error("积分订单退款回调(Yop) businessId 非法 paymentId: {}, businessId: {}", storePayment.getPaymentId(), storePayment.getBusinessId());
+            // 支付记录已更新成功,业务ID异常不影响主流程,记录日志即可
+            return "success";
+        }
+        FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(orderId);
+        if (order == null) {
+            log.error("积分订单退款回调(Yop) 积分订单不存在 orderId: {}", orderId);
+            return "success";
+        }
+        // 更新订单状态为已退款(status=-2),幂等处理:如果已经是已退款状态则跳过
+        if (order.getStatus() != null && order.getStatus() == -1) {
+            int rows = fsIntegralOrderMapper.refundOrder(orderId);
+            if (rows > 0) {
+                log.info("积分订单退款回调(Yop) 更新订单状态完成 orderId: {}, status: -1->-2", orderId);
+            } else {
+                log.warn("积分订单退款回调(Yop) 更新订单状态失败(可能已被其他流程处理) orderId: {}", orderId);
+            }
+        } else if (order.getStatus() != null && order.getStatus() == -2) {
+            log.info("积分订单退款回调(Yop) 订单已为已退款状态,幂等跳过 orderId: {}", orderId);
+        } else {
+            log.warn("积分订单退款回调(Yop) 订单状态非售后中,跳过订单更新 orderId: {}, status: {}", orderId, order.getStatus());
+        }
+
+        // 6. 更新售后单状态为退款成功(status=4, salesStatus=3)
+        updateAfterSalesStatusToRefunded(order.getOrderCode());
+        return "success";
+    }
+
+    /**
+     * 更新售后单状态为退款成功
+     * <p>退款回调成功后,将对应售后单的审核状态更新为退款成功(5),
+     * 售后状态更新为已完成(3),同时更新冗余的订单状态(order_status)为已退款(-2)。
+     * 前提条件:售后单审核状态为退款中(4)。</p>
+     *
+     * @param orderCode 订单编号
+     */
+    private void updateAfterSalesStatusToRefunded(String orderCode) {
+        if (StringUtils.isEmpty(orderCode)) {
+            return;
+        }
+        // 查询订单关联的售后单
+        List<FsIntegralAfterSales> afterSalesList = fsIntegralAfterSalesMapper.selectFsIntegralAfterSalesByOrderCode(orderCode);
+        if (afterSalesList == null || afterSalesList.isEmpty()) {
+            log.info("积分订单退款回调(Yop) 未找到售后单 orderCode: {}", orderCode);
+            return;
+        }
+        // 更新所有符合条件的售后单状态为退款成功(5)
+        for (FsIntegralAfterSales afterSales : afterSalesList) {
+            // 仅处理正常售后单(salesStatus=0),已取消/已拒绝的不更新
+            if (afterSales.getSalesStatus() == null || afterSales.getSalesStatus() != 0) {
+                continue;
+            }
+            // 仅处理审核状态为退款中(4)的售后单
+            Integer afterSalesStatus = afterSales.getStatus();
+            if (afterSalesStatus != null && afterSalesStatus == 4) {
+                FsIntegralAfterSales update = new FsIntegralAfterSales();
+                update.setId(afterSales.getId());
+                update.setStatus(5);              // 退款成功(回调确认)
+                update.setSalesStatus(3);          // 售后已完成
+                update.setOrderStatus(-2);         // 冗余订单状态:已退款
+                update.setUpdateTime(new Date());
+                fsIntegralAfterSalesMapper.updateFsIntegralAfterSales(update);
+                log.info("积分订单退款回调(Yop) 更新售后单状态完成 orderCode: {}, afterSalesId: {}, status: 4->5, salesStatus: 0->3, orderStatus: -2",
+                        orderCode, afterSales.getId());
+            }
+        }
+    }
+
     /**
      * 处理手机号脱敏
      */

+ 7 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -46,6 +46,7 @@ import com.fs.his.dto.PayConfigDTO;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.*;
 import com.fs.his.param.FsStorePaymentParam;
+import com.fs.his.enums.BusinessTypeEnum;
 import com.fs.his.param.PayOrderParam;
 import com.fs.his.param.WxSendRedPacketParam;
 import com.fs.his.service.IFsInquiryOrderService;
@@ -1301,8 +1302,12 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         yopDto.setPayWay("MINI_PROGRAM");
         yopDto.setChannel("WECHAT");
         yopDto.setUserId(user.getUserId());
-        yopDto.setCompanyId(payOrderParam.getCompanyId());
-        yopDto.setCompanyUserId(payOrderParam.getCompanyUserId());
+        // 积分订单不参与分账,不传companyId,确保资金走 REAL_TIME 实时结算到商户资金账户
+        // (传companyId会触发checkSplitInfo分账校验,若公司开启分账则走DELAY_SETTLE延迟结算,资金进入待分账账户)
+        if (payOrderParam.getBusinessType() != BusinessTypeEnum.INTEGRAL_ORDER) {
+            yopDto.setCompanyId(payOrderParam.getCompanyId());
+            yopDto.setCompanyUserId(payOrderParam.getCompanyUserId());
+        }
         yopDto.setGoodsName(payOrderParam.getBusinessType().getDesc());
 
         yopDto.setAppId(payOrderParam.getAppId());

+ 72 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralAfterSalesVO.java

@@ -0,0 +1,72 @@
+package com.fs.his.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.his.domain.FsIntegralAfterSales;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 积分订单售后单展示VO
+ * <p>包含售后单字段 + 关联查询字段(用户、企业、积分订单)</p>
+ *
+ * @author fs
+ * @date 2026-07-21
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FsIntegralAfterSalesVO extends FsIntegralAfterSales
+{
+    private static final long serialVersionUID = 1L;
+
+    // ========== 关联查询-用户信息 ==========
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String nickName;
+
+    /** 用户电话(已脱敏) */
+    @Excel(name = "用户电话")
+    private String userPhone;
+
+    /** 企业名称 */
+    @Excel(name = "企业名称")
+    private String companyName;
+
+    // ========== 关联查询-积分订单信息 ==========
+
+    /** 支付积分 */
+    @Excel(name = "支付积分")
+    private String integral;
+
+    /** 支付金额(现金部分) */
+    @Excel(name = "支付金额")
+    private BigDecimal payMoney;
+
+    /** 商品信息JSON(商品名称、原价、数量、积分、图片等统一从此字段解析) */
+    private String itemJson;
+
+    /** 收货地址 */
+    @Excel(name = "收货地址")
+    private String userAddress;
+
+    /** 订单创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date orderCreateTime;
+
+    /** 支付时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    /** 支付方式 */
+    private String payType;
+
+    /** 快递单号(订单发货物流) */
+    private String orderDeliverySn;
+
+    /** 快递名称(订单发货物流) */
+    private String orderDeliveryName;
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderListUVO.java

@@ -64,5 +64,11 @@ public class FsIntegralOrderListUVO implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd")
     private Date deliveryTime;
 
+    /** 完成时间(确认收货时间,用于售后时效校验) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
+    /** 是否可申请售后 0不可 1可(由 Controller 按订单状态+售后时效计算填充) */
+    private Integer isAfterSales;
 
 }

+ 6 - 3
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -740,11 +740,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             Integer maxDeductPercent = 50;
             BigDecimal minDeductAmount = new BigDecimal(100);
             Integer minUseIntegral = 0;
-            Integer deductEnabled = 1;
+            Integer deductEnabled = 0; // 积分抵扣开关,默认关闭,仅当配置启用且明确开启时才生效
             String json = configService.selectConfigByKey("points.grantRule");
             if (StringUtils.isNotEmpty(json)) {
                 try {
                     PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                    // 积分总开关 enabled=1 时才读取抵扣配置,否则抵扣保持关闭
                     if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
                         if (rule.getPayRate() != null && rule.getPayRate() > 0) {
                             payRate = rule.getPayRate();
@@ -6319,11 +6320,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 Integer maxDeductPercent = 50; // 最高抵扣比例,默认50%
                 BigDecimal minDeductAmount = new BigDecimal(100); // 满X元可抵扣,默认100
                 Integer minUseIntegral = 0;  // 最低起用积分,默认不限制
-                Integer deductEnabled = 1;   // 积分抵扣开关,默认开启
+                Integer deductEnabled = 0;   // 积分抵扣开关,默认关闭,仅当配置启用且明确开启时才生效
                 String json = configService.selectConfigByKey("points.grantRule");
                 if (StringUtils.isNotEmpty(json)) {
                     try {
                         PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                        // 积分总开关 enabled=1 时才读取抵扣配置,否则抵扣保持关闭
                         if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
                             if (rule.getPayRate() != null && rule.getPayRate() > 0) {
                                 payRate = rule.getPayRate();
@@ -6785,11 +6787,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 Integer maxDeductPercent = 50;
                 BigDecimal minDeductAmount = new BigDecimal(100);
                 Integer minUseIntegral = 0;
-                Integer deductEnabled = 1;
+                Integer deductEnabled = 0; // 积分抵扣开关,默认关闭,仅当配置启用且明确开启时才生效
                 String json = configService.selectConfigByKey("points.grantRule");
                 if (StringUtils.isNotEmpty(json)) {
                     try {
                         PointsGrantRuleConfig rule = JSONUtil.toBean(json, PointsGrantRuleConfig.class);
+                        // 积分总开关 enabled=1 时才读取抵扣配置,否则抵扣保持关闭
                         if (rule != null && rule.getEnabled() != null && rule.getEnabled() == 1) {
                             if (rule.getPayRate() != null && rule.getPayRate() > 0) {
                                 payRate = rule.getPayRate();

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

@@ -31,14 +31,14 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://nj-cdb-6306xy90.sql.tencentcdb.com:27077/fs_his_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://nj-cdb-6306xy90.sql.tencentcdb.com:27077/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: yjb
                     password: Ylrz_1q2w3e4r5t6y
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
                     enabled: true
-                    url: jdbc:mysql://nj-cdb-6306xy90.sql.tencentcdb.com:27077/fs_his_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://nj-cdb-6306xy90.sql.tencentcdb.com:27077/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: yjb
                     password: Ylrz_1q2w3e4r5t6y
                 # 初始连接数

+ 12 - 11
fs-service/src/main/resources/mapper/company/CompanyTagMapper.xml

@@ -8,11 +8,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="tagId"    column="tag_id"    />
         <result property="companyId"    column="company_id"    />
         <result property="tag"    column="tag"    />
+        <result property="companyUserId"    column="company_user_id"    />
         <result property="createTime"    column="create_time"    />
     </resultMap>
 
     <sql id="selectCompanyTagVo">
-        select tag_id, company_id, tag, create_time from company_tag
+        select tag_id, company_id, tag, company_user_id, create_time from company_tag
     </sql>
 
     <select id="selectCompanyTagList" parameterType="CompanyTag" resultMap="CompanyTagResult">
@@ -28,7 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where tag_id = #{tagId}
     </select>
 
-    <!-- 查询标签列表 -->
+    <!-- 管理员查询标签列表(可按 companyUserId 过滤指定销售的标签) -->
     <select id="selectCompanyTagListByMap" resultType="com.fs.company.domain.CompanyTag">
         select ct.* from company_tag ct
         <where>
@@ -41,6 +42,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="params.companyId != null">
                 and ct.company_id = #{params.companyId}
             </if>
+            <if test="params.companyUserId != null">
+                and ct.company_user_id = #{params.companyUserId}
+            </if>
             <if test="params.tagName != null">
                 and ct.tag = #{params.tagName}
             </if>
@@ -64,11 +68,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select tag_id,tag from company_tag
     </select>
 
-    <!-- 查询标签列表 -->
+    <!-- 非管理员查询标签列表(数据隔离:仅看自己创建的标签,不再依赖 JOIN)
+         新增标签时已写入 company_user_id,因此无需关联 fs_user_project_tag -->
     <select id="selectCompanyTagByList" resultType="com.fs.company.domain.CompanyTag">
         select ct.* from company_tag ct
-        inner join fs_user_project_tag ft on ct.tag_id =ft.tag_id
-        inner join fs_user_company_user fucu on fucu.id = ft.user_company_user_id
         <where>
             <if test="params.keyword != null and params.keyword.length > 0">
                 and
@@ -79,18 +82,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="params.companyId != null">
                 and ct.company_id = #{params.companyId}
             </if>
-
             <if test="params.companyUserId != null">
-                and fucu.company_user_id = #{params.companyUserId}
-            </if>
-            <if test="params.tagName != null">
-                and ct.tag = #{params.tagName}
+                and ct.company_user_id = #{params.companyUserId}
             </if>
             <if test="params.tagName != null">
                 and ct.tag = #{params.tagName}
             </if>
         </where>
-        group by ct.tag_id
     </select>
 
     <insert id="insertCompanyTag" parameterType="CompanyTag" useGeneratedKeys="true" keyProperty="tagId">
@@ -98,11 +96,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="companyId != null">company_id,</if>
             <if test="tag != null">tag,</if>
+            <if test="companyUserId != null">company_user_id,</if>
             <if test="createTime != null">create_time,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
             <if test="tag != null">#{tag},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
             <if test="createTime != null">#{createTime},</if>
          </trim>
     </insert>
@@ -112,6 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="SET" suffixOverrides=",">
             <if test="companyId != null">company_id = #{companyId},</if>
             <if test="tag != null">tag = #{tag},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
         </trim>
         where tag_id = #{tagId}

+ 328 - 0
fs-service/src/main/resources/mapper/his/FsIntegralAfterSalesMapper.xml

@@ -0,0 +1,328 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsIntegralAfterSalesMapper">
+
+    <resultMap type="FsIntegralAfterSales" id="FsIntegralAfterSalesResult">
+        <result property="id"               column="id"                />
+        <result property="orderId"          column="order_id"          />
+        <result property="orderCode"        column="order_code"        />
+        <result property="userId"           column="user_id"           />
+        <result property="companyId"        column="company_id"        />
+        <result property="companyUserId"    column="company_user_id"   />
+        <result property="refundAmount"     column="refund_amount"     />
+        <result property="refundIntegral"   column="refund_integral"   />
+        <result property="serviceType"      column="service_type"      />
+        <result property="reasons"          column="reasons"           />
+        <result property="explains"         column="explains"          />
+        <result property="explainImg"       column="explain_img"       />
+        <result property="shipperCode"      column="shipper_code"      />
+        <result property="deliverySn"       column="delivery_sn"       />
+        <result property="deliveryName"     column="delivery_name"     />
+        <result property="deliveryCode"     column="delivery_code"     />
+        <result property="status"           column="status"            />
+        <result property="salesStatus"      column="sales_status"      />
+        <result property="orderStatus"      column="order_status"      />
+        <result property="consignee"        column="consignee"         />
+        <result property="phoneNumber"      column="phone_number"      />
+        <result property="address"          column="address"           />
+        <result property="consigneePhone"   column="consignee_phone"   />
+        <result property="deptId"           column="dept_id"           />
+        <result property="operator"         column="operator"          />
+        <result property="remark"           column="remark"            />
+        <result property="isDel"            column="is_del"            />
+        <result property="createTime"       column="create_time"       />
+        <result property="createBy"         column="create_by"         />
+        <result property="updateTime"       column="update_time"       />
+        <result property="updateBy"         column="update_by"         />
+    </resultMap>
+
+    <sql id="selectFsIntegralAfterSalesVo">
+        select id, order_id, order_code, user_id, company_id, company_user_id,
+               refund_amount, refund_integral, service_type, reasons, explains, explain_img,
+               shipper_code, delivery_sn, delivery_name, delivery_code,
+               status, sales_status, order_status,
+               consignee, phone_number, address, consignee_phone, dept_id,
+               operator, remark, is_del,
+               create_time, create_by, update_time, update_by
+        from fs_integral_after_sales
+    </sql>
+
+    <!-- VO ResultMap(含关联查询字段) -->
+    <resultMap type="com.fs.his.vo.FsIntegralAfterSalesVO" id="FsIntegralAfterSalesVOResult" extends="FsIntegralAfterSalesResult">
+        <!-- 用户信息(fs_user) -->
+        <result property="nickName"          column="nick_name"            />
+        <result property="userPhone"         column="user_phone"           />
+        <!-- 企业信息(company) -->
+        <result property="companyName"       column="company_name"         />
+        <!-- 企业员工昵称(company_user) -->
+        <result property="companyUserNickName" column="company_user_nick_name" />
+        <!-- 积分订单信息(fs_integral_order)
+             商品名称/原价等信息不单独查询,统一从 item_json 解析 -->
+        <result property="integral"          column="integral"             />
+        <result property="payMoney"          column="pay_money"            />
+        <result property="itemJson"          column="item_json"            />
+        <result property="userAddress"       column="order_user_address"   />
+        <result property="orderCreateTime"   column="order_create_time"    />
+        <result property="payTime"           column="pay_time"             />
+        <result property="payType"           column="pay_type"             />
+        <result property="orderDeliverySn"   column="order_delivery_sn"    />
+        <result property="orderDeliveryName" column="order_delivery_name"  />
+    </resultMap>
+
+    <!-- VO 关联查询字段(售后单 + 用户 + 企业 + 员工 + 积分订单)
+         注意:fs_integral_order 表无 goods_name / ot_price 列,商品信息统一从 item_json 解析 -->
+    <sql id="selectFsIntegralAfterSalesVOColumns">
+        s.id, s.order_id, s.order_code, s.user_id, s.company_id, s.company_user_id,
+        s.refund_amount, s.refund_integral, s.service_type, s.reasons, s.explains, s.explain_img,
+        s.shipper_code, s.delivery_sn, s.delivery_name, s.delivery_code,
+        s.status, s.sales_status, s.order_status,
+        s.consignee, s.phone_number, s.address, s.consignee_phone, s.dept_id,
+        s.operator, s.remark, s.is_del,
+        s.create_time, s.create_by, s.update_time, s.update_by,
+        fu.nick_name,
+        fu.phone AS user_phone,
+        c.company_name,
+        cu.nick_name AS company_user_nick_name,
+        o.integral,
+        o.pay_money,
+        o.item_json,
+        o.user_address AS order_user_address,
+        o.create_time AS order_create_time,
+        o.pay_time,
+        o.pay_type,
+        o.delivery_sn AS order_delivery_sn,
+        o.delivery_name AS order_delivery_name
+    </sql>
+
+    <select id="selectFsIntegralAfterSalesList" parameterType="FsIntegralAfterSales" resultMap="FsIntegralAfterSalesResult">
+        <include refid="selectFsIntegralAfterSalesVo"/>
+        <where>
+            is_del = 0
+            <if test="orderCode != null and orderCode != ''"> and order_code = #{orderCode}</if>
+            <if test="userId != null"> and user_id = #{userId}</if>
+            <if test="companyId != null"> and company_id = #{companyId}</if>
+            <if test="companyUserId != null"> and company_user_id = #{companyUserId}</if>
+            <if test="serviceType != null"> and service_type = #{serviceType}</if>
+            <if test="status != null"> and status = #{status}</if>
+            <if test="salesStatus != null"> and sales_status = #{salesStatus}</if>
+            <if test="orderStatus != null"> and order_status = #{orderStatus}</if>
+            <if test="deliverySn != null and deliverySn != ''"> and delivery_sn like concat('%', #{deliverySn}, '%')</if>
+            <if test="deptId != null">
+                and (dept_id = #{deptId} OR dept_id IN (SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{deptId}, ancestors)))
+            </if>
+            <if test="beginTime != null and beginTime != ''">
+                AND date_format(create_time,'%y%m%d') &gt;= date_format(#{beginTime},'%y%m%d')
+            </if>
+            <if test="endTime != null and endTime != ''">
+                AND date_format(create_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
+            </if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectFsIntegralAfterSalesById" parameterType="Long" resultMap="FsIntegralAfterSalesResult">
+        <include refid="selectFsIntegralAfterSalesVo"/>
+        where id = #{id} and is_del = 0
+    </select>
+
+    <select id="selectFsIntegralAfterSalesByOrderCode" resultMap="FsIntegralAfterSalesResult">
+        <include refid="selectFsIntegralAfterSalesVo"/>
+        where order_code = #{orderCode} and is_del = 0
+        order by id desc
+    </select>
+
+    <select id="countUnfinishedByOrderCode" resultType="int">
+        select ifnull(count(1), 0)
+        from fs_integral_after_sales
+        where order_code = #{orderCode}
+          and is_del = 0
+          and sales_status = 0
+          and status != 3
+    </select>
+
+    <insert id="insertFsIntegralAfterSales" parameterType="FsIntegralAfterSales" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_integral_after_sales
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="orderId != null">order_id,</if>
+            <if test="orderCode != null">order_code,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="refundAmount != null">refund_amount,</if>
+            <if test="refundIntegral != null">refund_integral,</if>
+            <if test="serviceType != null">service_type,</if>
+            <if test="reasons != null">reasons,</if>
+            <if test="explains != null">explains,</if>
+            <if test="explainImg != null">explain_img,</if>
+            <if test="shipperCode != null">shipper_code,</if>
+            <if test="deliverySn != null">delivery_sn,</if>
+            <if test="deliveryName != null">delivery_name,</if>
+            <if test="deliveryCode != null">delivery_code,</if>
+            <if test="status != null">status,</if>
+            <if test="salesStatus != null">sales_status,</if>
+            <if test="orderStatus != null">order_status,</if>
+            <if test="consignee != null">consignee,</if>
+            <if test="phoneNumber != null">phone_number,</if>
+            <if test="address != null">address,</if>
+            <if test="consigneePhone != null">consignee_phone,</if>
+            <if test="deptId != null">dept_id,</if>
+            <if test="operator != null">operator,</if>
+            <if test="remark != null">remark,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="orderId != null">#{orderId},</if>
+            <if test="orderCode != null">#{orderCode},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="refundAmount != null">#{refundAmount},</if>
+            <if test="refundIntegral != null">#{refundIntegral},</if>
+            <if test="serviceType != null">#{serviceType},</if>
+            <if test="reasons != null">#{reasons},</if>
+            <if test="explains != null">#{explains},</if>
+            <if test="explainImg != null">#{explainImg},</if>
+            <if test="shipperCode != null">#{shipperCode},</if>
+            <if test="deliverySn != null">#{deliverySn},</if>
+            <if test="deliveryName != null">#{deliveryName},</if>
+            <if test="deliveryCode != null">#{deliveryCode},</if>
+            <if test="status != null">#{status},</if>
+            <if test="salesStatus != null">#{salesStatus},</if>
+            <if test="orderStatus != null">#{orderStatus},</if>
+            <if test="consignee != null">#{consignee},</if>
+            <if test="phoneNumber != null">#{phoneNumber},</if>
+            <if test="address != null">#{address},</if>
+            <if test="consigneePhone != null">#{consigneePhone},</if>
+            <if test="deptId != null">#{deptId},</if>
+            <if test="operator != null">#{operator},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsIntegralAfterSales" parameterType="FsIntegralAfterSales">
+        update fs_integral_after_sales
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="orderId != null">order_id = #{orderId},</if>
+            <if test="orderCode != null">order_code = #{orderCode},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="refundAmount != null">refund_amount = #{refundAmount},</if>
+            <if test="refundIntegral != null">refund_integral = #{refundIntegral},</if>
+            <if test="serviceType != null">service_type = #{serviceType},</if>
+            <if test="reasons != null">reasons = #{reasons},</if>
+            <if test="explains != null">explains = #{explains},</if>
+            <if test="explainImg != null">explain_img = #{explainImg},</if>
+            <if test="shipperCode != null">shipper_code = #{shipperCode},</if>
+            <if test="deliverySn != null">delivery_sn = #{deliverySn},</if>
+            <if test="deliveryName != null">delivery_name = #{deliveryName},</if>
+            <if test="deliveryCode != null">delivery_code = #{deliveryCode},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="salesStatus != null">sales_status = #{salesStatus},</if>
+            <if test="orderStatus != null">order_status = #{orderStatus},</if>
+            <if test="consignee != null">consignee = #{consignee},</if>
+            <if test="phoneNumber != null">phone_number = #{phoneNumber},</if>
+            <if test="address != null">address = #{address},</if>
+            <if test="consigneePhone != null">consignee_phone = #{consigneePhone},</if>
+            <if test="deptId != null">dept_id = #{deptId},</if>
+            <if test="operator != null">operator = #{operator},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsIntegralAfterSalesById" parameterType="Long">
+        update fs_integral_after_sales set is_del = 1 where id = #{id}
+    </delete>
+
+    <delete id="deleteFsIntegralAfterSalesByIds" parameterType="String">
+        update fs_integral_after_sales set is_del = 1 where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <!-- ==================== VO 关联查询 ==================== -->
+
+    <select id="selectFsIntegralAfterSalesVOList" parameterType="FsIntegralAfterSales" resultMap="FsIntegralAfterSalesVOResult">
+        select
+        <include refid="selectFsIntegralAfterSalesVOColumns"/>
+        from fs_integral_after_sales s
+        left join fs_user fu        on fu.user_id = s.user_id
+        left join company_user cu   on cu.user_id = s.company_user_id
+        left join company c         on c.company_id = s.company_id
+        left join fs_integral_order o on o.order_code = s.order_code
+        <where>
+            s.is_del = 0
+            <if test="orderCode != null and orderCode != ''"> and s.order_code = #{orderCode}</if>
+            <if test="userId != null"> and s.user_id = #{userId}</if>
+            <if test="nickName != null and nickName != ''"> and fu.nick_name like concat('%', #{nickName}, '%')</if>
+            <if test="companyId != null"> and s.company_id = #{companyId}</if>
+            <if test="companyUserId != null"> and s.company_user_id = #{companyUserId}</if>
+            <if test="serviceType != null"> and s.service_type = #{serviceType}</if>
+            <if test="status != null"> and s.status = #{status}</if>
+            <if test="salesStatus != null"> and s.sales_status = #{salesStatus}</if>
+            <if test="orderStatus != null"> and s.order_status = #{orderStatus}</if>
+            <if test="deliverySn != null and deliverySn != ''"> and s.delivery_sn like concat('%', #{deliverySn}, '%')</if>
+            <if test="deptId != null">
+                and (s.dept_id = #{deptId} OR s.dept_id IN (SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{deptId}, ancestors)))
+            </if>
+            <if test="params.beginTime != null and params.beginTime != ''">
+                AND date_format(s.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
+            </if>
+            <if test="params.endTime != null and params.endTime != ''">
+                AND date_format(s.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
+            </if>
+        </where>
+        order by s.id desc
+    </select>
+
+    <select id="selectFsIntegralAfterSalesVOById" parameterType="Long" resultMap="FsIntegralAfterSalesVOResult">
+        select
+        <include refid="selectFsIntegralAfterSalesVOColumns"/>
+        from fs_integral_after_sales s
+        left join fs_user fu        on fu.user_id = s.user_id
+        left join company_user cu   on cu.user_id = s.company_user_id
+        left join company c         on c.company_id = s.company_id
+        left join fs_integral_order o on o.order_code = s.order_code
+        where s.id = #{id} and s.is_del = 0
+    </select>
+
+    <!-- App 端列表查询:强制按 userId 过滤,status 为前端查询类型(0全部/1售后中/2已完成) -->
+    <select id="selectFsIntegralAfterSalesVOListByParam" parameterType="com.fs.his.param.FsIntegralAfterSalesQueryParam" resultMap="FsIntegralAfterSalesVOResult">
+        select
+        <include refid="selectFsIntegralAfterSalesVOColumns"/>
+        from fs_integral_after_sales s
+        left join fs_user fu        on fu.user_id = s.user_id
+        left join company_user cu   on cu.user_id = s.company_user_id
+        left join company c         on c.company_id = s.company_id
+        left join fs_integral_order o on o.order_code = s.order_code
+        <where>
+            s.is_del = 0
+            and s.user_id = #{userId}
+            <if test="status != null and status == 1">
+                <!-- 售后中:正常售后单(sales_status=0)且未到退款成功(status &lt; 5) -->
+                and s.sales_status = 0 and s.status &lt; 5
+            </if>
+            <if test="status != null and status == 2">
+                <!-- 已完成:退款成功(status=5)或已撤销/已拒绝(sales_status!=0) -->
+                and (s.status = 5 or s.sales_status != 0)
+            </if>
+            <if test="salesStatus != null"> and s.sales_status = #{salesStatus}</if>
+            <if test="serviceType != null"> and s.service_type = #{serviceType}</if>
+            <if test="orderCode != null and orderCode != ''"> and s.order_code like concat('%', #{orderCode}, '%')</if>
+        </where>
+        order by s.id desc
+    </select>
+
+</mapper>

+ 23 - 2
fs-service/src/main/resources/mapper/his/FsIntegralOrderMapper.xml

@@ -23,13 +23,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="deliveryName"    column="delivery_name"    />
         <result property="deliverySn"    column="delivery_sn"    />
         <result property="deliveryTime"    column="delivery_time"    />
+        <result property="finishTime"     column="finish_time"      />
         <result property="createTime"    column="create_time"    />
         <result property="remark"    column="remark"    />
         <result property="barCode"    column="bar_code"    />
     </resultMap>
 
     <sql id="selectFsIntegralOrderVo">
-        select order_id, order_code, user_id,bar_code, user_name, user_phone, user_address, item_json, integral, back_integral, pay_money,is_pay,pay_time,pay_type, status, delivery_code, delivery_name, delivery_sn, delivery_time, create_time,qw_user_id,company_user_id,company_id,remark from fs_integral_order
+        select order_id, order_code, user_id,bar_code, user_name, user_phone, user_address, item_json, integral, back_integral, pay_money,is_pay,pay_time,pay_type, status, delivery_code, delivery_name, delivery_sn, delivery_time, finish_time, create_time,qw_user_id,company_user_id,company_id,remark from fs_integral_order
     </sql>
 
     <select id="selectFsIntegralOrderList" parameterType="FsIntegralOrder" resultMap="FsIntegralOrderResult">
@@ -149,13 +150,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{orderId}
         </foreach>
     </delete>
+    <!-- 取消订单(status → 5 已取消):用户取消未支付订单/管理员取消未发货订单 -->
     <update id="cancelOrder" parameterType="FsIntegralOrder">
+        update fs_integral_order set status = 5
+        where order_id = #{orderId}
+    </update>
+
+    <!-- 标记订单为售后中(status → -1 售后中):用户提交售后申请 -->
+    <update id="markAfterSalesStatus">
         update fs_integral_order set status = -1
         where order_id = #{orderId}
     </update>
+
+    <!-- 标记订单为已退款(status → -2 已退款):退款成功(资金+积分退款完成) -->
+    <update id="refundOrder">
+        update fs_integral_order set status = -2
+        where order_id = #{orderId}
+    </update>
+
+    <!-- 恢复订单状态(撤销售后时调用):带旧状态校验的乐观更新 -->
+    <update id="restoreOrderStatus">
+        update fs_integral_order set status = #{newStatus}
+        where order_id = #{orderId} and status = #{oldStatus}
+    </update>
+
     <update id="finishOrder">
         UPDATE fs_integral_order
-        SET status = 3
+        SET status = 3, finish_time = NOW()
         WHERE order_id = #{orderId}
           AND status = #{oldStatus}
     </update>

+ 26 - 0
fs-user-app/src/main/java/com/fs/app/controller/InquiryOrderController.java

@@ -23,6 +23,7 @@ import com.fs.his.mapper.FsUserWxMapper;
 import com.fs.his.param.*;
 import com.fs.his.service.*;
 import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.config.StoreConfig;
 import com.fs.his.vo.FsInquiryOrderListUVO;
 import com.fs.his.vo.FsInquiryOrderPingListUVO;
 import com.fs.his.vo.FsInquiryOrderReportUVO;
@@ -112,6 +113,31 @@ public class InquiryOrderController extends  AppBaseController {
     private ISysConfigService configService;
     @Autowired
     private IFsUserWxService userWxService;
+
+    /**
+     * 获取 ERP 退货收货信息
+     * <p>从 sys_config 表 config_key='his.store' 的缓存数据中读取退货相关信息:
+     * refundConsignee(联系人)、refundPhoneNumber(手机号)、refundAddress(地址)。</p>
+     * <p>返回字段:returnReceiver、returnPhone、returnAddress。</p>
+     */
+    @Login
+    @GetMapping("/getErpReturnInfo")
+    @ApiOperation(value = "获取ERP退货收货信息", notes = "从 his.store 配置读取退货联系人/手机号/地址")
+    public R getErpReturnInfo() {
+        String json = configService.selectConfigByKey("his.store");
+        if (StringUtils.isEmpty(json)) {
+            return R.ok()
+                    .put("returnReceiver", "")
+                    .put("returnPhone", "")
+                    .put("returnAddress", "");
+        }
+        StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+        return R.ok()
+                .put("returnReceiver", config.getRefundConsignee())
+                .put("returnPhone", config.getRefundPhoneNumber())
+                .put("returnAddress", config.getRefundAddress());
+    }
+
     @Login
     @ApiOperation("确认订单")
     @PostMapping("/confirm")

+ 146 - 0
fs-user-app/src/main/java/com/fs/app/controller/IntegralAfterSalesController.java

@@ -0,0 +1,146 @@
+package com.fs.app.controller;
+
+import cn.hutool.core.lang.TypeReference;
+import cn.hutool.json.JSONUtil;
+import com.fs.app.annotation.Login;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsIntegralGoods;
+import com.fs.his.domain.FsIntegralOrder;
+import com.fs.his.param.FsIntegralAfterSalesQueryParam;
+import com.fs.his.service.IFsIntegralAfterSalesService;
+import com.fs.his.service.IFsIntegralOrderService;
+import com.fs.his.vo.FsIntegralAfterSalesVO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 积分订单售后Controller(App端)
+ * <p>提供积分订单售后单的列表查询、详情查询(含积分订单商品明细)能力,
+ * 参考 {@link com.fs.app.controller.store.StoreAfterSalesScrmController} 普通商品售后的接口设计。</p>
+ * <p>积分售后无独立商品明细表(商品信息统一存储在 {@code fs_integral_order.item_json} 中),
+ * 详情接口额外返回从 item_json 解析出的商品列表,对齐普通商品售后 items 结构。</p>
+ *
+ * @author fs
+ * @date 2026-07-23
+ */
+@Slf4j
+@Api("积分订单售后接口")
+@RestController
+@RequestMapping("/app/integralAfterSales")
+public class IntegralAfterSalesController extends AppBaseController
+{
+    @Autowired
+    private IFsIntegralAfterSalesService integralAfterSalesService;
+
+    @Autowired
+    private IFsIntegralOrderService integralOrderService;
+
+    /**
+     * 获取积分售后列表
+     * <p>参考普通商品售后 {@code /store/app/storeAfterSales/getStoreAfterSalesList} 实现,
+     * 使用 PageHelper 分页,强制按当前登录用户 userId 过滤,防止越权查看他人售后单。</p>
+     * <p>返回的 VO 已含关联订单信息(integral/payMoney/itemJson/userAddress/orderCreateTime 等),
+     * 前端无需再单独请求订单详情。</p>
+     */
+    @Login
+    @ApiOperation("获取积分售后列表")
+    @GetMapping("/getAfterSalesList")
+    public R getAfterSalesList(FsIntegralAfterSalesQueryParam param)
+    {
+        PageHelper.startPage(param.getPage(), param.getPageSize());
+        param.setUserId(Long.parseLong(getUserId()));
+        // 前端传入的 status 实为查询类型(0全部/1售后中/2已完成),非售后单 status 字段
+        List<FsIntegralAfterSalesVO> list = integralAfterSalesService.selectFsIntegralAfterSalesVOListByParam(param);
+        PageInfo<FsIntegralAfterSalesVO> listPageInfo = new PageInfo<>(list);
+        return R.ok().put("data", listPageInfo);
+    }
+
+    /**
+     * 获取积分售后详情(含积分订单商品明细)
+     * <p>参考普通商品售后 {@code /store/app/storeAfterSales/getAfterSalesDetails} 实现,
+     * 在其基础上额外返回关联的积分订单信息和商品明细列表,便于前端售后详情页一次性渲染。</p>
+     *
+     * <p>返回结构:
+     * <ul>
+     *   <li>sales:售后单信息(含关联订单的 integral/payMoney/itemJson/userAddress 等字段)</li>
+     *   <li>order:关联的积分订单信息(userPhone/userAddress 已脱敏)</li>
+     *   <li>items:从 order.itemJson 解析出的商品明细列表,对齐普通商品售后 items 结构</li>
+     * </ul>
+     *
+     * <p>安全校验:App 端只能查看本人的售后单,防止越权。</p>
+     *
+     * @param id 售后单ID
+     * @return sales:售后单;order:积分订单(脱敏);items:商品明细列表
+     */
+    @Login
+    @ApiOperation("获取积分售后详情")
+    @GetMapping("/getAfterSalesDetails")
+    public R getAfterSalesDetails(@RequestParam("salesId") Long id)
+    {
+        FsIntegralAfterSalesVO sales = integralAfterSalesService.selectFsIntegralAfterSalesVOById(id);
+        if (sales == null) {
+            return R.error("售后单不存在");
+        }
+        // 安全校验:仅允许查看本人的售后单
+        if (!Long.valueOf(getUserId()).equals(sales.getUserId())) {
+            return R.error("非法操作");
+        }
+
+        // 查询关联的积分订单,脱敏后返回
+        FsIntegralOrder order = integralOrderService.selectFsIntegralOrderByOrderId(sales.getOrderId());
+        List<FsIntegralGoods> items = new ArrayList<>();
+        if (order != null) {
+            order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
+            order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
+            // 先解析 itemJson 为商品列表,再清空订单原始 itemJson,避免重复传输
+            items = parseItemJsonToList(order.getItemJson());
+            order.setItemJson(null);
+        }
+
+        return R.ok().put("sales", sales).put("order", order).put("items", items);
+    }
+
+    /**
+     * 将 itemJson 解析为商品明细列表
+     * <p>兼容历史数据两种格式:
+     * <ul>
+     *   <li>JSON 数组格式 {@code [{"goodsId":1,...}]}:直接解析为 List</li>
+     *   <li>JSON 单对象格式 {@code {"goodsId":1,...}}:解析后包装为单元素 List</li>
+     * </ul>
+     * 解析失败时返回空列表,避免影响详情展示。</p>
+     *
+     * @param itemJson 订单商品 JSON 字符串
+     * @return 商品明细列表;空值或解析失败返回空列表(非 null)
+     */
+    private List<FsIntegralGoods> parseItemJsonToList(String itemJson) {
+        if (StringUtils.isBlank(itemJson)) {
+            return new ArrayList<>();
+        }
+        try {
+            if (itemJson.startsWith("[")) {
+                return JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
+            } else if (itemJson.startsWith("{")) {
+                FsIntegralGoods goods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
+                List<FsIntegralGoods> list = new ArrayList<>();
+                list.add(goods);
+                return list;
+            }
+        } catch (Exception e) {
+            log.warn("解析 itemJson 为商品列表失败, itemJson={}, error={}", itemJson, e.getMessage());
+        }
+        return new ArrayList<>();
+    }
+}

+ 220 - 1
fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

@@ -2,19 +2,27 @@ package com.fs.app.controller;
 
 
 import cn.hutool.core.lang.TypeReference;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.annotation.Login;
+import com.fs.app.param.FsStoreOrderExpressParam;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.his.domain.*;
+import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.enums.PaymentMethodEnum;
+import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.param.*;
 import com.fs.his.service.*;
 import com.fs.his.vo.*;
+import com.fs.hisStore.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -50,6 +58,10 @@ public class IntegralController extends  AppBaseController {
     private IFsIntegralCartService cartService;
     @Autowired
     private ISysConfigService configService;
+    @Autowired
+    private IFsIntegralAfterSalesService integralAfterSalesService;
+    @Autowired
+    private IFsExpressService expressService;
     @ApiOperation("获取积分商品列表")
     @GetMapping("/getIntegralGoodsList")
     @Cacheable(value = "getIntegralGoodsList", key = "#param")
@@ -89,7 +101,12 @@ public class IntegralController extends  AppBaseController {
         param.setUserId(Long.parseLong(getUserId()));
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsIntegralOrderListUVO> list=integralOrderService.selectFsIntegralOrderListUVO(param);
-        list.forEach(vo -> vo.setItemJson(adaptItemJson(vo.getItemJson())));
+        // 售后时效配置一次读取,循环复用
+        StoreConfig storeConfig = getStoreConfig();
+        list.forEach(vo -> {
+            vo.setItemJson(adaptItemJson(vo.getItemJson()));
+            vo.setIsAfterSales(computeIsAfterSales(vo.getStatus(), vo.getFinishTime(), storeConfig));
+        });
         PageInfo<FsIntegralOrderListUVO> listPageInfo=new PageInfo<>(list);
         return R.ok().put("data",listPageInfo);
     }
@@ -99,9 +116,86 @@ public class IntegralController extends  AppBaseController {
     public R getIntegralOrderById(@RequestParam("orderId")Long orderId, HttpServletRequest request){
         FsIntegralOrder order=integralOrderService.selectFsIntegralOrderByOrderId(orderId);
         order.setItemJson(adaptItemJson(order.getItemJson()));
+        order.setIsAfterSales(computeIsAfterSales(order.getStatus(), order.getFinishTime(), getStoreConfig()));
         return R.ok().put("data",order);
     }
 
+    /**
+     * 获取积分订单商品明细(售后申请页使用)
+     * <p>参考普通商品售后 {@link com.fs.app.controller.store.StoreAfterSalesScrmController#getMyStoreOrderById},
+     * 返回 order(脱敏后的订单信息)+ items(从 item_json 解析的商品列表)。</p>
+     * <p>积分订单商品信息统一存储在 {@code fs_integral_order.item_json} 字段(JSON 数组或单对象),
+     * 无独立商品明细表,故在此解析为 {@link FsIntegralGoods} 列表返回,对齐普通商品售后接口的 items 结构。</p>
+     *
+     * @param orderId 订单ID
+     * @param request 请求对象
+     * @return order:脱敏后的订单信息;items:商品明细列表
+     */
+    @Login
+    @ApiOperation("获取积分订单商品明细")
+    @GetMapping("/getMyStoreOrderItemByOrderId")
+    public R getMyStoreOrderItemByOrderId(@RequestParam("orderId") Long orderId, HttpServletRequest request){
+        FsIntegralOrder order = integralOrderService.selectFsIntegralOrderByOrderId(orderId);
+        // 用户隐私信息脱敏(手机号、地址),与普通商品售后接口保持一致
+        order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
+        order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
+        // 积分订单商品信息存在 item_json 中,解析为列表返回,兼容单对象/数组两种历史格式
+        List<FsIntegralGoods> items = parseItemJsonToList(order.getItemJson());
+        return R.ok().put("order", order).put("items", items);
+    }
+
+    /**
+     * 查询积分订单物流轨迹
+     * <p>参考普通商品售后 {@link com.fs.app.controller.store.StoreOrderScrmController#getExpress} 实现,
+     * 调用第三方物流查询接口返回快递轨迹。</p>
+     * <p>顺丰速运(ShipperCode=SF)需额外传收件人手机号后4位用于身份校验,
+     * 其他快递公司该参数传空字符串。</p>
+     * <p>异常兜底:物流接口异常时返回订单快递单号,前端可展示单号供用户手动查询。</p>
+     *
+     * @param param 订单ID参数
+     * @param request 请求对象
+     * @return data:物流轨迹信息;deliverySn:异常兜底返回的快递单号
+     */
+    @Login
+    @ApiOperation("查询积分订单物流轨迹")
+    @PostMapping("/getExpress")
+    public R getExpress(@Validated @RequestBody FsStoreOrderExpressParam param, HttpServletRequest request){
+        FsIntegralOrder order = integralOrderService.selectFsIntegralOrderByOrderId(param.getOrderId());
+        if (ObjectUtil.isNull(order)) {
+            throw new CustomException("订单不存在");
+        }
+        // 安全校验:仅允许查询本人的订单物流
+        if (!Long.valueOf(getUserId()).equals(order.getUserId())) {
+            throw new CustomException("非法操作");
+        }
+        // 校验是否已发货
+        if (StringUtils.isEmpty(order.getDeliverySn())) {
+            throw new CustomException("未发货订单不能查询");
+        }
+        try {
+            // 顺丰速运需传收件人手机号后4位进行身份校验
+            String lastFourNumber = "";
+            if (ShipperCodeEnum.SF.getValue().equals(order.getDeliveryCode())) {
+                String userPhone = order.getUserPhone();
+                if (StringUtils.isNotEmpty(userPhone) && userPhone.length() == 11) {
+                    lastFourNumber = StrUtil.sub(userPhone, userPhone.length(), -4);
+                }
+            }
+            ExpressInfoDTO expressInfoDTO = expressService.getExpressInfo(
+                    order.getOrderCode(),
+                    order.getDeliveryCode(),
+                    order.getDeliverySn(),
+                    lastFourNumber
+            );
+            return R.ok().put("data", expressInfoDTO);
+        } catch (Exception e) {
+            log.warn("查询积分订单物流轨迹失败, orderId={}, deliverySn={}, error={}",
+                    order.getOrderId(), order.getDeliverySn(), e.getMessage());
+            // 异常兜底:返回快递单号,前端可引导用户手动查询
+            return R.ok().put("deliverySn", order.getDeliverySn());
+        }
+    }
+
     /**
      * 兼容处理itemJson
      */
@@ -129,6 +223,100 @@ public class IntegralController extends  AppBaseController {
         return itemJson;
     }
 
+    /**
+     * 将 itemJson 解析为商品明细列表
+     * <p>兼容历史数据两种格式:
+     * <ul>
+     *   <li>JSON 数组格式 {@code [{"goodsId":1,...}]}:直接解析为 List</li>
+     *   <li>JSON 单对象格式 {@code {"goodsId":1,...}}:解析后包装为单元素 List</li>
+     * </ul>
+     * 解析失败时返回空列表,避免影响订单详情展示。</p>
+     *
+     * @param itemJson 订单商品 JSON 字符串
+     * @return 商品明细列表;空值或解析失败返回空列表(非 null)
+     */
+    private List<FsIntegralGoods> parseItemJsonToList(String itemJson) {
+        if (StringUtils.isBlank(itemJson)) {
+            return new ArrayList<>();
+        }
+        try {
+            if (itemJson.startsWith("[")) {
+                return JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
+            } else if (itemJson.startsWith("{")) {
+                FsIntegralGoods goods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
+                List<FsIntegralGoods> list = new ArrayList<>();
+                list.add(goods);
+                return list;
+            }
+        } catch (Exception e) {
+            log.warn("解析 itemJson 为商品列表失败, itemJson={}, error={}", itemJson, e.getMessage());
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取门店配置(售后时效配置来源)
+     * <p>读取 sys_config 表 config_key='store.config' 的缓存数据,
+     * 确保前端"可售后"标识与实际售后申请校验逻辑完全一致。</p>
+     *
+     * @return 门店配置对象;配置不存在或解析失败时返回 null
+     */
+    private StoreConfig getStoreConfig() {
+        String json = configService.selectConfigByKey("store.config");
+        if (StringUtils.isEmpty(json)) {
+            return null;
+        }
+        try {
+            return JSONUtil.toBean(json, StoreConfig.class);
+        } catch (Exception e) {
+            log.warn("解析 store.config 配置失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 计算订单是否可申请售后
+     * 结合积分订单状态字典(1待发货/2待收货/3已完成/4待支付/5已取消/-1退款中/-2已退款/-3已取消)调整:
+     * <ul>
+     *   <li>status=1/2(待发货/待收货):可售后</li>
+     *   <li>status=3(已完成):校验 finishTime + storeAfterSalesDay 是否超时,未超时可售后</li>
+     *   <li>status=4/5/-1/-2/-3(待支付/已取消/退款中/已退款/已取消):不可售后</li>
+     * </ul>
+     * 确保前端显示"可售后"的订单实际申请时也能通过。</p>
+     *
+     * @param status     订单状态
+     * @param finishTime 完成时间(status=3 时用于时效校验,可为 null)
+     * @param config     门店配置(含 storeAfterSalesDay 售后有效天数,可为 null)
+     * @return 1=可申请售后 0=不可申请售后
+     */
+    private Integer computeIsAfterSales(Integer status, Date finishTime, StoreConfig config) {
+        if (status == null) {
+            return 0;
+        }
+        // 待发货(1)/待收货(2):可售后
+        if (status == 1 || status == 2) {
+            return 1;
+        }
+        // 已完成(3):校验售后时效
+        if (status == 3) {
+            // 完成时间缺失(历史数据),不限制
+            if (finishTime == null) {
+                return 0;
+            }
+            // 未配置售后有效天数,不限制
+            if (config == null || config.getStoreAfterSalesDay() == null || config.getStoreAfterSalesDay() <= 0) {
+                return 0;
+            }
+            // finishTime + storeAfterSalesDay >= 当前时间 则可售后
+            Calendar calendarAfterSales = new GregorianCalendar();
+            calendarAfterSales.setTime(finishTime);
+            calendarAfterSales.add(Calendar.DATE, config.getStoreAfterSalesDay());
+            return calendarAfterSales.getTime().getTime() >= System.currentTimeMillis() ? 1 : 0;
+        }
+        // 待支付(4)/已取消(5)/退款中(-1)/已退款(-2)/已取消(-3):不可售后
+        return 0;
+    }
+
     @Login
     @ApiOperation("计算订单")
     @PostMapping("/compute")
@@ -295,4 +483,35 @@ public class IntegralController extends  AppBaseController {
         return integralOrderService.createCartOrder(param);
     }
 
+    /**
+     * 申请积分订单售后
+     * <p>参考普通商品售后流程,用户提交售后申请(仅退款/退货退款),
+     * 生成 fs_integral_after_sales 售后单,等待平台审核。</p>
+     * <p>支持"一步提交":同时传商家收货信息+物流信息时,serviceType 强制为退货退款。</p>
+     */
+    @Login
+    @ApiOperation("申请积分订单售后")
+    @PostMapping("/applyAfterSales")
+    @RepeatSubmit
+    public R applyAfterSales(@Validated @RequestBody FsIntegralAfterSalesApplyParam param){
+        return integralAfterSalesService.applyForAfterSales(Long.parseLong(getUserId()), param);
+    }
+
+    /**
+     * 撤销积分订单售后申请
+     * 用户可撤销处于"已提交待审核(0)"或"平台已审核(1)"状态的售后单。</p>
+     * <p>撤销后订单状态从 -1 退款中 恢复为售后申请前的原始状态,
+     * 用户可重新发起售后申请或正常使用订单。</p>
+     *
+     * @param param 售后单ID参数
+     * @return 操作结果
+     */
+    @Login
+    @ApiOperation("撤销积分订单售后申请")
+    @PostMapping("/revokeAfterSales")
+    @RepeatSubmit
+    public R revokeAfterSales(@Validated @RequestBody FsStoreAfterSalesRevokeParam param){
+        return integralAfterSalesService.revoke(Long.parseLong(getUserId()), param.getId());
+    }
+
 }

+ 118 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

@@ -686,6 +686,124 @@ public class PayScrmController {
         return "SUCCESS";
     }
 
+    /**
+     * 易宝聚合支付SDK积分订单退款结果通知
+     * <p>
+     * 与普通商品退款回调({@link #multiStoreYbRefundNotify})隔离,独立处理积分订单退款状态。
+     * <p>
+     * 通知格式:POST + FORM,参数有 response(密文) 和 customerIdentification
+     * 回调解密使用 DigitalEnvelopeUtils.decrypt(response, appKey)
+     * 解密后JSON包含:orderId(商户收款请求号)、uniqueOrderNo(易宝收款订单号)、
+     * refundRequestId(退款请求号,格式 refund-{payCode})、uniqueRefundNo(易宝退款订单号)、
+     * status(SUCCESS/FAILED)、refundAmount(退款金额)、refundSuccessDate(退款成功时间)、
+     * errorMessage(失败原因)等
+     * <p>
+     * 处理流程:
+     * 1. 解密回调密文,解析退款状态
+     * 2. 仅处理 status=SUCCESS 的退款,非成功也回写SUCCESS避免重复通知
+     * 3. 拆分 refundRequestId(refund-{payCode}) 获取支付单号
+     * 4. 加 Redisson 分布式锁防并发重复回调
+     * 5. 调用 integralOrderService.refundConfirmByYop 更新支付记录状态(-2→-1)
+     * <p>
+     * 注意:积分退还在发起退款时已同步完成(refundIntegralSafely),本回调仅更新资金退款状态。
+     */
+    @ApiOperation("易宝聚合支付SDK积分订单退款结果通知")
+    @PostMapping(value = "/multiStore/ybIntegralRefundNotify")
+    public String multiStoreYbIntegralRefundNotify(@RequestParam String response, @RequestParam(required = false) String customerIdentification) {
+        try {
+            //从平台配置获取appKey,用于解密
+            String configJson = configService.selectConfigByKey("his.pay");
+            FsPayConfig payConfig = new Gson().fromJson(configJson, FsPayConfig.class);
+            if (payConfig == null || StringUtils.isBlank(payConfig.getYbAppKey())) {
+                logger.error("易宝积分订单退款回调失败:支付配置或ybAppKey不存在");
+                return "SUCCESS";
+            }
+
+            //解密回调密文
+            String decrypted;
+            try {
+                decrypted = DigitalEnvelopeUtils.decrypt(response, payConfig.getYbAppKey(), "RSA2048");
+            } catch (Exception e) {
+                logger.error("易宝积分订单退款回调解密失败:{}", e.getMessage(), e);
+                return "SUCCESS";
+            }
+            logger.info("易宝积分订单退款回调解密明文:{}", decrypted);
+
+            //解析解密后的JSON
+            JSONObject notifyData = JSON.parseObject(decrypted);
+            String orderId = notifyData.getString("orderId");                 // 商户收款请求号
+            String uniqueOrderNo = notifyData.getString("uniqueOrderNo");     // 易宝收款订单号
+            String refundRequestId = notifyData.getString("refundRequestId"); // 商户退款请求号,格式 refund-{payCode}
+            String uniqueRefundNo = notifyData.getString("uniqueRefundNo");   // 易宝退款订单号
+            String status = notifyData.getString("status");                   // 退款状态 SUCCESS/FAILED
+            String refundAmount = notifyData.getString("refundAmount");       // 退款金额
+            String refundSuccessDate = notifyData.getString("refundSuccessDate"); // 退款成功时间
+            String errorMessage = notifyData.getString("errorMessage");       // 失败原因
+
+            logger.info("易宝积分订单退款回调:orderId={}, refundRequestId={}, status={}, refundAmount={}, uniqueRefundNo={}",
+                    orderId, refundRequestId, status, refundAmount, uniqueRefundNo);
+
+            //只处理退款成功
+            if (!"SUCCESS".equals(status)) {
+                logger.warn("易宝积分订单退款回调状态非成功:orderId={}, status={}, errorMessage={}", orderId, status, errorMessage);
+                return "SUCCESS";  // 非成功也回写SUCCESS,避免重复通知
+            }
+
+            //refundRequestId为空则无法处理
+            if (StringUtils.isBlank(refundRequestId)) {
+                logger.error("易宝积分订单退款回调refundRequestId为空,无法处理");
+                return "SUCCESS";
+            }
+
+            //拆分refundRequestId,格式:refund-{paymentId}(与普通商品退款一致,后缀为支付记录主键)
+            String[] requestParts = refundRequestId.split("-", 2);
+            if (requestParts.length < 2) {
+                logger.error("易宝积分订单退款回调refundRequestId格式不正确:{}", refundRequestId);
+                return "SUCCESS";
+            }
+            String paymentId = requestParts[1];
+
+            //加分布式锁,防止并发重复回调
+            RLock lock = redissonClient.getLock("yb:integral:refund:callback:" + paymentId);
+            try {
+                boolean locked = lock.tryLock(200, 15000, TimeUnit.MILLISECONDS);
+                if (!locked) {
+                    logger.warn("易宝积分订单退款回调获取锁失败,paymentId={}", paymentId);
+                    return "SUCCESS";
+                }
+
+                //调用积分订单退款回调确认,更新支付记录状态(-2 退款中 → -1 已退款)
+                long startMs = System.currentTimeMillis();
+                String result = integralOrderService.refundConfirmByYop(paymentId, refundAmount);
+                long costMs = System.currentTimeMillis() - startMs;
+
+                if ("success".equals(result)) {
+                    logger.info("【易宝积分订单退款回调-处理成功】paymentId={}, cost={}ms, uniqueRefundNo={}", paymentId, costMs, uniqueRefundNo);
+                    return "SUCCESS";
+                }
+                if (StringUtils.isBlank(result)) {
+                    // 空字符串:未找到支付记录或状态异常(非退款中),无需重试
+                    logger.warn("【易宝积分订单退款回调-返回空】paymentId={}, cost={}ms, result={} 未找到支付记录或状态非退款中,返回SUCCESS", paymentId, costMs, result);
+                    return "SUCCESS";
+                }
+                // FAIL:处理失败需重试
+                logger.error("【易宝积分订单退款回调-处理失败】paymentId={}, cost={}ms, result={} 返回FAIL触发易宝重试", paymentId, costMs, result);
+                return "FAIL";
+            } catch (InterruptedException e) {
+                logger.error("易宝积分订单退款回调获取锁被中断,paymentId={}", paymentId, e);
+                Thread.currentThread().interrupt();
+                return "SUCCESS";
+            } finally {
+                if (lock.isHeldByCurrentThread()) {
+                    lock.unlock();
+                }
+            }
+        } catch (Exception e) {
+            logger.error("易宝积分订单退款回调处理异常:{}", e.getMessage(), e);
+        }
+        return "SUCCESS";
+    }
+
     /**
      * 易宝结算结果通知
      * <p>

+ 114 - 0
积分售后表_数据库变更.sql

@@ -0,0 +1,114 @@
+-- =====================================================
+-- 积分订单售后单表 fs_integral_after_sales
+-- 创建时间:2026-07-21
+-- 说明:参考 fs_store_after_sales_scrm 表结构,针对积分订单特性优化
+--       - 新增 refund_integral 字段记录退回积分
+--       - 新增 refund_amount 字段记录退款金额
+--       - 不含 store_id(积分订单无店铺概念)
+--       - 不含 is_package / package_json(积分订单无套餐概念)
+-- =====================================================
+
+DROP TABLE IF EXISTS `fs_integral_after_sales`;
+CREATE TABLE `fs_integral_after_sales` (
+    `id`                BIGINT          NOT NULL                COMMENT '主键',
+    `order_id`          BIGINT          DEFAULT NULL            COMMENT '积分订单ID',
+    `order_code`        VARCHAR(64)     NOT NULL                COMMENT '订单编号',
+    `user_id`           BIGINT          DEFAULT NULL            COMMENT '用户ID',
+    `company_id`        BIGINT          DEFAULT NULL            COMMENT '企业ID',
+    `company_user_id`   BIGINT          DEFAULT NULL            COMMENT '企业员工ID',
+    `refund_amount`     DECIMAL(10,2)   DEFAULT '0.00'          COMMENT '退款金额',
+    `refund_integral`   BIGINT          DEFAULT '0'             COMMENT '退回积分',
+    `service_type`      TINYINT         DEFAULT 0               COMMENT '服务类型 0仅退款 1退货退款',
+    `reasons`           VARCHAR(255)    DEFAULT NULL            COMMENT '申请原因',
+    `explains`          VARCHAR(500)    DEFAULT NULL            COMMENT '说明',
+    `explain_img`       VARCHAR(1000)   DEFAULT NULL            COMMENT '说明图片,多个用逗号分割',
+    `shipper_code`      VARCHAR(50)     DEFAULT NULL            COMMENT '物流公司编码',
+    `delivery_sn`       VARCHAR(64)     DEFAULT NULL            COMMENT '物流单号',
+    `delivery_name`     VARCHAR(100)    DEFAULT NULL            COMMENT '物流名称',
+    `delivery_code`     VARCHAR(50)     DEFAULT NULL            COMMENT '物流公司编码(兼容字段)',
+    `status`            TINYINT         DEFAULT 0               COMMENT '审核状态 0已提交待平台审核 1平台已审核 2用户已发货待仓库审核 3仓库已审核待财务审核 4退款中(财务已发起退款,等待回调确认) 5退款成功(回调确认完成)',
+    `sales_status`      TINYINT         DEFAULT 0               COMMENT '售后状态 0正常 1用户取消 2商家拒绝 3已完成',
+    `order_status`      TINYINT         DEFAULT NULL            COMMENT '订单当前状态(冗余记录)',
+    `consignee`         VARCHAR(100)    DEFAULT NULL            COMMENT '商家收货人',
+    `phone_number`      VARCHAR(20)     DEFAULT NULL            COMMENT '商家手机号',
+    `address`           VARCHAR(500)    DEFAULT NULL            COMMENT '商家地址',
+    `consignee_phone`   VARCHAR(20)     DEFAULT NULL            COMMENT '商家收货电话',
+    `dept_id`           BIGINT          DEFAULT NULL            COMMENT '部门ID',
+    `operator`          VARCHAR(64)     DEFAULT NULL            COMMENT '操作人',
+    `remark`            VARCHAR(500)    DEFAULT NULL            COMMENT '备注',
+    `is_del`            TINYINT         DEFAULT 0               COMMENT '逻辑删除 0正常 1已删除',
+    `create_by`         VARCHAR(64)     DEFAULT NULL            COMMENT '创建者',
+    `create_time`       DATETIME        DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_by`         VARCHAR(64)     DEFAULT NULL            COMMENT '更新者',
+    `update_time`       DATETIME        DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_order_id`      (`order_id`),
+    KEY `idx_order_code`    (`order_code`),
+    KEY `idx_user_id`       (`user_id`),
+    KEY `idx_company_id`    (`company_id`),
+    KEY `idx_status`        (`status`, `sales_status`),
+    KEY `idx_create_time`   (`create_time`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '积分订单售后单';
+
+
+-- =====================================================
+-- 菜单与权限初始化
+-- 说明:以下 menu_id 使用占位值,执行前请根据实际 sys_menu 表的 max(menu_id) 调整
+--       parent_id 需替换为"积分订单"菜单的 menu_id(可执行下方查询获取)
+--       SELECT menu_id, menu_name FROM sys_menu WHERE menu_name LIKE '%积分%';
+-- =====================================================
+
+-- 1. 主菜单:积分售后管理(目录下挂载页面)
+-- 注意:parent_id 请替换为实际的"积分订单"父菜单ID
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3001, '积分售后管理', 0, 50, 'integralAfterSales', 'his/integralAfterSales/index', 1, 0, 'C', '0', '0', 'his:integralAfterSales:list', 'shopping', 'admin', NOW(), '积分订单售后管理菜单');
+
+-- 2. 按钮权限
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3002, '积分售后查询', 3001, 1, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:query', '#', 'admin', NOW(), '');
+
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3003, '积分售后导出', 3001, 2, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:export', '#', 'admin', NOW(), '');
+
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3004, '积分售后平台审核', 3001, 3, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:audit', '#', 'admin', NOW(), '');
+
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3005, '积分售后撤销', 3001, 4, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:cancel', '#', 'admin', NOW(), '');
+
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3006, '积分售后财务退款', 3001, 5, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:refund', '#', 'admin', NOW(), '');
+
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+VALUES (3007, '积分售后编辑物流', 3001, 6, '#', '', 1, 0, 'F', '0', '0', 'his:integralAfterSales:edit', '#', 'admin', NOW(), '');
+
+
+-- =====================================================
+-- 积分售后独立状态字典 integral_after_sales_status
+-- 创建时间:2026-07-24
+-- 说明:积分售后使用独立字典,与商城售后(store_after_sales_status)隔离
+--       仅退款(serviceType=0): 0→1→4→5
+--       退款退货(serviceType=1): 0→1→2→3→4→5
+-- =====================================================
+
+-- 1. 字典类型
+INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
+VALUES ('积分售后状态', 'integral_after_sales_status', '0', 'admin', NOW(), '积分订单售后审核状态字典');
+
+-- 2. 字典数据
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
+VALUES
+    (0, '已提交', '0', 'integral_after_sales_status', '', 'info', 'N', '0', 'admin', NOW(), '用户提交售后申请,待平台审核'),
+    (1, '已审核', '1', 'integral_after_sales_status', '', 'warning', 'N', '0', 'admin', NOW(), '平台已审核,仅退款等待财务退款/退款退货等待用户发货'),
+    (2, '已发货', '2', 'integral_after_sales_status', '', 'warning', 'N', '0', 'admin', NOW(), '用户已寄回商品,待仓库审核'),
+    (3, '仓库已审核', '3', 'integral_after_sales_status', '', 'primary', 'N', '0', 'admin', NOW(), '仓库已确认收货,待财务退款'),
+    (4, '退款中', '4', 'integral_after_sales_status', '', 'danger', 'N', '0', 'admin', NOW(), '财务已发起退款,等待回调确认'),
+    (5, '退款成功', '5', 'integral_after_sales_status', '', 'success', 'N', '0', 'admin', NOW(), '退款回调确认完成,售后流程结束');
+
+-- 3. 数据迁移说明
+--    若已有旧数据使用 status=3(原退款成功) 或 status=4(原退款成功),需根据实际业务状态调整:
+--    - 已发起退款但未回调成功的:status=4(退款中)
+--    - 回调成功的:status=5(退款成功)
+-- UPDATE fs_integral_after_sales SET status = 5 WHERE status = 4 AND sales_status = 3;
+-- UPDATE fs_integral_after_sales SET status = 4 WHERE status = 3 AND sales_status = 0;
+

+ 40 - 0
积分订单状态重构_数据库变更.sql

@@ -0,0 +1,40 @@
+-- =====================================================
+-- 积分订单状态字典重构 + 售后时效支持
+-- 创建时间:2026-07-22
+-- 更新时间:2026-07-24
+-- 说明:
+--   1. fs_integral_order 表新增 finish_time 字段(完成时间),用于售后时效校验
+--   2. 调整 sys_integral_order_status 字典:
+--      - 1待发货 / 2待收货 / 3已完成 / 4待支付 / 5已取消(原有)
+--      - -1售后中(用户提交售后申请)/ -2已退款(退款成功)
+--      (取消原 -3 已取消,与 5 已取消合并;原 -1 退款中 改名为 售后中)
+--   3. 状态与代码逻辑对齐:markAfterSalesStatus(-1售后中)、refundOrder(-2已退款)、cancelOrder(5已取消)
+-- =====================================================
+
+-- 1. fs_integral_order 新增 finish_time 字段
+ALTER TABLE `fs_integral_order`
+    ADD COLUMN `finish_time` DATETIME DEFAULT NULL COMMENT '完成时间(确认收货时间)'
+    AFTER `delivery_time`;
+
+-- 2. 积分订单状态字典(-1/-2)
+-- 注意:执行前先查询 SELECT dict_code FROM sys_dict_data WHERE dict_type='sys_integral_order_status' AND dict_value='-1';
+--       若已存在记录,改为 UPDATE 而非 INSERT
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
+VALUES
+    (-1, '售后中', '-1', 'sys_integral_order_status', '', 'warning', 'N', '0', 'admin', NOW(), '售后申请已提交,等待处理'),
+    (-2, '已退款', '-2', 'sys_integral_order_status', '', 'danger', 'N', '0', 'admin', NOW(), '退款成功');
+
+-- 3. 删除原 -3 已取消 字典(与 5 已取消 合并)
+DELETE FROM sys_dict_data WHERE dict_type = 'sys_integral_order_status' AND dict_value = '-3';
+
+-- 4. 老数据清洗
+--    4.1 原 status=-1 的订单(原"已取消"语义)应更新为 status=5(已取消)
+--    4.2 原 status=-3 的订单应更新为 status=5(已取消)
+--    注意:此清洗需要根据业务实际情况执行,以下为参考脚本(请先备份再执行)
+-- UPDATE fs_integral_order SET status = 5 WHERE status = -3;
+-- UPDATE fs_integral_order SET status = 5 WHERE status = -1 AND order_id NOT IN (
+--     SELECT order_id FROM fs_user_integral_logs WHERE log_type = 27
+-- );
+
+-- 5. 已完成订单回填 finish_time(用 update_time 近似)
+-- UPDATE fs_integral_order SET finish_time = update_time WHERE status = 3 AND finish_time IS NULL;