更新框架,增加云库搜索模块
This commit is contained in:
1 parent
8526e5e0d2
commit
f2caec6083
192 files changed
+3626
-804
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using MAX.Core.Infrastructure;
|
using MAX.Core.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
namespace MAX.Core.DependencyInjection;
|
using MAX.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace MAX.Core.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 依赖注册器:把当前模块的服务注册到 <see cref="IServiceRegistrar"/>。
|
/// 依赖注册器:把当前模块的服务注册到 <see cref="IServiceRegistrar"/>。
|
||||||
/// 底层使用哪个容器由宿主项目决定,MAX.Core 不引用任何容器。
|
/// 底层使用哪个容器由宿主项目决定,MAX.Core 不引用任何容器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDependencyRegistrar
|
public interface IDependencyRegistrar: IPriority
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册服务
|
/// 注册服务
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace MAX.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IPriority
|
||||||
|
{
|
||||||
|
/// <summary> 优先级(优先级越高越先执行) </summary>
|
||||||
|
int Priority { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IntPriority
|
||||||
|
{
|
||||||
|
public const int Highest = 5000;
|
||||||
|
|
||||||
|
public const int VeryHigh = 4000;
|
||||||
|
|
||||||
|
public const int Higher = 3000;
|
||||||
|
|
||||||
|
public const int High = 2000;
|
||||||
|
|
||||||
|
public const int AboveNormal = 1000;
|
||||||
|
|
||||||
|
public const int Normal = 0;
|
||||||
|
|
||||||
|
public const int BelowNormal = -1000;
|
||||||
|
|
||||||
|
public const int Low = -2000;
|
||||||
|
|
||||||
|
public const int Lower = -3000;
|
||||||
|
|
||||||
|
public const int VeryLow = -4000;
|
||||||
|
|
||||||
|
public const int Lowest = -5000;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
|
|
||||||
namespace MAX.Core.Infrastructure;
|
namespace MAX.Core.Interfaces;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 启动器:实现 <see cref="IDependencyRegistrar"/>,由宿主项目统一发现并按 <see cref="Priority"/> 排序执行。
|
/// 启动器:实现 <see cref="IDependencyRegistrar"/>,由宿主项目统一发现并按 <see cref="Priority"/> 排序执行。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace MAX.Core.Infrastructure;
|
namespace MAX.Core.Interfaces;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单例,需要在当前类中定义
|
/// 单例,需要在当前类中定义
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using MAX.Core.Infrastructure;
|
|
||||||
|
|
||||||
namespace MAX.Core.Logger;
|
namespace MAX.Core.Logger;
|
||||||
|
|
||||||
/// <summary>自定义日志记录器接口</summary>
|
/// <summary>自定义日志记录器接口</summary>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using MAX.Core.Infrastructure;
|
using MAX.Core.Interfaces;
|
||||||
|
|
||||||
namespace MAX.Core;
|
namespace MAX.Core;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Commons
|
||||||
|
{
|
||||||
|
public class AppException : Exception
|
||||||
|
{
|
||||||
|
public int Code { get; }
|
||||||
|
|
||||||
|
public AppException(string message, int code = ResultCode.BusinessError)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Commons
|
||||||
|
{
|
||||||
|
public class GlobalExceptionFilter : IExceptionFilter
|
||||||
|
{
|
||||||
|
private readonly ILogger<GlobalExceptionFilter> _logger;
|
||||||
|
|
||||||
|
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
var traceId = context.HttpContext.TraceIdentifier;
|
||||||
|
|
||||||
|
var result = context.Exception switch
|
||||||
|
{
|
||||||
|
AppException be => ApiResult.Fail(be.Message, be.Code),
|
||||||
|
ValidationException ve => ApiResult.Fail(ve.Message, ResultCode.ValidationError),
|
||||||
|
UnauthorizedAccessException => ApiResult.Fail("未授权访问", ResultCode.Unauthorized),
|
||||||
|
_ => ApiResult.Fail("服务器内部错误", ResultCode.ServerError)
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogError(context.Exception, "请求异常 TraceId: {TraceId}", traceId);
|
||||||
|
|
||||||
|
context.Result = new ObjectResult(result)
|
||||||
|
{
|
||||||
|
StatusCode = 200 // 关键:HTTP状态码统一200,业务状态用Code表达
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Commons
|
||||||
|
{
|
||||||
|
public class PageData<T>
|
||||||
|
{
|
||||||
|
public long Total { get; set; }
|
||||||
|
public int PageIndex { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
|
||||||
|
public List<T> List { get; set; } = new();
|
||||||
|
|
||||||
|
public PageData() { }
|
||||||
|
|
||||||
|
public PageData(List<T> list, long total, int pageIndex, int pageSize)
|
||||||
|
{
|
||||||
|
List = list;
|
||||||
|
Total = total;
|
||||||
|
PageIndex = pageIndex;
|
||||||
|
PageSize = pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Commons
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 返回过滤
|
||||||
|
/// </summary>
|
||||||
|
public class UnifyResultFilter : IActionFilter
|
||||||
|
{
|
||||||
|
public void OnActionExecuted(ActionExecutedContext context)
|
||||||
|
{
|
||||||
|
if (context.Result is ObjectResult objResult)
|
||||||
|
{
|
||||||
|
if (!IsAlreadyWrapped(objResult.Value))
|
||||||
|
{
|
||||||
|
var apiResult = ApiResult<object>.Success(objResult.Value);
|
||||||
|
|
||||||
|
objResult.Value = apiResult;
|
||||||
|
objResult.DeclaredType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Result is EmptyResult || context.Result is NoContentResult)
|
||||||
|
{
|
||||||
|
context.Result = new ObjectResult(ApiResult.Success());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnActionExecuting(ActionExecutingContext context) { }
|
||||||
|
private static bool IsAlreadyWrapped(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return false;
|
||||||
|
|
||||||
|
var type = value.GetType();
|
||||||
|
|
||||||
|
// 递归判断:只要类型链上有任何一层是 ApiResult<> 的开放泛型定义,或直接是 ApiResult,就算已包装
|
||||||
|
while (type is not null)
|
||||||
|
{
|
||||||
|
if (type == typeof(ApiResult)) return true;
|
||||||
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ApiResult<>)) return true;
|
||||||
|
type = type.BaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Commons
|
||||||
|
{
|
||||||
|
public class UnsupportedMediaTypeHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public UnsupportedMediaTypeHandlingMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// 用一个内存流替换原始的Response.Body,方便判断状态码后决定是否要改写
|
||||||
|
var originalBodyStream = context.Response.Body;
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
context.Response.Body = memoryStream;
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
|
||||||
|
if (context.Response.StatusCode == StatusCodes.Status415UnsupportedMediaType)
|
||||||
|
{
|
||||||
|
var result = ApiResult.Fail("请求参数格式不正确", ResultCode.ValidationError);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(result);
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
context.Response.Clear();
|
||||||
|
context.Response.StatusCode = StatusCodes.Status200OK; // 统一成200,和其他接口一致
|
||||||
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
context.Response.ContentLength = bytes.Length;
|
||||||
|
|
||||||
|
await context.Response.Body.WriteAsync(bytes);
|
||||||
|
await context.Response.Body.FlushAsync(originalBodyStream is null ? default : default);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 非415的情况,把内存流内容原样写回真正的Response.Body
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
await memoryStream.CopyToAsync(originalBodyStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Body = originalBodyStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.DependencyInjection
|
namespace MAX.Framework.DependencyInjection
|
||||||
{
|
{
|
||||||
public static class FreeSqlDB
|
public static class FreeSqlDB
|
||||||
{
|
{
|
||||||
+3
-3
@@ -1,15 +1,15 @@
|
|||||||
using Asp.Versioning.OpenApi;
|
using Asp.Versioning.OpenApi;
|
||||||
|
using MAX.Framework.Security;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.OpenApi;
|
using Microsoft.OpenApi;
|
||||||
using StoreHub.Framework.Security;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.DependencyInjection;
|
namespace MAX.Framework.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JWT 认证服务注册入口(由宿主 Program.cs 调用)。
|
/// JWT 认证服务注册入口(由宿主 Program.cs 调用)。
|
||||||
@@ -21,7 +21,7 @@ public static class JwtServiceCollectionExtensions
|
|||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item>绑定 <c>Jwt</c> 配置节到 <see cref="JwtOptions"/>,并在启动时校验签名密钥长度;</item>
|
/// <item>绑定 <c>Jwt</c> 配置节到 <see cref="JwtOptions"/>,并在启动时校验签名密钥长度;</item>
|
||||||
/// <item>注册 <see cref="ITokenService"/>(令牌生成/校验)与 <see cref="ICurrentUser"/>(当前用户访问器);</item>
|
/// <item>注册 <see cref="ITokenService"/>(令牌生成/校验)与 <see cref="ICurrentUser"/>(当前用户访问器);</item>
|
||||||
/// <item>注册 <see cref="JwtBearerDefaults"/> 认证,认证失败/授权失败统一返回 <see cref="StoreHub.Framework.Commons.ApiResult{T}"/> 格式;</item>
|
/// <item>注册 <see cref="JwtBearerDefaults"/> 认证,认证失败/授权失败统一返回 <see cref="Dtos.ApiResult{T}"/> 格式;</item>
|
||||||
/// <item>注册 <see cref="IOnlineUserStore"/>(基于 <c>IMemoryCache</c> 的在线会话存储):单点登录、在线人员查询、退出销毁;</item>
|
/// <item>注册 <see cref="IOnlineUserStore"/>(基于 <c>IMemoryCache</c> 的在线会话存储):单点登录、在线人员查询、退出销毁;</item>
|
||||||
/// <item>为 OpenAPI 文档声明 Bearer(JWT) 安全方案,并对标记了 [Authorize] 的接口声明需要携带令牌。</item>
|
/// <item>为 OpenAPI 文档声明 Bearer(JWT) 安全方案,并对标记了 [Authorize] 的接口声明需要携带令牌。</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NLog.Web;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class LoggerExtensions
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddLogger(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
//builder.Logging.SetMinimumLevel(LogLevel.Information);
|
||||||
|
builder.Host.UseNLog();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-3
@@ -1,10 +1,14 @@
|
|||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using MAX.Core.Infrastructure;
|
using MAX.Core.Interfaces;
|
||||||
|
using MAX.Framework.Commons;
|
||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using MAX.Framework.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace StoreHub.Framework.DependencyInjection
|
namespace MAX.Framework.DependencyInjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// IOC 注册入口(由宿主 Program.cs 调用)
|
/// IOC 注册入口(由宿主 Program.cs 调用)
|
||||||
@@ -28,10 +32,36 @@ namespace StoreHub.Framework.DependencyInjection
|
|||||||
public static IServiceCollection AddStoreHub(this IServiceCollection services)
|
public static IServiceCollection AddStoreHub(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
|
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
|
||||||
|
|
||||||
RegisterDependencyMarkers(services, assemblies);
|
RegisterDependencyMarkers(services, assemblies);
|
||||||
RunStartups(services, assemblies);
|
RunStartups(services, assemblies);
|
||||||
|
RunModules(services, assemblies);
|
||||||
|
|
||||||
|
|
||||||
|
services.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<GlobalExceptionFilter>();
|
||||||
|
options.Filters.Add<UnifyResultFilter>();
|
||||||
|
}).ConfigureApiBehaviorOptions(options =>
|
||||||
|
{
|
||||||
|
options.InvalidModelStateResponseFactory = context =>
|
||||||
|
{
|
||||||
|
// 取出所有校验错误信息,拼成一个字符串(或者只取第一个)
|
||||||
|
var errors = context.ModelState
|
||||||
|
.Where(e => e.Value?.Errors.Count > 0)
|
||||||
|
.SelectMany(e => e.Value!.Errors.Select(err => err.ErrorMessage))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var msg = string.Join("; ", errors);
|
||||||
|
|
||||||
|
var result = ApiResult.Fail(msg, ResultCode.ValidationError);
|
||||||
|
|
||||||
|
return new ObjectResult(result)
|
||||||
|
{
|
||||||
|
StatusCode = 200 // 和其他接口保持一致,用Code表达失败,而不是HTTP 400
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +115,17 @@ namespace StoreHub.Framework.DependencyInjection
|
|||||||
startup.Register(registrar);
|
startup.Register(registrar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private static void RunModules(IServiceCollection services, IEnumerable<Assembly> assemblies)
|
||||||
|
{
|
||||||
|
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
|
||||||
|
|
||||||
|
foreach (IModule startup in GetConcreteTypes(assemblies)
|
||||||
|
.Where(t => typeof(IModule).IsAssignableFrom(t))
|
||||||
|
.Select(t => (IModule)Activator.CreateInstance(t, true)!))
|
||||||
|
{
|
||||||
|
startup.AddModule(services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<Type> GetConcreteTypes(IEnumerable<Assembly> assemblies)
|
private static IEnumerable<Type> GetConcreteTypes(IEnumerable<Assembly> assemblies)
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace StoreHub.Framework.DependencyInjection
|
namespace MAX.Framework.DependencyInjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 <see cref="IServiceRegistrar"/> 适配到 Microsoft.Extensions.DependencyInjection
|
/// 将 <see cref="IServiceRegistrar"/> 适配到 Microsoft.Extensions.DependencyInjection
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using MAX.Core.Reflections;
|
using MAX.Core.Reflections;
|
||||||
|
|
||||||
namespace StoreHub.Framework.DependencyInjection
|
namespace MAX.Framework.DependencyInjection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
|
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
|
||||||
@@ -12,7 +12,7 @@ namespace StoreHub.Framework.DependencyInjection
|
|||||||
|
|
||||||
protected override List<string> AvailablePrefix()
|
protected override List<string> AvailablePrefix()
|
||||||
{
|
{
|
||||||
return ["StoreHub"];
|
return ["Store"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using MAX.Framework.Commons;
|
||||||
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Dtos
|
||||||
|
{
|
||||||
|
public static class ResultCode
|
||||||
|
{
|
||||||
|
public const int Success = 200;
|
||||||
|
public const int Fail = 500;
|
||||||
|
|
||||||
|
public const int Unauthorized = 401;
|
||||||
|
public const int Forbidden = 403;
|
||||||
|
public const int NotFound = 404;
|
||||||
|
/// <summary>
|
||||||
|
/// 参数校验失败
|
||||||
|
/// </summary>
|
||||||
|
public const int ValidationError = 1001;
|
||||||
|
/// <summary>
|
||||||
|
/// 业务逻辑异常
|
||||||
|
/// </summary>
|
||||||
|
public const int BusinessError = 2001;
|
||||||
|
/// <summary>
|
||||||
|
/// 服务器内部错误
|
||||||
|
/// </summary>
|
||||||
|
public const int ServerError = 5000;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 非泛型基类,用于统一处理
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ApiResultBase
|
||||||
|
{
|
||||||
|
/// <summary>业务状态码</summary>
|
||||||
|
public int Code { get; set; } = ResultCode.Success;
|
||||||
|
|
||||||
|
/// <summary>提示信息</summary>
|
||||||
|
public string? Msg { get; set; }
|
||||||
|
|
||||||
|
/// <summary>请求是否成功</summary>
|
||||||
|
public bool Success => Code == ResultCode.Success;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 泛型响应
|
||||||
|
/// </summary>
|
||||||
|
public class ApiResult<T> : ApiResultBase
|
||||||
|
{
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
public static ApiResult<T> Success(T data, string msg = "操作成功")
|
||||||
|
=> new ApiResult<T> { Code = ResultCode.Success, Data = data, Msg = msg };
|
||||||
|
|
||||||
|
public static ApiResult<T> Fail(string msg = "操作失败", int code = ResultCode.Fail, T? data = default)
|
||||||
|
=> new ApiResult<T> { Code = code, Msg = msg, Data = data };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 响应
|
||||||
|
/// </summary>
|
||||||
|
public class ApiResult : ApiResult<object>
|
||||||
|
{
|
||||||
|
public static ApiResult Success(string msg = "操作成功")
|
||||||
|
=> new ApiResult { Code = ResultCode.Success, Msg = msg };
|
||||||
|
|
||||||
|
public static ApiResult Fail(string msg = "操作失败", int code = ResultCode.Fail)
|
||||||
|
=> new ApiResult { Code = code, Msg = msg };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using System.Linq;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Helpers;
|
namespace MAX.Framework.Helpers;
|
||||||
|
|
||||||
public static class ObjectHelper
|
public static class ObjectHelper
|
||||||
{
|
{
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Helpers;
|
namespace MAX.Framework.Helpers;
|
||||||
|
|
||||||
public class WinCmdHelper
|
public class WinCmdHelper
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MAX.Framework.Interfaces
|
||||||
|
{
|
||||||
|
public interface IModule
|
||||||
|
{
|
||||||
|
void AddModule(IServiceCollection services);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//using MAX.Core.Infrastructure;
|
||||||
|
//using MAX.Core.Logger;
|
||||||
|
//using NLog;
|
||||||
|
//using System;
|
||||||
|
//using LLogLevel = MAX.Core.Logger.LogLevel;
|
||||||
|
//using NLogLevel = NLog.LogLevel;
|
||||||
|
|
||||||
|
//namespace MAX.Framework.Logger;
|
||||||
|
|
||||||
|
//public class NLogger : SingleInstance<NLogger>, ILoggerFacade
|
||||||
|
//{
|
||||||
|
// private readonly ILogger _log;
|
||||||
|
|
||||||
|
// private NLogger()
|
||||||
|
// {
|
||||||
|
// _log = LogManager.GetLogger("GlobalLogger");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public bool IsDebugEnabled { get; private set; } = true;
|
||||||
|
|
||||||
|
// public void Debug(Exception ex, string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Debug, ex, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Debug(string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Debug, message);
|
||||||
|
// }
|
||||||
|
// public void Error(Exception ex)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Error, ex);
|
||||||
|
// }
|
||||||
|
// public void Error(Exception ex, string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Error, ex, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Error(string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Error, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Fatal(Exception ex, string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Fatal, ex, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Fatal(string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Fatal, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Info(Exception ex, string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Info, ex, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Info(string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Info, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Warn(Exception ex, string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Warn, ex, message);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Warn(string message)
|
||||||
|
// {
|
||||||
|
// Execute(NLogLevel.Warn, message);
|
||||||
|
// }
|
||||||
|
// public void Log(string message, LLogLevel level)
|
||||||
|
// {
|
||||||
|
// NLogLevel lv = NLogLevel.Info;
|
||||||
|
// switch (level)
|
||||||
|
// {
|
||||||
|
// case LLogLevel.Trace:
|
||||||
|
// lv = NLogLevel.Trace;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.Debug:
|
||||||
|
// lv = NLogLevel.Debug;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.Info:
|
||||||
|
// lv = NLogLevel.Info;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.Warn:
|
||||||
|
// lv = NLogLevel.Warn;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.Error:
|
||||||
|
// lv = NLogLevel.Error;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.Fatal:
|
||||||
|
// lv = NLogLevel.Fatal;
|
||||||
|
// break;
|
||||||
|
// case LLogLevel.None:
|
||||||
|
// lv = NLogLevel.Off;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// Execute(lv, message);
|
||||||
|
// }
|
||||||
|
// /// <summary>
|
||||||
|
// /// 是否启用
|
||||||
|
// /// </summary>
|
||||||
|
// private bool Enabled(NLogLevel level)
|
||||||
|
// {
|
||||||
|
// if (level > NLogLevel.Debug)
|
||||||
|
// {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return IsDebugEnabled;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 执行
|
||||||
|
// /// </summary>
|
||||||
|
// private void Execute(NLogLevel level, Exception ex, string message = "")
|
||||||
|
// {
|
||||||
|
// if (Enabled(level))
|
||||||
|
// {
|
||||||
|
// _log.Log(level, ex, message, null);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /// <summary>
|
||||||
|
// /// 执行
|
||||||
|
// /// </summary>
|
||||||
|
// private void Execute(NLogLevel level, string message)
|
||||||
|
// {
|
||||||
|
// if (Enabled(level))
|
||||||
|
// {
|
||||||
|
// _log.Log(level, message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
//}
|
||||||
+9
-5
@@ -8,10 +8,6 @@
|
|||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
|
||||||
<PackageReference Include="Asp.Versioning.OpenApi" Version="10.2.1" />
|
<PackageReference Include="Asp.Versioning.OpenApi" Version="10.2.1" />
|
||||||
@@ -19,8 +15,16 @@
|
|||||||
<PackageReference Include="FreeSql.Provider.MySql" Version="3.5.311" />
|
<PackageReference Include="FreeSql.Provider.MySql" Version="3.5.311" />
|
||||||
<PackageReference Include="FreeSql.Repository" Version="3.5.311" />
|
<PackageReference Include="FreeSql.Repository" Version="3.5.311" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
||||||
<PackageReference Include="NLog" Version="6.1.4" />
|
<PackageReference Include="NLog.Web.AspNetCore" Version="6.2.0" />
|
||||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MAX.Core\MAX.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
+1
-1
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从 <see cref="IHttpContextAccessor"/> 读取当前请求认证票据的默认实现。
|
/// 从 <see cref="IHttpContextAccessor"/> 读取当前请求认证票据的默认实现。
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前登录用户访问器:从当前请求的认证票据中读取用户信息。
|
/// 当前登录用户访问器:从当前请求的认证票据中读取用户信息。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在线用户会话存储:基于 <see cref="Microsoft.Extensions.Caching.Memory.IMemoryCache"/> 的进程内实现,
|
/// 在线用户会话存储:基于 <see cref="Microsoft.Extensions.Caching.Memory.IMemoryCache"/> 的进程内实现,
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 令牌生成结果
|
/// 令牌生成结果
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using MAX.Framework.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using StoreHub.Framework.Commons;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JwtBearer 认证失败/授权失败时,统一返回 <see cref="ApiResult{T}"/> 格式的 JSON 响应。
|
/// JwtBearer 认证失败/授权失败时,统一返回 <see cref="ApiResult{T}"/> 格式的 JSON 响应。
|
||||||
@@ -38,7 +38,7 @@ internal static class JwtBearerResponseHandler
|
|||||||
httpContext.Response.StatusCode = statusCode;
|
httpContext.Response.StatusCode = statusCode;
|
||||||
httpContext.Response.ContentType = "application/json; charset=utf-8";
|
httpContext.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
|
||||||
string body = JsonSerializer.Serialize(ApiResult.Error(message, statusCode), JsonOptions);
|
string body = JsonSerializer.Serialize(ApiResult.Fail(message, statusCode), JsonOptions);
|
||||||
await httpContext.Response.WriteAsync(body, Encoding.UTF8);
|
await httpContext.Response.WriteAsync(body, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JWT 自定义声明类型常量。
|
/// JWT 自定义声明类型常量。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JWT 配置选项,默认绑定配置节 <c>Jwt</c>。
|
/// JWT 配置选项,默认绑定配置节 <c>Jwt</c>。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在线用户会话信息(单点登录:同一用户仅保留一个有效会话)。
|
/// 在线用户会话信息(单点登录:同一用户仅保留一个有效会话)。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基于 <see cref="IMemoryCache"/> 的在线用户会话存储。
|
/// 基于 <see cref="IMemoryCache"/> 的在线用户会话存储。
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Security;
|
namespace MAX.Framework.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基于 <see cref="JwtOptions"/> 的 JWT 令牌服务。
|
/// 基于 <see cref="JwtOptions"/> 的 JWT 令牌服务。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FreeSql;
|
||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using MAX.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace MAX.Framework
|
||||||
|
{
|
||||||
|
public class Startup : IStartup
|
||||||
|
{
|
||||||
|
public int Priority => IntPriority.Highest;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(IServiceRegistrar registrar)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MAX.Framework.Commons;
|
||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Store.Framework.Base;
|
||||||
|
using Store.Modules.CloudProduct.Dtos;
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
using Store.Modules.CloudProduct.ImportConverters;
|
||||||
|
using Store.Modules.CloudProduct.Services;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Controllers
|
||||||
|
{
|
||||||
|
[ApiVersionNeutral]
|
||||||
|
[Route("/CloudProduct/[action]")]
|
||||||
|
public class ProductController : AppControllerBase
|
||||||
|
{
|
||||||
|
private readonly IProductService _productService;
|
||||||
|
private readonly IOpenSearchServer _searchServer;
|
||||||
|
private readonly RawDataStorage _rawDataStorage;
|
||||||
|
|
||||||
|
public ProductController(IProductService productService, IOpenSearchServer searchServer, RawDataStorage rawDataStorage)
|
||||||
|
{
|
||||||
|
_productService = productService;
|
||||||
|
_searchServer = searchServer;
|
||||||
|
_rawDataStorage = rawDataStorage;
|
||||||
|
}
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<string> Time()
|
||||||
|
{
|
||||||
|
return DateTime.Now.ToLongDateString();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyword"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<PageData<ProductEntity>> SearchGoods(ProductSearchRequest request)
|
||||||
|
{
|
||||||
|
// 1. 参数基础校验
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new AppException("请求参数不能为空", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 分页参数校验
|
||||||
|
if (request.PageIndex < 1)
|
||||||
|
{
|
||||||
|
throw new AppException("页码必须大于0", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.PageSize < 1 || request.PageSize > 2000)
|
||||||
|
{
|
||||||
|
throw new AppException("每页数量必须在1-2000之间", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 价格区间校验
|
||||||
|
if (request.MinPrice.HasValue && request.MinPrice < 0)
|
||||||
|
{
|
||||||
|
throw new AppException("最低价格不能小于0", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MaxPrice.HasValue && request.MaxPrice < 0)
|
||||||
|
{
|
||||||
|
throw new AppException("最高价格不能小于0", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MinPrice.HasValue && request.MaxPrice.HasValue && request.MinPrice > request.MaxPrice)
|
||||||
|
{
|
||||||
|
throw new AppException("最低价格不能大于最高价格", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 销量区间校验
|
||||||
|
if (request.MinSoldQuantity.HasValue && request.MinSoldQuantity < 0)
|
||||||
|
{
|
||||||
|
throw new AppException("最低销量不能小于0", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MaxSoldQuantity.HasValue && request.MaxSoldQuantity < 0)
|
||||||
|
{
|
||||||
|
throw new AppException("最高销量不能小于0", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MinSoldQuantity.HasValue && request.MaxSoldQuantity.HasValue &&
|
||||||
|
request.MinSoldQuantity > request.MaxSoldQuantity)
|
||||||
|
{
|
||||||
|
throw new AppException("最低销量不能大于最高销量", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 类目层级校验
|
||||||
|
if (request.CatId2.HasValue && !request.CatId1.HasValue)
|
||||||
|
{
|
||||||
|
throw new AppException("选择二级类目时必须先选择一级类目", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.CatId3.HasValue && !request.CatId2.HasValue)
|
||||||
|
{
|
||||||
|
throw new AppException("选择三级类目时必须先选择二级类目", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.CatId4.HasValue && !request.CatId3.HasValue)
|
||||||
|
{
|
||||||
|
throw new AppException("选择四级类目时必须先选择三级类目", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 平台校验
|
||||||
|
if (request.Platform.HasValue && request.Platform < 0)
|
||||||
|
{
|
||||||
|
throw new AppException("平台类型无效", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 至少有一个搜索条件
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Title) &&
|
||||||
|
!request.MinPrice.HasValue && !request.MaxPrice.HasValue &&
|
||||||
|
!request.CatId1.HasValue && !request.CatId2.HasValue &&
|
||||||
|
!request.CatId3.HasValue && !request.CatId4.HasValue &&
|
||||||
|
!request.MinSoldQuantity.HasValue && !request.MaxSoldQuantity.HasValue &&
|
||||||
|
!request.Platform.HasValue)
|
||||||
|
{
|
||||||
|
throw new AppException("请至少提供一个搜索条件", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _productService.SearchGoods(request);
|
||||||
|
return new PageData<ProductEntity>
|
||||||
|
{
|
||||||
|
List = result.List,
|
||||||
|
Total = result.Total,
|
||||||
|
PageSize = request.PageSize,
|
||||||
|
PageIndex = request.PageIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<PageData<ProductSearchInfo>> SearchGoodsToIds(string keyword, int pageindex = 1, int pagesize = 20)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
throw new AppException("填写关键字", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
var ret = await _searchServer.SearchByTitleAsync(new ProductSearchRequest
|
||||||
|
{
|
||||||
|
Title = keyword,
|
||||||
|
PageIndex = pageindex,
|
||||||
|
PageSize = pagesize,
|
||||||
|
});
|
||||||
|
return new PageData<ProductSearchInfo>
|
||||||
|
{
|
||||||
|
PageIndex = pageindex,
|
||||||
|
PageSize = pagesize,
|
||||||
|
List = ret.list,
|
||||||
|
Total = ret.Total,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入测试数据:读取 ImportConverters/data.txt 并走完整解析 + 入库 + 分词流程。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<GoodsImportResult> ImportTestData()
|
||||||
|
{
|
||||||
|
var filePath = GetDataFilePath();
|
||||||
|
|
||||||
|
if (!System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
throw new AppException($"找不到测试数据文件: {filePath}", ResultCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = await System.IO.File.ReadAllTextAsync(filePath);
|
||||||
|
var goods = ConverterToGoodsFactory.ConvertContent(content);
|
||||||
|
if (goods == null)
|
||||||
|
{
|
||||||
|
throw new AppException("未找到匹配的转换器,无法解析测试数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _productService.ImportGoodsAsync(new[]
|
||||||
|
{
|
||||||
|
new GoodsImportItem { ProductEntity = goods, RawData = content }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入接口:接收客户端批量上传的 JSON 文件,解析后入库并写入分词索引。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<GoodsImportResult> ImportFiles([FromForm] List<IFormFile> files)
|
||||||
|
{
|
||||||
|
if (files == null || files.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AppException("请选择要导入的 JSON 文件", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<GoodsImportItem>();
|
||||||
|
var parseErrors = new List<string>();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(file.OpenReadStream());
|
||||||
|
var content = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var goods = ConverterToGoodsFactory.ConvertContent(content);
|
||||||
|
if (goods == null)
|
||||||
|
{
|
||||||
|
parseErrors.Add($"{file.FileName}: 未找到匹配的转换器");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.Add(new GoodsImportItem { ProductEntity = goods, RawData = content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
parseErrors.Add($"{file.FileName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _productService.ImportGoodsAsync(items);
|
||||||
|
result.Failed += parseErrors.Count;
|
||||||
|
result.Errors.InsertRange(0, parseErrors);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询 OpenSearch 状态。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<OpenSearchStatusDto> GetOpenSearchStatus()
|
||||||
|
{
|
||||||
|
return await _searchServer.GetStatusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取指定商品详情:解压缩原始数据并返回 JSON。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<JsonNode> GetGoodsDetail([FromQuery] long goodsId)
|
||||||
|
{
|
||||||
|
var raw = await GetRawDataOrThrowAsync(goodsId);
|
||||||
|
var node = JsonNode.Parse(raw);
|
||||||
|
return node ?? throw new AppException("商品详情解析失败", ResultCode.BusinessError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取指定商品的原始数据(解压缩后原样返回)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<string> GetGoodsRawData([FromQuery] long goodsId)
|
||||||
|
{
|
||||||
|
return await GetRawDataOrThrowAsync(goodsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重新从数据库读取指定商品,覆盖 OpenSearch 已有分词并重新分词。
|
||||||
|
/// ids 为空时重建全部商品。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<long> RebuildIndex([FromBody] long[]? ids)
|
||||||
|
{
|
||||||
|
return await _productService.RebuildSearchIndexAsync(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商品(按数据库主键 ID)。是否软删除由配置 Delete:SoftDelete 决定,
|
||||||
|
/// 软删除将 Status 置 1,硬删除物理删除记录;两种方式都会删除 OpenSearch 中对应文档。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<long> DeleteGoods([FromBody] long[]? ids)
|
||||||
|
{
|
||||||
|
if (ids == null || ids.Length == 0)
|
||||||
|
{
|
||||||
|
throw new AppException("请提供要删除的商品ID", ResultCode.ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _productService.DeleteGoodsAsync(ids);
|
||||||
|
}
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<bool> ClearSearch()
|
||||||
|
{
|
||||||
|
return await _searchServer.ClearIndexAsync();
|
||||||
|
}
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<long> GetSearchCount()
|
||||||
|
{
|
||||||
|
return await _searchServer.CountDocumentsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task<string> GetRawDataOrThrowAsync(long goodsId)
|
||||||
|
{
|
||||||
|
if (goodsId <= 0)
|
||||||
|
throw new AppException("商品ID无效", ResultCode.ValidationError);
|
||||||
|
|
||||||
|
var raw = await _rawDataStorage.ReadAsync(goodsId);
|
||||||
|
if (raw == null)
|
||||||
|
throw new AppException($"商品原始数据不存在: {goodsId}", ResultCode.NotFound);
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDataFilePath()
|
||||||
|
{
|
||||||
|
var outputPath = Path.Combine(AppContext.BaseDirectory, "ImportConverters", "data.txt");
|
||||||
|
if (System.IO.File.Exists(outputPath))
|
||||||
|
return outputPath;
|
||||||
|
|
||||||
|
var workPath = Path.Combine(Directory.GetCurrentDirectory(), "ImportConverters", "data.txt");
|
||||||
|
if (System.IO.File.Exists(workPath))
|
||||||
|
return workPath;
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Dtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导入项:商品模型 + 原始数据
|
||||||
|
/// </summary>
|
||||||
|
public class GoodsImportItem
|
||||||
|
{
|
||||||
|
public ProductEntity ProductEntity { get; set; } = null!;
|
||||||
|
|
||||||
|
public string RawData { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Dtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品导入结果
|
||||||
|
/// </summary>
|
||||||
|
public class GoodsImportResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 成功条数
|
||||||
|
/// </summary>
|
||||||
|
public int Success { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败条数
|
||||||
|
/// </summary>
|
||||||
|
public int Failed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数
|
||||||
|
/// </summary>
|
||||||
|
public int Total => Success + Failed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误明细
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Dtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// OpenSearch 运行状态
|
||||||
|
/// </summary>
|
||||||
|
public class OpenSearchStatusDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 节点地址
|
||||||
|
/// </summary>
|
||||||
|
public string NodeUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 索引名称
|
||||||
|
/// </summary>
|
||||||
|
public string IndexName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否可连接
|
||||||
|
/// </summary>
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 索引是否存在
|
||||||
|
/// </summary>
|
||||||
|
public bool IndexExists { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文档总数(索引不存在时为 -1)
|
||||||
|
/// </summary>
|
||||||
|
public long DocumentCount { get; set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 说明信息
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using OpenSearch.Client;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Dtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品搜索模型:Id 为主键,Title 用于分词搜索
|
||||||
|
/// </summary>
|
||||||
|
public class ProductSearchInfo
|
||||||
|
{
|
||||||
|
[Number(NumberType.Byte, Name = "platform")]
|
||||||
|
public byte Platform { get; set; }
|
||||||
|
|
||||||
|
[Number(NumberType.Long, Name = "id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Text(Name = "title", Analyzer = "ik_max_word", SearchAnalyzer = "ik_smart")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
[Text(Name = "thumbUrl")]
|
||||||
|
public string? ThumbUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>一级类目ID</summary>
|
||||||
|
[Number(NumberType.Integer, Name = "cat_id_1")]
|
||||||
|
public int? CatId1 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>二级类目ID</summary>
|
||||||
|
[Number(NumberType.Integer, Name = "cat_id_2")]
|
||||||
|
public int? CatId2 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>三级类目ID</summary>
|
||||||
|
[Number(NumberType.Integer, Name = "cat_id_3")]
|
||||||
|
public int? CatId3 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>四级类目ID</summary>
|
||||||
|
[Number(NumberType.Integer, Name = "cat_id_4")]
|
||||||
|
public int? CatId4 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最低价快照,用于价格区间过滤和按价格排序</summary>
|
||||||
|
[Number(NumberType.Long, Name = "min_price")]
|
||||||
|
public long? MinPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>销量,用于按销量排序</summary>
|
||||||
|
[Number(NumberType.Integer, Name = "sold_quantity")]
|
||||||
|
public int SoldQuantity { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => $"[Id={Id}] Title={Title} Cat=({CatId1},{CatId2},{CatId3},{CatId4}) Price={MinPrice} Sold={SoldQuantity}";
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索排序方式
|
||||||
|
/// </summary>
|
||||||
|
public enum GoodsSearchSortBy
|
||||||
|
{
|
||||||
|
Relevance = 0, // 相关性(默认)
|
||||||
|
PriceAsc = 1, // 价格从低到高
|
||||||
|
PriceDesc = 2, // 价格从高到低
|
||||||
|
SoldDesc = 3, // 销量从高到低
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Dtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品搜索请求
|
||||||
|
/// </summary>
|
||||||
|
public class ProductSearchRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 标题(模糊搜索)
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最低价格(分)
|
||||||
|
/// </summary>
|
||||||
|
public long? MinPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最高价格(分)
|
||||||
|
/// </summary>
|
||||||
|
public long? MaxPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一级类目ID
|
||||||
|
/// </summary>
|
||||||
|
public int? CatId1 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二级类目ID
|
||||||
|
/// </summary>
|
||||||
|
public int? CatId2 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 三级类目ID
|
||||||
|
/// </summary>
|
||||||
|
public int? CatId3 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 四级类目ID
|
||||||
|
/// </summary>
|
||||||
|
public int? CatId4 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最低销量
|
||||||
|
/// </summary>
|
||||||
|
public int? MinSoldQuantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最高销量
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxSoldQuantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台类型
|
||||||
|
/// </summary>
|
||||||
|
public byte? Platform { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码(默认1)
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页数量(默认20)
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段
|
||||||
|
/// </summary>
|
||||||
|
public string SortBy { get; set; } = "CreatedTime";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序方式:asc/desc(默认desc)
|
||||||
|
/// </summary>
|
||||||
|
public string SortOrder { get; set; } = "desc";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using FreeSql.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品表实体
|
||||||
|
/// </summary>
|
||||||
|
[Table(Name = "goods")] // 请根据实际表名调整
|
||||||
|
public class ProductEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 自增ID
|
||||||
|
/// </summary>
|
||||||
|
[Column(IsPrimary = true, IsIdentity = true)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台类型
|
||||||
|
/// 1 PDD
|
||||||
|
/// </summary>
|
||||||
|
[Column(IsNullable = false)]
|
||||||
|
public byte Platform { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品编号
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "goods_id", IsNullable = false)]
|
||||||
|
public long GoodsId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标题
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "title", IsNullable = false, StringLength = 500)]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "description", IsNullable = true)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缩略图
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "thumb_url", IsNullable = true, StringLength = 512)]
|
||||||
|
public string? ThumbUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一级类目ID
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "cat_id_1", IsNullable = true)]
|
||||||
|
public int? CatId1 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二级类目ID
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "cat_id_2", IsNullable = true)]
|
||||||
|
public int? CatId2 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 三级类目ID
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "cat_id_3", IsNullable = true)]
|
||||||
|
public int? CatId3 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 四级类目ID
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "cat_id_4", IsNullable = true)]
|
||||||
|
public int? CatId4 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最低价快照(元),导入时计算
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "min_price")]
|
||||||
|
public long? MinPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最高价快照(元),导入时计算
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "max_price")]
|
||||||
|
public long? MaxPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 销量
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "sold_quantity", IsNullable = false)]
|
||||||
|
public int SoldQuantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态:0正常 1已删除
|
||||||
|
/// </summary>
|
||||||
|
[Column(IsNullable = false)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "createdTime", IsNullable = false, ServerTime = DateTimeKind.Utc)]
|
||||||
|
[JsonIgnore]
|
||||||
|
|
||||||
|
public DateTime CreatedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间(自动更新)
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "updatedTime", IsNullable = false, ServerTime = DateTimeKind.Utc)]
|
||||||
|
[JsonIgnore]
|
||||||
|
|
||||||
|
public DateTime UpdatedTime { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已分词:0未 1已
|
||||||
|
/// </summary>
|
||||||
|
[Column(Name = "is_segmented", IsNullable = false)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte IsSegmented { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Store.Modules.CloudProduct.Enums
|
||||||
|
{
|
||||||
|
public enum PlatformType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
PDD = 1,
|
||||||
|
WX = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.ImportConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导入商品转换工厂:按顺序尝试各转换器,返回标准商品模型。
|
||||||
|
/// </summary>
|
||||||
|
public class ConverterToGoodsFactory
|
||||||
|
{
|
||||||
|
private static readonly List<IGoodsConverter> _converters = new List<IGoodsConverter>
|
||||||
|
{
|
||||||
|
new ConverterV1(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据本地 JSON 文件路径转换。
|
||||||
|
/// </summary>
|
||||||
|
public static ProductEntity? Convert(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException("找不到导入文件", filePath);
|
||||||
|
|
||||||
|
string content = File.ReadAllText(filePath);
|
||||||
|
return ConvertContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 JSON 内容字符串转换。
|
||||||
|
/// </summary>
|
||||||
|
public static ProductEntity? ConvertContent(string content)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
|
foreach (IGoodsConverter item in _converters)
|
||||||
|
{
|
||||||
|
item.ReadContent(content);
|
||||||
|
if (item.IsMatch())
|
||||||
|
{
|
||||||
|
var goods = item.Convert();
|
||||||
|
if (goods != null)
|
||||||
|
return goods;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
using Store.Modules.CloudProduct.Enums;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.ImportConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDD 前端商品详情数据转换器(数据源格式见 data.txt)
|
||||||
|
/// </summary>
|
||||||
|
internal class ConverterV1 : GoodsConverterBase
|
||||||
|
{
|
||||||
|
public override string ConverterName => "ConverterV1";
|
||||||
|
|
||||||
|
public override PlatformType Platform => PlatformType.PDD;
|
||||||
|
|
||||||
|
protected override bool IsMatchProvider()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (JsonNode.Parse(RawData) is not JsonObject root)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// data.txt:顶层包含 goods.goods_id
|
||||||
|
if (root["goods"] is JsonObject goodsObj && goodsObj["goods_id"] != null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// 也兼容顶层直接是商品对象的情况
|
||||||
|
if (root["goods_id"] != null && (root["goods_name"] != null || root["title"] != null))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ProductEntity ConverterProvider()
|
||||||
|
{
|
||||||
|
if (JsonNode.Parse(RawData) is not JsonObject root)
|
||||||
|
throw new Exception("无法解析 PDD 商品数据");
|
||||||
|
|
||||||
|
var goodsNode = root["goods"] as JsonObject ?? root;
|
||||||
|
var priceNode = root["price"] as JsonObject;
|
||||||
|
|
||||||
|
var goods = new ProductEntity
|
||||||
|
{
|
||||||
|
Platform = (byte)PlatformType.PDD,
|
||||||
|
GoodsId = GetLong(goodsNode, "goods_id"),
|
||||||
|
Title = GetString(goodsNode, "goods_name")
|
||||||
|
?? GetString(goodsNode, "short_name")
|
||||||
|
?? string.Empty,
|
||||||
|
Description = GetString(goodsNode, "share_desc") ?? GetString(goodsNode, "short_name"),
|
||||||
|
ThumbUrl = GetString(goodsNode, "hd_url") ?? GetString(goodsNode, "thumb_url") ?? GetString(goodsNode, "hd_thumb_url") ?? GetString(goodsNode, "image_url"),
|
||||||
|
CatId1 = GetIntOrNull(goodsNode, "cat_id_1"),
|
||||||
|
CatId2 = GetIntOrNull(goodsNode, "cat_id_2"),
|
||||||
|
CatId3 = GetIntOrNull(goodsNode, "cat_id_3"),
|
||||||
|
CatId4 = GetIntOrNull(goodsNode, "cat_id_4"),
|
||||||
|
SoldQuantity = GetInt(goodsNode, "sold_quantity"),
|
||||||
|
Status = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var price = GetPriceRange(priceNode, root);
|
||||||
|
goods.MinPrice = price.Min;
|
||||||
|
goods.MaxPrice = price.Max;
|
||||||
|
|
||||||
|
return goods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetString(JsonObject? obj, string key)
|
||||||
|
{
|
||||||
|
if (obj == null || !obj.TryGetPropertyValue(key, out var node) || node == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (node is JsonValue value && value.TryGetValue<string>(out var str))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
return node.ToJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GetLong(JsonObject? obj, string key)
|
||||||
|
{
|
||||||
|
if (obj == null || !obj.TryGetPropertyValue(key, out var node) || node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (node is JsonValue value)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue<long>(out var l)) return l;
|
||||||
|
if (value.TryGetValue<decimal>(out var d)) return (long)d;
|
||||||
|
if (long.TryParse(value.ToString(), out var l2)) return l2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetInt(JsonObject? obj, string key)
|
||||||
|
{
|
||||||
|
if (obj == null || !obj.TryGetPropertyValue(key, out var node) || node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (node is JsonValue value)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue<int>(out var i)) return i;
|
||||||
|
if (value.TryGetValue<long>(out var l)) return (int)l;
|
||||||
|
if (int.TryParse(value.ToString(), out var i2)) return i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? GetIntOrNull(JsonObject? obj, string key)
|
||||||
|
{
|
||||||
|
if (obj == null || !obj.TryGetPropertyValue(key, out var node) || node == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (node is JsonValue value)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue<int>(out var i) && i > 0) return i;
|
||||||
|
if (value.TryGetValue<long>(out var l) && l > 0) return (int)l;
|
||||||
|
if (int.TryParse(value.ToString(), out var i2) && i2 > 0) return i2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long? GetLongOrNull(JsonObject? obj, string key)
|
||||||
|
{
|
||||||
|
if (obj == null || !obj.TryGetPropertyValue(key, out var node) || node == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (node is JsonValue value)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue<long>(out var d)) return d;
|
||||||
|
if (value.TryGetValue<long>(out var l)) return l;
|
||||||
|
if (long.TryParse(value.ToString(), out var d2)) return d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算价格区间。优先使用顶层 price 节点,其次遍历 sku。
|
||||||
|
/// </summary>
|
||||||
|
private static (long? Min, long? Max) GetPriceRange(JsonObject? priceNode, JsonObject root)
|
||||||
|
{
|
||||||
|
long? min = null;
|
||||||
|
long? max = null;
|
||||||
|
|
||||||
|
if (priceNode != null)
|
||||||
|
{
|
||||||
|
min = GetLongOrNull(priceNode, "min_group_price");
|
||||||
|
max = GetLongOrNull(priceNode, "max_group_price");
|
||||||
|
if (min.HasValue || max.HasValue)
|
||||||
|
return (min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root["sku"] is JsonArray skuArray && skuArray.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var item in skuArray.OfType<JsonObject>())
|
||||||
|
{
|
||||||
|
var price = GetLongOrNull(item, "group_price") ?? GetLongOrNull(item, "normal_price");
|
||||||
|
if (price == null) continue;
|
||||||
|
|
||||||
|
min = min.HasValue ? Math.Min(min.Value, price.Value) : price;
|
||||||
|
max = max.HasValue ? Math.Max(max.Value, price.Value) : price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min.HasValue || max.HasValue)
|
||||||
|
return (min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
using Store.Modules.CloudProduct.Enums;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.ImportConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导入商品数据转换器
|
||||||
|
/// </summary>
|
||||||
|
public interface IGoodsConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 原始数据
|
||||||
|
/// </summary>
|
||||||
|
string RawData { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取转换器名称
|
||||||
|
/// </summary>
|
||||||
|
string ConverterName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持的平台类型
|
||||||
|
/// </summary>
|
||||||
|
PlatformType Platform { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 读取内容
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content"></param>
|
||||||
|
void ReadContent(string content);
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否匹配当前转换器
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>是否匹配</returns>
|
||||||
|
bool IsMatch();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为标准商品模型
|
||||||
|
/// </summary>
|
||||||
|
ProductEntity Convert();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class GoodsConverterBase : IGoodsConverter
|
||||||
|
{
|
||||||
|
private bool _isMatch = false;
|
||||||
|
public string RawData { get; private set; }
|
||||||
|
public abstract string ConverterName { get; }
|
||||||
|
public abstract PlatformType Platform { get; }
|
||||||
|
|
||||||
|
void IGoodsConverter.ReadContent(string filePath)
|
||||||
|
{
|
||||||
|
ReadContent(filePath);
|
||||||
|
}
|
||||||
|
protected virtual void ReadContent(string filePath)
|
||||||
|
{
|
||||||
|
RawData = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual bool IsMatch()
|
||||||
|
{
|
||||||
|
_isMatch = true;
|
||||||
|
return IsMatchProvider();
|
||||||
|
}
|
||||||
|
protected abstract bool IsMatchProvider();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 带校验的转换(基类统一实现)
|
||||||
|
/// </summary>
|
||||||
|
public virtual ProductEntity Convert()
|
||||||
|
{
|
||||||
|
if (_isMatch == false) throw new Exception("未校验数据");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JsonNode.Parse(RawData);
|
||||||
|
}
|
||||||
|
catch { throw new Exception("数据解析失败,不是有效的JSON格式"); }
|
||||||
|
|
||||||
|
|
||||||
|
var product = ConverterProvider();
|
||||||
|
if (product == null)
|
||||||
|
throw new Exception("转换失败");
|
||||||
|
// 基础校验
|
||||||
|
if (product.GoodsId == 0)
|
||||||
|
throw new Exception("商品ID不能为空");
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract ProductEntity ConverterProvider();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using MAX.Framework.Commons;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using MySqlX.XDevAPI;
|
||||||
|
using OpenSearch.Client;
|
||||||
|
using OpenSearch.Net;
|
||||||
|
using Store.Modules.CloudProduct.Dtos;
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Services
|
||||||
|
{
|
||||||
|
public interface IOpenSearchServer: ISingletonDependency
|
||||||
|
{
|
||||||
|
Task AddOneRecordAsync(ProductSearchInfo goods);
|
||||||
|
Task BulkIndexGoodsAsync(IEnumerable<ProductSearchInfo> docs);
|
||||||
|
Task<bool> ClearIndexAsync();
|
||||||
|
Task<long> CountDocumentsAsync();
|
||||||
|
Task<bool> CreateIndexAsync();
|
||||||
|
Task DeleteDocumentsAsync(IEnumerable<long> ids);
|
||||||
|
Task DeleteIndexAsync();
|
||||||
|
Task<OpenSearchStatusDto> GetStatusAsync();
|
||||||
|
Task<long> RebuildIndexAsync(IEnumerable<ProductEntity> goodsList);
|
||||||
|
Task<(long Total, List<ProductSearchInfo> list)> SearchByTitleAsync(ProductSearchRequest request, GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class OpenSearchServer : IOpenSearchServer
|
||||||
|
{
|
||||||
|
private readonly string nodeUrl;
|
||||||
|
private IOpenSearchClient _client = null!;
|
||||||
|
private readonly string IndexName = "";
|
||||||
|
|
||||||
|
// 查询类操作(search/count/status/ping)统一超时
|
||||||
|
private static readonly TimeSpan QueryTimeout = TimeSpan.FromSeconds(5);
|
||||||
|
// 批量写入/建索引/删除索引等管理类操作超时(数据量大时需要更长,可挪到配置文件)
|
||||||
|
private static readonly TimeSpan AdminTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
public OpenSearchServer(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var config = configuration.GetSection("Search");
|
||||||
|
nodeUrl = config.GetValue<string>("Url") ?? "";
|
||||||
|
IndexName = config.GetValue<string>("IndexName") ?? "";
|
||||||
|
_client = CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IOpenSearchClient CreateClient()
|
||||||
|
{
|
||||||
|
var pool = new SingleNodeConnectionPool(new Uri(nodeUrl));
|
||||||
|
var settings = new ConnectionSettings(pool)
|
||||||
|
.DefaultIndex(IndexName)
|
||||||
|
.DefaultMappingFor<ProductSearchInfo>(m => m.IndexName(IndexName))
|
||||||
|
.RequestTimeout(QueryTimeout)
|
||||||
|
.MaxRetryTimeout(QueryTimeout)
|
||||||
|
.PrettyJson()
|
||||||
|
.DisableDirectStreaming(); // 便于调试打印请求/响应报文
|
||||||
|
|
||||||
|
return new OpenSearchClient(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 1. 创建索引 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CreateIndexAsync()
|
||||||
|
{
|
||||||
|
var existsResponse = await _client.Indices.ExistsAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
if (existsResponse.Exists)
|
||||||
|
{
|
||||||
|
if (await IsIndexMappingHealthyAsync())
|
||||||
|
{
|
||||||
|
Console.WriteLine($"索引 [{IndexName}] 已存在且映射正确,无需重复创建。");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"索引 [{IndexName}] 的映射不完整(可能由清空索引等操作造成),正在重建...");
|
||||||
|
var deleteResponse = await _client.Indices.DeleteAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
if (!deleteResponse.IsValid)
|
||||||
|
{
|
||||||
|
throw new AppException("删除损坏索引失败: " + deleteResponse.DebugInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先尝试使用 IK 中文分词(需集群安装 analysis-ik 插件)
|
||||||
|
var createResponse = await TryCreateIndexAsync(useIk: true);
|
||||||
|
|
||||||
|
if (!createResponse.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine("IK 分词插件不可用,已回退到 standard 分析器,中文分词将失效,请检查集群是否安装 analysis-ik 插件。");
|
||||||
|
createResponse = await TryCreateIndexAsync(useIk: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createResponse.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"索引 [{IndexName}] 创建成功。");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AppException("索引创建失败: " + createResponse.DebugInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验索引映射是否健康:title 字段必须使用 IK 分析器
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> IsIndexMappingHealthyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getResponse = await _client.Indices.GetAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
if (!getResponse.IsValid || !getResponse.Indices.TryGetValue(IndexName, out var indexState))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapping = indexState.Mappings;
|
||||||
|
if (mapping?.Properties == null
|
||||||
|
|| !mapping.Properties.TryGetValue(new PropertyName("title"), out var property)
|
||||||
|
|| property is not ITextProperty textProperty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return textProperty.Analyzer == "ik_max_word" && textProperty.SearchAnalyzer == "ik_smart";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CreateIndexResponse> TryCreateIndexAsync(bool useIk)
|
||||||
|
{
|
||||||
|
var indexAnalyzer = useIk ? "ik_max_word" : "standard";
|
||||||
|
var searchAnalyzer = useIk ? "ik_smart" : "standard";
|
||||||
|
|
||||||
|
return await _client.Indices.CreateAsync(IndexName, c => c
|
||||||
|
.Settings(s => s
|
||||||
|
.NumberOfShards(1)
|
||||||
|
.NumberOfReplicas(0)
|
||||||
|
)
|
||||||
|
.Map<ProductSearchInfo>(m => m
|
||||||
|
.Properties(p => p
|
||||||
|
.Number(n => n.Name(x => x.Id).Type(NumberType.Long))
|
||||||
|
.Text(t => t
|
||||||
|
.Name(x => x.Title)
|
||||||
|
.Analyzer(indexAnalyzer)
|
||||||
|
.SearchAnalyzer(searchAnalyzer)
|
||||||
|
)
|
||||||
|
.Number(n => n.Name(x => x.CatId1).Type(NumberType.Integer))
|
||||||
|
.Number(n => n.Name(x => x.CatId2).Type(NumberType.Integer))
|
||||||
|
.Number(n => n.Name(x => x.CatId3).Type(NumberType.Integer))
|
||||||
|
.Number(n => n.Name(x => x.CatId4).Type(NumberType.Integer))
|
||||||
|
.Number(n => n.Name(x => x.MinPrice).Type(NumberType.Long))
|
||||||
|
.Number(n => n.Name(x => x.SoldQuantity).Type(NumberType.Integer))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 2. 删除索引 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task DeleteIndexAsync()
|
||||||
|
{
|
||||||
|
var response = await _client.Indices.DeleteAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
if (!response.IsValid)
|
||||||
|
{
|
||||||
|
throw new AppException("删除失败: " + response.DebugInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 3.1 批量写入商品搜索文档(存在则覆盖) ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task BulkIndexGoodsAsync(IEnumerable<ProductSearchInfo> docs)
|
||||||
|
{
|
||||||
|
if (docs == null) throw new ArgumentNullException(nameof(docs));
|
||||||
|
|
||||||
|
var list = docs.ToList();
|
||||||
|
if (list.Count == 0) return;
|
||||||
|
|
||||||
|
// 确保索引已按 IK 分词映射创建,否则中文分词无效
|
||||||
|
await CreateIndexAsync();
|
||||||
|
|
||||||
|
var bulkResponse = await _client.BulkAsync(b => b
|
||||||
|
.Index(IndexName)
|
||||||
|
.IndexMany(list, (descriptor, doc) => descriptor.Id(doc.Id))
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!bulkResponse.IsValid)
|
||||||
|
{
|
||||||
|
throw new AppException("写入 OpenSearch 失败: " + bulkResponse.DebugInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新索引,保证数据立即可搜索(仅用于演示,生产环境不建议频繁 Refresh)
|
||||||
|
await _client.Indices.RefreshAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 3.2 根据数据库商品重建搜索索引 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task<long> RebuildIndexAsync(IEnumerable<ProductEntity> goodsList)
|
||||||
|
{
|
||||||
|
if (goodsList == null) throw new ArgumentNullException(nameof(goodsList));
|
||||||
|
|
||||||
|
var docs = goodsList
|
||||||
|
.Where(x => x != null && x.GoodsId > 0)
|
||||||
|
.Select(x => new ProductSearchInfo
|
||||||
|
{
|
||||||
|
Id = x.GoodsId,
|
||||||
|
Title = x.Title ?? string.Empty,
|
||||||
|
CatId1 = x.CatId1,
|
||||||
|
CatId2 = x.CatId2,
|
||||||
|
CatId3 = x.CatId3,
|
||||||
|
CatId4 = x.CatId4,
|
||||||
|
MinPrice = x.MinPrice,
|
||||||
|
SoldQuantity = x.SoldQuantity
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (docs.Count == 0) return 0;
|
||||||
|
|
||||||
|
await BulkIndexGoodsAsync(docs);
|
||||||
|
return docs.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 3.3 按主键批量删除 OpenSearch 文档(软/硬删除共用) ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task DeleteDocumentsAsync(IEnumerable<long> ids)
|
||||||
|
{
|
||||||
|
var list = ids?.Distinct().ToList() ?? new List<long>();
|
||||||
|
if (list.Count == 0) return;
|
||||||
|
|
||||||
|
var exists = await _client.Indices.ExistsAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
if (!exists.Exists)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _client.BulkAsync(b =>
|
||||||
|
{
|
||||||
|
foreach (var id in list)
|
||||||
|
{
|
||||||
|
b.Delete<ProductSearchInfo>(d => d.Index(IndexName).Id(id));
|
||||||
|
}
|
||||||
|
b.RequestConfiguration(r => r.RequestTimeout(AdminTimeout));
|
||||||
|
return b;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.IsValid)
|
||||||
|
{
|
||||||
|
throw new AppException("删除 OpenSearch 文档失败: " + response.DebugInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.Indices.RefreshAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 4. 手动新增一条 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task AddOneRecordAsync(ProductSearchInfo goods)
|
||||||
|
{
|
||||||
|
if (goods == null) throw new ArgumentNullException(nameof(goods));
|
||||||
|
var response = await _client.IndexAsync(goods, i => i
|
||||||
|
.Index(IndexName)
|
||||||
|
.Id(goods.Id)
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.IsValid)
|
||||||
|
{
|
||||||
|
await _client.Indices.RefreshAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AppException("新增失败: " + response.DebugInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 5. 分词搜索(分页 + 类目/价格过滤 + 排序) ============
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyword">搜索关键词</param>
|
||||||
|
/// <param name="catId1">一级类目,不传则不限制</param>
|
||||||
|
/// <param name="catId2">二级类目,不传则不限制</param>
|
||||||
|
/// <param name="catId3">三级类目,不传则不限制</param>
|
||||||
|
/// <param name="catId4">四级类目,不传则不限制</param>
|
||||||
|
/// <param name="minPrice">最低价(含),不传则不限制</param>
|
||||||
|
/// <param name="maxPrice">最高价(含),不传则不限制</param>
|
||||||
|
/// <param name="sortBy">排序方式</param>
|
||||||
|
/// <param name="pageIndex">页码,从 1 开始</param>
|
||||||
|
/// <param name="pageSize">每页条数</param>
|
||||||
|
public async Task<(long Total, List<ProductSearchInfo> list)> SearchByTitleAsync(ProductSearchRequest request,
|
||||||
|
GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance)
|
||||||
|
{
|
||||||
|
string keyword = request.Title;
|
||||||
|
int? catId1 = request.CatId1;
|
||||||
|
int? catId2 = request.CatId2;
|
||||||
|
int? catId3 = request.CatId3;
|
||||||
|
int? catId4 = request.CatId4;
|
||||||
|
long? minPrice = request.MinPrice;
|
||||||
|
long? maxPrice = request.MaxPrice;
|
||||||
|
int? minsold = request.MinSoldQuantity;
|
||||||
|
int? maxsold = request.MaxSoldQuantity;
|
||||||
|
int? platform = request.Platform;
|
||||||
|
int pageIndex = request.PageIndex;
|
||||||
|
int pageSize = request.PageSize;
|
||||||
|
|
||||||
|
|
||||||
|
//if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
//{
|
||||||
|
// throw new AppException("关键词不能为空。");
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (pageIndex < 1) pageIndex = 1;
|
||||||
|
if (pageSize < 1) pageSize = 20;
|
||||||
|
if (pageSize > 2000) pageSize = 2000; // 避免深分页/单页过大拖垮性能,按需调整上限
|
||||||
|
|
||||||
|
var from = (pageIndex - 1) * pageSize;
|
||||||
|
|
||||||
|
ISearchResponse<ProductSearchInfo> searchResponse;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
searchResponse = await _client.SearchAsync<ProductSearchInfo>(s => s
|
||||||
|
.Index(IndexName)
|
||||||
|
.Query(q => q
|
||||||
|
.Bool(b =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword) == false)
|
||||||
|
{
|
||||||
|
b.Must(m => m.Match(mm => mm.Field(f => f.Title).Query(keyword)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters = new List<Func<QueryContainerDescriptor<ProductSearchInfo>, QueryContainer>>();
|
||||||
|
if (platform.HasValue && platform!.Value > 0) filters.Add(f => f.Term(t => t.Field(x => x.Platform).Value(platform.Value)));
|
||||||
|
|
||||||
|
if (catId1.HasValue) filters.Add(f => f.Term(t => t.Field(x => x.CatId1).Value(catId1.Value)));
|
||||||
|
if (catId2.HasValue) filters.Add(f => f.Term(t => t.Field(x => x.CatId2).Value(catId2.Value)));
|
||||||
|
if (catId3.HasValue) filters.Add(f => f.Term(t => t.Field(x => x.CatId3).Value(catId3.Value)));
|
||||||
|
if (catId4.HasValue) filters.Add(f => f.Term(t => t.Field(x => x.CatId4).Value(catId4.Value)));
|
||||||
|
|
||||||
|
if (minPrice.HasValue || maxPrice.HasValue)
|
||||||
|
{
|
||||||
|
filters.Add(f => f.Range(r =>
|
||||||
|
{
|
||||||
|
var range = r.Field(x => x.MinPrice);
|
||||||
|
if (minPrice.HasValue) range = range.GreaterThanOrEquals(minPrice.Value);
|
||||||
|
if (maxPrice.HasValue) range = range.LessThanOrEquals(maxPrice.Value);
|
||||||
|
return range;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minsold.HasValue || maxsold.HasValue)
|
||||||
|
{
|
||||||
|
filters.Add(f => f.Range(r =>
|
||||||
|
{
|
||||||
|
var range = r.Field(x => x.SoldQuantity);
|
||||||
|
if (minsold.HasValue) range = range.GreaterThanOrEquals(minsold.Value);
|
||||||
|
if (maxsold.HasValue) range = range.LessThanOrEquals(maxsold.Value);
|
||||||
|
return range;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (filters.Count > 0) b.Filter(filters.ToArray());
|
||||||
|
return b;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.Sort(sort => sortBy switch
|
||||||
|
{
|
||||||
|
GoodsSearchSortBy.PriceAsc => sort.Ascending(f => f.MinPrice),
|
||||||
|
GoodsSearchSortBy.PriceDesc => sort.Descending(f => f.MinPrice),
|
||||||
|
GoodsSearchSortBy.SoldDesc => sort.Descending(f => f.SoldQuantity),
|
||||||
|
_ => sort.Descending(SortSpecialField.Score)
|
||||||
|
})
|
||||||
|
.From(from)
|
||||||
|
.Size(pageSize)
|
||||||
|
.TrackTotalHits(true) // 命中数超过 10000 时默认不精确统计,需要显式开启
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (OpenSearchClientException ex) when (ex.Response?.Success == false)
|
||||||
|
{
|
||||||
|
throw new AppException($"搜索超时或失败({QueryTimeout.TotalSeconds}秒): " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchResponse.IsValid)
|
||||||
|
{
|
||||||
|
throw new AppException("搜索失败: " + searchResponse.DebugInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
//var ids = searchResponse.Documents.Select(d => d.Id).ToList();
|
||||||
|
return (searchResponse.Total, searchResponse.Documents.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 6. 统计文档数 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task<long> CountDocumentsAsync()
|
||||||
|
{
|
||||||
|
var countResponse = await _client.CountAsync<ProductSearchInfo>(c => c
|
||||||
|
.Index(IndexName)
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout))
|
||||||
|
);
|
||||||
|
if (countResponse.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"索引 [{IndexName}] 当前文档总数: {countResponse.Count}");
|
||||||
|
return countResponse.Count;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("统计失败: " + countResponse.DebugInformation);
|
||||||
|
throw new AppException(countResponse.DebugInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============ 7. 查询 OpenSearch 状态 ============
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OpenSearchStatusDto> GetStatusAsync()
|
||||||
|
{
|
||||||
|
var status = new OpenSearchStatusDto
|
||||||
|
{
|
||||||
|
NodeUrl = nodeUrl,
|
||||||
|
IndexName = IndexName,
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ping = await _client.PingAsync(p => p
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
status.Connected = ping.IsValid;
|
||||||
|
if (!ping.IsValid)
|
||||||
|
{
|
||||||
|
status.Message = ping.DebugInformation;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists = await _client.Indices.ExistsAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
status.IndexExists = exists.Exists;
|
||||||
|
|
||||||
|
if (exists.Exists)
|
||||||
|
{
|
||||||
|
var count = await _client.CountAsync<ProductSearchInfo>(c => c
|
||||||
|
.Index(IndexName)
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
status.DocumentCount = count.IsValid ? count.Count : -1;
|
||||||
|
if (!count.IsValid)
|
||||||
|
status.Message = count.DebugInformation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
status.Connected = false;
|
||||||
|
status.Message = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空索引:删除后按统一映射重建,重建期间该索引不可查(由后台定时任务负责回填数据,接受短暂空窗)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ClearIndexAsync()
|
||||||
|
{
|
||||||
|
var existsResponse = await _client.Indices.ExistsAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
|
||||||
|
if (!existsResponse.Exists)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"索引 {IndexName} 不存在");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteResponse = await _client.Indices.DeleteAsync(IndexName, d => d
|
||||||
|
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||||
|
if (!deleteResponse.IsValid)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"删除索引 {IndexName} 失败: {deleteResponse.DebugInformation}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"索引 {IndexName} 已删除");
|
||||||
|
|
||||||
|
return await CreateIndexAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using FreeSql;
|
||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Services
|
||||||
|
{
|
||||||
|
public class ProductRepository : BaseRepository<ProductEntity>, IScopedDependency
|
||||||
|
{
|
||||||
|
public ProductRepository(IFreeSql fsql) : base(fsql)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Store.Modules.CloudProduct.Dtos;
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Services
|
||||||
|
{
|
||||||
|
public interface IProductService : IScopedDependency
|
||||||
|
{
|
||||||
|
Task<int> DeleteGoodsAsync(IEnumerable<long> ids);
|
||||||
|
Task<List<ProductEntity>> GetGoodsByIdsAsync(IEnumerable<long> ids);
|
||||||
|
Task<GoodsImportResult> ImportGoodsAsync(IEnumerable<GoodsImportItem> items);
|
||||||
|
Task<int> IndexPendingGoodsAsync(int batchSize);
|
||||||
|
Task<long> RebuildSearchIndexAsync(IEnumerable<long>? ids = null);
|
||||||
|
Task<(List<ProductEntity> List, long Total)> SearchGoods(ProductSearchRequest request);
|
||||||
|
Task<int> UpdateImage(IEnumerable<ProductEntity> goods);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ProductService : IProductService
|
||||||
|
{
|
||||||
|
private readonly ProductRepository _repository;
|
||||||
|
private readonly OpenSearchServer _search;
|
||||||
|
private readonly RawDataStorage _rawDataStorage;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public ProductService(ProductRepository repository, OpenSearchServer search, RawDataStorage rawDataStorage, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_search = search;
|
||||||
|
_rawDataStorage = rawDataStorage;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(List<ProductEntity> List, long Total)> SearchGoods(ProductSearchRequest request)
|
||||||
|
{
|
||||||
|
// 1. 从搜索引擎获取标题匹配的商品ID列表
|
||||||
|
List<ProductEntity> goodsList = new();
|
||||||
|
long total = 0;
|
||||||
|
|
||||||
|
// 从搜索引擎查询标题匹配的ID
|
||||||
|
(long Total, List<ProductSearchInfo> list) searchRet = (await _search.SearchByTitleAsync(request));
|
||||||
|
goodsList = searchRet.list.Select(a => new ProductEntity
|
||||||
|
{
|
||||||
|
Platform = a.Platform,
|
||||||
|
GoodsId = a.Id,
|
||||||
|
Title = a.Title,
|
||||||
|
CatId1 = a.CatId1,
|
||||||
|
CatId2 = a.CatId2,
|
||||||
|
CatId3 = a.CatId3,
|
||||||
|
CatId4 = a.CatId4,
|
||||||
|
MinPrice = a.MinPrice,
|
||||||
|
MaxPrice = 0,
|
||||||
|
SoldQuantity = a.SoldQuantity,
|
||||||
|
ThumbUrl = a.ThumbUrl,
|
||||||
|
|
||||||
|
}).ToList();
|
||||||
|
//goodsIds = searchRet.list.Select(d => d.Id).ToList();
|
||||||
|
total = searchRet.Total;
|
||||||
|
//// 如果没有匹配的结果,直接返回空
|
||||||
|
//if (!goodsIds.Any())
|
||||||
|
//{
|
||||||
|
// return (new List<ProductEntity>(), 0);
|
||||||
|
//}
|
||||||
|
|
||||||
|
return (goodsList, total);
|
||||||
|
|
||||||
|
//// 2. 构建数据库查询(只查正常状态,排除软删除)
|
||||||
|
//var query = _repository.Select.Where(x => x.Status == 0);
|
||||||
|
|
||||||
|
//// 如果有标题搜索,先用ID过滤
|
||||||
|
//if (goodsIds.Any())
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => goodsIds.Contains(x.GoodsId));
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 3. 价格区间查询
|
||||||
|
//if (request.MinPrice.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.MinPrice >= request.MinPrice.Value);
|
||||||
|
//}
|
||||||
|
//if (request.MaxPrice.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.MaxPrice <= request.MaxPrice.Value);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 4. 类目查询
|
||||||
|
//if (request.CatId1.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.CatId1 == request.CatId1);
|
||||||
|
//}
|
||||||
|
//if (request.CatId2.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.CatId2 == request.CatId2);
|
||||||
|
//}
|
||||||
|
//if (request.CatId3.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.CatId3 == request.CatId3);
|
||||||
|
//}
|
||||||
|
//if (request.CatId4.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.CatId4 == request.CatId4);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 5. 销量区间查询
|
||||||
|
//if (request.MinSoldQuantity.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.SoldQuantity >= request.MinSoldQuantity.Value);
|
||||||
|
//}
|
||||||
|
//if (request.MaxSoldQuantity.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.SoldQuantity <= request.MaxSoldQuantity.Value);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 6. 平台过滤
|
||||||
|
//if (request.Platform.HasValue)
|
||||||
|
//{
|
||||||
|
// query = query.Where(x => x.Platform == request.Platform);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 8. 获取总数
|
||||||
|
//total = await query.CountAsync();
|
||||||
|
|
||||||
|
//// 9. 排序
|
||||||
|
//if (!string.IsNullOrEmpty(request.SortBy))
|
||||||
|
//{
|
||||||
|
// var isAsc = request.SortOrder?.ToLower() == "asc";
|
||||||
|
// query = query.OrderByPropertyName(request.SortBy, isAsc);
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// query = query.OrderByDescending(x => x.CreatedTime);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// 10. 分页查询
|
||||||
|
//var list = await query
|
||||||
|
// //.Skip((request.PageIndex - 1) * request.PageSize)
|
||||||
|
// //.Limit(request.PageSize)
|
||||||
|
// .ToListAsync();
|
||||||
|
////内存排序
|
||||||
|
//var orderMap = goodsIds.Select((id, idx) => new { id, idx })
|
||||||
|
// .ToDictionary(x => x.id, x => x.idx);
|
||||||
|
//list = list.OrderBy(x => orderMap.TryGetValue(x.GoodsId, out var idx) ? idx : int.MaxValue)
|
||||||
|
// .ToList();
|
||||||
|
//return (list, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量导入商品:先落库(按平台 + 商品编号去重)+ 保存原始数据,
|
||||||
|
/// 然后根据配置决定“立即分词”还是“标记未同步交给后台任务分词”。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<GoodsImportResult> ImportGoodsAsync(IEnumerable<GoodsImportItem> items)
|
||||||
|
{
|
||||||
|
var result = new GoodsImportResult();
|
||||||
|
if (items == null)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var indexImmediately = IsImmediateIndexing();
|
||||||
|
var docs = new List<ProductSearchInfo>();
|
||||||
|
var indexedIds = new List<long>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var goods = item?.ProductEntity;
|
||||||
|
if (goods == null)
|
||||||
|
{
|
||||||
|
result.Failed++;
|
||||||
|
result.Errors.Add("商品数据为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var goodsId = goods.GoodsId;
|
||||||
|
if (goodsId == 0)
|
||||||
|
{
|
||||||
|
result.Failed++;
|
||||||
|
result.Errors.Add("商品编号(GoodsId)不能为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 导入时统一先标记“未同步”;立即分词成功后再置为“已同步”,
|
||||||
|
// 后台模式下保持“未同步”,由 Hangfire 定时任务分词。
|
||||||
|
goods.IsSegmented = 0;
|
||||||
|
var saved = await UpsertGoodsAsync(goods);
|
||||||
|
|
||||||
|
var rawData = item?.RawData ?? string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(rawData))
|
||||||
|
{
|
||||||
|
await _rawDataStorage.SaveAsync(saved.GoodsId, rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexImmediately)
|
||||||
|
{
|
||||||
|
docs.Add(new ProductSearchInfo
|
||||||
|
{
|
||||||
|
Platform = saved.Platform,
|
||||||
|
Id = saved.GoodsId,
|
||||||
|
Title = saved.Title ?? string.Empty,
|
||||||
|
ThumbUrl = saved.ThumbUrl,
|
||||||
|
CatId1 = saved.CatId1,
|
||||||
|
CatId2 = saved.CatId2,
|
||||||
|
CatId3 = saved.CatId3,
|
||||||
|
CatId4 = saved.CatId4,
|
||||||
|
MinPrice = saved.MinPrice,
|
||||||
|
SoldQuantity = saved.SoldQuantity,
|
||||||
|
});
|
||||||
|
indexedIds.Add(saved.GoodsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Failed++;
|
||||||
|
result.Errors.Add($"GoodsId={goodsId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docs.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _search.BulkIndexGoodsAsync(docs);
|
||||||
|
await MarkIndexedAsync(indexedIds);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 写入失败时保持“未同步”,由后台定时任务重试
|
||||||
|
result.Failed += docs.Count;
|
||||||
|
result.Success -= docs.Count;
|
||||||
|
result.Errors.Add("写入 OpenSearch 失败: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 后台任务:批量处理未同步(IndexStatus=0)的商品,写入 OpenSearch 后标记为已同步。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> IndexPendingGoodsAsync(int batchSize)
|
||||||
|
{
|
||||||
|
if (batchSize <= 0)
|
||||||
|
batchSize = 200;
|
||||||
|
|
||||||
|
var pending = await _repository.Select
|
||||||
|
.Where(x => x.IsSegmented == 0 && x.Status == 0)
|
||||||
|
.OrderBy(x => x.GoodsId)
|
||||||
|
.Limit(batchSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (pending.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var docs = pending
|
||||||
|
.Select(x => new ProductSearchInfo
|
||||||
|
{
|
||||||
|
Platform = x.Platform,
|
||||||
|
Id = x.GoodsId,
|
||||||
|
Title = x.Title ?? string.Empty,
|
||||||
|
ThumbUrl = x.ThumbUrl,
|
||||||
|
CatId1 = x.CatId1,
|
||||||
|
CatId2 = x.CatId2,
|
||||||
|
CatId3 = x.CatId3,
|
||||||
|
CatId4 = x.CatId4,
|
||||||
|
MinPrice = x.MinPrice,
|
||||||
|
SoldQuantity = x.SoldQuantity,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await _search.BulkIndexGoodsAsync(docs);
|
||||||
|
|
||||||
|
await MarkIndexedAsync(pending.Select(x => x.GoodsId));
|
||||||
|
return pending.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按平台 + 商品编号查找已存在记录,存在则更新,不存在则新增。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ProductEntity> UpsertGoodsAsync(ProductEntity goods)
|
||||||
|
{
|
||||||
|
var existing = await _repository.Select
|
||||||
|
.Where(x => x.Platform == goods.Platform && x.GoodsId == goods.GoodsId)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
goods.Id = existing.Id;
|
||||||
|
goods.GoodsId = existing.GoodsId;
|
||||||
|
goods.CreatedTime = existing.CreatedTime;
|
||||||
|
goods.UpdatedTime = DateTime.Now;
|
||||||
|
await _repository.UpdateAsync(goods);
|
||||||
|
return goods;
|
||||||
|
}
|
||||||
|
|
||||||
|
goods.CreatedTime = DateTime.Now;
|
||||||
|
goods.UpdatedTime = DateTime.Now;
|
||||||
|
return await _repository.InsertAsync(goods);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据数据库主键 ID 列表读取商品。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<ProductEntity>> GetGoodsByIdsAsync(IEnumerable<long> ids)
|
||||||
|
{
|
||||||
|
var idList = ids?.ToList();
|
||||||
|
if (idList == null || idList.Count == 0)
|
||||||
|
return new List<ProductEntity>();
|
||||||
|
|
||||||
|
return await _repository.Select
|
||||||
|
.Where(x => idList.Contains(x.GoodsId))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重新从数据库读取指定商品,覆盖 OpenSearch 中已有的分词并重新分词。
|
||||||
|
/// ids 为空时重建全部商品。重建成功的记录标记为已同步。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<long> RebuildSearchIndexAsync(IEnumerable<long>? ids = null)
|
||||||
|
{
|
||||||
|
List<ProductEntity> goodsList;
|
||||||
|
|
||||||
|
var idList = ids?.ToList();
|
||||||
|
if (idList == null || idList.Count == 0)
|
||||||
|
{
|
||||||
|
goodsList = await _repository.Select.ToListAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
goodsList = await _repository.Select
|
||||||
|
.Where(x => idList.Contains(x.GoodsId))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = await _search.RebuildIndexAsync(goodsList);
|
||||||
|
await MarkIndexedAsync(goodsList.Where(x => x.GoodsId > 0).Select(x => x.GoodsId));
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商品:按配置决定软删除(Status=1)还是硬删除(物理删除),
|
||||||
|
/// 两种方式都会删除 OpenSearch 中对应的文档。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> DeleteGoodsAsync(IEnumerable<long> ids)
|
||||||
|
{
|
||||||
|
var idList = ids?.Distinct().ToList();
|
||||||
|
if (idList == null || idList.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (IsSoftDelete())
|
||||||
|
{
|
||||||
|
// 软删除:仅用 Status 标记(0正常 1已删除)
|
||||||
|
await _repository.Orm.Update<ProductEntity>()
|
||||||
|
.Set(x => x.Status, (byte)1)
|
||||||
|
.Where(x => idList.Contains(x.GoodsId))
|
||||||
|
.ExecuteAffrowsAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 硬删除:直接删除数据库记录
|
||||||
|
await _repository.Orm.Delete<ProductEntity>()
|
||||||
|
.Where(x => idList.Contains(x.GoodsId))
|
||||||
|
.ExecuteAffrowsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论软删除还是硬删除,都要删除 OpenSearch 中的对应文档
|
||||||
|
await _search.DeleteDocumentsAsync(idList);
|
||||||
|
return idList.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> UpdateImage(IEnumerable<ProductEntity> goods)
|
||||||
|
{
|
||||||
|
//string sql = _repository.Orm.Update<ProductEntity>().SetSource(goods, a => a.GoodsId).UpdateColumns(a => new { a.ThumbUrl }).ToSql();
|
||||||
|
return await _repository.Orm.Update<ProductEntity>()
|
||||||
|
.SetSource(goods, a => a.GoodsId)
|
||||||
|
.UpdateColumns(a => new { a.ThumbUrl })
|
||||||
|
.ExecuteAffrowsAsync();
|
||||||
|
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 是否导入时立即分词(Indexing:Mode = Immediate)。
|
||||||
|
/// </summary>
|
||||||
|
private bool IsImmediateIndexing()
|
||||||
|
{
|
||||||
|
var mode = _configuration.GetValue<string>("Indexing:Mode");
|
||||||
|
return string.Equals(mode, "Immediate", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否软删除(Delete:SoftDelete,默认 true)。
|
||||||
|
/// </summary>
|
||||||
|
private bool IsSoftDelete()
|
||||||
|
{
|
||||||
|
return _configuration.GetValue<bool?>("Delete:SoftDelete") ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MarkIndexedAsync(IEnumerable<long> ids)
|
||||||
|
{
|
||||||
|
var idList = ids?.ToList();
|
||||||
|
if (idList == null || idList.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _repository.Orm.Update<ProductEntity>()
|
||||||
|
.Set(x => x.IsSegmented, (byte)1)
|
||||||
|
.Where(x => idList.Contains(x.GoodsId))
|
||||||
|
.ExecuteAffrowsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品原始数据文件存储:按 goodsId % 100 分目录,文件名即商品ID,GZip 压缩保存。
|
||||||
|
/// </summary>
|
||||||
|
public class RawDataStorage : IScopedDependency
|
||||||
|
{
|
||||||
|
private readonly string _rootPath;
|
||||||
|
|
||||||
|
public RawDataStorage(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var configured = configuration.GetValue<string>("RawData:Path");
|
||||||
|
if (string.IsNullOrWhiteSpace(configured))
|
||||||
|
configured = "RawData";
|
||||||
|
|
||||||
|
_rootPath = Path.GetFullPath(configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存原始数据,返回保存后的完整路径。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> SaveAsync(long goodsId, string rawData)
|
||||||
|
{
|
||||||
|
var filePath = GetFilePath(goodsId);
|
||||||
|
var dir = Path.GetDirectoryName(filePath)!;
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
await using var gzip = new GZipStream(fileStream, CompressionLevel.Fastest);
|
||||||
|
await using var writer = new StreamWriter(gzip, new UTF8Encoding(false));
|
||||||
|
await writer.WriteAsync(rawData);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取并解压原始数据;文件不存在时返回 null。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> ReadAsync(long goodsId)
|
||||||
|
{
|
||||||
|
var filePath = GetFilePath(goodsId);
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
await using var gzip = new GZipStream(fileStream, CompressionMode.Decompress);
|
||||||
|
using var reader = new StreamReader(gzip, new UTF8Encoding(false));
|
||||||
|
return await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断指定商品的原始数据文件是否存在。
|
||||||
|
/// </summary>
|
||||||
|
public bool Exists(long goodsId)
|
||||||
|
{
|
||||||
|
return File.Exists(GetFilePath(goodsId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFilePath(long goodsId)
|
||||||
|
{
|
||||||
|
var subDir = (goodsId % 100).ToString();
|
||||||
|
return Path.Combine(_rootPath, subDir, goodsId.ToString());
|
||||||
|
}
|
||||||
|
public async Task<string?> ReadAsync(string filePath)
|
||||||
|
{
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
await using var gzip = new GZipStream(fileStream, CompressionMode.Decompress);
|
||||||
|
using var reader = new StreamReader(gzip, new UTF8Encoding(false));
|
||||||
|
return await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using MAX.Core.DependencyInjection;
|
||||||
|
using MAX.Core.Interfaces;
|
||||||
|
using MAX.Framework.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Store.Modules.CloudProduct
|
||||||
|
{
|
||||||
|
public class Startup : IStartup, IModule
|
||||||
|
{
|
||||||
|
public int Priority => IntPriority.Normal;
|
||||||
|
|
||||||
|
public void AddModule(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(IServiceRegistrar registrar)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Mappers\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenSearch.Client" Version="2.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,27 +1,25 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using MAX.Framework.Dtos;
|
||||||
|
using MAX.Framework.Security;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StoreHub.Framework.Commons;
|
|
||||||
using StoreHub.Framework.Security;
|
|
||||||
using StoreHub.Services;
|
|
||||||
using StoreHub.Services.Dtos;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace StoreHub.Controllers
|
namespace Store.Api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion("1.0")]
|
[ApiVersion("1.0")]
|
||||||
[Route("api/v{version:apiVersion}/[controller]/[action]")]
|
[Route("api/v{version:apiVersion}/[controller]/[action]")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
//private readonly IUserService _userService;
|
||||||
private readonly ITokenService _tokenService;
|
private readonly ITokenService _tokenService;
|
||||||
private readonly ICurrentUser _currentUser;
|
private readonly ICurrentUser _currentUser;
|
||||||
private readonly IOnlineUserStore _onlineUserStore;
|
private readonly IOnlineUserStore _onlineUserStore;
|
||||||
|
|
||||||
public AuthController(IUserService userService, ITokenService tokenService, ICurrentUser currentUser, IOnlineUserStore onlineUserStore)
|
public AuthController( ITokenService tokenService, ICurrentUser currentUser, IOnlineUserStore onlineUserStore)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
//_userService = userService;
|
||||||
_tokenService = tokenService;
|
_tokenService = tokenService;
|
||||||
_currentUser = currentUser;
|
_currentUser = currentUser;
|
||||||
_onlineUserStore = onlineUserStore;
|
_onlineUserStore = onlineUserStore;
|
||||||
@@ -36,23 +34,23 @@ namespace StoreHub.Controllers
|
|||||||
{
|
{
|
||||||
if (request is null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
if (request is null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
||||||
{
|
{
|
||||||
return ApiResult.Error("用户名或密码不能为空", 400);
|
return ApiResult.Fail("用户名或密码不能为空", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDto? user = await _userService.LoginAsync(request.UserName, request.Password);
|
//UserDto? user = await _userService.LoginAsync(request.UserName, request.Password);
|
||||||
if (user is null)
|
//if (user is null)
|
||||||
{
|
//{
|
||||||
return ApiResult.Error("用户名或密码错误", 401);
|
// return ApiResult.Error("用户名或密码错误", 401);
|
||||||
}
|
//}
|
||||||
|
|
||||||
var claims = new List<Claim>
|
//var claims = new List<Claim>
|
||||||
{
|
//{
|
||||||
new(JwtClaimTypes.UserName, user.UserName),
|
// new(JwtClaimTypes.UserName, user.UserName),
|
||||||
new(JwtClaimTypes.NickName, user.NickName),
|
// new(JwtClaimTypes.NickName, user.NickName),
|
||||||
};
|
//};
|
||||||
|
|
||||||
TokenResult token = _tokenService.CreateToken(claims);
|
//TokenResult token = _tokenService.CreateToken(claims);
|
||||||
return ApiResult.Success<object?>(token);
|
return ApiResult<object?>.Success(111);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -67,7 +65,7 @@ namespace StoreHub.Controllers
|
|||||||
_onlineUserStore.SignOut(username);
|
_onlineUserStore.SignOut(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResult.Success<object?>(null);
|
return ApiResult<object?>.Success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -77,7 +75,7 @@ namespace StoreHub.Controllers
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public ApiResult<object?> Me()
|
public ApiResult<object?> Me()
|
||||||
{
|
{
|
||||||
return ApiResult.Success<object?>(new
|
return ApiResult<object?>.Success(new
|
||||||
{
|
{
|
||||||
UserName = _currentUser.UserName,
|
UserName = _currentUser.UserName,
|
||||||
NickName = _currentUser.NickName,
|
NickName = _currentUser.NickName,
|
||||||
@@ -92,7 +90,7 @@ namespace StoreHub.Controllers
|
|||||||
public ApiResult<object?> OnlineUsers()
|
public ApiResult<object?> OnlineUsers()
|
||||||
{
|
{
|
||||||
IReadOnlyList<OnlineUserInfo> users = _onlineUserStore.GetOnlineUsers();
|
IReadOnlyList<OnlineUserInfo> users = _onlineUserStore.GetOnlineUsers();
|
||||||
return ApiResult.Success<object?>(new
|
return ApiResult<object?>.Success(new
|
||||||
{
|
{
|
||||||
Count = users.Count,
|
Count = users.Count,
|
||||||
Users = users,
|
Users = users,
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StoreHub.Framework.Repositorys;
|
using Store.Framework.Repositorys;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace StoreHub.Controllers
|
namespace Store.Api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Description("测试温度")]
|
[ApiVersionNeutral]
|
||||||
[ApiVersion("1.0")]
|
[Route("wxxd_user/[action]")]
|
||||||
[Route("api/v{version:apiVersion}/[controller]")]
|
|
||||||
public class UserController : ControllerBase
|
public class UserController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
+5
-4
@@ -1,9 +1,10 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using MAX.Framework.Dtos;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StoreHub.Framework.Commons;
|
using Store.Api;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace StoreHub.Controllers
|
namespace Store.Api.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Description("测试温度")]
|
[Description("测试温度")]
|
||||||
@@ -17,7 +18,7 @@ namespace StoreHub.Controllers
|
|||||||
];
|
];
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
[HttpGet(Name = "GetWeatherForecast")]
|
||||||
public ApiResult<IEnumerable<WeatherForecast>> Get()
|
public IEnumerable<WeatherForecast> Get()
|
||||||
{
|
{
|
||||||
IEnumerable<WeatherForecast> data = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
IEnumerable<WeatherForecast> data = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
{
|
{
|
||||||
@@ -27,7 +28,7 @@ namespace StoreHub.Controllers
|
|||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return ApiResult.Success(data);
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using MAX.Framework.Dtos;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StoreHub.Framework.Commons;
|
using Store.Api;
|
||||||
|
|
||||||
namespace StoreHub.Controllers.v2
|
namespace Store.Api.Controllers.v2
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiVersion("2.0")]
|
[ApiVersion("2.0")]
|
||||||
@@ -26,7 +27,7 @@ namespace StoreHub.Controllers.v2
|
|||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return ApiResult.Success(data);
|
return ApiResult<IEnumerable<WeatherForecastV2>>.Success(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using MAX.Framework.DependencyInjection;
|
||||||
using Scalar.AspNetCore;
|
using Scalar.AspNetCore;
|
||||||
using StoreHub.Framework.DependencyInjection;
|
|
||||||
|
|
||||||
public partial class Program
|
public partial class Program
|
||||||
{
|
{
|
||||||
@@ -38,13 +38,11 @@ public partial class Program
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// 初始化 KJD 迁移的静态提供者(MysqlUtils 连接提供 / 业务配置读取)
|
// 初始化 KJD 迁移的静态提供者(MysqlUtils 连接提供 / 业务配置读取)
|
||||||
StoreHub.KJDWebService.Commons.AdoProvider.SetServiceProvider(app.Services);
|
//StoreHub.KJDWebService.Commons.AdoProvider.SetServiceProvider(app.Services);
|
||||||
StoreHub.KJDWebService.Commons.KJDConfig.SetConfiguration(app.Configuration);
|
//StoreHub.KJDWebService.Commons.KJDConfig.SetConfiguration(app.Configuration);
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "scalar/v1",
|
"launchUrl": "scalar/v1",
|
||||||
"applicationUrl": "http://localhost:5295",
|
"applicationUrl": "http://localhost:9000",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "scalar/v1",
|
"launchUrl": "scalar/v1",
|
||||||
"applicationUrl": "https://localhost:7241;http://localhost:5295",
|
"applicationUrl": "https://localhost:7241;http://localhost:9000",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,13 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Log\**" />
|
||||||
|
<Content Remove="Log\**" />
|
||||||
|
<EmbeddedResource Remove="Log\**" />
|
||||||
|
<None Remove="Log\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||||
@@ -12,9 +19,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StoreHub.Framework\StoreHub.Framework.csproj" />
|
<ProjectReference Include="..\Models\CloudProduct\Store.Modules.CloudProduct.csproj" />
|
||||||
<ProjectReference Include="..\StoreHub.KJDWebService\StoreHub.KJDWebService.csproj" />
|
<ProjectReference Include="..\Store.Framework\Store.Framework.csproj" />
|
||||||
<ProjectReference Include="..\StoreHub.Services\StoreHub.Services.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
|||||||
namespace StoreHub
|
namespace Store.Api
|
||||||
{
|
{
|
||||||
public class WeatherForecast
|
public class WeatherForecast
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace StoreHub
|
namespace Store.Api
|
||||||
{
|
{
|
||||||
public class WeatherForecastV2
|
public class WeatherForecastV2
|
||||||
{
|
{
|
||||||
File renamed without changes.
@@ -6,12 +6,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=localhost;Port=3306;Database=erp;Uid=root;Pwd=123456;",
|
"DefaultConnection": "Server=192.168.0.200;Port=3306;Database=mysql;Uid=root;Pwd=123456;",
|
||||||
"UserConnection": "Server=localhost;Port=3306;Database=wxxd;Uid=root;Pwd=123456;",
|
"KJDConnection": "Server=192.168.0.200;Port=3306;Database=mysql;Uid=root;Pwd=123456;",
|
||||||
"KJDConnection": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-hl51pkfr.sql.tencentcdb.com';Port=27660;User Id='kjd';Password='Temukjd0625';Charset=utf8mb4;",
|
"ConnKJD": "Server=192.168.0.200;Port=3306;Database=mysql;Uid=root;Pwd=123456;"
|
||||||
"KJDBAKConnection": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-9s7x16mt.sql.tencentcdb.com';Port=24049;User Id='kjdbak';Password='Temukjd0126';Charset=utf8mb4;",
|
//"KJDConnection": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-hl51pkfr.sql.tencentcdb.com';Port=27660;User Id='kjd';Password='Temukjd0625';Charset=utf8mb4;",
|
||||||
"ConnKJD": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-hl51pkfr.sql.tencentcdb.com';Port=27660;User Id='kjd';Password='Temukjd0625';Charset=utf8mb4;",
|
//"KJDBAKConnection": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-9s7x16mt.sql.tencentcdb.com';Port=24049;User Id='kjdbak';Password='Temukjd0126';Charset=utf8mb4;",
|
||||||
"ConnKJDBAK": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-9s7x16mt.sql.tencentcdb.com';Port=24049;User Id='kjdbak';Password='Temukjd0126';Charset=utf8mb4;"
|
//"ConnKJD": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-hl51pkfr.sql.tencentcdb.com';Port=27660;User Id='kjd';Password='Temukjd0625';Charset=utf8mb4;",
|
||||||
|
//"ConnKJDBAK": "Max Pool Size=512;Database='kjd';Data Source='nj-cynosdbmysql-grp-9s7x16mt.sql.tencentcdb.com';Port=24049;User Id='kjdbak';Password='Temukjd0126';Charset=utf8mb4;"
|
||||||
},
|
},
|
||||||
"KJDConfig": {
|
"KJDConfig": {
|
||||||
"UpdateVersion": "525",
|
"UpdateVersion": "525",
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/Core/">
|
||||||
|
<Project Path="Core/MAX.Core/MAX.Core.csproj" />
|
||||||
|
<Project Path="Core/MAX.Framework/MAX.Framework.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Modules/">
|
||||||
|
<Project Path="Models/CloudProduct/Store.Modules.CloudProduct.csproj" Id="9720597a-661b-47d8-90c7-83e17c6d5920" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/_SolutionItems/">
|
||||||
|
<File Path="Directory.Build.props" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="Store.API/Store.Api.csproj" />
|
||||||
|
<Project Path="Store.Framework/Store.Framework.csproj" />
|
||||||
|
<Project Path="StoreHub.KJDWebService/StoreHub.KJDWebService.csproj" />
|
||||||
|
<Project Path="StoreHub.Services/Store.Services.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Store.Framework.Base
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
public class AppControllerBase : ControllerBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Models
|
namespace Store.Framework.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户实体
|
/// 用户实体
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
using FreeSql;
|
using FreeSql;
|
||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using StoreHub.Framework.Models;
|
using Store.Framework.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace StoreHub.Framework.Repositorys
|
namespace Store.Framework.Repositorys
|
||||||
{
|
{
|
||||||
public interface IUserRepository : IBaseRepository<User>
|
public interface IUserRepository : IBaseRepository<User>
|
||||||
{
|
{
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
using FreeSql;
|
using FreeSql;
|
||||||
using MAX.Core.DependencyInjection;
|
using MAX.Core.DependencyInjection;
|
||||||
using MAX.Core.Infrastructure;
|
using MAX.Core.Interfaces;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using StoreHub.Framework.Repositorys;
|
|
||||||
|
|
||||||
namespace StoreHub.Framework
|
namespace Store.Framework
|
||||||
{
|
{
|
||||||
public class Startup : IStartup
|
public class Startup : IStartup
|
||||||
{
|
{
|
||||||
@@ -18,23 +17,23 @@ namespace StoreHub.Framework
|
|||||||
|
|
||||||
public void Register(IServiceRegistrar registrar)
|
public void Register(IServiceRegistrar registrar)
|
||||||
{
|
{
|
||||||
Func<IServiceProvider, IFreeSql> userFsqlFactory = r =>
|
//Func<IServiceProvider, IFreeSql> userFsqlFactory = r =>
|
||||||
{
|
//{
|
||||||
var config = r.GetRequiredService<IConfiguration>();
|
// var config = r.GetRequiredService<IConfiguration>();
|
||||||
var connection = config.GetConnectionString("UserConnection");
|
// var connection = config.GetConnectionString("UserConnection");
|
||||||
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
|
// IFreeSql fsql = new FreeSql.FreeSqlBuilder()
|
||||||
.UseConnectionString(FreeSql.DataType.MySql, connection)
|
// .UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||||
.UseAdoConnectionPool(true)
|
// .UseAdoConnectionPool(true)
|
||||||
.Build();
|
// .Build();
|
||||||
return fsql;
|
// return fsql;
|
||||||
};
|
//};
|
||||||
registrar.RegisterSingleton<IFreeSql>("UserDB", (s, k) =>
|
//registrar.RegisterSingleton<IFreeSql>("UserDB", (s, k) =>
|
||||||
{
|
//{
|
||||||
return userFsqlFactory(s);
|
// return userFsqlFactory(s);
|
||||||
});
|
//});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Func<IServiceProvider, IFreeSql> KJDFsqlFactory = r =>
|
Func<IServiceProvider, IFreeSql> KJDFsqlFactory = r =>
|
||||||
{
|
{
|
||||||
var config = r.GetRequiredService<IConfiguration>();
|
var config = r.GetRequiredService<IConfiguration>();
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Core\MAX.Framework\MAX.Framework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
-1
@@ -15,7 +15,16 @@ namespace StoreHub.KJDWebService.Commons
|
|||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
public static string GetConnection(string key)
|
||||||
|
{
|
||||||
|
if (_configuration == null)
|
||||||
|
return null;
|
||||||
|
var v = _configuration[$"ConnectionStrings:{key}"];
|
||||||
|
if (!string.IsNullOrEmpty(v))
|
||||||
|
return v;
|
||||||
|
v = _configuration.GetConnectionString(key);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
/// <summary>读取配置值(对应旧 connectionStrings["key"].ConnectionString)</summary>
|
/// <summary>读取配置值(对应旧 connectionStrings["key"].ConnectionString)</summary>
|
||||||
public static string Get(string key)
|
public static string Get(string key)
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,835 @@
|
|||||||
|
using FreeSql;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
using System.Data;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
|
||||||
|
namespace StoreHub.KJDWebService.Commons
|
||||||
|
{
|
||||||
|
public class MysqlUtils
|
||||||
|
{
|
||||||
|
public string Message = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库连接字符串
|
||||||
|
/// </summary>
|
||||||
|
private string ConnString = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库连接
|
||||||
|
/// </summary>
|
||||||
|
private MySqlConnection Conn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库读取对象
|
||||||
|
/// </summary>
|
||||||
|
private MySqlDataReader reader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库执行对象
|
||||||
|
/// </summary>
|
||||||
|
private MySqlCommand cmd = new MySqlCommand();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息
|
||||||
|
/// </summary>
|
||||||
|
//public static string ErrorString = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超时(秒)
|
||||||
|
/// </summary>
|
||||||
|
public int TimeOut = 120;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接状态
|
||||||
|
/// </summary>
|
||||||
|
public bool Connected
|
||||||
|
{
|
||||||
|
get { return Conn != null && Conn.State == ConnectionState.Open; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否正在执行
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunning = false;
|
||||||
|
|
||||||
|
public bool IsUseTrans { get; set; } = false;
|
||||||
|
public MySqlTransaction Transaction { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化数据库链接
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connString">数据库链接</param>
|
||||||
|
public MysqlUtils(string connString, bool isUseTrans = false)
|
||||||
|
{
|
||||||
|
this.IsUseTrans = isUseTrans;
|
||||||
|
ConnString = KJDConfig.GetConnection(connString);
|
||||||
|
ConnTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 去掉SQL中的特殊字符
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">字符串</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ReplaceSql(string value)
|
||||||
|
{
|
||||||
|
return ReplaceSqlSpecial(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反转字符串,用于mysql反向索引
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="temp"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReverseString(string temp)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(temp))
|
||||||
|
return null;
|
||||||
|
char[] l = temp.ToCharArray();
|
||||||
|
Array.Reverse(l);
|
||||||
|
return new string(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 去掉SQL中的特殊字符
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">字符串</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReplaceSqlSpecial(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
value = value.Replace("\\", "\\\\");
|
||||||
|
value = value.Replace("'", "''");
|
||||||
|
//value = value.Replace("\"", "\\\"");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 开放一个Conn获取路径,以便外部使用事务提交
|
||||||
|
///// </summary>
|
||||||
|
//public MySqlConnection GetConn()
|
||||||
|
//{
|
||||||
|
// //Transaction = Conn.BeginTransaction();
|
||||||
|
// return Conn;
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="table"></param>
|
||||||
|
/// <param name="keys"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int Insert(string table, List<MysqlKeyValue> keys)
|
||||||
|
{
|
||||||
|
string temp = "", tempx = "";
|
||||||
|
for (int i = 0; i < keys.Count; i++)
|
||||||
|
{
|
||||||
|
temp += ",`" + keys[i].Key + "`";
|
||||||
|
tempx += ",'" + keys[i].Value + "'";
|
||||||
|
}
|
||||||
|
temp = temp.Substring(1);
|
||||||
|
tempx = tempx.Substring(1);
|
||||||
|
string sql = "insert into " + table + " (" + temp + ") values (" + tempx + ") ";
|
||||||
|
return (int)ExecuteInsertId(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回执行表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataTable ExecuteDataTable(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteDataTable(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行sql返回DataTable
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString">SQL语句</param>
|
||||||
|
/// <param name="parms">Sql参数</param>
|
||||||
|
/// <returns>DataTable</returns>
|
||||||
|
public DataTable ExecuteDataTable(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
DataSet ds = new DataSet();
|
||||||
|
MySqlDataAdapter adapter = new MySqlDataAdapter();
|
||||||
|
adapter.SelectCommand = cmd;
|
||||||
|
adapter.Fill(ds);
|
||||||
|
return ds.Tables[0];
|
||||||
|
//DataTable dt = new DataTable();
|
||||||
|
//try
|
||||||
|
//{
|
||||||
|
// reader = cmd.ExecuteReader();
|
||||||
|
// dt.Load(reader);
|
||||||
|
//}
|
||||||
|
//catch
|
||||||
|
//{
|
||||||
|
// reader = cmd.ExecuteReader();
|
||||||
|
// dt = Read(ref reader);
|
||||||
|
//}
|
||||||
|
//return dt;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 针对不同错误,执行不同的修正
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
private void ErrorDoing(string message)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
return;
|
||||||
|
if (message.Contains("Incorrect string value"))
|
||||||
|
return;
|
||||||
|
if (message.Contains("Connection must be valid and open"))
|
||||||
|
ConnTo();
|
||||||
|
if (message.Contains("There is already an open DataReader"))
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
ConnTo();
|
||||||
|
}
|
||||||
|
if (message.Contains("Fatal error encountered during command execution."))
|
||||||
|
ConnTo();
|
||||||
|
if (message.Contains(" StreamWriter ") || message.Contains("未将对象引用设置到对象的实例") || message.Contains("未将对象"))
|
||||||
|
ConnTo();
|
||||||
|
CloseReader();
|
||||||
|
Random ra = new Random(Guid.NewGuid().GetHashCode());
|
||||||
|
Thread.Sleep(ra.Next(200, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关闭读取线程
|
||||||
|
/// </summary>
|
||||||
|
private void CloseReader()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (reader != null && !reader.IsClosed)
|
||||||
|
reader.Close();
|
||||||
|
cmd.Dispose();
|
||||||
|
//reader = null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回执行行
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataRow ExecuteDataTableRow(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteDataTableRow(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一行
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataRow ExecuteDataTableRow(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
DataSet ds = new DataSet();
|
||||||
|
MySqlDataAdapter adapter = new MySqlDataAdapter();
|
||||||
|
adapter.SelectCommand = cmd;
|
||||||
|
adapter.Fill(ds);
|
||||||
|
DataTable dt = ds.Tables[0];
|
||||||
|
if (dt.Rows.Count > 0)
|
||||||
|
return dt.Rows[0];
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个执行结果
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int ExecuteRowCount(string SqlString)
|
||||||
|
{
|
||||||
|
string f = ExecuteFirst(SqlString, null);
|
||||||
|
int c;
|
||||||
|
int.TryParse(f, out c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ExecuteRowCount(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
string f = ExecuteFirst(SqlString, parms);
|
||||||
|
int c;
|
||||||
|
int.TryParse(f, out c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个执行结果
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ExecuteFirst(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteFirst(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个执行结果
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int ExecuteFirstInt(string SqlString, MySqlParameter[] parms = null)
|
||||||
|
{
|
||||||
|
string id = ExecuteFirst(SqlString, parms);
|
||||||
|
int i;
|
||||||
|
int.TryParse(id, out i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个执行结果
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public decimal ExecuteFirstDecimal(string SqlString, MySqlParameter[] parms = null)
|
||||||
|
{
|
||||||
|
string id = ExecuteFirst(SqlString, parms);
|
||||||
|
decimal i;
|
||||||
|
decimal.TryParse(id, out i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ExecuteFirst(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
return reader[0].ToString();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e.Message.Contains("Illegal mix of collations"))
|
||||||
|
return null;
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回更改、插入或删除的行数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <param name="parms"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int ExecuteAffectedRows(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteAffectedRows(SqlString, null);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 返回更改、插入或删除的行数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <param name="parms"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int ExecuteAffectedRows(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
return reader.RecordsAffected;
|
||||||
|
return reader.RecordsAffected;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e.Message.Contains("Illegal mix of collations"))
|
||||||
|
return 0;
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回插入ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public long ExecuteInsertId(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteInsertId(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回第一个值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public long ExecuteInsertId(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
long id = cmd.LastInsertedId;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e.Message.Contains(" for key 'PRIMARY'") && e.Message.Contains("Duplicate entry"))
|
||||||
|
return 0;
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无返回
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool ExecuteNonQuery(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteNonQuery(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 只保留mysql可识别的字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
public string JustUtf8Words(string text)
|
||||||
|
{
|
||||||
|
JustUtf8Words(ref text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 只保留mysql可识别的字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
public void JustUtf8Words(ref string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return;
|
||||||
|
var cs = text.ToCharArray();
|
||||||
|
List<char> cc = new List<char>();
|
||||||
|
foreach (char c in cs)
|
||||||
|
{
|
||||||
|
if (c > 7814 && c < 8416 || c > 57096 && c < 59501 || c > 63707 && c < 64309 || c > 65505 || c < 0)
|
||||||
|
{
|
||||||
|
if (cc.Contains(c))
|
||||||
|
continue;
|
||||||
|
text = text.Replace(c.ToString(), "");
|
||||||
|
cc.Add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行无返回SQL语句
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString">SQL语句</param>
|
||||||
|
/// <param name="parms">Sql参数</param>
|
||||||
|
///<returns>是否执行成功</returns>
|
||||||
|
public bool ExecuteNonQuery(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e.Message.Contains("Illegal mix of collations") || e.Message.Contains("Incorrect string value")
|
||||||
|
|| e.Message.Contains(" for key 'PRIMARY'") && e.Message.Contains("Duplicate entry"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool ExecuteExists(string SqlString)
|
||||||
|
{
|
||||||
|
return ExecuteExists(SqlString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SqlString">SQL语句</param>
|
||||||
|
/// <param name="parms">SQL参数</param>
|
||||||
|
/// <returns>是否存在</returns>
|
||||||
|
public bool ExecuteExists(string SqlString, MySqlParameter[] parms)
|
||||||
|
{
|
||||||
|
SleepOrGo();
|
||||||
|
bool frist = true;
|
||||||
|
Retry:
|
||||||
|
if (Conn == null || Conn.State != ConnectionState.Open)
|
||||||
|
ConnTo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
CmdIni();
|
||||||
|
cmd.CommandText = SqlString;
|
||||||
|
if (parms != null)
|
||||||
|
foreach (MySqlParameter pram in parms)
|
||||||
|
cmd.Parameters.Add(pram);
|
||||||
|
reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (frist)
|
||||||
|
{
|
||||||
|
frist = false;
|
||||||
|
ErrorDoing(e.Message);
|
||||||
|
goto Retry;
|
||||||
|
}
|
||||||
|
AddError(e.Message, SqlString);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReader();
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止还是前进
|
||||||
|
/// </summary>
|
||||||
|
private void SleepOrGo()
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
while (IsRunning && i < 300)
|
||||||
|
{
|
||||||
|
Thread.Sleep(100);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
IsRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化CMD
|
||||||
|
/// </summary>
|
||||||
|
private void CmdIni()
|
||||||
|
{
|
||||||
|
if (reader != null && !reader.IsClosed)
|
||||||
|
reader.Close();
|
||||||
|
if (cmd != null)
|
||||||
|
cmd.Dispose();
|
||||||
|
cmd = new MySqlCommand();
|
||||||
|
cmd.Connection = Conn;
|
||||||
|
cmd.CommandType = CommandType.Text;
|
||||||
|
cmd.CommandTimeout = TimeOut;
|
||||||
|
|
||||||
|
if (Transaction != null)
|
||||||
|
cmd.Transaction = Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接数据库
|
||||||
|
/// </summary>
|
||||||
|
private void ConnTo()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Conn = new MySqlConnection(ConnString);
|
||||||
|
Conn.Open();
|
||||||
|
|
||||||
|
if (IsUseTrans)
|
||||||
|
this.Transaction = Conn.BeginTransaction();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
AddError(e.Message, ConnString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="sql"></param>
|
||||||
|
private void AddError(string message, string sql)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message) || message.Contains("Illegal mix of collations"))
|
||||||
|
return;
|
||||||
|
ErrorDoing(message);
|
||||||
|
string errStr = "数据库连接错误:" + message + "\r\nSQL语句:" + sql;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists("Log"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory("Log");
|
||||||
|
}
|
||||||
|
File.AppendAllText("Log\\log-" + DateTime.Now.ToString("yyyyMMdd") + ".txt", errStr);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
if (Transaction != null)
|
||||||
|
Transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RollBack()
|
||||||
|
{
|
||||||
|
if (Transaction != null)
|
||||||
|
Transaction.Rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关闭数据库链接
|
||||||
|
/// </summary>
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Conn != null && Conn.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
Conn.Close();
|
||||||
|
}
|
||||||
|
Conn = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (reader != null && !reader.IsClosed)
|
||||||
|
reader.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
reader = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Transaction != null)
|
||||||
|
Transaction.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
Transaction = null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MysqlKeyValues
|
||||||
|
{
|
||||||
|
public List<MysqlKeyValue> List = new List<MysqlKeyValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MysqlKeyValue
|
||||||
|
{
|
||||||
|
public MysqlKeyValue(string key, object value)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Value = value == null ? "" : MysqlUtils.ReplaceSqlSpecial(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key = "";
|
||||||
|
public string Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据连接串 key 提供 FreeSql IAdo(由框架 DI 注册的 keyed IFreeSql 提供)
|
||||||
|
/// </summary>
|
||||||
|
public static class AdoProvider
|
||||||
|
{
|
||||||
|
private static IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public static void SetServiceProvider(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IAdo Get(string connKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(connKey))
|
||||||
|
connKey = "ConnKJD";
|
||||||
|
string key = connKey switch
|
||||||
|
{
|
||||||
|
"ConnKJD" => "KJDDB",
|
||||||
|
"ConnKJDBAK" => "KJDBAKDB",
|
||||||
|
_ => "KJDDB",
|
||||||
|
};
|
||||||
|
var fsql = _serviceProvider?.GetKeyedService<IFreeSql>(key);
|
||||||
|
if (fsql == null)
|
||||||
|
{
|
||||||
|
var fsql2 = _serviceProvider?.GetService<IFreeSql>();
|
||||||
|
if (fsql2 != null)
|
||||||
|
return fsql2.Ado;
|
||||||
|
throw new InvalidOperationException("IFreeSql not registered for key " + key);
|
||||||
|
}
|
||||||
|
return fsql.Ado;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StoreHub.Framework.Commons;
|
using Store.Framework.Commons;
|
||||||
using StoreHub.KJDWebService.Services;
|
using StoreHub.KJDWebService.Services;
|
||||||
|
|
||||||
namespace StoreHub.KJDWebService.Controllers
|
namespace StoreHub.KJDWebService.Controllers
|
||||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 192 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user