Sfoglia il codice sorgente

增加广告全局统计

懒得勤快 3 anni fa
parent
commit
e9e9140b66

+ 203 - 186
src/Masuit.MyBlogs.Core/Controllers/AdvertisementController.cs

@@ -23,220 +23,237 @@ using System.Linq.Expressions;
 using System.Net;
 using System.Text.RegularExpressions;
 
-namespace Masuit.MyBlogs.Core.Controllers
+namespace Masuit.MyBlogs.Core.Controllers;
+
+[Route("partner/[action]")]
+public class AdvertisementController : BaseController
 {
-    [Route("partner/[action]")]
-    public class AdvertisementController : BaseController
+    public IAdvertisementClickRecordService ClickRecordService { get; set; }
+
+    /// <summary>
+    /// 前往
+    /// </summary>
+    /// <param name="id">广告id</param>
+    /// <returns></returns>
+    [HttpGet("/p{id:int}"), HttpGet("{id:int}", Order = 1), ResponseCache(Duration = 3600)]
+    public async Task<IActionResult> Redirect(int id)
     {
-        public IAdvertisementClickRecordService ClickRecordService { get; set; }
-
-        /// <summary>
-        /// 前往
-        /// </summary>
-        /// <param name="id">广告id</param>
-        /// <returns></returns>
-        [HttpGet("/p{id:int}"), HttpGet("{id:int}", Order = 1), ResponseCache(Duration = 3600)]
-        public async Task<IActionResult> Redirect(int id)
+        var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("推广链接不存在");
+        if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + id)))
         {
-            var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("推广链接不存在");
-            if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + id)))
+            HttpContext.Session.Set("ads" + id, id.ToString());
+            ad.ClickRecords.Add(new AdvertisementClickRecord()
             {
-                HttpContext.Session.Set("ads" + id, id.ToString());
-                ad.ClickRecords.Add(new AdvertisementClickRecord()
-                {
-                    IP = ClientIP,
-                    Location = ClientIP.GetIPLocation(),
-                    Referer = Request.Headers[HeaderNames.Referer].ToString(),
-                    Time = DateTime.Now
-                });
-                await AdsService.SaveChangesAsync();
-                var start = DateTime.Today.AddMonths(-6);
-                await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
-            }
-
-            return Redirect(ad.Url);
+                IP = ClientIP,
+                Location = ClientIP.GetIPLocation(),
+                Referer = Request.Headers[HeaderNames.Referer].ToString(),
+                Time = DateTime.Now
+            });
+            await AdsService.SaveChangesAsync();
+            var start = DateTime.Today.AddMonths(-6);
+            await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
         }
 
-        /// <summary>
-        /// 获取分页
-        /// </summary>
-        /// <returns></returns>
-        [MyAuthorize]
-        public ActionResult GetPageData(int page = 1, [Range(1, int.MaxValue, ErrorMessage = "页大小必须大于0")] int size = 10, string kw = "")
-        {
-            Expression<Func<Advertisement, bool>> where = p => true;
-            if (!string.IsNullOrEmpty(kw))
-            {
-                kw = Regex.Escape(kw);
-                where = where.And(p => Regex.IsMatch(p.Title + p.Description + p.Url, kw, RegexOptions.IgnoreCase));
-            }
-
-            var list = AdsService.GetQuery(where).OrderByDescending(p => p.Status == Status.Available).ThenByDescending(a => a.Price).ThenByDescending(a => a.Id).ProjectTo<AdvertisementViewModel>(MapperConfig).ToPagedList(page, size);
-            return Ok(list);
-        }
+        return Redirect(ad.Url);
+    }
 
