Files
Docker/CloudBuilder/CloudBuilder.Master.Service/ArticleService.auto.cs
T
2026-10-22 13:55:27 +08:00

424 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CloudBuilder.Core.DatabaseAccessor;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.Extensions;
using CloudBuilder.Core.Policy;
using CloudBuilder.Core.Service;
using CloudBuilder.Master.Data;
using CloudBuilder.Master.Entity;
using CloudBuilder.Master.Policy;
using CloudBuilder.Master.SearchCriteria;
using CloudBuilder.Security.Entity;
using CloudBuilder.Security.Policy;
using CloudBuilder.Security.Service;
using CloudBuilder.Security.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
namespace CloudBuilder.Master.Service
{
/// <summary>
/// 有家文化传承(文章)服务:主表 article + 群组关联 article_family_group + 配图明细 article_filename。
/// 可见性:仅文章关联族谱群组的正式成员可见;未审核(approved_indc&lt;&gt;Y)仅作者本人可见。
/// 作者中文名/头像按 created_by join security_user 回填;封面与配图存物理名、按上传目录打包访问地址。
/// </summary>
public class ArticleService : IArticleService
{
private readonly IApplicationService service;
private IRepository<ArticleEntity> repository;
public ArticleService(IApplicationService service)
{
this.service = service;
repository = service.GetRepository<IRepository<ArticleEntity>>();
}
[PermissionOperate(name: MasterPermissionPolicy.Article, operate: Operate.Find)]
public ArticleData FindByKey(string key)
{
ArticleData data = new ArticleData();
if (string.IsNullOrEmpty(key)) return data;
ArticleEntity article = repository.DetachedEntities.Where(x => x.ArticleName == key).FirstOrDefault();
if (article == null) return data;
IRepository<ArticleFamilyGroupEntity> repositoryArticleFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
IRepository<FamilyGroupEntity> repositoryFamilyGroup = repository.GetRepository<IRepository<FamilyGroupEntity>>();
IQueryable<ArticleFamilyGroupDisplayData> q = from e in repositoryArticleFamilyGroup.DetachedEntities
join fg in repositoryFamilyGroup.DetachedEntities on e.FamilyName equals fg.FamilyName
where e.ArticleName == key
select new ArticleFamilyGroupDisplayData(fg)
{
CreatedBy = e.CreatedBy,
CreatedDatetime = e.CreatedDatetime,
UpdatedBy = e.UpdatedBy,
UpdatedDatetime = e.UpdatedDatetime,
ArticleName = e.ArticleName,
};
ArticleFamilyGroupDisplayData[] familyGroups = q.ToArray();
data.FamilyGroups = familyGroups;
// 归属与群组身份:作者全权;群成员可读;群管理员可改信息/群组/审核/置顶
string[] familyNames = familyGroups.Select(x => x.FamilyName).ToArray();
bool isOwner = article.CreatedBy == service.UserId;
bool isMember = isOwner;
bool isManager = isOwner;
if (!isOwner && familyNames.Length > 0)
{
IRepository<FamilyGroupManagerEntity> repositoryFamilyGroupManager = repository.GetRepository<IRepository<FamilyGroupManagerEntity>>();
FamilyGroupManagerEntity[] mine = repositoryFamilyGroupManager.DetachedEntities
.Where(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES && familyNames.Contains(x.FamilyName))
.ToArray();
isMember = mine.Length > 0;
isManager = mine.Any(x => x.ManagerIndc == YesNoPolicy.YES);
}
// 群组成员可见性闸门 + 未审核仅作者可见
if (!isOwner && !isMember)
{
throw new ValidatedException("没有权限.");
}
if (!isOwner && !YesNoPolicy.IsYes(article.ApprovedIndc))
{
throw new ValidatedException("文章尚未审核通过.");
}
ArticleDisplayData display = new ArticleDisplayData(article)
{
IsOwner = isOwner,
IsMember = isMember,
IsManager = isManager,
CoverUrl = PackCover(article.ArticleCoverFilename),
};
FillAuthor(display, article.CreatedBy);
data.Article = display;
IRepository<ArticleFilenameEntity> repositoryArticleFilename = repository.GetRepository<IRepository<ArticleFilenameEntity>>();
data.Filenames = repositoryArticleFilename.DetachedEntities
.Where(x => x.ArticleName == key)
.OrderBy(x => x.FileId)
.Select(x => new ArticleFilenameDisplayData(x))
.ToArray();
foreach (ArticleFilenameDisplayData f in data.Filenames)
{
f.ImageUrl = PackImage(f.FileName);
}
return data;
}
[PermissionOperate(name: MasterPermissionPolicy.Article, operate: Operate.Find)]
public ArticleResultData FindBySearchCriteria(ArticleSearchCriteria criteria)
{
ArticleResultData data = new ArticleResultData();
string userId = service.UserId;
// 我加入的群组(正式成员)
IRepository<FamilyGroupManagerEntity> repositoryFamilyGroupManager = repository.GetRepository<IRepository<FamilyGroupManagerEntity>>();
string[] myGroups = repositoryFamilyGroupManager.DetachedEntities
.Where(x => x.UserName == userId && x.ValidIndc == YesNoPolicy.YES)
.Select(x => x.FamilyName).Distinct().ToArray();
// 这些群组下挂的文章编号
IRepository<ArticleFamilyGroupEntity> repositoryArticleFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
string[] articleNamesInMyGroups = myGroups.Length == 0
? new string[0]
: repositoryArticleFamilyGroup.DetachedEntities
.Where(x => myGroups.Contains(x.FamilyName))
.Select(x => x.ArticleName).Distinct().ToArray();
IQueryable<ArticleEntity> query = repository.DetachedEntities
.Where(x => x.CreatedBy == userId || articleNamesInMyGroups.Contains(x.ArticleName))
.Where(x => x.ApprovedIndc == YesNoPolicy.YES || x.CreatedBy == userId);
// 关键字:命中 ArticleTitle 的过滤器取标题模糊(首页搜索用)
string keyword = criteria.Filter?
.FirstOrDefault(f => f.Column == ArticleEntity.ARTICLE_TITLE || f.Column == ArticleEntity.DB_ARTICLE_TITLE)?.Value;
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
query = query.Where(x => x.ArticleTitle.Contains(keyword));
}
// 排序:置顶优先,其次 OrderBy 升序(CloudBuilder 约定值小者靠前),最后按创建时间倒序兜底
IOrderedQueryable<ArticleEntity> ordered = query
.OrderByDescending(x => x.TopIndc == YesNoPolicy.YES)
.ThenBy(x => x.OrderBy)
.ThenByDescending(x => x.CreatedDatetime);
data.Total = ordered.Count();
int limit = criteria.limit > 0 ? criteria.limit : 20;
int page = criteria.page > 0 ? criteria.page : 1;
ArticleEntity[] slice = ordered.Skip((page - 1) * limit).Take(limit).ToArray();
ArticleDisplayData[] list = slice.Select(x => new ArticleDisplayData(x)
{
CoverUrl = PackCover(x.ArticleCoverFilename),
}).ToArray();
FillAuthors(list);
data.Limit = limit;
data.Page = page;
data.Datas = list;
return data;
}
[PermissionOperate(name: MasterPermissionPolicy.Article, operate: Operate.Insert)]
[UnitOfWork]
public ArticleData Insert(ArticleData data)
{
if (data == null || data.Article == null)
{
throw new ValidatedException("文章内容为空.");
}
Validate(data.Article);
ArticleEntity article = new ArticleEntity();
ObjectCopyHelper.Copy(data.Article, article);
ISecurityGeneratorNumberService securityGeneratorNumberService = service.ServiceProvider.GetService<ISecurityGeneratorNumberService>()!;
article.ArticleName = securityGeneratorNumberService.GetNo(MasterPrefixPolicy.ARTICLE, MasterPrefixPolicy.ARTICLE_ARTICLE_NAME, MasterPrefixPolicy.ARTICLE_PREFIX, YesNoPolicy.YES, MasterPrefixPolicy.ARTICLE_FORMAT);
article.CreatedBy = service.UserId;
article.UpdatedBy = service.UserId;
article.CreatedDatetime = DateTime.Now;
article.UpdatedDatetime = article.CreatedDatetime;
// 新文章默认未审核、不置顶(审核由作者/管理员在详情页开关控制)
if (string.IsNullOrEmpty(article.ApprovedIndc)) article.ApprovedIndc = YesNoPolicy.NO;
if (string.IsNullOrEmpty(article.TopIndc)) article.TopIndc = YesNoPolicy.NO;
repository.InsertNow(article);
// 先主表再子表,避免子表关联不到主键
SaveArticleFamilyGroup(article.ArticleName, data.FamilyGroups);
SaveArticleFilenames(article.ArticleName, data.Filenames);
return FindByKey(article.ArticleName);
}
[PermissionOperate(name: MasterPermissionPolicy.Article, operate: Operate.Update)]
[UnitOfWork]
public ArticleData Update(ArticleData data)
{
if (data == null || data.Article == null)
{
throw new ValidatedException("文章内容为空.");
}
ArticleEntity original = repository.DetachedEntities.Where(x => x.ArticleName == data.Article.ArticleName).FirstOrDefault();
if (original == null)
{
throw new ValidatedException("记录不存在.");
}
IRepository<ArticleFamilyGroupEntity> repositoryArticleFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
IRepository<FamilyGroupManagerEntity> repositoryFamilyGroupManager = repository.GetRepository<IRepository<FamilyGroupManagerEntity>>();
bool isOwner = original.CreatedBy == service.UserId;
bool isManager = false;
bool isMember = false;
if (!isOwner)
{
string[] familyNames = repositoryArticleFamilyGroup.DetachedEntities
.Where(x => x.ArticleName == original.ArticleName)
.Select(x => x.FamilyName)
.ToArray();
if (familyNames.Length == 0)
{
throw new ValidatedException("没有权限.");
}
FamilyGroupManagerEntity[] mine = repositoryFamilyGroupManager.DetachedEntities
.Where(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES && familyNames.Contains(x.FamilyName))
.ToArray();
isMember = mine.Length > 0;
isManager = mine.Any(x => x.ManagerIndc == YesNoPolicy.YES);
if (!isMember)
{
throw new ValidatedException("没有权限.");
}
}
// 非作者保留归属与建单信息,防客户端伪造
data.Article.CreatedBy = original.CreatedBy;
data.Article.CreatedDatetime = original.CreatedDatetime;
if (!isOwner && !isManager)
{
// 普通群成员不可改正文/封面/审核/置顶:一律以库中为准
data.Article.ArticleTitle = original.ArticleTitle;
data.Article.Brief = original.Brief;
data.Article.ArticleCoverFilename = original.ArticleCoverFilename;
data.Article.ApprovedIndc = original.ApprovedIndc;
data.Article.TopIndc = original.TopIndc;
data.Article.OrderBy = original.OrderBy;
}
ValidateHelper.Validate(service, data.Article, ArticleEntity.DB_NAME_ARTICLE);
ArticleEntity article = new ArticleEntity();
ObjectCopyHelper.Copy(data.Article, article);
article.UpdatedBy = service.UserId;
article.UpdatedDatetime = DateTime.Now;
repository.UpdateNow(article);
// 群组增删与配图只对作者/群管理员开放
if (isOwner || isManager)
{
SaveArticleFamilyGroup(article.ArticleName, data.FamilyGroups);
SaveArticleFilenames(article.ArticleName, data.Filenames);
}
return FindByKey(article.ArticleName);
}
[PermissionOperate(name: MasterPermissionPolicy.Article, operate: Operate.Delete)]
[UnitOfWork]
public void Delete(string key)
{
ArticleEntity article = repository.DetachedEntities.Where(x => x.ArticleName == key).FirstOrDefault();
if (article == null) return;
// 删除:作者或关联群组管理员
if (article.CreatedBy != service.UserId && !IsGroupManagerOfArticle(key))
{
throw new ValidatedException("没有权限.");
}
repository.DeleteNow(article);
IRepository<ArticleFamilyGroupEntity> repositoryFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
ArticleFamilyGroupEntity[] groups = repositoryFamilyGroup.Entities.Where(x => x.ArticleName == key).ToArray();
foreach (ArticleFamilyGroupEntity gp in groups)
{
repositoryFamilyGroup.DeleteNow(gp);
}
IRepository<ArticleFilenameEntity> repositoryFilename = repository.GetRepository<IRepository<ArticleFilenameEntity>>();
ArticleFilenameEntity[] files = repositoryFilename.Entities.Where(x => x.ArticleName == key).ToArray();
foreach (ArticleFilenameEntity f in files)
{
repositoryFilename.DeleteNow(f);
}
}
/// <summary>当前登录人是否为该文章任一关联群组的管理员(正式成员且 ManagerIndc=Y)</summary>
private bool IsGroupManagerOfArticle(string articleName)
{
IRepository<ArticleFamilyGroupEntity> repositoryArticleFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
IRepository<FamilyGroupManagerEntity> repositoryFamilyGroupManager = repository.GetRepository<IRepository<FamilyGroupManagerEntity>>();
string[] familyNames = repositoryArticleFamilyGroup.DetachedEntities
.Where(x => x.ArticleName == articleName)
.Select(x => x.FamilyName)
.ToArray();
if (familyNames.Length == 0) return false;
return repositoryFamilyGroupManager.DetachedEntities
.Any(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES && x.ManagerIndc == YesNoPolicy.YES && familyNames.Contains(x.FamilyName));
}
private void SaveArticleFamilyGroup(string articleName, ArticleFamilyGroupEntity[] familyGroups)
{
IRepository<ArticleFamilyGroupEntity> repositoryArticleFamilyGroup = repository.GetRepository<IRepository<ArticleFamilyGroupEntity>>();
ArticleFamilyGroupEntity[] olds = repositoryArticleFamilyGroup.Entities.Where(x => x.ArticleName == articleName).ToArray();
foreach (ArticleFamilyGroupEntity old in olds)
{
if (familyGroups == null || familyGroups.Length == 0 || familyGroups.Count(x => x.FamilyName == old.FamilyName) == 0)
{
repositoryArticleFamilyGroup.DeleteNow(old);
}
}
if (familyGroups != null)
{
foreach (ArticleFamilyGroupEntity dto in familyGroups)
{
if (olds.Any(x => x.FamilyName == dto.FamilyName)) continue;
repositoryArticleFamilyGroup.InsertNow(new ArticleFamilyGroupEntity()
{
ArticleName = articleName,
FamilyName = dto.FamilyName,
});
}
}
}
private void SaveArticleFilenames(string articleName, ArticleFilenameEntity[] filenames)
{
IRepository<ArticleFilenameEntity> repositoryArticleFilename = repository.GetRepository<IRepository<ArticleFilenameEntity>>();
ArticleFilenameEntity[] olds = repositoryArticleFilename.Entities.Where(x => x.ArticleName == articleName).ToArray();
foreach (ArticleFilenameEntity old in olds)
{
if (filenames == null || filenames.Length == 0 || filenames.Count(x => x.FileName == old.FileName) == 0)
{
repositoryArticleFilename.DeleteNow(old);
}
}
if (filenames != null)
{
foreach (ArticleFilenameEntity dto in filenames)
{
if (olds.Any(x => x.FileName == dto.FileName)) continue;
repositoryArticleFilename.InsertNow(new ArticleFilenameEntity()
{
ArticleName = articleName,
FileName = dto.FileName,
});
}
}
}
private void Validate(ArticleEntity article)
{
List<SysTableSchemaViewEntity> sysTableSchemas = service.ServiceProvider.GetRequiredService<ISysTableSchemaViewService>().FindByTableName(ArticleEntity.DB_NAME_ARTICLE);
ValidateHelper.ValidateString(article, sysTableSchemas.ToArray());
}
private string PackCover(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return "";
return FileTypePolicy.PackAvatarUrl(service, UploadFolderPolicy.ARTICLE_COVER, fileName) ?? "";
}
private string PackImage(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return "";
return FileTypePolicy.PackAvatarUrl(service, UploadFolderPolicy.ARTICLE_IMAGE, fileName) ?? "";
}
/// <summary>按用户名 join security_user 回填单个作者的中文名与头像。</summary>
private void FillAuthor(ArticleDisplayData display, string userName)
{
if (display == null || string.IsNullOrEmpty(userName)) return;
IRepository<SecurityUserEntity> repositoryUser = repository.GetRepository<IRepository<SecurityUserEntity>>();
SecurityUserEntity user = repositoryUser.DetachedEntities.Where(x => x.UserName == userName).FirstOrDefault();
display.AuthorLocalName = user?.UserLocalName ?? userName;
display.AuthorAvatarUrl = FileTypePolicy.PackAvatarUrl(service, UploadFolderPolicy.USER_AVATAR, user?.UserIconImgPath ?? "") ?? "";
}
/// <summary>批量 join:一次取回页内所有作者的中文名与头像,避免 N+1。</summary>
private void FillAuthors(ArticleDisplayData[] list)
{
if (list == null || list.Length == 0) return;
string[] names = list.Select(x => x.CreatedBy).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToArray();
if (names.Length == 0) return;
IRepository<SecurityUserEntity> repositoryUser = repository.GetRepository<IRepository<SecurityUserEntity>>();
SecurityUserEntity[] users = repositoryUser.DetachedEntities.Where(x => names.Contains(x.UserName)).ToArray();
foreach (ArticleDisplayData item in list)
{
SecurityUserEntity user = users.Where(y => y.UserName == item.CreatedBy).FirstOrDefault();
item.AuthorLocalName = user?.UserLocalName ?? item.CreatedBy;
item.AuthorAvatarUrl = FileTypePolicy.PackAvatarUrl(service, UploadFolderPolicy.USER_AVATAR, user?.UserIconImgPath ?? "") ?? "";
}
}
}
}