Parcourir la source

Merge branch 'test' of http://110.188.24.182:10023/Fengwo/hotline into test

xf il y a 5 jours
Parent
commit
6cd2149dd6
36 fichiers modifiés avec 600 ajouts et 42 suppressions
  1. 2 2
      src/Hotline.Api/Controllers/AiController.cs
  2. 11 0
      src/Hotline.Api/Controllers/Exam/TrainRecordController.cs
  3. 37 0
      src/Hotline.Api/Controllers/Snapshot/SnapshotUserController.cs
  4. 2 0
      src/Hotline.Application/Exam/Constants/ApiRoutes/TrainRecordApiRoute.cs
  5. 7 0
      src/Hotline.Application/Exam/Interface/Train/ITrainRecordService.cs
  6. 11 0
      src/Hotline.Application/Exam/QueryExtensions/Trains/TrainRecordQueryExtensions.cs
  7. 4 2
      src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs
  8. 4 4
      src/Hotline.Application/Exam/Service/Practices/PracticeService.cs
  9. 54 5
      src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs
  10. 12 12
      src/Hotline.Application/OrderApp/OrderApplication.cs
  11. 24 0
      src/Hotline.Application/Snapshot/Contracts/ISnapshotUserApplication.cs
  12. 83 0
      src/Hotline.Application/Snapshot/SnapshotUserApplication.cs
  13. 1 1
      src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamAnswerValidator.cs
  14. 0 1
      src/Hotline.Repository.SqlSugar/Exam/Validators/Sourcewares/SourcewareCategoryValidator.cs
  15. 19 0
      src/Hotline.Repository.SqlSugar/Snapshot/SafetyTypeRepository.cs
  16. 1 1
      src/Hotline.Share/Dtos/Ai/AiDto.cs
  17. 1 1
      src/Hotline.Share/Dtos/ExamManages/ExamTagDto.cs
  18. 76 1
      src/Hotline.Share/Dtos/Snapshot/SnapshotUserInfoDto.cs
  19. 17 6
      src/Hotline.Share/Dtos/Trains/TrainPracticeDto.cs
  20. 9 0
      src/Hotline.Share/Requests/Train/TrainPracticeRequest.cs
  21. 9 0
      src/Hotline.Share/Requests/Train/TrainRecordPagedRequest.cs
  22. 14 0
      src/Hotline.Share/Tools/TaskExtensions.cs
  23. 5 0
      src/Hotline/Caching/Interfaces/ISysDicDataCacheManager.cs
  24. 5 0
      src/Hotline/Caching/Services/SysDicDataCacheManager.cs
  25. 3 0
      src/Hotline/Orders/Citizen.cs
  26. 12 0
      src/Hotline/SeedData/SystemDicDataSeedData.cs
  27. 5 1
      src/Hotline/Settings/SysDicTypeConsts.cs
  28. 26 0
      src/Hotline/Snapshot/CitizenRelationSafetyType.cs
  29. 12 0
      src/Hotline/Snapshot/IRepository/ISafetyTypeRepository.cs
  30. 23 0
      src/Hotline/Snapshot/SafetyType.cs
  31. 1 1
      src/Hotline/Snapshot/Services/SnapshotPointsDomainService.cs
  32. 0 1
      src/Hotline/Validators/Exams/ExamManages/AddExamTagDtoValidator.cs
  33. 0 1
      src/Hotline/Validators/Exams/ExamManages/UpdateExamTagDtoValidator.cs
  34. 0 1
      src/Hotline/Validators/Exams/Sourcewares/AddSourcewareCategoryDtoValidator.cs
  35. 0 1
      src/Hotline/Validators/Exams/Sourcewares/UpdateSourceCategoryDtoValidator.cs
  36. 110 0
      test/Hotline.Tests/Application/SnapshotUserApplicationTest.cs

+ 2 - 2
src/Hotline.Api/Controllers/AiController.cs

@@ -559,7 +559,7 @@ namespace Hotline.Api.Controllers
                                 aiOrderVisitDetail.AiOrderVisitState = Share.Enums.Ai.EAiOrderVisitState.Ended; //更新AI子表
                                 aiOrderVisitDetail.AiVisitTime = DateTime.Now;
                                 aiOrderVisitDetail.CallTime = string.IsNullOrEmpty(callRecord.CallTime)? null : DateTime.Parse(callRecord.CallTime);
-                                aiOrderVisitDetail.SentenceList = _mapper.Map<List<SentenceList>>(callRecord.SentenceList);
+                                //aiOrderVisitDetail.SentenceList = _mapper.Map<List<SentenceList>>(callRecord.SentenceList);
                                 aiOrderVisit.VisitedCount++;
                                 //处理结果
                                 var visitDetail = _orderVisitDetailRepository.Queryable().Where(x => x.VisitId == aiOrderVisitDetail.OrderVisit.Id).ToList();
@@ -683,7 +683,7 @@ namespace Hotline.Api.Controllers
                                         //TODO 记录不满意原因到内容中供人工回访甄别选择不满意原因
                                         if (dto.CallTimes.Value>=1 && orgProcessingResults == null)
                                         {
-                                            orgProcessingResults = new Kv() { Key = "6", Value = "未接通" };
+                                            orgProcessingResults = new Kv() { Key = "0", Value = "默认满意" };
                                             aiOrderVisitDetail.OrderVisit.VisitState = EVisitState.Visited;
                                             aiOrderVisitDetail.OrderVisit.NowEvaluate = orgProcessingResults;
                                             x.VisitContent = "智能回访两次未接默认已回访";

+ 11 - 0
src/Hotline.Api/Controllers/Exam/TrainRecordController.cs

@@ -58,6 +58,17 @@ namespace Hotline.Api.Controllers.Exam
             return await _trainRecordService.GetTrainPracticeAsync(trainPracticeRequest);
         }
 
+        /// <summary>
+        /// 获取培训习题
+        /// </summary>
+        /// <param name="trainPracticeRequest"></param>
+        /// <returns></returns>
+        [HttpGet(TrainRecordApiRoute.GetSimpleTrainPractice)]
+        public async Task<SimpleTrainPracticeDto> GetSimpleTrainPractice([FromQuery] TrainPracticeRequest trainPracticeRequest)
+        {
+            return await _trainRecordService.GetSimpleTrainPracticeAsync(trainPracticeRequest);
+        }
+
         /// <summary>
         /// 完成培训课件学习
         /// </summary>