-        /// <summary>
-        /// 保存广告
-        /// </summary>
-        /// <param name="model"></param>
-        /// <returns></returns>
-        [HttpPost, MyAuthorize]
-        public async Task<IActionResult> Save([FromBodyOrDefault] AdvertisementDto model)
+    /// <summary>
+    /// 获取分页
+    /// </summary>
+    /// <returns></returns>
+    [MyAuthorize]
+    public ActionResult GetPageData(int page = 1, [Range(1, int.MaxValue, ErrorMessage = "页大小必须大于0")] int size = 10, string kw = "")
+    {
+        Expression<Func<Advertisement, bool>> where = p => true;
+        if (!string.IsNullOrEmpty(kw))
         {
-            var entity = AdsService[model.Id] ?? new Advertisement();
-            model.CategoryIds = model.CategoryIds?.Replace("null", "");
-            model.Regions = Regex.Replace(model.Regions ?? "", @"(\p{P}|\p{Z}|\p{S})+", "|");
-            if (model.RegionMode == RegionLimitMode.All)
-            {
-                model.Regions = null;
-            }
-
-            if (model.Types.Contains(AdvertiseType.Banner.ToString("D")) && string.IsNullOrEmpty(model.ImageUrl))
-            {
-                return ResultData(null, false, "宣传大图不能为空");
-            }
-
-            if (model.Types.Length > 3 && string.IsNullOrEmpty(model.ThumbImgUrl))
-            {
-                return ResultData(null, false, "宣传小图不能为空");
-            }
-
-            Mapper.Map(model, entity);
-            var b = await AdsService.AddOrUpdateSavedAsync(a => a.Id, entity) > 0;
-            return ResultData(null, b, b ? "保存成功" : "保存失败");
+            kw = Regex.Escape(kw);
+            where = where.And(p => Regex.IsMatch(p.Title + p.Description + p.Url, kw, RegexOptions.IgnoreCase));
         }
 
-        /// <summary>
-        /// 删除广告
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpPost("{id}"), HttpGet("{id}"), MyAuthorize]
-        public async Task<IActionResult> Delete(int id)
+        var list = AdsService.GetQuery(where).OrderByDescending(p => p.Status == Status.Available).ThenByDescending(a => a.Price).ThenByDescending(a => a.Id).ProjectTo<AdvertisementViewModel>(MapperConfig).ToPagedList(page, size);
+        return Ok(list);
+    }
+
+    /// <summary>
+    /// 保存广告
+    /// </summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    [HttpPost, MyAuthorize]
+    public async Task<IActionResult> Save([FromBodyOrDefault] AdvertisementDto model)
+    {
+        var entity = AdsService[model.Id] ?? new Advertisement();
+        model.CategoryIds = model.CategoryIds?.Replace("null", "");
+        model.Regions = Regex.Replace(model.Regions ?? "", @"(\p{P}|\p{Z}|\p{S})+", "|");
+        if (model.RegionMode == RegionLimitMode.All)
         {
-            bool b = await AdsService.DeleteByIdAsync(id) > 0;
-            return ResultData(null, b, b ? "删除成功" : "删除失败");
+            model.Regions = null;
         }
 
-        /// <summary>
-        /// 广告上下架
-        /// </summary>
-        /// <param name="id">文章id</param>
-        /// <returns></returns>
-        [MyAuthorize, HttpPost("{id}")]
-        public async Task<ActionResult> ChangeState(int id)
+        if (model.Types.Contains(AdvertiseType.Banner.ToString("D")) && string.IsNullOrEmpty(model.ImageUrl))
         {
-            var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告不存在!");
-            ad.Status = ad.Status == Status.Available ? Status.Unavailable : Status.Available;
-            return ResultData(null, await AdsService.SaveChangesAsync() > 0, ad.Status == Status.Available ? $"【{ad.Title}】已上架!" : $"【{ad.Title}】已下架!");
+            return ResultData(null, false, "宣传大图不能为空");
         }
 
-        /// <summary>
-        /// 随机前往一个广告
-        /// </summary>
-        /// <returns></returns>
-        [HttpGet("/partner-random")]
-        public async Task<ActionResult> RandomGo()
+        if (model.Types.Length > 3 && string.IsNullOrEmpty(model.ThumbImgUrl))
         {
-            var ad = AdsService.GetByWeightedPrice((AdvertiseType)new Random().Next(1, 4), Request.Location());
-            if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + ad.Id)))
-            {
-                HttpContext.Session.Set("ads" + ad.Id, ad.Id.ToString());
-                ad.ClickRecords.Add(new AdvertisementClickRecord()
-                {
-                    IP = ClientIP,
-                    Location = ClientIP.GetIPLocation(),
-                    Referer = Request.Headers[HeaderNames.Referer].ToString(),
-                    Time = DateTime.Now
-                });
-                await AdsService.SaveChangesAsync();
-                var start = DateTime.Today.AddMonths(-1);
-                await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
-            }
-
-            return Redirect(ad.Url);
+            return ResultData(null, false, "宣传小图不能为空");
         }
 
