Forráskód Böngészése

充值超过一年的订单退款,使用线下退款确认

skyline 2 hónapja
szülő
commit
4abbe71dc3

+ 43 - 1
admin-web/src/views/admin/refund/index.vue

@@ -133,7 +133,22 @@
               <ext-d-label type="Refund.status" v-model="row[field.prop]"/>
             </template>
             <template v-else-if="'action'===field.prop">
-              <el-button v-if="row.status==='NEW'"  size="small" plain  type="warning" @click="onRefundClick(row)">退款</el-button>
+              <el-button 
+                v-if="row.status==='NEW' && !row.isOverdue"  
+                size="small" 
+                plain  
+                type="warning" 
+                @click="onRefundClick(row)">
+                退款
+              </el-button>
+              <el-button 
+                v-if="row.status==='NEW' && row.isOverdue"  
+                size="small" 
+                plain 
+                type="danger" 
+                @click="onManualRefundClick(row)">
+                线下退款
+              </el-button>
             </template>
             <template v-else>
               <div>{{ row[field.prop] }}</div>
@@ -190,6 +205,7 @@ const state = reactive({
       {label: '退款状态', prop: 'status', resizable: true, width: 120},
       {label: '申请时间', prop: 'createTime', sortable: 'custom', resizable: true, width: 160},
       {label: '退款成功时间', prop: 'successTime', sortable: 'custom', resizable: true, width: 160},
+      {label: '充值时间', prop: 'rechargeTime', sortable: 'custom', resizable: true, width: 160},
       {label: '原充值订单金额', prop: 'total', resizable: true, width: 140},
       {label: '退款申请金额', prop: 'refund', resizable: true, width: 120},
       {label: '优惠金额', prop: 'discountAmount', resizable: true, width: 90},
@@ -279,6 +295,32 @@ const onRefundClick = ( row: any) => {
   })
 };
 