+ 37 - 0
src/Hotline.Api/Controllers/Snapshot/SnapshotUserController.cs

@@ -0,0 +1,37 @@
+using Hotline.Application.Snapshot.Contracts;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel;
+
+namespace Hotline.Api.Controllers.Snapshot;
+
+[Description("随手拍用户管理")]
+public class SnapshotUserController : BaseController
+{
+    private readonly ISnapshotUserApplication _snapshotUserApplication;
+
+    public SnapshotUserController(ISnapshotUserApplication snapshotUserApplication)
+    {
+        _snapshotUserApplication = snapshotUserApplication;
+    }
+
+    /// <summary>
+    /// 安全志愿者列表
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("citizen_relation")]
+    public async Task<PagedDto<CitizenRelationSafetyTypeOutDto>> GetCitizenRelationSafetyType([FromQuery] CitizenRelationSafetyTypeInDto dto)
+        => (await _snapshotUserApplication.GetCitizenRelationSafetyType(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 添加安全志愿者
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("citizen_relation")]
+    public async Task AddCitizenRelationSafetyTypeInDto([FromBody]AddCitizenRelationSafetyTypeInDto dto)
+        => await _snapshotUserApplication.AddCitizenRelationSafetyType(dto, HttpContext.RequestAborted);
+}

+ 2 - 0
src/Hotline.Application/Exam/Constants/ApiRoutes/TrainRecordApiRoute.cs

@@ -11,6 +11,8 @@ namespace Hotline.Application.Exam.Constants.ApiRoutes
 
         public const string GetTrainPractice = "GetTrainPractice";
 
+        public const string GetSimpleTrainPractice = "GetSimpleTrainPractice";
+
         public const string CompleteTrainKnowladge = "CompleteTrainKnowladge";
 
         public const string CompleteTrainRecord = "CompleteTrainRecord";

+ 7 - 0
src/Hotline.Application/Exam/Interface/Train/ITrainRecordService.cs

@@ -32,6 +32,13 @@ namespace Hotline.Application.Exam.Interface.Train
         /// <returns></returns>
         Task<TrainPracticeDto> GetTrainPracticeAsync(TrainPracticeRequest trainPracticeRequest);
 
+        /// <summary>
+        /// 获取培训习题
+        /// </summary>
+        /// <param name="trainPracticeRequest"></param>
+        /// <returns></returns>
+        Task<SimpleTrainPracticeDto> GetSimpleTrainPracticeAsync(TrainPracticeRequest trainPracticeRequest);
+
         /// <summary>
         /// 完成培训课件学习
         /// </summary>

+ 11 - 0
src/Hotline.Application/Exam/QueryExtensions/Trains/TrainRecordQueryExtensions.cs

@@ -26,6 +26,17 @@ namespace Hotline.Application.Exam.QueryExtensions.Trains
             return expression;
         }
 
+        public static Expression<Func<ExamTrainRecord, bool>> GetTrainRecordExpression(this TrainRecordPagedRequest trainRecordPagedRequest)
+        {
+            Expression<Func<ExamTrainRecord, bool>> expression = m => m.Id != null;
+
+            expression = ExpressionableUtility.CreateExpression<ExamTrainRecord>()
+                .AndIF(trainRecordPagedRequest.UserId.IsNotNull(), x => x.UserId == trainRecordPagedRequest.UserId)
+                .ToExpression();
+
+            return expression;
+        }
+
         public static Expression<Func<ExamTrainPractice, bool>> GetExpression(this TrainQuestionRequest trainQuestionRequest)
         {
             Expression<Func<ExamTrainPractice, bool>> expression = m => m.Id != null;

+ 4 - 2
src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs

@@ -676,7 +676,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             userExamItem.UserExamItemOptionses = await AddUserExamItemOptions(addUserExamItemDto, cancellationToken);
 
-            userExamItem.ExamAnswers = await AddExamAnswer(addUserExamItemDto, cancellationToken);
+            userExamItem.ExamAnswers = await AddExamAnswer(addUserExamItemDto, userExamItem.Id, cancellationToken);
+            
 
             await userExamItemRepository.AddNav(userExamItem)
                 .Include(x => x.UserExamItemOptionses)
@@ -731,7 +732,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             return null;
         }
 
-        private async Task<List<ExamAnswer>> AddExamAnswer(AddUserExamItemDto addUserExamItemDto, CancellationToken cancellationToken)
+        private async Task<List<ExamAnswer>> AddExamAnswer(AddUserExamItemDto addUserExamItemDto,string id, CancellationToken cancellationToken)
         {
             if (addUserExamItemDto.QuestionType.CheckSelectType()) return null;
 
@@ -740,6 +741,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             var examAnswer = new ExamAnswer
             {
                 UserId = _sessionContext.UserId,
+                UserExamItemId = id,
                 Answer = addUserExamItemDto.Answer
             };
 

+ 4 - 4
src/Hotline.Application/Exam/Service/Practices/PracticeService.cs

@@ -353,7 +353,7 @@ namespace Hotline.Application.Exam.Service.Practices
                                       .Where((p, r, i) => r.PracticeQuestionId == practiceQuestionRequest.PracticeQuestionId)
                                       .Select((p, r, i) => i);
 
-            return await practiceResultItems.ToListAsync();
+            return await practiceResultItems.Distinct().ToListAsync();
         }
 
         private async Task<List<PracticeQuestionKnowladgeDto>> GetPracticeQuestionKnowladge(PracticeQuestionRequest practiceQuestionRequest)
@@ -374,7 +374,7 @@ namespace Hotline.Application.Exam.Service.Practices
                 });
 
 
-            return await queryResult.ToListAsync();
+            return await queryResult.Distinct().ToListAsync();
         }
 
         private async Task<List<PracticeQuestionSourcewareDto>> GetPracticeQuestionSourceware(PracticeQuestionRequest practiceQuestionRequest)
@@ -397,7 +397,7 @@ namespace Hotline.Application.Exam.Service.Practices
                 });
 
 
