1、商城订单详情、列表增加"退款时间"的字段;

2、商城订单退货时如果时明慧帐户则走河马付退款流程;
3、新增河马付退款接口;
This commit is contained in:
chenlin 2025-05-30 10:03:52 +08:00
parent bbbc61f857
commit 452a7d54d9
3 changed files with 247 additions and 18 deletions

View File

@ -82,12 +82,17 @@ 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`
HmPayMerchantId = "664403000030115"
TimeFormat = "2006-01-02 15:04:05"
clientIp = "39.108.188.218" // 小程序服务器
clientIpDev = "112.33.14.191" // 移动云服务器
// HmPayMerchantId 明慧帐户
HmPayMerchantId = "664403000030115"
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"
HmPubKeySwitch = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDA4g8VFWIxEbOzxYC8ZIOgaOsLWK4Y5k9D8GwJ1Gige79LbTxbe3PH12KMc59DpCR1PnIDwlYWjIE7mZZAHgImXs0pSFihvlNS9srWk2uPlEXXQjjIZ3mnPoXtNhU0x5cYdkB8jtijcYMSGwKmdrIvpvPX3MrDKOX6dJ1T4ll+QIDAQAB"
@ -520,8 +525,15 @@ func TransactionOrderRefund(orderRefund OrderRefund, accountNum uint32) error {
if err != nil {
// 处理错误
log.Printf("call Create err:%s", 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
}

View File

@ -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)
}

View File

@ -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"
@ -340,6 +342,7 @@ type GoodsOrder struct {
RefundExpressNo string `json:"refund_express_no"` // 退货物流单号
RefundReason string `json:"refund_reason" gorm:"type:text;"`
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,12 +1378,63 @@ func (r *GoodsOrderRefundSendReceiveReq) Receive() error {
}
if goodsOrder.Rm != 0 { // 如果有支付,则走退款流程
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
}
}
}
// 更新订单为"已退货"状态
err = begin.Table("goods_order").Where("order_id=?", r.OrderId).Update("state", GoodsOrderStateRefunded).Error
@ -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