diff --git a/app/admin/apis/pay/wx_pay.go b/app/admin/apis/pay/wx_pay.go index 925eb3d..1b0d4ca 100644 --- a/app/admin/apis/pay/wx_pay.go +++ b/app/admin/apis/pay/wx_pay.go @@ -82,11 +82,16 @@ const ( HmPayApiUrl = "https://hmpay.sandpay.com.cn/gateway/api" PemBegin = "-----BEGIN RSA PRIVATE KEY-----\n" PemEnd = "\n-----END RSA PRIVATE KEY-----" - HmPubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDzGVH0Fxpb2M48U1BWr6lpNs2W3VHqtjO8X5RqWjtTwpQVKo8dqaiAGxVbsdnefPpsbI5l9rKquRAOJhWFU07hxSUgXZOk55QQmll03MBgRDXLgxyKfycLLQwhsCJAzDIWC7IWgok/RHV9m9AV2GbQxWBl+7iDE4prcbpgG8Z0HwIDAQAB` + + TimeFormat = "2006-01-02 15:04:05" + clientIp = "39.108.188.218" // 小程序服务器 + clientIpDev = "112.33.14.191" // 移动云服务器 + + // HmPayMerchantId 明慧帐户 HmPayMerchantId = "664403000030115" - TimeFormat = "2006-01-02 15:04:05" - clientIp = "39.108.188.218" // 小程序服务器 - clientIpDev = "112.33.14.191" // 移动云服务器 + HmPubKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDzGVH0Fxpb2M48U1BWr6lpNs2W3VHqtjO8X5RqWjtTwpQVKo8dqaiAGxVbsdnefPpsbI5l9rKquRAOJhWFU07hxSUgXZOk55QQmll03MBgRDXLgxyKfycLLQwhsCJAzDIWC7IWgok/RHV9m9AV2GbQxWBl+7iDE4prcbpgG8Z0HwIDAQAB` + HmPubKeyFp = "./config/hm_pay/private_key.pem" + TestHmPubKeyFp = "/Users/max/Documents/code/deovo/mh_goadmin_server/config/hm_pay/private_key.pem" // HmPayMerchantIdSwitch 任天堂项目-对私账户(密钥20240701203620) HmPayMerchantIdSwitch = "664403000021193" @@ -520,8 +525,15 @@ func TransactionOrderRefund(orderRefund OrderRefund, accountNum uint32) error { if err != nil { // 处理错误 - log.Printf("call Create err:%s", err) - return err + var apiErr *core.APIError + if errors.As(err, &apiErr) { + log.Printf("call Create err: %s", apiErr.Message) + return fmt.Errorf(apiErr.Message) + } else { + // 不是 APIError 类型,打印默认错误 + log.Printf("call Create err: %s", err.Error()) + return err + } } else { // 处理返回结果 log.Printf("status=%d resp=%s", result.Response.StatusCode, resp) @@ -696,6 +708,42 @@ type HmPayTradeCancelResp struct { ReqReserved string `json:"req_reserved"` // 商户自定义字段 } +// HmPayTradeRefundContent 订单退款 trade.refund 业务请求参数 +type HmPayTradeRefundContent struct { + OrderCreateTime string `json:"order_create_time,omitempty"` // 订单创建时间,格式:yyyyMMddHHmmss(可选,但与 out_order_no 联动必填) + OutOrderNo string `json:"out_order_no,omitempty"` // 商户订单号,三选一必填之一 + PlatTrxNo string `json:"plat_trx_no,omitempty"` // 平台交易流水号,三选一必填之一 + BankOrderNo string `json:"bank_order_no,omitempty"` // 银行订单号,三选一必填之一 + RefundAmount float64 `json:"refund_amount"` // 退款金额,单位元 + RefundRequestNo string `json:"refund_request_no"` // 商户退款请求号,同一订单退款不可重复 +} + +// HmPayTradeRefundResp 订单退款 trade.refund 业务响应参数 +type HmPayTradeRefundResp struct { + SubCode string `json:"sub_code"` // 业务返回码 + SubMsg string `json:"sub_msg"` // 业务返回信息 + RefundAmount float64 `json:"refund_amount"` // 退款金额 + OutOrderNo string `json:"out_order_no"` // 商户订单号 + PlatTrxNo string `json:"plat_trx_no"` // 平台交易流水号 + BankOrderNo string `json:"bank_order_no"` // 银行订单号 + BankTrxNo string `json:"bank_trx_no"` // 银行流水号 + PayWayCode string `json:"pay_way_code"` // 支付方式 + BuyerID string `json:"buyer_id"` // 买家ID + RefundRequestNo string `json:"refund_request_no"` // 商户退款请求号 + RefundPlatTrxNo string `json:"refund_plat_trx_no"` // 退款平台流水号 + RefundBankOrderNo string `json:"refund_bank_order_no"` // 退款银行订单号 + RefundBankTrxNo string `json:"refund_bank_trx_no,omitempty"` // 退款银行流水号(可选) + ReqReserved string `json:"req_reserved,omitempty"` // 自定义字段(可选) + DiscountDetail *DiscountDetail `json:"discount_detail,omitempty"` // 优惠详情(可选) +} + +// DiscountDetail 示例优惠详情结构体(根据业务自行扩展) +type DiscountDetail struct { + // 可根据具体业务定义字段结构 + DiscountType string `json:"discount_type,omitempty"` // 示例:优惠类型 + DiscountAmount float64 `json:"discount_amount,omitempty"` // 示例:优惠金额 +} + type HmPayUnifiedOrderPayData struct { TimeStamp string `json:"timeStamp"` Package string `json:"package"` @@ -1086,6 +1134,12 @@ func getStoreKeyConfig(storeId uint32) StoreKeyConfig { PubKey: HmPubKeyJBL, } } + case 100001: // 明慧帐户-小程序商城 + config = StoreKeyConfig{ + AppID: HmPayMerchantId, + FP: HmPubKeyFp, + PubKey: HmPubKey, + } } return config @@ -1264,9 +1318,10 @@ func HmCancelOrder(orderId string, storeId uint32) (*HmPayTradeCancelResp, error now := time.Now().Local() nonce := GenRandStr(NonceStringLength) + storeKey := getStoreKeyConfig(storeId) unifiedOrderReq := HmJsPayUnifiedOrderReq{} publicPara := HmPayPublicPara{ - AppId: HmPayMerchantIdSwitch, + AppId: storeKey.AppID, Method: "trade.cancel", SignType: "RSA", Sign: "", @@ -1326,3 +1381,75 @@ func HmCancelOrder(orderId string, storeId uint32) (*HmPayTradeCancelResp, error return &hmPayDetail, nil } + +// HmRefundOrder 订单退款 +func HmRefundOrder(req HmPayTradeRefundContent, storeId uint32) (*HmPayTradeRefundResp, error) { + now := time.Now().Local() + nonce := GenRandStr(NonceStringLength) + + storeKey := getStoreKeyConfig(storeId) + unifiedOrderReq := HmJsPayUnifiedOrderReq{} + publicPara := HmPayPublicPara{ + AppId: storeKey.AppID, + Method: "trade.refund", + SignType: "RSA", + Sign: "", + Timestamp: now.Format(TimeFormat), + Nonce: nonce, + } + + biz := HmPayTradeRefundContent{ + OutOrderNo: req.OutOrderNo, + PlatTrxNo: req.PlatTrxNo, + RefundAmount: req.RefundAmount, + RefundRequestNo: req.RefundRequestNo, + } + unifiedOrderReq.HmPayPublicPara = publicPara + + bizString, err := json.Marshal(&biz) + if err != nil { + logger.Error("marshal biz err:", logger.Field("err", err)) + return nil, err + } + unifiedOrderReq.HmPayPublicPara.BizContent = string(bizString) + m, err := struct2Map(unifiedOrderReq) + if err != nil { + logger.Error("HmJsPayUnifiedOrder struct2Map err:", logger.Field("err", err)) + return nil, err + } + + sign, err := GenHmPaySignDeovo(storeId, m) + if err != nil { + logger.Error("HmJsPayUnifiedOrder GenHmPaySign err:", logger.Field("err", err)) + return nil, err + } + unifiedOrderReq.Sign = sign + + unifiedOrderResp, err := HmPayUnifiedOrder(unifiedOrderReq) + if err != nil { + logger.Errorf("WxUnifiedOrder unified order error %#v", err) + return nil, err + } + + signContent, err := ToSignContent(unifiedOrderResp) + if err != nil { + logger.Errorf("ToSignContent err:", err) + return nil, err + } + err = HmVerifySha1RsaDeovo(storeId, signContent, unifiedOrderResp.Sign) + if err != nil { + logger.Errorf("HmVerifySha1Rsa err:", err) + return nil, err + } + fmt.Println("Resp Data:", unifiedOrderResp.Data) + + var hmPayDetail HmPayTradeRefundResp + err = json.Unmarshal([]byte(unifiedOrderResp.Data), &hmPayDetail) + if err != nil { + logger.Errorf("hm pay unified order pay data unmarshal error %#v", err) + return nil, err + } + fmt.Println("hmPayDetail:", hmPayDetail) + + return &hmPayDetail, nil +} diff --git a/app/admin/apis/pay/wx_pay_test.go b/app/admin/apis/pay/wx_pay_test.go index cb887ef..8bbbb75 100644 --- a/app/admin/apis/pay/wx_pay_test.go +++ b/app/admin/apis/pay/wx_pay_test.go @@ -53,9 +53,26 @@ func TestHmQueryOrder(t *testing.T) { // 撤销 func TestHmCancelOrder(t *testing.T) { - orderId := "sale2023122225067733" + orderId := "CF56C27AE0" - order, err := HmCancelOrder(orderId, 13) + order, err := HmCancelOrder(orderId, 100001) + if err != nil { + fmt.Println("err:", err) + } + + fmt.Println("order:", order) +} + +func TestHmRefundOrder(t *testing.T) { + + testReq := HmPayTradeRefundContent{ + OutOrderNo: "CF56C27AE0", + BankOrderNo: "HMP2505270003044236901990400", + RefundAmount: 0.11, + RefundRequestNo: "R202505291545301234567890", + } + + order, err := HmRefundOrder(testReq, 100001) if err != nil { fmt.Println("err:", err) } diff --git a/app/admin/models/mall.go b/app/admin/models/mall.go index 189d962..1119b83 100644 --- a/app/admin/models/mall.go +++ b/app/admin/models/mall.go @@ -7,12 +7,14 @@ import ( "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "github.com/xuri/excelize/v2" + "go-admin/app/admin/apis/pay" utils "go-admin/app/admin/models/tools" orm "go-admin/common/global" "go-admin/logger" "go-admin/tools" "go-admin/tools/config" "gorm.io/gorm" + "math/rand" "sort" "strconv" "strings" @@ -339,7 +341,8 @@ type GoodsOrder struct { RefundExpressCompanyNo string `json:"refund_express_company_no"` // 退货物流公司编号 RefundExpressNo string `json:"refund_express_no"` // 退货物流单号 RefundReason string `json:"refund_reason" gorm:"type:text;"` - ReceivedTime time.Time `json:"received_time"` // 签收时间 + ReceivedTime time.Time `json:"received_time"` // 签收时间 + RefundTime time.Time `json:"refund_time" gorm:"-"` // 退货时间 VersionId uint64 `json:"version_id"` // 乐观锁 @@ -635,6 +638,9 @@ func (m *GoodsOrderDetailReq) OrderDetail() (*GoodsOrder, error) { } goods.Attributes = attributes order.Goods = &goods + if order.State == GoodsOrderStateRefunded { // 已退货状态 + order.RefundTime = order.UpdatedAt + } var userAddress UserAddress err = orm.Eloquent.Table("user_address").Where("id=?", order.AddressId).Find(&userAddress).Error @@ -732,6 +738,9 @@ func GoodsOrderListSetGoods(orders []GoodsOrder) { ids := make([]uint32, 0, len(orders)) for i, _ := range orders { ids = append(ids, orders[i].GoodsId) + if orders[i].State == GoodsOrderStateRefunded { // 已退货状态 + orders[i].RefundTime = orders[i].UpdatedAt + } } goodsMap := GetGoodsMapByIds(ids) for i, _ := range orders { @@ -1369,10 +1378,61 @@ func (r *GoodsOrderRefundSendReceiveReq) Receive() error { } if goodsOrder.Rm != 0 { // 如果有支付,则走退款流程 - err = memberRecord.MallGoodsOrderRefund(outTradeNo, goods.GoodsAccountNum) - if err != nil { - log.Error().Msgf("order refund err:%#v", err) - return err + if goods.GoodsAccountNum == 1 { // 明慧,默认使用杉德河马付 + // 记录UserOpenMemberRecord + memberRecord.Uid = goodsOrder.Uid + memberRecord.OrderId = goodsOrder.ID + err = orm.Eloquent.Create(memberRecord).Error + if err != nil { + logger.Error("insert user open member record err:", logger.Field("err", err)) + return err + } + + // 查询付款信息 + var fundRecord FundRecord + err = orm.Eloquent.Table("fund_record").Where("out_trade_no=?", outTradeNo).Find(&fundRecord).Error + if err != nil || fundRecord.ID == 0 { + log.Error().Msgf("get fund_record err:%#v", err) + return errors.New("未查询到付款信息") + } + + refundReq := pay.HmPayTradeRefundContent{ + OutOrderNo: outTradeNo, + BankOrderNo: fundRecord.TransactionId, + RefundAmount: float64(fundRecord.Amount) / 100.0, + RefundRequestNo: GenerateRefundOrderNo(), + } + resp, err := pay.HmRefundOrder(refundReq, 100001) + if err != nil { + log.Error().Msgf("order refund err:%#v", err) + return err + } + if resp.SubCode != "REFUND_SUCCESS" { + log.Error().Msg("河马付退款失败") + return errors.New(resp.SubMsg) + } else { // 退款成功,记录订单信息 + refundRecord := &FundRecord{ + Uid: fundRecord.Uid, + FundType: "buy_goods_refund", + Amount: int64(resp.RefundAmount*100) * (-1), + TransactionId: resp.BankOrderNo, + OutTradeNo: resp.OutOrderNo, + RefundId: resp.RefundBankTrxNo, + Status: 2, + Remark: "商城购买退货", + } + err = orm.Eloquent.Create(refundRecord).Error + if err != nil { + logger.Errorf("create fund record err:", err.Error()) + return err + } + } + } else { + err = memberRecord.MallGoodsOrderRefund(outTradeNo, goods.GoodsAccountNum) + if err != nil { + log.Error().Msgf("order refund err:%#v", err) + return err + } } } @@ -1426,6 +1486,23 @@ func (r *GoodsOrderRefundSendReceiveReq) Receive() error { return nil } +// GenerateRefundOrderNo 生成退款订单号(例如:R202505271545301234567890) +func GenerateRefundOrderNo() string { + // 设置随机种子 + rand.Seed(time.Now().UnixNano()) + + // 当前时间:年月日时分秒(14位) + timestamp := time.Now().Format("20060102150405") + + // 生成 10 位随机数:范围是 1000000000 ~ 9999999999 + randomNum := rand.Int63n(9000000000) + 1000000000 // int63 支持更大范围 + + // 拼接最终退款订单号 + refundOrderNo := fmt.Sprintf("R%s%d", timestamp, randomNum) + + return refundOrderNo +} + func GetWxPayExpressFeeRefundRecord(orderId uint32) (string, error) { var openMemberRecord UserOpenMemberRecord err := orm.Eloquent.Table("user_open_member_record").Where("order_id=?", orderId). @@ -1441,11 +1518,19 @@ func OrderUpdateGoodsStockBack(attributeId, count uint32, gdb *gorm.DB) error { if gdb == nil { gdb = orm.Eloquent } - sql := fmt.Sprintf("UPDATE goods_attribute SET stock=stock+%d,sold_count=sold_count-%d WHERE id=%d ", - count, count, attributeId) - err := gdb.Exec(sql).Error + + // 防止 sold_count 小于 count,避免变成负数 + sql := ` + UPDATE goods_attribute + SET stock = stock + ?, + sold_count = sold_count - ? + WHERE id = ? + AND sold_count >= ? + ` + + err := gdb.Exec(sql, count, count, attributeId, count).Error if err != nil { - logger.Errorf("err:", err) + logger.Errorf("OrderUpdateGoodsStockBack error: %v", err) return err } return nil