package mysqldao import ( "context" "testing" "github.com/DATA-DOG/go-sqlmock" ) // TestRefundApplyWithOrderStatusCreatesFlowAndMarksRefunding 受理成功:订单 2 已支付 -> 6 申请退款中,并新建退款流水。 func TestRefundApplyWithOrderStatusCreatesFlowAndMarksRefunding(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &RefundFlowLogDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount"}). AddRow(8, "ORDER-1", 7, 2, 990)) mock.ExpectQuery("SELECT .* FROM `refund_flow_logs`"). WillReturnRows(sqlmock.NewRows([]string{"id"})) mock.ExpectExec("INSERT INTO `refund_flow_logs`").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("UPDATE `orders` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() flow, allowed, err := dao.RefundApplyWithOrderStatus(context.Background(), "ORDER-1", "RF-NEW", "不想要了", 1234) if err != nil || !allowed { t.Fatalf("allowed=%v err=%v", allowed, err) } if flow == nil || flow.RefundNo != "RF-NEW" || flow.Status != 0 || flow.RefundAmount != 990 || flow.UserID != 7 { t.Fatalf("unexpected flow: %+v", flow) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestRefundApplyWithOrderStatusRejectsNonPaidOrder 订单已处于申请退款中(6)时拒绝受理且不写任何数据。 func TestRefundApplyWithOrderStatusRejectsNonPaidOrder(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &RefundFlowLogDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}). AddRow(8, "ORDER-1", 7, 6)) mock.ExpectCommit() flow, allowed, err := dao.RefundApplyWithOrderStatus(context.Background(), "ORDER-1", "RF-NEW", "重复申请", 1234) if err != nil || allowed || flow != nil { t.Fatalf("allowed=%v flow=%v err=%v", allowed, flow, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestRefundApplyWithOrderStatusReusesFailedFlow 上次退款失败:复用旧流水并换新退款单号、重置为待处理。 func TestRefundApplyWithOrderStatusReusesFailedFlow(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &RefundFlowLogDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_amount"}). AddRow(8, "ORDER-1", 7, 2, 990)) mock.ExpectQuery("SELECT .* FROM `refund_flow_logs`"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "refund_no", "status"}). AddRow(5, "ORDER-1", 7, "RF-OLD", 3)) mock.ExpectExec("UPDATE `refund_flow_logs` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE `orders` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() flow, allowed, err := dao.RefundApplyWithOrderStatus(context.Background(), "ORDER-1", "RF-NEW", "再次申请", 2222) if err != nil || !allowed { t.Fatalf("allowed=%v err=%v", allowed, err) } if flow == nil || flow.ID != 5 || flow.RefundNo != "RF-NEW" || flow.Status != 0 || flow.ApplyTime != 2222 { t.Fatalf("unexpected flow: %+v", flow) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestRefundApplyWithOrderStatusRejectsInProgressFlow 已有待处理/退款中/成功的流水时拒绝重复申请。 func TestRefundApplyWithOrderStatusRejectsInProgressFlow(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &RefundFlowLogDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}). AddRow(8, "ORDER-1", 7, 2)) mock.ExpectQuery("SELECT .* FROM `refund_flow_logs`"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "refund_no", "status"}). AddRow(5, "ORDER-1", "RF-OLD", 0)) mock.ExpectCommit() flow, allowed, err := dao.RefundApplyWithOrderStatus(context.Background(), "ORDER-1", "RF-NEW", "重复申请", 1234) if err != nil || allowed || flow != nil { t.Fatalf("allowed=%v flow=%v err=%v", allowed, flow, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestOrderStatusRevertRefundingToPaid 退款失败回退依赖的条件流转:6 -> 2 仅在订单仍为 6 时生效。 func TestOrderStatusRevertRefundingToPaid(t *testing.T) { for _, tc := range []struct { name string rows int64 want bool }{ {name: "still_refunding", rows: 1, want: true}, {name: "already_settled", rows: 0, want: false}, } { t.Run(tc.name, func(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &OrderDao{db: base.db} // gorm 默认把单条写语句包在事务里执行。 mock.ExpectBegin() mock.ExpectExec("UPDATE `orders` SET .* WHERE \\(order_no = \\? AND order_status = \\?\\).*"). WillReturnResult(sqlmock.NewResult(0, tc.rows)) mock.ExpectCommit() ok, err := dao.OrderUpdateStatusByOrderNo(context.Background(), "ORDER-1", 6, 2) if err != nil || ok != tc.want { t.Fatalf("ok=%v err=%v, want %v", ok, err, tc.want) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } // TestOrderListVirtualRefunding 退款查单兜底扫描:仅取「虚拟支付 + 申请退款中 + updated_at 早于水位」的订单。 func TestOrderListVirtualRefunding(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &OrderDao{db: base.db} mock.ExpectQuery("SELECT .* FROM `orders` WHERE .*pay_channel = \\? AND order_status = \\? AND updated_at < \\?.*"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status", "pay_channel", "updated_at"}). AddRow(8, "ORDER-1", 7, 6, 2, 900)) orders, err := dao.OrderListVirtualRefunding(context.Background(), 1000, 50) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(orders) != 1 || orders[0].OrderNo != "ORDER-1" || orders[0].OrderStatus != 6 { t.Fatalf("unexpected orders: %+v", orders) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestRefundSettlementAcceptsRefundingOrder 退款成功结算放行状态 6,并按「权益是否已发放」决定是否创建回收任务。 func TestRefundSettlementAcceptsRefundingOrder(t *testing.T) { for _, tc := range []struct { name string orderStatus int8 wantRevoke bool }{ {name: "refunding_creates_revoke_task", orderStatus: 6, wantRevoke: true}, {name: "cancelled_paid_skips_revoke_task", orderStatus: 3, wantRevoke: false}, } { t.Run(tc.name, func(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &BenefitRevokeTaskDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}). AddRow(8, "ORDER-1", 7, tc.orderStatus)) mock.ExpectExec("UPDATE `orders` SET").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE `benefit_grant_tasks` SET").WillReturnResult(sqlmock.NewResult(0, 2)) if tc.wantRevoke { mock.ExpectExec("INSERT INTO `benefit_revoke_tasks`").WillReturnResult(sqlmock.NewResult(3, 1)) } mock.ExpectCommit() settled, err := dao.RefundSettleWithRevokeTask(context.Background(), "ORDER-1", 990, 1234) if err != nil || !settled { t.Fatalf("settled=%v err=%v", settled, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } // TestRefundSettlementSkipsAlreadyRefunded 已退款订单重复回调直接跳过(幂等,不重复回退权益)。 func TestRefundSettlementSkipsAlreadyRefunded(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &BenefitRevokeTaskDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}). AddRow(8, "ORDER-1", 7, 5)) mock.ExpectCommit() settled, err := dao.RefundSettleWithRevokeTask(context.Background(), "ORDER-1", 990, 1234) if err != nil || settled { t.Fatalf("settled=%v err=%v", settled, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // TestRefundSettlementRejectsExpiredOrder 未支付流转而来的终态(如已过期 4)不允许结算为已退款。 func TestRefundSettlementRejectsExpiredOrder(t *testing.T) { base, mock := newQuotaTestDAO(t) dao := &BenefitRevokeTaskDao{db: base.db} mock.ExpectBegin() mock.ExpectQuery("SELECT .* FROM `orders` .*FOR UPDATE"). WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "user_id", "order_status"}). AddRow(8, "ORDER-1", 7, 4)) mock.ExpectCommit() settled, err := dao.RefundSettleWithRevokeTask(context.Background(), "ORDER-1", 990, 1234) if err != nil || settled { t.Fatalf("settled=%v err=%v", settled, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }