From 16cdd3d46e23c5ed3cefd11a2d3cdd00f87cac61 Mon Sep 17 00:00:00 2001 From: Chfei Date: Sun, 13 Sep 2026 15:47:05 +0800 Subject: [PATCH] gx --- Store.Framework/Agent/AgentConfig.cs | 5 + Store.Framework/Apis/CommonApi.cs | 8 +- Store.Framework/Apis/UserApi.cs | 7 +- Store.Framework/ClientInfo.cs | 46 ---- Store.Framework/Commons/SignTool.cs | 70 ------ Store.Framework/Config.cs | 5 + .../Extensions/HttpItemExtensions.cs | 68 ------ .../HttpExtension/AppApiRequestBase.cs | 136 +++++++++-- .../HttpExtension/ServerHttpItem.cs | 50 ++-- .../HttpExtension/ServiceRequestBase.cs | 2 +- Store.Framework/Models/Cookies.cs | 185 ++++++++++++--- Store.Framework/Models/Shop.cs | 9 + Store.Framework/Models/ShopBase.cs | 23 +- .../Services/CurrentUserService.cs | 16 +- Store.Framework/Services/ShopManager.cs | 224 ++++++++++++++++++ Store.Framework/Services/ShopService.cs | 134 ----------- Store.Framework/Services/UserService.cs | 8 +- Store.Product/Models/Product/ProductBase.cs | 28 --- .../Models/Product/WxProductUploadProduct.cs | 28 +++ .../Wx/WxProductUploadParam.cs | 25 +- Store.Product/Publish/PublishContext .cs | 2 +- .../Publish/Steps/WXPublishParameters.cs | 9 +- .../Steps/Wx/AddRandomImageBorderStep.cs | 2 +- .../Publish/Steps/Wx/BuildSkuInfoStep.cs | 2 +- .../Publish/Steps/Wx/FissionTitleStep.cs | 6 +- .../Steps/Wx/InitializeProductDataStep.cs | 6 - .../Publish/Steps/Wx/MIGRATION_NOTES.md | 9 +- .../Publish/Steps/Wx/SetProductTitleStep.cs | 3 +- .../Steps/Wx/SkipUploadedProductStep.cs | 2 +- .../Publish/Steps/Wx/SubmitProductStep.cs | 11 +- .../Publish/Steps/Wx/WXPublishContext.cs | 21 ++ .../ProductForms/WXDBProdUploadForm.cs | 2 +- test/Store.Test/Store.Test.csproj | 4 +- test/Store.Test/UnitTest1.cs | 162 ++++++++++++- 34 files changed, 837 insertions(+), 481 deletions(-) delete mode 100644 Store.Framework/ClientInfo.cs delete mode 100644 Store.Framework/Commons/SignTool.cs delete mode 100644 Store.Framework/Extensions/HttpItemExtensions.cs create mode 100644 Store.Framework/Models/Shop.cs create mode 100644 Store.Framework/Services/ShopManager.cs delete mode 100644 Store.Framework/Services/ShopService.cs create mode 100644 Store.Product/Models/Product/WxProductUploadProduct.cs diff --git a/Store.Framework/Agent/AgentConfig.cs b/Store.Framework/Agent/AgentConfig.cs index 8bf107a..25aa10e 100644 --- a/Store.Framework/Agent/AgentConfig.cs +++ b/Store.Framework/Agent/AgentConfig.cs @@ -15,5 +15,10 @@ namespace Store.Client.Agent /// 代理码 /// public static string AgentCode { get; set; } + + /// + /// 代理名称 + /// + public static string AgentName { get; set; } } } diff --git a/Store.Framework/Apis/CommonApi.cs b/Store.Framework/Apis/CommonApi.cs index ffb1e71..09f4788 100644 --- a/Store.Framework/Apis/CommonApi.cs +++ b/Store.Framework/Apis/CommonApi.cs @@ -21,7 +21,7 @@ namespace Store.Client.Apis /// public DateTime GetServerTime() { - HttpResult result = ServerRequest(new ServerHttpItem { URL = $"{_config.Host}/time.php" }); + HttpResult result = ServerRequest(new HttpItem { URL = $"{_config.Host}/time.php" }); if (result.StatusCode == HttpStatusCode.OK) { if (DateTime.TryParse(result.Html, out DateTime dt)) @@ -42,7 +42,7 @@ namespace Store.Client.Apis /// public string GetAuthConfig() { - ServerHttpItem item = new ServerHttpItem(); + HttpItem item = new HttpItem(); item.URL = $"{_config.Host}?obj=api/config&act=auth_config"; HttpResult result = ServerRequest(item); if (result.StatusCode == HttpStatusCode.OK) @@ -59,7 +59,7 @@ namespace Store.Client.Apis public string GetCacheServerRatio() { - ServerHttpItem item = new ServerHttpItem(); + HttpItem item = new HttpItem(); item.URL = _config.Host + "?" + _config.ObjUrl + "config&act=cache_service_mode"; item.Method = MethodType.Post; //item.SignUser(); @@ -80,7 +80,7 @@ namespace Store.Client.Apis //public HttpResult GetImageCache(string imgUrl) //{ // long id = MD5Helper.GetLong(imgUrl); - // ServerHttpItem item = new ServerHttpItem(); + // HttpItem item = new HttpItem(); // item.Method = MethodType.Post; // item.URL = _config.Host + "?" + _config.ObjUrl + "add_img"; // item.Postdata = "act=get&table=" + Math.Abs(id % 1024) + "&id=" + id; diff --git a/Store.Framework/Apis/UserApi.cs b/Store.Framework/Apis/UserApi.cs index 5e3d559..8f6c8d1 100644 --- a/Store.Framework/Apis/UserApi.cs +++ b/Store.Framework/Apis/UserApi.cs @@ -2,8 +2,6 @@ using MAX.Core.Extensions; using MAX.Core.Helpers; using Store.Client.Agent; -using Store.Client.Commons; -using Store.Client.Extensions; using Store.Client.HttpExtension; using Store.Framework; using Store.Framework.Tools; @@ -26,7 +24,7 @@ namespace Store.Client.Apis { _logger.Info("123"); int r2 = RandomHelper.GetRandomNumber(100000000, 899999999); //生成随机码 - ServerHttpItem item = new ServerHttpItem(); + HttpItem item = new HttpItem(); item.URL = base.BuildServiceUrl("user/user_login"); item.Method = MethodType.Post; Dictionary param = new Dictionary @@ -44,14 +42,13 @@ namespace Store.Client.Apis //{ "client_addr", _client.Address } }; item.Postdata = param.ToUrlParams(); - item.Sign(); HttpResult result = ServerRequest(item); if (result.StatusCode == HttpStatusCode.OK) { string chCode = Regex.Match(result.Html, "([^<]+)")?.Groups[1]?.Value ?? ""; if (!string.IsNullOrEmpty(chCode)) { - string codex = SignTool.Encryption(r2); + string codex = Encryption(r2); if (chCode != codex) { result.StatusCode = HttpStatusCode.Unauthorized; diff --git a/Store.Framework/ClientInfo.cs b/Store.Framework/ClientInfo.cs deleted file mode 100644 index cd06bed..0000000 --- a/Store.Framework/ClientInfo.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MAX.Core.Infrastructure; -using Store.Framework.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Store.Framework -{ - /// - /// 客户端信息 - /// - public class ClientInfo : SingleInstance - { - - public static UserInfo User - { - get - { - return Instance.UserInfo; - } - } - - - - private ClientInfo() - { - - } - /// - /// 客户端IP - /// - public string Ip { get; } - /// - /// 客户端地址 - /// - public string Address { get; set; } - - /// - /// 获取当前登录用户 - /// - public UserInfo UserInfo { get; private set; } - - } -} diff --git a/Store.Framework/Commons/SignTool.cs b/Store.Framework/Commons/SignTool.cs deleted file mode 100644 index 1e176fa..0000000 --- a/Store.Framework/Commons/SignTool.cs +++ /dev/null @@ -1,70 +0,0 @@ -using MAX.Core.Cryptography; -using MAX.Core.Helpers; -using Store.Client; -using Store.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Store.Client.Commons -{ - /// - /// 签名工具 - /// - public class SignTool - { - /// - /// 获取签名 - /// - /// - public static string GetSign() - { - long time = GetReviseUnix(); - string s = Encryption(time); - return "time=" + time + "&code=" + s + "&NbCodeVersion=1"; - } - /// - /// 用户签名 - /// - /// - public static string GetUserSign() - { - return GetUserSign("", ""); - } - /// - /// 获取当前用户签名 - /// - /// - /// - /// - public static string GetUserSign(string shopId, string shopName) - { - long time = GetReviseUnix(); - string time1 = Encryption(time); - return "time=" + time + "&code=" + time1 + "&NbCodeVersion=1" + "&uid=" + ClientInfo.User.UserId + "&key=" + ClientInfo.User.UserKey + - "&cid=" + ClientInfo.User.CompanyId + "&company=" + UrlHelper.Encode(ClientInfo.User.CompanyName) + - "&sid=" + shopId + "&shop=" + UrlHelper.Encode(shopName); - } - - /// - /// 获取校正后的时间戳 - /// - /// - public static long GetReviseUnix() - { - return DateHelper.GetCurrentUnixTimestampInSeconds() + ServerConfig.TimeDifferent; - } - /// - /// 签名算法 - /// - /// 随机数 - /// 加密签名结果 - public static string Encryption(long s) - { - string value = $"{s}sdhewu*/fhAAue48485ew5+5%@3485esGFFHFD"; - return MD5Helper.Md5_32(value); - } - } -} diff --git a/Store.Framework/Config.cs b/Store.Framework/Config.cs index 81644fe..5aa506a 100644 --- a/Store.Framework/Config.cs +++ b/Store.Framework/Config.cs @@ -13,6 +13,11 @@ public static class Config { public const string AppVersion = "1312"; + /// + /// API 项目路径。 + /// + public const string MainPath = "29wx"; + public static bool IsRelease { get diff --git a/Store.Framework/Extensions/HttpItemExtensions.cs b/Store.Framework/Extensions/HttpItemExtensions.cs deleted file mode 100644 index 33c3718..0000000 --- a/Store.Framework/Extensions/HttpItemExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using MAX.Core.Helpers; -using Store.Client.Commons; -using Store.Client.HttpExtension; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Store.Client.Extensions -{ - /// - /// 请求扩展 - /// - public static class HttpItemExtensions - { - /// - /// 增加签名 - /// - /// - public static void Sign(this ServerHttpItem item) - { - SetSign(item, SignTool.GetSign()); - } - /// - /// 增加用户签名 - /// - /// - public static void SignUser(this ServerHttpItem item) - { - item.SignUser("", ""); - } - /// - /// 增加用户签名 - /// - /// - /// - /// - public static void SignUser(this ServerHttpItem item, string shopId, string shopName) - { - SetSign(item, SignTool.GetUserSign(shopId, shopName)); - } - private static void SetSign(ServerHttpItem item, string sign) - { - if (item.Method == MethodType.Get) - { - if (item.URL.Contains("?")) - { - item.URL += "&"; - } - else - { - item.URL += "?"; - } - item.URL += sign; - } - else - { - if (item.Postdata.Length > 0) - { - item.Postdata += "&"; - } - item.Postdata += sign; - } - item.IsSign = true; - } - } -} diff --git a/Store.Framework/HttpExtension/AppApiRequestBase.cs b/Store.Framework/HttpExtension/AppApiRequestBase.cs index 5148529..1b165e2 100644 --- a/Store.Framework/HttpExtension/AppApiRequestBase.cs +++ b/Store.Framework/HttpExtension/AppApiRequestBase.cs @@ -1,9 +1,15 @@ -using MAX.Core.Autofac; +using MAX.Core.Autofac; +using MAX.Core.Cryptography; using MAX.Core.DependencyInjection; using MAX.Core.Helpers; using MAX.Core.Logger; -using Store.Client.Extensions; +using Store.Client.Agent; +using Store.Framework; +using Store.Framework.Inerterfaces; +using Store.Framework.Models; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Text; @@ -21,6 +27,7 @@ namespace Store.Client.HttpExtension [Autowired] protected ILoggerFacade _logger => LazyServiceProvider.Instance.LazyGetService(); protected IApiServerConfig _config => LazyServiceProvider.Instance.LazyGetService(); + protected ICurrentUser CurrentUser => LazyServiceProvider.Instance.LazyGetService(); /// /// 默认服务请求。 @@ -69,39 +76,36 @@ namespace Store.Client.HttpExtension { return Request(BuildServiceUrl(urlpath), postdata); } + /// /// 使用当前 Key 构建服务请求地址。 /// protected string BuildServiceUrl(string urlPath) { - return $"{_config.Host}/wxxd_{urlPath}?obj=29wx/wxxd_{urlPath}"; + return $"{_config.Host}/wxxd_{urlPath}?obj={Config.MainPath}/wxxd_{urlPath}"; } - /// /// 创建并执行服务端请求,默认使用当前用户签名。 /// protected HttpResult Request(string url, string postdata = "", MethodType method = MethodType.Post, bool isUserSign = true) { - ServerHttpItem item = new ServerHttpItem + HttpItem item = new HttpItem { URL = url, Method = method, Postdata = postdata ?? string.Empty }; - if (isUserSign) - { - item.SignUser(); - } + ApplySign(item, isUserSign); - return ServerRequest(item); + return Send(item); } /// /// 服务器请求。 /// - public HttpResult ServerRequest(ServerHttpItem item) + public HttpResult ServerRequest(HttpItem item) { if (item == null) { @@ -111,26 +115,15 @@ namespace Store.Client.HttpExtension #if RELEASE item.WebProxy = null; #endif - if (!item.IsSign) - { - if (item.Shop != null) - { - item.SignUser(item.Shop.Id, item.Shop.Name); - } - else - { - item.Sign(); - } - } + ApplySign(item, false); return Send(item); } - /// /// 执行底层 HTTP 请求。 /// - protected HttpResult Send(ServerHttpItem item) + protected HttpResult Send(HttpItem item) { if (item == null) { @@ -155,5 +148,98 @@ namespace Store.Client.HttpExtension return result; } + + /// + /// 为请求添加公共签名或用户签名。 + /// + private void ApplySign(HttpItem item, bool isUserSign) + { + string sign = isUserSign + ? GetUserSign("", "") + : GetSign(); + + SetSign(item, sign); + } + + /// + /// 获取公共签名。 + /// + private static string GetSign() + { + long time = GetReviseUnix(); + string code = Encryption(time); + return "time=" + time + "&code=" + code + "&NbCodeVersion=1"; + } + + /// + /// 获取当前用户签名。 + /// + private string GetUserSign(string shopId, string shopName) + { + UserInfo user = CurrentUser.User; + if (user == null) + { + throw new InvalidOperationException("当前用户未登录,无法生成用户签名。"); + } + + long time = GetReviseUnix(); + string code = Encryption(time); + var dict = new Dictionary + { + { "time", time.ToString() }, + { "code", code }, + { "code_version", "1" }, + { "version", Config.Version }, + { "user_name", user.UserName }, + { "path", Config.MainPath } + }; + if (!string.IsNullOrEmpty(AgentConfig.AgentCode)) + { + dict.Add("agent_code", AgentConfig.AgentCode); + dict.Add("agent_name", AgentConfig.AgentName); + } + + return string.Join("&", dict.Select(kv => $"{kv.Key}={kv.Value}")); + } + + /// + /// 获取校正后的时间戳。 + /// + protected static long GetReviseUnix() + { + return DateHelper.GetCurrentUnixTimestampInSeconds() + ServerConfig.TimeDifferent; + } + + /// + /// 签名算法。 + /// + /// 随机数 + /// 加密签名结果 + protected static string Encryption(long s) + { + string value = $"{s}sdhewu*/fhAAue48485ew5+5%@3485esGFFHFD"; + return MD5Helper.Md5_32(value); + } + + /// + /// 将签名参数拼接到请求。 + /// + private static void SetSign(HttpItem item, string sign) + { + if (item.Method == MethodType.Get) + { + item.URL += item.URL.Contains("?") ? "&" : "?"; + item.URL += sign; + } + else + { + item.Postdata = item.Postdata ?? string.Empty; + if (item.Postdata.Length > 0) + { + item.Postdata += "&"; + } + item.Postdata += sign; + } + } } -} \ No newline at end of file +} diff --git a/Store.Framework/HttpExtension/ServerHttpItem.cs b/Store.Framework/HttpExtension/ServerHttpItem.cs index 6f4f06e..1323b9a 100644 --- a/Store.Framework/HttpExtension/ServerHttpItem.cs +++ b/Store.Framework/HttpExtension/ServerHttpItem.cs @@ -1,28 +1,28 @@  -using MAX.Core.Helpers; -using Store.Framework.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +//using MAX.Core.Helpers; +//using Store.Framework.Models; +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; -namespace Store.Client.HttpExtension -{ - public class ServerHttpItem : HttpItem - { - public ShopBase Shop { get; private set; } - public ServerHttpItem() - { +//namespace Store.Client.HttpExtension +//{ +// public class HttpItem : HttpItem +// { +// public ShopBase Shop { get; private set; } +// public HttpItem() +// { - } - public ServerHttpItem(ShopBase shop) - { - Shop = shop; - } - /// - /// 是否已签名 - /// - public bool IsSign { get; set; } - } -} +// } +// public HttpItem(ShopBase shop) +// { +// Shop = shop; +// } +// /// +// /// 是否已签名 +// /// +// public bool IsSign { get; set; } +// } +//} diff --git a/Store.Framework/HttpExtension/ServiceRequestBase.cs b/Store.Framework/HttpExtension/ServiceRequestBase.cs index 08f38ac..23d5803 100644 --- a/Store.Framework/HttpExtension/ServiceRequestBase.cs +++ b/Store.Framework/HttpExtension/ServiceRequestBase.cs @@ -76,7 +76,7 @@ // /// // /// 执行底层 HTTP 请求。 // /// -// protected HttpResult Send(ServerHttpItem item) +// protected HttpResult Send(HttpItem item) // { // if (item == null) // { diff --git a/Store.Framework/Models/Cookies.cs b/Store.Framework/Models/Cookies.cs index e3e3738..2584fcc 100644 --- a/Store.Framework/Models/Cookies.cs +++ b/Store.Framework/Models/Cookies.cs @@ -1,21 +1,26 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Store.Framework.Models { + /// + /// Cookie集合,支持普通 Cookie 字符串和 API 返回的 Cookie JSON。 + /// + [JsonConverter(typeof(CookiesJsonConverter))] public class Cookies : Dictionary { private readonly string oldCookie; + public Cookies(string cookie) { oldCookie = cookie; - SplitCookie(oldCookie); + Load(cookie); } - + /// /// Cookie字符串 /// @@ -26,28 +31,6 @@ namespace Store.Framework.Models return GetCookieString(); } } - - /// - /// 拆分Cookie - /// - private void SplitCookie(string cookie) - { - this.Clear(); - Regex regCookie = new Regex("(?.+?)=(?.+?)(;|$)"); - if (regCookie.IsMatch(cookie)) - { - MatchCollection matchs = regCookie.Matches(cookie); - foreach (Match item in matchs) - { - string key = item.Groups["key"].Value.Trim(); - string value = item.Groups["value"].Value.Trim(); - if (!ContainsKey(key)) - { - this.Add(key.Trim(), value.Trim()); - } - } - } - } /// /// 获取指定值 @@ -63,6 +46,7 @@ namespace Store.Framework.Models } return defvalue; } + /// /// 添加或修改 /// @@ -79,6 +63,7 @@ namespace Store.Framework.Models this[key] = value; } } + /// /// 获取Cookies字符串 /// @@ -89,14 +74,156 @@ namespace Store.Framework.Models return string.Join(";", list); } + /// + /// 转为浏览器 Cookie 字符串。 + /// + public override string ToString() + { + var list = this.Select(a => { return $"{a.Key}={a.Value}"; }); + return string.Join("; ", list); + } /// /// 隐式转换 /// /// - public static implicit operator string (Cookies cookie) + public static implicit operator string(Cookies cookie) { return cookie.GetCookieString(); } + internal static Cookies FromToken(JToken token) + { + Cookies cookies = new Cookies(string.Empty); + if (token == null || token.Type == JTokenType.Null) + return cookies; + + if (token.Type == JTokenType.String) + return new Cookies(token.ToString()); + + cookies.AddToken(token); + return cookies; + } + + private void Load(string cookie) + { + Clear(); + if (string.IsNullOrWhiteSpace(cookie)) + return; + + try + { + AddToken(JToken.Parse(cookie)); + if (Count > 0) + return; + } + catch (JsonException) + { + } + + SplitCookie(Decode(cookie)); + } + + private void AddToken(JToken token) + { + if (token == null || token.Type == JTokenType.Null) + return; + + if (token is JArray array) + { + foreach (JToken item in array) + { + AddCookie(item); + } + return; + } + + AddCookie(token); + } + + private void AddCookie(JToken token) + { + if (token == null || token.Type == JTokenType.Null) + return; + + if (token.Type == JTokenType.String) + { + string value = token.ToString(); + int index = value.IndexOf('='); + if (index <= 0) + return; + + AddOrUpdate(value.Substring(0, index).Trim(), value.Substring(index + 1).Trim()); + return; + } + + JToken name = token["Name"] ?? token["name"]; + JToken valueToken = token["Value"] ?? token["value"]; + if (name == null || valueToken == null) + return; + + AddOrUpdate(name.ToString(), valueToken.ToString()); + } + + /// + /// 拆分普通 Cookie 字符串。 + /// + private void SplitCookie(string cookie) + { + Clear(); + if (string.IsNullOrEmpty(cookie)) + return; + + Regex regCookie = new Regex("(?.+?)=(?.+?)(;|$)"); + if (regCookie.IsMatch(cookie)) + { + MatchCollection matchs = regCookie.Matches(cookie); + foreach (Match item in matchs) + { + string key = item.Groups["key"].Value.Trim(); + string value = item.Groups["value"].Value.Trim(); + if (!ContainsKey(key)) + { + this.Add(key, value); + } + } + } + } + + private static string Decode(string value) + { + return (value ?? string.Empty) + .Replace("&", "&") + .Replace("%", "%") + .Replace("+", "+") + .Replace(""", "\""); + } } -} + + /// + /// Cookie JSON 转换器。 + /// + public class CookiesJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Cookies).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + if (reader.TokenType == JsonToken.String) + return new Cookies(reader.Value?.ToString()); + + return Cookies.FromToken(JToken.Load(reader)); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + Cookies cookies = value as Cookies; + writer.WriteValue(cookies?.GetCookieString()); + } + } +} \ No newline at end of file diff --git a/Store.Framework/Models/Shop.cs b/Store.Framework/Models/Shop.cs new file mode 100644 index 0000000..8d7af43 --- /dev/null +++ b/Store.Framework/Models/Shop.cs @@ -0,0 +1,9 @@ +namespace Store.Framework.Models +{ + /// + /// 店铺信息。 + /// + public class Shop : ShopBase + { + } +} \ No newline at end of file diff --git a/Store.Framework/Models/ShopBase.cs b/Store.Framework/Models/ShopBase.cs index 9223e76..9d59c1d 100644 --- a/Store.Framework/Models/ShopBase.cs +++ b/Store.Framework/Models/ShopBase.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,35 +10,45 @@ namespace Store.Framework.Models /// /// 商铺信息 /// - public class ShopBase + public abstract class ShopBase { + [JsonProperty("id")] public string Id { get; set; } + + [JsonProperty("shop_account")] public string Name { get; set; } + + [JsonProperty("login_pass")] public string Password { get; set; } + /// /// Cookie信息 - public Cookies Cookie { get; set; } /// + [JsonProperty("cookie")] + public Cookies Cookie { get; set; } + /// /// 分组名称 /// + [JsonProperty("group_name")] public string GroupName { get; set; } /// /// 商铺提示信息 /// + [JsonProperty("login_status")] public string Msg { get; set; } } + /// /// 商铺信息 /// /// 状态码 - public class ShopBase : ShopBase + public abstract class ShopBase : ShopBase { /// /// 状态 /// public TStatus Status { get; set; } - } -} +} \ No newline at end of file diff --git a/Store.Framework/Services/CurrentUserService.cs b/Store.Framework/Services/CurrentUserService.cs index e242caa..472ca3f 100644 --- a/Store.Framework/Services/CurrentUserService.cs +++ b/Store.Framework/Services/CurrentUserService.cs @@ -10,11 +10,11 @@ using System.Threading.Tasks; namespace Store.Framework.Services; -public record UserNotification(UserInfo user) : INotification; +//public record UserNotification(UserInfo user) : INotification; /// /// 当前用户 /// -internal class CurrentUserService : ICurrentUser, ISingletonDependency, INotificationHandler +internal class CurrentUserService : ICurrentUser, ISingletonDependency//, INotificationHandler { public string UserName => User?.UserName; @@ -28,10 +28,14 @@ internal class CurrentUserService : ICurrentUser, ISingletonDependency, INotific { return User.UserAuthority.Contains(code); } - - Task INotificationHandler.Handle(UserNotification notification, CancellationToken cancellationToken) + internal void SetUser(UserInfo user) { - User = notification.user; - return Task.CompletedTask; + User = user; } + + //Task INotificationHandler.Handle(UserNotification notification, CancellationToken cancellationToken) + //{ + // User = notification.user; + // return Task.CompletedTask; + //} } diff --git a/Store.Framework/Services/ShopManager.cs b/Store.Framework/Services/ShopManager.cs new file mode 100644 index 0000000..c72142d --- /dev/null +++ b/Store.Framework/Services/ShopManager.cs @@ -0,0 +1,224 @@ +using MAX.Core.DependencyInjection; +using MAX.Core.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Store.Client.Apis; +using Store.Framework.Apis; +using Store.Framework.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; + +namespace Store.Client.Services +{ + /// + /// 商铺管理服务 + /// + public class ShopManager : ITransientDependency + { + private const int DefaultPageSize = 100; + private const int MaxPageCount = 10000; + + private readonly ShopApi _shopApi; + + public ShopManager(ShopApi shopApi) + { + _shopApi = shopApi ?? throw new ArgumentNullException(nameof(shopApi)); + } + + /// + /// 获取当前用户的所有商铺。 + /// + /// 每页数量。 + /// 商铺列表。 + public List GetShops(int pageSize = DefaultPageSize) + { + return GetShops(false, pageSize); + } + + /// + /// 获取商铺列表。 + /// + /// 是否调用管理端接口。 + /// 每页数量。 + /// 商铺列表。 + public List GetShops(bool isAdmin, int pageSize = DefaultPageSize) + { + List shops = new List(); + if (pageSize <= 0) + pageSize = DefaultPageSize; + + int page = 0; + while (page < MaxPageCount) + { + page++; + + string postdata = "page=" + page + "&page_size=" + pageSize; + HttpResult result = isAdmin + ? _shopApi.AdminGetShopList(postdata) + : _shopApi.GetShopList(postdata); + + ApiResponse> response = DeserializeShopResponse(result); + if (response == null || response.Code != 200 || response.Data == null) + break; + + if (response.Data.Count == 0) + break; + + shops.AddRange(response.Data); + if (response.Data.Count < pageSize) + break; + } + + return shops; + } + + /// + /// 获取服务端商铺列表,兼容旧调用。 + /// + public List GetServerShop() + { + return GetShops(); + } + + /// + /// 从服务端刷新商铺 Cookie。 + /// + /// 待刷新商铺。 + /// 服务端原始响应。 + public string GetShopCookie(List shops) + { + if (shops == null || shops.Count == 0) + return string.Empty; + + string shopJson = JsonConvert.SerializeObject(shops); + string postdata = "shops=" + Uri.EscapeDataString(shopJson); + HttpResult result = _shopApi.GetPddShopCookiesList(postdata); + if (result == null || result.StatusCode != HttpStatusCode.OK || string.IsNullOrEmpty(result.Html)) + return string.Empty; + + MatchCollection matches = Regex.Matches( + result.Html, + @"[^""]*?)""[^>]*)>(?[\w\W]*?)", + RegexOptions.IgnoreCase); + + foreach (Match match in matches) + { + string key = match.Groups["key"].Value; + Shop shop = shops.FirstOrDefault(s => s.Id == key || s.Name == key); + if (shop == null) + continue; + + string cookie = ParseCookie(match.Groups["json"].Value); + if (cookie != null) + shop.Cookie = new Cookies(DecodeCookie(cookie)); + } + + return result.Html; + } + + /// + /// 检测商铺登录状态。 + /// + /// 商铺。 + public void CheckShopStatus(Shop shop) + { + if (shop == null) + throw new ArgumentNullException(nameof(shop)); + + if (shop.Cookie == null) + { + shop.Msg = "Cookie为空"; + return; + } + + HttpItem item = new HttpItem + { + URL = "https://myseller.taobao.com/ajaxProxy.do?_ksTS=1548152154745_13&callback=seller_layout_head&action=FrameworkLayoutAction&event_submit_do_layout_data=true", + Referer = "https://myseller.taobao.com", + Method = MethodType.Post, + Cookie = shop.Cookie.GetCookieString() + }; + + HttpResult result = HttpHelper.Request(item); + shop.Msg = result != null + && result.StatusCode == HttpStatusCode.OK + && !string.IsNullOrEmpty(result.Html) + && result.Html.Contains("\"shopName\":\"") + ? "登录成功" + : "会话已过期"; + } + + /// + /// 删除服务端商铺。 + /// + /// 店铺登录名。 + /// 服务端原始响应。 + public string DeleteShop(string loginName) + { + if (string.IsNullOrWhiteSpace(loginName)) + throw new ArgumentException("店铺登录名不能为空。", nameof(loginName)); + + HttpResult result = _shopApi.DelShop("login_name=" + Uri.EscapeDataString(loginName)); + return result?.Html ?? string.Empty; + } + + /// + /// 删除服务端店铺,兼容旧方法命名。 + /// + public string DelServerDouShop(string shopId) + { + return DeleteShop(shopId); + } + + private static ApiResponse> DeserializeShopResponse(HttpResult result) + { + if (result == null || result.StatusCode != HttpStatusCode.OK || string.IsNullOrWhiteSpace(result.Html)) + return null; + + try + { + return JsonConvert.DeserializeObject>>(result.Html); + } + catch (JsonException) + { + return null; + } + } + + private static string ParseCookie(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + try + { + JObject root = JObject.Parse(json); + return GetString(root, "Cookies") ?? GetString(root, "cookie") ?? GetString(root, "Cookie"); + } + catch (JsonException) + { + return null; + } + } + + private static string GetString(JObject source, string propertyName) + { + JToken token = source?[propertyName]; + return token == null || token.Type == JTokenType.Null + ? null + : token.ToString(); + } + + private static string DecodeCookie(string cookie) + { + return (cookie ?? string.Empty) + .Replace("&", "&") + .Replace("%", "%") + .Replace("+", "+") + .Replace(""", "\""); + } + } +} \ No newline at end of file diff --git a/Store.Framework/Services/ShopService.cs b/Store.Framework/Services/ShopService.cs deleted file mode 100644 index 2091d42..0000000 --- a/Store.Framework/Services/ShopService.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -//using FkServices = LittleCow.Framework.Services; - -namespace Store.Client.Services -{ - ///// - ///// 商铺服务 - ///// - //internal class ShopService : ITransientDependency - //{ - // private readonly ShopApi _shopApi; - // private readonly IEventBus _eventBus; - - // public ShopService(ShopApi shopApi, IEventBus eventBus) - // { - // _shopApi = shopApi; - // _eventBus = eventBus; - // } - - - // /// - // /// 获取所有商铺信息 - // /// - // /// - // /// - // public List GetShops() where T : ShopBase - // { - // HttpResult result = _shopApi.GetShops(); - // if (result.StatusCode == System.Net.HttpStatusCode.OK) - // { - // StringBuilder sb = new StringBuilder(); - // sb.Append("["); - // Regex regShopList = new Regex("]+?)>"); - // if (regShopList.IsMatch(result.Html)) - // { - // int index = 0; - // MatchCollection matchs = regShopList.Matches(result.Html); - // foreach (Match item in matchs) - // { - // string value = item.Value; - // value = value.Replace("", "\"}"); - // value = value.Replace("':'", "\":\""); - // value = value.Replace("' '", "\",\""); - // if (index > 0) - // { - // sb.Append(","); - // } - // sb.Append(value); - // index++; - - // } - // sb.Append("]"); - // List list = JsonConvert.DeserializeObject>(sb.ToString()); - // list.RemoveAll(a => string.IsNullOrEmpty(a.Name)); - // return list; - // } - // } - // return new List(); - // } - - - // public string GetShopCookie(List shops) - // { - // HttpResult result = _shopApi.GetShopsCookie(shops); - // if (result.StatusCode == System.Net.HttpStatusCode.OK) - // { - // MatchCollection matchs = Regex.Matches(result.Html, @"([\w\W]+?)"); - // if (matchs != null) - // { - // foreach (Match item in matchs) - // { - // string shopId = item.Groups[1].Value; - // string json = item.Groups[2].Value; - // var rootToken = JToken.Parse(json); - // string cookie = rootToken["Cookies"]?.ToString() ?? ""; - // var shop = shops.FirstOrDefault(a => a.Id == shopId); - // if (shop != null) - // { - // shop.Cookie = new Cookies(cookie); - // } - // } - // } - // } - // return null; - // } - - - // /// - // /// 检测商铺登录状态 - // /// - // public void CheckShopStatus(ShopBase shop) - // { - // HttpItem item = new HttpItem(); - // item.URL = "https://myseller.taobao.com/ajaxProxy.do?_ksTS=1548152154745_13&callback=seller_layout_head&action=FrameworkLayoutAction&event_submit_do_layout_data=true"; - // item.Referer = "https://myseller.taobao.com"; - // item.Method = MethodType.Post; - // item.Cookie = shop.Cookie.GetCookieString(); - // HttpResult result = HttpHelper.Request(item); - // if (result.StatusCode == System.Net.HttpStatusCode.OK) - // { - // string a = result.Html.Contains("\"shopName\":\"") ? "登录成功" : "会话已过期"; - // } - // //string urlx = - // //string refere = - // //string htmlx = GetPostTaobaoHtml.GetUrl(urlx, refere, true, "", Shop.Cookies); - // //Shop.MsgForCookiesLogin = htmlx.Contains("\"shopName\":\"") ? "登录成功" : "会话已过期"; - // //GetTaobaoShops.ShopsTemp.Add(Shop); - // } - // /// - // /// 获取值 - // /// - // /// - // /// - // /// - // private string GetUserValue(string html, string reg) - // { - // string temp = Regex.Match(html, @"[""]" + reg + @"[""][\s]*:[\s]*[""]([^""]*?)[""]")?.Value; - // if (string.IsNullOrEmpty(temp)) - // temp = Regex.Match(html, @"[']" + reg + @"['][\s]*:[\s]*[']([^']*?)[']")?.Groups[1].Value; - // if (string.IsNullOrEmpty(temp)) - // temp = Regex.Match(html, @"['""]" + reg + @"['""][\s]*:[\s]*([\d]+)")?.Value; - - // return temp; - // } - //} -} diff --git a/Store.Framework/Services/UserService.cs b/Store.Framework/Services/UserService.cs index 891daa9..f1be233 100644 --- a/Store.Framework/Services/UserService.cs +++ b/Store.Framework/Services/UserService.cs @@ -5,7 +5,9 @@ using MAX.Core.Helpers; using Newtonsoft.Json; using Store.Client.Apis; using Store.Framework.Apis; +using Store.Framework.Inerterfaces; using Store.Framework.Models; +using Store.Framework.Services; using System; using System.Collections.Generic; using System.Linq; @@ -21,10 +23,12 @@ namespace Store.Client.Services public class UserService : ITransientDependency { private readonly UserApi _userApi; + private readonly ICurrentUser _currentUser; - public UserService(UserApi userApi) + public UserService(UserApi userApi, ICurrentUser currentUser) { _userApi = userApi; + _currentUser = currentUser; } /// @@ -75,6 +79,8 @@ namespace Store.Client.Services ret.Success = true; ret.Msg = apiResponse.Msg; ret.Data = result; + (_currentUser as CurrentUserService).SetUser(user); + //_mediator.Publish(new UserNotification(user)); return ret; } diff --git a/Store.Product/Models/Product/ProductBase.cs b/Store.Product/Models/Product/ProductBase.cs index 0d8d237..faf74f9 100644 --- a/Store.Product/Models/Product/ProductBase.cs +++ b/Store.Product/Models/Product/ProductBase.cs @@ -179,10 +179,6 @@ public class ProductBase /// public string SkuSecretJson { get; set; } /// - /// 新增:微信图片映射JSON。 - /// - public string WxImgMapJson { get; set; } - /// /// 新增:商品来源类型名称。 /// public string ProductGetTypeText { get; set; } @@ -195,14 +191,6 @@ public class ProductBase /// public string SpecialProdType { get; set; } /// - /// 新增:是否存在微信回采格式数据。 - /// - public string HasWxFormat { get; set; } - /// - /// 新增:微信安全状态。 - /// - public string WxSafeStatus { get; set; } - /// /// 新增:价格文本。 /// public string PriceText { get; set; } @@ -215,22 +203,6 @@ public class ProductBase /// public string AddUserName { get; set; } /// - /// 新增:微信一级类目ID。 - /// - public long? WxCat1 { get; set; } - /// - /// 新增:微信二级类目ID。 - /// - public long? WxCat2 { get; set; } - /// - /// 新增:微信三级类目ID。 - /// - public long? WxCat3 { get; set; } - /// - /// 新增:微信叶子类目ID。 - /// - public long? WxCatLeaf { get; set; } - /// /// 新增:是否发送到外部服务。 /// public int? SendToOutServer { get; set; } diff --git a/Store.Product/Models/Product/WxProductUploadProduct.cs b/Store.Product/Models/Product/WxProductUploadProduct.cs new file mode 100644 index 0000000..3caa156 --- /dev/null +++ b/Store.Product/Models/Product/WxProductUploadProduct.cs @@ -0,0 +1,28 @@ +namespace Store.Product.Models.Product; + +/// +/// 微信商品上传使用的商品模型。继承通用商品基类,并承载微信平台专属字段。 +/// +public class WxProductUploadProduct : ProductBase +{ + /// 微信图片映射 JSON。 + public string WxImgMapJson { get; set; } + + /// 是否存在微信回采格式数据。 + public string HasWxFormat { get; set; } + + /// 微信安全状态。 + public string WxSafeStatus { get; set; } + + /// 微信一级类目 ID。 + public long? WxCat1 { get; set; } + + /// 微信二级类目 ID。 + public long? WxCat2 { get; set; } + + /// 微信三级类目 ID。 + public long? WxCat3 { get; set; } + + /// 微信叶子类目 ID。 + public long? WxCatLeaf { get; set; } +} diff --git a/Store.Product/Models/PublishParameters/Wx/WxProductUploadParam.cs b/Store.Product/Models/PublishParameters/Wx/WxProductUploadParam.cs index 88126c9..1d3a801 100644 --- a/Store.Product/Models/PublishParameters/Wx/WxProductUploadParam.cs +++ b/Store.Product/Models/PublishParameters/Wx/WxProductUploadParam.cs @@ -5,6 +5,7 @@ using AllBase.Model.Result.PDDResult; using AllBase.Model.Result.WXResult; using Newtonsoft.Json.Linq; using Store.Framework.Extensions; +using Store.Product.Models.Product; using System; using System.Collections.Generic; using System.Drawing; @@ -34,11 +35,27 @@ public class WxProductUploadParam public class WxProductUploadSource { public ShopLoginInfo Shop { get; set; } - public KJDDBProduct DbProduct { get; set; } - public TemuDBProductSelect SelectProduct { get; set; } + /// 已完成商品与选品定制组合后的商品数据。 + public WxProductUploadProduct Product { get; set; } = new(); + /// 上传任务状态,仅用于结果回写,不作为商品数据源。 + public WxProductPublishTask Task { get; set; } = new(); public DateTime StartTime { get; set; } = DateTime.Now; } +/// +/// 上传任务结果。只保存任务状态和最终草稿参数,不保存原始商品数据。 +/// +public class WxProductPublishTask +{ + public long id { get; set; } + public string out_product_Id { get; set; } + public string shop_id { get; set; } + public string draft_id { get; set; } + public string status { get; set; } + public DateTime add_time { get; set; } + public WXUploadProductParam wxDraftProduct { get; set; } +} + /// /// 原 UploadProduct 方法中跨 #region 使用的可变状态。 /// TODO(迁移): 个别字段当前为 dynamic,需按 Store.Product 最终 DTO 改为明确类型。 @@ -100,7 +117,7 @@ public class WxProductPublishState public List AddPriceRules { get; set; } = new(); public dynamic WxUploadRule { get; set; } public string PddProductCates { get; set; } - public dynamic UploadDraftProductList { get; set; } + public List UploadDraftProductList { get; set; } = new(); } /// @@ -260,4 +277,4 @@ public class WxControlState Text = numericUpDown?.Text ?? "" }; } -} \ No newline at end of file +} diff --git a/Store.Product/Publish/PublishContext .cs b/Store.Product/Publish/PublishContext .cs index 11b1d7b..aa28196 100644 --- a/Store.Product/Publish/PublishContext .cs +++ b/Store.Product/Publish/PublishContext .cs @@ -19,7 +19,7 @@ public class PublishContextBase /// /// 商铺 /// - public ShopBase Shop { get; set; } + public Shop Shop { get; set; } /// /// 商铺名称 /// diff --git a/Store.Product/Publish/Steps/WXPublishParameters.cs b/Store.Product/Publish/Steps/WXPublishParameters.cs index 754ed50..7e8276b 100644 --- a/Store.Product/Publish/Steps/WXPublishParameters.cs +++ b/Store.Product/Publish/Steps/WXPublishParameters.cs @@ -28,8 +28,11 @@ public class WXPublishParameters: PublishParameters public bool IsCompleted { get; set; } /// 候补:当前上传店铺。 public ShopLoginInfo Shop { get; set; } - /// 候补:当前选品商品。 - public TemuDBProductSelect SelectProduct { get; set; } + /// + /// 上传任务对象。仅在上传完成后回写状态、草稿 ID 等任务信息, + /// 不参与商品标题、裂变、SKU、图片等商品内容构造。 + /// + public WxProductPublishTask Task { get; set; } = new WxProductPublishTask(); /// 候补:本次上传开始时间。 public DateTime StartTime { get; set; } = DateTime.Now; /// 候补:实际提交的佣金比例。 @@ -121,7 +124,7 @@ public class WXPublishParameters: PublishParameters /// 候补:资质图片列表。 public List QualificationImages { get; set; } = new List(); /// 候补:待上传草稿商品列表。 - public dynamic UploadDraftProductList { get; set; } + public List UploadDraftProductList { get; set; } = new List(); /// /// 预售天数(页面限制为 3-45 天)。 /// diff --git a/Store.Product/Publish/Steps/Wx/AddRandomImageBorderStep.cs b/Store.Product/Publish/Steps/Wx/AddRandomImageBorderStep.cs index 3e39943..df1f2b1 100644 --- a/Store.Product/Publish/Steps/Wx/AddRandomImageBorderStep.cs +++ b/Store.Product/Publish/Steps/Wx/AddRandomImageBorderStep.cs @@ -34,7 +34,7 @@ public sealed class AddRandomImageBorderStep : WxProductPublishStepBase protected override Task ExecuteProvider(WXPublishContext context, CancellationToken cancellationToken) { - if (context.Parameters.SelectProduct.is_fission_title && context.Parameters.IsRandomImageBorder) + if (context.Parameters.IsFissionTitle && context.Parameters.IsRandomImageBorder) { // selectProd.isFission int mainCount = 9; diff --git a/Store.Product/Publish/Steps/Wx/BuildSkuInfoStep.cs b/Store.Product/Publish/Steps/Wx/BuildSkuInfoStep.cs index a328d45..cb90ba4 100644 --- a/Store.Product/Publish/Steps/Wx/BuildSkuInfoStep.cs +++ b/Store.Product/Publish/Steps/Wx/BuildSkuInfoStep.cs @@ -99,7 +99,7 @@ public sealed class BuildSkuInfoStep : WxProductPublishStepBase // 聚水潭上微信小店,加运费(其他平台shipPrice默认为0) skuParam.productSkuInfo.salePrice += (context.Parameters.ShipPrice * context.Parameters.DefaultAddPriceRule.ShippingPriceTimes).ParseToInt(); // 商品裂变随机增加价格 - if (context.Parameters.SelectProduct.is_fission_title && context.Parameters.IsFissionRandomPriceChange) + if (context.Parameters.IsFissionTitle && context.Parameters.IsFissionRandomPriceChange) { Invoke(new Action(() => { diff --git a/Store.Product/Publish/Steps/Wx/FissionTitleStep.cs b/Store.Product/Publish/Steps/Wx/FissionTitleStep.cs index da18b34..a35d622 100644 --- a/Store.Product/Publish/Steps/Wx/FissionTitleStep.cs +++ b/Store.Product/Publish/Steps/Wx/FissionTitleStep.cs @@ -43,11 +43,11 @@ public sealed class FissionTitleStep : WxProductPublishStepBase { throw new Exception("裂变标题数量不足,将暂停上传"); } - context.Parameters.SelectProduct.product_name = titles[0]; - context.Parameters.SelectProduct.is_fission_title = true; + context.Product.Title = titles[0]; + context.Parameters.IsFissionTitle = true; titles.RemoveAt(0); context.Parameters.FissionTitles = string.Join("\r\n", titles); - context.Progress.Report("标题裂变成功,更改为:" + context.Parameters.SelectProduct.product_name); + context.Progress.Report("标题裂变成功,更改为:" + context.Product.Title); return Task.CompletedTask; } diff --git a/Store.Product/Publish/Steps/Wx/InitializeProductDataStep.cs b/Store.Product/Publish/Steps/Wx/InitializeProductDataStep.cs index a5dfe1e..eda6755 100644 --- a/Store.Product/Publish/Steps/Wx/InitializeProductDataStep.cs +++ b/Store.Product/Publish/Steps/Wx/InitializeProductDataStep.cs @@ -48,8 +48,6 @@ public sealed class InitializeProductDataStep : WxProductPublishStepBase { if (context.Parameters.IsSkipHasFilterWordsProd) { - if (lstTitleReplaceText.Any(t => context.Parameters.SelectProduct.product_name.Contains(t))) - throw new Exception("命中敏感词"); if (lstTitleReplaceText.Any(t => context.Product.Title.Contains(t))) throw new Exception("命中敏感词"); var goods_name = context.Parameters.ProdJObject["detail"]["goods_name"].ParseToString(); @@ -70,7 +68,6 @@ public sealed class InitializeProductDataStep : WxProductPublishStepBase foreach (var txt in lstTitleReplaceText) { context.Product.Title = context.Product.Title.Replace(txt, ""); - context.Parameters.SelectProduct.product_name = context.Parameters.SelectProduct.product_name.Replace(txt, ""); } var goods_name = context.Parameters.ProdJObject["detail"]["goods_name"].ParseToString(); @@ -111,9 +108,6 @@ public sealed class InitializeProductDataStep : WxProductPublishStepBase if (context.Product.Title.Contains(ch) && context.Product.Title.IndexOf(ch) < context.Product.Title.Length / 2 && context.Product.Title.Length > 10) context.Product.Title = context.Product.Title.Substring(context.Product.Title.IndexOf(ch)).Trim(ch); - if (context.Parameters.SelectProduct.product_name.Contains(ch) && context.Parameters.SelectProduct.product_name.IndexOf(ch) < context.Parameters.SelectProduct.product_name.Length / 2 && context.Parameters.SelectProduct.product_name.Length > 10) - context.Parameters.SelectProduct.product_name = context.Parameters.SelectProduct.product_name.Substring(context.Parameters.SelectProduct.product_name.IndexOf(ch)).Trim(ch); - var goods_name = context.Parameters.ProdJObject["detail"]["goods_name"].ParseToString(); if (goods_name.Contains(ch) && goods_name.IndexOf(ch) < goods_name.Length / 2 && goods_name.Length > 10) context.Parameters.ProdJObject["detail"]["goods_name"] = goods_name.Substring(goods_name.IndexOf(ch)).Trim(ch); diff --git a/Store.Product/Publish/Steps/Wx/MIGRATION_NOTES.md b/Store.Product/Publish/Steps/Wx/MIGRATION_NOTES.md index 724f851..35591b8 100644 --- a/Store.Product/Publish/Steps/Wx/MIGRATION_NOTES.md +++ b/Store.Product/Publish/Steps/Wx/MIGRATION_NOTES.md @@ -21,6 +21,13 @@ 6. `WxShopTokenInfoList` 当前在入口处做了列表快照;若步骤内需要直接回写窗体静态列表,需要改为共享集合或增加回调。 7. `SubmitProductStep` 中的 `DataGridViewRow` 统计逻辑仍直接依赖表格控件,后续建议迁到运行时回调。 +## 新上传参数边界 + +- 商品内容在进入步骤流水线前完成组合,步骤内统一使用 `WXPublishContext.Product`,其类型为继承 `ProductBase` 的 `WxProductUploadProduct`。 +- 最终提交参数只在 `WXPublishParameters.ProductParameter` 上继续修改。 +- 裂变状态使用 `WXPublishParameters.IsFissionTitle`,不再从 `TemuDBProductSelect` 读取。 +- 上传任务结果使用 `WxProductPublishTask`,只保存状态、草稿 ID 和草稿参数,不参与商品内容判断。 + ## 步骤顺序 -步骤顺序由 `WxProductPublishPipelineFactory.CreateDefault()` 统一维护,和原 `UploadProduct` 从上到下的执行顺序一致。 \ No newline at end of file +步骤顺序由 `WxProductPublishPipelineFactory.CreateDefault()` 统一维护,和原 `UploadProduct` 从上到下的执行顺序一致。 diff --git a/Store.Product/Publish/Steps/Wx/SetProductTitleStep.cs b/Store.Product/Publish/Steps/Wx/SetProductTitleStep.cs index a303288..b10d758 100644 --- a/Store.Product/Publish/Steps/Wx/SetProductTitleStep.cs +++ b/Store.Product/Publish/Steps/Wx/SetProductTitleStep.cs @@ -34,9 +34,8 @@ public sealed class SetProductTitleStep : WxProductPublishStepBase protected override Task ExecuteProvider(WXPublishContext context, CancellationToken cancellationToken) { - context.Product.Title = context.Parameters.SelectProduct.product_name; if (context.Product.Title.IsNullOrEmpty()) - context.Product.Title = context.Product.Title.IsNullOrEmpty() ? context.Parameters.ProdInfo.detail.goods_name : context.Product.Title; + context.Product.Title = context.Parameters.ProdInfo.detail.goods_name; context.Parameters.InitParameter.product.info.title = context.Product.Title; context.Parameters.ProductParameter.product.info.title = context.Product.Title; diff --git a/Store.Product/Publish/Steps/Wx/SkipUploadedProductStep.cs b/Store.Product/Publish/Steps/Wx/SkipUploadedProductStep.cs index d502b93..e5b590d 100644 --- a/Store.Product/Publish/Steps/Wx/SkipUploadedProductStep.cs +++ b/Store.Product/Publish/Steps/Wx/SkipUploadedProductStep.cs @@ -46,7 +46,7 @@ public sealed class SkipUploadedProductStep : WxProductPublishStepBase protected override Task ExecuteProvider(WXPublishContext context, CancellationToken cancellationToken) { // 裂变按标题名称 - if (context.Parameters.SelectProduct.is_fission_title) + if (context.Parameters.IsFissionTitle) { HttpResult result = WXApi.searchProductWithTitle(context.Shop.Cookie, GetHtml.UrlEncodeUtf8(context.Product.Title)); string searchRst = result.Html; diff --git a/Store.Product/Publish/Steps/Wx/SubmitProductStep.cs b/Store.Product/Publish/Steps/Wx/SubmitProductStep.cs index 083797b..948449b 100644 --- a/Store.Product/Publish/Steps/Wx/SubmitProductStep.cs +++ b/Store.Product/Publish/Steps/Wx/SubmitProductStep.cs @@ -50,11 +50,12 @@ public sealed class SubmitProductStep : WxProductPublishStepBase if (context.Parameters.UpDraft) { - context.Parameters.SelectProduct.wxDraftProduct = context.Parameters.ProductParameter; - context.Parameters.SelectProduct.draft_id = context.Parameters.ProductParameter.product.productKey.productId; - context.Parameters.SelectProduct.status = "草稿箱"; - context.Parameters.SelectProduct.add_time = DateTime.Now; - context.Parameters.UploadDraftProductList.Add(context.Parameters.SelectProduct); + // Task 仅用于回写上传任务状态,最终商品数据只来自 ProductParameter。 + context.Parameters.Task.wxDraftProduct = context.Parameters.ProductParameter; + context.Parameters.Task.draft_id = context.Parameters.ProductParameter.product.productKey.productId; + context.Parameters.Task.status = "草稿箱"; + context.Parameters.Task.add_time = DateTime.Now; + context.Parameters.UploadDraftProductList.Add(context.Parameters.Task); NaiBa.Util.Helper.ConfigHelper.SaveConfig("wx_select_upload_draft_products", context.Parameters.UploadDraftProductList); StopWithResult("{\"success\":true,\"draftId\":" + context.Parameters.ProductParameter.product.productKey.productId + ",\"status\":\"草稿箱\",\"errMsg\":\"指定上传到草稿箱" + context.Parameters.CommissionRstStr + "\"}"); diff --git a/Store.Product/Publish/Steps/Wx/WXPublishContext.cs b/Store.Product/Publish/Steps/Wx/WXPublishContext.cs index 12e9ca0..471f2cf 100644 --- a/Store.Product/Publish/Steps/Wx/WXPublishContext.cs +++ b/Store.Product/Publish/Steps/Wx/WXPublishContext.cs @@ -3,10 +3,31 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Store.Product.Models.Product; namespace Store.Product.Publish.Steps.Wx; public class WXPublishContext : PublishContextBase { + private WxProductUploadProduct _product = new WxProductUploadProduct(); + + /// + /// 微信商品上传模型 + /// + public new WxProductUploadProduct Product + { + get => _product; + set + { + _product = value ?? new WxProductUploadProduct(); + base.Product = _product; + } + } + + public WXPublishContext() + { + base.Product = _product; + } + public IProgress Progress { get; set; } } diff --git a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs index c623ae6..6e31b46 100644 --- a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs +++ b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs @@ -1326,7 +1326,7 @@ namespace TemuKJD.WXForms.ProductForms { this.pbLoading.Visible = false; })); - + GlobalConfig.ShopLoginList Task.Run(() => { DateTime startTime = DateTime.Now; diff --git a/test/Store.Test/Store.Test.csproj b/test/Store.Test/Store.Test.csproj index 1abc697..a9e8144 100644 --- a/test/Store.Test/Store.Test.csproj +++ b/test/Store.Test/Store.Test.csproj @@ -2,6 +2,7 @@ net462 + x64 latest enable enable @@ -17,6 +18,7 @@ + @@ -24,4 +26,4 @@ - \ No newline at end of file + diff --git a/test/Store.Test/UnitTest1.cs b/test/Store.Test/UnitTest1.cs index 08c1a36..bc66f6e 100644 --- a/test/Store.Test/UnitTest1.cs +++ b/test/Store.Test/UnitTest1.cs @@ -1,4 +1,4 @@ -using Autofac; +using Autofac; using LittleCow.Core.Infrastructure; using MAX.Core.Autofac; using MAX.Core.Cryptography; @@ -7,6 +7,10 @@ using Store.Client; using Store.Client.Apis; using Store.Client.HttpExtension; using Store.Client.Services; +using Store.Framework.Models; +using Store.Product.Models.Product; +using Store.Product.Publish; +using Store.Product.Publish.Steps.Wx; namespace Store.Test; @@ -15,7 +19,8 @@ public class Tests [SetUp] public void Setup() { - Bootstrap.Builder(); + Bootstrap.Builder(); + TestUserLogin(); } [Test] @@ -28,5 +33,156 @@ public class Tests }); //new UserApi().Login("18990991234", MD5Helper.Md5_Lower32("18990991234")); } + [Test] + public void TestGetShops() + { + var ss = IocManager.Resolve().GetShops(); + } + [Test] + public void TestNewWxUploadProductUsesProductBase() + { + var context = new WXPublishContext(); + var product = new WxProductUploadProduct + { + Title = "新版微信商品", + SpuId = "wx-spu-1001", + WxCatLeaf = 547591 + }; -} \ No newline at end of file + context.Product = product; + + Assert.Multiple(() => + { + Assert.That(context.Product, Is.TypeOf()); + Assert.That(context.Product, Is.InstanceOf()); + Assert.That(((PublishContextBase)context).Product, Is.SameAs(product)); + Assert.That(context.Product.Title, Is.EqualTo("新版微信商品")); + Assert.That(context.Product.WxCatLeaf, Is.EqualTo(547591)); + }); + } + + [Test] + public async Task TestNewWxUploadFissionTitleUpdatesProductModel() + { + var context = new WXPublishContext + { + Product = new WxProductUploadProduct { Title = "原商品标题" }, + Progress = new Progress(_ => { }) + }; + context.Parameters.IsFissionTitle = true; + context.Parameters.FissionTitles = "裂变标题一\r\n裂变标题二"; + + var step = new FissionTitleStep(); + await step.ExecuteAsync(context, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(context.Product.Title, Is.EqualTo("裂变标题一")); + Assert.That(context.Parameters.IsFissionTitle, Is.True); + Assert.That(context.Parameters.FissionTitles, Is.EqualTo("裂变标题二")); + }); + } + + [Test] + public async Task TestNewWxUploadSetTitleWritesFinalParameters() + { + var context = new WXPublishContext + { + Product = new WxProductUploadProduct { Title = "最终上传标题" }, + Progress = new Progress(_ => { }) + }; + + var step = new SetProductTitleStep(); + await step.ExecuteAsync(context, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(context.Parameters.InitParameter.product.info.title, Is.EqualTo("最终上传标题")); + Assert.That(context.Parameters.ProductParameter.product.info.title, Is.EqualTo("最终上传标题")); + Assert.That( + context.Parameters.ProductParameter.sessionInfo.sessionId, + Is.EqualTo(context.Parameters.InitParameter.product.sessionId)); + }); + } + + [Test] + public async Task TestNewWxUploadPipelineRunsCompositionToSubmit() + { + var context = new WXPublishContext + { + Shop = new Shop + { + Name = "乐乐人小店", + Cookie = new Cookies("biz_token=b_000001a0_80071145_44612ea2_a87897a5_12c2c339; biz_ticket=; biz_magic=09bcc3e825c8826c562d62ca3442e35e8eb4ce13b73e4097661f11c9bd90f46e; biz_rand=CAESIO762C8Iu/hxVBpyNsXZiqDGlOsa5U3WBKBd3PvU0p6d; device_id=7b7080668b28f2aadba6d7f208ca35833c4ce75fe4c107eca6a5e5785fccc8ad") + }, + Product = new WxProductUploadProduct { Title = "原商品标题" }, + Progress = new Progress(_ => { }) + }; + context.Parameters.IsFissionTitle = true; + context.Parameters.FissionTitles = "裂变商品标题"; + + var pipeline = new WXPublishPipeline(new IPublishStep[] + { + new FissionTitleStep(), + new SkipUploadedProductStep(), + new SetProductTitleStep(), + new TestSubmitProductStep() + }); + + await pipeline.ExecuteAsync(context, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(context.Product.Title, Is.EqualTo("裂变商品标题")); + Assert.That(context.Parameters.InitParameter.product.info.title, Is.EqualTo("裂变商品标题")); + Assert.That(context.Parameters.ProductParameter.product.info.title, Is.EqualTo("裂变商品标题")); + Assert.That(context.Parameters.Result, Is.EqualTo("{\"success\":true}")); + Assert.That(context.Parameters.IsCompleted, Is.True); + }); + } + + private sealed class TestSubmitProductStep : IPublishStep + { + public string Description => "测试提交商品"; + + public Task CanExecuteAsync(WXPublishContext context, CancellationToken token) + { + return Task.FromResult(true); + } + + public Task ExecuteAsync(WXPublishContext context, CancellationToken token) + { + var title = context.Parameters.ProductParameter.product.info.title; + if (title != context.Product.Title) + { + throw new InvalidOperationException("提交参数中的标题不是最终组合标题。"); + } + + context.Parameters.Result = "{\"success\":true}"; + context.Parameters.IsCompleted = true; + return Task.CompletedTask; + } + } + + private sealed class AlwaysExecuteStep : IPublishStep + { + private readonly IPublishStep _inner; + + public AlwaysExecuteStep(IPublishStep inner) + { + _inner = inner; + } + + public string Description => _inner.Description; + + public Task CanExecuteAsync(WXPublishContext context, CancellationToken token) + { + return Task.FromResult(true); + } + + public Task ExecuteAsync(WXPublishContext context, CancellationToken token) + { + return _inner.ExecuteAsync(context, token); + } + } +}