+// 手动确认退款(线下退款)
+const onManualRefundClick = (row: any) => {
+  Msg.confirm(
+    `请确认是否执行线下退款操作?\n` +
+    `用户手机:${row.mobilePhone}\n` +
+    `退款金额:${u.fmt.fmtMoney(row.refund)}\n` +
+    `该订单已超过365天退款时限,请确保已通过线下方式完成实际退款后再确认!`, 
+    '重要提醒',
+    {
+      confirmButtonText: '确认线下退款',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }
+  ).then(() => {
+    $get(`/finance/manualConfirmRefund/${row.refundLogId}`).then((res: any) => {
+      Msg.message(`线下退款确认成功`)
+      loadData(true)
+    }).catch(e => {
+      console.error(e)
+      Msg.message(`操作失败:${e.message}`, 'error')
+    })
+  }).catch(() => {
+    // 用户取消操作
+  });
+};
+
 
 const handleTableSelectionChange = (selection: any) => {
   console.log("handleTableSelectionChange>>", selection)

+ 7 - 0
admin/src/main/java/com/kym/admin/controller/FinanceController.java

@@ -74,6 +74,13 @@ public class FinanceController {
         return R.success();
     }
 
+    @SysLog("手动确认退款(线下退款)")
+    @GetMapping("/manualConfirmRefund/{refundLogId}")
+    R<?> manualConfirmRefund(@PathVariable("refundLogId") long refundLogId) {
+        wxPayService.manualConfirmRefund(refundLogId);
+        return R.success();
+    }
+
     @GetMapping("/getUserTitle/{applyId}")
     Object getUserTitle(@PathVariable String applyId) {
         return wxPayService.userTitle(applyId);

+ 31 - 0
entity/src/main/java/com/kym/entity/miniapp/vo/RefundVo.java

@@ -107,4 +107,35 @@ public class RefundVo {
      */
     private String adminUsername;
 
+    /**
+     * 支付成功时间
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime paySuccessTime;
+
+    /**
+     * 是否超过退款时限(365天)
+     */
+    private Boolean isOverdue;
+
+    /**
+     * 充值时间(来自t_pay_log的success_time)
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime rechargeTime;
+
+    // 添加 getter/setter 方法以确保正确映射
+    public Boolean getIsOverdue() {
+        return isOverdue;
+    }
+
+    public void setIsOverdue(Boolean isOverdue) {
+        this.isOverdue = isOverdue;
+    }
+
+    // 为了处理 MySQL 返回的整数类型,添加一个辅助方法
+    public void setIsOverdueFromInt(Integer value) {
+        this.isOverdue = (value != null && value == 1);
+    }
+
 }

+ 12 - 2
mapper/src/main/resources/mappers/miniapp/RefundLogMapper.xml

@@ -43,6 +43,9 @@
         <result column="reason" property="reason" />
         <result column="adminUserId" property="admin_user_id" />
         <result column="adminUsername" property="admin_username" />
+        <result column="pay_success_time" property="paySuccessTime" />
+        <result column="is_overdue" property="isOverdue" typeHandler="org.apache.ibatis.type.BooleanTypeHandler" />
+        <result column="recharge_time" property="rechargeTime" />
     </resultMap>
 
     <!-- 通用查询结果列 -->
@@ -68,11 +71,16 @@
             t1.`total`,
             t1.`refund`,
             t1.`discount_amount`,
-            t1.`channel`,
             t1.`currency`,
             t1.`reason`,
             t1.`admin_user_id`,
-            t1.`admin_username`
+            t1.`admin_username`,
+            t5.success_time as pay_success_time,
+            t5.success_time as recharge_time,
+            CASE 
+                WHEN t5.success_time IS NOT NULL AND DATE_ADD(t5.success_time, INTERVAL 365 DAY) &lt; NOW() THEN 1 
+                ELSE 0 
+            END as is_overdue
         FROM
             t_refund_log t1
                 LEFT JOIN
@@ -86,6 +94,8 @@
                      LEFT JOIN t_account t3
                                ON t2.id = t3.user_id) t4
             ON t1.user_id = t4.user_id
+                LEFT JOIN t_pay_log t5
+                    ON CONVERT(t1.out_trade_no USING utf8mb4) = CONVERT(t5.out_trade_no USING utf8mb4)
         <where>
             <if test="params.mobilePhone != null and params.mobilePhone != ''  ">
                 and t4.mobile_phone = #{params.mobilePhone}

+ 2 - 2
service/src/main/java/com/kym/service/wechat/WxPayService.java

@@ -6,7 +6,6 @@ import com.kym.entity.wechat.*;
 import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
 import com.wechat.pay.java.service.refund.model.Refund;
 import jakarta.servlet.http.HttpServletRequest;
-import lombok.SneakyThrows;
 import org.springframework.http.ResponseEntity;
 
 import java.io.IOException;
@@ -22,9 +21,10 @@ public interface WxPayService {
 
     void wxRefund(long refundLogId);
 
+    void manualConfirmRefund(long refundLogId);
+
     Refund queryByOutRefundNo(String outRefundNo);
 
-    @SneakyThrows
     ResponseEntity<Object> wxRefundNotify(HttpServletRequest request);
 
     PrepayWithRequestPaymentResponse wxPay(JSONObject rechargeAmount);

+ 89 - 0
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -492,6 +492,95 @@ public class WxPayServiceImpl implements WxPayService {
     }
 
 
+    /**
+     * 手动确认退款(针对超过365天的订单)
+     *
+     * @param refundLogId 退款记录ID
+     */
+    @DS("db-miniapp")
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void manualConfirmRefund(long refundLogId) {
+        // 通过退款申请id获取退款申请记录
+        var refundLog = refundLogService.getById(refundLogId);
+        if (refundLog == null) {
+            throw new BusinessException("退款记录不存在");
+        }
+
+        // 检查退款状态,只能处理已申请状态的退款
+        if (!RefundLog.STATUS_退款已申请.equals(refundLog.getStatus())) {
+            throw new BusinessException("退款状态不正确,只能处理已申请状态的退款");
+        }
+
+        // 验证是否超过退款时限(365天)
+        var payLog = payLogService.lambdaQuery()
+                .eq(PayLog::getOutTradeNo, refundLog.getOutTradeNo())
+                .eq(PayLog::getTradeState, PayLog.STATUS_充值成功)
+                .one();
+        
+        if (payLog == null) {
+            throw new BusinessException("找不到对应的支付记录");
+        }
+
+        // 检查是否超过365天退款时限
+        LocalDateTime paySuccessTime = payLog.getSuccessTime();
+        boolean isOverdue = paySuccessTime.plusDays(365).isBefore(LocalDateTime.now());
+        
+        if (!isOverdue) {
+            throw new BusinessException("该订单未超过365天退款时限,应使用正常退款流程");
+        }
+
+        // 钱包流水
+        var walletDetail = new WalletDetail()
+                .setType(WalletDetail.TYPE_提现)
+                .setStatus(WalletDetail.STATUS_待确认)
+                .setUserId(refundLog.getUserId())
+                .setAmount(refundLog.getRefund())
+                .setOrderNo(refundLog.getOutRefundNo());
+        walletDetailService.save(walletDetail);
+
+        // 执行与微信退款成功后相同的业务逻辑
+        processRefundSuccess(refundLog, walletDetail);
+        
+        // 记录操作人员信息
+        refundLogService.lambdaUpdate()
+                .set(RefundLog::getAdminUserId, StpUtil.getLoginIdAsLong())
+                .set(RefundLog::getAdminUsername, StpUtil.getSession().getString("username"))
+                .eq(RefundLog::getId, refundLogId)
+                .update();
+        
+        LOGGER.info("手动确认退款完成,退款记录ID:{},操作人员:{}", refundLogId, StpUtil.getSession().getString("username"));
+    }
+
+    /**
+     * 处理退款成功的公共逻辑
+     *
+     * @param refundLog 退款记录
+     * @param walletDetail 钱包流水
+     */
+    private void processRefundSuccess(RefundLog refundLog, WalletDetail walletDetail) {
+        // 更新退款记录状态为退款成功
+        LocalDateTime successTime = LocalDateTime.now();
+        refundLogService.lambdaUpdate()
+                .set(RefundLog::getStatus, RefundLog.STATUS_退款成功)
+                .set(RefundLog::getSuccessTime, successTime)
+                .set(RefundLog::getUserReceivedAccount, "线下退款")
+                .eq(RefundLog::getId, refundLog.getId())
+                .update();
+
+        // 冻结金额扣减此次(退款金额+优惠金额),优惠金额字段减去申请退款时的优惠金额
+        accountService.lambdaUpdate().setSql("frozen_amount = (frozen_amount - (%d + %d)) , discount_amount = (discount_amount - %d)"
+                        .formatted(refundLog.getRefund(), refundLog.getDiscountAmount(), refundLog.getDiscountAmount()))
+                .eq(Account::getUserId, refundLog.getUserId()).update();
+
+        // 更新资金流水
+        walletDetailService.lambdaUpdate()
+                .set(WalletDetail::getStatus, WalletDetail.STATUS_已确认)
+                .set(WalletDetail::getTransactionTime, successTime)
+                .set(WalletDetail::getAmount, refundLog.getRefund())
+                .eq(WalletDetail::getId, walletDetail.getId()).update();
+    }
+
     /**
      * 处理退款
      *