220 lines
13 KiB
C#
220 lines
13 KiB
C#
using AllBase;
|
|
using AllBase.Model.Result;
|
|
using CsharpHttpHelper;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
|
|
namespace Work.Api.Wx
|
|
{
|
|
/// <summary>微信小店公共 HTTP 请求层,使用指定来源的 CsharpHttpHelper.HttpHelper。</summary>
|
|
/// <remarks>根据原 WXHttpResponseBLL 保留不同场景的 Cookie 和请求头。同步返回原始正文或错误文字;不更新 ShopLoginInfo、不重试。新业务优先调用分类 API。</remarks>
|
|
public static class WxHttpClient
|
|
{
|
|
/// <summary>微信小店后台 HTTPS 根地址。</summary>
|
|
public const string ShopHost = "https://store.weixin.qq.com";
|
|
|
|
// 原视频上传器的店铺请求发送完整 Cookie,并额外包含 logistics_magic。
|
|
internal static string SendMedia(ShopLoginInfo shop, string path, string productId, string data = null)
|
|
{
|
|
return Send(() =>
|
|
{
|
|
var item = CreateRequest(shop, ShopHost + path, ShopHost + "/shop/goods/entry?productId=" + Uri.EscapeDataString(productId),
|
|
data == null ? MethodType.Get : MethodType.Post, data);
|
|
var cookies = JsonHelper.DeserializeJsonToList<CefSharp.Cookie>(Convert.ToString(shop.Cookies)) ?? new List<CefSharp.Cookie>();
|
|
item.Cookie = string.Concat(cookies.Select(c => c.Name + "=" + c.Value + "; "));
|
|
item.Header["logistics_magic"] = "";
|
|
return item;
|
|
});
|
|
}
|
|
|
|
/// <summary>发送微信 GET 请求,沿用 WeiXinGetResponse 仅发送 biz 前缀 Cookie 的约定。</summary>
|
|
/// <param name="shop">店铺登录信息,Cookies 为 CefSharp.Cookie 列表 JSON。</param>
|
|
/// <param name="url">store.weixin.qq.com 域名下的完整 HTTPS 请求地址。</param>
|
|
/// <param name="referer">来源页面;普通接口使用原页面地址,默认空字符串。</param>
|
|
/// <param name="isGetDocument">true 模拟首页文档导航,并忽略 referer;false 模拟普通接口请求。</param>
|
|
/// <returns>原始 JSON、HTML 或错误文字;调用方判断格式和业务状态。</returns>
|
|
public static string Get(ShopLoginInfo shop, string url, string referer = "", bool isGetDocument = false)
|
|
{
|
|
return Send(() => CreateRequest(shop, url, referer, MethodType.Get, document: isGetDocument));
|
|
}
|
|
|
|
/// <summary>发送微信 JSON POST 请求,携带完整 Cookie 和 biz_magic,对应 WeiXinPostResponse。</summary>
|
|
/// <param name="shop">店铺登录信息,Cookies 为 CefSharp.Cookie 列表 JSON。</param>
|
|
/// <param name="url">store.weixin.qq.com 域名下的完整 HTTPS 请求地址。</param>
|
|
/// <param name="data">完整 JSON 请求正文;按 UTF-8 编码,发送前检查 JSON 格式。</param>
|
|
/// <param name="referer">原业务页面 Referer。</param>
|
|
/// <returns>原始响应正文或错误文字,不自动重试写操作。</returns>
|
|
public static string Post(ShopLoginInfo shop, string url, string data, string referer)
|
|
{
|
|
return Send(() => CreateRequest(shop, url, referer, MethodType.Post, data));
|
|
}
|
|
|
|
/// <summary>发送带货助手请求,对应 WeiXinAssistantPostResponse,使用 talent_magic 而非 biz_magic 请求头。</summary>
|
|
/// <param name="shop">带货助手登录信息,Cookies 必须包含对应 talent_magic 凭证。</param>
|
|
/// <param name="url">store.weixin.qq.com 域名下的完整 HTTPS 请求地址。</param>
|
|
/// <param name="data">完整 UTF-8 JSON 请求正文。</param>
|
|
/// <param name="referer">带货助手页面 Referer。</param>
|
|
/// <returns>原始响应正文或错误文字,由调用方检查业务状态。</returns>
|
|
public static string PostAssistant(ShopLoginInfo shop, string url, string data, string referer)
|
|
{
|
|
return Send(() => CreateRequest(shop, url, referer, MethodType.Post, data, assistant: true));
|
|
}
|
|
|
|
/// <summary>发送达人客服会话请求,对应 WeiXinTalentOpenKfPostResponse,补充 user_scene=1002、kf_scene=6。</summary>
|
|
/// <param name="shop">店铺登录信息,Cookies 为 CefSharp.Cookie 列表 JSON。</param>
|
|
/// <param name="url">store.weixin.qq.com 域名下的完整 HTTPS 请求地址。</param>
|
|
/// <param name="data">完整 UTF-8 JSON 请求正文。</param>
|
|
/// <param name="referer">原达人客服业务页面 Referer。</param>
|
|
/// <returns>原始响应正文或错误文字,由调用方检查业务状态。</returns>
|
|
public static string PostCustomerService(ShopLoginInfo shop, string url, string data, string referer)
|
|
{
|
|
return Send(() =>
|
|
{
|
|
var item = CreateRequest(shop, url, referer, MethodType.Post, data);
|
|
item.Header["user_scene"] = "1002";
|
|
item.Header["kf_scene"] = "6";
|
|
item.Header["logistics_magic"] = "";
|
|
return item;
|
|
});
|
|
}
|
|
|
|
/// <summary>对应 WeiXinImgPostResponse,将 Base64 解码为原始字节上传,不附带店铺 Cookie。</summary>
|
|
/// <param name="shop">店铺登录信息,openid 用于 X-Arguments 上传签名上下文。</param>
|
|
/// <param name="url">westorealpha.video.qq.com 域名下的完整 HTTPS 图片上传地址。</param>
|
|
/// <param name="base64">纯 Base64 图片字符串,不含 data URI 前缀。</param>
|
|
/// <param name="dis_k">临时上传凭证 dis_k。</param>
|
|
/// <param name="dis_t">临时上传凭证 dis_t,原值透传。</param>
|
|
/// <param name="ext">临时上传凭证 ext,原值透传。</param>
|
|
/// <param name="referer">来源页面,默认微信小店根地址。</param>
|
|
/// <returns>原始响应正文或错误文字;无效 Base64 不会发起请求。</returns>
|
|
public static string UploadImage(ShopLoginInfo shop, string url, string base64, string dis_k, long dis_t,
|
|
string ext, string referer = ShopHost + "/")
|
|
{
|
|
return Send(() =>
|
|
{
|
|
// 图片上传使用独立的视频域名和临时签名,不发送店铺 Cookie。
|
|
var item = CreateRequest(shop, url, referer, MethodType.Post, image: true);
|
|
item.Header["X-Requested-With"] = "XMLHttpRequest";
|
|
item.Header["X-Arguments"] = "dis_k=" + dis_k + "&dis_t=" + dis_t + "&ext=" + ext +
|
|
"&apptype=160&filetype=20304&openid=" + shop.openid + "&groupid=0&taskid=" + Guid.NewGuid();
|
|
item.PostDataType = PostDataType.Byte;
|
|
item.PostdataByte = Convert.FromBase64String(base64);
|
|
return item;
|
|
});
|
|
}
|
|
|
|
/// <summary>对应 WeiXinUploadResponse,构造 multipart 二进制正文并发送 biz 前缀 Cookie。</summary>
|
|
/// <param name="shop">店铺登录信息,Cookies 为 CefSharp.Cookie 列表 JSON。</param>
|
|
/// <param name="url">store.weixin.qq.com 域名下的完整 HTTPS 上传地址。</param>
|
|
/// <param name="body">原 multipart 文本模板;成对的 <!> 标记之间为本地文件完整路径,文本统一换行为 CRLF。</param>
|
|
/// <param name="boundary">与正文分隔符一致的 multipart boundary,不含正文额外的两个前导短横线。</param>
|
|
/// <param name="referer">原上传页面 Referer。</param>
|
|
/// <returns>原始响应正文或错误文字;文件不可读或标记不完整时不发送请求。</returns>
|
|
public static string UploadMultipart(ShopLoginInfo shop, string url, string body, string boundary, string referer)
|
|
{
|
|
return Send(() =>
|
|
{
|
|
var item = CreateRequest(shop, url, referer, MethodType.Post, bizCookiesOnly: true);
|
|
item.ContentType = "multipart/form-data; boundary=" + boundary;
|
|
item.PostDataType = PostDataType.Byte;
|
|
item.PostdataByte = BuildMultipartBody(body);
|
|
return item;
|
|
});
|
|
}
|
|
|
|
internal static byte[] BuildMultipartBody(string body)
|
|
{
|
|
if (body == null) throw new ArgumentNullException(nameof(body));
|
|
using (var stream = new MemoryStream())
|
|
{
|
|
// 保留原上传协议:<!> 包围的片段为本地文件路径,其余片段规范化为 CRLF。
|
|
var parts = body.Split(new[] { "<!>" }, StringSplitOptions.None);
|
|
if (parts.Length % 2 == 0) throw new ArgumentException("上传文件标记必须成对出现。", nameof(body));
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
string part = parts[i];
|
|
byte[] bytes = i % 2 == 1 ? File.ReadAllBytes(part) :
|
|
Encoding.UTF8.GetBytes(part.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n"));
|
|
stream.Write(bytes, 0, bytes.Length);
|
|
}
|
|
return stream.ToArray();
|
|
}
|
|
}
|
|
|
|
internal static HttpItem CreateRequest(ShopLoginInfo shop, string url, string referer, MethodType method,
|
|
string data = null, bool assistant = false, bool document = false, bool image = false, bool bizCookiesOnly = false)
|
|
{
|
|
if (shop == null) throw new ArgumentNullException(nameof(shop));
|
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps ||
|
|
(image ? uri.Host != "westorealpha.video.qq.com" : uri.Host != "store.weixin.qq.com"))
|
|
throw new ArgumentException("微信请求地址不在允许的 HTTPS 域名内。", nameof(url));
|
|
|
|
if (data != null) Newtonsoft.Json.Linq.JToken.Parse(data);
|
|
|
|
var cookies = JsonHelper.DeserializeJsonToList<CefSharp.Cookie>(Convert.ToString(shop.Cookies)) ?? new List<CefSharp.Cookie>();
|
|
var item = new HttpItem
|
|
{
|
|
URL = url,
|
|
Method = method,
|
|
Referer = document ? "" : referer,
|
|
Accept = document ? "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" : "application/json, text/plain, */*",
|
|
ContentType = method == MethodType.Post ? "application/json" : null,
|
|
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
|
Encoding = Encoding.UTF8,
|
|
PostEncoding = Encoding.UTF8,
|
|
Postdata = data,
|
|
PostDataType = PostDataType.String,
|
|
KeepAlive = true,
|
|
Expect100Continue = false,
|
|
Allowautoredirect = true,
|
|
ReadWriteTimeout = 300000,
|
|
Cookie = image ? "" : string.Concat(cookies.Where(c => !(method == MethodType.Get || bizCookiesOnly) || c.Name.StartsWith("biz", StringComparison.Ordinal)).Select(c => c.Name + "=" + c.Value + ";")),
|
|
Header = new WebHeaderCollection()
|
|
};
|
|
item.Header["sec-ch-ua"] = "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\"";
|
|
item.Header["sec-ch-ua-mobile"] = "?0";
|
|
item.Header["sec-ch-ua-platform"] = "\"Windows\"";
|
|
item.Header["Sec-Fetch-Site"] = document ? "none" : image ? "same-site" : "same-origin";
|
|
item.Header["Sec-Fetch-Mode"] = document ? "navigate" : "cors";
|
|
item.Header["Sec-Fetch-Dest"] = document ? "document" : "empty";
|
|
// HttpHelper 只解压 gzip,因此不宣告支持 br、zstd、deflate。
|
|
item.Header[HttpRequestHeader.AcceptEncoding] = "gzip";
|
|
item.Header[HttpRequestHeader.AcceptLanguage] = method == MethodType.Post && !image && !bizCookiesOnly ?
|
|
"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" : "zh-CN,zh;q=0.9";
|
|
if (document)
|
|
{
|
|
item.Header["Upgrade-Insecure-Requests"] = "1";
|
|
item.Header["Sec-Fetch-User"] = "?1";
|
|
}
|
|
else if (!image)
|
|
{
|
|
item.Header["wecom_magic"] = "";
|
|
item.Header["supplier_magic"] = "";
|
|
item.Header["mcn_magic"] = "";
|
|
item.Header["potter-scene"] = "weixinShop";
|
|
item.Header["biz_magic"] = assistant ? "" : cookies.FirstOrDefault(c => c.Name == "biz_magic")?.Value ?? "";
|
|
item.Header["talent_magic"] = assistant ? cookies.FirstOrDefault(c => c.Name == "talent_magic")?.Value ?? "" : "";
|
|
}
|
|
if (method == MethodType.Post) item.Header["Origin"] = ShopHost;
|
|
return item;
|
|
}
|
|
|
|
private static string Send(Func<HttpItem> createRequest)
|
|
{
|
|
try
|
|
{
|
|
return HttpHelper.Request(createRequest()).Html;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 兼容现有调用约定:传输失败返回错误文字,业务失败返回原 JSON。
|
|
return ex.Message;
|
|
}
|
|
}
|
|
}
|
|
}
|