diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..11bcc7a --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,51 @@ +name: Lint Check + +on: + push: + branches: + - main + - release/** + pull_request: + branches: + - main + - release/** +permissions: read-all +jobs: + gofmt: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - name: Setup Go Environment + uses: actions/setup-go@v3 + with: + go-version: '1.20.3' + - name: Run gofmt Check + working-directory: ./ + run: | + diffs=`gofmt -l .` + if [[ -n $diffs ]]; then + echo "Files are not formatted by gofmt:" + echo $diffs + exit 1 + fi + golint: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + - name: Setup Go Environment + uses: actions/setup-go@v3 + with: + go-version: '1.20.3' + - name: Install jq + run: sudo apt install -y jq + - uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Download golangci-lint + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 + - name: Run Golang Linters + working-directory: ./ + run: | + PATH=${PATH}:$(go env GOPATH)/bin make lint diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..593ff1c --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +export GO111MODULE=on +export GOPROXY=https://goproxy.io + +default: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +.PHONY: help + +lint: ## Apply go lint check + @golangci-lint run --timeout 10m ./... +.PHONY: lint + +build: ## Build CLI for this project + @go mod tidy + @go build -o cli/showstart cli/main.go +.PHONY: build diff --git a/cli/Makefile b/cli/Makefile index 4cddba1..3ef86ee 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,9 +1,2 @@ # 构建脚本 -export GO111MODULE=on -export GOPROXY=https://goproxy.io - -.PHONY: build -build: - go mod tidy - go build -o showstart \ No newline at end of file diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 0223d01..b189845 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -16,7 +16,7 @@ import ( ) var cfgFile string -var globalConfig *showstart.WapEncryptConfig +var globalConfig *showstart.WapEncryptConfigV3 //var globalClient = resty.New() @@ -100,12 +100,13 @@ func initConfig() { // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { fmt.Println("使用的配置文件:", viper.ConfigFileUsed()) - globalConfig = &showstart.WapEncryptConfig{ - Sign: viper.GetString("sign"), - StFlpv: viper.GetString("st_flpv"), - Token: viper.GetString("token"), - UserId: viper.GetUint32("userId"), - AesKey: viper.GetString("aesKey"), + globalConfig = &showstart.WapEncryptConfigV3{ + Sign: viper.GetString("sign"), + StFlpv: viper.GetString("st_flpv"), + Token: viper.GetString("token"), + UserId: viper.GetUint32("userId"), + AccessToken: viper.GetString("accessToken"), + IdToken: viper.GetString("idToken"), } log.Printf("globalConfig is %+v\n", globalConfig) } diff --git a/go.mod b/go.mod index 81eca0c..d43b522 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module xiudong -go 1.18 +go 1.20 require ( github.com/forgoer/openssl v1.1.1 github.com/go-resty/resty/v2 v2.7.0 + github.com/magiconair/properties v1.8.5 github.com/mitchellh/go-homedir v1.1.0 github.com/modood/table v0.0.0-20200225102042-88de94bb9876 + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.0 github.com/zeromicro/go-zero v1.3.2 @@ -17,7 +19,6 @@ require ( github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/magiconair/properties v1.8.5 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect diff --git a/go.sum b/go.sum index ee95189..a2dbf9d 100644 --- a/go.sum +++ b/go.sum @@ -425,6 +425,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/showstart/api.go b/showstart/api.go index da0891b..2298593 100644 --- a/showstart/api.go +++ b/showstart/api.go @@ -1,9 +1,13 @@ package showstart import ( + "context" + "encoding/json" "fmt" + "strings" "github.com/go-resty/resty/v2" + "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" ) @@ -16,55 +20,39 @@ type API interface { } type ShowStart struct { - Cookies *WapEncryptConfig + Cookies *WapEncryptConfigV3 Address *Address Client *resty.Client } -const OKShowstartState = "1" - -func NewShowStart(cookies *WapEncryptConfig, client *resty.Client) API { +func NewShowStart(cookies *WapEncryptConfigV3, client *resty.Client) API { if client == nil { - client = resty.New() + client = resty.New().SetRetryCount(2) } + client.SetRetryCount(2) return &ShowStart{Cookies: cookies, Client: client} } func (s *ShowStart) GetAddressList(pageNo int64) ([]Address, error) { - w, err := NewWapEncrypt(s.Cookies, &Source{ + source := &SourceV3{ URL: "/wap/address/list.json", Method: "POST", Data: map[string]interface{}{ "pageNo": pageNo, }, - }) - if err != nil { - return nil, err } // POST Map, default is JSON content type. No need to set one - resp, err := s.Client.R(). - SetBody(w.Source.Data). - SetHeaders(w.Source.Headers). - SetResult(&AddressResp{}). - Post(w.GetRequestUrl()) + var resp AddressResp + err := s.SendRequest(context.TODO(), source, &resp) if err != nil { logx.Errorf("GetAddress 请求发生错误: %v", err) return nil, err } - v, ok := resp.Result().(*AddressResp) - if !ok { - logx.Errorf("AddressResp 断言失败") - return nil, fmt.Errorf("AddressResp 断言失败") - } - - logx.Infof("addressResp is %+v", v) - if v.State != OKShowstartState { - return nil, fmt.Errorf("showstart 状态码错误: %v", v.State) - } + logx.Infof("addressResp is %+v", resp) - return v.Result, nil + return resp.Result, nil } // GetAddress 获取用户地址列表 @@ -86,82 +74,126 @@ func (s *ShowStart) GetAddress() (*Address, error) { func (s *ShowStart) GetCpList(pageNo int64) ([]CpItem, error) { // 目前看来 pageNo 貌似没有特别的作用 例如传了 1 和 2 都是一样的返回结果 不知道秀动服务端如何处理的 - w, err := NewWapEncrypt(s.Cookies, &Source{ + source := &SourceV3{ URL: "/wap/cp/list.json", Method: "POST", Data: map[string]interface{}{ "pageNo": pageNo, }, - }) - if err != nil { - return nil, err } // POST Map, default is JSON content type. No need to set one - resp, err := s.Client.R(). - SetBody(w.Source.Data). - SetHeaders(w.Source.Headers). - SetResult(&CpResp{}). - Post(w.GetRequestUrl()) + var resp CpResp + err := s.SendRequest(context.TODO(), source, &resp) if err != nil { logx.Errorf("GetCpList 请求发生错误: %v", err) return nil, err } - v, ok := resp.Result().(*CpResp) - if !ok { - logx.Errorf("CpResp 断言失败") - return nil, fmt.Errorf("CpResp 断言失败") - } - - logx.Infof("GetCpList is %+v\n", v) - if v.State != OKShowstartState { - return nil, fmt.Errorf("showstart 状态码错误: %v", v.State) - } - - return v.Result, nil + return resp.Result, nil } // GetTicketList 获取场次票种列表 func (s *ShowStart) GetTicketList(activityId string) ([]TicketListResult, error) { - w, err := NewWapEncrypt(s.Cookies, &Source{ + source := &SourceV3{ URL: "/wap/activity/V2/ticket/list", Method: "POST", Data: map[string]interface{}{ "activityId": activityId, "coupon": "", // 优惠卷 默认先为空吧 }, - }) + } + + var resp TicketResp + err := s.SendRequest(context.TODO(), source, &resp) if err != nil { + logx.Errorf("GetTicketList 请求发生错误: %v", err) return nil, err } - // POST Map, default is JSON content type. No need to set one - resp, err := s.Client.R(). - SetBody(w.Source.Data). - SetHeaders(w.Source.Headers). - SetResult(&TicketResp{}). - Post(w.GetRequestUrl()) + if len(resp.Result) == 0 { + return nil, fmt.Errorf("没有获取到对应场次票种列表") + } + + return resp.Result, nil +} +func (s *ShowStart) GetWafToken() (*WafTokenResult, error) { + // reset + s.Cookies.AccessToken = "" + s.Cookies.IdToken = "" + + source := &SourceV3{ + URL: "/waf/gettoken", + Data: map[string]interface{}{ + "sign": s.Cookies.Sign, + "st_flpv": s.Cookies.StFlpv, + }, + } + + var resp WafTokenResp + err := s.SendRequest(context.TODO(), source, &resp) if err != nil { - logx.Errorf("GetTicketList 请求发生错误: %v", err) + logx.Errorf("GetWafToken 请求发生错误: %v\n", err) return nil, err } - v, ok := resp.Result().(*TicketResp) - if !ok { - logx.Errorf("TicketResp 断言失败") - return nil, fmt.Errorf("TicketResp 断言失败") + s.Cookies.AccessToken = resp.Result.AccessToken.AccessToken + s.Cookies.IdToken = resp.Result.IDToken.IDToken + + return &resp.Result, nil +} + +func (s *ShowStart) SendRequest(ctx context.Context, source *SourceV3, response interface{}) error { + request := s.Client.R().AddRetryCondition(func(resp *resty.Response, err error) bool { + if err != nil { + return false + } + var sr ResponseWrapper + if err = json.Unmarshal(resp.Body(), &sr); err != nil { + return false + } + // "state":"token-expire-at","sleep":0.4,"msg":"访问令牌已过期" + // check token-expire-at + if !strings.Contains(resp.Request.URL, "/waf/gettoken") && (getState(sr.State) == "token-expire-at" || + getState(sr.State) == "token-clean-at") { + // get waf token then return true + wafTokenResp, err := s.GetWafToken() + if err != nil { + logx.Errorf("get waf token err: %v", err) + return false + } + //s.Infof("get waf token is: %+v", wafTokenResp) + s.Cookies.AccessToken = wafTokenResp.AccessToken.AccessToken + s.Cookies.IdToken = wafTokenResp.IDToken.IDToken + // regen request + w, err := NewWapEncryptV3(s.Cookies, source) + if err != nil { + logx.Errorf("retry gen encrypt v3 err: %v", err) + return false + } + // set request + resp.Request = resp.Request.SetBody(w.Source.Data).SetHeaders(w.Source.Headers) + return true + } + return false + }) + + w, err := NewWapEncryptV3(s.Cookies, source) + if err != nil { + return err } - //logx.Infof("GetTicketList is %+v\n", v) - if v.State != OKShowstartState { - logx.Errorf("获取票种列表状态发生错误: %v", v.State) // 可以反应出是否是帐号的凭证过期之类 - return nil, fmt.Errorf("showstart 状态码错误: %v", v.State) + resp, err := request.SetContext(ctx).SetHeaders(w.Source.Headers).SetBody(w.Source.Data).SetResult(&response).Post(w.GetRequestUrl()) + if err != nil { + logx.Errorf("send request failed, resp: %v", resp.String()) + return errors.Wrap(err, "failed to send post request") } - if len(v.Result) == 0 { - return nil, fmt.Errorf("没有获取到对应场次票种列表") + var sr ResponseWrapper + if err = json.Unmarshal(resp.Body(), &sr); err != nil { + logx.Errorf("resp.Body is %v\n", resp.String()) + return errors.Wrap(err, "json unmarshal") } - return v.Result, nil + return checkState(sr.State) } diff --git a/showstart/api_test.go b/showstart/api_test.go index 015452f..5e37260 100644 --- a/showstart/api_test.go +++ b/showstart/api_test.go @@ -12,12 +12,11 @@ func getTestShowStart() *ShowStart { // 读取配置文件 默认在 ../.showstart.yaml viper.SetConfigFile("../.showstart.yaml") if err := viper.ReadInConfig(); err == nil { - config := &WapEncryptConfig{ + config := &WapEncryptConfigV3{ Sign: viper.GetString("sign"), StFlpv: viper.GetString("st_flpv"), Token: viper.GetString("token"), UserId: viper.GetUint32("userId"), - AesKey: viper.GetString("aesKey"), } log.Printf("config is %+v\n", config) return &ShowStart{ diff --git a/showstart/api_types.go b/showstart/api_types.go index 5b7051a..69e1636 100644 --- a/showstart/api_types.go +++ b/showstart/api_types.go @@ -86,3 +86,23 @@ type TicketItem struct { ShowTime string `json:"showTime"` MemberNum int `json:"memberNum"` } + +type WafTokenResult struct { + AccessToken struct { + Expire int `json:"expire"` + AccessToken string `json:"access_token"` + Msg string `json:"msg"` + } `json:"accessToken"` + IDToken struct { + IDToken string `json:"id_token"` + Expire int `json:"expire"` + Msg string `json:"msg"` + } `json:"idToken"` +} + +type WafTokenResp struct { + Result WafTokenResult `json:"result"` + Success bool `json:"success"` + State interface{} `json:"state"` + Msg string `json:"msg"` +} diff --git a/showstart/error.go b/showstart/error.go new file mode 100644 index 0000000..a487dc2 --- /dev/null +++ b/showstart/error.go @@ -0,0 +1,17 @@ +package showstart + +import ( + "fmt" +) + +var _ error = &ResponseWrapper{} + +type ResponseWrapper struct { + Success bool `json:"success"` + State interface{} `json:"state"` + Msg string `json:"msg"` +} + +func (s *ResponseWrapper) Error() string { + return fmt.Sprintf("msg: %v, success: %v, state: %v", s.Msg, s.Success, s.State) +} diff --git a/showstart/state.go b/showstart/state.go index d964ff2..a5686ca 100644 --- a/showstart/state.go +++ b/showstart/state.go @@ -6,10 +6,7 @@ import ( "github.com/zeromicro/go-zero/core/logx" ) -var errState = fmt.Errorf("state != 1") - func checkState(state interface{}) error { - logx.Infof("state is : %v", state) switch state.(type) { case float64: // 变成了 float64 if state == float64(1) { @@ -21,6 +18,11 @@ func checkState(state interface{}) error { } } + logx.Errorf("err state: %v", state) // 其他情况都是返回错误 - return errState + return fmt.Errorf("err state: %v", state) +} + +func getState(state interface{}) string { + return fmt.Sprintf("%v", state) } diff --git a/showstart/wapencrypt.go b/showstart/wapencrypt.go index cf30a0f..5ea415c 100644 --- a/showstart/wapencrypt.go +++ b/showstart/wapencrypt.go @@ -35,6 +35,7 @@ type WapEncrypt struct { Source *Source } +// Deprecated: NewWapEncrypt use v3 instead func NewWapEncrypt(config *WapEncryptConfig, source *Source) (*WapEncrypt, error) { if config.Token == "" { // fix issue #36 config.Token = getRandStr(32) @@ -101,15 +102,6 @@ type sourceDataTmp struct { RandStr string `json:"ranstr"` // 这个 key 是 ranstr } -// 暂时不需要用到 仅作为标识 -type sourceDataNew struct { - Data string `json:"data"` - Sign string `json:"sign"` - AppId string `json:"appid"` - Terminal string `json:"terminal"` - Version string `json:"version"` -} - // getInjectData data 注入 func (w *WapEncrypt) getInjectData(qtime int64, randStr string) (map[string]interface{}, error) { if w.Source.Data == nil { diff --git a/showstart/wapencrypt_v3.go b/showstart/wapencrypt_v3.go new file mode 100644 index 0000000..28cd2d5 --- /dev/null +++ b/showstart/wapencrypt_v3.go @@ -0,0 +1,131 @@ +package showstart + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/pkg/errors" +) + +type WapEncryptConfigV3 struct { + Sign string `json:"sign"` + StFlpv string `json:"st_flpv"` + Token string `json:"token"` + UserId uint32 `json:"user_id"` + AccessToken string `json:"accessToken"` + IdToken string `json:"idToken"` +} + +type WapEncryptV3 struct { + Config *WapEncryptConfigV3 + Source SourceV3 +} + +type SourceV3 struct { + URL string `json:"url"` + Method string `json:"method"` + Data map[string]interface{} `json:"data"` + APISource string `json:"apiSource"` + APIId uint32 `json:"apiId"` + EventID string `json:"eventId"` + ParamsType string `json:"paramsType"` + Headers map[string]string `json:"headers"` + Secret bool `json:"secret"` +} + +func NewWapEncryptV3(config *WapEncryptConfigV3, source *SourceV3) (*WapEncryptV3, error) { + if config.Token == "" { + config.Token = getRandStr(32) + } + w := &WapEncryptV3{ + Config: config, + Source: *source, + } + err := w.InjectSource() + if err != nil { + return nil, err + } + + return w, nil +} + +func (w *WapEncryptV3) InjectSource() error { + w.Source.URL = strings.TrimSuffix(w.Source.URL, ".json") + if w.Source.APISource == "appnj" { + w.Source.APISource = "nj" + } + if len(w.Source.APISource) > 0 { // need concat + w.Source.URL = "/" + w.Source.APISource + w.Source.URL + } + if w.Source.Data == nil { + w.Source.Data = map[string]interface{}{} + } + w.Source.Data["st_flpv"] = w.Config.StFlpv + w.Source.Data["sign"] = w.Config.Sign + w.Source.Data["trackPath"] = "" + + headers, err := w.getInjectHeaders(time.Now().Unix()) + if err != nil { + return err + } + w.Source.Headers = headers + + return nil +} + +func (w *WapEncryptV3) GetRequestUrl() string { + apiUrlPrefix := "https://wap.showstart.com/v3" + return apiUrlPrefix + w.Source.URL +} + +const deviceInfo = "%7B%22vendorName%22:%22%22,%22deviceMode%22:%22iPhone%22,%22deviceName%22:%22%22,%22systemName%22:%22ios%22,%22systemVersion%22:%2213.2.3%22,%22cpuMode%22:%22%20%22,%22cpuCores%22:%22%22,%22cpuArch%22:%22%22,%22memerySize%22:%22%22,%22diskSize%22:%22%22,%22network%22:%22UNKNOWN%22,%22resolution%22:%22390*844%22,%22pixelResolution%22:%22%22%7D" + +func (w *WapEncryptV3) getInjectHeaders(now int64) (map[string]string, error) { + headers := map[string]string{ + "Cookie": fmt.Sprintf("Hm_lvt_da038bae565bb601b53cc9cb25cdca74=1689569273; Hm_lpvt_da038bae565bb601b53cc9cb25cdca74=%d", now), + "Content-Type": "application/json", + "st_flpv": w.Config.StFlpv, + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", + "Origin": "https://wap.showstart.com", + "Referer": "https://wap.showstart.com", + } + + headers["CUSAT"] = valueOrNil(w.Config.AccessToken) + headers["CUSUT"] = valueOrNil(w.Config.Sign) + headers["CUSIT"] = valueOrNil(w.Config.IdToken) + headers["CUSID"] = valueOrNil(fmt.Sprintf("%d", w.Config.UserId)) + headers["CUSNAME"] = "nil" + headers["CDEVICENO"] = w.Config.Token + headers["CUUSERREF"] = w.Config.Token + headers["CVERSION"] = "997" + headers["CTERMINAL"] = "wap" + headers["CSAPPID"] = "wap" + + headers["CDEVICEINFO"] = deviceInfo + headers["CRTRACEID"] = fmt.Sprintf("%s%d", getRandStr(32), now*1000) + + headers["CTRACKPATH"] = "" + headers["CSOURCEPATH"] = "" + + // 生成签名 + dumpRes, err := json.Marshal(w.Source.Data) + if err != nil { + return nil, errors.Wrap(err, "marshal source data again") + } + // accessToken + sign + idToken + userId + "wap" + token + I + n['url'] + "997" + "wap" + traceId + signValue := w.Config.AccessToken + w.Config.Sign + w.Config.IdToken + fmt.Sprintf("%d", w.Config.UserId) + "wap" + + w.Config.Token + string(dumpRes) + w.Source.URL + "997" + + "wap" + headers["CRTRACEID"] + headers["CRPSIGN"] = Md5Sum(signValue) + + return headers, nil +} + +func valueOrNil(a string) string { + if len(a) > 0 { + return a + } + return "nil" +} diff --git a/showstart/wapencrypt_v3_test.go b/showstart/wapencrypt_v3_test.go new file mode 100644 index 0000000..504141b --- /dev/null +++ b/showstart/wapencrypt_v3_test.go @@ -0,0 +1,67 @@ +package showstart + +import ( + "testing" + "time" + + "github.com/magiconair/properties/assert" +) + +func TestWapEncryptV3(t *testing.T) { + type fields struct { + Config *WapEncryptConfigV3 + Source *SourceV3 + } + type args struct { + now int64 + } + + x := time.Now().Unix() + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "one", + fields: fields{ + Config: &WapEncryptConfigV3{ + Sign: "05cedc227d1axxxx1174d92e50d871ea", + StFlpv: "nJmxxxxd8Qt7aWor4W3", + Token: "c559xxxx7834dfa16c939420794b038e", + UserId: 4612249, + }, + Source: &SourceV3{ + URL: "/wap/activity/V2/ticket/list", + Method: "POST", + Data: map[string]interface{}{ + "activityId": "204504", + "coupon": "", + }, + Headers: map[string]string{}, + Secret: false, + }, + }, + args: args{ + now: x, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s3, err := NewWapEncryptV3(tt.fields.Config, tt.fields.Source) + if err != nil { + t.Error(err) + return + } + headers := s3.Source.Headers + for k, v := range headers { + t.Logf("key: %s, value: %s", k, v) + } + url := s3.GetRequestUrl() + assert.Equal(t, url, "https://wap.showstart.com/v3/wap/activity/V2/ticket/list") + }) + } +}