OssDirectUploadService.cs
24.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using NCC.Common.Core.Manager;
using NCC.Common.Filter;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqUploadRecord;
using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_upload_record;
using NCC.System.Entitys.Permission;
using Newtonsoft.Json;
using SqlSugar;
using Yitter.IdGenerator;
namespace NCC.Extend
{
/// <summary>
/// OSS 直传服务(前端直传 OSS,服务端签名 + 确认入库 + 异步审核)
/// </summary>
[ApiDescriptionSettings(Tag = "Common", Name = "OssDirectUpload", Order = 160)]
[Route("api/Extend/[controller]")]
public class OssDirectUploadService : IDynamicApiController, ITransient
{
private readonly ISqlSugarClient _db;
private readonly IUserManager _userManager;
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _webHostEnvironment;
private static readonly string[] ImageExtensions = { "jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg" };
/// <summary>
/// 初始化 OSS 直传服务
/// </summary>
public OssDirectUploadService(
ISqlSugarClient db,
IUserManager userManager,
IConfiguration configuration,
IWebHostEnvironment webHostEnvironment)
{
_db = db;
_userManager = userManager;
_configuration = configuration;
_webHostEnvironment = webHostEnvironment;
_db.CodeFirst.InitTables(typeof(LqUploadRecordEntity));
}
/// <summary>
/// OSS 控制台「图片样式」名称,用于列表缩略图(x-oss-process=style/xxx)
/// </summary>
private string GetOssThumbnailStyleName() =>
_configuration["NCC_App:AliyunOSS:ThumbnailStyle"]
?? _configuration["NCC_APP:AliyunOSS:ThumbnailStyle"]
?? OssImageDisplayUrlHelper.DefaultThumbnailStyleName;
/// <summary>
/// 将相对路径与 LocalFileBaseUrl/Domain 拼成带域名的完整 URL;已是 http(s) 则原样返回
/// </summary>
private static string CombinePublicUrl(string publicBase, string pathOrAbsolute)
{
if (string.IsNullOrWhiteSpace(pathOrAbsolute))
return null;
if (pathOrAbsolute.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| pathOrAbsolute.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
return pathOrAbsolute;
if (string.IsNullOrWhiteSpace(publicBase))
return pathOrAbsolute.StartsWith("/") ? pathOrAbsolute : "/" + pathOrAbsolute;
var path = pathOrAbsolute.StartsWith("/") ? pathOrAbsolute : "/" + pathOrAbsolute;
return $"{publicBase.TrimEnd('/')}{path}";
}
/// <summary>
/// 按当前 OSS / 域名配置,将带域名的原图与缩略图 URL 写回实体(重试审核等)
/// </summary>
private void RefillPublicImageUrlsForRecord(LqUploadRecordEntity entity)
{
var ext = entity.Extension ?? "";
if (string.IsNullOrEmpty(entity.ObjectKey) || !ImageExtensions.Contains(ext.ToLower()))
{
entity.AccessUrl = null;
entity.ThumbnailPublicUrl = null;
return;
}
var publicBase = (_configuration["NCC_App:LocalFileBaseUrl"] ?? _configuration["NCC_App:Domain"] ?? "")
.Trim().TrimEnd('/');
var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"];
var fileName = Path.GetFileName(entity.ObjectKey);
string url;
if (!string.IsNullOrEmpty(customDomain))
url = $"{customDomain.TrimEnd('/')}/{entity.ObjectKey}";
else
url = $"/api/File/Image/{entity.FileType}/{fileName}";
entity.AccessUrl = CombinePublicUrl(publicBase, url);
var thumbStyle = GetOssThumbnailStyleName();
var thumb = OssImageDisplayUrlHelper.IsRasterImageExtension(ext)
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(url, ext, thumbStyle)
: url;
entity.ThumbnailPublicUrl = CombinePublicUrl(publicBase, thumb);
}
/// <summary>
/// 获取 OSS 直传凭证
/// </summary>
/// <remarks>
/// 前端调用此接口获取签名凭证,然后直接从浏览器上传到 OSS,不经过业务服务器。
/// 返回的 fileId + objectKeyPrefix 组合后拼接扩展名即为完整 objectKey。
/// </remarks>
/// <param name="type">文件类型(如 annexpic、workFlow 等),用于 OSS 路径分类</param>
/// <returns>OSS 直传凭证(含签名、policy、host 等)</returns>
/// <response code="200">返回直传凭证</response>
[HttpGet("GetUploadCredential")]
public GetUploadCredentialOutput GetUploadCredential([FromQuery] string type)
{
if (string.IsNullOrWhiteSpace(type))
type = "annexpic";
var accessKeyId = _configuration["NCC_App:AliyunOSS:AccessKeyId"];
var accessKeySecret = _configuration["NCC_App:AliyunOSS:AccessKeySecret"];
var endpoint = _configuration["NCC_App:AliyunOSS:Endpoint"];
var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"];
var bucketName = _configuration["NCC_App:BucketName"];
var now = DateTime.Now;
var objectKeyPrefix = $"{type}/{now:yyyy}/{now:MM}/{now:dd}/";
var fileId = $"{now:yyyyMMdd}_{YitIdHelper.NextId()}";
var expireTime = now.AddMinutes(15).ToUniversalTime();
var policyObj = new
{
expiration = expireTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
conditions = new object[]
{
new object[] { "content-length-range", 0, 10485760 },
new object[] { "starts-with", "$key", objectKeyPrefix }
}
};
var policyJson = JsonConvert.SerializeObject(policyObj);
var policyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(policyJson));
string signature;
using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(accessKeySecret)))
{
var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(policyBase64));
signature = Convert.ToBase64String(signatureBytes);
}
var host = $"https://{bucketName}.{endpoint}";
return new GetUploadCredentialOutput
{
host = host,
accessKeyId = accessKeyId,
policy = policyBase64,
signature = signature,
objectKeyPrefix = objectKeyPrefix,
fileId = fileId,
expire = new DateTimeOffset(expireTime).ToUnixTimeSeconds(),
customDomain = customDomain
};
}
/// <summary>
/// 确认上传(前端直传 OSS 成功后回调)
/// </summary>
/// <remarks>
/// 前端直传 OSS 完成后调用此接口:
/// 1. 在数据库中记录上传信息
/// 2. 如果是图片文件,触发异步审核任务
/// 3. 返回文件访问 URL
/// </remarks>
/// <param name="input">上传确认入参(objectKey、原始文件名、文件类型)</param>
/// <returns>文件信息(包含访问 URL、审核状态等)</returns>
/// <response code="200">确认成功,返回文件信息</response>
[HttpPost("ConfirmUpload")]
public async Task<dynamic> ConfirmUpload([FromBody] ConfirmUploadInput input)
{
if (string.IsNullOrWhiteSpace(input?.objectKey))
throw new Exception("objectKey 不能为空");
var extension = Path.GetExtension(input.objectKey)?.TrimStart('.') ?? "";
var fileName = Path.GetFileName(input.objectKey);
var fileId = Path.GetFileNameWithoutExtension(input.objectKey);
var fileType = input.type ?? "annexpic";
var isImage = ImageExtensions.Contains(extension.ToLower());
var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"];
var publicBase = (_configuration["NCC_App:LocalFileBaseUrl"] ?? _configuration["NCC_App:Domain"] ?? "")
.Trim().TrimEnd('/');
string proxyUrl = $"/api/File/Image/{fileType}/{fileName}";
string url;
if (!string.IsNullOrEmpty(customDomain))
{
url = $"{customDomain.TrimEnd('/')}/{input.objectKey}";
}
else
{
url = proxyUrl;
}
var thumbStyle = GetOssThumbnailStyleName();
var thumbnailUrl = isImage
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(url, extension, thumbStyle)
: null;
var thumbnailProxyUrl = isImage
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(proxyUrl, extension, thumbStyle)
: null;
string accessPersist = null;
string thumbPersist = null;
if (isImage)
{
accessPersist = CombinePublicUrl(publicBase, url);
thumbPersist = string.IsNullOrEmpty(thumbnailUrl)
? null
: CombinePublicUrl(publicBase, thumbnailUrl);
}
var entity = new LqUploadRecordEntity
{
Id = YitIdHelper.NextId().ToString(),
ObjectKey = input.objectKey,
OriginalFileName = input.originalFileName ?? fileName,
FileType = fileType,
Extension = extension,
AuditStatus = (int)ImageAuditStatusEnum.Pending,
UserId = _userManager?.UserId,
CreateTime = DateTime.Now,
AccessUrl = accessPersist,
ThumbnailPublicUrl = thumbPersist
};
await _db.Insertable(entity).ExecuteCommandAsync();
if (isImage)
{
BackgroundJob.Enqueue<ImageAuditJob>(x => x.ExecuteAsync(entity.Id));
}
else
{
entity.AuditStatus = (int)ImageAuditStatusEnum.Pass;
entity.AuditTime = DateTime.Now;
entity.AuditReason = "非图片文件,跳过审核";
await _db.Updateable(entity)
.UpdateColumns(e => new { e.AuditStatus, e.AuditTime, e.AuditReason })
.ExecuteCommandAsync();
}
return new
{
name = entity.OriginalFileName,
fileId = fileId,
uploadRecordId = entity.Id,
url = url,
thumbnailUrl = thumbnailUrl,
proxyUrl = proxyUrl,
thumbnailProxyUrl = thumbnailProxyUrl,
objectKey = input.objectKey,
auditStatus = entity.AuditStatus
};
}
/// <summary>
/// 按上传记录读取违规落盘后的本地图片(仅审核状态为违规且已成功落盘时可访问)
/// </summary>
/// <param name="id">上传记录主键</param>
/// <returns>图片二进制流</returns>
[HttpGet("IllegalImage/{id}")]
[AllowAnonymous]
[NonUnify]
public IActionResult IllegalImage(string id)
{
var entity = _db.Queryable<LqUploadRecordEntity>()
.Where(x => x.Id == id && x.AuditStatus == (int)ImageAuditStatusEnum.Reject)
.First();
if (entity == null || string.IsNullOrWhiteSpace(entity.IllegalLocalPath))
return new NotFoundResult();
if (entity.IllegalLocalPath.StartsWith("保存失败", StringComparison.Ordinal))
return new NotFoundResult();
var fullPath = ResolveIllegalImageFullPath(entity.IllegalLocalPath);
if (string.IsNullOrEmpty(fullPath) || !global::System.IO.File.Exists(fullPath))
return new NotFoundResult();
var ext = (entity.Extension ?? Path.GetExtension(fullPath)?.TrimStart('.'))?.ToLowerInvariant() ?? "jpg";
var contentType = ext switch
{
"png" => "image/png",
"gif" => "image/gif",
"webp" => "image/webp",
"bmp" => "image/bmp",
"svg" => "image/svg+xml",
_ => "image/jpeg"
};
var stream = new global::System.IO.FileStream(
fullPath, global::System.IO.FileMode.Open, global::System.IO.FileAccess.Read, global::System.IO.FileShare.Read);
return new FileStreamResult(stream, contentType)
{
FileDownloadName = Path.GetFileName(fullPath)
};
}
private string ResolveIllegalImageFullPath(string illegalLocalPath)
{
if (string.IsNullOrWhiteSpace(illegalLocalPath))
return null;
if (global::System.IO.File.Exists(illegalLocalPath))
return illegalLocalPath;
if (Path.IsPathRooted(illegalLocalPath))
return null;
var combinedContentRoot = Path.GetFullPath(
Path.Combine(_webHostEnvironment.ContentRootPath, illegalLocalPath));
if (global::System.IO.File.Exists(combinedContentRoot))
return combinedContentRoot;
var combinedCurrentDir = Path.GetFullPath(
Path.Combine(Directory.GetCurrentDirectory(), illegalLocalPath));
return global::System.IO.File.Exists(combinedCurrentDir) ? combinedCurrentDir : null;
}
private static readonly Dictionary<int, string> AuditStatusTextMap = new Dictionary<int, string>
{
{ (int)ImageAuditStatusEnum.Pending, "待审核" },
{ (int)ImageAuditStatusEnum.Pass, "通过" },
{ (int)ImageAuditStatusEnum.Reject, "违规" },
{ (int)ImageAuditStatusEnum.Failed, "审核失败" }
};
/// <summary>
/// 按上传记录 ID 查询审核状态与当前应对外使用的图片 URL(业务只绑记录 ID 时可轮询本接口)
/// </summary>
/// <param name="id">上传记录主键</param>
/// <returns>审核状态、解析后的 imageUrl / thumbnailUrl</returns>
[HttpGet("GetUploadRecord/{id}")]
public async Task<dynamic> GetUploadRecord(string id)
{
var row = await _db.Queryable<LqUploadRecordEntity>().Where(x => x.Id == id).FirstAsync();
if (row == null)
throw new Exception("上传记录不存在");
var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"];
string imageUrl = null;
string thumbnailUrl = null;
var publicBase = (_configuration["NCC_App:LocalFileBaseUrl"] ?? _configuration["NCC_App:Domain"] ?? "")
.Trim().TrimEnd('/');
if (!string.IsNullOrWhiteSpace(row.AccessUrl))
{
imageUrl = row.AccessUrl;
thumbnailUrl = string.IsNullOrWhiteSpace(row.ThumbnailPublicUrl)
? row.AccessUrl
: row.ThumbnailPublicUrl;
}
else if (!string.IsNullOrEmpty(row.ObjectKey))
{
if (!string.IsNullOrEmpty(customDomain))
{
imageUrl = $"{customDomain.TrimEnd('/')}/{row.ObjectKey}";
var thumbStyle = GetOssThumbnailStyleName();
thumbnailUrl = OssImageDisplayUrlHelper.IsRasterImageExtension(row.Extension)
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(imageUrl, row.Extension, thumbStyle)
: imageUrl;
}
else
{
var fileName = Path.GetFileName(row.ObjectKey);
var relImg = $"/api/File/Image/{row.FileType}/{fileName}";
imageUrl = CombinePublicUrl(publicBase, relImg);
var thumbStyle = GetOssThumbnailStyleName();
var relThumb = OssImageDisplayUrlHelper.IsRasterImageExtension(row.Extension)
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(relImg, row.Extension, thumbStyle)
: relImg;
thumbnailUrl = CombinePublicUrl(publicBase, relThumb);
}
}
return new
{
id = row.Id,
auditStatus = row.AuditStatus,
auditStatusText = AuditStatusTextMap.TryGetValue(row.AuditStatus, out var t) ? t : "未知",
imageUrl,
thumbnailUrl,
accessUrl = row.AccessUrl,
thumbnailPublicUrl = row.ThumbnailPublicUrl,
auditReason = row.AuditReason,
auditRawResponse = row.AuditRawResponse
};
}
/// <summary>
/// 分页查询上传记录
/// </summary>
/// <remarks>
/// 管理端查询上传记录列表,支持按审核状态、关键词、时间范围筛选。
/// keyword 模糊匹配原始文件名或 ObjectKey。
/// </remarks>
/// <param name="input">分页查询入参</param>
/// <returns>分页列表(含用户姓名、图片访问 URL)</returns>
/// <response code="200">返回分页数据</response>
[HttpGet("GetUploadRecords")]
public async Task<dynamic> GetUploadRecords([FromQuery] UploadRecordListQueryInput input)
{
var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"];
var publicBase = (_configuration["NCC_App:LocalFileBaseUrl"] ?? _configuration["NCC_App:Domain"] ?? "")
.Trim().TrimEnd('/');
var data = await _db.Queryable<LqUploadRecordEntity>()
.WhereIF(input.auditStatus.HasValue, x => x.AuditStatus == input.auditStatus.Value)
.WhereIF(!string.IsNullOrWhiteSpace(input.keyword),
x => x.OriginalFileName.Contains(input.keyword) || x.ObjectKey.Contains(input.keyword))
.WhereIF(input.startTime.HasValue, x => x.CreateTime >= input.startTime.Value)
.WhereIF(input.endTime.HasValue, x => x.CreateTime <= input.endTime.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.Select(x => new UploadRecordListOutput
{
id = x.Id,
objectKey = x.ObjectKey,
originalFileName = x.OriginalFileName,
fileType = x.FileType,
extension = x.Extension,
auditStatus = x.AuditStatus,
auditStatusText = "",
auditReason = x.AuditReason,
auditRawResponse = x.AuditRawResponse,
illegalLocalPath = x.IllegalLocalPath,
accessUrl = x.AccessUrl,
thumbnailPublicUrl = x.ThumbnailPublicUrl,
userId = x.UserId,
userName = SqlFunc.Subqueryable<UserEntity>()
.Where(u => u.Id == x.UserId).Select(u => u.RealName),
createTime = x.CreateTime,
auditTime = x.AuditTime,
imageUrl = "",
thumbnailUrl = ""
})
.ToPagedListAsync(input.currentPage, input.pageSize);
foreach (var item in data.list)
{
item.auditStatusText = AuditStatusTextMap.ContainsKey(item.auditStatus)
? AuditStatusTextMap[item.auditStatus]
: "未知";
if (!string.IsNullOrEmpty(item.objectKey))
{
if (!string.IsNullOrWhiteSpace(item.accessUrl))
{
item.imageUrl = item.accessUrl;
item.thumbnailUrl = string.IsNullOrWhiteSpace(item.thumbnailPublicUrl)
? item.accessUrl
: item.thumbnailPublicUrl;
}
else if (!string.IsNullOrEmpty(customDomain))
{
item.imageUrl = $"{customDomain.TrimEnd('/')}/{item.objectKey}";
var thumbStyle = GetOssThumbnailStyleName();
item.thumbnailUrl = OssImageDisplayUrlHelper.IsRasterImageExtension(item.extension)
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(item.imageUrl, item.extension, thumbStyle)
: item.imageUrl;
}
else
{
var fileName = Path.GetFileName(item.objectKey);
var relImg = $"/api/File/Image/{item.fileType}/{fileName}";
item.imageUrl = CombinePublicUrl(publicBase, relImg);
var thumbStyle = GetOssThumbnailStyleName();
var relThumb = OssImageDisplayUrlHelper.IsRasterImageExtension(item.extension)
? OssImageDisplayUrlHelper.AppendThumbnailToUrl(relImg, item.extension, thumbStyle)
: relImg;
item.thumbnailUrl = CombinePublicUrl(publicBase, relThumb);
}
}
}
return PageResult<UploadRecordListOutput>.SqlSugarPageResult(data);
}
/// <summary>
/// 手动重新审核
/// </summary>
/// <remarks>
/// 将指定上传记录的审核状态重置为待审核,并重新触发异步审核任务。
/// 适用于审核失败或需要重新检查的记录。
/// </remarks>
/// <param name="id">上传记录 ID</param>
/// <returns>操作结果</returns>
/// <response code="200">已重新提交审核</response>
[HttpPost("RetryAudit/{id}")]
public async Task<dynamic> RetryAudit(string id)
{
var entity = await _db.Queryable<LqUploadRecordEntity>()
.Where(x => x.Id == id)
.FirstAsync();
if (entity == null)
throw new Exception("上传记录不存在");
entity.AuditStatus = (int)ImageAuditStatusEnum.Pending;
entity.AuditReason = null;
entity.AuditTime = null;
entity.AuditRawResponse = null;
entity.IllegalLocalPath = null;
RefillPublicImageUrlsForRecord(entity);
await _db.Updateable(entity)
.UpdateColumns(e => new
{
e.AuditStatus,
e.AuditReason,
e.AuditTime,
e.AuditRawResponse,
e.IllegalLocalPath,
e.AccessUrl,
e.ThumbnailPublicUrl
})
.ExecuteCommandAsync();
BackgroundJob.Enqueue<ImageAuditJob>(x => x.ExecuteAsync(id));
return new { success = true, message = "已重新提交审核" };
}
/// <summary>
/// 获取审核统计
/// </summary>
/// <remarks>
/// 返回各审核状态的记录数量,用于管理端仪表盘展示。
/// </remarks>
/// <returns>各状态数量统计</returns>
/// <response code="200">返回审核统计数据</response>
[HttpGet("GetAuditStats")]
public async Task<AuditStatsOutput> GetAuditStats()
{
var allRecords = await _db.Queryable<LqUploadRecordEntity>()
.GroupBy(x => x.AuditStatus)
.Select(x => new
{
status = x.AuditStatus,
count = SqlFunc.AggregateCount(x.Id)
})
.ToListAsync();
var result = new AuditStatsOutput
{
pending = allRecords.FirstOrDefault(x => x.status == (int)ImageAuditStatusEnum.Pending)?.count ?? 0,
pass = allRecords.FirstOrDefault(x => x.status == (int)ImageAuditStatusEnum.Pass)?.count ?? 0,
reject = allRecords.FirstOrDefault(x => x.status == (int)ImageAuditStatusEnum.Reject)?.count ?? 0,
failed = allRecords.FirstOrDefault(x => x.status == (int)ImageAuditStatusEnum.Failed)?.count ?? 0
};
result.total = result.pending + result.pass + result.reject + result.failed;
return result;
}
}
}