版本更新:V1.5.6

This commit is contained in:
liweiyi 2025-06-20 19:07:45 +08:00
parent e55f263d8f
commit 3b838ec2b5
300 changed files with 5609 additions and 2598 deletions

125
README.md
View File

@ -1,4 +1,4 @@
# ChestnutCMS v1.5.5
# ChestnutCMS v1.5.6
### 系统简介
@ -14,33 +14,34 @@ ChestnutCMS是前后端分离的企业级内容管理系统。项目基于[RuoYi
资讯站演示地址:<https://news.1000mz.com>会员演示账号xxx333@126.com / a123456
图片站演示地址PC端<http://tpz.1000mz.com> 移动端:<http://mtpz.1000mz.com>
图片站演示地址PC端<https://tpz.1000mz.com> 移动端:<https://mtpz.1000mz.com>
游戏站演示地址PC端<http://game.1000mz.com> 移动端:<http://mgame.1000mz.com>
游戏站演示地址PC端<https://game.1000mz.com> 移动端:<https://mgame.1000mz.com>
影视站演示地址PC端<http://movie.1000mz.com> 移动端:<http://movie.1000mz.com>
影视站演示地址PC端<https://movie.1000mz.com> 移动端:<https://movie.1000mz.com>
更多演示站访问:<https://www.1000mz.com/themes/>
### 开发环境
- OpenJDK 17
- Maven 3.8+
- MySQL 8.0+
- Redis 5.x+
- Maven 3.8
- MySQL 8.0
- Redis 7.0
- NodeJS 16.20.2
### 主要技术框架
| 技术框架 | 版本 | 应用说明 |
|-------------------|---------|---------|
| Spring Boot | 3.1.7 | 基础开发框架 |
| Spring Boot Admin | 3.1.7 | 监控框架 |
| Mybatis Plus | 3.5.5 | ORM框架 |
| Flyway | 9.22.3 | 数据库版本管理 |
| Yitter | 1.0.6 | 雪花ID |
| Redisson | 3.25.2 | 分布式锁 |
| Spring Boot | 3.4.5 | 基础开发框架 |
| Spring Boot Admin | 3.4.5 | 监控框架 |
| Mybatis Plus | 3.5.12 | ORM框架 |
| Flyway | 10.20.1 | 数据库版本管理 |
| Redisson | 3.46.0 | 分布式锁 |
| FreeMarker | 2.3.32 | 模板引擎 |
| Sa-Token | 1.37.0 | 权限认证 |
| Xxl-Job | 2.4.0 | 任务调度 |
| Lombok | 1.18.26 | 你懂的 |
| Sa-Token | 1.43.0 | 权限认证 |
| Xxl-Job | 3.1.0 | 任务调度 |
| Lombok | 1.18.38 | 你懂的 |
### 相关文档
@ -59,49 +60,55 @@ ChestnutCMS是前后端分离的企业级内容管理系统。项目基于[RuoYi
### 功能模块
| 模块 | 简介 |
|--------------|----------------------------------------|
| 站点管理 | 多站点,支持图片水印、标题查重、扩展模型等扩展配置 |
| 栏目管理 | 普通栏目+链接栏目,扩展配置优先级高于站点扩展配置 |
| 内容管理 | 内容类型:文章+图片集+音视频集,页面部件:动态自定义区块+广告,内容回收站 |
| 资源管理 | 图片、音视频等各类静态资源管理支持OSS/COS/MinIO对象存储 |
| 发布通道 | 支持多通道不同类型静态文件发布可同时发布到PC、H5html、json等 |
| 模板管理 | 静态化模板,支持在线编辑 |
| 模板指令 | FreeMarker自定义标签、模板函数及动态模板的参数及用法说明 |
| 文件管理 | 当前站点资源目录及发布通道静态化目录管理,支持文本在线编辑 |
| 扩展模型 | 站点、栏目及内容的动态模型扩展,系统默认数据表保存,支持自定义 |
| 词汇管理 | 热词、TAG词、敏感词、易错词 |
| 内容索引 | 默认支持ElasticSearch+IK创建内容索引支持标题内容全文检索 |
| 检索词库 | 自定义检索词库,支持扩展词和停用词动态扩展 |
| 检索日志 | 用户搜索的日志记录 |
| 友链管理 | 友情链接 |
| 广告管理 | 广告基于页面部件扩展的简单广告功能,支持权重及定时上下线,广告点击/展现统计 |
| 评论管理 | 基础功能模块 |
| 调查问卷 | 基础功能模块,默认支持文字类型单选、多选、输入、图片、富文本 |
| 自定义表单 | 基于元数据模块扩展,支持模板标签 |
| 会员管理 | 支持自定义会员等级,等级经验值来源动态配置 |
| 访问统计 | 对接百度统计API |
| 用户管理 | 后台用户管理,支持用户独立权限配置 |
| 机构管理 | 多级系统组织机构(公司、部门、小组) |
| 角色管理 | 支持按角色分配菜单权限、站点和栏目相关操作权限配置 |
| 岗位管理 | 配置系统用户所属担任职务 |
| 菜单管理 | 配置系统菜单,操作权限,按钮权限标识等 |
| 字典管理 | 对系统中经常使用的一些固定的数据进行维护,代码层面定义 |
| 参数管理 | 对系统动态配置常用参数,代码层面定义 |
| 通知公告 | 系统通知公告信息发布维护 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 国际化 | 为菜单等动态数据国际化配置提供基础支持,可覆盖后台代码配置 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 系统日志 | 统一日志管理,支持扩展 |
| 操作日志 | 系统操作日志扩展,记录操作参数、异常信息及请求耗时 |
| 登录日志 | 系统登录日志扩展,记录用户登录日志,包含登录异常 |
| 在线用户 | 当前系统中活跃用户状态监控,支持踢下线 |
| 任务调度 | 基于XXL-JOB的分布式任务调度 |
| 定时任务 | 基于Spring的TaskScheduler实现的单机定时任务 |
| 异步任务 | 异步任务状态查看,支持手动结束 |
| 服务监控 | 监视当前系统CPU、内存、磁盘、堆栈等相关信息 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等 |
| GroovyScript | 支持Groovy脚本在线执行 |
| 模块 | 简介 |
|--------------|----------------------------------------------|
| 站点管理 | 多站点,支持图片水印、标题查重、扩展模型等扩展配置 |
| 栏目管理 | 普通栏目+链接栏目,扩展配置优先级高于站点扩展配置 |
| 内容管理 | 内容类型:文章+图片集+音视频集,页面部件:动态自定义区块+广告,内容回收站 |
| 资源管理 | 图片、音视频等各类静态资源管理支持对象存储OSS/COS/MinIO/AmazonS3 |
| 发布通道 | 支持多通道不同类型静态文件发布可同时发布到PC、H5html、json等 |
| 模板管理 | 静态化模板,支持在线编辑 |
| 模板指令 | FreeMarker自定义标签、模板函数及动态模板的参数及用法说明自定义动态模板 |
| 文件管理 | 当前站点资源目录及发布通道静态化目录管理,支持文本在线编辑 |
| 扩展模型 | 站点、栏目及内容的动态模型扩展,系统默认数据表保存,支持自定义 |
| 词汇管理 | 热词、TAG词、敏感词、易错词支持文章编辑器敏感词/易错词检测,一键替换 |
| 内容索引 | 默认支持ElasticSearch+IK创建内容索引支持标题内容全文检索 |
| 检索词库 | 自定义检索词库,支持扩展词和停用词动态扩展 |
| 检索日志 | 用户搜索的日志记录 |
| 友链管理 | 友情链接 |
| 广告管理 | 广告基于页面部件扩展的简单广告功能,支持权重及定时上下线,广告点击/展现统计 |
| 评论管理 | 基础功能模块 |
| 调查问卷 | 基础功能模块,默认支持文字类型单选、多选、输入、图片、富文本 |
| 自定义表单 | 基于元数据模块扩展,支持模板标签 |
| 会员管理 | 支持自定义会员等级,等级经验值来源动态配置 |
| 访问统计 | 对接百度统计API |
| 用户管理 | 后台用户管理,支持用户独立权限配置、角色权限继承 |
| 机构管理 | 多级系统组织机构(公司、部门、小组) |
| 角色管理 | 支持按角色分配菜单权限、站点和栏目相关操作权限配置 |
| 岗位管理 | 配置系统用户所属担任职务 |
| 菜单管理 | 配置系统菜单,操作权限,按钮权限标识等 |
| 字典管理 | 对系统中经常使用的一些固定的数据进行维护,代码层面定义 |
| 参数管理 | 对系统动态配置常用参数,代码层面定义 |
| 通知公告 | 系统通知公告信息发布维护 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 国际化 | 菜单等动态数据国际化配置 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 系统日志 | 统一日志管理,支持扩展 |
| 操作日志 | 系统操作日志扩展,记录操作参数、异常信息及请求耗时 |
| 登录日志 | 系统登录日志扩展,记录用户登录日志,包含登录异常 |
| 在线用户 | 当前系统中活跃用户状态监控,支持踢下线 |
| 任务调度 | 基于XXL-JOB的分布式任务调度 |
| 定时任务 | 基于Spring的TaskScheduler实现的单机定时任务 |
| 异步任务 | 异步任务状态监控 |
| 服务监控 | 监视当前系统CPU、内存、磁盘、堆栈等相关信息 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等 |
| GroovyScript | 支持Groovy脚本在线执行 |
### 版权说明
「ChestnutCMS 栗子内容管理系统」软件著作权登记号2023SR1343023产品受到相关法律法规的保护开源不代表放弃版权
请务必仔细阅读[版权声明](https://www.1000mz.com/docs/others/668421333913669.shtml),避免不必要的版权纠纷,。
### QQ交流群

View File

@ -34,4 +34,4 @@ VOLUME ["/home/app/logs","/home/app/uploadPath","/home/app/wwwroot_release","/ho
EXPOSE $SERVER_PORT $NETTY_SOCKET_PORT
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]

View File

@ -3,7 +3,7 @@
<parent>
<artifactId>chestnut</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
@ -165,6 +165,9 @@
<configuration>
<!-- 如果没有该配置devtools不会生效 -->
<!-- <fork>true</fork> -->
<layers>
<enabled>true</enabled>
</layers>
</configuration>
<executions>
<execution>

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.5
version: 1.5.6
# 版权年份
copyrightYear: 2022-2024
system:
@ -13,8 +13,6 @@ chestnut:
demoMode: false
# 文件路径 示例( Windows配置D:/chestnut/uploadPathLinux配置 /home/chestnut/uploadPath
uploadPath: 'E:/dev/workspace_chestnut/uploadPath'
# 验证码类型 math 数组计算 char 字符验证
captchaType: math
member:
uploadPath: 'E:/dev/workspace_chestnut/_xy_member/'
cms:
@ -66,9 +64,9 @@ spring:
servlet:
multipart:
# 单个文件大小
max-file-size: 100MB
max-file-size: 200MB
# 设置总上传的文件大小
max-request-size: 100MB
max-request-size: 200MB
# 服务模块
devtools:
restart:

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.5
version: 1.5.6
# 版权年份
copyrightYear: 2022-2024
system:
@ -13,8 +13,6 @@ chestnut:
demoMode: true
# 文件路径 示例( Windows配置D:/chestnut/uploadPathLinux配置 /home/app/uploadPath
uploadPath: /home/app/uploadPath
# 验证码类型 math 数组计算 char 字符验证
captchaType: math
freemarker:
templateLoaderPath: /home/app/statics
cms:
@ -63,9 +61,9 @@ spring:
servlet:
multipart:
# 单个文件大小
max-file-size: 100MB
max-file-size: 200MB
# 设置总上传的文件大小
max-request-size: 100MB
max-request-size: 200MB
# 服务模块
devtools:
restart:

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.5
version: 1.5.6
# 版权年份
copyrightYear: 2022-2024
system:

View File

@ -0,0 +1,4 @@
ALTER TABLE cms_custom_form MODIFY COLUMN `templates` varchar(1000);
ALTER TABLE cms_page_widget MODIFY COLUMN `publish_pipe_code` varchar(50);
ALTER TABLE cms_page_widget ADD COLUMN `templates` varchar(1000) COMMENT '模板配置';

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-advertisement</artifactId>

View File

@ -25,7 +25,6 @@ import com.chestnut.common.security.anno.Priv;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.security.web.PageRequest;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IPageWidget;
@ -41,9 +40,9 @@ import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
import freemarker.template.TemplateException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@ -111,28 +110,30 @@ public class AdSpaceController extends BaseRestController {
}
@PostMapping
public R<?> addAdSpace(HttpServletRequest request) throws IOException {
PageWidgetAddDTO dto = JacksonUtils.from(request.getInputStream(), PageWidgetAddDTO.class);
public R<?> addAdSpace(@RequestBody @Validated PageWidgetAddDTO dto) {
dto.setType(pageWidgetType.getId());
CmsPageWidget cmsPageWdiget = new CmsPageWidget();
BeanUtils.copyProperties(dto, cmsPageWdiget);
CmsSite site = siteService.getCurrentSite(ServletUtils.getRequest());
CmsPageWidget pageWidget = new CmsPageWidget();
BeanUtils.copyProperties(dto, pageWidget);
pageWidget.setSiteId(site.getSiteId());
pageWidget.setTemplates(dto.getPublishPipeTemplateMap());
IPageWidget pw = pageWidgetType.newInstance();
pw.setPageWidgetEntity(cmsPageWdiget);
pw.setPageWidgetEntity(pageWidget);
pw.setOperator(StpAdminUtil.getLoginUser());
CmsSite site = this.siteService.getCurrentSite(request);
pw.getPageWidgetEntity().setSiteId(site.getSiteId());
this.pageWidgetService.addPageWidget(pw);
return R.ok();
}
@PutMapping
public R<?> editAdSpace(HttpServletRequest request) throws IOException {
PageWidgetEditDTO dto = JacksonUtils.from(request.getInputStream(), PageWidgetEditDTO.class);
CmsPageWidget cmsPageWdiget = new CmsPageWidget();
BeanUtils.copyProperties(dto, cmsPageWdiget);
public R<?> editAdSpace(@RequestBody @Validated PageWidgetEditDTO dto) {
CmsPageWidget pageWidget = new CmsPageWidget();
BeanUtils.copyProperties(dto, pageWidget);
pageWidget.setTemplates(dto.getPublishPipeTemplateMap());
IPageWidget pw = pageWidgetType.newInstance();
pw.setPageWidgetEntity(cmsPageWdiget);
pw.setPageWidgetEntity(pageWidget);
pw.setOperator(StpAdminUtil.getLoginUser());
this.pageWidgetService.savePageWidget(pw);
return R.ok();

View File

@ -32,6 +32,9 @@ import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.security.web.PageRequest;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
import jakarta.validation.constraints.Min;
@ -59,6 +62,10 @@ public class AdvertisementController extends BaseRestController {
private final IAdvertisementService advertisementService;
private final ISiteService siteService;
private final IResourceService resourceService;
@GetMapping("/types")
public R<?> listAdvertisements() {
List<Map<String, String>> list = advertisementService.getAdvertisementTypeList().stream()
@ -89,7 +96,11 @@ public class AdvertisementController extends BaseRestController {
public R<AdvertisementVO> getAdvertisementInfo(@PathVariable("advertisementId") @Min(1) Long advertisementId) {
CmsAdvertisement ad = this.advertisementService.getById(advertisementId);
Assert.notNull(ad, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("advertisementId", advertisementId));
return R.ok(new AdvertisementVO(ad).dealPreviewResourcePath());
CmsSite site = siteService.getSite(ad.getSiteId());
AdvertisementVO vo = new AdvertisementVO(ad).dealPreviewResourcePath();
resourceService.dealDefaultThumbnail(site, vo.getResourcePath(), vo::setResourceSrc);
return R.ok(vo);
}
@Log(title = "新增广告", businessType = BusinessType.INSERT)

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-article</artifactId>

View File

@ -28,12 +28,14 @@ import com.chestnut.contentcore.enums.ContentCopyType;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.util.ResourceUtils;
import com.chestnut.system.fixed.dict.YesOrNo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
@Slf4j
public class ArticleContent extends AbstractContent<CmsArticleDetail> {
private IArticleService articleService;
@ -48,15 +50,13 @@ public class ArticleContent extends AbstractContent<CmsArticleDetail> {
CmsArticleDetail articleDetail = this.getExtendEntity();
articleDetail.setContentId(this.getContentEntity().getContentId());
articleDetail.setSiteId(this.getContentEntity().getSiteId());
// 处理内部链接
String contentHtml = ResourceUtils.dealHtmlInternalUrl(articleDetail.getContentHtml());
// 处理文章正文远程图片
if (YesOrNo.isYes(articleDetail.getDownloadRemoteImage())) {
AsyncTaskManager.setTaskPercent(90);
contentHtml = this.getResourceService().downloadRemoteImages(contentHtml, this.getSite(),
String contentHtml = this.getResourceService().downloadRemoteImages(articleDetail.getContentHtml(), this.getSite(),
this.getOperatorUName());
articleDetail.setContentHtml(contentHtml);
}
articleDetail.setContentHtml(contentHtml);
// 正文首图作为logo
if (StringUtils.isEmpty(this.getContentEntity().getImages())
&& AutoArticleLogo.getValue(this.getSite().getConfigProps())) {
@ -76,15 +76,13 @@ public class ArticleContent extends AbstractContent<CmsArticleDetail> {
return;
}
CmsArticleDetail articleDetail = this.getExtendEntity();
// 处理内部链接
String contentHtml = ResourceUtils.dealHtmlInternalUrl(articleDetail.getContentHtml());
// 处理文章正文远程图片
if (YesOrNo.isYes(articleDetail.getDownloadRemoteImage())) {
AsyncTaskManager.setTaskPercent(90);
contentHtml = this.getResourceService().downloadRemoteImages(contentHtml, this.getSite(),
String contentHtml = this.getResourceService().downloadRemoteImages(articleDetail.getContentHtml(), this.getSite(),
this.getOperatorUName());
articleDetail.setContentHtml(contentHtml);
}
articleDetail.setContentHtml(contentHtml);
// 正文首图作为logo
if (StringUtils.isEmpty(this.getContentEntity().getImages())
&& AutoArticleLogo.getValue(this.getSite().getConfigProps())) {
@ -116,6 +114,10 @@ public class ArticleContent extends AbstractContent<CmsArticleDetail> {
@Override
protected void delete0() {
if (this.hasExtendEntity()) {
if (Objects.isNull(this.getExtendEntity())) {
log.warn("The content extend entity is null: {}", this.getContentEntity().getContentId());
return;
}
this.getArticleService().dao()
.deleteByIdAndBackup(this.getExtendEntity(), this.getOperatorUName());
}

View File

@ -32,23 +32,23 @@ import com.chestnut.contentcore.core.IContent;
import com.chestnut.contentcore.core.IContentType;
import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
import com.chestnut.contentcore.domain.*;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.domain.vo.ContentVO;
import com.chestnut.contentcore.enums.ContentCopyType;
import com.chestnut.contentcore.fixed.dict.ContentOpType;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IContentService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.*;
import com.chestnut.system.fixed.dict.YesOrNo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Slf4j
@Component(IContentType.BEAN_NAME_PREFIX + ArticleContentType.ID)
@RequiredArgsConstructor
public class ArticleContentType implements IContentType {
@ -67,6 +67,8 @@ public class ArticleContentType implements IContentType {
private final IContentService contentService;
private final IResourceService resourceService;
@Override
public String getId() {
return ID;
@ -119,8 +121,15 @@ public class ArticleContentType implements IContentType {
content.setContentEntity(contentEntity);
content.setExtendEntity(extendEntity);
content.setParams(dto.getParams());
if (content.hasExtendEntity() && StringUtils.isEmpty(extendEntity.getContentHtml())) {
throw CommonErrorCode.NOT_EMPTY.exception("contentHtml");
if (content.hasExtendEntity()) {
if (StringUtils.isEmpty(extendEntity.getContentHtml())) {
throw CommonErrorCode.NOT_EMPTY.exception("contentHtml");
}
IArticleBodyFormat format = articleService.getArticleBodyFormat(extendEntity.getFormat());
if (Objects.nonNull(format)) {
String contentHtml = format.onSave(extendEntity.getContentHtml());
extendEntity.setContentHtml(contentHtml);
}
}
return content;
}
@ -129,6 +138,7 @@ public class ArticleContentType implements IContentType {
public ContentVO initEditor(Long catalogId, Long contentId) {
CmsCatalog catalog = this.catalogService.getCatalog(catalogId);
Assert.notNull(catalog, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("catalogId", catalogId));
CmsSite site = siteService.getSite(catalog.getSiteId());
List<CmsPublishPipe> publishPipes = this.publishPipeService.getPublishPipes(catalog.getSiteId());
ArticleVO vo;
if (IdUtils.validate(contentId)) {
@ -138,7 +148,7 @@ public class ArticleContentType implements IContentType {
CmsArticleDetail extendEntity = this.articleService.dao().getById(contentId);
vo = ArticleVO.newInstance(contentEntity, extendEntity);
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, contentEntity.getPublishPipeProps());
vo.setPublishPipeProps(publishPipeProps);
} else {
@ -146,16 +156,27 @@ public class ArticleContentType implements IContentType {
vo.setContentId(IdUtils.getSnowflakeId());
vo.setCatalogId(catalog.getCatalogId());
vo.setContentType(ID);
CmsSite site = siteService.getSite(catalog.getSiteId());
vo.setDownloadRemoteImage(DownloadRemoteImage.getValue(site.getConfigProps()));
// 发布通道初始数据
vo.setPublishPipe(publishPipes.stream().map(CmsPublishPipe::getCode).toArray(String[]::new));
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, null);
vo.setPublishPipeProps(publishPipeProps);
}
vo.setCatalogName(catalog.getName());
// 内容引导图缩略图处理
resourceService.dealDefaultThumbnail(site, vo.getImages(), thumbnails -> {
vo.setImagesSrc(thumbnails);
vo.setLogoSrc(thumbnails.get(0));
});
// 文章正文内容处理
IArticleBodyFormat articleBodyFormat = articleService.getArticleBodyFormat(vo.getFormat());
if (Objects.nonNull(articleBodyFormat)) {
vo.setContentHtml(articleBodyFormat.initEditor(vo.getContentHtml()));
} else {
log.warn("Unsupported article body format: " + vo.getFormat());
}
return vo;
}

View File

@ -26,15 +26,13 @@ import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.ICoreDataHandler;
import com.chestnut.contentcore.core.SiteExportContext;
import com.chestnut.contentcore.core.SiteImportContext;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.Objects;
/**
* 文章内容核心数据处理器
@ -49,8 +47,6 @@ public class ArticleCoreDataHandler implements ICoreDataHandler {
private final IArticleService articleService;
private final Map<String, IArticleBodyFormat> articleBodyFormatMap;
@Override
public void onSiteExport(SiteExportContext context) {
// cms_article_detail
@ -90,23 +86,14 @@ public class ArticleCoreDataHandler implements ICoreDataHandler {
Long contentId = context.getContentIdMap().get(oldContentId);
data.setContentId(contentId);
data.setSiteId(context.getSite().getSiteId());
String contentHtml = data.getContentHtml();
// 替换正文内部资源地址
StringBuilder html = new StringBuilder();
int index = 0;
Matcher matcher = InternalUrlUtils.InternalUrlTagPattern.matcher(data.getContentHtml());
while (matcher.find()) {
String tagStr = matcher.group();
String iurl = matcher.group(1);
String newIurl = context.dealInternalUrl(iurl);
tagStr = StringUtils.replaceEx(tagStr, iurl, newIurl);
html.append(contentHtml, index, matcher.start()).append(tagStr);
index = matcher.end();
if (StringUtils.isEmpty(data.getFormat())) {
data.setFormat(ArticleBodyFormat_RichText.ID);
}
html.append(contentHtml.substring(index));
data.setContentHtml(html.toString());
if (!articleBodyFormatMap.containsKey(IArticleBodyFormat.BEAN_PREFIX + data.getFormat())) {
data.setFormat(ArticleBodyFormat_RichText.ID); // 不支持的文章格式一律设置为富文本格式
String contentHtml = data.getContentHtml();
IArticleBodyFormat format = articleService.getArticleBodyFormat(data.getFormat());
if (Objects.nonNull(format)) {
contentHtml = format.onSiteThemImport(context, contentHtml);
data.setContentHtml(contentHtml);
}
articleService.dao().save(data);
} catch (Exception e) {

View File

@ -117,7 +117,7 @@ public class ArticleUtils {
* 替换为ssi引用标签
* </p>
*/
public static String dealPageWidget(CmsContent content, String articleBody, boolean isPreview) {
public static String dealPageWidget(CmsContent content, String articleBody, String publishPipeCode, boolean isPreview) {
if (StringUtils.isBlank(articleBody)) {
return articleBody;
}
@ -138,17 +138,17 @@ public class ArticleUtils {
if (isPreview) {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(InternalDataType_PageWidget.ID);
placeholderImgTag = internalDataType.getPageData(new IInternalDataType.RequestData(pw.getPageWidgetId(),
1, pw.getPublishPipeCode(), true, null));
1, publishPipeCode, true, null));
} else {
boolean ssiEnabled = EnableSSIProperty.getValue(site.getConfigProps());
if (catalog.isStaticize() && ssiEnabled) {
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(pw.getPublishPipeCode()));
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(publishPipeCode));
String staticFilePath = pw.getPath() + staticFileName;
placeholderImgTag = StringUtils.messageFormat(CmsIncludeTag.SSI_INCLUDE_TAG, "/" + staticFilePath);
} else {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(InternalDataType_PageWidget.ID);
placeholderImgTag = internalDataType.getPageData(new IInternalDataType.RequestData(pw.getPageWidgetId(),
1, pw.getPublishPipeCode(), false, null));
1, publishPipeCode, false, null));
}
}
}

View File

@ -15,6 +15,8 @@
*/
package com.chestnut.article;
import com.chestnut.contentcore.core.SiteImportContext;
/**
* 文正正文文档格式
*
@ -42,6 +44,18 @@ public interface IArticleBodyFormat {
return contentHtml;
}
/**
* 编辑器内容保存处理
*/
default String onSave(String contentHtml) {
return contentHtml;
}
/**
* 站点主题导入处理
*/
String onSiteThemImport(SiteImportContext context, String contentHtml);
/**
* 文章正文内容发布预览处理
*/

View File

@ -15,7 +15,7 @@
*/
package com.chestnut.article;
import com.alibaba.excel.util.StringUtils;
import cn.idev.excel.util.StringUtils;
import com.chestnut.contentcore.core.IPublishPipeProp;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;

View File

@ -17,9 +17,14 @@ package com.chestnut.article.format;
import com.chestnut.article.ArticleUtils;
import com.chestnut.article.IArticleBodyFormat;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.SiteImportContext;
import com.chestnut.contentcore.util.InternalUrlUtils;
import com.chestnut.contentcore.util.ResourceUtils;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
/**
* 文章正文文档格式富文本
*
@ -46,6 +51,30 @@ public class ArticleBodyFormat_RichText implements IArticleBodyFormat {
return InternalUrlUtils.dealResourceInternalUrl(contentHtml);
}
@Override
public String onSave(String contentHtml) {
return ResourceUtils.dealHtmlInternalUrl(contentHtml);
}
@Override
public String onSiteThemImport(SiteImportContext context, String contentHtml) {
// 替换正文内部资源地址
StringBuilder html = new StringBuilder();
int index = 0;
Matcher matcher = InternalUrlUtils.InternalUrlTagPattern.matcher(contentHtml);
while (matcher.find()) {
String tagStr = matcher.group();
String iurl = matcher.group(1);
String newIurl = context.dealInternalUrl(iurl);
tagStr = StringUtils.replaceEx(tagStr, iurl, newIurl);
html.append(contentHtml, index, matcher.start()).append(tagStr);
index = matcher.end();
}
html.append(contentHtml.substring(index));
return html.toString();
}
@Override
public String deal(String contentHtml, String publishPipeCode, boolean isPreview) {
// 处理内容扩展模板占位符

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-block</artifactId>

View File

@ -68,9 +68,9 @@ public class ManualPageWidgetType implements IPageWidgetType {
}
@Override
public IPageWidget loadPageWidget(CmsPageWidget cmsPageWdiget) {
public IPageWidget loadPageWidget(CmsPageWidget cmsPageWidget) {
ManualPageWidget pw = new ManualPageWidget();
pw.setPageWidgetEntity(cmsPageWdiget);
pw.setPageWidgetEntity(cmsPageWidget);
return pw;
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-comment</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-contentcore</artifactId>

View File

@ -38,14 +38,12 @@ import com.chestnut.contentcore.core.impl.CatalogType_Link;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.*;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.domain.vo.CatalogVO;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
import com.chestnut.contentcore.perms.CatalogPermissionType.CatalogPrivItem;
import com.chestnut.contentcore.perms.ContentCorePriv;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.IPublishService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.user.preference.CatalogTreeExpandModePreference;
import com.chestnut.contentcore.util.CmsPrivUtils;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
@ -92,6 +90,8 @@ public class CatalogController extends BaseRestController {
private final AsyncTaskManager asyncTaskManager;
private final IResourceService resourceService;
/**
* 查询栏目数据列表
*/
@ -118,8 +118,9 @@ public class CatalogController extends BaseRestController {
if (StringUtils.isNotEmpty(dto.getLogo())) {
dto.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(dto.getLogo()));
}
resourceService.dealDefaultThumbnail(site, dto.getLogo(), dto::setLogoSrc);
// 发布通道数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
PublishPipePropUseType.Catalog, catalog.getPublishPipeProps());
dto.setPublishPipeDatas(publishPipeProps);
return R.ok(dto);
@ -179,12 +180,20 @@ public class CatalogController extends BaseRestController {
@Log(title = "删除", businessType = BusinessType.DELETE)
@DeleteMapping("/{catalogId}")
public R<String> deleteCatalog(@PathVariable("catalogId") @LongId Long catalogId) {
CmsCatalog catalog = catalogService.getById(catalogId);
Assert.notNull(catalog, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("catalogId", catalog));
LoginUser operator = StpAdminUtil.getLoginUser();
AsyncTask task = new AsyncTask("Catalog-" + catalogId) {
@Override
public void run0() {
catalogService.deleteCatalog(catalogId, operator);
List<CmsCatalog> list = catalogService.lambdaQuery()
.likeRight(CmsCatalog::getAncestors, catalog.getAncestors())
.list();
list.sort((c1, c2) -> c2.getTreeLevel() - c1.getTreeLevel());
list.forEach(c -> {
catalogService.deleteCatalog(c, operator);
});
}
};
this.asyncTaskManager.execute(task);

View File

@ -43,10 +43,7 @@ import com.chestnut.contentcore.listener.event.AfterContentEditorInitEvent;
import com.chestnut.contentcore.perms.CatalogPermissionType.CatalogPrivItem;
import com.chestnut.contentcore.properties.ShortTitleLabelProperty;
import com.chestnut.contentcore.properties.SubTitleLabelProperty;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IContentService;
import com.chestnut.contentcore.service.IPublishService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.user.preference.IncludeChildContentPreference;
import com.chestnut.contentcore.user.preference.ShowContentSubTitlePreference;
import com.chestnut.contentcore.util.CmsPrivUtils;
@ -90,6 +87,8 @@ public class ContentController extends BaseRestController {
private final IPublishService publishService;
private final IResourceService resourceService;
private final ApplicationContext applicationContext;
/**
@ -136,6 +135,11 @@ public class ContentController extends BaseRestController {
}
Page<CmsContent> page = q.page(new Page<>(pr.getPageNumber(), pr.getPageSize(), true));
List<ListContentVO> list = page.getRecords().stream().map(ListContentVO::newInstance).toList();
// 内容引导图缩略图处理
list.forEach(vo -> resourceService.dealDefaultThumbnail(site, vo.getImages(), thumbnails -> {
vo.setImagesSrc(thumbnails);
vo.setLogoSrc(thumbnails.get(0));
}));
return this.bindDataTable(list, (int) page.getTotal());
}

View File

@ -95,7 +95,7 @@ public class CoreController extends BaseRestController {
String pageData = internalDataType.getPageData(data);
response.getWriter().write(pageData);
} catch (Exception e) {
e.printStackTrace(response.getWriter());
response.getWriter().write(e.getMessage());
}
}

View File

@ -34,15 +34,18 @@ import com.chestnut.contentcore.core.IPageWidget;
import com.chestnut.contentcore.core.IPageWidgetType;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.PageWidgetAddDTO;
import com.chestnut.contentcore.domain.dto.PageWidgetEditDTO;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.contentcore.domain.vo.PageWidgetVO;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
import com.chestnut.contentcore.perms.ContentCorePriv;
import com.chestnut.contentcore.perms.SitePermissionType;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.CmsPrivUtils;
import com.chestnut.system.security.AdminUserType;
@ -76,6 +79,8 @@ public class PageWidgetController extends BaseRestController {
private final IPageWidgetService pageWidgetService;
private final IPublishPipeService publishPipeService;
@Priv(type = AdminUserType.TYPE)
@GetMapping("/types")
public R<?> getPageWidgetTypes() {
@ -131,7 +136,14 @@ public class PageWidgetController extends BaseRestController {
IPageWidgetType pwt = this.pageWidgetService.getPageWidgetType(pageWidget.getType());
PageWidgetVO vo = pwt.getPageWidgetVO(pageWidget);
// 发布通道模板
List<CmsPublishPipe> publishPipes = this.publishPipeService.getPublishPipes(pageWidget.getSiteId());
List<PublishPipeTemplate> templates = publishPipes.stream().map(pp -> new PublishPipeTemplate(
pp.getName(),
pp.getCode(),
pageWidget.getTemplate(pp.getCode())
)).toList();
vo.setTemplates(templates);
if (pageWidget.getCatalogId() > 0) {
CmsCatalog catalog = this.catalogService.getCatalog(pageWidget.getCatalogId());
vo.setCatalogName(catalog.getName());
@ -154,6 +166,7 @@ public class PageWidgetController extends BaseRestController {
CmsPageWidget pageWidget= new CmsPageWidget();
BeanUtils.copyProperties(dto, pageWidget);
pageWidget.setSiteId(site.getSiteId());
pageWidget.setTemplates(dto.getPublishPipeTemplateMap());
IPageWidget pw = pwt.newInstance();
pw.setPageWidgetEntity(pageWidget);
@ -175,7 +188,8 @@ public class PageWidgetController extends BaseRestController {
BeanUtils.copyProperties(dto, pageWidget);
pageWidget.setContent(dto.getContentStr());
pageWidget.setTemplates(dto.getPublishPipeTemplateMap());
IPageWidgetType pwt = this.pageWidgetService.getPageWidgetType(pageWidget.getType());
IPageWidget pw = pwt.newInstance();
pw.setPageWidgetEntity(pageWidget);

View File

@ -29,7 +29,7 @@ import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.perms.ContentCorePriv;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
@ -71,8 +71,8 @@ public class PublishPipeController extends BaseRestController {
@GetMapping("/selectData")
public R<?> bindSelectData() {
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
List<PublishPipeProp> datalist = this.publishPipeService.getPublishPipes(site.getSiteId())
.stream().map(p -> PublishPipeProp.newInstance(p.getCode(), p.getName(), null))
List<PublishPipeProps> datalist = this.publishPipeService.getPublishPipes(site.getSiteId())
.stream().map(p -> PublishPipeProps.newInstance(p.getCode(), p.getName(), null))
.collect(Collectors.toList());
return this.bindDataTable(datalist);
}

View File

@ -35,14 +35,17 @@ import com.chestnut.common.utils.StringUtils;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.contentcore.core.IProperty.UseType;
import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.*;
import com.chestnut.contentcore.domain.dto.PublishSiteDTO;
import com.chestnut.contentcore.domain.dto.SiteDTO;
import com.chestnut.contentcore.domain.dto.SiteDefaultTemplateDTO;
import com.chestnut.contentcore.domain.dto.SiteExportDTO;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.domain.vo.SiteDashboardVO;
import com.chestnut.contentcore.perms.ContentCorePriv;
import com.chestnut.contentcore.perms.SitePermissionType.SitePrivItem;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.IPublishService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.service.impl.SiteThemeService;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
import com.chestnut.contentcore.util.InternalUrlUtils;
@ -97,6 +100,8 @@ public class SiteController extends BaseRestController {
private final SiteThemeService siteExportService;
private final IResourceService resourceService;
/**
* 获取当前站点数据
*
@ -109,6 +114,14 @@ public class SiteController extends BaseRestController {
return R.ok(Map.of("siteId", site.getSiteId(), "siteName", site.getName()));
}
@Priv(type = AdminUserType.TYPE)
@GetMapping("/getDashboardSiteInfo")
public R<SiteDashboardVO> getDashboardSiteInfo() {
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
List<CmsPublishPipe> publishPipes = this.publishPipeService.getPublishPipes(site.getSiteId());
return R.ok(SiteDashboardVO.create(site, publishPipes));
}
/**
* 设置当前站点
*
@ -161,8 +174,9 @@ public class SiteController extends BaseRestController {
site.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(site.getLogo()));
}
SiteDTO dto = SiteDTO.newInstance(site);
resourceService.dealDefaultThumbnail(site, dto.getLogo(), dto::setLogoSrc);
// 发布通道数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
PublishPipePropUseType.Site, site.getPublishPipeProps());
dto.setPublishPipeDatas(publishPipeProps);
return R.ok(dto);
@ -298,7 +312,7 @@ public class SiteController extends BaseRestController {
SiteDefaultTemplateDTO dto = new SiteDefaultTemplateDTO();
dto.setSiteId(siteId);
// 发布通道数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(site.getSiteId(),
PublishPipePropUseType.Site, site.getPublishPipeProps());
dto.setPublishPipeProps(publishPipeProps);
return R.ok(dto);

View File

@ -40,6 +40,7 @@ import com.chestnut.contentcore.util.InternalUrlUtils;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.system.permission.PermissionUtils;
import lombok.Setter;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.BeanUtils;
import java.time.Instant;
@ -164,8 +165,8 @@ public abstract class AbstractContent<T> implements IContent<T> {
content.setCopyType(ContentCopyType.NONE);
}
content.createBy(this.getOperatorUName());
this.getContentService().dao().save(this.getContentEntity());
this.add0();
this.getContentService().dao().save(this.getContentEntity());
// 栏目内容数+1
this.getCatalogService().changeContentCount(catalog.getCatalogId(), 1);
ContentLogUtils.addLog(ContentOpType.ADD, this.getContentEntity(), this.getOperator());
@ -213,8 +214,8 @@ public abstract class AbstractContent<T> implements IContent<T> {
content.setStatus(ContentStatus.EDITING);
}
content.updateBy(this.getOperatorUName());
contentService.dao().updateById(this.getContentEntity());
this.save0();
contentService.dao().updateById(this.getContentEntity());
ContentLogUtils.addLog(ContentOpType.UPDATE, this.getContentEntity(), this.getOperator());
SpringUtils.publishEvent(new AfterContentSaveEvent(this, this, false));
return this.getContentEntity().getContentId();
@ -222,6 +223,10 @@ public abstract class AbstractContent<T> implements IContent<T> {
protected abstract void save0();
public boolean isDeleteByCatalog() {
return MapUtils.getBoolean(this.getParams(), PARAM_IS_DELETE_BY_CATALOG, false);
}
@Override
public void delete() {
this.checkLock();
@ -232,8 +237,10 @@ public abstract class AbstractContent<T> implements IContent<T> {
this.getContentService().dao().remove(new LambdaQueryWrapper<CmsContent>()
.eq(CmsContent::getCopyType, ContentCopyType.Mapping)
.eq(CmsContent::getCopyId, this.getContentEntity().getContentId()));
// 栏目内容数-1
this.getCatalogService().changeContentCount(getCatalogId(), -1);
if (!isDeleteByCatalog()) {
// 栏目内容数-1, 删除栏目时不需要更新栏目的内容数量
this.getCatalogService().changeContentCount(getCatalogId(), -1);
}
ContentLogUtils.addLog(ContentOpType.DELETE, this.getContentEntity(), this.getOperator());
SpringUtils.publishEvent(new AfterContentDeleteEvent(this, this));

View File

@ -20,14 +20,11 @@ import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.fixed.dict.PageWidgetStatus;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.contentcore.service.IPublishService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.util.SiteUtils;
import freemarker.template.TemplateException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -36,6 +33,7 @@ import org.springframework.beans.BeanUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
@Slf4j
@Getter
@ -52,6 +50,8 @@ public abstract class AbstractPageWidget implements IPageWidget {
private IPublishService publishService;
private IPublishPipeService publishPipeService;
private LoginUser operator;
@Override
@ -98,9 +98,12 @@ public abstract class AbstractPageWidget implements IPageWidget {
this.getPageWidgetService().removeById(this.getPageWidgetEntity());
try {
// 删除静态文件
FileUtils.delete(new File(this.getStaticFilePath()));
List<CmsPublishPipe> publishPipes = this.getPublishPipeService().getAllPublishPipes(this.getPageWidgetEntity().getSiteId());
for (CmsPublishPipe publishPipe : publishPipes) {
FileUtils.delete(new File(this.getStaticFilePath(publishPipe.getCode())));
}
} catch (IOException e) {
log.error("Delete file failed: {}", this.getStaticFilePath());
log.error("Delete page-widget static file failed: {}", this.getPageWidgetEntity().getPageWidgetId());
}
}
@ -114,11 +117,11 @@ public abstract class AbstractPageWidget implements IPageWidget {
this.getPublishService().pageWidgetStaticize(this);
}
public String getStaticFilePath() {
public String getStaticFilePath(String publishPipeCode) {
CmsSite site = this.getSiteService().getSite(this.getPageWidgetEntity().getSiteId());
String siteRoot = SiteUtils.getSiteRoot(site, this.getPageWidgetEntity().getPublishPipeCode());
String siteRoot = SiteUtils.getSiteRoot(site, publishPipeCode);
return siteRoot + this.getPageWidgetEntity().getPath() + this.getPageWidgetEntity().getPageWidgetId()
+ StringUtils.DOT + site.getStaticSuffix(this.getPageWidgetEntity().getPublishPipeCode());
+ StringUtils.DOT + site.getStaticSuffix(publishPipeCode);
}
public ISiteService getSiteService() {
@ -148,4 +151,11 @@ public abstract class AbstractPageWidget implements IPageWidget {
}
return publishService;
}
public IPublishPipeService getPublishPipeService() {
if(this.publishPipeService == null) {
this.publishPipeService = SpringUtils.getBean(IPublishPipeService.class);
}
return publishPipeService;
}
}

View File

@ -33,6 +33,8 @@ import java.util.Objects;
*/
public interface IContent<T> {
String PARAM_IS_DELETE_BY_CATALOG = "isDeleteByCatalog";
/**
* 获取站点ID
*/

View File

@ -15,12 +15,13 @@
*/
package com.chestnut.contentcore.core;
import java.io.IOException;
import org.apache.commons.lang3.ArrayUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsResource;
import org.apache.commons.lang3.ArrayUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* 资源类型
@ -71,16 +72,16 @@ public interface IResourceType {
/**
* 处理资源提取资源属性添加水印等
*
* @param resource
* @throws IOException
*
* @param resource 资源记录
* @param tempFile 资源文件
* @return
* @throws IOException ex
*/
default byte[] process(CmsResource resource, byte[] bytes) throws IOException {
resource.setFileSize((long) bytes.length);
return bytes;
default List<File> process(CmsResource resource, File tempFile) throws IOException {
return List.of();
}
default void asyncProcess(CmsResource resource) {
}
}

View File

@ -95,7 +95,7 @@ public class InternalURL {
this.params = new HashMap<>();
}
if (key.equals("id")) {
throw new RuntimeException("内部链接自定义参数不可使用固定参数id");
throw new RuntimeException("Conflict with fixed parameter: id.");
}
this.params.put(key, value);
return this.params;
@ -107,24 +107,28 @@ public class InternalURL {
String content = url.substring(IURLProtocol.length());
int i = content.lastIndexOf("?");
if (i < 0) {
throw new InternalUrlParseException("Invalid iurl: missing parameters.");
throw new InternalUrlParseException("missing parameters.");
}
// 默认iurl的路径部分就是内部数据类型如果参数中含有type则使用参数type路径部分作为path
String type = content.substring(0, i);
iurl.setType(type);
try {
// 默认iurl的路径部分就是内部数据类型如果参数中含有type则使用参数type路径部分作为path
String type = content.substring(0, i);
iurl.setType(type);
String params = content.substring(i + 1);
Map<String, String> args = StringUtils.splitToMap(params, "&", "=");
if (args.containsKey("type")) {
iurl.setPath(iurl.getType());
iurl.setType(args.get("type"));
args.remove("type");
String params = content.substring(i + 1);
Map<String, String> args = StringUtils.splitToMap(params, "&", "=");
if (args.containsKey("type")) {
iurl.setPath(iurl.getType());
iurl.setType(args.get("type"));
args.remove("type");
}
iurl.setId(Long.valueOf(args.get("id")));
args.remove("id");
// 自定义参数
iurl.setParams(args);
return iurl;
} catch (Exception e) {
throw new InternalUrlParseException(url, e);
}
iurl.setId(Long.valueOf(args.get("id")));
args.remove("id");
// 自定义参数
iurl.setParams(args);
return iurl;
}
public String toIUrl() {

View File

@ -20,7 +20,9 @@ import com.chestnut.contentcore.config.CMSConfig;
import com.chestnut.contentcore.core.impl.InternalDataType_Catalog;
import com.chestnut.contentcore.core.impl.InternalDataType_Content;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
import com.chestnut.contentcore.core.impl.InternalDataType_Site;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.exception.InternalUrlParseException;
import com.chestnut.contentcore.util.InternalUrlUtils;
import com.chestnut.contentcore.util.SiteUtils;
import lombok.Getter;
@ -101,12 +103,19 @@ public class SiteImportContext implements ISiteThemeContext {
}
public String dealInternalUrl(String iurl) {
InternalURL internalURL = InternalUrlUtils.parseInternalUrl(iurl);
InternalURL internalURL;
try {
internalURL = InternalUrlUtils.parseInternalUrl(iurl);
} catch (InternalUrlParseException e) {
// Ignore parse err
return iurl;
}
if (Objects.nonNull(internalURL)) {
Long id = switch (internalURL.getType()) {
case InternalDataType_Catalog.ID -> catalogIdMap.get(internalURL.getId());
case InternalDataType_Content.ID -> contentIdMap.get(internalURL.getId());
case InternalDataType_Resource.ID -> resourceIdMap.get(internalURL.getId());
case InternalDataType_Site.ID -> site.getSiteId();
default -> null;
};
if (IdUtils.validate(id)) {

View File

@ -15,19 +15,17 @@
*/
package com.chestnut.contentcore.core.impl;
import java.io.IOException;
import org.springframework.stereotype.Component;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.utils.Assert;
import com.chestnut.contentcore.core.IInternalDataType;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.contentcore.service.IPublishService;
import freemarker.template.TemplateException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 内部数据类型页面组件
@ -55,6 +53,6 @@ public class InternalDataType_PageWidget implements IInternalDataType {
CmsPageWidget pageWidget = pageWidgetService.getById(data.getDataId());
Assert.notNull(pageWidget, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("pageWidgetId", data.getDataId()));
return this.publishService.getPageWidgetPageData(pageWidget, data.isPreview());
return this.publishService.getPageWidgetPageData(pageWidget, data.getPublishPipeCode(), data.isPreview());
}
}

View File

@ -18,18 +18,19 @@ package com.chestnut.contentcore.core.impl;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.storage.IFileStorageType;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.common.utils.image.ImageHelper;
import com.chestnut.common.utils.image.ImageUtils;
import com.chestnut.common.utils.image.WatermarkPosition;
import com.chestnut.contentcore.core.IResourceType;
import com.chestnut.contentcore.domain.CmsResource;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.properties.*;
import com.chestnut.contentcore.properties.ImageWatermarkArgsProperty;
import com.chestnut.contentcore.properties.ImageWatermarkArgsProperty.ImageWatermarkArgs;
import com.chestnut.contentcore.properties.ImageWatermarkProperty;
import com.chestnut.contentcore.properties.ThumbnailHeightProperty;
import com.chestnut.contentcore.properties.ThumbnailWidthProperty;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.FileStorageHelper;
import com.chestnut.contentcore.util.ResourceUtils;
import com.chestnut.contentcore.util.SiteUtils;
import lombok.RequiredArgsConstructor;
@ -43,11 +44,11 @@ import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -97,50 +98,90 @@ public class ResourceType_Image implements IResourceType {
}
@Override
public void asyncProcess(CmsResource resource) {
asyncTaskManager.execute(() -> {
CmsSite site = siteService.getSite(resource.getSiteId());
String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = fileStorageTypeMap.get(IFileStorageType.BEAN_NAME_PREIFX + fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
String tempDirectory = ResourceUtils.getResourceTempDirectory(site);
File tempFile = new File(tempDirectory + resource.getPath());
FileExUtils.mkdirs(tempFile.getParent());
Map<String, File> paths = new HashMap<>();
try (InputStream read = fileStorageHelper.read(resource.getPath())) {
FileExUtils.transfer(read, tempFile);
// 获取图片属性
BufferedImage bi = ImageIO.read(tempFile);
resource.setWidth(bi.getWidth());
resource.setHeight(bi.getHeight());
// 先处理图片水印
if (watermark(site, bi, tempFile)) {
paths.put(resource.getPath(), tempFile);
resource.setFileSize(tempFile.length());
}
// 默认缩略图处理
thumbnail(site, resource, tempFile, paths);
} catch (IOException e) {
log.error("Image resource process fail.", e);
} finally {
for (Map.Entry<String, File> entry : paths.entrySet()) {
try {
Path path = entry.getValue().toPath();
try (InputStream is = Files.newInputStream(path)) {
fileStorageHelper.write(entry.getKey(), is);
}
// 删除临时图片文件
Files.deleteIfExists(path);
} catch (IOException e) {
log.error("Delete temp file failed.", e);
}
}
resourceService.updateById(resource);
public List<File> process(CmsResource resource, File file) throws IOException {
CmsSite site = siteService.getSite(resource.getSiteId());
boolean needWatermark = needWatermark(site);
boolean needThumbnail = needThumbnail(site);
if (!needWatermark && !needThumbnail) {
return List.of(); // 不需要处理
}
List<File> files = new ArrayList<>();
try {
// 获取图片属性
BufferedImage bi = ImageIO.read(file);
resource.setWidth(bi.getWidth());
resource.setHeight(bi.getHeight());
// 先处理图片水印
if (needWatermark) {
watermark(site, bi, file);
resource.setFileSize(file.length());
}
});
// 默认缩略图处理
thumbnail(site, resource, file, files);
} catch (IOException e) {
log.error("Image resource process fail.", e);
}
return files;
}
private boolean watermark(CmsSite site, BufferedImage bi, File output) {
// @Override
// public void asyncProcess(CmsResource resource) {
// CmsSite site = siteService.getSite(resource.getSiteId());
// boolean needWatermark = needWatermark(site);
// boolean needThumbnail = needThumbnail(site);
// if (!needWatermark && !needThumbnail) {
// return; // 不需要处理
// }
// asyncTaskManager.execute(() -> {
// String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
// IFileStorageType fst = fileStorageTypeMap.get(IFileStorageType.BEAN_NAME_PREIFX + fileStorageType);
// FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
// String tempDirectory = ResourceUtils.getResourceTempDirectory(site);
// File tempFile = new File(tempDirectory + resource.getPath());
// FileExUtils.mkdirs(tempFile.getParent());
// Map<String, File> paths = new HashMap<>();
// try (InputStream read = fileStorageHelper.read(resource.getPath())) {
// FileExUtils.transfer(read, tempFile);
// // 获取图片属性
// BufferedImage bi = ImageIO.read(tempFile);
// resource.setWidth(bi.getWidth());
// resource.setHeight(bi.getHeight());
// // 先处理图片水印
// if (needWatermark) {
// watermark(site, bi, tempFile);
// paths.put(resource.getPath(), tempFile);
// resource.setFileSize(tempFile.length());
// }
// // 默认缩略图处理
// thumbnail(site, resource, tempFile);
// // 更新文件
// for (Map.Entry<String, File> entry : paths.entrySet()) {
// try (FileInputStream is = new FileInputStream(entry.getValue())) {
// fileStorageHelper.write(entry.getKey(), is, entry.getValue().length());
// } catch (IOException e) {
// log.error("Write temp file to storage {} failed.", fst.getType(), e);
// }
// }
// if (needWatermark) {
// resourceService.updateById(resource);
// }
// } catch (IOException e) {
// log.error("Image resource process fail.", e);
// } finally {
// try {
// // 删除临时文件
// Files.deleteIfExists(tempFile.toPath());
// for (File file : paths.values()) {
// Files.deleteIfExists(file.toPath());
// }
// } catch (IOException e) {
// log.error("Delete temp file failed.", e);
// }
// }
// });
// }
private boolean needWatermark(CmsSite site) {
if (!ImageWatermarkProperty.getValue(site.getConfigProps())) {
return false;
}
@ -150,9 +191,13 @@ public class ResourceType_Image implements IResourceType {
}
String siteResourceRoot = SiteUtils.getSiteResourceRoot(site);
File file = new File(siteResourceRoot + args.getImage());
if (!file.exists()) {
return false;
}
return file.exists();
}
private void watermark(CmsSite site, BufferedImage bi, File output) {
ImageWatermarkArgs args = ImageWatermarkArgsProperty.getValue(site.getConfigProps());
String siteResourceRoot = SiteUtils.getSiteResourceRoot(site);
File file = new File(siteResourceRoot + args.getImage());
try {
BufferedImage biWatermarkImage = ImageIO.read(file);
String ext = FilenameUtils.getExtension(output.getName());
@ -163,14 +208,18 @@ public class ResourceType_Image implements IResourceType {
WatermarkPosition.str2Position(args.getPosition())
).toFile(output);
log.debug("Watermark success: {}", output.getAbsolutePath());
return true;
} catch (IOException e) {
log.error("Watermark image fail.", e);
}
return false;
}
private void thumbnail(CmsSite site, CmsResource resource, File tempFile, Map<String, File> paths) {
private boolean needThumbnail(CmsSite site) {
int w = ThumbnailWidthProperty.getValue(site.getConfigProps());
int h = ThumbnailHeightProperty.getValue(site.getConfigProps());
return w > 0 && h > 0;
}
private void thumbnail(CmsSite site, CmsResource resource, File tempFile, List<File> files) {
// 读取存储配置
int w = ThumbnailWidthProperty.getValue(site.getConfigProps());
int h = ThumbnailHeightProperty.getValue(site.getConfigProps());
@ -182,13 +231,13 @@ public class ResourceType_Image implements IResourceType {
File output = new File(tempDirectory + thumbnailPath);
try {
ImageHelper.of(tempFile).format(ext).resize(w, h).toFile(output);
paths.put(thumbnailPath, output);
files.add(output);
log.debug("Thumbnail success: {}", output.getAbsolutePath());
} catch (IOException e) {
try {
// 生成缩略图失败直接使用源图作为缩略图
Files.copy(tempFile.toPath(), Path.of(thumbnailPath), StandardCopyOption.REPLACE_EXISTING);
paths.put(thumbnailPath, output);
files.add(output);
} catch (IOException ex) {
log.error("Copy thumbnail file err.", ex);
}

View File

@ -19,10 +19,18 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.chestnut.common.db.domain.BaseEntity;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.MapUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 页面部件表对象 [cms_page_widget]
@ -84,12 +92,20 @@ public class CmsPageWidget extends BaseEntity {
/**
* 发布通道
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String publishPipeCode;
/**
* 关联模板
* 模板路径
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String template;
/**
* 发布通道模板配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> templates;
/**
* 静态化目录
@ -103,4 +119,13 @@ public class CmsPageWidget extends BaseEntity {
@TableField(exist = false)
private Object contentObj;
public String getTemplate(String publishPipeCode) {
String templatePath = MapUtils.getString(templates, publishPipeCode);
// 兼容历史版本数据
if (StringUtils.isEmpty(templatePath) && StringUtils.equals(publishPipeCode, this.publishPipeCode)) {
templatePath = this.template;
}
return templatePath;
}
}

View File

@ -66,7 +66,9 @@ public class CmsResource extends BaseEntity {
/**
* 存储类型默认local
* @deprecated 1.5.6版本开始不再根据此字段生成资源路径资源路径统一根据当前站点资源存储配置生成
*/
@Deprecated(forRemoval = true, since = "1.5.6")
private String storageType;
/**

View File

@ -40,8 +40,9 @@ public interface InitByContent {
if (StringUtils.isNotEmpty(content.getImages())) {
this.setLogo(content.getImages().get(0));
if (preview) {
this.setImagesSrc(content.getImages().stream().map(InternalUrlUtils::getActualPreviewUrl).toList());
this.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(content.getLogo()));
List<String> srcList = content.getImages().stream().map(InternalUrlUtils::getActualPreviewUrl).toList();
this.setImagesSrc(srcList);
this.setLogoSrc(srcList.get(0));
}
} else {
this.setImages(List.of());

View File

@ -16,6 +16,7 @@
package com.chestnut.contentcore.domain.dto;
import com.chestnut.common.security.domain.BaseDTO;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.system.validator.Dict;
import com.chestnut.system.validator.LongId;
@ -114,7 +115,7 @@ public class CatalogUpdateDTO extends BaseDTO {
/*
* 栏目发布通道数据
*/
private List<PublishPipeProp> publishPipeDatas;
private List<PublishPipeProps> publishPipeDatas;
/*
* 自定义参数

View File

@ -20,6 +20,7 @@ import com.chestnut.common.utils.Assert;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.InitByContent;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import com.chestnut.contentcore.fixed.dict.ContentOpType;
import com.chestnut.contentcore.service.ICatalogService;
@ -220,7 +221,7 @@ public class ContentDTO implements InitByContent {
/**
* 发布通道配置
*/
private List<PublishPipeProp> publishPipeProps;
private List<PublishPipeProps> publishPipeProps;
/**
* 栏目扩展配置

View File

@ -16,6 +16,7 @@
package com.chestnut.contentcore.domain.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
@ -32,6 +33,7 @@ public class FileOperateDTO {
/*
* 重名名名称
*/
@Pattern(regexp = "^[^/\\\\]*$", message = "{VALIDATOR.CMS.FILE_NAME.REGEXP_ERR}")
private String rename;
/*

View File

@ -15,6 +15,8 @@
*/
package com.chestnut.contentcore.domain.dto;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.system.validator.LongId;
import jakarta.validation.constraints.Min;
@ -23,6 +25,10 @@ import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Getter
@Setter
public class PageWidgetAddDTO {
@ -55,14 +61,20 @@ public class PageWidgetAddDTO {
/**
* 发布通道编码
*/
@NotEmpty
private String publishPipeCode;
@Deprecated(since = "1.5.6", forRemoval = true)
private String publishPipeCode;
/**
* 模板
* 模板路径
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String template;
/**
* 发布通道模板配置
*/
private List<PublishPipeTemplate> templates;
/**
* 静态化目录
*/
@ -72,4 +84,11 @@ public class PageWidgetAddDTO {
* 备注
*/
private String remark;
public Map<String, String> getPublishPipeTemplateMap() {
if (StringUtils.isEmpty(this.templates)) {
return Map.of();
}
return this.templates.stream().collect(Collectors.toMap(PublishPipeTemplate::code, PublishPipeTemplate::template));
}
}

View File

@ -15,13 +15,18 @@
*/
package com.chestnut.contentcore.domain.dto;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.system.validator.LongId;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Getter
@Setter
public class PageWidgetEditDTO {
@ -47,13 +52,19 @@ public class PageWidgetEditDTO {
/**
* 发布通道编码
*/
@NotEmpty
@Deprecated(since = "1.5.6", forRemoval = true)
private String publishPipeCode;
/**
* 模板路径
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String template;
/**
* 发布通道模板配置
*/
private List<PublishPipeTemplate> templates;
/**
* 静态化目录
@ -70,4 +81,11 @@ public class PageWidgetEditDTO {
* 备注
*/
private String remark;
public Map<String, String> getPublishPipeTemplateMap() {
if (StringUtils.isEmpty(this.templates)) {
return Map.of();
}
return this.templates.stream().collect(Collectors.toMap(PublishPipeTemplate::code, PublishPipeTemplate::template));
}
}

View File

@ -18,6 +18,7 @@ package com.chestnut.contentcore.domain.dto;
import java.util.List;
import java.util.Map;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import jakarta.validation.constraints.Pattern;
import org.springframework.beans.BeanUtils;
@ -61,7 +62,7 @@ public class SiteDTO extends BaseDTO {
private Map<String, String> configProps;
private List<PublishPipeProp> publishPipeDatas;
private List<PublishPipeProps> publishPipeDatas;
private Map<String, Object> params;

View File

@ -18,6 +18,7 @@ package com.chestnut.contentcore.domain.dto;
import java.util.List;
import com.chestnut.common.security.domain.BaseDTO;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.system.validator.LongId;
import jakarta.validation.constraints.NotEmpty;
@ -43,5 +44,5 @@ public class SiteDefaultTemplateDTO extends BaseDTO {
* 默认模板属性
*/
@NotEmpty(message = "{VALIDATOR.CMS.SITE.PUBLISH_PIPE_PROPS_EMPTY}")
public List<PublishPipeProp> publishPipeProps;
public List<PublishPipeProps> publishPipeProps;
}

View File

@ -13,13 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.contentcore.domain.dto;
package com.chestnut.contentcore.domain.pojo;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class PublishPipeProp {
/**
* PublishPipeProps
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Setter
@Getter
public class PublishPipeProps {
private String pipeCode;
@ -27,35 +38,11 @@ public class PublishPipeProp {
private Map<String, Object> props;
public static PublishPipeProp newInstance(String pipeCode, String pipeName, Map<String, Object> props) {
PublishPipeProp ppp = new PublishPipeProp();
public static PublishPipeProps newInstance(String pipeCode, String pipeName, Map<String, Object> props) {
PublishPipeProps ppp = new PublishPipeProps();
ppp.setPipeCode(pipeCode);
ppp.setPipeName(pipeName);
ppp.setProps(Objects.requireNonNullElse(props, new HashMap<>()));
return ppp;
}
public String getPipeCode() {
return pipeCode;
}
public void setPipeCode(String pipeCode) {
this.pipeCode = pipeCode;
}
public String getPipeName() {
return pipeName;
}
public void setPipeName(String pipeName) {
this.pipeName = pipeName;
}
public Map<String, Object> getProps() {
return props;
}
public void setProps(Map<String, Object> prop) {
this.props = prop;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.contentcore.domain.pojo;
/**
* PublishPipeTemplate
*
* @author 兮玥
* @email 190785909@qq.com
*/
public record PublishPipeTemplate(String name, String code, String template) {
}

View File

@ -21,7 +21,7 @@ import java.util.Map;
import org.springframework.beans.BeanUtils;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@ -186,7 +186,7 @@ public class CatalogVO {
/*
* 栏目发布通道数据
*/
private List<PublishPipeProp> publishPipeDatas;
private List<PublishPipeProps> publishPipeDatas;
public static CatalogVO newInstance(CmsCatalog catalog) {
CatalogVO dto = new CatalogVO();

View File

@ -16,7 +16,7 @@
package com.chestnut.contentcore.domain.vo;
import com.chestnut.contentcore.domain.InitByContent;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import lombok.Getter;
import lombok.Setter;
@ -217,7 +217,7 @@ public class ContentVO implements InitByContent {
/**
* 发布通道配置
*/
private List<PublishPipeProp> publishPipeProps;
private List<PublishPipeProps> publishPipeProps;
/**
* SEO标题

View File

@ -15,12 +15,13 @@
*/
package com.chestnut.contentcore.domain.vo;
import org.springframework.beans.BeanUtils;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.BeanUtils;
import java.util.List;
@Getter
@Setter
@ -64,13 +65,20 @@ public class PageWidgetVO {
/*
* 发布通道编码
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String publishPipeCode;
/*
* 模板路径
*/
@Deprecated(since = "1.5.6", forRemoval = true)
private String template;
/*
* 发布通道模板配置
*/
private List<PublishPipeTemplate> templates;
/*
* 静态化目录
*/

View File

@ -0,0 +1,47 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.contentcore.domain.vo;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class SiteDashboardVO {
private Long siteId;
private String name;
private List<PublishPipeProps> urls;
public static SiteDashboardVO create(CmsSite site, List<CmsPublishPipe> publishPipes) {
SiteDashboardVO vo = new SiteDashboardVO();
vo.setSiteId(site.getSiteId());
vo.setName(site.getName());
List<PublishPipeProps> urls = publishPipes.stream().map(pp -> PublishPipeProps.newInstance(
pp.getCode(), pp.getName(), Map.of("url", site.getUrl(pp.getCode()))
)).toList();
vo.setUrls(urls);
return vo;
}
}

View File

@ -17,25 +17,34 @@ package com.chestnut.contentcore.job;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.contentcore.core.IContent;
import com.chestnut.contentcore.core.IContentType;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.fixed.dict.ContentStatus;
import com.chestnut.contentcore.publish.staticize.CatalogStaticizeType;
import com.chestnut.contentcore.publish.staticize.ContentStaticizeType;
import com.chestnut.contentcore.publish.staticize.SiteStaticizeType;
import com.chestnut.contentcore.listener.event.AfterContentPublishEvent;
import com.chestnut.contentcore.publish.IPublishStrategy;
import com.chestnut.contentcore.publish.staticize.CatalogStaticizeType;
import com.chestnut.contentcore.publish.staticize.SiteStaticizeType;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IContentService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.ContentCoreUtils;
import com.chestnut.system.schedule.IScheduledHandler;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 定时发布任务<br/>
@ -47,7 +56,7 @@ import java.util.List;
*/
@RequiredArgsConstructor
@Component(IScheduledHandler.BEAN_PREFIX + SitePublishJobHandler.JOB_NAME)
public class SitePublishJobHandler extends IJobHandler implements IScheduledHandler {
public class SitePublishJobHandler extends IJobHandler implements IScheduledHandler, ApplicationContextAware {
static final String JOB_NAME = "SitePublishJobHandler";
@ -59,6 +68,8 @@ public class SitePublishJobHandler extends IJobHandler implements IScheduledHand
private final IPublishStrategy publishStrategy;
private ApplicationContext applicationContext;
@Override
public String getId() {
return JOB_NAME;
@ -74,6 +85,7 @@ public class SitePublishJobHandler extends IJobHandler implements IScheduledHand
logger.info("Job start: {}", JOB_NAME);
long s = System.currentTimeMillis();
List<CmsSite> sites = this.siteService.list();
Set<Long> catalogIds = new HashSet<>();
for (CmsSite site : sites) {
List<CmsCatalog> catalogList = catalogService
.list(new LambdaQueryWrapper<CmsCatalog>().eq(CmsCatalog::getSiteId, site.getSiteId()));
@ -89,15 +101,32 @@ public class SitePublishJobHandler extends IJobHandler implements IScheduledHand
for (int i = 0; i * pageSize < total; i++) {
Page<CmsContent> page = contentService.dao().page(new Page<>(i, pageSize, false), q);
for (CmsContent xContent : page.getRecords()) {
publishStrategy.publish(ContentStaticizeType.TYPE, xContent.getContentId().toString());
IContentType contentType = ContentCoreUtils.getContentType(xContent.getContentType());
IContent<?> content = contentType.loadContent(xContent);
if (content.publish()) {
applicationContext.publishEvent(new AfterContentPublishEvent(contentType, content));
}
}
}
if (total > 0) {
catalogIds.add(catalog.getCatalogId());
// 发布关联栏目内容所属栏目及其所有父级栏目
long parentId = catalog.getParentId();
while (parentId > 0) {
CmsCatalog parent = catalogService.getCatalog(parentId);
if (parent == null) {
break;
}
catalogIds.add(parent.getCatalogId());
parentId = parent.getParentId();
}
}
}
// 发布栏目
for (CmsCatalog catalog : catalogList) {
publishStrategy.publish(CatalogStaticizeType.TYPE, catalog.getCatalogId().toString());
}
// 发布站点
// 发布相关栏目
catalogIds.forEach(catalogId -> {
publishStrategy.publish(CatalogStaticizeType.TYPE, catalogId.toString());
});
// 发布站点首页
publishStrategy.publish(SiteStaticizeType.TYPE, site.getSiteId().toString());
}
logger.info("Job '{}' completed, cost: {}ms", JOB_NAME, System.currentTimeMillis() - s);
@ -108,4 +137,9 @@ public class SitePublishJobHandler extends IJobHandler implements IScheduledHand
public void execute() throws Exception {
this.exec();
}
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View File

@ -200,7 +200,7 @@ public class ContentCoreListener {
public void beforeCatalogDelete(BeforeCatalogDeleteEvent event) {
CmsCatalog catalog = event.getCatalog();
// 删除栏目内容
this.contentService.deleteContentsByCatalog(catalog, true, event.getOperator());
this.contentService.deleteContentsByCatalog(catalog, false, event.getOperator());
// 删除页面部件
this.pageWidgetService.deletePageWidgetsByCatalog(catalog);
}

View File

@ -15,17 +15,17 @@
*/
package com.chestnut.contentcore.properties;
import java.util.Map;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import com.chestnut.common.serializer.DesensitizationJsonSerializer;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 资源存储策略参数
@ -77,12 +77,14 @@ public class FileStorageArgsProperty implements IProperty {
@Getter
@Setter
public static class FileStorageArgs {
private String accessKey;
private String accessSecret;
private String endpoint;
private String region;
private String bucket;

View File

@ -54,12 +54,12 @@ public class ThumbnailHeightProperty implements IProperty {
@Override
public boolean validate(String value) {
return StringUtils.isEmpty(value) || NumberUtils.isCreatable(value);
return NumberUtils.isCreatable(value);
}
@Override
public Integer defaultValue() {
return 128;
return 218;
}
@Override

View File

@ -54,7 +54,7 @@ public class ThumbnailWidthProperty implements IProperty {
@Override
public boolean validate(String value) {
return StringUtils.isEmpty(value) || NumberUtils.isCreatable(value);
return NumberUtils.isCreatable(value);
}
@Override

View File

@ -16,6 +16,7 @@
package com.chestnut.contentcore.publish.staticize;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.StaticizeService;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.utils.IdUtils;
@ -30,9 +31,12 @@ import com.chestnut.contentcore.template.impl.SiteTemplateType;
import com.chestnut.contentcore.util.SiteUtils;
import com.chestnut.contentcore.util.TemplateUtils;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
@ -100,7 +104,11 @@ public class SiteStaticizeType implements IStaticizeType {
long s = System.currentTimeMillis();
context.setDirectory(SiteUtils.getSiteRoot(site, publishPipeCode));
context.setFirstFileName("index" + StringUtils.DOT + site.getStaticSuffix(publishPipeCode));
this.staticizeService.process(context);
StringWriter writer = new StringWriter();
this.staticizeService.process(context, writer);
String text = FreeMarkerUtils.createBy(writer.toString());
String filePath = context.getStaticizeFilePath(context.getPageIndex());
FileUtils.writeStringToFile(new File(filePath), text, StandardCharsets.UTF_8);
logger.debug("[{}]首页模板解析:{},耗时:{}ms", publishPipeCode, site.getName(), (System.currentTimeMillis() - s));
} catch (Exception e) {
logger.error(AsyncTaskManager.addErrMessage(StringUtils.messageFormat("[{0}][{1}]站点首页解析失败:{2}",

View File

@ -18,8 +18,10 @@ package com.chestnut.contentcore.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.common.async.AsyncTask;
import com.chestnut.common.domain.TreeNode;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.security.domain.LoginUser;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.utils.Assert;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.dto.*;
@ -96,7 +98,13 @@ public interface ICatalogService extends IService<CmsCatalog> {
* @param catalogId
* @return
*/
CmsCatalog deleteCatalog(long catalogId, LoginUser operator);
default void deleteCatalog(long catalogId, LoginUser operator) {
CmsCatalog catalog = getById(catalogId);
Assert.notNull(catalog, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("catalogId", catalog));
deleteCatalog(catalog, operator);
}
void deleteCatalog(CmsCatalog catalog, LoginUser operator);
/**
* 获取栏目链接

View File

@ -19,7 +19,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import java.io.IOException;
import java.util.List;
@ -79,8 +79,8 @@ public interface IPublishPipeService extends IService<CmsPublishPipe> {
* @param props 数据集合
* @return 结果列表
*/
List<PublishPipeProp> getPublishPipeProps(Long siteId, PublishPipePropUseType useType,
Map<String, Map<String, Object>> props);
List<PublishPipeProps> getPublishPipeProps(Long siteId, PublishPipePropUseType useType,
Map<String, Map<String, Object>> props);
/**
* 获取发布通道属性值

View File

@ -32,14 +32,12 @@ import java.util.List;
public interface IPublishService {
/**
* 发布站点首页<br/>
* <p>
* 发布站点首页<br>
* 此方法供API发布站点首页调用做基础校验
*
* @param site
* @return
* @throws IOException
* @throws TemplateException
* @param site 站点
* @throws IOException e1
* @throws TemplateException e2
*/
void publishSiteIndex(CmsSite site) throws IOException, TemplateException;
@ -48,20 +46,20 @@ public interface IPublishService {
* <p>
* 发布站点下所有栏目及指定状态内容
*
* @param site
* @param contentStatus
* @return
* @param site 站点
* @param contentStatus 内容状态
* @return 结果
*/
AsyncTask publishAll(CmsSite site, final String contentStatus, final LoginUser operator);
/**
* 站点首页页面内容
*
* @param site
* @param requestData
* @return
* @throws IOException
* @throws TemplateException
* @param site 站点
* @param requestData 请求数据
* @return 结果
* @throws IOException e1
* @throws TemplateException e2
*/
String getSitePageData(CmsSite site, IInternalDataType.RequestData requestData)
throws IOException, TemplateException;
@ -69,12 +67,12 @@ public interface IPublishService {
/**
* 获取栏目模板页面内容
*
* @param catalog
* @param requestData
* @param listFlag
* @return
* @throws IOException
* @throws TemplateException
* @param catalog 栏目
* @param requestData 请求数据
* @param listFlag 是否分页
* @return 结果
* @throws IOException e1
* @throws TemplateException e2
*/
String getCatalogPageData(CmsCatalog catalog, IInternalDataType.RequestData requestData, boolean listFlag)
throws IOException, TemplateException;
@ -82,11 +80,11 @@ public interface IPublishService {
/**
* 发布栏目异步任务
*
* @param catalog
* @param catalog 栏目
* @param publishChild 是否发布子栏目
* @param publishDetail 是否发布详情页
* @param publishStatus 指定发布内容状态
* @return
* @return 结果
*/
AsyncTask publishCatalog(CmsCatalog catalog, boolean publishChild, boolean publishDetail,
String publishStatus, final LoginUser operator);
@ -94,11 +92,11 @@ public interface IPublishService {
/**
* 获取内容模板页面结果
*
* @param content
* @param requestData
* @return
* @throws IOException
* @throws TemplateException
* @param content 内容
* @param requestData 请求数据
* @return 结果
* @throws IOException e1
* @throws TemplateException e2
*/
String getContentPageData(CmsContent content, IInternalDataType.RequestData requestData)
throws IOException, TemplateException;
@ -106,29 +104,27 @@ public interface IPublishService {
/**
* 发布内容创建异步任务发布
*
* @param content
* @param content 内容
*/
void asyncPublishContent(IContent<?> content);
/**
* 发布内容
*
* @param contentIds
* @return
* @throws IOException
* @throws TemplateException
* @param contentIds 内容ID列表
* @param operator 操作人
*/
void publishContent(List<Long> contentIds, LoginUser operator) ;
/**
* 获取内容扩展模板解析内容
*
* @param content
* @param publishPipeCode
* @param isPreview
* @return
* @throws IOException
* @throws TemplateException
* @param content 内容
* @param publishPipeCode 发布通道编码
* @param isPreview 是否预览
* @return 结果
* @throws IOException e1
* @throws TemplateException e2
*/
String getContentExPageData(CmsContent content, String publishPipeCode, boolean isPreview)
throws IOException, TemplateException;
@ -136,18 +132,19 @@ public interface IPublishService {
/**
* 获取页面部件模板解析内容
*
* @param pageWidget
* @param isPreview
* @return
* @throws IOException
* @throws TemplateException
* @param pageWidget 页面部件
* @param publishPipeCode 发布通道编码
* @param isPreview 是否预览
* @return 结果
* @throws IOException e1
* @throws TemplateException e2
*/
String getPageWidgetPageData(CmsPageWidget pageWidget, boolean isPreview) throws IOException, TemplateException;
String getPageWidgetPageData(CmsPageWidget pageWidget, String publishPipeCode, boolean isPreview) throws IOException, TemplateException;
/**
* 页面部件静态化
*
* @param pageWidget
* @param pageWidget 页面部件
*/
void pageWidgetStaticize(IPageWidget pageWidget);
}

View File

@ -16,8 +16,8 @@
package com.chestnut.contentcore.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.common.storage.IFileStorageType;
import com.chestnut.contentcore.core.IResourceType;
import com.chestnut.contentcore.core.InternalURL;
import com.chestnut.contentcore.domain.CmsResource;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.ResourceUploadDTO;
@ -26,14 +26,10 @@ import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
public interface IResourceService extends IService<CmsResource> {
/**
* 获取存储方式
*/
IFileStorageType getFileStorageType(String type);
/**
* 上传资源
*/
@ -102,4 +98,31 @@ public interface IResourceService extends IService<CmsResource> {
* @param operator 操作人
*/
String downloadRemoteImages(String html, CmsSite site, String operator);
/**
* 检查图片资源缩略图如果不存在则生成
*
* @param internalURL 内部链接
* @param width 缩略图宽度
* @param height 缩略图高度
*/
void createThumbnailIfNotExists(InternalURL internalURL, int width, int height) throws IOException;
/**
* 处理默认缩略图
*
* @param site 站点
* @param images 源图列表
* @param thumbnailsConsumer 缩略图消费者
*/
void dealDefaultThumbnail(CmsSite site, List<String> images, Consumer<List<String>> thumbnailsConsumer);
/**
* 处理内容引导图缩略图
*
* @param site 站点
* @param imageSrc 源图列表
* @param thumbnailConsumer 缩略图消费者
*/
void dealDefaultThumbnail(CmsSite site, String imageSrc, Consumer<String> thumbnailConsumer);
}

View File

@ -39,6 +39,7 @@ import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.*;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
import com.chestnut.contentcore.listener.event.*;
import com.chestnut.contentcore.mapper.CmsCatalogMapper;
@ -279,7 +280,7 @@ public class CatalogServiceImpl extends ServiceImpl<CmsCatalogMapper, CmsCatalog
BeanUtils.copyProperties(dto, catalog);
// 发布通道数据处理
Map<String, Map<String, Object>> publishPipeProps = dto.getPublishPipeDatas().stream()
.collect(Collectors.toMap(PublishPipeProp::getPipeCode, PublishPipeProp::getProps));
.collect(Collectors.toMap(PublishPipeProps::getPipeCode, PublishPipeProps::getProps));
catalog.setPublishPipeProps(publishPipeProps);
catalog.updateBy(dto.getOperator().getUsername());
this.updateById(catalog);
@ -303,8 +304,7 @@ public class CatalogServiceImpl extends ServiceImpl<CmsCatalogMapper, CmsCatalog
@Override
@Transactional(rollbackFor = Exception.class)
public CmsCatalog deleteCatalog(long catalogId, LoginUser operator) {
CmsCatalog catalog = this.getById(catalogId);
public void deleteCatalog(CmsCatalog catalog, LoginUser operator) {
long childCount = lambdaQuery().eq(CmsCatalog::getParentId, catalog.getCatalogId()).count();
Assert.isTrue(childCount == 0, ContentCoreErrorCode.DEL_CHILD_FIRST::exception);
// 删除前事件发布
@ -316,11 +316,10 @@ public class CatalogServiceImpl extends ServiceImpl<CmsCatalogMapper, CmsCatalog
parentCatalog.setChildCount(parentCatalog.getChildCount() - 1);
updateById(parentCatalog);
}
removeById(catalogId);
removeById(catalog);
clearCache(catalog);
applicationContext.publishEvent(new AfterCatalogDeleteEvent(this, catalog));
return catalog;
}
@Override
@ -387,8 +386,8 @@ public class CatalogServiceImpl extends ServiceImpl<CmsCatalogMapper, CmsCatalog
List<CmsCatalog> toCatalogs = this.list(q);
if (!toCatalogs.isEmpty()) {
for (CmsCatalog toCatalog : toCatalogs) {
List<PublishPipeProp> publishPipeProps = dto.getPublishPipeProps();
for (PublishPipeProp publishPipeProp : publishPipeProps) {
List<PublishPipeProps> publishPipeProps = dto.getPublishPipeProps();
for (PublishPipeProps publishPipeProp : publishPipeProps) {
Map<String, Object> sitePublishPipeProp = site.getPublishPipeProps()
.get(publishPipeProp.getPipeCode());
Map<String, Object> catalogPublishPipeProp = toCatalog
@ -604,6 +603,9 @@ public class CatalogServiceImpl extends ServiceImpl<CmsCatalogMapper, CmsCatalog
lock.lock();
try {
CmsCatalog catalog = getById(catalogId);
if (Objects.isNull(catalog)) {
return; // 删除栏目时会先删除栏目在删除内容导致删除内容时栏目已经不存在了
}
lambdaUpdate().set(CmsCatalog::getContentCount, catalog.getContentCount() + delta)
.eq(CmsCatalog::getCatalogId, catalog.getCatalogId()).update();
} finally {

View File

@ -156,6 +156,7 @@ public class ContentServiceImpl implements IContentService {
IContentType contentType = ContentCoreUtils.getContentType(content.getContentType());
IContent<?> icontent = contentType.loadContent(content);
icontent.setOperator(operator);
icontent.getParams().put(IContent.PARAM_IS_DELETE_BY_CATALOG, true);
icontent.delete();
});
}

View File

@ -33,12 +33,15 @@ import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.util.SiteUtils;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -149,27 +152,28 @@ public class FileServiceImpl implements IFileService {
@Override
public void renameFile(CmsSite site, String filePath, String rename) throws IOException {
this.checkFileType(rename);
String ext = FilenameUtils.getExtension(rename);
if (StringUtils.isNotEmpty(ext)) {
this.checkFileType(rename);
}
String path = FileExUtils.normalizePath(filePath);
if (path.startsWith("/")) {
path = path.substring(1);
}
this.checkSiteDirectory(site, filePath);
this.checkSiteDirectory(site, path);
String root = CMSConfig.getResourceRoot();
File file = new File(root + path);
Assert.isTrue(file.exists(), ContentCoreErrorCode.FILE_NOT_EXIST::exception);
String dest = root + StringUtils.substringBeforeLast(path, "/") + "/" + rename;
File destFile = new File(dest);
Assert.isFalse(destFile.exists(), ContentCoreErrorCode.FILE_ALREADY_EXISTS::exception);
Path dest = Path.of(file.getParent(), rename);
Assert.isFalse(Files.exists(dest), ContentCoreErrorCode.FILE_ALREADY_EXISTS::exception);
if (file.isDirectory()) {
FileUtils.moveDirectory(file, destFile);
FileUtils.moveDirectory(file, dest.toFile());
} else {
this.checkFileType(rename);
FileUtils.moveFile(file, destFile);
FileUtils.moveFile(file, dest.toFile());
}
}
@ -205,7 +209,8 @@ public class FileServiceImpl implements IFileService {
path = path.substring(1);
}
this.checkSiteDirectory(site, path);
if (!EDITABLE_FILE_TYPE.contains(FileExUtils.getExtension(path))) {
String ext = FilenameUtils.getExtension(path);
if (!EDITABLE_FILE_TYPE.contains(ext)) {
throw ContentCoreErrorCode.NOT_EDITABLE_FILE.exception();
}
String root = CMSConfig.getResourceRoot();

View File

@ -29,7 +29,7 @@ import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
import com.chestnut.contentcore.core.impl.PublishPipeProp_IndexTemplate;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.fixed.config.TemplateSuffix;
import com.chestnut.contentcore.mapper.CmsPublishPipeMapper;
import com.chestnut.contentcore.properties.EnableSiteDeleteBackupProperty;
@ -65,13 +65,13 @@ public class PublishPipeServiceImpl extends ServiceImpl<CmsPublishPipeMapper, Cm
private final List<IPublishPipeProp> publishPipeProps;
@Override
public List<PublishPipeProp> getPublishPipeProps(Long siteId, PublishPipePropUseType useType,
Map<String, Map<String, Object>> props) {
public List<PublishPipeProps> getPublishPipeProps(Long siteId, PublishPipePropUseType useType,
Map<String, Map<String, Object>> props) {
List<IPublishPipeProp> list = this.publishPipeProps.stream()
.filter(p -> p.getUseTypes().contains(useType))
.toList();
return this.getPublishPipes(siteId).stream().map(pp -> {
PublishPipeProp prop = PublishPipeProp.newInstance(pp.getCode(), pp.getName(),
PublishPipeProps prop = PublishPipeProps.newInstance(pp.getCode(), pp.getName(),
Objects.isNull(props) ? null : props.get(pp.getCode()));
list.forEach(p -> {
if (!prop.getProps().containsKey(p.getKey())) {

View File

@ -46,6 +46,7 @@ import com.chestnut.contentcore.util.*;
import com.chestnut.system.fixed.dict.YesOrNo;
import freemarker.template.TemplateException;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -106,7 +107,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
this.staticizeService.process(context, writer);
return writer.toString();
} finally {
logger.debug("[{}]首页模板解析:{}\t耗时{}ms", requestData.getPublishPipeCode(), site.getName(), System.currentTimeMillis() - s);
logger.debug("[{}]Parse index template: {}\t, cost: {}ms", requestData.getPublishPipeCode(), site.getName(), System.currentTimeMillis() - s);
}
}
@ -229,7 +230,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
if (listFlag) {
String catalogLink = this.catalogService.getCatalogListLink(catalog, 1, requestData.getPublishPipeCode(), requestData.isPreview());
templateContext.setFirstFileName(catalogLink);
templateContext.setOtherFileName(catalogLink + "&pi=" + TemplateContext.PlaceHolder_PageNo);
templateContext.setOtherFileName(TemplateUtils.appendPageIndexParam(catalogLink, TemplateContext.PlaceHolder_PageNo));
}
try (StringWriter writer = new StringWriter()) {
this.staticizeService.process(templateContext, writer);
@ -372,7 +373,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
// 分页链接
String contentLink = this.contentService.getContentLink(content, 1, requestData.getPublishPipeCode(), requestData.isPreview());
templateContext.setFirstFileName(contentLink);
templateContext.setOtherFileName(contentLink + "&pi=" + TemplateContext.PlaceHolder_PageNo);
templateContext.setOtherFileName(TemplateUtils.appendPageIndexParam(contentLink, TemplateContext.PlaceHolder_PageNo));
// staticize
this.staticizeService.process(templateContext, writer);
logger.debug("[{}][{}]内容模板解析:{},耗时:{}", requestData.getPublishPipeCode(), contentType.getId(), content.getTitle(),
@ -484,22 +485,20 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
}
@Override
public String getPageWidgetPageData(CmsPageWidget pageWidget, boolean isPreview)
public String getPageWidgetPageData(CmsPageWidget pageWidget, String publishPipeCode, boolean isPreview)
throws IOException, TemplateException {
CmsSite site = this.siteService.getById(pageWidget.getSiteId());
File templateFile = this.templateService.findTemplateFile(site, pageWidget.getTemplate(),
pageWidget.getPublishPipeCode());
Assert.notNull(templateFile,
() -> ContentCoreErrorCode.TEMPLATE_EMPTY.exception(pageWidget.getName(), pageWidget.getCode()));
String template = MapUtils.getString(pageWidget.getTemplates(), publishPipeCode, "");
File templateFile = this.templateService.findTemplateFile(site, template,
publishPipeCode);
Assert.notNull(templateFile, () -> ContentCoreErrorCode.TEMPLATE_EMPTY.exception(template));
// 生成静态页面
try (StringWriter writer = new StringWriter()) {
long s = System.currentTimeMillis();
// 模板ID = 通道:站点目录:模板文件名
String templateKey = SiteUtils.getTemplateKey(site, pageWidget.getPublishPipeCode(),
pageWidget.getTemplate());
TemplateContext templateContext = new TemplateContext(templateKey, isPreview,
pageWidget.getPublishPipeCode());
String templateKey = SiteUtils.getTemplateKey(site, publishPipeCode, template);
TemplateContext templateContext = new TemplateContext(templateKey, isPreview, publishPipeCode);
// init template global variables
TemplateUtils.initGlobalVariables(site, templateContext);
templateContext.getVariables().put(TemplateUtils.TemplateVariable_PageWidget, pageWidget);
@ -508,7 +507,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
templateType.initTemplateData(site.getSiteId(), templateContext);
// staticize
this.staticizeService.process(templateContext, writer);
logger.debug("[{}]页面部件【{}#{}】模板解析耗时:{}ms", pageWidget.getPublishPipeCode(), pageWidget.getName(),
logger.debug("[{}]页面部件【{}#{}】模板解析耗时:{}ms", publishPipeCode, pageWidget.getName(),
pageWidget.getCode(), System.currentTimeMillis() - s);
return writer.toString();
}
@ -516,23 +515,35 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
@Override
public void pageWidgetStaticize(IPageWidget pageWidget) {
long s = System.currentTimeMillis();
CmsPageWidget pw = pageWidget.getPageWidgetEntity();
CmsSite site = this.siteService.getSite(pw.getSiteId());
File templateFile = this.templateService.findTemplateFile(site, pw.getTemplate(), pw.getPublishPipeCode());
if (Objects.isNull(templateFile)) {
logger.warn(StringUtils.messageFormat("页面部件【{0}%s#{1}%s】模板未配置或文件不存在", pw.getName(), pw.getCode()));
if (StringUtils.isEmpty(pw.getTemplates())) {
logger.warn("页面部件【{}#{}】未配置模板", pw.getName(), pw.getCode());
return;
}
CmsSite site = this.siteService.getSite(pw.getSiteId());
List<CmsPublishPipe> allPublishPipes = this.publishPipeService.getAllPublishPipes(pw.getSiteId());
for (CmsPublishPipe publishPipe : allPublishPipes) {
String template = pw.getTemplate(publishPipe.getCode());
File templateFile = this.templateService.findTemplateFile(site, template, publishPipe.getCode());
if (Objects.nonNull(templateFile)) {
pageWidgetStaticize0(site, pw, publishPipe.getCode(), template);
} else {
logger.warn("页面部件【{}#{}】模板未配置或文件不存在", pw.getName(), pw.getCode());
}
}
}
private void pageWidgetStaticize0(CmsSite site, CmsPageWidget pw, String publishPipeCode, String template) {
long s = System.currentTimeMillis();
try {
// 静态化目录
String dirPath = SiteUtils.getSiteRoot(site, pw.getPublishPipeCode()) + pw.getPath();
String dirPath = SiteUtils.getSiteRoot(site, publishPipeCode) + pw.getPath();
FileExUtils.mkdirs(dirPath);
// 自定义模板上下文
String templateKey = SiteUtils.getTemplateKey(site, pw.getPublishPipeCode(), pw.getTemplate());
TemplateContext templateContext = new TemplateContext(templateKey, false, pw.getPublishPipeCode());
String templateKey = SiteUtils.getTemplateKey(site, publishPipeCode, template);
TemplateContext templateContext = new TemplateContext(templateKey, false, publishPipeCode);
templateContext.setDirectory(dirPath);
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(pw.getPublishPipeCode()));
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(publishPipeCode));
templateContext.setFirstFileName(staticFileName);
// init template datamode
TemplateUtils.initGlobalVariables(site, templateContext);
@ -542,10 +553,10 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
templateType.initTemplateData(site.getSiteId(), templateContext);
// staticize
this.staticizeService.process(templateContext);
logger.debug("[{}]页面部件模板解析:{},耗时:{}ms", pw.getPublishPipeCode(), pw.getCode(), System.currentTimeMillis() - s);
logger.debug("[{}]页面部件模板解析:{},耗时:{}ms", publishPipeCode, pw.getCode(), System.currentTimeMillis() - s);
} catch (TemplateException | IOException e) {
logger.error(AsyncTaskManager.addErrMessage(StringUtils.messageFormat("[{0}]页面部件模板解析失败:{1}#{2}",
pw.getPublishPipeCode(), pw.getName(), pw.getCode())), e);
publishPipeCode, pw.getName(), pw.getCode())), e);
}
}

View File

@ -18,15 +18,15 @@ package com.chestnut.contentcore.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.storage.FileStorageService;
import com.chestnut.common.storage.IFileStorageType;
import com.chestnut.common.storage.StorageReadArgs;
import com.chestnut.common.storage.StorageReadArgs.StorageReadArgsBuilder;
import com.chestnut.common.storage.exception.StorageErrorCode;
import com.chestnut.common.utils.*;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.common.utils.image.ImageHelper;
import com.chestnut.common.utils.image.ImageUtils;
import com.chestnut.contentcore.core.IResourceStat;
import com.chestnut.contentcore.core.IResourceType;
import com.chestnut.contentcore.core.InternalURL;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
import com.chestnut.contentcore.core.impl.ResourceType_Image;
import com.chestnut.contentcore.domain.CmsResource;
@ -34,32 +34,35 @@ import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.ResourceUploadDTO;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
import com.chestnut.contentcore.mapper.CmsResourceMapper;
import com.chestnut.contentcore.properties.FileStorageArgsProperty;
import com.chestnut.contentcore.properties.FileStorageArgsProperty.FileStorageArgs;
import com.chestnut.contentcore.properties.FileStorageTypeProperty;
import com.chestnut.contentcore.properties.ThumbnailHeightProperty;
import com.chestnut.contentcore.properties.ThumbnailWidthProperty;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.*;
import com.chestnut.system.fixed.dict.EnableOrDisable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Matcher;
@Service
@RequiredArgsConstructor
public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResource> implements IResourceService {
private final Map<String, IFileStorageType> fileStorageTypes;
private final FileStorageService fileStorageService;
private final List<IResourceStat> resourceStats;
@ -144,23 +147,42 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
CmsResource resource = this.getById(dto.getResourceId());
Assert.notNull(resource, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("resourceId", dto.getResourceId()));
String oldResourceType = resource.getResourceType();
String oldPath = resource.getPath();
resource.setResourceType(resourceType.getId());
resource.setFileName(dto.getFile().getOriginalFilename());
resource.setName(StringUtils.isEmpty(dto.getName()) ? dto.getFile().getOriginalFilename() : dto.getName());
resource.setSuffix(suffix);
String fileName = resource.getResourceId() + StringUtils.DOT + suffix;
String path = StringUtils.substringBeforeLast(resource.getPath(), "/") + fileName;
String path = StringUtils.substringBeforeLast(resource.getPath(), "/") + "/" + fileName;
resource.setPath(path);
resource.updateBy(dto.getOperator().getUsername());
resource.setRemark(dto.getRemark());
// 删除原文件
if (!resource.getPath().equals(oldPath)) {
String fileStorageType = FileStorageTypeProperty.getValue(dto.getSite().getConfigProps());
IFileStorageType fst = fileStorageService.getFileStorageType(fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, dto.getSite());
deleteResource(oldPath, oldResourceType, fileStorageHelper);
}
// 处理资源
this.processResource(resource, resourceType, dto.getSite(), dto.getFile().getBytes());
this.updateById(resource);
return resource;
}
private void deleteResource(String path, String resourceType, FileStorageHelper fileStorageHelper) {
fileStorageHelper.remove(path);
// 删除图片缩略图
if (ResourceType_Image.ID.equals(resourceType)) {
String fileName = StringUtils.substringAfterLast(path, "/");
final String fileNamePrefix = StringUtils.substringBeforeLast(fileName, ".") + "_";
path = StringUtils.substringBeforeLast(path, "/") + "/" + fileNamePrefix;
fileStorageHelper.removeByPrefix(path);
}
}
@Override
public CmsResource addBase64Image(CmsSite site, String operator, String base64Data) throws IOException {
if (!ImageUtils.isBase64Image(base64Data)) {
@ -234,17 +256,36 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
resource.setWidth(0);
resource.setHeight(0);
resource.setFileSize((long) bytes.length);
// 读取存储配置
String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = getFileStorageType(fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
// 写入磁盘/OSS
fileStorageHelper.write(resource.getPath(), bytes);
// 设置资源参数
resource.setStorageType(fileStorageType);
resource.setInternalUrl(InternalDataType_Resource.getInternalUrl(resource));
// 默认缩略图处理
resourceType.asyncProcess(resource);
// 保存临时文件处理
String tempDirectory = ResourceUtils.getResourceTempDirectory(site);
File tempFile = new File(tempDirectory + resource.getPath());
FileUtils.writeByteArrayToFile(tempFile, bytes);
List<File> processed = resourceType.process(resource, tempFile);
try {
// 读取存储配置
String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = fileStorageService.getFileStorageType(fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
// 写入磁盘/OSS
fileStorageHelper.write(resource.getPath(), tempFile);
if (!processed.isEmpty()) {
String prefix = StringUtils.substringBeforeLast(resource.getPath(), "/") + "/";
for (File f : processed) {
fileStorageHelper.write(prefix + f.getName(), f);
}
}
// 设置资源参数
resource.setStorageType(fileStorageType);
resource.setInternalUrl(InternalDataType_Resource.getInternalUrl(resource));
// 异步后续处理
resourceType.asyncProcess(resource);
} finally {
// 删除临时文件
FileUtils.delete(tempFile);
for (File file : processed) {
FileUtils.delete(file);
}
}
}
@Override
@ -252,26 +293,13 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
List<CmsResource> resources = this.listByIds(resourceIds);
if (!resources.isEmpty()) {
CmsSite site = siteService.getSite(resources.get(0).getSiteId());
String siteRoot = SiteUtils.getSiteResourceRoot(site);
String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = fileStorageService.getFileStorageType(fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
resources.forEach(r -> {
// 删除资源文件
try {
File file = new File(siteRoot + r.getPath());
FileUtils.delete(file);
final String fileNamePrefix = StringUtils.substringBeforeLast(file.getName(), ".");
// 删除图片缩略图
File[] others = file.getParentFile().listFiles(
(dir, name) -> name.startsWith(fileNamePrefix)
);
if (Objects.nonNull(others)) {
for (File f : others) {
FileUtils.delete(f);
}
}
} catch (IOException e) {
log.error("Delete resource file failed: " + r.getPath());
}
// 删除数据库记录
// 删除文件
deleteResource(r.getPath(), r.getResourceType(), fileStorageHelper);
// 删除数据库记录
this.removeById(r.getResourceId());
});
}
@ -287,31 +315,15 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
@Override
public void downloadResource(CmsResource resource, HttpServletResponse response) {
CmsSite site = this.siteService.getSite(resource.getSiteId());
IFileStorageType storagetType = this.getFileStorageType(resource.getStorageType());
StorageReadArgsBuilder builder = StorageReadArgs.builder();
FileStorageArgs fileStorageArgs = FileStorageArgsProperty.getValue(site.getConfigProps());
if (fileStorageArgs != null) {
builder.endpoint(fileStorageArgs.getEndpoint())
.accessKey(fileStorageArgs.getAccessKey())
.accessSecret(fileStorageArgs.getAccessSecret())
.bucket(fileStorageArgs.getBucket())
.path(resource.getPath());
}
InputStream is = storagetType.read(builder.build());
try {
IFileStorageType storagetType = fileStorageService.getFileStorageType(resource.getStorageType());
FileStorageHelper fileStorageHelper = FileStorageHelper.of(storagetType, site);
try (InputStream is = fileStorageHelper.read(resource.getPath())) {
IOUtils.copy(is, response.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public IFileStorageType getFileStorageType(String type) {
IFileStorageType fileStorageType = fileStorageTypes.get(IFileStorageType.BEAN_NAME_PREIFX + type);
Assert.notNull(fileStorageType, () -> StorageErrorCode.UNSUPPORTED_STORAGE_TYPE.exception(type));
return fileStorageType;
}
/**
* 解析html中的图片标签如果是远程地址图片则下载到资源库中并将图片标签的src替换为资源内部链接
*
@ -359,13 +371,83 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
return sb.toString();
}
@Override
public void createThumbnailIfNotExists(InternalURL internalUrl, int width, int height) throws IOException {
if (!InternalDataType_Resource.ID.equals(internalUrl.getType())
|| !ResourceType_Image.isImage(internalUrl.getPath())) {
return; // 非图片资源忽略
}
Long siteId = MapUtils.getLong(internalUrl.getParams(), InternalDataType_Resource.InternalUrl_Param_SiteId);
CmsSite site = siteService.getSite(siteId);
String filePath = internalUrl.getPath();
String thumbnailPath = ImageUtils.getThumbnailFileName(filePath, width, height);
String storageTypeId = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = fileStorageService.getFileStorageType(storageTypeId);
FileStorageHelper storageHelper = FileStorageHelper.of(fst, site);
if (storageHelper.exists(thumbnailPath)) {
return;
}
CmsResource resource = this.getById(internalUrl.getId());
if (Objects.nonNull(resource)) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (InputStream is = storageHelper.read(resource.getPath())) {
String format = FileExUtils.getExtension(resource.getPath());
ImageHelper.of(is).format(format).resize(width, height).to(os);
}
storageHelper.write(thumbnailPath, os.toByteArray());
}
}
}
@Override
public void dealDefaultThumbnail(CmsSite site, List<String> internalUrls, Consumer<List<String>> consumer) {
if (internalUrls.isEmpty()) {
return;
}
int w = ThumbnailWidthProperty.getValue(site.getConfigProps());
int h = ThumbnailHeightProperty.getValue(site.getConfigProps());
if (w > 0 && h > 0) {
List<String> thumbnails = dealThumbnails(internalUrls, w, h);
consumer.accept(thumbnails);
}
}
@Override
public void dealDefaultThumbnail(CmsSite site, String internalUrl, Consumer<String> consumer) {
if (StringUtils.isEmpty(internalUrl)) {
return;
}
dealDefaultThumbnail(site, List.of(internalUrl), thumbnails -> consumer.accept(thumbnails.get(0)));
}
private List<String> dealThumbnails(List<String> internalUrls, int w, int h) {
return internalUrls.stream().map(iurl -> {
try {
InternalURL internalURL = InternalUrlUtils.parseInternalUrl(iurl);
if (Objects.nonNull(internalURL)) {
// 先检查是否存在缩略图如果不存在需要生成
createThumbnailIfNotExists(internalURL, w, h);
// 返回缩略图路径
return ImageUtils.getThumbnailFileName(InternalUrlUtils.getActualPreviewUrl(internalURL), w, h);
}
// 非内部链接返回原路径
return iurl;
} catch (Exception e) {
log.error("Generate thumbnail fail: " + iurl, e);
return iurl;
}
}).toList();
}
/**
* 统计资源引用
*/
public void statResourceUsage(Long siteId) throws InterruptedException {
Map<Long, Long> quotedResources = new HashMap<>();
for (IResourceStat resourceStat : this.resourceStats) {
resourceStat.statQuotedResource(siteId, quotedResources);
resourceStat.statQuotedResource(siteId, quotedResources);
}
}
}

View File

@ -19,6 +19,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.security.domain.LoginUser;
import com.chestnut.common.storage.FileStorageService;
import com.chestnut.common.storage.IFileStorageType;
import com.chestnut.common.utils.*;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.contentcore.ContentCoreConsts;
@ -27,7 +29,7 @@ import com.chestnut.contentcore.config.CMSConfig;
import com.chestnut.contentcore.core.IProperty;
import com.chestnut.contentcore.core.IPublishPipeProp;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.domain.dto.SiteDTO;
import com.chestnut.contentcore.domain.dto.SiteDefaultTemplateDTO;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
@ -39,9 +41,11 @@ import com.chestnut.contentcore.mapper.CmsSiteMapper;
import com.chestnut.contentcore.perms.SitePermissionType;
import com.chestnut.contentcore.perms.SitePermissionType.SitePrivItem;
import com.chestnut.contentcore.properties.EnableSiteDeleteBackupProperty;
import com.chestnut.contentcore.properties.FileStorageTypeProperty;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.CmsPrivUtils;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
import com.chestnut.contentcore.util.FileStorageHelper;
import com.chestnut.contentcore.util.SiteUtils;
import com.chestnut.system.security.StpAdminUtil;
import com.chestnut.system.service.ISysPermissionService;
@ -77,6 +81,10 @@ public class SiteServiceImpl extends ServiceImpl<CmsSiteMapper, CmsSite> impleme
private final SiteMonitoredCache siteCache;
private final FileStorageService fileStorageService;
private final AsyncTaskManager asyncTaskManager;
@Override
public CmsSite getSite(Long siteId) {
CmsSite site = siteCache.getCache(siteId, () -> this.getById(siteId));
@ -165,7 +173,7 @@ public class SiteServiceImpl extends ServiceImpl<CmsSiteMapper, CmsSite> impleme
prop.getProps().entrySet().removeIf(e -> !publishPipeProps.containsKey(IPublishPipeProp.BEAN_PREFIX + e.getKey()));
});
Map<String, Map<String, Object>> publishPipeProps = dto.getPublishPipeDatas().stream()
.collect(Collectors.toMap(PublishPipeProp::getPipeCode, PublishPipeProp::getProps));
.collect(Collectors.toMap(PublishPipeProps::getPipeCode, PublishPipeProps::getProps));
site.setPublishPipeProps(publishPipeProps);
site.updateBy(dto.getOperator().getUsername());
this.updateById(site);
@ -226,8 +234,8 @@ public class SiteServiceImpl extends ServiceImpl<CmsSiteMapper, CmsSite> impleme
@Override
public void saveSiteDefaultTemplate(SiteDefaultTemplateDTO dto) {
CmsSite site = this.getSite(dto.getSiteId());
List<PublishPipeProp> publishPipeProps = dto.getPublishPipeProps();
for (PublishPipeProp ppp : publishPipeProps) {
List<PublishPipeProps> publishPipeProps = dto.getPublishPipeProps();
for (PublishPipeProps ppp : publishPipeProps) {
Map<String, Object> sitePublishPipeProps = site.getPublishPipeProps(ppp.getPipeCode());
sitePublishPipeProps.putAll(ppp.getProps());
}
@ -245,6 +253,13 @@ public class SiteServiceImpl extends ServiceImpl<CmsSiteMapper, CmsSite> impleme
site.updateBy(operator);
this.updateById(site);
this.clearCache(site.getSiteId());
// 重新加载StorageClient
asyncTaskManager.execute(() -> {
String fileStorageType = FileStorageTypeProperty.getValue(site.getConfigProps());
IFileStorageType fst = fileStorageService.getFileStorageType(fileStorageType);
FileStorageHelper fileStorageHelper = FileStorageHelper.of(fst, site);
fileStorageHelper.reloadClient();
});
}
@Override

View File

@ -308,6 +308,9 @@ public class SiteThemeService {
} finally {
FileUtils.deleteDirectory(new File(destDir));
this.setProgressInfo(100, "导入完成");
if (log.isDebugEnabled() && StringUtils.isNotEmpty(this.getErrMessages())) {
this.getErrMessages().forEach(log::debug);
}
}
}
};

View File

@ -65,7 +65,7 @@ public class FileExtractorFunction extends AbstractFunc {
@Override
public Object exec0(Object... args) throws TemplateModelException {
if (args.length < 1) {
return StringUtils.EMPTY;
return List.of();
}
List<String> suffixArray = List.of();
if (args.length == 2 && Objects.nonNull(args[1])) {

View File

@ -18,14 +18,10 @@ package com.chestnut.contentcore.template.func;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.staticize.func.AbstractFunc;
import com.chestnut.common.storage.local.LocalFileStorageType;
import com.chestnut.common.utils.ObjectUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.common.utils.image.ImageHelper;
import com.chestnut.contentcore.core.InternalURL;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
import com.chestnut.contentcore.domain.CmsResource;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.util.InternalUrlUtils;
@ -33,15 +29,11 @@ import com.chestnut.contentcore.util.SiteUtils;
import freemarker.core.Environment;
import freemarker.template.SimpleNumber;
import freemarker.template.SimpleScalar;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateModelException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@ -67,8 +59,6 @@ public class ImageSizeFunction extends AbstractFunc {
private static final String ARG3_NAME = "{FREEMARKER.FUNC." + FUNC_NAME + ".Arg3.Name}";
private static final String ARG4_NAME = "{FREEMARKER.FUNC." + FUNC_NAME + ".Arg4.Name}";
private final ISiteService siteService;
private final IResourceService resourceService;
@ -97,48 +87,27 @@ public class ImageSizeFunction extends AbstractFunc {
if (!InternalUrlUtils.isInternalUrl(iurl)) {
return iurl; // 非内部链接直接返回
}
boolean crop = false; // 是否在缩放后进行居中裁剪默认false
if (args.length == 4) {
crop = ((TemplateBooleanModel) args[3]).getAsBoolean();
}
TemplateContext context = FreeMarkerUtils.getTemplateContext(Environment.getCurrentEnvironment());
InternalURL internalUrl = InternalUrlUtils.parseInternalUrl(iurl);
if (Objects.isNull(internalUrl) || !InternalDataType_Resource.ID.equals(internalUrl.getType())) {
return iurl;
}
String actualUrl = InternalUrlUtils.getActualUrl(internalUrl, context.getPublishPipeCode(),
context.isPreview());
String originalUrl = InternalUrlUtils.getActualUrl(internalUrl, context.getPublishPipeCode(), context.isPreview());
String siteResourceRoot = SiteUtils.getSiteResourceRoot(siteService.getSite(Long.valueOf(internalUrl.getParams().get("sid"))));
String destPath = StringUtils.substringBeforeLast(internalUrl.getPath(), ".") + "_" + width + "x" + height
+ "." + StringUtils.substringAfterLast(internalUrl.getPath(), ".");
String thumbnailUrl = StringUtils.substringBeforeLast(originalUrl, ".") + "_" + width + "x" + height + "."
+ StringUtils.substringAfterLast(originalUrl, ".");
if (Files.exists(Path.of(siteResourceRoot + destPath))) {
return StringUtils.substringBeforeLast(actualUrl, ".") + "_" + width + "x" + height + "."
+ StringUtils.substringAfterLast(actualUrl, ".");
return thumbnailUrl;
}
try {
CmsResource resource = this.resourceService.getById(internalUrl.getId());
if (Objects.nonNull(resource) && LocalFileStorageType.TYPE.equals(resource.getStorageType())) {
if (crop) {
String ext = FileExUtils.getExtension(resource.getPath());
File file = new File(siteResourceRoot + resource.getPath());
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (FileInputStream is = new FileInputStream(file)) {
ImageHelper.of(is).format(ext).centerCropAndResize(width, height).to(os);
}
Files.write(file.toPath(), os.toByteArray());
}
} else {
ImageHelper.of(new File(siteResourceRoot + resource.getPath()))
.resize(width, height)
.toFile(new File(siteResourceRoot + destPath));
}
actualUrl = StringUtils.substringBeforeLast(actualUrl, ".") + "_" + width + "x" + height + "."
+ StringUtils.substringAfterLast(actualUrl, ".");
}
resourceService.createThumbnailIfNotExists(internalUrl, width, height);
return thumbnailUrl;
} catch (Exception e) {
log.warn("Generate thumbnail failed: " + actualUrl, e);
log.warn("Generate thumbnail failed: " + originalUrl, e);
}
return actualUrl;
return originalUrl;
}
@Override
@ -146,8 +115,7 @@ public class ImageSizeFunction extends AbstractFunc {
return List.of(
new FuncArg(ARG1_NAME, FuncArgType.String, true, ARG1_DESC),
new FuncArg(ARG2_NAME, FuncArgType.Int, true),
new FuncArg(ARG3_NAME, FuncArgType.Int, true),
new FuncArg(ARG4_NAME, FuncArgType.Boolean, false)
new FuncArg(ARG3_NAME, FuncArgType.Int, true)
);
}
}

View File

@ -111,17 +111,18 @@ public class CmsPageWidgetTag extends AbstractTag {
if (!PageWidgetStatus.PUBLISHED.equals(pw.getState())) {
return null;
}
String template = pw.getTemplate(context.getPublishPipeCode());
CmsSite site = this.siteService.getSite(siteId);
File templateFile = this.templateService.findTemplateFile(site, pw.getTemplate(), context.getPublishPipeCode());
Assert.notNull(templateFile, () -> new IncludeTemplateNotFoundException(pw.getTemplate(), env));
File templateFile = this.templateService.findTemplateFile(site, template, context.getPublishPipeCode());
Assert.notNull(templateFile, () -> new IncludeTemplateNotFoundException(template, env));
boolean ssi = MapUtils.getBoolean(attrs, ATTR_SSI, EnableSSIProperty.getValue(site.getConfigProps()));
String templateKey = SiteUtils.getTemplateKey(site, pw.getPublishPipeCode(), pw.getTemplate());
String templateKey = SiteUtils.getTemplateKey(site, context.getPublishPipeCode(), template);
if (context.isPreview()) {
env.getOut().write(this.processTemplate(env, pw, templateKey));
} else {
String siteRoot = SiteUtils.getSiteRoot(site, context.getPublishPipeCode());
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(pw.getPublishPipeCode()));
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(context.getPublishPipeCode()));
String staticFilePath = pw.getPath() + staticFileName;
if (ssi) {
// 读取页面部件静态化内容

View File

@ -15,21 +15,17 @@
*/
package com.chestnut.contentcore.util;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.collections4.MapUtils;
import com.chestnut.common.utils.NumberUtils;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IProperty;
import com.chestnut.contentcore.core.IProperty.UseType;
import com.chestnut.contentcore.exception.ContentCoreErrorCode;
import org.apache.commons.collections4.MapUtils;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public class ConfigPropertyUtils {
@ -120,9 +116,28 @@ public class ConfigPropertyUtils {
String v = null;
if (prop != null) {
v = MapUtils.getString(firstProps, prop.getId());
if (StringUtils.isEmpty(v)) {
if (Objects.isNull(v) && Objects.nonNull(secondProps)) {
v = MapUtils.getString(secondProps, prop.getId());
}
if (Objects.isNull(v)) {
v = prop.defaultValue().toString();
}
}
return v;
}
public static Object getValue(String propertyKey, Map<String, String> firstProps,
Map<String, String> secondProps) {
IProperty prop = getConfigProperty(propertyKey);
Object v = null;
if (prop != null) {
v = MapUtils.getObject(firstProps, prop.getId());
if (Objects.isNull(v) && Objects.nonNull(secondProps)) {
v = MapUtils.getObject(secondProps, prop.getId());
}
if (Objects.isNull(v)) {
v = prop.defaultValue();
}
}
return v;
}
@ -151,12 +166,20 @@ public class ConfigPropertyUtils {
int intV = 0;
if (prop != null) {
String v = MapUtils.getString(firstProps, prop.getId());
if (Objects.isNull(v) && Objects.nonNull(secondProps)) {
v = MapUtils.getString(secondProps, prop.getId());
}
if (NumberUtils.isCreatable(v)) {
intV = NumberUtils.toInt(v);
} else {
v = MapUtils.getString(secondProps, prop.getId());
if (NumberUtils.isCreatable(v)) {
intV = NumberUtils.toInt(v);
Object defaultV = prop.defaultValue();
if (defaultV instanceof Integer defaultIntV) {
intV = defaultIntV;
} else {
v = defaultV.toString();
if (NumberUtils.isCreatable(v)) {
intV = NumberUtils.toInt(v);
}
}
}
}

View File

@ -15,19 +15,16 @@
*/
package com.chestnut.contentcore.util;
import com.chestnut.common.storage.IFileStorageType;
import com.chestnut.common.storage.StorageListArgs;
import com.chestnut.common.storage.StorageReadArgs;
import com.chestnut.common.storage.StorageWriteArgs;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.storage.*;
import com.chestnut.common.storage.local.LocalFileStorageType;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.properties.FileStorageArgsProperty;
import com.chestnut.contentcore.properties.FileStorageTypeProperty;
import lombok.Getter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.util.List;
/**
@ -48,6 +45,8 @@ public class FileStorageHelper {
private String endpoint;
private String region;
private String pipeline;
@Getter
@ -66,36 +65,87 @@ public class FileStorageHelper {
helper.accessSecret = args.getAccessSecret();
helper.bucket = args.getBucket();
helper.endpoint = args.getEndpoint();
helper.region = args.getRegion();
helper.pipeline = args.getPipeline();
helper.domain = args.getDomain();
helper.validate();
return helper;
}
private void validate() {
if (LocalFileStorageType.TYPE.equals(this.fileStorage.getType())) {
return; // 本地存储不需要校验
}
if (StringUtils.isEmpty(this.accessKey)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.accessKey");
}
if (StringUtils.isEmpty(this.accessSecret)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.accessSecret");
}
if (StringUtils.isEmpty(this.endpoint)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.endpoint");
}
if (StringUtils.isEmpty(this.region)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.region");
}
if (StringUtils.isEmpty(this.bucket)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.bucket");
}
if (StringUtils.isEmpty(this.domain)) {
throw CommonErrorCode.NOT_EMPTY.exception("fileStorage.domain");
}
}
public void reloadClient() {
fileStorage.reloadClient(this.endpoint, this.region, this.accessKey, this.accessSecret);
}
public boolean exists(String thumbnailPath) {
StorageExistArgs args = StorageExistArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.path(thumbnailPath)
.build();
return fileStorage.exists(args);
}
public InputStream read(String path) {
StorageReadArgs storageReadArgs = StorageReadArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.path(path)
.build();
return fileStorage.read(storageReadArgs);
}
public void write(String path, byte[] bytes) throws IOException {
try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) {
write(path, is);
public void write(String path, File file) throws IOException {
try (FileInputStream is = new FileInputStream(file)) {
write(path, is, file.length());
}
}
public void write(String path, InputStream is) {
public void write(String path, byte[] bytes) throws IOException {
try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) {
write(path, is, bytes.length);
}
}
public void write(String path, InputStream is, long length) {
StorageWriteArgs args = StorageWriteArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.path(path)
.inputStream(is)
.length(length)
.build();
this.fileStorage.write(args);
}
@ -110,9 +160,46 @@ public class FileStorageHelper {
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.prefix(prefix)
.maxKeys(count)
.build();
return this.fileStorage.list(storageListArgs);
}
public void remove(String path) {
StorageRemoveArgs args = StorageRemoveArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.path(path)
.build();
this.fileStorage.remove(args);
}
public void removeByPrefix(String prefix) {
StorageListArgs storageListArgs = StorageListArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.prefix(prefix)
.maxKeys(100)
.build();
List<String> list = this.fileStorage.list(storageListArgs);
for (String path : list) {
StorageRemoveArgs args = StorageRemoveArgs.builder()
.accessKey(this.accessKey)
.accessSecret(this.accessSecret)
.bucket(this.bucket)
.endpoint(this.endpoint)
.region(this.region)
.path(path)
.build();
this.fileStorage.remove(args);
}
}
}

View File

@ -94,6 +94,10 @@ public class InternalUrlUtils {
return getActualUrl(iurl, null, true);
}
public static String getActualPreviewUrl(InternalURL internalURL) {
return getActualUrl(internalURL, null, true);
}
/**
* 判断是否是内部资源URL
*/

View File

@ -17,6 +17,7 @@ package com.chestnut.contentcore.util;
import cn.dev33.satoken.config.SaTokenConfig;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.StaticizeConstants;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.utils.ReflectASMUtils;
import com.chestnut.common.utils.StringUtils;
@ -44,7 +45,7 @@ public class TemplateUtils {
/**
* 模板变量请求参数
*/
public final static String TemplateVariable_Request = "Request";
public final static String TemplateVariable_Request = StaticizeConstants.TemplateVariable_Request;
/**
* 模板变量<@cms_include>标签file属性请求参数下个大版本移除
@ -252,33 +253,6 @@ public class TemplateUtils {
}
}
public static String appendTokenParameter(String url, Environment env) throws TemplateModelException {
if (StringUtils.isEmpty(url)) {
return url;
}
String tokenName = FreeMarkerUtils.getStringVariable(env, TemplateUtils.TemplateVariable_TokenName);
String token = FreeMarkerUtils.getStringVariable(env, TemplateUtils.TemplateVariable_Token);
if (url.contains("?")) {
return url + "&" + tokenName + "=" + token;
}
return url + "?" + token + "=" + token;
}
public static String appendTokenParameter(String url) {
if (StringUtils.isEmpty(url)) {
return url;
}
SaTokenConfig config = StpAdminUtil.getStpLogic().getConfigOrGlobal();
String token = StpAdminUtil.getTokenValue();
if (StringUtils.isNotEmpty(config.getTokenPrefix())) {
token = config.getTokenPrefix() + " " + token;
}
if (url.contains("?")) {
return url + "&" + config.getTokenName() + "=" + token;
}
return url + "?" + config.getTokenName() + "=" + token;
}
/**
* 页面区块静态文件相对路径
@ -293,4 +267,36 @@ public class TemplateUtils {
includeTemplateName.length() - TemplateSuffix.getValue().length())
+ "." + site.getStaticSuffix(publishPipeCode);
}
public static String appendParam(String path, String name, String value) {
if (path.contains("?")) {
return path + "&" + name + "=" + value;
}
return path + "?" + name + "=" + value;
}
public static String appendPageIndexParam(String path, String pageIndex) {
return appendParam(path, StaticizeConstants.TemplateParam_PageIndex, pageIndex);
}
public static String appendTokenParameter(String url, Environment env) throws TemplateModelException {
if (StringUtils.isEmpty(url)) {
return url;
}
String tokenName = FreeMarkerUtils.getStringVariable(env, TemplateUtils.TemplateVariable_TokenName);
String token = FreeMarkerUtils.getStringVariable(env, TemplateUtils.TemplateVariable_Token);
return appendParam(url, tokenName, token);
}
public static String appendTokenParameter(String url) {
if (StringUtils.isEmpty(url)) {
return url;
}
SaTokenConfig config = StpAdminUtil.getStpLogic().getConfigOrGlobal();
String token = StpAdminUtil.getTokenValue();
if (StringUtils.isNotEmpty(config.getTokenPrefix())) {
token = config.getTokenPrefix() + " " + token;
}
return appendParam(url, config.getTokenName(), token);
}
}

View File

@ -88,6 +88,7 @@ ERRCODE.CMS.CONTENTCORE.RESOURCE_ACCEPT_SIZE_LIMIT=上传文件大小超过限
ERRCODE.CMS.CONTENTCORE.UNSUPPORTED_RESOURCE_STORAGE=资源存储方式与当前站点配置不一致
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_IS_EMPTY=被合并栏目不存在
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_NOT_LEAF=被合并栏目不能包含子栏目
ERRCODE.CMS.CONTENTCORE.DENY_LINK_TO_LINK_INTERNAL_DATA=不能链接到链接内容或栏目
# freemarker模板标签
FREEMARKER.TAG.cms_site.NAME=站点列表标签
@ -161,9 +162,8 @@ FREEMARKER.FUNC.imageSize.Arg1.Name=图片资源内部路径iurl://
FREEMARKER.FUNC.imageSize.Arg1.Desc=仅支持处理内部资源图片iurl://
FREEMARKER.FUNC.imageSize.Arg2.Name=宽度
FREEMARKER.FUNC.imageSize.Arg3.Name=高度
FREEMARKER.FUNC.imageSize.Arg4.Name=是否居中裁剪
FREEMARKER.FUNC.internalUrl.DESC=将内部链接“iurl://”解析为正常http(s)访问地址,例如:`${internalUrl(content.redirectUrl)}`
FREEMARKER.FUNC.internalUrl.Arg1.Name=內部鏈iurl://
FREEMARKER.FUNC.internalUrl.Arg1.Name=内部链iurl://
FREEMARKER.FUNC.siteUrl.DESC=获取站点指定发布通道访问链接,例如:`${siteUrl(Site.siteId, 'h5')}`
FREEMARKER.FUNC.siteUrl.Arg1.Name=站点ID
FREEMARKER.FUNC.siteUrl.Arg2.Name=发布通道编码
@ -198,6 +198,7 @@ FREEMARKER.FUNC.fileExtractor.Arg2.Name=文件类型/后缀
# 校验规则
VALIDATOR.CMS.SITE.PUBLISH_PIPE_PROPS_EMPTY=发布通道配置不能为空
VALIDATOR.CMS.SITE_PROPERTY.REGEXP_ERR=属性编码只能使用大小写字母、数字和下划线
VALIDATOR.CMS.FILE_NAME.REGEXP_ERR=文件名不能包含斜杠或反斜杠
# 定时任务
SCHEDULED_TASK.RecycleExpireJobHandler=回收站过期内容删除任务

View File

@ -88,6 +88,7 @@ ERRCODE.CMS.CONTENTCORE.RESOURCE_ACCEPT_SIZE_LIMIT=Upload file size exceeded.
ERRCODE.CMS.CONTENTCORE.UNSUPPORTED_RESOURCE_STORAGE=The resource storage type is inconsistent with the site configuration.
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_IS_EMPTY=Merge catalog not exists.
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_NOT_LEAF=Cannot merge catalog which has children.
ERRCODE.CMS.CONTENTCORE.DENY_LINK_TO_LINK_INTERNAL_DATA=Cannot link to link-catalog or link-content.
# 模板freemarker
FREEMARKER.TAG.cms_site.NAME=Site list tag
@ -161,7 +162,6 @@ FREEMARKER.FUNC.imageSize.Arg1.Name=Internal image resource link (iurl://)
FREEMARKER.FUNC.imageSize.Arg1.Desc=Only supports internal resource images with prefix "iurl://".
FREEMARKER.FUNC.imageSize.Arg2.Name=Width
FREEMARKER.FUNC.imageSize.Arg3.Name=Height
FREEMARKER.FUNC.imageSize.Arg4.Name=Center cutting
FREEMARKER.FUNC.internalUrl.DESC=Use `${internalUrl(site.logo)}` in template to parse "iurl://xx" to http(s) link.
FREEMARKER.FUNC.internalUrl.Arg1.Name=Internal resource url(iurl://)
FREEMARKER.FUNC.siteUrl.DESC=Use `${siteUrl(Site.siteId,'h5')}` in template to get the site publishpipe url.
@ -198,6 +198,7 @@ FREEMARKER.FUNC.fileExtractor.Arg2.Name=File type or suffix
# 校验规则
VALIDATOR.CMS.SITE.PUBLISH_PIPE_PROPS_EMPTY=The site publish pipe props cannot be empty.
VALIDATOR.CMS.SITE_PROPERTY.REGEXP_ERR=The code can only use uppercase/lowercase letters, numbers and underscores.
VALIDATOR.CMS.FILE_NAME.REGEXP_ERR=The file name cannot contain slashes or backslashes.
# 定时任务
SCHEDULED_TASK.RecycleExpireJobHandler=Recycle Content Expired Task

View File

@ -88,6 +88,7 @@ ERRCODE.CMS.CONTENTCORE.RESOURCE_ACCEPT_SIZE_LIMIT=上傳文件大小超過限
ERRCODE.CMS.CONTENTCORE.UNSUPPORTED_RESOURCE_STORAGE=資源存儲方式與當前站點配置不一致
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_IS_EMPTY=被合並欄目不存在
ERRCODE.CMS.CONTENTCORE.MERGE_CATALOG_NOT_LEAF=被合並欄目不能包含子欄目
ERRCODE.CMS.CONTENTCORE.DENY_LINK_TO_LINK_INTERNAL_DATA=不能链接到链接内容或栏目
# freemarker模板標籤
FREEMARKER.TAG.cms_site.NAME=站點列表標籤
@ -161,7 +162,6 @@ FREEMARKER.FUNC.imageSize.Arg1.Name=圖片資源內部路徑iurl://
FREEMARKER.FUNC.imageSize.Arg1.Desc=僅支持處理內部資源圖片iurl://
FREEMARKER.FUNC.imageSize.Arg2.Name=寬度
FREEMARKER.FUNC.imageSize.Arg3.Name=高度
FREEMARKER.FUNC.imageSize.Arg4.Name=是否居中裁剪
FREEMARKER.FUNC.internalUrl.DESC=將內部連結“iurl://”解析為正常http(s)訪問地址,例如:`${internalUrl(content.redirectUrl)}`
FREEMARKER.FUNC.internalUrl.Arg1.Name=內部鏈接iurl://
FREEMARKER.FUNC.siteUrl.DESC=獲取站點指定發布通道訪問連結,例如:`${siteUrl(Site.siteId, 'h5')}`
@ -198,6 +198,7 @@ FREEMARKER.FUNC.fileExtractor.Arg2.Name=文件類型/後綴
# 校驗規則
VALIDATOR.CMS.SITE.PUBLISH_PIPE_PROPS_EMPTY=發布通道配置不能為空
VALIDATOR.CMS.SITE_PROPERTY.REGEXP_ERR=屬性編碼只能使用大小寫字母、數字和下劃線
VALIDATOR.CMS.FILE_NAME.REGEXP_ERR=文件名不能包含斜杠或反斜杠
# 定時任務
SCHEDULED_TASK.RecycleExpireJobHandler=資源回收筒過期內容刪除任務

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-customform</artifactId>

View File

@ -1,69 +0,0 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.cache;
import com.chestnut.common.redis.IMonitoredCache;
import com.chestnut.common.redis.RedisCache;
import com.chestnut.system.SysConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* CustomFormCaptchaMonitoredCache
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Component(IMonitoredCache.BEAN_PREFIX + CustomFormCaptchaMonitoredCache.ID)
@RequiredArgsConstructor
public class CustomFormCaptchaMonitoredCache implements IMonitoredCache<String> {
public static final String ID = "CustomFormCaptcha";
public static final String CACHE_PREFIX = "cms:customform:captcha:";
private final RedisCache redisCache;
@Override
public String getId() {
return ID;
}
@Override
public String getCacheName() {
return "{MONITORED.CACHE.CUSTOM_FORM_CAPTCHA}";
}
@Override
public String getCacheKey() {
return CACHE_PREFIX;
}
@Override
public String getCache(String cacheKey) {
return redisCache.getCacheObject(cacheKey, String.class);
}
public void deleteCache(String cacheKey) {
this.redisCache.deleteObject(cacheKey);
}
public void setCache(String cacheKey, String code, Integer captchaExpiration, TimeUnit timeUnit) {
this.redisCache.setCacheObject(cacheKey, code, captchaExpiration, timeUnit);
}
}

View File

@ -27,6 +27,7 @@ import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.customform.domain.CmsCustomForm;
@ -46,7 +47,6 @@ import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* <p>
@ -96,12 +96,12 @@ public class CustomFormController extends BaseRestController {
Assert.notNull(form, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("formId", formId));
CustomFormVO vo = CustomFormVO.from(form);
List<Map<String, String>> templates = this.publishPipeService.getPublishPipes(form.getSiteId())
.stream().map(pp -> Map.of(
"name", pp.getName(),
"code", pp.getCode(),
"template", form.getTemplates().getOrDefault(pp.getCode(), "")
)).toList();
List<PublishPipeTemplate> templates = this.publishPipeService.getPublishPipes(form.getSiteId())
.stream().map(pp -> new PublishPipeTemplate(
pp.getName(),
pp.getCode(),
form.getTemplates().getOrDefault(pp.getCode(), "")
)).toList();
vo.setTemplates(templates);
return R.ok(vo);
}

View File

@ -15,46 +15,34 @@
*/
package com.chestnut.customform.controller.front;
import com.chestnut.common.captcha.CaptchaType;
import com.chestnut.common.config.CaptchaConfig;
import com.chestnut.common.captcha.CaptchaData;
import com.chestnut.common.captcha.CaptchaService;
import com.chestnut.common.captcha.ICaptchaType;
import com.chestnut.common.captcha.math.MathCaptchaType;
import com.chestnut.common.domain.R;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.exception.GlobalException;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.common.utils.*;
import com.chestnut.customform.CmsCustomFormMetaModelType;
import com.chestnut.customform.CustomFormConsts;
import com.chestnut.customform.cache.CustomFormCaptchaMonitoredCache;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.exception.CustomFormErrorCode;
import com.chestnut.customform.fixed.config.CustomFormCaptchaExpireSeconds;
import com.chestnut.customform.service.ICustomFormApiService;
import com.chestnut.customform.service.ICustomFormService;
import com.chestnut.member.security.StpMemberUtil;
import com.chestnut.system.annotation.IgnoreDemoMode;
import com.chestnut.system.config.properties.SysProperties;
import com.chestnut.system.domain.vo.ImageCaptchaVO;
import com.chestnut.system.exception.SysErrorCode;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.system.validator.LongId;
import com.google.code.kaptcha.Producer;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* <p>
@ -73,14 +61,12 @@ public class CustomFormApiController extends BaseRestController {
private final ICustomFormApiService customFormApiService;
private final CustomFormCaptchaMonitoredCache captchaCache;
private final Map<String, Producer> captchaProducers;
private final SysProperties properties;
@GetMapping("/captchaImage")
public R<?> getCaptchaImage(@RequestParam @LongId Long formId, HttpServletRequest request) {
private final CaptchaService captchaService;
@GetMapping("/captcha")
public R<?> getCaptcha(@RequestParam @LongId Long formId, HttpServletRequest request) {
CmsCustomForm form = this.customFormService.getById(formId);
Assert.notNull(form, CustomFormErrorCode.FORM_NOT_FOUND::exception);
// 是否需要验证码
@ -93,40 +79,12 @@ public class CustomFormApiController extends BaseRestController {
}
String uuid = CustomFormConsts.tryToGetUUID(request);
Assert.notEmpty(uuid, CustomFormErrorCode.MISSING_UUID::exception);
// 保存验证码信息
String verifyKey = CustomFormCaptchaMonitoredCache.CACHE_PREFIX + uuid;
String capStr;
String code;
BufferedImage image;
Producer captchaProducer = captchaProducers.get(CaptchaConfig.BEAN_PREFIX + this.properties.getCaptchaType());
Assert.notNull(captchaProducer, () -> SysErrorCode.CAPTCHA_CONFIG_ERR.exception(this.properties.getCaptchaType()));
// 生成验证码
String captchaType = properties.getCaptchaType();
if (CaptchaType.MATH.equals(captchaType)) {
String capText = captchaProducer.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducer.createImage(capStr);
} else if (CaptchaType.CHAR.equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
} else {
throw new GlobalException("Unknown captcha type: " + captchaType);
}
Integer expireSeconds = CustomFormCaptchaExpireSeconds.getSeconds();
captchaCache.setCache(verifyKey, code, expireSeconds, TimeUnit.SECONDS);
try(FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
ImageIO.write(image, "jpg", os);
ImageCaptchaVO vo = ImageCaptchaVO.builder().captchaEnabled(true).uuid(uuid)
.img(Base64.getEncoder().encodeToString(os.toByteArray())).build();
return R.ok(vo);
} catch (IOException e) {
return R.fail(e.getMessage());
}
// TODO 目前固定使用数学计算验证码待修改成自定义表单后台配置
ICaptchaType captchaType = captchaService.getCaptchaType(MathCaptchaType.TYPE);
CaptchaData captchaData = new CaptchaData(MathCaptchaType.TYPE);
captchaData.setToken(uuid);
Object captcha = captchaType.create(captchaData);
return R.ok(captcha);
}
@IgnoreDemoMode
@ -165,16 +123,13 @@ public class CustomFormApiController extends BaseRestController {
return R.ok();
}
private void validateCaptcha(String code, String uuid) {
Assert.notEmpty(code, () -> CommonErrorCode.INVALID_REQUEST_ARG.exception("captcha"));
String cacheKey = CustomFormCaptchaMonitoredCache.CACHE_PREFIX + Objects.requireNonNullElse(uuid, StringUtils.EMPTY);
String cacheValue = captchaCache.getCache(cacheKey);
// 过期判断
Assert.notNull(cacheValue, CommonErrorCode.CAPTCHA_EXPIRED::exception);
// 未过期判断是否与输入验证码一致
Assert.isTrue(StringUtils.equals(code, cacheValue), CommonErrorCode.INVALID_CAPTCHA::exception);
// 移除缓存
captchaCache.deleteCache(cacheKey);
private void validateCaptcha(String captchaJson, String uuid) {
Assert.notEmpty(captchaJson, () -> CommonErrorCode.INVALID_REQUEST_ARG.exception("captcha"));
CaptchaData captchaData = JacksonUtils.from(captchaJson, CaptchaData.class);
captchaData.setToken(uuid);
ICaptchaType captchaType = captchaService.getCaptchaType(MathCaptchaType.TYPE);
if (!captchaType.isTokenValidated(captchaData)) {
throw CommonErrorCode.INVALID_CAPTCHA.exception();
}
}
}

View File

@ -16,6 +16,7 @@
package com.chestnut.customform.domain.dto;
import com.chestnut.common.security.domain.BaseDTO;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.system.validator.Dict;
import com.chestnut.system.validator.LongId;
@ -24,7 +25,6 @@ import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
/**
* 自定义表单编辑DTO
@ -77,7 +77,7 @@ public class CustomFormEditDTO extends BaseDTO {
/**
* 模板配置
*/
private List<Map<String, String>> templates;
private List<PublishPipeTemplate> templates;
/**
* 备注

View File

@ -15,18 +15,13 @@
*/
package com.chestnut.customform.domain.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.system.validator.Dict;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.Map;
/**
* 自定义表单详情VO
@ -56,7 +51,7 @@ public class CustomFormVO {
private String ruleLimit;
private List<Map<String, String>> templates;
private List<PublishPipeTemplate> templates;
public static CustomFormVO from(CmsCustomForm form) {
CustomFormVO vo = new CustomFormVO();

View File

@ -25,6 +25,7 @@ import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.pojo.PublishPipeTemplate;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.ITemplateService;
@ -56,6 +57,7 @@ import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@ -127,7 +129,7 @@ public class CustomFormServiceImpl extends ServiceImpl<CustomFormMapper, CmsCust
form.setNeedLogin(dto.getNeedLogin());
form.setRuleLimit(dto.getRuleLimit());
form.setRemark(dto.getRemark());
dto.getTemplates().forEach(item -> form.getTemplates().put(item.get("code"), item.get("template")));
form.setTemplates(dto.getTemplates().stream().collect(Collectors.toMap(PublishPipeTemplate::code, PublishPipeTemplate::template)));
this.updateById(form);
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-dynamic</artifactId>

View File

@ -66,10 +66,11 @@ public class DynamicCoreHandler implements ICoreDataHandler {
data.setSiteId(context.getSite().getSiteId());
data.createBy(context.getOperator());
Long count = dynamicPageService.lambdaQuery().eq(CmsDynamicPage::getPath, data.getPath()).count();
if (count > 0) {
if (count > 0 || dynamicPageService.isRequestMappingExists(data)) {
data.setPath(data.getPath() + "_" + data.getPageId());
}
dynamicPageService.save(data);
dynamicPageService.registerDynamicPageMapping(data.getCode(), data.getPath());
} catch (Exception e) {
AsyncTaskManager.addErrMessage("导入自定义动态模板页面失败:" + data.getName() + "[" + data.getCode() + "]");
log.error("Import dynamic page failed: {}", data.getCode(), e);

View File

@ -29,7 +29,7 @@ public interface IDynamicPageInitData {
String BEAN_PREFIX = "DynamicPageInitData.";
void initTemplateData(TemplateContext context, Map<String, String> parameters);
void initTemplateData(TemplateContext context, String path, Map<String, String> parameters);
String getType();

View File

@ -47,7 +47,7 @@ public class MemberDynamicPageInitData implements IDynamicPageInitData {
}
@Override
public void initTemplateData(TemplateContext context, Map<String, String> parameters) {
public void initTemplateData(TemplateContext context, String path, Map<String, String> parameters) {
if (StpMemberUtil.isLogin()) {
LoginUser loginUser = StpMemberUtil.getLoginUser();
context.getVariables().put(CmsMemberConstants.TEMPLATE_VARIABLE_MEMBER, loginUser.getUser());

View File

@ -16,7 +16,9 @@
package com.chestnut.cms.dynamic.core.impl;
import com.chestnut.cms.dynamic.core.IDynamicPageInitData;
import com.chestnut.common.staticize.StaticizeConstants;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.contentcore.util.TemplateUtils;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
@ -44,8 +46,12 @@ public class PaginationDynamicPageInitData implements IDynamicPageInitData {
}
@Override
public void initTemplateData(TemplateContext context, Map<String, String> parameters) {
int pageIndex = MapUtils.getIntValue(parameters, "pi", 1);
public void initTemplateData(TemplateContext context, String path, Map<String, String> parameters) {
int pageIndex = MapUtils.getIntValue(parameters, StaticizeConstants.TemplateParam_PageIndex, 1);
context.setPageIndex(pageIndex);
String prefix = context.getVariables().get(context.isPreview() ? TemplateUtils.TemplateVariable_ApiPrefix
: TemplateUtils.TemplateVariable_Prefix).toString();;
context.setFirstFileName(prefix + path);
context.setOtherFileName(prefix + TemplateUtils.appendPageIndexParam(path, TemplateContext.PlaceHolder_PageNo));
}
}

View File

@ -31,5 +31,11 @@ public interface IDynamicPageService extends IService<CmsDynamicPage> {
void deleteDynamicPage(List<Long> dynamicPageIds);
void generateDynamicPage(String uri, Long siteId, String publishPipeCode, Boolean preview, Map<String, String> parameters, HttpServletResponse response) throws IOException;
boolean isRequestMappingExists(CmsDynamicPage dynamicPage);
void registerDynamicPageMapping(String code, String path);
void unregisterDynamicPageMapping(String code, String path);
void generateDynamicPage(String uri, Long siteId, String publishPipeCode, Boolean preview, Map<String, String> parameters, HttpServletResponse response) throws IOException;
}

View File

@ -79,7 +79,7 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
dynamicPage.setPageId(IdUtils.getSnowflakeId());
this.save(dynamicPage);
this.registerDynamicPageMapping(dynamicPage);
this.registerDynamicPageMapping(dynamicPage.getCode(), dynamicPage.getPath());
dynamicPageHelper.updateCache(dynamicPage);
}
@ -91,13 +91,18 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
CmsDynamicPage dbDynamicPage = this.getById(dynamicPage.getPageId());
Assert.notNull(dbDynamicPage, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("pageId", dynamicPage.getPageId()));
String oldPath = dbDynamicPage.getPath();
String oldCode = dbDynamicPage.getCode();
dbDynamicPage.setName(dynamicPage.getName());
dbDynamicPage.setCode(dynamicPage.getCode());
dbDynamicPage.setDescription(dynamicPage.getDescription());
dbDynamicPage.setInitDataTypes(dynamicPage.getInitDataTypes());
dbDynamicPage.setTemplates(dynamicPage.getTemplates());
this.updateById(dbDynamicPage);
if (!oldPath.equals(dynamicPage.getCode()) || !oldPath.equals(dynamicPage.getPath())) {
unregisterDynamicPageMapping(oldCode, oldPath);
}
dynamicPageHelper.updateCache(dbDynamicPage);
}
@ -107,8 +112,8 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
this.removeByIds(dynamicPages);
dynamicPages.forEach(dynamicPage -> {
this.unregisterDynamicPageMapping(dynamicPage);
dynamicPageHelper.clearCache(dynamicPage);
this.unregisterDynamicPageMapping(dynamicPage.getCode(), dynamicPage.getPath());
});
}
@ -118,25 +123,34 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
.or().eq(CmsDynamicPage::getCode, dynamicPage.getCode()))
.ne(IdUtils.validate(dynamicPage.getPageId()), CmsDynamicPage::getPageId, dynamicPage.getPageId())
.count();
Assert.isTrue(count == 0, () -> CommonErrorCode.DATA_CONFLICT.exception(dynamicPage.getPath() + "||" + dynamicPage.getPath()));
Assert.isTrue(count == 0, () -> CommonErrorCode.DATA_CONFLICT.exception(dynamicPage.getPath()));
// 校验路径是冲突
if (!IdUtils.validate(dynamicPage.getPageId())) {
for (RequestMappingHandlerMapping mapping : allRequestMapping) {
Set<RequestMappingInfo> requestMappingInfos = mapping.getHandlerMethods().keySet();
for (RequestMappingInfo requestMappingInfo : requestMappingInfos) {
Assert.isFalse(requestMappingInfo.getPatternValues().contains(dynamicPage.getPath()),
() -> new GlobalException("Conflict request handler mapping: " + dynamicPage.getPath()));
}
}
Assert.isFalse(isRequestMappingExists(dynamicPage),
() -> new GlobalException("Conflict request handler mapping: " + dynamicPage.getPath()));
}
}
private void registerDynamicPageMapping(CmsDynamicPage dynamicPage) {
@Override
public boolean isRequestMappingExists(CmsDynamicPage dynamicPage) {
for (RequestMappingHandlerMapping mapping : allRequestMapping) {
Set<RequestMappingInfo> requestMappingInfos = mapping.getHandlerMethods().keySet();
for (RequestMappingInfo requestMappingInfo : requestMappingInfos) {
if (requestMappingInfo.getPatternValues().contains(dynamicPage.getPath())) {
return true;
}
}
}
return false;
}
@Override
public void registerDynamicPageMapping(String code, String path) {
try {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(dynamicPage.getPath())
.paths(path)
.methods(RequestMethod.GET)
.mappingName(dynamicPage.getCode());
.mappingName(code);
RequestMappingInfo mappingInfo = builder.options(this.dynamicPageRequestMapping.getBuilderConfiguration()).build();
DynamicPageFrontController handler = SpringUtils.getBean(DynamicPageFrontController.class);
@ -148,12 +162,13 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
}
}
private void unregisterDynamicPageMapping(CmsDynamicPage dynamicPage) {
@Override
public void unregisterDynamicPageMapping(String code, String path) {
try {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(dynamicPage.getPath())
.paths(path)
.methods(RequestMethod.GET)
.mappingName(dynamicPage.getCode());
.mappingName(code);
RequestMappingInfo mappingInfo = builder.options(this.dynamicPageRequestMapping.getBuilderConfiguration()).build();
this.dynamicPageRequestMapping.unregisterMapping(mappingInfo);
@ -168,7 +183,7 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
this.list().forEach(dynamicPage -> {
dynamicPageHelper.updateCache(dynamicPage);
this.registerDynamicPageMapping(dynamicPage);
this.registerDynamicPageMapping(dynamicPage.getCode(), dynamicPage.getPath());
});
}
@ -206,7 +221,7 @@ public class DynamicPageServiceImpl extends ServiceImpl<CmsDynamicPageMapper, Cm
for (String initDataType : dynamicPage.getInitDataTypes()) {
IDynamicPageInitData initData = dynamicPageHelper.getDynamicPageInitData(initDataType);
if (Objects.nonNull(initData)) {
initData.initTemplateData(templateContext, parameters);
initData.initTemplateData(templateContext, dynamicPage.getPath(), parameters);
}
}
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-exmodel</artifactId>

View File

@ -23,6 +23,7 @@ import com.chestnut.common.log.enums.BusinessType;
import com.chestnut.common.security.anno.Priv;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.security.web.PageRequest;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsSite;
@ -78,9 +79,12 @@ public class EXModelController extends BaseRestController {
@Priv(type = AdminUserType.TYPE)
@GetMapping("/options")
public R<?> getModelOptions() {
public R<?> getModelOptions(@RequestParam(required = false) Long siteId) {
PageRequest pr = this.getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
if (IdUtils.validate(siteId)) {
site = this.siteService.getSite(siteId);
}
LambdaQueryWrapper<XModel> q = new LambdaQueryWrapper<XModel>()
.eq(XModel::getOwnerType, CmsExtendMetaModelType.TYPE)
.eq(XModel::getOwnerId, site.getSiteId());

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-image</artifactId>

View File

@ -30,16 +30,11 @@ import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.contentcore.core.IContent;
import com.chestnut.contentcore.core.IContentType;
import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
import com.chestnut.contentcore.domain.BCmsContent;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.dto.PublishPipeProp;
import com.chestnut.contentcore.domain.*;
import com.chestnut.contentcore.domain.pojo.PublishPipeProps;
import com.chestnut.contentcore.domain.vo.ContentVO;
import com.chestnut.contentcore.enums.ContentCopyType;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IContentService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.util.InternalUrlUtils;
import com.chestnut.system.fixed.dict.YesOrNo;
import lombok.RequiredArgsConstructor;
@ -57,6 +52,8 @@ public class ImageContentType implements IContentType {
private final static String NAME = "{CMS.CONTENTCORE.CONTENT_TYPE." + ID + "}";
private final ISiteService siteService;
private final IContentService contentService;
private final IImageService imageService;
@ -65,6 +62,8 @@ public class ImageContentType implements IContentType {
private final IPublishPipeService publishPipeService;
private final IResourceService resourceService;
@Override
public String getId() {
return ID;
@ -135,7 +134,7 @@ public class ImageContentType implements IContentType {
});
vo = ImageAlbumVO.newInstance(contentEntity, list);
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, contentEntity.getPublishPipeProps());
vo.setPublishPipeProps(publishPipeProps);
} else {
@ -146,11 +145,17 @@ public class ImageContentType implements IContentType {
// 发布通道初始数据
vo.setPublishPipe(publishPipes.stream().map(CmsPublishPipe::getCode).toArray(String[]::new));
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
List<PublishPipeProps> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, null);
vo.setPublishPipeProps(publishPipeProps);
}
vo.setCatalogName(catalog.getName());
// 内容引导图缩略图处理
CmsSite site = siteService.getSite(catalog.getSiteId());
resourceService.dealDefaultThumbnail(site, vo.getImages(), thumbnails -> {
vo.setImagesSrc(thumbnails);
vo.setLogoSrc(thumbnails.get(0));
});
return vo;
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-link</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.5.5</version>
<version>1.5.6</version>
</parent>
<artifactId>chestnut-cms-media</artifactId>

Some files were not shown because too many files have changed in this diff Show More