-        /// <summary>
-        /// 广告访问记录
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [HttpGet("/partner/{id}/records"), MyAuthorize]
-        public async Task<IActionResult> ClickRecords(int id, int page = 1, int size = 15, string kw = "")
-        {
-            Expression<Func<AdvertisementClickRecord, bool>> where = e => e.AdvertisementId == id;
-            if (!string.IsNullOrEmpty(kw))
-            {
-                kw = Regex.Escape(kw);
-                where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer, kw));
-            }
+        Mapper.Map(model, entity);
+        var b = await AdsService.AddOrUpdateSavedAsync(a => a.Id, entity) > 0;
+        return ResultData(null, b, b ? "保存成功" : "保存失败");
+    }
 
-            var pages = await ClickRecordService.GetPagesAsync<DateTime, AdvertisementClickRecordViewModel>(page, size, where, e => e.Time, false);
-            return Ok(pages);
-        }
+    /// <summary>
+    /// 删除广告
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpPost("{id}"), HttpGet("{id}"), MyAuthorize]
+    public async Task<IActionResult> Delete(int id)
+    {
+        bool b = await AdsService.DeleteByIdAsync(id) > 0;
+        return ResultData(null, b, b ? "删除成功" : "删除失败");
+    }
 
-        /// <summary>
-        /// 导出广告访问记录
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="page"></param>
-        /// <param name="size"></param>
-        /// <returns></returns>
-        [HttpGet("/partner/{id}/records-export"), MyAuthorize]
-        public IActionResult ExportClickRecords(int id)
-        {
-            using var list = ClickRecordService.GetQuery<DateTime, AdvertisementClickRecordViewModel>(e => e.AdvertisementId == id, e => e.Time, false).ToPooledList();
-            using var ms = list.ToDataTable().ToExcel();
-            var advertisement = AdsService[id];
-            return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, advertisement.Title + "访问记录.xlsx");
-        }
+    /// <summary>
+    /// 广告上下架
+    /// </summary>
+    /// <param name="id">文章id</param>
+    /// <returns></returns>
+    [MyAuthorize, HttpPost("{id}")]
+    public async Task<ActionResult> ChangeState(int id)
+    {
+        var ad = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告不存在!");
+        ad.Status = ad.Status == Status.Available ? Status.Unavailable : Status.Available;
+        return ResultData(null, await AdsService.SaveChangesAsync() > 0, ad.Status == Status.Available ? $"【{ad.Title}】已上架!" : $"【{ad.Title}】已下架!");
+    }
 
-        /// <summary>
-        /// 广告访问记录图表
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="cancellationToken"></param>
-        /// <returns></returns>
-        [HttpGet("/partner/{id}/records-chart"), MyAuthorize]
-        [ProducesResponseType((int)HttpStatusCode.OK)]
-        public async Task<IActionResult> ClickRecordsChart(int id, CancellationToken cancellationToken)
+    /// <summary>
+    /// 随机前往一个广告
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("/partner-random")]
+    public async Task<ActionResult> RandomGo()
+    {
+        var ad = AdsService.GetByWeightedPrice((AdvertiseType)new Random().Next(1, 4), Request.Location());
+        if (!Request.IsRobot() && string.IsNullOrEmpty(HttpContext.Session.Get<string>("ads" + ad.Id)))
         {
-            var list = await ClickRecordService.GetQuery(e => e.AdvertisementId == id).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+            HttpContext.Session.Set("ads" + ad.Id, ad.Id.ToString());
+            ad.ClickRecords.Add(new AdvertisementClickRecord()
             {
-                Date = g.Key,
-                Count = g.Count()
-            }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
-            return Ok(list);
+                IP = ClientIP,
+                Location = ClientIP.GetIPLocation(),
+                Referer = Request.Headers[HeaderNames.Referer].ToString(),
+                Time = DateTime.Now
+            });
+            await AdsService.SaveChangesAsync();
+            var start = DateTime.Today.AddMonths(-1);
+            await ClickRecordService.GetQuery(a => a.Time < start).DeleteFromQueryAsync();
         }
 
