as is
This commit is contained in:
@@ -19,6 +19,11 @@ public static class Consts
|
||||
/// </summary>
|
||||
public const string DEFAULT_ADMIN_ACCOUNT = "stopshopping";
|
||||
|
||||
/// <summary>
|
||||
/// 文件服务httpclient名
|
||||
/// </summary>
|
||||
public const string FILE_API_CLIENT_NAME = "file_api_client";
|
||||
|
||||
public static class CacheKeys
|
||||
{
|
||||
public static string AccessTokenBlacklist(string token)
|
||||
|
||||
@@ -5,17 +5,22 @@ namespace StopShopping.Services.Extensions;
|
||||
/// </summary>
|
||||
public record AppOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件服务站点
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string FileApiDomain { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 文件服务本地站点
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string FileApiLocalDomain { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// .bjbj.me
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string CookieDomain { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 域名,http(s)://www.xxx.xx
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string DomainPath { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// anti-forgery 请求头
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using FileSignatures;
|
||||
using FileSignatures.Formats;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StopShopping.EF;
|
||||
using StopShopping.Services;
|
||||
using StopShopping.Services.Extensions;
|
||||
@@ -21,16 +20,18 @@ public static class ServicesExtensions
|
||||
|
||||
services.Configure<AppOptions>(appOptions);
|
||||
|
||||
var imageFormats = FileFormatLocator.GetFormats().OfType<Image>();
|
||||
var imageInspector = new FileFormatInspector(imageFormats);
|
||||
services.AddSingleton<IFileFormatInspector>(imageInspector);
|
||||
services.AddHttpClient(Consts.FILE_API_CLIENT_NAME, (sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<AppOptions>>();
|
||||
client.BaseAddress = new Uri(options.Value.FileApiLocalDomain);
|
||||
});
|
||||
|
||||
services.AddSingleton<ICipherService, CipherService>();
|
||||
services.AddSingleton<ISerialNoGenerator, NanoidSerialNoGenerator>();
|
||||
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.AddScoped<IDistrictService, DistrictService>();
|
||||
services.AddScoped<IClaimsService, ClaimsService>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
services.AddScoped<IAccessTokenService, AccessTokenService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<ICategoryService, CategoryService>();
|
||||
|
||||
@@ -24,5 +24,5 @@ public interface IClaimsService
|
||||
/// 获取当前登录用户id
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
int GetCurrentUserId();
|
||||
int? GetCurrentUserId();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface IFileService
|
||||
/// </summary>
|
||||
/// <param name="payload"></param>
|
||||
/// <returns></returns>
|
||||
Task<ApiResponse<FileUpload>> UploadFileAsync(UploadParams payload);
|
||||
Task<ApiResponse<FileUploadResp>> UploadFileAsync(UploadParams payload);
|
||||
/// <summary>
|
||||
/// 获取文件链接
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Data.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StopShopping.EF;
|
||||
using StopShopping.Services.Models;
|
||||
using StopShopping.Services.Models.Req;
|
||||
using StopShopping.Services.Models.Resp;
|
||||
|
||||
@@ -182,8 +183,8 @@ public class CategoryService : ICategoryService
|
||||
{
|
||||
Id = model.Id,
|
||||
LogoUrl = string.IsNullOrWhiteSpace(model.Logo)
|
||||
? ""
|
||||
: _fileService.GetFileUrl(Models.UploadScences.Category, model.Logo),
|
||||
? ""
|
||||
: _fileService.GetFileUrl(UploadScences.Category, model.Logo),
|
||||
Name = model.Name,
|
||||
Order = model.Order,
|
||||
ParentId = model.ParentId,
|
||||
|
||||
@@ -41,13 +41,13 @@ public class ClaimsService : IClaimsService
|
||||
return claimsIdentity;
|
||||
}
|
||||
|
||||
public int GetCurrentUserId()
|
||||
public int? GetCurrentUserId()
|
||||
{
|
||||
var currUserId = _httpContextAccessor.HttpContext
|
||||
?.User.FindFirstValue(JwtRegisteredClaimNames.Sub);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currUserId))
|
||||
throw new InvalidOperationException("在错误的位置获取当前登录用户");
|
||||
return null;
|
||||
|
||||
return Convert.ToInt32(currUserId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StopShopping.Services.Extensions;
|
||||
using StopShopping.Services.Models;
|
||||
@@ -9,50 +14,66 @@ namespace StopShopping.Services.Implementions;
|
||||
|
||||
public class FileService : IFileService
|
||||
{
|
||||
public FileService(IOptions<AppOptions> appOptions,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
public FileService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<AppOptions> options,
|
||||
ILogger<FileService> logger)
|
||||
{
|
||||
_appOptions = appOptions.Value;
|
||||
_env = webHostEnvironment;
|
||||
_fileClient = httpClientFactory.CreateClient(Consts.FILE_API_CLIENT_NAME);
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly HttpClient _fileClient;
|
||||
private readonly AppOptions _options;
|
||||
private readonly ILogger<FileService> _logger;
|
||||
|
||||
public async Task<ApiResponse<FileUpload>> UploadFileAsync(UploadParams payload)
|
||||
private const string UPLOAD_PATH = "/upload";
|
||||
|
||||
public async Task<ApiResponse<FileUploadResp>> UploadFileAsync(UploadParams payload)
|
||||
{
|
||||
var newName = Guid.NewGuid().ToString("N").ToLower();
|
||||
var extension = Path.GetExtension(payload.File!.FileName);
|
||||
var newFullName = $"{newName}{extension}";
|
||||
var relativeToRootPath = GetRelativeToRootPath(payload.Scences, newFullName);
|
||||
var targetPath = Path.Combine(_env.WebRootPath, GetRelativeToRootPath(payload.Scences));
|
||||
|
||||
if (!Directory.Exists(targetPath))
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(targetPath);
|
||||
var formData = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(((int)payload.Scences).ToString()), nameof(payload.Scences) },
|
||||
};
|
||||
var fileContent = new StreamContent(payload.File!.OpenReadStream());
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(payload.File.ContentType);
|
||||
formData.Add(fileContent, nameof(payload.File), payload.File.FileName);
|
||||
|
||||
var resp = await _fileClient.PostAsync(UPLOAD_PATH, formData);
|
||||
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var deserialized = await resp.Content.ReadFromJsonAsync<ApiResponse<FileUploadResp>>();
|
||||
if (null == deserialized)
|
||||
throw new JsonException("上传失败");
|
||||
return deserialized;
|
||||
}
|
||||
else
|
||||
{
|
||||
var deserialized = await resp.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
if (null == deserialized)
|
||||
throw new BadHttpRequestException("上传失败", (int)resp.StatusCode);
|
||||
throw new BadHttpRequestException(
|
||||
deserialized.Title ?? deserialized.Detail ?? "上传失败",
|
||||
deserialized.Status ?? StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
using var file = new FileStream(
|
||||
Path.Combine(_env.WebRootPath, relativeToRootPath),
|
||||
FileMode.CreateNew,
|
||||
FileAccess.Write);
|
||||
|
||||
await payload.File.CopyToAsync(file);
|
||||
|
||||
FileUpload result = new()
|
||||
catch (Exception e)
|
||||
{
|
||||
NewName = newFullName,
|
||||
Url = GetFileUrl(payload.Scences, newFullName)
|
||||
};
|
||||
|
||||
return new ApiResponse<FileUpload>(result);
|
||||
_logger.LogError(e, "上传失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetFileUrl(UploadScences scences, string fileName)
|
||||
{
|
||||
var relativeToRootPath = GetRelativeToRootPath(scences, fileName);
|
||||
|
||||
return $"{_appOptions.DomainPath}/{relativeToRootPath.Replace(Path.DirectorySeparatorChar, '/')}";
|
||||
return string.IsNullOrWhiteSpace(fileName)
|
||||
? ""
|
||||
: $"{_options.FileApiDomain}/{GetRelativeToRootPath(scences, fileName)
|
||||
.Replace(Path.DirectorySeparatorChar, '/')}";
|
||||
}
|
||||
|
||||
private string GetRelativeToRootPath(UploadScences scences, string fileName = "")
|
||||
|
||||
@@ -44,7 +44,9 @@ public class ProductService : IProductService
|
||||
var qry = _dbContext.Products
|
||||
.AsNoTracking()
|
||||
.Include(p => p.Category)
|
||||
.Where(p => !p.Deleted && p.UserId == userId);
|
||||
.Where(p => !p.Deleted);
|
||||
if (userId.HasValue)
|
||||
qry = qry.Where(p => p.UserId == userId);
|
||||
if (model.CategoryId > 0)
|
||||
{
|
||||
var categoryPath = (await _dbContext.Categories
|
||||
@@ -74,7 +76,8 @@ public class ProductService : IProductService
|
||||
|
||||
public async Task<ApiResponse> EditAsync(EditProductParams model)
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
|
||||
EF.Models.Product? product = null;
|
||||
if (model.Id > 0)
|
||||
{
|
||||
@@ -112,7 +115,7 @@ public class ProductService : IProductService
|
||||
|
||||
public async Task<ApiResponse> DeleteAsync(ProductIdParams model)
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
var product = await _dbContext.Products
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.Id == model.ProductId
|
||||
@@ -169,11 +172,11 @@ public class ProductService : IProductService
|
||||
CreateTime = product.CreateTime.ToFormatted(),
|
||||
Description = product.Description,
|
||||
Id = product.Id,
|
||||
LogoUrl = _fileService.GetFileUrl(Models.UploadScences.Product, product.Logo),
|
||||
MinimumUnit = product.MinimumUnit,
|
||||
Name = product.Name,
|
||||
UnitPrice = product.UnitPrice,
|
||||
SoldAmount = product.SoldAmount,
|
||||
LogoUrl = _fileService.GetFileUrl(Models.UploadScences.Product, product.Logo)
|
||||
};
|
||||
if (result is ProductInfo)
|
||||
{
|
||||
@@ -184,10 +187,5 @@ public class ProductService : IProductService
|
||||
return result;
|
||||
}
|
||||
|
||||
private string BuildCategoryPath(int categoryId)
|
||||
{
|
||||
return $"/{categoryId}/";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class ReplyService : IReplyService
|
||||
|
||||
public async Task<ApiResponse> ReplyAsync(ReplyParams model)
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
|
||||
using var trans = await _dbContext.Database.BeginTransactionAsync();
|
||||
try
|
||||
|
||||
@@ -26,7 +26,7 @@ public class RequestService : IRequestService
|
||||
public async Task<ApiResponse> PublishRequestAsync(CreateRequestParams model)
|
||||
{
|
||||
var serialNo = _serialNoGenerator.GenerateRequestNo();
|
||||
var userId = _claimService.GetCurrentUserId();
|
||||
var userId = _claimService.GetCurrentUserId()!.Value;
|
||||
|
||||
EF.Models.Request req = new()
|
||||
{
|
||||
@@ -62,7 +62,7 @@ public class RequestService : IRequestService
|
||||
|
||||
public async Task<ApiResponse> DeleteRequestAsync(RequestIdParams model)
|
||||
{
|
||||
var userId = _claimService.GetCurrentUserId();
|
||||
var userId = _claimService.GetCurrentUserId()!.Value;
|
||||
|
||||
var request = await _dbContext.Requests
|
||||
.Include(r => r.Replies)
|
||||
@@ -126,7 +126,7 @@ public class RequestService : IRequestService
|
||||
|
||||
if (userRoles.HasValue)
|
||||
{
|
||||
var userId = _claimService.GetCurrentUserId();
|
||||
var userId = _claimService.GetCurrentUserId()!.Value;
|
||||
qry = userRoles.Value switch
|
||||
{
|
||||
UserRoles.Seller => qry.Where(q => q.Replies.Any(r => r.UserId == userId)),
|
||||
|
||||
@@ -150,7 +150,7 @@ public class UserService : IUserService
|
||||
|
||||
public async Task<ApiResponse> ChangePasswordAsync(ChangePasswordParams model)
|
||||
{
|
||||
int userId = _claimsService.GetCurrentUserId();
|
||||
int userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
var user = await _dbContext.Users
|
||||
.FirstAsync(u => u.Id == userId);
|
||||
|
||||
@@ -166,7 +166,7 @@ public class UserService : IUserService
|
||||
|
||||
public async Task<ApiResponse<User>> GetUserInfoAsync()
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
var model = await _dbContext.Users
|
||||
.FirstAsync(u => u.Id == userId);
|
||||
|
||||
@@ -187,7 +187,7 @@ public class UserService : IUserService
|
||||
|
||||
public async Task<ApiResponse> EditAsync(EditUserParams model)
|
||||
{
|
||||
int userId = _claimsService.GetCurrentUserId();
|
||||
int userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
var user = await _dbContext.Users.FirstAsync(u => u.Id == userId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.AvatarFileName))
|
||||
@@ -205,7 +205,7 @@ public class UserService : IUserService
|
||||
|
||||
public ApiResponse<List<Address>> GetAddresses()
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
|
||||
var addresses = _dbContext.Addresses
|
||||
.Where(a => a.UserId == userId)
|
||||
@@ -221,7 +221,7 @@ public class UserService : IUserService
|
||||
{
|
||||
EF.Models.Address? address = null;
|
||||
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
|
||||
if (model.Id.HasValue && model.Id > 0)
|
||||
{
|
||||
@@ -253,7 +253,7 @@ public class UserService : IUserService
|
||||
|
||||
public async Task<ApiResponse> DeleteAddressAsync(int id)
|
||||
{
|
||||
var userId = _claimsService.GetCurrentUserId();
|
||||
var userId = _claimsService.GetCurrentUserId()!.Value;
|
||||
await _dbContext.Addresses
|
||||
.Where(a => a.Id == id && a.UserId == userId)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
22
StopShopping.Services/Models/Req/NameUrlParams.cs
Normal file
22
StopShopping.Services/Models/Req/NameUrlParams.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StopShopping.Services.Models.Req;
|
||||
|
||||
/// <summary>
|
||||
/// url查询参数
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public record NameUrlParams
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public UploadScences Scences { get; set; }
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
[Required]
|
||||
public string[] Names { get; set; } = [];
|
||||
}
|
||||
@@ -15,10 +15,9 @@ public record UploadParams
|
||||
/// <value></value>
|
||||
public UploadScences Scences { get; set; }
|
||||
/// <summary>
|
||||
/// 文件
|
||||
/// 文件,2m,图片格式
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
[Required]
|
||||
[ImageFileValidation(2 * 1024 * 1024)]
|
||||
public IFormFile? File { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace StopShopping.Services.Models.Resp;
|
||||
/// <summary>
|
||||
/// 文件上传结果
|
||||
/// </summary>
|
||||
public class FileUpload
|
||||
public class FileUploadResp
|
||||
{
|
||||
/// <summary>
|
||||
/// 新名,上传后重命名
|
||||
14
StopShopping.Services/Models/Resp/NameUrlResp.cs
Normal file
14
StopShopping.Services/Models/Resp/NameUrlResp.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace StopShopping.Services.Models.Resp;
|
||||
|
||||
/// <summary>
|
||||
/// url查询响应
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public class NameUrlResp
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名:文件链接
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public KeyValuePair<string, string>[] NameUrls { get; set; } = [];
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using FileSignatures;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class ImageFileValidationAttribute : ValidationAttribute
|
||||
{
|
||||
public ImageFileValidationAttribute(long length)
|
||||
{
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public long Length { get; set; }
|
||||
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
var fileInspector = validationContext.GetRequiredService<IFileFormatInspector>();
|
||||
if (null == fileInspector)
|
||||
{
|
||||
return new ValidationResult("未配置文件验证器");
|
||||
}
|
||||
if (value is IFormFile file)
|
||||
{
|
||||
var result = Validate(fileInspector, file);
|
||||
if (null != result)
|
||||
return result;
|
||||
}
|
||||
else if (value is IFormFileCollection files)
|
||||
{
|
||||
foreach (var f in files)
|
||||
{
|
||||
var result = Validate(fileInspector, f);
|
||||
if (null != result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
private ValidationResult? Validate(IFileFormatInspector fileInspector, IFormFile file)
|
||||
{
|
||||
if (file.Length > Length)
|
||||
return new ValidationResult("文件太大");
|
||||
var format = fileInspector.DetermineFileFormat(file.OpenReadStream());
|
||||
if (null == format)
|
||||
return new ValidationResult("文件格式不支持");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FileSignatures" Version="7.0.0" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user