diff --git a/internal/biz/mixrepos/cmb.go b/internal/biz/mixrepos/cmb.go index 6925a27..ec5fdcd 100644 --- a/internal/biz/mixrepos/cmb.go +++ b/internal/biz/mixrepos/cmb.go @@ -6,5 +6,7 @@ import ( ) 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) } diff --git a/internal/data/mixrepoimpl/cmb.go b/internal/data/mixrepoimpl/cmb.go index a0af044..f002932 100644 --- a/internal/data/mixrepoimpl/cmb.go +++ b/internal/data/mixrepoimpl/cmb.go @@ -2,6 +2,8 @@ package mixrepoimpl import ( "context" + "encoding/json" + "errors" "fmt" "strings" "time" @@ -20,28 +22,59 @@ func NewCmbMixRepoImpl(bc *conf.Bootstrap) mixrepos.CmbMixRepo { return &CmbMixRepoImpl{bc: bc} } -func (s *CmbMixRepoImpl) GetRequestData(_ context.Context, funcName, bizJsonStr string) (*v1.CmbRequest, error) { - // 我们的sm2 公钥加密 - // 请求到我们这边 使用 我们的私钥解密 - encryptBody, err := cmb.Encrypt(s.bc.Cmb.Sm2Puk, bizJsonStr) +func (s *CmbMixRepoImpl) OrderVerify(ctx context.Context, req *v1.CmbRequest, funcName string) (*v1.CmbOrderRequest, error) { + bizStr, err := s.Verify(ctx, req, funcName) if err != nil { return nil, err } - date := time.Now().Format("20060102150405") - - reply := &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: "", + var resp *v1.CmbOrderRequest + if err = json.Unmarshal([]byte(bizStr), &resp); err != nil { + return nil, err } - 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 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)) } - 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) if err != nil { 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) { diff --git a/internal/pkg/request/request.go b/internal/pkg/request/request.go new file mode 100644 index 0000000..ff0f0d6 --- /dev/null +++ b/internal/pkg/request/request.go @@ -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 +} diff --git a/internal/pkg/request/request_test.go b/internal/pkg/request/request_test.go new file mode 100644 index 0000000..4f13710 --- /dev/null +++ b/internal/pkg/request/request_test.go @@ -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) +} diff --git a/internal/server/http.go b/internal/server/http.go index 8125102..52f73b2 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -40,8 +40,8 @@ func NewHTTPServer( cmb.POST("/v1/orderMock", voucherService.CmbOrderMock) cmb.POST("/v1/order", voucherService.CmbOrder) - cmb.POST("/v1/productQueryMock", voucherService.CmbProductQueryMock) - cmb.POST("/v1/productQuery", voucherService.CmbProductQuery) + cmb.POST("/v1/product/QueryMock", voucherService.CmbProductQueryMock) + cmb.POST("/v1/product/query", voucherService.CmbProductQuery) return srv } diff --git a/internal/service/cmb.go b/internal/service/cmb.go index 04612f0..40edec8 100644 --- a/internal/service/cmb.go +++ b/internal/service/cmb.go @@ -7,81 +7,87 @@ import ( "voucher/internal/biz/vo" ) +const ( + cmbOrderFuncName = "/voucher/cmb/v1/order" + cmbProductQueryFuncName = "/voucher/cmb/v1/product/query" +) + func (s *VoucherService) CmbOrder(ctx http.Context) error { - var req v1.CmbOrderReply - if err := ctx.BindForm(&req); err != nil { + reply, err := s.cmbOrder(ctx) + if err != nil { return err } - reply := &v1.CmbReply{ - //RespCode: "1000", - //RespMsg: "成功", - //Date: "", - //KeyAlias: "", - //CmbKeyAlias: "", - //EncryptBody: "", - //Sign: "", - } - 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 { - return "", err + return nil, err } 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{ - //OutBizNo: req.TransactionId, - //ProductNo: req.ActivityId, - //Account: req.CmbUid, + OutBizNo: bizContent.TransactionId, + ProductNo: bizContent.ActivityId, + Account: bizContent.CmbUid, AccountType: vo.OrderAccountTypeOpenId, } orderNo, err := s.VoucherBiz.CmbOrder(ctx, boReq) 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 { - var req v1.CmbQueryProductRequest - 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) + reply, err := s.cmbProductQuery(ctx) if err != nil { return err } - // 数据构造 加签 返回 - rep := &v1.CmbQueryProductReply{ - //RespCode: "", - //RespMsg: "", - //Date: "", - //KeyAlias: "", - //CmbKeyAlias: "", - //EncryptBody: "", - //Sign: "", - ActivityName: "", - ActivityId: *q.StockId, + return ctx.JSON(200, reply) +} + +func (s *VoucherService) cmbProductQuery(ctx http.Context) (*v1.CmbQueryProductReply, error) { + var req *v1.CmbRequest + if err := ctx.BindForm(&req); err != nil { + return nil, err + } + + if err := req.Validate(); err != nil { + 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: "", MinAmount: "", AvailableType: "", @@ -90,7 +96,5 @@ func (s *VoucherService) CmbProductQuery(ctx http.Context) error { EndTime: "", AvailableStock: "", Detail: "", - } - - return ctx.JSON(200, rep) + }, nil }