localStorage.setItem('demoFake','1');location.reload() 即可在订单列表/退款单据/退款抽屉实地看到自动续费扣款、系统自动审批等示意数据;localStorage.removeItem('demoFake') 关闭。仅 [DEV-ONLY] localStorage 开关控制,不进正常流程。| refundType | 渠道 | 资金动作 | 审核后流向 | 退款上限 |
|---|---|---|---|---|
1 | 原路退回 | 泰融支付通道原路退 | 审核 → 自动完成(status=3) | 实收金额 |
2 | 撤销核销 | 团购通道撤销、恢复券 | 审核 → 自动完成(status=3) | 整笔 paidAmount |
3 | 财务打款 | 线下银行转账 | 审核 → 待打款(status=2)→ 财务回填OA → 完成 | 最终实收 = (实收−已退)×(1−费率) |
4 | 团购平台原路退款 | 无资金动作,仅标记+扣权益 | 审核 → 自动完成(status=3) | 团购实付金额 |
refundType 初值在 apply() 阶段由原订单 payType 推导,非用户随意选;自动审批后团购/线下单财务可在抽屉显式改选渠道(撤销核销 / 团购平台原路退款 / 手动打款)。权益扣减已迁移到 pay() 退款完成时(详见「退款自动审批工作流」一节);在此之上新增「申请即冻结」中间态——用户一提交退款(apply)即冻结对应权益(暂时停用会员卡/私教课,审核打款期间不可约课/核销/进场),驳回(status=-1)或用户撤销(status=-2)自动解冻恢复使用,退款打款完成(status=3)才正式扣减退出。
src/views/finance/refundList.vue(约 1448 行,合并自 origin/Release260309 后重构)+ 后端 RefundBusiness.java。
audit(): autoPayout = refundType==1 || refundType==2 || refundType==4 → 命中则 audit 内串接 pay(),status 直推 3 → refundType==4 成功文案「审核通过,已标记团购平台原路退款」 pay() : 新增 else-if payType==4 分支 —— 纯标记: setRefundType(4)/setStatus(3)/setStatusName("打款完成")/setPayTime 不走通道、不动 tuangou_order
4:'tgPlatform';channel-pill ch-4 绿色?refnum= / ?orderNumber= query 直达用户提交 / 后台发起的退款,命中条件时系统自动审批;并把权益扣减时机从 apply 阶段迁移到「退款最终完成(pay)」——钱未确认退之前一律不实际扣减。同时在 apply 阶段新增「申请即冻结」:用户一提交退款即冻结对应权益(暂时停用,审核打款期间不可约课/核销/进场),被运营驳回(status=-1)或用户撤销(status=-2)则自动解冻、恢复正常使用,退款打款完成(status=3)才正式扣减退出。
| 条件 | 判定 |
|---|---|
| 下单窗口内(按商品类型区分) 会员卡(时长卡/次数卡):48 小时内 私教(次数课/时长课):7 天内 | 会员卡:order.orderTime >= now - 48h私教: order.orderTime >= now - 7d |
| 本订单商品权益一次都未使用 | 会员卡:lian_face_record 在本卡权益有效期窗口(userRights.startTime~endTime)内签到数 = 0(新增 selectNumByPhoneInRange,替换原全局 selectNumByUserId);私教时长课:checkRightsIdScheduleCount(rightsId)=0;次数/体验课:times==remain |
apply() 新增「冻结权益」:用户一提交退款即冻结对应权益(暂时停用会员卡/私教课,审核打款期间不可约课/核销/进场);userCancle()(用户撤销 status=-2)与审核驳回(status=-1)阶段「解冻」恢复正常使用;实际扣减仍保留在 pay() 退款完成时(冻结只是新加在申请时的中间态,不改变扣减时机)apply() 新增 autoApprovable() 分支:refundType=1 原路退回 → 自动构造 AuditRefundOrderReq 调 audit()(其 autoPayout 串 pay() 完成);其余(refundType=2 撤销核销 / 4 团购平台原路退款 / 3 手动打款)→ 直接置 status=2 待打款+auditUser="系统自动审批"+同步订单 approveStatus,不串 pay、不实际扣减权益(已冻结),由财务在抽屉显式选定资金渠道(含团购平台原路退款)并确认refundMemCardOrder/refundPrivate 的 if(apply){…} 块移除,迁至 pay() 退款完成收敛点(订单置「已退款」处)统一 cancelMemberRightsByOrder/cancelRightsByOrderselectNumByPhoneInRange(phone,startTime,endTime);会员卡 nouse 改按本卡权益期精确判定列表与处理抽屉对 auditUser==='系统自动审批' 的单据展示绿色「系统自动审批」标识;自动审批后的团购/线下单(含团购平台原路退款)天然落入既有 status=2 财务「选渠道 + 确认」流,财务在抽屉选 撤销核销 / 团购平台原路退款 / 手动打款 之一确认,无需改流程。
apply)即冻结对应权益——暂时停用会员卡/私教课,审核与打款期间不可约课/核销/进场;被运营驳回(status=-1)或用户撤销(userCancle, status=-2)→自动解冻、恢复正常使用;退款打款完成(status=3)→ 权益正式扣减退出。实现上:apply() 新增冻结、userCancle()/驳回解冻、pay() 退款完成时的实际扣减(cancelMemberRightsByOrder/cancelRightsByOrder)保留不变。src/views/order/components/RefundDrawer.vue(约 800 行,新建/重构)— 从 order/member.vue & course.vue 的「退款」按钮唤起,渠道优先流。
setInterval + tickerSubs 多实例订阅,美团 10min / 抖音 60min 窗口,过期 disabledresidualValue → recommendedRefundable → cappedRecommended 全 computed 链路$emit('submit') mock,待接后端抽屉对该来源做统一处理:
OrderBusiness.refundAutoOrder()(微信代扣协议原路退,WxPayRefundRequest,按订单号代扣原路退),refundType=1;命中自动审批可 autoPayout 自动完成。团购渠道(撤销核销 / 团购平台原路退款)对自动续费不适用——抽屉两张团购卡 v-if="hasTuangou && !isAutoRenewal" 天然不出现;财务打款仅作原路退不可行时的人工兜底,不作为常规渠道。canOriginal = !!payNo && remainingPayable>0,自动续费若没有泰隆 payNo 会被禁用并默认落到「财务打款」——不对。已改为 remainingPayable>0 && (isAutoRenewal || !!payNo):自动续费走 refundAutoOrder 不依赖 payNo,恒可原路退;默认渠道对 isAutoRenewal 强制 original。原路退回卡标题/副标题显示「原路退回 · 自动续费代扣 / 微信代扣协议原路退」并打「自动续费扣款」标。discountSupplement(自动续约优惠补差 =(产品原价 − 续费价)按月累加)仅 isAutoRenewal 显示并计入 recommendedRefundable = 残值 − 违约金 − 优惠补差。form.terminateAutoRenew(默认 true),并入 buildPayload(连同 isAutoRenewal);确认弹窗按勾选给出「同时终止签约 / ⚠ 保留签约下期仍扣」文案。range),退款仅针对该笔周期订单,历史已消耗周期不退。RefundBusiness 退款链路未调用任何解约逻辑(解约在 RenewalBusiness),即退款不会终止签约 → 不解约就「退一笔又扣一笔」。需后端在退款完成时按前端传入的 terminateAutoRenew 调 RenewalBusiness 终止签约(后端先改不提交)。isAutoRenewal 订单显示。form.terminateAutoRenew(el-switch,解约 / 保留),默认 true(解约);reset()/applyModeDefaults() 均复位为 true。buildPayload(连同 isAutoRenewal);非自动续费单为 undefined 不下发。terminateAutoRenew 调 RenewalBusiness 终止签约(见上「后端缺口」,待接入)。// 残值:课时卡按比例;无单位数据兜底为剩余可退(要点②) residualValue = !totalUnits ? remainingPayable : round2(paidAmount * remainUnits / totalUnits) recommendedRefundable = max(0, residualValue - penaltyFee - discountSupplement) maxRefund = mode==='tuangou' ? paidAmount : min(channelMax, remainingPayable) cappedRecommended = min(recommendedRefundable, maxRefund) // 财务打款上限 = 最终实收(要点①) finalReceived = round2(max(0, paidAmount - alreadyRefunded) * (1 - feeRate)) feeRate = hasTuangou ? 0.08 : 0.006
前端口径(RefundDrawer.vue computed,统一展示用)
// 单位:会员时长卡=天 / 会员次数卡=次 / 私教=节 paidAmount(实收) = orderPrice ?? (totalPrice - discountPrice) remainingPayable = max(0, 实收 - alreadyRefunded(已退)) totalUnits(总单位) = orderDetail.times usedUnits(已用) = usedTimes ?? (totalUnits - surplusTimes) ?? 0 remainUnits(剩余) = max(0, totalUnits - usedUnits) unitPrice(单价) = round2(实收 / totalUnits) // 残值 residualValue = totalUnits==0 ? remainingPayable // 无单位数据兜底 : round2(实收 × remainUnits / totalUnits) // ≡ 单价 × 剩余 defaultPenaltyFee = round2(residualValue × 0.2) // 默认违约金20%,可手改
后端口径(RefundBusiness.java,落库权威,按 orderType + cardType/courseCoachType 分支)
| 类型 | 窗口内 且 未使用(会员卡 48 小时 / 私教 7 天) | 其他情况(残值 + 违约金) |
|---|---|---|
会员卡 · 时长卡 cardType=1 | 下单 48 小时内且未使用:全额,refundAmt=订单实收,服务费0,退款=总天数 | 残值 = 实收 × 剩余天数 / 总天数 剩余天数 = endTime − max(startTime, now) 服务费 = 残值 × 20%;refundAmt = 残值 − 服务费 |
会员卡 · 次数卡 cardType=2 | 下单 48 小时内且未使用:全额,refundAmt=订单实收,服务费0,退款=总次数 | 单价 = 实收 / 总次数 残值 = 单价 × 剩余次数;服务费 = 残值 × 20%;refundAmt = 残值 − 服务费 |
私教 · 次数课 courseCoachType=1 | 下单 7 天内且未使用:全额,refundAmt=订单实收,服务费0 | 单价 = 实收 / 总次数;7 天内:refundAmt = 单价 × 剩余次(不扣违约金);超 7 天:再扣 20% 违约金 |
私教 · 时长课 courseCoachType=2 | 下单 7 天内全额(且无任一已签到排课) | 按剩余时长比例折算 + 20% 违约金;当前订单有未签到排课则禁止退款(HAS_COURSE_NOT_ALLOW_REFUND) |
实收 × 剩余/总 精确折算用于展示与推荐;后端时长卡按整天比例落库(days/totalday),二者在跨天/边界可能有取整差,最终以后端 audit/pay 落库金额为准;违约金默认 20%、自动续费订单含「优惠补差」扣项,运营可在抽屉手动覆盖可退金额。channelMax() 的 offline 分支取 finalReceived(最终实收),label 为「最终实收(通道/平台扣费后)」,渠道卡副标题与上限提示按费率口径说明。与订单列表 computeFinalReceived 口径一致(团购 8% / 通道 0.6%)。
切换渠道 / 改违约金后可退金额需实时联动;订单无单位数据时也要给出合理默认值(非 0)。
① watch + manualOverride 标志 + $nextTick 的多重异步联动时序难控,故用 computed 取代。
② queryMemberOrderDetail 返回的 MemberOrderInfoRes 无 usedTimes/surplusTimes,时长卡/年卡 times 可空 → totalUnits=0,若残值硬返回 0 推荐值会塌成 0,故无单位数据时残值兜底为剩余可退。
// 重构:computed 驱动取代 watch/nextTick isManualRefundable() { return this.manualRefundable !== null } effectiveRefundable: { get() { return manualRefundable != null ? manualRefundable : cappedRecommended }, set(v){ const n=num(v); manualRefundable = |n-cappedRecommended|<0.001 ? null : n } } // 兜底:无单位数据 → 残值 = 剩余可退(实付−已退),而非 0 residualValue(){ if(!totalUnits) return this.remainingPayable; ... }
同步删除 syncRefundable/onRefundableChange/resetRefundable 方法、form.penaltyFee/discountSupplement 两个 watch、未用的 hasSettleAmount;applyModeDefaults/reset 改为只置 manualRefundable=null。
el-input-number 的 value watcher 是 immediate:true 且在 handler 内 $emit('input')。computed setter 已用「与 cappedRecommended 近似即视为回到自动态」消化该回灌,避免误标手动覆盖。业务语义:用户已在美团/抖音 App 自助退款且到账,本系统仅做登记标记 + 权益扣减,不发起任何实际退款。仅对团购订单(hasTuangou)开放。
| 层 | 改动 |
|---|---|
| 前端 RefundDrawer | 新增 mode='tgPlatform' 第 4 卡片、limit/提示文案、buildPayload 分支、确认弹窗文案 |
| 前端 refundList | refundTypeList 增 {4,团购平台原路退款}、modeKey、ch-4 pill、各处文案分支 |
| 前端 RefundDetailDrawer | 4 渠道卡(ch-4) |
| 样式 | refund-drawer.scss 增 icon-tgp / pill-tgPlatform(#f0f9eb / #67c23a 绿) |
| 后端 | RefundBusiness audit/pay 的 type==4 分支(见 §4,未提交) |
内部商品(会员卡 cardId / 私教课 courseId) 1 ── N 团购套餐 deal;团购核销发权益、撤销核销恢复券、refundType=4 扣权益均依赖此绑定。本次将绑定能力从分散的 groupCoupon/bingCoupon.vue 重构进 member/config.vue(门店配卡页),反向唯一:一个 dealId 只能绑一个内部商品。
// 内部商品(会员卡 cardId / 私教课 courseId) 1 ── N 团购绑定 binding = { channel:1美团点评|2抖音, dealId, dealGroupId, procuctType:1会员卡|2私教课, procuctId, procuctName } // 反向唯一:一个 dealId 只能绑一个内部商品 → boundDealInfo(dealId) 命中即提示「已绑定到 XX」
| 层 / 文件 | 改动 |
|---|---|
| member/config.vue | 去 el-table 改门店纵向面板;每店左右分列会员卡/私教,商品卡片化;卡片下 getCardBindings/getCourseBindings 渲染绑定 chip(binding-mt/binding-dy);openAddBinding 选商户 key→storeTuangouList 拉 deals→单选→tunagouBindProduct;deleteBinding→deleteTuangouProduct(二次确认);boundDealInfo 做反向唯一校验提示;loading 覆盖 stores+cards+courses+bindings 全量 |
| member/components/BatchBindDrawer.vue 新建 | 从 groupCoupon/bingCoupon.vue 移植的批量绑定抽屉,顶部「批量绑定团购」入口 |
| member/components/CardEditDialog.vue 新建 | 从 member/index.vue 抽出会员卡编辑弹窗,配卡页点卡片直接复用 |
| course/components/CourseEditDialog.vue 新建 | 从 course/index.vue 抽出私教课编辑弹窗,同上复用 |
| API | tunagouBindList / tunagouBindProduct / deleteTuangouProduct / storeTuangouList(@/api/mt)、tuangouKeyList(@/api/tk) |
| 路由 | 新增 router/modules/goods.js(+54);member.js/course.js 相应迁移 |
一对多绑定铺开后,门店配卡页的加载方式是前端 N×M 扇出 + 后端无批量聚合接口,数据量一上来查询会慢的要死:
loadAllStoreBindings():对当前分页每个门店 × 每个团购商户 key 各发一次 tunagouBindList(storeId,keyId) → 请求数 O(门店数 × key数);后端按 (storeId,keyId) 反复扫绑定表/团购表loadCoursesAndIndex():getCourseBatchs({pageSize:9999}) 全量拉课程,再逐课程 getBatchCourseCoachs/getBatchCourseShops 建「店→课」索引 → 典型 N+1,O(课程数) 次请求loadAllStoreCards() 同样全量拉所有卡,无服务端分页pageSize:9999 全量与逐课程 N+1;③ 绑定表对 (store_id, deal_id) / procuct_id 建索引。order/member.vue(会员卡订单 +1080 行重构)/ order/course.vue(私教订单 +1592 行重构):列表由 8+ 列收敛为 7 列,并配套新建 5 个抽屉/页面组件。
| 列 | 会员卡订单 member.vue | 私教订单 course.vue |
|---|---|---|
| 订单 | 订单号 + 渠道类型 tag(chlType,团购 warning 色);chlType=1 文案「正常下单」→「小程序下单」;付款来源单独区分:isAuto==1(自动续费签约扣款)渲染独立 success 标签「自动续费扣款」,不再混入「小程序下单」(其退款渠道由后端 payType==0 || isAuto==1 → refundType=1 原路退回);私教补录审核状态语义聚合进本列(方案2) | |
| 门店 / 商品 | 门店 / 会员卡 | 门店 / 课程 / 教练 |
| 用户 | displayUserName 昵称 + 用户 ID + 用户维度 tag(全新 / 复购 / 体验)集中本列;移除重复「体验课」tag;后端 list 已 join 真实昵称,移除 [est] 假角标 | |
| 金额 / 退款 | 退款金额并入本列 + 退款「详情」链接(viewRefund→RefundDetailDrawer);order_price 统一「应付」语义;「实付」语义修正(方案 A);新增 computeFinalPaid「最终实付」/ computeFinalReceived「最终实收」(团购 8% / 通道 0.6% 前端估算,带 est 角标),仅 orderStatus≥2 已支付订单展示 | |
| 状态 / 支付 | 原「支付」列改名「订单状态」;原「团购信息」列并入本列;团购单(chlType 4/5)给团购核销详情入口;支付/团购核销详情由 modal 统一重构为抽屉 viewPayment→OrderPaymentDrawer | |
| 属性 / 审核 | 属性 / 审核:财务打款(chlType=3)补录审核态(approveStatus 审核通过/待审核/未通过) | 课时 / 属性 |
| 操作 | 退款 / 删除 / 补录批量审核等按钮整理 | |
旧版来源语义混乱:chlType=1 笼统叫「正常下单」,把用户手动小程序下单和自动续费签约代扣混为一类;真实支付方式(payType)又和来源混在一起看不清。本次把「从哪来(来源)」「怎么付的(支付方式)」「订单类型」三个维度拆开、各自明确标识。
| 维度 | 取值 / 标识 | 说明 |
|---|---|---|
来源 chlType(+isAuto)「订单」列 chip | 1 小程序下单 | 原「正常下单」语义厘清为「小程序下单」(用户主动在小程序下单) |
isAuto==1 自动续费扣款 | 独立绿色 success 标,优先于 chlType=1;签约代扣产生,非用户手动下单 | |
2 外推码下单 | 外推码渠道 | |
3 补录订单 | 线下/非线上补录,配补录审核态聚合同列 | |
4 美团点评 / 5 抖音 | 团购来源,warning 色;并展示团购渠道与核销信息 | |
| — | 筛选「渠道类型」下拉与该枚举一致 | |
支付方式 payType | 隆泰 / 支付宝 / 微信 … | 真实收款通道,从来源中剥离,收进 OrderPaymentDrawer 支付详情抽屉;也是退款渠道(refundType)推导依据 |
订单类型 preSale | 预售卡 / 正式卡 | 独立 orderTypeText 标,与来源分开不再混淆 |
展示优化:来源/类型/订单号/补录审核态聚合进「订单」列同一处,色彩区分(团购 warning、自动续费 success、其余默认);支付方式与团购核销明细统一收进抽屉,列表只呈现「来源」这一眼判断维度。
chlType 4/5 分两种形态;原支付/团购详情 modal 重构为抽屉storeListRich 下拉补充门店地址等信息、券核销结果卡el-dialog → el-drawer + section 分组卡片化(共享 adjust-order-drawer.scss);支持补录 / 修改订单 / 补录批量审核| 文件 | 改动 |
|---|---|
RefundBusiness.java · audit/pay | audit() autoPayout 含 1/2/4 并串接 pay();pay() 新增 payType==4 纯标记分支;type=4 成功文案 |
RefundBusiness.java · apply() 本次 | 新增 autoApprovable() 自动审批分支:refundType=1 自动构造 AuditReq 串 audit→pay;2/3/4 置 status=2 待打款 + auditUser「系统自动审批」+ 同步原始订单 approveStatus;无理由窗口·权益未用判定(applyType 1/2 皆适用;窗口按商品类型区分:会员卡 orderTime>=now-48h / 私教 orderTime>=now-7d) |
RefundBusiness.java · 权益扣减迁移 本次 | 从 refundMemCardOrder/refundPrivate 的 apply 块移除 cancelMemberRightsByOrder/cancelRightsByOrder,迁至 pay() 退款完成收敛点统一扣减(钱确认退才扣);会员卡 nouse 改按本卡权益期精确判定 |
RefundBusiness.java · apply 冻结 / userCancle·驳回 解冻 本次 | 新增「申请即冻结权益」:apply() 阶段冻结对应会员卡/私教课(暂时停用,审核打款期间不可约课/核销/进场);userCancle()(用户撤销 status=-2)与审核驳回(status=-1)解冻恢复正常使用;实际扣减仍在 pay() 退款完成时(冻结只是新加在申请时的中间态,扣减时机不变)(先改不提交) |
LianerFaceRecordMapper(.java/.xml) 本次 | 新增 selectNumByPhoneInRange(phone,startTime,endTime),按本卡权益有效期窗口统计门禁签到(替代全局 selectNumByUserId) |
RenewalBusiness 解约 待接入 | 退款完成时须按前端 terminateAutoRenew 调 RenewalBusiness 终止自动续费签约(目前 RefundBusiness 链路未调,缺口见 ④/⑩) |
PrivateOrderBackImpl.java / MemberOrderBackImpl.java | list 接口 join 真实会员昵称(userBaseApi.getUsers(userIds)) |
MemberOrderListBackRes.java | 新增 private String userName; |
dev_gkbn/test2 分支但本地 master 缺失,已选择性 checkout + 补 cross-cutting 依赖(PaymentCodeAndMsg/UserRightsBusiness/OrderBusiness 等)至编译通过;财务退款单 UI 合并自 origin/Release260309 @72f0e60。线上 refund 表实际在用(约 1.9w 行)。$emit('submit') + message),主动退款发起接口待接入,需后端确认入参契约(mode/refundAmount/penaltyFee/discountSupplement/manualOverride…)。apply)即冻结对应权益,审核打款期间不可约课/核销/进场,被驳回(status=-1)或撤销(status=-2)自动解冻恢复使用。后端需在 apply 冻结、userCancle/驳回 解冻(pay 退款完成时的实际扣减保持不变)。boundDealInfo 提示,需后端在 tunagouBindProduct 做唯一约束兜底,否则并发/绕过前端会产生一 deal 多商品脏数据,核销发错权益。tunagouBindList + 课程/卡全量无分页,真实数据量下查询极慢,上线前需后端提供批量聚合绑定接口,否则页面不可用。terminateAutoRenew 在退款完成时调 RenewalBusiness 终止签约,否则退款后下个周期仍自动扣款。