-            return await queryResult.ToListAsync();
+            return await queryResult.Distinct().ToListAsync();
         }
 
         private async Task<List<PracticeQuestionOptionsDto>> GetPracticeQuestionOptions(PracticeQuestionRequest practiceQuestionRequest)
@@ -415,7 +415,7 @@ namespace Hotline.Application.Exam.Service.Practices
                     QuestionOptionId = o.QuestionOptionId
                 });
 
-            return await queryResult.ToListAsync();
+            return await queryResult.Distinct().ToListAsync();
         }
         private SqlSugar.ISugarQueryable<PracticeViewResponse> QueryResult(PracticePagedRequest queryRequest)
         {

+ 54 - 5
src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs

@@ -28,6 +28,10 @@ using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
 using Hotline.Share.Exams.Extensions;
 using NPOI.SS.Formula.Functions;
+using Hotline.Repository.SqlSugar.Exam.Repositories.Trains;
+using static NPOI.HSSF.Util.HSSFColor;
+using DocumentFormat.OpenXml.Drawing.Charts;
+using JiebaNet.Segmenter.Common;
 
 namespace Hotline.Application.Exam.Service.Trains
 {
@@ -131,14 +135,16 @@ namespace Hotline.Application.Exam.Service.Trains
 
         public async Task<TrainPracticeDto> GetTrainPracticeAsync(TrainPracticeRequest trainPracticeRequest)
         {
-            var trainPracticeRepostitory = new TrainPracticeRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            SqlSugar.ISugarQueryable<ExamTrainPractice> trainPractices = GetTrainPractices(trainPracticeRequest);
 
-            var trainPractice = await trainPracticeRepostitory.GetAsync(trainPracticeRequest.TrainPracticeId);
+            var trainPractice = await trainPractices.FirstAsync();
 
             if (trainPractice == null) return null;
 
             var trainPracticeDto = _mapper.Map<TrainPracticeDto>(trainPractice);
 
+            trainPracticeRequest.TrainPracticeId = trainPractice.Id;
+
             trainPracticeDto.TrainPracticeOptionsDtos = await GetTrainQuestionOptions(trainPracticeRequest);
 
             trainPracticeDto.TrainPracticeSourcewareDtos = await GetTrainPracticeSourcewares(trainPracticeRequest);
@@ -148,6 +154,23 @@ namespace Hotline.Application.Exam.Service.Trains
             return trainPracticeDto;
         }
 
+        public async Task<SimpleTrainPracticeDto> GetSimpleTrainPracticeAsync(TrainPracticeRequest trainPracticeRequest)
+        {
+            SqlSugar.ISugarQueryable<ExamTrainPractice> trainPractices = GetTrainPractices(trainPracticeRequest);
+
+            var trainPractice = await trainPractices.FirstAsync();
+
+            if (trainPractice == null) return null;
+
+            var trainPracticeDto = _mapper.Map<TrainPracticeDto>(trainPractice);
+
+            trainPracticeRequest.TrainPracticeId = trainPractice.Id;
+
+            trainPracticeDto.TrainPracticeOptionsDtos = await GetTrainQuestionOptions(trainPracticeRequest);
+
+            return trainPracticeDto;
+        }
+
         public async Task<TrainResultPagedViewResponse> AnalysisTrainResult(TrainResultReportPagedRequest trainResultReportPagedRequest)
         {
             trainResultReportPagedRequest.ResoleEndTime();
@@ -244,9 +267,11 @@ namespace Hotline.Application.Exam.Service.Trains
         private SqlSugar.ISugarQueryable<TrainRecordViewResponse> QueryResult(TrainRecordPagedRequest queryRequest)
         {
             var trainPlanRepository = new TrainPlanRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            queryRequest.UserId = _sessionContext.UserId;
             var expression = queryRequest.GetExpression();
             var trainPlanTable = trainPlanRepository.Queryable().Where(expression);
-            var trainRecordTable = _repository.Queryable();
+            var traiRecordExpression = queryRequest.GetTrainRecordExpression();
+            var trainRecordTable = _repository.Queryable().Where(traiRecordExpression);
 
             var queryResult = trainPlanTable.InnerJoin(trainRecordTable, (p, r) => p.Id == r.TrainPlanId)
                             .Select((p, r) => new TrainRecordViewResponse
@@ -358,12 +383,36 @@ namespace Hotline.Application.Exam.Service.Trains
             return trainPracticeDto;
         }
 
+        private SqlSugar.ISugarQueryable<ExamTrainPractice> GetTrainPractices(TrainPracticeRequest trainPracticeRequest)
+        {
+            var trainPracticeRepostitory = new TrainPracticeRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var trainPlanRepository = new TrainPlanRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var trainPlanTemplateRepository = new TrainPlanTemplateRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+
+            var trainRecordTable = _repository.Queryable().Where(x => x.Id == trainPracticeRequest.Id);
+            var trainPlanTable = trainPlanRepository.Queryable();
+            var trainPracticeTable = trainPracticeRepostitory.Queryable().WhereIF(!string.IsNullOrEmpty(trainPracticeRequest.TrainPracticeId),x=>x.Id == trainPracticeRequest.TrainPracticeId);
+            var tranPlanTemplateTable = trainPlanTemplateRepository.Queryable();
+
+            var trainPractices = trainPracticeTable.InnerJoin(tranPlanTemplateTable, (tp, tpt) => tp.TrainTemplateId == tpt.TrainTemplateId)
+                .InnerJoin(trainPlanTable, (tp, tpt, tpn) => tpt.TrainPlanId == tpn.Id)
+                .InnerJoin(trainRecordTable, (tp, tpt, tpn, tr) => tpn.Id == tr.TrainPlanId)
+                .Select((tp, tpt, tpn, tr) => tp)
+                .OrderBy(tp => tp.CreationTime);
+            return trainPractices;
+        }
+
         private async Task<List<SimpleTrainPracticeOptionsDto>> GetTrainQuestionOptions(TrainPracticeRequest trainPracticeRequest)
         {
-            var trainPracticeRepository = new ExamRepository<ExamTrainPracticeOptions>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var trainPracticeOptionsRepository = new ExamRepository<ExamTrainPracticeOptions>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             var trainRecordAnswerRepository = new ExamRepository<ExamTrainRecordAnswer>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             // TODO: 获取未阅卷的第一道题
-            var trainPracticeOptions = trainPracticeRepository.Queryable().Where(x => x.TrainPracticeId == trainPracticeRequest.TrainPracticeId);
+            var trainPractices = GetTrainPractices(trainPracticeRequest);
+
+            var trainPracticeOptions = await trainPracticeOptionsRepository.Queryable()
+                .InnerJoin(trainPractices, (tpo, tp) => tpo.TrainPracticeId == tp.Id)
+                .Where((tpo, tp)=> tp.Id == trainPracticeRequest.TrainPracticeId)
+                .Select((tpo, tp) => tpo).ToListAsync();
 
             var trainRecordOptions = await trainRecordAnswerRepository.Queryable()
                 .InnerJoin<ExamTrainRecord>((t,x)=>t.TrainRecordId == x.Id)

+ 12 - 12
src/Hotline.Application/OrderApp/OrderApplication.cs

@@ -5608,17 +5608,17 @@ public class OrderApplication : IOrderApplication, IScopeDependency
     /// <returns></returns>
     public async Task<List<SendOrderReportOutDto>> SendOrderReportAsync_LZ(QuerySendOrderRequest dto)
     {
-        var itemsHandled = _workflowTraceRepository.Queryable()
-               .LeftJoin<Workflow>((x, w) => x.WorkflowId == w.Id)
-               .Where((x, w) => w.ModuleCode == WorkflowModuleConsts.OrderHandle && x.Status == EWorkflowStepStatus.Handled && (x.Name == "派单组" || x.Name == "班长审批"))
-               .Where((x, w) => x.HandleTime >= dto.StartTime.Value)
-               .Where((x, w) => x.HandleTime <= dto.EndTime.Value)
-               .WhereIF(!string.IsNullOrEmpty(dto.UserName), (x, w) => x.HandlerName == dto.UserName)
-               .GroupBy((x, w) => new { x.HandlerId, x.HandlerName, x.Name })
-               .Select((x, w) => new BiOrderSendVo
-               {
-                   UserId = x.HandlerId,
-                   UserName = x.HandlerName,
+		var itemsHandled = _workflowTraceRepository.Queryable()
+			   .LeftJoin<Workflow>((x, w) => x.WorkflowId == w.Id)
+			   .Where((x, w) => w.ModuleCode == WorkflowModuleConsts.OrderHandle && x.Status == EWorkflowStepStatus.Handled && !SqlFunc.JsonListObjectAny(x.NextHandlers, "OrgId", OrgSeedData.CenterId) && (x.Name =="派单组" || x.Name == "班长审批"))
+			   .Where((x, w) => x.HandleTime >= dto.StartTime.Value)
+			   .Where((x, w) => x.HandleTime <= dto.EndTime.Value)
+			   .WhereIF(!string.IsNullOrEmpty(dto.UserName), (x, w) => x.HandlerName == dto.UserName)
+			   .GroupBy((x, w) => new { x.HandlerId,x.HandlerName,x.Name})
+			   .Select((x, w) => new BiOrderSendVo
+			   {
+				   UserId = x.HandlerId,
+				   UserName = x.HandlerName,
                    StepName = x.Name,
                    SendOrderNum = SqlFunc.AggregateDistinctCount(w.ExternalId),
                    NoSendOrderNum = 0,
@@ -5726,7 +5726,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
                 .Where((x, su) => x.ModuleCode == WorkflowModuleConsts.OrderHandle && x.BusinessType == EBusinessType.Send)
                 .Where((x, su) => x.CreationTime >= dto.StartTime.Value && x.CreationTime <= dto.EndTime.Value && su.UserId == dto.UserId)
                 .WhereIF(dto.TitleCode.ToUpper() == "NOSENDORDERNUM", (x, su) => x.Status != EWorkflowStepStatus.Handled)
-                .WhereIF(dto.TitleCode.ToUpper() == "SENDORDERNUM", (x, su) => x.Status == EWorkflowStepStatus.Handled)
+                .WhereIF(dto.TitleCode.ToUpper() == "SENDORDERNUM", (x, su) => x.Status == EWorkflowStepStatus.Handled && !SqlFunc.JsonListObjectAny(x.NextHandlers, "OrgId", OrgSeedData.CenterId))
                 .GroupBy((x, su) => x.ExternalId)
                 .Select((x, su) => new { Id = x.ExternalId })
                 .MergeTable()

+ 24 - 0
src/Hotline.Application/Snapshot/Contracts/ISnapshotUserApplication.cs

@@ -0,0 +1,24 @@
+using Hotline.Share.Dtos.Snapshot;
+using SqlSugar;
+
+namespace Hotline.Application.Snapshot.Contracts;
+
+public interface ISnapshotUserApplication
+{
+    ISugarQueryable<CitizenRelationSafetyTypeOutDto> GetCitizenRelationSafetyType(CitizenRelationSafetyTypeInDto dto);
+
+    /// <summary>
+    /// 添加安全员类型和市民关系
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <param name="token"></param>
+    /// <returns></returns>
+    Task AddCitizenRelationSafetyType(AddCitizenRelationSafetyTypeInDto dto, CancellationToken token);
+
+    /// <summary>
+    /// 删除关系
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    Task DeleteCitizenRelationSafetyAsync(DeleteCitizenRelationSafetyTypeInDto dto);
+}

+ 83 - 0
src/Hotline.Application/Snapshot/SnapshotUserApplication.cs

@@ -0,0 +1,83 @@
+using DocumentFormat.OpenXml.Vml.Office;
+using Hotline.Application.Snapshot.Contracts;
+using Hotline.Caching.Interfaces;
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot;
+using Hotline.Snapshot.IRepository;
+using Hotline.ThirdAccountDomainServices;
+using Hotline.Tools;
+using Mapster;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Snapshot;
+
+public class SnapshotUserApplication : ISnapshotUserApplication, IScopeDependency
+{
+    private readonly ICitizenRepository _citizenRepository;
+    private readonly ISystemDicDataCacheManager _dicData;
+    private readonly IRepository<SafetyType> _safetyTypeRepository;
+
+    public SnapshotUserApplication(ICitizenRepository citizenRepository, ISystemDicDataCacheManager dicData, IRepository<SafetyType> safetyTypeRepository)
+    {
+        _citizenRepository = citizenRepository;
+        _dicData = dicData;
+        _safetyTypeRepository = safetyTypeRepository;
+    }
+
+    public async Task AddCitizenRelationSafetyType(AddCitizenRelationSafetyTypeInDto dto, CancellationToken token)
+    {
+        dto.ValidateObject();
+
+        var safeType = new SafetyType
+        {
+            Id = dto.SafetyTypeId,
+            Citizens = []
+        };
+        foreach (var item in dto.CitizenIds)
+        {
+            safeType.Citizens.Add(new Citizen { Id = item });
+        }
+
+        await _safetyTypeRepository.AddNav(safeType)
+            .Include(m => m.Citizens)
+            .ExecuteCommandAsync();
+    }
+
+    public ISugarQueryable<CitizenRelationSafetyTypeOutDto> GetCitizenRelationSafetyType(CitizenRelationSafetyTypeInDto dto)
+    {
+        var query = _citizenRepository.Queryable()
+            .LeftJoin<CitizenRelationSafetyType>((citizen, relation) => citizen.Id == relation.CitizenId)
+            .LeftJoin<SafetyType>((citizen, relation, safety) => relation.SafetyTypeId == safety.Id)
+            .WhereIF(dto.SafetyTypeId.NotNullOrEmpty(), (citizen, relation, safety) => safety.Id == dto.SafetyTypeId)
+            .Select((citizen, relation, safety) => new CitizenRelationSafetyTypeOutDto
+            {
+                CitizenId = citizen.Id,
+                CitizenName = citizen.Name,
+                PhoneNumber = citizen.PhoneNumber,
+                SafetyTypeName = safety.Name,
+                SafetyTypeId = safety.Id
+            });
+        return query;
+    }
+
+    public async Task DeleteCitizenRelationSafetyAsync(DeleteCitizenRelationSafetyTypeInDto dto)
+    {
+        foreach (var item in dto.Items)
+        {
+            await _citizenRepository.RemoveNav(m => m.Id == item.CitizenId)
+                .Include(m => m.SafetyTypes.Where(s => s.Id == item.SafetyTypeId).First())
+                .ExecuteCommandAsync();
+        }
+    }
+}

+ 1 - 1
src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamAnswerValidator.cs

@@ -35,7 +35,7 @@ namespace Hotline.Repository.SqlSugar.Exam.Validators.ExamManages
         {
             base.BaseValidateRule();
             RuleFor(m=>m.UserId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamAnswer.UserId))));
-            RuleFor(m=>m.UserExamItemId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamAnswer.UserId))));
+            RuleFor(m=>m.UserExamItemId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamAnswer.UserExamItemId))));
         }
 
         protected override void ValidateRuleWithAdd()

+ 0 - 1
src/Hotline.Repository.SqlSugar/Exam/Validators/Sourcewares/SourcewareCategoryValidator.cs

@@ -47,7 +47,6 @@ namespace Exam.Repository.Sqlsugar.Validators.Sourcewares
         {
             base.BaseValidateRule();
             RuleFor(m => m.Name).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.Name))));