-        /// <summary>
-        /// 广告访问记录分析
-        /// </summary>
-        /// <param name="id"></param>
-        /// <returns></returns>
-        [HttpGet("/partner/{id}/insight"), MyAuthorize]
-        public IActionResult ClickRecordsInsight(int id)
+        return Redirect(ad.Url);
+    }
+
+    /// <summary>
+    /// 广告访问记录
+    /// </summary>
+    /// <param name="id"></param>
+    /// <param name="page"></param>
+    /// <param name="size"></param>
+    /// <returns></returns>
+    [HttpGet("/partner/{id}/records"), MyAuthorize]
+    public async Task<IActionResult> ClickRecords(int id, int page = 1, int size = 15, string kw = "")
+    {
+        Expression<Func<AdvertisementClickRecord, bool>> where = e => e.AdvertisementId == id;
+        if (!string.IsNullOrEmpty(kw))
         {
-            return View(AdsService[id]);
+            kw = Regex.Escape(kw);
+            where = where.And(e => Regex.IsMatch(e.IP + e.Location + e.Referer, kw));
         }
 
-        /// <summary>
-        /// 设置分类
-        /// </summary>
-        /// <param name="id"></param>
-        /// <param name="cids"></param>
-        /// <returns></returns>
-        /// <exception cref="NotFoundException"></exception>
-        [HttpPost("/partner/{id}/categories")]
-        public async Task<ActionResult> SetCategories(int id, [FromBodyOrDefault] string cids)
+        var pages = await ClickRecordService.GetPagesAsync<DateTime, AdvertisementClickRecordViewModel>(page, size, where, e => e.Time, false);
+        return Ok(pages);
+    }
+
+    /// <summary>
+    /// 导出广告访问记录
+    /// </summary>
+    /// <param name="id"></param>
+    /// <param name="page"></param>
+    /// <param name="size"></param>
+    /// <returns></returns>
+    [HttpGet("/partner/{id}/records-export"), MyAuthorize]
+    public IActionResult ExportClickRecords(int id)
+    {
+        using var list = ClickRecordService.GetQuery<DateTime, AdvertisementClickRecordViewModel>(e => e.AdvertisementId == id, e => e.Time, false).ToPooledList();
+        using var ms = list.ToDataTable().ToExcel();
+        var advertisement = AdsService[id];
+        return this.ResumeFile(ms.ToArray(), ContentType.Xlsx, advertisement.Title + "访问记录.xlsx");
+    }
+
+    /// <summary>
+    /// 广告访问记录图表
+    /// </summary>
+    /// <param name="id"></param>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    [HttpGet("/partner/{id}/records-chart"), MyAuthorize]
+    [ProducesResponseType((int)HttpStatusCode.OK)]
+    public async Task<IActionResult> ClickRecordsChart(int id, CancellationToken cancellationToken)
+    {
+        var list = await ClickRecordService.GetQuery(e => e.AdvertisementId == id).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
         {
-            var entity = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告未找到");
-            entity.CategoryIds = cids;
-            await AdsService.SaveChangesAsync();
-            return Ok();
-        }
+            Date = g.Key,
+            Count = g.Count()
+        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+        return Ok(list);
+    }
+
+    /// <summary>
+    /// 广告访问记录图表
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    [HttpGet("/partner/records-chart"), MyAuthorize]
+    [ProducesResponseType((int)HttpStatusCode.OK)]
+    public async Task<IActionResult> ClickRecordsChart(CancellationToken cancellationToken)
+    {
+        var start = DateTime.Now.AddMonths(-1);
+        var list = await ClickRecordService.GetQuery(e => e.Time >= start).Select(e => e.Time).GroupBy(t => t.Date).Select(g => new
+        {
+            Date = g.Key,
+            Count = g.Count()
+        }).OrderBy(a => a.Date).ToListAsync(cancellationToken);
+        return Ok(list);
+    }
+
+    /// <summary>
+    /// 广告访问记录分析
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("/partner/{id}/insight"), MyAuthorize]
+    public IActionResult ClickRecordsInsight(int id)
+    {
+        return View(AdsService[id]);
+    }
+
+    /// <summary>
+    /// 设置分类
+    /// </summary>
+    /// <param name="id"></param>
+    /// <param name="cids"></param>
+    /// <returns></returns>
+    /// <exception cref="NotFoundException"></exception>
+    [HttpPost("/partner/{id}/categories")]
+    public async Task<ActionResult> SetCategories(int id, [FromBodyOrDefault] string cids)
+    {
+        var entity = await AdsService.GetByIdAsync(id) ?? throw new NotFoundException("广告未找到");
+        entity.CategoryIds = cids;
+        await AdsService.SaveChangesAsync();
+        return Ok();
     }
