RepositoryExtension.Post.cs 81 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786
  1. using Aliyun.OSS;
  2. using Elasticsearch.Net;
  3. using JiaZhiQuan.Common.AliOSS;
  4. using JiaZhiQuan.Common.Config;
  5. using JiaZhiQuan.Common.ElasticSearch;
  6. using JiaZhiQuan.Common.ElasticSearch.Models;
  7. using JiaZhiQuan.Common.SnowFlake;
  8. using Newtonsoft.Json;
  9. using Newtonsoft.Json.Linq;
  10. using Org.BouncyCastle.Utilities;
  11. using Polly;
  12. using Senparc.Weixin.MP.Containers;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.Data;
  16. using System.Diagnostics;
  17. using System.Dynamic;
  18. using System.Linq;
  19. using System.Net.Http;
  20. using System.Reflection;
  21. using System.Security.Cryptography;
  22. using System.Text;
  23. using System.Threading.Tasks;
  24. using Wicture.DbRESTFul;
  25. using Wicture.DbRESTFul.Cache;
  26. using Wicture.DbRESTFul.Infrastructure.Repository;
  27. using Wicture.DbRESTFul.Rpc.Client;
  28. namespace JiaZhiQuan.Common
  29. {
  30. public class PostHotValue
  31. {
  32. public static int GetRoleScore(int role, bool superior)
  33. {
  34. int basic = 100;
  35. if (role > 0)
  36. {
  37. basic += 70;
  38. }
  39. if (superior)
  40. {
  41. basic += 50;
  42. }
  43. if (role == 2)
  44. {
  45. basic += 50;
  46. }
  47. return basic;
  48. }
  49. /// <summary>
  50. /// 客观事实数据(阅读量相关)
  51. /// </summary>
  52. public float fact { get; set; } = 0;
  53. /// <summary>
  54. /// 平台主观策略分(内容评定分)
  55. /// </summary>
  56. public float subjectiveScore { get; set; }
  57. /// <summary>
  58. /// 账号风险系数
  59. /// </summary>
  60. public float cntHotCoefficient { get; set; }
  61. public float hot { get; set; }
  62. }
  63. public class PostScore
  64. {
  65. public long id { get; set; }
  66. public long userId { get; set; }
  67. public double hot { get; set; }
  68. public PostScore Clone()
  69. {
  70. return new PostScore
  71. {
  72. id = id,
  73. userId = userId,
  74. hot = hot
  75. };
  76. }
  77. }
  78. public static partial class RepositoryExtension
  79. {
  80. /// <summary>
  81. /// 通过给定的PostScoreList基础上,来计算此用户个人的推荐列表,最多返回maxPostCount个结果。如果用户未关注任何人,则无需计算
  82. /// </summary>
  83. /// <param name="list">给定的内容分列表,在此基础上添加个性推荐分</param>
  84. /// <param name="userCntHotCoefficientDic">账号热度系数</param>
  85. private static async Task<List<PostScore>> GetPersonalRecommendList(DbRESTFulRepository repository, List<PostScore> list, Dictionary<long, float> userCntHotCoefficientDic, long userId, IRpcClient rpcClient, int maxPostCount = 1000)
  86. {
  87. if (list == null || list.Count == 0) return new List<PostScore>();
  88. // 查出用户最新关注的500用户
  89. var focusUserIds = userId > 0 ? (await repository.QueryAsync<long>($"select userId from n_user_fan where fanId={userId} order by id desc")).ToList() : new List<long>();
  90. // 因为现在只有关注才影响热度值,所以没有关注,则无需计算
  91. // if (focusUserIds.Count == 0) return new List<PostScore>();
  92. var now = DateTime.Now;
  93. if (userId > 0)
  94. {
  95. var changed = false;
  96. list.ForEach(e =>
  97. {
  98. if (focusUserIds.Contains(e.userId))
  99. {
  100. e.hot += 200 / Math.Ceiling((now - IdWorker.DecodeId(e.id).Time).TotalDays) * userCntHotCoefficientDic[e.userId];
  101. if (!changed) changed = true;
  102. }
  103. });
  104. var list1 = changed ? list.OrderByDescending(x => x.hot).ToList() : list;
  105. // 过滤已看过的,如果未看的内容不足最大推荐数量,则将已看内容添加至未看列表末尾
  106. var filterRst = (await rpcClient.InvokeRpcAsync(repository.Context, "UserVisitService", "FilterNotExist", string.Join(',', list1.Select(e => $"{userId}_{e.id}")))).Data;
  107. var filteredList = (JsonConvert.DeserializeObject<string>(filterRst) ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).Select(e => long.Parse(e.Split('_')[1])).ToHashSet();
  108. if (filteredList.Count > 0)
  109. {
  110. var validList = new List<PostScore>();
  111. var readList = new List<PostScore>();
  112. foreach (var e in list1)
  113. {
  114. if (filteredList.Contains(e.id))
  115. {
  116. validList.Add(e);
  117. if (validList.Count >= maxPostCount) break;
  118. }
  119. else
  120. {
  121. readList.Add(e);
  122. }
  123. }
  124. if (validList.Count < maxPostCount && readList.Count > 0)
  125. {
  126. foreach (var e in readList)
  127. {
  128. validList.Add(e);
  129. if (validList.Count >= maxPostCount) break;
  130. }
  131. }
  132. return validList;
  133. }
  134. else
  135. {
  136. return list1.Count > maxPostCount ? list1.Take(maxPostCount).ToList() : list1;
  137. }
  138. }
  139. else
  140. {
  141. return list.Count > maxPostCount ? list.Take(maxPostCount).ToList() : list;
  142. }
  143. }
  144. /// <summary>
  145. /// 对postIds列表重新排列,将连续相同用户间隔开
  146. /// </summary>
  147. /// <param name="postIds">文章Id列表</param>
  148. /// <param name="userIds">用户Id列表,个数与文章Id列表一致,如果传空,则会通过PostIds在数据库中查询对应的用户Id</param>
  149. /// <param name="firstFixed">是否锁定第一个</param>
  150. /// <returns>返回(文章编号列表, 是否存在顺序重排)</returns>
  151. public static async Task<(List<long>, bool)> RearrangePostIds(this DbRESTFulRepository repository, List<long> postIds, List<long> userIds, bool firstFixed = false, IDbConnection conn = null)
  152. {
  153. var rearranged = false;
  154. // 找出文章id对应的用户编号, 500为一批进行查询
  155. var size = 500;
  156. userIds ??= new List<long>(postIds.Count);
  157. if (userIds.Count == 0)
  158. {
  159. for (var i = 0; i < postIds.Count; i += size)
  160. {
  161. var leftCount = postIds.Count - i;
  162. var subList = postIds.Skip(i).Take(leftCount >= size ? size : leftCount).ToList();
  163. // 查出对应的用户id
  164. var map = (await repository.QueryAsync<dynamic>("select id, userId from n_post where id in @ids",
  165. new { ids = subList }, conn, null, false)).ToDictionary(e => (long)e.id);
  166. postIds.ForEach(postId =>
  167. {
  168. userIds.Add(map.ContainsKey(postId) ? (long)map[postId].userId : 0);
  169. });
  170. }
  171. }
  172. // 如果文章id列表与用户id列表无法一一对应,则直接返回文章id列表
  173. if (userIds.Count != postIds.Count) return (postIds, rearranged);
  174. var count = postIds.Count;
  175. var hasRepetitiveItems = false;
  176. for (var i = 1; i < count; i++)
  177. {
  178. if (userIds[i - 1] == userIds[i])
  179. {
  180. var exchanged = false;
  181. // 如果当前文章用户与上一个文章用户相同,则向后查找一个与当前用户不同的记录
  182. for (var j = i + 1; j < count; j++)
  183. {
  184. if (userIds[j] != userIds[i])
  185. {
  186. // 交换
  187. postIds[i] ^= postIds[j];
  188. postIds[j] = postIds[i] ^ postIds[j];
  189. postIds[i] ^= postIds[j];
  190. userIds[i] ^= userIds[j];
  191. userIds[j] = userIds[i] ^ userIds[j];
  192. userIds[i] ^= userIds[j];
  193. exchanged = true;
  194. rearranged = true;
  195. break;
  196. }
  197. }
  198. // 如果未找到可以替换的,则表示后面的全都是相同用户的内容,无需进行后续查找与替换
  199. if (!exchanged)
  200. {
  201. hasRepetitiveItems = true;
  202. break;
  203. }
  204. }
  205. }
  206. // 如果存在未能替换的,则反向执行替换一次
  207. if (hasRepetitiveItems)
  208. {
  209. var reverseEndIdx = firstFixed ? 1 : 0;
  210. for (var i = count - 2; i > reverseEndIdx; i--)
  211. {
  212. if (userIds[i + 1] == userIds[i])
  213. {
  214. var exchanged = false;
  215. // 如果当前文章用户与上一个文章用户相同,则向后查找一个与当前用户不同的记录
  216. for (var j = i - 1; j >= reverseEndIdx; j--)
  217. {
  218. if (userIds[j] != userIds[i])
  219. {
  220. // 交换
  221. postIds[i] ^= postIds[j];
  222. postIds[j] = postIds[i] ^ postIds[j];
  223. postIds[i] ^= postIds[j];
  224. userIds[i] ^= userIds[j];
  225. userIds[j] = userIds[i] ^ userIds[j];
  226. userIds[i] ^= userIds[j];
  227. exchanged = true;
  228. rearranged = true;
  229. break;
  230. }
  231. }
  232. // 如果未找到可以替换的,则表示后面的全都是相同用户的内容,无需进行后续查找与替换
  233. if (!exchanged)
  234. {
  235. break;
  236. }
  237. }
  238. }
  239. }
  240. return (postIds, rearranged);
  241. }
  242. /// <summary>
  243. /// 将推荐的内容保存到Redis中
  244. /// </summary>
  245. /// <param name="categoryType">如果没有,则传null</param>
  246. /// <param name="contentType">null表示所有,1为带图片的,2为带视频的</param>
  247. /// <param name="hasTopic">如果没有,则传null</param>
  248. private static async Task UpdatePersonalRecommendListToRedis(DbRESTFulRepository repository, RedisCacheProvider cacheProvider, long[] ids, long userId, int? categoryType, int? contentType, bool? hasTopic, int dayOfMonth)
  249. {
  250. // 重排文章列表(将连续用户分开)
  251. ids = (await RearrangePostIds(repository, ids.ToList(), null)).Item1.ToArray();
  252. var key = CacheKeys.PostRecommendList(dayOfMonth, userId, categoryType, contentType, hasTopic);
  253. await cacheProvider.CSRedisClient.DelAsync(key);
  254. if (ids.Length == 0) return;
  255. await cacheProvider.CSRedisClient.RPushAsync(key, ids);
  256. await cacheProvider.CSRedisClient.ExpireAsync(key, CacheKeys.PostRecommendListCacheSecs);
  257. }
  258. /// <summary>
  259. /// 获取满足条件的文章列表(热度>0的)
  260. /// </summary>
  261. /// <param name="categoryType">如果没有,则传null</param>
  262. /// <param name="contentType">null表示所有,1为带图片的,2为带视频的</param>
  263. /// <param name="hasTopic">如果没有,则传null</param>
  264. /// <param name="userCntHotCoefficientDic">如果传入的字典不为空,则在查询结果后,会将结果中用户对应的账号热度系数查出来,并存入字典</param>
  265. /// <param name="maxPostCount">最多获取多少条</param>
  266. private static async Task<List<PostScore>> GetHotPostList(DbRESTFulRepository repository, ConfigFromDb config, int categoryType, int? contentType, bool? hasTopic = null, Dictionary<long, float> userCntHotCoefficientDic = null, int maxPostCount = 5000, IDbConnection conn = null)
  267. {
  268. var postList = new List<PostScore>();
  269. var minId = IdWorker.GetStartIdByDate(
  270. string.IsNullOrEmpty(config.PostStartRecommendDate)
  271. ? DateTime.Now.AddDays(-config.PostMaxRecommendDays)
  272. : DateTime.Parse(config.PostStartRecommendDate)
  273. );
  274. var mustConditions = new List<dynamic>
  275. {
  276. new { term = new { state = 1 } },
  277. new { range = new { hot = new { gt = 0 } } },
  278. new { term = new { categoryType } },
  279. new { range = new { id = new { gt = minId } } },
  280. };
  281. // 如果是带图片的
  282. if (contentType == 1)
  283. {
  284. mustConditions.Add(new { term = new { type = 0 } });
  285. mustConditions.Add(new { range = new { coverWidth = new { gt = 0 } } });
  286. }
  287. else if (contentType == 2)
  288. {
  289. mustConditions.Add(new { term = new { type = 1 } });
  290. }
  291. if (hasTopic != null)
  292. {
  293. mustConditions.Add(new { script = new { script = "doc['validTopicIds'].length>0" } });
  294. }
  295. var query = new
  296. {
  297. _source = new string[] { "id", "userId", "hot" },
  298. size = maxPostCount,
  299. query = new
  300. {
  301. @bool = new
  302. {
  303. filter = new
  304. {
  305. @bool = new
  306. {
  307. must = mustConditions
  308. }
  309. }
  310. }
  311. },
  312. sort = new List<dynamic>
  313. {
  314. new
  315. {
  316. hot = new { order = "desc" }
  317. },
  318. new
  319. {
  320. id = new { order = "desc" }
  321. }
  322. }
  323. };
  324. var resp = await ESHelper.Client.SearchAsync<StringResponse>(ESConstants.ESIndexName.Post, PostData.Serializable(query));
  325. if (resp.Success)
  326. {
  327. var obj = JsonConvert.DeserializeObject<JToken>(resp.Body);
  328. var list = obj.Value<JObject>("hits")?.Value<JArray>("hits")?.Select(e =>
  329. {
  330. var source = e.Value<JToken>("_source");
  331. return new PostScore
  332. {
  333. id = source.Value<long>("id"),
  334. userId = source.Value<long>("userId"),
  335. hot = source.Value<double>("hot"),
  336. };
  337. })?.ToList();
  338. if (list != null)
  339. {
  340. postList = list;
  341. }
  342. }
  343. else
  344. {
  345. LoggerManager.Logger.Error("GetHotPostList ES查询出错 " + resp?.Body);
  346. }
  347. if (userCntHotCoefficientDic != null)
  348. {
  349. var ids = new HashSet<long>();
  350. postList.ForEach(e =>
  351. {
  352. if (userCntHotCoefficientDic.ContainsKey(e.userId) || ids.Contains(e.userId)) return;
  353. ids.Add(e.userId);
  354. });
  355. if (ids.Count > 0)
  356. {
  357. var batchSize = 500;
  358. for (var i = 0; i < ids.Count; i += batchSize)
  359. {
  360. var count = i + batchSize > ids.Count ? ids.Count - i : batchSize;
  361. var subList = i == 0 && ids.Count <= batchSize ? ids : ids.Skip(i).Take(count);
  362. var userCntHotCoefficients = await repository.QueryAsync<dynamic>("select id, cntHotCoefficient from n_user where id in @ids", new { ids = subList }, conn);
  363. userCntHotCoefficients.ForEach(e =>
  364. {
  365. userCntHotCoefficientDic[(long)e.id] = (float)e.cntHotCoefficient;
  366. });
  367. }
  368. }
  369. }
  370. return postList;
  371. }
  372. /// <summary>
  373. /// 更新推荐缓存列表
  374. /// </summary>
  375. /// <param name="updatePublicOnly">是否仅更新未登录用户的热门列表</param>
  376. public static async Task UpdateUserRecommendPostCache(this DbRESTFulRepository repository, bool calcPersonal, RedisCacheProvider cacheProvider, IRpcClient rpcClient, ConfigFromDb config)
  377. {
  378. // 每类型中取出最大值
  379. var maxPostCount = 10000;
  380. Dictionary<long, float> userCntHotCoefficientDic = null;
  381. if (calcPersonal)
  382. {
  383. userCntHotCoefficientDic = new Dictionary<long, float>();
  384. }
  385. // 记录推荐缓存同步记录
  386. var syncRecordId = await repository.QuerySingleOrDefaultAsync<int>("insert into n_post_recommend_records(startAt) values(now()); select last_insert_id() as id;", null, null, null, false);
  387. var syncNote = new StringBuilder();
  388. using var conn = repository.ConnectionManager.GetConnection();
  389. conn.Open();
  390. var days = 10; // 10天内活跃的,且有关注记录的用户
  391. var now = DateTime.Now;
  392. //var standpoints = await GetHotPostList(repository, 2, null, null, userCntHotCoefficientDic, maxPostCount, conn);
  393. // -9999表示所有一级分类
  394. var diaryLst = new Dictionary<int, List<PostScore>>();
  395. diaryLst[-9999] = await GetHotPostList(repository, config, 1, null, null, userCntHotCoefficientDic, maxPostCount, conn);
  396. // diaryLst[-9998] = await GetHotPostList(repository, config, 1, null, true, userCntHotCoefficientDic, maxPostCount, conn);
  397. // 带图片的图文笔记
  398. diaryLst[1] = await GetHotPostList(repository, config, 1, 1, null, userCntHotCoefficientDic, maxPostCount, conn);
  399. // 视频笔记
  400. diaryLst[2] = await GetHotPostList(repository, config, 1, 2, null, userCntHotCoefficientDic, maxPostCount, conn);
  401. var dayOfMonth = now.Day;
  402. foreach (var di in diaryLst)
  403. {
  404. var nrl = await GetPersonalRecommendList(repository, diaryLst[di.Key], userCntHotCoefficientDic, 0, rpcClient, 1000);
  405. await UpdatePersonalRecommendListToRedis(repository, cacheProvider, nrl.Select(i => i.id).ToArray(), 0, 1,
  406. di.Key < 0 ? null : (int?)di.Key, null, dayOfMonth);
  407. }
  408. if (calcPersonal)
  409. {
  410. var startDate = now.AddDays(-days).ToString("yyyy-MM-dd");
  411. // 查出10天内活跃用户
  412. var userIds = (await repository.QueryAsync<long>($"select distinct userId from n_user_online where `date`>='{startDate}'", null, conn)).ToList();
  413. var userFetchSize = 500;
  414. var emptyPostIds = Array.Empty<long>();
  415. for (var i = 0; i < userIds.Count; i += userFetchSize)
  416. {
  417. var count = i + userFetchSize > userIds.Count ? userIds.Count - i : userFetchSize;
  418. var subList = i == 0 && userIds.Count <= userFetchSize ? userIds : userIds.Skip(i).Take(count).ToList();
  419. // 查询出有关注的用户
  420. var validSubList = (await repository.QueryAsync<long>("select userId from n_user_statistic where focusUser>0 and userId in @subList", new { subList }, conn)).ToHashSet();
  421. foreach (var userId in subList)
  422. {
  423. if (CommonUtils.IsMockUser(userId)) continue;
  424. // 如果是无关注的用户,则推荐设置为[]
  425. if (!validSubList.Contains(userId))
  426. {
  427. foreach (var di in diaryLst)
  428. {
  429. await UpdatePersonalRecommendListToRedis(repository, cacheProvider, emptyPostIds, userId, 1,
  430. di.Key < 0 ? null : (int?)di.Key, null, dayOfMonth);
  431. }
  432. continue;
  433. }
  434. foreach (var di in diaryLst)
  435. {
  436. // 热度会被计算,所以得Clone出副本
  437. var rl = await GetPersonalRecommendList(repository,
  438. diaryLst[di.Key].Select(e => e.Clone()).ToList(), userCntHotCoefficientDic, userId,
  439. rpcClient, 1000);
  440. await UpdatePersonalRecommendListToRedis(repository, cacheProvider, rl.Select(i => i.id).ToArray(),
  441. userId, 1, di.Key < 0 ? null : (int?)di.Key, null,
  442. dayOfMonth);
  443. }
  444. }
  445. }
  446. syncNote.Append($"含用户个性化推荐【{userIds.Count}】");
  447. }
  448. if (syncRecordId > 0)
  449. {
  450. var endAt = DateTime.Now;
  451. await repository.QueryAsync<dynamic>("update n_post_recommend_records set endAt=@endAt, totalSecs=@totalSecs, note=@note where id=@id", new
  452. {
  453. id = syncRecordId,
  454. endAt,
  455. totalSecs = (float)(endAt - now).TotalSeconds,
  456. note = syncNote.ToString()
  457. }, null, null, false);
  458. }
  459. }
  460. /// <summary>
  461. /// 生成新文章推荐缓存,如果对应的缓存已存在,则不生成
  462. /// </summary>
  463. public static async Task GenerateNewPostRecommend(this DbRESTFulRepository repository, DateTime time,
  464. int categoryType, int? contentType, bool? hasTopic, RedisCacheProvider cacheProvider, ConfigFromDb config)
  465. {
  466. var minute = (time.Minute / 5) * 5;
  467. DateTime startTime;
  468. // 查询上次推荐缓存同步时间,如果有,则取同步时间,如果此时间与当前时间相差超过一天,则取24小时前的时间
  469. var syncTime =
  470. await repository.QuerySingleOrDefaultAsync<DateTime?>(
  471. "select startAt from n_post_recommend_records order by id desc limit 1");
  472. if (syncTime != null)
  473. {
  474. if ((syncTime.Value - time).TotalDays > 1)
  475. {
  476. startTime = time.AddHours(-config.PostIgnoreHotRecentHours - 24);
  477. }
  478. else
  479. {
  480. startTime = syncTime.Value.AddHours(-config.PostIgnoreHotRecentHours);
  481. }
  482. }
  483. else
  484. {
  485. startTime = time.AddHours(-config.PostIgnoreHotRecentHours);
  486. }
  487. var startPostId = IdWorker.GetStartIdByDate(startTime);
  488. var key = CacheKeys.PostNewRecommendCache($"{time.ToString("MMddHH")}{minute.ToString("00")}",
  489. categoryType, contentType, hasTopic);
  490. if (await cacheProvider.KeyExist(key)) return;
  491. var mustConditions = new List<dynamic>
  492. {
  493. new { range = new { id = new { gt = startPostId } } },
  494. new { term = new { state = 1 } },
  495. new { range = new { subjectiveScore = new { gt = 0 } } },
  496. new { term = new { categoryType } },
  497. };
  498. if (contentType == 1)
  499. {
  500. mustConditions.Add(new { term = new { type = 0 } });
  501. mustConditions.Add(new { range = new { coverWidth = new { gt = 0 } } });
  502. }
  503. else if (contentType == 2)
  504. {
  505. mustConditions.Add(new { term = new { type = 1 } });
  506. }
  507. var postList = new List<(double, object)>();
  508. var query = new
  509. {
  510. _source = new string[] { "id", "subjectiveScore" },
  511. size = 10000,
  512. query = new
  513. {
  514. @bool = new
  515. {
  516. filter = new
  517. {
  518. @bool = new
  519. {
  520. must = mustConditions
  521. }
  522. }
  523. }
  524. }
  525. };
  526. var resp = await ESHelper.Client.SearchAsync<StringResponse>(ESConstants.ESIndexName.Post,
  527. PostData.Serializable(query));
  528. if (resp.Success)
  529. {
  530. var obj = JsonConvert.DeserializeObject<JToken>(resp.Body);
  531. int curScore = 0;
  532. obj.Value<JObject>("hits")?.Value<JArray>("hits")?.ForEach(e =>
  533. {
  534. var source = e.Value<JToken>("_source");
  535. curScore += (int)Math.Ceiling(source.Value<float>("subjectiveScore"));
  536. postList.Add((curScore, source.Value<long>("id")));
  537. });
  538. }
  539. else
  540. {
  541. LoggerManager.Logger.Error("GetHotPostList ES查询出错 " + resp?.Body);
  542. }
  543. if (postList.Count == 0)
  544. {
  545. postList.Add((0, 0));
  546. }
  547. await cacheProvider.CSRedisClient.ZAddAsync(key, postList.ToArray());
  548. // 缓存10分钟
  549. await cacheProvider.CSRedisClient.ExpireAsync(key, 60 * 10);
  550. }
  551. /// <summary>
  552. /// 批量更新文章的冷却值
  553. /// </summary>
  554. public static async Task RecalcPostCoolingFactorAll(this DbRESTFulRepository repository, long startPostId, long? endPostId, ConfigFromDb config)
  555. {
  556. var batchSize = 1000;
  557. using var conn = repository.ConnectionManager.GetConnection(false);
  558. conn.Open();
  559. while (true)
  560. {
  561. var extQuery = endPostId == null ? "" : $" and id<{endPostId}";
  562. var infos = (await repository.QueryAsync<dynamic>($"select id, userId, coolingFactor from n_post where id>{startPostId} {extQuery} and state=1 and categoryType=1 order by id asc limit {batchSize}", null, conn)).ToList();
  563. if (infos.Count == 0) break;
  564. var firstPostTime = IdWorker.DecodeId(infos[0].id).Time;
  565. var localMinId = IdWorker.GetStartIdByDate(firstPostTime.AddHours(config.PostCoolingPeriodHours)); // 满足条件可在本地计算冷却系数的最小文章编号
  566. var localStartIndex = infos.FindIndex(e => e.id > localMinId); // 此这里开始就可以在本地算冷却系数了
  567. var dic = new Dictionary<long, float>();
  568. if (localStartIndex > -1)
  569. {
  570. for (var j = localStartIndex; j < infos.Count; j++)
  571. {
  572. var item = infos[j];
  573. var id = (long)item.id;
  574. var minId = IdWorker.GetStartIdByDate(IdWorker.DecodeId(id).Time.AddHours(-config.PostCoolingPeriodHours));
  575. // 向前找X小时内同用户的帖子数量
  576. var count = 1;
  577. for (var i = j - 1; i >= 0; i--)
  578. {
  579. var item1 = infos[i];
  580. if (item1.userId == item.userId && item1.id > minId)
  581. {
  582. count++;
  583. }
  584. }
  585. float val;
  586. if (count <= config.PostIgnoreCoolingMaxCountPerPeriod)
  587. {
  588. val = 1;
  589. }
  590. else
  591. {
  592. val = (float)(1 / Math.Pow(count, 0.5));
  593. }
  594. if (item.coolingFactor != val)
  595. {
  596. dic[id] = val;
  597. }
  598. }
  599. }
  600. else
  601. {
  602. localStartIndex = infos.Count;
  603. }
  604. var cacheUserPostIds = new Dictionary<long, List<long>>();
  605. for (var j = 0; j < localStartIndex; j++)
  606. {
  607. var item = infos[j];
  608. var id = (long)item.id;
  609. var minId = IdWorker.GetStartIdByDate(IdWorker.DecodeId((long)item.id).Time.AddHours(-config.PostCoolingPeriodHours));
  610. var userId = (long)item.userId;
  611. List<long> cachedList;
  612. if (cacheUserPostIds.ContainsKey(userId))
  613. {
  614. cachedList = cacheUserPostIds[userId];
  615. }
  616. else
  617. {
  618. cachedList = (await repository.QueryAsync<long>($"select id from n_post where id>{minId} and userId={userId} and state=1 order by id asc")).ToList();
  619. cacheUserPostIds[userId] = cachedList;
  620. }
  621. var count = cachedList.Count(e => e > minId);
  622. float val;
  623. if (count <= config.PostIgnoreCoolingMaxCountPerPeriod)
  624. {
  625. val = 1;
  626. }
  627. else
  628. {
  629. val = (float)(1 / Math.Pow(count, 0.5));
  630. }
  631. if (item.coolingFactor != val)
  632. {
  633. dic[id] = val;
  634. }
  635. cachedList.Add(item.id);
  636. }
  637. if (dic.Count > 0)
  638. {
  639. // 更新冷却值
  640. var list = dic.Select(e => new
  641. {
  642. id = e.Key,
  643. coolingFactor = e.Value
  644. });
  645. await repository.QueryAsync<dynamic>("update n_post set coolingFactor=@coolingFactor, updateAt=now() where id=@id", list, conn, null, false);
  646. }
  647. if (infos.Count < batchSize) break;
  648. startPostId = infos.Last().id;
  649. }
  650. }
  651. /// <summary>
  652. /// 更新单一某个文章的冷却值,同时会更新其后面的N个
  653. /// </summary>
  654. /// <param name="authorId">不传则从数据库中查询</param>
  655. public static async Task RecalcPostCoolingFactor(this DbRESTFulRepository repository, long postId, long authorId, ConfigFromDb config)
  656. {
  657. // 计算冷却系数
  658. var postCoolingPeriodHours = config.PostCoolingPeriodHours; // 冷却小时数,即在多久内计算冷却系数
  659. var postIgnoreCoolingMaxCountPerPeriod = config.PostIgnoreCoolingMaxCountPerPeriod; // 在冷却周期内,小于等于此值时,冷却系数为1
  660. // 找出此文章冷却向前向后小时已发布的文章,计算当前的以及更新向后的
  661. var createAt = IdWorker.DecodeId(postId).Time;
  662. var startId = IdWorker.GetStartIdByDate(createAt.AddHours(-postCoolingPeriodHours));
  663. var endId = IdWorker.GetStartIdByDate(createAt.AddHours(postCoolingPeriodHours));
  664. if (authorId == 0)
  665. {
  666. authorId = await repository.QuerySingleOrDefaultAsync<long>($"select userId from n_post where id={postId}");
  667. }
  668. // 未找到记录,则无法计算
  669. if (authorId == 0) return;
  670. var postInfos = (await repository.QueryAsync<dynamic>($"select id, coolingFactor, state from n_post where id>{startId} and id<{endId} and userId={authorId} order by id asc")).ToList();
  671. var beforeCount = 1;
  672. var isBefore = true;
  673. var dic = new Dictionary<long, float>();
  674. for (var i = 0; i < postInfos.Count; i++)
  675. {
  676. var id = (long)postInfos[i].id;
  677. var state = (int)postInfos[i].state;
  678. if (id != postId)
  679. {
  680. if (isBefore)
  681. {
  682. if (state == 1) beforeCount++;
  683. }
  684. else
  685. {
  686. // 如果状态不是通过,则不计算
  687. if (state == 1)
  688. {
  689. var _createAt = IdWorker.DecodeId(id).Time;
  690. var _startId = IdWorker.GetStartIdByDate(_createAt);
  691. var tmp = 1;
  692. for (var j = i; j >= 0; j--)
  693. {
  694. if ((long)postInfos[j].id > _startId && postInfos[j].state == 1)
  695. {
  696. tmp++;
  697. }
  698. }
  699. float val = 1;
  700. if (tmp > postIgnoreCoolingMaxCountPerPeriod)
  701. {
  702. val = (float)(1 / Math.Pow(tmp, 0.5));
  703. }
  704. if (val != (float)postInfos[i].coolingFactor)
  705. {
  706. dic[id] = val;
  707. }
  708. }
  709. }
  710. }
  711. else
  712. {
  713. isBefore = false;
  714. if (beforeCount >= 1 && state == 1)
  715. {
  716. float val = 1;
  717. // 计算当前的coolingfact
  718. if (beforeCount > postIgnoreCoolingMaxCountPerPeriod)
  719. {
  720. val = (float)(1 / Math.Pow(beforeCount, 0.5));
  721. }
  722. if (val != (float)postInfos[i].coolingFactor)
  723. {
  724. dic[id] = val;
  725. }
  726. }
  727. }
  728. }
  729. if (dic.Count > 0)
  730. {
  731. // 更新冷却值
  732. var list = dic.Select(e => new
  733. {
  734. id = e.Key,
  735. coolingFactor = e.Value
  736. });
  737. await repository.QueryAsync<dynamic>("update n_post set coolingFactor=@coolingFactor, updateAt=now() where id=@id", list, null, null, false);
  738. }
  739. }
  740. /// <summary>
  741. /// 更新所有文章的平台主观策略分、计算客观事实分 以及 热度值(此处不计算文章的冷却系数)
  742. /// </summary>
  743. public static async Task UpdateAllPostContentScore(this DbRESTFulRepository repository, ConfigFromDb config)
  744. {
  745. var size = 1000;
  746. var now = DateTime.Now;
  747. var validEndTime = now.AddHours(-config.PostIgnoreHotRecentHours);
  748. var endId = IdWorker.GetStartIdByDate(validEndTime);
  749. var sql =
  750. $"select p.id, p.userId, p.type, p.categoryType, p.checkCntScore, p.manualExtraScore, p.coolingFactor, u.mpRole, u.superior, u.cntHotCoefficient from n_post as p inner join n_user as u on p.userId=u.id where p.id>@startId and p.id<{endId} and categoryType=1 and p.state=1 and u.state=0 order by p.id asc limit {size}";
  751. using var conn = repository.ConnectionManager.GetConnection();
  752. conn.Open();
  753. var calcTaskId = await repository.QuerySingleOrDefaultAsync<int>(
  754. "insert into n_post_hot_all_calc_records(startAt) values(@startAt); select last_insert_id() as id;",
  755. new { startAt = now }, null, null, false);
  756. var totalCount = 0;
  757. var startId = IdWorker.GetStartIdByDate(
  758. string.IsNullOrEmpty(config.PostStartRecommendDate)
  759. ? DateTime.Now.AddDays(-config.PostMaxRecommendDays)
  760. : DateTime.Parse(config.PostStartRecommendDate)
  761. );
  762. var originalStartId = startId;
  763. while (true)
  764. {
  765. var items = (await repository.QueryAsync<dynamic>(sql, new { startId }, conn)).ToList();
  766. if (items.Count == 0)
  767. {
  768. break;
  769. }
  770. totalCount += items.Count;
  771. var itemsCount = items.Count;
  772. var lastId = items.Last().id;
  773. // 用来存文章热度值中间值
  774. var dic = new Dictionary<long, PostHotValue>();
  775. items.ForEach(e => dic[(long)e.id] = new PostHotValue());
  776. var itemsInBlackList = new List<dynamic>();
  777. // 查出在小黑屋的,如果在小黑屋,则热度为0
  778. var postIdInBlacklist =
  779. (await repository.QueryAsync<long>(
  780. $"select b.postId from n_stat_post_blacklist as b left join n_post as p on b.postId=p.id where b.postId>{startId} and b.postId<={lastId} and b.ctrlType>0"))
  781. .ToList();
  782. if (postIdInBlacklist.Count > 0)
  783. {
  784. var validItems = new List<dynamic>();
  785. items.ForEach(e =>
  786. {
  787. if (!postIdInBlacklist.Contains(e.id))
  788. {
  789. validItems.Add(e);
  790. }
  791. else
  792. {
  793. itemsInBlackList.Add(e);
  794. }
  795. });
  796. items = validItems;
  797. }
  798. // 计算当前文章列表的有效阅读量
  799. // 判断是否是今天的内容,如果是,则需要查出今天的有效阅读量
  800. var todayStartTime = now.AddHours(-now.Hour).AddMinutes(-now.Minute).AddSeconds(-now.Second)
  801. .AddMilliseconds(-now.Millisecond);
  802. var todayStartId = IdWorker.GetStartIdByDate(todayStartTime);
  803. var todayItems = items.Where(e => e.id > todayStartId && e.id < endId && e.checkCntScore > 0).ToList();
  804. if (todayItems.Count > 0)
  805. {
  806. var todayItemIds = todayItems.Select(e => (long)e.id).ToList();
  807. #region 统计有效阅读量
  808. // 统计阅读量
  809. var resp = await ESHelper.Client.SearchAsync<StringResponse>("postreadrecord",
  810. PostData.Serializable(new
  811. {
  812. query = new
  813. {
  814. @bool = new
  815. {
  816. must = new List<dynamic>
  817. {
  818. new
  819. {
  820. range = new
  821. {
  822. createAt = new
  823. {
  824. gte = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss")
  825. }
  826. }
  827. },
  828. new
  829. {
  830. terms = new
  831. {
  832. postId = todayItemIds
  833. }
  834. }
  835. },
  836. must_not = new List<dynamic>
  837. {
  838. new
  839. {
  840. term = new { notValidRecord = true }
  841. }
  842. }
  843. }
  844. },
  845. size = 0,
  846. aggs = new
  847. {
  848. post = new
  849. {
  850. terms = new { field = "postId", size = 50000 }
  851. }
  852. }
  853. }));
  854. if (resp.Success)
  855. {
  856. var rst = JsonConvert.DeserializeObject<JObject>(resp.Body);
  857. var buckets = rst.Value<JObject>("aggregations").Value<JObject>("post")
  858. .Value<JArray>("buckets");
  859. if (buckets.Count > 0)
  860. {
  861. buckets.ForEach(e =>
  862. {
  863. var postId = e.Value<long>("key");
  864. var count = e.Value<int>("doc_count") * 3;
  865. dic[postId].fact += count;
  866. });
  867. }
  868. }
  869. // 统计分享量
  870. resp = await ESHelper.Client.SearchAsync<StringResponse>("postsharerecord", PostData.Serializable(
  871. new
  872. {
  873. query = new
  874. {
  875. @bool = new
  876. {
  877. must = new List<dynamic>
  878. {
  879. new
  880. {
  881. range = new
  882. {
  883. createAt = new
  884. {
  885. gte = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss")
  886. }
  887. }
  888. },
  889. new
  890. {
  891. terms = new
  892. {
  893. postId = todayItemIds
  894. }
  895. }
  896. },
  897. must_not = new List<dynamic>
  898. {
  899. new
  900. {
  901. term = new { notValidRecord = true }
  902. }
  903. }
  904. }
  905. },
  906. size = 0,
  907. aggs = new
  908. {
  909. post = new
  910. {
  911. terms = new { field = "postId", size = 50000 }
  912. }
  913. }
  914. }));
  915. if (resp.Success)
  916. {
  917. var rst = JsonConvert.DeserializeObject<JObject>(resp.Body);
  918. var buckets = rst.Value<JObject>("aggregations").Value<JObject>("post")
  919. .Value<JArray>("buckets");
  920. if (buckets.Count > 0)
  921. {
  922. buckets.ForEach(e =>
  923. {
  924. var postId = e.Value<long>("key");
  925. var count = e.Value<int>("doc_count") * 6;
  926. dic[postId].fact += count;
  927. });
  928. }
  929. }
  930. var todayStartTimeStr = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss");
  931. // 统计有效点赞
  932. var list = (await repository.QueryAsync<dynamic>(@$"
  933. SELECT tg.targetId as postId, count(1) as count FROM `n_user_thumbsup_gain` as tg left join n_stat_thumbsup_history as th on tg.targetId=th.postId and tg.userId=th.userId where tg.targetId>{startId} and tg.targetId<{endId} and
  934. tg.userId<>tg.authorId and tg.type=0 and tg.categoryType=1 and tg.createAt>='{todayStartTimeStr}' and
  935. th.userId is null
  936. group by tg.targetId
  937. ")).ToList();
  938. if (list.Count > 0)
  939. {
  940. list.ForEach(e =>
  941. {
  942. if (dic.ContainsKey(e.postId))
  943. {
  944. dic[(long)e.postId].fact += e.count * 6;
  945. }
  946. });
  947. }
  948. // 统计有效收藏
  949. list = (await repository.QueryAsync<dynamic>(@$"
  950. SELECT tg.postId, count(1) as count FROM `n_user_collect_post` as tg left join n_stat_collection_history as th on tg.postId=th.postId and tg.userId=th.userId where tg.postId>{startId} and tg.postId<{endId} and
  951. tg.userId<>tg.authorId and tg.categoryType=1 and tg.createAt>='{todayStartTimeStr}' and
  952. th.userId is null
  953. group by tg.postId
  954. ")).ToList();
  955. if (list.Count > 0)
  956. {
  957. list.ForEach(e =>
  958. {
  959. if (dic.ContainsKey(e.postId))
  960. {
  961. dic[(long)e.postId].fact += e.count * 6;
  962. }
  963. });
  964. }
  965. // 统计有效评论
  966. list = (await repository.QueryAsync<dynamic>(@$"
  967. select t1.postId, count(1) as count from (
  968. select distinct t.userId, t.postId from (
  969. select distinct c.userId, c.postId from n_post_comment as c
  970. where c.state=0 and c.isAuthor=0 and c.createAt>='{todayStartTimeStr}' and c.postId>{startId} and c.postId<{endId}
  971. UNION
  972. select distinct r.userId, r.postId from n_post_reply as r left join n_post_comment as c1 on r.commentId=c1.id
  973. where r.state=0 and r.isAuthor=0 and r.createAt>='{todayStartTimeStr}' and c1.state=0 and r.postId>{startId} and r.postId<{endId}
  974. ) as t
  975. ) as t1 left join n_stat_comment_history as ch on t1.postId=ch.postId and t1.userId=ch.userId
  976. where ch.postId is null
  977. group by t1.postId
  978. ")).ToList();
  979. if (list.Count > 0)
  980. {
  981. list.ForEach(e =>
  982. {
  983. if (dic.ContainsKey(e.postId))
  984. {
  985. dic[(long)e.postId].fact += e.count * 6;
  986. }
  987. });
  988. }
  989. #endregion 统计有效阅读量
  990. }
  991. // 看是否存在非今日的,如果存在,则需要在每日统计表中查询数据
  992. var notTodayItems = items.Where(e => e.id < todayStartId && e.id < endId && e.checkCntScore > 0)
  993. .ToList();
  994. if (notTodayItems.Count > 0)
  995. {
  996. var notTodayItemIds = notTodayItems.Select(e => (long)e.id).ToList();
  997. // 各表分别统计
  998. var readStat = await repository.QueryAsync<dynamic>(
  999. "select postId, sum(normalReadCount+fanReadCount) as count from n_stat_read where postId in @ids group by postId",
  1000. new { ids = notTodayItemIds });
  1001. readStat.ForEach(e => { dic[(long)e.postId].fact += (float)e.count * 3; });
  1002. var commentStat = await repository.QueryAsync<dynamic>(
  1003. "select postId, sum(commentCount) as count from n_stat_comment where postId in @ids group by postId",
  1004. new { ids = notTodayItemIds });
  1005. commentStat.ForEach(e => { dic[(long)e.postId].fact += (float)e.count * 6; });
  1006. var thumbsupStat = await repository.QueryAsync<dynamic>(
  1007. "select postId, sum(thumbsupCount) as count from n_stat_thumbsup where postId in @ids group by postId",
  1008. new { ids = notTodayItemIds });
  1009. thumbsupStat.ForEach(e => { dic[(long)e.postId].fact += (float)e.count * 6; });
  1010. var collectionStat = await repository.QueryAsync<dynamic>(
  1011. "select postId, sum(collectionCount) as count from n_stat_collection where postId in @ids group by postId",
  1012. new { ids = notTodayItemIds });
  1013. collectionStat.ForEach(e => { dic[(long)e.postId].fact += (float)e.count * 6; });
  1014. var shareStat = await repository.QueryAsync<dynamic>(
  1015. "select postId, sum(shareCount) as count from n_stat_share where postId in @ids group by postId",
  1016. new { ids = notTodayItemIds });
  1017. shareStat.ForEach(e => { dic[(long)e.postId].fact += (float)e.count * 6; });
  1018. }
  1019. // 如果客观事实数据最大为3500
  1020. dic.ForEach(e =>
  1021. {
  1022. if (e.Value.fact > 3500) dic[e.Key].fact = 3500;
  1023. });
  1024. var esRecords = new List<dynamic>();
  1025. items.ForEach(e =>
  1026. {
  1027. var id = (long)e.id;
  1028. var roleScore = PostHotValue.GetRoleScore(e.mpRole, e.superior > 0);
  1029. var checkCntScore = (float)e.checkCntScore;
  1030. var cntTypeFactor = e.type > 0 ? 1.3f : 1;
  1031. var coolingFactor = (float)e.coolingFactor;
  1032. var manualExtraScore = (float)e.manualExtraScore;
  1033. var cntHotCoefficient = (float)e.cntHotCoefficient;
  1034. var subjectiveScore = checkCntScore > 0
  1035. ? (roleScore + checkCntScore) * cntTypeFactor * coolingFactor + manualExtraScore
  1036. : 0;
  1037. var days = Math.Ceiling((now - IdWorker.DecodeId(id).Time).TotalDays);
  1038. if (days == 0) days = 1;
  1039. var objectiveScore = dic.ContainsKey(id) ? dic[id].fact : 0;
  1040. var cntHot = checkCntScore > 0
  1041. ? (float)((subjectiveScore + objectiveScore) / Math.Pow(days, 1.6) * cntHotCoefficient)
  1042. : 0;
  1043. esRecords.Add(new
  1044. {
  1045. update = new
  1046. {
  1047. _id = id
  1048. }
  1049. });
  1050. esRecords.Add(new
  1051. {
  1052. doc = new
  1053. {
  1054. subjectiveScore,
  1055. hot = cntHot
  1056. }
  1057. });
  1058. });
  1059. if (esRecords.Count > 0)
  1060. {
  1061. var resp = await ESHelper.Client.BulkAsync<StringResponse>(ESConstants.ESIndexName.Post,
  1062. PostData.MultiJson(esRecords));
  1063. if (!resp.Success)
  1064. {
  1065. LoggerManager.Logger.Error("ES更新笔记热度值失败\r\n" + resp.Body);
  1066. }
  1067. }
  1068. startId = lastId;
  1069. if (itemsCount < size) break;
  1070. }
  1071. if (originalStartId > 0)
  1072. {
  1073. // 将设置过期的文章热度值为0
  1074. var rsp = await ESHelper.Client.UpdateByQueryAsync<StringResponse>(ESConstants.ESIndexName.Post,
  1075. PostData.Serializable(new
  1076. {
  1077. script = new { source = $"ctx._source['hot']=0" },
  1078. query = new
  1079. {
  1080. @bool = new
  1081. {
  1082. must = new List<dynamic>
  1083. {
  1084. new { term = new { categoryType = 1 } },
  1085. new { range = new { hot = new { gt = 0 } } },
  1086. new { range = new { id = new { lt = originalStartId } } },
  1087. }
  1088. }
  1089. }
  1090. }));
  1091. if (!rsp.Success)
  1092. {
  1093. LoggerManager.Logger.Error("ES文章热度归零失败:\r\n" + rsp.Body);
  1094. }
  1095. }
  1096. if (calcTaskId > 0)
  1097. {
  1098. var endAt = DateTime.Now;
  1099. await repository.QueryAsync<dynamic>(
  1100. "update n_post_hot_all_calc_records set endAt=@endAt, totalSecs=@totalSecs, note=@note where id=@id",
  1101. new
  1102. {
  1103. id = calcTaskId,
  1104. endAt,
  1105. totalSecs = (float)(endAt - now).TotalSeconds,
  1106. note = $"处理总计{totalCount}条文章记录"
  1107. }, null, null, false);
  1108. }
  1109. }
  1110. /// <summary>
  1111. /// 计算某些文章的平台主观策略分、客观事实分 以及 热度值(此处不计算文章的冷却系数)
  1112. /// </summary>
  1113. /// <param name="calcReadScore">是否计算客观事件(有效阅读)及综合热度值</param>
  1114. /// <param name="extraManualExtraScore">额外附加的人工干预分数,用来测算合适的人工干预分</param>
  1115. public static async Task<Dictionary<long, PostHotValue>> CalcPostsHot(this DbRESTFulRepository repository, List<long> postIds, ConfigFromDb config, bool calcReadScore = true, float? extraManualExtraScore = null, DateTime? specifyCalcTime = null)
  1116. {
  1117. postIds = postIds.OrderBy(e => e).ToList();
  1118. // 用来存文章热度值中间值
  1119. var dic = new Dictionary<long, PostHotValue>();
  1120. postIds.ForEach(e =>
  1121. {
  1122. dic[e] = new PostHotValue();
  1123. });
  1124. if (postIds.Count == 0)
  1125. {
  1126. return dic;
  1127. }
  1128. var sql = $"select p.id, p.userId, p.type, p.categoryType, p.checkCntScore, p.manualExtraScore, p.coolingFactor, u.mpRole, u.superior, u.cntHotCoefficient from n_post as p left join n_user as u on p.userId=u.id where p.id in @postIds and categoryType=1 and p.state=1 and p.checkCntScore>0 order by p.id asc";
  1129. using var conn = repository.ConnectionManager.GetConnection();
  1130. conn.Open();
  1131. var items = (await repository.QueryAsync<dynamic>(sql, new { postIds }, conn)).ToList();
  1132. if (items.Count == 0)
  1133. {
  1134. return dic;
  1135. }
  1136. var calcTime = specifyCalcTime ?? DateTime.Now;
  1137. var itemsInBlackList = new List<dynamic>();
  1138. var validPostIds = items.Select(e => (long)e.id).ToList();
  1139. if (validPostIds.Count > 0)
  1140. {
  1141. // 查出在小黑屋的,如果在小黑屋,则热度为0
  1142. var postIdInBlacklist = (await repository.QueryAsync<long>($"select postId from n_stat_post_blacklist where postId in @postIds and ctrlType>0", new { postIds = validPostIds }, conn)).ToList();
  1143. if (postIdInBlacklist.Count > 0)
  1144. {
  1145. var validItems = new List<dynamic>();
  1146. items.ForEach(e =>
  1147. {
  1148. if (postIdInBlacklist.Contains(e.id))
  1149. {
  1150. validItems.Add(e);
  1151. }
  1152. else
  1153. {
  1154. itemsInBlackList.Add(e);
  1155. }
  1156. });
  1157. items = validItems;
  1158. }
  1159. }
  1160. // 综合热度及客观事实分
  1161. // 不计算最近6小时内的
  1162. var validEndTime = calcTime.AddHours(-config.PostIgnoreHotRecentHours);
  1163. var validEndId = IdWorker.GetStartIdByDate(validEndTime);
  1164. var expiredMinId = IdWorker.GetStartIdByDate(
  1165. string.IsNullOrEmpty(config.PostStartRecommendDate)
  1166. ? DateTime.Now.AddDays(-config.PostMaxRecommendDays)
  1167. : DateTime.Parse(config.PostStartRecommendDate)
  1168. );
  1169. if (calcReadScore)
  1170. {
  1171. // 计算当前文章列表的有效阅读量
  1172. // 判断是否是今天的内容,如果是,则需要查出今天的有效阅读量
  1173. var todayStartTime = calcTime.AddHours(-calcTime.Hour).AddMinutes(-calcTime.Minute).AddSeconds(-calcTime.Second).AddMilliseconds(-calcTime.Millisecond);
  1174. var todayStartId = IdWorker.GetStartIdByDate(todayStartTime);
  1175. var todayItems = items.Where(e => e.id > todayStartId && e.id < validEndId).ToList();
  1176. if (todayItems.Count > 0)
  1177. {
  1178. var todayItemIds = todayItems.Select(e => (long)e.id).ToList();
  1179. #region 统计有效阅读量
  1180. // 统计阅读量
  1181. var resp = await ESHelper.Client.SearchAsync<StringResponse>("postreadrecord", PostData.Serializable(new
  1182. {
  1183. query = new
  1184. {
  1185. @bool = new
  1186. {
  1187. must = new List<dynamic>
  1188. {
  1189. new
  1190. {
  1191. range = new
  1192. {
  1193. createAt = new
  1194. {
  1195. gte = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss")
  1196. }
  1197. }
  1198. },
  1199. new
  1200. {
  1201. terms = new
  1202. {
  1203. postId = todayItemIds
  1204. }
  1205. }
  1206. },
  1207. must_not = new List<dynamic>
  1208. {
  1209. new
  1210. {
  1211. term = new { notValidRecord = true }
  1212. }
  1213. }
  1214. }
  1215. },
  1216. size = 0,
  1217. aggs = new
  1218. {
  1219. post = new
  1220. {
  1221. terms = new { field = "postId", size = 50000 }
  1222. }
  1223. }
  1224. }));
  1225. if (resp.Success)
  1226. {
  1227. var rst = JsonConvert.DeserializeObject<JObject>(resp.Body);
  1228. var buckets = rst.Value<JObject>("aggregations").Value<JObject>("post").Value<JArray>("buckets");
  1229. if (buckets.Count > 0)
  1230. {
  1231. buckets.ForEach(e =>
  1232. {
  1233. var postId = e.Value<long>("key");
  1234. var count = e.Value<int>("doc_count") * 3;
  1235. dic[postId].fact += count;
  1236. });
  1237. }
  1238. }
  1239. // 统计分享量
  1240. resp = await ESHelper.Client.SearchAsync<StringResponse>("postsharerecord", PostData.Serializable(new
  1241. {
  1242. query = new
  1243. {
  1244. @bool = new
  1245. {
  1246. must = new List<dynamic>
  1247. {
  1248. new
  1249. {
  1250. range = new
  1251. {
  1252. createAt = new
  1253. {
  1254. gte = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss")
  1255. }
  1256. }
  1257. },
  1258. new
  1259. {
  1260. terms = new
  1261. {
  1262. postId = todayItemIds
  1263. }
  1264. }
  1265. },
  1266. must_not = new List<dynamic>
  1267. {
  1268. new
  1269. {
  1270. term = new { notValidRecord = true }
  1271. }
  1272. }
  1273. }
  1274. },
  1275. size = 0,
  1276. aggs = new
  1277. {
  1278. post = new
  1279. {
  1280. terms = new { field = "postId", size = 50000 }
  1281. }
  1282. }
  1283. }));
  1284. if (resp.Success)
  1285. {
  1286. var rst = JsonConvert.DeserializeObject<JObject>(resp.Body);
  1287. var buckets = rst.Value<JObject>("aggregations").Value<JObject>("post").Value<JArray>("buckets");
  1288. if (buckets.Count > 0)
  1289. {
  1290. buckets.ForEach(e =>
  1291. {
  1292. var postId = e.Value<long>("key");
  1293. var count = e.Value<int>("doc_count") * 6;
  1294. dic[postId].fact += count;
  1295. });
  1296. }
  1297. }
  1298. var todayStartTimeStr = todayStartTime.ToString("yyyy-MM-dd HH:mm:ss");
  1299. // 统计有效点赞
  1300. var list = (await repository.QueryAsync<dynamic>(@$"
  1301. SELECT tg.targetId as postId, count(1) as count FROM `n_user_thumbsup_gain` as tg left join n_stat_thumbsup_history as th on tg.targetId=th.postId and tg.userId=th.userId where tg.targetId in @todayItemIds and
  1302. tg.userId<>tg.authorId and tg.type=0 and tg.categoryType=1 and tg.createAt>='{todayStartTimeStr}' and
  1303. th.userId is null
  1304. group by tg.targetId
  1305. ", new { todayItemIds }, conn)).ToList();
  1306. if (list.Count > 0)
  1307. {
  1308. list.ForEach(e =>
  1309. {
  1310. dic[(long)e.postId].fact += e.count * 6;
  1311. });
  1312. }
  1313. // 统计有效收藏
  1314. list = (await repository.QueryAsync<dynamic>(@$"
  1315. SELECT tg.postId, count(1) as count FROM `n_user_collect_post` as tg left join n_stat_collection_history as th on tg.postId=th.postId and tg.userId=th.userId where tg.postId in @todayItemIds and
  1316. tg.userId<>tg.authorId and tg.categoryType=1 and tg.createAt>='{todayStartTimeStr}' and
  1317. th.userId is null
  1318. group by tg.postId
  1319. ", new { todayItemIds }, conn)).ToList();
  1320. if (list.Count > 0)
  1321. {
  1322. list.ForEach(e =>
  1323. {
  1324. if (dic.ContainsKey(e.postId))
  1325. {
  1326. dic[(long)e.postId].fact += e.count * 6;
  1327. }
  1328. });
  1329. }
  1330. // 统计有效评论
  1331. list = (await repository.QueryAsync<dynamic>(@$"
  1332. select t1.postId, count(1) as count from (
  1333. select distinct t.userId, t.postId from (
  1334. select distinct c.userId, c.postId from n_post_comment as c
  1335. where c.state=0 and c.isAuthor=0 and c.createAt>='{todayStartTimeStr}' and c.postId in @todayItemIds
  1336. UNION
  1337. select distinct r.userId, r.postId from n_post_reply as r left join n_post_comment as c1 on r.commentId=c1.id
  1338. where r.state=0 and r.isAuthor=0 and r.createAt>='{todayStartTimeStr}' and c1.state=0 and r.postId in @todayItemIds
  1339. ) as t
  1340. ) as t1 left join n_stat_comment_history as ch on t1.postId=ch.postId and t1.userId=ch.userId
  1341. where ch.postId is null
  1342. group by t1.postId
  1343. ", new { todayItemIds }, conn)).ToList();
  1344. if (list.Count > 0)
  1345. {
  1346. list.ForEach(e =>
  1347. {
  1348. dic[(long)e.postId].fact += e.count * 6;
  1349. });
  1350. }
  1351. #endregion 统计有效阅读量
  1352. }
  1353. // 看是否存在非今日的,如果存在,则需要在每日统计表中查询数据
  1354. var notTodayItems = items.Where(e => e.id < todayStartId && e.id > expiredMinId && e.id < validEndId).ToList();
  1355. if (notTodayItems.Count > 0)
  1356. {
  1357. var notTodayItemIds = notTodayItems.Select(e => (long)e.id).ToList();
  1358. // 各表分别统计
  1359. var readStat = await repository.QueryAsync<dynamic>("select postId, sum(normalReadCount+fanReadCount) as count from n_stat_read where postId in @ids group by postId", new { ids = notTodayItemIds }, conn);
  1360. readStat.ForEach(e =>
  1361. {
  1362. dic[(long)e.postId].fact += (float)e.count * 3;
  1363. });
  1364. var commentStat = await repository.QueryAsync<dynamic>("select postId, sum(commentCount) as count from n_stat_comment where postId in @ids group by postId", new { ids = notTodayItemIds }, conn);
  1365. commentStat.ForEach(e =>
  1366. {
  1367. dic[(long)e.postId].fact += (float)e.count * 6;
  1368. });
  1369. var thumbsupStat = await repository.QueryAsync<dynamic>("select postId, sum(thumbsupCount) as count from n_stat_thumbsup where postId in @ids group by postId", new { ids = notTodayItemIds }, conn);
  1370. thumbsupStat.ForEach(e =>
  1371. {
  1372. dic[(long)e.postId].fact += (float)e.count * 6;
  1373. });
  1374. var collectionStat = await repository.QueryAsync<dynamic>("select postId, sum(collectionCount) as count from n_stat_collection where postId in @ids group by postId", new { ids = notTodayItemIds });
  1375. collectionStat.ForEach(e =>
  1376. {
  1377. dic[(long)e.postId].fact += (float)e.count * 6;
  1378. });
  1379. var shareStat = await repository.QueryAsync<dynamic>("select postId, sum(shareCount) as count from n_stat_share where postId in @ids group by postId", new { ids = notTodayItemIds }, conn);
  1380. shareStat.ForEach(e =>
  1381. {
  1382. dic[(long)e.postId].fact += (float)e.count * 6;
  1383. });
  1384. }
  1385. // 如果客观事实数据最大为3500
  1386. dic.ForEach(e =>
  1387. {
  1388. if (e.Value.fact > 3500) dic[e.Key].fact = 3500;
  1389. });
  1390. }
  1391. items.ForEach(e =>
  1392. {
  1393. var id = (long)e.id;
  1394. var roleScore = PostHotValue.GetRoleScore(e.mpRole, e.superior > 0);
  1395. var checkCntScore = (float)e.checkCntScore;
  1396. var cntTypeFactor = e.type > 0 ? 1.3f : 1;
  1397. var coolingFactor = (float)e.coolingFactor;
  1398. var manualExtraScore = ((float)e.manualExtraScore + (extraManualExtraScore ?? 0));
  1399. var cntHotCoefficient = (float)e.cntHotCoefficient;
  1400. var subjectiveScore = checkCntScore > 0 ? (roleScore + checkCntScore) * cntTypeFactor * coolingFactor + manualExtraScore : 0;
  1401. var expired = e.id < expiredMinId;
  1402. dic[id].subjectiveScore = subjectiveScore;
  1403. dic[id].cntHotCoefficient = cntHotCoefficient;
  1404. if (calcReadScore && !expired && id < validEndId)
  1405. {
  1406. var days = (float)Math.Ceiling((calcTime - IdWorker.DecodeId(id).Time).TotalDays);
  1407. if (days == 0) days = 1;
  1408. var objectiveScore = dic.ContainsKey(id) ? dic[id].fact : 0;
  1409. dic[id].hot = (float)((subjectiveScore + objectiveScore) / Math.Pow(days, 1.6) * cntHotCoefficient);
  1410. }
  1411. });
  1412. return dic;
  1413. }
  1414. /// <summary>
  1415. /// 同步单篇内容
  1416. /// </summary>
  1417. public static async Task SyncPostSingle(this DbRESTFulRepository repository, long id, OSSConfig ossConfig, OssClient ossClient, ConfigFromDb config, bool calcReadScore = true)
  1418. {
  1419. var postInfo = await repository.QuerySingleOrDefaultAsync<dynamic>("select id, state, title, thumbnails, type, originalTopicNames, tags, summary, contentState, videoId, content, createAt, userId, categoryType, onlyInTopic, noMoreCont, resourceCode, primaryType, otherPrimaryTypes, isBest, coverWidth, coverHeight, previewable, reprintSource, reprintLink, checkCntScore from n_post where id=@id", new { id });
  1420. if (postInfo == null)
  1421. {
  1422. await ESHelper.Client.DeleteAsync<StringResponse>(ESConstants.ESIndexName.Post, id.ToString());
  1423. }
  1424. else
  1425. {
  1426. await repository.RecalcPostCoolingFactor(id, (long)postInfo.userId, config);
  1427. await repository.SyncPostList2ES(DateTime.Now, new List<dynamic> { postInfo }, ossConfig, ossClient, config, calcReadScore);
  1428. }
  1429. }
  1430. /// <summary>
  1431. /// 返回最后一条同步的动态Id,如果没有,则返回null。(id, state, title, thumbnails, type, originalTopicNames, tags, summary, contentState, videoId, content, createAt, userId, categoryType, onlyInTopic, noMoreCont, resourceCode, primaryType, otherPrimaryTypes, isBest, coverWidth, coverHeight, previewable, reprintSource, reprintLink, checkCntScore)
  1432. /// </summary>
  1433. public static async Task<long?> SyncPostList2ES(this DbRESTFulRepository repository, DateTime now, List<dynamic> posts, OSSConfig ossConfig, OssClient ossClient, ConfigFromDb config, bool calcReadScore = true)
  1434. {
  1435. if (posts.Count == 0) return null;
  1436. long? lastId = null;
  1437. var userIds = posts.Select(e => e.userId).Distinct().ToList();
  1438. Dictionary<long, string> userDic = null;
  1439. if (userIds.Count > 0)
  1440. {
  1441. var users = (await repository.QueryAsync<dynamic>("select id, alias from n_user where id in @ids", new { ids = userIds })).ToList();
  1442. userDic = new Dictionary<long, string>(users.Count);
  1443. foreach (var item in users)
  1444. {
  1445. userDic[item.id] = item.alias;
  1446. }
  1447. }
  1448. var postTopicNamesDic = new Dictionary<long, List<string>>();
  1449. var allTopicNames = new HashSet<string>();
  1450. posts.Where(e => !string.IsNullOrEmpty((string)e.originalTopicNames)).ForEach(e =>
  1451. {
  1452. var names = ((string)e.originalTopicNames).Split('\n', StringSplitOptions.RemoveEmptyEntries).Distinct().ToList();
  1453. postTopicNamesDic[(long)e.id] = names;
  1454. names.ForEach(n => allTopicNames.Add(n));
  1455. });
  1456. Dictionary<string, dynamic> topicNameInfoDic = new Dictionary<string, dynamic>();
  1457. if (allTopicNames.Count > 0)
  1458. {
  1459. var topics = (await repository.QueryAsync<dynamic>("select id, title, disabled from n_topic where title in @names", new { names = allTopicNames })).ToList();
  1460. topics.ForEach(e =>
  1461. {
  1462. topicNameInfoDic[(string)e.title] = e;
  1463. });
  1464. }
  1465. var postIds = new List<long>();
  1466. var statistic = await repository.QueryAsync<dynamic>("select postId, hot, thumbsup, collected, `comment`, `read` from n_post_statistic where postId in @ids", new { ids = posts.Select(e => e.id) });
  1467. var statisticDic = new Dictionary<long, dynamic>();
  1468. foreach (var item in statistic)
  1469. {
  1470. statisticDic[(long)item.postId] = item;
  1471. }
  1472. var resourceDic = new Dictionary<string, string>();
  1473. var resourceCodes = new HashSet<string>();
  1474. posts.ForEach(e =>
  1475. {
  1476. postIds.Add((long)e.id);
  1477. if (!string.IsNullOrEmpty(e.resourceCode))
  1478. {
  1479. ((string)e.resourceCode).Split(',', StringSplitOptions.RemoveEmptyEntries).ForEach(i => resourceCodes.Add(i));
  1480. }
  1481. });
  1482. if (resourceCodes.Count > 0)
  1483. {
  1484. var resourceInfos = await repository.QueryAsync<dynamic>("select code, name from n_resource where code in @codes", new { codes = resourceCodes.ToList() });
  1485. resourceInfos.ForEach(e =>
  1486. {
  1487. resourceDic.Add(e.code, e.name);
  1488. });
  1489. }
  1490. var hotDic = await repository.CalcPostsHot(postIds, config, calcReadScore);
  1491. var records = new List<dynamic>();
  1492. foreach (var post in posts)
  1493. {
  1494. var postId = (long)post.id;
  1495. records.Add(new
  1496. {
  1497. delete = new { _id = postId }
  1498. });
  1499. // ES只存储审核通过且内容完整(即视频是上传处理完成的)的动态
  1500. if (post.state != 1 || post.contentState != 1) continue;
  1501. records.Add(new
  1502. {
  1503. create = new { _id = postId }
  1504. });
  1505. string tagsStr = post.tags;
  1506. List<string> tags = tagsStr.Split(',', StringSplitOptions.RemoveEmptyEntries).Distinct().ToList();
  1507. var statisticItem = statisticDic.ContainsKey(post.id) ? statisticDic[post.id] : null;
  1508. string content = string.Empty;
  1509. try
  1510. {
  1511. if (post.noMoreCont > 0)
  1512. {
  1513. content = post.content ?? string.Empty;
  1514. }
  1515. else
  1516. {
  1517. content = await FileOperator.GetFileContent("jzq_" + post.id.ToString(), ossConfig.OSSBucket, ossClient);
  1518. }
  1519. }
  1520. catch
  1521. {
  1522. // ignored
  1523. }
  1524. var primaryTypes = new List<string>();
  1525. primaryTypes.Add(post.primaryType.ToString());
  1526. if (!string.IsNullOrEmpty(post.otherPrimaryTypes))
  1527. {
  1528. primaryTypes.AddRange((post.otherPrimaryTypes as string).Split(',', StringSplitOptions.RemoveEmptyEntries));
  1529. }
  1530. var codes = ((string)post.resourceCode).Split(',', StringSplitOptions.RemoveEmptyEntries);
  1531. var validCodes = new List<string>();
  1532. var validNames = new List<string>();
  1533. codes.ForEach(e =>
  1534. {
  1535. if (!resourceDic.TryGetValue(e, out var value)) return;
  1536. validCodes.Add(e);
  1537. validNames.Add(value);
  1538. });
  1539. var topicInfoList = postTopicNamesDic.ContainsKey(postId) && postTopicNamesDic[postId].Count > 0 ? postTopicNamesDic[postId].Where(e => topicNameInfoDic.ContainsKey(e)).Select(e => topicNameInfoDic[e]).ToList() : new List<dynamic>();
  1540. var topicId = 0;
  1541. var topicName = string.Empty;
  1542. int[] topicIds = null;
  1543. int[] validTopicIds = null;
  1544. string[] topicNames = null;
  1545. string[] validTopicNames = null;
  1546. if (topicInfoList.Count > 0)
  1547. {
  1548. topicIds = topicInfoList.Select(e => (int)e.id).ToArray();
  1549. topicNames = topicInfoList.Select(e => (string)e.title).ToArray();
  1550. validTopicIds = topicInfoList.Where(e => (int)e.disabled == 0).Select(e => (int)e.id).ToArray();
  1551. validTopicNames = topicInfoList.Where(e => (int)e.disabled == 0).Select(e => (string)e.title).ToArray();
  1552. topicId = topicInfoList.First().id;
  1553. topicName = topicInfoList.First().name;
  1554. }
  1555. else
  1556. {
  1557. topicIds = Array.Empty<int>();
  1558. topicNames = Array.Empty<string>();
  1559. validTopicIds = Array.Empty<int>();
  1560. validTopicNames = Array.Empty<string>();
  1561. }
  1562. records.Add(new ESPostModel()
  1563. {
  1564. id = postId,
  1565. title = post.title,
  1566. tags = tags.ToArray(),
  1567. thumbnails = post.thumbnails,
  1568. type = (short)post.type,
  1569. userId = post.userId,
  1570. userAlias = userDic != null && userDic.ContainsKey(post.userId) ? userDic[post.userId] : "",
  1571. summary = post.summary,
  1572. contentState = post.contentState,
  1573. videoId = post.videoId,
  1574. content = content,
  1575. createAt = post.createAt.ToString("yyyy-MM-dd HH:mm:ss"),
  1576. subjectiveScore = post.checkCntScore > 0 && hotDic.ContainsKey(postId) ? hotDic[postId].subjectiveScore : 0,
  1577. hot = post.checkCntScore > 0 && hotDic.ContainsKey(postId) ? hotDic[postId].hot : 0,
  1578. topicId = topicId,
  1579. topicName = topicName,
  1580. topicIds = topicIds,
  1581. topicNames = topicNames,
  1582. validTopicIds = validTopicIds,
  1583. validTopicNames = validTopicNames,
  1584. noMoreCont = post.noMoreCont,
  1585. state = (short)post.state,
  1586. categoryType = (short)post.categoryType,
  1587. onlyInTopic = (short)post.onlyInTopic,
  1588. resourceCodes = validCodes.ToArray(),
  1589. resourceNames = validNames.ToArray(),
  1590. primaryType = post.primaryType,
  1591. primaryTypes = primaryTypes.Distinct().ToArray(),
  1592. isBest = post.isBest,
  1593. previewable = post.previewable > 0,
  1594. coverHeight = post.coverHeight,
  1595. coverWidth = post.coverWidth,
  1596. reprintSource = post.reprintSource,
  1597. reprintLink = post.reprintLink,
  1598. });
  1599. lastId = post.id;
  1600. }
  1601. await ESHelper.Client.BulkAsync<StringResponse>(ESConstants.ESIndexName.Post, PostData.MultiJson(records));
  1602. return lastId;
  1603. }
  1604. /// <summary>
  1605. /// 单独给某一篇文章添加有效阅读量
  1606. /// </summary>
  1607. public static async Task AddSingleReadRecord(this DbRESTFulRepository repository, long postId, long authorId, int categoryType, int count = 1)
  1608. {
  1609. if (count == 0) return;
  1610. var esRecrods = new List<dynamic>();
  1611. for (int i = 0; i < count; i++)
  1612. {
  1613. esRecrods.Add(new { create = new { } });
  1614. esRecrods.Add(new ESPostReadRecordModel
  1615. {
  1616. postId = postId,
  1617. authorId = authorId,
  1618. categoryType = categoryType,
  1619. userId = 0,
  1620. isAuthor = false,
  1621. isFan = false,
  1622. isMock = true,
  1623. clientId = "",
  1624. ip = "",
  1625. createAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
  1626. });
  1627. }
  1628. // 存入ES
  1629. await ESHelper.Client.BulkAsync<StringResponse>("postreadrecord", PostData.MultiJson(esRecrods));
  1630. // 更新数据库中统计数据
  1631. await repository.QueryAsync<dynamic>($"update n_post_statistic set `read`=`read`+{count}, `normalRead`=`normalRead`+{count} where postId={postId};", null, null, null, false);
  1632. }
  1633. /// <summary>
  1634. /// 尝试删除某文章对应的热门讨论配置
  1635. /// </summary>
  1636. public static async Task HotDiscussTryToRemove(this DbRESTFulRepository repository, long postId, int? topicId = null)
  1637. {
  1638. var query = topicId > 0 ? $"postId={postId} and topicId={topicId}" : $"postId={postId}";
  1639. using var conn = repository.ConnectionManager.GetConnection(false);
  1640. conn.Open();
  1641. await repository.QueryAsync<dynamic>($"delete from n_post_topic_hot where {query}", null,
  1642. conn);
  1643. await repository.QueryAsync<dynamic>(@$"update n_post_topic_hot_records set removeAt=now()
  1644. where {query} and removeAt is null", null, conn);
  1645. }
  1646. }
  1647. }