-            RuleFor(m => m.ParentId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.ParentId))));
             RuleFor(m => m.SortIndex).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.SortIndex))));
         }
 

+ 19 - 0
src/Hotline.Repository.SqlSugar/Snapshot/SafetyTypeRepository.cs

@@ -0,0 +1,19 @@
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Snapshot;
+using Hotline.Snapshot.IRepository;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+
+namespace Hotline.Repository.SqlSugar.Snapshot;
+
+public class SafetyTypeRepository : BaseRepository<SafetyType>, ISafetyTypeRepository, IScopeDependency
+{
+    public SafetyTypeRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder, IServiceProvider serviceProvider) : base(uow, dataPermissionFilterBuilder, serviceProvider)
+    {
+    }
+}

+ 1 - 1
src/Hotline.Share/Dtos/Ai/AiDto.cs

@@ -230,7 +230,7 @@ namespace Hotline.Share.Dtos.Ai
         /// <summary>
         /// 语音段列表信息
         /// </summary>
-        public List<SentenceListDto> SentenceList { get; set; }
+        //public List<SentenceListDto> SentenceList { get; set; }
     }
 
     public class SentenceListDto

+ 1 - 1
src/Hotline.Share/Dtos/ExamManages/ExamTagDto.cs

@@ -29,7 +29,7 @@ namespace Hotline.Share.Dtos.ExamManages
         /// 父级ID
         /// </summary>
         [Description("父级ID")]