-}
+}

+ 66 - 67
src/Masuit.MyBlogs.Core/Infrastructure/Services/AdvertisementService.cs

@@ -13,86 +13,85 @@ using System.Linq.Expressions;
 using System.Text.RegularExpressions;
 using Z.EntityFramework.Plus;
 
-namespace Masuit.MyBlogs.Core.Infrastructure.Services
+namespace Masuit.MyBlogs.Core.Infrastructure.Services;
+
+public partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
 {
-    public partial class AdvertisementService : BaseService<Advertisement>, IAdvertisementService
-    {
-        public ICacheManager<List<Advertisement>> CacheManager { get; set; }
+    public ICacheManager<List<Advertisement>> CacheManager { get; set; }
 
-        public ICategoryRepository CategoryRepository { get; set; }
+    public ICategoryRepository CategoryRepository { get; set; }
 
-        private readonly ILuceneIndexSearcher _luceneIndexSearcher;
+    private readonly ILuceneIndexSearcher _luceneIndexSearcher;
 
-        public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
-        {
-            _luceneIndexSearcher = searcher;
-        }
+    public AdvertisementService(IBaseRepository<Advertisement> repository, ISearchEngine<DataContext> searchEngine, ILuceneIndexSearcher searcher) : base(repository, searchEngine, searcher)
+    {
+        _luceneIndexSearcher = searcher;
+    }
 
-        /// <summary>
-        /// 按价格随机筛选一个元素
-        /// </summary>
-        /// <param name="type">广告类型</param>
-        /// <param name="location"></param>
-        /// <param name="cid">分类id</param>
-        /// <param name="keywords"></param>
-        /// <returns></returns>
-        public Advertisement GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
-        {
-            return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
-        }
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="type">广告类型</param>
+    /// <param name="location"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public Advertisement GetByWeightedPrice(AdvertiseType type, IPLocation location, int? cid = null, string keywords = "")
+    {
+        return GetsByWeightedPrice(1, type, location, cid, keywords).FirstOrDefault();
+    }
 
-        /// <summary>
-        /// 按价格随机筛选一个元素
-        /// </summary>
-        /// <param name="count">数量</param>
-        /// <param name="type">广告类型</param>
-        /// <param name="ipinfo"></param>
-        /// <param name="cid">分类id</param>
-        /// <param name="keywords"></param>
-        /// <returns></returns>
-        public List<Advertisement> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+    /// <summary>
+    /// 按价格随机筛选一个元素
+    /// </summary>
+    /// <param name="count">数量</param>
+    /// <param name="type">广告类型</param>
+    /// <param name="ipinfo"></param>
+    /// <param name="cid">分类id</param>
+    /// <param name="keywords"></param>
+    /// <returns></returns>
+    public List<Advertisement> GetsByWeightedPrice(int count, AdvertiseType type, IPLocation ipinfo, int? cid = null, string keywords = "")
+    {
+        var (location, _, _) = ipinfo;
+        return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
         {
-            var (location, _, _) = ipinfo;
-            return CacheManager.GetOrAdd($"Advertisement:{location.Crc32()}:{type}:{count}-{cid}-{keywords}", _ =>
+            var atype = type.ToString("D");
+            Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
+            var catCount = CategoryRepository.Count(_ => true);
+            where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
+            if (cid.HasValue)
             {
-                var atype = type.ToString("D");
-                Expression<Func<Advertisement, bool>> where = a => a.Types.Contains(atype) && a.Status == Status.Available;
-                var catCount = CategoryRepository.Count(_ => true);
-                where = where.And(a => a.RegionMode == RegionLimitMode.All || (a.RegionMode == RegionLimitMode.AllowRegion ? Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase) : !Regex.IsMatch(location, a.Regions, RegexOptions.IgnoreCase)));
-                if (cid.HasValue)
+                var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => c.ParentId + "|" + c.Parent.ParentId).FromCache(new MemoryCacheEntryOptions()
                 {
-                    var pids = CategoryRepository.GetQuery(c => c.Id == cid).Select(c => c.ParentId + "|" + c.Parent.ParentId).FromCache(new MemoryCacheEntryOptions()
-                    {
-                        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(5)
-                    }).ToArray();
-                    var scid = pids.Select(s => s.Trim('|')).Where(s => !string.IsNullOrEmpty(s)).Append(cid + "").Join("|");
-                    if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
-                    {
-                        where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
-                    }
-                }
-
-                if (!keywords.IsNullOrEmpty())
+                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(5)
+                }).ToArray();
+                var scid = pids.Select(s => s.Trim('|')).Where(s => !string.IsNullOrEmpty(s)).Append(cid + "").Join("|");
+                if (Any(a => Regex.IsMatch(a.CategoryIds, scid)))
                 {
-                    var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
-                    where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
+                    where = where.And(a => Regex.IsMatch(a.CategoryIds, scid) || string.IsNullOrEmpty(a.CategoryIds));
                 }
+            }
 
-                var array = GetQuery(a => a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
-                var list = GetQuery(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).ToList();
-                if (list.Count == 0 && keywords is { Length: > 0 })
-                {
-                    return GetsByWeightedPrice(count, type, ipinfo, cid);
-                }
+            if (!keywords.IsNullOrEmpty())
+            {
+                var regex = _luceneIndexSearcher.CutKeywords(keywords).Select(Regex.Escape).Join("|");
+                where = where.And(a => Regex.IsMatch(a.Title + a.Description, regex));
+            }
 
-                var ids = list.Select(a => a.Id).ToArray();
-                GetQuery(a => ids.Contains(a.Id)).UpdateFromQuery(a => new Advertisement
-                {
-                    DisplayCount = a.DisplayCount + 1
-                });
+            var array = GetQuery(a => a.Status == Status.Available).GroupBy(a => a.Merchant).Select(g => g.OrderBy(_ => EF.Functions.Random()).FirstOrDefault().Id).Take(50).ToArray();
+            var list = GetQuery(where).Where(a => array.Contains(a.Id)).OrderBy(a => -Math.Log(EF.Functions.Random()) / ((double)a.Price / a.Types.Length * catCount / (string.IsNullOrEmpty(a.CategoryIds) ? catCount : (a.CategoryIds.Length + 1)))).Take(count).ToList();
+            if (list.Count == 0 && keywords is { Length: > 0 })
+            {
+                return GetsByWeightedPrice(count, type, ipinfo, cid);
+            }
 
-                return list;
+            var ids = list.Select(a => a.Id).ToArray();
+            GetQuery(a => ids.Contains(a.Id)).UpdateFromQuery(a => new Advertisement
+            {
+                DisplayCount = a.DisplayCount + 1
             });
-        }
+
+            return list;
+        });
     }
