初始化代码

master
姜玉琦 2023-12-24 10:26:58 +08:00
parent e79a78cac6
commit d21e382159
2081 changed files with 941326 additions and 0 deletions

257
FcNetCMS/Jenkinsfile vendored 100644
View File

@ -0,0 +1,257 @@
pipeline {
agent any
environment {
DOCKER_HUB_URL = 'registry.cn-hangzhou.aliyuncs.com'
DOCKER_HUB_WORKSPACE = 'xxxxxx'
DINGTALK_ID = 'xxxxxx'
// 获取Maven pom.xml项目版本号
//APP_VERSION = readMavenPom().getVersion()
APP_VERSION = '0.1.0'
}
parameters {
choice(
choices: [ 'N', 'Y' ],
description: '是否发布服务端',
name: 'DEPLOY_SERVER')
choice(
choices: [ 'N', 'Y' ],
description: '是否发布wwwroot_release',
name: 'DEPLOY_WWWROOT')
choice(
choices: [ 'N', 'Y' ],
description: '是否发布前端',
name: 'DEPLOY_UI')
choice(
choices: [ 'latest' ],
description: '镜像TAG',
name: 'IMAGE_TAG')
choice(
choices: [ 'prod' ],
description: '发布环境',
name: 'DEPLOY_ENV')
}
options {
//设置在项目打印日志时带上对应时间
timestamps()
//不允许同时执行流水线,被用来防止同时访问共享资源等
disableConcurrentBuilds()
// 表示保留n次构建历史
buildDiscarder(logRotator(numToKeepStr: '2'))
}
stages {
stage("Preparing") {
steps {
dingtalk (
robot: '${DINGTALK_ID}',
type: 'MARKDOWN',
at: [],
atAll: false,
title: '(${JOB_NAME} #${BUILD_NUMBER})',
text: [
'### 开始构建(${JOB_NAME} #${BUILD_NUMBER})',
'---',
'- DEPLOY_SERVER: ${DEPLOY_SERVER}',
'- DEPLOY_UI: ${DEPLOY_UI}',
'- IMAGE_TAG: ${IMAGE_TAG}',
'- DEPLOY_ENV: ${DEPLOY_ENV}',
],
messageUrl: '${BUILD_URL}',
picUrl: ''
)
}
}
stage("Checkout") {
steps {
dir('./ChestnutCMS') {
checkout([$class: 'GitSCM', branches: [[name: '*/dev']], extensions: [], userRemoteConfigs: [[credentialsId: 'LwyGitee', url: 'https://gitee.com/liweiyi/ChestnutCMS.git']]])
}
dir('./wwwroot_release') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'gitea', url: 'http://gitea.huaray.com/liweiyi/swikoon_wwwroot_release.git']]])
}
}
}
stage("Build") {
when {
expression { return params.DEPLOY_SERVER == 'Y' }
}
steps {
dir('./ChestnutCMS') {
withEnv(['JAVA_HOME=/var/jenkins_home/jdk/jdk-17.0.4.1']) {
withMaven(maven: 'M3.8') {
sh 'mvn -U clean package -Dmaven.test.skip=true'
}
}
}
}
}
stage("chestnut-admin") {
when {
expression { return params.DEPLOY_SERVER == 'Y' }
}
steps {
withEnv(['APP_PATH=chestnut-admin', 'APP_NAME=chestnut-admin']) {
echo "docker build start: ${APP_PATH}#${APP_VERSION}"
dir('./ChestnutCMS') {
withCredentials([usernamePassword(credentialsId: 'ALIYUN-DOCKER-REGISTRY-LWY', passwordVariable: 'DOCKERPWD', usernameVariable: 'DOCKERUSER')]) {
sh '''
cd ${APP_PATH}
echo ${DOCKERPWD} | docker login --username=${DOCKERUSER} --password-stdin ${DOCKER_HUB_URL}
docker build -t ${DOCKER_HUB_URL}/${DOCKER_HUB_WORKSPACE}/${APP_NAME}:${IMAGE_TAG} . --build-arg APP_NAME=${APP_PATH} --build-arg APP_VERSION=${APP_VERSION}
docker logout ${DOCKER_HUB_URL}
'''
}
}
echo "docker push start: ${APP_PATH}#${APP_VERSION}"
dir('./ChestnutCMS') {
withCredentials([usernamePassword(credentialsId: 'ALIYUN-DOCKER-REGISTRY-LWY', passwordVariable: 'DOCKERPWD', usernameVariable: 'DOCKERUSER')]) {
sh '''
echo ${DOCKERPWD} | docker login --username=${DOCKERUSER} --password-stdin ${DOCKER_HUB_URL}
docker push ${DOCKER_HUB_URL}/${DOCKER_HUB_WORKSPACE}/${APP_NAME}:${IMAGE_TAG}
docker logout ${DOCKER_HUB_URL}
cp -f bin/docker-image-clear.sh docker-image-clear.sh
sed -i "s/{{DOCKER_HUB_URL}}/${DOCKER_HUB_URL}/g" docker-image-clear.sh
sed -i "s/{{IMAGE_REPOSITORY}}/${DOCKER_HUB_WORKSPACE}\\/${APP_NAME}/g" docker-image-clear.sh
/bin/bash docker-image-clear.sh
rm -f docker-image-clear.sh
'''
}
}
// deploy
dir('./ChestnutCMS') {
withCredentials([usernamePassword(credentialsId: 'ALIYUN-DOCKER-REGISTRY-LWY', passwordVariable: 'DOCKERPWD', usernameVariable: 'DOCKERUSER')]) {
sh '''
cp -f bin/docker-deploy.sh ${APP_PATH}
cd ${APP_PATH}
cp -f ../bin/docker-deploy.sh docker-deploy.sh
sed -i "s/{{DOCKERUSER}}/${DOCKERUSER}/g" docker-deploy.sh
sed -i "s/{{DOCKERPWD}}/${DOCKERPWD}/g" docker-deploy.sh
sed -i "s/{{DOCKER_HUB_URL}}/${DOCKER_HUB_URL}/g" docker-deploy.sh
sed -i "s/{{IMAGE_REPOSITORY}}/${DOCKER_HUB_WORKSPACE}\\/${APP_NAME}/g" docker-deploy.sh
sed -i "s/{{IMAGE_TAG}}/${IMAGE_TAG}/g" docker-deploy.sh
cp -f docker/docker-compose_${DEPLOY_ENV}.yml docker-compose.yml
sed -i "s/{{DOCKER_IMAGE}}/${DOCKER_HUB_URL}\\/${DOCKER_HUB_WORKSPACE}\\/${APP_NAME}:${IMAGE_TAG}/g" docker-compose.yml
'''
sshPublisher(publishers: [sshPublisherDesc(configName: 'GameCluster', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''
mkdir -p /www/docker/chestnut-admin
cd /www/docker/chestnut-admin
sh docker-deploy.sh
rm -f docker-deploy.sh
''', execTimeout: 600000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'chestnut-admin/',
remoteDirectorySDF: false, removePrefix: 'chestnut-admin/',
sourceFiles: 'chestnut-admin/docker-compose.yml,chestnut-admin/docker-deploy.sh')],
usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
}
}
// delete tmp file
dir('./ChestnutCMS') {
sh 'rm -f ${APP_PATH}/docker-deploy.sh'
sh 'rm -f ${APP_PATH}/docker-compose.yml'
}
echo "build end: ${APP_PATH}"
}
}
}
stage("wwwroot_release") {
when {
expression { return params.DEPLOY_WWWROOT == 'Y' }
}
steps {
dir('./wwwroot_release') {
sh 'zip -q -r wwwroot_release.zip * --exclude *.svn* --exclude *.git*'
sshPublisher(publishers: [sshPublisherDesc(configName: 'GameCluster', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''
cd /www/docker/chestnut-admin/wwwroot_release
unzip -o -q wwwroot_release.zip
rm -f wwwroot_release.zip
''', execTimeout: 600000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'chestnut-admin/wwwroot_release/',
remoteDirectorySDF: false, removePrefix: '',
sourceFiles: 'wwwroot_release.zip')],
usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
}
}
}
stage("chestnut-ui") {
when {
expression { return params.DEPLOY_UI == 'Y' }
}
steps {
dir('./ChestnutCMS/chestnut-ui') {
nodejs('NodeJS16_13') {
sh '''
npm install --registry=https://registry.npmmirror.com
npm run build:prod
cd dist
zip -q -r ui.zip *
'''
}
}
dir('./ChestnutCMS/chestnut-ui/dist') {
sshPublisher(publishers: [sshPublisherDesc(configName: 'GameCluster', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''
mkdir -p /www/docker/chestnut-ui
cd /www/docker/chestnut-ui
unzip -o -q ui.zip
rm -f ui.zip
''', execTimeout: 600000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'chestnut-ui/',
remoteDirectorySDF: false, removePrefix: '',
sourceFiles: 'ui.zip')],
usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
sh 'rm -f ui.zip'
}
}
}
}
post {
success {
dingtalk (
robot: '${DINGTALK_ID}',
type: 'MARKDOWN',
at: [],
atAll: false,
title: '${JOB_NAME} #${BUILD_NUMBER}',
text: [
'### 构建成功(${JOB_NAME} #${BUILD_NUMBER})',
'---',
'- IMAGE_TAG: ${IMAGE_TAG}',
'- DEPLOY_ENV: ${DEPLOY_ENV}',
],
messageUrl: '${BUILD_URL}',
picUrl: ''
)
}
failure {
dingtalk (
robot: '${DINGTALK_ID}',
type: 'LINK',
at: [],
atAll: false,
title: '${JOB_NAME} #${BUILD_NUMBER}',
text: [
'构建失败',
],
messageUrl: '${BUILD_URL}',
picUrl: ''
)
}
unstable {
dingtalk (
robot: '${DINGTALK_ID}',
type: 'LINK',
at: [],
atAll: false,
title: '${JOB_NAME} #${BUILD_NUMBER}',
text: [
'构建流程可能出现问题,详情请查看流程日志',
],
messageUrl: '${BUILD_URL}',
picUrl: ''
)
}
}
}

201
FcNetCMS/LICENSE 100644
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [兮玥|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.

113
FcNetCMS/README.md 100644
View File

@ -0,0 +1,113 @@
# ChestnutCMS v1.3.21
### 系统简介
ChestnutCMS是前后端分离的企业级内容管理系统。项目基于[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)重构,集成[SaToken](https://gitee.com/dromara/sa-token)用户权限,[xxl-job](https://gitee.com/xuxueli0323/xxl-job)任务调度。支持站群管理、多平台静态化、元数据模型扩展、轻松组织各种复杂内容形态、多语言、全文检索。
### 系统预览
后台预览地址:<http://admin.1000mz.com>
账号demo / a123456
企业站演示地址:<http://swikoon.1000mz.com>
> 企业演示站的静态资源已提交到仓库[chestnut-cms-wwwroot](https://gitee.com/liweiyi/chestnut-cms-wwwroot)。
资讯站演示地址:<http://news.1000mz.com>会员演示账号xxx333@126.com / a123456
图片站演示地址:<http://tpz.1000mz.com>
### 开发环境
- JDK 17
- Maven 3.8
- MySQL 8.0
- Redis 6.x
### 主要技术框架
| 技术框架 | 版本 | 应用说明 |
|-------------------|---------|---------|
| Spring Boot | 3.1.5 | 基础开发框架 |
| Spring Boot Admin | 3.1.7 | 监控框架 |
| Mybatis Plus | 3.5.3.2 | ORM框架 |
| Flyway | 9.22.3 | 数据库版本管理 |
| Yitter | 1.0.6 | 雪花ID |
| Redisson | 3.24.1 | 分布式锁 |
| FreeMarker | 2.3.32 | 模板引擎 |
| Sa-Token | 1.37.0 | 权限认证 |
| Xxl-Job | 2.4.0 | 任务调度 |
| Lombok | 1.18.20 | 你懂的 |
### 相关文档
- [Wiki-快速上手](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
- [WiKi-常用配置](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE%E8%AF%B4%E6%98%8E)
- [WiKi-Docker部署说明](https://gitee.com/liweiyi/ChestnutCMS/wikis/Docker%E9%83%A8%E7%BD%B2%E8%AF%B4%E6%98%8E)
- [WiKi-站点访问配置](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E7%AB%99%E7%82%B9%E8%AE%BF%E9%97%AE%E9%85%8D%E7%BD%AE)
- [WiKi-使用手册](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E7%B3%BB%E7%BB%9F%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8C/%E5%BB%BA%E7%AB%99%E6%B5%81%E7%A8%8B)
- [WiKi-模板手册](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E6%A8%A1%E6%9D%BF%E6%89%8B%E5%86%8C/%E6%A8%A1%E6%9D%BF%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F%E8%AF%B4%E6%98%8E)
- [WiKi-二次开发手册](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1)
- [WiKi-常见问题](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)
- [WiKi-版权声明](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E7%89%88%E6%9D%83%E5%A3%B0%E6%98%8E)
- [WiKi-免责声明](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E5%85%8D%E8%B4%A3%E5%A3%B0%E6%98%8E)
- [WiKi-版本变更日志](https://gitee.com/liweiyi/ChestnutCMS/wikis/%E7%89%88%E6%9C%AC%E5%8F%98%E6%9B%B4%E6%97%A5%E5%BF%97)
### 功能模块
| 模块 | 简介 |
|--------------|----------------------------------------------|
| 站点管理 | 多站点,支持图片水印、标题查重、扩展模型等扩展配置 |
| 栏目管理 | 普通栏目+链接栏目,扩展配置优先级高于站点扩展配置 |
| 内容管理 | 内容类型:文章+图片集+音视频集,页面部件:动态自定义区块+广告,内容回收站 |
| 资源管理 | 图片、音视频等各类静态资源管理支持OSS/COS/MinIO对象存储 |
| 发布通道 | 支持多通道不同类型静态文件发布可同时发布到PC、H5html、json等 |
| 模板管理 | 静态化模板,支持在线编辑 |
| 模板指令 | FreeMarker自定义标签及模板函数的参数及用法说明 |
| 文件管理 | 当前站点资源目录及发布通道静态化目录管理,支持文本在线编辑 |
| 扩展模型 | 站点、栏目及内容的动态模型扩展,系统默认数据表保存,支持自定义 |
| 词汇管理 | 热词、TAG词、敏感词、易错词 |
| 内容索引 | 默认支持ElasticSearch+IK创建内容索引支持标题内容全文检索 |
| 检索词库 | 自定义检索词库,支持扩展词和停用词动态扩展 |
| 检索日志 | 用户搜索的日志记录,支持一键加入扩展词库 |
| 友链管理 | 友情链接 |
| 广告管理 | 广告基于页面部件扩展的简单广告功能,支持权重及上下线时间配置,支持广告点击/展现日志统计 |
| 评论管理 | 基础功能模块 |
| 调查问卷 | 基础功能模块,默认支持文字类型单选、多选、输入 |
| 自定义表单 | 基于元数据模块扩展,支持模板标签 |
| 会员管理 | 支持自定义会员等级,等级经验值来源动态配置 |
| 访问统计 | 对接百度统计API |
| 用户管理 | 后台用户管理,支持用户独立权限配置 |
| 机构管理 | 多级系统组织机构(公司、部门、小组) |
| 角色管理 | 支持按角色分配菜单权限、站点和栏目相关操作权限配置 |
| 岗位管理 | 配置系统用户所属担任职务 |
| 菜单管理 | 配置系统菜单,操作权限,按钮权限标识等 |
| 字典管理 | 对系统中经常使用的一些固定的数据进行维护,代码层面定义 |
| 参数管理 | 对系统动态配置常用参数,代码层面定义 |
| 通知公告 | 系统通知公告信息发布维护 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 国际化 | 为菜单等动态数据国际化配置提供基础支持,可覆盖后台代码配置 |
| 安全配置 | 密码强度、密码过期、首次登陆强制修改、登陆异常策略配置 |
| 系统日志 | 统一日志管理,支持扩展 |
| 操作日志 | 系统操作日志扩展,记录操作参数、异常信息及请求耗时 |
| 登录日志 | 系统登录日志扩展,记录用户登录日志,包含登录异常 |
| 在线用户 | 当前系统中活跃用户状态监控,支持踢下线 |
| 任务调度 | 基于XXL-JOB的分布式任务调度 |
| 定时任务 | 基于Spring的TaskScheduler实现的单机定时任务 |
| 异步任务 | 异步任务状态查看,支持手动结束 |
| 服务监控 | 监视当前系统CPU、内存、磁盘、堆栈等相关信息 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等 |
| GroovyScript | 支持Groovy脚本在线执行 |
### 开源协议补充说明
1. ChestnutCMS 遵循《Apache-2.0开源协议》,使用本系统不得用于违反国家有关政策的相关软件和应用。
2. 系统可免费商用,但是必须包含原始版权声明和许可声明 不可移除后台登录页面底部的版权申明“Copyright © 2022-2023 ChestnutCMS (1000mz.com) All Rights Reserved.”。
3. 本项目所包含的第三方源码版权信息需另行标注。
### QQ交流群
群号568506424 口令:举个栗子
如果本项目对您有一丢丢小帮助 :kissing_heart: 点个小星星吧 :star2:

View File

@ -0,0 +1,35 @@
#!/bin/bash
DOCKERUSER={{DOCKERUSER}}
DOCKERPWD={{DOCKERPWD}}
DOCKER_HUB_URL={{DOCKER_HUB_URL}}
IMAGE_REPOSITORY={{IMAGE_REPOSITORY}}
IMAGE_TAG={{IMAGE_TAG}}
DOCKER_IMAGE=${DOCKER_HUB_URL}/${IMAGE_REPOSITORY}
# pull docker image
echo ${DOCKERPWD} | docker login --username=${DOCKERUSER} --password-stdin ${DOCKER_HUB_URL}
docker pull ${DOCKER_IMAGE}:${IMAGE_TAG}
docker logout ${DOCKER_HUB_URL}
# 处理none镜像
NONE_IMAGE_ID_ARR=$(docker images | grep "${DOCKER_IMAGE}" | grep "<none>" | awk '{print $3}')
for NONE_IMAGE_ID in ${NONE_IMAGE_ID_ARR[*]}; do
# 停止容器
NONE_RUNNING_CONTAINER_ID_ARR=$(docker ps -a | grep $NONE_IMAGE_ID | awk '{print $1}')
for NONE_RUNNING_CONTAINER_ID in ${NONE_RUNNING_CONTAINER_ID_ARR[*]}; do
docker stop ${NONE_RUNNING_CONTAINER_ID}
echo ">>>>> Stop docker container done. CONTAINER_ID: ${NONE_RUNNING_CONTAINER_ID}"
done
# 删除容器
NONE_STOPPED_CONTAINER_ID_ARR=$(docker ps -a | grep $NONE_IMAGE_ID | grep 'Exited' | awk '{print $1}')
for NONE_STOPPED_CONTAINER_ID in ${NONE_STOPPED_CONTAINER_ID_ARR[*]}; do
docker rm ${NONE_STOPPED_CONTAINER_ID}
echo ">>>>> Delete docker container done. CONTAINER_ID: ${NONE_STOPPED_CONTAINER_ID}"
done
# 删除镜像
docker rmi $NONE_IMAGE_ID
echo ">>>>>delete docker <none> image done: $NONE_IMAGE_ID"
done
# 启动容器
docker-compose up -d

View File

@ -0,0 +1,22 @@
#!/bin/bash
DOCKER_IMAGE={{DOCKER_HUB_URL}}/{{IMAGE_REPOSITORY}}
# 删除docker镜像
NONE_IMAGE_ID_ARR=$(docker images | grep "${DOCKER_IMAGE}" | awk '{print $3}')
for NONE_IMAGE_ID in ${NONE_IMAGE_ID_ARR[*]}; do
# 停止容器
NONE_RUNNING_CONTAINER_ID_ARR=$(docker ps -a | grep $NONE_IMAGE_ID | awk '{print $1}')
for NONE_RUNNING_CONTAINER_ID in ${NONE_RUNNING_CONTAINER_ID_ARR[*]}; do
docker stop ${NONE_RUNNING_CONTAINER_ID}
echo ">>>>> Stop docker container done. CONTAINER_ID: ${NONE_RUNNING_CONTAINER_ID}"
done
# 删除容器
NONE_STOPPED_CONTAINER_ID_ARR=$(docker ps -a | grep $NONE_IMAGE_ID | grep 'Exited' | awk '{print $1}')
for NONE_STOPPED_CONTAINER_ID in ${NONE_STOPPED_CONTAINER_ID_ARR[*]}; do
docker rm ${NONE_STOPPED_CONTAINER_ID}
echo ">>>>> Delete docker container done. CONTAINER_ID: ${NONE_STOPPED_CONTAINER_ID}"
done
# 删除镜像
docker rmi $NONE_IMAGE_ID
echo ">>>>>delete docker image done: $NONE_IMAGE_ID"
done

View File

@ -0,0 +1,37 @@
FROM openjdk:17-jdk-oraclelinux8 as builder
ARG APP_NAME
ARG APP_VERSION
ENV SPRING_PROFILES_ACTIVE=prod
WORKDIR /home/app
COPY target/$APP_NAME.jar app.jar
RUN java -Djarmode=layertools -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -jar app.jar extract
FROM openjdk:17-jdk-oraclelinux8
WORKDIR /home/app
COPY --from=builder /home/app/dependencies/ ./
COPY --from=builder /home/app/spring-boot-loader/ ./
COPY --from=builder /home/app/snapshot-dependencies/ ./
COPY --from=builder /home/app/application ./
ENV TZ="Asia/Shanghai" \
SERVER_PORT=8090 \
JVM_OPTS="-Xms2g -Xmx2g -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m" \
JAVA_OPTS=""
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& mkdir -p logs
VOLUME ["/home/app/logs","/home/app/uploadPath","/home/app/wwwroot_release","/home/app/_xy_member"]
EXPOSE $SERVER_PORT $NETTY_SOCKET_PORT
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]

View File

@ -0,0 +1,106 @@
version: '3'
networks:
default:
external:
name: cc_bridge
services:
cc-mysql:
image: mysql:8.0.32
container_name: cc-mysql
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=hello1234
- TZ=Asia/Shanghai
networks:
- default
ports:
- '3306:3306'
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf/my.cnf:/etc/my.cnf
- ./mysql/init:/docker-entrypoint-initdb.d
cc-redis:
image: redis:6.2.13
container_name: cc-redis
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
volumes:
- "./redis/conf:/usr/local/etc/redis"
- "./redis/data:/data"
command:
redis-server --port 6379 --requirepass "b18a03" --appendonly yes
networks:
- default
cc-minio:
image: minio/minio:latest
container_name: cc-minio
ports:
- 9000:9000
- 9999:9999
volumes:
- ./minio/data:/data
- ./minio/config:/root/.minio
environment:
MINIO_ROOT_USER: "root"
MINIO_ROOT_PASSWORD: "minioadmin"
MINIO_ACCESS_KEY: minioccadmin
MINIO_SECRET_KEY: minioccadmin
logging:
options:
max-size: "50M" # 最大文件上传限制
max-file: "10"
driver: json-file
command: server /data --console-address ":9999"
restart: always
networks:
- default
cc-xxl-job-admin:
image: xuxueli/xxl-job-admin:2.4.0
restart: unless-stopped
container_name: cc-xxl-job-admin
ports:
- 18080:8080
environment:
TZ: Asia/Shanghai
PARAMS: "--spring.datasource.url=jdbc:mysql://cc-mysql/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=xxxxxx"
volumes:
- ./xxl-job/logs:/data/applogs
depends_on:
- cc-mysql
networks:
- default
cc-elasticsearch:
# 构建镜像的相关配置在 chestnut-modules/chestnut-search/docker 目录下
image: elasticsearch-ik:8.5.2
restart: unless-stopped
container_name: cc-elasticsearch
healthcheck:
test: [ "CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
ports:
- 9200:9200
- 9300:9300
environment:
discovery.type: single-node
ES_JAVA_OPTS: -Xms1024m -Xmx1024m
volumes:
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./elasticsearch/data:/usr/share/elasticsearch/data
- ./elasticsearch/logs:/usr/share/elasticsearch/logs
- ./elasticsearch/ik-config:/usr/share/elasticsearch/plugins/ik/config
ulimits:
memlock:
soft: -1
hard: -1
networks:
- default

View File

@ -0,0 +1,24 @@
version: '3'
networks:
default:
external:
name: cc_bridge
services:
chestnut-cms:
image: {{DOCKER_IMAGE}}
container_name: chestnut-cms
restart: unless-stopped
networks:
- default
environment:
SERVER_PORT: 8090
SPRING_PROFILES_ACTIVE: prod
JVM_OPTS: "-Xms2048M -Xmx2048m -Xmn512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
ports:
- "8090:8090"
- "8899:8899"
volumes:
- ./logs:/home/app/logs
- ./uploadPath:/home/app/uploadPath
- ./_xy_member:/home/app/_xy_member
- ./wwwroot_release:/home/app/wwwroot_release

View File

@ -0,0 +1,29 @@
[mysqld]
datadir=/var/lib/mysql/data
port = 3306
socket=/tmp/mysql.sock
symbolic-links=0
#log-error=/var/log/mysqld.log
pid-file=/tmp/mysqld/mysqld.pid
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server_id=1
max_connections=1000
lower_case_table_names=1
#user=mysql
init_connect='SET collation_connection = utf8_general_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_general_ci
skip-character-set-client-handshake

View File

@ -0,0 +1 @@
requirepass b18a03

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>chestnut</artifactId>
<groupId>com.chestnut</groupId>
<version>1.3.21</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>chestnut-admin</artifactId>
<description>
web服务入口
</description>
<dependencies>
<!-- spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 表示依赖不会传递 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 系统模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-system</artifactId>
</dependency>
<!-- 代码生成 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-generator</artifactId>
</dependency>
<!-- 元数据模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-meta</artifactId>
</dependency>
<!-- 数据统计模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-stat</artifactId>
</dependency>
<!-- 会员模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-member</artifactId>
</dependency>
<!-- 文章 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-article</artifactId>
</dependency>
<!-- 广告 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-advertisement</artifactId>
</dependency>
<!-- 页面区块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-block</artifactId>
</dependency>
<!-- 扩展模型 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-exmodel</artifactId>
</dependency>
<!-- 图集 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-image</artifactId>
</dependency>
<!-- 音视频集 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-media</artifactId>
</dependency>
<!-- 友链 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-link</artifactId>
</dependency>
<!-- 词汇 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-word</artifactId>
</dependency>
<!-- 内容索引 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-search</artifactId>
</dependency>
<!-- CMS评论模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-comment</artifactId>
</dependency>
<!-- CMS调查投票模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-vote</artifactId>
</dependency>
<!-- CMS访问统计模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-stat</artifactId>
</dependency>
<!-- CMS自定义表单模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-customform</artifactId>
</dependency>
<!-- CMS会员扩展模块 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-member</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!--spring-boot打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<!-- 如果没有该配置devtools不会生效 -->
<!-- <fork>true</fork> -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<finalName>${project.artifactId}</finalName>
</build>
</project>

View File

@ -0,0 +1,22 @@
package com.chestnut;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
*
*
* @author
* @email 190785909@qq.com
*/
@SpringBootApplication
public class ChestnutApplication {
public static void main(String[] args) {
long s = System.currentTimeMillis();
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(ChestnutApplication.class, args);
System.out.println("ChestnutApplication startup, cost: " + (System.currentTimeMillis() - s) + "ms");
}
}

View File

@ -0,0 +1,18 @@
package com.chestnut;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* web
*
* @author
* @email 190785909@qq.com
*/
public class ChestnutServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ChestnutApplication.class);
}
}

View File

@ -0,0 +1,265 @@
# 项目相关配置
chestnut:
# 名称
name: ChestnutCMS
# 版本
version: 1.3.21
# 版权年份
copyrightYear: 2023
system:
# 演示模式开关
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/'
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
# 开启优雅停机
shutdown: graceful
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.chestnut: debug
org.springframework: warn
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
lifecycle:
# 设置停机缓冲时间默认30s
timeout-per-shutdown-phase: 20s
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 20MB
# 设置总上传的文件大小
max-request-size: 100MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: false
freemarker:
check-template-location: false
elasticsearch:
uris: http://127.0.0.1:9200
username: elastic
password: hello1234
# redis 配置
data:
redis:
# 地址
host: 140.143.157.1
# 端口默认为6379
port: 6379
# 数据库索引
database: 15
# 密码
password: x7Zhj3twzSdDwx2A
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
flyway:
enabled: false
# 迁移sql脚本文件存放路径默认classpath:db/migration
locations: classpath:db/migration/mysql
# 迁移sql脚本文件名称的前缀默认V
sql-migration-prefix: V
# 迁移sql脚本文件名称分隔符默认2个下划线__
sql-migration-separator: __
# 迁移sql脚本文件名称后缀
sql-migration-suffixes: .sql
# 迁移时是否进行校验
validate-on-migrate: true
# 当迁移发现数据库非空且存在没有元数据的表时自动执行基准迁移新建schema_version表
baseline-on-migrate: true
# 数据库配置
datasource:
type: com.zaxxer.hikari.HikariDataSource
dynamic:
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
# 主库
datasource:
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://cd-cynosdbmysql-grp-9rqrhxsm.sql.tencentcdb.com:27981/cms?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: Sxyanzhu@cf
# 从库
#slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://127.0.0.1:3308/ry_cms1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
# username:
# password:
hikari:
# 连接池名
pool-name: HikariCP
# 连接超时时间:毫秒, 默认30秒
connection-timeout: 2000
# 最小空闲连接默认值10小于0或大于maximum-pool-size都会重置为maximum-pool-size
minimum-idle: 5
# 最大连接数小于等于0会被重置为默认值10大于零小于1会被重置为minimum-idle的值
maximum-pool-size: 20
# 空闲连接最大存活时间默认值60000010分钟大于等于max-lifetime且max-lifetime>0会被重置为0不等于0且小于10秒会被重置为10秒。
idle-timeout: 200000
# 连接池返回的连接默认自动提交,默认只 true
auto-commit: true
# 连接最大存活时间不等于0且小于30秒会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
max-lifetime: 1800000
# 用于测试连接是否可用的查询语句
connection-test-query: SELECT 1
# 邮件配置
mail:
host: smtp.163.com
port: 465
username: xxx@163.com
# 授权码
password: xxx
# 编码格式
default-encoding: utf-8
# 协议
protocol: smtps
properties:
mail:
smtp:
ssl:
enable: true
auth: true
starttls:
enable: true
required: true
# 监控配置
application:
name: "ChestnutCMS"
boot:
admin:
client:
# 增加客户端开关
enabled: false
# Admin Server URL
url: http://127.0.0.1:8090/admin
instance:
service-host-type: IP
username: chestnut
password: 123456
# Actuator 监控端点的配置项
management:
trace:
http:
enabled: true
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/client.log
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token前缀
token-prefix: Bearer
# token有效期单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
active-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
# MyBatis配置
mybatis-plus:
global-config:
enable-sql-runner: true
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 搜索指定包别名
typeAliasesPackage: com.chestnut.**.domain
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
mode: clean
# 过滤链接
urlPatterns:
- /system/*
- /monitor/*
- /tool/*
xxl:
job:
enable: false
accessToken: default_token
adminAddresses: http://127.0.0.1:18080/xxl-job-admin
executor:
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
appname: chestnut-admin
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
#address:
### 执行器IP [选填]默认为空表示自动获取IP多网卡时可手动设置指定IP该IP不会绑定Host仅作为通讯实用地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"
ip:
### 执行器端口号 [选填]小于等于0则自动获取默认端口为9999单机部署多个执行器时注意要配置不同执行器端口
port: 9968
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: E:/dev/workspace_chestnut/ChestnutCMS/chestnut-modules/chestnut-xxljob/jobhandler
### 执行器日志文件保存天数 [选填] 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能默认30
logretentiondays: 30
jasypt:
encryptor:
password: qsakjdnfij234234sdf67

View File

@ -0,0 +1,245 @@
# 项目相关配置
chestnut:
# 名称
name: ChestnutCMS
# 版本
version: 1.3.21
# 版权年份
copyrightYear: 2023
system:
# 演示模式开关
demoMode: true
# 文件路径 示例( Windows配置D:/chestnut/uploadPathLinux配置 /home/app/uploadPath
uploadPath: /home/app/uploadPath
# 验证码类型 math 数组计算 char 字符验证
captchaType: math
freemarker:
templateLoaderPath: /home/app/statics
cms:
resourceRoot: /home/app/wwwroot_release
publish:
consumerCount: 1
# 开发环境配置
server:
# 服务器的HTTP端口默认为8090
port: 8090
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.chestnut: debug
org.springframework: warn
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 20MB
# 设置总上传的文件大小
max-request-size: 100MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: false
freemarker:
check-template-location: false
elasticsearch:
uris: http://cc-elasticsearch:9200
username: elastic
password: hello1234
# redis 配置
data:
redis:
# 地址
host: 140.143.157.1
# 端口默认为6379
port: 6379
# 数据库索引
database: 15
# 密码
password: x7Zhj3twzSdDwx2A
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
flyway:
enabled: false
# 迁移sql脚本文件存放路径默认classpath:db/migration
locations: classpath:db/migration/mysql
# 迁移sql脚本文件名称的前缀默认V
sql-migration-prefix: V
# 迁移sql脚本文件名称分隔符默认2个下划线__
sql-migration-separator: __
# 迁移sql脚本文件名称后缀
sql-migration-suffixes: .sql
# 迁移时是否进行校验
validate-on-migrate: true
# 当迁移发现数据库非空且存在没有元数据的表时自动执行基准迁移新建schema_version表
baseline-on-migrate: true
# 数据库配置
datasource:
type: com.zaxxer.hikari.HikariDataSource
dynamic:
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
# 主库
datasource:
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://cd-cynosdbmysql-grp-9rqrhxsm.sql.tencentcdb.com:27981/cms?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: Sxyanzhu@cf
# 从库
# slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
# username:
# password:
hikari:
# 连接池名
pool-name: HikariCP
# 连接超时时间:毫秒, 默认30秒
connection-timeout: 2000
# 最小空闲连接默认值10小于0或大于maximum-pool-size都会重置为maximum-pool-size
minimum-idle: 5
# 最大连接数小于等于0会被重置为默认值10大于零小于1会被重置为minimum-idle的值
maximum-pool-size: 20
# 空闲连接最大存活时间默认值60000010分钟大于等于max-lifetime且max-lifetime>0会被重置为0不等于0且小于10秒会被重置为10秒。
idle-timeout: 200000
# 连接池返回的连接默认自动提交,默认只 true
auto-commit: true
# 连接最大存活时间不等于0且小于30秒会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
max-lifetime: 1800000
# 用于测试连接是否可用的查询语句
connection-test-query: SELECT 1
# 邮件配置
mail:
host: smtp.163.com
port: 465
username: xxx@163.com
# 授权码
password: xxx
# 编码格式
default-encoding: utf-8
# 协议
protocol: smtps
properties:
mail:
smtp:
ssl:
enable: true
auth: true
starttls:
enable: true
required: true
# 监控配置
application:
name: "ChestnutCMS"
boot:
admin:
client:
# 增加客户端开关
enabled: false
# Admin Server URL
url: http://127.0.0.1:8090/admin
instance:
service-host-type: IP
username: chestnut
password: 123456
# SaToken配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token前缀
token-prefix: Bearer
# token有效期单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
active-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
# MyBatis配置
mybatis-plus:
global-config:
enable-sql-runner: true
# 搜索指定包别名
typeAliasesPackage: com.chestnut.**.domain
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
mode: clean
# 过滤链接
urlPatterns:
- /system/*
- /monitor/*
- /tool/*
xxl:
job:
enable: false
accessToken: default_token
adminAddresses: http://127.0.0.1:18080/xxl-job-admin
executor:
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
appname: chestnut-admin
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
#address:
### 执行器IP [选填]默认为空表示自动获取IP多网卡时可手动设置指定IP该IP不会绑定Host仅作为通讯实用地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"
ip:
### 执行器端口号 [选填]小于等于0则自动获取默认端口为9999单机部署多个执行器时注意要配置不同执行器端口
port: 9968
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: E:/dev/workspace_chestnut/ChestnutCMS/chestnut-modules/chestnut-xxljob/jobhandler
### 执行器日志文件保存天数 [选填] 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能默认30
logretentiondays: 30
jasypt:
encryptor:
password: qsakjdnfij234234sdf67

View File

@ -0,0 +1,220 @@
# 项目相关配置
chestnut:
# 名称
name: ChestnutCMS
# 版本
version: 1.3.21
# 版权年份
copyrightYear: 2023
system:
# 演示模式开关
demoMode: true
# 文件路径 示例( Windows配置D:/chestnut/uploadPathLinux配置 /home/app/uploadPath
uploadPath: /home/app/uploadPath
# 验证码类型 math 数组计算 char 字符验证
captchaType: math
freemarker:
templateLoaderPath: /home/app/statics
cms:
resourceRoot: /home/app/wwwroot_release
# 开发环境配置
server:
# 服务器的HTTP端口默认为8090
port: 8090
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.chestnut: debug
org.springframework: warn
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 20MB
# 设置总上传的文件大小
max-request-size: 100MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: false
freemarker:
check-template-location: false
elasticsearch:
uris: http://cc-elasticsearch:9200
username: elastic
password: hello1234
# redis 配置
data:
redis:
# 地址
host: 140.143.157.1
# 端口默认为6379
port: 6379
# 数据库索引
database: 15
# 密码
password: x7Zhj3twzSdDwx2A
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
flyway:
enabled: false
# 迁移sql脚本文件存放路径默认classpath:db/migration
locations: classpath:db/migration/mysql
# 迁移sql脚本文件名称的前缀默认V
sql-migration-prefix: V
# 迁移sql脚本文件名称分隔符默认2个下划线__
sql-migration-separator: __
# 迁移sql脚本文件名称后缀
sql-migration-suffixes: .sql
# 迁移时是否进行校验
validate-on-migrate: true
# 当迁移发现数据库非空且存在没有元数据的表时自动执行基准迁移新建schema_version表
baseline-on-migrate: true
# 数据库配置
datasource:
# type: com.alibaba.druid.pool.DruidDataSource
type: com.zaxxer.hikari.HikariDataSource
dynamic:
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
# 主库
datasource:
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://cd-cynosdbmysql-grp-9rqrhxsm.sql.tencentcdb.com:27981/cms?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: Sxyanzhu@cf
# 从库
#slave:
# lazy: true
# type: ${spring.datasource.type}
# driverClassName: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
# username:
# password:
hikari:
# 连接池名
pool-name: HikariCP
# 连接超时时间:毫秒, 默认30秒
connection-timeout: 2000
# 最小空闲连接默认值10小于0或大于maximum-pool-size都会重置为maximum-pool-size
minimum-idle: 5
# 最大连接数小于等于0会被重置为默认值10大于零小于1会被重置为minimum-idle的值
maximum-pool-size: 20
# 空闲连接最大存活时间默认值60000010分钟大于等于max-lifetime且max-lifetime>0会被重置为0不等于0且小于10秒会被重置为10秒。
idle-timeout: 200000
# 连接池返回的连接默认自动提交,默认只 true
auto-commit: true
# 连接最大存活时间不等于0且小于30秒会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
max-lifetime: 1800000
# 用于测试连接是否可用的查询语句
connection-test-query: SELECT 1
# 监控配置
application:
name: "ChestnutCMS"
boot:
admin:
client:
# 增加客户端开关
enabled: false
# Admin Server URL
url: http://127.0.0.1:8090/admin
instance:
service-host-type: IP
username: chestnut
password: 123456
# SaToken配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token前缀
token-prefix: Bearer
# token有效期单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
active-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
# MyBatis配置
mybatis-plus:
global-config:
enable-sql-runner: true
# 搜索指定包别名
typeAliasesPackage: com.chestnut.**.domain
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
mode: clean
# 过滤链接
urlPatterns:
- /system/*
- /monitor/*
- /tool/*
xxl:
job:
enable: false
accessToken: default_token
adminAddresses: http://127.0.0.1:18080/xxl-job-admin
executor:
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
appname: chestnut-admin
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
#address:
### 执行器IP [选填]默认为空表示自动获取IP多网卡时可手动设置指定IP该IP不会绑定Host仅作为通讯实用地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"
ip:
### 执行器端口号 [选填]小于等于0则自动获取默认端口为9999单机部署多个执行器时注意要配置不同执行器端口
port: 9968
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: E:/dev/workspace_chestnut/ChestnutCMS/chestnut-modules/chestnut-xxljob/jobhandler
### 执行器日志文件保存天数 [选填] 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能默认30
logretentiondays: 30

View File

@ -0,0 +1,5 @@
# Spring配置
spring:
profiles:
active: dev

View File

@ -0,0 +1,2 @@
Application Version: ${chestnut.version}
Spring Boot Version: ${spring-boot.version}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,43 @@
-- ----------------------------
-- Table structure for sys_scheduled_task
-- ----------------------------
DROP TABLE IF EXISTS `sys_scheduled_task`;
CREATE TABLE `sys_scheduled_task` (
`task_id` bigint NOT NULL,
`task_type` varchar(50) NOT NULL,
`status` varchar(1) NOT NULL,
`task_trigger` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`trigger_args` varchar(255) NOT NULL,
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`task_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_scheduled_task_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_scheduled_task_log`;
CREATE TABLE `sys_scheduled_task_log` (
`log_id` bigint NOT NULL,
`task_id` bigint NOT NULL,
`task_type` varchar(50) NOT NULL,
`ready_time` datetime NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`interrupt_time` datetime DEFAULT NULL,
`percent` int DEFAULT NULL,
`result` varchar(1) NOT NULL,
`message` varchar(2000) DEFAULT NULL,
`log_time` datetime NOT NULL,
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `sys_menu` VALUES (2081, '定时任务', 2, 2, 'task', 'monitor/task/index', NULL, 'N', 'Y', 'C', 'Y', '0', NULL, 'job', 'admin', '2023-05-06 19:11:08', '', NULL, '');
INSERT INTO `sys_i18n_dict` VALUES (239, 'zh-CN', 'MENU.NAME.2081', '定时任务');
INSERT INTO `sys_i18n_dict` VALUES (240, 'en', 'MENU.NAME.2081', 'Scheduled Task');

View File

@ -0,0 +1,33 @@
ALTER TABLE cms_video MODIFY file_size BIGINT NULL;
ALTER TABLE cms_video MODIFY format VARCHAR(20) NULL;
ALTER TABLE cms_video MODIFY duration BIGINT NULL;
ALTER TABLE cms_video MODIFY width INT NULL;
ALTER TABLE cms_video MODIFY height INT NULL;
ALTER TABLE cms_video MODIFY bit_rate INT NULL;
ALTER TABLE cms_video MODIFY frame_rate INT NULL;
ALTER TABLE cms_video MODIFY type VARCHAR(10) NULL;
ALTER TABLE cms_video_backup MODIFY file_size BIGINT NULL;
ALTER TABLE cms_video_backup MODIFY format VARCHAR(20) NULL;
ALTER TABLE cms_video_backup MODIFY duration BIGINT NULL;
ALTER TABLE cms_video_backup MODIFY width INT NULL;
ALTER TABLE cms_video_backup MODIFY height INT NULL;
ALTER TABLE cms_video_backup MODIFY bit_rate INT NULL;
ALTER TABLE cms_video_backup MODIFY frame_rate INT NULL;
ALTER TABLE cms_video_backup MODIFY type VARCHAR(10) NULL;
ALTER TABLE cms_audio MODIFY file_size BIGINT NULL;
ALTER TABLE cms_audio MODIFY duration BIGINT NULL;
ALTER TABLE cms_audio MODIFY channels INT NULL;
ALTER TABLE cms_audio MODIFY bit_rate INT NULL;
ALTER TABLE cms_audio MODIFY sampling_rate INT NULL;
ALTER TABLE cms_audio MODIFY type VARCHAR(10) NULL;
ALTER TABLE cms_audio MODIFY format VARCHAR(20) NULL;
ALTER TABLE cms_audio_backup MODIFY file_size BIGINT NULL;
ALTER TABLE cms_audio_backup MODIFY duration BIGINT NULL;
ALTER TABLE cms_audio_backup MODIFY channels INT NULL;
ALTER TABLE cms_audio_backup MODIFY bit_rate INT NULL;
ALTER TABLE cms_audio_backup MODIFY sampling_rate INT NULL;
ALTER TABLE cms_audio_backup MODIFY type VARCHAR(10) NULL;
ALTER TABLE cms_audio_backup MODIFY format VARCHAR(20) NULL;

View File

@ -0,0 +1,228 @@
CREATE TABLE `cms_custom_form` (
`form_id` bigint NOT NULL COMMENT 'ID',
`site_id` bigint NOT NULL COMMENT '站点ID ',
`model_id` bigint NOT NULL COMMENT '关联元数据模型ID',
`name` varchar(100) NOT NULL COMMENT '表单名称',
`code` varchar(50) NOT NULL COMMENT '同站点唯一编码',
`status` int NOT NULL COMMENT '状态',
`templates` varchar(100) DEFAULT NULL COMMENT '模板',
`need_captcha` varchar(1) NOT NULL COMMENT '是否启用验证码',
`need_login` varchar(1) NOT NULL COMMENT '是否需要会员登录',
`rule_limit` varchar(1) NOT NULL COMMENT '唯一性规则限制IP/浏览器指纹)',
`create_by` varchar(64) NOT NULL COMMENT '创建者',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT '' COMMENT '备注',
PRIMARY KEY (`form_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `cms_cfd_default` (
`data_id` bigint NOT NULL COMMENT '数据主键ID',
`model_id` bigint NOT NULL COMMENT '自定义表单ID元数据模型ID',
`site_id` bigint NOT NULL COMMENT '所属站点ID',
`client_ip` varchar(64) NOT NULL COMMENT 'IP',
`uuid` varchar(128) NOT NULL COMMENT '用户唯一标识浏览器指纹、会员ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`short_text1` varchar(50) DEFAULT NULL,
`short_text2` varchar(50) DEFAULT NULL,
`short_text3` varchar(50) DEFAULT NULL,
`short_text4` varchar(50) DEFAULT NULL,
`short_text5` varchar(50) DEFAULT NULL,
`short_text6` varchar(50) DEFAULT NULL,
`short_text7` varchar(50) DEFAULT NULL,
`short_text8` varchar(50) DEFAULT NULL,
`short_text9` varchar(50) DEFAULT NULL,
`short_text10` varchar(50) DEFAULT NULL,
`short_text11` varchar(50) DEFAULT NULL,
`short_text12` varchar(50) DEFAULT NULL,
`short_text13` varchar(50) DEFAULT NULL,
`short_text14` varchar(50) DEFAULT NULL,
`short_text15` varchar(50) DEFAULT NULL,
`short_text16` varchar(50) DEFAULT NULL,
`short_text17` varchar(50) DEFAULT NULL,
`short_text18` varchar(50) DEFAULT NULL,
`short_text19` varchar(50) DEFAULT NULL,
`short_text20` varchar(50) DEFAULT NULL,
`short_text21` varchar(50) DEFAULT NULL,
`short_text22` varchar(50) DEFAULT NULL,
`short_text23` varchar(50) DEFAULT NULL,
`short_text24` varchar(50) DEFAULT NULL,
`short_text25` varchar(50) DEFAULT NULL,
`medium_text1` varchar(200) DEFAULT NULL,
`medium_text2` varchar(200) DEFAULT NULL,
`medium_text3` varchar(200) DEFAULT NULL,
`medium_text4` varchar(200) DEFAULT NULL,
`medium_text5` varchar(200) DEFAULT NULL,
`medium_text6` varchar(200) DEFAULT NULL,
`medium_text7` varchar(200) DEFAULT NULL,
`medium_text8` varchar(200) DEFAULT NULL,
`medium_text9` varchar(200) DEFAULT NULL,
`medium_text10` varchar(200) DEFAULT NULL,
`medium_text11` varchar(200) DEFAULT NULL,
`medium_text12` varchar(200) DEFAULT NULL,
`medium_text13` varchar(200) DEFAULT NULL,
`medium_text14` varchar(200) DEFAULT NULL,
`medium_text15` varchar(200) DEFAULT NULL,
`medium_text16` varchar(200) DEFAULT NULL,
`medium_text17` varchar(200) DEFAULT NULL,
`medium_text18` varchar(200) DEFAULT NULL,
`medium_text19` varchar(200) DEFAULT NULL,
`medium_text20` varchar(200) DEFAULT NULL,
`medium_text21` varchar(200) DEFAULT NULL,
`medium_text22` varchar(200) DEFAULT NULL,
`medium_text23` varchar(200) DEFAULT NULL,
`medium_text24` varchar(200) DEFAULT NULL,
`medium_text25` varchar(200) DEFAULT NULL,
`large_text1` varchar(2000) DEFAULT NULL,
`large_text2` varchar(2000) DEFAULT NULL,
`large_text3` varchar(2000) DEFAULT NULL,
`large_text4` varchar(2000) DEFAULT NULL,
`clob_text1` mediumtext,
`long1` bigint DEFAULT NULL,
`long2` bigint DEFAULT NULL,
`long3` bigint DEFAULT NULL,
`long4` bigint DEFAULT NULL,
`long5` bigint DEFAULT NULL,
`long6` bigint DEFAULT NULL,
`long7` bigint DEFAULT NULL,
`long8` bigint DEFAULT NULL,
`long9` bigint DEFAULT NULL,
`long10` bigint DEFAULT NULL,
`double1` double(255,0) DEFAULT NULL,
`double2` double(255,0) DEFAULT NULL,
`double3` double(255,0) DEFAULT NULL,
`double4` double(255,0) DEFAULT NULL,
`double5` double(255,0) DEFAULT NULL,
`double6` double(255,0) DEFAULT NULL,
`double7` double(255,0) DEFAULT NULL,
`double8` double(255,0) DEFAULT NULL,
`double9` double(255,0) DEFAULT NULL,
`double10` double(255,0) DEFAULT NULL,
`date1` datetime DEFAULT NULL,
`date2` datetime DEFAULT NULL,
`date3` datetime DEFAULT NULL,
`date4` datetime DEFAULT NULL,
`date5` datetime DEFAULT NULL,
`date6` datetime DEFAULT NULL,
`date7` datetime DEFAULT NULL,
`date8` datetime DEFAULT NULL,
`date9` datetime DEFAULT NULL,
`date10` datetime DEFAULT NULL,
PRIMARY KEY (`data_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE `cms_exd_default` (
`data_id` bigint NOT NULL COMMENT '关联数据ID',
`data_type` varchar(20) NOT NULL COMMENT '关联数据类型',
`model_id` bigint NOT NULL COMMENT '元数据模型ID',
`short_text1` varchar(50) DEFAULT NULL,
`short_text2` varchar(50) DEFAULT NULL,
`short_text3` varchar(50) DEFAULT NULL,
`short_text4` varchar(50) DEFAULT NULL,
`short_text5` varchar(50) DEFAULT NULL,
`short_text6` varchar(50) DEFAULT NULL,
`short_text7` varchar(50) DEFAULT NULL,
`short_text8` varchar(50) DEFAULT NULL,
`short_text9` varchar(50) DEFAULT NULL,
`short_text10` varchar(50) DEFAULT NULL,
`short_text11` varchar(50) DEFAULT NULL,
`short_text12` varchar(50) DEFAULT NULL,
`short_text13` varchar(50) DEFAULT NULL,
`short_text14` varchar(50) DEFAULT NULL,
`short_text15` varchar(50) DEFAULT NULL,
`short_text16` varchar(50) DEFAULT NULL,
`short_text17` varchar(50) DEFAULT NULL,
`short_text18` varchar(50) DEFAULT NULL,
`short_text19` varchar(50) DEFAULT NULL,
`short_text20` varchar(50) DEFAULT NULL,
`short_text21` varchar(50) DEFAULT NULL,
`short_text22` varchar(50) DEFAULT NULL,
`short_text23` varchar(50) DEFAULT NULL,
`short_text24` varchar(50) DEFAULT NULL,
`short_text25` varchar(50) DEFAULT NULL,
`medium_text1` varchar(200) DEFAULT NULL,
`medium_text2` varchar(200) DEFAULT NULL,
`medium_text3` varchar(200) DEFAULT NULL,
`medium_text4` varchar(200) DEFAULT NULL,
`medium_text5` varchar(200) DEFAULT NULL,
`medium_text6` varchar(200) DEFAULT NULL,
`medium_text7` varchar(200) DEFAULT NULL,
`medium_text8` varchar(200) DEFAULT NULL,
`medium_text9` varchar(200) DEFAULT NULL,
`medium_text10` varchar(200) DEFAULT NULL,
`medium_text11` varchar(200) DEFAULT NULL,
`medium_text12` varchar(200) DEFAULT NULL,
`medium_text13` varchar(200) DEFAULT NULL,
`medium_text14` varchar(200) DEFAULT NULL,
`medium_text15` varchar(200) DEFAULT NULL,
`medium_text16` varchar(200) DEFAULT NULL,
`medium_text17` varchar(200) DEFAULT NULL,
`medium_text18` varchar(200) DEFAULT NULL,
`medium_text19` varchar(200) DEFAULT NULL,
`medium_text20` varchar(200) DEFAULT NULL,
`medium_text21` varchar(200) DEFAULT NULL,
`medium_text22` varchar(200) DEFAULT NULL,
`medium_text23` varchar(200) DEFAULT NULL,
`medium_text24` varchar(200) DEFAULT NULL,
`medium_text25` varchar(200) DEFAULT NULL,
`large_text1` varchar(2000) DEFAULT NULL,
`large_text2` varchar(2000) DEFAULT NULL,
`large_text3` varchar(2000) DEFAULT NULL,
`large_text4` varchar(2000) DEFAULT NULL,
`clob_text1` mediumtext,
`long1` bigint DEFAULT NULL,
`long2` bigint DEFAULT NULL,
`long3` bigint DEFAULT NULL,
`long4` bigint DEFAULT NULL,
`long5` bigint DEFAULT NULL,
`long6` bigint DEFAULT NULL,
`long7` bigint DEFAULT NULL,
`long8` bigint DEFAULT NULL,
`long9` bigint DEFAULT NULL,
`long10` bigint DEFAULT NULL,
`double1` double(255,0) DEFAULT NULL,
`double2` double(255,0) DEFAULT NULL,
`double3` double(255,0) DEFAULT NULL,
`double4` double(255,0) DEFAULT NULL,
`double5` double(255,0) DEFAULT NULL,
`double6` double(255,0) DEFAULT NULL,
`double7` double(255,0) DEFAULT NULL,
`double8` double(255,0) DEFAULT NULL,
`double9` double(255,0) DEFAULT NULL,
`double10` double(255,0) DEFAULT NULL,
`date1` datetime DEFAULT NULL,
`date2` datetime DEFAULT NULL,
`date3` datetime DEFAULT NULL,
`date4` datetime DEFAULT NULL,
`date5` datetime DEFAULT NULL,
`date6` datetime DEFAULT NULL,
`date7` datetime DEFAULT NULL,
`date8` datetime DEFAULT NULL,
`date9` datetime DEFAULT NULL,
`date10` datetime DEFAULT NULL,
PRIMARY KEY (`data_id`,`data_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `sys_menu` VALUES (430649774219333, '自定义表单', 2035, 5, 'customform', 'cms/customform/index', NULL, 'N', 'Y', 'C', 'Y', '0', 'cms:custom:view', 'form', 'admin', '2023-06-20 10:12:08', 'admin', '2023-06-20 10:13:09', '');
INSERT INTO `sys_i18n_dict` VALUES (null, 'zh-CN', 'MENU.NAME.430649774219333', '自定义表单');
INSERT INTO `sys_i18n_dict` VALUES (null, 'en', 'MENU.NAME.430649774219333', 'Custom Form');
ALTER TABLE cms_content add column deleted tinyint DEFAULT 0;
ALTER TABLE cms_article_detail add column deleted tinyint DEFAULT 0;
ALTER TABLE cms_image add column deleted tinyint DEFAULT 0;
ALTER TABLE cms_audio add column deleted tinyint DEFAULT 0;
ALTER TABLE cms_video add column deleted tinyint DEFAULT 0;
ALTER TABLE sys_config modify column config_id bigint;
ALTER TABLE sys_notice modify column notice_id bigint;
DROP TABLE x_model_data;
DROP TABLE cms_content_backup;
DROP TABLE cms_article_detail_backup;
DROP TABLE cms_image_backup;
DROP TABLE cms_audio_backup;
DROP TABLE cms_video_backup;
alter table cc_comment drop column del_flag;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
ALTER TABLE cms_cfd_default CHANGE COLUMN date1 datetime1 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date2 datetime2 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date3 datetime3 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date4 datetime4 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date5 datetime5 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date6 datetime6 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date7 datetime7 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date8 datetime8 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date9 datetime9 datetime;
ALTER TABLE cms_cfd_default CHANGE COLUMN date10 datetime10 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date1 datetime1 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date2 datetime2 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date3 datetime3 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date4 datetime4 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date5 datetime5 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date6 datetime6 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date7 datetime7 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date8 datetime8 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date9 datetime9 datetime;
ALTER TABLE cms_exd_default CHANGE COLUMN date10 datetime10 datetime;

View File

@ -0,0 +1,22 @@
ALTER TABLE cms_content ADD COLUMN like_count bigint default 0;
ALTER TABLE cms_content ADD COLUMN comment_count bigint default 0;
ALTER TABLE cms_content ADD COLUMN favorite_count bigint default 0;
ALTER TABLE cms_content ADD COLUMN view_count bigint default 0;
CREATE TABLE `cms_member_favorites` (
`log_id` bigint NOT NULL,
`site_id` bigint NOT NULL COMMENT '站点ID',
`content_id` bigint NOT NULL COMMENT '内容ID',
`member_id` bigint NOT NULL COMMENT '会员ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `cms_member_like` (
`log_id` bigint NOT NULL,
`site_id` bigint NOT NULL COMMENT '站点ID',
`content_id` bigint NOT NULL COMMENT '内容ID',
`member_id` bigint NOT NULL COMMENT '会员ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

View File

@ -0,0 +1,58 @@
ALTER TABLE cms_content ADD COLUMN contributor_id bigint default 0;
ALTER TABLE cc_member ADD COLUMN slogan varchar(255);
ALTER TABLE cc_member ADD COLUMN description varchar(500);
ALTER TABLE cc_member ADD COLUMN cover varchar(100);
DROP TABLE cms_member_favorites;
DROP TABLE cms_member_like;
CREATE TABLE `cc_member_favorites` (
`log_id` bigint NOT NULL,
`member_id` bigint NOT NULL COMMENT '会员ID',
`data_type` varchar(100) NOT NULL COMMENT '收藏数据类型',
`data_id` bigint NOT NULL COMMENT '收藏数据ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `cc_member_like` (
`log_id` bigint NOT NULL,
`member_id` bigint NOT NULL COMMENT '会员ID',
`data_type` varchar(100) NOT NULL COMMENT '点赞数据类型',
`data_id` bigint NOT NULL COMMENT '点赞数据ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `cc_member_follow` (
`log_id` bigint NOT NULL,
`member_id` bigint NOT NULL COMMENT '会员ID',
`follow_member_id` bigint NOT NULL COMMENT '关注的会员ID',
PRIMARY KEY (`log_id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `cc_member_stat_data` (
`member_id` bigint NOT NULL COMMENT '会员ID',
`int_value1` int NOT NULL DEFAULT '0',
`int_value2` int NOT NULL DEFAULT '0',
`int_value3` int NOT NULL DEFAULT '0',
`int_value4` int NOT NULL DEFAULT '0',
`int_value5` int NOT NULL DEFAULT '0',
`int_value6` int NOT NULL DEFAULT '0',
`int_value7` int NOT NULL DEFAULT '0',
`int_value8` int NOT NULL DEFAULT '0',
`int_value9` int NOT NULL DEFAULT '0',
`int_value10` int NOT NULL DEFAULT '0',
`int_value11` int NOT NULL DEFAULT '0',
`int_value12` int NOT NULL DEFAULT '0',
`int_value13` int NOT NULL DEFAULT '0',
`int_value14` int NOT NULL DEFAULT '0',
`int_value15` int NOT NULL DEFAULT '0',
`int_value16` int NOT NULL DEFAULT '0',
`int_value17` int NOT NULL DEFAULT '0',
`int_value18` int NOT NULL DEFAULT '0',
`int_value19` int NOT NULL DEFAULT '0',
`int_value20` int NOT NULL DEFAULT '0',
PRIMARY KEY (`member_id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

View File

@ -0,0 +1 @@
ALTER TABLE cms_video ADD COLUMN cover varchar(255);

View File

@ -0,0 +1,33 @@
ALTER TABLE cms_exd_default DROP PRIMARY KEY, ADD PRIMARY KEY(data_id, data_type, model_id);
ALTER TABLE cc_comment CHANGE COLUMN deleted del_flag int;
ALTER TABLE cc_member CHANGE COLUMN phonenumber phone_number varchar(20);
ALTER TABLE sys_user CHANGE COLUMN phonenumber phone_number varchar(20);
ALTER TABLE x_model_field ADD COLUMN sort_flag bigint DEFAULT '0' COMMENT '排序字段';
CREATE TABLE `cms_catalog_content_stat` (
`catalog_id` bigint NOT NULL COMMENT '栏目ID',
`site_id` bigint NOT NULL COMMENT '站点ID',
`draft_total` int NOT NULL DEFAULT '0' COMMENT '初稿内容数',
`to_publish_total` int NOT NULL DEFAULT '0' COMMENT '待发布内容数',
`published_total` int NOT NULL DEFAULT '0' COMMENT '已发布内容数',
`offline_total` int NOT NULL DEFAULT '0' COMMENT '已下线内容数',
`editing_total` int NOT NULL DEFAULT '0' COMMENT '重新编辑内容数',
PRIMARY KEY (`catalog_id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `cms_user_content_stat` (
`id` varchar(100) NOT NULL COMMENT 'ID',
`site_id` bigint NOT NULL COMMENT '站点ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`user_name` varchar(100) NOT NULL COMMENT '用户名',
`draft_total` int NOT NULL DEFAULT '0' COMMENT '初稿内容数',
`to_publish_total` int NOT NULL DEFAULT '0' COMMENT '待发布内容数',
`published_total` int NOT NULL DEFAULT '0' COMMENT '已发布内容数',
`offline_total` int NOT NULL DEFAULT '0' COMMENT '已下线内容数',
`editing_total` int NOT NULL DEFAULT '0' COMMENT '重新编辑内容数',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

View File

@ -0,0 +1,2 @@
ALTER TABLE x_model_field ADD COLUMN validations varchar(500) DEFAULT '' COMMENT '校验规则';
ALTER TABLE x_model_field DROP COLUMN mandatory_flag;

View File

@ -0,0 +1 @@
ALTER TABLE cc_comment add column deleted tinyint DEFAULT 0;

View File

@ -0,0 +1 @@
ALTER TABLE search_log add column location varchar(255);

View File

@ -0,0 +1,13 @@
#错误消息
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.login.success=登录成功
user.register.success=注册成功
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB
upload.filename.exceed.length=上传的文件名最长{0}个字符

View File

@ -0,0 +1,13 @@
#错误消息
user.jcaptcha.error=Invalid captcha.
user.jcaptcha.expire=Captcha expired.
user.password.not.match=User not exists or password error.
user.password.retry.limit.count=Password input error {0} times.
user.password.retry.limit.exceed=Password input error {0} times, account locking {1} minutes.
user.login.success=Login success.
user.register.success=Register success.
##文件上传消息
upload.exceed.maxSize=Upload file size limit, max size is: {0}MB.
upload.filename.exceed.length=Upload file name length limit ,max length is: {0}.

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="out" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/out.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/out.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 定时任务输出 -->
<appender name="cron" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/cron.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/cron.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.chestnut" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!--控制台日志-->
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="out" />
<appender-ref ref="error" />
</root>
<!--系统定时任务日志-->
<logger name="cron" level="info">
<appender-ref ref="cron"/>
</logger>
</configuration>

View File

@ -0,0 +1,28 @@
package com.chestnut.member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.chestnut.member.domain.Member;
import com.chestnut.member.service.IMemberExpConfigService;
import com.chestnut.member.service.IMemberService;
@SpringBootTest
public class MemberTest {
@Autowired
private IMemberService memberService;
@Autowired
private IMemberExpConfigService expConfigService;
@Test
void testMemberSignIn() {
Member member = this.memberService.getById(398339741712453L);
expConfigService.list().forEach(expConfig -> {
expConfigService.triggerExpOperation(expConfig.getOpType(), member.getMemberId());
});
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.3.21</version>
</parent>
<artifactId>chestnut-cms-advertisement</artifactId>
<description>广告模块</description>
<dependencies>
<!-- 数据统计 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-stat</artifactId>
</dependency>
<!-- 内容核心 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-contentcore</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,27 @@
package com.chestnut.advertisement;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.contentcore.core.AbstractPageWidget;
/**
* 广
*
* @author
* @email 190785909@qq.com
*/
public class AdSpacePageWidget extends AbstractPageWidget {
private final IAdvertisementService advertisementService = SpringUtils.getBean(IAdvertisementService.class);
@Override
public void delete() {
super.delete();
// 删除广告版位相关的广告
this.advertisementService.remove(new LambdaQueryWrapper<CmsAdvertisement>()
.eq(CmsAdvertisement::getAdSpaceId, this.getPageWidgetEntity().getPageWidgetId()));
// TODO 删除广告统计数据
}
}

View File

@ -0,0 +1,70 @@
package com.chestnut.advertisement;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import com.chestnut.advertisement.pojo.AdSpaceProps;
import com.chestnut.advertisement.pojo.vo.AdSpaceVO;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.contentcore.core.IPageWidget;
import com.chestnut.contentcore.core.IPageWidgetType;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.vo.PageWidgetVO;
/**
* 广
*
* @author
* @email 190785909@qq.com
*/
@Component(IPageWidgetType.BEAN_NAME_PREFIX + AdSpacePageWidgetType.ID)
public class AdSpacePageWidgetType implements IPageWidgetType {
public final static String ID = "ads";
public final static String NAME = "{CMS.CONTENCORE.PAGEWIDGET." + ID + "}";
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getIcon() {
return "el-icon-list";
}
@Override
public String getRoute() {
return "/cms/adspace/editor";
}
@Override
public IPageWidget loadPageWidget(CmsPageWidget cmsPageWdiget) {
AdSpacePageWidget pw = new AdSpacePageWidget();
pw.setPageWidgetEntity(cmsPageWdiget);
return pw;
}
@Override
public IPageWidget newInstance() {
return new AdSpacePageWidget();
}
@Override
public PageWidgetVO getPageWidgetVO(CmsPageWidget pageWidget) {
AdSpaceVO vo = new AdSpaceVO();
BeanUtils.copyProperties(pageWidget, vo);
vo.setContent(this.parseContent(pageWidget, null, true));
return vo;
}
@Override
public AdSpaceProps parseContent(CmsPageWidget pageWidget, String publishPipeCode, boolean isPreview) {
return JacksonUtils.from(pageWidget.getContent(), AdSpaceProps.class);
}
}

View File

@ -0,0 +1,16 @@
package com.chestnut.advertisement;
/**
* 广
*/
public interface IAdvertisementType {
/**
* Bean
*/
public static final String BEAN_NAME_PREFIX = "AdvertisementType_";
public String getId();
public String getName();
}

View File

@ -0,0 +1,21 @@
package com.chestnut.advertisement;
import org.springframework.stereotype.Component;
@Component(IAdvertisementType.BEAN_NAME_PREFIX + ImageAdvertisementType.ID)
public class ImageAdvertisementType implements IAdvertisementType {
public static final String ID = "image";
public static final String NAME = "{ADVERTISEMENT.TYPE." + ID + "}";
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return NAME;
}
}

View File

@ -0,0 +1,122 @@
package com.chestnut.advertisement.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.advertisement.domain.CmsAdClickLog;
import com.chestnut.advertisement.domain.CmsAdHourStat;
import com.chestnut.advertisement.domain.CmsAdViewLog;
import com.chestnut.advertisement.mapper.CmsAdClickLogMapper;
import com.chestnut.advertisement.mapper.CmsAdHourStatMapper;
import com.chestnut.advertisement.mapper.CmsAdViewLogMapper;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.domain.R;
import com.chestnut.common.security.anno.Priv;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.DateUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.system.security.AdminUserType;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@Priv(type = AdminUserType.TYPE)
@RequiredArgsConstructor
@RestController
@RequestMapping("/cms/ad/stat")
public class AdLogController extends BaseRestController {
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyyMMddHH");
private final ISiteService siteService;
private final IAdvertisementService advService;
private final CmsAdHourStatMapper advHourStatMapper;
private final CmsAdClickLogMapper adClickLogMapper;
private final CmsAdViewLogMapper adViewLogMapper;
@GetMapping
public R<?> getAdStatSum(@RequestParam(required = false) Date beginTime,
@RequestParam(required = false) Date endTime) {
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
String begin = Objects.isNull(beginTime) ? null : FORMAT.format(beginTime);
String end = Objects.isNull(endTime) ? null : FORMAT.format(endTime);
List<CmsAdHourStat> list = this.advHourStatMapper.selectGroupByAdvId(site.getSiteId(), begin, end);
if (list.size() > 0) {
Map<String, String> map = this.advService.getAdvertisementMap();
list.forEach(l -> l.setAdName(map.get(l.getAdvertisementId().toString())));
}
return this.bindDataTable(list);
}
@GetMapping("/chart")
public R<?> getLineChartStatDatas(@RequestParam @Min(1) Long advertisementId, @RequestParam Date beginTime, @RequestParam Date endTime) {
List<CmsAdHourStat> list = this.advHourStatMapper.selectHourStat(advertisementId, FORMAT.format(beginTime), FORMAT.format(endTime));
if (list.size() > 0) {
Map<String, String> map = this.advService.getAdvertisementMap();
list.forEach(l -> l.setAdName(map.get(l.getAdvertisementId().toString())));
}
Map<String, CmsAdHourStat> collect = list.stream().collect(Collectors.toMap(CmsAdHourStat::getHour, s -> s));
List<String> xAxisDatas = new ArrayList<>();
Map<String, List<Integer>> lineDatas = new HashMap<>();
List<Integer> clickDatas = new ArrayList<>();
List<Integer> viewDatas = new ArrayList<>();
while (!beginTime.after(endTime)) {
String hourStr = DateUtils.parseDateToStr("yyyyMMddHH", beginTime);
xAxisDatas.add(hourStr);
CmsAdHourStat stat = collect.get(hourStr);
clickDatas.add(Objects.isNull(stat) ? 0 : stat.getClick());
viewDatas.add(Objects.isNull(stat) ? 0 : stat.getView());
beginTime = DateUtils.addHours(beginTime, 1);
}
lineDatas.put("Click", clickDatas);
lineDatas.put("View", viewDatas);
return R.ok(Map.of("xAxisDatas", xAxisDatas, "lineDatas", lineDatas));
}
@GetMapping("/click")
public R<?> listAdClickLogs() {
PageRequest pr = getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
LambdaQueryWrapper<CmsAdClickLog> q = new LambdaQueryWrapper<CmsAdClickLog>()
.eq(CmsAdClickLog::getSiteId, site.getSiteId()).orderByDesc(CmsAdClickLog::getLogId);
Page<CmsAdClickLog> page = adClickLogMapper.selectPage(new Page<>(pr.getPageNumber(), pr.getPageSize(), true),
q);
if (page.getRecords().size() > 0) {
Map<String, String> map = this.advService.getAdvertisementMap();
page.getRecords().forEach(l -> l.setAdName(map.get(l.getAdId().toString())));
}
return this.bindDataTable(page);
}
@GetMapping("/view")
public R<?> listAdViewLogs() {
PageRequest pr = getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
LambdaQueryWrapper<CmsAdViewLog> q = new LambdaQueryWrapper<CmsAdViewLog>()
.eq(CmsAdViewLog::getSiteId, site.getSiteId()).orderByDesc(CmsAdViewLog::getLogId);
Page<CmsAdViewLog> page = adViewLogMapper.selectPage(new Page<>(pr.getPageNumber(), pr.getPageSize(), true), q);
if (page.getRecords().size() > 0) {
Map<String, String> map = this.advService.getAdvertisementMap();
page.getRecords().forEach(l -> l.setAdName(map.get(l.getAdId().toString())));
}
return this.bindDataTable(page);
}
}

View File

@ -0,0 +1,140 @@
package com.chestnut.advertisement.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.advertisement.AdSpacePageWidgetType;
import com.chestnut.advertisement.pojo.vo.AdSpaceVO;
import com.chestnut.common.domain.R;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.security.anno.Priv;
import com.chestnut.common.security.domain.LoginUser;
import com.chestnut.common.security.web.BaseRestController;
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;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsPageWidget;
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.vo.PageWidgetVO;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPageWidgetService;
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.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 广
* </p>
*
* @author
* @email 190785909@qq.com
*/
@Priv(type = AdminUserType.TYPE)
@RequiredArgsConstructor
@RestController
@RequestMapping("/cms/adspace")
public class AdSpaceController extends BaseRestController {
private final ISiteService siteService;
private final ICatalogService catalogService;
private final IPageWidgetService pageWidgetService;
private final AdSpacePageWidgetType pageWidgetType;
@GetMapping
public R<?> listAdSpaces(@RequestParam(name = "catalogId", required = false) Long catalogId,
@RequestParam(name = "name", required = false) String name,
@RequestParam(name = "state", required = false) Integer state) {
PageRequest pr = getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
LambdaQueryWrapper<CmsPageWidget> q = new LambdaQueryWrapper<CmsPageWidget>()
.eq(CmsPageWidget::getSiteId, site.getSiteId())
.eq(catalogId != null && catalogId > 0, CmsPageWidget::getCatalogId, catalogId)
.like(StringUtils.isNotEmpty(name), CmsPageWidget::getName, name)
.eq(CmsPageWidget::getType, AdSpacePageWidgetType.ID)
.eq(state != null && state > -1, CmsPageWidget::getState, state)
.orderByDesc(CmsPageWidget::getCreateTime);
Page<CmsPageWidget> page = pageWidgetService.page(new Page<>(pr.getPageNumber(), pr.getPageSize(), true), q);
List<AdSpaceVO> list = new ArrayList<>();
page.getRecords().forEach(pw -> {
AdSpaceVO vo = (AdSpaceVO) pageWidgetType.getPageWidgetVO(pw);
if (pw.getCatalogId() > 0) {
CmsCatalog catalog = catalogService.getCatalog(pw.getCatalogId());
vo.setCatalogName(catalog != null ? catalog.getName() : "未知");
}
list.add(vo);
});
return this.bindDataTable(list, (int) page.getTotal());
}
@GetMapping("/{adSpaceId}")
public R<PageWidgetVO> getAdSpaceInfo(@PathVariable("adSpaceId") Long adSpaceId) {
CmsPageWidget pageWidget = this.pageWidgetService.getById(adSpaceId);
if (pageWidget == null) {
return R.fail("数据未找到:" + adSpaceId);
}
AdSpaceVO vo = (AdSpaceVO) pageWidgetType.getPageWidgetVO(pageWidget);
CmsCatalog catalog = this.catalogService.getCatalog(pageWidget.getCatalogId());
vo.setCatalogName(catalog != null ? catalog.getName() : "未知");
return R.ok(vo);
}
@PostMapping
public R<?> addAdSpace(HttpServletRequest request) throws IOException {
PageWidgetAddDTO dto = JacksonUtils.from(request.getInputStream(), PageWidgetAddDTO.class);
dto.setType(pageWidgetType.getId());
CmsPageWidget cmsPageWdiget = new CmsPageWidget();
BeanUtils.copyProperties(dto, cmsPageWdiget);
IPageWidget pw = pageWidgetType.newInstance();
pw.setPageWidgetEntity(cmsPageWdiget);
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);
IPageWidget pw = pageWidgetType.newInstance();
pw.setPageWidgetEntity(cmsPageWdiget);
pw.setOperator(StpAdminUtil.getLoginUser());
this.pageWidgetService.savePageWidget(pw);
return R.ok();
}
@DeleteMapping
public R<?> deleteAdSpaces(@RequestBody List<Long> adSpaceIds) {
Assert.notEmpty(adSpaceIds, () -> CommonErrorCode.INVALID_REQUEST_ARG.exception("adSpaceIds"));
this.pageWidgetService.deletePageWidgets(adSpaceIds, StpAdminUtil.getLoginUser());
return R.ok();
}
@PostMapping("/publish")
public R<?> publishPageWidgets(@RequestBody List<Long> adSpaceIds) throws TemplateException, IOException {
Assert.notEmpty(adSpaceIds, () -> CommonErrorCode.INVALID_REQUEST_ARG.exception("adSpaceIds"));
this.pageWidgetService.publishPageWidgets(adSpaceIds, StpAdminUtil.getLoginUser());
return R.ok();
}
}

View File

@ -0,0 +1,137 @@
package com.chestnut.advertisement.controller;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.advertisement.IAdvertisementType;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.permission.CmsAdvertisementPriv;
import com.chestnut.advertisement.pojo.dto.AdvertisementDTO;
import com.chestnut.advertisement.pojo.vo.AdvertisementVO;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.domain.R;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.i18n.I18nUtils;
import com.chestnut.common.log.annotation.Log;
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.utils.Assert;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
/**
* <p>
* 广
* </p>
*
* @author
* @email 190785909@qq.com
*/
@Priv(type = AdminUserType.TYPE, value = CmsAdvertisementPriv.View)
@RequiredArgsConstructor
@RestController
@RequestMapping("/cms/advertisement")
public class AdvertisementController extends BaseRestController {
private final IAdvertisementService advertisementService;
@GetMapping("/types")
public R<?> listAdvertisements() {
List<Map<String, String>> list = advertisementService.getAdvertisementTypeList().stream()
.map(t -> Map.of("id", t.getId(), "name", I18nUtils.get(t.getName()))).toList();
return this.bindDataTable(list);
}
@GetMapping
public R<?> listAdvertisements(@RequestParam(name = "adSpaceId") @Min(1) Long adSpaceId,
@RequestParam(name = "name", required = false) String name,
@RequestParam(name = "state", required = false) Integer state) {
PageRequest pr = getPageRequest();
Page<CmsAdvertisement> page = this.advertisementService.lambdaQuery()
.eq(CmsAdvertisement::getAdSpaceId, adSpaceId)
.like(StringUtils.isNotEmpty(name), CmsAdvertisement::getName, name)
.eq(state != null && state > -1, CmsAdvertisement::getState, state)
.orderByDesc(CmsAdvertisement::getCreateTime).page(new Page<>(pr.getPageNumber(), pr.getPageSize(), true));
page.getRecords().forEach(adv -> {
IAdvertisementType advertisementType = this.advertisementService.getAdvertisementType(adv.getType());
if (Objects.nonNull(advertisementType)) {
adv.setTypeName(I18nUtils.get(advertisementType.getName()));
}
});
return this.bindDataTable(page.getRecords(), (int) page.getTotal());
}
@GetMapping("/{advertisementId}")
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());
}
@Log(title = "新增广告", businessType = BusinessType.INSERT)
@PostMapping
public R<?> addAdvertisement(@RequestBody AdvertisementDTO dto) throws IOException {
dto.setOperator(StpAdminUtil.getLoginUser());
this.advertisementService.addAdvertisement(dto);
return R.ok();
}
@Log(title = "编辑广告", businessType = BusinessType.UPDATE)
@PutMapping
public R<?> editAdvertisement(@RequestBody AdvertisementDTO dto) throws IOException {
dto.setOperator(StpAdminUtil.getLoginUser());
this.advertisementService.saveAdvertisement(dto);
return R.ok();
}
@Log(title = "删除广告", businessType = BusinessType.DELETE)
@DeleteMapping
public R<?> deleteAdvertisements(@RequestBody List<Long> advertisementIds) {
if (StringUtils.isEmpty(advertisementIds)) {
return R.fail(StringUtils.messageFormat("参数[{0}]不能为空", "advertisementIds"));
}
this.advertisementService.deleteAdvertisement(advertisementIds);
return R.ok();
}
@Log(title = "启用广告", businessType = BusinessType.UPDATE)
@PutMapping("/enable")
public R<?> enableAdvertisements(@RequestBody List<Long> advertisementIds) {
if (StringUtils.isEmpty(advertisementIds)) {
return R.fail(StringUtils.messageFormat("参数[{0}]不能为空", "advertisementIds"));
}
this.advertisementService.enableAdvertisement(advertisementIds,
StpAdminUtil.getLoginUser().getUsername());
return R.ok();
}
@Log(title = "禁用广告", businessType = BusinessType.UPDATE)
@PutMapping("/disable")
public R<?> disableAdvertisements(@RequestBody List<Long> advertisementIds) {
if (StringUtils.isEmpty(advertisementIds)) {
return R.fail(StringUtils.messageFormat("参数[{0}]不能为空", "advertisementIds"));
}
this.advertisementService.disableAdvertisement(advertisementIds,
StpAdminUtil.getLoginUser().getUsername());
return R.ok();
}
}

View File

@ -0,0 +1,74 @@
package com.chestnut.advertisement.controller.front;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import com.chestnut.common.utils.StringUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.chestnut.advertisement.domain.CmsAdClickLog;
import com.chestnut.advertisement.domain.CmsAdViewLog;
import com.chestnut.advertisement.service.IAdvertisementStatService;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.ServletUtils;
import lombok.RequiredArgsConstructor;
/**
* 广
*
* @author
* @email 190785909@qq.com
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/adv")
public class AdApiController extends BaseRestController {
private final IAdvertisementStatService adStatService;
@GetMapping("/redirect")
public void statAndRedirect(@RequestParam("sid") Long siteId,
@RequestParam("aid") Long advertisementId,
@RequestParam("url") String redirectUrl,
HttpServletResponse response) throws IOException {
this.adClick(siteId, advertisementId);
response.sendRedirect(URLDecoder.decode(redirectUrl, StandardCharsets.UTF_8));
}
@GetMapping("/click")
public void adClick(@RequestParam("sid") Long siteId, @RequestParam("aid") Long advertisementId) {
try {
CmsAdClickLog log = new CmsAdClickLog();
log.fill(ServletUtils.getRequest());
log.setSiteId(siteId);
log.setAdId(advertisementId);
log.setEvtTime(LocalDateTime.now());
this.adStatService.adClick(log);
} catch (Exception e) {
log.error("Advertisement click stat failed: " + advertisementId, e);
}
}
@GetMapping("/view")
public void adView(@RequestParam("sid") Long siteId, @RequestParam("aid") Long advertisementId) {
try {
CmsAdViewLog log = new CmsAdViewLog();
log.fill(ServletUtils.getRequest());
log.setSiteId(siteId);
log.setAdId(advertisementId);
log.setEvtTime(LocalDateTime.now());
this.adStatService.adView(log);
} catch (Exception e) {
log.error("Advertisement view stat failed: " + advertisementId, e);
}
}
}

View File

@ -0,0 +1,41 @@
package com.chestnut.advertisement.domain;
import java.io.Serializable;
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.chestnut.stat.RequestEvent;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName(CmsAdClickLog.TABLE_NAME)
public class CmsAdClickLog extends RequestEvent implements Serializable {
private static final long serialVersionUID = 1L;
public final static String TABLE_NAME = "cms_ad_click_log";
@TableId(value = "log_id", type = IdType.AUTO)
private Long logId;
/**
* ID
*/
private Long siteId;
/**
* 广ID
*/
private Long adId;
/**
* 广
*/
@TableField(exist = false)
private String adName;
}

View File

@ -0,0 +1,61 @@
package com.chestnut.advertisement.domain;
import java.io.Serializable;
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 lombok.Getter;
import lombok.Setter;
/**
* 广/
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
@TableName(CmsAdHourStat.TABLE_NAME)
public class CmsAdHourStat implements Serializable {
private static final long serialVersionUID = 1L;
public final static String TABLE_NAME = "cms_ad_hour_stat";
@TableId(value = "stat_id", type = IdType.AUTO)
private Long statId;
/**
* ID
*/
private Long siteId;
/**
* yyyyMMddHH
*/
private String hour;
/**
* 广ID
*/
private Long advertisementId;
/**
*
*/
private Integer click;
/**
*
*/
private Integer view;
/**
* 广
*/
@TableField(exist = false)
private String adName;
}

View File

@ -0,0 +1,41 @@
package com.chestnut.advertisement.domain;
import java.io.Serializable;
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.chestnut.stat.RequestEvent;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName(CmsAdViewLog.TABLE_NAME)
public class CmsAdViewLog extends RequestEvent implements Serializable {
private static final long serialVersionUID = 1L;
public final static String TABLE_NAME = "cms_ad_view_log";
@TableId(value = "log_id", type = IdType.AUTO)
private Long logId;
/**
* ID
*/
private Long siteId;
/**
* 广ID
*/
private Long adId;
/**
* 广
*/
@TableField(exist = false)
private String adName;
}

View File

@ -0,0 +1,99 @@
package com.chestnut.advertisement.domain;
import java.time.LocalDateTime;
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.chestnut.common.db.domain.BaseEntity;
import com.chestnut.system.fixed.dict.EnableOrDisable;
import lombok.Getter;
import lombok.Setter;
/**
* 广 [cms_advertisement]
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
@TableName(CmsAdvertisement.TABLE_NAME)
public class CmsAdvertisement extends BaseEntity {
private static final long serialVersionUID = 1L;
public static final String TABLE_NAME = "cms_advertisement";
@TableId(value = "advertisement_id", type = IdType.INPUT)
private Long advertisementId;
/**
* ID
*/
private Long siteId;
/**
* 广IDID
*/
private Long adSpaceId;
/**
*
*/
private String type;
/**
*
*/
@TableField(exist = false)
private String typeName;
/**
*
*/
private String name;
/**
*
*/
private Integer weight;
/**
*
*/
private String keywords;
/**
*
*
* @see EnableOrDisable
*/
private String state;
/**
* 线
*/
private LocalDateTime onlineDate;
/**
* 线
*/
private LocalDateTime offlineDate;
/**
*
*/
private String redirectUrl;
/**
*
*/
private String resourcePath;
public boolean isEnable() {
return EnableOrDisable.isEnable(this.state);
}
}

View File

@ -0,0 +1,98 @@
package com.chestnut.advertisement.job;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chestnut.advertisement.AdSpacePageWidgetType;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.contentcore.core.IPageWidgetType;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.fixed.dict.PageWidgetStatus;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.contentcore.service.IPublishService;
import com.chestnut.system.fixed.dict.EnableOrDisable;
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.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* 广线
*
* @author
* @email 190785909@qq.com
*/
@RequiredArgsConstructor
@Component(IScheduledHandler.BEAN_PREFIX + AdvertisementPublishJob.JOB_NAME)
public class AdvertisementPublishJob extends IJobHandler implements IScheduledHandler {
static final String JOB_NAME = "AdvertisementPublishJob";
private final IPageWidgetService pageWidgetService;
private final IAdvertisementService advertisementService;
private final IPublishService publishService;
@Override
public String getId() {
return JOB_NAME;
}
@Override
public String getName() {
return "{SCHEDULED_TASK." + JOB_NAME + "}";
}
@Override
public void exec() throws Exception {
logger.info("Job start: {}", JOB_NAME);
long s = System.currentTimeMillis();
LocalDateTime now = LocalDateTime.now();
List<CmsPageWidget> list = this.pageWidgetService.list(new LambdaQueryWrapper<CmsPageWidget>()
.eq(CmsPageWidget::getState, PageWidgetStatus.PUBLISHED)
.eq(CmsPageWidget::getType, AdSpacePageWidgetType.ID));
for (CmsPageWidget adSpace : list) {
boolean changed = false;
List<CmsAdvertisement> toOnlineList = this.advertisementService.list(new LambdaQueryWrapper<CmsAdvertisement>()
.eq(CmsAdvertisement::getState, EnableOrDisable.DISABLE)
.eq(CmsAdvertisement::getAdSpaceId, adSpace.getPageWidgetId())
.le(CmsAdvertisement::getOnlineDate, now)
.ge(CmsAdvertisement::getOfflineDate, now));
if (toOnlineList != null && toOnlineList.size() > 0) {
changed = true;
for (CmsAdvertisement ad : toOnlineList) {
ad.setState(EnableOrDisable.ENABLE);
}
this.advertisementService.updateBatchById(toOnlineList);
}
// 下线时间小于当前时间的启用广告标记为停用
List<CmsAdvertisement> toOfflineList = this.advertisementService.list(new LambdaQueryWrapper<CmsAdvertisement>()
.eq(CmsAdvertisement::getState, EnableOrDisable.ENABLE)
.eq(CmsAdvertisement::getAdSpaceId, adSpace.getPageWidgetId())
.lt(CmsAdvertisement::getOfflineDate, now));
if (toOfflineList != null && toOfflineList.size() > 0) {
changed = true;
for (CmsAdvertisement ad : toOfflineList) {
ad.setState(EnableOrDisable.DISABLE);
}
this.advertisementService.updateBatchById(toOfflineList);
}
// 有变化重新发布广告版位
if (changed) {
IPageWidgetType pwt = this.pageWidgetService.getPageWidgetType(adSpace.getType());
this.publishService.pageWidgetStaticize(pwt.loadPageWidget(adSpace));
}
}
logger.info("Job '{}' completed, cost: {}ms", JOB_NAME, System.currentTimeMillis() - s);
}
@Override
@XxlJob(JOB_NAME)
public void execute() throws Exception {
this.exec();
}
}

View File

@ -0,0 +1,117 @@
package com.chestnut.advertisement.job;
import com.chestnut.advertisement.domain.CmsAdHourStat;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.service.IAdHourStatService;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.advertisement.service.impl.AdvertisementStatServiceImpl;
import com.chestnut.common.redis.RedisCache;
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.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 广
*
* @author
* @email 190785909@qq.com
*/
@RequiredArgsConstructor
@Component(IScheduledHandler.BEAN_PREFIX + AdvertisementStatJob.JOB_NAME)
public class AdvertisementStatJob extends IJobHandler implements IScheduledHandler {
static final String JOB_NAME = "AdvertisementStatJob";
private final IAdHourStatService adStatService;
private final IAdvertisementService advertisementService;
private final RedisCache redisCache;
@Override
public String getId() {
return JOB_NAME;
}
@Override
public String getName() {
return "{SCHEDULED_TASK." + JOB_NAME + "}";
}
@Override
public void exec() throws Exception {
logger.info("Job start: {}", JOB_NAME);
long s = System.currentTimeMillis();
try {
// 数据更新
String hour = LocalDateTime.now().format(AdvertisementStatServiceImpl.DATE_TIME_FORMAT);
this.saveToDb(hour, false);
// 尝试更新上一个小时数据并删除cache
String yestoday = LocalDateTime.now().minusHours(1).format(AdvertisementStatServiceImpl.DATE_TIME_FORMAT);
this.saveToDb(yestoday, true);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
logger.info("Job '{}' completed, cost: {}ms", JOB_NAME, System.currentTimeMillis() - s);
}
private void saveToDb(String hour, boolean deleteCache) {
String clickCacheKey = AdvertisementStatServiceImpl.CLIC_CACHE_PREFIX + hour;
String viewCacheKey = AdvertisementStatServiceImpl.VIEW_CACHE_PREFIX + hour;
Map<Long, CmsAdHourStat> stats = this.adStatService.lambdaQuery().eq(CmsAdHourStat::getHour, hour).list()
.stream().collect(Collectors.toMap(CmsAdHourStat::getAdvertisementId, stat -> stat));
Map<Long, Long> advertisements = advertisementService.lambdaQuery()
.select(List.of(CmsAdvertisement::getAdvertisementId, CmsAdvertisement::getSiteId)).list().stream()
.collect(Collectors.toMap(CmsAdvertisement::getAdvertisementId, CmsAdvertisement::getSiteId));
List<Long> insertAdvIds = new ArrayList<>();
for (Long advertisementId : advertisements.keySet()) {
int click = this.redisCache.getZsetScore(clickCacheKey, advertisementId.toString()).intValue();
int view = this.redisCache.getZsetScore(viewCacheKey, advertisementId.toString()).intValue();
if (click > 0 || view > 0) {
CmsAdHourStat stat = stats.get(advertisementId);
if (Objects.isNull(stat)) {
stat = new CmsAdHourStat();
stat.setSiteId(advertisements.get(advertisementId));
stat.setHour(hour);
stat.setAdvertisementId(advertisementId);
stats.put(advertisementId, stat);
insertAdvIds.add(advertisementId);
}
stat.setClick(Math.max(click, 0));
stat.setView(Math.max(view, 0));
}
}
// 更新数据库
List<CmsAdHourStat> inserts = stats.values().stream()
.filter(stat -> insertAdvIds.contains(stat.getAdvertisementId())).toList();
this.adStatService.saveBatch(inserts);
List<CmsAdHourStat> updates = stats.values().stream()
.filter(stat -> !insertAdvIds.contains(stat.getAdvertisementId())).toList();
this.adStatService.updateBatchById(updates);
// 清理过期缓存
if (deleteCache) {
this.redisCache.deleteObject(clickCacheKey);
this.redisCache.deleteObject(viewCacheKey);
}
}
@Override
@XxlJob(JOB_NAME)
public void execute() throws Exception {
this.exec();
}
}

View File

@ -0,0 +1,37 @@
package com.chestnut.advertisement.listener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.listener.event.BeforeSiteDeleteEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class AdvertisementListener {
private final IAdvertisementService advertisementService;
@EventListener
public void beforeSiteDelete(BeforeSiteDeleteEvent event) {
CmsSite site = event.getSite();
int pageSize = 500;
// 删除广告数据
try {
long total = this.advertisementService
.count(new LambdaQueryWrapper<CmsAdvertisement>().eq(CmsAdvertisement::getSiteId, site.getSiteId()));
for (int i = 0; i * pageSize < total; i++) {
AsyncTaskManager.setTaskProgressInfo((int) (i * pageSize * 100 / total), "正在删除广告数据:" + (i * pageSize) + "/" + total);
this.advertisementService.remove(new LambdaQueryWrapper<CmsAdvertisement>()
.eq(CmsAdvertisement::getSiteId, site.getSiteId()).last("limit " + pageSize));
}
} catch (Exception e) {
e.printStackTrace();
AsyncTaskManager.addErrMessage("删除广告数据错误:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,9 @@
package com.chestnut.advertisement.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chestnut.advertisement.domain.CmsAdClickLog;
public interface CmsAdClickLogMapper extends BaseMapper<CmsAdClickLog> {
}

View File

@ -0,0 +1,36 @@
package com.chestnut.advertisement.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chestnut.advertisement.domain.CmsAdHourStat;
public interface CmsAdHourStatMapper extends BaseMapper<CmsAdHourStat> {
@Select("""
<script>
SELECT * FROM `cms_ad_hour_stat`
WHERE advertisement_id = #{advertisementId}
<if test='begin != null'> and hour &gt;= #{begin} </if>
<if test='end != null'> and hour &lt;= #{end} </if>
ORDER BY hour ASC
</script>
""")
public List<CmsAdHourStat> selectHourStat(@Param("advertisementId") Long advertisementId,
@Param("begin") String begin, @Param("end") String end);
@Select("""
<script>
SELECT advertisement_id, sum(click) click, sum(view) view FROM `cms_ad_hour_stat`
WHERE site_id = #{siteId}
<if test='begin != null'> AND hour &gt;= #{begin} </if>
<if test='end != null'> AND hour &lt;= #{end} </if>
GROUP BY advertisement_id ORDER BY `click` DESC, `view` DESC
</script>
""")
public List<CmsAdHourStat> selectGroupByAdvId(@Param("siteId") Long siteId, @Param("begin") String begin,
@Param("end") String end);
}

View File

@ -0,0 +1,9 @@
package com.chestnut.advertisement.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chestnut.advertisement.domain.CmsAdViewLog;
public interface CmsAdViewLogMapper extends BaseMapper<CmsAdViewLog> {
}

View File

@ -0,0 +1,12 @@
package com.chestnut.advertisement.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chestnut.advertisement.domain.CmsAdvertisement;
/**
* 广
*/
public interface CmsAdvertisementMapper extends BaseMapper<CmsAdvertisement> {
}

View File

@ -0,0 +1,6 @@
package com.chestnut.advertisement.permission;
public interface CmsAdvertisementPriv {
public String View = "cms:advertisement:view";
}

View File

@ -0,0 +1,20 @@
package com.chestnut.advertisement.pojo;
import lombok.Getter;
import lombok.Setter;
/**
* 广
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
public class AdSpaceProps {
/**
* 广
*/
private String strategy;
}

View File

@ -0,0 +1,74 @@
package com.chestnut.advertisement.pojo.dto;
import java.time.LocalDateTime;
import jakarta.validation.constraints.NotNull;
import com.chestnut.common.security.domain.BaseDTO;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AdvertisementDTO extends BaseDTO {
/**
* 广ID
*/
@NotNull
private Long advertisementId;
/**
* 广IDID
*/
@NotNull
private Long adSpaceId;
/**
* 广
*/
@NotNull
private String type;
/**
*
*/
@NotNull
private String name;
/**
*
*/
@NotNull
private Integer weight;
/**
*
*/
private String keywords;
/**
* 线
*/
@NotNull
private LocalDateTime onlineDate;
/**
* 线
*/
@NotNull
private LocalDateTime offlineDate;
/**
*
*/
@NotNull
private String redirectUrl;
/**
*
*/
@NotNull
private String resourcePath;
}

View File

@ -0,0 +1,19 @@
package com.chestnut.advertisement.pojo.vo;
import com.chestnut.advertisement.pojo.AdSpaceProps;
import com.chestnut.contentcore.domain.vo.PageWidgetVO;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Getter
@Setter
@Accessors(chain = true)
public class AdSpaceVO extends PageWidgetVO {
/**
* 广
*/
private AdSpaceProps content;
}

View File

@ -0,0 +1,125 @@
package com.chestnut.advertisement.pojo.vo;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 广VO
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
@NoArgsConstructor
public class AdvertisementVO {
/**
* 广ID
*/
private Long advertisementId;
/**
* 广ID
*/
private Long adSpaceId;
/**
*
*/
private String type;
/**
*
*/
private String name;
/**
*
*/
private Integer weight;
/**
*
*/
private String keywords;
/**
*
*/
private String state;
/**
* 线
*/
private LocalDateTime onlineDate;
/**
* 线
*/
private LocalDateTime offlineDate;
/**
*
*/
private String redirectUrl;
/**
*
*/
private String link;
/**
*
*/
private String resourcePath;
/**
*
*/
private String resourceSrc;
/**
*
*/
private String createBy;
/**
*
*/
private LocalDateTime createTime;
public AdvertisementVO(CmsAdvertisement ad) {
this.advertisementId = ad.getAdvertisementId();
this.adSpaceId = ad.getAdSpaceId();
this.type = ad.getType();
this.name = ad.getName();
this.weight = ad.getWeight();
this.keywords = ad.getKeywords();
this.state = ad.getState();
this.onlineDate = ad.getOnlineDate();
this.offlineDate = ad.getOfflineDate();
this.redirectUrl = ad.getRedirectUrl();
this.resourcePath = ad.getResourcePath();
this.createBy = ad.getCreateBy();
this.createTime = ad.getCreateTime();
}
public AdvertisementVO dealPreviewResourcePath() {
return dealResourcePath(null, true);
}
public AdvertisementVO dealResourcePath(String publishPipeCode, boolean isPreview) {
if (StringUtils.isNotEmpty(this.getResourcePath())) {
this.setResourceSrc(InternalUrlUtils.getActualUrl(this.getResourcePath(), publishPipeCode, isPreview));
}
return this;
}
}

View File

@ -0,0 +1,11 @@
package com.chestnut.advertisement.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.advertisement.domain.CmsAdHourStat;
/**
* 广
*/
public interface IAdHourStatService extends IService<CmsAdHourStat> {
}

View File

@ -0,0 +1,84 @@
package com.chestnut.advertisement.service;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.advertisement.IAdvertisementType;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.pojo.dto.AdvertisementDTO;
/**
* 广Service
*/
public interface IAdvertisementService extends IService<CmsAdvertisement> {
/**
* 广<ID, NAME>
*
* @return Map
*/
Map<String, String> getAdvertisementMap();
/**
* 广
*
* @param dto 广DTO
* @return CmsAdvertisement
*/
CmsAdvertisement addAdvertisement(AdvertisementDTO dto);
/**
* 广
*
* @param dto 广DTO
* @return CmsAdvertisement
*/
CmsAdvertisement saveAdvertisement(AdvertisementDTO dto);
/**
* 广
*
* @param advertisementIds 广ID
*/
void deleteAdvertisement(List<Long> advertisementIds);
/**
* 广
*
* @param typeId 广
* @return 广
*/
IAdvertisementType getAdvertisementType(String typeId);
/**
* 广
*
* @return 广
*/
List<IAdvertisementType> getAdvertisementTypeList();
/**
* 广
*
* @param advertisementIds 广ID
*/
void enableAdvertisement(List<Long> advertisementIds, String operator);
/**
* 广
*
* @param advertisementIds 广ID
*/
void disableAdvertisement(List<Long> advertisementIds, String operator);
/**
* 广
*
* @param adv 广
* @param publishPipeCode
* @return 广
*/
String getAdvertisementStatLink(CmsAdvertisement adv, String publishPipeCode);
}

View File

@ -0,0 +1,26 @@
package com.chestnut.advertisement.service;
import com.chestnut.advertisement.domain.CmsAdClickLog;
import com.chestnut.advertisement.domain.CmsAdViewLog;
/**
* 广Service
*/
public interface IAdvertisementStatService {
/**
* 广
*
* @param advertisementIds
* @return
*/
public void adClick(CmsAdClickLog log);
/**
* 广
*
* @param advertisementIds
* @return
*/
public void adView(CmsAdViewLog log);
}

View File

@ -0,0 +1,25 @@
package com.chestnut.advertisement.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.advertisement.domain.CmsAdHourStat;
import com.chestnut.advertisement.mapper.CmsAdHourStatMapper;
import com.chestnut.advertisement.service.IAdHourStatService;
import lombok.RequiredArgsConstructor;
/**
* <p>
* 广
* </p>
*
* @author
* @email 190785909@qq.com
*/
@Service
@RequiredArgsConstructor
public class AdHourStatServiceImpl extends ServiceImpl<CmsAdHourStatMapper, CmsAdHourStat>
implements IAdHourStatService {
}

View File

@ -0,0 +1,141 @@
package com.chestnut.advertisement.service.impl;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.properties.SiteApiUrlProperty;
import com.chestnut.contentcore.service.ISiteService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.advertisement.IAdvertisementType;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.mapper.CmsAdvertisementMapper;
import com.chestnut.advertisement.pojo.dto.AdvertisementDTO;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.redis.RedisCache;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.contentcore.config.CMSConfig;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.system.fixed.dict.EnableOrDisable;
import lombok.RequiredArgsConstructor;
/**
* <p>
* 广
* </p>
*
* @author
* @email 190785909@qq.com
*/
@Service
@RequiredArgsConstructor
public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper, CmsAdvertisement>
implements IAdvertisementService {
private static final String CACHE_KEY_ADV_IDS = CMSConfig.CachePrefix + "adv-ids";
private final RedisCache redisCache;
private final Map<String, IAdvertisementType> advertisementTypes;
private final IPageWidgetService pageWidgetService;
private final ISiteService siteService;
@Override
public IAdvertisementType getAdvertisementType(String typeId) {
return this.advertisementTypes.get(IAdvertisementType.BEAN_NAME_PREFIX + typeId);
}
@Override
public List<IAdvertisementType> getAdvertisementTypeList() {
return this.advertisementTypes.values().stream().toList();
}
@Override
public Map<String, String> getAdvertisementMap() {
return this.redisCache.getCacheMap(CACHE_KEY_ADV_IDS,
() -> this.lambdaQuery().select(List.of(CmsAdvertisement::getAdvertisementId, CmsAdvertisement::getName)).list()
.stream().collect(
Collectors.toMap(ad -> ad.getAdvertisementId().toString(), CmsAdvertisement::getName)));
}
@Override
public CmsAdvertisement addAdvertisement(AdvertisementDTO dto) {
CmsPageWidget pageWidget = this.pageWidgetService.getById(dto.getAdSpaceId());
Assert.notNull(pageWidget,
() -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("adSpaceId", dto.getAdSpaceId()));
CmsAdvertisement advertisement = new CmsAdvertisement();
BeanUtils.copyProperties(dto, advertisement);
advertisement.setAdvertisementId(IdUtils.getSnowflakeId());
advertisement.setSiteId(pageWidget.getSiteId());
advertisement.setState(EnableOrDisable.ENABLE);
advertisement.createBy(dto.getOperator().getUsername());
this.save(advertisement);
this.redisCache.deleteObject(CACHE_KEY_ADV_IDS);
return advertisement;
}
@Override
public CmsAdvertisement saveAdvertisement(AdvertisementDTO dto) {
CmsAdvertisement advertisement = this.getById(dto.getAdvertisementId());
Assert.notNull(advertisement,
() -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("advertisementId", dto.getAdvertisementId()));
BeanUtils.copyProperties(dto, advertisement, "adSpaceId");
advertisement.updateBy(dto.getOperator().getUsername());
this.updateById(advertisement);
return advertisement;
}
@Override
public void deleteAdvertisement(List<Long> advertisementIds) {
this.removeByIds(advertisementIds);
this.redisCache.deleteObject(CACHE_KEY_ADV_IDS);
}
@Override
public void enableAdvertisement(List<Long> advertisementIds, String operator) {
List<CmsAdvertisement> list = this.listByIds(advertisementIds);
for (CmsAdvertisement ad : list) {
if (!ad.isEnable()) {
ad.setState(EnableOrDisable.ENABLE);
ad.updateBy(operator);
}
}
this.updateBatchById(list);
}
@Override
public void disableAdvertisement(List<Long> advertisementIds, String operator) {
List<CmsAdvertisement> list = this.listByIds(advertisementIds);
for (CmsAdvertisement ad : list) {
if (ad.isEnable()) {
ad.setState(EnableOrDisable.DISABLE);
ad.updateBy(operator);
}
}
this.updateBatchById(list);
// todo 重新发布
}
@Override
public String getAdvertisementStatLink(CmsAdvertisement adv, String publishPipeCode) {
CmsSite site = this.siteService.getSite(adv.getSiteId());
String apiUrl = SiteApiUrlProperty.getValue(site, publishPipeCode);
return apiUrl + "api/adv/redirect?sid=" + adv.getSiteId() + "&aid=" + adv.getAdvertisementId()
+ "&url=" + URLEncoder.encode(adv.getRedirectUrl(), StandardCharsets.UTF_8);
}
}

View File

@ -0,0 +1,73 @@
package com.chestnut.advertisement.service.impl;
import com.chestnut.advertisement.domain.CmsAdClickLog;
import com.chestnut.advertisement.domain.CmsAdViewLog;
import com.chestnut.advertisement.mapper.CmsAdClickLogMapper;
import com.chestnut.advertisement.mapper.CmsAdViewLogMapper;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.advertisement.service.IAdvertisementStatService;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.redis.RedisCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class AdvertisementStatServiceImpl implements IAdvertisementStatService {
public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH");
public static final String CLIC_CACHE_PREFIX = "adv:stat-click:";
public static final String VIEW_CACHE_PREFIX = "adv:stat-view:";
private final CmsAdClickLogMapper clickLogMapper;
private final CmsAdViewLogMapper viewLogMapper;
private final RedisCache redisCache;
private final IAdvertisementService advService;
private final AsyncTaskManager asyncTaskManager;
@Override
public void adClick(CmsAdClickLog clickLog) {
Map<String, String> advMap = advService.getAdvertisementMap();
if (Objects.isNull(advMap) || Objects.isNull(clickLog.getAdId())
|| !advMap.containsKey(clickLog.getAdId().toString())) {
log.warn("Cms adv click log err, invalid id: " + clickLog.getAdId());
return;
}
this.asyncTaskManager.execute(() -> {
// redis 广告小时点击数+1
String cacheKey = CLIC_CACHE_PREFIX + clickLog.getEvtTime().format(DATE_TIME_FORMAT);
redisCache.zsetIncr(cacheKey, clickLog.getAdId().toString(), 1);
// 记录点击日志
this.clickLogMapper.insert(clickLog);
});
}
@Override
public void adView(CmsAdViewLog viewLog) {
Map<String, String> advMap = advService.getAdvertisementMap();
if (Objects.isNull(advMap) || Objects.isNull(viewLog.getAdId())
|| !advMap.containsKey(viewLog.getAdId().toString())) {
log.warn("Cms adv view log err, invalid id: " + viewLog.getAdId());
return;
}
this.asyncTaskManager.execute(() -> {
// redis 广告日展现数+1
String cacheKey = VIEW_CACHE_PREFIX + viewLog.getEvtTime().format(DATE_TIME_FORMAT);
redisCache.zsetIncr(cacheKey, viewLog.getAdId().toString(), 1);
// 记录展现日志
this.viewLogMapper.insert(viewLog);
});
}
}

View File

@ -0,0 +1,22 @@
package com.chestnut.advertisement.stat;
import java.util.List;
import org.springframework.stereotype.Component;
import com.chestnut.stat.IStatType;
import com.chestnut.stat.StatMenu;
@Component
public class AdvertisementStatType implements IStatType {
private final static List<StatMenu> STAT_MENU = List.of(new StatMenu("CmsAdv", "", "{STAT.MENU.CmsAdv}", 2),
new StatMenu("CmsAdStat", "CmsAdv", "{STAT.MENU.CmsAdStat}", 1),
new StatMenu("CmsAdClickLog", "CmsAdv", "{STAT.MENU.CmsAdClickLog}", 2),
new StatMenu("CmsAdViewLog", "CmsAdv", "{STAT.MENU.CmsAdViewLog}", 3));
@Override
public List<StatMenu> getStatMenus() {
return STAT_MENU;
}
}

View File

@ -0,0 +1,126 @@
package com.chestnut.advertisement.template.tag;
import java.util.List;
import java.util.Map;
import com.chestnut.common.staticize.tag.TagAttrOption;
import com.chestnut.contentcore.fixed.config.SiteApiUrl;
import com.chestnut.contentcore.properties.SiteApiUrlProperty;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.pojo.vo.AdvertisementVO;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.staticize.enums.TagAttrDataType;
import com.chestnut.common.staticize.tag.AbstractListTag;
import com.chestnut.common.staticize.tag.TagAttr;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.system.fixed.dict.EnableOrDisable;
import freemarker.core.Environment;
import freemarker.template.TemplateException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class CmsAdvertisementTag extends AbstractListTag {
public final static String TAG_NAME = "cms_advertisement";
public final static String NAME = "{FREEMARKER.TAG.NAME." + TAG_NAME + "}";
public final static String DESC = "{FREEMARKER.TAG.DESC." + TAG_NAME + "}";
final static String TagAttr_Code = "code";
final static String TagAttr_RedirectType = "type";
private final IAdvertisementService advertisementService;
private final IPageWidgetService pageWidgetService;
@Override
public List<TagAttr> getTagAttrs() {
List<TagAttr> tagAttrs = super.getTagAttrs();
tagAttrs.add(new TagAttr(TagAttr_Code, true, TagAttrDataType.STRING, "广告位编码"));
tagAttrs.add(new TagAttr(TagAttr_RedirectType, false, TagAttrDataType.STRING, "广告跳转方式",
RedirectType.toTagAttrOptions(), RedirectType.None.name()));
return tagAttrs;
}
@Override
public TagPageData prepareData(Environment env, Map<String, String> attrs, boolean page, int size, int pageIndex) throws TemplateException {
String code = MapUtils.getString(attrs, TagAttr_Code);
String redirectType = MapUtils.getString(attrs, TagAttr_RedirectType, RedirectType.None.name());
Long siteId = FreeMarkerUtils.evalLongVariable(env, "Site.siteId");
CmsPageWidget adSpace = this.pageWidgetService.getOne(new LambdaQueryWrapper<CmsPageWidget>()
.eq(CmsPageWidget::getSiteId, siteId)
.eq(CmsPageWidget::getCode, code));
if (adSpace == null) {
throw new TemplateException(StringUtils.messageFormat("<@{0}>AD place `{1}` not exists.", this.getTagName(), code), env) ;
}
String condition = MapUtils.getString(attrs, TagAttr.AttrName_Condition);
LambdaQueryWrapper<CmsAdvertisement> q = new LambdaQueryWrapper<CmsAdvertisement>()
.eq(CmsAdvertisement::getAdSpaceId, adSpace.getPageWidgetId())
.eq(CmsAdvertisement::getState, EnableOrDisable.ENABLE);
q.apply(StringUtils.isNotEmpty(condition), condition);
Page<CmsAdvertisement> pageResult = this.advertisementService.page(new Page<>(pageIndex, size, page), q);
if (pageIndex > 1 & pageResult.getRecords().size() == 0) {
throw new TemplateException(StringUtils.messageFormat("Page data empty: pageIndex = {0}", pageIndex), env) ;
}
TemplateContext context = FreeMarkerUtils.getTemplateContext(env);
List<AdvertisementVO> list = pageResult.getRecords().stream().map(ad ->{
AdvertisementVO vo = new AdvertisementVO(ad);
if (RedirectType.isStat(redirectType)) {
vo.setLink(this.advertisementService.getAdvertisementStatLink(ad, context.getPublishPipeCode()));
} else {
vo.setLink(vo.getRedirectUrl());
}
return vo;
}).toList();
return TagPageData.of(list, pageResult.getTotal());
}
@Override
public String getTagName() {
return TAG_NAME;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getDescription() {
return DESC;
}
private enum RedirectType {
None("原始链接"),
Stat("统计链接");
private final String desc;
RedirectType(String desc) {
this.desc = desc;
}
static boolean isStat(String level) {
return Stat.name().equalsIgnoreCase(level);
}
static List<TagAttrOption> toTagAttrOptions() {
return List.of(
new TagAttrOption(None.name(), None.desc),
new TagAttrOption(Stat.name(), Stat.desc)
);
}
}
}

View File

@ -0,0 +1,19 @@
# 页面部件类型
CMS.CONTENCORE.PAGEWIDGET.ads=广告位
# 广告类型
ADVERTISEMENT.TYPE.image=图片
# 模板freemarker
FREEMARKER.TAG.NAME.cms_advertisement=广告列表标签
FREEMARKER.TAG.DESC.cms_advertisement=获取广告数据列表,内嵌<#list DataList as ad>${ad.name}</#list>遍历数据
# 统计菜单
STAT.MENU.CmsAdv=广告数据统计
STAT.MENU.CmsAdStat=广告综合统计
STAT.MENU.CmsAdClickLog=广告点击日志
STAT.MENU.CmsAdViewLog=广告展现日志
# 定时任务
SCHEDULED_TASK.AdvertisementStatJob=广告统计任务
SCHEDULED_TASK.AdvertisementPublishJob=广告定时发布下线任务

View File

@ -0,0 +1,19 @@
# 页面部件类型
CMS.CONTENCORE.PAGEWIDGET.ads=AD
# 广告类型
ADVERTISEMENT.TYPE.image=Image
# 模板freemarker
FREEMARKER.TAG.NAME.cms_advertisement=Advertisement list tag
FREEMARKER.TAG.DESC.cms_advertisement=Fetch advertising data list, use <#list> in tag like "<#list DataList as ad>${ad.name}</#list>" to walk through the list of ad.
# 统计菜单
STAT.MENU.CmsAdv=Advertising Statistics
STAT.MENU.CmsAdStat=Overview
STAT.MENU.CmsAdClickLog=Click Logs
STAT.MENU.CmsAdViewLog=View Logs
# 定时任务
SCHEDULED_TASK.AdvertisementStatJob=AD Statistics Task
SCHEDULED_TASK.AdvertisementPublishJob=AD Publish/Offline Task

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.3.21</version>
</parent>
<artifactId>chestnut-cms-article</artifactId>
<description>文章内容类型扩展模块</description>
<dependencies>
<!-- 内容核心 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-contentcore</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,5 @@
package com.chestnut.article;
public class ArticleConsts {
}

View File

@ -0,0 +1,128 @@
package com.chestnut.article;
import java.util.regex.Matcher;
import org.springframework.beans.BeanUtils;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.article.properties.AutoArticleLogo;
import com.chestnut.article.service.IArticleService;
import com.chestnut.article.service.impl.ArticleServiceImpl;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.utils.HtmlUtils;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.AbstractContent;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.system.fixed.dict.YesOrNo;
public class ArticleContent extends AbstractContent<CmsArticleDetail> {
private IArticleService articleService;
@Override
public Long add() {
super.add();
if (!this.hasExtendEntity()) {
this.getContentService().save(this.getContentEntity());
return this.getContentEntity().getContentId();
}
CmsArticleDetail articleDetail = this.getExtendEntity();
articleDetail.setContentId(this.getContentEntity().getContentId());
articleDetail.setSiteId(this.getContentEntity().getSiteId());
// 处理内部链接
String contentHtml = this.getArticleService().saveInternalUrl(articleDetail.getContentHtml());
// 处理文章正文远程图片
if (YesOrNo.isYes(articleDetail.getDownloadRemoteImage())) {
AsyncTaskManager.setTaskPercent(90);
contentHtml = this.getArticleService().downloadRemoteImages(contentHtml, this.getSite(),
this.getOperator().getUsername());
}
articleDetail.setContentHtml(contentHtml);
// 正文首图作为logo
if (StringUtils.isEmpty(this.getContentEntity().getLogo())
&& AutoArticleLogo.getValue(this.getSite().getConfigProps())) {
this.getContentEntity().setLogo(this.getFirstImage(articleDetail.getContentHtml()));
}
this.getContentService().save(this.getContentEntity());
this.getArticleService().save(articleDetail);
return this.getContentEntity().getContentId();
}
@Override
public Long save() {
super.save();
// 非映射内容或标题内容修改文章详情
if (!this.hasExtendEntity()) {
this.getContentService().updateById(this.getContentEntity());
return this.getContentEntity().getContentId();
}
CmsArticleDetail articleDetail = this.getExtendEntity();
// 处理内部链接
String contentHtml = this.getArticleService().saveInternalUrl(articleDetail.getContentHtml());
// 处理文章正文远程图片
if (YesOrNo.isYes(articleDetail.getDownloadRemoteImage())) {
AsyncTaskManager.setTaskPercent(90);
contentHtml = this.getArticleService().downloadRemoteImages(contentHtml, this.getSite(),
this.getOperator().getUsername());
}
articleDetail.setContentHtml(contentHtml);
// 正文首图作为logo
if (StringUtils.isEmpty(this.getContentEntity().getLogo())
&& AutoArticleLogo.getValue(this.getSite().getConfigProps())) {
this.getContentEntity().setLogo(this.getFirstImage(articleDetail.getContentHtml()));
}
this.getContentService().updateById(this.getContentEntity());
this.getArticleService().updateById(articleDetail);
return this.getContentEntity().getContentId();
}
/**
*
*/
private String getFirstImage(String contentHtml) {
if (StringUtils.isEmpty(contentHtml)) {
return contentHtml;
}
Matcher matcher = ArticleServiceImpl.ImgHtmlTagPattern.matcher(contentHtml);
if (matcher.find()) {
String imgSrc = matcher.group(1);
if (StringUtils.isNotEmpty(imgSrc)) {
return imgSrc;
}
}
return null;
}
@Override
public void delete() {
super.delete();
if (this.hasExtendEntity()) {
this.getArticleService().removeById(this.getContentEntity().getContentId());
}
}
@Override
public void copyTo(CmsCatalog toCatalog, Integer copyType) {
super.copyTo(toCatalog, copyType);
if (this.hasExtendEntity()) {
Long newContentId = (Long) this.getParams().get("NewContentId");
CmsArticleDetail newArticleDetail = new CmsArticleDetail();
BeanUtils.copyProperties(this.getExtendEntity(), newArticleDetail, "contentId");
newArticleDetail.setContentId(newContentId);
this.getArticleService().save(newArticleDetail);
}
}
@Override
public String getFullText() {
return super.getFullText() + StringUtils.SPACE + HtmlUtils.clean(this.getExtendEntity().getContentHtml());
}
public IArticleService getArticleService() {
if (this.articleService == null) {
this.articleService = SpringUtils.getBean(IArticleService.class);
}
return this.articleService;
}
}

View File

@ -0,0 +1,163 @@
package com.chestnut.article;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.article.domain.dto.ArticleDTO;
import com.chestnut.article.domain.vo.ArticleVO;
import com.chestnut.article.mapper.CmsArticleDetailMapper;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IContent;
import com.chestnut.contentcore.core.IContentType;
import com.chestnut.contentcore.core.IPublishPipeProp.PublishPipePropUseType;
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.vo.ContentVO;
import com.chestnut.contentcore.enums.ContentOpType;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import com.chestnut.contentcore.mapper.CmsContentMapper;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.util.InternalUrlUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@Component(IContentType.BEAN_NAME_PREFIX + ArticleContentType.ID)
@RequiredArgsConstructor
public class ArticleContentType implements IContentType {
public final static String ID = "article";
private final static String NAME = "{CMS.CONTENTCORE.CONTENT_TYPE." + ID + "}";
private final CmsContentMapper contentMapper;
private final CmsArticleDetailMapper articleMapper;
private final ICatalogService catalogService;
private final IPublishPipeService publishPipeService;
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getComponent() {
return "cms/article/editor";
}
@Override
public IContent<?> newContent() {
return new ArticleContent();
}
@Override
public IContent<?> loadContent(CmsContent xContent) {
ArticleContent articleContent = new ArticleContent();
articleContent.setContentEntity(xContent);
CmsArticleDetail articleDetail = this.articleMapper.selectById(xContent.getContentId());
articleContent.setExtendEntity(articleDetail);
return articleContent;
}
@Override
public IContent<?> readRequest(HttpServletRequest request) throws IOException {
ArticleDTO dto = JacksonUtils.from(request.getInputStream(), ArticleDTO.class);
CmsContent contentEntity;
if (dto.getOpType() == ContentOpType.UPDATE) {
contentEntity = this.contentMapper.selectById(dto.getContentId());
Assert.notNull(contentEntity,
() -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("contentId", dto.getContentId()));
} else {
contentEntity = new CmsContent();
}
BeanUtils.copyProperties(dto, contentEntity);
CmsCatalog catalog = this.catalogService.getCatalog(dto.getCatalogId());
contentEntity.setSiteId(catalog.getSiteId());
contentEntity.setAttributes(ContentAttribute.convertInt(dto.getAttributes()));
// 发布通道配置
Map<String, Map<String, Object>> publishPipProps = new HashMap<>();
dto.getPublishPipeProps().forEach(prop -> {
publishPipProps.put(prop.getPipeCode(), prop.getProps());
});
contentEntity.setPublishPipeProps(publishPipProps);
CmsArticleDetail extendEntity = new CmsArticleDetail();
BeanUtils.copyProperties(dto, extendEntity);
ArticleContent content = new ArticleContent();
content.setContentEntity(contentEntity);
content.setExtendEntity(extendEntity);
content.setParams(dto.getParams());
if (content.hasExtendEntity() && StringUtils.isEmpty(extendEntity.getContentHtml())) {
throw CommonErrorCode.NOT_EMPTY.exception("contentHtml");
}
return content;
}
@Override
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));
List<CmsPublishPipe> publishPipes = this.publishPipeService.getPublishPipes(catalog.getSiteId());
ArticleVO vo;
if (IdUtils.validate(contentId)) {
CmsContent contentEntity = this.contentMapper.selectById(contentId);
Assert.notNull(contentEntity, () -> CommonErrorCode.DATA_NOT_FOUND_BY_ID.exception("contentId", contentId));
CmsArticleDetail extendEntity = this.articleMapper.selectById(contentId);
vo = ArticleVO.newInstance(contentEntity, extendEntity);
if (StringUtils.isNotEmpty(vo.getLogo())) {
vo.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(vo.getLogo()));
}
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, contentEntity.getPublishPipeProps());
vo.setPublishPipeProps(publishPipeProps);
} else {
vo = new ArticleVO();
vo.setContentId(IdUtils.getSnowflakeId());
vo.setCatalogId(catalog.getCatalogId());
vo.setContentType(ID);
// 发布通道初始数据
vo.setPublishPipe(publishPipes.stream().map(CmsPublishPipe::getCode).toArray(String[]::new));
// 发布通道模板数据
List<PublishPipeProp> publishPipeProps = this.publishPipeService.getPublishPipeProps(catalog.getSiteId(),
PublishPipePropUseType.Content, null);
vo.setPublishPipeProps(publishPipeProps);
}
vo.setCatalogName(catalog.getName());
return vo;
}
@Override
public void recover(Long contentId) {
this.articleMapper.recoverById(contentId);
}
@Override
public void deleteBackups(Long contentId) {
this.articleMapper.deleteLogicDeletedById(contentId);
}
}

View File

@ -0,0 +1,153 @@
package com.chestnut.article;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IInternalDataType;
import com.chestnut.contentcore.core.impl.InternalDataType_PageWidget;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.properties.EnableSSIProperty;
import com.chestnut.contentcore.service.*;
import com.chestnut.contentcore.template.tag.CmsIncludeTag;
import com.chestnut.contentcore.util.ContentCoreUtils;
import com.chestnut.contentcore.util.ContentUtils;
import com.chestnut.contentcore.util.PageWidgetUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ArticleUtils {
private static final ISiteService siteService = SpringUtils.getBean(ISiteService.class);
private static final ICatalogService catalogService = SpringUtils.getBean(ICatalogService.class);
private static final IContentService contentService = SpringUtils.getBean(IContentService.class);
private static final IPageWidgetService pageWidgetService = SpringUtils.getBean(IPageWidgetService.class);
private static final IPublishService publishService = SpringUtils.getBean(IPublishService.class);
/**
*
*/
public static final Pattern ContentExPlaceholderTagPattern = Pattern.compile("<img[^>]+ex_cid\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
/**
*
*
* <img src="/UEditorPlus/themes/default/images/spacer.gif" ex_cid="{content.id}" title="{content.title}" class="img_group_placeholder" />
* ssi
*
* @return
*/
public static String dealContentEx(String articleBody, String publishPipeCode, boolean isPreview) {
if (StringUtils.isBlank(articleBody)) {
return articleBody;
}
Matcher matcher = ContentExPlaceholderTagPattern.matcher(articleBody);
int lastEndIndex = 0;
StringBuilder sb = new StringBuilder();
while (matcher.find(lastEndIndex)) {
int s = matcher.start();
sb.append(articleBody.substring(lastEndIndex, s));
String placeholderImgTag = matcher.group();
String contentId = matcher.group(1);
try {
CmsContent _content = contentService.getById(contentId);
if (_content != null) {
CmsSite site = siteService.getSite(_content.getSiteId());
CmsCatalog catalog = catalogService.getCatalog(_content.getCatalogId());
if (isPreview) {
// 获取预览内容
placeholderImgTag = publishService.getContentExPageData(_content, publishPipeCode, true);
} else {
boolean ssiEnabled = EnableSSIProperty.getValue(site.getConfigProps());
if (catalog.isStaticize() && ssiEnabled) {
String staticFilePath = ContentUtils.getContentExPath(site, catalog, _content, publishPipeCode);
placeholderImgTag = StringUtils.messageFormat(CmsIncludeTag.SSI_INCLUDE_TAG, "/" + staticFilePath);
} else {
// 获取浏览内容
placeholderImgTag = publishService.getContentExPageData(_content, publishPipeCode, false);;
}
}
} else {
placeholderImgTag = StringUtils.EMPTY;
}
} catch (Exception e1) {
AsyncTaskManager.addErrMessage("Replace content-ex placeholder failed: " + contentId);
}
sb.append(placeholderImgTag);
lastEndIndex = matcher.end();
}
sb.append(articleBody.substring(lastEndIndex));
return sb.toString();
}
/**
*
*/
public static final Pattern PageWidgetImgHtmlTagPattern = Pattern.compile("<img[^>]+pw_code\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
/**
*
*
* <img src="/UEditorPlus/themes/default/images/spacer.gif" pw_code="{pageWidget.code}" title="{pageWidget.name}" class="pw_placeholder" />
* ssi
*
* @return
*/
public static String dealPageWidget(CmsContent content, String articleBody, boolean isPreview) {
if (StringUtils.isBlank(articleBody)) {
return articleBody;
}
CmsSite site = siteService.getSite(content.getSiteId());
CmsCatalog catalog = catalogService.getCatalog(content.getCatalogId());
Matcher matcher = PageWidgetImgHtmlTagPattern.matcher(articleBody);
int lastEndIndex = 0;
StringBuilder sb = new StringBuilder();
while (matcher.find(lastEndIndex)) {
int s = matcher.start();
sb.append(articleBody.substring(lastEndIndex, s));
String placeholderImgTag = matcher.group();
String pageWidgetCode = matcher.group(1);
try {
CmsPageWidget pw = pageWidgetService.getPageWidget(site.getSiteId(), pageWidgetCode);
if (pw != null) {
if (isPreview) {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(InternalDataType_PageWidget.ID);
String pageData = internalDataType.getPageData(new IInternalDataType.RequestData(pw.getPageWidgetId(),
1, pw.getPublishPipeCode(), true, null));
placeholderImgTag = pageData;
} else {
boolean ssiEnabled = EnableSSIProperty.getValue(site.getConfigProps());
if (catalog.isStaticize() && ssiEnabled) {
String staticFileName = PageWidgetUtils.getStaticFileName(pw, site.getStaticSuffix(pw.getPublishPipeCode()));
String staticFilePath = pw.getPath() + staticFileName;
placeholderImgTag = StringUtils.messageFormat(CmsIncludeTag.SSI_INCLUDE_TAG, "/" + staticFilePath);
} else {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(InternalDataType_PageWidget.ID);
String pageData = internalDataType.getPageData(new IInternalDataType.RequestData(pw.getPageWidgetId(),
1, pw.getPublishPipeCode(), false, null));
placeholderImgTag = pageData;
}
}
}
} catch (Exception e1) {
AsyncTaskManager.addErrMessage("Replace page widget placeholder failed: " + pageWidgetCode);
}
sb.append(placeholderImgTag);
lastEndIndex = matcher.end();
}
sb.append(articleBody.substring(lastEndIndex));
return sb.toString();
}
}

View File

@ -0,0 +1,28 @@
package com.chestnut.article;
import java.util.List;
import org.springframework.stereotype.Component;
import com.chestnut.contentcore.core.IPublishPipeProp;
@Component
public class PublishPipeProp_ArticleDetailTemplate implements IPublishPipeProp {
public static final String KEY = DetailTemplatePropPrefix + ArticleContentType.ID;
@Override
public String getKey() {
return KEY;
}
@Override
public String getName() {
return "文章详情页模板";
}
@Override
public List<PublishPipePropUseType> getUseTypes() {
return List.of(PublishPipePropUseType.Catalog);
}
}

View File

@ -0,0 +1,28 @@
package com.chestnut.article;
import java.util.List;
import org.springframework.stereotype.Component;
import com.chestnut.contentcore.core.IPublishPipeProp;
@Component
public class PublishPipeProp_DefaultArticleDetailTemplate implements IPublishPipeProp {
public static final String KEY = DefaultDetailTemplatePropPrefix + ArticleContentType.ID;
@Override
public String getKey() {
return KEY;
}
@Override
public String getName() {
return "文章详情页默认模板";
}
@Override
public List<PublishPipePropUseType> getUseTypes() {
return List.of(PublishPipePropUseType.Site);
}
}

View File

@ -0,0 +1,24 @@
package com.chestnut.article.controller;
import com.chestnut.common.security.anno.Priv;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.system.security.AdminUserType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
*
* </p>
*
* @author
* @email 190785909@qq.com
*/
@Priv(type = AdminUserType.TYPE)
@RestController
@RequestMapping("/cms/article")
public class ArticleController extends BaseRestController {
}

View File

@ -0,0 +1,138 @@
package com.chestnut.article.controller.front;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.article.domain.vo.ArticleApiVO;
import com.chestnut.article.service.IArticleService;
import com.chestnut.common.domain.R;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsCatalog;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.vo.ContentApiVO;
import com.chestnut.contentcore.domain.vo.ContentDynamicDataVO;
import com.chestnut.contentcore.domain.vo.ContentVO;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import com.chestnut.contentcore.fixed.dict.ContentStatus;
import com.chestnut.contentcore.service.ICatalogService;
import com.chestnut.contentcore.service.IContentService;
import com.chestnut.contentcore.service.impl.ContentDynamicDataService;
import com.chestnut.contentcore.util.CatalogUtils;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* API
*
* @author
* @email 190785909@qq.com
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/cms/article")
public class ArticleApiController extends BaseRestController {
private final ICatalogService catalogService;
private final IContentService contentService;
private final IArticleService articleService;
@GetMapping("/list")
public R<List<ArticleApiVO>> getContentList(
@RequestParam("sid") Long siteId,
@RequestParam(value = "cid", required = false, defaultValue = "0") Long catalogId,
@RequestParam(value = "lv", required = false, defaultValue = "Root") String level,
@RequestParam(value = "attrs", required = false) String hasAttributes,
@RequestParam(value = "no_attrs", required = false) String noAttributes,
@RequestParam(value = "st", required = false, defaultValue = "Recent") String sortType,
@RequestParam(value = "ps", required = false, defaultValue = "16") Integer pageSize,
@RequestParam(value = "pn", required = false, defaultValue = "1") Long pageNumber,
@RequestParam(value = "pp") String publishPipeCode,
@RequestParam(value = "preview", required = false, defaultValue = "false") Boolean preview,
@RequestParam(value = "text", required = false, defaultValue = "false") Boolean text
) {
if (!"Root".equalsIgnoreCase(level) && !IdUtils.validate(catalogId)) {
return R.fail("The parameter cid is required where lv is `Root`.");
}
LambdaQueryWrapper<CmsContent> q = new LambdaQueryWrapper<>();
q.eq(CmsContent::getSiteId, siteId).eq(CmsContent::getStatus, ContentStatus.PUBLISHED);
if (!"Root".equalsIgnoreCase(level)) {
CmsCatalog catalog = this.catalogService.getCatalog(catalogId);
if (Objects.isNull(catalog)) {
return R.fail("Catalog not found: " + catalogId);
}
if ("Current".equalsIgnoreCase(level)) {
q.eq(CmsContent::getCatalogId, catalog.getCatalogId());
} else if ("Child".equalsIgnoreCase(level)) {
q.likeRight(CmsContent::getCatalogAncestors, catalog.getAncestors() + CatalogUtils.ANCESTORS_SPLITER);
} else if ("CurrentAndChild".equalsIgnoreCase(level)) {
q.likeRight(CmsContent::getCatalogAncestors, catalog.getAncestors());
}
}
if (StringUtils.isNotEmpty(hasAttributes)) {
int attrTotal = ContentAttribute.convertInt(hasAttributes.split(","));
q.apply(attrTotal > 0, "attributes&{0}={1}", attrTotal, attrTotal);
}
if (StringUtils.isNotEmpty(noAttributes)) {
String[] contentAttrs = noAttributes.split(",");
int attrTotal = ContentAttribute.convertInt(contentAttrs);
for (String attr : contentAttrs) {
int bit = ContentAttribute.bit(attr);
q.apply(bit > 0, "attributes&{0}<>{1}", attrTotal, bit);
}
}
if ("Recent".equalsIgnoreCase(sortType)) {
q.orderByDesc(CmsContent::getPublishDate);
} else {
q.orderByDesc(Arrays.asList(CmsContent::getTopFlag, CmsContent::getSortFlag));
}
Page<CmsContent> pageResult = this.contentService.page(new Page<>(pageNumber, pageSize, false), q);
if (pageResult.getRecords().isEmpty()) {
return R.ok(List.of());
}
List<Long> contentIds = pageResult.getRecords().stream().map(CmsContent::getContentId).toList();
Map<Long, CmsArticleDetail> articleDetails = this.articleService.listByIds(contentIds)
.stream().collect(Collectors.toMap(c -> c.getContentId(), c -> c));
Map<Long, CmsCatalog> loadedCatalogs = new HashMap<>();
List<ArticleApiVO> list = new ArrayList<>();
pageResult.getRecords().forEach(c -> {
ArticleApiVO dto = ArticleApiVO.newInstance(c);
CmsCatalog catalog = loadedCatalogs.get(c.getCatalogId());
if (Objects.isNull(catalog)) {
catalog = this.catalogService.getCatalog(c.getCatalogId());
loadedCatalogs.put(catalog.getCatalogId(), catalog);
}
dto.setCatalogName(catalog.getName());
dto.setCatalogLink(catalogService.getCatalogLink(catalog, 1, publishPipeCode, preview));
dto.setLink(this.contentService.getContentLink(c, 1, publishPipeCode, preview));
dto.setLogoSrc(InternalUrlUtils.getActualUrl(c.getLogo(), publishPipeCode, preview));
CmsArticleDetail articleDetail = articleDetails.get(c.getContentId());
if (Objects.nonNull(articleDetail)) {
dto.setContentHtml(articleDetail.getContentHtml());
}
list.add(dto);
});
return R.ok(list);
}
/**
*
*/
public R<ContentVO> getHotContentList() {
return R.ok();
}
}

View File

@ -0,0 +1,53 @@
package com.chestnut.article.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.chestnut.common.db.domain.LogicDeleteEntity;
import lombok.Getter;
import lombok.Setter;
/**
* [cms_article_detail]
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
@TableName(CmsArticleDetail.TABLE_NAME)
public class CmsArticleDetail extends LogicDeleteEntity {
public static final String TABLE_NAME = "cms_article_detail";
/**
* ID
*/
@TableId(value = "content_id", type = IdType.INPUT)
private Long contentId;
/**
* ID
*/
private Long siteId;
/**
* json
*/
private String contentJson;
/**
* html
*/
private String contentHtml;
/**
*
*/
private String pageTitles;
/**
*
*/
private String downloadRemoteImage;
}

View File

@ -0,0 +1,44 @@
package com.chestnut.article.domain.dto;
import org.springframework.beans.BeanUtils;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.dto.ContentDTO;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ArticleDTO extends ContentDTO {
/**
* html
*/
private String contentHtml;
/**
*
*/
private String downloadRemoteImage;
/**
*
*/
private String pageTitles;
public static ArticleDTO newInstance(CmsContent content, CmsArticleDetail articleDetail) {
ArticleDTO dto = new ArticleDTO();
BeanUtils.copyProperties(content, dto);
dto.setAttributes(ContentAttribute.convertStr(content.getAttributes()));
if (StringUtils.isNotEmpty(dto.getLogo())) {
dto.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(dto.getLogo()));
}
BeanUtils.copyProperties(articleDetail, dto);
return dto;
}
}

View File

@ -0,0 +1,50 @@
package com.chestnut.article.domain.vo;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.vo.ContentApiVO;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import lombok.Getter;
import lombok.Setter;
import java.time.ZoneOffset;
/**
* <TODO description class purpose>
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
public class ArticleApiVO extends ContentApiVO {
private String contentHtml;
public static ArticleApiVO newInstance(CmsContent cmsContent) {
ArticleApiVO dto = new ArticleApiVO();
dto.setAuthor(cmsContent.getAuthor());
dto.setCatalogId(cmsContent.getCatalogId());
dto.setContentId(cmsContent.getContentId());
dto.setContentType(cmsContent.getContentType());
dto.setEditor(cmsContent.getEditor());
dto.setKeywords(cmsContent.getKeywords());
dto.setLogo(cmsContent.getLogo());
dto.setOriginal(cmsContent.getOriginal());
dto.setPublishDate(cmsContent.getPublishDate().toInstant(ZoneOffset.UTC).toEpochMilli());
dto.setShortTitle(cmsContent.getShortTitle());
dto.setSubTitle(cmsContent.getSubTitle());
dto.setTitle(cmsContent.getTitle());
dto.setSource(cmsContent.getSource());
dto.setSourceUrl(cmsContent.getSourceUrl());
dto.setSummary(cmsContent.getSummary());
dto.setTags(cmsContent.getTags());
dto.setTitleStyle(cmsContent.getTitleStyle());
dto.setTopFlag(cmsContent.getTopFlag());
dto.setAttributes(ContentAttribute.convertStr(cmsContent.getAttributes()));
dto.setViewCount(cmsContent.getViewCount());
dto.setLikeCount(cmsContent.getLikeCount());
dto.setCommentCount(cmsContent.getCommentCount());
dto.setFavoriteCount(cmsContent.getFavoriteCount());
return dto;
}
}

View File

@ -0,0 +1,48 @@
package com.chestnut.article.domain.vo;
import java.util.Objects;
import org.springframework.beans.BeanUtils;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.domain.vo.ContentVO;
import com.chestnut.contentcore.fixed.dict.ContentAttribute;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ArticleVO extends ContentVO {
/**
* html
*/
private String contentHtml;
/**
*
*/
private String downloadRemoteImage;
/**
*
*/
private String pageTitles;
public static ArticleVO newInstance(CmsContent content, CmsArticleDetail articleDetail) {
ArticleVO dto = new ArticleVO();
BeanUtils.copyProperties(content, dto);
dto.setAttributes(ContentAttribute.convertStr(content.getAttributes()));
if (StringUtils.isNotEmpty(dto.getLogo())) {
dto.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(dto.getLogo()));
}
if (Objects.nonNull(articleDetail)) {
BeanUtils.copyProperties(articleDetail, dto);
}
return dto;
}
}

View File

@ -0,0 +1,43 @@
package com.chestnut.article.listener;
import com.chestnut.article.domain.vo.ArticleVO;
import com.chestnut.article.mapper.CmsArticleDetailMapper;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.listener.event.AfterContentEditorInitEvent;
import com.chestnut.contentcore.listener.event.BeforeSiteDeleteEvent;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ArticleListener {
private final CmsArticleDetailMapper articleMapper;
@EventListener
public void beforeSiteDelete(BeforeSiteDeleteEvent event) {
CmsSite site = event.getSite();
int pageSize = 500;
// 删除文章数据
try {
long total = this.articleMapper.selectCountBySiteIdIgnoreLogicDel(site.getSiteId());
for (int i = 0; i * pageSize < total; i++) {
AsyncTaskManager.setTaskProgressInfo((int) (i * pageSize * 100 / total), "正在删除文章详情备份数据:" + (i * pageSize) + "/" + total);
this.articleMapper.deleteBySiteIdIgnoreLogicDel(site.getSiteId(), pageSize);
}
} catch (Exception e) {
e.printStackTrace();
AsyncTaskManager.addErrMessage("删除文章详情错误:" + e.getMessage());
}
}
@EventListener
public void afterContentEditorInit(AfterContentEditorInitEvent event) {
if (event.getContentVO() instanceof ArticleVO vo) {
vo.setContentHtml(InternalUrlUtils.dealResourceInternalUrl(vo.getContentHtml()));
}
}
}

View File

@ -0,0 +1,58 @@
package com.chestnut.article.mapper;
import com.chestnut.common.db.DBConstants;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chestnut.article.domain.CmsArticleDetail;
import org.apache.ibatis.annotations.Update;
/**
* <p>
* Mapper
* </p>
*
* @author
* @email 190785909@qq.com
*/
public interface CmsArticleDetailMapper extends BaseMapper<CmsArticleDetail> {
/**
* deleted=1
*
* @param contentId
* @return
*/
@Delete("DELETE FROM cms_article_detail WHERE deleted = 1 AND content_id = #{contentId}")
Long deleteLogicDeletedById(@Param("contentId") Long contentId);
/**
*
*
* @param siteId
* @param limit
* @return
*/
@Delete("DELETE FROM cms_article_detail WHERE site_id = #{siteId} limit ${limit}")
Long deleteBySiteIdIgnoreLogicDel(@Param("siteId") Long siteId, @Param("limit") Integer limit);
/**
*
*
* @param siteId
* @return
*/
@Select("SELECT count(*) FROM cms_article_detail WHERE site_id = #{siteId}")
Long selectCountBySiteIdIgnoreLogicDel(@Param("siteId") Long siteId);
/**
* cms_article_detaildeleted0
*
* @param contentId
* @return
*/
@Update("UPDATE cms_article_detail SET deleted = " + DBConstants.DELETED_NO + " WHERE content_id = #{contentId}")
Long recoverById(@Param("contentId") Long contentId);
}

View File

@ -0,0 +1,57 @@
package com.chestnut.article.properties;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Component;
import com.chestnut.contentcore.core.IProperty;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
/**
* 600
*/
@Component(IProperty.BEAN_NAME_PREFIX + ArticleImageHeight.ID)
public class ArticleImageHeight implements IProperty {
public final static String ID = "ArticleImageHeight";
static UseType[] UseTypes = new UseType[] { UseType.Site, UseType.Catalog };
@Override
public UseType[] getUseTypes() {
return UseTypes;
}
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "文章正文图片高度";
}
@Override
public boolean validate(String value) {
return Objects.nonNull(value) && NumberUtils.isDigits(value.toString());
}
@Override
public Integer defaultValue() {
return 600;
}
@Override
public Integer getPropValue(Map<String, String> configProps) {
return MapUtils.getInteger(configProps, getId(), defaultValue());
}
public static int getValue(Map<String, String> firstProps, Map<String, String> secondProps) {
int value = ConfigPropertyUtils.getIntValue(ID, firstProps, secondProps);
return value <= 0 ? 600 : value;
}
}

View File

@ -0,0 +1,56 @@
package com.chestnut.article.properties;
import java.util.Map;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Component;
import com.chestnut.contentcore.core.IProperty;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
/**
* 600
*/
@Component(IProperty.BEAN_NAME_PREFIX + ArticleImageWidth.ID)
public class ArticleImageWidth implements IProperty {
public final static String ID = "ArticleImageWidth";
static UseType[] UseTypes = new UseType[] { UseType.Site, UseType.Catalog };
@Override
public UseType[] getUseTypes() {
return UseTypes;
}
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "文章正文图片宽度";
}
@Override
public boolean validate(String value) {
return NumberUtils.isDigits(value.toString());
}
@Override
public Integer defaultValue() {
return 600;
}
@Override
public Integer getPropValue(Map<String, String> configProps) {
return MapUtils.getInteger(configProps, getId(), defaultValue());
}
public static int getValue(Map<String, String> firstProps, Map<String, String> secondProps) {
int value = ConfigPropertyUtils.getIntValue(ID, firstProps, secondProps);
return value <= 0 ? 600 : value;
}
}

View File

@ -0,0 +1,44 @@
package com.chestnut.article.properties;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.chestnut.contentcore.core.IProperty;
import com.chestnut.contentcore.util.ConfigPropertyUtils;
import com.chestnut.system.fixed.dict.YesOrNo;
/**
* Logo
*/
@Component(IProperty.BEAN_NAME_PREFIX + AutoArticleLogo.ID)
public class AutoArticleLogo implements IProperty {
public final static String ID = "AutoArticleLogo";
static UseType[] UseTypes = new UseType[] { UseType.Site };
@Override
public UseType[] getUseTypes() {
return UseTypes;
}
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "自动提取文章正文首图作为Logo";
}
@Override
public String defaultValue() {
return YesOrNo.NO;
}
public static boolean getValue(Map<String, String> props) {
return YesOrNo.isYes(ConfigPropertyUtils.getStringValue(ID, props));
}
}

View File

@ -0,0 +1,34 @@
package com.chestnut.article.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.contentcore.domain.CmsSite;
/**
*
*
* @author
* @email 190785909@qq.com
*/
public interface IArticleService extends IService<CmsArticleDetail> {
/**
*
*
* iurlsrc/hrefiurliurl
*
* @param content
* @return
*/
String saveInternalUrl(String content);
/**
*
*
* @param content
* @param site
* @param operator
* @return
*/
String downloadRemoteImages(String content, CmsSite site, String operator);
}

View File

@ -0,0 +1,145 @@
package com.chestnut.article.service.impl;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.chestnut.contentcore.domain.*;
import com.chestnut.contentcore.service.*;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.article.mapper.CmsArticleDetailMapper;
import com.chestnut.article.service.IArticleService;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl extends ServiceImpl<CmsArticleDetailMapper, CmsArticleDetail>
implements IArticleService {
/**
* src
*/
public static final Pattern ImgHtmlTagPattern = Pattern.compile("<img[^>]+src\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
/**
* iurl
*/
private static final Pattern IUrlTagPattern = Pattern.compile("<[^>]+iurl\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
private static final Pattern TagAttrSrcPattern = Pattern.compile("src\\s*=\\s*['\"]([^'\"]+)['\"]",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
private static final Pattern TagAttrHrefPattern = Pattern.compile("href\\s*=\\s*['\"]([^'\"]+)['\"]",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
private static final Pattern TagAttrIUrlPattern = Pattern.compile("iurl\\s*=\\s*['\"]([^'\"]+)['\"]",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
private final IResourceService resourceService;
private final IPageWidgetService pageWidgetService;
private final ISiteService siteService;
private final ICatalogService catalogService;
private final IContentService contentService;
@Override
public String saveInternalUrl(String content) {
if (StringUtils.isBlank(content)) {
return content;
}
Matcher matcher = IUrlTagPattern.matcher(content);
int lastEndIndex = 0;
StringBuilder sb = new StringBuilder();
while (matcher.find(lastEndIndex)) {
int s = matcher.start();
sb.append(content.substring(lastEndIndex, s));
String tagStr = matcher.group();
String iurl = matcher.group(1);
// begin
try {
// 移除iurl属性
tagStr = TagAttrIUrlPattern.matcher(tagStr).replaceAll("");
// 查找src属性替换成iurl
Matcher matcherSrc = TagAttrSrcPattern.matcher(tagStr);
if (matcherSrc.find()) {
tagStr = tagStr.substring(0, matcherSrc.start()) + "src=\"" + iurl + "\""
+ tagStr.substring(matcherSrc.end());
} else {
// 无src属性则继续查找href属性
Matcher matcherHref = TagAttrHrefPattern.matcher(tagStr);
if (matcherHref.find()) {
tagStr = tagStr.substring(0, matcherHref.start()) + "href=\"" + iurl + "\""
+ tagStr.substring(matcherHref.end());
}
}
} catch (Exception e) {
log.warn("InternalUrl parse failed: " + iurl, e);
}
// end
sb.append(tagStr.replace("\\s+", " "));
lastEndIndex = matcher.end();
}
sb.append(content.substring(lastEndIndex));
return sb.toString();
}
/**
* saveInternalUrliurl
*/
@Override
public String downloadRemoteImages(String content, CmsSite site, String operator) {
if (StringUtils.isBlank(content)) {
return content;
}
Matcher matcher = ImgHtmlTagPattern.matcher(content);
int lastEndIndex = 0;
StringBuilder sb = new StringBuilder();
while (matcher.find(lastEndIndex)) {
int s = matcher.start();
sb.append(content.substring(lastEndIndex, s));
String imgTag = matcher.group();
String src = matcher.group(1);
try {
if (StringUtils.startsWithIgnoreCase(src, "data:image/")) {
// base64图片保存到资源库
CmsResource resource = resourceService.addBase64Image(site, operator, src);
if (Objects.nonNull(resource)) {
imgTag = imgTag.replaceFirst("data:image/([^'\"]+)", src);
}
} else if (!InternalUrlUtils.isInternalUrl(src) && ServletUtils.isHttpUrl(src)) {
// 非iurl的http链接则下载图片
CmsResource resource = resourceService.downloadImageFromUrl(src, site.getSiteId(), operator);
if (Objects.nonNull(resource)) {
imgTag = StringUtils.replaceEx(imgTag, src, resource.getInternalUrl());
}
}
} catch (Exception e1) {
String imgSrc = (src.startsWith("data:image/") ? src.substring(0, 20) : src);
log.warn("Save image failed: " + imgSrc);
AsyncTaskManager.addErrMessage("Download remote image failed: " + imgSrc);
}
sb.append(imgTag);
lastEndIndex = matcher.end();
}
sb.append(content.substring(lastEndIndex));
return sb.toString();
}
}

View File

@ -0,0 +1,60 @@
package com.chestnut.article.template.func;
import com.chestnut.article.ArticleUtils;
import com.chestnut.article.service.IArticleService;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.staticize.func.AbstractFunc;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.util.InternalUrlUtils;
import freemarker.core.Environment;
import freemarker.template.SimpleScalar;
import freemarker.template.TemplateModelException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* FreemarkerHtml
*/
@Component
@RequiredArgsConstructor
public class dealArticleBodyFunction extends AbstractFunc {
static final String FUNC_NAME = "dealArticleBody";
private static final String DESC = "{FREEMARKER.FUNC.DESC." + FUNC_NAME + "}";
private final IArticleService articleService;
@Override
public String getFuncName() {
return FUNC_NAME;
}
@Override
public String getDesc() {
return DESC;
}
@Override
public Object exec0(Object... args) throws TemplateModelException {
if (args.length < 1) {
return StringUtils.EMPTY;
}
TemplateContext context = FreeMarkerUtils.getTemplateContext(Environment.getCurrentEnvironment());
SimpleScalar simpleScalar = (SimpleScalar) args[0];
String contentHtml = simpleScalar.getAsString();
// 处理内容扩展模板占位符
contentHtml = ArticleUtils.dealContentEx(contentHtml, context.getPublishPipeCode(), context.isPreview());
// 处理正文内部链接
contentHtml = InternalUrlUtils.dealInternalUrl(contentHtml, context.getPublishPipeCode(), context.isPreview());
return contentHtml;
}
@Override
public List<FuncArg> getFuncArgs() {
return List.of(new FuncArg("HTML文章正文内容", FuncArgType.String, true, null));
}
}

View File

@ -0,0 +1,114 @@
package com.chestnut.article.template.tag;
import com.chestnut.article.domain.CmsArticleDetail;
import com.chestnut.article.mapper.CmsArticleDetailMapper;
import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.StaticizeConstants;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.staticize.enums.TagAttrDataType;
import com.chestnut.common.staticize.tag.AbstractTag;
import com.chestnut.common.staticize.tag.TagAttr;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsContent;
import com.chestnut.contentcore.enums.ContentCopyType;
import com.chestnut.contentcore.mapper.CmsContentMapper;
import freemarker.core.Environment;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@RequiredArgsConstructor
@Component
public class CmsArticleTag extends AbstractTag {
public static final String TAG_NAME = "cms_article";
public final static String NAME = "{FREEMARKER.TAG.NAME." + TAG_NAME + "}";
public final static String DESC = "{FREEMARKER.TAG.DESC." + TAG_NAME + "}";
public static final String TagAttr_ContentId = "contentId";
public static final String TagAttr_Page = "page";
public static final String TemplateVariable_ArticleContent = "ArticleContent";
// CKEditor5: <div class="page-break" style="page-break-after:always;"><span style="display:none;">&nbsp;</span></div>
// private static final String PAGE_BREAK_SPLITER = "<div[^>]+class=['\"]page-break['\"].*?</div>";
private static final String PAGE_BREAK_SPLITER = "__XY_UEDITOR_PAGE_BREAK__";
private final CmsContentMapper contentMapper;
private final CmsArticleDetailMapper articleMapper;
@Override
public List<TagAttr> getTagAttrs() {
List<TagAttr> tagAttrs = new ArrayList<>();
tagAttrs.add(new TagAttr(TagAttr_ContentId, true, TagAttrDataType.INTEGER, "文章内容ID"));
tagAttrs.add(new TagAttr(TagAttr_Page, false, TagAttrDataType.BOOLEAN, "是否分页默认false"));
return tagAttrs;
}
@Override
public Map<String, TemplateModel> execute0(Environment env, Map<String, String> attrs)
throws TemplateException, IOException {
String contentHtml = null;
long contentId = MapUtils.getLongValue(attrs, TagAttr_ContentId, 0);
if (contentId <= 0) {
throw new TemplateException("Invalid contentId: " + contentId, env);
}
CmsContent content = this.contentMapper.selectById(contentId);
if (content.isLinkContent()) {
return Map.of(TemplateVariable_ArticleContent, this.wrap(env, StringUtils.EMPTY));
}
if (ContentCopyType.isMapping(content.getCopyType())) {
contentId = content.getCopyId();
}
CmsArticleDetail articleDetail = this.articleMapper.selectById(contentId);
if (Objects.isNull(articleDetail)) {
throw new TemplateException("Article details not found: " + contentId, env);
}
contentHtml = articleDetail.getContentHtml();
TemplateContext context = FreeMarkerUtils.getTemplateContext(env);
boolean page = MapUtils.getBooleanValue(attrs, TagAttr_Page, false);
if (page) {
if (context.isPaged()) {
throw new TemplateException("分页标识已被其他标签激活", env);
}
context.setPaged(true);
String[] pageContents = contentHtml.split(PAGE_BREAK_SPLITER);
if (context.getPageIndex() > pageContents.length) {
throw new TemplateException(StringUtils.messageFormat("文章内容分页越界:{0}, 最大页码:{1}。", context.getPageIndex(),
pageContents.length), env);
}
context.setPageTotal(pageContents.length);
env.setGlobalVariable(StaticizeConstants.TemplateVariable_PageTotal,
this.wrap(env, context.getPageTotal()));
contentHtml = pageContents[context.getPageIndex() - 1];
}
return Map.of(TemplateVariable_ArticleContent, this.wrap(env, contentHtml));
}
@Override
public String getTagName() {
return TAG_NAME;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getDescription() {
return DESC;
}
}

View File

@ -0,0 +1,7 @@
# 内容类型
CMS.CONTENTCORE.CONTENT_TYPE.article=文章
# 模板freemarker
FREEMARKER.TAG.NAME.cms_article=文章正文数据标签
FREEMARKER.TAG.DESC.cms_article=获取文章正文数据,标签内使用${ArticleContent}获取正文详情
FREEMARKER.FUNC.DESC.dealArticleBody=文章正文处理函数,主要用来处理文章内容中的内部链接和扩展模板占位符

View File

@ -0,0 +1,7 @@
# 内容类型
CMS.CONTENTCORE.CONTENT_TYPE.article=Article
# 模板freemarker
FREEMARKER.TAG.NAME.cms_article=Article body tag
FREEMARKER.TAG.DESC.cms_article=Fetch article body details, use "${ArticleContent}" in tag to get body text.
FREEMARKER.FUNC.DESC.dealArticleBody=Article body deal function

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms</artifactId>
<version>1.3.21</version>
</parent>
<artifactId>chestnut-cms-block</artifactId>
<description>区块页面部件扩展模块</description>
<dependencies>
<!-- 内容核心 -->
<dependency>
<groupId>com.chestnut</groupId>
<artifactId>chestnut-cms-contentcore</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,44 @@
package com.chestnut.block;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.chestnut.block.ManualPageWidgetType.RowData;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.contentcore.core.AbstractPageWidget;
import com.chestnut.contentcore.domain.CmsPageWidget;
/**
*
*
* @author
* @email 190785909@qq.com
*/
public class ManualPageWidget extends AbstractPageWidget {
@Override
public void add() {
this.dealContentIngoreFields();
super.add();
}
@Override
public void save() {
this.dealContentIngoreFields();
super.save();
}
private void dealContentIngoreFields() {
CmsPageWidget pageWidgetEntity = this.getPageWidgetEntity();
List<RowData> rows = JacksonUtils.fromList(pageWidgetEntity.getContent(), RowData.class);
if (Objects.nonNull(rows)) {
rows.forEach(row -> {
row.getItems().forEach(item -> item.setLogoSrc(null));
});
} else {
rows = Collections.emptyList();
}
pageWidgetEntity.setContent(JacksonUtils.to(rows));
}
}

View File

@ -0,0 +1,116 @@
package com.chestnut.block;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import com.chestnut.block.domain.vo.ManualPageWidgetVO;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IPageWidget;
import com.chestnut.contentcore.core.IPageWidgetType;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.vo.PageWidgetVO;
import com.chestnut.contentcore.util.InternalUrlUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
/**
* <br/>
*
*
* @author
* @email 190785909@qq.com
*/
@RequiredArgsConstructor
@Component(IPageWidgetType.BEAN_NAME_PREFIX + ManualPageWidgetType.ID)
public class ManualPageWidgetType implements IPageWidgetType {
public final static String ID = "manual";
public final static String NAME = "{CMS.CONTENCORE.PAGEWIDGET." + ID + "}";
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getIcon() {
return "el-icon-list";
}
@Override
public String getRoute() {
return "/cms/block/manual/editor";
}
@Override
public IPageWidget loadPageWidget(CmsPageWidget cmsPageWdiget) {
ManualPageWidget pw = new ManualPageWidget();
pw.setPageWidgetEntity(cmsPageWdiget);
return pw;
}
@Override
public IPageWidget newInstance() {
return new ManualPageWidget();
}
@Override
public PageWidgetVO getPageWidgetVO(CmsPageWidget pageWidget) {
ManualPageWidgetVO vo = new ManualPageWidgetVO();
BeanUtils.copyProperties(pageWidget, vo);
vo.setContent(this.parseContent(pageWidget, null, true));
return vo;
}
@Override
public List<RowData> parseContent(CmsPageWidget pageWidget, String publishPipeCode, boolean isPreview) {
List<RowData> list = null;
if (StringUtils.isNotEmpty(pageWidget.getContent())) {
list = JacksonUtils.fromList(pageWidget.getContent(), RowData.class);
}
if (list == null) {
list = List.of();
}
list.forEach(rd -> rd.getItems().forEach(item -> item.setLogoSrc(InternalUrlUtils.getActualPreviewUrl(item.logo))));
return list;
}
@Getter
@Setter
public static class RowData {
private List<ItemData> items;
}
@Getter
@Setter
public static class ItemData {
private String title;
private String titleStyle;
private String summary;
private String url;
private String logo;
private String logoSrc;
private LocalDateTime publishDate;
}
}

View File

@ -0,0 +1,27 @@
package com.chestnut.block.domain.vo;
import java.util.List;
import com.chestnut.block.ManualPageWidgetType.RowData;
import com.chestnut.contentcore.domain.vo.PageWidgetVO;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
/**
*
*
* @author
* @email 190785909@qq.com
*/
@Getter
@Setter
@Accessors(chain = true)
public class ManualPageWidgetVO extends PageWidgetVO {
/**
*
*/
private List<RowData> content;
}

View File

@ -0,0 +1,10 @@
package com.chestnut.block.listener;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class BlockListener {
}

View File

@ -0,0 +1,2 @@
# 页面部件类型
CMS.CONTENCORE.PAGEWIDGET.manual=自定义区块

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