-        public string ParentId { get; set; }
+        public string? ParentId { get; set; }
     }
 
     /// <summary>

+ 76 - 1
src/Hotline.Share/Dtos/Snapshot/SnapshotUserInfoDto.cs

@@ -1,4 +1,6 @@
-using Hotline.Share.Tools;
+using Hotline.Share.Requests;
+using Hotline.Share.Tools;
+using System.ComponentModel.DataAnnotations;
 
 namespace Hotline.Share.Dtos.Snapshot;
 public class SnapshotUserInfoOutDto
@@ -49,3 +51,76 @@ public class SnapshotUserInfoOutDto
     public int AppraiseCount { get; set; }
 }
 
+public record CitizenRelationSafetyTypeInDto : PagedRequest
+{
+    /// <summary>
+    /// 志愿者类型
+    /// </summary>
+    public string? SafetyTypeId { get; set; }
+}
+
+public class CitizenRelationSafetyTypeOutDto
+{
+    /// <summary>
+    /// Id
+    /// </summary>
+    public string CitizenId { get; set; }
+
+    /// <summary>
+    /// 用户名
+    /// </summary>
+    public string? CitizenName { get; set; }
+
+    /// <summary>
+    /// 志愿者类型名称
+    /// </summary>
+    public string SafetyTypeName { get; set; }
+
+    /// <summary>
+    /// id
+    /// </summary>
+    public string SafetyTypeId { get; set; }
+
+    /// <summary>
+    /// 电话号码
+    /// </summary>
+    public string PhoneNumber { get; set; }
+}
+
+public class AddCitizenRelationSafetyTypeInDto
+{
+    /// <summary>
+    /// 志愿者类型
+    /// </summary>
+    [Required(ErrorMessage = "请选择志愿者类型")]
+    public string SafetyTypeId { get; set; }
+
+    /// <summary>
+    /// 市民Id
+    /// </summary>
+    [Required]
+    public IList<string> CitizenIds { get; set; }
+}
+
+
+public class DeleteCitizenRelationSafetyTypeInDto
+{
+    /// <summary>
+    /// 市民和志愿者类型集合
+    /// </summary>
+    public IList<CitizenIdRelationSafetyTypeId> Items { get; set; }
+}
+
+public class CitizenIdRelationSafetyTypeId
+{
+    /// <summary>
+    /// 市民Id
+    /// </summary>
+    [Required]
+    public string CitizenId { get; set; }
+
+    /// <summary>
+    /// 志愿者类型Id
+    /// </summary>
+    public string SafetyTypeId { get; set; }
+}

