This commit is contained in:
李子铭 2025-03-06 09:52:25 +08:00
parent 9b8823cc29
commit 9851c1f61b
6 changed files with 330 additions and 67 deletions

View File

@ -6,5 +6,7 @@ import (
) )
type CmbMixRepo interface { type CmbMixRepo interface {
OrderVerify(ctx context.Context, req *v1.CmbRequest, funcName string) (*v1.CmbOrderRequest, error)
ProductQueryVerify(ctx context.Context, req *v1.CmbRequest, funcName string) (*v1.CmbQueryProductRequest, error)
GetRequestData(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) GetRequestData(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error)
} }

View File

@ -2,6 +2,8 @@ package mixrepoimpl
import ( import (
"context" "context"
"encoding/json"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -20,28 +22,59 @@ func NewCmbMixRepoImpl(bc *conf.Bootstrap) mixrepos.CmbMixRepo {
return &CmbMixRepoImpl{bc: bc} return &CmbMixRepoImpl{bc: bc}
} }
func (s *CmbMixRepoImpl) GetRequestData(_ context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) { func (s *CmbMixRepoImpl) OrderVerify(ctx context.Context, req *v1.CmbRequest, funcName string) (*v1.CmbOrderRequest, error) {
// 我们的sm2 公钥加密 bizStr, err := s.Verify(ctx, req, funcName)
// 请求到我们这边 使用 我们的私钥解密
encryptBody, err := cmb.Encrypt(s.bc.Cmb.Sm2Puk, bizJsonStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
date := time.Now().Format("20060102150405") var resp *v1.CmbOrderRequest
if err = json.Unmarshal([]byte(bizStr), &resp); err != nil {
reply := &v1.CmbRequest{ return nil, err
Mid: s.bc.Cmb.Mid,
Aid: s.bc.Cmb.Aid,
Date: date,
Random: string(cmb.RandomBytes(16)),
KeyAlias: s.bc.Cmb.KeyAlias,
CmbKeyAlias: s.bc.Cmb.CmbKeyAlias,
EncryptBody: encryptBody,
Sign: "",
} }
kvRows := helper.SortStructFieldsByKey(reply) return resp, nil
}
func (s *CmbMixRepoImpl) ProductQueryVerify(ctx context.Context, req *v1.CmbRequest, funcName string) (*v1.CmbQueryProductRequest, error) {
bizStr, err := s.Verify(ctx, req, funcName)
if err != nil {
return nil, err
}
var resp *v1.CmbQueryProductRequest
if err = json.Unmarshal([]byte(bizStr), &resp); err != nil {
return nil, err
}
return resp, nil
}
func (s *CmbMixRepoImpl) Verify(ctx context.Context, req *v1.CmbRequest, funcName string) (string, error) {
str, err := s.getSignStr(ctx, req, funcName)
if err != nil {
return "", err
}
b, err := cmb.Verify(s.bc.Cmb.CmbSm2Puk, str, req.Sign)
if err != nil {
return "", err
}
if !b {
return "", errors.New("签名验证失败")
}
bizStr, err := cmb.Decrypt(s.bc.Cmb.Sm2Prk, req.EncryptBody)
if err != nil {
return "", err
}
return bizStr, nil
}
func (s *CmbMixRepoImpl) getSignStr(_ context.Context, req *v1.CmbRequest, funcName string) (string, error) {
kvRows := helper.SortStructFieldsByKey(req)
var strToBeSigned strings.Builder var strToBeSigned strings.Builder
for _, kv := range kvRows { for _, kv := range kvRows {
@ -54,15 +87,75 @@ func (s *CmbMixRepoImpl) GetRequestData(_ context.Context, funcName, bizJsonStr
strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value))
} }
str := fmt.Sprintf("%s?%s", funcName, strings.TrimRight(strToBeSigned.String(), "&")) return fmt.Sprintf("%s?%s", funcName, strings.TrimRight(strToBeSigned.String(), "&")), nil
}
func (s *CmbMixRepoImpl) GetRequestData(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) {
encryptBody, err := cmb.Encrypt(s.bc.Cmb.Sm2Puk, bizJsonStr)
if err != nil {
return nil, err
}
date := time.Now().Format("20060102150405")
req := &v1.CmbRequest{
Mid: s.bc.Cmb.Mid,
Aid: s.bc.Cmb.Aid,
Date: date,
Random: string(cmb.RandomBytes(16)),
KeyAlias: s.bc.Cmb.KeyAlias,
CmbKeyAlias: s.bc.Cmb.CmbKeyAlias,
EncryptBody: encryptBody,
Sign: "",
}
str, err := s.getSignStr(ctx, req, funcName)
if err != nil {
return nil, err
}
sing, err := cmb.Sign(s.bc.Cmb.CmbSm2Pik, str) sing, err := cmb.Sign(s.bc.Cmb.CmbSm2Pik, str)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reply.Sign = sing req.Sign = sing
return reply, nil return req, nil
}
func (s *CmbMixRepoImpl) GetResponseData(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) {
encryptBody, err := cmb.Encrypt(s.bc.Cmb.Sm2Puk, bizJsonStr)
if err != nil {
return nil, err
}
date := time.Now().Format("20060102150405")
req := &v1.CmbRequest{
Mid: s.bc.Cmb.Mid,
Aid: s.bc.Cmb.Aid,
Date: date,
Random: string(cmb.RandomBytes(16)),
KeyAlias: s.bc.Cmb.KeyAlias,
CmbKeyAlias: s.bc.Cmb.CmbKeyAlias,
EncryptBody: encryptBody,
Sign: "",
}
str, err := s.getSignStr(ctx, req, funcName)
if err != nil {
return nil, err
}
sing, err := cmb.Sign(s.bc.Cmb.CmbSm2Pik, str)
if err != nil {
return nil, err
}
req.Sign = sing
return req, nil
} }
func (s *CmbMixRepoImpl) Request(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) { func (s *CmbMixRepoImpl) Request(ctx context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) {

View File

@ -0,0 +1,98 @@
package request
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"time"
)
type Options struct {
Headers http.Header
StatusCodeFunc func(int) bool
Timeout time.Duration
}
func NewOptions(options ...Option) *Options {
o := &Options{
Headers: http.Header{
"Content-Type": []string{"application/json"},
},
StatusCodeFunc: func(code int) bool {
return code == http.StatusOK
},
Timeout: 15 * time.Second,
}
for _, option := range options {
option(o)
}
return o
}
type Option func(*Options)
func WithTimeout(timeout time.Duration) Option {
return func(options *Options) {
options.Timeout = timeout
}
}
func WithHeaders(headers http.Header) Option {
return func(options *Options) {
options.Headers = headers
}
}
func WithStatusCodeFunc(statusCodeFunc func(int) bool) Option {
return func(options *Options) {
options.StatusCodeFunc = statusCodeFunc
}
}
func Post(ctx context.Context, url string, body []byte, options ...Option) (http.Header, []byte, error) {
return Request(ctx, http.MethodPost, url, body, NewOptions(options...))
}
func Get(ctx context.Context, url string, options ...Option) (http.Header, []byte, error) {
return Request(ctx, http.MethodGet, url, nil, NewOptions(options...))
}
func Put(ctx context.Context, url string, body []byte, options ...Option) (http.Header, []byte, error) {
return Request(ctx, http.MethodPut, url, body, NewOptions(options...))
}
func Request(_ context.Context, method, url string, body []byte, o *Options) (http.Header, []byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
if err != nil {
return nil, nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
req.Header = o.Headers
httpClient := &http.Client{
Timeout: o.Timeout,
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("读取响应体失败: %w", err)
}
if !o.StatusCodeFunc(resp.StatusCode) {
return nil, nil, fmt.Errorf("请求异常:%s", resp.Status)
}
return resp.Header, bodyBytes, nil
}

View File

@ -0,0 +1,66 @@
package request
import (
"context"
"net/http"
"net/url"
"testing"
"time"
)
func Test_Get(t *testing.T) {
uri := "https://gateway.dev.cdlsxd.cn/adminyx/admin/v1/key_batch/list"
uv := url.Values{}
uv.Set("page", "1")
uv.Set("limit", "2")
h := http.Header{
"Content-Type": []string{"application/x-www-form-urlencoded"},
}
respHeader, respBody, err := Get(context.Background(), uri+"?"+uv.Encode(), WithHeaders(h))
if err != nil {
t.Error(err)
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
}
func Test_RequestHeaders(t *testing.T) {
uri := "http://example.com/api"
body := []byte("request body")
h := http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{"Bearer token"},
}
respHeader, respBody, err := Post(context.Background(), uri, body, WithTimeout(10*time.Second), WithHeaders(h))
if err != nil {
t.Error(err)
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
}
func Test_RequestStatusCode(t *testing.T) {
uri := "http://example.com/api/update"
body := []byte("update data")
isSuccess := func(code int) bool {
return code == http.StatusOK || code == http.StatusCreated
}
respHeader, respBody, err := Put(context.Background(), uri, body, WithStatusCodeFunc(isSuccess))
if err != nil {
t.Error(err)
return
}
t.Logf("响应体:", string(respBody))
t.Logf("响应头:", respHeader)
}

View File

@ -40,8 +40,8 @@ func NewHTTPServer(
cmb.POST("/v1/orderMock", voucherService.CmbOrderMock) cmb.POST("/v1/orderMock", voucherService.CmbOrderMock)
cmb.POST("/v1/order", voucherService.CmbOrder) cmb.POST("/v1/order", voucherService.CmbOrder)
cmb.POST("/v1/productQueryMock", voucherService.CmbProductQueryMock) cmb.POST("/v1/product/QueryMock", voucherService.CmbProductQueryMock)
cmb.POST("/v1/productQuery", voucherService.CmbProductQuery) cmb.POST("/v1/product/query", voucherService.CmbProductQuery)
return srv return srv
} }

View File

@ -7,81 +7,87 @@ import (
"voucher/internal/biz/vo" "voucher/internal/biz/vo"
) )
const (
cmbOrderFuncName = "/voucher/cmb/v1/order"
cmbProductQueryFuncName = "/voucher/cmb/v1/product/query"
)
func (s *VoucherService) CmbOrder(ctx http.Context) error { func (s *VoucherService) CmbOrder(ctx http.Context) error {
var req v1.CmbOrderReply reply, err := s.cmbOrder(ctx)
if err := ctx.BindForm(&req); err != nil { if err != nil {
return err return err
} }
reply := &v1.CmbReply{
//RespCode: "1000",
//RespMsg: "成功",
//Date: "",
//KeyAlias: "",
//CmbKeyAlias: "",
//EncryptBody: "",
//Sign: "",
}
return ctx.JSON(200, reply) return ctx.JSON(200, reply)
} }
func (s *VoucherService) cmbOrder(ctx http.Context) (string, error) { func (s *VoucherService) cmbOrder(ctx http.Context) (*v1.CmbOrderReply, error) {
var req v1.CmbRequest var req *v1.CmbRequest
if err := ctx.BindForm(&req); err != nil { if err := ctx.BindForm(&req); err != nil {
return "", err return nil, err
} }
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return "", err return nil, err
} }
// todo 签名验证 bizContent, err := s.CmbMixRepo.OrderVerify(ctx, req, cmbOrderFuncName)
if err != nil {
return nil, err
}
boReq := &bo.OrderCreateReqBo{ boReq := &bo.OrderCreateReqBo{
//OutBizNo: req.TransactionId, OutBizNo: bizContent.TransactionId,
//ProductNo: req.ActivityId, ProductNo: bizContent.ActivityId,
//Account: req.CmbUid, Account: bizContent.CmbUid,
AccountType: vo.OrderAccountTypeOpenId, AccountType: vo.OrderAccountTypeOpenId,
} }
orderNo, err := s.VoucherBiz.CmbOrder(ctx, boReq) orderNo, err := s.VoucherBiz.CmbOrder(ctx, boReq)
if err != nil { if err != nil {
return "", err return nil, err
} }
return orderNo, nil return &v1.CmbOrderReply{
CodeNo: orderNo,
}, nil
} }
func (s *VoucherService) CmbProductQuery(ctx http.Context) error { func (s *VoucherService) CmbProductQuery(ctx http.Context) error {
var req v1.CmbQueryProductRequest reply, err := s.cmbProductQuery(ctx)
if err := ctx.BindForm(&req); err != nil {
return err
}
if err := req.Validate(); err != nil {
return err
}
q, err := s.VoucherBiz.CmbProductQuery(ctx, req.ActivityId)
if err != nil { if err != nil {
return err return err
} }
// 数据构造 加签 返回 return ctx.JSON(200, reply)
rep := &v1.CmbQueryProductReply{ }
//RespCode: "",
//RespMsg: "", func (s *VoucherService) cmbProductQuery(ctx http.Context) (*v1.CmbQueryProductReply, error) {
//Date: "", var req *v1.CmbRequest
//KeyAlias: "", if err := ctx.BindForm(&req); err != nil {
//CmbKeyAlias: "", return nil, err
//EncryptBody: "", }
//Sign: "",
ActivityName: "", if err := req.Validate(); err != nil {
ActivityId: *q.StockId, return nil, err
}
bizContent, err := s.CmbMixRepo.ProductQueryVerify(ctx, req, cmbProductQueryFuncName)
if err != nil {
return nil, err
}
wechatResp, err := s.VoucherBiz.CmbProductQuery(ctx, bizContent.ActivityId)
if err != nil {
return nil, err
}
return &v1.CmbQueryProductReply{
ActivityName: *wechatResp.GoodsName,
ActivityId: "",
Amount: "", Amount: "",
MinAmount: "", MinAmount: "",
AvailableType: "", AvailableType: "",
@ -90,7 +96,5 @@ func (s *VoucherService) CmbProductQuery(ctx http.Context) error {
EndTime: "", EndTime: "",
AvailableStock: "", AvailableStock: "",
Detail: "", Detail: "",
} }, nil
return ctx.JSON(200, rep)
} }