-}
+}

+ 1 - 0
src/Masuit.MyBlogs.Core/wwwroot/ng-views/app/route.config.js

@@ -132,6 +132,7 @@ myApp.config([
                         return $ocLazyLoad.load([{
                                 files: [
                                     "https://maplemei.gitee.io/xm-select/xm-select.js",
+                                    "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js",
                                 ],
                                 cache: true
                             }, cpath + "/partner.js"

+ 61 - 0
src/Masuit.MyBlogs.Core/wwwroot/ng-views/controllers/partner.js

@@ -416,4 +416,65 @@
             content: '/partner/' + row.Id + '/insight'
         }));
     }
+    
+    window.fetch("/partner/records-chart", {
+        credentials: 'include',
+        method: 'GET',
+        mode: 'cors'
+    }).then(function(response) {
+        return response.json();
+    }).then(function(res) {
+        var data = [];
+        for (let item of res) {
+            data.push([Date.parse(item.Date), item.Count]);
+        }
+        var chartDom = document.getElementById('chart');
+        var myChart = echarts.init(chartDom);
+        var option = {
+            tooltip: {
+                trigger: 'axis',
+                position: function(pt) {
+                    return [pt[0], '10%'];
+                }
+            },
+            title: {
+                left: 'center',
+                text: '最近30天每日总点击趋势,日均:' + (res.reduce((acr, cur) => acr + cur.Count, 0) / (new Date() - new Date(res[0].Date)) * (1000 * 60 * 60 * 24)).toFixed(2)
+            },
+            xAxis: {
+                type: 'time',
+                axisLabel: {
+                    formatter:function (value){
+                        var dt=new Date(value);
+                        return dt.toLocaleDateString();
+                    }
+                }
+            },
+            yAxis: {
+                type: 'value'
+            },
+            series: [
+                {
+                    name: '点击量',
+                    type: 'line',
+                    smooth: true,
+                    symbol: 'none',
+                    areaStyle: {},
+                    data: data,
+                    markPoint: {
+                        data: [
+                            { type: 'max', name: '最大值' },
+                            { type: 'min', name: '最小值' }
+                        ]
+                    },
+                    markLine: {
+                        data: [
+                            { type: 'average', name: '平均值' }
+                        ]
+                    }
+                }
+            ]
+        };
+        myChart.setOption(option);
+    });
 }]);

