diff --git a/SBF.API/.config/dotnet-tools.json b/SBF.API/.config/dotnet-tools.json
new file mode 100644
index 0000000..b0e38ab
--- /dev/null
+++ b/SBF.API/.config/dotnet-tools.json
@@ -0,0 +1,5 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {}
+}
\ No newline at end of file
diff --git a/SBF.API/Controllers/BaseApiController.cs b/SBF.API/Controllers/BaseApiController.cs
new file mode 100644
index 0000000..524f1db
--- /dev/null
+++ b/SBF.API/Controllers/BaseApiController.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Primitives;
+
+namespace SBF.API.Controllers
+{
+ [Produces("application/json")]
+ [Route("Api/[Controller]/[Action]")]
+ [ApiController]
+ [EnableCors("cors")]
+ public class BaseApiController : ControllerBase
+ {
+ protected IHttpContextAccessor httpContextAccessor;
+ public BaseApiController(IHttpContextAccessor httpContextAccessor)
+ {
+ this.httpContextAccessor = httpContextAccessor;
+ }
+
+ protected string GetUserId()
+ {
+ return httpContextAccessor?.HttpContext?.User.Claims.Where(x => x.Type == "userId")?.FirstOrDefault()?.Value;
+ }
+
+ protected string GetToken()
+ {
+ httpContextAccessor.HttpContext.Request.Headers.TryGetValue("Authorization", out StringValues token);
+ return token;
+ }
+
+ protected string GetClientCode()
+ {
+ httpContextAccessor.HttpContext.Request.Headers.TryGetValue("ClientCode", out StringValues clientCode);
+ return clientCode;
+ }
+ }
+}
diff --git a/SBF.API/Controllers/TrusteeshipController.cs b/SBF.API/Controllers/TrusteeshipController.cs
new file mode 100644
index 0000000..5bcc785
--- /dev/null
+++ b/SBF.API/Controllers/TrusteeshipController.cs
@@ -0,0 +1,49 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using SBF.Business;
+using SBF.Model.Dto;
+
+namespace SBF.API.Controllers
+{
+
+ public class TrusteeshipController : BaseApiController
+ {
+ private TrusteeshipBusiness trusteeshipBusiness;
+ public TrusteeshipController(IHttpContextAccessor httpContextAccessor, TrusteeshipBusiness trusteeshipBusiness) : base(httpContextAccessor)
+ {
+ this.trusteeshipBusiness = trusteeshipBusiness;
+ }
+
+ ///
+ /// 搜索Sku参与的推广渠道
+ ///
+ ///
+ ///
+ [HttpPost]
+ public IList SearchSkuJoinPopularizeChannel([FromBody] SearchSkuJoinPopularizeChannelRequest request)
+ {
+ return trusteeshipBusiness.SearchSkuJoinPopularizeChannel(request);
+ }
+
+ ///
+ /// 查询托管任务列表
+ ///
+ ///
+ ///
+ [HttpPost]
+ public ListResponse QueryTrusteeship([FromBody] QueryTrusteeshipRequest request)
+ {
+ return trusteeshipBusiness.QueryTrusteeship(request);
+ }
+
+ ///
+ /// 创建托管任务
+ ///
+ ///
+ [HttpPost]
+ public void CreateTrusteeship([FromBody] CreateTrusteeshipRequest request)
+ {
+ trusteeshipBusiness.CreateTrusteeship(request);
+ }
+ }
+}
diff --git a/SBF.API/Filters/ResultFilter.cs b/SBF.API/Filters/ResultFilter.cs
new file mode 100644
index 0000000..9739f92
--- /dev/null
+++ b/SBF.API/Filters/ResultFilter.cs
@@ -0,0 +1,30 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using SBF.Common.Models;
+
+namespace SBF.API.Filters
+{
+ public class ResultFilter : IResultFilter
+ {
+ public void OnResultExecuted(ResultExecutedContext context)
+ {
+
+ }
+
+ public void OnResultExecuting(ResultExecutingContext context)
+ {
+ if (context.Result is ObjectResult)
+ {
+ var objectResult = context.Result as ObjectResult;
+ if (!(objectResult.Value is ApiResponse))
+ {
+ objectResult.Value = new ApiResponse() { Data = objectResult.Value };
+ }
+ }
+ else if (context.Result is EmptyResult)
+ {
+ context.Result = new ObjectResult(new ApiResponse());
+ }
+ }
+ }
+}
diff --git a/SBF.API/Middlewares/ClientVersionValidationMiddleWare.cs b/SBF.API/Middlewares/ClientVersionValidationMiddleWare.cs
new file mode 100644
index 0000000..78a15f2
--- /dev/null
+++ b/SBF.API/Middlewares/ClientVersionValidationMiddleWare.cs
@@ -0,0 +1,55 @@
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using SBF.Common.Models;
+
+namespace SBF.API.Middlewares
+{
+ public class ClientVersionValidationMiddleWare
+ {
+ ///
+ /// 管道请求委托
+ ///
+ private RequestDelegate _next;
+
+ private IDictionary apiVersionDictionary;
+
+ private IOptionsMonitor> _monitor;
+
+ public ClientVersionValidationMiddleWare(RequestDelegate requestDelegate, IOptionsMonitor> monitor)
+ {
+ _next = requestDelegate;
+ _monitor = monitor;
+ apiVersionDictionary = new Dictionary();
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ Console.WriteLine(context.Request.Path);
+ var apiRequirement = _monitor.CurrentValue.FirstOrDefault(x => x.Api.Equals(context.Request.Path, StringComparison.CurrentCultureIgnoreCase));
+ if (apiRequirement != null)
+ {
+ if (!context.Request.Headers.TryGetValue("ClientVersion", out StringValues clientVersionStr))
+ throw new BusinessException("缺少版本信息,请更新司南");
+ if (!int.TryParse(clientVersionStr, out int clientVersion))
+ throw new BusinessException("版本信息不正确,请更新司南");
+ if (clientVersion < apiRequirement.MinimumVersion)
+ throw new BusinessException("当前请求需更新司南");
+ }
+ await _next(context); //调用管道执行下一个中间件
+ }
+ catch
+ {
+ throw;
+ }
+ }
+ }
+
+ public class ClientVersionValidationModel
+ {
+ public string Api { get; set; }
+
+ public int MinimumVersion { get; set; }
+ }
+}
diff --git a/SBF.API/Middlewares/CustomExceptionMiddleWare.cs b/SBF.API/Middlewares/CustomExceptionMiddleWare.cs
new file mode 100644
index 0000000..ac949ac
--- /dev/null
+++ b/SBF.API/Middlewares/CustomExceptionMiddleWare.cs
@@ -0,0 +1,86 @@
+using Newtonsoft.Json;
+using SBF.Common.Log;
+using SBF.Common.Models;
+using System.Text;
+
+namespace SBF.API.Middlewares
+{
+ public class CustomExceptionMiddleWare
+ {
+ ///
+ /// 管道请求委托
+ ///
+ private RequestDelegate _next;
+
+ ///
+ /// 需要处理的状态码字典
+ ///
+ private IDictionary _exceptionStatusCodeDic;
+
+ //private NLogManager nLogManager;
+
+ private NLogManager nLogManager;
+
+ public CustomExceptionMiddleWare(RequestDelegate next, NLogManager nLogManager)
+ {
+ _next = next;
+ //this.logger = logger;
+ this.nLogManager = nLogManager;
+ _exceptionStatusCodeDic = new Dictionary
+ {
+ { 401, "未授权的请求" },
+ { 404, "找不到该资源" },
+ { 403, "访问被拒绝" },
+ { 500, "服务器发生意外的错误" },
+ { 503, "服务不可用" }
+ //其余状态自行扩展
+ };
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await _next(context); //调用管道执行下一个中间件
+ }
+ catch (Exception ex)
+ {
+ if (ex is BusinessException)
+ {
+ var busEx = ex as BusinessException;
+ context.Response.StatusCode = 200; //业务异常时将Http状态码改为200
+ await ErrorHandle(context, busEx.Code, busEx.Message);
+ }
+ else
+ {
+ context.Response.Clear();
+ context.Response.StatusCode = 500; //发生未捕获的异常,手动设置状态码
+ //logger.Error(ex); //记录错误
+ nLogManager.Default().Error(ex);
+ }
+ }
+ finally
+ {
+ if (_exceptionStatusCodeDic.TryGetValue(context.Response.StatusCode, out string exMsg))
+ {
+ await ErrorHandle(context, context.Response.StatusCode, exMsg);
+ }
+ }
+ }
+
+ ///
+ /// 处理方式:返回Json格式
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task ErrorHandle(HttpContext context, int code, string exMsg)
+ {
+ var apiResponse = ApiResponse.Error(code, exMsg);
+ var serialzeStr = JsonConvert.SerializeObject(apiResponse);
+ context.Response.ContentType = "application/json";
+ await context.Response.WriteAsync(serialzeStr, Encoding.UTF8);
+ }
+ }
+}
diff --git a/SBF.API/Program.cs b/SBF.API/Program.cs
new file mode 100644
index 0000000..7ee8d88
--- /dev/null
+++ b/SBF.API/Program.cs
@@ -0,0 +1,131 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.OpenApi.Models;
+using Newtonsoft.Json.Serialization;
+using SBF.API.Filters;
+using SBF.API.Middlewares;
+using SBF.Business;
+using SBF.Common.Extensions;
+using SBF.Common.Http;
+using SBF.Common.Log;
+using SBF.Common.Models;
+using SBF.Model;
+using System.Reflection;
+using Yitter.IdGenerator;
+
+var builder = WebApplication.CreateBuilder(args);
+var services = builder.Services;
+var configuration = builder.Configuration;
+
+services.AddMemoryCache();
+var idOption = new IdGeneratorOptions(1);
+var idGenerator = new DefaultIdGenerator(idOption);
+services.AddSingleton(typeof(IIdGenerator), idGenerator);
+
+var fsql = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, configuration.GetConnectionString("BBWYCDB")).Build();
+services.AddSingleton(typeof(IFreeSql), fsql);
+var fsql2 = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, configuration.GetConnectionString("MDSDB")).Build();
+var fsql3 = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, configuration.GetConnectionString("XXDB")).Build();
+
+services.AddSingleton(new FreeSqlMultiDBManager()
+{
+ MDSfsql = fsql2,
+ BBWYCfsql = fsql,
+ XXfsql = fsql3
+});
+
+services.AddSingleton();
+services.AddSingleton();
+services.AddSingleton();
+services.BatchRegisterServices(new Assembly[] { Assembly.Load("SBF.Business") }, typeof(IDenpendency), ServiceLifetime.Singleton);
+services.AddMemoryCache();
+services.AddControllers();
+services.AddHttpContextAccessor();
+services.AddHttpClient();
+services.AddHttpClient("gzip").ConfigurePrimaryHttpMessageHandler(handler => new HttpClientHandler()
+{
+ AutomaticDecompression = System.Net.DecompressionMethods.GZip
+});
+services.AddCors(options =>
+{
+ options.AddPolicy("cors", p =>
+ {
+ p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
+ });
+});
+services.AddControllers(c =>
+{
+ c.Filters.Add();
+ c.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
+}).AddNewtonsoftJson(setupAction =>
+{
+ setupAction.SerializerSettings.ContractResolver = new DefaultContractResolver();
+ setupAction.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
+ //setupAction.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
+ //setupAction.SerializerSettings.DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Include;
+});
+
+services.AddMapper(new MappingProfiles());
+services.AddEndpointsApiExplorer();
+services.AddSwaggerGen(c =>
+{
+ c.SwaggerDoc("v1", new OpenApiInfo
+ {
+ Version = "v1.0.0",
+ Title = "SBF API",
+ Description = "ע\r\n1.زƲôշ\r\n2.ApiResponseΪض(Code,Data,Message),ӿеķֵData\r\n3.Code=200"
+ });
+ //JWT֤
+ c.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
+ {
+ Scheme = JwtBearerDefaults.AuthenticationScheme,
+ BearerFormat = "JWT",
+ Type = SecuritySchemeType.ApiKey,
+ Name = "Authorization",
+ In = ParameterLocation.Header,
+ Description = "Authorization:Bearer {your JWT token}
",
+ });
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme{Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = JwtBearerDefaults.AuthenticationScheme
+ }
+ },
+ new string[] { }
+ }
+ });
+
+ var executingAssembly = Assembly.GetExecutingAssembly();
+ var assemblyNames = executingAssembly.GetReferencedAssemblies().Union(new AssemblyName[] { executingAssembly.GetName() }).ToArray();
+ Array.ForEach(assemblyNames, (assemblyName) =>
+ {
+ //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ var xmlFile = $"{assemblyName.Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (!File.Exists(xmlPath))
+ return;
+ c.IncludeXmlComments(xmlPath, true);
+ });
+});
+
+
+
+var app = builder.Build();
+
+app.UseSwagger(c => c.SerializeAsV2 = true)
+ .UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "SiNan API");
+ c.RoutePrefix = string.Empty;
+ });
+
+app.UseMiddleware();
+app.UseRouting();
+app.UseCors("cors");
+app.UseMiddleware();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
diff --git a/SBF.API/Properties/launchSettings.json b/SBF.API/Properties/launchSettings.json
new file mode 100644
index 0000000..fcb6328
--- /dev/null
+++ b/SBF.API/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:13038",
+ "sslPort": 44318
+ }
+ },
+ "profiles": {
+ "SBF.API": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "https://localhost:7055;http://localhost:5055",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/SBF.API/SBF.API.csproj b/SBF.API/SBF.API.csproj
new file mode 100644
index 0000000..0f5e3ae
--- /dev/null
+++ b/SBF.API/SBF.API.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net6.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SBF.API/appsettings.Development.json b/SBF.API/appsettings.Development.json
new file mode 100644
index 0000000..770d3e9
--- /dev/null
+++ b/SBF.API/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "DetailedErrors": true,
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/SBF.API/appsettings.json b/SBF.API/appsettings.json
new file mode 100644
index 0000000..5306809
--- /dev/null
+++ b/SBF.API/appsettings.json
@@ -0,0 +1,16 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Secret": "D96BFA5B-F2AF-45BC-9342-5A55C3F9BBB0",
+ "ConnectionStrings": {
+ //"DB": "data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;port=3306;user id=qyroot;password=kaicn1132+-;initial catalog=bbwy;charset=utf8;sslmode=none;"
+ "BBWYCDB": "data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;port=3306;user id=qyroot;password=kaicn1132+-;initial catalog=bbwy_test;charset=utf8;sslmode=none;",
+ "MDSDB": "data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;port=3306;user id=qyroot;password=kaicn1132+-;initial catalog=mds;charset=utf8;sslmode=none;",
+ "XXDB": "data source=rm-bp1508okrh23710yfao.mysql.rds.aliyuncs.com;user id=qyroot;password=kaicn1132+-;initial catalog=jdxx;charset=utf8;sslmode=none;"
+ }
+}
diff --git a/SBF.Business/BaseBusiness.cs b/SBF.Business/BaseBusiness.cs
new file mode 100644
index 0000000..336e2dd
--- /dev/null
+++ b/SBF.Business/BaseBusiness.cs
@@ -0,0 +1,19 @@
+using SBF.Common.Log;
+using Yitter.IdGenerator;
+
+namespace SiNan.Business
+{
+ public class BaseBusiness
+ {
+ protected IFreeSql fsql;
+ protected NLogManager nLogManager;
+ protected IIdGenerator idGenerator;
+
+ public BaseBusiness(IFreeSql fsql, NLogManager nLogManager, IIdGenerator idGenerator)
+ {
+ this.fsql = fsql;
+ this.nLogManager = nLogManager;
+ this.idGenerator = idGenerator;
+ }
+ }
+}
diff --git a/SBF.Business/FreeSqlMultiDBManager.cs b/SBF.Business/FreeSqlMultiDBManager.cs
new file mode 100644
index 0000000..445627f
--- /dev/null
+++ b/SBF.Business/FreeSqlMultiDBManager.cs
@@ -0,0 +1,13 @@
+namespace SBF.Business
+{
+ public class FreeSqlMultiDBManager
+ {
+ //public IFreeSql BBWYBfsql { get; set; }
+
+ public IFreeSql MDSfsql { get; set; }
+
+ public IFreeSql BBWYCfsql { get; set; }
+
+ public IFreeSql XXfsql { get; set; }
+ }
+}
diff --git a/SBF.Business/SBF.Business.csproj b/SBF.Business/SBF.Business.csproj
new file mode 100644
index 0000000..baf0b83
--- /dev/null
+++ b/SBF.Business/SBF.Business.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net6.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SBF.Business/TaskSchedulerManager.cs b/SBF.Business/TaskSchedulerManager.cs
new file mode 100644
index 0000000..9c1d764
--- /dev/null
+++ b/SBF.Business/TaskSchedulerManager.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks.Schedulers;
+
+namespace SBF.Business
+{
+ public class TaskSchedulerManager
+ {
+ public LimitedConcurrencyLevelTaskScheduler AggregationSpuGOIScheduler { get; private set; }
+
+
+ public LimitedConcurrencyLevelTaskScheduler AggregationCampaignGOIScheduler { get; private set; }
+
+ public LimitedConcurrencyLevelTaskScheduler AggregationAdGroupGOIScheduler { get; private set; }
+
+ public TaskSchedulerManager()
+ {
+ AggregationSpuGOIScheduler = new LimitedConcurrencyLevelTaskScheduler(5);
+ AggregationCampaignGOIScheduler = new LimitedConcurrencyLevelTaskScheduler(5);
+ AggregationAdGroupGOIScheduler = new LimitedConcurrencyLevelTaskScheduler(5);
+ }
+ }
+}
diff --git a/SBF.Business/TrusteeshipBusiness.cs b/SBF.Business/TrusteeshipBusiness.cs
new file mode 100644
index 0000000..c5904dd
--- /dev/null
+++ b/SBF.Business/TrusteeshipBusiness.cs
@@ -0,0 +1,253 @@
+using SBF.Common.Extensions;
+using SBF.Common.Log;
+using SBF.Common.Models;
+using SBF.Model.Db;
+using SBF.Model.Dto;
+using SiNan.Business;
+using Yitter.IdGenerator;
+
+namespace SBF.Business
+{
+ public class TrusteeshipBusiness : BaseBusiness, IDenpendency
+ {
+ public TrusteeshipBusiness(IFreeSql fsql, NLogManager nLogManager, IIdGenerator idGenerator) : base(fsql, nLogManager, idGenerator)
+ {
+ }
+
+ ///
+ /// 搜索SKU参与的推广渠道
+ ///
+ ///
+ ///
+ public IList SearchSkuJoinPopularizeChannel(SearchSkuJoinPopularizeChannelRequest request)
+ {
+ if (request.ShopId == null || request.ShopId == 0)
+ throw new BusinessException("缺少ShopId");
+
+
+ var skuList = new List();
+ if (request.SkuList != null && request.SkuList.Count() > 0)
+ {
+ request.Sku = string.Empty;
+ request.Spu = string.Empty;
+ skuList.AddRange(request.SkuList);
+ }
+
+ if (!string.IsNullOrEmpty(request.Sku))
+ {
+ request.Spu = string.Empty;
+ skuList.Add(request.Sku);
+ }
+
+ if (!string.IsNullOrEmpty(request.Spu))
+ {
+ skuList.AddRange(fsql.Select()
+ .InnerJoin((ps, p) => ps.ProductId == p.Id)
+ .Where((ps, p) => ps.State == 1 && p.State == 8)
+ .WhereIf(!string.IsNullOrEmpty(request.Spu), (ps, p) => ps.ProductId == request.Spu)
+ .ToList((ps, p) => ps.Id));
+ }
+
+ if (skuList.Count == 0)
+ throw new BusinessException("缺少sku信息");
+
+ var yesterDay = DateTime.Now.Date.AddDays(-1);
+ var list = fsql.Select()
+ .LeftJoin((ads, c, ad) => ads.CampaignId == c.CampaignId && ads.Date == c.Date)
+ .LeftJoin((ads, c, ad) => ads.AdGroupId == ad.AdGroupId && ads.Date == ad.Date)
+ .Where((ads, c, ad) => ads.ShopId == request.ShopId && ads.Date == yesterDay)
+ .WhereIf(skuList.Count() > 1, (ads, c, ad) => skuList.Contains(ads.Sku))
+ .WhereIf(skuList.Count() == 1, (ads, c, ad) => ads.Sku == skuList[0])
+ .Where((ads, c, ad) => !fsql.Select()
+ .Where(s => s.ShopId == request.ShopId && s.CampaignId == ads.CampaignId && s.SkuId == ads.Sku)
+ .Any())
+ .GroupBy((ads, c, ad) => new { ads.BusinessType, ads.CampaignId, c.CampaignName, ads.AdGroupId, ad.AdGroupName, ads.AdId, ads.AdName, ads.Sku })
+ .ToList(g => new SkuJoinPopularizeChannelResponse
+ {
+ BusinessType = g.Key.BusinessType.Value,
+ CampaignId = g.Key.CampaignId,
+ CampaignName = g.Key.CampaignName,
+ AdGroupId = g.Key.AdGroupId,
+ AdGroupName = g.Key.AdGroupName,
+ AdId = g.Key.AdId,
+ AdName = g.Key.AdName,
+ Sku = g.Key.Sku
+ });
+ return list;
+ }
+
+
+ ///
+ /// 查询托管任务
+ ///
+ ///
+ ///
+ public ListResponse QueryTrusteeship(QueryTrusteeshipRequest request)
+ {
+ if (request.ShopId == null || request.ShopId == 0)
+ throw new BusinessException("缺少ShopId");
+
+ var list = fsql.Select()
+ .InnerJoin((t, p, ps) => t.SpuId == p.Id)
+ .InnerJoin((t, p, ps) => t.SkuId == ps.Id)
+ .Where((t, p, ps) => t.ShopId == request.ShopId)
+ .WhereIf(request.Stage != null, (t, p, ps) => p.Stage == request.Stage)
+ .WhereIf(!string.IsNullOrEmpty(request.Spu), (t, p, ps) => t.SpuId == request.Spu)
+ .WhereIf(!string.IsNullOrEmpty(request.Sku), (t, p, ps) => t.SkuId == request.Sku)
+ .WhereIf(!string.IsNullOrEmpty(request.Title), (t, p, ps) => p.Title.StartsWith(request.Title))
+ .OrderByDescending((t, p, ps) => t.CreateTime)
+ .Page(request.PageIndex, request.PageSize)
+ .Count(out var count)
+ .ToList((t, p, ps) => new Sbf_TrusteeshipTask()
+ {
+ Id = t.Id,
+ ShopId = t.ShopId,
+ SpuId = t.SpuId,
+ SkuId = t.SkuId,
+ ActualAmountInTrusteeship = t.ActualAmountInTrusteeship,
+ AdGroupId = t.AdGroupId,
+ AdGroupName = t.AdGroupName,
+ AdId = t.AdId,
+ AdName = t.AdName,
+ BidPrice = t.BidPrice,
+ Budget = t.Budget,
+ BusinessType = t.BusinessType,
+ CampaginName = t.CampaginName,
+ CampaignId = t.CampaignId,
+ CostInTrusteeship = t.CostInTrusteeship,
+ CreateTime = t.CreateTime,
+ EndTime = t.EndTime,
+ IsEnd = t.IsEnd,
+ StartTrusteeshipDate = t.StartTrusteeshipDate,
+
+ Logo = ps.Logo,
+ Price = ps.Price,
+ SkuTitle = ps.Title,
+ SkuState = ps.State,
+ SkuCreateTime = ps.CreateTime,
+ CategoryId = ps.CategoryId,
+ CategoryName = ps.CategoryName,
+
+ MainSkuId = p.MainSkuId,
+ ProductCreateTime = p.CreateTime,
+ ProductItemNum = p.ProductItemNum,
+ ProductState = p.State,
+ ProductTitle = p.Title,
+ Stage = p.Stage,
+ Platform = p.Platform
+ })
+ .Map>();
+
+ var startDate = DateTime.Now.Date.AddDays(-7);
+ var endDate = DateTime.Now.Date.AddDays(-1);
+
+ var skuIdList = list.Select(x => x.SkuId).Distinct().ToList();
+ var spuIdList = list.Select(x => x.SpuId).Distinct().ToList();
+
+
+ #region 推广花费
+ var costList = fsql.Select()
+ .Where(x => x.ShopId == request.ShopId &&
+ x.Date >= startDate && x.Date <= endDate &&
+ skuIdList.Contains(x.SkuId))
+ .ToList(x => new
+ {
+ x.Date,
+ x.SkuId,
+ x.CampaignId,
+ x.BusinessType,
+ x.Cost
+ });
+ //.GroupBy(x => new { x.Date, x.SkuId, x.CampaignId, x.BusinessType })
+ //.ToList(g => new
+ //{
+ // Date = g.Key.Date,
+ // SkuId = g.Key.SkuId,
+ // CampaignId = g.Key.CampaignId,
+ // BusinessType = g.Key.BusinessType,
+ // Cost = g.Sum(g.Value.Cost)
+ //});
+ #endregion
+
+ #region 商品营业额
+ var actualAmountList = fsql.Select()
+ .Where(x => x.ShopId == request.ShopId &&
+ x.Date >= startDate && x.Date <= endDate &&
+ spuIdList.Contains(x.ProductId))
+ .ToList(x => new
+ {
+ x.Date,
+ x.ProductId,
+ x.Cost,
+ x.ActualAmount
+ });
+
+ #endregion
+
+ #region 免费访客量
+
+ #endregion
+
+ foreach (var task in list)
+ {
+ task.CostByDateList = costList.Where(x => x.SkuId == task.SkuId && x.CampaignId == task.CampaignId && x.BusinessType == task.BusinessType)
+ .OrderBy(x => x.Date)
+ .Select(x => new NumberByDate()
+ {
+ Date = x.Date,
+ Value = x.Cost
+ }).ToList();
+
+ task.ActualAmountByDateList = actualAmountList.Where(x => x.ProductId == task.SpuId)
+ .OrderBy(x => x.Date)
+ .Select(x => new NumberByDate()
+ {
+ Date = x.Date,
+ Value = x.ActualAmount
+ }).ToList();
+ }
+
+ return new ListResponse() { ItemList = list, Count = count };
+ }
+
+ public void CreateTrusteeship(CreateTrusteeshipRequest request)
+ {
+ if (request.SkuList == null || request.SkuList.Count() == 0)
+ throw new BusinessException("缺少SkuList");
+ var joinList = SearchSkuJoinPopularizeChannel(new SearchSkuJoinPopularizeChannelRequest()
+ {
+ ShopId = request.ShopId,
+ SkuList = request.SkuList
+ });
+
+ var insertList = joinList.Select(x => new Sbf_TrusteeshipTask()
+ {
+ Id = idGenerator.NewLong(),
+ ActualAmountInTrusteeship = 0M,
+ AdGroupId = x.AdGroupId,
+ AdGroupName = x.AdGroupName,
+ AdId = x.AdId,
+ AdName = x.AdName,
+ BidPrice = 0M,
+ Budget = 0M,
+ BusinessType = x.BusinessType,
+ CampaginName = x.CampaignName,
+ CampaignId = x.CampaignId,
+ CostInTrusteeship = 0M,
+ CreateTime = DateTime.Now,
+ EndTime = null,
+ IsEnd = false,
+ StartTrusteeshipDate = DateTime.Now.Date.AddDays(1),
+ ShopId = request.ShopId,
+ SkuId = x.Sku
+ }).ToList();
+
+ var skuIdList = insertList.Select(x => x.SkuId).Distinct().ToList();
+ var psList = fsql.Select(skuIdList).ToList();
+ foreach (var insertTask in insertList)
+ insertTask.SpuId = psList.FirstOrDefault(ps => ps.Id == insertTask.SkuId)?.ProductId;
+
+ fsql.Insert(insertList).ExecuteAffrows();
+ }
+ }
+}
diff --git a/SBF.Common/Extensions/ConverterExtensions.cs b/SBF.Common/Extensions/ConverterExtensions.cs
new file mode 100644
index 0000000..31df3ef
--- /dev/null
+++ b/SBF.Common/Extensions/ConverterExtensions.cs
@@ -0,0 +1,29 @@
+namespace SBF.Common.Extensions
+{
+ public static class ConverterExtensions
+ {
+ public static long? ToInt64(this object? o)
+ {
+ try
+ {
+ return Convert.ToInt64(o);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public static int? ToInt32(this object? o)
+ {
+ try
+ {
+ return Convert.ToInt32(o);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/SBF.Common/Extensions/CopyExtensions.cs b/SBF.Common/Extensions/CopyExtensions.cs
new file mode 100644
index 0000000..e4c9867
--- /dev/null
+++ b/SBF.Common/Extensions/CopyExtensions.cs
@@ -0,0 +1,12 @@
+using Newtonsoft.Json;
+
+namespace SBF.Common.Extensions
+{
+ public static class CopyExtensions
+ {
+ public static T Copy(this T p)
+ {
+ return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(p));
+ }
+ }
+}
diff --git a/SBF.Common/Extensions/DateTimeExtension.cs b/SBF.Common/Extensions/DateTimeExtension.cs
new file mode 100644
index 0000000..8c5e241
--- /dev/null
+++ b/SBF.Common/Extensions/DateTimeExtension.cs
@@ -0,0 +1,99 @@
+using System.Runtime.InteropServices;
+
+namespace SBF.Common.Extensions
+{
+ public static class DateTimeExtension
+ {
+ private static readonly DateTime beginTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
+
+ ///
+ /// 时间戳转时间
+ ///
+ /// 时间
+ /// true:13, false:10
+ ///
+ [Obsolete]
+ public static DateTime StampToDateTime(this long timeStamp)
+ {
+ DateTime dt = TimeZone.CurrentTimeZone.ToLocalTime(beginTime);
+ return timeStamp.ToString().Length == 13 ? dt.AddMilliseconds(timeStamp) : dt.AddSeconds(timeStamp);
+ }
+
+ ///
+ /// 时间转时间戳
+ ///
+ /// 时间
+ /// true:13, false:10
+ ///
+ public static long DateTimeToStamp(this DateTime time, bool len13 = true)
+ {
+ TimeSpan ts = time.ToUniversalTime() - beginTime;
+ if (len13)
+ return Convert.ToInt64(ts.TotalMilliseconds);
+ else
+ return Convert.ToInt64(ts.TotalSeconds);
+ }
+
+ ///
+ /// 将秒数转换为时分秒形式
+ ///
+ ///
+ ///
+ public static string FormatToHHmmss(long second)
+ {
+ if (second < 60)
+ return $"00:00:{(second >= 10 ? $"{second}" : $"0{second}")}";
+ if (second < 3600)
+ {
+ var minute = second / 60;
+ var s = second % 60;
+ return $"00:{(minute >= 10 ? $"{minute}" : $"0{minute}")}:{(s >= 10 ? $"{s}" : $"0{s}")}";
+ }
+ else
+ {
+ var hour = second / 3600;
+ var minute = (second - (hour * 3600)) / 60;
+ var s = (second - ((hour * 3600) + minute * 60)) % 60;
+ return $"{(hour >= 10 ? $"{hour}" : $"0{hour}")}:{(minute >= 10 ? $"{minute}" : $"0{minute}")}:{(s >= 10 ? $"{s}" : $"0{s}")}";
+ }
+ }
+
+ #region SetLocalTime
+ [DllImport("Kernel32.dll")]
+ private static extern bool SetLocalTime(ref SYSTEMTIME lpSystemTime);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SYSTEMTIME
+ {
+ public ushort wYear;
+ public ushort wMonth;
+ public ushort wDayOfWeek;
+ public ushort wDay;
+ public ushort wHour;
+ public ushort wMinute;
+ public ushort wSecond;
+ public ushort wMilliseconds;
+ }
+
+ ///
+ /// 修改系统时间(需要管理员权限)
+ ///
+ ///
+ public static void SetSystemTime(DateTime date)
+ {
+ SYSTEMTIME lpTime = new SYSTEMTIME();
+ lpTime.wYear = Convert.ToUInt16(date.Year);
+ lpTime.wMonth = Convert.ToUInt16(date.Month);
+ lpTime.wDayOfWeek = Convert.ToUInt16(date.DayOfWeek);
+ lpTime.wDay = Convert.ToUInt16(date.Day);
+ DateTime time = date;
+ lpTime.wHour = Convert.ToUInt16(time.Hour);
+ lpTime.wMinute = Convert.ToUInt16(time.Minute);
+ lpTime.wSecond = Convert.ToUInt16(time.Second);
+ lpTime.wMilliseconds = Convert.ToUInt16(time.Millisecond);
+ var r = SetLocalTime(ref lpTime);
+ Console.WriteLine($"修改系统时间 {r}");
+ }
+ #endregion
+ }
+}
diff --git a/SBF.Common/Extensions/EncryptionExtension.cs b/SBF.Common/Extensions/EncryptionExtension.cs
new file mode 100644
index 0000000..025f8e5
--- /dev/null
+++ b/SBF.Common/Extensions/EncryptionExtension.cs
@@ -0,0 +1,82 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace SBF.Common.Extensions
+{
+ public static class EncryptionExtension
+ {
+
+ public static string Md5Encrypt(this string originStr)
+ {
+ using (var md5 = MD5.Create())
+ {
+ return string.Join(string.Empty, md5.ComputeHash(Encoding.UTF8.GetBytes(originStr)).Select(x => x.ToString("x2")));
+ }
+ }
+
+ //AES加密 传入,要加密的串和, 解密key
+ public static string AESEncrypt(this string input)
+ {
+ var key = "dataplatform2019";
+ var ivStr = "1012132405963708";
+
+ var encryptKey = Encoding.UTF8.GetBytes(key);
+ var iv = Encoding.UTF8.GetBytes(ivStr); //偏移量,最小为16
+ using (var aesAlg = Aes.Create())
+ {
+ using (var encryptor = aesAlg.CreateEncryptor(encryptKey, iv))
+ {
+ using (var msEncrypt = new MemoryStream())
+ {
+ using (var csEncrypt = new CryptoStream(msEncrypt, encryptor,
+ CryptoStreamMode.Write))
+
+ using (var swEncrypt = new StreamWriter(csEncrypt))
+ {
+ swEncrypt.Write(input);
+ }
+ var decryptedContent = msEncrypt.ToArray();
+
+ return Convert.ToBase64String(decryptedContent);
+ }
+ }
+ }
+ }
+
+ public static string AESDecrypt(this string cipherText)
+ {
+ var fullCipher = Convert.FromBase64String(cipherText);
+
+ var ivStr = "1012132405963708";
+ var key = "dataplatform2019";
+
+ var iv = Encoding.UTF8.GetBytes(ivStr);
+ var decryptKey = Encoding.UTF8.GetBytes(key);
+
+ using (var aesAlg = Aes.Create())
+ {
+ using (var decryptor = aesAlg.CreateDecryptor(decryptKey, iv))
+ {
+ string result;
+ using (var msDecrypt = new MemoryStream(fullCipher))
+ {
+ using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
+ {
+ using (var srDecrypt = new StreamReader(csDecrypt))
+ {
+ result = srDecrypt.ReadToEnd();
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+ }
+
+ public static string Base64Encrypt(this string originStr)
+ {
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(originStr));
+ }
+ }
+}
diff --git a/SBF.Common/Extensions/MapperExtension.cs b/SBF.Common/Extensions/MapperExtension.cs
new file mode 100644
index 0000000..70f0464
--- /dev/null
+++ b/SBF.Common/Extensions/MapperExtension.cs
@@ -0,0 +1,59 @@
+using AutoMapper;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace SBF.Common.Extensions
+{
+ public static class MapperExtension
+ {
+ private static IMapper _mapper;
+
+ ///
+ /// 注册对象映射器
+ ///
+ ///
+ ///
+ ///
+ public static IServiceCollection AddMapper(this IServiceCollection services, Profile profile)
+ {
+ var config = new MapperConfiguration(cfg =>
+ {
+ cfg.AddProfile(profile);
+ });
+ _mapper = config.CreateMapper();
+ return services;
+ }
+
+ ///
+ /// 设置对象映射执行者
+ ///
+ /// 映射执行者
+ public static void SetMapper(IMapper mapper)
+ {
+ _mapper = mapper;
+ }
+
+ ///
+ /// 将对象映射为指定类型
+ ///
+ /// 要映射的目标类型
+ /// 源对象
+ /// 目标类型的对象
+ public static TTarget Map(this object source)
+ {
+ return _mapper.Map(source);
+ }
+
+ ///
+ /// 使用源类型的对象更新目标类型的对象
+ ///
+ /// 源类型
+ /// 目标类型
+ /// 源对象
+ /// 待更新的目标对象
+ /// 更新后的目标类型对象
+ public static TTarget Map(this TSource source, TTarget target)
+ {
+ return _mapper.Map(source, target);
+ }
+ }
+}
diff --git a/SBF.Common/Extensions/StartupExtension.cs b/SBF.Common/Extensions/StartupExtension.cs
new file mode 100644
index 0000000..24174c4
--- /dev/null
+++ b/SBF.Common/Extensions/StartupExtension.cs
@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using System.Reflection;
+
+namespace SBF.Common.Extensions
+{
+ public static class StartupExtension
+ {
+ public static void BatchRegisterServices(this IServiceCollection serviceCollection, Assembly[] assemblys, Type baseType, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton, bool registerSelf = true)
+ {
+ List typeList = new List(); //所有符合注册条件的类集合
+ foreach (var assembly in assemblys)
+ {
+ var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsSealed && !t.IsAbstract && baseType.IsAssignableFrom(t));
+ typeList.AddRange(types);
+ }
+
+ if (typeList.Count() == 0)
+ return;
+
+ foreach (var instanceType in typeList)
+ {
+ if (registerSelf)
+ {
+ serviceCollection.Add(new ServiceDescriptor(instanceType, instanceType, serviceLifetime));
+ continue;
+ }
+ var interfaces = instanceType.GetInterfaces();
+ if (interfaces != null && interfaces.Length > 0)
+ foreach (var interfaceType in interfaces)
+ serviceCollection.Add(new ServiceDescriptor(interfaceType, instanceType, serviceLifetime));
+ }
+ }
+ }
+}
diff --git a/SBF.Common/Http/HttpDownloader.cs b/SBF.Common/Http/HttpDownloader.cs
new file mode 100644
index 0000000..a8fc576
--- /dev/null
+++ b/SBF.Common/Http/HttpDownloader.cs
@@ -0,0 +1,452 @@
+using System.Net;
+
+namespace SBF.Common.Http
+{
+ ///
+ /// Http下载器
+ ///
+ public class HttpDownloader
+ {
+
+ private IHttpClientFactory httpClientFactory;
+
+ public HttpDownloader(IHttpClientFactory httpClientFactory, DownloadSetting dwSetting = null)
+ {
+ this.httpClientFactory = httpClientFactory;
+ complateArgs = new DownloadCompletedEventArgs();
+ changeArgs = new DownloadProgressChangedEventArgs();
+ if (dwSetting == null)
+ dwSetting = new DownloadSetting();
+ downloadSetting = dwSetting;
+ buffer = new byte[downloadSetting.BufferSize];
+ }
+
+ ~HttpDownloader()
+ {
+ this.buffer = null;
+ this.downloadSetting = null;
+ this.complateArgs.Error = null;
+ this.complateArgs = null;
+ this.changeArgs = null;
+ }
+
+ #region Property
+
+ ///
+ /// 下载进度变化参数
+ ///
+ private DownloadProgressChangedEventArgs changeArgs;
+
+ ///
+ /// 下载完成参数
+ ///
+ private DownloadCompletedEventArgs complateArgs;
+
+ ///
+ /// 下载参数配置类
+ ///
+ private DownloadSetting downloadSetting;
+
+ ///
+ /// 下载缓冲区
+ ///
+ private byte[] buffer;
+
+ #endregion
+
+ #region Delegate
+ public delegate void DownloadProgressChangedEventHandler(object sender, DownloadProgressChangedEventArgs e);
+
+ public delegate void DownloadCompletedEventHandler(object sender, DownloadCompletedEventArgs e);
+
+ public delegate void ReDownloadEventHandler(object sender, DownloadCompletedEventArgs e);
+ #endregion
+
+ #region Event
+ ///
+ /// 下载进度变化事件
+ ///
+ public event DownloadProgressChangedEventHandler OnDownloadProgressChanged;
+
+ ///
+ /// 下载完成事件
+ ///
+ public event DownloadCompletedEventHandler OnDownloadComplated;
+
+ ///
+ /// 自动重下事件
+ ///
+ public event ReDownloadEventHandler OnReDownload;
+ #endregion
+
+ #region Method
+ ///
+ /// 设置下载参数
+ ///
+ ///
+ public void SetDownloadSetting(DownloadSetting dwSetting)
+ {
+ if (dwSetting != null)
+ downloadSetting = dwSetting;
+ }
+
+ private void DoProcessChangedEvent(DownloadProgressChangedEventArgs args)
+ {
+ OnDownloadProgressChanged?.Invoke(this, args);
+ }
+
+ private void DoCompletedEvent(DownloadCompletedEventArgs args)
+ {
+ OnDownloadComplated?.Invoke(this, args);
+ }
+
+ private void DoReDownloadEvent(DownloadCompletedEventArgs args)
+ {
+ OnReDownload?.Invoke(this, args);
+ }
+
+ private void ArgsInit()
+ {
+ changeArgs.Cancel = false;
+ complateArgs.Cancelled = false;
+ complateArgs.Error = null;
+ }
+
+ ///
+ /// 获取文件总大小
+ ///
+ ///
+ ///
+ public long GetTotalLength(string url)
+ {
+ long length = 0;
+ try
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ var requestMessage = new HttpRequestMessage(HttpMethod.Head, url);
+ var httpResponse = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).Result;
+ if (httpResponse.IsSuccessStatusCode)
+ length = httpResponse.Content.Headers.ContentLength ?? 0;
+ }
+
+ //var req = (HttpWebRequest)WebRequest.Create(new Uri(url));
+ //req.Method = "HEAD";
+ //req.Timeout = 5000;
+ //var res = (HttpWebResponse)req.GetResponse();
+ //if (res.StatusCode == HttpStatusCode.OK)
+ //{
+ // length = res.ContentLength;
+ //}
+ //res.Close();
+ return length;
+ }
+ catch (WebException wex)
+ {
+ throw wex;
+ }
+ }
+
+
+ private bool DownloadFile(string url, string savePath, long startPosition, long totalSize)
+ {
+ long currentReadSize = 0; //当前已经读取的字节数
+ if (startPosition != 0)
+ currentReadSize = startPosition;
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ try
+ {
+ httpClient.Timeout = new TimeSpan(0, 0, 20);
+ if (currentReadSize != 0 && currentReadSize == totalSize)
+ {
+ changeArgs.CurrentSize = changeArgs.TotalSize = totalSize;
+ return true;
+ }
+ if (currentReadSize != 0)
+ {
+ try
+ {
+ httpClient.DefaultRequestHeaders.Range = new System.Net.Http.Headers.RangeHeaderValue(currentReadSize, totalSize);
+ }
+ catch (Exception ex)
+ {
+ //http 416
+ currentReadSize = 0;
+ }
+ }
+
+ using (var response = httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).Result)
+ {
+ using (var responseStream = response.Content.ReadAsStreamAsync().Result)
+ {
+ changeArgs.TotalSize = totalSize;
+ using (var fs = new FileStream(savePath, currentReadSize == 0 ? FileMode.Create : FileMode.Append, FileAccess.Write))
+ {
+ //判断服务器是否支持断点续下
+ if (currentReadSize != 0 &&
+ response.StatusCode == HttpStatusCode.PartialContent &&
+ response.Content.Headers.ContentLength != totalSize)
+ fs.Seek(startPosition, SeekOrigin.Begin); //支持,从本地文件流末尾进行写入
+ else
+ currentReadSize = 0; //不支持,从本地文件流开始进行写入
+
+ if (buffer.Length != downloadSetting.BufferSize)
+ buffer = new byte[downloadSetting.BufferSize];
+ int size = responseStream.Read(buffer, 0, buffer.Length);
+ while (size != 0)
+ {
+ fs.Write(buffer, 0, size);
+ if (buffer.Length != downloadSetting.BufferSize)
+ buffer = new byte[downloadSetting.BufferSize];
+ size = responseStream.Read(buffer, 0, buffer.Length);
+ currentReadSize += size; //累计下载大小
+ //执行下载进度变化事件
+ changeArgs.CurrentSize = currentReadSize;
+ DoProcessChangedEvent(changeArgs);
+ //判断挂起状态
+ if (changeArgs.Cancel)
+ return true;
+ }
+
+ //执行下载进度变化事件
+ changeArgs.CurrentSize = changeArgs.TotalSize;
+ DoProcessChangedEvent(changeArgs);
+ fs.Flush(true);
+ }
+
+ //验证远程服务器文件字节数和本地文件字节数是否一致
+ long localFileSize = 0;
+ try
+ {
+ localFileSize = new FileInfo(savePath).Length;
+ }
+ catch (Exception ex)
+ {
+ localFileSize = changeArgs.CurrentSize;
+ }
+
+ if (totalSize != localFileSize)
+ {
+ complateArgs.Error = new Exception(string.Format("远程文件字节:[{0}]与本地文件字节:[{1}]不一致", totalSize, localFileSize));
+ }
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ complateArgs.Error = ex;
+ }
+ catch (WebException ex)
+ {
+ complateArgs.Error = ex;
+ }
+ catch (Exception ex)
+ {
+ complateArgs.Error = ex;
+ }
+ }
+ return complateArgs.Error == null;
+ }
+
+
+ public bool DownloadFile(string url, string saveFolderPath, string saveFileName, long? totalSize)
+ {
+ ArgsInit();
+ var result = false;
+ //验证文件夹
+ try
+ {
+ if (!Directory.Exists(saveFolderPath))
+ Directory.CreateDirectory(saveFolderPath);
+ }
+ catch (Exception ex)
+ {
+ complateArgs.Error = ex;
+ DoCompletedEvent(complateArgs);
+ return result;
+ }
+
+ if (totalSize == null || totalSize.Value == 0)
+ {
+ try
+ {
+ totalSize = GetTotalLength(url);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("获取远程服务器字节数失败 {0}", ex.Message);
+ complateArgs.Error = ex;
+ DoCompletedEvent(complateArgs);
+ return result;
+ }
+ }
+
+ string saveFilePath = Path.Combine(saveFolderPath, saveFileName);
+ long startPosition = 0;
+ for (var i = 1; i <= downloadSetting.TryCount; i++)
+ {
+ if (File.Exists(saveFilePath))
+ {
+ try
+ {
+ startPosition = new FileInfo(saveFilePath).Length;
+ }
+ catch
+ {
+ startPosition = 0;
+ }
+ }
+
+ result = DownloadFile(url, saveFilePath, startPosition, totalSize.Value);
+ if (result)
+ {
+ break;
+ }
+ else
+ {
+ if (i != downloadSetting.TryCount)
+ {
+ complateArgs.TryCount = i + 1;
+ DoReDownloadEvent(complateArgs);
+ Thread.Sleep(downloadSetting.SleepTime);
+ if (complateArgs.Cancelled)
+ break;
+ ArgsInit();
+ }
+ }
+ if (complateArgs.Cancelled)
+ break;
+ }
+ DoCompletedEvent(complateArgs);
+ return result;
+ }
+
+ public byte[] DwonloadBytes(string url)
+ {
+ ArgsInit();
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ return httpClient.GetByteArrayAsync(url).Result;
+ }
+
+ }
+
+ ///
+ /// 取消/暂停下载
+ ///
+ public void CancelDownload()
+ {
+ this.changeArgs.Cancel = true;
+ this.complateArgs.Cancelled = true;
+ }
+ #endregion
+ }
+
+
+ ///
+ /// 下载进度变化参数
+ ///
+ public class DownloadProgressChangedEventArgs
+ {
+ public DownloadProgressChangedEventArgs()
+ {
+ ProgressPercentage = 0.0;
+ Cancel = false;
+ }
+ ///
+ /// 下载进度百分比
+ ///
+ public double ProgressPercentage;
+
+
+ private long currentSize;
+
+ ///
+ /// 当前下载总大小
+ ///
+ public long CurrentSize
+ {
+ get { return currentSize; }
+ set
+ {
+ currentSize = value;
+ this.ProgressPercentage = Math.Round((CurrentSize * 1.0 / TotalSize) * 100, 2);
+ }
+ }
+
+ ///
+ /// 文件总大小
+ ///
+ public long TotalSize;
+
+ ///
+ /// 取消状态
+ ///
+ public bool Cancel;
+ }
+
+ ///
+ /// 下载完成参数
+ ///
+ public class DownloadCompletedEventArgs
+ {
+ public DownloadCompletedEventArgs()
+ {
+ Cancelled = false;
+ Error = null;
+ }
+ ///
+ /// 下载异常
+ ///
+ public Exception Error;
+
+ ///
+ /// 重试次数
+ ///
+ public int TryCount;
+
+ ///
+ /// 下载操作是否被取消 【取消则为true;默认false】
+ ///
+ public bool Cancelled;
+ }
+
+ public class DownloadSetting
+ {
+ public DownloadSetting()
+ {
+ this.BufferSize = 1024 * 1024 * 1;
+ this.TryCount = 5;
+ this.SleepTime = 5000;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// 毫秒
+ public DownloadSetting(int bufferSize, int tryCount, int sleepTime)
+ {
+ this.BufferSize = bufferSize;
+ this.TryCount = tryCount;
+ this.SleepTime = sleepTime;
+ }
+
+ ///
+ /// 下载缓冲区大小
+ ///
+ public int BufferSize;
+
+ ///
+ /// 重试次数
+ ///
+ public int TryCount;
+
+ ///
+ /// 自动重下休眠时间, 毫秒
+ ///
+ public int SleepTime;
+ }
+}
diff --git a/SBF.Common/Http/RestAPIService.cs b/SBF.Common/Http/RestAPIService.cs
new file mode 100644
index 0000000..dda3910
--- /dev/null
+++ b/SBF.Common/Http/RestAPIService.cs
@@ -0,0 +1,117 @@
+using Newtonsoft.Json;
+using SBF.Common.Extensions;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace SBF.Common.Http
+{
+ public class RestApiService
+ {
+ public const string ContentType_Json = "application/json";
+ public const string ContentType_Form = "application/x-www-form-urlencoded";
+ public TimeSpan TimeOut { get; set; } = new TimeSpan(0, 0, 40);
+
+ private IHttpClientFactory httpClientFactory;
+
+ public RestApiService(IHttpClientFactory httpClientFactory)
+ {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ ///
+ /// 发送请求
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RestApiResult SendRequest(string apiHost,
+ string apiPath,
+ object param,
+ IDictionary requestHeaders,
+ HttpMethod httpMethod,
+ string contentType = ContentType_Json,
+ ParamPosition paramPosition = ParamPosition.Body,
+ bool enableRandomTimeStamp = false,
+ bool getResponseHeader = false,
+ HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
+ string httpClientName = "",
+ int timeOutSeconds = 0)
+ {
+ //Get和Delete强制使用QueryString形式传参
+ if (httpMethod == HttpMethod.Get)
+ paramPosition = ParamPosition.Query;
+
+ //拼接Url
+
+ var url = $"{apiHost}{(apiHost.EndsWith("/") ? string.Empty : (string.IsNullOrEmpty(apiPath) ? string.Empty : "/"))}{(apiPath.StartsWith("/") ? apiPath.Substring(1) : apiPath)}";
+
+ var isCombineParam = false;
+ if (paramPosition == ParamPosition.Query && param != null)
+ {
+ url = $"{url}{(param.ToString().StartsWith("?") ? string.Empty : "?")}{param}";
+ isCombineParam = true;
+ }
+
+ //使用时间戳绕过CDN
+ if (enableRandomTimeStamp)
+ url = $"{url}{(isCombineParam ? "&" : "?")}t={DateTime.Now.DateTimeToStamp()}";
+
+ using (var httpClient = string.IsNullOrEmpty(httpClientName) ? httpClientFactory.CreateClient() : httpClientFactory.CreateClient(httpClientName))
+ {
+ if (timeOutSeconds == 0)
+ httpClient.Timeout = TimeOut;
+ else
+ httpClient.Timeout = TimeSpan.FromSeconds(timeOutSeconds);
+ using (var request = new HttpRequestMessage(httpMethod, url))
+ {
+ if (requestHeaders != null && requestHeaders.Count > 0)
+ foreach (var key in requestHeaders.Keys)
+ request.Headers.Add(key, requestHeaders[key]);
+
+ if (paramPosition == ParamPosition.Body && param != null)
+ request.Content = new StringContent(contentType == ContentType_Json ? JsonConvert.SerializeObject(param) : param.ToString(), Encoding.UTF8, contentType);
+
+ using (var response = httpClient.SendAsync(request, httpCompletionOption).Result)
+ {
+ return new RestApiResult()
+ {
+ StatusCode = response.StatusCode,
+ Content = httpCompletionOption == HttpCompletionOption.ResponseContentRead ? response.Content.ReadAsStringAsync().Result :
+ string.Empty,
+ Headers = getResponseHeader ? response.Headers : null
+ };
+ }
+ }
+ }
+ }
+ }
+
+ public class RestApiResult
+ {
+ public HttpStatusCode StatusCode { get; set; }
+
+ public string Content { get; set; }
+
+ public HttpResponseHeaders Headers { get; set; }
+ }
+
+ ///
+ /// 参数传递位置
+ ///
+ public enum ParamPosition
+ {
+ Query,
+ Body
+ }
+}
diff --git a/SBF.Common/Models/ApiResponse.cs b/SBF.Common/Models/ApiResponse.cs
new file mode 100644
index 0000000..5bb593f
--- /dev/null
+++ b/SBF.Common/Models/ApiResponse.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace SBF.Common.Models
+{
+ public class ApiResponse
+ {
+ public bool Success { get { return Code == 200; } }
+
+ ///
+ /// 接口调用状态
+ ///
+ public int Code { get; set; } = 200;
+
+ ///
+ /// 返回消息
+ ///
+ public string Msg { get; set; }
+
+ ///
+ /// 数据内容
+ ///
+ public T Data { get; set; }
+
+ public static ApiResponse Error(int code, string msg)
+ {
+ return new ApiResponse() { Code = code, Msg = msg };
+ }
+ }
+
+ public class ApiResponse : ApiResponse