diff --git a/README.md b/README.md
index a7c2028..14db04a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
-### 兄 Dei,有用能不能给个Star呀
-### 兄 Dei,有用能不能给个Star呀
-### 兄 Dei,有用能不能给个Star呀
+- 想看哪个模块就打开那个模块就行,因为没有使用pom管理
+
### 项目目录介绍
- [hello word](https://rstyro.github.io/blog/2017/07/25/Spring%20Boot%20%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E5%88%9D%E8%AF%86%E4%B9%8B%E5%85%A5%E9%97%A8%E7%AF%87/) *最简单的版本*
- [Springboot-web](https://rstyro.github.io/blog/2017/07/27/Spring%20Boot%20(%E4%BA%8C)%EF%BC%9AWeb%20%E5%BC%80%E5%8F%91%E7%AF%87/) *web 版本的*
@@ -23,6 +22,21 @@
- [SpringBoot2-Redisson](https://rstyro.github.io/blog/2019/06/25/SpringBoot2%E4%B8%8ERedisson%E7%A2%B0%E6%92%9E/) *SpringBoot 与Redisson 整合之分布式锁与发布订阅*
- [SpringBoot2-RedisCacheManager](https://rstyro.github.io/blog/2019/04/16/SpringBoot%E4%B8%8ERedisCacheManager%E6%95%B4%E5%90%88/) *SpringBoot 与RedisCacheManager整合*
- [Springboot2-api-encrypt](https://rstyro.github.io/blog/2020/10/22/Springboot2接口加解密全过程详解(含前端代码)/) *SpringBoot接口RSA+AES加解密(含前端代码)*
-- [springboot-elk](https://rstyro.gitee.io/blog/2021/04/28/Centos7搭建ELK与Springboot整合/) *SpringBoot与ELK整合demo)*
-
+- [Springboot-elk](https://rstyro.gitee.io/blog/2021/04/28/Centos7搭建ELK与Springboot整合/) *SpringBoot与ELK整合demo)*
+- [Springboot-sqlite](https://github.com/rstyro/spring-boot/tree/master/springboot-sqlite/) *SpringBoot与SQLite整合demo)*
+- [Springboot-es](https://github.com/rstyro/spring-boot/tree/master/springboot-es/) *SpringBoot与ES 7版本以上整合demo)*
+- [Springboot-neo4j-multiple-sdn](https://github.com/rstyro/spring-boot/tree/master/springboot-neo4j-multiple-sdn/) *springboot与neo4j多数据源Demo*
+- [Springboot-mqtt](https://github.com/rstyro/spring-boot/tree/master/springboot-mqtt/) *Springboot集成mqtt支持多客户端*
+- [Springboot-camunda](https://github.com/rstyro/spring-boot/tree/master/springboot-camunda/) *Springboot集成camunda工作流*
+- [Springboot-2FA](https://github.com/rstyro/Springboot/tree/master/springboot-2FA) *Springboot集成2FA二步验证*
+- [Springboot-shedlock](https://github.com/rstyro/Springboot/tree/master/springboot-shedlock) *Springboot集群部署之定时任务分布式锁*
+- [Springboot-Jasypt](https://github.com/rstyro/Springboot/tree/master/springboot-jasypt) *Springboot集成Jasypt,配置加密*
- ...持续更新
+
+
+
+
+
+## Star History
+
+[](https://star-history.com/#rstyro/Springboot&Date)
diff --git a/SpringBoot-limit/pom.xml b/SpringBoot-limit/pom.xml
index 6bab3b1..3ce52f5 100644
--- a/SpringBoot-limit/pom.xml
+++ b/SpringBoot-limit/pom.xml
@@ -16,6 +16,7 @@
1.8
+ 3.22.0
@@ -28,7 +29,11 @@
org.projectlombok
lombok
- 1.18.6
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
@@ -41,18 +46,18 @@
commons-pool2
-
-
- com.alibaba
- fastjson
- 1.2.56
-
-
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.redisson
+ redisson
+ ${redisson.version}
+
+
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/LeakyBucketLimit.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/LeakyBucketLimit.java
new file mode 100644
index 0000000..58f009c
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/LeakyBucketLimit.java
@@ -0,0 +1,28 @@
+package top.lrshuai.limit.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 漏桶限流注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface LeakyBucketLimit {
+
+ /**
+ * 限流key,支持SpEL表达式
+ */
+ String key() default "";
+
+ /**
+ * 桶的容量(最大请求数)
+ */
+ int capacity() default 100;
+
+ /**
+ * 流出速率(每秒处理多少个请求)
+ */
+ int rate() default 10;
+
+}
\ No newline at end of file
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RedissonRateLimit.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RedissonRateLimit.java
new file mode 100644
index 0000000..493581b
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RedissonRateLimit.java
@@ -0,0 +1,32 @@
+package top.lrshuai.limit.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * redisson限流注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RedissonRateLimit {
+
+ /**
+ * 限流key,支持SpEL表达式
+ */
+ String key() default "";
+
+ /**
+ * 令牌生成速率 (每秒生成的令牌数)
+ */
+ long rate() default 10;
+
+ /**
+ * 每次请求消耗的令牌数
+ */
+ int tokens() default 1;
+
+ /**
+ * 限流时的提示信息
+ */
+ String message() default "请求过于频繁,请稍后再试";
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RequestLimit.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RequestLimit.java
index 99a52d5..b0daf10 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RequestLimit.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/RequestLimit.java
@@ -3,7 +3,7 @@
import java.lang.annotation.*;
/**
- * 请求限制的自定义注解
+ * 请求限制的自定义注解: 固定计数器限流
*
* @Target 注解可修饰的对象范围,ElementType.METHOD 作用于方法,ElementType.TYPE 作用于类
* (ElementType)取值有:
@@ -32,7 +32,17 @@
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
- // 在 second 秒内,最大只能请求 maxCount 次
+ /**
+ * 资源key,用于区分不同的接口,默认为方法名
+ */
+ String key() default "";
+
+ /**
+ * 在 second 秒内,最大只能请求 maxCount 次
+ */
int second() default 1;
+ /**
+ * 在时间窗口内允许访问的次数
+ */
int maxCount() default 1;
}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/SlidingWindowLimit.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/SlidingWindowLimit.java
new file mode 100644
index 0000000..e7ca35b
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/SlidingWindowLimit.java
@@ -0,0 +1,28 @@
+package top.lrshuai.limit.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 滑动时间窗口计数器限流注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SlidingWindowLimit {
+
+ /**
+ * 限流key,支持SpEL表达式
+ */
+ String key() default "";
+
+ /**
+ * 时间窗口大小(秒)
+ */
+ int window() default 60;
+
+ /**
+ * 时间窗口内允许的最大请求数
+ */
+ int maxCount() default 100;
+
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/TokenBucketRateLimit.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/TokenBucketRateLimit.java
new file mode 100644
index 0000000..3db63b8
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/annotation/TokenBucketRateLimit.java
@@ -0,0 +1,37 @@
+package top.lrshuai.limit.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 令牌桶限流注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface TokenBucketRateLimit {
+
+ /**
+ * 限流key,支持SpEL表达式
+ */
+ String key() default "";
+
+ /**
+ * 令牌生成速率 (每秒生成的令牌数)
+ */
+ double rate() default 10.0;
+
+ /**
+ * 桶容量
+ */
+ int capacity() default 20;
+
+ /**
+ * 每次请求消耗的令牌数
+ */
+ int tokens() default 1;
+
+ /**
+ * 限流时的提示信息
+ */
+ String message() default "请求过于频繁,请稍后再试";
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/LeakyBucketLimitAspect.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/LeakyBucketLimitAspect.java
new file mode 100644
index 0000000..956d424
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/LeakyBucketLimitAspect.java
@@ -0,0 +1,72 @@
+package top.lrshuai.limit.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import top.lrshuai.limit.annotation.LeakyBucketLimit;
+import top.lrshuai.limit.common.ApiException;
+import top.lrshuai.limit.common.ApiResultEnum;
+import top.lrshuai.limit.service.LeakyBucketRateLimiter;
+import top.lrshuai.limit.util.AopUtil;
+
+import java.lang.reflect.Method;
+
+
+@Slf4j
+@Aspect
+@Component
+public class LeakyBucketLimitAspect {
+
+ @Autowired
+ private LeakyBucketRateLimiter rateLimiter;
+
+ @Around("@annotation(leakyBucketLimit)")
+ public Object around(ProceedingJoinPoint joinPoint, LeakyBucketLimit leakyBucketLimit) throws Throwable {
+ String key = buildRateLimitKey(joinPoint, leakyBucketLimit);
+ int capacity = leakyBucketLimit.capacity();
+ int rate = leakyBucketLimit.rate();
+
+ LeakyBucketRateLimiter.BucketStatus bucketStatus = rateLimiter.getBucketStatus(key);
+ log.debug("bucket status: key={}, water={},lastLeakTime={},ttl={}",key,
+ bucketStatus.getCurrentWater(), bucketStatus.getLastLeakTime(), bucketStatus.getTtl());
+ if (!rateLimiter.tryAcquire(key, capacity, rate, 1)) {
+ throw new ApiException(ApiResultEnum.REQUEST_LIMIT);
+ }
+
+ return joinPoint.proceed();
+ }
+
+ /**
+ * 构建限流key
+ */
+ private String buildRateLimitKey(ProceedingJoinPoint joinPoint, LeakyBucketLimit rateLimit) {
+ String key = rateLimit.key();
+
+ // 如果key为空,使用默认格式
+ if (key.isEmpty()) {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ String className = method.getDeclaringClass().getSimpleName();
+ String methodName = method.getName();
+
+ // 尝试获取用户信息
+ String userKey = getCurrentUserId();
+ return String.format("leaky_bucket:%s:%s:%s", className, methodName, userKey);
+ }
+
+ // 如果key包含SpEL表达式,进行解析
+ if (key.contains("#")) {
+ return AopUtil.parseSpel(key, joinPoint);
+ }
+ return key;
+ }
+
+ private String getCurrentUserId() {
+ // 实际项目中从安全上下文获取
+ return "user123";
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RateLimitAspect.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RateLimitAspect.java
new file mode 100644
index 0000000..b5b2f50
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RateLimitAspect.java
@@ -0,0 +1,97 @@
+package top.lrshuai.limit.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import top.lrshuai.limit.annotation.RequestLimit;
+import top.lrshuai.limit.common.ApiResultEnum;
+import top.lrshuai.limit.common.R;
+import top.lrshuai.limit.util.IpUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Slf4j
+public class RateLimitAspect {
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+
+ /**
+ * 环绕通知,切入所有被@RateLimit注解标记的方法
+ * "@annotation(requestLimit)" 只匹配方法上的
+ * "@within(requestLimit)" 匹配类上的
+ */
+ @Around("(@annotation(requestLimit) || @within(requestLimit))")
+ public Object around(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
+
+ // 获取HttpServletRequest对象,从而拿到客户端IP
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attributes == null) {
+ // 非Web请求,直接放行
+ return joinPoint.proceed();
+ }
+ HttpServletRequest request = attributes.getRequest();
+ // 获取客户端真实IP的方法
+ String ip = IpUtil.getClientIpAddress(request);
+
+ // 优先从方法上获取注解
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ requestLimit = getTagAnnotation(method, RequestLimit.class);
+
+
+ // 构建Redis的key,格式为:rate_limit:接口key:IP
+ String methodName = method.getName();
+ String key = requestLimit.key().isEmpty() ? methodName : requestLimit.key();
+ String redisKey = "rate_limit:" + key + ":" + ip;
+
+ // 操作Redis,进行计数和判断
+ ValueOperations valueOps = redisTemplate.opsForValue();
+ Integer currentCount = (Integer) valueOps.get(redisKey);
+
+ if (currentCount == null) {
+ // 第一次访问,设置key,初始值为1,并设置过期时间
+ valueOps.set(redisKey, 1, requestLimit.second(), TimeUnit.SECONDS);
+ } else if (currentCount < requestLimit.maxCount()) { + // 计数未达到阈值,计数器+1 (注意:这里Redis的过期时间保持不变) + valueOps.increment(redisKey); + } else { + // 计数已达到或超过阈值,抛出异常或返回错误信息 + log.warn("IP【{}】访问接口【{}】过于频繁,已被限流", ip, methodName); + return R.fail(ApiResultEnum.REQUEST_LIMIT); + } + + // 执行目标方法(即正常的业务逻辑) + return joinPoint.proceed(); + } + + /** + * 获取目标注解 + * 如果方法上有注解就返回方法上的注解配置,否则类上的 + * @param method + * @param annotationClass + * @param
+ * @return
+ */
+ public A getTagAnnotation(Method method, Class annotationClass) {
+ // 获取方法中是否包含注解
+ Annotation methodAnnotate = method.getAnnotation(annotationClass);
+ //获取 类中是否包含注解,也就是controller 是否有注解
+ Annotation classAnnotate = method.getDeclaringClass().getAnnotation(annotationClass);
+ return (A) (methodAnnotate!= null?methodAnnotate:classAnnotate);
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RedissonRateLimitAspect.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RedissonRateLimitAspect.java
new file mode 100644
index 0000000..e037045
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/RedissonRateLimitAspect.java
@@ -0,0 +1,63 @@
+package top.lrshuai.limit.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RRateLimiter;
+import org.redisson.api.RateIntervalUnit;
+import org.redisson.api.RateType;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import top.lrshuai.limit.annotation.RedissonRateLimit;
+import top.lrshuai.limit.common.R;
+import top.lrshuai.limit.util.AopUtil;
+
+import java.lang.reflect.Method;
+
+@Aspect
+@Component
+@Slf4j
+public class RedissonRateLimitAspect {
+
+ @Autowired
+ private RedissonClient redissonClient;
+
+ /**
+ * 切片-方法级别
+ */
+ @Around("@annotation(rateLimit)")
+ public Object around(ProceedingJoinPoint joinPoint, RedissonRateLimit rateLimit) throws Throwable {
+ String key = buildRateLimitKey(joinPoint, rateLimit);
+ RRateLimiter rRateLimiter = redissonClient.getRateLimiter(key);
+ // 初始化限流器
+ rRateLimiter.trySetRate(RateType.OVERALL, rateLimit.rate(), 1, RateIntervalUnit.SECONDS);
+ if (!rRateLimiter.tryAcquire(rateLimit.tokens())) {
+ log.warn("接口限流触发 - key: {}, 方法: {}", key, joinPoint.getSignature().getName());
+ return R.fail(rateLimit.message());
+ }
+ return joinPoint.proceed();
+ }
+
+ /**
+ * 构建限流key
+ */
+ private String buildRateLimitKey(ProceedingJoinPoint joinPoint, RedissonRateLimit rateLimit) {
+ String key = rateLimit.key();
+ // 如果key为空,使用默认格式
+ if (key.isEmpty()) {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ String className = method.getDeclaringClass().getSimpleName();
+ String methodName = method.getName();
+ return String.format("rate_limit:%s:%s", className, methodName);
+ }
+ // 如果key包含SpEL表达式,进行解析
+ if (key.contains("#")) {
+ return AopUtil.parseSpel(key, joinPoint);
+ }
+ return key;
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/SlidingWindowLimitAspect.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/SlidingWindowLimitAspect.java
new file mode 100644
index 0000000..5d77a35
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/SlidingWindowLimitAspect.java
@@ -0,0 +1,79 @@
+package top.lrshuai.limit.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import top.lrshuai.limit.annotation.LeakyBucketLimit;
+import top.lrshuai.limit.annotation.SlidingWindowLimit;
+import top.lrshuai.limit.common.ApiException;
+import top.lrshuai.limit.common.ApiResultEnum;
+import top.lrshuai.limit.service.SlidingWindowRateLimiter;
+import top.lrshuai.limit.util.AopUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+@Slf4j
+@Aspect
+@Component
+public class SlidingWindowLimitAspect {
+
+ private final SlidingWindowRateLimiter rateLimiter;
+
+ public SlidingWindowLimitAspect(SlidingWindowRateLimiter rateLimiter) {
+ this.rateLimiter = rateLimiter;
+ }
+
+ @Around("@annotation(slidingWindowLimit)")
+ public Object around(ProceedingJoinPoint joinPoint, SlidingWindowLimit slidingWindowLimit) throws Throwable {
+ String key = buildRateLimitKey(joinPoint, slidingWindowLimit);
+ int window = slidingWindowLimit.window();
+ int maxCount = slidingWindowLimit.maxCount();
+
+ if (!rateLimiter.tryAcquire(key, window, maxCount, 1)) {
+ throw new ApiException(ApiResultEnum.REQUEST_LIMIT);
+ }
+
+ return joinPoint.proceed();
+ }
+
+ /**
+ * 构建限流key
+ */
+ private String buildRateLimitKey(ProceedingJoinPoint joinPoint, SlidingWindowLimit rateLimit) {
+ String key = rateLimit.key();
+
+ // 如果key为空,使用默认格式
+ if (key.isEmpty()) {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ String className = method.getDeclaringClass().getSimpleName();
+ String methodName = method.getName();
+
+ // 尝试获取用户信息
+ String userKey = getCurrentUserId();
+ return String.format("sliding_window:%s:%s:%s", className, methodName, userKey);
+ }
+
+ // 如果key包含SpEL表达式,进行解析
+ if (key.contains("#")) {
+ return AopUtil.parseSpel(key, joinPoint);
+ }
+ return key;
+ }
+
+ private String getCurrentUserId() {
+ // 实际项目中从安全上下文获取
+ return "user123";
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/TokenBucketRateLimitAspect.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/TokenBucketRateLimitAspect.java
new file mode 100644
index 0000000..9ab9e28
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/aspect/TokenBucketRateLimitAspect.java
@@ -0,0 +1,98 @@
+package top.lrshuai.limit.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import top.lrshuai.limit.annotation.TokenBucketRateLimit;
+import top.lrshuai.limit.common.R;
+import top.lrshuai.limit.service.TokenBucketRateLimiter;
+import top.lrshuai.limit.util.AopUtil;
+import top.lrshuai.limit.util.IpUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+@Aspect
+@Component
+@Slf4j
+public class TokenBucketRateLimitAspect {
+
+ @Autowired
+ private TokenBucketRateLimiter rateLimiter;
+
+ /**
+ * 切片-方法级别
+ */
+ @Around("@annotation(rateLimit)")
+ public Object around(ProceedingJoinPoint joinPoint, TokenBucketRateLimit rateLimit) throws Throwable {
+ String key = buildRateLimitKey(joinPoint, rateLimit);
+
+ boolean allowed = rateLimiter.tryAcquire(key,rateLimit.rate(),rateLimit.capacity(),rateLimit.tokens());
+ if (!allowed) {
+ log.warn("接口限流触发 - key: {}, 方法: {}", key, joinPoint.getSignature().getName());
+ // 这里可以返回统一的错误结果
+ return R.fail(rateLimit.message());
+ }
+ return joinPoint.proceed();
+ }
+
+ /**
+ * 构建限流key
+ */
+ private String buildRateLimitKey(ProceedingJoinPoint joinPoint, TokenBucketRateLimit rateLimit) {
+ String key = rateLimit.key();
+
+ // 如果key为空,使用默认格式
+ if (key.isEmpty()) {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ String className = method.getDeclaringClass().getSimpleName();
+ String methodName = method.getName();
+
+ // 尝试获取用户信息,实现更细粒度的限流
+ String userKey = getUserKey();
+ return String.format("rate_limit:%s:%s:%s", className, methodName, userKey);
+ }
+
+ // 如果key包含SpEL表达式,进行解析
+ if (key.contains("#")) {
+ return AopUtil.parseSpel(key, joinPoint);
+ }
+
+ return key;
+ }
+
+ /**
+ * 获取用户标识(用户ID或IP)
+ */
+ private String getUserKey() {
+ try {
+ ServletRequestAttributes attributes = (ServletRequestAttributes)
+ RequestContextHolder.getRequestAttributes();
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ // 优先使用登录用户ID
+ String userId = (String) request.getAttribute("userId");
+ if (userId != null) {
+ return "user:" + userId;
+ }
+ // 降级为使用IP
+ return "ip:" + IpUtil.getClientIpAddress(request);
+ }
+ } catch (Exception e) {
+ log.debug("获取用户标识失败", e);
+ }
+ return "anonymous";
+ }
+
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiException.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiException.java
index b9d67ef..0aad269 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiException.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiException.java
@@ -11,14 +11,14 @@
@Data
public class ApiException extends RuntimeException{
private static final long serialVersionUID = 1L;
- private String status;
+ private int status;
private String message;
private Object data;
private Exception exception;
public ApiException() {
super();
}
- public ApiException(String status, String message, Object data, Exception exception) {
+ public ApiException(int status, String message, Object data, Exception exception) {
this.status = status;
this.message = message;
this.data = data;
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiResultEnum.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiResultEnum.java
index be4247b..0b2dc60 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiResultEnum.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/ApiResultEnum.java
@@ -1,30 +1,30 @@
package top.lrshuai.limit.common;
public enum ApiResultEnum {
- SUCCESS("200","ok"),
- FAILED("400","请求失败"),
- ERROR("500","不知名错误"),
- ERROR_NULL("501","空指针异常"),
- ERROR_CLASS_CAST("502","类型转换异常"),
- ERROR_RUNTION("503","运行时异常"),
- ERROR_IO("504","上传文件异常"),
- ERROR_MOTHODNOTSUPPORT("505","请求方法错误"),
+ SUCCESS(200,"ok"),
+ FAILED(400,"请求失败"),
+ ERROR(500,"不知名错误"),
+ ERROR_NULL(501,"空指针异常"),
+ ERROR_CLASS_CAST(502,"类型转换异常"),
+ ERROR_RUNTIME(503,"运行时异常"),
+ ERROR_IO(504,"上传文件异常"),
+ ERROR_MONTH_NOT_SUPPORT(505,"请求方法错误"),
- REQUST_LIMIT("10001","请求次数受限"),
+ REQUEST_LIMIT(10001,"请求次数受限"),
;
private String message;
- private String status;
+ private int status;
public String getMessage() {
return message;
}
- public String getStatus() {
+ public int getStatus() {
return status;
}
- private ApiResultEnum(String status, String message) {
+ private ApiResultEnum(int status, String message) {
this.message = message;
this.status = status;
}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/R.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/R.java
new file mode 100644
index 0000000..1c0f7b5
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/R.java
@@ -0,0 +1,137 @@
+package top.lrshuai.limit.common;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 响应信息主体
+ */
+@Data
+public class R implements Serializable {
+
+ /**
+ * 成功
+ */
+ public static final int SUCCESS = 200;
+ public static final String SUCCESS_MSG = "success";
+
+ /**
+ * 失败
+ */
+ public static final int FAIL = 500;
+ public static final String FAIL_MSG = "fail";
+
+ private int code;
+
+ private String msg;
+
+ private String trackerId;
+
+ private T data;
+
+ private Map extendMap;
+
+ /**
+ * 空构造,避免反序列化问题
+ */
+ public R() {
+ this.code = SUCCESS;
+ this.msg = SUCCESS_MSG;
+ }
+
+ public R(T data, int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ }
+
+ public static R ok() {
+ return restResult(null, SUCCESS, SUCCESS_MSG);
+ }
+
+ public static R ok(T data) {
+ return restResult(data, SUCCESS, SUCCESS_MSG);
+ }
+
+ public static R ok(T data, String msg) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static R fail() {
+ return restResult(null, FAIL, FAIL_MSG);
+ }
+
+ public static R fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static R fail(ApiResultEnum resultEnum) {
+ return restResult(null, resultEnum.getStatus(), resultEnum.getMessage());
+ }
+
+ public static R fail(T data) {
+ return restResult(data, FAIL, FAIL_MSG);
+ }
+
+ public static R fail(T data, String msg) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static R fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+
+ private static R restResult(T data, int code, String msg) {
+ return new R(data,code,msg);
+ }
+
+ public static Boolean isError(R ret) {
+ return !isSuccess(ret);
+ }
+
+ public static Boolean isSuccess(R ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+
+ public boolean isSuccess(){
+ return R.SUCCESS == code;
+ }
+
+ /**
+ * 链式调用
+ */
+ public R code(int code) {
+ this.code = code;
+ return this;
+ }
+
+ public R msg(String msg) {
+ this.msg = msg;
+ return this;
+ }
+
+ public R data(T data) {
+ this.data = data;
+ return this;
+ }
+
+ /**
+ * 添加扩展参数
+ * @param key key
+ * @param data value
+ * @return this
+ */
+ public R addExtend(String key,Object data){
+ if(extendMap==null){
+ extendMap=new HashMap();
+ }
+ extendMap.put(key,data);
+ return this;
+ }
+
+
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/Result.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/Result.java
deleted file mode 100644
index 9c54843..0000000
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/common/Result.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package top.lrshuai.limit.common;
-
-import java.util.HashMap;
-import java.util.Map;
-
-
-public class Result extends HashMap {
-
- private static final long serialVersionUID = 1L;
-
- public Result() {
- put("status", 200);
- put("message", "ok");
- }
-
- public static Result error() {
- return error("500", "系统错误,请联系管理员");
- }
-
- public static Result error(String msg) {
- return error("500", msg);
- }
-
- public static Result error(String status, String msg) {
- Result r = new Result();
- r.put("status", status);
- r.put("message", msg);
- return r;
- }
-
- public static Result error(ApiResultEnum resultEnum) {
- Result r = new Result();
- r.put("status", resultEnum.getStatus());
- r.put("message", resultEnum.getMessage());
- return r;
- }
-
- public static Result ok(Map map) {
- Result r = new Result();
- r.putAll(map);
- return r;
- }
- public static Result ok(Object data) {
- Result r = new Result();
- r.put("data",data);
- return r;
- }
-
- public static Result ok() {
- return new Result();
- }
-
- @Override
- public Result put(String key, Object value) {
- super.put(key, value);
- return this;
- }
-}
\ No newline at end of file
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/GlobalExceptionHandler.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/GlobalExceptionHandler.java
index c3769f3..8f2eb45 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/GlobalExceptionHandler.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/GlobalExceptionHandler.java
@@ -7,7 +7,7 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.lrshuai.limit.common.ApiException;
import top.lrshuai.limit.common.ApiResultEnum;
-import top.lrshuai.limit.common.Result;
+import top.lrshuai.limit.common.R;
import java.io.IOException;
@@ -22,45 +22,45 @@ public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(NullPointerException.class)
- public Result NullPointer(NullPointerException ex){
+ public R NullPointer(NullPointerException ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR_NULL);
+ return R.fail(ApiResultEnum.ERROR_NULL);
}
@ExceptionHandler(ClassCastException.class)
- public Result ClassCastException(ClassCastException ex){
+ public R ClassCastException(ClassCastException ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR_CLASS_CAST);
+ return R.fail(ApiResultEnum.ERROR_CLASS_CAST);
}
@ExceptionHandler(IOException.class)
- public Result IOException(IOException ex){
+ public R IOException(IOException ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR_IO);
+ return R.fail(ApiResultEnum.ERROR_IO);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
- public Result HttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex){
+ public R HttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR_MOTHODNOTSUPPORT);
+ return R.fail(ApiResultEnum.ERROR_MONTH_NOT_SUPPORT);
}
@ExceptionHandler(ApiException.class)
- public Result ApiException(ApiException ex) {
+ public R ApiException(ApiException ex) {
logger.error(ex.getMessage(),ex);
- return Result.error(ex.getStatus(),ex.getMessage());
+ return R.fail(ex.getStatus(),ex.getMessage());
}
@ExceptionHandler(RuntimeException.class)
- public Result RuntimeException(RuntimeException ex){
+ public R RuntimeException(RuntimeException ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR_RUNTION);
+ return R.fail(ApiResultEnum.ERROR_RUNTIME);
}
@ExceptionHandler(Exception.class)
- public Result exception(Exception ex){
+ public R exception(Exception ex){
logger.error(ex.getMessage(),ex);
- return Result.error(ApiResultEnum.ERROR);
+ return R.fail(ApiResultEnum.ERROR);
}
}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedisConfig.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedisConfig.java
index 360cb9b..365f7a5 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedisConfig.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedisConfig.java
@@ -3,22 +3,19 @@
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.springframework.cache.CacheManager;
-import org.springframework.cache.annotation.CachingConfigurerSupport;
-import org.springframework.cache.interceptor.KeyGenerator;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.data.redis.cache.RedisCacheManager;
-import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
-import javax.annotation.Resource;
-import java.lang.reflect.Method;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.List;
/**
*
@@ -27,49 +24,64 @@
*
*/
@Configuration
-//@EnableCaching // 开启缓存支持
-public class RedisConfig extends CachingConfigurerSupport {
- @Resource
- private LettuceConnectionFactory lettuceConnectionFactory;
+public class RedisConfig{
+ /**
+ * 令牌桶-lua脚本
+ */
+ @Value("classpath:lua/tokenRate.lua")
+ private Resource tokenLuaFile;
+
+ /**
+ * 漏牌-lua脚本
+ */
+ @Value("classpath:lua/leakyBucket.lua")
+ private Resource leakyLuaFile;
+
+ /**
+ * 漏牌-lua脚本
+ */
+ @Value("classpath:lua/slidingWindow.lua")
+ private Resource slidingWindowLuaFile;
+
+ /**
+ * 令牌桶限流 Lua 脚本
+ */
@Bean
- public KeyGenerator keyGenerator() {
- return new KeyGenerator() {
- @Override
- public Object generate(Object target, Method method, Object... params) {
- StringBuffer sb = new StringBuffer();
- sb.append(target.getClass().getName());
- sb.append(method.getName());
- for (Object obj : params) {
- sb.append(obj.toString());
- }
- return sb.toString();
- }
- };
+ public RedisScript tokenBucketScript() {
+ DefaultRedisScript redisScript = new DefaultRedisScript();
+ redisScript.setLocation(tokenLuaFile);
+ redisScript.setResultType(List.class);
+ return redisScript;
}
-
- // 缓存管理器
+ /**
+ * 漏桶限流 Lua 脚本
+ */
@Bean
- public CacheManager cacheManager() {
- RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
- .fromConnectionFactory(lettuceConnectionFactory);
- @SuppressWarnings("serial")
- Set cacheNames = new HashSet() {
- {
- add("codeNameCache");
- }
- };
- builder.initialCacheNames(cacheNames);
- return builder.build();
+ public RedisScript leakyBucketScript() {
+ DefaultRedisScript redisScript = new DefaultRedisScript();
+ redisScript.setLocation(leakyLuaFile);
+ redisScript.setResultType(Long.class);
+ return redisScript;
}
+ /**
+ * 滑动时间窗口计数器限流 Lua 脚本
+ */
+ @Bean
+ public RedisScript slidingWindowScript() {
+ DefaultRedisScript redisScript = new DefaultRedisScript();
+ redisScript.setLocation(slidingWindowLuaFile);
+ redisScript.setResultType(Long.class);
+ return redisScript;
+ }
/**
* RedisTemplate配置
*/
@Bean
- public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
+ public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 设置序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(
Object.class);
@@ -79,7 +91,7 @@ public RedisTemplate redisTemplate(LettuceConnectionFactory lett
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate redisTemplate = new RedisTemplate();
- redisTemplate.setConnectionFactory(lettuceConnectionFactory);
+ redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);// key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedissonConfig.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedissonConfig.java
new file mode 100644
index 0000000..1c8c1ea
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/RedissonConfig.java
@@ -0,0 +1,46 @@
+package top.lrshuai.limit.config;
+
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.StringUtils;
+
+/**
+ * redisson 配置,下面是单节点配置:
+ * 官方wiki地址:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#26-%E5%8D%95redis%E8%8A%82%E7%82%B9%E6%A8%A1%E5%BC%8F
+ *
+ */
+@Configuration
+public class RedissonConfig {
+
+ @Value("${spring.redis.host}")
+ private String host;
+
+ @Value("${spring.redis.port}")
+ private String port;
+
+ @Value("${spring.redis.password}")
+ private String password;
+
+ @Bean
+ public RedissonClient redissonClient(){
+ Config config = new Config();
+ //单节点
+ config.useSingleServer().setAddress("redis://" + host + ":" + port);
+ if(StringUtils.isEmpty(password)){
+ config.useSingleServer().setPassword(null);
+ }else{
+ config.useSingleServer().setPassword(password);
+ }
+ //添加主从配置
+// config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
+
+ // 集群模式配置 setScanInterval()扫描间隔时间,单位是毫秒, //可以用"rediss://"来启用SSL连接
+// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");
+ return Redisson.create(config);
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/WebMvcConfig.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/WebMvcConfig.java
deleted file mode 100644
index 35a29ee..0000000
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/config/WebMvcConfig.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package top.lrshuai.limit.config;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import top.lrshuai.limit.interceptor.RequestLimitIntercept;
-
-@Slf4j
-@Component
-public class WebMvcConfig implements WebMvcConfigurer {
-
- @Autowired
- private RequestLimitIntercept requestLimitIntercept;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- log.info("添加拦截");
- registry.addInterceptor(requestLimitIntercept);
- }
-}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/IndexController.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/IndexController.java
index 1ea7b5a..3428903 100644
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/IndexController.java
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/IndexController.java
@@ -4,11 +4,11 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.lrshuai.limit.annotation.RequestLimit;
-import top.lrshuai.limit.common.Result;
+import top.lrshuai.limit.common.R;
@RestController
@RequestMapping("/index")
-@RequestLimit(maxCount = 5,second = 1)
+@RequestLimit(maxCount = 5,second = 10)
public class IndexController {
/**
@@ -17,9 +17,9 @@ public class IndexController {
*/
@GetMapping("/test1")
@RequestLimit
- public Result test(){
+ public R test(){
//TODO ...
- return Result.ok();
+ return R.ok();
}
/**
@@ -27,8 +27,8 @@ public Result test(){
* @return
*/
@GetMapping("/test2")
- public Result test2(){
+ public R test2(){
//TODO ...
- return Result.ok();
+ return R.ok();
}
}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/LeakyRateController.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/LeakyRateController.java
new file mode 100644
index 0000000..2d03dac
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/LeakyRateController.java
@@ -0,0 +1,51 @@
+package top.lrshuai.limit.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import top.lrshuai.limit.annotation.LeakyBucketLimit;
+import top.lrshuai.limit.common.R;
+import top.lrshuai.limit.service.LeakyBucketRateLimiter;
+
+@RestController
+@RequestMapping("/leakyRate")
+public class LeakyRateController {
+
+ @Autowired
+ private LeakyBucketRateLimiter leakyBucketRateLimiter;
+
+
+ /**
+ * 测试
+ */
+ @GetMapping("/test1")
+ @LeakyBucketLimit(rate = 1, capacity = 3)
+ public R test1() {
+ //TODO ...
+ return R.ok();
+ }
+
+ @GetMapping("/test2")
+ @LeakyBucketLimit(key = "leaky_rate:test2",rate = 2, capacity = 1)
+ public R test2() {
+ //TODO ...
+ return R.ok();
+ }
+
+ @LeakyBucketLimit(key = "'user :' + #username", rate = 1, capacity = 5)
+ @GetMapping("/search")
+ public R search(@RequestParam String username) {
+ // 搜索逻辑 - 这里key会根据username动态变化
+ return R.ok("username:" + username);
+ }
+
+ @GetMapping("/status/{key}")
+ public R getStatus(@PathVariable String key) {
+ return R.ok(leakyBucketRateLimiter.getBucketStatus( key));
+ }
+
+ @PostMapping("/reset/{key}")
+ public R reset(@PathVariable String key) {
+ leakyBucketRateLimiter.reset(key);
+ return R.ok();
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/RedissonRateController.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/RedissonRateController.java
new file mode 100644
index 0000000..e96a54a
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/RedissonRateController.java
@@ -0,0 +1,19 @@
+package top.lrshuai.limit.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import top.lrshuai.limit.annotation.RedissonRateLimit;
+import top.lrshuai.limit.common.R;
+
+@RestController
+@RequestMapping("/redissonRate")
+public class RedissonRateController {
+
+ @GetMapping("/queryQuotaInfo")
+ @RedissonRateLimit(key = "'queryQuotaInfo:' + #storageType",rate = 1)
+ public R queryQuotaInfo(@RequestParam(value = "storageType") String storageType) {
+ return R.ok("storageType:"+storageType);
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/SlidingWindowRateController.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/SlidingWindowRateController.java
new file mode 100644
index 0000000..1004ee8
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/SlidingWindowRateController.java
@@ -0,0 +1,50 @@
+package top.lrshuai.limit.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import top.lrshuai.limit.annotation.SlidingWindowLimit;
+import top.lrshuai.limit.common.R;
+import top.lrshuai.limit.service.SlidingWindowRateLimiter;
+
+@RestController
+@RequestMapping("/SlidingWindowRate")
+public class SlidingWindowRateController {
+
+ @Autowired
+ private SlidingWindowRateLimiter slidingWindowRateLimiter;
+
+ /**
+ * 测试
+ */
+ @GetMapping("/test1")
+ @SlidingWindowLimit(window = 3, maxCount = 1)
+ public R test1() {
+ //TODO ...
+ return R.ok();
+ }
+
+ @GetMapping("/test2")
+ @SlidingWindowLimit(key = "sliding_window:test2",window = 60, maxCount = 5)
+ public R test2() {
+ //TODO ...
+ return R.ok();
+ }
+
+ @SlidingWindowLimit(key = "'user :' + #username")
+ @GetMapping("/search")
+ public R search(@RequestParam String username) {
+ // 搜索逻辑 - 这里key会根据username动态变化
+ return R.ok("username:" + username);
+ }
+
+ @GetMapping("/status/{key}")
+ public R getStatus(@PathVariable String key, int window) {
+ return R.ok(slidingWindowRateLimiter.getWindowStatus(key, window));
+ }
+
+ @PostMapping("/reset/{key}")
+ public R reset(@PathVariable String key) {
+ slidingWindowRateLimiter.reset(key);
+ return R.ok();
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/TokenRateController.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/TokenRateController.java
new file mode 100644
index 0000000..ecb895f
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/controller/TokenRateController.java
@@ -0,0 +1,33 @@
+package top.lrshuai.limit.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import top.lrshuai.limit.annotation.TokenBucketRateLimit;
+import top.lrshuai.limit.common.R;
+
+@RestController
+@RequestMapping("/tokenRate")
+public class TokenRateController {
+
+ /**
+ * 测试发送短信
+ */
+ @GetMapping("/sendSms")
+ @TokenBucketRateLimit(rate = 0.1, capacity = 2, message = "短信发送过于频繁")
+ public R sendSms(){
+ //TODO ...
+ return R.ok();
+ }
+
+ /**
+ * "@TokenBucketRateLimit(rate = 5.0, capacity = 20)" 每秒5次,突发20次
+ */
+ @TokenBucketRateLimit(key = "'search:' + #keyword", rate = 5.0, capacity = 10)
+ @GetMapping("/search")
+ public R search(@RequestParam String keyword) {
+ // 搜索逻辑 - 这里key会根据keyword动态变化
+ return R.ok("搜索结果:"+keyword);
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/interceptor/RequestLimitIntercept.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/interceptor/RequestLimitIntercept.java
deleted file mode 100644
index 525b6a4..0000000
--- a/SpringBoot-limit/src/main/java/top/lrshuai/limit/interceptor/RequestLimitIntercept.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package top.lrshuai.limit.interceptor;
-
-import com.alibaba.fastjson.JSONObject;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-import org.springframework.web.method.HandlerMethod;
-import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
-import top.lrshuai.limit.annotation.RequestLimit;
-import top.lrshuai.limit.common.ApiResultEnum;
-import top.lrshuai.limit.common.Result;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.concurrent.TimeUnit;
-
-/**
- * 请求拦截
- */
-@Slf4j
-@Component
-public class RequestLimitIntercept extends HandlerInterceptorAdapter {
-
- @Autowired
- private RedisTemplate redisTemplate;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- /**
- * isAssignableFrom() 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口
- * isAssignableFrom()方法是判断是否为某个类的父类
- * instanceof关键字是判断是否某个类的子类
- */
- if(handler.getClass().isAssignableFrom(HandlerMethod.class)){
- //HandlerMethod 封装方法定义相关的信息,如类,方法,参数等
- HandlerMethod handlerMethod = (HandlerMethod) handler;
- Method method = handlerMethod.getMethod();
- // 如果 方法上有注解就优先选择方法上的参数,否则类上的参数
- RequestLimit requestLimit = getTagAnnotation(method, RequestLimit.class);
- if(requestLimit != null){
- if(isLimit(request,requestLimit)){
- resonseOut(response,Result.error(ApiResultEnum.REQUST_LIMIT));
- return false;
- }
- }
- }
- return super.preHandle(request, response, handler);
- }
- //判断请求是否受限
- public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){
- // 受限的redis 缓存key ,因为这里用浏览器做测试,我就用sessionid 来做唯一key,如果是app ,可以使用 用户ID 之类的唯一标识。
- String limitKey = request.getServletPath()+request.getSession().getId();
- // 从缓存中获取,当前这个请求访问了几次
- Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
- if(redisCount == null){
- //初始 次数
- redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
- }else{
- if(redisCount.intValue()>= requestLimit.maxCount()){
- return true;
- }
- // 次数自增
- redisTemplate.opsForValue().increment(limitKey);
- }
- return false;
- }
-
- /**
- * 获取目标注解
- * 如果方法上有注解就返回方法上的注解配置,否则类上的
- * @param method
- * @param annotationClass
- * @param
- * @return
- */
- public A getTagAnnotation(Method method, Class annotationClass) {
- // 获取方法中是否包含注解
- Annotation methodAnnotate = method.getAnnotation(annotationClass);
- //获取 类中是否包含注解,也就是controller 是否有注解
- Annotation classAnnotate = method.getDeclaringClass().getAnnotation(annotationClass);
- return (A) (methodAnnotate!= null?methodAnnotate:classAnnotate);
- }
-
- /**
- * 回写给客户端
- * @param response
- * @param result
- * @throws IOException
- */
- private void resonseOut(HttpServletResponse response, Result result) throws IOException {
- response.setCharacterEncoding("UTF-8");
- response.setContentType("application/json; charset=utf-8");
- PrintWriter out = null ;
- String json = JSONObject.toJSON(result).toString();
- out = response.getWriter();
- out.append(json);
- }
-}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/LeakyBucketRateLimiter.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/LeakyBucketRateLimiter.java
new file mode 100644
index 0000000..692fcf4
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/LeakyBucketRateLimiter.java
@@ -0,0 +1,108 @@
+package top.lrshuai.limit.service;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+
+@Slf4j
+@Service
+public class LeakyBucketRateLimiter {
+
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ @Autowired
+ private RedisScript leakyBucketScript;
+
+ /**
+ * 尝试获取通行证
+ * @param key 限流key
+ * @param capacity 桶容量
+ * @param rate 流出速率(每秒请求数)
+ * @param requestCount 请求数量
+ * @return true-允许访问,false-被限流
+ */
+ public boolean tryAcquire(String key, int capacity, int rate, int requestCount) {
+ long now = System.currentTimeMillis() / 1000; // 使用秒级时间戳
+
+ Long result = redisTemplate.execute(leakyBucketScript,
+ Collections.singletonList(key),
+ capacity,rate,now,requestCount);
+
+ return result != null && result == 1;
+ }
+
+ /**
+ * 获取桶的当前状态(用于监控和调试)
+ */
+ public BucketStatus getBucketStatus(String key) {
+ try {
+ // 使用 RedisTemplate 的哈希操作获取值
+ Object waterObj = redisTemplate.opsForHash().get(key, "water");
+ Object lastLeakTimeObj = redisTemplate.opsForHash().get(key, "lastLeakTime");
+ Long ttl = redisTemplate.getExpire(key);
+
+ long water = 0;
+ long lastLeakTime = 0;
+
+ // 转换值
+ if (waterObj != null) {
+ water = Long.parseLong(waterObj.toString());
+ }
+ if (lastLeakTimeObj != null) {
+ lastLeakTime = Long.parseLong(lastLeakTimeObj.toString());
+ }
+
+ return new BucketStatus(
+ water,
+ lastLeakTime,
+ ttl != null ? ttl : -2
+ );
+ } catch (Exception e) {
+ log.error("Failed to get bucket status for key: {}", key, e);
+ return new BucketStatus(0, 0, -2);
+ }
+ }
+
+
+ /**
+ * 清理限流数据
+ */
+ public void reset(String key) {
+ redisTemplate.delete(key);
+ }
+
+ /**
+ * 桶状态信息
+ */
+ @Data
+ @AllArgsConstructor
+ public static class BucketStatus {
+ /**
+ * 当前桶中积压的请求数量
+ * 这个值表示漏桶中当前有多少个"水单位",每个水单位代表一个待处理的请求
+ * 当有请求进入系统时,currentWater 会增加
+ * 随着时间推移,水会以恒定速率从桶底漏出,currentWater 会相应减少
+ * 如果 currentWater>= capacity(桶容量),新的请求会被拒绝
+ */
+ private long currentWater;
+ /**
+ * 最后一次计算漏水的时间戳
+ * 用于计算从上次漏水到现在应该漏掉多少水
+ * 计算公式:漏水量 = (当前时间 - lastLeakTime) ×ばつ 流出速率
+ */
+ private long lastLeakTime;
+ /**
+ * Redis 中该限流 key 的剩余生存时间(单位:秒)
+ * 表示这个限流桶还有多少秒会被 Redis 自动删除
+ */
+ private long ttl;
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/SlidingWindowRateLimiter.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/SlidingWindowRateLimiter.java
new file mode 100644
index 0000000..e8fb51d
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/SlidingWindowRateLimiter.java
@@ -0,0 +1,136 @@
+package top.lrshuai.limit.service;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class SlidingWindowRateLimiter {
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ @Autowired
+ private RedisScript slidingWindowScript;
+
+ /**
+ * 尝试获取通行证
+ * @param key 限流key
+ * @param window 时间窗口大小(秒)
+ * @param maxCount 时间窗口内允许的最大请求数
+ * @param requestCount 请求数量
+ * @return true-允许访问,false-被限流
+ */
+ public boolean tryAcquire(String key, int window, int maxCount, int requestCount) {
+ long now = System.currentTimeMillis() / 1000; // 使用秒级时间戳
+ Long result = redisTemplate.execute(slidingWindowScript,
+ Collections.singletonList(key), window, maxCount, now, requestCount);
+
+ return result != null && result == 1;
+ }
+
+ /**
+ * 获取时间窗口的当前状态
+ */
+ public WindowStatus getWindowStatus(String key, int window) {
+ try {
+ long now = System.currentTimeMillis() / 1000;
+ long windowStart = now - window;
+
+ // 获取时间窗口内的请求总数
+ Long count = redisTemplate.opsForZSet().count(key, windowStart, Double.MAX_VALUE);
+
+ // 获取最早和最晚的请求时间
+ Set members = redisTemplate.opsForZSet().range(key, 0, -1);
+ long earliestTime = 0;
+ long latestTime = 0;
+
+ if (members != null && !members.isEmpty()) {
+ List times = members.stream()
+ .map(member -> Long.parseLong(member.toString()) / 1000) // 转回秒级
+ .sorted()
+ .collect(Collectors.toList());
+
+ earliestTime = times.get(0);
+ latestTime = times.get(times.size() - 1);
+ }
+ Long ttl = redisTemplate.getExpire(key);
+ return new WindowStatus(
+ count != null ? count : 0,
+ windowStart,
+ now,
+ earliestTime,
+ latestTime,
+ ttl != null ? ttl : -2
+ );
+ } catch (Exception e) {
+ log.error("Failed to get window status for key: {}", key, e);
+ return new WindowStatus(0, 0, 0, 0, 0, -2);
+ }
+ }
+
+ /**
+ * 清理限流数据
+ */
+ public void reset(String key) {
+ redisTemplate.delete(key);
+ }
+
+ /**
+ * 时间窗口状态信息
+ */
+ @Data
+ @AllArgsConstructor
+ public static class WindowStatus {
+ /**
+ * 当前时间窗口内的请求数量
+ */
+ private long currentCount;
+
+ /**
+ * 时间窗口起始时间戳(秒)
+ */
+ private long windowStart;
+
+ /**
+ * 当前时间戳(秒)
+ */
+ private long currentTime;
+
+ /**
+ * 时间窗口内最早的请求时间戳(秒)
+ */
+ private long earliestRequestTime;
+
+ /**
+ * 时间窗口内最晚的请求时间戳(秒)
+ */
+ private long latestRequestTime;
+
+ /**
+ * key的剩余生存时间(秒)
+ */
+ private long ttl;
+
+ @Override
+ public String toString() {
+ return String.format(
+ "WindowStatus{currentCount=%d, windowStart=%d, currentTime=%d, " +
+ "earliestRequest=%d, latestRequest=%d, ttl=%d}",
+ currentCount, windowStart, currentTime,
+ earliestRequestTime, latestRequestTime, ttl
+ );
+ }
+ }
+
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/TokenBucketRateLimiter.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/TokenBucketRateLimiter.java
new file mode 100644
index 0000000..77d8cd5
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/service/TokenBucketRateLimiter.java
@@ -0,0 +1,64 @@
+package top.lrshuai.limit.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+@Slf4j
+public class TokenBucketRateLimiter {
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ @Autowired
+ private RedisScript tokenBucketScript;
+
+ /**
+ * 尝试获取令牌
+ *
+ * @param key 限流key
+ * @param rate 令牌生成速率 (个/秒)
+ * @param capacity 桶容量
+ * @param tokenRequest 请求的令牌数
+ * @return 是否允许访问
+ */
+ public boolean tryAcquire(String key, double rate, int capacity, int tokenRequest) {
+ // 转换为秒
+ long now = System.currentTimeMillis() / 1000;
+
+ List keys = Arrays.asList(key);
+
+ @SuppressWarnings("unchecked")
+ List result = (List) redisTemplate.execute(tokenBucketScript, keys, rate, capacity, now, tokenRequest);
+
+ if (ObjectUtils.isEmpty(result)) {
+ log.warn("令牌桶限流脚本执行异常, key: {}", key);
+ return false;
+ }
+
+ boolean allowed = result.get(0) == 1;
+ long remainingTokens = result.get(1);
+ long bucketCapacity = result.get(2);
+
+ if (log.isDebugEnabled()) {
+ log.debug("限流检查 - key: {}, 允许: {}, 剩余令牌: {}/{}, 请求令牌数: {}",
+ key, allowed, remainingTokens, bucketCapacity, tokenRequest);
+ }
+
+ return allowed;
+ }
+
+ /**
+ * 简化方法 - 默认请求1个令牌
+ */
+ public boolean tryAcquire(String key, double rate, int capacity) {
+ return tryAcquire(key, rate, capacity, 1);
+ }
+}
diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/AopUtil.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/AopUtil.java
new file mode 100644
index 0000000..2266406
--- /dev/null
+++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/AopUtil.java
@@ -0,0 +1,38 @@
+package top.lrshuai.limit.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+@Slf4j
+public class AopUtil {
+
+ private static final ExpressionParser parser = new SpelExpressionParser();
+
+ /**
+ * 解析SpEL表达式
+ */
+ public static String parseSpel(String expression, ProceedingJoinPoint joinPoint) {
+ try {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ StandardEvaluationContext context = new StandardEvaluationContext();
+
+ // 设置方法参数
+ String[] parameterNames = signature.getParameterNames();
+ Object[] args = joinPoint.getArgs();
+ for (int i = 0; i < parameterNames.length; i++) { + context.setVariable(parameterNames[i], args[i]); + } + + Expression expr = parser.parseExpression(expression); + return expr.getValue(context, String.class); + } catch (Exception e) { + log.warn("解析SpEL表达式失败: {}", expression, e); + return expression; + } + } +} diff --git a/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/IpUtil.java b/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/IpUtil.java new file mode 100644 index 0000000..bb75c46 --- /dev/null +++ b/SpringBoot-limit/src/main/java/top/lrshuai/limit/util/IpUtil.java @@ -0,0 +1,64 @@ +package top.lrshuai.limit.util; + +import javax.servlet.http.HttpServletRequest; + +public class IpUtil { + + /** + * 获取客户端真实IP地址 + * 优先级: X-Forwarded-For -> X-Real-IP -> Proxy-Client-IP -> WL-Proxy-Client-IP -> RemoteAddr
+ * @param request HttpServletRequest对象
+ * @return 客户端的真实IP地址
+ */
+ public static String getClientIpAddress(HttpServletRequest request) {
+ String ip = null;
+
+ // 1. 优先检查X-Forwarded-For头部
+ ip = getIpFromHeader(request, "X-Forwarded-For");
+ if (isValidIp(ip)) {
+ // 取第一个非unknown的有效IP
+ String[] ips = ip.split(",");
+ for (String i : ips) {
+ i = i.trim();
+ if (isValidIp(i) && !"unknown".equalsIgnoreCase(i)) {
+ return i; // 返回第一个有效的客户端IP
+ }
+ }
+ }
+
+ // 2. 检查其他头部,按优先级排序
+ String[] headers = {"X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP"};
+ for (String header : headers) {
+ ip = getIpFromHeader(request, header);
+ if (isValidIp(ip)) {
+ return ip;
+ }
+ }
+
+ // 3. 最后使用远程地址
+ ip = request.getRemoteAddr();
+ return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; // 处理本地IPv6回环地址
+ }
+
+ /**
+ * 从请求头中获取IP值
+ */
+ public static String getIpFromHeader(HttpServletRequest request, String headerName) {
+ String ip = request.getHeader(headerName);
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ return null;
+ }
+ return ip.trim();
+ }
+
+ /**
+ * 基础IP地址有效性验证
+ */
+ private static boolean isValidIp(String ip) {
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ return false;
+ }
+ // 基础格式验证,可根据需要增强(例如使用正则表达式或InetAddress验证)
+ return ip.chars().allMatch(ch -> ch == '.' || Character.isDigit(ch) || (ch>= 'a' && ch <= 'f') || (ch>= 'A' && ch <= 'F') || ch == ':'); + } +} diff --git a/SpringBoot-limit/src/main/resources/application-dev.yml b/SpringBoot-limit/src/main/resources/application-dev.yml index e1b8564..2ded96a 100644 --- a/SpringBoot-limit/src/main/resources/application-dev.yml +++ b/SpringBoot-limit/src/main/resources/application-dev.yml @@ -2,7 +2,7 @@ server: port: 8000 spring: redis: - password: + password: abcsee2see database: 0 port: 6379 host: 127.0.0.1 @@ -13,3 +13,7 @@ spring: max-active: 10 max-wait: -1ms timeout: 10000ms +logging: + level: + root: info + top.lrshuai.limit: debug diff --git a/SpringBoot-limit/src/main/resources/lua/leakyBucket.lua b/SpringBoot-limit/src/main/resources/lua/leakyBucket.lua new file mode 100644 index 0000000..d76b88f --- /dev/null +++ b/SpringBoot-limit/src/main/resources/lua/leakyBucket.lua @@ -0,0 +1,47 @@ +-- 漏桶限流算法 Lua 脚本 + +-- 参数说明: +-- KEYS[1]: 限流的key +-- ARGV[1]: 桶的容量 +-- ARGV[2]: 流出速率(每秒处理数) +-- ARGV[3]: 当前时间戳(秒) +-- ARGV[4]: 本次请求数量 + +local key = KEYS[1] +local capacity = tonumber(ARGV[1]) +local rate = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requestCount = tonumber(ARGV[4]) + +-- 获取桶的当前状态 +local bucketInfo = redis.call('hmget', key, 'water', 'lastLeakTime') +local currentWater = 0 +local lastLeakTime = now + +-- 如果桶存在,获取当前水量和上次漏水时间 +if bucketInfo[1] then + currentWater = tonumber(bucketInfo[1]) +end + +if bucketInfo[2] then + lastLeakTime = tonumber(bucketInfo[2]) +end + +-- 计算从上次漏水到现在的漏出量 +local leakAmount = (now - lastLeakTime) * rate +if leakAmount> 0 then
+ currentWater = math.max(0, currentWater - leakAmount)
+ lastLeakTime = now
+end
+
+-- 检查桶是否有足够空间容纳新请求
+if currentWater + requestCount <= capacity then + -- 允许请求,更新桶状态 + currentWater = currentWater + requestCount + redis.call('hmset', key, 'water', currentWater, 'lastLeakTime', lastLeakTime) + redis.call('expire', key, 3600) -- 设置过期时间,防止内存泄漏 + return 1 -- 允许访问 +else + -- 桶已满,拒绝请求 + return 0 -- 被限流 +end \ No newline at end of file diff --git a/SpringBoot-limit/src/main/resources/lua/slidingWindow.lua b/SpringBoot-limit/src/main/resources/lua/slidingWindow.lua new file mode 100644 index 0000000..20355c5 --- /dev/null +++ b/SpringBoot-limit/src/main/resources/lua/slidingWindow.lua @@ -0,0 +1,41 @@ +-- 滑动时间窗口计数器限流算法 + +-- 参数说明: +-- KEYS[1]: 限流的key +-- ARGV[1]: 时间窗口大小(秒) +-- ARGV[2]: 时间窗口内允许的最大请求数 +-- ARGV[3]: 当前时间戳(秒) +-- ARGV[4]: 本次请求数量(默认为1) + +local key = KEYS[1] +local window = tonumber(ARGV[1]) +local maxCount = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requestCount = tonumber(ARGV[4]) or 1 + +-- 计算时间窗口的起始时间戳 +local windowStart = now - window + +-- 移除时间窗口之前的数据 +redis.call('zremrangebyscore', key, 0, windowStart) + +-- 获取当前时间窗口内的请求总数 +local currentCount = redis.call('zcard', key) + +-- 检查是否超过限制 +if currentCount + requestCount <= maxCount then + -- 没有超过限制,添加当前请求 + for i = 1, requestCount do + -- 使用毫秒级时间戳+随机数确保成员唯一性 + local member = now * 1000 + math.random(0, 999) + redis.call('zadd', key, member, member) + end + + -- 设置key的过期时间为窗口大小+1秒,确保数据自动清理 + redis.call('expire', key, window + 1) + + return 1 -- 允许访问 +else + -- 超过限制,拒绝请求 + return 0 -- 被限流 +end \ No newline at end of file diff --git a/SpringBoot-limit/src/main/resources/lua/tokenRate.lua b/SpringBoot-limit/src/main/resources/lua/tokenRate.lua new file mode 100644 index 0000000..df9db86 --- /dev/null +++ b/SpringBoot-limit/src/main/resources/lua/tokenRate.lua @@ -0,0 +1,51 @@ +-- 令牌桶限流 Lua 脚本 +-- KEYS[1]: 限流的key +-- ARGV[1]: 令牌生成速率 (每秒生成的令牌数) +-- ARGV[2]: 桶的容量 (最大令牌数) +-- ARGV[3]: 当前时间戳 (秒) +-- ARGV[4]: 本次请求的令牌数 (默认为1) + +local key = KEYS[1] +local rate = tonumber(ARGV[1]) +local capacity = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) +local requested = tonumber(ARGV[4]) + +-- 计算填满桶需要的时间,用于设置key的过期时间 +local fill_time = capacity / rate +local ttl = math.floor(fill_time * 2) -- 过期时间为填满时间的2倍 + +-- 从Redis获取上次的令牌数和刷新时间 +local last_tokens = tonumber(redis.call("get", key)) +if last_tokens == nil then + last_tokens = capacity -- 第一次访问,令牌数为桶容量 +end + +local last_refreshed = tonumber(redis.call("get", key .. ":ts")) +if last_refreshed == nil then + last_refreshed = now -- 第一次访问,刷新时间为当前时间 +end + +-- 计算时间差和应该补充的令牌数 +local delta = math.max(0, now - last_refreshed) +local filled_tokens = math.min(capacity, last_tokens + (delta * rate)) + +-- 判断是否允许本次请求 +local allowed = filled_tokens>= requested
+local new_tokens = filled_tokens
+local allowed_num = 0
+
+if allowed then
+ new_tokens = filled_tokens - requested
+ allowed_num = 1
+ -- 更新令牌数和时间戳
+ redis.call("setex", key, ttl, new_tokens)
+ redis.call("setex", key .. ":ts", ttl, now)
+else
+ -- 即使不允许,也更新状态(为了计算下一次的令牌数)
+ redis.call("setex", key, ttl, new_tokens)
+ redis.call("setex", key .. ":ts", ttl, last_refreshed)
+end
+
+-- 返回结果:是否允许(1/0),剩余令牌数,桶容量
+return {allowed_num, new_tokens, capacity}
\ No newline at end of file
diff --git a/springboot-2FA/.gitignore b/springboot-2FA/.gitignore
new file mode 100644
index 0000000..667aaef
--- /dev/null
+++ b/springboot-2FA/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/springboot-2FA/pom.xml b/springboot-2FA/pom.xml
new file mode 100644
index 0000000..82967aa
--- /dev/null
+++ b/springboot-2FA/pom.xml
@@ -0,0 +1,51 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.7
+
+
+ top.lrshuai.ai
+ springboot-2FA
+ 0.0.1-SNAPSHOT
+ springboot-2FA
+ 身份验证器 demo
+
+
+ 17
+ 1.19.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/springboot-2FA/src/main/java/top/lrshuai/totp/Springboot2FaApplication.java b/springboot-2FA/src/main/java/top/lrshuai/totp/Springboot2FaApplication.java
new file mode 100644
index 0000000..b39f761
--- /dev/null
+++ b/springboot-2FA/src/main/java/top/lrshuai/totp/Springboot2FaApplication.java
@@ -0,0 +1,13 @@
+package top.lrshuai.totp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Springboot2FaApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Springboot2FaApplication.class, args);
+ }
+
+}
diff --git a/springboot-2FA/src/main/java/top/lrshuai/totp/auth/GoogleAuthenticator.java b/springboot-2FA/src/main/java/top/lrshuai/totp/auth/GoogleAuthenticator.java
new file mode 100644
index 0000000..83699c1
--- /dev/null
+++ b/springboot-2FA/src/main/java/top/lrshuai/totp/auth/GoogleAuthenticator.java
@@ -0,0 +1,612 @@
+package top.lrshuai.totp.auth;
+
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.codec.binary.Hex;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Google Authenticator 工具类
+ * 基于 TOTP (Time-based One-Time Password) 算法实现双因素认证
+ * 支持 HMAC-SHA1、HMAC-SHA256、HMAC-SHA512 算法
+ * 参考 RFC 6238 标准,兼容 Google Authenticator 移动应用
+ *
+ * 主要功能:
+ * - 生成随机密钥(支持不同算法推荐长度)
+ * - 生成TOTP动态验证码
+ * - 生成Google Authenticator可识别的二维码数据
+ * - 验证用户输入的验证码
+ * - 支持多种HMAC算法
+ *
+ * @author rstyro
+ */
+public final class GoogleAuthenticator {
+
+ /** 默认密钥长度(字节)- SHA1 */
+ public static final int DEFAULT_SECRET_KEY_LENGTH_SHA1 = 20;
+ /** SHA256算法推荐密钥长度(字节) */
+ public static final int DEFAULT_SECRET_KEY_LENGTH_SHA256 = 32;
+ /** SHA512算法推荐密钥长度(字节) */
+ public static final int DEFAULT_SECRET_KEY_LENGTH_SHA512 = 64;
+
+ /** 默认密钥长度 */
+ private static final int DEFAULT_SECRET_KEY_LENGTH = DEFAULT_SECRET_KEY_LENGTH_SHA1;
+
+ /** 默认时间窗口大小(30秒单位) */
+ private static final int DEFAULT_WINDOW_SIZE = 2;
+ /** 最大允许的时间窗口大小 */
+ private static final int MAX_WINDOW_SIZE = 17;
+ /** 时间步长(秒) */
+ private static final long TIME_STEP = 30L;
+ /** 验证码位数 */
+ private static final int CODE_DIGITS = 6;
+
+ /** 算法名称常量 */
+ public static final String HMAC_SHA1 = "HmacSHA1";
+ public static final String HMAC_SHA256 = "HmacSHA256";
+ public static final String HMAC_SHA512 = "HmacSHA512";
+
+ /** 默认算法 */
+ private static final String DEFAULT_ALGORITHM = HMAC_SHA1;
+
+ /** 算法对应的推荐密钥长度映射 */
+ private static final Map ALGORITHM_KEY_LENGTH_MAP = new HashMap();
+
+ static {
+ ALGORITHM_KEY_LENGTH_MAP.put(HMAC_SHA1, DEFAULT_SECRET_KEY_LENGTH_SHA1);
+ ALGORITHM_KEY_LENGTH_MAP.put(HMAC_SHA256, DEFAULT_SECRET_KEY_LENGTH_SHA256);
+ ALGORITHM_KEY_LENGTH_MAP.put(HMAC_SHA512, DEFAULT_SECRET_KEY_LENGTH_SHA512);
+ }
+
+ /** 当前时间窗口大小 */
+ private static int windowSize = DEFAULT_WINDOW_SIZE;
+ /** 当前使用的算法 */
+ private static String currentAlgorithm = DEFAULT_ALGORITHM;
+
+ /**
+ * 私有构造方法,防止实例化
+ */
+ private GoogleAuthenticator() {
+ throw new AssertionError("GoogleAuthenticator是工具类,不能实例化");
+ }
+
+ // ==================== 密钥生成相关方法 ====================
+
+ /**
+ * 生成随机的Base32编码密钥(使用默认算法和长度)
+ * 密钥用于在客户端和服务器端之间共享,用于生成验证码
+ *
+ * @return Base32编码的随机密钥(大写,无分隔符)
+ * @throws SecurityException 如果随机数生成失败
+ */
+ public static String generateRandomSecretKey() {
+ return generateRandomSecretKey(DEFAULT_SECRET_KEY_LENGTH);
+ }
+
+ /**
+ * 生成指定长度的随机Base32编码密钥
+ *
+ * @param length 密钥长度(字节)
+ * @return Base32编码的随机密钥
+ */
+ public static String generateRandomSecretKey(int length) {
+ try {
+ SecureRandom random = SecureRandom.getInstanceStrong();
+ byte[] bytes = new byte[length];
+ random.nextBytes(bytes);
+
+ Base32 base32 = new Base32();
+ return base32.encodeToString(bytes).toUpperCase();
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("安全随机数生成器不可用", e);
+ }
+ }
+
+ /**
+ * 为指定算法生成推荐长度的随机密钥
+ *
+ * @param algorithm 算法(HMAC_SHA1, HMAC_SHA256, HMAC_SHA512)
+ * @return Base32编码的随机密钥
+ */
+ public static String generateRandomSecretKey(String algorithm) {
+ Integer length = ALGORITHM_KEY_LENGTH_MAP.get(algorithm);
+ if (length == null) {
+ throw new IllegalArgumentException("不支持的算法: " + algorithm +
+ ",支持的算法: " + ALGORITHM_KEY_LENGTH_MAP.keySet());
+ }
+ return generateRandomSecretKey(length);
+ }
+
+ /**
+ * 生成指定算法和长度的随机密钥
+ *
+ * @param algorithm 算法
+ * @param length 密钥长度
+ * @return Base32编码的随机密钥
+ */
+ public static String generateRandomSecretKey(String algorithm, int length) {
+ Integer recommendedLength = ALGORITHM_KEY_LENGTH_MAP.get(algorithm);
+ if (recommendedLength != null && length < recommendedLength) { + System.err.println("警告: 密钥长度" + length + "字节小于" + algorithm + + "推荐长度" + recommendedLength + "字节,可能存在安全风险"); + } + return generateRandomSecretKey(length); + } + + // ==================== TOTP生成方法 ==================== + + /** + * 生成当前时间的TOTP验证码(默认SHA1算法) + * + * @param secretKey Base32编码的共享密钥 + * @return 6位数字的TOTP验证码 + * @throws IllegalArgumentException 如果密钥为空或格式错误 + * @throws SecurityException 如果加密操作失败 + */ + public static String generateTOTPCode(String secretKey) { + return generateTOTPCode(secretKey, DEFAULT_ALGORITHM); + } + + /** + * 生成当前时间的TOTP验证码(指定算法) + * + * @param secretKey Base32编码的共享密钥 + * @param algorithm 算法(HMAC_SHA1, HMAC_SHA256, HMAC_SHA512) + * @return 6位数字的TOTP验证码 + */ + public static String generateTOTPCode(String secretKey, String algorithm) { + validateSecretKey(secretKey); + validateAlgorithm(algorithm); + + try { + // 标准化密钥:移除空格并转为大写 + String normalizedKey = secretKey.replace(" ", "").toUpperCase(); + Base32 base32 = new Base32(); + byte[] decodedBytes = base32.decode(normalizedKey); + String hexKey = Hex.encodeHexString(decodedBytes); + + // 计算当前时间窗口 + long timeWindow = (System.currentTimeMillis() / 1000L) / TIME_STEP; + String hexTime = Long.toHexString(timeWindow); + + // 调用TOTP类的对应方法 + switch (algorithm) { + case HMAC_SHA256: + return TOTP.generateTOTP256(hexKey, hexTime, CODE_DIGITS); + case HMAC_SHA512: + return TOTP.generateTOTP512(hexKey, hexTime, CODE_DIGITS); + case HMAC_SHA1: + default: + return TOTP.generateTOTP(hexKey, hexTime, CODE_DIGITS, algorithm); + } + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("无效的密钥格式: " + e.getMessage(), e); + } catch (Exception e) { + throw new SecurityException("生成TOTP验证码失败: " + e.getMessage(), e); + } + } + + /** + * 生成当前时间的TOTP验证码(SHA256算法) + * + * @param secretKey Base32编码的共享密钥 + * @return 6位数字的TOTP验证码 + */ + public static String generateTOTPCode256(String secretKey) { + return generateTOTPCode(secretKey, HMAC_SHA256); + } + + /** + * 生成当前时间的TOTP验证码(SHA512算法) + * + * @param secretKey Base32编码的共享密钥 + * @return 6位数字的TOTP验证码 + */ + public static String generateTOTPCode512(String secretKey) { + return generateTOTPCode(secretKey, HMAC_SHA512); + } + + /** + * 生成指定时间戳的TOTP验证码 + * + * @param secretKey Base32编码的共享密钥 + * @param timestamp 时间戳(毫秒) + * @param algorithm 算法 + * @return 6位数字的TOTP验证码 + */ + public static String generateTOTPCode(String secretKey, long timestamp, String algorithm) { + validateSecretKey(secretKey); + validateAlgorithm(algorithm); + + try { + String normalizedKey = secretKey.replace(" ", "").toUpperCase(); + Base32 base32 = new Base32(); + byte[] decodedBytes = base32.decode(normalizedKey); + String hexKey = Hex.encodeHexString(decodedBytes); + + long timeWindow = (timestamp / 1000L) / TIME_STEP; + String hexTime = Long.toHexString(timeWindow); + + switch (algorithm) { + case HMAC_SHA256: + return TOTP.generateTOTP256(hexKey, hexTime, CODE_DIGITS); + case HMAC_SHA512: + return TOTP.generateTOTP512(hexKey, hexTime, CODE_DIGITS); + case HMAC_SHA1: + default: + return TOTP.generateTOTP(hexKey, hexTime, CODE_DIGITS, algorithm); + } + } catch (Exception e) { + throw new SecurityException("生成TOTP验证码失败: " + e.getMessage(), e); + } + } + + // ==================== 二维码生成方法 ==================== + + /** + * 生成Google Authenticator二维码内容URL(默认SHA1算法) + * + * @param secretKey 共享密钥 + * @param account 用户账号(如邮箱或用户名) + * @param issuer 发行者名称(应用或网站名称) + * @return 二维码内容URL + * @throws IllegalArgumentException 如果参数为空或格式错误 + */ + public static String generateQRCodeUrl(String secretKey, String account, String issuer) { + return generateQRCodeUrl(secretKey, account, issuer, DEFAULT_ALGORITHM); + } + + /** + * 生成Google Authenticator二维码内容URL(指定算法) + * 注意:Google Authenticator应用可能不支持SHA256/SHA512 + * + * @param secretKey 共享密钥 + * @param account 用户账号 + * @param issuer 发行者名称 + * @param algorithm 算法 + * @return 二维码内容URL + */ + public static String generateQRCodeUrl(String secretKey, String account, String issuer, String algorithm) { + validateParameters(secretKey, account, issuer); + validateAlgorithm(algorithm); + + String normalizedKey = secretKey.replace(" ", "").toUpperCase(); + + // 构建OTP Auth URL,符合Google Authenticator标准格式 + StringBuilder url = new StringBuilder("otpauth://totp/") + .append(URLEncoder.encode(issuer + ":" + account, StandardCharsets.UTF_8).replace("+", "%20")) + .append("?secret=").append(URLEncoder.encode(normalizedKey, StandardCharsets.UTF_8).replace("+", "%20")) + .append("&issuer=").append(URLEncoder.encode(issuer, StandardCharsets.UTF_8).replace("+", "%20")); + + // 添加算法参数(SHA1是默认值,可以省略) + if (!HMAC_SHA1.equals(algorithm)) { + url.append("&algorithm=").append(algorithm.toUpperCase()); + } + + // 添加位数参数 + url.append("&digits=").append(CODE_DIGITS); + + // 添加时间步长参数 + url.append("&period=").append(TIME_STEP); + + return url.toString(); + } + + // ==================== 验证方法 ==================== + + /** + * 验证TOTP验证码(默认SHA1算法) + * 考虑时间窗口偏移,以处理客户端和服务端之间的时间差异 + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码 + * @param timestamp 时间戳(毫秒) + * @return 验证是否成功 + * @throws IllegalArgumentException 如果参数无效 + */ + public static boolean verifyCode(String secretKey, long code, long timestamp) { + return verifyCode(secretKey, code, timestamp, DEFAULT_ALGORITHM); + } + + /** + * 验证TOTP验证码(指定算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码 + * @param timestamp 时间戳(毫秒) + * @param algorithm 算法 + * @return 验证是否成功 + */ + public static boolean verifyCode(String secretKey, long code, long timestamp, String algorithm) { + validateSecretKey(secretKey); + validateAlgorithm(algorithm); + + if (code < 0 || code> 999999) {
+ throw new IllegalArgumentException("验证码必须是6位数字");
+ }
+
+ // 计算基准时间窗口
+ long timeWindow = (timestamp / 1000L) / TIME_STEP;
+
+ // 检查当前及前后时间窗口内的验证码
+ for (int i = -windowSize; i <= windowSize; i++) { + try { + String generatedCode = generateTOTPCode(secretKey, timestamp + (i * TIME_STEP * 1000L), algorithm); + if (Long.parseLong(generatedCode) == code) { + return true; + } + } catch (Exception e) { + // 记录日志但继续检查其他时间窗口 + System.err.println("验证码验证过程中出现异常: " + e.getMessage()); + } + } + + return false; + } + + /** + * 验证当前时间的TOTP验证码(默认SHA1算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode(String secretKey, long code) { + return verifyCode(secretKey, code, System.currentTimeMillis(), DEFAULT_ALGORITHM); + } + + /** + * 验证当前时间的TOTP验证码(指定算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码 + * @param algorithm 算法 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode(String secretKey, long code, String algorithm) { + return verifyCode(secretKey, code, System.currentTimeMillis(), algorithm); + } + + /** + * 验证当前时间的TOTP验证码字符串(更易用的方法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码字符串 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode(String secretKey, String code) { + return verifyCurrentCode(secretKey, code, DEFAULT_ALGORITHM); + } + + /** + * 验证当前时间的TOTP验证码字符串(指定算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码字符串 + * @param algorithm 算法 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode(String secretKey, String code, String algorithm) { + try { + long codeValue = Long.parseLong(code); + return verifyCurrentCode(secretKey, codeValue, algorithm); + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 验证当前时间的TOTP验证码字符串(SHA256算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码字符串 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode256(String secretKey, String code) { + return verifyCurrentCode(secretKey, code, HMAC_SHA256); + } + + /** + * 验证当前时间的TOTP验证码字符串(SHA512算法) + * + * @param secretKey 共享密钥 + * @param code 待验证的验证码字符串 + * @return 验证是否成功 + */ + public static boolean verifyCurrentCode512(String secretKey, String code) { + return verifyCurrentCode(secretKey, code, HMAC_SHA512); + } + + // ==================== 配置方法 ==================== + + /** + * 设置验证时间窗口大小 + * 时间窗口大小决定了允许的时间偏移范围(每个窗口30秒) + * @param size 窗口大小(1-17) + * @throws IllegalArgumentException 如果窗口大小超出范围 + */ + public static void setWindowSize(int size) { + if (size < 1 || size> MAX_WINDOW_SIZE) {
+ throw new IllegalArgumentException("窗口大小必须在1到" + MAX_WINDOW_SIZE + "之间");
+ }
+ windowSize = size;
+ }
+
+ /**
+ * 设置默认算法
+ *
+ * @param algorithm 算法(HMAC_SHA1, HMAC_SHA256, HMAC_SHA512)
+ */
+ public static void setDefaultAlgorithm(String algorithm) {
+ validateAlgorithm(algorithm);
+ currentAlgorithm = algorithm;
+ }
+
+ /**
+ * 获取当前时间窗口大小
+ *
+ * @return 当前时间窗口大小
+ */
+ public static int getWindowSize() {
+ return windowSize;
+ }
+
+ /**
+ * 获取当前默认算法
+ *
+ * @return 当前算法
+ */
+ public static String getDefaultAlgorithm() {
+ return currentAlgorithm;
+ }
+
+ /**
+ * 获取算法对应的推荐密钥长度
+ *
+ * @param algorithm 算法
+ * @return 推荐密钥长度(字节)
+ */
+ public static int getRecommendedKeyLength(String algorithm) {
+ Integer length = ALGORITHM_KEY_LENGTH_MAP.get(algorithm);
+ if (length == null) {
+ throw new IllegalArgumentException("不支持的算法: " + algorithm);
+ }
+ return length;
+ }
+
+ /**
+ * 获取支持的算法列表
+ *
+ * @return 支持的算法名称数组
+ */
+ public static String[] getSupportedAlgorithms() {
+ return new String[]{HMAC_SHA1, HMAC_SHA256, HMAC_SHA512};
+ }
+
+ // ==================== 辅助方法 ====================
+
+ /**
+ * 验证密钥格式
+ */
+ private static void validateSecretKey(String secretKey) {
+ if (secretKey == null || secretKey.trim().isEmpty()) {
+ throw new IllegalArgumentException("密钥不能为空");
+ }
+ if (!secretKey.matches("^[A-Z2-7=\\s]+$")) {
+ throw new IllegalArgumentException("密钥必须包含有效的Base32字符(A-Z, 2-7)");
+ }
+ }
+
+ /**
+ * 验证算法
+ */
+ private static void validateAlgorithm(String algorithm) {
+ if (!ALGORITHM_KEY_LENGTH_MAP.containsKey(algorithm)) {
+ throw new IllegalArgumentException("不支持的算法: " + algorithm +
+ ",支持的算法: " + String.join(", ", ALGORITHM_KEY_LENGTH_MAP.keySet()));
+ }
+ }
+
+ /**
+ * 验证二维码生成参数
+ */
+ private static void validateParameters(String secretKey, String account, String issuer) {
+ validateSecretKey(secretKey);
+
+ if (account == null || account.trim().isEmpty()) {
+ throw new IllegalArgumentException("账号不能为空");
+ }
+ if (issuer == null || issuer.trim().isEmpty()) {
+ throw new IllegalArgumentException("发行者名称不能为空");
+ }
+ }
+
+ /**
+ * 获取算法的显示名称
+ *
+ * @param algorithm 算法标识
+ * @return 显示名称
+ */
+ public static String getAlgorithmDisplayName(String algorithm) {
+ switch (algorithm) {
+ case HMAC_SHA1: return "HMAC-SHA1";
+ case HMAC_SHA256: return "HMAC-SHA256";
+ case HMAC_SHA512: return "HMAC-SHA512";
+ default: return algorithm;
+ }
+ }
+
+ // ==================== 测试方法 ====================
+
+ /**
+ * 完整测试示例
+ */
+ public static void testAllAlgorithms() {
+ System.out.println("=== Google Authenticator 多算法测试 ===\n");
+
+ String[] algorithms = {HMAC_SHA1, HMAC_SHA256, HMAC_SHA512};
+
+ for (String algorithm : algorithms) {
+ System.out.println("\n--- 测试 " + getAlgorithmDisplayName(algorithm) + " 算法 ---");
+
+ // 生成密钥
+ String secretKey = generateRandomSecretKey(algorithm);
+ int recommendedLength = getRecommendedKeyLength(algorithm);
+ System.out.println("1. 生成密钥 (" + recommendedLength + "字节): " + secretKey);
+
+ // 生成当前验证码
+ String totpCode = generateTOTPCode(secretKey, algorithm);
+ System.out.println("2. 当前TOTP验证码: " + totpCode);
+
+ // 生成二维码URL
+ String qrCodeUrl = generateQRCodeUrl(secretKey, "test@example.com", "TOTP-Test", algorithm);
+ System.out.println("3. 二维码URL: " + (qrCodeUrl.length()> 100 ? qrCodeUrl.substring(0, 100) + "..." : qrCodeUrl));
+
+ // 验证验证码
+ boolean isValid = verifyCurrentCode(secretKey, totpCode, algorithm);
+ System.out.println("4. 验证码验证结果: " + (isValid ? "✓ 通过" : "✗ 失败"));
+
+ // 错误验证码测试
+ boolean isInvalid = verifyCurrentCode(secretKey, "123456", algorithm);
+ System.out.println("5. 错误验证码测试: " + (!isInvalid ? "✓ 测试通过" : "✗ 测试不通过-错误验证码也通过"));
+ }
+
+ System.out.println("\n=== 测试完成 ===");
+ }
+
+ /**
+ * 主方法:测试多算法支持
+ */
+ public static void main(String[] args) {
+ // 测试所有算法
+// testAllAlgorithms();
+
+ // 或者单独测试特定算法
+ String secretKey = "NIHMRAK5ZS73PC3HOAGDTK65QDNCZ6QY";
+ String totp1 = generateTOTPCode(secretKey);
+ System.out.println("URL: " +generateQRCodeUrl(secretKey, "test-sha1@example.com", "TOTP-Test", HMAC_SHA1));
+ System.out.println("当前SHA1验证码: " + totp1);
+ System.out.println("当前SHA1验证码: " + verifyCurrentCode(secretKey, "050761"));
+ System.out.println();
+
+ String secretKey256 = "VBS6IG6VLRSRVPZUQBFM6G6WE6YGXRF7SCFTUVBJPTWUMPRBAWVQ====";
+ String totp256 = generateTOTPCode256(secretKey256);
+ System.out.println("URL: " +generateQRCodeUrl(secretKey256, "testsha256@example.com", "TOTP-Test", HMAC_SHA256));
+ System.out.println("当前SHA256验证码: " + totp256);
+ System.out.println("当前SHA256验证码: " + verifyCurrentCode256(secretKey256, "794120"));
+ System.out.println();
+
+ String secretKey512 = "Z535MJVUZWDXKRXHB7LMDS7YMTZOEZE37ZUXAXF6TKMU4MLOZGCHFFPAPY43EMW7MUZJZ7W74T2PFCEUVWRN4Z36XXGPZIX6W7XVIKI=";
+ String totp512 = generateTOTPCode256(secretKey512);
+ System.out.println("URL: " +generateQRCodeUrl(secretKey512, "testsha512@example.com", "TOTP-Test", HMAC_SHA512));
+ System.out.println("当前SHA512验证码: " + totp512);
+ System.out.println("当前SHA512验证码: " + verifyCurrentCode512(secretKey512, "149488"));
+
+ }
+}
\ No newline at end of file
diff --git a/springboot-2FA/src/main/java/top/lrshuai/totp/auth/TOTP.java b/springboot-2FA/src/main/java/top/lrshuai/totp/auth/TOTP.java
new file mode 100644
index 0000000..7cb7085
--- /dev/null
+++ b/springboot-2FA/src/main/java/top/lrshuai/totp/auth/TOTP.java
@@ -0,0 +1,307 @@
+package top.lrshuai.totp.auth;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * TOTP (Time-based One-Time Password) 算法实现
+ * 基于 RFC 6238 标准,用于生成基于时间的一次性密码。
+ * 该类是工具类,所有方法均为静态方法,不可实例化。
+ * 功能特点:
+ * - 支持 HMAC-SHA1、HMAC-SHA256、HMAC-SHA512 算法
+ * - 可自定义密码位数(1-8位)和时间步长
+ * - 提供密码验证功能,支持时间偏移容错
+ * 使用示例:
+ * String key = "3132333435363738393031323334353637383930";
+ * String totp = TOTP.generateCurrentTOTP(key);
+ * boolean isValid = TOTP.verifyTOTP(key, "123456");
+ *
+ * @author rstyro
+ */
+public final class TOTP {
+
+ /**
+ * 数字幂数组,用于计算10的n次方,索引对应位数(1-8位)
+ * 例如:DIGITS_POWER[6] = 1000000
+ */
+ private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
+
+ /** HMAC-SHA1 算法标识 */
+ public static final String HMAC_SHA1 = "HmacSHA1";
+ /** HMAC-SHA256 算法标识 */
+ public static final String HMAC_SHA256 = "HmacSHA256";
+ /** HMAC-SHA512 算法标识 */
+ public static final String HMAC_SHA512 = "HmacSHA512";
+
+ /** 默认动态密码位数(6位) */
+ private static final int DEFAULT_DIGITS = 6;
+ /** 默认时间步长(秒) */
+ private static final long DEFAULT_TIME_STEP = 30L;
+ /** 默认起始时间(Unix纪元) */
+ private static final long DEFAULT_START_TIME = 0L;
+ /** 默认验证时间窗口大小(允许前后偏移的步数) */
+ private static final int DEFAULT_TIME_WINDOW = 1;
+
+ /**
+ * 私有构造方法,防止类实例化
+ * 工具类应避免实例化,所有方法均为静态方法
+ */
+ private TOTP() {
+ throw new AssertionError("TOTP 是工具类,不能实例化");
+ }
+
+ /**
+ * 使用HMAC算法计算哈希值
+ *
+ * @param crypto 加密算法 (HmacSHA1, HmacSHA256, HmacSHA512)
+ * @param keyBytes 密钥字节数组
+ * @param text 要认证的消息文本
+ * @return HMAC哈希值
+ * @throws GeneralSecurityException 安全算法异常
+ */
+ private static byte[] hmacSha(String crypto, byte[] keyBytes, byte[] text)
+ throws GeneralSecurityException {
+ Mac hmac = Mac.getInstance(crypto);
+ SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
+ hmac.init(macKey);
+ return hmac.doFinal(text);
+ }
+
+ /**
+ * 将十六进制字符串转换为字节数组
+ *
+ * @param hex 十六进制字符串
+ * @return 字节数组
+ * @throws IllegalArgumentException 当十六进制字符串格式错误时
+ */
+ private static byte[] hexStr2Bytes(String hex) {
+ // 使用BigInteger处理十六进制字符串,确保正确转换
+ byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
+ byte[] ret = new byte[bArray.length - 1];
+ System.arraycopy(bArray, 1, ret, 0, ret.length);
+ return ret;
+ }
+
+ /**
+ * 生成TOTP值
+ *
+ * @param key 共享密钥,十六进制编码字符串
+ * @param time 时间计数器值,十六进制编码字符串
+ * @param returnDigits 返回的TOTP位数,必须在1到8之间
+ * @param crypto 加密算法,如 "HmacSHA1"
+ * @return TOTP数值字符串,指定位数
+ * @throws IllegalArgumentException 如果位数无效或参数错误
+ * @throws RuntimeException 如果安全算法出错
+ */
+ public static String generateTOTP(String key, String time, int returnDigits, String crypto) {
+ // 参数校验
+ if (returnDigits < 1 || returnDigits> 8) {
+ throw new IllegalArgumentException("TOTP位数必须在1到8之间");
+ }
+ if (key == null || key.isEmpty() || time == null || time.isEmpty()) {
+ throw new IllegalArgumentException("密钥和时间参数不能为空");
+ }
+
+ // 时间字符串填充至16字符(64位十六进制表示)
+ String paddedTime = time;
+ while (paddedTime.length() < 16) { + paddedTime = "0" + paddedTime; + } + + try { + byte[] msg = hexStr2Bytes(paddedTime); + byte[] k = hexStr2Bytes(key); + byte[] hash = hmacSha(crypto, k, msg); + + // 动态截取:取最后一字节的低4位作为偏移量 + int offset = hash[hash.length - 1] & 0x0f; + + // 从偏移位置取4字节,按大端序组合为整数 + int binary = ((hash[offset] & 0x7f) << 24) + | ((hash[offset + 1] & 0xff) << 16) + | ((hash[offset + 2] & 0xff) << 8) + | (hash[offset + 3] & 0xff); + + // 取模得到指定位数的TOTP值 + int otp = binary % DIGITS_POWER[returnDigits]; + + // 格式化为指定位数字符串,不足位补零 + return String.format("%0" + returnDigits + "d", otp); + + } catch (GeneralSecurityException e) { + throw new RuntimeException("TOTP生成安全错误: " + e.getMessage(), e); + } catch (Exception e) { + throw new RuntimeException("TOTP生成失败: " + e.getMessage(), e); + } + } + + + /** + * 生成TOTP(默认6位数,HMAC-SHA1算法) + */ + public static String generateTOTP(String key, String time) { + return generateTOTP(key, time, DEFAULT_DIGITS, HMAC_SHA1); + } + + /** + * 生成TOTP(指定位数,HMAC-SHA1算法) + */ + public static String generateTOTP(String key, String time, int returnDigits) { + return generateTOTP(key, time, returnDigits, HMAC_SHA1); + } + + /** + * 生成TOTP(指定位数,HMAC-SHA256算法) + */ + public static String generateTOTP256(String key, String time, int returnDigits) { + return generateTOTP(key, time, returnDigits, HMAC_SHA256); + } + + /** + * 生成TOTP(指定位数,HMAC-SHA512算法) + */ + public static String generateTOTP512(String key, String time, int returnDigits) { + return generateTOTP(key, time, returnDigits, HMAC_SHA512); + } + + /** + * 基于当前时间生成TOTP + * @param key 共享密钥(十六进制字符串) + * @return TOTP值(6位数) + */ + public static String generateCurrentTOTP(String key) { + long currentTime = System.currentTimeMillis() / 1000; + long timeStep = (currentTime - DEFAULT_START_TIME) / DEFAULT_TIME_STEP; + return generateTOTP(key, Long.toHexString(timeStep).toUpperCase()); + } + + /** + * 验证TOTP代码,考虑时间偏移容错 + * + * @param key 共享密钥 + * @param code 要验证的代码 + * @param timeWindow 时间窗口大小(允许前后偏移的步数) + * @return 验证是否成功 + */ + public static boolean verifyTOTP(String key, String code, int timeWindow) { + if (key == null || key.isEmpty() || code == null || code.isEmpty()) { + return false; + } + + long currentTime = System.currentTimeMillis() / 1000; + long currentTimeStep = (currentTime - DEFAULT_START_TIME) / DEFAULT_TIME_STEP; + + // 检查当前时间步及其前后时间窗口内的步数 + for (long i = -timeWindow; i <= timeWindow; i++) { + long timeStep = currentTimeStep + i; + String steps = Long.toHexString(timeStep).toUpperCase(); + try { + String totp = generateTOTP(key, steps); + if (totp.equals(code)) { + return true; + } + } catch (Exception e) { + // 忽略单个时间步的错误,继续验证其他步数 + continue; + } + } + return false; + } + + /** + * 验证TOTP代码(使用默认时间窗口) + */ + public static boolean verifyTOTP(String key, String code) { + return verifyTOTP(key, code, DEFAULT_TIME_WINDOW); + } + + /** + * 主方法:测试TOTP算法实现 + * 使用RFC 6238中的测试向量验证算法正确性,并演示当前TOTP生成 + */ + public static void main(String[] args) { + System.out.println("TOTP算法测试程序"); + System.out.println("================\n"); + + // RFC 6238 测试向量 + String seed20 = "3132333435363738393031323334353637383930"; // 20字节密钥(SHA1) + String seed32 = "3132333435363738393031323334353637383930313233343536373839303132"; // 32字节密钥(SHA256) + String seed64 = "3132333435363738393031323334353637383930" + + "3132333435363738393031323334353637383930" + + "3132333435363738393031323334353637383930" + + "31323334"; // 64字节密钥(SHA512) + + // 测试时间点(Unix时间戳) + long[] testTime = {59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L, 20000000000L}; + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("UTC")); + + // 打印测试结果表格头 + System.out.println("RFC 6238 测试向量验证结果:"); + System.out.println("+---------------+-----------------------+------------------+----------+----------+"); + System.out.println("| 时间(秒) | UTC时间 | T值(十六进制) | TOTP值 | 算法 |"); + System.out.println("+---------------+-----------------------+------------------+----------+----------+"); + + // 测试每个时间点 + for (long timeValue : testTime) { + long T = (timeValue - DEFAULT_START_TIME) / DEFAULT_TIME_STEP; + String steps = Long.toHexString(T).toUpperCase(); + // 填充至16字符 + while (steps.length() < 16) { + steps = "0" + steps; + } + + String fmtTime = String.format("%1$-11s", timeValue); + String utcTime = formatter.format(Instant.ofEpochSecond(timeValue)); + + // 测试SHA1算法 + printResult(fmtTime, utcTime, steps, generateTOTP(seed20, steps, 8, HMAC_SHA1), "SHA1"); + + // 测试SHA256算法 + printResult(fmtTime, utcTime, steps, generateTOTP(seed32, steps, 8, HMAC_SHA256), "SHA256"); + + // 测试SHA512算法 + printResult(fmtTime, utcTime, steps, generateTOTP(seed64, steps, 8, HMAC_SHA512), "SHA512"); + + System.out.println("+---------------+-----------------------+------------------+----------+----------+"); + } + + // 演示当前时间TOTP生成 + System.out.println("\n当前时间TOTP演示:"); + System.out.println("----------------"); + + String currentTOTP = generateCurrentTOTP(seed20); + System.out.println("共享密钥: " + seed20); + System.out.println("当前TOTP: " + currentTOTP); + + // 验证演示 + boolean isValid = verifyTOTP(seed20, currentTOTP); + System.out.println("TOTP验证: " + (isValid ? "通过" : "失败")); + + // 错误代码验证测试 + boolean isInvalid = verifyTOTP(seed20, "000000"); + System.out.println("错误代码验证: " + (isInvalid ? "通过" : "错误")); + + System.out.println("\n测试完成"); + } + + /** + * 打印单行测试结果 + * + * @param time 时间戳字符串 + * @param utcTime UTC时间字符串 + * @param steps 时间步十六进制值 + * @param totp TOTP值 + * @param mode 算法模式 + */ + private static void printResult(String time, String utcTime, String steps, + String totp, String mode) { + System.out.printf("| %s | %s | %s | %s | %-8s |%n", + time, utcTime, steps, totp, mode); + } +} \ No newline at end of file diff --git a/springboot-2FA/src/main/resources/application.yml b/springboot-2FA/src/main/resources/application.yml new file mode 100644 index 0000000..7ed7e2f --- /dev/null +++ b/springboot-2FA/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + application: + name: springboot-2FA diff --git a/springboot-2FA/src/test/java/top/lrshuai/totp/Springboot2FaApplicationTests.java b/springboot-2FA/src/test/java/top/lrshuai/totp/Springboot2FaApplicationTests.java new file mode 100644 index 0000000..4af8abf --- /dev/null +++ b/springboot-2FA/src/test/java/top/lrshuai/totp/Springboot2FaApplicationTests.java @@ -0,0 +1,13 @@ +package top.lrshuai.totp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Springboot2FaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/springboot-camunda/.gitattributes b/springboot-camunda/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/springboot-camunda/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/springboot-camunda/.gitignore b/springboot-camunda/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/springboot-camunda/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/springboot-camunda/README.md b/springboot-camunda/README.md new file mode 100644 index 0000000..31e76c6 --- /dev/null +++ b/springboot-camunda/README.md @@ -0,0 +1,680 @@ +## 引言 + +### 为什么需要工作流引擎? + +在当今快速变化的商业环境中,企业需要处理越来越复杂的业务流程。想象一下:一个员工请假申请需要经过部门经理审批、HR备案、财务记录等多个环节;一个电商订单需要经历库存检查、支付确认、发货通知、物流跟踪等步骤。这些业务流程如果硬编码在系统中,不仅难以维护,更无法快速适应业务变化。 + + + +## 一、什么是Camunda? + +- Camunda 是一个开源的工作流和业务流程管理平台,基于BPMN 2.0(业务流程模型与标记)标准构建。它提供了一个强大的流程引擎,允许开发人员将复杂的业务流程建模、执行、监控和优化。 + + + +### 1、传统开发 vs Camunda开发的对比: + + + +```java +// 传统硬编码方式 - 紧密耦合,难以维护 +public class LeaveApplicationService { + public void applyLeave(LeaveRequest request) { + // 1. 保存申请 + leaveRepository.save(request); + + // 2. 通知部门经理 + emailService.notifyManager(request); + + // 3. 如果经理批准,通知HR + // 4. 如果HR通过,更新考勤系统 + // ... 更多嵌套的条件判断 + } +} + +// 使用Camunda - 关注点分离,易于维护 +@Service +public class LeaveApplicationService { + + @Autowired + private RuntimeService runtimeService; + + public void applyLeave(LeaveRequest request) { + // 启动流程,具体步骤在BPMN图中定义 + runtimeService.startProcessInstanceByKey( + "LeaveProcess", + Variables.putValue("leaveRequest", request) + ); + } +} +``` + + + +### 2、核心组件 + +| 组件 | 功能描述 | 适用场景 | +| :------------------- | :----------------------------- | :--------------- | +| **Camunda Engine** | 核心流程引擎,负责执行BPMN流程 | 嵌入到Java应用中 | +| **Camunda Modeler** | 图形化流程设计工具 | 业务流程建模 | +| **Camunda Tasklist** | 用户任务管理界面 | 人工任务处理 | +| **Camunda Cockpit** | 流程监控和管理控制台 | 流程运维和监控 | +| **Camunda Optimize** | 流程分析和优化工具 | 性能分析和改进 | + + + + + +## 二、Springboot快速开始 + +### 1、引入依赖 + + +```text + +
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter
+ ${camunda.version}
+
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter-webapp
+ ${camunda.version}
+
+
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter-rest
+ ${camunda.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ ${mysql.version}
+
+
+```
+
+### 2、配置yml
+
+```yml
+server:
+ port: 8081
+
+camunda.bpm:
+ database:
+ type: mysql
+ schema-update: true # 首次启动设置为true,自动创建表
+ admin-user:
+ id: admin #用户名
+ password: admin #密码
+ firstName: rstyro-
+ filter:
+ create: All tasks
+ # 自动部署resources/下的BPMN文件
+ auto-deployment-enabled: true
+ # 历史级别: none, activity, audit, full
+ history-level: full
+ generic-properties:
+ properties:
+ historyTimeToLive: P30D # 设置全局默认历史记录生存时间为30天
+ enforceHistoryTimeToLive: false # 可选:禁用强制TTL检查
+ # 作业执行配置
+ job-execution:
+ enabled: true
+ core-pool-size: 3
+ max-pool-size: 10
+
+# mysql连接信息
+spring:
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ type: com.mysql.cj.jdbc.MysqlDataSource
+ url: jdbc:mysql://localhost:3306/camunda
+ username: root
+ password: root
+ jackson:
+ date-format: yyyy-MM-dd HH:mm:ss
+ time-zone: GMT+8
+
+# 日志配置
+logging:
+ level:
+ org.camunda: INFO
+ org.springframework.web: INFO
+```
+
+
+### 3、camunda的表解释
+
+
+| 表类别与前缀 | 核心职责 | 数据生命周期特点 | 代表性数据表 |
+| :---------------------- | :--------------------------------------------- | :----------------------------------------------------- | :---------------------------------------------------- |
+| **ACT_GE_*** (通用数据) | 存储引擎的二进制资源、属性配置和版本日志。 | 静态或长期存在,与流程定义同生命周期。 | `ACT_GE_BYTEARRAY`, `ACT_GE_PROPERTY` |
+| **ACT_RE_*** (资源存储) | 存储流程定义、决策规则等"静态"部署资源。 | 静态数据,部署后一般不变化,是流程的蓝图。 | `ACT_RE_PROCDEF`, `ACT_RE_DEPLOYMENT` |
+| **ACT_RU_*** (运行时) | 存储正在运行的流程实例、任务、变量等实时数据。 | **临时数据**,流程实例结束后立即被删除,保持表小而快。 | `ACT_RU_TASK`, `ACT_RU_EXECUTION`, `ACT_RU_VARIABLE` |
+| **ACT_HI_*** (历史记录) | 记录所有流程实例的完整历史、活动和变量变更。 | **历史数据**,长期保存,用于查询、报告与审计。 | `ACT_HI_PROCINST`, `ACT_HI_ACTINST`, `ACT_HI_VARINST` |
+| **ACT_ID_*** (身份认证) | 管理用户、用户组以及他们之间的关联关系。 | 基础主数据,独立于流程生命周期。 | `ACT_ID_USER`, `ACT_ID_GROUP`, `ACT_ID_MEMBERSHIP` |
+
+### 4、业务流程建模
+
+
+#### 安装Camunda Modeler
+
+我们一般会在`Camunda Modeler` 画出整个工作流的流程,然后导出 `.bpmn` 文件,然后在代码里面加载文件,进行编码的。
+
+- Camunda Modeler下载地址:[https://camunda.com/download/modeler/](https://camunda.com/download/modeler/)
+- 下载安装完成之后,我们可以新建一个请假流程。
+
+
+
+
+
+
+
+
+我们的BPMN文件内容放在`src/main/resources/process/leave.bpmn`中:
+
+```text
+
+
+
+
+ Flow_StartToApply
+
+
+
+
+
+ 年假
+ 病假
+ 事假
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_StartToApply
+ Flow_ApplyToGateway
+
+
+ Flow_ApplyToGateway
+ Flow_GatewayToManager
+ Flow_GatewayToDirector
+
+
+
+
+
+ 同意
+ 拒绝
+
+
+
+
+ Flow_GatewayToManager
+ Flow_ManagerToEnd
+ Flow_ManagerReject
+
+
+
+
+
+ 同意
+ 拒绝
+
+
+
+
+ Flow_GatewayToDirector
+ Flow_DirectorToEnd
+ Flow_DirectorReject
+
+
+ Flow_ManagerToEnd
+ Flow_DirectorToEnd
+ Flow_HRToEnd
+
+
+ Flow_ManagerReject
+ Flow_DirectorReject
+ Flow_NotifyToEnd
+
+
+ Flow_HRToEnd
+
+
+ Flow_NotifyToEnd
+
+
+
+
+ ${leaveDays <= 3}
+
+
+ ${leaveDays > 3}
+
+
+ ${managerApproved == true}
+
+
+ ${managerApproved == false}
+
+
+ ${directorApproved == true}
+
+
+ ${directorApproved == false}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### 5、Java服务实现
+
+创建相应的Java服务类来处理业务流程:
+
+
+
+```java
+@RestController
+@RequestMapping("/api/leave")
+public class LeaveProcessController {
+
+ @Resource
+ private RuntimeService runtimeService;
+
+ @Resource
+ private TaskService taskService;
+
+ @Resource
+ private HistoryService historyService;
+
+ @Resource
+ private IdentityService identityService;
+
+ /**
+ * 启动请假流程
+ */
+ @PostMapping("/start")
+ public ResponseEntity> startLeaveProcess(@RequestBody LeaveApplicationDto application) {
+ try {
+ // 设置流程启动者
+ identityService.setAuthenticatedUserId(application.getApplicant());
+
+ Map variables = new HashMap();
+ variables.put("applicant", application.getApplicant());
+ variables.put("leaveType", application.getLeaveType());
+ variables.put("startDate", application.getStartDate());
+ variables.put("endDate", application.getEndDate());
+ variables.put("leaveDays", application.getLeaveDays());
+ variables.put("reason", application.getReason());
+ // 设置审批人(实际项目中可以从用户服务获取)
+ variables.put("departmentManager", "manager_" + getDepartment(application.getApplicant()));
+ variables.put("director", "director_company");
+
+ var instance = runtimeService.startProcessInstanceByKey("LeaveProcess", variables);
+
+ Map result = new HashMap();
+ result.put("processInstanceId", instance.getId());
+ result.put("message", "请假流程已启动");
+
+ return ResponseEntity.ok(result);
+
+ } finally {
+ identityService.clearAuthentication();
+ }
+ }
+
+ /**
+ * 获取用户待办任务
+ * @param userId 用户
+ */
+ @GetMapping("/tasks/{userId}")
+ public ResponseEntity>> getUserTasks(@PathVariable String userId) {
+ List tasks = taskService.createTaskQuery()
+ .taskAssignee(userId)
+ .orderByTaskCreateTime()
+ .desc()
+ .list();
+
+ List> taskList = tasks.stream().map(task -> {
+ Map taskInfo = new HashMap();
+ taskInfo.put("taskId", task.getId());
+ taskInfo.put("taskName", task.getName());
+ taskInfo.put("processInstanceId", task.getProcessInstanceId());
+ taskInfo.put("createTime", task.getCreateTime());
+ taskInfo.put("dueDate", task.getDueDate());
+
+ // 获取流程变量
+ Map variables = taskService.getVariables(task.getId());
+ taskInfo.put("applicant", variables.get("applicant"));
+ taskInfo.put("leaveType", variables.get("leaveType"));
+ taskInfo.put("leaveDays", variables.get("leaveDays"));
+ taskInfo.put("startDate", variables.get("startDate"));
+
+ return taskInfo;
+ }).collect(Collectors.toList());
+
+ return ResponseEntity.ok(taskList);
+ }
+
+ /**
+ * 审批任务
+ * @param taskId 任务id
+ * @param approved 审批变量
+ * @param comment 审批意见
+ */
+ @PostMapping("/approve/{taskId}")
+ public ResponseEntity> approveTask(
+ @PathVariable String taskId,
+ @RequestParam Boolean approved,
+ @RequestParam(required = false) String comment) {
+
+ Map variables = new HashMap();
+
+ Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
+ if (task == null) {
+ throw new RuntimeException("任务不存在");
+ }
+
+ // 根据任务ID设置对应的审批变量
+ if ("UserTask_ManagerApprove".equals(task.getTaskDefinitionKey())) {
+ variables.put("managerApproved", approved);
+ variables.put("managerComment", comment);
+ } else if ("UserTask_DirectorApprove".equals(task.getTaskDefinitionKey())) {
+ variables.put("directorApproved", approved);
+ variables.put("directorComment", comment);
+ }
+
+ taskService.complete(taskId, variables);
+
+ Map result = new HashMap();
+ result.put("message", "审批完成");
+ result.put("taskId", taskId);
+ result.put("approved", approved);
+
+ return ResponseEntity.ok(result);
+ }
+
+ /**
+ * 获取流程历史
+ * @param processInstanceId 请假实例id
+ */
+ @GetMapping("/history/{processInstanceId}")
+ public ResponseEntity> getProcessHistory(@PathVariable String processInstanceId) {
+ HistoricProcessInstance processInstance = historyService
+ .createHistoricProcessInstanceQuery()
+ .processInstanceId(processInstanceId)
+ .singleResult();
+
+ List activities = historyService
+ .createHistoricActivityInstanceQuery()
+ .processInstanceId(processInstanceId)
+ .orderByHistoricActivityInstanceStartTime()
+ .asc()
+ .list();
+
+ Map history = new HashMap();
+ history.put("processInstance", processInstance);
+ history.put("activities", activities);
+
+ return ResponseEntity.ok(history);
+ }
+
+ private String getDepartment(String userId) {
+ // 模拟根据用户ID获取部门信息
+ // 实际项目中应该调用用户服务
+ return "tech"; // 返回部门代码
+ }
+}
+
+
+// 人事备案
+@Slf4j
+@Component
+public class HRRecordService implements JavaDelegate {
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception {
+ String processInstanceId = execution.getProcessInstanceId();
+ String applicant = (String) execution.getVariable("applicant");
+ Double leaveDays = (Double) execution.getVariable("leaveDays");
+ String leaveType = (String) execution.getVariable("leaveType");
+
+ log.info("人事备案 - 流程实例: {}, 申请人: {}, 请假类型: {}, 天数: {}",
+ processInstanceId, applicant, leaveType, leaveDays);
+
+ // 这里可以添加实际的HR系统集成逻辑
+ // 如更新考勤系统、记录请假记录等
+
+ execution.setVariable("hrRecorded", true);
+ execution.setVariable("recordTime", LocalDateTime.now());
+ }
+}
+
+
+// 发送通知
+@Slf4j
+@Component
+public class NotificationService implements JavaDelegate {
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception {
+ String applicant = (String) execution.getVariable("applicant");
+ Boolean approved = false;
+ String comment = "";
+
+ // 判断是经理审批还是总监审批的拒绝
+ if (execution.hasVariable("managerApproved")) {
+ approved = (Boolean) execution.getVariable("managerApproved");
+ comment = (String) execution.getVariable("managerComment");
+ } else if (execution.hasVariable("directorApproved")) {
+ approved = (Boolean) execution.getVariable("directorApproved");
+ comment = (String) execution.getVariable("directorComment");
+ }
+
+ if (!approved) {
+ log.info("发送通知 - 申请人: {}, 审批结果: 拒绝, 原因: {}", applicant, comment);
+ // 这里可以集成邮件、短信、企业微信等通知方式
+ }
+ }
+}
+```
+
+
+
+- 上面提供:启动请假流程、用户待办任务列表、还有审批接口、获取历史接口
+- 从发起请求流程开始一步一步完成整个流程的
+
+
+
+
+
+
+
+
+因为我这里设置开始流程之后,必须经过一个由申请人自己处理的"提交请假申请"任务,所以申请人能看到自己的任务(也就是刚提交的请假流程)
+
+
+
+
+
+
+
+
+当申请人审批之后,就正常的流转到排他网关,通过申请的请假天数判断是给经理审批(<=3天)还是总监审批(>3天)。
+
+
+
+我们这里设置的是3天,所以还是经理审批,经理审批的分配变量=`${departmentManager}`,总监分配变量=`${director}`。在代码我们可以看到`departmentManager`=`manager_tech`。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+所以通过`manager_tech`这个人得到待审核的任务进行审核,然后又调用审批接口,完成整个流程。
+
+
+
+
+
+
+
+
+
+## 三、总结
+
+
+
+Camunda工作流引擎就像业务流程的"操作系统",它让复杂的业务流程变得**可视化、可管理、可监控**。
+
+
+
+#### 什么时候应该使用工作流引擎?
+
+- 业务流程复杂,涉及多个环节和角色
+- 业务流程频繁变更
+- 需要详细的过程跟踪和审计
+- 有跨系统流程整合需求
+
+
+
+#### 资源获取:
+
+本文完整代码已上传至 GitHub,欢迎 Star ⭐ 和 Fork: [https://github.com/rstyro/Springboot/tree/master/springboot-camunda](https://github.com/rstyro/Springboot/tree/master/springboot-camunda)
+
+
+
+**欢迎分享你的经验**:
+在实际使用工作流时,你有哪些独到的见解或踩坑经验?欢迎在评论区交流讨论,让我们一起进步
diff --git a/springboot-camunda/pom.xml b/springboot-camunda/pom.xml
new file mode 100644
index 0000000..ece2361
--- /dev/null
+++ b/springboot-camunda/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.6
+
+
+ top.lrshuai.camunda
+ springboot-camunda
+ 0.0.1-SNAPSHOT
+ springboot-camunda
+ springboot-camunda
+
+
+ 17
+ 7.24.0
+ 8.3.0
+ 1.18.30
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter
+ ${camunda.version}
+
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter-webapp
+ ${camunda.version}
+
+
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter-rest
+ ${camunda.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ ${mysql.version}
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/SpringbootCamundaApplication.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/SpringbootCamundaApplication.java
new file mode 100644
index 0000000..ccac563
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/SpringbootCamundaApplication.java
@@ -0,0 +1,31 @@
+package top.lrshuai.camunda;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.Environment;
+
+@Slf4j
+@SpringBootApplication
+public class SpringbootCamundaApplication {
+
+ public static void main(String[] args) {
+ ConfigurableApplicationContext application = SpringApplication.run(SpringbootCamundaApplication.class, args);
+ Environment env = application.getEnvironment();
+// String ip = NetUtil.getLocalhostStr();
+ String ip = "127.0.0.1";
+ String port = env.getProperty("server.port");
+ String contextPath = env.getProperty("server.servlet.context-path","");
+ String banner = """
+ \n\t
+ ----------------------------------------------------------
+ SpringbootCamundaApplication is running! Access URLs:
+ Local: \t\thttp://localhost:%s%s/
+ External: \thttp://%s:%s%s/
+ ----------------------------------------------------------
+ """.formatted(port, contextPath, ip, port, contextPath);
+ log.info(banner);
+ }
+
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/config/ProcessAutoDeployerConfig.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/config/ProcessAutoDeployerConfig.java
new file mode 100644
index 0000000..1f55439
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/config/ProcessAutoDeployerConfig.java
@@ -0,0 +1,24 @@
+package top.lrshuai.camunda.config;
+
+import jakarta.annotation.Resource;
+import org.camunda.bpm.engine.RepositoryService;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+//@Component
+public class ProcessAutoDeployerConfig implements CommandLineRunner {
+
+ @Resource
+ private RepositoryService repositoryService;
+
+ @Override
+ public void run(String... args) throws Exception {
+ // 自动部署resources目录下的BPMN文件
+ repositoryService.createDeployment()
+ .name("LeaveProcessDeployment")
+ .addClasspathResource("process/leave.bpmn") // 替换为您的BPMN文件路径
+ .deploy();
+
+ System.out.println("流程部署完成");
+ }
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/controller/LeaveProcessController.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/controller/LeaveProcessController.java
new file mode 100644
index 0000000..9452f79
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/controller/LeaveProcessController.java
@@ -0,0 +1,165 @@
+package top.lrshuai.camunda.controller;
+
+import jakarta.annotation.Resource;
+import org.camunda.bpm.engine.HistoryService;
+import org.camunda.bpm.engine.IdentityService;
+import org.camunda.bpm.engine.RuntimeService;
+import org.camunda.bpm.engine.TaskService;
+import org.camunda.bpm.engine.history.HistoricActivityInstance;
+import org.camunda.bpm.engine.history.HistoricProcessInstance;
+import org.camunda.bpm.engine.task.Task;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import top.lrshuai.camunda.dto.LeaveApplicationDto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/leave")
+public class LeaveProcessController {
+
+ @Resource
+ private RuntimeService runtimeService;
+
+ @Resource
+ private TaskService taskService;
+
+ @Resource
+ private HistoryService historyService;
+
+ @Resource
+ private IdentityService identityService;
+
+ /**
+ * 启动请假流程
+ */
+ @PostMapping("/start")
+ public ResponseEntity> startLeaveProcess(@RequestBody LeaveApplicationDto application) {
+ try {
+ // 设置流程启动者
+ identityService.setAuthenticatedUserId(application.getApplicant());
+
+ Map variables = new HashMap();
+ variables.put("applicant", application.getApplicant());
+ variables.put("leaveType", application.getLeaveType());
+ variables.put("startDate", application.getStartDate());
+ variables.put("endDate", application.getEndDate());
+ variables.put("leaveDays", application.getLeaveDays());
+ variables.put("reason", application.getReason());
+ // 设置审批人(实际项目中可以从用户服务获取)
+ variables.put("departmentManager", getDepartment(application.getApplicant()));
+ variables.put("director", "总监");
+
+ var instance = runtimeService.startProcessInstanceByKey("LeaveProcess", variables);
+
+ Map result = new HashMap();
+ result.put("processInstanceId", instance.getId());
+ result.put("message", "请假流程已启动");
+
+ return ResponseEntity.ok(result);
+
+ } finally {
+ identityService.clearAuthentication();
+ }
+ }
+
+ /**
+ * 获取用户待办任务
+ * @param userId 用户
+ */
+ @GetMapping("/tasks/{userId}")
+ public ResponseEntity>> getUserTasks(@PathVariable String userId) {
+ List tasks = taskService.createTaskQuery()
+ .taskAssignee(userId)
+ .orderByTaskCreateTime()
+ .desc()
+ .list();
+
+ List> taskList = tasks.stream().map(task -> {
+ Map taskInfo = new HashMap();
+ taskInfo.put("taskId", task.getId());
+ taskInfo.put("taskName", task.getName());
+ taskInfo.put("processInstanceId", task.getProcessInstanceId());
+ taskInfo.put("createTime", task.getCreateTime());
+ taskInfo.put("dueDate", task.getDueDate());
+
+ // 获取流程变量
+ Map variables = taskService.getVariables(task.getId());
+ taskInfo.put("applicant", variables.get("applicant"));
+ taskInfo.put("leaveType", variables.get("leaveType"));
+ taskInfo.put("leaveDays", variables.get("leaveDays"));
+ taskInfo.put("startDate", variables.get("startDate"));
+
+ return taskInfo;
+ }).collect(Collectors.toList());
+
+ return ResponseEntity.ok(taskList);
+ }
+
+ /**
+ * 审批任务
+ * @param taskId 任务id
+ * @param approved 审批变量
+ * @param comment 审批意见
+ */
+ @PostMapping("/approve/{taskId}")
+ public ResponseEntity> approveTask(
+ @PathVariable String taskId,
+ @RequestParam Boolean approved,
+ @RequestParam(required = false) String comment) {
+
+ Map variables = new HashMap();
+
+ Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
+ if (task == null) {
+ throw new RuntimeException("任务不存在");
+ }
+
+ // 根据任务ID设置对应的审批变量,task.getTaskDefinitionKey()
+ variables.put("approved", approved);
+ variables.put("comment", comment);
+
+ taskService.complete(taskId, variables);
+
+ Map result = new HashMap();
+ result.put("message", "审批完成");
+ result.put("taskId", taskId);
+ result.put("approved", approved);
+
+ return ResponseEntity.ok(result);
+ }
+
+ /**
+ * 获取流程历史
+ * @param processInstanceId 请假实例id
+ */
+ @GetMapping("/history/{processInstanceId}")
+ public ResponseEntity> getProcessHistory(@PathVariable String processInstanceId) {
+ HistoricProcessInstance processInstance = historyService
+ .createHistoricProcessInstanceQuery()
+ .processInstanceId(processInstanceId)
+ .singleResult();
+
+ List activities = historyService
+ .createHistoricActivityInstanceQuery()
+ .processInstanceId(processInstanceId)
+ .orderByHistoricActivityInstanceStartTime()
+ .asc()
+ .list();
+
+ Map history = new HashMap();
+ history.put("processInstance", processInstance);
+ history.put("activities", activities);
+
+ return ResponseEntity.ok(history);
+ }
+
+ private String getDepartment(String userId) {
+ // 模拟根据用户ID获取部门信息
+ // 实际项目中应该调用用户服务
+ return "经理"; // 返回部门代码
+ }
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/dto/LeaveApplicationDto.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/dto/LeaveApplicationDto.java
new file mode 100644
index 0000000..a975fcf
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/dto/LeaveApplicationDto.java
@@ -0,0 +1,27 @@
+package top.lrshuai.camunda.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+@Data
+public class LeaveApplicationDto {
+ //申请人
+ private String applicant;
+ // 请假类型:年假、事假...
+ private String leaveType;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime startDate;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime endDate;
+ // 请假天数
+ private Double leaveDays;
+ //备注
+ private String reason;
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/entity/R.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/entity/R.java
new file mode 100644
index 0000000..f270980
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/entity/R.java
@@ -0,0 +1,131 @@
+package top.lrshuai.camunda.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 响应信息主体
+ */
+@Data
+public class R implements Serializable {
+
+ /**
+ * 成功
+ */
+ public static final int SUCCESS = 200;
+ public static final String SUCCESS_MSG = "ok";
+ public static final String FAIL_MSG = "error";
+
+ /**
+ * 失败
+ */
+ public static final int FAIL = 500;
+
+ private int code;
+
+ private String msg;
+
+ private String trackerId;
+
+ private T data;
+
+ private Map extendMap;
+
+ /**
+ * 空构造,避免反序列化问题
+ */
+ public R() {
+ this.code = SUCCESS;
+ this.msg = SUCCESS_MSG;
+ }
+
+ public R(T data, int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ }
+
+ public static R ok() {
+ return restResult(null, SUCCESS, SUCCESS_MSG);
+ }
+
+ public static R ok(T data) {
+ return restResult(data, SUCCESS, SUCCESS_MSG);
+ }
+
+ public static R ok(T data, String msg) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static R fail() {
+ return restResult(null, FAIL, FAIL_MSG);
+ }
+
+ public static R fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static R fail(T data) {
+ return restResult(data, FAIL, FAIL_MSG);
+ }
+
+ public static R fail(T data, String msg) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static R fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+ private static R restResult(T data, int code, String msg) {
+ return new R(data,code,msg);
+ }
+
+ public static Boolean isError(R ret) {
+ return !isSuccess(ret);
+ }
+
+ public static Boolean isSuccess(R ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+
+ public boolean isSuccess(){
+ return R.SUCCESS == code;
+ }
+
+ /**
+ * 链式调用
+ */
+ public R code(int code) {
+ this.code = code;
+ return this;
+ }
+
+ public R msg(String msg) {
+ this.msg = msg;
+ return this;
+ }
+
+ public R data(T data) {
+ this.data = data;
+ return this;
+ }
+
+ /**
+ * 添加扩展参数
+ * @param key key
+ * @param data value
+ * @return this
+ */
+ public R addExtend(String key,Object data){
+ if(extendMap==null){
+ extendMap=new HashMap();
+ }
+ extendMap.put(key,data);
+ return this;
+ }
+
+
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/listener/SampleExecutionListener.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/listener/SampleExecutionListener.java
new file mode 100644
index 0000000..f84af31
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/listener/SampleExecutionListener.java
@@ -0,0 +1,20 @@
+package top.lrshuai.camunda.listener;
+
+import org.camunda.bpm.engine.delegate.DelegateExecution;
+import org.camunda.bpm.engine.delegate.ExecutionListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 简单监听器
+ */
+@Component
+public class SampleExecutionListener implements ExecutionListener {
+
+ @Override
+ public void notify(DelegateExecution execution) throws Exception {
+ String eventName = execution.getEventName();
+ if (eventName.equals("start")) {
+ System.out.println("Process started: " + execution.getProcessInstanceId());
+ }
+ }
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/service/HRRecordService.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/service/HRRecordService.java
new file mode 100644
index 0000000..9ff069d
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/service/HRRecordService.java
@@ -0,0 +1,30 @@
+package top.lrshuai.camunda.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.camunda.bpm.engine.delegate.DelegateExecution;
+import org.camunda.bpm.engine.delegate.JavaDelegate;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Slf4j
+@Component
+public class HRRecordService implements JavaDelegate {
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception {
+ String processInstanceId = execution.getProcessInstanceId();
+ String applicant = (String) execution.getVariable("applicant");
+ Double leaveDays = (Double) execution.getVariable("leaveDays");
+ String leaveType = (String) execution.getVariable("leaveType");
+
+ log.info("人事备案 - 流程实例: {}, 申请人: {}, 请假类型: {}, 天数: {}",
+ processInstanceId, applicant, leaveType, leaveDays);
+
+ // 这里可以添加实际的HR系统集成逻辑
+ // 如更新考勤系统、记录请假记录等
+
+ execution.setVariable("hrRecorded", true);
+ execution.setVariable("recordTime", LocalDateTime.now());
+ }
+}
diff --git a/springboot-camunda/src/main/java/top/lrshuai/camunda/service/NotificationService.java b/springboot-camunda/src/main/java/top/lrshuai/camunda/service/NotificationService.java
new file mode 100644
index 0000000..a983684
--- /dev/null
+++ b/springboot-camunda/src/main/java/top/lrshuai/camunda/service/NotificationService.java
@@ -0,0 +1,30 @@
+package top.lrshuai.camunda.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.camunda.bpm.engine.delegate.DelegateExecution;
+import org.camunda.bpm.engine.delegate.JavaDelegate;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class NotificationService implements JavaDelegate {
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception {
+ String applicant = (String) execution.getVariable("applicant");
+ Boolean approved = (Boolean) execution.getVariable("approved");
+ String comment = (String) execution.getVariable("comment");
+
+ // 判断是经理审批还是总监审批的拒绝
+// if (execution.hasVariable("managerApproved")) {
+//
+// } else if (execution.hasVariable("directorApproved")) {
+// approved = (Boolean) execution.getVariable("directorApproved");
+// comment = (String) execution.getVariable("directorComment");
+// }
+
+
+ // 这里可以集成邮件、短信、企业微信等通知方式
+ log.info("发送通知 - 申请人: {}, 审批结果: {}, 原因: {}", applicant,!approved?"拒绝":"通过", comment);
+ }
+}
diff --git a/springboot-camunda/src/main/resources/application.yml b/springboot-camunda/src/main/resources/application.yml
new file mode 100644
index 0000000..bc1412c
--- /dev/null
+++ b/springboot-camunda/src/main/resources/application.yml
@@ -0,0 +1,44 @@
+server:
+ port: 8081
+
+camunda.bpm:
+ database:
+ type: mysql
+ schema-update: true # 首次启动设置为true,自动创建表
+ admin-user:
+ id: admin #用户名
+ password: admin #密码
+ firstName: rstyro-
+ filter:
+ create: All tasks
+ # 自动部署resources/下的BPMN文件
+ auto-deployment-enabled: true
+ # 历史级别: none, activity, audit, full
+ history-level: full
+ generic-properties:
+ properties:
+ historyTimeToLive: P30D # 设置全局默认历史记录生存时间为30天
+ enforceHistoryTimeToLive: false # 可选:禁用强制TTL检查
+ # 作业执行配置
+ job-execution:
+ enabled: true
+ core-pool-size: 3
+ max-pool-size: 10
+
+# mysql连接信息
+spring:
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ type: com.mysql.cj.jdbc.MysqlDataSource
+ url: jdbc:mysql://localhost:3306/camunda
+ username: root
+ password: root
+ jackson:
+ date-format: yyyy-MM-dd HH:mm:ss
+ time-zone: GMT+8
+
+# 日志配置
+logging:
+ level:
+ org.camunda: INFO
+ org.springframework.web: INFO
\ No newline at end of file
diff --git a/springboot-camunda/src/main/resources/process/leave.bpmn b/springboot-camunda/src/main/resources/process/leave.bpmn
new file mode 100644
index 0000000..4e67acd
--- /dev/null
+++ b/springboot-camunda/src/main/resources/process/leave.bpmn
@@ -0,0 +1,198 @@
+
+
+
+
+ Flow_StartToApply
+
+
+
+
+
+ 年假
+ 病假
+ 事假
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_StartToApply
+ Flow_ApplyToGateway
+
+
+ Flow_ApplyToGateway
+ Flow_GatewayToManager
+ Flow_GatewayToDirector
+
+
+
+
+
+ 同意
+ 拒绝
+
+
+
+
+ Flow_GatewayToManager
+ Flow_ManagerToEnd
+ Flow_ManagerReject
+
+
+
+
+
+ 同意
+ 拒绝
+
+
+
+
+ Flow_GatewayToDirector
+ Flow_DirectorToEnd
+ Flow_DirectorReject
+
+
+ Flow_ManagerToEnd
+ Flow_DirectorToEnd
+ Flow_HRToEnd
+
+
+
+ Flow_ManagerReject
+ Flow_DirectorReject
+ Flow_0q9p1jy
+
+
+ Flow_09s8940
+
+
+
+
+ ${leaveDays <= 3}
+
+
+ ${leaveDays > 3}
+
+
+ ${approved == true}
+
+
+
+ ${approved == true}
+
+
+
+
+ Flow_0q9p1jy
+ Flow_HRToEnd
+ Flow_09s8940
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/springboot-camunda/src/test/java/top/lrshuai/camunda/SpringbootCamundaApplicationTests.java b/springboot-camunda/src/test/java/top/lrshuai/camunda/SpringbootCamundaApplicationTests.java
new file mode 100644
index 0000000..b4c4cc2
--- /dev/null
+++ b/springboot-camunda/src/test/java/top/lrshuai/camunda/SpringbootCamundaApplicationTests.java
@@ -0,0 +1,13 @@
+package top.lrshuai.camunda;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class SpringbootCamundaApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/springboot-es/README.md b/springboot-es/README.md
new file mode 100644
index 0000000..f8a463c
--- /dev/null
+++ b/springboot-es/README.md
@@ -0,0 +1,5 @@
+# Springboot 与 ES整合Demo
+- 1、先创建ES 索引,索引文件为:`poetry_index.json`在src的script目录下
+- 2、执行test目录下的`PoetryApiApplicationTests.testAddES()` 方法把`唐诗三百首.json`文件到入到ES中,就可以请求接口了
+- 3、可以浏览器访问:`http://localhost:8008/search/detail/2d0385e7-0c72-423f-a538-f66acc9d4c46`
+- 4、飞花令接口:`http://localhost:8008/search/flyFlower?text=花`
\ No newline at end of file
diff --git a/springboot-es/pom.xml b/springboot-es/pom.xml
new file mode 100644
index 0000000..4a222cc
--- /dev/null
+++ b/springboot-es/pom.xml
@@ -0,0 +1,112 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.2.RELEASE
+
+
+ top.rstyro.es
+ springboot-es
+ 0.0.1-SNAPSHOT
+ pringboot-es
+ Demo project for Spring Boot
+
+ 11
+ 7.17.5
+ 1.18.2
+ 5.8.2
+ 2.0.22
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ org.elasticsearch.client
+ elasticsearch-rest-high-level-client
+ ${elasticsearch.version}
+
+
+
+ cn.hutool
+ hutool-core
+
+
+
+ cn.hutool
+ hutool-crypto
+
+
+
+
+
+
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ 2.0.22
+
+
+
+
+ io.gitee.liuzhihai520
+ ZHConverter
+ 1.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ com.alibaba
+ transmittable-thread-local
+ 2.14.0
+
+
+
+
+
+
+ cn.hutool
+ hutool-bom
+ ${hutool.version}
+ pom
+ import
+
+
+
+
+
+ poetry
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/springboot-es/script/poetry_index.json b/springboot-es/script/poetry_index.json
new file mode 100644
index 0000000..6c35d9b
--- /dev/null
+++ b/springboot-es/script/poetry_index.json
@@ -0,0 +1,87 @@
+{
+ "settings": {
+ "number_of_shards": 3,
+ "number_of_replicas": 0,
+ },
+ "mappings": {
+ "properties": {
+ "section": {
+ "type": "keyword"
+ },
+ "tags": {
+ "type": "keyword"
+ },
+ "dynasty": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "translations": {
+ "type": "text",
+ "analyzer": "ik_max_word",
+ "search_analyzer": "ik_smart"
+ },
+ "author": {
+ "type": "text",
+ "analyzer": "ik_max_word",
+ "search_analyzer": "ik_smart",
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ },
+ "suggest": {
+ "type": "completion",
+ "analyzer": "ik_smart",
+ "preserve_separators": true,
+ "preserve_position_increments": true,
+ "max_input_length": 10
+ }
+ }
+ },
+ "title": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ },
+ "suggest": {
+ "type": "completion",
+ "analyzer": "ik_smart",
+ "preserve_separators": true,
+ "preserve_position_increments": true,
+ "max_input_length": 50
+ }
+ },
+ "analyzer": "ik_max_word",
+ "search_analyzer": "ik_smart"
+ },
+ "content": {
+ "type": "text",
+ "analyzer": "ik_max_word",
+ "search_analyzer": "ik_smart",
+ "term_vector": "with_positions",
+ "fields": {
+ "suggest": {
+ "type": "completion",
+ "analyzer": "ik_smart",
+ "preserve_separators": true,
+ "preserve_position_increments": true,
+ "max_input_length": 50
+ }
+ }
+ },
+ "view_count": {
+ "type": "integer"
+ },
+ "update_time": {
+ "type": "date",
+ "format": "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd || epoch_millis"
+ },
+ "create_time": {
+ "type": "date",
+ "format": "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd || epoch_millis"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git "a/springboot-es/script/345円224円220円350円257円227円344円270円211円347円231円276円351円246円226円.json" "b/springboot-es/script/345円224円220円350円257円227円344円270円211円347円231円276円351円246円226円.json"
new file mode 100644
index 0000000..9c6f5e8
--- /dev/null
+++ "b/springboot-es/script/345円224円220円350円257円227円344円270円211円347円231円276円351円246円226円.json"
@@ -0,0 +1,6978 @@
+[
+ {
+ "author": "駱賓王",
+ "paragraphs": [
+ "西陸蟬聲唱,南冠客思侵。",
+ "那堪玄鬢影,來對白頭吟。",
+ "露重飛難進,風多響易沈。",
+ "無人信高潔,誰爲表予心。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "咏物",
+ "咏物诗",
+ "五言律诗"
+ ],
+ "title": "在嶽詠蟬",
+ "id": "c65539db-4e2b-4ce4-a22b-563b6ef3f4f1"
+ },
+ {
+ "author": "陳子昂",
+ "paragraphs": [
+ "前不見古人,後不見來者。",
+ "念天地之悠悠,獨愴然而涕下。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "八年级下册(课外)",
+ "伤怀",
+ "初中古诗",
+ "七言古诗"
+ ],
+ "title": "登幽州臺歌",
+ "id": "c244a5b4-0ed0-48fe-8694-95309acac184"
+ },
+ {
+ "author": "明皇帝",
+ "paragraphs": [
+ "夫子何爲者?栖栖一代中。",
+ "地猶鄹氏邑,宅即魯王宮。",
+ "歎鳳嗟身否,傷麟怨道窮。",
+ "今看兩楹奠,當與夢時同。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "伤怀",
+ "写人",
+ "五言律诗"
+ ],
+ "title": "經鄒魯祭孔子而歎之",
+ "id": "a1824eb8-4e2e-4d53-9a33-e1dcfe7d1c73"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "君不見黃河之水天上來,奔流到海不復回。",
+ "君不見高堂明鏡悲白髮,朝如青絲暮成雪。",
+ "人生得意須盡歡,莫使金尊空對月。",
+ "天生我材必有用,千金散盡還復來。",
+ "烹羊宰牛且爲樂,會須一飲三百杯。",
+ "岑夫子,丹丘生,將進酒,杯莫停。",
+ "與君歌一曲,請君爲我側耳聽。",
+ "鐘鼓饌玉不足貴,但願長醉不復醒。",
+ "古來聖賢皆寂寞,惟有飲者留其名。",
+ "陳王昔時宴平樂,斗酒十千恣歡謔。",
+ "主人何爲言少錢,徑須酤取對君酌。",
+ "五花馬,千金裘,呼兒將出換美酒,與爾同銷萬古愁。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄河",
+ "咏物",
+ "抒情",
+ "鼓吹曲辞",
+ "乐府",
+ "宴饮",
+ "哲理",
+ "水",
+ "咏物诗"
+ ],
+ "title": "鼓吹曲辭 將進酒",
+ "id": "0f7504df-cda2-4fe2-bc63-867ec2e418e7"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "秦時明月漢時關,萬里長征人未還。",
+ "但使龍城飛將在,不教胡馬度陰山。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "写景",
+ "忧国忧民",
+ "爱国",
+ "乐府",
+ "山",
+ "边塞"
+ ],
+ "title": "橫吹曲辭 出塞 一",
+ "id": "2d0385e7-0c72-423f-a538-f66acc9d4c46"
+ },
+ {
+ "author": "王之渙",
+ "paragraphs": [
+ "黃砂直上白雲間,一片孤城萬仞山。",
+ "羌笛何須怨楊柳,春風不度玉門關。"
+ ],
+ "tags": [
+ "山水",
+ "写景",
+ "唐诗三百首",
+ "乐府",
+ "思乡",
+ "边塞",
+ "将士"
+ ],
+ "title": "橫吹曲辭 出塞",
+ "id": "bcdf6f13-4cb9-418f-9279-c690522e0a27"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "明月出天山,蒼茫雲海間。",
+ "長風幾萬里,吹度玉門關。",
+ "漢下白登道,胡窺青海灣。",
+ "由來征戰地,不見有人還。",
+ "戍客望邊色,思歸多苦顏。",
+ "高樓當此夜,歎息未應閑。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "中秋",
+ "边塞",
+ "山",
+ "思乡",
+ "乐府",
+ "描写山",
+ "写山",
+ "征人"
+ ],
+ "title": "橫吹曲辭 關山月",
+ "id": "0167687e-8325-48bf-8da4-3749c9ce0a74"
+ },
+ {
+ "author": "王建",
+ "paragraphs": [
+ "三日入廚下,洗手作羹湯。",
+ "未諳姑食性,先遣小姑嘗。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "新婚",
+ "五言绝句",
+ "女子",
+ "生活"
+ ],
+ "title": "新嫁娘詞三首 三",
+ "id": "efaaa86e-5628-40d3-a137-1ae8fcbe41b8"
+ },
+ {
+ "author": "王建",
+ "paragraphs": [
+ "寥落古行宮,宮花寂寞紅。",
+ "白頭宮女在,閑坐說玄宗。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "怀古",
+ "宫怨",
+ "五言绝句"
+ ],
+ "title": "故行宮",
+ "id": "a6c85d29-ad02-4f54-a347-63800213a7b4"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "錦瑟無端五十絃,一絃一柱思華年。",
+ "莊生曉夢迷蝴蝶,望帝春心託杜鵑。",
+ "滄海月明珠有淚,藍田日暖玉生煙。",
+ "此情可待成追憶,只是當時已惘然。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "七言律诗",
+ "抒情",
+ "爱情"
+ ],
+ "title": "錦瑟",
+ "id": "e0eb3016-9288-4dc0-9257-3de36e5ad73c"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "本以高難飽,徒勞恨費聲。",
+ "五更疎欲斷,一樹碧無情。",
+ "薄宦梗猶汎,故園蕪已平。",
+ "煩君最相警,我亦舉家清。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "咏物",
+ "咏物诗",
+ "五言律诗"
+ ],
+ "title": "蟬",
+ "id": "b3de619f-cc8b-457d-942b-49f06651aba4"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "向晚意不適,驅車登古原。",
+ "夕陽無限好,只是近黃昏。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "惜时",
+ "哲理"
+ ],
+ "title": "樂遊原",
+ "id": "d7c6d072-6e84-482e-856a-5a4c21226a3c"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "君問歸期未有期,巴山夜雨漲秋池。",
+ "何當共剪西窗燭,却話巴山夜雨時。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "写雨",
+ "写景",
+ "景中情",
+ "初中古诗",
+ "高中古诗",
+ "七言绝句",
+ "爱情",
+ "三年级上册",
+ "雨",
+ "七年级上册(课外)"
+ ],
+ "title": "夜雨寄北",
+ "id": "75db753d-e7da-48a1-a6c0-6ed9147e58db"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "元和天子神武姿,彼何人哉軒與羲。",
+ "誓將上雪列聖恥,坐法宮中朝四夷。",
+ "淮西有賊五十載,封狼生貙貙生羆。",
+ "不據山河據平地,長戈利矛日可麾。",
+ "帝得聖相相曰度,賊斫不死神扶持。",
+ "腰懸相印作都統,陰風慘澹天王旗。",
+ "愬武古通作牙爪,儀曹外郎載筆隨。",
+ "行軍司馬智且勇,十四萬衆猶虎貔。",
+ "入蔡縛賊獻太廟,功無與讓恩不訾。",
+ "帝曰汝度功第一,汝從事愈宜爲辭。",
+ "愈拜稽首蹈且舞,金石刻畫臣能爲。",
+ "古者世稱大手筆,此事不繫于職司。",
+ "當仁自古有不讓,言訖屢頷天子頤。",
+ "公退齋戒坐小閣,濡染大筆何淋漓。",
+ "點竄堯典舜典字,塗改清廟生民詩。",
+ "文成破體書在紙,清晨再拜鋪丹墀。",
+ "表曰臣愈昧死上,詠神聖功書之碑。",
+ "碑高三丈字如斗,負以靈鼇蟠以螭。",
+ "句奇語重喻者少,讒之天子言其私。",
+ "長繩百尺拽碑倒,麤砂大石相磨治。",
+ "公之斯文若元氣,先時已入人肝脾。",
+ "湯盤孔鼎有述作,今無其器存其辭。",
+ "嗚呼聖皇及聖相,相與烜赫流淳熙。",
+ "公之斯文不示後,曷與三五相攀追。",
+ "願書萬本誦萬過,口角流沫右手胝。",
+ "傳之七十有二代,以爲封禪玉檢明堂基。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "叙事",
+ "七言古诗",
+ "咏史"
+ ],
+ "title": "韓碑",
+ "id": "0d2c5fba-d350-4c3f-b55e-72a491a84df6"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "淒涼寶劍篇,羈泊欲窮年。",
+ "黃葉仍風雨,青樓自管絃。",
+ "新知遭薄俗,舊好隔良緣。",
+ "心斷新豐酒,銷愁斗幾千。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "风雨",
+ "五言律诗",
+ "怀才不遇"
+ ],
+ "title": "風雨",
+ "id": "d32a2610-09d7-49ad-8cdf-dd9c649144c7"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "嵩雲秦樹久離居,雙鯉迢迢一紙書。",
+ "休問梁園舊賓客,茂陵秋雨病相如。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "写景",
+ "七言绝句",
+ "友情"
+ ],
+ "title": "寄令狐郎中",
+ "id": "fb376b71-91c6-44ee-b2e4-40f5b3f71b8f"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "紫泉宮殿鎖煙霞,欲取蕪城作帝家。",
+ "玉璽不緣歸日角,錦帆應是到天涯。",
+ "于今腐草無螢火,終古垂楊有暮鴉。",
+ "地下若逢陳後主,豈宜重問後庭花。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "讽刺",
+ "咏史"
+ ],
+ "title": "隋宮",
+ "id": "670009c2-a701-4140-aa31-b4973f76d632"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "猿鳥猶疑畏簡書,風雲常爲護儲胥。",
+ "徒令上將揮神筆,終見降王走傳車。",
+ "管樂有才終不忝,關張無命欲何如。",
+ "他年錦里經祠廟,梁父吟成恨有餘。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "典故",
+ "写人",
+ "咏史怀古"
+ ],
+ "title": "籌筆驛",
+ "id": "131c9f87-273f-4684-9b67-bd7b58ad4008"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "昨夜星辰昨夜風,畫樓西畔桂堂東。",
+ "身無綵鳳雙飛翼,心有靈犀一點通。",
+ "隔座送鉤春酒暖,分曹射覆蠟燈紅。",
+ "嗟余聽鼓應官去,走馬蘭臺類斷蓬。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "宴饮",
+ "七言律诗",
+ "爱情",
+ "相思",
+ "感慨",
+ "组诗"
+ ],
+ "title": "無題二首 一",
+ "id": "3410e7ce-df3a-4ab8-bea3-7f681661b240"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "來是空言去絕踪,月斜樓上五更鐘。",
+ "夢爲遠別啼難喚,書被催成墨未濃。",
+ "蠟照半籠金翡翠,麝熏微度繡芙蓉。",
+ "劉郎已恨蓬山遠,更隔蓬山一萬重。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "记梦",
+ "七言律诗",
+ "爱情"
+ ],
+ "title": "無題四首 一",
+ "id": "7377e34a-1aab-47e3-a6dd-376c98a5d6b7"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "颯颯東風細雨來,芙蓉塘外有輕雷。",
+ "金蟾齧鏁燒香入,玉虎牽絲汲井迴。",
+ "賈氏窺簾韓掾少,宓妃留枕魏王才。",
+ "春心莫共花爭發,一寸相思一寸灰。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "相思",
+ "女子",
+ "爱情",
+ "七言律诗"
+ ],
+ "title": "無題四首 二",
+ "id": "811a5f9e-0df5-46f1-a587-0d4df7bca5a0"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "乘興南遊不戒嚴,九重誰省諫書函。",
+ "春風舉國裁宮錦,半作障泥半作帆。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "咏史怀古",
+ "讽刺"
+ ],
+ "title": "隋宮",
+ "id": "91f95b30-c42f-470a-9df8-dd6284fca474"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "高閣客竟去,小園花亂飛。",
+ "參差連曲陌,迢遰送斜暉。",
+ "腸斷未忍掃,眼穿仍欲歸。",
+ "芳心向春盡,所得是沾衣。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "咏物",
+ "五言律诗",
+ "写花",
+ "抒怀",
+ "花",
+ "咏物诗"
+ ],
+ "title": "落花",
+ "id": "da157c27-d317-4205-9cdc-a254445659fc"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "爲有雲屏無限嬌,鳳城寒盡怕春宵。",
+ "無端嫁得金龜壻,辜負香衾事早朝。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "怨情"
+ ],
+ "title": "爲有",
+ "id": "974914ee-1fc0-4952-a863-820bad2e4065"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "相見時難別亦難,東風無力百花殘。",
+ "春蠶到死絲方盡,蠟炬成灰淚始乾。",
+ "曉鏡但愁雲鬢改,夜吟應覺月光寒。",
+ "蓬山此去無多路,青鳥殷勤爲探看。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "七言律诗",
+ "八年级下册(课外)",
+ "初中古诗",
+ "爱情"
+ ],
+ "title": "無題",
+ "id": "ed79f9bc-d5d1-4251-b22d-d091e0758a3f"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "瑤池阿母綺窗開,黃竹歌聲動地哀。",
+ "八駿日行三萬里,穆王何事不重來。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "神话",
+ "讽刺"
+ ],
+ "title": "瑤池",
+ "id": "2161a569-6250-4886-9500-2168821b62be"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "悵臥新春白袷衣,白門寥落意多違。",
+ "紅樓隔雨相望冷,珠箔飄燈獨自歸。",
+ "遠路應悲春晼晚,殘宵猶得夢依稀。",
+ "玉璫緘札何由達,萬里雲羅一雁飛。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雨",
+ "七言律诗",
+ "爱情",
+ "雨"
+ ],
+ "title": "春雨",
+ "id": "8331c76b-a250-4d07-9c28-b8a456e0974c"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "雲母屏風燭影深,長河漸落曉星沈。",
+ "常娥應悔偷靈藥,碧海青天夜夜心。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "中秋",
+ "写人",
+ "三年级下册",
+ "七言绝句",
+ "小学古诗"
+ ],
+ "title": "常娥",
+ "id": "f7c9c409-9571-4eb8-abd9-91ec6c1ff564"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "鳳尾香羅薄幾重,碧文圓頂夜深縫。",
+ "扇裁月魄羞難掩,車走雷聲語未通。",
+ "曾是寂寥金燼暗,斷無消息石榴紅。"
+ ],
+ "tags": [
+ "思念",
+ "七言律诗",
+ "唐诗三百首"
+ ],
+ "title": "無題二首 一",
+ "id": "e006dfd3-40be-40ef-ba3b-552428931ae3"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "重帷深下莫愁堂,臥後清宵細細長。",
+ "神女生涯原是夢,小姑居處本無郎。",
+ "風波不信菱枝弱,月露誰教桂葉香。",
+ "直道相思了無益,未妨惆悵是清狂。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "七言律诗",
+ "抒情",
+ "爱情"
+ ],
+ "title": "無題二首 二",
+ "id": "b6475dea-1d69-4f0d-b98a-39393f47741e"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "宣室求賢訪逐臣,賈生才調更無倫。",
+ "可憐夜半虛前席,不問蒼生問鬼神。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "托古讽今",
+ "七言绝句",
+ "怀才不遇"
+ ],
+ "title": "賈生",
+ "id": "02456f60-6ab5-4de8-b9ed-f7519818f0ca"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "客去波平檻,蟬休露滿枝。",
+ "永懷當此節,倚立自移時。",
+ "北斗兼春遠,南陵寓使遲。",
+ "天涯占夢數,疑誤有新知。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "愁思",
+ "五言律诗"
+ ],
+ "title": "涼思",
+ "id": "15c6cbc9-753a-4f09-86bf-4fe2d8bb1fee"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "殘陽西入崦,茅屋訪孤僧。",
+ "落葉人何在,寒雲路幾層。",
+ "獨敲初夜磬,閑倚一枝藤。",
+ "世界微塵裏,吾寧愛與憎。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "寻访",
+ "五言律诗"
+ ],
+ "title": "北青蘿",
+ "id": "9380d4f6-e19d-4ff1-8309-07f4ede69f7c"
+ },
+ {
+ "author": "李商隱",
+ "paragraphs": [
+ "折戟沈沙鐵未銷,自將磨洗認前朝。",
+ "東風不與周郎便,銅雀春深鎖二喬。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "战争",
+ "八年级下册(课内)",
+ "初中古诗",
+ "七言绝句",
+ "咏史怀古",
+ "怀才不遇"
+ ],
+ "title": "赤壁",
+ "id": "ae660471-84bc-4a58-a438-005b8a99c3c1"
+ },
+ {
+ "author": "韋莊",
+ "paragraphs": [
+ "清瑟怨遙夜,遶弦風雨哀。",
+ "孤燈聞楚角,殘月下章臺。",
+ "芳草已云暮,故人殊未來。",
+ "鄉書不可寄,秋雁又南迴。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "怀人",
+ "思乡",
+ "五言律诗"
+ ],
+ "title": "章臺夜思",
+ "id": "19796737-ec7b-4c33-8df4-a81a7ca2cc10"
+ },
+ {
+ "author": "韋莊",
+ "paragraphs": [
+ "誰謂傷心畫不成,畫人心逐世人情。",
+ "君看六幅南朝事,老木寒雲滿故城。"
+ ],
+ "tags": [
+ "隋・唐・五代",
+ "唐诗三百首",
+ "题画",
+ "七言绝句",
+ "咏史怀古",
+ "带有地名",
+ "地名"
+ ],
+ "title": "金陵圖",
+ "id": "4c5b328d-2ab8-4f4d-a0c2-627b6cb0c92b"
+ },
+ {
+ "author": "韋莊",
+ "paragraphs": [
+ "江雨霏霏江草齊,六朝如夢鳥空啼。",
+ "無情最是臺城柳,依舊煙籠十里堤。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "写景",
+ "抒情",
+ "咏史怀古"
+ ],
+ "title": "臺城",
+ "id": "9154fc22-5e19-45b7-a506-435298bb26fc"
+ },
+ {
+ "author": "薛逢",
+ "paragraphs": [
+ "十二樓中盡曉妝,望仙樓上望君王。",
+ "鎖銜金獸連環冷,水滴銅龍晝漏長。",
+ "雲髻罷梳還對鏡,羅衣欲換更添香。",
+ "遙窺正殿簾開處,袍袴宮人掃御牀。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "宫怨",
+ "七言律诗"
+ ],
+ "title": "宮詞",
+ "id": "f8d102c4-b25b-412f-afce-35db35c339d9"
+ },
+ {
+ "author": "馬戴",
+ "paragraphs": [
+ "露氣寒光集,微陽下楚丘。",
+ "猨啼洞庭樹,人在木蘭舟。",
+ "廣澤生明月,蒼山夾亂流。",
+ "雲中君不降,竟夕自悲秋。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "写景",
+ "五言律诗",
+ "怀古"
+ ],
+ "title": "楚江懷古三首 一",
+ "id": "7456691c-636e-46d7-9093-edc778b0a7ea"
+ },
+ {
+ "author": "馬戴",
+ "paragraphs": [
+ "灞原風雨定,晚見鴈行頻。",
+ "落葉他鄉樹,寒燈獨夜人。",
+ "空園白露滴,孤壁野僧隣。",
+ "寄臥郊扉久,何門致此身。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "秋天",
+ "五言律诗",
+ "怀才不遇"
+ ],
+ "title": "灞上秋居",
+ "id": "f2f04c30-149d-4b2b-bce1-1334adf84739"
+ },
+ {
+ "author": "鄭畋",
+ "paragraphs": [
+ "[玄]宗回馬楊妃死,雲雨雖亡日月新。",
+ "終是聖明天子事,景陽宮井又何人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "带有地名",
+ "咏史",
+ "地名"
+ ],
+ "title": "馬嵬坡",
+ "id": "7e75853a-bf7a-4d8d-b3e9-0537f4ed913a"
+ },
+ {
+ "author": "張籍",
+ "paragraphs": [
+ "前年伐月支,城上沒全師。",
+ "蕃漢斷消息,死生長別離。",
+ "無人收廢帳,歸馬識殘旗。",
+ "欲祭疑君在,天涯哭此時。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "怀人",
+ "五言律诗"
+ ],
+ "title": "沒蕃故人",
+ "id": "5ccd5188-0e5d-450e-8ec5-d6e661fac4f7"
+ },
+ {
+ "author": "金昌緒",
+ "paragraphs": [
+ "打起黃鶯兒,莫教枝上啼。",
+ "啼時驚妾夢,不得到遼西。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "怀人",
+ "五言绝句",
+ "闺怨"
+ ],
+ "title": "春怨",
+ "id": "b41e25f5-5a57-497f-a3a2-84bdfb9331f5"
+ },
+ {
+ "author": "元稹",
+ "paragraphs": [
+ "謝公最小偏憐女,嫁與黔婁百事乖。",
+ "顧我無衣搜畫篋,泥他沽酒拔金釵。",
+ "野蔬充膳甘長藿,落葉添薪仰古槐。",
+ "今日俸錢過十萬,與君營奠復營齋。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "悼亡"
+ ],
+ "title": "遣悲懷三首 一",
+ "id": "8b57e03d-6d1f-4a4a-ba34-1f58e2551ca1"
+ },
+ {
+ "author": "元稹",
+ "paragraphs": [
+ "昔日戲言身後意,今朝皆到眼前來。",
+ "衣裳已施行看盡,針線猶存未忍開。",
+ "尚想舊情憐婢僕,也曾因夢送錢財。",
+ "誠知此恨人人有,貧賤夫妻百事哀。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "追忆",
+ "悼亡",
+ "七言律诗"
+ ],
+ "title": "遣悲懷三首 二",
+ "id": "ea3f91f0-487e-4df7-826a-3692ef82e64d"
+ },
+ {
+ "author": "元稹",
+ "paragraphs": [
+ "閑坐悲君亦自悲,百年都是幾多時。",
+ "鄧攸無子尋知命,潘岳悼亡猶費詞。",
+ "同穴窅冥何所望,他生緣會更難期。",
+ "唯將終夜長開眼,報荅平生未展眉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "悼亡"
+ ],
+ "title": "遣悲懷三首 三",
+ "id": "82f57261-b608-4ee9-8d88-7fde20c49a7a"
+ },
+ {
+ "author": "元稹",
+ "paragraphs": [
+ "寥落古行宮,宮花寂寞紅。",
+ "白頭宮女在,閑坐說玄宗。"
+ ],
+ "tags": [
+ "隋・唐・五代",
+ "怀古",
+ "宫怨",
+ "唐诗三百首",
+ "五言绝句"
+ ],
+ "title": "行宮",
+ "id": "02943f6d-2c91-4d9c-9690-247eab9ae3ec"
+ },
+ {
+ "author": "西鄙人",
+ "paragraphs": [
+ "北斗七星高,哥舒夜帶刀。",
+ "至今窺牧馬,不敢過臨洮。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "赞颂",
+ "五言绝句",
+ "民歌"
+ ],
+ "title": "哥舒歌",
+ "id": "ac2ec0ba-10b1-4ae3-8859-1e7752625331"
+ },
+ {
+ "author": "無名氏",
+ "paragraphs": [
+ "近寒食雨草萋萋,著麥苗風柳映堤。",
+ "早是有家歸未得,杜鵑休向耳邊啼。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "寒食节",
+ "七言绝句",
+ "思乡",
+ "感伤"
+ ],
+ "title": "雜詩 十三",
+ "id": "31e9a6b0-5035-4b1b-a9d3-ff1c279f53b6"
+ },
+ {
+ "author": "沈佺期",
+ "paragraphs": [
+ "聞道黃龍戍,頻年不解兵。",
+ "可憐閨裏月,長在漢家營。",
+ "少婦今春意,良人昨夜情。",
+ "誰能將旗鼓,一爲取龍城。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "妇女",
+ "五言律诗",
+ "相思"
+ ],
+ "title": "雜詩三首 三",
+ "id": "1fb10ea4-f46c-4c08-bd06-f039f1907be6"
+ },
+ {
+ "author": "沈佺期",
+ "paragraphs": [
+ "盧家少婦鬱金堂,海燕雙棲玳瑁梁。",
+ "九月寒砧催木葉,十年征戍憶遼陽。",
+ "白狼河北音書斷,丹鳳城南秋夜長。",
+ "誰謂含愁獨不見,更教明月照流黃。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "闺怨",
+ "乐府"
+ ],
+ "title": "古意呈補闕喬知之",
+ "id": "b9521e2b-963e-4d90-acf3-d87c2a135071"
+ },
+ {
+ "author": "王灣",
+ "paragraphs": [
+ "客路青山外,行舟綠水前。",
+ "潮平兩岸闊,風正一帆懸。",
+ "海日生殘夜,江春入舊年。",
+ "鄉書何處達,歸雁洛陽邊。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七年级上册(课内)",
+ "地点",
+ "五言律诗",
+ "初中古诗",
+ "哲理",
+ "思乡"
+ ],
+ "title": "次北固山下",
+ "id": "5bb5c590-5de8-4bfb-aee6-30bc4bcdf99a"
+ },
+ {
+ "author": "張旭",
+ "paragraphs": [
+ "隱隱飛橋隔野煙,石磯西畔問漁船。",
+ "桃花盡日隨流水,洞在清谿何處邊。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "描写水",
+ "写景",
+ "抒情",
+ "七言绝句",
+ "写水"
+ ],
+ "title": "桃花谿",
+ "id": "1f28ab03-8388-4a2f-8c4f-3ce47789e221"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "下馬飲君酒,問君何所之。",
+ "君言不得意,歸臥南山陲。",
+ "但去莫復問,白雲無盡時。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "抒情",
+ "写景",
+ "友情",
+ "送别"
+ ],
+ "title": "送別",
+ "id": "31cc87f3-da0f-421d-8674-8753530077e2"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "聖代無隱者,英靈盡來歸。",
+ "遂令東山客,不得顧采薇。",
+ "既至君門遠,孰云吾道非。",
+ "江淮度寒食,京洛縫春衣。",
+ "置酒臨長道,同心與我違。",
+ "行當浮桂櫂,未幾拂荆扉。",
+ "遠樹帶行客,孤村當落暉。",
+ "吾謀適不用,勿謂知音稀。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "五言古诗",
+ "慰勉",
+ "友情"
+ ],
+ "title": "送綦毋潛落第還鄉",
+ "id": "56bec3c2-9c0c-4d40-9750-2e28bdc9a20e"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "言入黃花川,每逐青谿水。",
+ "隨山將萬轉,趣途無百里。",
+ "聲喧亂石中,色靜深松裏。",
+ "漾漾汎菱荇,澄澄映葭葦。",
+ "我心素已閑,清川澹如此。",
+ "請留盤石上,垂釣將已矣。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "托物寄情",
+ "山水"
+ ],
+ "title": "青谿",
+ "id": "a0576d39-c4ac-4608-a4e2-249b6c31fd1d"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "斜陽照墟落,窮巷牛羊歸。",
+ "野老念牧童,倚杖候荆扉。",
+ "雉雊麥苗秀,蠶眠桑葉稀。",
+ "田夫荷鋤至,相見語依依。",
+ "即此羨閑逸,悵然吟式微。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "归隐",
+ "田园"
+ ],
+ "title": "渭川田家",
+ "id": "204e287b-8194-4c2d-808b-11cc260412ae"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "豔色天下重,西施寧久微。",
+ "朝仍越溪女,暮作吳宮妃。",
+ "賤日豈殊衆,貴來方悟稀。",
+ "邀人傅香粉,不自著羅衣。",
+ "君寵益嬌態,君憐無是非。",
+ "當時浣紗伴,莫得同車歸。",
+ "持謝鄰家子,效顰安可希。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "五言古诗",
+ "怀古",
+ "写人",
+ "怀才不遇"
+ ],
+ "title": "西施詠",
+ "id": "4d6f83e9-8232-4db1-903e-f9e619df5ecb"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "少年十五二十時,步行奪得胡馬射。",
+ "射殺中山白額虎,肯數鄴下黃鬚兒。",
+ "一身轉戰三千里,一劒曾當百萬師。",
+ "漢兵奮迅如霹靂,虜騎崩騰畏蒺藜。",
+ "衞青不敗由天幸,李廣無功緣數奇。",
+ "自從棄置便衰朽,世事磋跎成白首。",
+ "昔時飛箭無全目,今日垂楊生左肘。",
+ "路傍時賣故侯瓜,門前學種先生柳。",
+ "蒼茫古木連窮巷,寥落寒山對虛牖。",
+ "誓令疏勒出飛泉,不似潁川空使酒。",
+ "賀蘭山下陣如雲,羽檄交馳日夕聞。",
+ "節使三河募年少,詔書五道出將軍。",
+ "試拂鐵衣如雪色,聊持寶劒動星文。",
+ "願得燕弓射天將,恥令越甲鳴吳軍。",
+ "莫嫌舊日雲中守,猶堪一戰取功勳。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "乐府",
+ "写人",
+ "新乐府辞",
+ "赞颂"
+ ],
+ "title": "老將行",
+ "id": "d5d66aff-b6ae-48cc-9160-16b096d4ea49"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "漁舟逐水愛山春,兩岸桃花夾去津。",
+ "坐看紅樹不知遠,行盡青溪不見人。",
+ "山口潛行始隈隩,山開曠望旋平陸。",
+ "遙看一處攢雲樹,近入千家散花竹。",
+ "樵客初傳漢姓名,居人未改秦衣服。",
+ "居人共住武陵源,還從物外起田園。",
+ "月明松下房櫳靜,日出雲中雞犬喧。",
+ "驚聞俗客爭來集,競引還家問都邑。",
+ "平明閭巷埽花開,薄暮漁樵乘水入。",
+ "初因避地去人間,及至成仙遂不還。",
+ "峽裏誰知有人事,世中遙望空雲山。",
+ "不疑靈境難聞見,塵心未盡思鄉縣。",
+ "出洞無論隔山水,辭家終擬長游衍。",
+ "自謂經過舊不迷,安知峰壑今來變。",
+ "當時只記入山深,青溪幾曲到雲林。",
+ "春來遍是桃花水,不辨仙源何處尋。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "乐府",
+ "新乐府辞",
+ "水"
+ ],
+ "title": "桃源行",
+ "id": "3b847cf5-16b1-449d-a436-e5995c6433c4"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "洛陽女兒對門居,纔可容顏十五餘。",
+ "良人玉勒乘驄馬,侍女金盤鱠鯉魚。",
+ "畫閣朱樓盡相望,紅桃綠柳垂簷向。",
+ "羅幃送上七香車,寶扇迎歸九華帳。",
+ "狂夫富貴在青春,意氣驕奢劇季倫。",
+ "自憐碧玉親教舞,不惜珊瑚持與人。",
+ "春窗曙滅九微火,九微片片飛花璅。",
+ "戲罷曾無理曲時,妝成秪是薰香坐。",
+ "城中相識盡繁華,日夜經過趙李家。",
+ "誰憐越女顏如玉,貧賤江頭自浣紗。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "女子",
+ "乐府",
+ "新乐府辞"
+ ],
+ "title": "洛陽女兒行",
+ "id": "4925c861-b0f8-4b2f-923e-9cba2df1323e"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "寒山轉蒼翠,秋水日潺湲。",
+ "倚杖柴門外,臨風聽暮蟬。",
+ "渡頭餘落日,墟里上孤煙。",
+ "復值接輿醉,狂歌五柳前。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "抒情",
+ "秋天",
+ "五言律诗"
+ ],
+ "title": "輞川閑居贈裴秀才迪",
+ "id": "a2b4e8ca-92e3-4101-8a54-739b8f1d8184"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "晚年唯好靜,萬事不關心。",
+ "自顧無長策,空知返舊林。",
+ "松風吹解帶,山月照彈琴。",
+ "君問窮通理,漁歌入浦深。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "赠别",
+ "友情",
+ "规劝",
+ "五言律诗"
+ ],
+ "title": "酬張少府",
+ "id": "65e9c24b-bdbd-4ef9-9e14-012c8fde79b8"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "萬壑樹參天,千山響杜鵑。",
+ "山中一夜雨,樹杪百重泉。",
+ "漢女輸橦布,巴人訟芋田。",
+ "文翁翻教授,不敢倚先賢。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友情",
+ "送别",
+ "五言律诗"
+ ],
+ "title": "送梓州李使君",
+ "id": "263b4a33-fd51-47e1-b49a-d84dd99a2e0f"
+ },
+ {
+ "author": "權德輿",
+ "paragraphs": [
+ "昨夜裙帶解,今朝蟢子飛。",
+ "鉛華不可棄,莫是藁砧歸。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "五言绝句",
+ "女子"
+ ],
+ "title": "玉臺體十二首 十一",
+ "id": "9a5070c8-8b6a-46d3-a976-ce942baf42e7"
+ },
+ {
+ "author": "韓愈",
+ "paragraphs": [
+ "山石犖確行徑微,黃昏到寺蝙蝠飛。",
+ "升堂坐階新雨足,芭蕉葉大支子肥。",
+ "僧言古壁佛畫好,以火來照所見稀。",
+ "鋪牀拂席置羹飯,疎糲亦足飽我飢。",
+ "夜深靜臥百蟲絕,清月出嶺光入扉。",
+ "天明獨去無道路,出入高下窮煙霏。",
+ "山紅澗碧紛爛漫,時見松櫪皆十圍。",
+ "當流赤足蹋澗石,水聲激激風吹衣。",
+ "人生如此自可樂,豈必局束爲人鞿。",
+ "嗟哉吾黨二三子,安得至老不更歸。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "纪游",
+ "写景",
+ "抒情",
+ "七言古诗"
+ ],
+ "title": "山石",
+ "id": "e73e2743-3752-4ba3-938a-fb59cd3965ee"
+ },
+ {
+ "author": "韓愈",
+ "paragraphs": [
+ "纖雲四卷天無河,清風吹空月舒波。",
+ "沙平水息聲影絕,一桮相屬君當歌。",
+ "君歌聲酸辭且苦,不能聽終淚如雨。",
+ "洞庭連天九疑高,蛟龍出沒猩鼯號。",
+ "十生九死到官所,幽居默默如藏逃。",
+ "下牀畏蛇食畏藥,海氣濕蟄熏腥臊。",
+ "昨者州前搥大鼓,嗣皇繼聖登夔臯。",
+ "赦書一日行萬里,罪從大辟皆除死。",
+ "遷者追廻流者還,滌瑕蕩垢清朝班。",
+ "州家申名使家抑,坎軻祗得移荆蠻。",
+ "判司卑官不堪說,未免捶楚塵埃間。",
+ "同時輩流多上道,天路幽險難追攀。",
+ "君歌且休聽我歌,我歌今與君殊科。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "中秋节",
+ "七言古诗",
+ "抒怀",
+ "饮酒"
+ ],
+ "title": "八月十五夜贈張功曹",
+ "id": "88e98bed-27fa-4dbe-8d6e-e7cb8c3e4d97"
+ },
+ {
+ "author": "韓愈",
+ "paragraphs": [
+ "五嶽祭秩皆三公,四方環鎮嵩當中。",
+ "火維地荒足妖怪,天假神柄專其雄。",
+ "噴雲泄霧藏半腹,雖有絕頂誰能窮。",
+ "我來正逢秋雨節,陰氣晦昧無清風。",
+ "潛心默禱若有應,豈非正直能感通。",
+ "須臾靜掃衆峰出,仰見突兀撐青空。",
+ "紫蓋連延接天柱,石廩騰擲堆祝融。",
+ "森然魄動下馬拜,松柏一逕趨靈宮。",
+ "粉牆丹柱動光彩,鬼物圖畫填青紅。",
+ "升階傴僂薦脯酒,欲以菲薄明其衷。",
+ "廟令老人識神意,睢盱偵伺能鞠躬。",
+ "手持桮珓導我擲,云此最吉餘難同。",
+ "竄逐蠻荒幸不死,衣食纔足甘長終。",
+ "侯王將相望久絕,神縱欲福難爲功。",
+ "夜投佛寺上高閣,星月掩暎雲朣朧。",
+ "猿鳴鐘動不知曙,杲杲寒日生於東。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "登高",
+ "七言古诗",
+ "寺庙"
+ ],
+ "title": "謁衡嶽廟遂宿嶽寺題門樓",
+ "id": "14ae27a0-2641-41e6-8c0a-60a9185f8b6d"
+ },
+ {
+ "author": "韓偓",
+ "paragraphs": [
+ "碧闌干外繡簾垂,猩血屏風畫折枝。",
+ "八尺龍鬚方錦褥,已涼天氣未寒時。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "爱情",
+ "写景",
+ "婉约"
+ ],
+ "title": "已涼",
+ "id": "1deac6d5-6da0-4d9f-a182-2f0697cbb932"
+ },
+ {
+ "author": "杜荀鶴",
+ "paragraphs": [
+ "早被嬋娟誤,欲妝臨鏡慵。",
+ "承恩不在貌,教妾若爲容。",
+ "風暖鳥聲碎,日高花影重。",
+ "年年越溪女,相憶採芙蓉。"
+ ],
+ "tags": [
+ "宫怨",
+ "春天",
+ "写景",
+ "女子",
+ "唐诗三百首",
+ "五言律诗"
+ ],
+ "title": "春宮怨",
+ "id": "8eaf97fd-82bb-4651-9dde-9a5a6f78182c"
+ },
+ {
+ "author": "朱慶餘",
+ "paragraphs": [
+ "洞房昨夜停紅燭,待曉堂前拜舅姑。",
+ "[妝]罷低聲問夫壻,畫眉深淺入時無。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "新婚",
+ "七言绝句",
+ "考试"
+ ],
+ "title": "近試上張籍水部",
+ "id": "27ff54ab-4861-43d8-b3c1-69113d2e4e10"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "清時有味是無能,閑愛孤雲靜愛僧。",
+ "欲把一麾江海去,樂遊原上望昭陵。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "爱国"
+ ],
+ "title": "將赴吳興登樂遊原一絕",
+ "id": "24ca9268-9cbd-423b-b423-12e1a9aa9041"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "折戟沈沙鐵未銷,自將磨洗認前朝。",
+ "東風不與周郎便,銅雀春深鏁二喬。"
+ ],
+ "tags": [
+ "隋・唐・五代",
+ "战争",
+ "八年级下册(课内)",
+ "唐诗三百首",
+ "初中古诗",
+ "七言绝句",
+ "咏史怀古",
+ "怀才不遇"
+ ],
+ "title": "赤壁",
+ "id": "7c93c162-d424-4d91-b71b-7c9b81713b8c"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "煙籠寒水月籠沙,夜泊秦淮近酒家。",
+ "商女不知亡國恨,隔江猶唱後庭花。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "爱国",
+ "写景",
+ "初中古诗",
+ "七言绝句",
+ "抒怀",
+ "咏史怀古",
+ "七年级上册(课外)"
+ ],
+ "title": "泊秦淮",
+ "id": "8de30193-fba7-4aef-9ff9-c3726a22104d"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "娉娉褭褭十三餘,豆蔻梢頭二月初。",
+ "春風十里揚州路,卷上珠簾總不如。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "贈別二首 一",
+ "id": "54008cd0-f9bf-424f-bc55-f15c1e9025f3"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "多情却似總無情,唯覺尊前笑不成。",
+ "蠟燭有心還惜別,替人垂淚到天明。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "离别",
+ "七言绝句",
+ "惜别"
+ ],
+ "title": "贈別二首 二",
+ "id": "a7f425de-143b-493d-bb51-13e27c68e871"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "落魄江南載酒行,楚腰腸斷掌中輕。",
+ "十年一覺揚州夢,贏得青樓薄倖名。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "抒怀"
+ ],
+ "title": "遣懷",
+ "id": "92ae04dc-89af-45d6-9e31-204a01df801d"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "紅燭秋光冷畫屏,輕羅小扇撲流螢。",
+ "天階夜色涼如水,坐看牽牛織女星。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "秋夕",
+ "id": "d12edd29-d467-4b19-b3f5-533fa5c8d56a"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "繁華事散逐香塵,流水無情草自春。",
+ "日暮東風怨啼鳥,落花猶似墮樓人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "怀古"
+ ],
+ "title": "金谷園",
+ "id": "80159919-0b66-4956-a563-ef9bcf6a05c0"
+ },
+ {
+ "author": "杜牧",
+ "paragraphs": [
+ "旅館無良伴,凝情自悄然。",
+ "寒燈思舊事,斷鴈警愁眠。",
+ "遠夢歸侵曉,家書到隔年。",
+ "湘江好煙月,門繫釣魚船。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "羁旅",
+ "思乡",
+ "五言律诗"
+ ],
+ "title": "旅宿",
+ "id": "200fe9fa-0221-4123-86de-4d63ce9d3daf"
+ },
+ {
+ "author": "許渾",
+ "paragraphs": [
+ "遙夜泛清瑟,西風生翠蘿。",
+ "殘螢委玉露,早鴈拂銀河。",
+ "高樹曉還密,遠山晴更多。",
+ "淮南一葉下,自覺老煙波。"
+ ],
+ "tags": [
+ "写景",
+ "秋天",
+ "唐诗三百首",
+ "五言律诗"
+ ],
+ "title": "早秋三首 一",
+ "id": "fedb06cc-37da-4aaf-8f39-85ebcb08b947"
+ },
+ {
+ "author": "許渾",
+ "paragraphs": [
+ "紅葉晚蕭蕭,長亭酒一瓢。",
+ "殘雲歸太華,疎雨過中條。",
+ "樹色隨山迥,河聲入海遙。",
+ "帝鄉明日到,猶自夢漁樵。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "送别",
+ "五言律诗"
+ ],
+ "title": "秋日赴闕題潼關驛樓",
+ "id": "292c14cd-151f-4321-a94c-46fc86693eb3"
+ },
+ {
+ "author": "張泌",
+ "paragraphs": [
+ "別夢依依到謝家,小廊迴合曲闌斜。",
+ "多情只有春庭月,猶爲離人照落花。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "七言绝句",
+ "月亮",
+ "爱情"
+ ],
+ "title": "寄人 一",
+ "id": "afc2e6d6-e67f-4513-a6ab-82841d2f97cc"
+ },
+ {
+ "author": "陳陶",
+ "paragraphs": [
+ "誓掃匈奴不顧身,五千貂錦喪胡塵。",
+ "可憐無定河邊骨,猶是春閨夢裏人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "战争",
+ "妇女",
+ "边塞",
+ "七言绝句"
+ ],
+ "title": "隴西行四首 二",
+ "id": "fde30eaa-9b2c-47ce-ba49-c1356353ac9a"
+ },
+ {
+ "author": "釋明辯",
+ "paragraphs": [
+ "打起黄鶯兒,莫教枝上啼。",
+ "幾回驚妾夢,不得到遼西。"
+ ],
+ "tags": [
+ "怀人",
+ "五言绝句",
+ "闺怨",
+ "唐诗三百首"
+ ],
+ "title": "頌古三十二首 其二三",
+ "id": "228c87f2-092b-411b-9029-c5454cc2cb1c"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "淚濕羅巾夢不成,夜深前殿按歌聲。",
+ "紅顏未老恩先斷,斜倚薰籠坐到明。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "宫怨"
+ ],
+ "title": "後宮詞",
+ "id": "549a5a94-a8f5-436e-bc9c-1253d01c1940"
+ },
+ {
+ "author": "李益",
+ "paragraphs": [
+ "十年離亂後,長大一相逢。",
+ "問姓驚初見,稱名憶舊容。",
+ "別來滄海事,語罷暮天鐘。",
+ "明日巴陵道,秋山又幾重。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "离别",
+ "五言律诗"
+ ],
+ "title": "喜見外弟又言別",
+ "id": "d755f74b-18fd-44d1-a6e1-43e77490bb09"
+ },
+ {
+ "author": "李益",
+ "paragraphs": [
+ "嫁得瞿塘賈,朝朝悞妾期。",
+ "早知潮有信,嫁與弄潮兒。"
+ ],
+ "tags": [
+ "闺怨",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "江南詞",
+ "id": "9bd49546-5289-4351-97f4-d69296720dfc"
+ },
+ {
+ "author": "李益",
+ "paragraphs": [
+ "回樂峰前沙似雪,受降城下月如霜。",
+ "不知何處吹蘆管,一夜征人盡望鄉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "战争",
+ "冬天",
+ "边塞",
+ "七言绝句",
+ "月亮"
+ ],
+ "title": "夜上受降城聞笛",
+ "id": "992df222-c445-4925-a656-0caa4447d5bb"
+ },
+ {
+ "author": "李端",
+ "paragraphs": [
+ "鳴箏金粟柱,素手玉房前。",
+ "欲得周郎顧,時時誤拂弦。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "音乐"
+ ],
+ "title": "聽箏",
+ "id": "7f3799de-78b4-49bc-b6d8-a5fa3ecacd33"
+ },
+ {
+ "author": "司空曙",
+ "paragraphs": [
+ "世亂同南去,時清獨北還。",
+ "他鄉生白髮,舊國見青山。",
+ "曉月過殘壘,繁星宿故關。",
+ "寒禽與衰草,處處伴愁顏。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "酬赠",
+ "感伤",
+ "五言律诗"
+ ],
+ "title": "賊平後送人北歸",
+ "id": "4c745990-657a-41fe-ba2c-d9cf371ca563"
+ },
+ {
+ "author": "司空曙",
+ "paragraphs": [
+ "故人江海別,幾度隔山川。",
+ "乍見翻疑夢,相悲各問年。",
+ "孤燈寒照雨,溼竹暗浮煙。",
+ "更有明朝恨,離杯惜共傳。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友情",
+ "五言律诗",
+ "惜别"
+ ],
+ "title": "雲陽館與韓紳宿別",
+ "id": "3138fe30-6cf6-45b0-a441-2d5398107a9a"
+ },
+ {
+ "author": "司空曙",
+ "paragraphs": [
+ "靜夜四無鄰,荒居舊業貧。",
+ "雨中黃葉樹,燈下白頭人。",
+ "以我獨沈久,愧君相見頻。",
+ "平生自有分,況是蔡家親。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "五言律诗",
+ "年老"
+ ],
+ "title": "喜外弟盧綸見宿",
+ "id": "f0cd6afd-c89d-4fc3-b214-bcbf63042e8e"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "泠泠七絲上,靜聽松風寒。",
+ "古調雖自愛,今人多不彈。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "抒情",
+ "音乐",
+ "五言绝句"
+ ],
+ "title": "聽彈琴",
+ "id": "72aab9ef-65a9-4a8a-b36b-61932c09ce85"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "孤雲將野鶴,豈向人間住。",
+ "莫買沃洲山,時人已知處。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "五言绝句"
+ ],
+ "title": "送方外上人",
+ "id": "c26445b5-a433-4cd7-9020-435866b845f9"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "蒼蒼竹林寺,杳杳鐘聲晚。",
+ "荷笠帶夕陽,青山獨歸遠。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "山水",
+ "抒情",
+ "送别",
+ "初中古诗",
+ "即景抒情",
+ "五言绝句",
+ "七年级下册(课外)"
+ ],
+ "title": "送靈澈上人",
+ "id": "a746d1f0-a72c-44d2-8b1c-c5c990749257"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "鄉心新歲切,天畔獨潸然。",
+ "老至居人下,春歸在客先。",
+ "嶺猨同旦暮,江柳共風煙。",
+ "已是長沙傅,從今又幾年。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言律诗",
+ "春节",
+ "思乡"
+ ],
+ "title": "新年作",
+ "id": "6556f501-f5b9-4353-bde2-75ec0e1bb616"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "古臺搖落後,秋日望鄉心。",
+ "野寺人來少,雲峰水隔深。",
+ "夕陽依舊壘,寒磬滿空林。",
+ "惆悵南朝事,長江獨至今。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "怀古",
+ "秋天",
+ "五言律诗"
+ ],
+ "title": "秋日登吳公臺上寺遠眺寺即陳將吳明徹戰場",
+ "id": "fdc07d9f-95b8-45f7-b1b3-9d272b3416a4"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "一路經行處,莓苔見履痕。",
+ "白雲依靜渚,春草閉閑門。",
+ "過雨看松色,隨山到水源。",
+ "溪花與禪意,相對亦忘言。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "五言律诗"
+ ],
+ "title": "尋南溪常山道人隱居",
+ "id": "04af557e-bac8-4fbf-92e7-d8a87fa11038"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "望君煙水闊,揮手淚霑巾。",
+ "飛鳥沒何處,青山空向人。",
+ "長江一帆遠,落日五湖春。",
+ "誰見汀洲上,相思愁白蘋。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "友情",
+ "送别",
+ "五言律诗"
+ ],
+ "title": "餞別王十一南遊",
+ "id": "1487452b-5f49-4f4f-9913-1f3ec1cddf95"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "生涯豈料承優詔,世事空知學醉歌。",
+ "江上月明胡鴈過,淮南木落楚山多。",
+ "寄身且喜滄洲近,顧影無如白髮何。",
+ "今日龍鍾人共棄,媿君猶遣慎風波。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "七言律诗"
+ ],
+ "title": "江州重別薛六柳八二員外",
+ "id": "d9dd3520-9aab-4b1b-bd57-810f29391667"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "三年謫宦此棲遲,萬古惟留楚客悲。",
+ "秋草獨尋人去後,寒林空見日斜時。",
+ "漢文有道恩猶薄,湘水無情弔豈知。",
+ "寂寂江山搖落處,憐君何事到天涯。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "迁谪",
+ "生活",
+ "怀古",
+ "七言律诗"
+ ],
+ "title": "長沙過賈誼宅",
+ "id": "8cefa67f-2b9a-4e13-8158-4d38cbfb331b"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "[汀]洲無浪復無煙,楚客相思益渺然。",
+ "漢口夕陽斜渡鳥,洞庭秋水遠連天。",
+ "孤城背嶺寒吹角,獨戍臨江夜泊船。",
+ "賈誼上書憂漢室,長沙謫去古今憐。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "贬谪",
+ "七言律诗",
+ "触景感怀"
+ ],
+ "title": "自夏口至鸚鵡洲夕望岳陽寄源中丞",
+ "id": "3f049d66-1513-407f-bb4e-cf25a7b5069a"
+ },
+ {
+ "author": "劉長卿",
+ "paragraphs": [
+ "鶯啼燕語報新年,馬邑龍堆路幾千。",
+ "家住層城臨漢苑,[心]隨明月到胡天。",
+ "機中錦字論長恨,樓上花枝笑獨眠。",
+ "爲問元戎竇車騎,何時返斾勒燕然。"
+ ],
+ "tags": [
+ "思念",
+ "七言律诗",
+ "春天",
+ "妇女",
+ "唐诗三百首",
+ "闺怨"
+ ],
+ "title": "賦得",
+ "id": "41d292d2-b752-4c30-8b6b-7b7380f2f9bd"
+ },
+ {
+ "author": "崔曙",
+ "paragraphs": [
+ "漢文皇帝有高臺,此日登臨曙色開。",
+ "三晉雲山皆北向,二陵風雨自東來。",
+ "關門令尹誰能識,河上仙翁去不回。",
+ "且欲近尋彭澤宰,陶然共醉菊花杯。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "写景",
+ "登高",
+ "重阳节"
+ ],
+ "title": "九日登望仙臺呈劉明府容",
+ "id": "1c1d545e-07a1-46a9-bb76-b7473c1fde43"
+ },
+ {
+ "author": "王翰",
+ "paragraphs": [
+ "葡萄美酒夜光杯,欲飲琵琶馬上催。",
+ "醉臥沙場君莫笑,古來征戰幾人回。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "豪放",
+ "战争",
+ "边塞",
+ "宴饮",
+ "七言绝句"
+ ],
+ "title": "涼州詞二首 一",
+ "id": "0445f296-5816-4ab9-a617-7d8ed29088ea"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "北山白雲裏,隱者自怡悅。",
+ "相望試登高,心飛逐鳥滅。",
+ "愁因薄暮起,興是清秋發。",
+ "時見歸村人,沙行渡頭歇。",
+ "天邊樹若薺,江畔舟如月。",
+ "何當載酒來,共醉重陽節。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "秋天",
+ "登高",
+ "山",
+ "怀人"
+ ],
+ "title": "秋登蘭山寄張五",
+ "id": "56ca5b29-4bb9-4f12-9200-6bc7c72e4e47"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "山光忽西落,池月漸東上。",
+ "散髮乘夕涼,開軒臥閑敞。",
+ "荷風送香氣,竹露滴清響。",
+ "欲取鳴琴彈,恨無知音賞。",
+ "感此懷故人,中宵勞夢想。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "友情",
+ "夏天",
+ "怀人"
+ ],
+ "title": "夏日南亭懷辛大",
+ "id": "f8afed4e-e8cd-467f-afae-51dc149921f7"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "夕陽度西嶺,羣壑倏已暝。",
+ "松月生夜涼,風泉滿清聽。",
+ "樵人歸欲盡,煙鳥棲初定。",
+ "之子期宿來,孤琴候蘿逕。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "写景"
+ ],
+ "title": "宿業師山房期丁大不至",
+ "id": "3e504df9-b22d-480e-9e7c-9f8ab8591b91"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "山寺鐘鳴晝已昏,漁梁渡頭爭渡喧。",
+ "人隨沙路向江村,余亦乘舟歸鹿門。",
+ "鹿門月照開煙樹,忽到龐公棲隱處。",
+ "巖扉松徑長寂寥,惟有幽人夜來去。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "归隐",
+ "七言古诗"
+ ],
+ "title": "夜歸鹿門山歌",
+ "id": "cc0e7ac3-bde4-4491-8ae5-027106709e72"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "八月湖水平,涵虛混太清。",
+ "氣蒸雲夢澤,波撼岳陽城。",
+ "欲濟無舟楫,端居恥聖明。",
+ "坐觀垂釣者,空有羨魚情。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "描写水",
+ "山水",
+ "八年级上册(课外)",
+ "五言律诗",
+ "初中古诗",
+ "水",
+ "带有地名",
+ "写水",
+ "地名"
+ ],
+ "title": "望洞庭湖贈張丞相",
+ "id": "6975adab-25d0-40c6-af3a-3a05847e7717"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "一丘常欲臥,三徑苦無資。",
+ "北土非吾願,東林懷我師。",
+ "黃金然桂盡,壯志逐年衰。",
+ "日夕涼風至,聞蟬但益悲。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "归隐",
+ "抒情",
+ "五言律诗"
+ ],
+ "title": "秦中感秋寄遠上人",
+ "id": "0613f575-4f07-4326-8591-603fb3d1da45"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "山暝聞猿愁,滄江急夜流。",
+ "風鳴兩岸葉,月照一孤舟。",
+ "建德非吾土,維揚憶舊遊。",
+ "還將兩行淚,遙寄海西頭。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "描写水",
+ "五言律诗",
+ "怀人",
+ "写水"
+ ],
+ "title": "宿桐廬江寄廣陵舊遊",
+ "id": "b1dad78c-4dd8-4b99-b37b-741ed761f26a"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "木落雁南度,北風江上寒。",
+ "我家襄水上,遙隔楚雲端。",
+ "鄉淚客中盡,孤帆天際看。",
+ "迷津欲有問,平海夕漫漫。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "八年级上册(课外)",
+ "思归",
+ "五言律诗",
+ "初中古诗",
+ "思乡"
+ ],
+ "title": "早寒江上有懷",
+ "id": "8e18183e-5a58-4805-b672-1726969d6526"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "寂寂竟何待,朝朝空自歸。",
+ "欲尋芳草去,惜與故人違。",
+ "當路誰相假,知音世所稀。",
+ "秪應守索寞,還掩故園扉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "归隐",
+ "友情",
+ "离别",
+ "哀怨",
+ "五言律诗"
+ ],
+ "title": "留別王侍御維",
+ "id": "7f4cacb5-cde8-40de-9b14-e223879cc2f4"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "林臥愁春盡,開軒覽物華。",
+ "忽逢青鳥使,邀入赤松家。",
+ "丹竈初開火,仙桃正落花。",
+ "童顏若可駐,何惜醉流霞。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言律诗"
+ ],
+ "title": "清明日宴梅道士房",
+ "id": "d2b5090e-e6fe-4d4f-9dd6-fe94ff6d4b5c"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "人事有代謝,往來成古今。",
+ "江山留勝跡,我輩復登臨。",
+ "水落魚梁淺,天寒夢澤深。",
+ "羊公碑字在,讀罷淚沾襟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "吊古伤今",
+ "隋・唐・五代",
+ "五言律诗"
+ ],
+ "title": "與諸子登峴山",
+ "id": "b71d7c9c-7e92-4797-b2c9-8715dd0c2881"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "故人具雞黍,邀我至田家。",
+ "綠樹村邊合,青山郭外斜。",
+ "開筵面場圃,把酒話桑麻。",
+ "待到重陽日,還來就菊花。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "田园",
+ "友情",
+ "初中古诗",
+ "五言律诗",
+ "山",
+ "七年级上册(课外)",
+ "四年级上册"
+ ],
+ "title": "過故人莊",
+ "id": "9309205e-1d08-4a7b-99ef-4a05a9ee4370"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "北闕休上書,南山歸敝廬。",
+ "不才明主棄,多病故人疎。",
+ "白髮催年老,青陽逼歲除。",
+ "永懷愁不寐,松月夜窗虛。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "抒情",
+ "失意"
+ ],
+ "title": "歲暮歸南山",
+ "id": "542cb326-1037-4bd9-b0f6-6479d165dc4c"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "迢遞三巴路,羈危萬里身。",
+ "亂山殘雪夜,孤燈異鄉人。",
+ "漸與骨肉遠,轉於奴僕親。",
+ "那堪正飄泊,來日歲華新。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "羁旅",
+ "五言律诗",
+ "春节",
+ "思乡"
+ ],
+ "title": "歲除夜有懷",
+ "id": "0f5af383-765a-4d56-b00b-79ee2ccd5111"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "春眠不覺曉,處處聞啼鳥。",
+ "夜來風雨聲,花落知多少。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "春",
+ "写景",
+ "一年级下册",
+ "惜春",
+ "五言绝句",
+ "小学古诗"
+ ],
+ "title": "春曉",
+ "id": "cb168b3b-d104-4df7-9868-1e1225ddb941"
+ },
+ {
+ "author": "孟浩然",
+ "paragraphs": [
+ "移舟泊煙渚,日暮客愁新。",
+ "野曠天低樹,江清月近人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "描写水",
+ "写景",
+ "思乡",
+ "水",
+ "月亮",
+ "五言绝句",
+ "写水"
+ ],
+ "title": "宿建德江",
+ "id": "0bed9b02-991e-4e28-b631-86640147a712"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "噫吁戲!危乎高哉!蜀道之難難於上青天!蠶叢及魚鳧,開國何茫然。",
+ "爾來四萬八千歲,不與秦塞通人煙。",
+ "西當太白有鳥道,可以橫絕峨眉巔。",
+ "地崩山摧壯士死,然後天梯石棧相鉤連。",
+ "上有六龍回日之高標,下有衝波逆折之回川。",
+ "黃鶴之飛尚不得過,猨猱欲度愁攀援。",
+ "青泥何盤盤,百步九折縈巖巒。",
+ "捫參歷井仰脅息,以手撫膺坐長歎。",
+ "問君西遊何時還?畏途巉巖不可攀。",
+ "但見悲鳥號古木,雄飛雌從繞林間。",
+ "又聞子規啼夜月,愁空山,蜀道之難難於上青天,使人聽此凋朱顏。",
+ "連峯去天不盈尺,枯松倒挂倚絕壁。",
+ "飛湍瀑流爭喧豗,砅厓轉石萬壑雷。",
+ "其險也如此,嗟爾遠道之人胡爲乎來哉!劒閣崢嶸而崔嵬,一夫當關,萬夫莫開。",
+ "所守或匪親,化爲狼與豺。",
+ "朝避猛虎,夕避長蛇。",
+ "磨牙吮血,殺人如麻。",
+ "錦城雖云樂,不如早還家。",
+ "蜀道之難難於上青天,側身西望長咨嗟。"
+ ],
+ "tags": [
+ "山水",
+ "抒情",
+ "唐诗三百首",
+ "乐府",
+ "相和歌辞"
+ ],
+ "title": "蜀道難",
+ "id": "f15c4b50-ee89-4927-8cf3-15e3a6a6ab95"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "君不見黃河之水天上來,奔流到海不復迴。",
+ "君不見高堂明鏡悲白髮,朝如青絲暮成雪。",
+ "人生得意須盡歡,莫使金樽空對月。",
+ "天生我材必有用,千金散盡還復來。",
+ "烹羊宰牛且爲樂,會須一飲三百盃。",
+ "岑夫子,丹丘生,將進酒,君莫停。",
+ "與君歌一曲,請君爲我側耳聽。",
+ "鐘鼓饌玉不足貴,但願長醉不願醒。",
+ "古來聖賢皆寂寞,惟有飲者留其名。",
+ "陳王昔時宴平樂,斗酒十千恣讙謔。",
+ "主人何爲言少錢,徑須沽取對君酌。",
+ "五花馬,千金裘,呼兒將出換美酒,與爾同銷萬古愁。"
+ ],
+ "tags": [
+ "黄河",
+ "咏物",
+ "抒情",
+ "鼓吹曲辞",
+ "唐诗三百首",
+ "乐府",
+ "宴饮",
+ "哲理",
+ "水",
+ "咏物诗"
+ ],
+ "title": "將進酒",
+ "id": "e5e5f969-ddac-4491-9cfe-d77742e1416d"
+ },
+ {
+ "author": "杜荀鶴",
+ "paragraphs": [
+ "早被嬋娟誤,欲妝臨鏡慵。",
+ "承恩不在貌,教妾若爲容。",
+ "風暖鳥聲碎,日高花影重。",
+ "年年越溪女,相憶採芙蓉。"
+ ],
+ "tags": [
+ "宫怨",
+ "春天",
+ "写景",
+ "女子",
+ "唐诗三百首",
+ "五言律诗"
+ ],
+ "title": "春宮怨",
+ "id": "d3357800-9021-4647-a0a0-98499a0ee3c5"
+ },
+ {
+ "author": "戴叔倫",
+ "paragraphs": [
+ "天秋月又滿,城闕夜千重。",
+ "還作江南會,翻疑夢裏逢。",
+ "風枝驚暗鵲,露草覆寒蛩。",
+ "羈旅長堪醉,相留畏曉鐘。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友情",
+ "写景",
+ "秋天",
+ "五言律诗",
+ "相聚"
+ ],
+ "title": "客夜與故人偶集",
+ "id": "94ae3e7e-eb5d-414c-bfd6-124f50687ce5"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "鷲翎金僕姑,燕尾繡蝥弧。",
+ "獨立揚新令,千營共一呼。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "将军",
+ "乐府",
+ "高中古诗",
+ "边塞",
+ "赞美"
+ ],
+ "title": "和張僕射塞下曲 一",
+ "id": "ce491725-b96b-46a7-b83c-463d2094ef3f"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "林暗草驚風,將軍夜引弓。",
+ "平明尋白羽,沒在石稜中。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "将军",
+ "乐府",
+ "边塞",
+ "赞美",
+ "狩猎"
+ ],
+ "title": "和張僕射塞下曲 二",
+ "id": "6d2e12c0-c1b8-4242-888c-9dba1a8f93ce"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "月黑鴈飛高,單于夜遁逃。",
+ "欲將輕騎逐,大雪滿弓刀。"
+ ],
+ "tags": [
+ "战争",
+ "冬天",
+ "将军",
+ "唐诗三百首",
+ "乐府",
+ "边塞",
+ "赞美"
+ ],
+ "title": "和張僕射塞下曲 三",
+ "id": "ee2a3000-9127-43fa-8618-89a1937322cb"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "野幕敞瓊筵,羌戎賀勞旋。",
+ "醉和金甲舞,雷鼓動山川。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "乐府",
+ "宴饮",
+ "边塞",
+ "赞颂",
+ "将士"
+ ],
+ "title": "和張僕射塞下曲 四",
+ "id": "9a4d3eae-fe01-46c7-a28b-8b518614a508"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "雲開遠見漢陽城,猶是孤帆一日程。",
+ "估客晝眠知浪靜,舟人夜語覺潮生。",
+ "三湘衰鬢逢秋色,萬里歸心對月明。",
+ "舊業已隨征戰盡,更堪江上鼓鼙聲。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "写景",
+ "抒情",
+ "七言律诗",
+ "思乡"
+ ],
+ "title": "晚次鄂州",
+ "id": "fae577a5-da28-4672-83ff-94c97ae2a4a4"
+ },
+ {
+ "author": "盧綸",
+ "paragraphs": [
+ "故關衰草遍,離別自堪悲。",
+ "路出寒雲外,人歸暮雪時。",
+ "少孤爲客早,多難識君遲。",
+ "掩淚空相向,風塵何處期。"
+ ],
+ "tags": [
+ "冬天",
+ "友情",
+ "送别",
+ "唐诗三百首",
+ "五言律诗",
+ "伤怀"
+ ],
+ "title": "李端公",
+ "id": "f4301f96-e872-4eae-aa51-0d12328059d1"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "不知香積寺,數里入雲峰。",
+ "古木無人逕,深山何處鐘。",
+ "泉聲咽危石,日色冷青松。",
+ "薄暮空潭曲,安禪制毒龍。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "五言律诗"
+ ],
+ "title": "過香積寺",
+ "id": "e4354c00-c83e-43da-9341-7007889dd625"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "空山新雨後,天氣晚來秋。",
+ "明月松間照,清泉石上流。",
+ "竹喧歸浣女,蓮動下漁舟。",
+ "隨意春芳歇,王孫自可留。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "山水",
+ "写景",
+ "秋雨",
+ "五言律诗",
+ "写人"
+ ],
+ "title": "山居秋暝",
+ "id": "00f12a19-bb96-4980-a4f5-11a1b9d604dd"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "中歲頗好道,晚家南山陲。",
+ "興來每獨往,勝事空自知。",
+ "行到水窮處,坐看雲起時。",
+ "偶然值林叟,談笑無還期。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "田园",
+ "抒情",
+ "五言律诗",
+ "山",
+ "写人"
+ ],
+ "title": "終南別業",
+ "id": "6d098c75-038d-45f4-8f86-73976c355e44"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "清川帶長薄,車馬去閑閑。",
+ "流水如有意,暮禽相與還。",
+ "荒城臨古渡,落日滿秋山。",
+ "迢遞嵩高下,歸來且閉關。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "山水",
+ "写景",
+ "抒情",
+ "五言律诗"
+ ],
+ "title": "歸嵩山作",
+ "id": "40cd6eec-c6ce-4659-a225-61063cd210e9"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "太乙近天都,連山接海隅。",
+ "白雲迴望合,青靄入看無。",
+ "分野中峰變,陰晴衆壑殊。",
+ "欲投人處宿,隔水問樵夫。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "终南山",
+ "一年级下册",
+ "五言律诗",
+ "高中古诗",
+ "描写山",
+ "写山"
+ ],
+ "title": "終南山",
+ "id": "a5b1f5c9-1ba7-4533-b497-55b1d4af1bbb"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "楚塞三湘接,荆門九派通。",
+ "江流天地外,山色有無中。",
+ "郡邑浮前浦,波瀾動遠空。",
+ "襄陽好風日,留醉與山翁。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "山水",
+ "抒情",
+ "五言律诗"
+ ],
+ "title": "漢江臨汎",
+ "id": "9080a103-2750-4b9b-a4fa-1167cd670a0c"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "渭水自縈秦塞曲,黃山舊遶漢宮斜。",
+ "鑾輿迥出千門柳,閣道廻看上苑花。",
+ "雲裏帝城雙鳳闕,雨中春樹萬人家。",
+ "爲乘陽氣行時令,不是宸遊玩物華。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄山",
+ "应制",
+ "七言律诗"
+ ],
+ "title": "奉和聖製從蓬萊向興慶閣道中留春雨中春望之作應制",
+ "id": "ce71032a-c075-492c-9554-51eb080348a5"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "絳幘雞人送曉籌,尚衣方進翠雲裘。",
+ "九天閶闔開宮殿,萬國衣冠拜冕旒。",
+ "日色纔臨仙掌動,香煙欲傍衮龍浮。",
+ "朝罷須裁五色詔,佩聲歸向鳳池頭。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "宫廷",
+ "七言律诗",
+ "早朝"
+ ],
+ "title": "和賈舍人早朝大明宮之作",
+ "id": "a364504e-3cfc-44c6-9203-173856452cdd"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "洞門高閣靄餘輝,桃李陰陰柳絮飛。",
+ "禁裏疎鐘官舍晚,省中啼鳥吏人稀。",
+ "晨搖玉佩趨金殿,夕奉天書拜瑣闈。",
+ "強欲從君無那老,將因臥病解朝衣。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "酬和",
+ "七言律诗"
+ ],
+ "title": "酬郭給事",
+ "id": "7950a49f-f51d-4926-9d1e-e5f76b153829"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "積雨空林煙火遲,蒸藜炊黍餉東菑。",
+ "漠漠水田飛白鷺,陰陰夏木囀黃鸝。",
+ "山中習靜觀朝槿,松下清齋折露葵。",
+ "野老與人爭席罷,海鷗何事更相疑。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "田园"
+ ],
+ "title": "積雨輞川莊作",
+ "id": "0949fe2a-aadf-4cd4-b1c3-25fcdaecdb96"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "空山不見人,但聞人語響。",
+ "返景入深林,復照青苔上。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "山",
+ "描写山",
+ "写山",
+ "五言绝句"
+ ],
+ "title": "輞川集 鹿柴",
+ "id": "2e8999a9-9df8-4483-ab1e-0231c3e46320"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "獨坐幽篁裏,彈琴復長嘯。",
+ "深林人不知,明月來相照。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "生活",
+ "初中古诗",
+ "月亮",
+ "归隐",
+ "五言绝句",
+ "七年级下册(课外)"
+ ],
+ "title": "輞川集 竹里館",
+ "id": "36e09cb4-4fb7-48be-b6c6-c9bd906d9331"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "山中相送罷,日暮掩柴扉。",
+ "春草明年綠,王孫歸不歸。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "送别",
+ "友情"
+ ],
+ "title": "送別",
+ "id": "c8a4faa6-8666-44f9-b4c9-df78d7af844d"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "君自故鄉來,應知故鄉事。",
+ "來日綺窗前,寒梅着花未。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "景中情",
+ "写景",
+ "思亲",
+ "梅花",
+ "思乡",
+ "五言绝句"
+ ],
+ "title": "雜詩三首 二",
+ "id": "4c7205af-d1e8-48ed-8220-fe095418b95a"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "紅豆生南國,秋來發故枝。",
+ "願君多采擷,此物最相思。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "咏物",
+ "抒情",
+ "咏物诗",
+ "五言绝句"
+ ],
+ "title": "相思",
+ "id": "8baa7c35-9afb-40bf-b5e4-41d9895e2710"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "獨在異鄉爲異客,每逢佳節倍思親。",
+ "遙知兄弟登高處,遍插茱萸少一人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "思乡",
+ "三年级上册",
+ "重阳节",
+ "小学古诗"
+ ],
+ "title": "九月九日憶山東兄弟",
+ "id": "6bbd1c2c-8c43-4ffb-b7b3-506d74266cee"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "渭城朝雨浥輕塵,客舍青青楊柳春。",
+ "勸君更盡一杯酒,西出陽關無故人。"
+ ],
+ "tags": [
+ "八年级下册(课外)",
+ "友情",
+ "小学古诗",
+ "送别",
+ "初中古诗",
+ "乐府",
+ "唐诗三百首",
+ "近代曲辞",
+ "四年级上册"
+ ],
+ "title": "渭城曲",
+ "id": "1575c835-1241-4d7b-99dc-6d700549ac65"
+ },
+ {
+ "author": "裴迪",
+ "paragraphs": [
+ "歸山深淺去,須盡丘壑美。",
+ "莫學武陵人,暫游桃源裏。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "离别",
+ "五言绝句",
+ "规劝"
+ ],
+ "title": "崔九欲往南山馬上口號與別",
+ "id": "80fe70cd-6e63-48eb-9c1e-ab26330d45d9"
+ },
+ {
+ "author": "丘爲",
+ "paragraphs": [
+ "絕頂一茅茨,直[上]三十里。",
+ "扣關無僮僕,窺室唯案几。",
+ "若非巾柴車,應是釣秋水。",
+ "差池不相見,黽勉空仰止。",
+ "草色新雨中,松聲晚牕裏。",
+ "及茲契幽絕,自足蕩心耳。",
+ "雖無賓主意,頗得清淨理。",
+ "興盡方下山,何必待之子。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "友情"
+ ],
+ "title": "尋西山隱者不遇",
+ "id": "10a0aedc-4b39-494b-a36c-28c6df28a668"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "昔人已乘白雲去,此地空餘黃鶴樓。",
+ "黃鶴一去不復返,白雲千載空悠悠。",
+ "晴川歷歷漢陽樹,春草萋萋鸚鵡洲。",
+ "日暮鄉關何處是,煙波江上使人愁。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄鹤楼",
+ "隋・唐・五代",
+ "七言律诗",
+ "八年级上册(课外)",
+ "写景",
+ "初中古诗",
+ "思乡",
+ "名楼、庙宇",
+ "怀古",
+ "带有地名",
+ "地名"
+ ],
+ "title": "黃鶴樓",
+ "id": "2188f98e-d5f0-4740-b94b-e1497a3619ee"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "岧嶤太華俯咸京,天外三峰削不成。",
+ "武帝祠前雲欲散,仙人掌上雨初晴。",
+ "河山北枕秦關險,驛樹西連漢畤平。",
+ "借問路傍名利客,無如此處學長生。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "怀古",
+ "山水"
+ ],
+ "title": "行經華陰",
+ "id": "39b2954c-04df-4561-ad6e-f4ddbae0db1e"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "君家何處住?妾住在橫塘。",
+ "停船暫借問,或恐是同鄉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "女子",
+ "思乡",
+ "乐府"
+ ],
+ "title": "長干曲四首 一",
+ "id": "c39225f9-16b2-4713-a11c-d7030ec3b1c9"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "家臨九江水,來去九江側。",
+ "同是長干人,自小不相識。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "家乡",
+ "女子",
+ "热爱",
+ "乐府"
+ ],
+ "title": "長干曲四首 二",
+ "id": "4a732464-35a5-4ffc-94d0-37d653eef2a1"
+ },
+ {
+ "author": "祖詠",
+ "paragraphs": [
+ "燕臺一望客心驚,簫鼓喧喧漢將營。",
+ "萬里寒光生積雪,三邊曙色動危旌。",
+ "沙場烽火連胡月,海畔雲山擁薊城。",
+ "少小雖非投筆吏,論功還欲請長纓。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "冬天",
+ "边塞"
+ ],
+ "title": "望薊門",
+ "id": "6beb5cc2-ae49-4e23-952a-1cb92fa11512"
+ },
+ {
+ "author": "祖詠",
+ "paragraphs": [
+ "終南陰嶺秀,積雪浮雲端。",
+ "林表明霽色,城中增暮寒。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雪",
+ "冬天",
+ "写景",
+ "雪",
+ "冬",
+ "五言绝句"
+ ],
+ "title": "終南望餘雪",
+ "id": "b4780bd1-1703-4bce-a801-d5ed566826b5"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "白日登山望烽火,黃昏飲馬傍交河。",
+ "行人刁斗風沙暗,公主琵琶幽怨多。",
+ "野雲萬里無城郭,雨雪紛紛連大漠。",
+ "胡鴈哀鳴夜夜飛,胡兒眼淚雙雙落。",
+ "聞道玉門猶被遮,應將性命逐輕車。",
+ "年年戰骨埋荒外,空見蒲桃入漢家。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "冬天",
+ "乐府",
+ "边塞",
+ "相和歌辞"
+ ],
+ "title": "古從軍行",
+ "id": "bea6f283-8f90-49b5-8842-1cb906816548"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "主人有酒歡今夕,請奏鳴琴廣陵客。",
+ "月照城頭烏半飛,霜淒萬樹風入衣。",
+ "銅鑪華燭燭增輝,初彈淥水後楚妃。",
+ "一聲已動物皆靜,四座無言星欲稀。",
+ "清淮奉使千餘里,敢告雲山從此始。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "音乐",
+ "宴会",
+ "七言古诗"
+ ],
+ "title": "琴歌",
+ "id": "7d906794-b4d1-4922-9c59-eee5b19602ad"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "四月南風大麥黃,棗花未落桐陰長。",
+ "青山朝別暮還見,嘶馬出門思舊鄉。",
+ "陳侯立身何坦蕩,虬鬚虎眉仍大顙。",
+ "腹中貯書一萬卷,不肯低頭在草莽。",
+ "東門酤酒飲我曹,心輕萬事皆鴻毛。",
+ "醉臥不知白日暮,有時空望孤雲高。",
+ "長河浪頭連天黑,津口停舟渡不得。",
+ "鄭國遊人未及家,洛陽行子空歎息。",
+ "聞道故林相識多,罷官昨日今如何。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "七言古诗"
+ ],
+ "title": "送陳章甫",
+ "id": "9e6cbc1b-95ed-406b-8c9d-d05876a1342c"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "南山截竹爲觱篥,此樂本自龜茲出。",
+ "流傳漢地曲轉奇,涼州胡人爲我吹。",
+ "傍鄰聞者多歎息,遠客思鄉皆淚垂。",
+ "世人解聽不解賞,長飆風中自來往。",
+ "枯桑老柏寒颼飀,九雛鳴鳳亂啾啾。",
+ "龍吟虎嘯一時發,萬籟百泉相與秋。",
+ "忽然更作漁陽摻,黃雲蕭條白日暗。",
+ "變調如聞楊柳春,上林繁花照眼新。",
+ "歲夜高堂列明燭,美酒一杯聲一曲。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "音乐",
+ "七言古诗"
+ ],
+ "title": "聽安萬善吹觱篥歌",
+ "id": "2d66a9e4-7b4d-4be4-91fd-6a4b2cd8817b"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "男兒事長征,少小幽燕客。",
+ "賭勝馬蹄下,由來輕七尺。",
+ "殺人莫敢前,鬚如蝟毛磔。",
+ "黃雲隴底白雪飛,未得報恩不能歸。",
+ "遼東小婦年十五,慣彈琵琶解歌舞。",
+ "今爲羗笛出塞聲,使我三軍淚如雨。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "豪侠",
+ "拟古",
+ "戍边",
+ "七言古诗"
+ ],
+ "title": "古意",
+ "id": "f1496cf4-4f85-4f9e-9e0e-dd05370641d3"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "蔡女昔造胡笳聲,一彈一十有八拍。",
+ "胡人落淚沾邊草,漢使斷腸對歸客。",
+ "古戍蒼蒼烽火寒,大荒沈沈飛雪白。",
+ "先拂商弦後角羽,四郊秋葉驚摵摵。",
+ "董夫子,通神明,深山竊聽來妖精。",
+ "言遲更速皆應手,將往復旋如有情。",
+ "空山百鳥散還合,萬里浮雲陰且晴。",
+ "嘶酸雛鴈失羣夜,斷絕胡兒戀母聲。",
+ "川爲淨其波,鳥亦罷其鳴。",
+ "烏孫部落家鄉遠,邏娑沙塵哀怨生。",
+ "幽音變調忽飄灑,長風吹林雨墮瓦。",
+ "迸泉颯颯飛木末,野鹿呦呦走堂下。",
+ "長安城連東掖垣,鳳凰池對青瑣門。",
+ "高才脫略名與利,日夕望君抱琴至。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长诗",
+ "七言古诗",
+ "抒情",
+ "古体"
+ ],
+ "title": "聽董大彈胡笳聲兼寄語弄房給事",
+ "id": "361a3e5e-ef71-4749-b39a-aa2f5d9180e4"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "朝聞遊子唱離歌,昨夜微霜初渡河。",
+ "鴻鴈不堪愁裏聽,雲山況是客中過。",
+ "關城樹色催寒近,御苑砧聲向晚多。",
+ "莫見長安行樂處,空令歲月易蹉跎。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "叙事",
+ "七言律诗",
+ "写景",
+ "友情",
+ "抒情",
+ "送别"
+ ],
+ "title": "送魏萬之京",
+ "id": "6340ef19-35e2-4194-96a9-3ee0a667905b"
+ },
+ {
+ "author": "綦毋潛",
+ "paragraphs": [
+ "幽意無斷絕,此去隨所偶。",
+ "晚風吹行舟,花路入溪口。",
+ "際夜轉西壑,隔山望南斗。",
+ "潭煙飛溶溶,林月低向後。",
+ "生事且彌漫,願爲持竿叟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "抒情",
+ "五言古诗"
+ ],
+ "title": "春泛若耶溪",
+ "id": "6290b8f0-48de-407f-be2c-629140d91f83"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "蟬鳴空桑林,八月蕭關道。",
+ "出塞入塞寒,處處黃蘆草。",
+ "從來幽幷客,皆共塵沙老。",
+ "莫學遊俠兒,矜夸紫騮好。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "游侠",
+ "乐府",
+ "讽刺",
+ "边塞",
+ "新乐府辞"
+ ],
+ "title": "塞下曲四首 一",
+ "id": "8d58a158-1a9b-4085-8ef7-b7b4e6adb66d"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "飲馬渡秋水,水寒風似刀。",
+ "平沙日未沒,黯黯見臨洮。",
+ "昔日長城戰,咸言意氣高。",
+ "黃塵足今古,白骨亂蓬蒿。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "新乐府辞",
+ "乐府"
+ ],
+ "title": "塞下曲四首 二",
+ "id": "59fb3f01-65b2-416a-aefb-c13e502bfba1"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "高臥南齋時,開帷月初吐。",
+ "清輝淡水木,演漾在窗戶。",
+ "苒苒幾盈虛,澄澄變今古。",
+ "美人清江畔,是夜越吟苦。",
+ "千里其如何,微風吹蘭杜。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "月亮",
+ "怀人"
+ ],
+ "title": "同從弟銷南齋玩月憶山陰崔少府",
+ "id": "e2fec465-cea1-4829-b12d-3ba37a267562"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "秦時明月漢時關,萬里長征人未還。",
+ "但使龍城飛將在,不教胡馬度陰山。"
+ ],
+ "tags": [
+ "战争",
+ "写景",
+ "忧国忧民",
+ "爱国",
+ "唐诗三百首",
+ "乐府",
+ "山",
+ "边塞"
+ ],
+ "title": "出塞二首 一",
+ "id": "de6b7e3d-03cd-4daa-bdc2-7fb820fda441"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "昨夜風開露井桃,未央前殿月輪高。",
+ "平陽歌舞新承寵,簾外春寒賜錦袍。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "宫怨",
+ "女子"
+ ],
+ "title": "春宮曲",
+ "id": "d0fa1454-f337-4343-ab4a-2a42dfdf4e8f"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "閨中少婦不曾愁,春日凝妝上翠樓。",
+ "忽見陌頭楊柳色,悔教夫壻覓封侯。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "妇女",
+ "春天",
+ "七言绝句",
+ "闺怨"
+ ],
+ "title": "閨怨",
+ "id": "4ac3f099-73da-43e8-a4e5-1dbbd9427491"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "寒雨連天夜入湖,平明送客楚山孤。",
+ "洛陽親友如相問,一片冰心在玉壺。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雨",
+ "抒情",
+ "送别",
+ "七言绝句",
+ "雨",
+ "六年级下册",
+ "小学古诗"
+ ],
+ "title": "芙蓉樓送辛漸二首 一",
+ "id": "090b7792-2942-4f3c-a34d-caccd066c76f"
+ },
+ {
+ "author": "常建",
+ "paragraphs": [
+ "清溪深不測,隱處唯孤雲。",
+ "松際露微月,清光猶爲君。",
+ "茅亭宿花影,藥院滋苔紋。",
+ "余亦謝時去,西山鸞鶴羣。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隐逸",
+ "五言古诗",
+ "山水"
+ ],
+ "title": "宿王昌齡隱居",
+ "id": "c854a853-b015-4848-823b-a786659d33b3"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "雲想衣裳花想容,春風拂檻露華濃。",
+ "若非羣玉山頭見,會向瑤臺月下逢。"
+ ],
+ "tags": [
+ "女子",
+ "唐诗三百首",
+ "乐府",
+ "赞美",
+ "近代曲辞"
+ ],
+ "title": "清平調 一",
+ "id": "d5da9d7d-1e52-4992-8be5-73e556b07e0b"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "一枝紅豔露凝香,雲雨巫山枉斷腸。",
+ "借問漢宮誰得似,可憐飛燕倚新妝。"
+ ],
+ "tags": [
+ "女子",
+ "近代曲辞",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "清平調 二",
+ "id": "59063901-7dbc-4f22-974f-ddccab675cbc"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "名花傾國兩相歡,常得君王帶笑看。",
+ "解得春風無限恨,沈香亭北倚闌干。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "乐府",
+ "写人",
+ "花",
+ "写花"
+ ],
+ "title": "清平調 三",
+ "id": "f91f3117-2bf2-4d12-999a-aca174124715"
+ },
+ {
+ "author": "黄拱",
+ "paragraphs": [
+ "少小離家老大回,鄉音難改鬢毛衰。",
+ "兒童相見不相識,借問客從何處來。"
+ ],
+ "tags": [
+ "二年级上册",
+ "唐诗三百首",
+ "伤老",
+ "七言绝句",
+ "思乡",
+ "感慨",
+ "小学古诗"
+ ],
+ "title": "還鄉偶書 其二",
+ "id": "569ef0e1-887d-4d98-b869-9634f099c8e9"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "漢皇重色思傾國,御宇多年求不得。",
+ "楊家有女初長成,養在深閨人未識。",
+ "天生麗質難自棄,一朝選在君王側。",
+ "回眸一笑百媚生,六宮粉黛無顏色。",
+ "春寒賜浴華清池,溫泉水滑洗凝脂。",
+ "侍兒扶起嬌無力,始是新承恩澤時。",
+ "雲鬢花顏金步搖,芙蓉帳暖度春宵。",
+ "春宵苦短日高起,從此君王不早朝。",
+ "承歡侍宴無閑暇,春從春遊夜專夜。",
+ "後宮佳麗三千人,三千寵愛在一身。",
+ "金屋妝成嬌侍夜,玉樓宴罷醉和春。",
+ "姊妹弟兄皆列土,可憐光彩生門戶。",
+ "遂令天下父母心,不重生男重生女。",
+ "驪宮高處入青雲,仙樂風飄處處聞。",
+ "緩歌慢舞凝絲竹,盡日君王看不足。",
+ "漁陽鞞鼓動地來,驚破霓裳羽衣曲。",
+ "九重城闕煙塵生,千乘萬騎西南行。",
+ "翠華搖搖行復止,西出都門百餘里。",
+ "六軍不發無奈何,宛轉蛾眉馬前死。",
+ "花鈿委地無人收,翠翹金雀玉搔頭。",
+ "君王掩面救不得,回看血淚相和流。",
+ "黃埃散漫風蕭索,雲棧縈紆登劒閣。",
+ "峨嵋山下少人行,旌旗無光日色薄。",
+ "蜀江水碧蜀山青,聖主朝朝暮暮情。",
+ "行宮見月傷心色,夜雨聞鈴腸斷聲。",
+ "天旋日轉迴龍馭,到此躊躇不能去。",
+ "馬嵬坡下泥土中,不見玉顏空死處。",
+ "君臣相顧盡霑衣,東望都門信馬歸。",
+ "歸來池苑皆依舊,太液芙蓉未央柳。",
+ "芙蓉如面柳如眉,對此如何不淚垂?",
+ "春風桃李花開夜,秋雨梧桐葉落時。",
+ "西宮南苑多秋草,宮葉滿階紅不埽。",
+ "棃園弟子白髮新,椒房阿監青娥老。",
+ "夕殿螢飛思悄然,孤燈挑盡未成眠。",
+ "遲遲鐘鼓初長夜,耿耿星河欲曙天。",
+ "鴛鴦瓦冷霜華重,翡翠衾寒誰與共。",
+ "悠悠生死別經年,魂魄不曾來入夢。",
+ "臨邛道士鴻都客,能以精誠致魂魄。",
+ "爲感君王展轉思,遂教方士殷勤覓。",
+ "排空馭氣奔如電,升天入地求之徧。",
+ "上窮碧落下黃泉,兩處茫茫皆不見。",
+ "忽聞海上有仙山,山在虛無縹緲間。",
+ "樓閣玲瓏五雲起,其中綽約多仙子。",
+ "中有一人字太真,雪膚花貌參差是。",
+ "金闕西廂叩玉扃,轉教小玉報雙成。",
+ "聞道漢家天子使,九華帳裏夢魂驚。",
+ "攬衣推枕起裴回,珠箔銀屏邐迤開。",
+ "雲鬢半偏新睡覺,花冠不整下堂來。",
+ "風吹仙袂飄颻舉,猶似霓裳羽衣舞。",
+ "玉容寂莫淚闌干,棃花一枝春帶雨。",
+ "含情凝睇謝君王,一別音容兩渺茫。",
+ "昭陽殿裏恩愛絕,蓬萊宮中日月長。",
+ "回頭下望人寰處,不見長安見塵霧。",
+ "唯將舊物表深情,鈿合金釵寄將去。",
+ "釵留一股合一扇,釵擘黃金合分鈿。",
+ "但教心似金鈿堅,天上人間會相見。",
+ "臨別殷勤重寄詞,詞中有誓兩心知。",
+ "七月七日長生殿,夜半無人私語時。",
+ "在天願作比翼鳥,在地願爲連理枝。",
+ "天長地久有時盡,此恨緜緜無絕期。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "峨眉山",
+ "叙事",
+ "写人",
+ "爱情",
+ "七言古诗",
+ "长诗",
+ "讽喻"
+ ],
+ "title": "長恨歌",
+ "id": "0a0f9231-d88c-4f81-b941-83b67a736194"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "潯陽江頭夜送客,楓葉荻花秋索索。",
+ "主人下馬客在船,舉酒欲飲無管弦。",
+ "醉不成歡慘將別,別時茫茫江浸月。",
+ "忽聞水上琵琶聲,主人忘歸客不發。",
+ "尋聲暗問彈者誰,琵琶聲停欲語遲。",
+ "移船相近邀相見,添酒迴燈重開宴。",
+ "千呼萬喚始出來,猶抱琵琶半遮面。",
+ "轉軸撥弦三兩聲,未成曲調先有情。",
+ "弦弦掩抑聲聲思,似訴平生不得意。",
+ "低眉信手續續彈,說盡心中無限事。",
+ "輕攏慢撚抹復挑,初爲霓裳後六幺。",
+ "大弦嘈嘈如急雨,小弦切切如私語。",
+ "嘈嘈切切錯雜彈,大珠小珠落玉盤。",
+ "間關鶯語花底滑,幽咽泉流水下灘。",
+ "水泉冷澀弦疑絕,疑絕不通聲暫歇。",
+ "別有幽愁暗恨生,此時無聲勝有聲。",
+ "銀缾乍破水漿迸,鐵騎突出刀槍鳴。",
+ "曲終收撥當心畫,四弦一聲如裂帛。",
+ "東舟西舫悄無言,唯見江心秋月白。",
+ "沈吟放撥插弦中,整頓衣裳起斂容。",
+ "自言本是京城女,家在蝦蟇陵下住。",
+ "十三學得琵琶成,名蜀教坊第一部。",
+ "曲罷曾教善才伏,妝成每被秋娘妬。",
+ "五陵年少爭纏頭,一曲紅綃不知數。",
+ "鈿頭雲箆擊節碎,血色羅帬飜酒汙。",
+ "今年歡笑復明年,秋月春風等閑度。",
+ "弟走從軍阿姨死,暮去朝來顏色故。",
+ "門前冷落鞍馬稀,老大嫁作商人婦。",
+ "商人重利輕別離,前月浮梁買茶去。",
+ "去來江口守空船,繞船月明江水寒。",
+ "夜深忽夢少年事,夢啼妝淚紅闌干。",
+ "我聞琵琶已歎息,又聞此語重唧唧。",
+ "同是天涯淪落人,相逢何必曾相識。",
+ "我從去年辭帝京,謫居臥病潯陽城。",
+ "潯陽小處無音樂,終歲不聞絲竹聲。",
+ "住近湓江地低濕,黃蘆苦竹繞宅生。",
+ "其間旦暮聞何物,杜鵑啼血猨哀鳴。",
+ "春江花朝秋月夜,往往取酒還獨傾。",
+ "豈無山歌與村笛,嘔啞嘲哳難爲聽。",
+ "今夜聞君琵琶語,如聽仙樂耳暫明。",
+ "莫辭更坐彈一曲,爲君飜作琵琶行。",
+ "感我此言良久立,却坐促弦弦轉急。",
+ "淒淒不似向前聲,滿座重聞皆掩泣。",
+ "座中泣下誰最多,江州司馬青衫濕。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言古诗",
+ "高中古诗",
+ "写人",
+ "乐府",
+ "三年级下册",
+ "愤懑"
+ ],
+ "title": "琵琶引",
+ "id": "0fb0189c-8ff3-4106-af94-f6d26d528a1d"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "離離原上草,一歲一枯榮。",
+ "野火燒不盡,春風吹又生。",
+ "遠芳侵古道,晴翠接荒城。",
+ "又送王孫去,萋萋滿別情。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写草",
+ "二年级下册",
+ "咏物",
+ "送别",
+ "五言律诗",
+ "咏物诗",
+ "小学古诗"
+ ],
+ "title": "賦得古原草送別",
+ "id": "0174cbbc-31fa-4daf-b1cf-7f91c6a646a9"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "時難年饑世業空,弟兄羇旅各西東。",
+ "田園寥落干戈後,骨肉流離道路中。",
+ "弔影分爲千里鴈,辭根散作九秋蓬。",
+ "共看明月應垂淚,一夜鄉心五處同。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "自河南經亂關內阻饑兄弟離散各在一處因望月有感聊書所懷寄上浮梁大兄於潛七兄烏江十五兄兼示符離及下邽弟妹",
+ "id": "a2aed629-465d-412c-a68d-780068488725"
+ },
+ {
+ "author": "白居易",
+ "paragraphs": [
+ "綠螘新醅酒,紅泥小火壚。",
+ "晚來天欲雪,能飲一杯無。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "生活",
+ "饮酒",
+ "友情"
+ ],
+ "title": "問劉十九",
+ "id": "9972f7bb-ae5b-49e4-87df-381386af66b6"
+ },
+ {
+ "author": "賈島",
+ "paragraphs": [
+ "松下問童子,言師採藥去。",
+ "只在此山中,雲深不知處。"
+ ],
+ "tags": [
+ "五言绝句",
+ "写人",
+ "寻访",
+ "唐诗三百首"
+ ],
+ "title": "尋隱者不遇",
+ "id": "4f6575cc-12cc-4986-8bfc-5cedac7bb68f"
+ },
+ {
+ "author": "溫庭筠",
+ "paragraphs": [
+ "澹然空水對斜暉,曲島蒼茫接翠微。",
+ "波上馬嘶看櫂去,柳邊人歇待船歸。",
+ "數叢沙草羣鷗散,萬頃江田一鷺飛。",
+ "誰解乘舟尋范蠡,五湖煙水獨忘機。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "抒怀",
+ "渡江",
+ "山水"
+ ],
+ "title": "利州南渡",
+ "id": "ac97d42b-8fef-42c9-829f-baafe89b4cdc"
+ },
+ {
+ "author": "溫庭筠",
+ "paragraphs": [
+ "冰簟銀牀夢不成,碧天如水夜雲輕。",
+ "雁聲遠過瀟湘去,十二樓中月自明。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "离别",
+ "七言绝句",
+ "闺怨"
+ ],
+ "title": "瑤瑟怨",
+ "id": "b7d50789-e0d5-4291-8014-eddf65a124df"
+ },
+ {
+ "author": "溫庭筠",
+ "paragraphs": [
+ "荒戍落黃葉,浩然離故關。",
+ "高風漢陽渡,初日郢門山。",
+ "江上幾人在,天涯孤櫂還。",
+ "何當重相見,尊酒慰離顏。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友情",
+ "送别",
+ "五言律诗",
+ "惜别",
+ "秋天"
+ ],
+ "title": "送人東遊",
+ "id": "315bc0e1-f76c-49d5-9940-b212e5f9c2ae"
+ },
+ {
+ "author": "溫庭筠",
+ "paragraphs": [
+ "蘇武魂銷漢使前,古祠高樹兩茫然。",
+ "雲邊雁斷胡天月,隴上羊歸塞草煙。",
+ "廻日樓臺非甲帳,去時冠劒是丁年。",
+ "茂陵不見封侯印,空向秋波哭逝川。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "写人",
+ "凭吊古迹",
+ "赞颂"
+ ],
+ "title": "蘇武廟",
+ "id": "cae8a893-7ae6-4824-b4c5-eada730b075f"
+ },
+ {
+ "author": "李頻",
+ "paragraphs": [
+ "嶺外音書絕,經年復歷春。",
+ "近鄉情更怯,不敢問來人。"
+ ],
+ "tags": [
+ "五言绝句",
+ "思乡",
+ "唐诗三百首",
+ "渡江"
+ ],
+ "title": "渡漢江",
+ "id": "a6144ae0-3bcf-4503-b58d-9abf24d6608c"
+ },
+ {
+ "author": "秦韜玉",
+ "paragraphs": [
+ "蓬門未識綺羅香,擬託良媒益自傷。",
+ "誰愛風流高格調,共憐時世儉梳妝。",
+ "敢將十指誇偏巧,不把雙眉鬬畫長。",
+ "苦恨年年壓金線,爲他人作嫁衣裳。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "哲理",
+ "七言律诗",
+ "生活",
+ "女子",
+ "怀才不遇"
+ ],
+ "title": "貧女",
+ "id": "16ddf43c-ac3a-47bc-95a5-36a49bceb30b"
+ },
+ {
+ "author": "周朴",
+ "paragraphs": [
+ "早被嬋娟誤,欲妝臨鏡慵。",
+ "承恩不在貌,教妾若爲容。",
+ "風暖鳥聲碎,日高花影重。",
+ "年年越溪女,相憶采芙蓉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "宫怨",
+ "春天",
+ "写景",
+ "女子",
+ "五言律诗"
+ ],
+ "title": "春宮怨",
+ "id": "1d6885a4-d11e-4428-80a8-fcf75d7a41aa"
+ },
+ {
+ "author": "崔塗",
+ "paragraphs": [
+ "幾行歸去盡,片影獨何之。",
+ "暮雨相呼失,寒塘獨下遲。",
+ "渚雲低暗度,關月冷遙隨。",
+ "未必逢矰繳,孤飛自可疑。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "羁旅",
+ "五言律诗",
+ "写鸟",
+ "组诗",
+ "鸟"
+ ],
+ "title": "孤鴈 二",
+ "id": "c836e52f-2346-4a3e-84bb-97514b17faf6"
+ },
+ {
+ "author": "崔塗",
+ "paragraphs": [
+ "迢遰三巴路,羇危萬里身。",
+ "亂山殘雪夜,孤燭異鄉春。",
+ "漸與骨肉遠,轉於僮僕親。",
+ "那堪正漂泊,明日歲華新。"
+ ],
+ "tags": [
+ "羁旅",
+ "唐诗三百首",
+ "五言律诗",
+ "春节",
+ "思乡"
+ ],
+ "title": "巴山道中除夜書懷",
+ "id": "9d33c118-cde9-49af-b572-c43427a10b48"
+ },
+ {
+ "author": "張祜",
+ "paragraphs": [
+ "故國三千里,深宮二十年。",
+ "一聲河滿子,雙淚落君前。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "宫怨"
+ ],
+ "title": "宮詞二首 一",
+ "id": "d850db6a-e07b-4fee-b642-57a70bdbbed2"
+ },
+ {
+ "author": "張祜",
+ "paragraphs": [
+ "禁門宮樹月痕過,媚眼唯看宿燕窠。",
+ "斜拔玉釵燈影畔,剔開紅焰救飛蛾。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "宫怨",
+ "女子"
+ ],
+ "title": "贈內人",
+ "id": "84bd91bd-11f7-4218-b9e2-b05b06ce8b99"
+ },
+ {
+ "author": "張祜",
+ "paragraphs": [
+ "日光斜照集靈臺,紅樹花迎曉露開。",
+ "昨夜上皇新授籙,太真含笑入簾來。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写人",
+ "七言绝句",
+ "讽刺"
+ ],
+ "title": "集靈臺二首 一",
+ "id": "19867a30-1b67-4123-b367-44565bab6516"
+ },
+ {
+ "author": "張祜",
+ "paragraphs": [
+ "虢國夫人承主恩,平明騎馬入宮門。",
+ "却嫌脂粉汚顏色,淡掃蛾眉朝至尊。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写人",
+ "七言绝句",
+ "讽刺"
+ ],
+ "title": "集靈臺二首 二",
+ "id": "51262a8c-de53-41ee-958a-de08d5f5a446"
+ },
+ {
+ "author": "張祜",
+ "paragraphs": [
+ "金陵津渡小山樓,一宿行人自可愁。",
+ "潮落夜江斜月裏,兩三星火是瓜州。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "抒情",
+ "七言绝句",
+ "旅途"
+ ],
+ "title": "題金陵渡",
+ "id": "bce40ff4-816c-490c-be30-04de15f7c25e"
+ },
+ {
+ "author": "朱慶餘",
+ "paragraphs": [
+ "寂寂花時閉院門,美人相並立瓊軒。",
+ "含情欲說宮中事,鸚鵡前頭不敢言。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言绝句",
+ "宫怨",
+ "写景"
+ ],
+ "title": "宮詞",
+ "id": "ab4b1a7f-4fbe-438b-817f-9e1ed2d39f4f"
+ },
+ {
+ "author": "韓愈",
+ "paragraphs": [
+ "張生手持石鼓文,勸我試作石鼓歌。",
+ "少陵無人謫仙死,才薄將奈石鼓何。",
+ "周綱陵遲四海沸,宣王憤起揮天戈。",
+ "大開明堂受朝賀,諸侯劒佩鳴相磨。",
+ "蒐于岐陽騁雄俊,萬里禽獸皆遮羅。",
+ "鐫功勒成告萬世,鑿石作鼓隳嵯峨。",
+ "從臣才藝咸第一,揀選撰刻留山阿。",
+ "雨淋日炙野火燎,鬼物守護煩撝呵。",
+ "公從何處得紙本,毫髮盡備無差訛。",
+ "辭嚴義密讀難曉,字體不類隸與科。",
+ "年深豈免有缺畫,快劒斫斷生蛟鼉。",
+ "鸞翔鳳翥衆僊下,珊瑚碧樹交枝柯。",
+ "金繩鐵索鎖紐壯,古鼎躍水龍騰梭。",
+ "陋儒編詩不收入,二雅褊迫無委蛇。",
+ "孔子西行不到秦,掎摭星宿遺羲娥。",
+ "嗟予好古生苦晚,對此涕淚雙滂沱。",
+ "憶昔初蒙博士徴,其年始改稱元和。",
+ "故人從軍在右輔,爲我度量掘臼科。",
+ "濯冠沐浴告祭酒,如此至寶存豈多。",
+ "氊包席裹可立致,十鼓祗載數駱駝。",
+ "薦諸太廟比郜鼎,光價豈止百倍過。",
+ "聖恩若許留太學,諸生講解得切磋。",
+ "觀經鴻都尚填咽,坐見舉國來奔波。",
+ "剜苔剔蘚露節角,安置妥帖平不頗。",
+ "大廈深簷與蓋覆,經歷久遠期無佗。",
+ "中朝大官老於事,詎肯感激徒媕婀。",
+ "牧童敲火牛礪角,誰復著手爲摩挲。",
+ "日銷月鑠就埋沒,六年西顧空吟哦。",
+ "羲之俗書趁姿媚,數紙尚可博白鵝。",
+ "繼周八代爭戰罷,無人收拾理則那。",
+ "方今太平日無事,柄任儒術崇丘軻。",
+ "安能以此上論列,願借辨口如懸河。",
+ "石鼓之歌止於此,嗚呼吾意其蹉跎。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言古诗",
+ "题咏"
+ ],
+ "title": "石鼓歌",
+ "id": "1b9b8047-2410-47ae-a24c-aad7342b338f"
+ },
+ {
+ "author": "王涯",
+ "paragraphs": [
+ "桂魄初生秋露微,輕羅已薄未更衣。",
+ "銀箏夜久殷勤弄,心怯空房不忍歸。"
+ ],
+ "tags": [
+ "思念",
+ "秋天",
+ "女子",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "秋夜曲",
+ "id": "0055db4e-e8d3-41a0-b201-0e3b970e27b6"
+ },
+ {
+ "author": "柳宗元",
+ "paragraphs": [
+ "汲井漱寒齒,清心拂塵服。",
+ "閒持貝葉書,步出東齋讀。",
+ "真源了無取,妄跡世所逐。",
+ "遺言冀可冥,繕性何由熟。",
+ "道人庭宇靜,苔色連深竹。",
+ "日出霧露餘,青松如膏沐。",
+ "澹然離言說,悟悅心自足。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "抒情",
+ "贬谪"
+ ],
+ "title": "晨詣超師院讀禪經",
+ "id": "2954ddd2-f1c7-41b1-8c9e-42058398cde0"
+ },
+ {
+ "author": "柳宗元",
+ "paragraphs": [
+ "城上高樓接大荒,海天愁思正茫茫。",
+ "驚風亂颭芙蓉水,密雨斜侵薜荔牆。",
+ "嶺樹重遮千里目,江流曲似九廻腸。",
+ "共來百越文身地,猶自音書滯一鄉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "抒情",
+ "怀人"
+ ],
+ "title": "登柳州城樓寄漳汀封連四州",
+ "id": "3d26433c-a673-4094-8c83-cadb1b08412a"
+ },
+ {
+ "author": "柳宗元",
+ "paragraphs": [
+ "久爲簪組累,幸此南夷謫。",
+ "閑依農圃鄰,偶似山林客。",
+ "曉耕翻露草,夜榜響溪石。",
+ "來往不逢人,長歌楚天碧。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "五言古诗",
+ "生活"
+ ],
+ "title": "溪居",
+ "id": "469dec8b-ba5e-4faf-b52f-2ad203e38d81"
+ },
+ {
+ "author": "柳宗元",
+ "paragraphs": [
+ "千山鳥飛絕,萬逕人蹤滅。",
+ "孤舟蓑笠翁,獨釣寒江雪。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雪",
+ "山水",
+ "冬天",
+ "雪",
+ "冬",
+ "写景",
+ "写鸟",
+ "五言绝句",
+ "鸟"
+ ],
+ "title": "江雪",
+ "id": "1f92134b-bca9-46eb-b2fe-87b30413041a"
+ },
+ {
+ "author": "柳宗元",
+ "paragraphs": [
+ "漁翁夜傍西巖宿,曉汲清湘燃楚竹。",
+ "煙銷日出不見人,[欸]乃一聲山水綠。",
+ "迴看天際下中流,巖上無心雲相逐。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写人",
+ "七言古诗",
+ "山水"
+ ],
+ "title": "漁翁",
+ "id": "51b1aeeb-9a66-412d-b15c-4afaa6c6b1d7"
+ },
+ {
+ "author": "劉禹錫",
+ "paragraphs": [
+ "天地英雄氣,千秋尚凜然。",
+ "勢分三足鼎,業復五銖錢。",
+ "得相能開國,生兒不象賢。",
+ "淒涼蜀故妓,來舞魏宮前。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "咏史怀古",
+ "五言律诗"
+ ],
+ "title": "蜀先主廟",
+ "id": "2f37297b-77d5-412b-bc73-105bfa4d011d"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "今夜鄜州月,閨中只獨看。",
+ "遙憐小兒女,未解憶長安。",
+ "香霧雲鬟濕,清輝玉臂寒。",
+ "何時倚虛幌,雙照淚痕乾。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "女子",
+ "五言律诗",
+ "高中古诗",
+ "月亮",
+ "三年级上册"
+ ],
+ "title": "月夜",
+ "id": "59c7eaa9-3c05-4857-8cff-10376706ccb9"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "國破山河在,城春草木深。",
+ "感時花濺淚,恨別鳥驚心。",
+ "烽火連三月,家書抵萬金。",
+ "白頭搔更短,渾欲不勝簪。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "忧国忧民",
+ "爱国",
+ "五言律诗",
+ "初中古诗",
+ "八年级上册(课内)",
+ "思乡",
+ "写鸟",
+ "鸟"
+ ],
+ "title": "春望",
+ "id": "e9e6425c-6c14-4f69-8694-87a8b96a43de"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "花隱掖垣暮,啾啾棲鳥過。",
+ "星臨萬戶動,月傍九霄多。",
+ "不寢聽金鑰,因風想玉珂。",
+ "明朝有封事,數問夜如何。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "爱国",
+ "写景",
+ "五言律诗"
+ ],
+ "title": "春宿左省",
+ "id": "2769113d-0cac-4e67-83cb-0c10685f8381"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "此道昔歸順,西郊胡正繁。",
+ "至今殘破膽,應有未招魂。",
+ "近得歸京邑,移官豈至尊。",
+ "無才日衰老,駐馬望千門。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "岁月",
+ "五言律诗",
+ "怀念"
+ ],
+ "title": "至德二載甫自京金光門出問道歸鳳翔乾元初從左拾遺移華州掾與親故別因出此門有悲往事",
+ "id": "f88a6dbd-bf83-458c-b5e2-85fdd1ed98b2"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "戍鼓斷人行,秋邊一雁聲。",
+ "露從今夜白,月是故鄉明。",
+ "有弟皆分散,無家問死生。",
+ "寄書長不避,況乃未休兵。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "月夜",
+ "秋天",
+ "边塞",
+ "思乡",
+ "五言律诗",
+ "怀人",
+ "中秋"
+ ],
+ "title": "月夜憶舍弟",
+ "id": "c1280e87-ac81-4f9b-9de5-377d6af38ae8"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "涼風起天末,君子意如何。",
+ "鴻雁幾時到,江湖秋水多。",
+ "文章憎命達,魑魅喜人過。",
+ "應共冤魂語,投詩贈汩羅。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写风",
+ "五言律诗",
+ "风",
+ "怀人"
+ ],
+ "title": "天末憶李白",
+ "id": "2c036362-9f1d-434c-854f-b97a2a4bb19b"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "丞相祠堂何處尋,錦官城外柏森森。",
+ "映堦碧草自春色,隔葉黃鸝空好音。",
+ "三顧頻煩天下計,兩朝開濟老臣心。",
+ "出師未捷身先死,長使英雄淚滿襟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "忧国忧民",
+ "写人",
+ "咏史怀古",
+ "赞颂"
+ ],
+ "title": "蜀相",
+ "id": "a353dcb1-12cc-4ca8-af1e-55d1d62445f8"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "舍南舍北皆春水,但見羣鷗日日來。",
+ "花徑不曾緣客掃,蓬門今始爲君開。",
+ "盤餐市遠無兼味,樽酒家貧只舊醅。",
+ "肯與鄰翁相對飲,隔籬呼取盡餘桮。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "喜悦",
+ "写景",
+ "友情",
+ "待客"
+ ],
+ "title": "客至",
+ "id": "9da0cbe5-52d4-4efc-8be2-2f78f1a8f097"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "西山白雪三奇戍,南浦清江萬里橋。",
+ "海內風塵諸弟隔,天涯涕淚一身遙。",
+ "唯將遲暮供多病,未有涓埃荅聖朝。",
+ "跨馬出郊時極目,不堪人事日蕭條。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "写景",
+ "抒情"
+ ],
+ "title": "野望",
+ "id": "d77f340f-1a04-4c5b-9704-2536e4249c6b"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "遠送從此別,青山空復情。",
+ "幾時桮重把,昨夜月同行。",
+ "列郡謳歌惜,三朝出入榮。",
+ "江村獨歸處,寂寞養殘生。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "五言律诗",
+ "惜别"
+ ],
+ "title": "奉濟驛重送嚴公四韻",
+ "id": "c2c83ee2-940f-4ae3-9ab3-4f509d2991f3"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "劒外忽傳收薊北,初聞涕淚滿衣裳。",
+ "却看妻子愁何在,漫卷詩書喜欲狂。",
+ "白日放歌須縱酒,青春作伴好還鄉。",
+ "即從巴峽穿巫峽,便下襄陽向洛陽。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "思归",
+ "六年级下册",
+ "小学古诗"
+ ],
+ "title": "聞官軍收河南河北",
+ "id": "22e90c0d-2b59-4500-8e5d-80d38ec4692c"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "風急天高猨嘯哀,渚清沙白鳥飛迴。",
+ "無邊落木蕭蕭下,不盡長江衮衮來。",
+ "萬里悲秋常作客,百年多病獨登臺。",
+ "艱難苦恨繁霜鬢,潦倒新停濁酒桮。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "七言律诗",
+ "写景",
+ "忧国忧民",
+ "秋天",
+ "登高",
+ "抒情"
+ ],
+ "title": "登高",
+ "id": "a4d718ee-c020-42b4-97df-499cd0076f7b"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "他鄉復行役,駐馬別孤墳。",
+ "近淚無乾土,低空有斷雲。",
+ "對碁陪謝傅,把劒覓徐君。",
+ "唯見林花落,鸎啼送客聞。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "忧国忧民",
+ "感伤",
+ "五言律诗",
+ "悼亡"
+ ],
+ "title": "別房太尉墓",
+ "id": "c19e64b1-604b-40b7-9e41-6ef57b9c6f7b"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "花近高樓傷客心,萬方多難此登臨。",
+ "錦江春色來天地,玉壘浮雲變古今。",
+ "北極朝廷終不改,西山寇盜莫相侵。",
+ "可憐後主還祠廟,日暮聊爲梁甫吟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "写景",
+ "怀古",
+ "一年级下册",
+ "忧国忧民",
+ "感时",
+ "初中古诗",
+ "高中古诗",
+ "九年级下册(课外)"
+ ],
+ "title": "登樓",
+ "id": "12919986-2d09-4d2e-b5ad-f2485e40ada5"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "清秋幕府井梧寒,獨宿江城蠟炬殘。",
+ "永夜角聲悲自語,中天月色好誰看。",
+ "風塵荏苒音書絕,關塞蕭條行路難。",
+ "已忍伶俜十年事,強移栖息一枝安。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "感时",
+ "孤独",
+ "七言律诗"
+ ],
+ "title": "宿府",
+ "id": "f540f519-e3c4-41c2-b299-f1d255977705"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "細草微風岸,危檣獨夜舟。",
+ "星垂平野闊,月湧大江流。",
+ "名豈文章著,官因老病休。",
+ "飄飄何所似,天地一沙鷗。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "孤独",
+ "抒怀",
+ "五言律诗"
+ ],
+ "title": "旅夜書懷",
+ "id": "f96506ea-e8ac-47e0-a0a6-ccd306e56645"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "歲暮陰陽催短景,天涯霜雪霽寒宵。",
+ "五更鼓角聲悲壯,三峽星河影動搖。",
+ "野哭幾家聞戰伐,夷歌數處起漁樵。",
+ "臥龍躍馬終黃土,人事依依漫寂寥。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "閣夜",
+ "id": "0f5874bf-3748-44c9-bc3a-be4a8c3eb2f6"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "功蓋三分國,名高八陣圖。",
+ "江流石不轉,遺恨失吞吳。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "怀古",
+ "哲理",
+ "写人",
+ "五言绝句"
+ ],
+ "title": "八陣圖",
+ "id": "927908c0-999f-4d3f-8192-d67d28f93576"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "支離東北風塵際,漂泊西南天地間。",
+ "三峽樓臺淹日月,五溪衣服共雲山。",
+ "羯胡事主終無賴,詞客衰時且未還。",
+ "庾信平生最蕭瑟,暮年詩賦動江關。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "感叹",
+ "怀古"
+ ],
+ "title": "詠懷古跡五首 一",
+ "id": "03eb4001-023a-4920-bbc7-a0fc52b2836d"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "搖落深知宋玉悲,風流儒雅亦吾師。",
+ "悵望千秋一灑淚,蕭條異代不同時。",
+ "江山故宅空文藻,雲雨荒臺豈夢思。",
+ "最是楚宮俱泯滅,舟人指點到今疑。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "怀古",
+ "抒怀"
+ ],
+ "title": "詠懷古跡五首 二",
+ "id": "acbe1729-cefd-4678-9719-09c5c9c27db0"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "羣山萬壑赴荆門,生長明妃尚有村。",
+ "一去紫臺連朔漠,獨留青塚向黃昏。",
+ "畫圖省識春風面,環佩空歸月夜魂。",
+ "千載琵琶作胡語,分明怨恨曲中論。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "怀古",
+ "思乡"
+ ],
+ "title": "詠懷古跡五首 三",
+ "id": "3b5db620-1964-4674-a01d-ad70ba3ebd2a"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "蜀主窺吳幸三峽,崩年亦在永安宮。",
+ "翠華想像空山裏,玉殿虛無野寺中。",
+ "古廟杉松巢水鶴,歲時伏臘走村翁。",
+ "武侯祠屋常鄰近,一體君臣祭祀同。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "怀古",
+ "抒怀"
+ ],
+ "title": "詠懷古跡五首 四",
+ "id": "733afc7b-e370-4724-b755-e9342c574d22"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "諸葛大名垂宇宙,宗臣遺像肅清高。",
+ "三分割據紆籌策,萬古雲霄一羽毛。",
+ "伯仲之間見伊呂,指揮若定失蕭曹。",
+ "福移漢祚難恢復,志決身殲軍務勞。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "怀古",
+ "议论"
+ ],
+ "title": "詠懷古跡五首 五",
+ "id": "87f5949b-bdad-4b5e-b488-7015bd45cc4f"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "歧王宅裏尋常見,崔九堂前幾度聞。",
+ "正是江南好風景,落花時節又逢君。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "初中古诗",
+ "七言绝句",
+ "怀念",
+ "感伤",
+ "七年级下册(课外)"
+ ],
+ "title": "江南逢李龜年",
+ "id": "ae5b1a15-0b5c-4578-8f31-1b60030633a4"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "昔聞洞庭水,今上岳陽樓。",
+ "吳楚東南坼,乾坤日夜浮。",
+ "親朋無一字,老病有孤舟。",
+ "戎馬關山北,憑軒涕泗流。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "岳阳楼",
+ "抱负",
+ "五言律诗",
+ "山水"
+ ],
+ "title": "登岳陽樓",
+ "id": "5d05fef6-e4aa-48e5-917d-c29d0e6d6b33"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "虢國夫人承主恩,平明上馬入宮門。",
+ "却嫌脂粉涴顏色,澹埽蛾眉朝至尊。"
+ ],
+ "tags": [
+ "写人",
+ "七言绝句",
+ "唐诗三百首",
+ "讽刺"
+ ],
+ "title": "虢國夫人",
+ "id": "15180235-db35-499b-8fd5-b8d9fa92f5f2"
+ },
+ {
+ "author": "張九齡",
+ "paragraphs": [
+ "海上生明月,天涯共此時。",
+ "情人怨遙夜,竟夕起相思。",
+ "滅燭憐光滿,披衣覺露滋。",
+ "不堪盈手贈,還寢夢佳期。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "中秋节",
+ "中秋",
+ "思乡",
+ "五言律诗",
+ "月亮"
+ ],
+ "title": "望月懷遠",
+ "id": "2c152693-c25f-45ce-8ef2-cbedce1a62bc"
+ },
+ {
+ "author": "宋之問",
+ "paragraphs": [
+ "陽月南飛雁,傳聞至此回。",
+ "我行殊未已,何日復歸來?江靜潮初落,林昏瘴不開。",
+ "明朝望鄉處,應見隴頭梅。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言律诗",
+ "思乡",
+ "带有地名",
+ "地名"
+ ],
+ "title": "題大庾嶺北驛",
+ "id": "058db0d1-0656-4e8f-ac49-87622e16b19b"
+ },
+ {
+ "author": "宋之問",
+ "paragraphs": [
+ "嶺外音書斷,經冬復歷春。",
+ "近鄉情更怯,不敢問來人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "思乡",
+ "渡江"
+ ],
+ "title": "渡漢江",
+ "id": "eb9530d2-e4c7-4c34-9214-06c28ca586bf"
+ },
+ {
+ "author": "宋之問",
+ "paragraphs": [
+ "鄉心新歲切,天畔獨澘然。",
+ "老至居人下,春歸在客先。",
+ "嶺猨同旦暮,江柳共風煙。",
+ "已似長沙傅,從今又幾年。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言律诗",
+ "春节",
+ "思乡"
+ ],
+ "title": "新年作",
+ "id": "368fc6bb-1636-419e-8ddb-cdc1dd07a1ca"
+ },
+ {
+ "author": "王勃",
+ "paragraphs": [
+ "城闕輔三秦,風煙望五津。",
+ "與君離別意,同是宦遊人。",
+ "海內存知己,天涯若比隣。",
+ "無爲在岐路,兒女共霑巾。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "八年级下册(课外)",
+ "友情",
+ "送别",
+ "初中古诗",
+ "五言律诗",
+ "哲理"
+ ],
+ "title": "杜少府之任蜀州",
+ "id": "fe6f4b72-855c-43b9-8883-8b3fe3cd8bd4"
+ },
+ {
+ "author": "杜審言",
+ "paragraphs": [
+ "獨有宦遊人,偏驚物候新。",
+ "雲霞出海曙,梅柳渡江春。",
+ "淑氣催黃鳥,晴光轉綠蘋。",
+ "忽聞歌古調,歸思欲霑巾。"
+ ],
+ "tags": [
+ "春天",
+ "唐诗三百首",
+ "伤怀",
+ "和诗",
+ "思乡",
+ "五言律诗"
+ ],
+ "title": "和晉陵陸丞早春遊望",
+ "id": "b881b056-6d92-45e8-9620-4710a375776f"
+ },
+ {
+ "author": "朱斌",
+ "paragraphs": [
+ "白日依山盡,黃河入海流。",
+ "欲窮千里目,更上一層樓。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄河",
+ "山水",
+ "励志",
+ "登楼",
+ "写景",
+ "哲理",
+ "名楼、庙宇",
+ "五言绝句"
+ ],
+ "title": "登樓",
+ "id": "19340af6-e25d-41c8-90fc-b465d9be1134"
+ },
+ {
+ "author": "高適",
+ "paragraphs": [
+ "嗟君此別意何如,駐馬銜桮問謫居。",
+ "巫峽啼猿數行淚,衡陽歸雁幾封書。",
+ "青楓江上秋天遠,白帝城邊古木疎。",
+ "聖代即今多雨露,暫時分手莫躊躇。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "同情",
+ "送别",
+ "贬谪",
+ "边塞",
+ "七言律诗"
+ ],
+ "title": "送李少府貶峽中王少府貶長沙",
+ "id": "2c24cec1-4105-4511-bf8b-bc6d5d8d48c7"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "岱宗夫如何,齊魯青未了。",
+ "造化鍾神秀,陰陽割昏曉。",
+ "盪胷生曾雲,決眥入歸鳥。",
+ "會當凌絕頂,一覽衆山小。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "泰山",
+ "五言古诗",
+ "励志",
+ "写景",
+ "初中古诗",
+ "山",
+ "哲理",
+ "八年级上册(课内)",
+ "写鸟",
+ "写山",
+ "描写山",
+ "鸟"
+ ],
+ "title": "望嶽",
+ "id": "12fdaad8-b197-4526-b56d-c7c713267248"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "車轔轔,馬蕭蕭,行人弓箭各在腰。",
+ "耶孃妻子走相送,塵埃不見咸陽橋。",
+ "牽衣頓足闌道哭,哭聲直上干雲霄。",
+ "道傍過者問行人,行人但云點行頻。",
+ "或從十五北防河,便至四十西營田。",
+ "去時里正與裹頭,歸來頭白還戍邊。",
+ "邊亭流血成海水,武皇開邊意未已。",
+ "君不聞漢家山東二百州,千村萬落生荆杞。",
+ "縱有健婦把鋤犂,禾生隴畝無東西。",
+ "況復秦兵耐苦戰,被驅不異犬與雞。",
+ "長者雖有問,役夫敢申恨。",
+ "且如今年冬,未休關西卒。",
+ "縣官急索租,租稅從何出?信知生男惡,反是生女好。",
+ "生女猶是嫁比鄰,生男埋沒隨百草。",
+ "君不見青海頭,古來白骨無人收。",
+ "新鬼煩冤舊鬼哭,天陰雨濕聲啾啾。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "叙事",
+ "战争",
+ "忧国忧民",
+ "乐府",
+ "新乐府辞"
+ ],
+ "title": "兵車行",
+ "id": "bd57a0b0-28a1-4d53-bc09-51899d2dac38"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "人生不相見,動如參與商。",
+ "今夕復何夕,共此燈燭光。",
+ "少壯能幾時,鬢髮各已蒼。",
+ "訪舊半爲鬼,驚呼熱中腸。",
+ "焉知二十載,重上君子堂。",
+ "昔別君未婚,兒女忽成行。",
+ "怡然敬父執,問我來何方。",
+ "問荅乃未已,兒女羅酒漿。",
+ "夜雨剪春韭,新炊間黃粱。",
+ "主稱會面難,一舉累十觴。",
+ "十觴亦不醉,感子故意長。",
+ "明日隔山岳,世事兩茫茫。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "时光",
+ "五言古诗",
+ "人生",
+ "友情"
+ ],
+ "title": "贈衛八處士",
+ "id": "6c7c45a6-c944-4ff3-aa99-f5cb0440436f"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "三月三日天氣新,長安水邊多麗人。",
+ "態濃意遠淑且真,肌理細膩骨肉勻。",
+ "繡羅衣裳照暮春,蹙金孔雀銀麒麟。",
+ "頭上何所有,翠微㔩葉垂鬢脣。",
+ "背後何所見,珠壓腰衱穩稱身。",
+ "就中雲幕椒房親,賜名大國虢與秦。",
+ "紫駝之峰出翠釜,水精之盤行素鱗。",
+ "犀箸厭飫久未下,鑾刀縷切空紛綸。",
+ "黃門飛鞚不動塵,御廚絡繹送八珍。",
+ "簫鼓哀吟感鬼神,賓從雜遝實要津。",
+ "後來鞍馬何逡巡,當軒下馬入錦茵。",
+ "楊花雪落覆白蘋,青鳥飛去銜紅巾。",
+ "炙手可熱勢絕倫,慎莫近前丞相嗔。"
+ ],
+ "tags": [
+ "杂曲歌辞",
+ "唐诗三百首",
+ "乐府",
+ "讽刺",
+ "咏叹"
+ ],
+ "title": "麗人行",
+ "id": "370b53ae-949e-45cf-92b0-3aca072c1b86"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "少陵野老吞聲哭,春日潛行曲江曲。",
+ "江頭宮殿鎖千門,細柳新蒲爲誰綠。",
+ "憶昔霓旌下南苑,苑中萬物生顏色。",
+ "昭陽殿裏第一人,同輦隨君侍君側。",
+ "輦前才人帶弓箭,白馬嚼齧黃金勒。",
+ "翻身向天仰射雲,一箭正墜雙飛翼。",
+ "明眸皓齒今何在,血污遊魂歸不得。",
+ "清渭東流劒閣深,去住彼此無消息。",
+ "人生有情淚霑臆,江水江花豈終極。",
+ "黃昏胡騎塵滿城,欲往城南忘南北。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "描写水",
+ "爱国",
+ "伤怀",
+ "乐府",
+ "新乐府辞",
+ "写水"
+ ],
+ "title": "哀江頭",
+ "id": "8474f5c4-d18e-4b42-9702-19ad909b9ea2"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "長安城頭頭白烏,夜飛延秋門上呼。",
+ "又向人家啄大屋,屋底達官走避胡。",
+ "金鞭斷折九馬死,骨肉不待同馳驅。",
+ "腰下實玦青珊瑚,可憐王孫泣路隅。",
+ "問之不肯道姓名,但道困苦乞爲奴。",
+ "已經百日竄荆棘,身上無有完肌膚。",
+ "高帝子孫盡隆準,龍種自與常人殊。",
+ "豺狼在邑龍在野,王孫善保千金軀。",
+ "不敢長語臨交衢,且爲王孫立斯須。",
+ "昨夜東風吹血腥,東來橐駝滿舊都。",
+ "朔方健兒好身手,昔何勇銳今何愚。",
+ "竊聞天子已傳位,賢德北服南單于。",
+ "花門剺面請雪恥,慎勿出口他人狙。",
+ "哀哉王孫慎勿疎,五陵佳氣無時無。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "叙事",
+ "抒情",
+ "新乐府辞",
+ "乐府",
+ "写鸟",
+ "鸟"
+ ],
+ "title": "哀王孫",
+ "id": "bd7949f8-c961-4bfd-a307-1fe9e621028c"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "絕代有佳人,幽居在空谷。",
+ "自云良家子,零落依草木。",
+ "關中昔喪敗,兄弟遭殺戮。",
+ "官高何足論,不得收骨肉。",
+ "世情惡衰歇,萬事隨轉燭。",
+ "夫壻輕薄兒,新人已如玉。",
+ "合昏尚知時,鴛鴦不獨宿。",
+ "但見新人笑,那聞舊人哭。",
+ "在山泉水清,出山泉水濁。",
+ "侍婢賣珠回,牽蘿補茅屋。",
+ "摘花不插髮,采柏動盈匊。",
+ "天寒翠袖薄,日暮倚修竹。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "赞美",
+ "五言古诗",
+ "闺怨",
+ "女子"
+ ],
+ "title": "佳人",
+ "id": "f2db30a1-f125-44b9-a8d8-287ccb69238a"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "死別已吞聲,生別常惻惻。",
+ "江南瘴癘地,逐客無消息。",
+ "故人入我夢,明我長相憶。",
+ "恐非平生魂,路遠不可測。",
+ "魂來楓葉青,魂返關塞黑。",
+ "君今在羅網,何以有羽翼。",
+ "落月滿屋梁,猶疑照顏色。",
+ "水深波浪闊,無使蛟龍得。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "夢李白二首 一",
+ "id": "c9362aba-fb50-481b-a4cd-08ddd65cdd2a"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "浮雲終日行,遊子久不至。",
+ "三夜頻夢君,情親見君意。",
+ "告歸常局促,苦道來不易。",
+ "江湖多風波,舟楫恐失墜。",
+ "出門搔白首,若負平生志。",
+ "冠蓋滿京華,斯人獨顦顇。",
+ "孰云網恢恢,將老身反累。",
+ "千秋萬歲名,寂莫身後事。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友人",
+ "同情",
+ "记梦",
+ "五言古诗"
+ ],
+ "title": "夢李白二首 二",
+ "id": "0710a746-8660-4bc1-b073-8ce51ac9f4d9"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "國初已來畫鞍馬,神妙獨數江都王。",
+ "將軍得名三十載,人間又見真乘黃。",
+ "曾貌先帝照夜白,龍池十日飛霹靂。",
+ "內府殷紅馬腦盌,倢伃傳詔才人索。",
+ "盌賜將軍拜舞歸,輕紈細綺相追飛。",
+ "貴戚權門得筆跡,始覺屏障生光輝。",
+ "昔日太宗拳毛騧,近時郭家師子花。",
+ "今之新圖有二馬,復令識者久歎嗟。",
+ "此皆騎戰一敵萬,縞素漠漠開風沙。",
+ "其餘七匹亦殊絕,迥若寒空動煙雪。",
+ "霜蹄蹴踏長楸間,馬官廝養森成列。",
+ "可憐九馬爭神駿,顧視清高氣深穩。",
+ "借問苦心愛者誰,後有韋諷前支遁。",
+ "憶昔巡幸新豐宮,翠華拂天來向東。",
+ "騰驤磊落三萬匹,皆與此圖筋骨同。",
+ "自從獻寶朝河宗,無復射蛟江水中。",
+ "君不見金粟堆前松柏裏,龍媒去盡鳥呼風。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写马",
+ "七言古诗",
+ "题画",
+ "马"
+ ],
+ "title": "韋諷錄事宅觀曹將軍畫馬圖",
+ "id": "827e44b1-7064-4ff7-9a9c-192eece94e2d"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "將軍魏武之子孫,於今爲庶爲清門。",
+ "英雄割據雖已矣,文彩風流猶尚存。",
+ "學書初學衛夫人,但恨無過王右軍。",
+ "丹青不知老將至,富貴於我如浮雲。",
+ "開元之中常引見,承恩數上南熏殿。",
+ "凌煙功臣少顏色,將軍下筆開生面。",
+ "良相頭上進賢冠,猛將腰間大羽箭。",
+ "褒公鄂公毛髮動英姿颯爽來酣戰。",
+ "先帝天馬玉花驄,畫工如山貌不同。",
+ "是日牽來赤墀下,迥立閶闔生長風。",
+ "詔謂將軍拂絹素,意匠慘澹經營中。",
+ "斯須九重真龍出,一洗萬古凡馬空。",
+ "玉花却在御榻上,榻上庭前屹相向。",
+ "至尊含笑催賜金,圉人太僕皆惆悵。",
+ "弟子韓幹早入室,亦能畫馬窮殊相。",
+ "幹惟畫肉不畫骨,忍使驊騮氣凋喪。",
+ "將軍畫善蓋有神,必逢佳士亦寫真。",
+ "即今飄泊干戈際,屢貌尋常行路人。",
+ "途窮反遭俗眼白,世上未有如公貧。",
+ "但看古來盛名下,終日坎壈纏其身。"
+ ],
+ "tags": [
+ "写人",
+ "同情",
+ "唐诗三百首",
+ "七言古诗",
+ "怅惘"
+ ],
+ "title": "丹青引贈曹將軍霸",
+ "id": "13b12259-3750-4c6d-87ee-35283ad63dab"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "今我不樂思岳陽,身欲奮飛病在牀。",
+ "美人娟娟隔秋水,濯足洞庭望八荒。",
+ "鴻飛冥冥日月白,青楓葉赤天雨霜。",
+ "玉京羣帝集北斗,或騎騏驎翳鳳皇。",
+ "芙蓉旌旗煙霧樂,影動倒景搖瀟湘。",
+ "星宮之君醉瓊漿,羽人稀少不在旁。",
+ "似聞昨者赤松子,恐是漢代韓張良。",
+ "昔隨劉氏定長安,帷幄未改神慘傷。",
+ "國家成敗吾豈敢,色難腥腐餐風香。",
+ "周南留滯古所惜,南極老人應壽昌。",
+ "美人胡爲隔秋水,焉得置之貢玉堂。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖",
+ "游仙",
+ "七言古诗"
+ ],
+ "title": "寄韓諫議",
+ "id": "6d303931-f900-4f29-a8b9-caea611a521f"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "孔明廟前有老柏,柯如青銅根如石。",
+ "霜皮溜雨四十圍,黛色參天二千尺。",
+ "君臣已與時際會,樹木猶爲人愛惜。",
+ "雲來氣接巫峽長,月出寒通雪山白。",
+ "憶昨路繞錦亭東,先主武侯同閟宮。",
+ "崔嵬枝幹郊原古,窈窕丹青戶牖空。",
+ "落落盤踞雖得地,冥冥孤高多烈風。",
+ "扶持自是神明力,正直原因造化功。",
+ "大廈如傾要梁棟,萬牛回首丘山重。",
+ "不露文章世已驚,未辭剪伐誰能送。",
+ "苦心豈免容螻蟻,香葉終經宿鸞鳳。",
+ "志士幽人莫怨嗟,古來材大難爲用。"
+ ],
+ "tags": [
+ "隋・唐・五代",
+ "咏物",
+ "唐诗三百首",
+ "七言古诗",
+ "咏物诗",
+ "怀才不遇"
+ ],
+ "title": "古柏行",
+ "id": "383d41a8-5155-4fff-b91f-87d5585ff03e"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "昔有佳人公孫氏,一舞劒氣動四方。",
+ "觀者如山色沮喪,天地爲之久低昂。",
+ "㸌如羿射九日落,矯如羣帝驂龍翔。",
+ "來如雷霆收震怒,罷如江海凝清光。",
+ "絳脣珠袖兩寂莫,況有弟子傳芬芳。",
+ "臨潁美人在白帝,妙舞此曲神揚揚。",
+ "與余問答既有以,感時撫事增惋傷。",
+ "先帝侍女八千人,公孫劒器初第一。",
+ "五十年間似反掌,風塵傾動昏王室。",
+ "棃園子弟散如煙,女樂餘姿映寒日。",
+ "金粟堆南木已拱,瞿唐石城草蕭瑟。",
+ "玳筵急管曲復終,樂極哀來月東出。",
+ "老夫不知其所往,足繭荒山轉愁疾。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "觀公孫大娘弟子舞劒器行",
+ "id": "61304178-6b16-4f98-87b2-4ad6bc98da98"
+ },
+ {
+ "author": "張佖",
+ "paragraphs": [
+ "別夢依依到謝家,小廊迴合曲闌斜。",
+ "多情只有春庭月,猶爲離人照落花。"
+ ],
+ "tags": [
+ "思念",
+ "唐诗三百首",
+ "七言绝句",
+ "月亮",
+ "爱情"
+ ],
+ "title": "寄人二首 其一",
+ "id": "4ddb6dda-68be-4bb2-8874-8508c6c6e3ad"
+ },
+ {
+ "author": "不詳",
+ "paragraphs": [
+ "秦時明月漢時關,萬里征人尚未還。",
+ "但願龍庭神將在,不教胡馬渡陰山。"
+ ],
+ "tags": [
+ "战争",
+ "写景",
+ "忧国忧民",
+ "爱国",
+ "唐诗三百首",
+ "乐府",
+ "山",
+ "边塞"
+ ],
+ "title": "雜曲歌辭 蓋羅縫 一",
+ "id": "7066e611-faa5-427c-8fce-e0bf10c55416"
+ },
+ {
+ "author": "楊敬述進",
+ "paragraphs": [
+ "迴樂峰前沙似雪,受降城外月如霜。",
+ "不知何處吹蘆管?一夜征人盡望鄉。"
+ ],
+ "tags": [
+ "思念",
+ "战争",
+ "冬天",
+ "唐诗三百首",
+ "边塞",
+ "七言绝句",
+ "月亮"
+ ],
+ "title": "雜曲歌辭 婆羅門",
+ "id": "f1320c1f-949a-4cbe-ad23-4806878632c5"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "雲想衣裳花想容,春風拂檻露華濃。",
+ "若非羣玉山頭見,會向瑤臺月下逢。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "女子",
+ "乐府",
+ "赞美",
+ "近代曲辞"
+ ],
+ "title": "雜曲歌辭 清平調 一",
+ "id": "b5f1094b-b9be-43cf-8e04-9b984642a630"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "一枝紅豔露凝香,雲雨巫山枉斷腸。",
+ "借問漢宮誰得似,可憐飛燕倚新妝。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "女子",
+ "近代曲辞",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 清平調 二",
+ "id": "73dadc56-88ff-4f11-9ff5-da0e0adf533c"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "名花傾國兩相歡,長得君王帶笑看。",
+ "解釋春風無限恨,沈香亭北倚闌干。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "乐府",
+ "写人",
+ "花",
+ "写花",
+ "近代曲辞"
+ ],
+ "title": "雜曲歌辭 清平調 三",
+ "id": "0db5450e-f1e0-4d53-8106-529def535537"
+ },
+ {
+ "author": "王維",
+ "paragraphs": [
+ "謂城朝雨浥輕塵,客舍青青柳色春。",
+ "勸君更盡一杯酒,西出陽關無故人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "八年级下册(课外)",
+ "友情",
+ "小学古诗",
+ "送别",
+ "初中古诗",
+ "乐府",
+ "近代曲辞",
+ "四年级上册"
+ ],
+ "title": "雜曲歌辭 渭城曲",
+ "id": "e4a87504-f9a3-4599-b770-aba4afd3cb04"
+ },
+ {
+ "author": "不詳",
+ "paragraphs": [
+ "勸君莫惜金縷衣,勸君惜取少年時。",
+ "花開堪折直須折,莫待無花空折枝。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "惜时",
+ "励志",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 金縷衣",
+ "id": "a8617715-ad59-4da8-8071-e9aeb1760e6a"
+ },
+ {
+ "author": "張九齡",
+ "paragraphs": [
+ "蘭葉春葳蕤,桂華秋皎潔。",
+ "欣欣此生意,自爾爲佳節。",
+ "誰知林棲者,聞風坐相悅。",
+ "草木有本心,何求美人折。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "哲理",
+ "抒志",
+ "生活",
+ "五言古诗",
+ "植物"
+ ],
+ "title": "感遇十二首 一",
+ "id": "9b4f2d56-6df4-4999-a82c-11283529dd51"
+ },
+ {
+ "author": "張九齡",
+ "paragraphs": [
+ "幽林歸獨臥,滯慮洗孤清。",
+ "持此謝高鳥,因之傳遠情。",
+ "日夕懷空意,人誰感至精。",
+ "飛沈理自隔,何所慰吾誠。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "寓人",
+ "五言古诗",
+ "哲理",
+ "写鸟",
+ "寓言",
+ "鸟"
+ ],
+ "title": "感遇十二首 二",
+ "id": "6df38a77-6e5d-43ae-a115-7de36475f53e"
+ },
+ {
+ "author": "張九齡",
+ "paragraphs": [
+ "孤鴻海上來,池潢不敢顧。",
+ "側見雙翠鳥,巢在三珠樹。",
+ "矯矯珍木巔,得無金丸懼。",
+ "美服患人指,高明逼神惡。",
+ "今我遊冥冥,弋者何所慕。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "寓人",
+ "五言古诗",
+ "生活",
+ "哲理",
+ "抒怀",
+ "写鸟",
+ "鸟"
+ ],
+ "title": "感遇十二首 四",
+ "id": "f9798830-48c1-45e3-be90-4028d93954dc"
+ },
+ {
+ "author": "張九齡",
+ "paragraphs": [
+ "江南有丹橘,經冬猶綠林。",
+ "豈伊地氣暖,自有歲寒心。",
+ "可以薦嘉客,奈何阻重深。",
+ "運命唯所遇,循環不可尋。",
+ "徒言樹桃李,此木豈無陰。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "寓人",
+ "五言古诗",
+ "咏物",
+ "人格",
+ "咏物诗",
+ "写树"
+ ],
+ "title": "感遇十二首 七",
+ "id": "11b013f9-aa6a-4e55-8e68-195dc62f161f"
+ },
+ {
+ "author": "皎然",
+ "paragraphs": [
+ "移家雖帶郭,野徑入桑麻。",
+ "近種籬邊菊,秋來未著花。",
+ "扣門無犬吠,欲去問西家。",
+ "報道山中去,歸時每日斜。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隐士",
+ "访友",
+ "生活",
+ "五言律诗"
+ ],
+ "title": "尋陸鴻漸不遇",
+ "id": "565d931a-ff49-4f37-b633-524b48c2f4f0"
+ },
+ {
+ "author": "劉禹錫",
+ "paragraphs": [
+ "西晉樓船下益州,金陵王氣黯然收。",
+ "千尋鐵鎖沈江底,一片降旛出石頭。",
+ "人世幾回傷往事,山形依舊枕江流。",
+ "今逢四海爲家日,故壘蕭蕭蘆荻秋。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "咏史怀古"
+ ],
+ "title": "西塞山懷古",
+ "id": "80beb8cc-8338-44d4-8b96-7f9d32133c97"
+ },
+ {
+ "author": "劉禹錫",
+ "paragraphs": [
+ "朱雀橋邊野草花,烏衣巷口夕陽斜。",
+ "舊時王謝堂前燕,飛入尋常百姓家。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "怀古",
+ "七言绝句",
+ "带有地名",
+ "地名"
+ ],
+ "title": "金陵五題 烏衣巷",
+ "id": "4fa92777-9bd6-45ca-9272-bac30459cf5e"
+ },
+ {
+ "author": "劉禹錫",
+ "paragraphs": [
+ "新妝面面下朱樓,深鎖春光一院愁。",
+ "行到中庭數花朵,蜻蜓飛上玉搔頭。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "春天",
+ "女子",
+ "七言绝句"
+ ],
+ "title": "和樂天春詞",
+ "id": "297dd100-6aab-4685-a52d-983fb0999c7a"
+ },
+ {
+ "author": "孟郊",
+ "paragraphs": [
+ "梧桐相待老,鴛鴦會雙死。",
+ "貞女貴狥夫,捨生亦如此。",
+ "波瀾誓不起,妾心井中水。"
+ ],
+ "tags": [
+ "赞颂",
+ "妇女",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "列女操",
+ "id": "a9d3bfca-8402-4946-ad02-53db1964a3b2"
+ },
+ {
+ "author": "孟郊",
+ "paragraphs": [
+ "慈母手中線,遊子身上衣。",
+ "臨行密密縫,意恐遲遲歸。",
+ "誰言寸草心,報得三春暉。"
+ ],
+ "tags": [
+ "杂曲歌辞",
+ "唐诗三百首",
+ "乐府",
+ "母爱",
+ "三年级下册",
+ "赞颂"
+ ],
+ "title": "遊子吟",
+ "id": "4cf19354-4c0a-4e55-980b-69fa7f81ed0f"
+ },
+ {
+ "author": "蔡襄",
+ "paragraphs": [
+ "隠隠飛橋隔野煙,石磯西畔問漁船。",
+ "桃花盡日隨流水,洞在清溪何處邊。"
+ ],
+ "tags": [
+ "描写水",
+ "写景",
+ "抒情",
+ "唐诗三百首",
+ "七言绝句",
+ "写水"
+ ],
+ "title": "度南澗",
+ "id": "e72db286-3b6a-425b-ad02-75248ea5a790"
+ },
+ {
+ "author": "錢起",
+ "paragraphs": [
+ "泉壑帶茅茨,雲霞生薜帷。",
+ "竹憐新雨後,山愛夕陽時。",
+ "閑鷺棲常早,秋花落更遲。",
+ "家童掃蘿逕,昨與故人期。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "五言律诗",
+ "邀请"
+ ],
+ "title": "谷口書齋寄楊補闕",
+ "id": "b700208d-d859-4e36-96bf-883c81d4df0f"
+ },
+ {
+ "author": "錢起",
+ "paragraphs": [
+ "上國隨緣住,來途若夢行。",
+ "浮天滄海遠,去世法舟輕。",
+ "水月通禪觀,魚龍聽梵聲。",
+ "惟憐一燈影,萬里眼中明。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "五言律诗",
+ "惜别"
+ ],
+ "title": "送僧歸日本",
+ "id": "15274300-bd45-4898-ab14-9a3dead4b620"
+ },
+ {
+ "author": "錢起",
+ "paragraphs": [
+ "二月黃鶯飛上林,春城紫禁曉陰陰。",
+ "長樂鐘聲花外盡,龍池柳色雨中深。",
+ "陽和不散窮途恨,霄漢長懷捧日新。",
+ "獻賦十年猶未遇,羞將白髮對華簪。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "援引",
+ "七言律诗"
+ ],
+ "title": "贈闕下裴舍人",
+ "id": "42f17f00-bbc1-4b5b-82c1-3e9e012725ba"
+ },
+ {
+ "author": "錢起",
+ "paragraphs": [
+ "月黑雁飛高,單于夜遁逃。",
+ "欲將輕騎逐,大雪滿弓刀。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "冬天",
+ "将军",
+ "乐府",
+ "边塞",
+ "赞美"
+ ],
+ "title": "和張僕射塞下曲",
+ "id": "c8210485-a1f4-4c76-a1f6-2f5e57d39d29"
+ },
+ {
+ "author": "元結",
+ "paragraphs": [
+ "昔歲逢太平,山林二十年。",
+ "泉源在庭戶,洞壑當門前。",
+ "井稅有常期,日[晏]猶得眠。",
+ "忽然遭世變,數歲親戎旃。",
+ "今來典斯郡,山夷又紛然。",
+ "城小賊不屠,人貧傷可憐。",
+ "是以陷隣境,此州獨見全。",
+ "使臣將王命,豈不如賊焉。",
+ "今彼徴斂者,迫之如火煎。",
+ "誰能絕人命,以作時世賢。",
+ "思欲委符節,引竿自刺船。",
+ "將家就魚麥,歸老江湖邊。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "同情",
+ "五言古诗"
+ ],
+ "title": "賊退示官吏",
+ "id": "1ce94bde-41ec-42b8-abef-e1e1be55be7c"
+ },
+ {
+ "author": "元結",
+ "paragraphs": [
+ "石魚湖,似洞庭,夏水欲滿君山青。",
+ "山爲樽,水爲沼,酒徒歷歷坐洲島。",
+ "長風連日作大浪,不能廢人運酒舫。",
+ "我持長瓢坐巴丘,酌飲四坐以散愁。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "洞庭湖"
+ ],
+ "title": "石魚湖上醉歌",
+ "id": "8af643c9-a8b0-49b7-b8e9-88920ab0bbd8"
+ },
+ {
+ "author": "張繼",
+ "paragraphs": [
+ "月落烏啼霜滿天,江楓漁父對愁眠。",
+ "姑蘇城外寒山寺,夜半鐘聲到客船。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "秋天",
+ "七言绝句",
+ "思乡",
+ "秋"
+ ],
+ "title": "楓橋夜泊",
+ "id": "3cda5a92-ffa2-4af0-9217-97ce36113705"
+ },
+ {
+ "author": "韓翃",
+ "paragraphs": [
+ "長簟迎風早,空城澹月華。",
+ "星河秋一雁,砧杵夜千家。",
+ "節候看應晚,心期臥亦賒。",
+ "向來吟秀句,不覺已鳴鴉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写景",
+ "酬答",
+ "五言律诗"
+ ],
+ "title": "酬程延秋夜即事見贈",
+ "id": "ce493923-7c4b-41a9-94e9-cf95741c018c"
+ },
+ {
+ "author": "韓翃",
+ "paragraphs": [
+ "仙臺下見五城樓,風物淒淒宿雨收。",
+ "山色遙連秦樹晚,砧聲近報漢宮秋。",
+ "疎松影落空壇靜,細草香閑小洞幽。",
+ "何用別尋方外去,人間亦自有丹丘。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "向往",
+ "七言律诗",
+ "游历"
+ ],
+ "title": "同題仙游觀",
+ "id": "b411ce1e-ac09-4427-ab32-f8dc7a57e566"
+ },
+ {
+ "author": "韓翃",
+ "paragraphs": [
+ "春城無處不飛花,寒食東風御柳斜。",
+ "日暮漢宮傳蠟燭,輕煙散入五侯家。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "清明",
+ "寒食节",
+ "典故",
+ "七言绝句",
+ "讽刺"
+ ],
+ "title": "寒食",
+ "id": "77ac62ee-6396-4ac0-a3ab-cb84ba2e5cd0"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "去年花裏逢君別,今日花開已一年。",
+ "世事茫茫難自料,春愁黯黯獨成眠。",
+ "身多疾病思田里,邑有流亡愧俸錢。",
+ "聞道欲來相問訊,西樓望月幾廻圓。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "友人",
+ "忧国忧民",
+ "七言律诗"
+ ],
+ "title": "寄李儋元錫",
+ "id": "8435b46d-bbf1-49e5-af66-6f2d5681f1bf"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "今朝郡齋冷,忽念山中客。",
+ "澗底束荆薪,歸來煮白石。",
+ "欲持一瓢酒,遠慰風雨夕。",
+ "落葉滿空山,何處尋行跡。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "怀念"
+ ],
+ "title": "寄全椒山中道士",
+ "id": "46503396-3bb3-449c-9c84-f2ff232f7949"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "懷君屬秋夜,散步詠涼天。",
+ "山空松子落,幽人應未眠。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "秋天",
+ "怀人",
+ "五言绝句"
+ ],
+ "title": "秋夜寄丘二十二員外",
+ "id": "96455db1-a0c6-402c-8892-05c6b9a3d029"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "楚江微雨裏,建業暮鐘時。",
+ "漠漠帆來重,冥冥鳥去遲。",
+ "海門深不見,浦樹遠含滋。",
+ "相送情無限,沾襟比散絲。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雨",
+ "友情",
+ "送别",
+ "五言律诗",
+ "雨"
+ ],
+ "title": "賦得暮雨送李胄",
+ "id": "83dcb67c-ccbc-4592-b232-368a6b5275ce"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "永日方慼慼,出門復悠悠。",
+ "女子今有行,大江泝輕舟。",
+ "爾輩況無恃,撫念益慈柔。",
+ "幼爲長所育,兩別泣不休。",
+ "對此結中腸,義往難復留。",
+ "自小闕內訓,事姑貽我憂。",
+ "賴茲託令門,仁恤庶無尤。",
+ "貧儉誠所尚,資從豈待周。",
+ "孝恭遵婦道,容止順其猷。",
+ "別離在今晨,見爾當何秋。",
+ "居閑始自遣,臨感忽難收。",
+ "歸來視幼女,零淚緣纓流。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "送别",
+ "感伤",
+ "五言古诗"
+ ],
+ "title": "送楊氏女",
+ "id": "f800050a-d7a0-4f12-a614-17fe23df8f41"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "客從東方來,衣上灞陵雨。",
+ "問客何爲來,采山因買斧。",
+ "冥冥花正開,颺颺燕新乳。",
+ "昨別今已春,鬢絲生幾縷。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "叙事",
+ "同情",
+ "抒情",
+ "五言古诗"
+ ],
+ "title": "長安遇馮著",
+ "id": "b8ae44b3-ba8d-42d3-85a7-a70aa8608379"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "落帆逗淮鎮,停舫臨孤驛。",
+ "浩浩風起波,冥冥日沈夕。",
+ "人歸山郭暗,雁下蘆洲白。",
+ "獨夜憶秦關,聽鐘未眠客。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "写景",
+ "抒情",
+ "思乡"
+ ],
+ "title": "夕次盱眙縣",
+ "id": "11424603-9a22-4564-89a4-95e1766f9685"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "吏舍跼終年,出郊曠清曙。",
+ "楊柳散和風,青山澹吾慮。",
+ "依叢適自憩,緣澗還復去。",
+ "微雨靄芳原,春鳩鳴何處?樂幽心屢止,遵事跡猶遽。",
+ "終罷斯結廬,慕陶真可庶。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "春天",
+ "生活"
+ ],
+ "title": "東郊",
+ "id": "3e0e1d96-c249-47c4-b16d-4d91dd6e7cce"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "獨憐幽草澗邊生,上有黃鸝深樹鳴。",
+ "春潮帶雨晚來急,野渡無人舟自橫。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "春",
+ "山水",
+ "写景",
+ "抒情",
+ "初中古诗",
+ "七言绝句",
+ "水",
+ "七年级下册(课外)"
+ ],
+ "title": "滁州西澗",
+ "id": "0ae0e3bd-a32e-481f-9719-b283da2da964"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "獨有宦遊人,偏驚物候新。",
+ "雲霞出海曙,梅柳渡江春。",
+ "淑氣催黃鳥,晴光照綠蘋。",
+ "忽聞歌苦調,歸思欲霑巾。"
+ ],
+ "tags": [
+ "春天",
+ "唐诗三百首",
+ "伤怀",
+ "和诗",
+ "思乡",
+ "五言律诗"
+ ],
+ "title": "和晉陵陸丞早春遊望",
+ "id": "b67ddf05-43fb-4859-a1a9-34f95d45aabf"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "塔勢如湧出,孤高聳天宮。",
+ "登臨出世界,磴道盤虛空。",
+ "突兀壓神州,崢嶸如鬼工。",
+ "四角礙白日,七層摩蒼穹。",
+ "下窺指高鳥,俯聽聞驚風。",
+ "連山若波濤,奔湊似朝東。",
+ "青槐夾馳道,宮館何玲瓏。",
+ "秋色從西來,蒼然滿關中。",
+ "五陵北原上,萬古青濛濛。",
+ "淨理了可悟,勝因夙所宗。",
+ "誓將挂冠去,覺道資無窮。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "景点",
+ "写景",
+ "忧思",
+ "写塔"
+ ],
+ "title": "與高適薛據慈恩寺浮圖",
+ "id": "93d82a12-5e4d-45c2-b837-d0c54447fba3"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "北風捲地白草折,胡天八月即飛雪。",
+ "忽然一夜春風來,千樹萬樹梨花開。",
+ "散入珠簾濕羅幕,狐裘不煖錦衾薄。",
+ "將軍角弓不得控,都護鐵衣冷難着。",
+ "瀚海闌干百丈冰,愁雲黲淡萬里凝。",
+ "中軍置酒飲歸客,胡琴琵琶與羌笛。",
+ "紛紛暮雪下轅門,風掣紅旗凍不翻。",
+ "輪臺東門送君去,去時雪滿天山路。",
+ "山迴路轉不見君,雪上空留馬行處。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写雪",
+ "友情",
+ "咏物",
+ "八年级下册(课内)",
+ "送别",
+ "雪",
+ "初中古诗",
+ "冬天",
+ "山",
+ "七言古诗",
+ "咏物诗"
+ ],
+ "title": "白雪歌送武判官歸京",
+ "id": "294bc7b2-b4f8-4a53-be17-a40ded4afd1a"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "輪臺城頭夜吹角,輪臺城北旄頭落。",
+ "羽書昨夜過渠黎,單于已在金山西。",
+ "戍樓西望煙塵黑,漢兵屯在輪臺北。",
+ "上將擁旄西出征,平明吹笛大軍行。",
+ "四邊伐鼓雪海湧,三軍大呼陰山動。",
+ "虜塞兵氣連雲屯,戰場白骨纏草根。",
+ "劒河風急雪片闊,沙口石凍馬蹄脫。",
+ "亞相勤王甘苦辛,誓將報主靜邊塵。",
+ "古來青史誰不見,今見功名勝古人。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "战争",
+ "生活",
+ "七言古诗",
+ "赞扬",
+ "边塞",
+ "将士"
+ ],
+ "title": "輪臺歌奉送封大夫出師西征",
+ "id": "d129dc0f-cec9-4073-bb89-cb9312564d9d"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "君不見走馬川行雪海邊,平沙莽莽黃入天。",
+ "輪臺九月風夜吼,一川碎石大如斗,隨風滿地石亂走。",
+ "匈奴草黃馬正肥,金山西見煙塵飛。",
+ "漢家大將西出師,將軍金甲夜不脫。",
+ "半夜軍行戈相撥,風頭如刀面如割。",
+ "馬毛帶雪汗氣蒸,五花連錢旋作冰。",
+ "幕中草檄硯水凝,虜騎聞之應膽懾。",
+ "料知短兵不敢接,車師西門佇獻捷。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "写马",
+ "写风",
+ "送别",
+ "初中古诗",
+ "七言古诗",
+ "九年级下册(课外)",
+ "风",
+ "边塞",
+ "赞美",
+ "马",
+ "将士"
+ ],
+ "title": "走馬川行奉送出師西征",
+ "id": "f5e021d7-ea56-4341-947c-8fd024018e1c"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "聯步趨丹陛,分曹限紫微。",
+ "曉隨天仗入,暮惹御香歸。",
+ "白髮悲花落,青雲羨鳥飛。",
+ "聖朝無闕事,自覺諫書稀。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "仕途",
+ "五言律诗",
+ "感慨"
+ ],
+ "title": "寄左省杜拾遺",
+ "id": "2779f710-4ab4-46cb-a2b6-1889752f26c2"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "雞鳴紫陌曙光寒,鶯囀皇州春色闌。",
+ "金闕曉鐘開萬戶,玉階仙仗擁千官。",
+ "花迎劒珮星初落,柳拂旌旗露未乾。",
+ "獨有鳳皇池上客,陽春一曲和皆難。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言律诗",
+ "唱和"
+ ],
+ "title": "奉和中書舍人賈至早朝大明宮",
+ "id": "8df8e94b-ffe4-4930-af4c-491d7cb04651"
+ },
+ {
+ "author": "岑參",
+ "paragraphs": [
+ "故園東望路漫漫,雙袖龍鍾淚不乾。",
+ "馬上相逢無紙筆,憑君傳語報平安。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "边塞",
+ "初中古诗",
+ "七言绝句",
+ "思乡",
+ "七年级下册(课外)"
+ ],
+ "title": "逢入京使",
+ "id": "07238ad9-c348-43d4-b1d7-25d89369286d"
+ },
+ {
+ "author": "孫革",
+ "paragraphs": [
+ "松下問童子,言師採藥去。",
+ "只在此山中,雲深不知處。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "写人",
+ "寻访"
+ ],
+ "title": "訪羊尊師",
+ "id": "162eb552-a496-4979-87e2-e441847d4e6f"
+ },
+ {
+ "author": "張喬",
+ "paragraphs": [
+ "調角斷清秋,征人倚戍樓。",
+ "春風對青塚,白日落梁州。",
+ "大漢無兵阻,窮邊有客遊。",
+ "蕃情似此水,長願向南流。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "生活",
+ "五言律诗",
+ "边塞"
+ ],
+ "title": "書邊事",
+ "id": "8324db66-b75a-40ff-9469-cba276d2f88f"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "金樽清酒斗十千,玉盤珍羞直萬錢。",
+ "停杯投筯不能食,拔劒四顧心茫然。",
+ "欲渡黃河冰塞川,將登太行雪滿山。",
+ "閑來垂釣碧溪上,忽復乘舟夢日邊。",
+ "行路難,行路難,多岐路,今安在。",
+ "長風破浪會有時,直挂雲帆濟滄海。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄河",
+ "励志",
+ "友情",
+ "八年级下册(课内)",
+ "初中古诗",
+ "乐府",
+ "宴饮",
+ "哲理",
+ "怀才不遇"
+ ],
+ "title": "行路難三首 一",
+ "id": "c348bc2e-f50d-436b-88b7-c198d63dacfc"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "大道如青天,我獨不得出。",
+ "羞逐長安社中兒,赤雞白狗賭梨栗。",
+ "彈劒作歌奏苦聲,曳裾王門不稱情。",
+ "淮陰市井笑韓信,漢朝公卿忌賈生。",
+ "君不見昔時燕家重郭隗,擁篲折節無嫌猜。",
+ "劇辛樂毅感恩分,輸肝剖膽效英才。",
+ "昭王白骨縈爛草,誰人更掃黃金臺。",
+ "行路難,歸去來。"
+ ],
+ "tags": [
+ "乐府",
+ "失意",
+ "叹息",
+ "唐诗三百首"
+ ],
+ "title": "行路難三首 二",
+ "id": "801c3192-8001-4238-8ecf-0111f490e84c"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "有耳莫洗潁川水,有口莫食首陽蕨。",
+ "含光混世貴無名,何用孤高比雲月。",
+ "吾觀自古賢達人,功成不退皆殞身。",
+ "子胥既棄吳江上,屈原終投湘水濱。",
+ "陸機雄才豈自保,李斯稅駕苦不早。",
+ "華亭鶴唳詎可聞,上蔡蒼鷹何足道。",
+ "君不見吳中張翰稱達生,秋風忽憶江東行。",
+ "且樂生前一杯酒,何須身後千載名。"
+ ],
+ "tags": [
+ "人生",
+ "唐诗三百首",
+ "乐府",
+ "态度"
+ ],
+ "title": "行路難三首 三",
+ "id": "a7b8e17f-ee93-4bdc-a144-b1ba5ab32bb5"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "長相思,在長安,絡緯秋啼金井闌。",
+ "微霜淒淒簟色寒,孤燈不明思欲絕。",
+ "卷帷望月空長歎,美人如花隔雲端。",
+ "上有青冥之長天,下有淥水之波瀾。",
+ "天長路遠魂飛苦,夢魂不到關山難。",
+ "長相思,摧心肝。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "杂曲歌辞",
+ "乐府",
+ "伤怀"
+ ],
+ "title": "長相思",
+ "id": "3c1693fc-f4e6-43b7-a66f-6379c3a26eef"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "明月出天山,蒼茫雲海間。",
+ "長風幾萬里,吹度玉門關。",
+ "漢下白登道,胡窺青海灣。",
+ "由來征戰地,不見有人還。",
+ "[戍]客望邊色,思歸多苦顏。",
+ "高樓當此夜,歎息未應閑。"
+ ],
+ "tags": [
+ "中秋",
+ "唐诗三百首",
+ "边塞",
+ "山",
+ "思乡",
+ "乐府",
+ "描写山",
+ "写山",
+ "征人"
+ ],
+ "title": "關山月",
+ "id": "4be77185-992e-4614-9746-de2e73c8a3c3"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "玉階生白露,夜久侵羅襪。",
+ "却下水晶簾,玲瓏望秋月。"
+ ],
+ "tags": [
+ "孤独",
+ "宫怨",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "玉階怨",
+ "id": "a65e5646-14b1-4f75-924b-bbf262b242d7"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "雲想衣裳花想容,春風拂檻露華濃。",
+ "若非羣玉山頭見,會向瑤臺月下逢。"
+ ],
+ "tags": [
+ "女子",
+ "唐诗三百首",
+ "乐府",
+ "赞美",
+ "近代曲辞"
+ ],
+ "title": "清平調詞三首 一",
+ "id": "948fa40a-492e-4fa1-99f4-b002ae87cd24"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "一枝穠豔露凝香,雲雨巫山枉斷腸。",
+ "借問漢宮誰得似,可憐飛燕倚新妝。"
+ ],
+ "tags": [
+ "女子",
+ "近代曲辞",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "清平調詞三首 二",
+ "id": "99167d13-8e2c-4b75-bf93-62b0682dfdd0"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "牀前看月光,疑是地上霜。",
+ "舉頭望山月,低頭思故鄉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "新乐府辞",
+ "思乡",
+ "中秋",
+ "一年级上册",
+ "月亮",
+ "五言绝句",
+ "小学古诗"
+ ],
+ "title": "靜夜思",
+ "id": "ca2c489a-e433-4c0f-8248-77d354f0665e"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "燕草如碧絲,秦桑低綠枝。",
+ "當君懷歸日,是妾斷腸時。",
+ "春風不相識,何事入羅幃。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "五言古诗",
+ "春天",
+ "坚贞",
+ "妇女"
+ ],
+ "title": "春思",
+ "id": "7277c420-9f3f-4093-a5b2-6331494721e8"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "秦地羅敷女,採桑綠水邊。",
+ "素手青條上,紅妝白日鮮。",
+ "蠶飢妾欲去,五馬莫留連。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "清商曲辞",
+ "春天",
+ "女子",
+ "乐府",
+ "赞美"
+ ],
+ "title": "子夜吳歌 春歌",
+ "id": "57203d7f-380c-4052-b21b-d1ea79fbe231"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "鏡湖三百里,菡萏發荷花。",
+ "五月西施採,人看隘若耶。",
+ "回舟不待月,歸去越王家。"
+ ],
+ "tags": [
+ "清商曲辞",
+ "夏天",
+ "唐诗三百首",
+ "乐府",
+ "哲理",
+ "写人",
+ "荷花"
+ ],
+ "title": "子夜吳歌 夏歌",
+ "id": "abb3e465-f10e-43dd-b564-a5de08010478"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "長安一片月,萬戶擣衣聲。",
+ "秋風吹不盡,總是玉關情。",
+ "何日平胡虜,良人罷遠征。"
+ ],
+ "tags": [
+ "思念",
+ "清商曲辞",
+ "妇女",
+ "秋天",
+ "唐诗三百首",
+ "乐府",
+ "月亮",
+ "闺怨"
+ ],
+ "title": "子夜吳歌 秋歌",
+ "id": "baccf46a-41c4-4a0f-b90f-37cc35b04a64"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "明朝驛使發,一夜絮征袍。",
+ "素手抽針冷,那堪把剪刀。",
+ "裁縫寄遠道,幾日到臨洮。"
+ ],
+ "tags": [
+ "思念",
+ "清商曲辞",
+ "妇女",
+ "冬天",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "子夜吳歌 冬歌",
+ "id": "f990af2e-d929-48da-a9b3-fdbc706ca4a3"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "吾愛孟夫子,風流天下聞。",
+ "紅顏棄軒冕,白首臥松雲。",
+ "醉月頻中聖,迷花不事君。",
+ "高山安可仰,徒此揖清芬。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "赠别",
+ "五言律诗",
+ "敬爱"
+ ],
+ "title": "贈孟浩然",
+ "id": "9d1b6a18-571c-4f25-bfdb-3fe260115c3f"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "我本楚狂人,鳳歌笑孔丘。",
+ "手持綠玉杖,朝別黃鶴樓。",
+ "五嶽尋仙不辭遠,一生好入名山遊。",
+ "廬山秀出南斗傍,屏風九疊雲錦張,影落明湖青黛光。",
+ "金闕前開二峰長,銀河倒挂三石梁。",
+ "香爐瀑布遙相望,迴厓沓嶂凌蒼蒼。",
+ "翠影紅霞映朝日,鳥飛不到吳天長。",
+ "登高壯觀天地間,大江茫茫去不還。",
+ "黃雲萬里動風色,白波九道流雪山。",
+ "好爲廬山謠,興因廬山發。",
+ "閑窺石鏡清我心,謝公行處蒼苔沒。",
+ "早服還丹無世情,琴心三疊道初成。",
+ "遙見仙人綵雲裏,手把芙蓉朝玉京。",
+ "先期汗漫九垓上,願接盧敖遊太清。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "庐山",
+ "黄鹤楼",
+ "写景",
+ "归隐",
+ "七言古诗",
+ "山水"
+ ],
+ "title": "廬山謠寄盧侍御虛舟",
+ "id": "4e491c2f-c3c8-487f-8e76-63bd0a811b30"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "海客談瀛洲,煙濤微茫信難求。",
+ "越人語天姥,雲霓明滅或可覩。",
+ "天姥連天向天橫,勢拔五嶽掩赤城。",
+ "天台四萬八千丈,對此欲倒東南傾。",
+ "我欲因之夢吳越,一夜飛度鏡湖月。",
+ "湖月照我影,送我至剡溪。",
+ "謝公宿處今尚在,淥水蕩漾清猨啼。",
+ "脚著謝公屐,身登青雲梯。",
+ "半壁見海日,空中聞天雞。",
+ "千巖萬轉路不定,迷花倚石忽已暝。",
+ "熊咆龍吟殷巖泉,慄深林兮驚層巔。",
+ "雲青青兮欲雨,水澹澹兮生煙。",
+ "列缺霹靂,丘巒崩摧。",
+ "洞天石扇,訇然中開。",
+ "青冥浩蕩不見底,日月照耀金銀臺。",
+ "霓爲衣兮風爲馬,雲之君兮紛紛而來下。",
+ "虎鼓瑟兮鸞迴車,仙之人兮列如麻。",
+ "忽魂悸以魄動,怳驚起而長嗟。",
+ "惟覺時之枕席,失向來之煙霞。",
+ "世間行樂亦如此,古來萬事東流水。",
+ "別君去時何時還,且放白鹿青崖間,須行即騎訪名山。",
+ "安能摧眉折腰事權貴?使我不得開心顏。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "一年级下册",
+ "七言古诗",
+ "高中古诗",
+ "记梦",
+ "古体"
+ ],
+ "title": "夢遊天姥吟留別",
+ "id": "606d0bd8-31d2-4a7a-97ee-341956e7be60"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "風吹柳花滿店香,吳姬壓酒喚客嘗。",
+ "金陵子弟來相送,欲行不行各盡觴。",
+ "請君試問東流水,別意與之誰短長。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "七言古诗",
+ "离别",
+ "饮酒"
+ ],
+ "title": "金陵酒肆留別",
+ "id": "809ad73b-9a8e-436e-9874-f9295b624af0"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "故人西辭黃鶴樓,煙花三月下揚州。",
+ "孤帆遠影碧山盡,唯見長江天際流。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "黄鹤楼",
+ "离别",
+ "带有地名",
+ "小学古诗",
+ "地名",
+ "描写水",
+ "友情",
+ "写景",
+ "送别",
+ "七言绝句",
+ "水",
+ "名楼、庙宇",
+ "写水",
+ "四年级上册"
+ ],
+ "title": "黃鶴樓送孟浩然之廣陵",
+ "id": "d43a4767-2240-44b7-8d5b-c2acdc689839"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "渡遠荆門外,來從楚國遊。",
+ "山隨平野盡,江入大荒流。",
+ "月下飛天鏡,雲生結海樓。",
+ "仍連故鄉水,萬里送行舟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "送别",
+ "五言律诗",
+ "初中古诗",
+ "八年级上册(课内)",
+ "思乡"
+ ],
+ "title": "渡荆門送別",
+ "id": "bf8918b8-a05a-4ccf-bb2e-3f9b81f6bbfc"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "青山橫北郭,白水遶東城。",
+ "此地一爲別,孤蓬萬里征。",
+ "浮雲遊子意,落日故人情。",
+ "揮手自茲去,蕭蕭班馬鳴。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "八年级上册(课外)",
+ "友情",
+ "写景",
+ "送别",
+ "五言律诗",
+ "初中古诗",
+ "惜别"
+ ],
+ "title": "送友人",
+ "id": "1f83fccf-7476-464b-8cd4-8a2b4569c7a0"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "棄我去者昨日之日不可留,亂我心者今日之日多煩憂。",
+ "長風萬里送秋雁,對此可以酣高樓。",
+ "蓬萊文章建安骨,中間小謝又清發。",
+ "俱懷逸興壯思飛,欲上青天覽日月。",
+ "抽刀斷水水更流,舉杯銷愁愁更愁。",
+ "人生在世不稱意,明朝散髮弄扁舟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "八年级下册(课外)",
+ "七言古诗",
+ "初中古诗",
+ "哲理",
+ "离别",
+ "抒怀",
+ "水",
+ "豪迈"
+ ],
+ "title": "宣州謝朓樓餞別校書叔雲",
+ "id": "5593d522-b820-4c06-bab6-0ab5a0fc1d46"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "暮從碧山下,山月隨人歸。",
+ "却顧所來徑,蒼蒼橫翠微。",
+ "相攜及田家,童稚開荆扉。",
+ "綠竹入幽徑,青蘿拂行衣。",
+ "歡言得所憩,美酒聊共揮。",
+ "長歌吟松風,曲盡河星稀。",
+ "我醉君復樂,陶然共忘機。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "终南山",
+ "五言古诗",
+ "田园",
+ "写景",
+ "饮酒"
+ ],
+ "title": "下終南山過斛斯山人宿置酒",
+ "id": "ca4cc6b3-a324-48c0-89c0-bb36c09c4930"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "鳳凰臺上鳳凰遊,鳳去臺空江自流。",
+ "吳宮花草埋幽徑,晉代衣冠成古丘。",
+ "三山半落青天外,二水中分白鷺洲。",
+ "總爲浮雲能蔽日,長安不見使人愁。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "隋・唐・五代",
+ "七言律诗",
+ "一年级下册",
+ "高中古诗",
+ "咏史怀古"
+ ],
+ "title": "登金陵鳳凰臺",
+ "id": "4af86c76-18b3-4069-be90-6d8cef430bc7"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "朝辭白帝彩雲間,千里江陵一日還。",
+ "兩岸猨聲啼不盡,輕舟已過萬重山。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "长江",
+ "山",
+ "喜悦",
+ "七言绝句",
+ "描写山",
+ "写山",
+ "带有地名",
+ "地名"
+ ],
+ "title": "早發白帝城",
+ "id": "9525f99d-39cb-4d04-8650-263493d223e0"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "牛渚西江夜,青天無片雲。",
+ "登舟望秋月,空憶謝將軍。",
+ "余亦能高詠,斯人不可聞。",
+ "明朝挂帆席,楓葉落紛紛。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "怀古",
+ "五言律诗",
+ "月亮"
+ ],
+ "title": "夜泊牛渚懷古",
+ "id": "7e1be9c9-fa2f-44c2-a6bc-3edcece745fe"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "花間一壺酒,獨酌無相親。",
+ "舉杯邀明月,對影成三人。",
+ "月既不解飲,影徒隨我身。",
+ "暫伴月將影,行樂須及春。",
+ "我歌月裴回,我舞影零亂。",
+ "醒時同交歡,醉後各分散。",
+ "永結無情遊,相期邈雲漢。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "五言古诗",
+ "景中情",
+ "抒情",
+ "中秋",
+ "初中古诗",
+ "月下",
+ "饮酒",
+ "九年级下册(课外)"
+ ],
+ "title": "月下獨酌四首 一",
+ "id": "83e70b6e-ff59-4a3b-8e81-249ab0cda0c3"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "蜀僧抱綠綺,西下峨眉峰。",
+ "爲我一揮手,如聽萬壑松。",
+ "客心洗流水,餘響入霜鐘。",
+ "不覺碧山暮,秋雲暗幾重。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "音乐",
+ "五言律诗"
+ ],
+ "title": "聽蜀僧濬彈琴",
+ "id": "71999aa1-55dd-4449-81b0-41f04686bf2e"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "美人捲珠簾,深坐顰蛾眉。",
+ "但見淚痕濕,不知心恨誰。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言绝句",
+ "女子"
+ ],
+ "title": "怨情",
+ "id": "22e72306-f41c-4621-a925-83bab7f9d76e"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "江漢曾爲客,相逢每醉還。",
+ "浮雲一別後,流水十年間。",
+ "歡笑情如舊,蕭疎鬢已斑。",
+ "何因北歸去,淮上對秋山。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "友情",
+ "五言律诗"
+ ],
+ "title": "淮上喜會梁川故人",
+ "id": "a68013c8-4630-41c6-a5ee-a658b57f58fe"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "兵衛森畫戟,宴寢凝清香。",
+ "海上風雨至,逍遙池閣涼。",
+ "煩疴近消散,嘉賓復滿堂。",
+ "自慙居處崇,未覩斯民康。",
+ "理會是非遣,性達形迹忘。",
+ "鮮肥屬時禁,蔬果幸見嘗。",
+ "俯飲一杯酒,仰聆金玉章。",
+ "神歡體自輕,意欲凌風翔。",
+ "吳中盛文史,羣彥今汪洋。",
+ "方知大藩地,豈曰財賦疆。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "五言古诗",
+ "宴会"
+ ],
+ "title": "郡齋雨中與諸文士燕集",
+ "id": "9f5ded08-4f6f-4641-878e-897904483479"
+ },
+ {
+ "author": "韋應物",
+ "paragraphs": [
+ "悽悽去親愛,泛泛入煙霧。",
+ "歸棹洛陽人,殘鐘廣陵樹。",
+ "今朝此爲別,何處還相遇。",
+ "世事波上舟,沿洄安得住。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "离别",
+ "五言古诗",
+ "依恋"
+ ],
+ "title": "初發揚子寄元大校書",
+ "id": "5b3a5651-2a80-4fb1-b1bf-c9fb5ec38905"
+ },
+ {
+ "author": "李益",
+ "paragraphs": [
+ "嫁得瞿塘賈,朝朝誤妾期。",
+ "早知潮有信,嫁與弄潮兒。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "闺怨",
+ "乐府"
+ ],
+ "title": "相和歌辭 江南曲",
+ "id": "bb111748-d8f1-4a5e-b618-b70272a7a642"
+ },
+ {
+ "author": "高適",
+ "paragraphs": [
+ "漢家煙塵在東北,漢將辭家破殘賊。",
+ "男兒本自重橫行,天子非常賜顏色。",
+ "摐金伐鼓下榆關,旌旗逶迤碣石間。",
+ "校尉羽書飛瀚海,單于獵火照狼山。",
+ "山川蕭條極邊土,胡騎憑凌雜風雨。",
+ "戰士軍前半死生,美人帳下猶歌舞。",
+ "大漠窮秋塞草衰,孤城落日鬬兵稀。",
+ "身當恩遇常輕敵,力盡關山未解圍。",
+ "鐵衣遠戍辛勤久,玉箸應啼別離後。",
+ "少婦城南欲斷腸,征人薊北空回首。",
+ "邊風飄飄那可度,絕域蒼茫更何有。",
+ "殺氣三日作陣雲,寒聲一夜傳刁斗。",
+ "相看白刃血紛紛,死節從來豈顧勳。",
+ "君不見沙場征戰苦,至今猶憶李將軍。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "相和歌辭 燕歌行",
+ "id": "09788f19-1e11-4394-a543-0edf2cfdcae9"
+ },
+ {
+ "author": "李頎",
+ "paragraphs": [
+ "白日登山望烽火,昏黃飲馬傍交河。",
+ "行人刁斗風砂暗,公主琵琶幽怨多。",
+ "野營萬里無城郭,雨雪紛紛連大漠。",
+ "胡雁哀鳴夜夜飛,胡兒眼淚雙雙落。",
+ "聞道玉門猶被遮,應將性命逐輕車。",
+ "年年戰骨埋荒外,空見蒲萄入漢家。"
+ ],
+ "tags": [
+ "生活",
+ "冬天",
+ "唐诗三百首",
+ "乐府",
+ "边塞",
+ "相和歌辞"
+ ],
+ "title": "相和歌辭 從軍行",
+ "id": "0ed53ef4-30ca-41ac-a629-d778e5cc2bfa"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "噫吁嚱!危乎高哉!蜀道之難難於上青天!蠶叢及魚鳧,開國何茫然。",
+ "爾來四萬八千歲,乃與秦塞通人煙。",
+ "西當太白有鳥道,可以橫絕峨眉巔。",
+ "地崩山摧壯士死,然後天梯石棧方鉤連。",
+ "上有六龍迴日之高標,下有衝波逆折之迴川。",
+ "黃鶴之飛尚不得,猨猱欲度愁攀緣。",
+ "青泥何盤盤,百步九折縈巖巒。",
+ "捫參歷井仰脅息,以手撫膺坐長歎。",
+ "問君西遊何時還?畏途巉巖不可攀。",
+ "但見悲鳥號枯木,雄飛呼雌繞林間。",
+ "又聞子規啼夜月,愁空山,蜀道之難難於上青天!使人聽此彫朱顏。",
+ "連峰去天不盈尺,枯松倒挂倚絕壁。",
+ "飛湍瀑流相喧豗,砅崖轉石萬壑雷。",
+ "其嶮也若此,嗟爾遠道之人胡爲乎來哉?劒閣崢嶸而崔嵬,一夫當關,萬夫莫開。",
+ "所守或匪親,化爲狼與豺。",
+ "朝避猛虎,夕避長蛇。",
+ "磨牙吮血,殺人如麻。",
+ "錦城雖云樂,不如早還家。",
+ "蜀道之難難於上青天,側身西望長咨嗟。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "山水",
+ "抒情",
+ "乐府",
+ "相和歌辞"
+ ],
+ "title": "相和歌辭 蜀道難",
+ "id": "e117d224-39ca-4eba-a047-0d36ce8b8c26"
+ },
+ {
+ "author": "王昌齡",
+ "paragraphs": [
+ "奉帚平明金殿開,暫將團扇共裴回。",
+ "玉顏不及寒鵶色,猶帶昭陽日影來。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "相和歌辭 長信怨 二",
+ "id": "1b6f2389-2d09-4d51-982a-e7efb35113c5"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "玉階生白露,夜久侵羅襪。",
+ "却下水精簾,玲瓏望秋月。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "宫怨",
+ "乐府"
+ ],
+ "title": "相和歌辭 玉階怨",
+ "id": "f93545e7-747e-4d12-a6c8-e484c46b2860"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "秦地羅敷女,采桑綠水邊。",
+ "素手青條上,紅妝白日鮮。",
+ "蠶飢妾欲去,五馬莫留連。"
+ ],
+ "tags": [
+ "清商曲辞",
+ "春天",
+ "女子",
+ "唐诗三百首",
+ "乐府",
+ "赞美"
+ ],
+ "title": "相和歌辭 子夜四時歌四首 春歌",
+ "id": "4e92a96d-aa74-4153-8314-b6df22d1e18a"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "鏡湖三百里,菡萏發荷花。",
+ "五月西施采,人看隘若邪。",
+ "迴舟不待月,歸去越王家。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "清商曲辞",
+ "夏天",
+ "乐府",
+ "哲理",
+ "写人",
+ "荷花"
+ ],
+ "title": "相和歌辭 子夜四時歌四首 夏歌",
+ "id": "4bd5be2b-5788-42c0-a300-9d106a73d46e"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "長安一片月,萬戶擣衣聲。",
+ "秋風吹不盡,總是玉關情。",
+ "何日平胡虜?良人罷遠征。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "清商曲辞",
+ "妇女",
+ "秋天",
+ "乐府",
+ "月亮",
+ "闺怨"
+ ],
+ "title": "相和歌辭 子夜四時歌四首 秋歌",
+ "id": "49194c4b-b775-4d21-a506-fc0a2ff308f1"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "明朝驛使發,一夜絮征袍。",
+ "素手抽針冷,那堪把剪刀。",
+ "裁縫寄遠道,幾日到臨洮。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "清商曲辞",
+ "妇女",
+ "冬天",
+ "乐府"
+ ],
+ "title": "相和歌辭 子夜四時歌四首 冬歌",
+ "id": "35319d7e-9cf6-43b0-bfa3-991cf82a49f9"
+ },
+ {
+ "author": "孟郊",
+ "paragraphs": [
+ "梧桐相待老,鴛鴦會雙死。",
+ "貞婦貴徇夫,舍生亦如此。",
+ "波瀾誓不起,妾心井中水。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "赞颂",
+ "妇女",
+ "乐府"
+ ],
+ "title": "琴曲歌辭 列女操",
+ "id": "7ccd9915-51a1-4748-9989-af243036455b"
+ },
+ {
+ "author": "孟郊",
+ "paragraphs": [
+ "慈母手中線,遊子身上衣。",
+ "臨行密密縫,意恐遲遲歸。",
+ "誰言寸草心,報得三春暉。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "杂曲歌辞",
+ "乐府",
+ "母爱",
+ "三年级下册",
+ "赞颂"
+ ],
+ "title": "雜曲歌辭 遊子吟",
+ "id": "22923091-18da-429d-9ef1-6b367fd74b1a"
+ },
+ {
+ "author": "杜甫",
+ "paragraphs": [
+ "三月三日天氣新,長安水邊多麗人。",
+ "態濃意遠淑且真,肌理細膩骨肉勻。",
+ "繡羅衣裳照暮春,蹙金孔雀銀麒麟。",
+ "頭上何所有?翠微㔩葉垂鬢脣。",
+ "背後何所見?珠壓腰衱穩稱身。",
+ "就中雲幕椒房親,賜名大國虢與秦。",
+ "紫駞之峯出翠釜,水晶之盤行素鱗。",
+ "犀筯厭飫久未下,鸞刀縷切空紛綸。",
+ "黃門飛鞚不動塵,御廚絲絡送八珍。",
+ "簫鼓哀吟感鬼神,賓從雜遝實要津。",
+ "後來鞍馬何逡巡,當軒下馬入錦茵。",
+ "楊花雪落覆白蘋,青鳥飛去銜紅巾。",
+ "炙手可熱勢絕倫,慎莫近前丞相嗔。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "杂曲歌辞",
+ "乐府",
+ "讽刺",
+ "咏叹"
+ ],
+ "title": "雜曲歌辭 麗人行",
+ "id": "aaae5882-9b74-4b45-9bb5-9f773a721119"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "長相思,在長安,絡緯秋啼金井欄。",
+ "微霜淒淒簟色寒,孤燈不明思欲絕。",
+ "卷帷望月空長歎,美人如花隔雲端。",
+ "上有青冥之長天,下有綠水之波瀾。",
+ "天長路遠魂飛苦,夢魂不到關山難。",
+ "長相思,摧心肝。"
+ ],
+ "tags": [
+ "思念",
+ "杂曲歌辞",
+ "唐诗三百首",
+ "乐府",
+ "伤怀"
+ ],
+ "title": "雜曲歌辭 長相思三首 一",
+ "id": "d4baf2d1-ea61-4fb3-a284-e889275bdca2"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "日色已盡花含煙,月明欲素愁不眠。",
+ "趙瑟初停鳳凰柱,蜀琴欲奏鴛鴦弦。",
+ "此曲有意無人傳,願隨春風寄燕然,憶君迢迢隔青天。",
+ "昔日橫波目,今成流淚泉。",
+ "不信妾腸斷,歸來看取明鏡前。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 長相思三首 二",
+ "id": "caa023cf-9f55-450c-9638-1cf5c0223cc1"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "金尊清酒斗十千,玉盤珍羞直萬錢。",
+ "停杯投筯不能食,拔劒四顧心茫然。",
+ "欲渡黃河冰塞川,將登太行雪暗天。",
+ "閑來垂釣坐溪上,忽復乘舟夢日邊。",
+ "行路難,行路難,多岐路,今安在?長風破浪會有時,直挂雲帆濟滄海。"
+ ],
+ "tags": [
+ "黄河",
+ "励志",
+ "友情",
+ "八年级下册(课内)",
+ "初中古诗",
+ "乐府",
+ "唐诗三百首",
+ "宴饮",
+ "哲理",
+ "怀才不遇"
+ ],
+ "title": "雜曲歌辭 行路難三首 一",
+ "id": "527eba4b-b35c-4d29-99e7-8f40c6f3d5b7"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "大道如青天,我獨不得出。",
+ "羞逐長安社中兒,赤雞白狗賭梨栗。",
+ "彈劒作歌奏苦聲,曳裾王門不稱情。",
+ "淮陰市井笑韓信,漢朝公卿忌賈生。",
+ "君不見昔時燕家重郭隗,擁篲折腰無嫌猜。",
+ "劇辛樂毅感恩分,輸肝剖膽效英才。",
+ "昭王白骨縈蔓草,誰人更掃黃金臺。",
+ "行路難,歸去來。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "乐府",
+ "失意",
+ "叹息"
+ ],
+ "title": "雜曲歌辭 行路難三首 二",
+ "id": "e12edd83-36cd-4e8a-a630-91874631c51f"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "有耳莫洗潁川水,有口莫食首陽蕨。",
+ "含光混世貴無名,何用孤高比雲月。",
+ "吾觀自古賢達人,功成不退皆殞身。",
+ "子胥既棄吳江上,屈原終投湘水濱。",
+ "陸機才多豈自保,李斯稅駕苦不早。",
+ "華亭鶴唳詎可聞,上蔡蒼鷹何足道?君不見吳中張翰稱達士,秋風忽憶江東行。",
+ "且樂生前一杯酒,何須身後千載名。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "人生",
+ "乐府",
+ "态度"
+ ],
+ "title": "雜曲歌辭 行路難三首 三",
+ "id": "b4f9c5b3-0108-4127-bf3e-73fd207176f3"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "君家定何處?妾住在橫塘。",
+ "停舟暫借問,或恐是同鄉。"
+ ],
+ "tags": [
+ "孤独",
+ "女子",
+ "思乡",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 長干曲四首 一",
+ "id": "4bef9a5e-f4a1-44bd-a78c-cb0fe38ccdff"
+ },
+ {
+ "author": "崔顥",
+ "paragraphs": [
+ "家臨九江水,去來九江側。",
+ "同是長干人,生小不相識。"
+ ],
+ "tags": [
+ "家乡",
+ "女子",
+ "热爱",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 長干曲四首 二",
+ "id": "8d480633-1876-4082-aaca-a7b3a26c04d7"
+ },
+ {
+ "author": "李白",
+ "paragraphs": [
+ "妾髮初覆額,折花門前劇。",
+ "郎騎竹馬來,遶牀弄青梅。",
+ "同居長干里,兩小無嫌猜。",
+ "十四爲君婦,羞顏尚不開。",
+ "低頭向暗壁,千喚不一迴。",
+ "十五始展眉,願同塵與灰。",
+ "常存抱柱信,豈上望夫臺。",
+ "十六君遠行,瞿塘灩預堆。",
+ "五月不可觸,猨鳴天上哀。",
+ "門前遲行跡,一一生綠苔。",
+ "苔深不能掃,落葉秋風早。",
+ "八月蝴蝶來,雙飛西園草。",
+ "感此傷妾心,坐愁紅顏老。",
+ "早晚下三巴,預將書報家。",
+ "相迎不道遠,直至長風沙。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "雜曲歌辭 長干行二首 一",
+ "id": "1352fbf1-2c3d-459c-b4ac-061e0c6befb8"
+ },
+ {
+ "author": "沈佺期",
+ "paragraphs": [
+ "盧家小婦鬱金堂,海燕雙栖玳瑁梁。",
+ "九月寒砧催下葉,十年征戍憶遼陽。",
+ "白狼河北音書斷,丹鳳城南秋夜長。",
+ "誰知含愁獨不見,使妾明月照流黃。"
+ ],
+ "tags": [
+ "思念",
+ "闺怨",
+ "唐诗三百首",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 獨不見",
+ "id": "15079155-e95e-4abc-b36e-f6b605848f37"
+ },
+ {
+ "author": "王涯",
+ "paragraphs": [
+ "桂魄初生秋露微,輕羅已薄未更衣。",
+ "銀箏夜久殷勤弄,心怯空房不忍歸。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "秋天",
+ "女子",
+ "乐府"
+ ],
+ "title": "雜曲歌辭 秋夜曲",
+ "id": "1f66d2ec-8418-485e-a434-e21a0d439dda"
+ },
+ {
+ "author": "皇甫冉",
+ "paragraphs": [
+ "鶯啼燕語報新年,馬邑龍堆路幾千。",
+ "家住秦城鄰漢苑,心隨明月到胡天。",
+ "機中錦字論長恨,樓上花枝笑獨眠。",
+ "爲問元戎竇車騎,何時反斾勒燕然。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "思念",
+ "七言律诗",
+ "春天",
+ "妇女",
+ "闺怨"
+ ],
+ "title": "春思",
+ "id": "d35f583a-c20c-453c-a886-e13f98b2e4b4"
+ },
+ {
+ "author": "劉方平",
+ "paragraphs": [
+ "更深月色半人家,北斗闌干南斗斜。",
+ "今夜偏知春氣暖,蟲聲新透綠窗紗。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "月亮",
+ "春天",
+ "田园",
+ "九年级上册(课外)",
+ "中秋",
+ "初中古诗",
+ "七言绝句"
+ ],
+ "title": "夜月",
+ "id": "79e7ec6e-0d6c-4f95-8181-4671c42e1faf"
+ },
+ {
+ "author": "劉方平",
+ "paragraphs": [
+ "紗窗日落漸黃昏,金屋無人見淚痕。",
+ "寂寞空庭春欲晚,梨花滿地不開門。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "孤独",
+ "七言绝句",
+ "宫怨"
+ ],
+ "title": "春怨",
+ "id": "fc1bc01d-c281-40fd-9e0d-4c484539e4ae"
+ },
+ {
+ "author": "王之渙",
+ "paragraphs": [
+ "白日依山盡,黃河入海流。",
+ "欲窮千里目,更上一層樓。"
+ ],
+ "tags": [
+ "黄河",
+ "山水",
+ "励志",
+ "唐诗三百首",
+ "登楼",
+ "写景",
+ "哲理",
+ "名楼、庙宇",
+ "五言绝句"
+ ],
+ "title": "登鸛雀樓",
+ "id": "63950163-6a10-4e74-af8a-09886e4ef2a8"
+ },
+ {
+ "author": "王之渙",
+ "paragraphs": [
+ "黃河遠上白雲間,一片孤城萬仞山。",
+ "羌笛何須怨楊柳,春光不度玉門關。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄河",
+ "山水",
+ "写景",
+ "乐府",
+ "思乡",
+ "边塞",
+ "将士"
+ ],
+ "title": "涼州詞二首" 一",
+ "id": "526d4ecc-e123-4b9d-a213-958bb947593d"
+ },
+ {
+ "author": "劉眘虛",
+ "paragraphs": [
+ "道由白雲盡,春與青溪長。",
+ "時有落花至,遠隨流水香。",
+ "閑門向山路,深柳讀書堂。",
+ "幽映每白日,清輝照衣裳。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "春天",
+ "写景",
+ "五言律诗"
+ ],
+ "title": "闕題",
+ "id": "a2b4fe60-9809-422d-afa7-143a2088f045"
+ },
+ {
+ "author": "柳中庸",
+ "paragraphs": [
+ "歲歲金河復玉關,朝朝馬策與刀環。",
+ "三春白雪歸青冢,萬里黃河遶黑山。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "黄河",
+ "边塞",
+ "七言绝句"
+ ],
+ "title": "征怨",
+ "id": "92dd65da-8301-4c83-b8f3-986b6095c481"
+ },
+ {
+ "author": "嚴維",
+ "paragraphs": [
+ "故關衰草遍,離別正堪悲。",
+ "路出寒雲外,人歸暮雪時。",
+ "少孤爲客早,多難識君遲。",
+ "揜泣空相向,風塵何所期。"
+ ],
+ "tags": [
+ "唐诗三百首",
+ "冬天",
+ "友情",
+ "送别",
+ "五言律诗",
+ "伤怀"
+ ],
+ "title": "送李端",
+ "id": "67c18ea1-db49-44b5-8b21-6176e4a86416"
+ },
+ {
+ "author": "顧況",
+ "paragraphs": [
+ "玉樓天半起笙歌,風送宮嬪笑語和。",
+ "月殿影開聞夜漏,水精簾卷近銀河。"
+ ],
+ "tags": [
+ "唐诗三百首"
+ ],
+ "title": "宮詞五首 二",
+ "id": "aa9f9e4d-aa34-496e-89cf-cfef61efa89d"
+ }
+]
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/PoetryApplication.java b/springboot-es/src/main/java/top/rstyro/poetry/PoetryApplication.java
new file mode 100644
index 0000000..7b4f21e
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/PoetryApplication.java
@@ -0,0 +1,13 @@
+package top.rstyro.poetry;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class PoetryApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PoetryApplication.class, args);
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiException.java b/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiException.java
new file mode 100644
index 0000000..a0ef0bd
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiException.java
@@ -0,0 +1,41 @@
+package top.rstyro.poetry.commons;
+
+import lombok.Data;
+
+/**
+ * 自定义的api异常
+ * @author rstyro
+ *
+ */
+@Data
+public class ApiException extends RuntimeException{
+ private static final long serialVersionUID = 1L;
+ private int status;
+ private String message;
+ private Object data;
+ private Exception exception;
+ private Object[] parameters;
+ public ApiException() {
+ super();
+ }
+ public ApiException(int status, String message, Object data,Object[] parameters, Exception exception) {
+ this.status = status;
+ this.parameters=parameters;
+ for (int i = 0; this.parameters != null && i < this.parameters.length; i++) { + message = message.replace("{" + i +"}", this.parameters[i].toString()); + } + this.message = message; + this.data = data; + this.exception = exception; + } + public ApiException(ApiExceptionCode apiExceptionCode) { + this(apiExceptionCode.getStatus(),apiExceptionCode.getMessage(),null,null,null); + } + public ApiException(ApiExceptionCode apiExceptionCode,Object... parameters) { + this(apiExceptionCode.getStatus(),apiExceptionCode.getMessage(),null,parameters,null); + } + public ApiException(ApiExceptionCode apiExceptionCode, Object data, Exception exception) { + this(apiExceptionCode.getStatus(),apiExceptionCode.getMessage(),data,null,exception); + } + +} \ No newline at end of file diff --git a/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiExceptionCode.java b/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiExceptionCode.java new file mode 100644 index 0000000..156453e --- /dev/null +++ b/springboot-es/src/main/java/top/rstyro/poetry/commons/ApiExceptionCode.java @@ -0,0 +1,34 @@ +package top.rstyro.poetry.commons; + +public enum ApiExceptionCode { + // 基础报错code + SUCCESS(200,"成功"), + FAILED(400,"请求失败"), + ERROR(500,"不知名错误"), + ERROR_NULL(501,"空指针异常"), + ERROR_CLASS_CAST(502,"类型转换异常"), + ERROR_RUNTIME(503,"运行时异常"), + ERROR_IO(504,"上传文件异常"), + ERROR_METHOD(505,"请求方法错误"), + + + // 业务code + ES_OVER_MAX_RESULT(10001,"超过最大分页限制"); + + ; + private String message; + private int status; + + public String getMessage() { + return message; + } + + public int getStatus() { + return status; + } + + ApiExceptionCode( int status,String message) { + this.message = message; + this.status = status; + } +} diff --git a/springboot-es/src/main/java/top/rstyro/poetry/commons/Const.java b/springboot-es/src/main/java/top/rstyro/poetry/commons/Const.java new file mode 100644 index 0000000..be99044 --- /dev/null +++ b/springboot-es/src/main/java/top/rstyro/poetry/commons/Const.java @@ -0,0 +1,22 @@ +package top.rstyro.poetry.commons; + +/** + * 常量类 + * @author rstyro + */ +public interface Const { + /** + * 分页参数 + */ + public final static String PAGE_NO="pageNum"; + public final static String PAGE_SIZE="pageSize"; + /** + * 请求追踪ID + */ + public static final String TRACKER_ID = "trackerId"; + + /** + * ES 最大结果数 + */ + public final static int MAX_RESULT=10000; +} diff --git a/springboot-es/src/main/java/top/rstyro/poetry/commons/ContextVo.java b/springboot-es/src/main/java/top/rstyro/poetry/commons/ContextVo.java new file mode 100644 index 0000000..fd3064f --- /dev/null +++ b/springboot-es/src/main/java/top/rstyro/poetry/commons/ContextVo.java @@ -0,0 +1,22 @@ +package top.rstyro.poetry.commons; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 上下文参数 vo + */ +@Data +@Accessors(chain = true) +public class ContextVo { + /** + * 请求追踪ID + */ + private String trackerId; + /** + * 分页参数 + */ + private Integer pageNo=1; + private Integer pageSize=10; + +} diff --git a/springboot-es/src/main/java/top/rstyro/poetry/commons/R.java b/springboot-es/src/main/java/top/rstyro/poetry/commons/R.java new file mode 100644 index 0000000..7119cb6 --- /dev/null +++ b/springboot-es/src/main/java/top/rstyro/poetry/commons/R.java @@ -0,0 +1,51 @@ +package top.rstyro.poetry.commons; + +import lombok.Data; +import top.rstyro.poetry.util.ContextUtil; + +import java.io.Serializable; + +/** + * 自定义的接口返回实体类 + * @author rstyro + * + */ +@Data +public class R implements Serializable {
+ private int code;
+ private String msg;
+ private String trackerId;
+ private T data;
+
+ public R(int code, String msg, T data) {
+ this.code = code;
+ this.msg = msg;
+ this.trackerId = ContextUtil.getTrackerId();
+ this.data = data;
+ }
+
+ public static R success() {
+ return new R(ApiExceptionCode.SUCCESS.getStatus(), ApiExceptionCode.SUCCESS.getMessage(), null);
+ }
+ public static R success(E data) {
+ return new R(ApiExceptionCode.SUCCESS.getStatus(), ApiExceptionCode.SUCCESS.getMessage(), data);
+ }
+
+ public static R fail(ApiExceptionCode apiExceptionCode) {
+ return new R(apiExceptionCode.getStatus(), apiExceptionCode.getMessage(), null);
+ }
+ public static R fail(ApiExceptionCode apiExceptionCode,Object[] parameters) {
+ String message = apiExceptionCode.getMessage();
+ for (int i = 0; parameters != null && i < parameters.length; i++) { + message = message.replace("{" + i +"}", parameters[i].toString()); + } + return new R(apiExceptionCode.getStatus(), message, null);
+ }
+
+ public static R fail(String msg) {
+ return new R(-1, msg, null);
+ }
+ public static R fail(int code,String msg) {
+ return new R(-1, msg, null);
+ }
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/config/ElasticsearchConfiguration.java b/springboot-es/src/main/java/top/rstyro/poetry/config/ElasticsearchConfiguration.java
new file mode 100644
index 0000000..adb8aa6
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/config/ElasticsearchConfiguration.java
@@ -0,0 +1,92 @@
+package top.rstyro.poetry.config;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * elasticsearch 客户端配置
+ */
+@ConditionalOnProperty(prefix = "elasticsearch",name = "enable",havingValue = "true")
+@Configuration
+public class ElasticsearchConfiguration {
+ /**
+ * elasticsearch 连接地址多个地址使用,分隔
+ */
+ @Value("${elasticsearch.hosts}")
+ private String hosts;
+
+ /**
+ * es 用户名
+ */
+ @Value("${elasticsearch.username}")
+ private String username;
+
+ /**
+ * es 密码
+ */
+ @Value("${elasticsearch.password}")
+ private String password;
+
+ /**
+ * 连接目标url最大超时
+ */
+ @Value("${elasticsearch.connectTimeOut}")
+ private Integer connectTimeOut;
+
+ /**
+ * 等待响应(读数据)最大超时
+ */
+ @Value("${elasticsearch.socketTimeOut}")
+ private Integer socketTimeOut;
+
+ /**
+ * 从连接池中获取可用连接最大超时时间
+ */
+ @Value("${elasticsearch.connectionRequestTime}")
+ private Integer connectionRequestTime;
+
+ /**
+ * 默认:DefaultConnectionKeepAliveStrategy 返回-1 (无限制)
+ * 服务器 keepAlive短于 客户端:会报 java.io.IOException: Connection reset by peer
+ * httpclient5 DEFAULT_CONN_KEEP_ALIVE = TimeValue.ofMinutes(3); 3分钟
+ */
+ private final static long KEEP_ALIVE=180000;
+
+ /**
+ * 构建RestHighLevelClient
+ * @return
+ */
+ @Bean(destroyMethod = "close")
+ public RestHighLevelClient restHighLevelClient() {
+ final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
+ HttpHost[] httpHosts = Arrays.stream(hosts.split(",")).map(host -> new HttpHost(host.split(":")[0], Integer.parseInt(host.split(":")[1]))).filter(Objects::nonNull).toArray(HttpHost[]::new);
+ RestClientBuilder builder = RestClient.builder(httpHosts)
+ .setHttpClientConfigCallback(httpClientBuilder -> {
+ RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
+ .setConnectTimeout(connectTimeOut)
+ .setSocketTimeout(socketTimeOut)
+ .setConnectionRequestTimeout(connectionRequestTime);
+ httpClientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());
+ httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+ httpClientBuilder.setKeepAliveStrategy((response, context) -> KEEP_ALIVE);
+ return httpClientBuilder;
+ });
+ return new RestHighLevelClient(builder);
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/config/GlobalExceptionHandlerAdvice.java b/springboot-es/src/main/java/top/rstyro/poetry/config/GlobalExceptionHandlerAdvice.java
new file mode 100644
index 0000000..fdaab0c
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/config/GlobalExceptionHandlerAdvice.java
@@ -0,0 +1,68 @@
+package top.rstyro.poetry.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import top.rstyro.poetry.commons.ApiException;
+import top.rstyro.poetry.commons.R;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.ValidationException;
+
+/**
+ * 描述:全局统一异常处理
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandlerAdvice {
+
+ /**
+ * 参数验证异常
+ */
+ @ExceptionHandler(value = {MethodArgumentNotValidException.class})
+ public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
+ if (StringUtils.hasLength(errorMsg)) {
+ return R.fail(errorMsg);
+ }
+ log.error(e.getMessage(),e);
+ return R.fail(e.getMessage());
+ }
+
+ /**
+ * 参数验证异常
+ */
+ @ExceptionHandler(value = {ValidationException.class})
+ public R constraintViolationException(ValidationException e) {
+ if(e instanceof ConstraintViolationException){
+ ConstraintViolationException err = (ConstraintViolationException) e;
+ ConstraintViolation> constraintViolation = err.getConstraintViolations().stream().findFirst().get();
+ String messageTemplate = constraintViolation.getMessageTemplate();
+ if(!StringUtils.isEmpty(messageTemplate)){
+ return R.fail(messageTemplate);
+ }
+ }
+ log.error(e.getMessage(),e);
+ return R.fail(e.getMessage());
+ }
+
+ @ExceptionHandler(ApiException.class)
+ public R ApiException(ApiException ex) {
+ log.error(ex.getMessage(),ex);
+ return R.fail(ex.getStatus(),ex.getMessage());
+ }
+
+
+ /**
+ * 默认异常
+ */
+ @ExceptionHandler(value = Exception.class)
+ public R defaultException(Exception e) {
+ log.error("系统异常:" + e.getMessage(), e);
+ return R.fail(e.getMessage());
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/config/WebInterceptorConfig.java b/springboot-es/src/main/java/top/rstyro/poetry/config/WebInterceptorConfig.java
new file mode 100644
index 0000000..bfd6512
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/config/WebInterceptorConfig.java
@@ -0,0 +1,33 @@
+package top.rstyro.poetry.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import top.rstyro.poetry.interceptor.ContextInterceptor;
+
+@Configuration
+public class WebInterceptorConfig implements WebMvcConfigurer {
+ private ContextInterceptor contextInterceptor;
+
+ @Autowired
+ public void setContextInterceptor(ContextInterceptor contextInterceptor) {
+ this.contextInterceptor = contextInterceptor;
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(contextInterceptor).addPathPatterns("/**");
+ }
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ .allowedOrigins("*")// 允许跨域的域名,可以用*表示允许任何域名使用
+ .allowedMethods("*")// 允许任何方法(post、get等)
+ .allowCredentials(true)// 带上cookie信息
+ .maxAge(3600) // 表明在3600秒内,不需要再发送预检验请求,可以缓存该结果
+ .allowedHeaders("*");
+ }
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/controller/SearchController.java b/springboot-es/src/main/java/top/rstyro/poetry/controller/SearchController.java
new file mode 100644
index 0000000..98ed187
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/controller/SearchController.java
@@ -0,0 +1,54 @@
+package top.rstyro.poetry.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import top.rstyro.poetry.commons.R;
+import top.rstyro.poetry.dto.SearchDto;
+import top.rstyro.poetry.es.vo.EsSearchResultVo;
+import top.rstyro.poetry.service.IPoetryService;
+import top.rstyro.poetry.vo.SearchVo;
+import javax.validation.Valid;
+
+/**
+ * 检索相关
+ *
+ * @author rstyro
+ */
+@Validated
+@RestController
+@RequestMapping("/search")
+public class SearchController {
+
+ private IPoetryService poetryService;
+
+ @Autowired
+ public void setPoetryService(IPoetryService poetryService) {
+ this.poetryService = poetryService;
+ }
+
+ /**
+ * 搜索
+ */
+ @PostMapping("/list")
+ public R> list(@RequestBody @Valid SearchDto dto){
+ return R.success(poetryService.getList(dto));
+ }
+
+ /**
+ * 详情
+ */
+ @GetMapping("/detail/{id}")
+ public R detail(@PathVariable("id") String id){
+ return R.success(poetryService.getDetail(id));
+ }
+
+ /**
+ * 飞花令
+ */
+ @GetMapping("/flyFlower")
+ public R getFlyFlower(String text){
+ return R.success(poetryService.getFlyFlower(text));
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchAggsDto.java b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchAggsDto.java
new file mode 100644
index 0000000..9e8c05f
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchAggsDto.java
@@ -0,0 +1,11 @@
+package top.rstyro.poetry.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+public class SearchAggsDto {
+ private String key;
+ private Integer size=10;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchDto.java b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchDto.java
new file mode 100644
index 0000000..0dd6179
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchDto.java
@@ -0,0 +1,28 @@
+package top.rstyro.poetry.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+public class SearchDto {
+ /**
+ * 检索关键词
+ */
+// @NotEmpty(message = "关键字不能为空")
+ private String kw;
+ /**
+ * 过滤项
+ */
+ private SearchFilterDto filters;
+ /**
+ * 聚类
+ */
+ private List aggsList;
+ /**
+ * 需要查询结果
+ */
+ Boolean needRecords = true;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchFilterDto.java b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchFilterDto.java
new file mode 100644
index 0000000..10d667e
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/dto/SearchFilterDto.java
@@ -0,0 +1,15 @@
+package top.rstyro.poetry.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+public class SearchFilterDto {
+ private List tags;
+ private List dynastyList;
+ private List typeList;
+ private List authorList;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsBaseAggs.java b/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsBaseAggs.java
new file mode 100644
index 0000000..78513b7
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsBaseAggs.java
@@ -0,0 +1,15 @@
+package top.rstyro.poetry.es.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 聚合基类
+ */
+@Data
+@Accessors(chain = true)
+public class EsBaseAggs implements Serializable {
+ private String key;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsResult.java b/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsResult.java
new file mode 100644
index 0000000..c8f40c5
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/base/EsResult.java
@@ -0,0 +1,46 @@
+package top.rstyro.poetry.es.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.elasticsearch.search.aggregations.Aggregations;
+import org.elasticsearch.search.suggest.Suggest;
+import top.rstyro.poetry.es.index.EsBaseIndex;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 通用返回
+ * @param
+ */
+@Data
+@Accessors(chain = true)
+public class EsResult implements Serializable {
+
+ /**
+ * 请求时间
+ */
+ private long took;
+ /**
+ * 总数
+ */
+ private long total;
+ /**
+ * 是否超时
+ */
+ private boolean timed_out;
+ /**
+ * 数据集合
+ */
+ private List records;
+
+ /**
+ * 聚合
+ */
+ Aggregations aggregation;
+ /**
+ * 自动补全
+ */
+ Suggest suggest;
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/base/FieldAggs.java b/springboot-es/src/main/java/top/rstyro/poetry/es/base/FieldAggs.java
new file mode 100644
index 0000000..c34d0ce
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/base/FieldAggs.java
@@ -0,0 +1,29 @@
+package top.rstyro.poetry.es.base;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 检索返回实体类基类
+ */
+@Data
+@Accessors(chain = true)
+public class FieldAggs extends EsBaseAggs implements Serializable {
+ private List list = new ArrayList();
+
+ public void add(AggVo aggs){
+ list.add(aggs);
+ }
+
+ @Data
+ @AllArgsConstructor
+ public class AggVo{
+ private String key;
+ private long docCount;
+ }
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/index/EsBaseIndex.java b/springboot-es/src/main/java/top/rstyro/poetry/es/index/EsBaseIndex.java
new file mode 100644
index 0000000..bd8cd30
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/index/EsBaseIndex.java
@@ -0,0 +1,44 @@
+package top.rstyro.poetry.es.index;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonFilter;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@Accessors(chain = true)
+public class EsBaseIndex implements Serializable {
+ /**
+ * es 主键ID
+ */
+ @JSONField(serialize = false)
+ private String _id;
+ /**
+ * 命中分数
+ */
+ @JSONField(serialize = false)
+ private float _score;
+
+
+ /**
+ * 高亮
+ */
+ @JSONField(serialize = false)
+ Map> highlight;
+
+ /**
+ * es 索引名称
+ * @return
+ */
+ @JSONField(serialize = false)
+ public String getIndexName() {
+ return StrUtil.toSymbolCase(StrUtil.removeSuffix(getClass().getSimpleName(), "Index"), '-');
+ }
+
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetIndex.java b/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetIndex.java
new file mode 100644
index 0000000..77b6756
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetIndex.java
@@ -0,0 +1,26 @@
+package top.rstyro.poetry.es.index;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Set;
+
+@Data
+@Accessors(chain = true)
+public class PoetIndex extends EsBaseIndex{
+ /**
+ * 简介
+ */
+ private String introduce;
+ /**
+ * 作者名称
+ */
+ private String author;
+
+ /**
+ * 标签
+ */
+ private Set tags;
+
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetryIndex.java b/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetryIndex.java
new file mode 100644
index 0000000..f1e279f
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/index/PoetryIndex.java
@@ -0,0 +1,72 @@
+package top.rstyro.poetry.es.index;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 古诗索引
+ */
+@Data
+@Accessors(chain = true)
+public class PoetryIndex extends EsBaseIndex{
+ /**
+ * 篇名 合集
+ */
+ private String section;
+ /**
+ * 章节
+ */
+ private String chapter;
+ /**
+ * 作者
+ */
+ private String author;
+ /**
+ * 标题
+ */
+ private String title;
+ /**
+ * 浏览数
+ */
+ private Integer view_count=0;
+ /**
+ * 内容
+ */
+ private List content;
+
+ /**
+ * 标签: 春天、写景、离别....等等
+ */
+ private Set tags;
+ /**
+ * 朝代: 唐 宋 ....
+ */
+ private Set dynasty;
+ /**
+ * 七言绝句、五言律诗、...
+ */
+ private Set type;
+
+ /**
+ * 译文
+ */
+ private List translations;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JSONField(format = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime update_time;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JSONField(format = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime create_time;
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/service/EsBaseService.java b/springboot-es/src/main/java/top/rstyro/poetry/es/service/EsBaseService.java
new file mode 100644
index 0000000..e253483
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/service/EsBaseService.java
@@ -0,0 +1,237 @@
+package top.rstyro.poetry.es.service;
+
+import cn.hutool.core.convert.Convert;
+import com.alibaba.fastjson2.JSON;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.action.DocWriteRequest;
+import org.elasticsearch.action.bulk.BulkRequest;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.get.GetRequest;
+import org.elasticsearch.action.get.GetResponse;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.action.update.UpdateResponse;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.common.text.Text;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
+import org.elasticsearch.xcontent.XContentType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
+import top.rstyro.poetry.es.base.EsResult;
+import top.rstyro.poetry.es.index.EsBaseIndex;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.util.*;
+
+@Slf4j
+public class EsBaseService {
+ private Class indexClass;
+ T t;
+
+ private RestHighLevelClient restHighLevelClient;
+
+ @Autowired
+ public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
+ this.restHighLevelClient = restHighLevelClient;
+ }
+
+ @SneakyThrows
+ @PostConstruct
+ private void initialize(){
+ indexClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
+ t= indexClass.getDeclaredConstructor().newInstance();
+ }
+
+
+ /**
+ * 添加文档
+ * @param doc 文档
+ * @return boolean
+ * @throws IOException
+ */
+ @SneakyThrows
+ public boolean saveDoc(T doc){
+ IndexRequest indexRequest = getSaveRequest(doc);
+ restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
+ return true;
+ }
+
+ /**
+ * 获取文档
+ *
+ * @param id
+ * @return
+ */
+ public Map getDocMapById(String id) throws IOException {
+ GetRequest request = new GetRequest(t.getIndexName(), id);
+ GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
+ Map docMap = response.getSourceAsMap();
+ docMap.put("_id",response.getId());
+ return docMap;
+ }
+
+ /**
+ * 获取文档
+ *
+ * @param id
+ * @return
+ */
+ public T getDocById(String id) throws IOException {
+ GetRequest request = new GetRequest(t.getIndexName(), id);
+ GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
+ T result = JSON.parseObject(response.getSourceAsString(), indexClass);
+ if(null!=result){
+ result.set_id(response.getId());
+ }
+ return result;
+ }
+
+ /**
+ * 批量新增
+ *
+ * @param list
+ * @return
+ * @throws IOException
+ */
+ public boolean batchSaveDoc(List list) throws IOException {
+ if (list == null || list.size() < 1) return false; + BulkRequest request = new BulkRequest(); + for (T item : list) { + IndexRequest saveRequest = getSaveRequest(item); + request.add(saveRequest); + } + BulkResponse bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT); + if(bulk.hasFailures()){ + log.error("batchSave error msg={}",bulk.buildFailureMessage()); + } + return !bulk.hasFailures(); + } + + private IndexRequest getSaveRequest(T item){ + IndexRequest indexRequest = new IndexRequest(item.getIndexName()); + if(!StringUtils.isEmpty(item.get_id())){ + indexRequest.id(item.get_id()); + indexRequest.routing(item.get_id()); + indexRequest.opType(DocWriteRequest.OpType.INDEX); + } + indexRequest.source(JSON.toJSONString(item.set_id(null)), XContentType.JSON); + return indexRequest; + } + + /** + * 更新文档 + * + * @param id + * @param data + * @return + */ + public boolean updateDocById(String id, T data) throws IOException { + UpdateRequest request = new UpdateRequest(data.getIndexName(), id).doc(JSON.toJSONString(data), XContentType.JSON); + UpdateResponse update = restHighLevelClient.update(request, RequestOptions.DEFAULT); + return true; + } + + /** + * Script更新数字字段 + * @param id + * @param fieldName + * @param oprateNum + * @return + * @throws IOException + */ + public boolean updateFieldNum(String id,String fieldName,int oprateNum) throws IOException { + UpdateRequest request = new UpdateRequest(t.getIndexName(),id).script(new Script("ctx._source."+fieldName+" += "+oprateNum)); + UpdateResponse update = restHighLevelClient.update(request, RequestOptions.DEFAULT); + return update.getShardInfo().getFailed()==0; + } + + /** + * 批量更新 + * + * @param list + * @return + */ + public boolean batchUpdateDoc(List list) throws IOException {
+ if (list == null || list.size() < 1) return false; + BulkRequest request = new BulkRequest(); + for (T item : list) { + UpdateRequest updateRequest = new UpdateRequest(item.getIndexName(), item.get_id()).doc(JSON.toJSONString(item), XContentType.JSON); + request.add(updateRequest); + } + BulkResponse bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT); + return !bulk.hasFailures(); + } + + /** + * 获取Id 列表 + */ + public EsResult searchIds(String... ids) throws IOException {
+ EsResult result = new EsResult();
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ searchSourceBuilder.query(QueryBuilders.idsQuery().addIds(ids)).size(10000);
+ SearchRequest searchRequest = new SearchRequest().indices(t.getIndexName()).source(searchSourceBuilder);
+ SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
+ parseResult(result,response);
+ return result;
+ }
+
+ @SneakyThrows
+ public EsResult search(SearchSourceBuilder sourceBuilder) {
+ EsResult result = new EsResult();
+ SearchRequest request = new SearchRequest();
+ request.source(sourceBuilder);
+ request.indices(t.getIndexName());
+ SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
+ parseResult(result,response);
+ return result;
+ }
+
+ public void parseResult(EsResult result,SearchResponse response){
+ List records = new ArrayList();
+ for (SearchHit hit : response.getHits()) {
+ if (hit.getSourceAsMap() == null) {
+ continue;
+ }
+ T index = Convert.convert(indexClass, hit.getSourceAsMap());
+ index.set_id(hit.getId());
+ if (hit.getHighlightFields() != null) {
+ Map> tempHighLightMap = new HashMap();
+ for (HighlightField oneResult : hit.getHighlightFields().values()) {
+ List tempStrList = new LinkedList();
+ for (Text fragment : oneResult.getFragments()) {
+ tempStrList.add(fragment.toString());
+ }
+ tempHighLightMap.put(oneResult.getName(), tempStrList);
+ }
+ index.setHighlight(tempHighLightMap);
+ }
+ records.add(index);
+ }
+ if (response.getHits() != null) {
+ result.setTotal(response.getHits().getTotalHits().value);
+ } else {
+ result.setTotal(0);
+ }
+ if (response.getAggregations() != null && response.getAggregations().get("_count") != null) {
+ Terms terms = response.getAggregations().get("_count");
+ result.setTotal(terms.getSumOfOtherDocCounts());
+ }
+ result.setTook(response.getTook().getMillis());
+ result.setRecords(records);
+ result.setAggregation(response.getAggregations());
+ result.setSuggest(response.getSuggest());
+ }
+
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/service/impl/PoetryEsService.java b/springboot-es/src/main/java/top/rstyro/poetry/es/service/impl/PoetryEsService.java
new file mode 100644
index 0000000..eda695b
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/service/impl/PoetryEsService.java
@@ -0,0 +1,10 @@
+package top.rstyro.poetry.es.service.impl;
+
+import org.springframework.stereotype.Service;
+import top.rstyro.poetry.es.index.PoetryIndex;
+import top.rstyro.poetry.es.service.EsBaseService;
+
+@Service
+public class PoetryEsService extends EsBaseService {
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/vo/AggregationVo.java b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/AggregationVo.java
new file mode 100644
index 0000000..a686bfa
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/AggregationVo.java
@@ -0,0 +1,24 @@
+package top.rstyro.poetry.es.vo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+@NoArgsConstructor
+public class AggregationVo implements Serializable {
+ String key;
+ List list;
+ long sumDoc;
+
+ public AggregationVo(String key, List list, long sumDoc) {
+ this.key = key;
+ this.list = list;
+ this.sumDoc=sumDoc;
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseAggregationVo.java b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseAggregationVo.java
new file mode 100644
index 0000000..ebc0511
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseAggregationVo.java
@@ -0,0 +1,12 @@
+package top.rstyro.poetry.es.vo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+public class BaseAggregationVo implements Serializable {
+ public String key;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseEsVo.java b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseEsVo.java
new file mode 100644
index 0000000..9cd08a2
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/BaseEsVo.java
@@ -0,0 +1,18 @@
+package top.rstyro.poetry.es.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 搜索基础VO
+ */
+@Data
+@Accessors(chain = true)
+public class BaseEsVo implements Serializable {
+ public String _id;
+ public Map> highlight;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/vo/EsSearchResultVo.java b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/EsSearchResultVo.java
new file mode 100644
index 0000000..652255b
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/EsSearchResultVo.java
@@ -0,0 +1,43 @@
+package top.rstyro.poetry.es.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Accessors(chain = true)
+@Data
+public class EsSearchResultVo implements Serializable {
+ /**
+ * 总数量
+ */
+ private long total;
+ /**
+ * 请求耗时
+ */
+ private long took;
+ /**
+ * 数据
+ */
+ private List records;
+ /**
+ * 聚合结果
+ */
+ private List> aggregation = new ArrayList();
+ /**
+ * 扩展数据
+ */
+ private Map extraParam = new HashMap();
+
+ public void addAggregation(AggregationVo> dto) {
+ aggregation.add(dto);
+ }
+
+ public void addExtraParam(String key, Object value) {
+ extraParam.put(key, value);
+ }
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/es/vo/TermAggregationVo.java b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/TermAggregationVo.java
new file mode 100644
index 0000000..92aa254
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/es/vo/TermAggregationVo.java
@@ -0,0 +1,10 @@
+package top.rstyro.poetry.es.vo;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class TermAggregationVo extends BaseAggregationVo {
+ public Long docCount;
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/handler/TangSongHandler.java b/springboot-es/src/main/java/top/rstyro/poetry/handler/TangSongHandler.java
new file mode 100644
index 0000000..046e821
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/handler/TangSongHandler.java
@@ -0,0 +1,120 @@
+package top.rstyro.poetry.handler;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.io.file.FileReader;
+import com.alibaba.fastjson2.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import top.rstyro.poetry.es.index.PoetryIndex;
+import top.rstyro.poetry.es.service.impl.PoetryEsService;
+import top.rstyro.poetry.util.Tools;
+import top.rstyro.poetry.vo.PoetryTangSongVo;
+
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 加工唐诗三百首到 ES 中
+ */
+@Slf4j
+@Component
+public class TangSongHandler {
+
+ private PoetryEsService poetryEsService;
+
+ int core = 8;
+ ThreadPoolExecutor executorService = new ThreadPoolExecutor(core, core, 1, TimeUnit.MINUTES, new LinkedBlockingQueue(1000));
+
+ @Autowired
+ public void setPoetryEsService(PoetryEsService poetryEsService) {
+ this.poetryEsService = poetryEsService;
+ }
+
+ public void handler(String filePath){
+ File file = new File(filePath);
+ File[] files = file.listFiles();
+ Arrays.stream(files).filter(f->f.getName().contains("唐诗")).forEach(f->{
+ FileReader fileReader = new FileReader(f);
+ String json = fileReader.readString();
+ String dynasty = "唐朝";
+ if(f.getName().contains("song")){
+ dynasty = "宋朝";
+ }
+ List list = JSON.parseArray(json, PoetryTangSongVo.class);
+ List> splitList = ListUtil.split(list, 1000);
+ Set dynastyList = new HashSet();
+ dynastyList.add(dynasty);
+ CountDownLatch countDownLatch = new CountDownLatch(splitList.size());
+ splitList.stream().forEach(split->{
+ if(executorService.getActiveCount(){
+ batchSavePoetry(split,dynastyList,countDownLatch);
+ });
+ }else {
+ batchSavePoetry(split,dynastyList,countDownLatch);
+ }
+ });
+ try {
+ countDownLatch.await();
+ } catch (InterruptedException e) {
+ log.error(e.getMessage(),e);
+ }
+ log.info("处理完文件={}",f.getName());
+ });
+ }
+
+ public void batchSavePoetry(List list,Set dynastyList, CountDownLatch countDownLatch) {
+ try {
+ List dataList = new ArrayList();
+ Set type = new HashSet();
+ type.add("诗词");
+ list.forEach(d->{
+ List contentList = d.getParagraphs();
+ if(!ObjectUtils.isEmpty(contentList)){
+ try {
+ PoetryIndex index = new PoetryIndex();
+ index.setAuthor(Tools.cnToSimple(d.getAuthor()));
+ index.setContent(Tools.cnToSimple(d.getParagraphs()));
+ index.setTitle(Tools.cnToSimple(d.getTitle()));
+ index.setSection(Tools.cnToSimple(d.getVolume()));
+ index.setTags(new HashSet(Tools.cnToSimple(d.getTags())));
+ index.setDynasty(dynastyList);
+ index.setType(type);
+ index.set_id(d.getId());
+ dataList.add(index);
+ }catch (Exception e){
+ log.error(e.getMessage(),e);
+ }
+ }
+ });
+ poetryEsService.batchSaveDoc(dataList);
+ }catch (Exception e){
+ log.error("保存数据时报错,err={}",e.getMessage(),e);
+ }finally {
+ countDownLatch.countDown();
+ }
+ }
+
+ /**
+ * 判断是否工整
+ * @return
+ */
+ public boolean isNeat(List content){
+ int linLength=-1;
+ for(String line:content){
+ if(linLength!=-1 && linLength!=line.length()){
+ return false;
+ }else {
+ linLength=line.length();
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/interceptor/ContextInterceptor.java b/springboot-es/src/main/java/top/rstyro/poetry/interceptor/ContextInterceptor.java
new file mode 100644
index 0000000..b7fa7c2
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/interceptor/ContextInterceptor.java
@@ -0,0 +1,33 @@
+package top.rstyro.poetry.interceptor;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+import top.rstyro.poetry.commons.Const;
+import top.rstyro.poetry.commons.ContextVo;
+import top.rstyro.poetry.util.ContextUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 上下文参数注入
+ */
+@Component
+public class ContextInterceptor implements HandlerInterceptor {
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ String pageNo = request.getHeader(Const.PAGE_NO);
+ String pageSize = request.getHeader(Const.PAGE_SIZE);
+ String trackerId = request.getHeader(Const.TRACKER_ID);
+ ContextVo contextVo = new ContextVo();
+ contextVo.setTrackerId(StringUtils.hasLength(trackerId)?trackerId:IdUtil.fastSimpleUUID());
+ contextVo.setPageNo(StringUtils.hasLength(pageNo)&&StrUtil.isNumeric(pageNo)?Integer.parseInt(pageNo):1);
+ contextVo.setPageSize(StringUtils.hasLength(pageSize)&&StrUtil.isNumeric(pageSize)?Integer.parseInt(pageSize):10);
+ ContextUtil.setVoLocal(contextVo);
+ return true;
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/service/IPoetryService.java b/springboot-es/src/main/java/top/rstyro/poetry/service/IPoetryService.java
new file mode 100644
index 0000000..84cb6d1
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/service/IPoetryService.java
@@ -0,0 +1,17 @@
+package top.rstyro.poetry.service;
+
+import top.rstyro.poetry.dto.SearchDto;
+import top.rstyro.poetry.es.vo.EsSearchResultVo;
+import top.rstyro.poetry.vo.FlyFlowerVo;
+import top.rstyro.poetry.vo.SearchDetailVo;
+import top.rstyro.poetry.vo.SearchVo;
+import top.rstyro.poetry.vo.SuggestVo;
+
+import java.util.List;
+
+public interface IPoetryService {
+ public EsSearchResultVo getList(SearchDto dto);
+
+ public SearchDetailVo getDetail(String id);
+ public EsSearchResultVo getFlyFlower(String text);
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/service/impl/PoetryServiceImpl.java b/springboot-es/src/main/java/top/rstyro/poetry/service/impl/PoetryServiceImpl.java
new file mode 100644
index 0000000..080b3bc
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/service/impl/PoetryServiceImpl.java
@@ -0,0 +1,239 @@
+package top.rstyro.poetry.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.Aggregations;
+import org.elasticsearch.search.aggregations.bucket.terms.ParsedTerms;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import top.rstyro.poetry.commons.ApiException;
+import top.rstyro.poetry.commons.ApiExceptionCode;
+import top.rstyro.poetry.commons.Const;
+import top.rstyro.poetry.dto.SearchAggsDto;
+import top.rstyro.poetry.dto.SearchDto;
+import top.rstyro.poetry.dto.SearchFilterDto;
+import top.rstyro.poetry.es.base.EsResult;
+import top.rstyro.poetry.es.index.PoetryIndex;
+import top.rstyro.poetry.es.service.impl.PoetryEsService;
+import top.rstyro.poetry.es.vo.AggregationVo;
+import top.rstyro.poetry.es.vo.EsSearchResultVo;
+import top.rstyro.poetry.es.vo.TermAggregationVo;
+import top.rstyro.poetry.service.IPoetryService;
+import top.rstyro.poetry.util.ContextUtil;
+import top.rstyro.poetry.util.LambdaUtil;
+import top.rstyro.poetry.vo.FlyFlowerVo;
+import top.rstyro.poetry.vo.SearchDetailVo;
+import top.rstyro.poetry.vo.SearchVo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class PoetryServiceImpl implements IPoetryService {
+
+ @Value("${spring.profiles.active}")
+ private String env;
+
+
+ private PoetryEsService poetryEsService;
+
+ @Autowired
+ public void setPoetryEsService(PoetryEsService poetryEsService) {
+ this.poetryEsService = poetryEsService;
+ }
+
+ @Override
+ public EsSearchResultVo getList(SearchDto dto) {
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
+ String kw = dto.getKw();
+ if (StringUtils.hasLength(kw)) {
+ BoolQueryBuilder keywordBool = QueryBuilders.boolQuery();
+ keywordBool.should(QueryBuilders.matchQuery(LambdaUtil.getFieldName(PoetryIndex::getTitle), kw));
+ keywordBool.should(QueryBuilders.matchQuery(LambdaUtil.getFieldName(PoetryIndex::getContent), kw));
+ keywordBool.should(QueryBuilders.matchQuery(LambdaUtil.getFieldName(PoetryIndex::getAuthor), kw));
+ boolQuery.must(keywordBool);
+ }
+ // 过滤项
+ if (!ObjectUtils.isEmpty(dto.getFilters())) {
+ SearchFilterDto filters = dto.getFilters();
+ if (!ObjectUtils.isEmpty(filters.getTags())) {
+ BoolQueryBuilder filterBool = QueryBuilders.boolQuery();
+// filterBool.must(QueryBuilders.termsQuery(LambdaUtil.getFieldName(PoetryIndex::getTags), filters.getTags()));
+ filters.getTags().stream().forEach(i->{
+ filterBool.must(QueryBuilders.termQuery(LambdaUtil.getFieldName(PoetryIndex::getTags), i));
+ });
+ boolQuery.must(filterBool);
+ }
+ if (!ObjectUtils.isEmpty(filters.getDynastyList())) {
+ BoolQueryBuilder filterBool = QueryBuilders.boolQuery();
+ filterBool.should(QueryBuilders.termsQuery(LambdaUtil.getFieldName(PoetryIndex::getDynasty), filters.getDynastyList()));
+ boolQuery.must(filterBool);
+ }
+ if (!ObjectUtils.isEmpty(filters.getTypeList())) {
+ BoolQueryBuilder filterBool = QueryBuilders.boolQuery();
+ filterBool.should(QueryBuilders.termsQuery(LambdaUtil.getFieldName(PoetryIndex::getType), filters.getTypeList()));
+ boolQuery.must(filterBool);
+ }
+
+ if (!ObjectUtils.isEmpty(filters.getAuthorList())) {
+ BoolQueryBuilder filterBool = QueryBuilders.boolQuery();
+ filterBool.should(QueryBuilders.termsQuery(LambdaUtil.getFieldName(PoetryIndex::getAuthor) + ".keyword", filters.getAuthorList()));
+ boolQuery.must(filterBool);
+ }
+ }
+ searchSourceBuilder.query(boolQuery);
+ // 高亮
+ HighlightBuilder highlightBuilder = new HighlightBuilder();
+ // * 全部字段
+ highlightBuilder.field(LambdaUtil.getFieldName(PoetryIndex::getTitle));
+ highlightBuilder.field(LambdaUtil.getFieldName(PoetryIndex::getAuthor));
+ highlightBuilder.field(LambdaUtil.getFieldName(PoetryIndex::getContent));
+ highlightBuilder.fragmentSize(200);
+ searchSourceBuilder.highlighter(highlightBuilder);
+ // 聚类
+ if (!ObjectUtils.isEmpty(dto.getAggsList())) {
+ List aggsList = dto.getAggsList();
+ aggsList.stream().forEach(aggs -> {
+ searchSourceBuilder.aggregation(AggregationBuilders.terms(aggs.getKey()).field(aggs.getKey()).size(aggs.getSize()).shardSize(1000));
+ });
+ }
+ // 是否需要结果集
+ if (dto.getNeedRecords()) {
+ setPageParams(searchSourceBuilder);
+
+ } else {
+ searchSourceBuilder.size(0);
+ }
+ if ("dev".equals(env)) {
+ log.info("ES-SQL={}", searchSourceBuilder.toString());
+ }
+ EsResult response = poetryEsService.search(searchSourceBuilder);
+ EsSearchResultVo resultVo = new EsSearchResultVo();
+ resultVo.setTook(response.getTook());
+ resultVo.setTotal(response.getTotal());
+ List list = new ArrayList();
+ List records = response.getRecords();
+ if (!ObjectUtils.isEmpty(records)) {
+ records.stream().forEach(i -> {
+ SearchVo vo = new SearchVo();
+ BeanUtil.copyProperties(i, vo);
+ List contentHighs = i.getHighlight().get(LambdaUtil.getFieldName(PoetryIndex::getContent));
+ if(!ObjectUtils.isEmpty(contentHighs)){
+ contentHighs.stream().forEach(c->{
+ String sourceC = c.replace("", "").replace("", "");
+ // 把高亮的句子 替换原文
+ List newContents = vo.getContent().stream().map(cc -> cc.replace(sourceC, c)).collect(Collectors.toList());
+ vo.setContent(newContents);
+ });
+ // 内容排序,高亮在前,截取前三个即可
+ List content = vo.getContent();
+ Collections.sort(content, (c1, c2) -> c1.contains("")?-1:(c2.contains("")?1:0));
+ vo.setContent(content.subList(0,Math.min(content.size(),3)));
+ }
+ List titleHighs = i.getHighlight().get(LambdaUtil.getFieldName(PoetryIndex::getTitle));
+ if(!ObjectUtils.isEmpty(titleHighs)){
+ vo.setTitle(titleHighs.get(0));
+ }
+ List auHighs = i.getHighlight().get(LambdaUtil.getFieldName(PoetryIndex::getAuthor));
+ if(!ObjectUtils.isEmpty(auHighs)){
+ vo.setAuthor(auHighs.get(0));
+ }
+ list.add(vo);
+ });
+ }
+ resultVo.setRecords(list);
+ // 聚类解析
+ Aggregations aggregation = response.getAggregation();
+ if (!ObjectUtils.isEmpty(aggregation)) {
+ dto.getAggsList().stream().forEach(a -> {
+ ParsedTerms parsedTerms = aggregation.get(a.getKey());
+ if (!ObjectUtils.isEmpty(parsedTerms)) {
+ List aggregationVoList = new ArrayList();
+ AtomicLong sum = new AtomicLong(0);
+ parsedTerms.getBuckets().forEach(bucket -> {
+ TermAggregationVo termAggregationVo = new TermAggregationVo();
+ termAggregationVo.setKey(bucket.getKeyAsString());
+ termAggregationVo.setDocCount(bucket.getDocCount());
+ sum.addAndGet(bucket.getDocCount());
+ aggregationVoList.add(termAggregationVo);
+ });
+ resultVo.addAggregation(new AggregationVo().setKey(a.getKey()).setList(aggregationVoList).setSumDoc(sum.get()));
+ }
+
+ });
+ }
+ return resultVo;
+ }
+
+ // 设置分页
+ public void setPageParams(SearchSourceBuilder searchSourceBuilder) {
+ int from = (ContextUtil.getPageNo() - 1) * ContextUtil.getPageSize();
+ if (from> Const.MAX_RESULT || (from + ContextUtil.getPageSize())> Const.MAX_RESULT) {
+ throw new ApiException(ApiExceptionCode.ES_OVER_MAX_RESULT);
+ }
+ searchSourceBuilder.from(from).size(ContextUtil.getPageSize());
+ // 显示总条数
+ searchSourceBuilder.trackTotalHits(true);
+ }
+
+ @SneakyThrows
+ @Override
+ public SearchDetailVo getDetail(String id) {
+ SearchDetailVo vo = new SearchDetailVo();
+ PoetryIndex docById = poetryEsService.getDocById(id);
+ BeanUtil.copyProperties(docById, vo);
+ return vo;
+ }
+
+ @Override
+ public EsSearchResultVo getFlyFlower(String text) {
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
+ boolQuery.must(QueryBuilders.matchPhraseQuery(LambdaUtil.getFieldName(PoetryIndex::getContent), text));
+ searchSourceBuilder.query(boolQuery);
+ // 高亮
+ HighlightBuilder highlightBuilder = new HighlightBuilder();
+ String fieldName = LambdaUtil.getFieldName(PoetryIndex::getContent);
+ highlightBuilder.field(fieldName);
+ highlightBuilder.fragmentSize(500);
+ searchSourceBuilder.highlighter(highlightBuilder);
+ searchSourceBuilder.fetchSource(new String[]{"title", "author"}, null);
+ // 分页
+ setPageParams(searchSourceBuilder);
+ EsResult response = poetryEsService.search(searchSourceBuilder);
+ List resultList = new ArrayList();
+ List records = response.getRecords();
+ EsSearchResultVo resultVo = new EsSearchResultVo();
+ resultVo.setTook(response.getTook()).setTotal(response.getTotal());
+ if (!ObjectUtils.isEmpty(records)) {
+ records.stream().forEach(i -> {
+ FlyFlowerVo flyFlowerVo = new FlyFlowerVo();
+ flyFlowerVo.setId(i.get_id()).setAuthor(i.getAuthor()).setTitle(i.getTitle()).set_id(i.get_id());
+ List list = i.getHighlight().get(fieldName);
+ if(!ObjectUtils.isEmpty(list)){
+ flyFlowerVo.setContent(list.get(0));
+ resultList.add(flyFlowerVo);
+ }else {
+ log.info("有毒,检索到但是没高亮,id={},text={}",i.get_id(),text);
+ }
+ });
+ }
+ resultVo.setRecords(resultList);
+ return resultVo;
+ }
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/util/ContextUtil.java b/springboot-es/src/main/java/top/rstyro/poetry/util/ContextUtil.java
new file mode 100644
index 0000000..56cdc05
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/util/ContextUtil.java
@@ -0,0 +1,40 @@
+package top.rstyro.poetry.util;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import top.rstyro.poetry.commons.ContextVo;
+
+/**
+ * 上下文工具类
+ * @author rstyro
+ */
+public class ContextUtil {
+
+ private final static TransmittableThreadLocal voLocal = new TransmittableThreadLocal();
+
+ public static ContextVo getVoLocal(){
+ ContextVo contextVo = voLocal.get();
+ if(contextVo==null){
+ contextVo = new ContextVo();
+ voLocal.set(contextVo);
+ }
+ return contextVo;
+ }
+
+ public static void setVoLocal(ContextVo contextVo) {
+ voLocal.set(contextVo);
+ }
+
+ public static Integer getPageNo() {
+ return getVoLocal().getPageNo();
+ }
+
+ public static Integer getPageSize() {
+ return getVoLocal().getPageSize();
+ }
+
+ public static String getTrackerId() {
+ return getVoLocal().getTrackerId();
+ }
+
+
+}
diff --git a/springboot-es/src/main/java/top/rstyro/poetry/util/LambdaUtil.java b/springboot-es/src/main/java/top/rstyro/poetry/util/LambdaUtil.java
new file mode 100644
index 0000000..911c19a
--- /dev/null
+++ b/springboot-es/src/main/java/top/rstyro/poetry/util/LambdaUtil.java
@@ -0,0 +1,84 @@
+package top.rstyro.poetry.util;
+
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
+
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+@Slf4j
+public class LambdaUtil {
+
+
+ /**
+ * 反射得到字段名称
+ */
+ public static String getFieldName(SFunction function) {
+ String fieldName = null;
+ try {
+ // 第1步 获取SerializedLambda
+ SerializedLambda serializedLambda = getSerializedLambda(function);
+ // 第2步 implMethodName 即为Field对应的Getter方法名
+ String implMethodName = serializedLambda.getImplMethodName();
+ fieldName=methodToProperty(implMethodName);
+ } catch (Exception e) {
+ log.error("反射获取属性异常,err={}",e.getMessage(),e);
+ }
+ return fieldName;
+ }
+
+ /**
+ * 反射得到字段属性
+ */
+ public static Field getField(SFunction