diff --git a/api/proto/vas/proto/op.go b/api/proto/vas/proto/op.go index c70dde4a..85a49df4 100644 --- a/api/proto/vas/proto/op.go +++ b/api/proto/vas/proto/op.go @@ -46,3 +46,11 @@ type DealOneCoinOrderReq struct { type DealOneOrderReq struct { OrderId string `json:"order_id"` } + +type RefundOrderReq struct { + OrderId string `json:"order_id"` + Operator string `json:"operator"` +} + +type RefundOrderData struct { +} diff --git a/app/mix/controller/init.go b/app/mix/controller/init.go index d6633c5d..3eb8b115 100644 --- a/app/mix/controller/init.go +++ b/app/mix/controller/init.go @@ -232,6 +232,7 @@ func Init(r *gin.Engine) { opVasPayGroup := r.Group("/op/vas", PrepareOp()) opVasPayGroup.POST("create_order", middleware.JSONParamValidator(vasproto.OpCreateOrderReq{}), OpCreateOrder) opVasPayGroup.POST("coin_order_list", middleware.JSONParamValidator(vasproto.OpCoinOrderListReq{}), OpOrderList) + opVasPayGroup.POST("refund_order", middleware.JSONParamValidator(vasproto.RefundOrderReq{}), RefundOrder) // 验证码 opVeriCodeGroup := r.Group("/op/veri_code", PrepareOp()) diff --git a/app/mix/controller/vas.go b/app/mix/controller/vas.go index d17d13a5..5ddc3fe4 100644 --- a/app/mix/controller/vas.go +++ b/app/mix/controller/vas.go @@ -308,3 +308,19 @@ func DealOneOrder(ctx *gin.Context) { } ReplyOk(ctx, nil) } + +// 订单退款 +func RefundOrder(ctx *gin.Context) { + req := ctx.MustGet("client_req").(*vasproto.RefundOrderReq) + ec, err := service.DefaultService.RefundOrder(ctx, req) + if ec != errcode.ErrCodeVasSrvOk { + logger.Error("RefundOrder fail, req: %v, ec: %v", util.ToJson(req), ec) + if err != nil { + ReplyErrorMsg(ctx, err.Error()) + return + } + ReplyErrCodeMsg(ctx, ec) + return + } + ReplyOk(ctx, nil) +} diff --git a/app/mix/dao/mysql.go b/app/mix/dao/mysql.go index 052699bc..57a2aaeb 100644 --- a/app/mix/dao/mysql.go +++ b/app/mix/dao/mysql.go @@ -701,6 +701,21 @@ func (m *Mysql) GetUserVasMembershipUnlock(ctx *gin.Context, tx *sqlx.Tx, mid in return } +// 删除会员解锁记录 +func (m *Mysql) DeleteUserVasMembershipUnlock(ctx *gin.Context, tx *sqlx.Tx, mid int64) (err error) { + sqlStr := fmt.Sprintf("delete from %s where mid=?", TableVasUserMembershipUnlock) + if tx != nil { + _, err = tx.ExecContext(ctx, sqlStr, mid) + } else { + db := m.getDBVas() + _, err = db.ExecContext(ctx, sqlStr, mid) + } + if err != nil { + return + } + return +} + // 获取解锁记录 func (m *Mysql) GetUnlockWechatList(ctx *gin.Context, tx *sqlx.Tx, mid int64, offset, limit int) (list []*dbstruct.UserVasUnlock, err error) { list = make([]*dbstruct.UserVasUnlock, 0) @@ -751,6 +766,40 @@ func (m *Mysql) GetIncomeCHList(ctx *gin.Context, tx *sqlx.Tx, orderId string) ( return } +// 获取消费历史 +func (m *Mysql) GetCostCHList(ctx *gin.Context, tx *sqlx.Tx, orderId string) (list []*dbstruct.ConsumeHistory, err error) { + list = make([]*dbstruct.ConsumeHistory, 0) + tableName, err := m.ChTableName(&dbstruct.ConsumeHistory{Type: goproto.Int32(dbstruct.CHTypeCost)}) + sqlStr := fmt.Sprintf("select * from %s where order_id=?", tableName) + if tx != nil { + err = tx.SelectContext(ctx, &list, sqlStr, orderId) + } else { + db := m.getDBVas() + err = db.SelectContext(ctx, &list, sqlStr, orderId) + } + if err != nil { + return + } + return +} + +// 获取充值历史 +func (m *Mysql) GetChargeCHList(ctx *gin.Context, tx *sqlx.Tx, orderId string) (list []*dbstruct.ConsumeHistory, err error) { + list = make([]*dbstruct.ConsumeHistory, 0) + tableName, err := m.ChTableName(&dbstruct.ConsumeHistory{Type: goproto.Int32(dbstruct.CHTypeCharge)}) + sqlStr := fmt.Sprintf("select * from %s where order_id=?", tableName) + if tx != nil { + err = tx.SelectContext(ctx, &list, sqlStr, orderId) + } else { + db := m.getDBVas() + err = db.SelectContext(ctx, &list, sqlStr, orderId) + } + if err != nil { + return + } + return +} + // 获取订单 func (m *Mysql) GetOrderCountGroupByStatus(ctx *gin.Context, tx *sqlx.Tx, orderStatuses []int32, ctStart *int64, ctEnd *int64) (list []*dbstruct.VasOrderStatusCount, err error) { var sql strings.Builder @@ -988,7 +1037,7 @@ func (m *Mysql) DeleteSuccessXxlJobLogs(ctx *gin.Context, tx *sqlx.Tx, ids []int return } -// 创建提现订单 +// 创建提现钻石历史 func (m *Mysql) CreateWithdrawDiamondsHis(ctx *gin.Context, tx *sqlx.Tx, h *dbstruct.WithdrawDiamondsHis) error { var err error sqlStr := "insert into " + TableWithdrawDiamondsHis + diff --git a/app/mix/service/logic/vas.go b/app/mix/service/logic/vas.go index 4b9f8948..7a7e2a5e 100644 --- a/app/mix/service/logic/vas.go +++ b/app/mix/service/logic/vas.go @@ -2426,8 +2426,8 @@ func (v *Vas) UnlockMembership(ctx *gin.Context, mid int64, product *dbstruct.Pr } // 订单退款,只退充值 -func (v *Vas) RefundOrder(ctx *gin.Context, orderId string) error { - order, err := v.store.GetOrderById(ctx, nil, orderId) +func (v *Vas) RefundOrder(ctx *gin.Context, req *vasproto.RefundOrderReq) error { + order, err := v.store.GetOrderById(ctx, nil, req.OrderId) if err != nil { return err } @@ -2437,13 +2437,13 @@ func (v *Vas) RefundOrder(ctx *gin.Context, orderId string) error { switch order.GetProductId() { case dbstruct.ProductIdMembership: - + return v.refundMembership(ctx, order, req) + default: + return errors.New("invalid product") } - - return nil } -func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order) error { +func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order, req *vasproto.RefundOrderReq) error { switch order.GetOrderStatus() { case dbstruct.VasOrderStatusNone, dbstruct.VasOrderStatusInit: return errors.New("订单还未支付,无法退款") @@ -2468,11 +2468,11 @@ func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order) error { ctx, &dbstruct.OplogOrder{ OrderId: orderId, - Action: dbstruct.OrderOpLogActionAdd, + Action: dbstruct.OrderOpLogActionUpdate, Operator: req.Operator, - Detail: fmt.Sprintf("后台充值%d金币", coins), - BeforeStatus: dbstruct.VasOrderStatusNone, - AfterStatus: util.DerefInt32(order.OrderStatus), + Detail: fmt.Sprintf("会员退款"), + BeforeStatus: order.GetOrderStatus(), + AfterStatus: dbstruct.VasOrderStatusRefund, }, err, ) @@ -2488,9 +2488,75 @@ func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order) error { } }() + // 用户的消费退款记录 + costChList, err := v.store.GetCostCHList(ctx, tx, orderId) + if err != nil { + logger.Error("GetCostCHList fail, err: %v", err) + return err + } + if len(costChList) <= 0 || len(costChList) > 1 { + err = errors.New("invalid cost ch list") + logger.Error("invalid cost ch list, orderId: %v, err: %v", orderId, err) + return err + } + costCh := costChList[0] + costChNew := &dbstruct.ConsumeHistory{ + Mid: goproto.Int64(costCh.GetMid()), + Uid: goproto.Int64(costCh.GetUid()), + Did: goproto.String(costCh.GetDid()), + Type: goproto.Int32(costCh.GetType()), + SType: goproto.Int32(dbstruct.CHSTypeCostRefundMembership), + TypeId: goproto.String(costCh.GetTypeId()), + OrderId: goproto.String(costCh.GetOrderId()), + Change: goproto.Int64(-costCh.GetChange()), + Ct: goproto.Int64(time.Now().Unix()), + } + err = v.store.CreateConsumeHistory(ctx, tx, costChNew) + if err != nil { + logger.Error("CreateConsumeHistory fail, costChNew: %v, err: %v", util.ToJson(costChNew), err) + return err + } + + // 用户的充值退款记录 + chargeChList, err := v.store.GetChargeCHList(ctx, tx, orderId) + if err != nil { + logger.Error("GetChargeCHList fail, orderId: %v, err: %v", orderId, err) + return err + } + if len(chargeChList) <= 0 || len(chargeChList) > 1 { + err = errors.New("invalid charge ch list") + logger.Error("invalid charge ch list, orderId: %v, err: %v", orderId, err) + return err + } + chargeCh := chargeChList[0] + chargeChNew := &dbstruct.ConsumeHistory{ + Mid: goproto.Int64(chargeCh.GetMid()), + Uid: goproto.Int64(chargeCh.GetUid()), + Did: goproto.String(chargeCh.GetDid()), + Type: goproto.Int32(chargeCh.GetType()), + SType: goproto.Int32(dbstruct.CHSTypeChargeRefundMembership), + TypeId: goproto.String(chargeCh.GetTypeId()), + OrderId: goproto.String(chargeCh.GetOrderId()), + Change: goproto.Int64(-chargeCh.GetChange()), + Ct: goproto.Int64(time.Now().Unix()), + } + err = v.store.CreateConsumeHistory(ctx, tx, chargeChNew) + if err != nil { + logger.Error("CreateConsumeHistory fail, chargeChNew: %v, err: %v", util.ToJson(chargeChNew), err) + return err + } + // 修改订单状态 xx -> 已退款 err = v.store.UpdateOrderStatus(ctx, tx, orderId, order.GetOrderStatus(), dbstruct.VasOrderStatusRefund) if err != nil { + logger.Error("UpdateOrderStatus fail, err: %v", err) + return err + } + + // 删除会员解锁记录 + err = v.store.DeleteUserVasMembershipUnlock(ctx, tx, order.GetMid()) + if err != nil { + logger.Error("DeleteUserVasMembershipUnlock fail, mid: %v, err: %v", order.GetMid(), err) return err } @@ -2499,21 +2565,21 @@ func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order) error { if err != nil { return err } - chList := make([]*dbstruct.ConsumeHistory, 0) + incomeChList := make([]*dbstruct.ConsumeHistory, 0) for _, ch := range chListTmp { if ch.GetMid() == common.OfficialMid { continue } - chList = append(chList, ch) + incomeChList = append(incomeChList, ch) } - if len(chList) > 1 { + if len(incomeChList) > 1 { err = errors.New("收入记录错误,请找开发同学排查") return err } // 有分成的情况 - if len(chList) > 0 { - ch := chList[0] + if len(incomeChList) > 0 { + ch := incomeChList[0] uid := ch.GetUid() if uid <= 0 { err = errors.New("收入uid错误") @@ -2563,9 +2629,49 @@ func (v *Vas) refundMembership(ctx *gin.Context, order *dbstruct.Order) error { return err } + // 提现钻石记录 + wh := &dbstruct.WithdrawDiamondsHis{ + Mid: goproto.Int64(uid), + IncomeChId: goproto.Int64(ch.GetId()), + OrderId: goproto.String(ch.GetOrderId()), + Ct: goproto.Int64(time.Now().Unix()), + BeforeWithdrawDiamonds: goproto.Int64(wallet.GetWithdrawDiamonds()), + AfterWithdrawDiamonds: goproto.Int64(wallet.GetWithdrawDiamonds() - change), + Change: goproto.Int64(-change), + ProductId: goproto.String(order.GetProductId()), + } + err = v.store.CreateWithdrawDiamondsHis(ctx, tx, wh) + if err != nil { + logger.Error("CreateWithdrawDiamondsHis fail, wh: %v, err: %v", util.ToJson(wh), err) + return err + } } } - // 扣主播金币 - v.store.DecDiamonds(ctx, tx) + // 退款 + switch order.GetPayType() { + case vasproto.PayTypeAlipay, vasproto.PayTypeAlipayH5: + alipayCli := alipaycli.GetAlipayClientByAppId(order.GetOid3()) + resp, err := alipayCli.RefundOne(ctx, &alipaycli.RefundOneParam{ + OutTradeNo: orderId, + RefundAmount: order.GetPayAmount(), + RefundReason: "发货D退款", + }) + if err != nil { + logger.Error("alipayCli.RefundOne fail, orderId: %v, resp: %v, err: %v", orderId, util.ToJson(resp), err) + return err + } + case vasproto.PayTypeWxpayNative, vasproto.PayTypeWxpayJsapi, vasproto.PayTypeWxpayH5: + wxpayCli := wxpaycli.GetDefaultWxpayClient() + resp, err := wxpayCli.RefundOne(ctx, &wxpaycli.RefundOneParam{ + OutTradeNo: orderId, + RefundAmount: order.GetPayAmount(), + RefundReason: "发货D退款", + }) + if err != nil { + logger.Error("wxpayCli.RefundOne fail, orderId: %v, resp: %v, err: %v", orderId, util.ToJson(resp), err) + return err + } + } + return nil } diff --git a/app/mix/service/vasservice.go b/app/mix/service/vasservice.go index 102b2f3b..37d04d02 100644 --- a/app/mix/service/vasservice.go +++ b/app/mix/service/vasservice.go @@ -523,3 +523,14 @@ func (s *Service) DealOneOrder(ctx *gin.Context, req *vasproto.DealOneOrderReq) } return } + +func (s *Service) RefundOrder(ctx *gin.Context, req *vasproto.RefundOrderReq) (ec errcode.ErrCode, err error) { + ec = errcode.ErrCodeVasSrvOk + err = _DefaultVas.RefundOrder(ctx, req) + if err != nil { + logger.Error("RefundOrder fail, req: %v, err: %v", util.ToJson(req), err) + ec = errcode.ErrCodeVasSrvFail + return + } + return +} diff --git a/dbstruct/vas.sql b/dbstruct/vas.sql index 7f23c710..4b042c43 100644 --- a/dbstruct/vas.sql +++ b/dbstruct/vas.sql @@ -156,6 +156,7 @@ CREATE INDEX ix_mid ON vas_withdraw_diamonds_his (mid); CREATE INDEX ix_ct ON vas_withdraw_diamonds_his (ct); CREATE INDEX ix_order_id ON vas_withdraw_diamonds_his (order_id); CREATE INDEX ix_chid ON vas_withdraw_diamonds_his (income_ch_id); + CREATE TABLE `vas_user_membership_unlock` ( `id` bigint AUTO_INCREMENT COMMENT 'id', @@ -166,4 +167,5 @@ CREATE TABLE `vas_user_membership_unlock` `order_id` varchar(128) DEFAULT NULL COMMENT '订单id', PRIMARY KEY (`id`) ); -CREATE INDEX uni_idx_mid_product_id ON vas_user_unlock (mid, product_id); \ No newline at end of file +CREATE INDEX ix_mid_product_id ON vas_user_membership_unlock (mid, product_id); +CREATE INDEX ix_orderid ON vas_user_membership_unlock (order_id); \ No newline at end of file diff --git a/dbstruct/vas_mysql.go b/dbstruct/vas_mysql.go index f7aaa04e..17f4b21b 100644 --- a/dbstruct/vas_mysql.go +++ b/dbstruct/vas_mysql.go @@ -490,13 +490,15 @@ const ( CHTypeIncome = 3 // 收入明细(钻石) CHTypeWithdraw = 4 // 提现明细(钻石) - CHSTypeCostContact = 10001 // 消费明细,联系方式 - CHSTypeCostRefund = 10002 // 消费明细,金币退款 - CHSTypeCostMembership = 10003 // 消费明细,会员资格解锁(伪金币记录,会员资格解锁中间无转金币过程) + CHSTypeCostContact = 10001 // 消费明细,联系方式 + CHSTypeCostRefund = 10002 // 消费明细,金币退款 + CHSTypeCostMembership = 10003 // 消费明细,会员资格解锁(伪金币记录,会员资格解锁中间无转金币过程) + CHSTypeCostRefundMembership = 10004 // 消费明细,会员资格解锁退款(伪金币记录,会员资格解锁中间无转金币过程) - CHSTypeChargeUser = 20001 // 充值明细,用户自己冲 - CHSTypeChargeOp = 20002 // 充值明细,OP充值 - CHSTypeChargeRefund = 20003 // 充值明细,现金退款 + CHSTypeChargeUser = 20001 // 充值明细,用户自己冲 + CHSTypeChargeOp = 20002 // 充值明细,OP充值 + CHSTypeChargeRefund = 20003 // 充值明细,现金退款 + CHSTypeChargeRefundMembership = 20004 // 充值明细,会员退款 CHSTypeIncomeContact = 30001 // 收入明细,联系方式 CHSTypeIncomeInvite = 30002 // 收入明细,邀请分成 diff --git a/library/idgenerator/genid.go b/library/idgenerator/genid.go index 96c26126..3d491d71 100644 --- a/library/idgenerator/genid.go +++ b/library/idgenerator/genid.go @@ -47,6 +47,7 @@ const ( NodeDailyStatement // node 每日报表表 NodeWithdrawOrderId // node 提现订单 NodeAppConfig // node 应用配置表 + NodeWxpayRefund // node 微信支付退款 ) func GenIdInt64(node int64) (int64, error) { @@ -210,3 +211,9 @@ func GenAppConfigId() int64 { id, _ := GenIdInt64(NodeAppConfig) return id } + +// wxpay refund +func GenWxpayRefundId() string { + id, _ := GenIdString(NodeWxpayRefund) + return id +} diff --git a/library/payclients/wxpaycli/client.go b/library/payclients/wxpaycli/client.go index ebe1eed1..5608b49e 100644 --- a/library/payclients/wxpaycli/client.go +++ b/library/payclients/wxpaycli/client.go @@ -14,6 +14,7 @@ import ( "fmt" "net/http" "os" + "service/library/idgenerator" "time" "github.com/go-pay/gopay" @@ -286,3 +287,28 @@ func (c *WxpayClient) H5Pay(ctx context.Context, param *H5PayParam) (wxpayH5Para logger.Info("wxpayv3 H5 success, code: %v, error: %v, response: %v", resp.Code, resp.Error, util.ToJson(resp.Response)) return } + +// 退款 +type RefundOneParam struct { + OutTradeNo string // 商家订单id,我们自己的订单id + RefundAmount int64 // 退款金额,单位:分 + RefundReason string // 退款理由 +} + +func (c *WxpayClient) RefundOne(ctx context.Context, param *RefundOneParam) (resp *wxpayv3.RefundRsp, err error) { + bm := gopay.BodyMap{ + "out_trade_no": param.OutTradeNo, + "out_refund_no": idgenerator.GenWxpayRefundId(), + "reason": param.RefundReason, + "amount": gopay.BodyMap{ + "refund": param.RefundAmount, + "total": param.RefundAmount, + "currency": "CNY", + }, + } + resp, err = c.clientV3.V3Refund(ctx, bm) + if err != nil { + return + } + return +} diff --git a/library/payclients/wxpaycli/client_test.go b/library/payclients/wxpaycli/client_test.go index 28cba7e5..e8ac9b44 100644 --- a/library/payclients/wxpaycli/client_test.go +++ b/library/payclients/wxpaycli/client_test.go @@ -113,3 +113,17 @@ func TestWxpayClient_H5Pay(t *testing.T) { } t.Log(resp) } + +func TestWxpayClient_RefundOne(t *testing.T) { + cli := GetDefaultWxpayClient() + resp, err := cli.RefundOne(context.Background(), &RefundOneParam{ + OutTradeNo: "1767889021582716928", + RefundAmount: 100, + RefundReason: "发货D退款", + }) + if err != nil { + t.Log(err.Error()) + return + } + t.Log(util.ToJson(resp)) +}