ソースを参照

feat[litemall-admin, litemall-admin-api]: 售后页面实现审核和退款功能

Junling Bu 6 年 前
コミット
11d3b5bdfe

+ 2 - 2
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/service/AdminOrderService.java

@@ -165,7 +165,7 @@ public class AdminOrderService {
         notifyService.notifySmsTemplate(order.getMobile(), NotifyType.REFUND,
                 new String[]{order.getOrderSn().substring(8, 14)});
 
-        logHelper.logOrderSucceed("退款", "订单编号 " + orderId);
+        logHelper.logOrderSucceed("退款", "订单编号 " + order.getOrderSn());
         return ResponseUtil.ok();
     }
 
@@ -210,7 +210,7 @@ public class AdminOrderService {
         // "您的订单已经发货,快递公司 {1},快递单 {2} ,请注意查收"
         notifyService.notifySmsTemplate(order.getMobile(), NotifyType.SHIP, new String[]{shipChannel, shipSn});
 
-        logHelper.logOrderSucceed("发货", "订单编号 " + orderId);
+        logHelper.logOrderSucceed("发货", "订单编号 " + order.getOrderSn());
         return ResponseUtil.ok();
     }
 

+ 1 - 0
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/util/AdminResponseCode.java

@@ -24,5 +24,6 @@ public class AdminResponseCode {
     public static final Integer GROUPON_GOODS_EXISTED = 651;
     public static final Integer GROUPON_GOODS_OFFLINE = 652;
     public static final Integer NOTICE_UPDATE_NOT_ALLOWED = 660;
+    public static final Integer AFTERSALE_NOT_ALLOWED = 670;
 
 }

+ 184 - 12
litemall-admin-api/src/main/java/org/linlinjava/litemall/admin/web/AdminAftersaleController.java

@@ -1,20 +1,38 @@
 package org.linlinjava.litemall.admin.web;
 
+import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.linlinjava.litemall.admin.annotation.RequiresPermissionsDesc;
+import org.linlinjava.litemall.admin.service.LogHelper;
+import org.linlinjava.litemall.admin.util.AdminResponseCode;
+import org.linlinjava.litemall.core.notify.NotifyService;
+import org.linlinjava.litemall.core.notify.NotifyType;
 import org.linlinjava.litemall.core.util.JacksonUtil;
 import org.linlinjava.litemall.core.util.ResponseUtil;
 import org.linlinjava.litemall.core.validator.Order;
 import org.linlinjava.litemall.core.validator.Sort;
 import org.linlinjava.litemall.db.domain.LitemallAftersale;
+import org.linlinjava.litemall.db.domain.LitemallGoodsProduct;
+import org.linlinjava.litemall.db.domain.LitemallOrder;
+import org.linlinjava.litemall.db.domain.LitemallOrderGoods;
 import org.linlinjava.litemall.db.service.*;
+import org.linlinjava.litemall.db.util.AftersaleConstant;
+import org.linlinjava.litemall.db.util.OrderUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.List;
 
+import static org.linlinjava.litemall.admin.util.AdminResponseCode.ORDER_REFUND_FAILED;
+
 @RestController
 @RequestMapping("/admin/aftersale")
 @Validated
@@ -27,34 +45,188 @@ public class AdminAftersaleController {
     private LitemallOrderService orderService;
     @Autowired
     private LitemallOrderGoodsService orderGoodsService;
+    @Autowired
+    private LitemallGoodsProductService goodsProductService;
+    @Autowired
+    private LogHelper logHelper;
+    @Autowired
+    private WxPayService wxPayService;
+    @Autowired
+    private NotifyService notifyService;
 
     @RequiresPermissions("admin:aftersale:list")
     @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "查询")
     @GetMapping("/list")
