Explorar el Código

feat(review-center): 待退款审核合并入审核中心(新增待退款审核 tab + 待审计数)

Sisyphus Agent hace 1 día
padre
commit
a4769b6a67

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReviewCenterController.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.entity.ActivityRegistration;
 import com.etotem.cfc.entity.Article;
 import com.etotem.cfc.entity.ButlerProfile;
 import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.PendingRefund;
 import com.etotem.cfc.entity.SupplyHierarchyChangeRequest;
 import com.etotem.cfc.entity.TaskTemplatePackage;
 import com.etotem.cfc.entity.User;
@@ -20,6 +21,7 @@ import com.etotem.cfc.mapper.SupplyHierarchyChangeRequestMapper;
 import com.etotem.cfc.mapper.TaskTemplatePackageMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.WithdrawalRequestMapper;
+import com.etotem.cfc.mapper.PendingRefundMapper;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -59,6 +61,9 @@ public class ReviewCenterController {
     @Resource
     private SupplyHierarchyChangeRequestMapper supplyHierarchyChangeRequestMapper;
 
+    @Resource
+    private PendingRefundMapper pendingRefundMapper;
+
     @PostMapping("/summary")
     public Result<Map<String, Integer>> summary() {
         Map<String, Integer> counts = new HashMap<>();
@@ -90,6 +95,9 @@ public class ReviewCenterController {
         counts.put("withdrawPending", withdrawalRequestMapper.selectCount(
                 new QueryWrapper<WithdrawalRequest>().eq("status", "pending")).intValue());
 
+        counts.put("refundPending", pendingRefundMapper.selectCount(
+                new QueryWrapper<PendingRefund>().eq("status", "pending")).intValue());
+
         counts.put("hierarchyChangePending", supplyHierarchyChangeRequestMapper.selectCount(
                 new QueryWrapper<SupplyHierarchyChangeRequest>().eq("status", "pending")).intValue());
 

+ 8 - 2
cfc-web/src/views/admin/ReviewCenter.vue

@@ -46,6 +46,9 @@
         <el-tab-pane label="提现审核" name="withdraw">
           <withdraw-audit ref="withdraw" />
         </el-tab-pane>
+        <el-tab-pane label="待退款审核" name="refund">
+          <pending-refund-audit ref="refund" />
+        </el-tab-pane>
         <el-tab-pane label="更换上级审核" name="hierarchy">
           <hierarchy-audit ref="hierarchy" />
         </el-tab-pane>
@@ -66,6 +69,7 @@ import ActivityAudit from './review/ActivityAudit.vue'
 import ActivityRegistrationAudit from './review/ActivityRegistrationAudit.vue'
 import WithdrawAudit from './review/WithdrawAudit.vue'
 import HierarchyAudit from './review/HierarchyAudit.vue'
+import PendingRefundAudit from './review/PendingRefundAudit.vue'
 import NutritionistAudit from './review/NutritionistAudit.vue'
 
 export default {
@@ -73,7 +77,7 @@ export default {
   components: {
     ArticleAudit, ProductAudit, GuideAudit, VendorAudit, ButlerAudit,
     PackageAudit, ActivityAudit, ActivityRegistrationAudit, WithdrawAudit, HierarchyAudit,
-    NutritionistAudit
+    NutritionistAudit, PendingRefundAudit
   },
   data() {
     return {
@@ -89,6 +93,7 @@ export default {
         { key: 'activity', label: '活动审核', icon: '🎯', count: 0 },
         { key: 'activityRegistration', label: '报名审核', icon: '📝', count: 0 },
         { key: 'withdraw', label: '提现审核', icon: '💰', count: 0 },
+        { key: 'refund', label: '待退款审核', icon: '💸', count: 0 },
         { key: 'hierarchy', label: '更换上级', icon: '🔗', count: 0 },
         { key: 'nutritionist', label: '营养师审核', icon: '🥗', count: 0 },
       ]
@@ -112,7 +117,8 @@ export default {
               vendor: 'vendorPending', butler: 'butlerPending', package: 'packagePending',
               nutritionist: 'nutritionistPending',
               activity: 'activityPending', activityRegistration: 'activityRegistrationPending',
-              withdraw: 'withdrawPending', hierarchy: 'hierarchyChangePending'
+              withdraw: 'withdrawPending', refund: 'refundPending',
+              hierarchy: 'hierarchyChangePending'
             }
             c.count = res.data[keyMap[c.key]] || 0
           })

+ 247 - 0
cfc-web/src/views/admin/review/PendingRefundAudit.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="pending-refund-audit">
+    <div class="filter-bar">
+      <el-select v-model="statusFilter" placeholder="退款状态" @change="loadData">
+        <el-option label="全部状态" value="" />
+        <el-option label="待处理" value="pending" />
+        <el-option label="处理中" value="processing" />
+        <el-option label="已成功" value="success" />
+        <el-option label="已失败" value="failed" />
+      </el-select>
+      <el-select v-model="typeFilter" placeholder="订单类型" @change="loadData">
+        <el-option label="全部类型" value="" />
+        <el-option label="提现退款" value="withdrawal" />
+        <el-option label="商品退款" value="product" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+
+    <div v-if="summary.totalPendingAmount > 0" class="summary-bar">
+      <el-alert
+        :title="'待退款总金额:¥' + (summary.totalPendingAmount / 100).toFixed(2) + '(' + summary.pendingCount + ' 笔待处理)'"
+        type="warning"
+        :closable="false"
+        show-icon
+      />
+    </div>
+
+    <div class="adm-table-wrap">
+      <el-table :data="list" v-loading="loading" border stripe height="100%">
+        <el-table-column prop="id" label="ID" width="70">
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="类型" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.orderType === 'product' ? 'primary' : 'warning'" size="small">
+              {{ row.orderType === 'product' ? '商品退款' : '提现退款' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="订单号" width="180">
+          <template slot-scope="{ row }">
+            {{ row.orderNo || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="提现申请ID" width="100">
+          <template slot-scope="{ row }">
+            {{ row.withdrawalRequestId || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="用户" width="120">
+          <template slot-scope="{ row }">
+            {{ row.userName || '用户' + row.userId }}
+          </template>
+        </el-table-column>
+        <el-table-column label="退款金额" width="120">
+          <template slot-scope="{ row }">
+            <span class="amount">{{ formatPriceWithSymbol(row.amount) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="实退金额" width="120">
+          <template slot-scope="{ row }">
+            <span class="amount">{{ row.refundedAmount ? formatPriceWithSymbol(row.refundedAmount) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="尝试次数" width="80">
+          <template slot-scope="{ row }">
+            {{ row.attemptCount }}/{{ row.maxAttempts }}
+          </template>
+        </el-table-column>
+        <el-table-column label="失败原因" min-width="180" show-overflow-tooltip>
+          <template slot-scope="{ row }">
+            {{ row.failureReason || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="160">
+          <template slot-scope="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button v-if="row.status === 'pending' || row.status === 'processing'" type="danger" size="mini" @click="handleCancel(row)">
+              中止退款
+            </el-button>
+            <span v-else style="color:#999">-</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <el-pagination
+      v-if="total > 0"
+      :current-page="page"
+      :page-size="pageSize"
+      :total="total"
+      layout="total, prev, pager, next"
+      @current-change="loadData"
+      style="margin-top:12px;text-align:right;"
+    />
+  </div>
+</template>
+
+<script>
+import SortMixin from '@/mixins/SortMixin'
+import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
+
+export default {
+  name: 'PendingRefundAudit',
+  mixins: [SortMixin],
+  data() {
+    return {
+      list: [],
+      sortableColumns: { id: 'ID', createdAt: '创建时间' },
+      loading: false,
+      statusFilter: '',
+      typeFilter: '',
+      page: 1,
+      pageSize: 20,
+      total: 0,
+      summary: {
+        totalPendingAmount: 0,
+        pendingCount: 0
+      }
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        await this.loadSummary()
+        const reqParams = {
+          status: this.statusFilter || undefined,
+          orderType: this.typeFilter || undefined,
+          page: this.page,
+          size: this.pageSize
+        }
+        if (this.sortState.length > 0) reqParams.sort = this.sortState
+        const res = await getPendingRefundList(reqParams)
+        if (res.code === 200) {
+          this.list = res.data.records || []
+          this.total = res.data.total || 0
+          await this.resolveUserNames(this.list, 'userId')
+        }
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadSummary() {
+      try {
+        const res = await getPendingRefundSummary()
+        if (res.code === 200) {
+          this.summary = res.data || { totalPendingAmount: 0, pendingCount: 0 }
+        }
+      } catch (e) {
+        console.error('加载待退款摘要失败', e)
+      }
+    },
+    async resolveUserNames(items, idField) {
+      if (!items || items.length === 0) return Promise.resolve()
+      var ids = []
+      items.forEach(function(item) {
+        if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
+      })
+      var nameMap = {}
+      return Promise.all(ids.map(function(id) {
+        return searchUsers({ keyword: String(id) })
+          .then(function(res) {
+            if (res.data && res.data.length > 0) {
+              var u = res.data[0]
+              nameMap[id] = u.nickname || u.realName || '用户' + id
+            } else {
+              nameMap[id] = '用户' + id
+            }
+          })
+          .catch(function() {
+            nameMap[id] = '用户' + id
+          })
+      })).then(function() {
+        items.forEach(function(item) {
+          item.userName = nameMap[item[idField]] || '用户' + item[idField]
+        })
+      })
+    },
+    async handleCancel(row) {
+      try {
+        await this.$confirm('确认中止该退款?订单将回到退款审核状态。', '确认中止', { type: 'warning' })
+        const res = await cancelPendingRefund({ refundId: row.id })
+        if (res.code === 200) {
+          this.$message.success('已中止退款')
+          this.loadData()
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error('操作失败')
+        }
+      }
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      const d = new Date(time)
+      const pad = n => n < 10 ? '0' + n : n
+      return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
+        ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
+    },
+    formatPriceWithSymbol(amount) {
+      if (amount == null) return '-'
+      return '¥' + (amount / 100).toFixed(2)
+    },
+    statusType(status) {
+      const map = { pending: 'warning', processing: 'primary', success: 'success', failed: 'danger', cancelled: 'info' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { pending: '待处理', processing: '处理中', success: '已成功', failed: '已失败', cancelled: '已中止' }
+      return map[status] || status
+    }
+  }
+}
+</script>
+
+<style scoped>
+.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); }
+.summary-bar { margin-bottom: 12px; }
+.sort-header { cursor: pointer; user-select: none; font-weight: 500; font-size: 12px; }
+.sort-header:hover { color: #409eff; }
+.sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
+.sort-asc-badge { background: #f56c6c; }
+.sort-desc-badge { background: #409eff; }
+.amount { font-weight: 500; color: #f56c6c; }
+</style>