+ 17 - 6
src/Hotline.Share/Dtos/Trains/TrainPracticeDto.cs

@@ -14,7 +14,7 @@ namespace Hotline.Share.Dtos.Trains
     /// 培训习题
     /// </summary>
     [Description("培训习题")]
-    public class TrainPracticeDto : UpdateTrainPracticeDto
+    public class TrainPracticeDto : SimpleTrainPracticeDto
     {
         // <summary>
         /// 关联课件
@@ -28,12 +28,16 @@ namespace Hotline.Share.Dtos.Trains
         [Description("关联知识")]
         public new List<TrainPracticeKnowladgeDto> TrainPracticeKnowladgeDtos { get; set; }
 
-        /// <summary>
-        /// 培训习题选项
-        /// </summary>
-        [Description("培训习题选项")]
-        public List<SimpleTrainPracticeOptionsDto> TrainPracticeOptionsDtos { get; set; }
 
+        
+    }
+
+    /// <summary>
+    /// 培训习题
+    /// </summary>
+    [Description("培训习题")]
+    public class SimpleTrainPracticeDto : UpdateTrainPracticeDto
+    {
         /// <summary>
         /// 题型
         /// </summary>
@@ -57,6 +61,13 @@ namespace Hotline.Share.Dtos.Trains
                 return DifficultyLevel.GetDescription();
             }
         }
+
+
+        /// <summary>
+        /// 培训习题选项
+        /// </summary>
+        [Description("培训习题选项")]
+        public List<SimpleTrainPracticeOptionsDto> TrainPracticeOptionsDtos { get; set; }
     }
 
     public class AddTrainPracticeDto : IAddRequest, IOperationStatus

+ 9 - 0
src/Hotline.Share/Requests/Train/TrainPracticeRequest.cs

@@ -1,10 +1,19 @@
 using Exam.Infrastructure.Data.Interface;
+using Newtonsoft.Json;
 using System.ComponentModel;
 
 namespace Hotline.Share.Requests.Train
 {
     public class TrainPracticeRequest:IQueryRequest
     {
+
+        /// <summary>
+        /// 培训记录Id
+        /// </summary>
+        [Description("培训记录Id")]
+        public string Id { get; set; }
+
+
         /// <summary>
         /// 培训习题Id
         /// </summary>

+ 9 - 0
src/Hotline.Share/Requests/Train/TrainRecordPagedRequest.cs

@@ -1,5 +1,6 @@
 using Exam.Infrastructure.Data.Interface;
 using System.ComponentModel;
+using System.Text.Json.Serialization;
 
 namespace Hotline.Share.Requests.Train
 {
@@ -40,5 +41,13 @@ namespace Hotline.Share.Requests.Train
         /// </summary>
         [Description("最大结束时间")]
         public DateTime? MaxEndTime { get; set; }
+
+        /// <summary>
+        /// 用户Id
+        /// </summary>
+        [Description("用户Id")]
+        [JsonIgnore]
+        public string UserId { get; set; }
+
     }
 }

+ 14 - 0
src/Hotline.Share/Tools/TaskExtensions.cs

@@ -19,6 +19,20 @@ public static class TaskExtensions
         }
     }
 
+    public static async Task<string> Then<T>(this Task<T> task, Func<T, Task<string>> action, Func<T, Task<string>> nullCatch)
+    {
+        var result = await task;
+
+        if (result != null)
+        {
+            return await action(result);
+        }
+        else
+        {
+            return await nullCatch(result);
+        }
+    }
+
     /// <summary>
     /// 为 Task 类型的扩展方法,如果实体不为 null,则执行指定的操作。
     /// </summary>

+ 5 - 0
src/Hotline/Caching/Interfaces/ISysDicDataCacheManager.cs

@@ -106,5 +106,10 @@ namespace Hotline.Caching.Interfaces
         /// 随手拍重办原因
         /// </summary>
         IReadOnlyList<SystemDicData> InstaShotSpecialReason { get; }
+
+        /// <summary>
+        /// 随手拍安全员类型
+        /// </summary>
+        IReadOnlyList<SystemDicData> SafetyType { get; }
     }
 }

+ 5 - 0
src/Hotline/Caching/Services/SysDicDataCacheManager.cs

@@ -162,6 +162,11 @@ namespace Hotline.Caching.Services
 
         public IReadOnlyList<SystemDicData> InstaShotSpecialReason => GetOrAddDic(SysDicTypeConsts.InstaShotSpecialReason);
 
+        /// <summary>
+        /// 随手拍安全员类型
+        /// </summary>
+        public IReadOnlyList<SystemDicData> SafetyType => GetOrAddDic(SysDicTypeConsts.SafetyType);
+
         public void RemoveSysDicDataCache(string code)
         {
             _cacheSysDicData.Remove(code);

+ 3 - 0
src/Hotline/Orders/Citizen.cs

@@ -8,6 +8,7 @@ using Hotline.Orders;
 using Hotline.Quality;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Snapshot;
+using Hotline.Snapshot;
 using SqlSugar;
 using XF.Domain.Repository;
 
@@ -118,5 +119,7 @@ namespace Hotline.Orders
         public bool? IsSecurityMax { get; set; }
         #endregion
 
+		[Navigate(typeof(CitizenRelationSafetyType), nameof(CitizenRelationSafetyType.CitizenId), nameof(CitizenRelationSafetyType.SafetyTypeId))]
+        public List<SafetyType> SafetyTypes { get; set; }
     }
 }

