feat(luban): 优化 channelTwo 处理逻辑并添加多重 URL 参数解码

- 新增 decodeURLParam 方法,实现对 PayURL 查询参数的多重 decodeURIComponent 解码
- channelTwo 中使用 decodeURLParam 解析 PayURL 查询参数,模拟浏览器端逻辑
- 根据解析结果动态构造请求 URL,并设置更完整的浏览器请求头
- 补充日志上报请求逻辑,增加对 urlPostStart 和 isLog 参数的处理
- 调整 HTTP 客户端超时设置及代理配置
- 优化日志输出,增加请求 URL 信息,方便跟踪卡密提交过程
This commit is contained in:
danial
2025-12-11 01:42:30 +08:00
parent bf67c931ef
commit 77029b910a
2 changed files with 87 additions and 25 deletions

View File

@@ -29,6 +29,49 @@ type SendCardTaskTypeLuban struct {
sendCardTaskTypeSendCardTaskBase
}
// decodeURLParam 对 PayURL 进行处理,模拟浏览器端 JavaScript 的 getUrlParams 逻辑
// 将 PayURL 作为 window.location 处理,对其 query 部分执行多重 decodeURIComponent
// JavaScript 逻辑: s.replace(/\+/g, " ") 然后执行四次 decodeURIComponent
func (s *SendCardTaskTypeLuban) decodeURLParam(payURL string) map[string]string {
parsedURL, err := url.Parse(payURL)
if err != nil {
return map[string]string{}
}
// 获取 search 部分
search := parsedURL.RawQuery
if search == "" {
return map[string]string{}
}
urlParams := make(map[string]string)
// 解析查询参数
queryValues, err := url.ParseQuery(search)
if err != nil {
return map[string]string{}
}
// 对每个参数进行解码
for key, values := range queryValues {
if len(values) > 0 {
// 先将 + 替换为空格,模拟 JavaScript 中的 s.replace(pl, " ")
decoded := strings.ReplaceAll(values[0], "+", " ")
// 执行四次 URL 解码,匹配 JavaScript 中的多重 decodeURIComponent 逻辑
for i := 0; i < 4; i++ {
unescaped, err := url.QueryUnescape(decoded)
if err != nil {
break
}
decoded = unescaped
}
urlParams[key] = decoded
}
}
return urlParams
}
func (s *SendCardTaskTypeLuban) CreateOrder(ctx context.Context, roadUid string, faceValue float64) (OrderPoolItem, error) {
orderPoolItem := OrderPoolItem{}
roadInfo := road.GetRoadInfoByRoadUid(ctx, roadUid)
@@ -94,7 +137,6 @@ func (s *SendCardTaskTypeLuban) CreateOrder(ctx context.Context, roadUid string,
}
func (s *SendCardTaskTypeLuban) channelOne(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
otelTrace.Logger.WithContext(ctx).Info("处理任务", zap.Any("orderItem", orderItem), zap.Any("task", task))
query, err := url.Parse(orderItem.PayURL)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("解析URL失败", zap.Error(err))
@@ -139,37 +181,29 @@ func (s *SendCardTaskTypeLuban) channelOne(ctx context.Context, orderItem OrderP
}
func (s *SendCardTaskTypeLuban) channelTwo(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
otelTrace.Logger.WithContext(ctx).Info("处理任务", zap.Any("orderItem", orderItem), zap.Any("task", task))
query, err := url.Parse(orderItem.PayURL)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("解析URL失败", zap.Error(err))
return errors.New("解析URL失败")
}
orderUrl := fmt.Sprintf("https://abc.pvcrr.com/baolingqiu/tdeal_order/postCardAndSecret")
time.Sleep(time.Second * 30)
// 使用有序结构体保证 JSON 序列化的字段顺序
params := struct {
TradeNo string `json:"tradeNo"`
Sign string `json:"sign"`
CardNo string `json:"card_no"`
CardPwd string `json:"card_pwd"`
}{
TradeNo: query.Query().Get("tradeNo"),
Sign: query.Query().Get("sign"),
CardNo: task.CardInfo.CardNo,
CardPwd: task.CardInfo.Data,
// 对 PayURL 的 query 部分执行多重解码,模拟 JavaScript 的 getUrlParams 函数
decodedParams := s.decodeURLParam(orderItem.PayURL)
// 获取 urlPostStart 参数,用于构建请求 URL
urlPostStart := decodedParams["urlPostStart"]
if urlPostStart == "" {
urlPostStart = "baolingqiu"
}
jsonBody, _ := json.Marshal(params)
client := resty.New().SetTimeout(time.Second * 5).SetRetryCount(0).SetHeaders(map[string]string{
// 创建 HTTP 客户端,设置完整的浏览器请求头
client := resty.New().SetTimeout(time.Second * 10).SetRetryCount(0).SetHeaders(map[string]string{
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": "no-cache",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"DNT": "1",
"Origin": "https://abc.pvcrr.com",
"Origin": fmt.Sprintf("%s://%s", query.Scheme, query.Host),
"Pragma": "no-cache",
"Referer": orderItem.PayURL,
"Sec-Fetch-Dest": "empty",
@@ -177,22 +211,51 @@ func (s *SendCardTaskTypeLuban) channelTwo(ctx context.Context, orderItem OrderP
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
"sec-ch-ua": `"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"`,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-ch-ua-platform": `"macOS"`,
"Connection": "keep-alive",
}).OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
proxy, _ := utils.GetProxy(ctx, utils.GenerateId(), string(SendCardTaskTypeEnumLuban+"channelTwo"))
proxy, _ := utils.GetProxy(ctx, task.LocalOrderID, string(SendCardTaskTypeEnumLuban+"channelTwo"))
if proxy != "" {
client.SetProxy(proxy)
}
return nil
})
otelresty.TraceClient(client)
// Step 1: 模拟访问页面
_, _ = client.R().SetContext(ctx).Get(orderItem.PayURL)
time.Sleep(time.Second * 30)
// Step 2: 发送日志上报请求 (如果有 urlPostStart 和 isLog 参数)
if decodedParams["urlPostStart"] != "" && decodedParams["isLog"] != "" {
logUrl := fmt.Sprintf("%s://%s/%s/tdeal_order/postRecodeLog/", query.Scheme, query.Host, urlPostStart)
_, err := client.R().SetContext(ctx).SetQueryParam("tradeNo", decodedParams["tradeNo"]).Get(logUrl)
if err != nil {
otelTrace.Logger.WithContext(ctx).Warn("日志上报请求失败", zap.Error(err))
}
}
// Step 4: 构造提交数据,使用有序结构体保证 JSON 序列化的字段顺序
// 按照 demo.html 中 imgJSON 的顺序: tradeNo, sign, saltSign, stealSign, card_no, card_pwd
params := struct {
TradeNo string `json:"tradeNo"`
Sign string `json:"sign"`
SaltSign string `json:"saltSign"`
StealSign string `json:"stealSign"`
CardNo string `json:"card_no"`
CardPwd string `json:"card_pwd"`
}{
TradeNo: decodedParams["tradeNo"],
Sign: decodedParams["sign"],
SaltSign: decodedParams["saltSign"],
StealSign: decodedParams["stealSign"],
CardNo: task.CardInfo.CardNo,
CardPwd: task.CardInfo.Data,
}
jsonBody, _ := json.Marshal(params)
orderUrl := fmt.Sprintf("%s://%s/%s/tdeal_order/postCardAndSecret", query.Scheme, query.Host, urlPostStart)
response, err := client.R().SetContext(ctx).SetQueryParams(map[string]string{
"json": string(jsonBody),
}).Get(orderUrl)
@@ -201,7 +264,7 @@ func (s *SendCardTaskTypeLuban) channelTwo(ctx context.Context, orderItem OrderP
return errors.New("请求失败")
}
otelTrace.Logger.WithContext(ctx).Info("回调", zap.String("response", response.String()), zap.Any("params", params))
otelTrace.Logger.WithContext(ctx).Info("卡密提交回调", zap.String("response", response.String()), zap.Any("params", params), zap.String("orderUrl", orderUrl))
submitData := struct {
Code int64 `json:"code"`

View File

@@ -50,7 +50,6 @@ func TestSendCardTaskTypeLuban_CreateOrder(t *testing.T) {
paramsStr[k] = convertor.ToString(v)
}
response, _ := webClient.R().SetFormData(paramsStr).Post("http://jiuzpay.xyz:56700/api/pay/create_order")
otelTrace.Logger.WithContext(t.Context()).Info("请求结果", zap.String("response", response.String()), zap.Any("params", params))
responseStruct := struct {
RetCode string `json:"retCode"`
PayUrl string `json:"payUrl"`