-    public Object list(Integer orderId, String aftersaleSn,
+    public Object list(Integer orderId, String aftersaleSn, Short status,
                        @RequestParam(defaultValue = "1") Integer page,
                        @RequestParam(defaultValue = "10") Integer limit,
                        @Sort @RequestParam(defaultValue = "add_time") String sort,
                        @Order @RequestParam(defaultValue = "desc") String order) {
-        List<LitemallAftersale> aftersaleList = aftersaleService.querySelective(orderId, aftersaleSn, page, limit, sort, order);
+        List<LitemallAftersale> aftersaleList = aftersaleService.querySelective(orderId, aftersaleSn, status, page, limit, sort, order);
         return ResponseUtil.okList(aftersaleList);
     }
 
+    @RequiresPermissions("admin:aftersale:recept")
+    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "审核通过")
+    @PostMapping("/recept")
+    public Object recept(@RequestBody LitemallAftersale aftersale) {
+        Integer id = aftersale.getId();
+        LitemallAftersale aftersaleOne = aftersaleService.findById(id);
+        if(aftersaleOne == null){
+            return ResponseUtil.fail(AdminResponseCode.AFTERSALE_NOT_ALLOWED, "售后不存在");
+        }
+        Short status = aftersaleOne.getStatus();
+        if(!status.equals(AftersaleConstant.STATUS_REQUEST)){
+            return ResponseUtil.fail(AdminResponseCode.AFTERSALE_NOT_ALLOWED, "售后不能进行审核通过操作");
+        }
+        aftersaleOne.setStatus(AftersaleConstant.STATUS_RECEPT);
+        aftersaleOne.setHandleTime(LocalDateTime.now());
+        aftersaleService.updateById(aftersaleOne);
+
+        // 订单也要更新售后状态
+        orderService.updateAftersaleStatus(aftersaleOne.getOrderId(), AftersaleConstant.STATUS_RECEPT);
+        return ResponseUtil.ok();
+    }
+
+    @RequiresPermissions("admin:aftersale:batch-recept")
+    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "批量通过")
+    @PostMapping("/batch-recept")
+    public Object batchRecept(@RequestBody String body) {
+        List<Integer> ids = JacksonUtil.parseIntegerList(body, "ids");
+        // NOTE
+        // 批量操作中,如果一部分数据项失败,应该如何处理
+        // 这里采用忽略失败,继续处理其他项。
+        // 当然开发者可以采取其他处理方式,具体情况具体分析,例如利用事务回滚所有操作然后返回用户失败信息
+        for(Integer id : ids) {
+            LitemallAftersale aftersale = aftersaleService.findById(id);
+            if(aftersale == null){
+                continue;
+            }
+            Short status = aftersale.getStatus();
+            if(!status.equals(AftersaleConstant.STATUS_REQUEST)){
+                continue;
+            }
+            aftersale.setStatus(AftersaleConstant.STATUS_RECEPT);
+            aftersale.setHandleTime(LocalDateTime.now());
+            aftersaleService.updateById(aftersale);
+
+            // 订单也要更新售后状态
+            orderService.updateAftersaleStatus(aftersale.getOrderId(), AftersaleConstant.STATUS_RECEPT);
+        }
+        return ResponseUtil.ok();
+    }
+
+    @RequiresPermissions("admin:aftersale:reject")
+    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "审核拒绝")
+    @PostMapping("/reject")
+    public Object reject(@RequestBody LitemallAftersale aftersale) {
+        Integer id = aftersale.getId();
+        LitemallAftersale aftersaleOne = aftersaleService.findById(id);
+        if(aftersaleOne == null){
+            return ResponseUtil.badArgumentValue();
+        }
+        Short status = aftersaleOne.getStatus();
+        if(!status.equals(AftersaleConstant.STATUS_REQUEST)){
+            return ResponseUtil.fail(AdminResponseCode.AFTERSALE_NOT_ALLOWED, "售后不能进行审核拒绝操作");
+        }
+        aftersaleOne.setStatus(AftersaleConstant.STATUS_REJECT);
+        aftersaleOne.setHandleTime(LocalDateTime.now());
+        aftersaleService.updateById(aftersaleOne);
 
-    @RequiresPermissions("admin:aftersale:delete")
-    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "删除")
-    @PostMapping("/delete")
-    public Object delete(@RequestBody LitemallAftersale aftersale) {
-        aftersaleService.deleteById(aftersale.getId());
+        // 订单也要更新售后状态
+        orderService.updateAftersaleStatus(aftersaleOne.getOrderId(), AftersaleConstant.STATUS_REJECT);
         return ResponseUtil.ok();
     }
 