+ 28 - 21
src/Masuit.MyBlogs.Core/wwwroot/ng-views/views/partner.html

@@ -3,6 +3,7 @@
         max-height: 800px;
         overflow-y: auto;
     }
+
     td .xm-body {
         position: unset
     }
@@ -159,41 +160,41 @@
                 <div class="form-group">
                     <div class="input-group">
                         <label for="desc" class="input-group-addon control-label">当前竞价:</label>
-                            <input type="number" class="form-control" id="price" placeholder="竞价" ng-model="partner.Price">
+                        <input type="number" class="form-control" id="price" placeholder="竞价" ng-model="partner.Price">
                     </div>
                 </div>
                 <div class="input-group">
                     <label for="desc" class="input-group-addon control-label">过期时间:</label>
                     <div class="fg-line">
-                        <input type="text" class="form-control" id="timespan" ng-model="partner.ExpireTime"/>
+                        <input type="text" class="form-control" id="timespan" ng-model="partner.ExpireTime" />
                     </div>
                 </div>
             </div>
-                <div class="form-group form-inline">
-                    <div class="input-group">
-                        <label for="region" class="input-group-addon control-label">投放地区:</label>
-                        <div class="fg-line">
-                            <input type="text" class="form-control" id="region" placeholder="竖线分隔,支持国家、地区、城市、运营商、ASN" ng-model="partner.Regions" style="width: 420px">
-                            <div class="form-control" id="regionMode" style="width: 100px"></div>
-                        </div>
+            <div class="form-group form-inline">
+                <div class="input-group">
+                    <label for="region" class="input-group-addon control-label">投放地区:</label>
+                    <div class="fg-line">
+                        <input type="text" class="form-control" id="region" placeholder="竖线分隔,支持国家、地区、城市、运营商、ASN" ng-model="partner.Regions" style="width: 420px">
+                        <div class="form-control" id="regionMode" style="width: 100px"></div>
                     </div>
                 </div>
-                <div class="form-group">
-                    <div class="col-xs-12">
-                        <div class="fg-line">
-                            <div class="btn-group">
-                                <button type="button" class="btn btn-info" ng-click="submit(partner)">
-                                    保存
-                                </button>
-                                <button type="button" class="btn btn-danger" ng-click="closeAll()">
-                                    取消
-                                </button>
-                            </div>
+            </div>
+            <div class="form-group">
+                <div class="col-xs-12">
+                    <div class="fg-line">
+                        <div class="btn-group">
+                            <button type="button" class="btn btn-info" ng-click="submit(partner)">
+                                保存
+                            </button>
+                            <button type="button" class="btn btn-danger" ng-click="closeAll()">
+                                取消
+                            </button>
                         </div>
                     </div>
                 </div>
             </div>
         </div>
+    </div>
 </div>
 <div id="detail" class="modal">
     <div class="container-fluid" style="margin: 15px 0;">
@@ -281,7 +282,7 @@
                 <span class="input-group-btn hidden">
                     <a href="javascript:void(0);" class="btn btn-info ">浏览</a>
                 </span>
-                <input type="file" class="uploadFile" name="file" onchange="getFile(this, 'file0')" style="width: 200px"/>
+                <input type="file" class="uploadFile" name="file" onchange="getFile(this, 'file0')" style="width: 200px" />
                 <span class="input-group-btn">
                     <button type="button" class="btn btn-primary" ng-click="uploadImage(imgField)">开始上传</button>
                 </span>
@@ -301,4 +302,10 @@
             </span>
         </div>
     </div>
+</div>
+
+<div class="row">
+    <div class="col-md-12">
+        <div id="chart" style="height: 500px"></div>
+    </div>
 </div>