版本更新:V1.5.5

This commit is contained in:
liweiyi 2025-04-16 09:27:44 +08:00
parent ffca051843
commit 8dee7904f8
171 changed files with 4681 additions and 2697 deletions

View File

@ -1,4 +1,4 @@
# ChestnutCMS v1.5.4
# ChestnutCMS v1.5.5
### 系统简介
@ -105,7 +105,7 @@ ChestnutCMS是前后端分离的企业级内容管理系统。项目基于[RuoYi
### QQ交流群
- [一群568506424(满)](https://qm.qq.com/q/rOw3kwePg)
- [一群568506424](https://qm.qq.com/q/rOw3kwePg)
- [二群643215654](https://qm.qq.com/q/BEC38NokKY)

View File

@ -3,7 +3,7 @@
<parent>
<artifactId>chestnut</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>

View File

@ -17,7 +17,8 @@ package com.chestnut;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import java.net.InetAddress;
/**
* 启动程序
@ -28,9 +29,10 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication
public class ChestnutApplication {
public static void main(String[] args) {
public static void main(String[] args) throws Exception {
long s = System.currentTimeMillis();
System.setProperty("spring.devtools.restart.enabled", "false");
System.setProperty("LOCAL_IP", InetAddress.getLocalHost().getHostAddress());
SpringApplication.run(ChestnutApplication.class, args);
System.out.println("ChestnutApplication startup, cost: " + (System.currentTimeMillis() - s) + "ms");
}

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.4
version: 1.5.5
# 版权年份
copyrightYear: 2022-2024
system:
@ -46,9 +46,12 @@ server:
# 日志配置
logging:
config: classpath:logback-dev.xml
level:
com.chestnut: debug
org.springframework: warn
com.chestnut: debug
cron: debug
publish: debug
# Spring配置
spring:

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.4
version: 1.5.5
# 版权年份
copyrightYear: 2022-2024
system:
@ -46,9 +46,12 @@ server:
# 日志配置
logging:
config: classpath:logback-prod.xml
level:
com.chestnut: debug
org.springframework: warn
com.chestnut: debug
cron: debug
publish: debug
# Spring配置
spring:

View File

@ -5,7 +5,7 @@ chestnut:
# 代号
alias: ChestnutCMS
# 版本
version: 1.5.4
version: 1.5.5
# 版权年份
copyrightYear: 2022-2024
system:
@ -40,9 +40,12 @@ server:
# 日志配置
logging:
config: classpath:logback-dev.xml
level:
com.chestnut: debug
org.springframework: warn
com.chestnut: debug
cron: debug
publish: debug
# Spring配置
spring:

View File

@ -0,0 +1,3 @@
ALTER TABLE cms_cfd_default ADD COLUMN `uid` bigint;
ALTER TABLE cms_custom_form MODIFY COLUMN `rule_limit` VARCHAR(20);

View File

@ -1,13 +0,0 @@
#错误消息
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

@ -1,13 +0,0 @@
#错误消息
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

@ -1,13 +0,0 @@
#錯誤消息
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,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.app.name" value="ChestnutCMS" />
<!-- 日志存放路径 -->
<property name="log.path" value="logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t][%C#%method,%L]: %msg%n" />
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 设置队列的最大容量默认256 -->
<queueSize>262144</queueSize>
<!-- 在队列快满时还剩20%容量),丢弃日志的水平,配置为 0 就是都不丢弃 -->
<discardingThreshold>0</discardingThreshold>
<!-- 设置是否在异步线程中包含调用者数据默认false其实就是是否可以输出代码位置 -->
<includeCallerData>true</includeCallerData>
<appender-ref ref="out" />
</appender>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<appender name="CC_CONSOLE" class="com.chestnut.system.logs.CcConsoleAppender">
<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}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 5天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${log.pattern}</pattern>
</encoder>
<!-- 默认true日志直接写入磁盘设置为false先写入内存buffer满后批量刷盘 -->
<immediateFlush>false</immediateFlush>
<!-- 日志写入内存容量上限 -->
<bufferSize>8192</bufferSize>
</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}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 发布日志输出 -->
<appender name="publish" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/publish.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/publish-%d{yyyy-MM-dd}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!--默认-->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="CC_CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
</root>
<!-- Spring日志 -->
<logger name="org.springframework" level="warn" />
<!-- 系统模块日志 -->
<logger name="com.chestnut" level="debug" />
<!-- 系统定时任务日志-->
<logger name="cron" level="debug">
<appender-ref ref="cron"/>
</logger>
<!--系统发布日志-->
<logger name="publish" level="debug">
<appender-ref ref="publish"/>
</logger>
</configuration>

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.app.name" value="ChestnutCMS" />
<!-- 日志存放路径 -->
<property name="log.path" value="logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t][%C#%method,%L]: %msg%n" />
<appender name="CC_CONSOLE" class="com.chestnut.system.logs.CcConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 设置队列的最大容量默认256 -->
<queueSize>262144</queueSize>
<!-- 在队列快满时还剩20%容量),丢弃日志的水平,配置为 0 就是都不丢弃 -->
<discardingThreshold>0</discardingThreshold>
<!-- 设置是否在异步线程中包含调用者数据默认false其实就是是否可以输出代码位置 -->
<includeCallerData>true</includeCallerData>
<appender-ref ref="out" />
</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}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 5天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${log.pattern}</pattern>
</encoder>
<!-- 默认true日志直接写入磁盘设置为false先写入内存buffer满后批量刷盘 -->
<immediateFlush>false</immediateFlush>
<!-- 日志写入内存容量上限 -->
<bufferSize>8192</bufferSize>
</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}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>WARN</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}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 发布日志输出 -->
<appender name="publish" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/publish.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/publish-%d{yyyy-MM-dd}-${LOCAL_IP}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>5</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!--默认-->
<root level="info">
<appender-ref ref="CC_CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
<appender-ref ref="error"/>
</root>
<!-- Spring日志 -->
<logger name="org.springframework" level="error" />
<!-- 系统模块日志 -->
<logger name="com.chestnut" level="info" />
<!-- 系统定时任务日志-->
<logger name="cron" level="info">
<appender-ref ref="cron"/>
</logger>
<!--系统发布日志-->
<logger name="publish" level="info">
<appender-ref ref="publish"/>
</logger>
</configuration>

View File

@ -1,112 +0,0 @@
<?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>
<!-- 发布日志输出 -->
<appender name="publish" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/publish.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/publish.%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>
<!--系统发布日志-->
<logger name="publish" level="debug">
<appender-ref ref="publish"/>
</logger>
</configuration>

View File

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

View File

@ -30,13 +30,15 @@ import java.util.function.Supplier;
* @author 兮玥
* @email 190785909@qq.com
*/
@Component(IMonitoredCache.BEAN_PREFIX + AdMonitoredCache.ID)
@Component(IMonitoredCache.BEAN_PREFIX + AdNameMonitoredCache.ID)
@RequiredArgsConstructor
public class AdMonitoredCache implements IMonitoredCache<Map<String, String>> {
public class AdNameMonitoredCache implements IMonitoredCache<Map<String, String>> {
public static final String ID = "AD";
public static final String ID = "AD_ID2NAME";
private static final String CACHE_PREFIX = CMSConfig.CachePrefix + "adv-ids";
static final String NAME = "{MONITORED.CACHE." + ID + "}";
private static final String CACHE_PREFIX = CMSConfig.CachePrefix + "adv-id2name";
private final RedisCache redisCache;
@ -47,7 +49,7 @@ public class AdMonitoredCache implements IMonitoredCache<Map<String, String>> {
@Override
public String getCacheName() {
return "{MONITORED.CACHE.AD}";
return NAME;
}
@Override
@ -64,7 +66,15 @@ public class AdMonitoredCache implements IMonitoredCache<Map<String, String>> {
return redisCache.getCacheMap(CACHE_PREFIX, String.class, supplier);
}
public void clear() {
this.redisCache.deleteObject(CACHE_PREFIX);
public String getCacheValue(Long adId) {
return redisCache.getCacheMapValue(CACHE_PREFIX, adId.toString());
}
public void update(Long advertisementId, String adName) {
this.redisCache.setCacheMapValue(CACHE_PREFIX, advertisementId.toString(), adName);
}
public void delete(Long advertisementId) {
this.redisCache.deleteCacheMapValue(CACHE_PREFIX, advertisementId.toString());
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.advertisement.cache;
import com.chestnut.common.redis.IMonitoredCache;
import com.chestnut.common.redis.RedisCache;
import com.chestnut.contentcore.config.CMSConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* AdRedirectUrlMonitoredCache
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Component(IMonitoredCache.BEAN_PREFIX + AdRedirectUrlMonitoredCache.ID)
@RequiredArgsConstructor
public class AdRedirectUrlMonitoredCache implements IMonitoredCache<String> {
public static final String ID = "AD_ID2URL";
static final String NAME = "{MONITORED.CACHE." + ID + "}";
private static final String CACHE_PREFIX = CMSConfig.CachePrefix + "adv-id2url:";
private final RedisCache redisCache;
@Override
public String getId() {
return ID;
}
@Override
public String getCacheName() {
return NAME;
}
@Override
public String getCacheKey() {
return CACHE_PREFIX;
}
@Override
public String getCache(String cacheKey) {
return redisCache.getCacheObject(cacheKey, String.class);
}
public String getCacheValue(Long siteId, Long advertisementId) {
return redisCache.getCacheObject(CACHE_PREFIX + siteId + ":" + advertisementId.toString(), String.class);
}
public void update(Long siteId, Long advertisementId, String redirectUrl) {
this.redisCache.setCacheObject(CACHE_PREFIX + siteId + ":" + advertisementId.toString(), redirectUrl);
}
public void delete(Long siteId, Long advertisementId) {
this.redisCache.deleteObject(CACHE_PREFIX + siteId + ":" + advertisementId.toString());
}
}

View File

@ -15,12 +15,16 @@
*/
package com.chestnut.advertisement.controller.front;
import com.chestnut.advertisement.cache.AdNameMonitoredCache;
import com.chestnut.advertisement.service.IAdvertisementService;
import com.chestnut.advertisement.stat.AdClickStatEventHandler;
import com.chestnut.advertisement.stat.AdViewStatEventHandler;
import com.chestnut.common.redis.RedisCache;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.stat.core.StatEvent;
import com.chestnut.stat.service.impl.StatEventService;
import com.fasterxml.jackson.databind.node.ObjectNode;
@ -33,8 +37,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
/**
@ -51,13 +53,28 @@ public class AdApiController extends BaseRestController {
private final StatEventService statEventService;
private final IAdvertisementService advertisementService;
private final AdNameMonitoredCache adNameMonitoredCache;
private final RedisCache redisCache;
@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));
if (!IdUtils.validate(siteId) || !IdUtils.validate(advertisementId)) {
log.warn("Invalid sid/aid: sid = {}, aid = {}", siteId, advertisementId);
return;
}
String redirectUrl = advertisementService.getRedirectUrlByAdId(siteId, advertisementId);
if (StringUtils.isEmpty(redirectUrl)) {
// TODO 跳转公共错误页面
response.getWriter().write("Invalid advertisement.");
return;
}
dealAdClick(siteId, advertisementId);
response.sendRedirect(redirectUrl);
}
@GetMapping("/click")
@ -66,6 +83,15 @@ public class AdApiController extends BaseRestController {
log.warn("Invalid sid/aid: sid = {}, aid = {}", siteId, advertisementId);
return;
}
boolean hasAd = redisCache.hasMapKey(adNameMonitoredCache.getCacheKey(), advertisementId.toString());
if (!hasAd) {
log.warn("Invalid advertisement id: {}", advertisementId);
return;
}
dealAdClick(siteId, advertisementId);
}
private void dealAdClick(Long siteId, Long advertisementId) {
StatEvent evt = new StatEvent();
evt.setType(AdClickStatEventHandler.TYPE);
ObjectNode objectNode = JacksonUtils.objectNode();
@ -83,6 +109,11 @@ public class AdApiController extends BaseRestController {
log.warn("Invalid sid/aid: sid = {}, aid = {}", siteId, advertisementId);
return;
}
boolean hasAd = redisCache.hasMapKey(adNameMonitoredCache.getCacheKey(), advertisementId.toString());
if (!hasAd) {
log.warn("Invalid advertisement id: {}", advertisementId);
return;
}
StatEvent evt = new StatEvent();
evt.setType(AdViewStatEventHandler.TYPE);
ObjectNode objectNode = JacksonUtils.objectNode();

View File

@ -15,15 +15,14 @@
*/
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;
import java.util.List;
import java.util.Map;
/**
* 广告数据管理Service
*/
@ -35,7 +34,16 @@ public interface IAdvertisementService extends IService<CmsAdvertisement> {
* @return Map
*/
Map<String, String> getAdvertisementMap();
/**
* 获取广告跳转地址
*
* @param siteId
* @param advertisementId
* @return
*/
String getRedirectUrlByAdId(Long siteId, Long advertisementId);
/**
* 添加广告数据
*

View File

@ -18,7 +18,8 @@ package com.chestnut.advertisement.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chestnut.advertisement.AdSpacePageWidgetType;
import com.chestnut.advertisement.IAdvertisementType;
import com.chestnut.advertisement.cache.AdMonitoredCache;
import com.chestnut.advertisement.cache.AdNameMonitoredCache;
import com.chestnut.advertisement.cache.AdRedirectUrlMonitoredCache;
import com.chestnut.advertisement.domain.CmsAdvertisement;
import com.chestnut.advertisement.mapper.CmsAdvertisementMapper;
import com.chestnut.advertisement.pojo.dto.AdvertisementDTO;
@ -32,20 +33,16 @@ import com.chestnut.contentcore.core.IPageWidgetType;
import com.chestnut.contentcore.domain.CmsPageWidget;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.properties.SiteApiUrlProperty;
import com.chestnut.contentcore.publish.IStaticizeType;
import com.chestnut.contentcore.service.IPageWidgetService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.system.fixed.dict.EnableOrDisable;
import com.chestnut.system.security.StpAdminUtil;
import freemarker.template.TemplateException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -61,9 +58,11 @@ import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper, CmsAdvertisement>
implements IAdvertisementService {
implements IAdvertisementService, CommandLineRunner {
private final AdMonitoredCache adCache;
private final AdNameMonitoredCache adNameCache;
private final AdRedirectUrlMonitoredCache adRedirectUrlCache;
private final Map<String, IAdvertisementType> advertisementTypes;
@ -85,7 +84,7 @@ public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper
@Override
public Map<String, String> getAdvertisementMap() {
return adCache.getCache(() -> {
return adNameCache.getCache(() -> {
return this.lambdaQuery()
.select(List.of(CmsAdvertisement::getAdvertisementId, CmsAdvertisement::getName))
.list().stream()
@ -93,6 +92,11 @@ public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper
});
}
@Override
public String getRedirectUrlByAdId(Long siteId, Long advertisementId) {
return adRedirectUrlCache.getCacheValue(siteId, advertisementId);
}
@Override
public CmsAdvertisement addAdvertisement(AdvertisementDTO dto) {
CmsPageWidget pageWidget = this.pageWidgetService.getById(dto.getAdSpaceId());
@ -106,8 +110,8 @@ public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper
advertisement.setState(EnableOrDisable.ENABLE);
advertisement.createBy(dto.getOperator().getUsername());
this.save(advertisement);
this.adCache.clear();
// 更新缓存
this.updateCache(advertisement);
return advertisement;
}
@ -120,14 +124,20 @@ public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper
BeanUtils.copyProperties(dto, advertisement, "adSpaceId");
advertisement.updateBy(dto.getOperator().getUsername());
this.updateById(advertisement);
// 更新缓存
this.updateCache(advertisement);
return advertisement;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAdvertisement(List<Long> advertisementIds) {
this.removeByIds(advertisementIds);
this.adCache.clear();
List<CmsAdvertisement> advertisements = this.listByIds(advertisementIds);
this.removeByIds(advertisements);
// 更新缓存
for (CmsAdvertisement advertisement : advertisements) {
this.deleteCache(advertisement.getSiteId(), advertisement.getAdvertisementId());
}
}
@Override
@ -168,7 +178,21 @@ public class AdvertisementServiceImpl extends ServiceImpl<CmsAdvertisementMapper
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);
return apiUrl + "api/adv/redirect?sid=" + adv.getSiteId() + "&aid=" + adv.getAdvertisementId();
}
private void updateCache(CmsAdvertisement advertisement) {
this.adNameCache.update(advertisement.getAdvertisementId(), advertisement.getName());
this.adRedirectUrlCache.update(advertisement.getSiteId(), advertisement.getAdvertisementId(), advertisement.getRedirectUrl());
}
private void deleteCache(Long siteId, Long advertisementId) {
this.adNameCache.delete(advertisementId);
this.adRedirectUrlCache.delete(siteId, advertisementId);
}
@Override
public void run(String... args) throws Exception {
this.list().forEach(this::updateCache);
}
}

View File

@ -22,4 +22,5 @@ STAT.MENU.CmsAdViewLog=广告展现日志
SCHEDULED_TASK.AdvertisementStatJob=广告统计任务
SCHEDULED_TASK.AdvertisementPublishJob=广告定时发布下线任务
MONITORED.CACHE.AD=广告
MONITORED.CACHE.AD_ID2NAME=广告名称
MONITORED.CACHE.AD_ID2URL=广告跳转链接

View File

@ -22,4 +22,5 @@ STAT.MENU.CmsAdViewLog=View Logs
SCHEDULED_TASK.AdvertisementStatJob=AD Statistics Task
SCHEDULED_TASK.AdvertisementPublishJob=AD Publish/Offline Task
MONITORED.CACHE.AD=Advertisement
MONITORED.CACHE.AD_ID2NAME=AD Name
MONITORED.CACHE.AD_ID2URL=AD Redirect URL

View File

@ -22,4 +22,5 @@ STAT.MENU.CmsAdViewLog=廣告展現日誌
SCHEDULED_TASK.AdvertisementStatJob=廣告統計任務
SCHEDULED_TASK.AdvertisementPublishJob=廣告定時發布下線任務
MONITORED.CACHE.AD=廣告
MONITORED.CACHE.AD_ID2NAME=廣告名稱
MONITORED.CACHE.AD_ID2URL=廣告跳轉鏈接

View File

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

View File

@ -18,6 +18,7 @@ package com.chestnut.article;
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.format.ArticleBodyFormat_RichText;
import com.chestnut.article.service.IArticleService;
import com.chestnut.common.async.AsyncTaskManager;
import com.chestnut.common.utils.JacksonUtils;
@ -32,6 +33,7 @@ import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
@ -47,6 +49,8 @@ public class ArticleCoreDataHandler implements ICoreDataHandler {
private final IArticleService articleService;
private final Map<String, IArticleBodyFormat> articleBodyFormatMap;
@Override
public void onSiteExport(SiteExportContext context) {
// cms_article_detail
@ -101,6 +105,9 @@ public class ArticleCoreDataHandler implements ICoreDataHandler {
}
html.append(contentHtml.substring(index));
data.setContentHtml(html.toString());
if (!articleBodyFormatMap.containsKey(IArticleBodyFormat.BEAN_PREFIX + data.getFormat())) {
data.setFormat(ArticleBodyFormat_RichText.ID); // 不支持的文章格式一律设置为富文本格式
}
articleService.dao().save(data);
} catch (Exception e) {
AsyncTaskManager.addErrMessage("导入文章数据`" + oldContentId + "`失败:" + e.getMessage());

View File

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

View File

@ -16,6 +16,7 @@
package com.chestnut.block;
import com.chestnut.block.domain.vo.ManualPageWidgetVO;
import com.chestnut.common.annotation.XComment;
import com.chestnut.common.utils.JacksonUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.IPageWidget;
@ -120,13 +121,15 @@ public class ManualPageWidgetType implements IPageWidgetType {
private String summary;
@Deprecated
private String url;
@XComment("与url字段同值仅为习惯添加")
private String link;
private String logo;
// TODO 下个大版本移除在模板使用${iurl(logo)}
@Deprecated(forRemoval = true)
private String logoSrc;
private LocalDateTime publishDate;

View File

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

View File

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

View File

@ -15,10 +15,9 @@
*/
package com.chestnut.contentcore.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* CMS配置属性
@ -44,5 +43,10 @@ public class CMSProperties {
/**
* 系统启动时是否清空cacheName前缀的所有缓存
*/
private Boolean resetCache = true;
private Boolean resetCache = false;
/**
* 资源分片文件过期时间默认24小时单位
*/
private long resourceChunkExpireSeconds = 24 * 60 * 60;
}

View File

@ -89,10 +89,14 @@ public class CoreController extends BaseRestController {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(dataType);
Assert.notNull(internalDataType, () -> ContentCoreErrorCode.UNSUPPORTED_INTERNAL_DATA_TYPE.exception(dataType));
IInternalDataType.RequestData data = new IInternalDataType.RequestData(dataId, pageIndex, publishPipe,
true, ServletUtils.getParamMap(ServletUtils.getRequest()));
String pageData = internalDataType.getPageData(data);
response.getWriter().write(pageData);
try {
IInternalDataType.RequestData data = new IInternalDataType.RequestData(dataId, pageIndex, publishPipe,
true, ServletUtils.getParamMap(ServletUtils.getRequest()));
String pageData = internalDataType.getPageData(data);
response.getWriter().write(pageData);
} catch (Exception e) {
e.printStackTrace(response.getWriter());
}
}
/**
@ -107,7 +111,7 @@ public class CoreController extends BaseRestController {
public void browse(@PathVariable("dataType") String dataType, @PathVariable("dataId") Long dataId,
@RequestParam(value = "pp") String publishPipe,
@RequestParam(value = "pi", required = false, defaultValue = "1") Integer pageIndex)
throws IOException, TemplateException {
throws IOException {
HttpServletResponse response = ServletUtils.getResponse();
response.setCharacterEncoding(Charset.defaultCharset().displayName());
@ -115,10 +119,14 @@ public class CoreController extends BaseRestController {
IInternalDataType internalDataType = ContentCoreUtils.getInternalDataType(dataType);
Assert.notNull(internalDataType, () -> ContentCoreErrorCode.UNSUPPORTED_INTERNAL_DATA_TYPE.exception(dataType));
IInternalDataType.RequestData data = new IInternalDataType.RequestData(dataId, pageIndex, publishPipe,
false, ServletUtils.getParamMap(ServletUtils.getRequest()));
String pageData = internalDataType.getPageData(data);
response.getWriter().write(pageData);
try {
IInternalDataType.RequestData data = new IInternalDataType.RequestData(dataId, pageIndex, publishPipe,
false, ServletUtils.getParamMap(ServletUtils.getRequest()));
String pageData = internalDataType.getPageData(data);
response.getWriter().write(pageData);
} catch (Exception e) {
e.printStackTrace(response.getWriter());
}
}
@GetMapping("/cms/ssi/virtual/")

View File

@ -46,6 +46,7 @@ import com.chestnut.contentcore.util.InternalUrlUtils;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
import com.chestnut.system.validator.LongId;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
@ -94,10 +95,12 @@ public class ResourceController extends BaseRestController {
public R<?> listData(@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "resourceType", required = false) String resourceType,
@RequestParam(value = "owner", required = false, defaultValue = "false") boolean owner,
@RequestParam(value = "siteId", required = false, defaultValue = "0") Long siteId,
@RequestParam(value = "beginTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date beginTime,
@RequestParam(value = "endTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endTime) {
@RequestParam(value = "endTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endTime,
HttpServletRequest request) {
PageRequest pr = this.getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
CmsSite site = siteService.getSiteOrCurrent(siteId, request);
LambdaQueryWrapper<CmsResource> q = new LambdaQueryWrapper<CmsResource>()
.eq(CmsResource::getSiteId, site.getSiteId())
.like(StringUtils.isNotEmpty(name), CmsResource::getFileName, name)

View File

@ -45,6 +45,7 @@ import com.chestnut.contentcore.util.CmsPrivUtils;
import com.chestnut.contentcore.util.SiteUtils;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
@ -83,9 +84,11 @@ public class TemplateController extends BaseRestController {
)
@GetMapping
public R<?> getTemplateList(@RequestParam(value = "publishPipeCode", required = false) String publishPipeCode,
@RequestParam(value = "filename", required = false) String filename) {
@RequestParam(value = "siteId", required = false, defaultValue = "0") Long siteId,
@RequestParam(value = "filename", required = false) String filename,
HttpServletRequest request) {
PageRequest pr = this.getPageRequest();
CmsSite site = this.siteService.getCurrentSite(ServletUtils.getRequest());
CmsSite site = siteService.getSiteOrCurrent(siteId, request);
this.templateService.scanTemplates(site);
Page<CmsTemplate> page = this.templateService.lambdaQuery().eq(CmsTemplate::getSiteId, site.getSiteId())
.eq(StringUtils.isNotEmpty(publishPipeCode), CmsTemplate::getPublishPipeCode, publishPipeCode)

View File

@ -15,17 +15,15 @@
*/
package com.chestnut.contentcore.service;
import java.io.IOException;
import java.util.Map;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.common.security.domain.LoginUser;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.domain.dto.SiteDTO;
import com.chestnut.contentcore.domain.dto.SiteDefaultTemplateDTO;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
public interface ISiteService extends IService<CmsSite> {
/**
@ -38,7 +36,16 @@ public interface ISiteService extends IService<CmsSite> {
*/
boolean checkSiteUnique(String siteName, String sitePath, Long siteId);
/**
/**
* 获取指定id站点数据如果不存在则返回当前站点数据
*
* @param siteId
* @param request
* @return
*/
CmsSite getSiteOrCurrent(Long siteId, HttpServletRequest request);
/**
* 获取当前站点保存在token中
*/
CmsSite getCurrentSite(HttpServletRequest request);

View File

@ -375,7 +375,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
templateContext.setOtherFileName(contentLink + "&pi=" + TemplateContext.PlaceHolder_PageNo);
// staticize
this.staticizeService.process(templateContext, writer);
logger.debug("[{}][{}]内容模板解析:{},耗时:{}", requestData.getPublishPipeCode(), contentType.getName(), content.getTitle(),
logger.debug("[{}][{}]内容模板解析:{},耗时:{}", requestData.getPublishPipeCode(), contentType.getId(), content.getTitle(),
System.currentTimeMillis() - s);
return writer.toString();
}
@ -477,7 +477,7 @@ public class PublishServiceImpl implements IPublishService, ApplicationContextAw
templateType.initTemplateData(content.getContentId(), templateContext);
// staticize
this.staticizeService.process(templateContext, writer);
logger.debug("[{}][{}]内容扩展模板解析:{},耗时:{}", publishPipeCode, contentType.getName(), content.getTitle(),
logger.debug("[{}][{}]内容扩展模板解析:{},耗时:{}", publishPipeCode, contentType.getId(), content.getTitle(),
System.currentTimeMillis() - s);
return writer.toString();
}

View File

@ -24,6 +24,7 @@ import com.chestnut.common.storage.StorageReadArgs.StorageReadArgsBuilder;
import com.chestnut.common.storage.exception.StorageErrorCode;
import com.chestnut.common.utils.*;
import com.chestnut.common.utils.file.FileExUtils;
import com.chestnut.common.utils.image.ImageUtils;
import com.chestnut.contentcore.core.IResourceStat;
import com.chestnut.contentcore.core.IResourceType;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
@ -43,7 +44,7 @@ import com.chestnut.system.fixed.dict.EnableOrDisable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;
import java.io.File;
@ -162,7 +163,7 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
@Override
public CmsResource addBase64Image(CmsSite site, String operator, String base64Data) throws IOException {
if (!base64Data.startsWith("data:image/")) {
if (!ImageUtils.isBase64Image(base64Data)) {
return null;
}
String suffix = base64Data.substring(11, base64Data.indexOf(";"));
@ -333,7 +334,7 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
String imgTag = matcher.group();
String src = matcher.group(1);
try {
if (StringUtils.startsWithIgnoreCase(src, "data:image/")) {
if (ImageUtils.isBase64Image(src)) {
// base64图片保存到资源库
CmsResource resource = addBase64Image(site, operator, src);
if (Objects.nonNull(resource)) {
@ -347,7 +348,7 @@ public class ResourceServiceImpl extends ServiceImpl<CmsResourceMapper, CmsResou
}
}
} catch (Exception e1) {
String imgSrc = (src.startsWith("data:image/") ? src.substring(0, 20) : src);
String imgSrc = (src.startsWith("data:image/") ? "base64Img" : src);
log.warn("Save image failed: " + imgSrc);
AsyncTaskManager.addErrMessage("Download remote image failed: " + imgSrc);
}

View File

@ -84,6 +84,18 @@ public class SiteServiceImpl extends ServiceImpl<CmsSiteMapper, CmsSite> impleme
return site;
}
@Override
public CmsSite getSiteOrCurrent(Long siteId, HttpServletRequest request) {
CmsSite site = null;
if (IdUtils.validate(siteId)) {
site = getSite(siteId);
}
if (Objects.isNull(site)) {
site = getCurrentSite(request);
}
return site;
}
@Override
public CmsSite getCurrentSite(HttpServletRequest request) {
LoginUser loginUser = StpAdminUtil.getLoginUser();

View File

@ -39,10 +39,7 @@ import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.*;
/**
* 站点导入导出
@ -75,6 +72,8 @@ public class SiteThemeService {
private final List<ICoreDataHandler> contentCoreHandlers;
private final Map<String, IContentType> contentTypes;
public AsyncTask importSiteTheme(CmsSite site, final File zipFile, LoginUser operator) {
AsyncTask asyncTask = new AsyncTask() {
@ -232,6 +231,9 @@ public class SiteThemeService {
list.forEach(content -> {
Long sourceContentId = content.getContentId();
try {
if (!contentTypes.containsKey(IContentType.BEAN_NAME_PREFIX + content.getContentType())) {
throw new RuntimeException("Unsupported content type: " + content.getContentType());
}
CmsCatalog catalog = catalogService.getCatalog(context.getCatalogIdMap().get(content.getCatalogId()));
if (Objects.isNull(catalog)) {
throw new RuntimeException("Catalog is missing.");

View File

@ -28,7 +28,10 @@ import com.chestnut.contentcore.fixed.config.TemplateSuffix;
import com.chestnut.contentcore.properties.SiteApiUrlProperty;
import com.chestnut.system.security.StpAdminUtil;
import freemarker.core.Environment;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@ -119,6 +122,14 @@ public class TemplateUtils {
*/
public final static String TemplateVariable_ClientType = "ClientType";
public static String evalPrefix(Environment env) throws TemplateModelException {
return FreeMarkerUtils.evalStringVariable(env, TemplateVariable_Prefix);
}
public static String evalApiPrefix(Environment env) throws TemplateModelException {
return FreeMarkerUtils.evalStringVariable(env, TemplateVariable_ApiPrefix);
}
public static Long evalSiteId(Environment env) throws TemplateModelException {
return FreeMarkerUtils.evalLongVariable(env, "Site.siteId");
}
@ -127,10 +138,42 @@ public class TemplateUtils {
return FreeMarkerUtils.evalLongVariable(env, "Catalog.catalogId");
}
public static Long findCatalogId(Environment env) {
try {
TemplateModel model = env.getVariable("Catalog");
if (!(model instanceof TemplateHashModel)) {
return null;
}
model = ((TemplateHashModel) model).get("catalogId");
if (model instanceof TemplateNumberModel m) {
return m.getAsNumber().longValue();
}
} catch (TemplateModelException e) {
// ignore
}
return null;
}
public static Long evalContentId(Environment env) throws TemplateModelException {
return FreeMarkerUtils.evalLongVariable(env, "Content.contentId");
}
public static Long findContentId(Environment env) {
try {
TemplateModel model = env.getVariable("Content");
if (!(model instanceof TemplateHashModel)) {
return null;
}
model = ((TemplateHashModel) model).get("contentId");
if (model instanceof TemplateNumberModel m) {
return m.getAsNumber().longValue();
}
} catch (TemplateModelException e) {
// ignore
}
return null;
}
/**
* 添加站点数据到模板上线文变量中
*

View File

@ -205,6 +205,7 @@ SCHEDULED_TASK.SitePublishJobHandler=定时发布任务
SCHEDULED_TASK.ContentTopCancelJobHandler=内容置顶取消任务
SCHEDULED_TASK.UpdateDynamicDataJobHandler=保存内容动态数据任务
SCHEDULED_TASK.ContentOfflineJobHandler=内容定时下线任务
SCHEDULED_TASK.ResourceChunkClearJobHandler=资源上传分片文件过期删除任务
# 缓存监控
MONITORED.CACHE.SITE=站点

View File

@ -205,6 +205,7 @@ SCHEDULED_TASK.SitePublishJobHandler=Site Publish Task
SCHEDULED_TASK.ContentTopCancelJobHandler=Content Top Cancel Task
SCHEDULED_TASK.UpdateDynamicDataJobHandler=Save Content Dynamic Data Task
SCHEDULED_TASK.ContentOfflineJobHandler=Content Offline Task
SCHEDULED_TASK.ResourceChunkClearJobHandler=Remove Expired Resource Chunks Task
# 内容静态化子目录划分规则
CONTENT_PATH_RULE.IdHash=/content id hash/

View File

@ -205,6 +205,7 @@ SCHEDULED_TASK.SitePublishJobHandler=定時發布任務
SCHEDULED_TASK.ContentTopCancelJobHandler=內容置頂取消任務
SCHEDULED_TASK.UpdateDynamicDataJobHandler=保存內容動態數據任務
SCHEDULED_TASK.ContentOfflineJobHandler=內容定時下線任務
SCHEDULED_TASK.ResourceChunkClearJobHandler=資源上傳分片文件過期刪除任務
# 缓存监控
MONITORED.CACHE.SITE=站點

View File

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

View File

@ -16,7 +16,6 @@
package com.chestnut.customform;
import com.chestnut.customform.domain.CmsCustomFormData;
import com.chestnut.exmodel.domain.CmsExtendModelData;
import com.chestnut.xmodel.core.IMetaModelType;
import com.chestnut.xmodel.core.MetaModelField;
import com.chestnut.xmodel.core.impl.MetaControlType_Input;
@ -68,8 +67,12 @@ public class CmsCustomFormMetaModelType implements IMetaModelType {
"site_id", false, MetaControlType_Input.TYPE, MetaFieldType.LONG);
public static final MetaModelField FIELD_CLIENT_IP = new MetaModelField("IP", "clientIp",
"client_ip", false, MetaControlType_Input.TYPE, MetaFieldType.SHORT_TEXT);
// 用户唯一标识未登录
public static final MetaModelField FIELD_UUID = new MetaModelField("UUID", "uuid",
"uuid", false, MetaControlType_Input.TYPE, MetaFieldType.MEDIUM_TEXT);
// 会员ID已登录
public static final MetaModelField FIELD_UID = new MetaModelField("UID", "uid",
"uid", false, MetaControlType_Input.TYPE, MetaFieldType.LONG);
public static final MetaModelField FIELD_CREATE_TIME = new MetaModelField("创建时间", "createTime",
"create_time", false, MetaControlType_Input.TYPE, MetaFieldType.DATETIME);
}

View File

@ -16,11 +16,13 @@
package com.chestnut.customform;
import com.chestnut.common.utils.ReflectASMUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.fixed.config.SiteApiUrl;
import com.chestnut.contentcore.properties.SiteApiUrlProperty;
import com.chestnut.contentcore.util.SiteUtils;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.member.security.StpMemberUtil;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
@ -36,6 +38,11 @@ public class CustomFormConsts {
public static final String TemplateVariable_CustomForm = "CustomForm";
/**
* uuid请求参数/header参数
*/
public static final String PARAMETER_UUID = "_cc_uuid";
public static String getCustomFormActionUrl(CmsSite site, String publishPipeCode) {
return SiteApiUrlProperty.getValue(site, publishPipeCode) + "api/customform/submit";
}
@ -45,4 +52,26 @@ public class CustomFormConsts {
map.put("action", getCustomFormActionUrl(site, publishPipeCode));
return map;
}
/**
* 尝试获取uuid
* 已登录用户取登录用户ID
* 未登录用户尝试从请求参数/header参数/cookie参数获取
*/
public static String tryToGetUUID(HttpServletRequest request) {
if (StpMemberUtil.isLogin()) {
return StpMemberUtil.getLoginUser().getUserId().toString();
}
String uuid = request.getParameter("uuid"); // 兼容老版本
if (StringUtils.isEmpty(uuid)) {
uuid = request.getParameter(PARAMETER_UUID);
if (StringUtils.isEmpty(uuid)) {
uuid = request.getHeader(PARAMETER_UUID);
if (StringUtils.isEmpty(uuid)) {
ServletUtils.getCookieValue(request, PARAMETER_UUID);
}
}
}
return uuid;
}
}

View File

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

View File

@ -34,6 +34,7 @@ import com.chestnut.customform.domain.dto.CustomFormAddDTO;
import com.chestnut.customform.domain.dto.CustomFormEditDTO;
import com.chestnut.customform.domain.vo.CustomFormVO;
import com.chestnut.customform.permission.CustomFormPriv;
import com.chestnut.customform.rule.ICustomFormLimitRule;
import com.chestnut.customform.service.ICustomFormService;
import com.chestnut.system.security.AdminUserType;
import com.chestnut.system.security.StpAdminUtil;
@ -66,6 +67,14 @@ public class CustomFormController extends BaseRestController {
private final ICustomFormService customFormService;
private final List<ICustomFormLimitRule> limitRules;
@Priv(type = AdminUserType.TYPE)
@GetMapping("/limit_rules")
public R<?> getLimitRules() {
return bindSelectOptions(this.limitRules, ICustomFormLimitRule::getId, ICustomFormLimitRule::getName);
}
@Priv(type = AdminUserType.TYPE, value = CustomFormPriv.View)
@GetMapping
public R<?> getList(@RequestParam(value = "query", required = false) String query,

View File

@ -15,36 +15,46 @@
*/
package com.chestnut.customform.controller.front;
import com.chestnut.common.captcha.CaptchaType;
import com.chestnut.common.config.CaptchaConfig;
import com.chestnut.common.domain.R;
import com.chestnut.common.exception.CommonErrorCode;
import com.chestnut.common.exception.GlobalException;
import com.chestnut.common.security.web.BaseRestController;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.ServletUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
import com.chestnut.contentcore.domain.CmsResource;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.customform.CmsCustomFormMetaModelType;
import com.chestnut.customform.CustomFormConsts;
import com.chestnut.customform.cache.CustomFormCaptchaMonitoredCache;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.exception.CustomFormErrorCode;
import com.chestnut.customform.fixed.config.CustomFormCaptchaExpireSeconds;
import com.chestnut.customform.service.ICustomFormApiService;
import com.chestnut.customform.service.ICustomFormService;
import com.chestnut.member.security.StpMemberUtil;
import com.chestnut.system.SysConstants;
import com.chestnut.system.annotation.IgnoreDemoMode;
import com.chestnut.system.config.properties.SysProperties;
import com.chestnut.system.domain.vo.ImageCaptchaVO;
import com.chestnut.system.exception.SysErrorCode;
import com.chestnut.system.fixed.dict.YesOrNo;
import com.chestnut.xmodel.service.IModelDataService;
import com.chestnut.system.validator.LongId;
import com.google.code.kaptcha.Producer;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* <p>
@ -61,54 +71,110 @@ public class CustomFormApiController extends BaseRestController {
private final ICustomFormService customFormService;
private final IModelDataService modelDataService;
private final ICustomFormApiService customFormApiService;
private final IResourceService resourceService;
private final CustomFormCaptchaMonitoredCache captchaCache;
private final ISiteService siteService;
private final Map<String, Producer> captchaProducers;
private final SysProperties properties;
@GetMapping("/captchaImage")
public R<?> getCaptchaImage(@RequestParam @LongId Long formId, HttpServletRequest request) {
CmsCustomForm form = this.customFormService.getById(formId);
Assert.notNull(form, CustomFormErrorCode.FORM_NOT_FOUND::exception);
// 是否需要验证码
if (!YesOrNo.isYes(form.getNeedCaptcha())) {
return R.ok(ImageCaptchaVO.builder().captchaEnabled(false).build());
}
// 是否登录
if (YesOrNo.isYes(form.getNeedLogin()) && !StpMemberUtil.isLogin()) {
throw CustomFormErrorCode.NOT_LOGIN.exception();
}
String uuid = CustomFormConsts.tryToGetUUID(request);
Assert.notEmpty(uuid, CustomFormErrorCode.MISSING_UUID::exception);
// 保存验证码信息
String verifyKey = CustomFormCaptchaMonitoredCache.CACHE_PREFIX + uuid;
String capStr;
String code;
BufferedImage image;
Producer captchaProducer = captchaProducers.get(CaptchaConfig.BEAN_PREFIX + this.properties.getCaptchaType());
Assert.notNull(captchaProducer, () -> SysErrorCode.CAPTCHA_CONFIG_ERR.exception(this.properties.getCaptchaType()));
// 生成验证码
String captchaType = properties.getCaptchaType();
if (CaptchaType.MATH.equals(captchaType)) {
String capText = captchaProducer.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducer.createImage(capStr);
} else if (CaptchaType.CHAR.equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
} else {
throw new GlobalException("Unknown captcha type: " + captchaType);
}
Integer expireSeconds = CustomFormCaptchaExpireSeconds.getSeconds();
captchaCache.setCache(verifyKey, code, expireSeconds, TimeUnit.SECONDS);
try(FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
ImageIO.write(image, "jpg", os);
ImageCaptchaVO vo = ImageCaptchaVO.builder().captchaEnabled(true).uuid(uuid)
.img(Base64.getEncoder().encodeToString(os.toByteArray())).build();
return R.ok(vo);
} catch (IOException e) {
return R.fail(e.getMessage());
}
}
@IgnoreDemoMode
@PostMapping("/submit")
public R<?> submitForm(@RequestBody @Validated Map<String, Object> formData) {
public R<?> submitForm(@RequestBody Map<String, Object> formData, HttpServletRequest request) throws IOException {
Long formId = MapUtils.getLong(formData, "formId");
if (!IdUtils.validate(formId)) {
return R.fail("Unknown form: " + formId);
throw CommonErrorCode.INVALID_REQUEST_ARG.exception("formId");
}
CmsCustomForm form = this.customFormService.getById(formId);
if (Objects.isNull(form)) {
return R.fail("Unknown form: " + formId);
}
Assert.notNull(form, CustomFormErrorCode.FORM_NOT_FOUND::exception);
// 判断登录
if (YesOrNo.isYes(form.getNeedLogin()) && !StpMemberUtil.isLogin()) {
return R.fail("Please login first.");
throw CustomFormErrorCode.NOT_LOGIN.exception();
}
// TODO 限制规则校验验证码IP浏览器指纹
// 获取用户标识
String uuid = CustomFormConsts.tryToGetUUID(request);
Assert.notEmpty(uuid, CustomFormErrorCode.MISSING_UUID::exception);
// 验证码
if (YesOrNo.isYes(form.getNeedCaptcha())) {
String captcha = MapUtils.getString(formData, "captcha", StringUtils.EMPTY);
this.validateCaptcha(captcha, uuid);
}
String uuid = MapUtils.getString(formData, "uuid", StringUtils.EMPTY);
String clientIp = ServletUtils.getIpAddr(ServletUtils.getRequest());
String clientIp = ServletUtils.getIpAddr(request);
formData.put(CmsCustomFormMetaModelType.FIELD_DATA_ID.getCode(), IdUtils.getSnowflakeId());
formData.put(CmsCustomFormMetaModelType.FIELD_MODEL_ID.getCode(), form.getFormId());
formData.put(CmsCustomFormMetaModelType.FIELD_SITE_ID.getCode(), form.getSiteId());
formData.put(CmsCustomFormMetaModelType.FIELD_CLIENT_IP.getCode(), clientIp);
formData.put(CmsCustomFormMetaModelType.FIELD_UUID.getCode(), uuid);
formData.put(CmsCustomFormMetaModelType.FIELD_CREATE_TIME.getCode(), LocalDateTime.now());
CmsSite site = siteService.getSite(form.getSiteId());
formData.forEach((k, v) -> {
if (Objects.nonNull(v) && v.toString().startsWith("data:image/png;base64,")) {
try {
CmsResource resource = resourceService.addBase64Image(site, SysConstants.SYS_OPERATOR, v.toString());
formData.put(k, InternalDataType_Resource.getInternalUrl(resource));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
this.modelDataService.saveModelData(form.getModelId(), formData);
if (YesOrNo.isYes(form.getNeedLogin())) {
formData.put(CmsCustomFormMetaModelType.FIELD_UID.getCode(), StpMemberUtil.getLoginUser().getUserId());
}
customFormApiService.submit(form, formData);
return R.ok();
}
private void validateCaptcha(String code, String uuid) {
Assert.notEmpty(code, () -> CommonErrorCode.INVALID_REQUEST_ARG.exception("captcha"));
String cacheKey = CustomFormCaptchaMonitoredCache.CACHE_PREFIX + Objects.requireNonNullElse(uuid, StringUtils.EMPTY);
String cacheValue = captchaCache.getCache(cacheKey);
// 过期判断
Assert.notNull(cacheValue, CommonErrorCode.CAPTCHA_EXPIRED::exception);
// 未过期判断是否与输入验证码一致
Assert.isTrue(StringUtils.equals(code, cacheValue), CommonErrorCode.INVALID_CAPTCHA::exception);
// 移除缓存
captchaCache.deleteCache(cacheKey);
}
}

View File

@ -23,6 +23,8 @@ import com.chestnut.xmodel.core.BaseModelData;
import lombok.Getter;
import lombok.Setter;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
@ -34,10 +36,11 @@ import java.time.LocalDateTime;
@Getter
@Setter
@TableName(value = CmsCustomFormData.TABLE_NAME, autoResultMap = true)
public class CmsCustomFormData extends BaseModelData {
public class CmsCustomFormData extends BaseModelData implements Serializable {
@Serial
private static final long serialVersionUID =1L;
private static final long serialVersionUID=1L;
public static final String TABLE_NAME = "cms_cfd_default";
@TableId(value = "data_id", type = IdType.INPUT)

View File

@ -0,0 +1,46 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.exception;
import com.chestnut.common.exception.ErrorCode;
public enum CustomFormErrorCode implements ErrorCode {
/**
* 表单不存在
*/
FORM_NOT_FOUND,
/**
* uuid不能为空
*/
MISSING_UUID,
/**
* 未登录
*/
NOT_LOGIN,
/**
* 不能重复提交
*/
CANNOT_RESUBMIT;
@Override
public String value() {
return "{ERR.CUSTOM_FORM." + this.name() + "}";
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.fixed.config;
import com.chestnut.common.utils.ConvertUtils;
import com.chestnut.common.utils.SpringUtils;
import com.chestnut.system.fixed.FixedConfig;
import com.chestnut.system.service.ISysConfigService;
import org.springframework.stereotype.Component;
/**
* 自定义表单验证码过期时间
*/
@Component(FixedConfig.BEAN_PREFIX + CustomFormCaptchaExpireSeconds.ID)
public class CustomFormCaptchaExpireSeconds extends FixedConfig {
public static final String ID = "CustomFormCaptchaExpireSeconds";
private static final ISysConfigService configService = SpringUtils.getBean(ISysConfigService.class);
/**
* 默认600秒
*/
private static final int DEFAULT_VALUE = 600;
public CustomFormCaptchaExpireSeconds() {
super(ID, "{CONFIG." + ID + "}", String.valueOf(DEFAULT_VALUE), null);
}
public static Integer getSeconds() {
String configValue = configService.selectConfigByKey(ID);
return ConvertUtils.toInteger(configValue, DEFAULT_VALUE);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.rule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chestnut.customform.CmsCustomFormMetaModelType;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.domain.CmsCustomFormData;
import com.chestnut.customform.mapper.CustomFormDataMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* CustomFormLimitRule_IP
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Order(2)
@RequiredArgsConstructor
@Component(ICustomFormLimitRule.BEAN_PREFIX + CustomFormLimitRule_IP.ID)
public class CustomFormLimitRule_IP implements ICustomFormLimitRule {
public static final String ID = "IP";
private final CustomFormDataMapper customFormDataMapper;
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "{CUSTOM_FORM.LIMIT_RULE.IP}";
}
@Override
public boolean check(CmsCustomForm form, Map<String, Object> dataMap) {
Object ipAddr = MapUtils.getString(dataMap, CmsCustomFormMetaModelType.FIELD_CLIENT_IP.getCode());
Long count = customFormDataMapper.selectCount(new LambdaQueryWrapper<CmsCustomFormData>()
.eq(CmsCustomFormData::getModelId, form.getFormId())
.eq(CmsCustomFormData::getClientIp, ipAddr));
return count == 0;
}
@Override
public int getOrder() {
return 2;
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.rule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chestnut.customform.CmsCustomFormMetaModelType;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.domain.CmsCustomFormData;
import com.chestnut.customform.mapper.CustomFormDataMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* CustomFormLimitRule_UUID
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Order(3)
@RequiredArgsConstructor
@Component(ICustomFormLimitRule.BEAN_PREFIX + CustomFormLimitRule_UUID.ID)
public class CustomFormLimitRule_UUID implements ICustomFormLimitRule {
public static final String ID = "UUID";
private final CustomFormDataMapper customFormDataMapper;
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "{CUSTOM_FORM.LIMIT_RULE.UUID}";
}
@Override
public boolean check(CmsCustomForm form, Map<String, Object> dataMap) {
Object uuid = MapUtils.getString(dataMap, CmsCustomFormMetaModelType.FIELD_UUID.getCode());
Long count = customFormDataMapper.selectCount(new LambdaQueryWrapper<CmsCustomFormData>()
.eq(CmsCustomFormData::getModelId, form.getFormId())
.eq(CmsCustomFormData::getUuid, uuid));
return count == 0;
}
@Override
public int getOrder() {
return 3;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.rule;
import com.chestnut.customform.domain.CmsCustomForm;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* CustomFormLimitRule_Unlimited
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Order(1)
@RequiredArgsConstructor
@Component(ICustomFormLimitRule.BEAN_PREFIX + CustomFormLimitRule_Unlimited.ID)
public class CustomFormLimitRule_Unlimited implements ICustomFormLimitRule {
public static final String ID = "Unlimited";
@Override
public String getId() {
return ID;
}
@Override
public String getName() {
return "{CUSTOM_FORM.LIMIT_RULE.UNLIMITED}";
}
@Override
public boolean check(CmsCustomForm form, Map<String, Object> dataMap) {
return true;
}
@Override
public int getOrder() {
return 1;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.rule;
import com.chestnut.customform.domain.CmsCustomForm;
import org.springframework.core.Ordered;
import java.util.Map;
/**
* 自定义表单校验规则接口
*
* @author 兮玥
* @email 190785909@qq.com
*/
public interface ICustomFormLimitRule extends Ordered {
String BEAN_PREFIX = "CustomFormLimitRule_";
String getId();
String getName();
boolean check(CmsCustomForm form, Map<String, Object> dataMap);
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.service;
import com.chestnut.customform.domain.CmsCustomForm;
import java.io.IOException;
import java.util.Map;
/**
* ICustomFormApiService
*
* @author 兮玥
* @email 190785909@qq.com
*/
public interface ICustomFormApiService {
/**
* 提交表单数据
*
* @param form 表单定义
* @param formData 表单数据
* @throws IOException ex
*/
void submit(CmsCustomForm form, Map<String, Object> formData) throws IOException;
}

View File

@ -19,6 +19,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.domain.dto.CustomFormAddDTO;
import com.chestnut.customform.domain.dto.CustomFormEditDTO;
import com.chestnut.customform.rule.ICustomFormLimitRule;
import com.chestnut.xmodel.domain.XModel;
import com.chestnut.xmodel.dto.XModelDTO;
@ -27,6 +28,8 @@ import java.util.List;
public interface ICustomFormService extends IService<CmsCustomForm> {
ICustomFormLimitRule getLimitRule(String ruleId);
/**
* 添加自定义表单
*

View File

@ -0,0 +1,91 @@
/*
* Copyright 2022-2025 兮玥(190785909@qq.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.chestnut.customform.service.impl;
import com.chestnut.common.utils.image.ImageUtils;
import com.chestnut.contentcore.core.impl.InternalDataType_Resource;
import com.chestnut.contentcore.domain.CmsResource;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.service.IResourceService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.customform.CmsCustomFormMetaModelType;
import com.chestnut.customform.domain.CmsCustomForm;
import com.chestnut.customform.exception.CustomFormErrorCode;
import com.chestnut.customform.rule.ICustomFormLimitRule;
import com.chestnut.customform.service.ICustomFormApiService;
import com.chestnut.customform.service.ICustomFormService;
import com.chestnut.system.SysConstants;
import com.chestnut.xmodel.service.IModelDataService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
/**
* CustomFormApiServiceImpl
*
* @author 兮玥
* @email 190785909@qq.com
*/
@Service
@RequiredArgsConstructor
public class CustomFormApiServiceImpl implements ICustomFormApiService {
private final ICustomFormService customFormService;
private final IModelDataService modelDataService;
private final IResourceService resourceService;
private final ISiteService siteService;
private final RedissonClient redissonClient;
@Override
@Transactional(rollbackFor = Throwable.class)
public void submit(CmsCustomForm form, Map<String, Object> formData) throws IOException {
String uuid = MapUtils.getString(formData, CmsCustomFormMetaModelType.FIELD_UUID.getCode());
RLock lock = redissonClient.getLock("CustomFormSubmit-" + uuid);
lock.lock();
try {
// 唯一提交限制校验
ICustomFormLimitRule limitRule = customFormService.getLimitRule(form.getRuleLimit());
if (Objects.nonNull(limitRule)) {
if (!limitRule.check(form, formData)) {
throw CustomFormErrorCode.CANNOT_RESUBMIT.exception();
}
}
// base64图片保存到资源库
CmsSite site = siteService.getSite(form.getSiteId());
for (Map.Entry<String, Object> entry : formData.entrySet()) {
if (ImageUtils.isBase64Image(entry.getValue())) {
CmsResource resource = resourceService.addBase64Image(site, SysConstants.SYS_OPERATOR, entry.getValue().toString());
entry.setValue(InternalDataType_Resource.getInternalUrl(resource));
}
}
// 保存数据
this.modelDataService.saveModelData(form.getModelId(), formData);
} finally {
lock.unlock();
}
}
}

View File

@ -22,11 +22,9 @@ import com.chestnut.common.staticize.StaticizeService;
import com.chestnut.common.staticize.core.TemplateContext;
import com.chestnut.common.utils.Assert;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.common.utils.ReflectASMUtils;
import com.chestnut.common.utils.StringUtils;
import com.chestnut.contentcore.domain.CmsPublishPipe;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.fixed.config.SiteApiUrl;
import com.chestnut.contentcore.service.IPublishPipeService;
import com.chestnut.contentcore.service.ISiteService;
import com.chestnut.contentcore.service.ITemplateService;
@ -42,6 +40,7 @@ import com.chestnut.customform.domain.dto.CustomFormEditDTO;
import com.chestnut.customform.fixed.dict.CustomFormStatus;
import com.chestnut.customform.mapper.CustomFormMapper;
import com.chestnut.customform.publishpipe.PublishPipeProp_CustomFormTemplate;
import com.chestnut.customform.rule.ICustomFormLimitRule;
import com.chestnut.customform.service.ICustomFormService;
import com.chestnut.xmodel.domain.XModel;
import com.chestnut.xmodel.service.IModelService;
@ -74,6 +73,13 @@ public class CustomFormServiceImpl extends ServiceImpl<CustomFormMapper, CmsCust
private final StaticizeService staticizeService;
private final Map<String, ICustomFormLimitRule> limitRuleMap;
@Override
public ICustomFormLimitRule getLimitRule(String ruleId) {
return this.limitRuleMap.get(ICustomFormLimitRule.BEAN_PREFIX + ruleId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void addCustomForm(CustomFormAddDTO dto) {

View File

@ -14,4 +14,15 @@ DICT.CustomFormStatus.20=已下线
DICT.CustomFormRule=自定义表单限制状态
DICT.CustomFormRule.0=无限制
DICT.CustomFormRule.1=IP
DICT.CustomFormRule.2=浏览器指纹
DICT.CustomFormRule.2=浏览器指纹
MONITORED.CACHE.CUSTOM_FORM_CAPTCHA=自定义表单验证码
CUSTOM_FORM.LIMIT_RULE.UNLIMITED=无限制
CUSTOM_FORM.LIMIT_RULE.IP=IP
CUSTOM_FORM.LIMIT_RULE.UUID=浏览器指纹
ERR.CUSTOM_FORM.FORM_NOT_FOUND=表单数据不存在
ERR.CUSTOM_FORM.MISSING_UUID=UUID参数不能为空
ERR.CUSTOM_FORM.NOT_LOGIN=请先登录
ERR.CUSTOM_FORM.CANNOT_RESUBMIT=请勿重复提交

View File

@ -14,4 +14,15 @@ DICT.CustomFormStatus.20=Offline
DICT.CustomFormRule=Custom Form Rule
DICT.CustomFormRule.0=Unlimited
DICT.CustomFormRule.1=IP
DICT.CustomFormRule.2=Browser Fingerprint
DICT.CustomFormRule.2=Browser Fingerprint
MONITORED.CACHE.CUSTOM_FORM_CAPTCHA=Custom form captcha
CUSTOM_FORM.LIMIT_RULE.UNLIMITED=Unlimited
CUSTOM_FORM.LIMIT_RULE.IP=IP
CUSTOM_FORM.LIMIT_RULE.UUID=Finger Print
ERR.CUSTOM_FORM.FORM_NOT_FOUND=Form not found.
ERR.CUSTOM_FORM.MISSING_UUID=Missing uuid.
ERR.CUSTOM_FORM.NOT_LOGIN=Please login first.
ERR.CUSTOM_FORM.CANNOT_RESUBMIT=Please do not resubmit

View File

@ -15,3 +15,14 @@ DICT.CustomFormRule=自定義表單限制狀態
DICT.CustomFormRule.0=無限制
DICT.CustomFormRule.1=IP
DICT.CustomFormRule.2=瀏覽器指紋
MONITORED.CACHE.CUSTOM_FORM_CAPTCHA=自定義表單驗證碼
CUSTOM_FORM.LIMIT_RULE.UNLIMITED=無限制
CUSTOM_FORM.LIMIT_RULE.IP=IP
CUSTOM_FORM.LIMIT_RULE.UUID=瀏覽器指紋
ERR.CUSTOM_FORM.FORM_NOT_FOUND=表單數據不存在
ERR.CUSTOM_FORM.MISSING_UUID=UUID參數不能為空
ERR.CUSTOM_FORM.NOT_LOGIN=請先登錄
ERR.CUSTOM_FORM.CANNOT_RESUBMIT=請勿重複提交

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,15 +23,15 @@ import com.chestnut.stat.core.IStatType;
import com.chestnut.stat.core.StatMenu;
@Component
public class SiteStatType implements IStatType {
public class SiteBaiduStatType implements IStatType {
private final static List<StatMenu> STAT_MENU = List.of(
new StatMenu("CmsSiteStat", "", "{STAT.MENU.CmsSiteStat}", 1),
new StatMenu("BdSiteTrendOverview", "CmsSiteStat", "{STAT.MENU.BdSiteTrendOverview}", 1),
new StatMenu("BdSiteTimeTrend", "CmsSiteStat", "{STAT.MENU.BdSiteTimeTrend}", 2),
new StatMenu("BdSiteVisitSource", "CmsSiteStat", "{STAT.MENU.BdSiteVisitSource}", 3),
new StatMenu("BdSiteEngineSource", "CmsSiteStat", "{STAT.MENU.BdSiteEngineSource}", 4),
new StatMenu("BdSiteSearchWordSource", "CmsSiteStat", "{STAT.MENU.BdSiteSearchWordSource}", 5),
new StatMenu("CmsSiteBaiduStat", "", "{STAT.MENU.CmsSiteStat}", 1),
new StatMenu("BdSiteTrendOverview", "CmsSiteBaiduStat", "{STAT.MENU.BdSiteTrendOverview}", 1),
new StatMenu("BdSiteTimeTrend", "CmsSiteBaiduStat", "{STAT.MENU.BdSiteTimeTrend}", 2),
new StatMenu("BdSiteVisitSource", "CmsSiteBaiduStat", "{STAT.MENU.BdSiteVisitSource}", 3),
new StatMenu("BdSiteEngineSource", "CmsSiteBaiduStat", "{STAT.MENU.BdSiteEngineSource}", 4),
new StatMenu("BdSiteSearchWordSource", "CmsSiteBaiduStat", "{STAT.MENU.BdSiteSearchWordSource}", 5),
new StatMenu("CmsContentStat", "", "{STAT.MENU.CmsContentStat}", 2),
new StatMenu("ContentDynamicStat", "CmsContentStat", "{STAT.MENU.ContentDynamicStat}", 1),
new StatMenu("ContentStatByCatalog", "CmsContentStat", "{STAT.MENU.ContentStatByCatalog}", 2),

View File

@ -19,7 +19,6 @@ import com.chestnut.cms.stat.baidu.BaiduTjMetrics;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.text.StringEscapeUtils;
import java.util.ArrayList;
import java.util.List;

View File

@ -19,7 +19,6 @@ import com.chestnut.cms.stat.baidu.BaiduTjMetrics;
import com.chestnut.common.utils.HttpUtils;
import com.chestnut.common.utils.JacksonUtils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

View File

@ -17,7 +17,6 @@ package com.chestnut.cms.stat.baidu.api;
import com.chestnut.common.utils.HttpUtils;
import com.chestnut.common.utils.JacksonUtils;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import java.net.URI;

View File

@ -99,6 +99,41 @@ public class CmsSiteVisitLog implements Serializable {
*/
private String locale;
/**
* 屏幕分辨率
*/
private Integer screenWidth;
/**
* 屏幕分辨率
*/
private Integer screenHeight;
/**
* 色彩深度
*/
private Integer colorDepth;
/**
* 是否允许cookie
*/
private Integer cookieEnabled;
/**
* 是否允许java
*/
private Integer javaEnabled;
/**
* 访问时长单位
*/
private Integer visitTime;
/**
* 访客唯一标识
*/
private String uuid;
/**
* 发生时间
*/

View File

@ -20,7 +20,6 @@ import com.chestnut.contentcore.domain.CmsCatalog;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Objects;
/**

View File

@ -17,12 +17,10 @@ package com.chestnut.cms.stat.service.impl;
import com.chestnut.cms.stat.baidu.BaiduTongjiConfig;
import com.chestnut.cms.stat.baidu.BaiduTongjiUtils;
import com.chestnut.cms.stat.baidu.api.SiteListResponse;
import com.chestnut.cms.stat.exception.CmsStatErrorCode;
import com.chestnut.cms.stat.properties.BaiduTjAccessTokenProperty;
import com.chestnut.cms.stat.properties.BaiduTjRefreshTokenProperty;
import com.chestnut.cms.stat.service.ICmsStatService;
import com.chestnut.common.domain.R;
import com.chestnut.common.utils.Assert;
import com.chestnut.contentcore.domain.CmsSite;
import com.chestnut.contentcore.service.ISiteService;

View File

@ -19,9 +19,11 @@ import com.chestnut.common.staticize.FreeMarkerUtils;
import com.chestnut.common.staticize.tag.AbstractTag;
import com.chestnut.common.utils.IdUtils;
import com.chestnut.contentcore.util.TemplateUtils;
import com.ulisesbocchio.jasyptspringboot.annotation.ConditionalOnMissingBean;
import freemarker.core.Environment;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.stereotype.Component;
import java.io.IOException;
@ -29,6 +31,7 @@ import java.util.Map;
import java.util.Objects;
@Component
@ConditionalOnMissingClass("com.chestnut.cms.statpro.template.tag.CmsStatTag")
public class CmsStatTag extends AbstractTag {
public final static String TAG_NAME = "cms_stat";

View File

@ -8,7 +8,7 @@ VALIDATOR.CMS.STAT.END_DATE_NOT_NULL=结束时间不能为空
VALIDATOR.CMS.STAT.TIME_GRAN_NOT_NULL=时间粒度不能为空
# 统计菜单
STAT.MENU.CmsSiteStat=网站访问统计
STAT.MENU.CmsSiteStat=百度访问统计
STAT.MENU.BdSiteTrendOverview=网站概况
STAT.MENU.BdSiteTimeTrend=趋势分析
STAT.MENU.BdSiteVisitSource=访问来源

View File

@ -8,7 +8,7 @@ VALID.CMS.STAT.END_DATE_NOT_NULL=End date cannot be null.
VALID.CMS.STAT.TIME_GRAN_NOT_NULL=Time granularity cannot be null.
# 统计菜单
STAT.MENU.CmsSiteStat=Site Statistics
STAT.MENU.CmsSiteStat=Baidu Statistics
STAT.MENU.BdSiteTrendOverview=Overview
STAT.MENU.BdSiteTimeTrend=Trend Analysis
STAT.MENU.BdSiteVisitSource=Visit Source

View File

@ -8,7 +8,7 @@ VALIDATOR.CMS.STAT.END_DATE_NOT_NULL=結束時間不能為空
VALIDATOR.CMS.STAT.TIME_GRAN_NOT_NULL=時間粒度不能為空
# 統計菜單
STAT.MENU.CmsSiteStat=網站訪問統計
STAT.MENU.CmsSiteStat=百度訪問統計
STAT.MENU.BdSiteTrendOverview=網站概況
STAT.MENU.BdSiteTimeTrend=趨勢分析
STAT.MENU.BdSiteVisitSource=訪問來源

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.chestnut</groupId>
<artifactId>chestnut</artifactId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>chestnut-common</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -90,10 +90,20 @@ public enum CommonErrorCode implements ErrorCode {
/**
* 上传文件不能为空
*/
UPLOAD_FILE_EMPTY;
UPLOAD_FILE_EMPTY,
/**
* 验证码错误
*/
INVALID_CAPTCHA,
/**
* 验证码过期
*/
CAPTCHA_EXPIRED;
@Override
public String value() {
return "{ERRCODE.COMMON." + this.name() + "}";
return "{ERR.COMMON." + this.name() + "}";
}
}

View File

@ -129,4 +129,20 @@ public class I18nUtils {
}
return PlaceholderHelper.replacePlaceholders(str, langKey -> messageSource.getMessage(langKey, args, locale));
}
public static boolean isLanguageTag(String s) {
int len = s.length();
return (len >= 2) && (len <= 8) && isAlphaString(s);
}
static boolean isAlphaString(String s) {
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (c != '-' && !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) {
return false;
}
}
return true;
}
}

View File

@ -51,6 +51,28 @@ public class ArrayUtils {
return indexOf(searchStr, arr) > -1;
}
public static int indexOf(Integer searchStr, Integer... arr) {
if (Objects.isNull(arr) || arr.length == 0) {
return -1;
}
for (int i = 0; i < arr.length; i++) {
if (searchStr == null) {
if (arr[i] == null) {
return i;
}
} else {
if (searchStr.equals(arr[i])) {
return i;
}
}
}
return -1;
}
public static boolean contains(Integer searchStr, Integer... arr) {
return indexOf(searchStr, arr) > -1;
}
/**
* 查找指定列表中符合条件的第一个元素并返回如果没有符合条件的元素直接抛出异常
*/
@ -142,4 +164,30 @@ public class ArrayUtils {
public static <T> boolean isNotEmpty(T[] arr) {
return !isEmpty(arr);
}
public static <T> long sumLongValue(List<T> list, Function<T, Long> getter) {
if (Objects.isNull(list)) {
return 0;
}
long sum = 0L;
for (T t : list) {
Long v = getter.apply(t);
if (Objects.nonNull(v)) {
sum += v;
}
}
return sum;
}
public static <T> int sumIntValue(List<T> list, Function<T, Integer> getter) {
if (Objects.isNull(list)) {
return 0;
}
int sum = 0;
for (T t : list) {
Integer v = getter.apply(t);
sum += Objects.requireNonNullElse(v, 0);
}
return sum;
}
}

View File

@ -51,7 +51,7 @@ public class HttpUtils {
return USER_AGENTS[index];
}
private static SSLContext trustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException {
public static SSLContext trustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
@ -94,22 +94,25 @@ public class HttpUtils {
* @param uri
* @return
*/
public static String get(URI uri) {
public static String get(URI uri, Map<String, String> headers) {
try {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.followRedirects(Redirect.ALWAYS)
.build();
HttpRequest httpRequest = HttpRequest.newBuilder(uri)
.header("User-Agent", USER_AGENTS[0])
.GET()
.build();
HttpRequest.Builder builder = HttpRequest.newBuilder(uri);
headers.forEach(builder::setHeader);
HttpRequest httpRequest = builder.GET().build();
return httpClient.send(httpRequest, BodyHandlers.ofString()).body();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
public static String get(URI uri) {
return get(uri, Map.of("User-Agent", USER_AGENTS[0]));
}
public static String post(URI uri, String body, Map<String, String> headers) throws Exception {
if (Objects.isNull(body)) {
body = StringUtils.EMPTY;

View File

@ -15,14 +15,14 @@
*/
package com.chestnut.common.utils;
import java.io.IOException;
import java.io.InputStream;
import org.lionsoul.ip2region.xdb.Searcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
import java.io.InputStream;
/**
* IP2Region工具类内存查询
*/
@ -40,24 +40,38 @@ public class IP2RegionUtils {
cBuff = FileCopyUtils.copyToByteArray(is);
searcher = Searcher.newWithBuffer(cBuff);
} catch (IOException e1) {
logger.error("Load ip2region.xdb failed: {}", e1);
logger.error("Load ip2region.xdb failed.", e1);
}
}
/**
* ip转区域格式
*
* @param ip IPv4/IPv6
* @return 国家|0|省份|城市|运营商
*/
public static String ip2Region(String ip) {
try {
if (ServletUtils.isUnknown(ip)) {
return ServletUtils.UNKNOWN;
}
if (ServletUtils.internalIp(ip)) {
return "内网";
if (ServletUtils.isInternalIp(ip)) {
return ServletUtils.INTERNAL_IP;
}
return searcher.search(ip);
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.error("Ip2region failed: {}", e);
logger.error("Ip2region failed: {}", ip, e);
}
return ServletUtils.UNKNOWN;
}
}
public static void close() {
try {
searcher.close();
} catch (IOException e) {
logger.error("Close ip2region searcher failed.", e);
}
}
}

View File

@ -84,13 +84,15 @@ public class ServletUtils {
*/
public static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
public static final String UNKNOWN = "unknown";
public static final String UNKNOWN = "0";
public static final String INTERNAL_IP = "InternalIP";
public static boolean isHttpUrl(String url) {
return StringUtils.startsWithIgnoreCase(url, HTTP) || StringUtils.startsWithIgnoreCase(url, HTTPS);
}
public static String getAcceptLanaguage(HttpServletRequest request) {
public static String getAcceptLanguage(HttpServletRequest request) {
return getHeader(request, HEADER_ACCEPT_LANGUAGE);
}
@ -560,9 +562,9 @@ public class ServletUtils {
* @param ip IP地址
* @return 结果
*/
public static boolean internalIp(String ip) {
public static boolean isInternalIp(String ip) {
byte[] addr = textToNumericFormatV4(ip);
return internalIp(addr) || "127.0.0.1".equals(ip);
return isInternalIp(addr) || "127.0.0.1".equals(ip);
}
/**
@ -571,7 +573,7 @@ public class ServletUtils {
* @param addr byte地址
* @return 结果
*/
private static boolean internalIp(byte[] addr) {
private static boolean isInternalIp(byte[] addr) {
if (Objects.isNull(addr) || addr.length < 2) {
return true;
}

View File

@ -43,10 +43,26 @@ public class TimeUtils {
return LocalDateTime.ofInstant(instant, ZONE_OFFSET);
}
public static LocalDateTime epochSecondToLocalDateTime(long epochSecond) {
return toLocalDateTime(Instant.ofEpochSecond(epochSecond));
}
public static long epochSecond(LocalDateTime localDateTime) {
return localDateTime.toEpochSecond(ZONE_OFFSET);
}
public static String epochSecondFormat(long epochSecond, DateTimeFormatter formatter) {
return formatter.format(toLocalDateTime(Instant.ofEpochSecond(epochSecond)));
}
public static String localDateTimeFormat(long epochMilli) {
return YYYY_MM_DD_HH_MM_SS.format(toLocalDateTime(Instant.ofEpochMilli(epochMilli)));
}
public static String localDateTimeFormat(long epochMilli, DateTimeFormatter formatter) {
return formatter.format(toLocalDateTime(Instant.ofEpochMilli(epochMilli)));
}
public static String localDateFormat(long epochMilli) {
return YYYY_MM_DD.format(toLocalDateTime(Instant.ofEpochMilli(epochMilli)));
}

View File

@ -153,4 +153,19 @@ public class ImageUtils {
}
throw new ImageException("Unsupported image format: " + imageFormat);
}
public static boolean isBase64Image(String base64) {
if (!StringUtils.startsWithIgnoreCase(base64, "data:image/")) {
return false;
}
String encode = StringUtils.substringAfter(StringUtils.substringBefore(base64, ","), ";");
return "base64".equalsIgnoreCase(encode);
}
public static boolean isBase64Image(Object v) {
if (Objects.isNull(v)) {
return false;
}
return isBase64Image(v.toString());
}
}

View File

@ -1,20 +1,22 @@
#错误消息
ERRCODE.COMMON.NOT_NULL=参数[{0}]不能为NULL
ERRCODE.COMMON.NOT_EMPTY=参数[{0}]不能为空
ERRCODE.COMMON.DATA_NOT_FOUND=数据不存在
ERRCODE.COMMON.DATA_NOT_FOUND_BY_ID=数据不存在:{0}={1}
ERRCODE.COMMON.DATA_CONFLICT=数据[{0}]冲突
ERRCODE.COMMON.INVALID_REQUEST_ARG=请求参数[{0}]不符合校验规则
ERRCODE.COMMON.INVALID_REQUEST_METHOD=请求方法错误
ERRCODE.COMMON.SYSTEM_ERROR=系统内部错误
ERRCODE.COMMON.UNKNOWN_ERROR=未知错误
ERRCODE.COMMON.DATABASE_FAIL=数据库操作失败
ERRCODE.COMMON.REQUEST_FAILED=Http(s)请求失败:{0}
ERRCODE.COMMON.FIXED_DICT=系统固定字典数据不允许删除或修改类型
ERRCODE.COMMON.FIXED_DICT_NOT_ALLOW_ADD=此系统固定字典类型不能添加子项
ERRCODE.COMMON.FIXED_CONFIG_DEL=系统固定配置参数[{0}]不能删除
ERRCODE.COMMON.FIXED_CONFIG_UPDATE=系统固定配置参数[{0}]不能修改键名
ERRCODE.COMMON.ASYNC_TASK_RUNNING=任务“{0}”正在运行中
ERRCODE.COMMON.UPLOAD_FILE_EMPTY=上传文件不能为空
ERR.COMMON.NOT_NULL=参数[{0}]不能为NULL
ERR.COMMON.NOT_EMPTY=参数[{0}]不能为空
ERR.COMMON.DATA_NOT_FOUND=数据不存在
ERR.COMMON.DATA_NOT_FOUND_BY_ID=数据不存在:{0}={1}
ERR.COMMON.DATA_CONFLICT=数据[{0}]冲突
ERR.COMMON.INVALID_REQUEST_ARG=请求参数[{0}]不符合校验规则
ERR.COMMON.INVALID_REQUEST_METHOD=请求方法错误
ERR.COMMON.SYSTEM_ERROR=系统内部错误
ERR.COMMON.UNKNOWN_ERROR=未知错误
ERR.COMMON.DATABASE_FAIL=数据库操作失败
ERR.COMMON.REQUEST_FAILED=Http(s)请求失败:{0}
ERR.COMMON.FIXED_DICT=系统固定字典数据不允许删除或修改类型
ERR.COMMON.FIXED_DICT_NOT_ALLOW_ADD=此系统固定字典类型不能添加子项
ERR.COMMON.FIXED_CONFIG_DEL=系统固定配置参数[{0}]不能删除
ERR.COMMON.FIXED_CONFIG_UPDATE=系统固定配置参数[{0}]不能修改键名
ERR.COMMON.ASYNC_TASK_RUNNING=任务“{0}”正在运行中
ERR.COMMON.UPLOAD_FILE_EMPTY=上传文件不能为空
ERR.COMMON.INVALID_CAPTCHA=验证码错误
ERR.COMMON.CAPTCHA_EXPIRED=验证码已过期
AsyncTask.SuccessMsg=任务执行成功

View File

@ -1,20 +1,22 @@
#错误消息
ERRCODE.COMMON.NOT_NULL=Parameter "{0}" cannot be null.
ERRCODE.COMMON.NOT_EMPTY=Parameter "{0}" cannot be empty.
ERRCODE.COMMON.DATA_NOT_FOUND=Data not found.
ERRCODE.COMMON.DATA_NOT_FOUND_BY_ID=Data not found: {0}={1}
ERRCODE.COMMON.DATA_CONFLICT=Data conflict: [{0}]
ERRCODE.COMMON.INVALID_REQUEST_ARG=Invalid parameter: {0}
ERRCODE.COMMON.INVALID_REQUEST_METHOD=Invalid request method.
ERRCODE.COMMON.SYSTEM_ERROR=System error.
ERRCODE.COMMON.UNKNOWN_ERROR=Unknown error.
ERRCODE.COMMON.DATABASE_FAIL=Database error.
ERRCODE.COMMON.REQUEST_FAILED=Http(s) request failed: {0}
ERRCODE.COMMON.FIXED_DICT=The fixed dict cannot be delete or modify type.
ERRCODE.COMMON.FIXED_DICT_NOT_ALLOW_ADD=The fixed dict not allow to add data items.
ERRCODE.COMMON.FIXED_CONFIG_DEL=The fixed config "{0}" cannot be delete.
ERRCODE.COMMON.FIXED_CONFIG_UPDATE=The fixed config "{0}" cannot modify key.
ERRCODE.COMMON.ASYNC_TASK_RUNNING=The task "{0}" is running.
ERRCODE.COMMON.UPLOAD_FILE_EMPTY=Upload file cannot be empty.
ERR.COMMON.NOT_NULL=Parameter "{0}" cannot be null.
ERR.COMMON.NOT_EMPTY=Parameter "{0}" cannot be empty.
ERR.COMMON.DATA_NOT_FOUND=Data not found.
ERR.COMMON.DATA_NOT_FOUND_BY_ID=Data not found: {0}={1}
ERR.COMMON.DATA_CONFLICT=Data conflict: [{0}]
ERR.COMMON.INVALID_REQUEST_ARG=Invalid parameter: {0}
ERR.COMMON.INVALID_REQUEST_METHOD=Invalid request method.
ERR.COMMON.SYSTEM_ERROR=System error.
ERR.COMMON.UNKNOWN_ERROR=Unknown error.
ERR.COMMON.DATABASE_FAIL=Database error.
ERR.COMMON.REQUEST_FAILED=Http(s) request failed: {0}
ERR.COMMON.FIXED_DICT=The fixed dict cannot be delete or modify type.
ERR.COMMON.FIXED_DICT_NOT_ALLOW_ADD=The fixed dict not allow to add data items.
ERR.COMMON.FIXED_CONFIG_DEL=The fixed config "{0}" cannot be delete.
ERR.COMMON.FIXED_CONFIG_UPDATE=The fixed config "{0}" cannot modify key.
ERR.COMMON.ASYNC_TASK_RUNNING=The task "{0}" is running.
ERR.COMMON.UPLOAD_FILE_EMPTY=Upload file cannot be empty.
ERR.COMMON.INVALID_CAPTCHA=Invalid captcha.
ERR.COMMON.CAPTCHA_EXPIRED=The captcha is expired.
AsyncTask.SuccessMsg=Task completed.

View File

@ -1,20 +1,22 @@
#錯誤消息
ERRCODE.COMMON.NOT_NULL=參數[{0}]不能為NULL
ERRCODE.COMMON.NOT_EMPTY=參數[{0}]不能為空
ERRCODE.COMMON.DATA_NOT_FOUND=數據不存在
ERRCODE.COMMON.DATA_NOT_FOUND_BY_ID=數據不存在:{0}={1}
ERRCODE.COMMON.DATA_CONFLICT=數據[{0}]衝突
ERRCODE.COMMON.INVALID_REQUEST_ARG=請求參數[{0}]不符合校驗規則
ERRCODE.COMMON.INVALID_REQUEST_METHOD=請求方法錯誤
ERRCODE.COMMON.SYSTEM_ERROR=系統內部錯誤
ERRCODE.COMMON.UNKNOWN_ERROR=未知錯誤
ERRCODE.COMMON.DATABASE_FAIL=資料庫操作失敗
ERRCODE.COMMON.REQUEST_FAILED=Http(s)請求失敗:{0}
ERRCODE.COMMON.FIXED_DICT=系統固定字典數據不允許刪除或修改類型
ERRCODE.COMMON.FIXED_DICT_NOT_ALLOW_ADD=此系統固定字典類型不能添加子項
ERRCODE.COMMON.FIXED_CONFIG_DEL=系統固定配置參數[{0}]不能刪除
ERRCODE.COMMON.FIXED_CONFIG_UPDATE=系統固定配置參數[{0}]不能修改鍵名
ERRCODE.COMMON.ASYNC_TASK_RUNNING=任務“{0}”正在運行中
ERRCODE.COMMON.UPLOAD_FILE_EMPTY=上傳文件不能為空
ERR.COMMON.NOT_NULL=參數[{0}]不能為NULL
ERR.COMMON.NOT_EMPTY=參數[{0}]不能為空
ERR.COMMON.DATA_NOT_FOUND=數據不存在
ERR.COMMON.DATA_NOT_FOUND_BY_ID=數據不存在:{0}={1}
ERR.COMMON.DATA_CONFLICT=數據[{0}]衝突
ERR.COMMON.INVALID_REQUEST_ARG=請求參數[{0}]不符合校驗規則
ERR.COMMON.INVALID_REQUEST_METHOD=請求方法錯誤
ERR.COMMON.SYSTEM_ERROR=系統內部錯誤
ERR.COMMON.UNKNOWN_ERROR=未知錯誤
ERR.COMMON.DATABASE_FAIL=資料庫操作失敗
ERR.COMMON.REQUEST_FAILED=Http(s)請求失敗:{0}
ERR.COMMON.FIXED_DICT=系統固定字典數據不允許刪除或修改類型
ERR.COMMON.FIXED_DICT_NOT_ALLOW_ADD=此系統固定字典類型不能添加子項
ERR.COMMON.FIXED_CONFIG_DEL=系統固定配置參數[{0}]不能刪除
ERR.COMMON.FIXED_CONFIG_UPDATE=系統固定配置參數[{0}]不能修改鍵名
ERR.COMMON.ASYNC_TASK_RUNNING=任務“{0}”正在運行中
ERR.COMMON.UPLOAD_FILE_EMPTY=上傳文件不能為空
ERR.COMMON.INVALID_CAPTCHA=驗證碼錯誤
ERR.COMMON.CAPTCHA_EXPIRED=驗證碼已過期
AsyncTask.SuccessMsg=任務執行成功

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>chestnut-common</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>chestnut-common</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>chestnut-common</artifactId>
<groupId>com.chestnut</groupId>
<version>1.5.4</version>
<version>1.5.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

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