-    @RequiresPermissions("admin:aftersale:batch-delete")
-    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "批量删除")
-    @PostMapping("/batch-delete")
-    public Object batchDelete(@RequestBody String body) {
+    @RequiresPermissions("admin:aftersale:batch-reject")
+    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "批量拒绝")
+    @PostMapping("/batch-reject")
+    public Object batchReject(@RequestBody String body) {
         List<Integer> ids = JacksonUtil.parseIntegerList(body, "ids");
-        aftersaleService.deleteByIds(ids);
+        for(Integer id : ids) {
+            LitemallAftersale aftersale = aftersaleService.findById(id);
+            if(aftersale == null){
+                continue;
+            }
+            Short status = aftersale.getStatus();
+            if(!status.equals(AftersaleConstant.STATUS_REQUEST)){
+                continue;
+            }
+            aftersale.setStatus(AftersaleConstant.STATUS_REJECT);
+            aftersale.setHandleTime(LocalDateTime.now());
+            aftersaleService.updateById(aftersale);
+
+            // 订单也要更新售后状态
+            orderService.updateAftersaleStatus(aftersale.getOrderId(), AftersaleConstant.STATUS_REJECT);
+        }
+        return ResponseUtil.ok();
+    }
+
+    @RequiresPermissions("admin:aftersale:refund")
+    @RequiresPermissionsDesc(menu = {"商城管理", "售后管理"}, button = "退款")
+    @PostMapping("/refund")
+    public Object refund(@RequestBody LitemallAftersale aftersale) {
+        Integer id = aftersale.getId();
+        LitemallAftersale aftersaleOne = aftersaleService.findById(id);
+        if(aftersaleOne == null){
+            return ResponseUtil.badArgumentValue();
+        }
+        if(!aftersaleOne.getStatus().equals(AftersaleConstant.STATUS_RECEPT)){
+            return ResponseUtil.fail(AdminResponseCode.AFTERSALE_NOT_ALLOWED, "售后不能进行退款操作");
+        }
+        Integer orderId = aftersaleOne.getOrderId();
+        LitemallOrder order = orderService.findById(orderId);
+
+        // 微信退款
+        WxPayRefundRequest wxPayRefundRequest = new WxPayRefundRequest();
+        wxPayRefundRequest.setOutTradeNo(order.getOrderSn());
+        wxPayRefundRequest.setOutRefundNo("refund_" + order.getOrderSn());
+        // 元转成分
+        Integer totalFee = aftersaleOne.getAmount().multiply(new BigDecimal(100)).intValue();
+        wxPayRefundRequest.setTotalFee(totalFee);
+        wxPayRefundRequest.setRefundFee(totalFee);
+
+        WxPayRefundResult wxPayRefundResult;
+        try {
+            wxPayRefundResult = wxPayService.refund(wxPayRefundRequest);
+        } catch (WxPayException e) {
+            logger.error(e.getMessage(), e);
+            return ResponseUtil.fail(ORDER_REFUND_FAILED, "订单退款失败");
+        }
+        if (!wxPayRefundResult.getReturnCode().equals("SUCCESS")) {
+            logger.warn("refund fail: " + wxPayRefundResult.getReturnMsg());
+            return ResponseUtil.fail(ORDER_REFUND_FAILED, "订单退款失败");
+        }
+        if (!wxPayRefundResult.getResultCode().equals("SUCCESS")) {
+            logger.warn("refund fail: " + wxPayRefundResult.getReturnMsg());
+            return ResponseUtil.fail(ORDER_REFUND_FAILED, "订单退款失败");
+        }
+
+        aftersaleOne.setStatus(AftersaleConstant.STATUS_REFUND);
+        aftersaleOne.setHandleTime(LocalDateTime.now());
+        aftersaleService.updateById(aftersaleOne);
+
+        orderService.updateAftersaleStatus(orderId, AftersaleConstant.STATUS_REFUND);
+
+        // NOTE
+        // 如果是“退货退款”类型的售后,这里退款说明用户的货已经退回,则需要商品货品数量增加
+        // 开发者也可以删除一下代码,在其他地方增加商品货品入库操作
+        if(aftersale.getType().equals(AftersaleConstant.TYPE_GOODS_REQUIRED)) {
+            List<LitemallOrderGoods> orderGoodsList = orderGoodsService.queryByOid(orderId);
+            for (LitemallOrderGoods orderGoods : orderGoodsList) {
+                Integer productId = orderGoods.getProductId();
+                Short number = orderGoods.getNumber();
+                goodsProductService.addStock(productId, number);
+            }
+        }
+
+        // 发送短信通知,这里采用异步发送
+        // 退款成功通知用户, 例如“您申请的订单退款 [ 单号:{1} ] 已成功,请耐心等待到账。”
+        // TODO 注意订单号只发后6位
+        notifyService.notifySmsTemplate(order.getMobile(), NotifyType.REFUND,
+                new String[]{order.getOrderSn().substring(8, 14)});
+
+        logHelper.logOrderSucceed("退款", "订单编号 " + order.getOrderSn() + " 售后编号 " + aftersale.getAftersaleSn());
         return ResponseUtil.ok();
     }
 }

