StartupHelper.cs 18 KB

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