+ 12 - 0
src/Hotline/SeedData/SystemDicDataSeedData.cs

@@ -203,6 +203,14 @@ public class SystemDicDataSeedData : ISeedData<SystemDicData>
                 new () { Id = "08dcd790-e54b-4da2-8a1a-bba70490b08c", DicDataValue = "随手拍整改时间过长", DicDataName = "随手拍整改时间过长", Sort = 8}
                 ];
         }
+        if (dicTypeCode == SysDicTypeConsts.SafetyType)
+        {
+            return [
+                new() { Id = "08dd7db8-5dc1-4f52-8d87-bc0772399a47", DicDataName = "安全志愿者", DicDataValue = "1", Sort = 1},
+                new() { Id = "08dd78a1-4823-4940-8ebd-8951d7a07e75", DicDataName = "宣传员", DicDataValue = "2", Sort = 2},
+                new() { Id = "08dd7bc9-44f5-4075-80dd-e4e5c3ee6af7", DicDataName = "安全卫士", DicDataValue = "3", Sort = 3},
+                ];
+        }
 
         throw new NotImplementedException();
     }
@@ -210,6 +218,10 @@ public class SystemDicDataSeedData : ISeedData<SystemDicData>
     public SystemDicType GetType(string dicTypeCode)
     {
         var dicType = new string[2];
+        if (dicTypeCode == SysDicTypeConsts.SafetyType)
+        {
+            dicType = ["08dd7c8d-974f-4c1b-84e9-d9aafcb53c1f", "安全志愿者类型"];
+        }
         if (dicTypeCode == SysDicTypeConsts.InstaShotSpecialReason)
         {
             dicType = ["81c202b7-5c50-45e5-bbde-fb1904957f85", "随手拍特提原因"];

+ 5 - 1
src/Hotline/Settings/SysDicTypeConsts.cs

@@ -341,5 +341,9 @@ public class SysDicTypeConsts
     /// 政治身份
     /// </summary>
     public static string PoliticalIdentity = "PoliticalIdentity";
-    
+
+    /// <summary>
+    /// 随手拍安全员类型
+    /// </summary>
+    public const string SafetyType = "SafetyType";
 }

+ 26 - 0
src/Hotline/Snapshot/CitizenRelationSafetyType.cs

@@ -0,0 +1,26 @@
+using SqlSugar;
+using System.ComponentModel;
+using XF.Domain.Entities;
+
+namespace Hotline.Snapshot;
+
+/// <summary>
+/// 市民关联随手拍安全角色分类
+/// </summary>
+[Description("市民关联随手拍安全角色分类")]
+public class CitizenRelationSafetyType : ITable, IEntity
+{
+    /// <summary>
+    /// 市民Id
+    /// <inheritdoc cref="Hotline.Orders.Citizen"/>
+    /// </summary>
+    [SugarColumn(ColumnDescription = "市民Id", IsPrimaryKey = true)]
+    public string CitizenId { get; set; }
+
+    /// <summary>
+    /// 安全员类型Id
+    /// <inheritdoc cref="SafetyType"/>
+    /// </summary>
+    [SugarColumn(ColumnDescription = "安全员类型Id", IsPrimaryKey = true)]
+    public string SafetyTypeId { get; set; }
+}

+ 12 - 0
src/Hotline/Snapshot/IRepository/ISafetyTypeRepository.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Snapshot.IRepository;
+
+public interface ISafetyTypeRepository : IRepository<SafetyType>
+{
+}

+ 23 - 0
src/Hotline/Snapshot/SafetyType.cs

@@ -0,0 +1,23 @@
+using Hotline.Identity.Accounts;
+using Hotline.Orders;
+using SqlSugar;
+using System.ComponentModel;
+using XF.Domain.Repository;
+
+namespace Hotline.Snapshot;
+
+/// <summary>
+/// 安全卫士类型 
+/// </summary>
+[Description("安全卫士类型")]
+public class SafetyType : CreationSoftDeleteEntity
+{
+    /// <summary>
+    /// 名字
+    /// </summary>
+    [SugarColumn(ColumnDescription = "安全卫士类型名称")]
+    public string Name { get; set; }
+
+    [Navigate(typeof(CitizenRelationSafetyType), nameof(CitizenRelationSafetyType.SafetyTypeId), nameof(CitizenRelationSafetyType.CitizenId))]
+    public List<Citizen> Citizens { get; set; }
+}

+ 1 - 1
src/Hotline/Snapshot/Services/SnapshotPointsDomainService.cs

@@ -42,7 +42,7 @@ public class SnapshotPointsDomainService : ISnapshotPointsDomainService, IScopeD
             OrderId = orderId,
             Points = order.ReportPoints.Value,
             Source = source,
-            Direction = Share.Enums.CallCenter.EPointsDirection.In
+            Direction = Share.Enums.CallCenter.EPointsDirection.In,
         };
         if (source == EPointsSource.Report)
             points.Points = order.ReportPoints.Value;

+ 0 - 1
src/Hotline/Validators/Exams/ExamManages/AddExamTagDtoValidator.cs

@@ -11,7 +11,6 @@ namespace Hotline.Validators.Exams
         public AddExamTagDtoValidator()
         {
             RuleFor(m => m.Name).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamTag.Name))));
-            RuleFor(m => m.ParentId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamTag.ParentId))));
         }
     }
 }

+ 0 - 1
src/Hotline/Validators/Exams/ExamManages/UpdateExamTagDtoValidator.cs

@@ -18,7 +18,6 @@ namespace Hotline.Validators.Exams
         {
             RuleFor(m => m.Id).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamTag.Id))));
             RuleFor(m => m.Name).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamTag.Name))));
-            RuleFor(m => m.ParentId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamTag.ParentId))));
 
         }
     }

+ 0 - 1
src/Hotline/Validators/Exams/Sourcewares/AddSourcewareCategoryDtoValidator.cs