+ 28 - 4
litemall-admin/src/api/aftersale.js

@@ -8,17 +8,41 @@ export function listAftersale(query) {
   })
 }
 
-export function deleteAftersale(data) {
+export function receptAftersale(data) {
   return request({
-    url: '/aftersale/delete',
+    url: '/aftersale/recept',
     method: 'post',
     data
   })
 }
 
-export function batchDeleteAftersale(data) {
+export function batchReceptAftersale(data) {
   return request({
-    url: '/aftersale/batch-delete',
+    url: '/aftersale/batch-recept',
+    method: 'post',
+    data
+  })
+}
+
+export function rejectAftersale(data) {
+  return request({
+    url: '/aftersale/reject',
+    method: 'post',
+    data
+  })
+}
+
+export function batchRejectAftersale(data) {
+  return request({
+    url: '/aftersale/batch-reject',
+    method: 'post',
+    data
+  })
+}
+
+export function refundAftersale(data) {
+  return request({
+    url: '/aftersale/refund',
     method: 'post',
     data
   })

+ 95 - 24
litemall-admin/src/views/mall/aftersale.vue

@@ -3,21 +3,27 @@
 
     <!-- 查询和其他操作 -->
     <div class="filter-container">
-      <el-input v-model="listQuery.aftersaleSn" clearable class="filter-item" style="width: 200px;" placeholder="请输入服务编号" />
+      <el-input v-model="listQuery.aftersaleSn" clearable class="filter-item" style="width: 200px;" placeholder="请输入售后编号" />
       <el-input v-model="listQuery.orderId" clearable class="filter-item" style="width: 200px;" placeholder="请输入订单ID" />
       <el-button v-permission="['GET /admin/aftersale/list']" class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">查找</el-button>
       <el-button :loading="downloadLoading" class="filter-item" type="primary" icon="el-icon-download" @click="handleDownload">导出</el-button>
     </div>
 
     <div class="operator-container">
-      <el-button v-permission="['GET /admin/aftersale/batch-delete']" class="filter-item" type="danger" icon="el-icon-delete" @click="handleBatchDelete">批量删除</el-button>
+      <el-button v-permission="['GET /admin/aftersale/batch-recept']" class="filter-item" type="success" icon="el-icon-info" @click="handleBatchRecept">批量通过</el-button>
+      <el-button v-permission="['GET /admin/aftersale/batch-reject']" class="filter-item" type="danger" icon="el-icon-delete" @click="handleBatchReject">批量拒绝</el-button>
     </div>
 
-    <!-- 查询结果 -->
-    <el-table v-loading="listLoading" :data="list" element-loading-text="正在查询中。。。" border fit highlight-current-row @selection-change="handleSelectionChange">
+    <el-tabs v-model="tab" @tab-click="handleClick">
+      <el-tab-pane label="全部" name="all" />
+      <el-tab-pane label="待审核" name="uncheck" />
+      <el-tab-pane label="待退款" name="unrefund" />
+    </el-tabs>
+
+    <el-table v-loading="listLoading" :data="list" element-loading-text="正在查询中。。。" fit highlight-current-row @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" />
 
-      <el-table-column align="center" label="服务编号" prop="aftersaleSn" />
+      <el-table-column align="center" label="售后编号" prop="aftersaleSn" />
       <el-table-column align="center" label="订单ID" prop="orderId" />
       <el-table-column align="center" label="用户ID" prop="userId" />
       <el-table-column align="center" label="售后类型" prop="type">
@@ -29,10 +35,12 @@
       <el-table-column align="center" label="退款价格" prop="amount" />
       <el-table-column align="center" label="申请时间" prop="addTime" />
 
-      <el-table-column align="center" label="操作" min-width="100" class-name="small-padding fixed-width">
+      <el-table-column align="center" label="操作" min-width="120" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <el-button type="primary" size="mini" @click="handleRead(scope.row)">详情</el-button>
-          <el-button v-permission="['POST /admin/aftersale/delete']" type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
+          <el-button v-permission="['POST /admin/aftersale/detail']" type="primary" size="mini" @click="handleRead(scope.row)">详情</el-button>
+          <el-button v-if="scope.row.status === 1" v-permission="['POST /admin/aftersale/recept']" type="success" size="mini" @click="handleRecept(scope.row)">通过</el-button>
+          <el-button v-if="scope.row.status === 1" v-permission="['POST /admin/aftersale/reject']" type="danger" size="mini" @click="handleReject(scope.row)">拒绝</el-button>
+          <el-button v-if="scope.row.status === 2" v-permission="['POST /admin/aftersale/refund']" type="warning" size="mini" @click="handleRefund(scope.row)">退款</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -47,24 +55,26 @@
 </template>
 
 <script>
-import { listAftersale, deleteAftersale, batchDeleteAftersale } from '@/api/aftersale'
+import { listAftersale, receptAftersale, batchReceptAftersale, rejectAftersale, batchRejectAftersale, refundAftersale } from '@/api/aftersale'
 import BackToTop from '@/components/BackToTop'
 import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
 import _ from 'lodash'
 
 export default {
-  name: 'Topic',
+  name: 'Aftersale',
   components: { BackToTop, Pagination },
   data() {
     return {
       list: [],
       total: 0,
       listLoading: true,
+      tab: 'all',
       listQuery: {
         page: 1,
         limit: 20,
         aftersaleSn: undefined,
         orderId: undefined,
+        status: '',
         sort: 'add_time',
         order: 'desc'
       },
@@ -106,15 +116,27 @@ export default {
       this.listQuery.page = 1
       this.getList()
     },
-    handleDelete(row) {
-      deleteAftersale(row)
+    handleSelectionChange(val) {
+      this.multipleSelection = val
+    },
+    handleClick() {
+      if (this.tab === 'all') {
+        this.listQuery.status = ''
+      } else if (this.tab === 'uncheck') {
+        this.listQuery.status = '1'
+      } else if (this.tab === 'unrefund') {
+        this.listQuery.status = '2'
+      }
+      this.getList()
+    },
+    handleRecept(row) {
+      receptAftersale(row)
         .then(response => {
           this.$notify.success({
             title: '成功',
-            message: '删除专题成功'
+            message: '审核通过操作成功'
           })
-          const index = this.list.indexOf(row)
-          this.list.splice(index, 1)
+          this.getList()
         })
         .catch(response => {
           this.$notify.error({
@@ -123,14 +145,47 @@ export default {
           })
         })
     },
-    handleSelectionChange(val) {
-      this.multipleSelection = val
+    handleBatchRecept() {
+      if (this.multipleSelection.length === 0) {
+        this.$message.error('请选择至少一条记录')
+        return
+      }
+      const ids = []
+      _.forEach(this.multipleSelection, function(item) {
+        ids.push(item.id)
+      })
+      batchReceptAftersale({ ids: ids })
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '批量通过操作成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
     },
-    showContent(content) {
-      this.contentDetail = content
-      this.contentDialogVisible = true
+    handleReject(row) {
+      rejectAftersale(row)
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '审核拒绝操作成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
     },
-    handleBatchDelete() {
+    handleBatchReject() {
       if (this.multipleSelection.length === 0) {
         this.$message.error('请选择至少一条记录')
         return
@@ -139,11 +194,27 @@ export default {
       _.forEach(this.multipleSelection, function(item) {
         ids.push(item.id)
       })
-      batchDeleteAftersale({ ids: ids })
+      batchRejectAftersale({ ids: ids })
+        .then(response => {
+          this.$notify.success({
+            title: '成功',
+            message: '批量拒绝操作成功'
+          })
+          this.getList()
+        })
+        .catch(response => {
+          this.$notify.error({
+            title: '失败',
+            message: response.data.errmsg
+          })
+        })
+    },
+    handleRefund(row) {
+      refundAftersale(row)
         .then(response => {
           this.$notify.success({
             title: '成功',
-            message: '批量删除售后成功'
+            message: '退款操作成功'
           })
           this.getList()
         })
@@ -158,7 +229,7 @@ export default {
       this.downloadLoading = true
       import('@/vendor/Export2Excel').then(excel => {
         const tHeader = [
-          '服务编号',
+          '售后编号',
           '订单ID',
           '用户ID',
           '售后类型',