StartupHelper.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. using System.IdentityModel.Tokens.Jwt;
  2. using System.IO.Compression;
  3. using System.Reflection;
  4. using System.Text;
  5. using Hotline.Api.Filter;
  6. using Hotline.Application;
  7. using Hotline.Application.Jobs;
  8. using Hotline.Caching.Interfaces;
  9. using Hotline.CallCenter.Configs;
  10. using Hotline.Configurations;
  11. using Hotline.DI;
  12. using Hotline.EventBus;
  13. using Hotline.Identity;
  14. using Hotline.Repository.SqlSugar;
  15. using Hotline.Repository.SqlSugar.Ts;
  16. using Hotline.Settings;
  17. using Mapster;
  18. using MapsterMapper;
  19. using MediatR.Pipeline;
  20. using Microsoft.AspNetCore.Authentication.JwtBearer;
  21. using Microsoft.AspNetCore.ResponseCompression;
  22. using Microsoft.IdentityModel.JsonWebTokens;
  23. using Microsoft.IdentityModel.Tokens;
  24. using Microsoft.OpenApi.Models;
  25. using Quartz;
  26. using Quartz.AspNetCore;
  27. using XF.Domain.Cache;
  28. using XF.Domain.Entities;
  29. using XF.Domain.Exceptions;
  30. using XF.Domain.Options;
  31. using XF.Domain.Repository;
  32. using XF.EasyCaching;
  33. namespace Hotline.Api
  34. {
  35. public static class StartupHelper
  36. {
  37. /// <summary>
  38. /// Authentication
  39. /// </summary>
  40. /// <param name="services"></param>
  41. /// <param name="configuration"></param>
  42. /// <returns></returns>
  43. public static IServiceCollection RegisterAuthentication(this IServiceCollection services, ConfigurationManager configuration)
  44. {
  45. var jwtOptions = configuration.GetSection("IdentityConfiguration").Get<IdentityConfiguration>().Jwt;
  46. #region remote ids
  47. //JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
  48. //services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  49. // .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, d =>
  50. // {
  51. // d.Authority = identityConfigs.IdentityUrl;
  52. // d.RequireHttpsMetadata = false;
  53. // d.TokenValidationParameters = new TokenValidationParameters
  54. // {
  55. // ValidateAudience = false
  56. // };
  57. // //d.Audience = "hotline_api";
  58. // d.Events = new JwtBearerEvents
  59. // {
  60. // OnMessageReceived = context =>
  61. // {
  62. // var accessToken = context.Request.Query["access_token"];
  63. // // If the request is for our hub...
  64. // var path = context.HttpContext.Request.Path;
  65. // if (!string.IsNullOrEmpty(accessToken) &&
  66. // (path.StartsWithSegments("/hubs/callcenter")))
  67. // {
  68. // // Read the token out of the query string
  69. // context.Token = accessToken;
  70. // }
  71. // return Task.CompletedTask;
  72. // }
  73. // };
  74. // });
  75. #endregion
  76. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  77. .AddJwtBearer(d =>
  78. {
  79. byte[] bytes = Encoding.UTF8.GetBytes(jwtOptions.SecretKey);
  80. var secKey = new SymmetricSecurityKey(bytes);
  81. d.TokenValidationParameters = new()
  82. {
  83. ValidateIssuer = false,
  84. ValidateAudience = true,
  85. ValidateLifetime = true,
  86. ValidateIssuerSigningKey = true,
  87. IssuerSigningKey = secKey,
  88. AudienceValidator = (audiences, token, parameters) =>
  89. {
  90. if (token is /*JwtSecurityToken*/ JsonWebToken jwtToken)
  91. {
  92. using var serviceProvider = services.BuildServiceProvider();
  93. var cacheAudience = serviceProvider.GetService<ITypedCache<AudienceTicket>>();
  94. var audience = cacheAudience.Get(jwtToken.Subject);
  95. return audiences != null && audiences.Any(a => a == audience?.Ticket);
  96. }
  97. return false;
  98. }
  99. };
  100. //d.Audience = "hotline_api";
  101. d.Events = new JwtBearerEvents
  102. {
  103. OnMessageReceived = context =>
  104. {
  105. var accessToken = context.Request.Query["access_token"];
  106. // If the request is for our hub...
  107. var path = context.HttpContext.Request.Path;
  108. if (!string.IsNullOrEmpty(accessToken) &&
  109. (path.StartsWithSegments("/hubs/hotline")))
  110. {
  111. // Read the token out of the query string
  112. context.Token = accessToken;
  113. }
  114. return Task.CompletedTask;
  115. }
  116. };
  117. })
  118. ;
  119. return services;
  120. }
  121. public static IServiceCollection RegisterAuthorization(this IServiceCollection services, ConfigurationManager configuration)
  122. {
  123. services.AddAuthorization(options =>
  124. {
  125. options.AddPolicy(AppDefaults.AuthPolicy.Hotline, d => d.RequireClaim("scope", AppDefaults.AuthPolicy.Hotline));
  126. });
  127. return services;
  128. }
  129. /// <summary>
  130. /// Swagger
  131. /// </summary>
  132. /// <param name="services"></param>
  133. /// <returns></returns>
  134. public static IServiceCollection RegisterSwagger(this IServiceCollection services)
  135. {
  136. services.AddSwaggerGen(c =>
  137. {
  138. //添加文档
  139. c.SwaggerDoc("v1", new OpenApiInfo() { Title = "Hotline Api", Version = "v1.0", Description = "智慧热线api" });
  140. var files = Directory.GetFiles(AppContext.BaseDirectory).Where(d => d.EndsWith(".xml"));
  141. foreach (var file in files)
  142. {
  143. c.IncludeXmlComments(file, true);
  144. }
  145. var scheme = new OpenApiSecurityScheme()
  146. {
  147. Description = "Authorization header. \r\nExample: 'Bearer ***'",
  148. Reference = new OpenApiReference
  149. {
  150. Type = ReferenceType.SecurityScheme,
  151. Id = "Authorization"
  152. },
  153. Scheme = "oauth2",
  154. Name = "Authorization",
  155. In = ParameterLocation.Header,
  156. Type = SecuritySchemeType.ApiKey,
  157. };
  158. c.AddSecurityDefinition("Authorization", scheme);
  159. var requirement = new OpenApiSecurityRequirement();
  160. requirement[scheme] = new List<string>();
  161. c.AddSecurityRequirement(requirement);
  162. });
  163. return services;
  164. }
  165. /// <summary>
  166. /// Cors
  167. /// </summary>
  168. /// <param name="services"></param>
  169. /// <param name="configuration"></param>
  170. /// <param name="corsOrigins"></param>
  171. /// <returns></returns>
  172. public static IServiceCollection RegisterCors(this IServiceCollection services, ConfigurationManager configuration, string corsOrigins)
  173. {
  174. services.AddCors(options =>
  175. {
  176. options.AddPolicy(name: corsOrigins,
  177. builder =>
  178. {
  179. var origins = configuration.GetSection("Cors:Origins").Get<string[]>();
  180. builder.SetIsOriginAllowed(a =>
  181. {
  182. return origins.Any(origin => origin.StartsWith("*.", StringComparison.Ordinal)
  183. ? a.EndsWith(origin[1..], StringComparison.Ordinal)
  184. : a.Equals(origin, StringComparison.Ordinal));
  185. })
  186. .AllowAnyHeader()
  187. .AllowAnyMethod()
  188. .AllowCredentials();
  189. });
  190. });
  191. return services;
  192. }
  193. /// <summary>
  194. /// Mapper
  195. /// </summary>
  196. /// <param name="services"></param>
  197. /// <returns></returns>
  198. public static IServiceCollection RegisterMapper(this IServiceCollection services)
  199. {
  200. var config = TypeAdapterConfig.GlobalSettings;
  201. config.ForDestinationType<IDataPermission>()
  202. .Ignore(d => d.CreatorId)
  203. .Ignore(d => d.CreatorOrgId)
  204. //.Ignore(d => d.CreatorOrgCode)
  205. .Ignore(d => d.AreaId);
  206. config.ForDestinationType<IWorkflow>()
  207. .Ignore(d => d.ExpiredTimeConfigId);
  208. config.ForDestinationType<IHasCreationTime>()
  209. .Ignore(d => d.CreationTime);
  210. config.ForDestinationType<IHasDeletionTime>().Ignore(d => d.DeletionTime);
  211. config.ForDestinationType<ISoftDelete>().Ignore(d => d.IsDeleted);
  212. config.ForDestinationType<IHasModificationTime>().Ignore(d => d.LastModificationTime);
  213. config.ForDestinationType<Entity>().Ignore(d => d.Id);
  214. services.AddSingleton(config);
  215. services.AddScoped<IMapper, ServiceMapper>();
  216. return services;
  217. }
  218. /// <summary>
  219. /// SignalR
  220. /// </summary>
  221. /// <param name="services"></param>
  222. /// <param name="configuration"></param>
  223. /// <returns></returns>
  224. public static IServiceCollection RegisterSignalR(this IServiceCollection services, ConfigurationManager configuration)
  225. {
  226. //var connectionString = configuration.GetConnectionString("Redis");
  227. //if (string.IsNullOrEmpty(connectionString))
  228. // throw new UserFriendlyException("未配置signalR的redis连接");
  229. //services.AddSignalR().AddStackExchangeRedis(connectionString, options =>
  230. //{
  231. // options.Configuration.ChannelPrefix = "Hotline:signalr:";
  232. //});
  233. var cacheConfig = configuration.GetRequiredSection("Cache").Get<CacheOptions>();
  234. services.AddSignalR().AddStackExchangeRedis($"{cacheConfig.Host}:{cacheConfig.Port}", options =>
  235. {
  236. options.Configuration.ChannelPrefix = "Hotline:signalr:";
  237. options.Configuration.Password = cacheConfig.Password;
  238. options.Configuration.DefaultDatabase = cacheConfig.Database;
  239. });
  240. return services;
  241. }
  242. public static IServiceCollection RegisterJob(this IServiceCollection services,AppConfiguration appConfiguration )
  243. {
  244. services.AddQuartz(d =>
  245. {
  246. d.SchedulerId = "default_scheduler";
  247. d.InterruptJobsOnShutdown = true;
  248. d.InterruptJobsOnShutdownWithWait = true;
  249. d.MaxBatchSize = 3;
  250. //load send order job
  251. //即将超期和超期短信
  252. //var autoSendOverTimeSmsKey = new JobKey(nameof(SendOverTimeSmsJob), "send overtime order task");
  253. //d.AddJob<SendOverTimeSmsJob>(autoSendOverTimeSmsKey);
  254. //d.AddTrigger(t => t
  255. // .WithIdentity("task-send-overtime-order-trigger")
  256. // .ForJob(autoSendOverTimeSmsKey)
  257. // .StartNow()
  258. // .WithCronSchedule("0 30 09,14 * * ?"));
  259. var autoSendOrderKey = new JobKey(nameof(SendOrderJob), "send order task");
  260. switch (appConfiguration.AppScope)
  261. {
  262. //智能化任务
  263. case AppDefaults.AppScope.YiBin:
  264. var aiVisitStatusKey = new JobKey(nameof(CheckAiVisitStateJob), "check aivisit state task");
  265. d.AddJob<CheckAiVisitStateJob>(aiVisitStatusKey);
  266. d.AddTrigger(t => t
  267. .WithIdentity("task-check-aivisit-state-trigger")
  268. .ForJob(aiVisitStatusKey)
  269. .StartNow()
  270. .WithCronSchedule("0 0/5 * * * ? *"));
  271. //d.AddJob<SendOrderJob>(autoSendOrderKey);
  272. //d.AddTrigger(t => t
  273. // .WithIdentity("task-send-order-trigger")
  274. // .ForJob(autoSendOrderKey)
  275. // .StartNow()
  276. // .WithCronSchedule("0 10 9 * * ?")
  277. //);
  278. break;
  279. case AppDefaults.AppScope.ZiGong:
  280. //d.AddJob<SendOrderJob>(autoSendOrderKey);
  281. //d.AddTrigger(t => t
  282. // .WithIdentity("task-send-order-trigger")
  283. // .ForJob(autoSendOrderKey)
  284. // .StartNow()
  285. // .WithCronSchedule("0 10 9 * * ?")
  286. //);
  287. break;
  288. default:
  289. break;
  290. }
  291. switch (appConfiguration.GetDefaultAppScopeConfiguration().CallCenterType)
  292. {
  293. case AppDefaults.CallCenterType.XingTang:
  294. var getCallsJobKey = new JobKey(nameof(XingTangCallsSyncJob));
  295. d.AddJob<XingTangCallsSyncJob>(getCallsJobKey);
  296. d.AddTrigger(t => t
  297. .WithIdentity("get-callsxt-trigger")
  298. .ForJob(getCallsJobKey)
  299. .StartNow()
  300. .WithCronSchedule("0/5 * * * * ?")
  301. );
  302. var getOperationsJobKey = new JobKey(nameof(XingTangTelOperationSyncJob));
  303. d.AddJob<XingTangTelOperationSyncJob>(getOperationsJobKey);
  304. d.AddTrigger(t => t
  305. .WithIdentity("get-operationsxt-trigger")
  306. .ForJob(getOperationsJobKey)
  307. .StartNow()
  308. .WithCronSchedule("0/10 * * * * ?")
  309. );
  310. var getCallSatisfactionJobKey = new JobKey(nameof(XingTangCallSatisfactionSyncJob));
  311. d.AddJob<XingTangCallSatisfactionSyncJob>(getCallSatisfactionJobKey);
  312. d.AddTrigger(t => t
  313. .WithIdentity("get-callsatisfaction-trigger")
  314. .ForJob(getCallSatisfactionJobKey)
  315. .StartNow()
  316. .WithCronSchedule("0/30 * * * * ?")
  317. );
  318. break;
  319. }
  320. });
  321. services.AddQuartzServer(d =>
  322. {
  323. d.WaitForJobsToComplete = true;
  324. d.StartDelay = TimeSpan.FromSeconds(5);
  325. });
  326. return services;
  327. }
  328. public static IServiceCollection RegisterRepository(this IServiceCollection services)
  329. {
  330. services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
  331. services.AddScoped(typeof(IRepositoryWorkflow<>), typeof(BaseRepositoryWorkflow<>));
  332. services.AddScoped(typeof(IRepositoryTextSearch<>), typeof(BaseRepositoryTextSearch<>));
  333. return services;
  334. }
  335. public static IServiceCollection RegisterMediatR(this IServiceCollection services, AppConfiguration appConfiguration)
  336. {
  337. services.AddScoped<Publisher>();
  338. services.AddMediatR(cfg =>
  339. {
  340. cfg.TypeEvaluator = d =>
  341. {
  342. var attr = d.GetCustomAttribute(typeof(InjectionAttribute)) as InjectionAttribute;
  343. if (attr is null) return true;
  344. return attr.IsEnable(appConfiguration.AppScope,
  345. appConfiguration.GetDefaultAppScopeConfiguration().CallCenterType);
  346. //if (!attr.Enable) return false;
  347. //switch (attr.EventHandlerType)
  348. //{
  349. // case EEventHandlerType.CallCenter:
  350. // return attr.Label == appConfiguration.GetDefaultAppScopeConfiguration().CallCenterType;
  351. // default:
  352. // return attr.AppScopes.ToString()
  353. // .Split(',', StringSplitOptions.RemoveEmptyEntries)
  354. // .Any(x => string.Compare(x, appConfiguration.AppScope, StringComparison.Ordinal) == 0);
  355. //}
  356. };
  357. //cfg.RegisterServicesFromAssemblyContaining<Program>();
  358. cfg.RegisterServicesFromAssembly(typeof(ApplicationStartupExtensions).Assembly);
  359. });
  360. return services;
  361. }
  362. public static IServiceCollection RegisterCompression(this IServiceCollection services)
  363. {
  364. services.Configure<BrotliCompressionProviderOptions>(options =>
  365. {
  366. options.Level = CompressionLevel.Optimal;
  367. }).Configure<GzipCompressionProviderOptions>(options =>
  368. {
  369. options.Level = CompressionLevel.Optimal;
  370. }).AddResponseCompression(options =>
  371. {
  372. options.EnableForHttps = true;
  373. options.Providers.Add<BrotliCompressionProvider>();
  374. options.Providers.Add<GzipCompressionProvider>();
  375. options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat([
  376. "application/json; charset=utf-8",
  377. "text/html; charset=utf-8",
  378. "application/xhtml+xml",
  379. "application/atom+xml",
  380. "image/svg+xml"
  381. ]);
  382. });
  383. return services;
  384. }
  385. }
  386. }