@@ -16,7 +16,6 @@ namespace Hotline.Validators.Exams
         public AddSourcewareCategoryDtoValidator()
         {
             RuleFor(m => m.Name).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.Name))));
-            RuleFor(m => m.ParentId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.ParentId))));
 
         }
     }

+ 0 - 1
src/Hotline/Validators/Exams/Sourcewares/UpdateSourceCategoryDtoValidator.cs

@@ -17,7 +17,6 @@ namespace Hotline.Validators.Exams
         {
             RuleFor(m => m.Id).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.Id))));
             RuleFor(m => m.Name).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.Name))));
-            RuleFor(m => m.ParentId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamSourcewareCategory.ParentId))));
         }
     }
 }

+ 110 - 0
test/Hotline.Tests/Application/SnapshotUserApplicationTest.cs

@@ -0,0 +1,110 @@
+using Hotline.Api.Controllers;
+using Hotline.Application.Snapshot.Contracts;
+using Hotline.Caching.Interfaces;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot;
+using Hotline.Snapshot.IRepository;
+using Hotline.ThirdAccountDomainServices;
+using Hotline.ThirdAccountDomainServices.Interfaces;
+using Hotline.Users;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using XF.Domain.Cache;
+using XF.Domain.Repository;
+
+namespace Hotline.Tests.Application;
+
+public class SnapshotUserApplicationTest : TestBase
+{
+    private readonly ISnapshotUserApplication _snapshotUserApplication;
+    private readonly ISystemDicDataCacheManager _dicData;
+    private readonly ICitizenRepository _citizenRepository;
+    private readonly ISafetyTypeRepository _safetyTypeRepository;
+    public SnapshotUserApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, ITypedCache<SystemSetting> cacheSettingData, ThirdAccounSupplierFactory thirdAccountDomainFactory, IServiceProvider serviceProvider, ISnapshotUserApplication snapshotUserApplication, ISystemDicDataCacheManager dicData, ICitizenRepository citizenRepository, ISafetyTypeRepository safetyTypeRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository, cacheSettingData, thirdAccountDomainFactory, serviceProvider)
+    {
+        _snapshotUserApplication = snapshotUserApplication;
+        _dicData = dicData;
+        _citizenRepository = citizenRepository;
+        _safetyTypeRepository = safetyTypeRepository;
+    }
+
+    [Fact]
+    public async Task SnapshotUserApplication_Test()
+    {
+        var newSafetyType = new SafetyType { Name = "安全卫士" };
+        var safetyType = await _safetyTypeRepository.Queryable().Where(m => m.Name == newSafetyType.Name).FirstAsync();
+        safetyType ??= new SafetyType
+            {
+                Id = await _safetyTypeRepository.AddAsync(newSafetyType)
+            };
+
+        var citizen = await _citizenRepository.Queryable()
+            .LeftJoin<CitizenRelationSafetyType>((citizen, relation) => relation.CitizenId == citizen.Id)
+            .Where((citizen, relation) => relation.CitizenId == null)
+            .FirstAsync();
+        var addDto = new AddCitizenRelationSafetyTypeInDto
+        {
+            CitizenIds = [citizen.Id],
+            SafetyTypeId = safetyType.Id,
+        };
+
+        await _snapshotUserApplication.AddCitizenRelationSafetyType(addDto, CancellationToken.None);
+
+        var inDto = new CitizenRelationSafetyTypeInDto
+        {
+            SafetyTypeId = safetyType.Id
+        };
+        var items = await _snapshotUserApplication.GetCitizenRelationSafetyType(inDto).ToListAsync();
+        var item = items.FirstOrDefault(m => m.CitizenId == citizen.Id && m.SafetyTypeId == safetyType.Id);
+        item.ShouldNotBeNull();
+        item.SafetyTypeName.ShouldBe(safetyType.Name);
+        item.SafetyTypeId.ShouldBe(safetyType.Id);
+        item.CitizenId.ShouldBe(citizen.Id);
+        item.CitizenName.ShouldBe(citizen.Name);
+
+        var newSafetyTypeXuanChuan = new SafetyType { Name = "宣传员" };
+        var safetyTypeXuanChuan = await _safetyTypeRepository.Queryable()
+            .Where(m => m.Name == newSafetyTypeXuanChuan.Name).FirstAsync();
+        safetyTypeXuanChuan ??= new SafetyType
+            {
+                Id = await _safetyTypeRepository.AddAsync(newSafetyTypeXuanChuan)
+            };
+        addDto = new AddCitizenRelationSafetyTypeInDto
+        {
+            CitizenIds = [citizen.Id],
+            SafetyTypeId = safetyTypeXuanChuan.Id,
+        };
+        await _snapshotUserApplication.AddCitizenRelationSafetyType(addDto, CancellationToken.None);
+        var deleteInDto = new DeleteCitizenRelationSafetyTypeInDto
+        {
+            Items =
+            [
+                new() {
+                    CitizenId = citizen.Id,
+                    SafetyTypeId = safetyTypeXuanChuan.Id
+                }
+            ]
+        };
+        await _snapshotUserApplication.DeleteCitizenRelationSafetyAsync(deleteInDto);
+
+        items = await _snapshotUserApplication.GetCitizenRelationSafetyType(inDto).ToListAsync();
+        item = items.FirstOrDefault(m => m.CitizenId == citizen.Id && m.SafetyTypeId == safetyType.Id);
+        item.ShouldBeNull();
+        inDto = new CitizenRelationSafetyTypeInDto
+        {
+            SafetyTypeId = safetyTypeXuanChuan.Id
+        };
+        items = await _snapshotUserApplication.GetCitizenRelationSafetyType(inDto).ToListAsync();
+        item = items.FirstOrDefault(m => m.CitizenId == citizen.Id && m.SafetyTypeId == safetyTypeXuanChuan.Id);
+        item.SafetyTypeName.ShouldBe(safetyTypeXuanChuan.Name);
+        item.SafetyTypeId.ShouldBe(safetyTypeXuanChuan.Id);
+        item.CitizenId.ShouldBe(citizen.Id);
+        item.CitizenName.ShouldBe(citizen.Name);
+    }
+}