From feb2494e588cbed65d8495058a1f2af05cdd0da9 Mon Sep 17 00:00:00 2001 From: HiYnga <3535736368@qq.com> Date: Thu, 9 Jul 2026 16:48:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BD=BF=E7=94=A8=20?= =?UTF-8?q?Redis=20=E7=9A=84=E4=BD=BF=E7=94=A8=E6=96=B9=E6=B3=95=E5=92=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GradeSystem/src/com/yngang/demo/Main.java | 56 ++ 2026-07-03/spring-demo/pom.xml | 8 + .../yngang/springdemo/config/RedisConfig.java | 29 + .../exception/BusinessException.java | 2 +- .../springdemo/service/RedisDemoService.java | 6 + .../service/impl/RedisDemoServiceImpl.java | 50 ++ .../service/impl/StudentServiceImpl.java | 44 +- .../com/yngang/springdemo/util/RedisUtil.java | 616 ++++++++++++++++++ .../src/main/resources/application.yml | 13 + .../service/impl/StudentServiceImplTest.java | 108 +++ 2026-07-07/rag_agent_mvp.py | 282 ++++++++ 11 files changed, 1210 insertions(+), 4 deletions(-) create mode 100644 2026-07-01/backend/GradeSystem/src/com/yngang/demo/Main.java create mode 100644 2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/config/RedisConfig.java create mode 100644 2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/RedisDemoService.java create mode 100644 2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/RedisDemoServiceImpl.java create mode 100644 2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/util/RedisUtil.java create mode 100644 2026-07-03/spring-demo/src/test/java/com/yngang/springdemo/service/impl/StudentServiceImplTest.java create mode 100644 2026-07-07/rag_agent_mvp.py diff --git a/2026-07-01/backend/GradeSystem/src/com/yngang/demo/Main.java b/2026-07-01/backend/GradeSystem/src/com/yngang/demo/Main.java new file mode 100644 index 0000000..8ea5d35 --- /dev/null +++ b/2026-07-01/backend/GradeSystem/src/com/yngang/demo/Main.java @@ -0,0 +1,56 @@ +package com.yngang.demo; + +// 需要对入库操作正确加锁,不加锁会导致库存不一致, +// 要是两个线程同时执行到这行代码,都先加了5件,最后库存为10件会多出来5件库存导致库存不一致问题 +public class Main { + + static class Warehouse { + private int stock; + public Warehouse(int stock) { + this.stock = stock; + } + public int getStock() { + return stock; + } + public void setStock(int stock) { + this.stock = stock; + } + } + + static class MoveTask implements Runnable { + private Warehouse warehouse; + private String name; + private int moveCount; + public MoveTask(Warehouse warehouse, String name, int moveCount) { + this.warehouse = warehouse; + this.name = name; + this.moveCount = moveCount; + } + @Override + public void run() { + for (int i = 0; i < moveCount; i++) { + synchronized (warehouse) { + warehouse.setStock(warehouse.getStock() + 5); + } + System.out.println(name + " 入库 5 件,当前库存 " + warehouse.getStock()); + } + } + } + + public static void main(String[] args) { + Warehouse warehouse = new Warehouse(0); + MoveTask moveTaskA = new MoveTask(warehouse, "搬运工A", 10); + MoveTask moveTaskB = new MoveTask(warehouse, "搬运工B", 10); + Thread threadA = new Thread(moveTaskA); + Thread threadB = new Thread(moveTaskB); + threadA.start(); + threadB.start(); + try { + threadA.join(); + threadB.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.printf("最终库存:%d 件,是否为100件:%b", warehouse.getStock(), (warehouse.getStock() == 100)); + } +} diff --git a/2026-07-03/spring-demo/pom.xml b/2026-07-03/spring-demo/pom.xml index 3149978..5036a53 100644 --- a/2026-07-03/spring-demo/pom.xml +++ b/2026-07-03/spring-demo/pom.xml @@ -42,6 +42,14 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + org.projectlombok lombok diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/config/RedisConfig.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/config/RedisConfig.java new file mode 100644 index 0000000..9fdf27a --- /dev/null +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/config/RedisConfig.java @@ -0,0 +1,29 @@ +package com.yngang.springdemo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + // 设置序列化器 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + redisTemplate.afterPropertiesSet(); + + return redisTemplate; + } + +} diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/exception/BusinessException.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/exception/BusinessException.java index 0c78ea0..ec71c0d 100644 --- a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/exception/BusinessException.java +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/exception/BusinessException.java @@ -6,7 +6,7 @@ import lombok.Getter; * 自定义业务异常:抛出此异常时,全局异常处理器会自动捕获并返回友好的错误信息 */ @Getter -public class BusinessException extends Exception { +public class BusinessException extends RuntimeException { private static final int DEFAULT_CODE = 500; diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/RedisDemoService.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/RedisDemoService.java new file mode 100644 index 0000000..620e051 --- /dev/null +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/RedisDemoService.java @@ -0,0 +1,6 @@ +package com.yngang.springdemo.service; + +public interface RedisDemoService { + + void demo(); +} diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/RedisDemoServiceImpl.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/RedisDemoServiceImpl.java new file mode 100644 index 0000000..ecff477 --- /dev/null +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/RedisDemoServiceImpl.java @@ -0,0 +1,50 @@ +package com.yngang.springdemo.service.impl; + +import com.yngang.springdemo.service.RedisDemoService; +import com.yngang.springdemo.util.RedisUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class RedisDemoServiceImpl implements RedisDemoService { + + private final RedisUtil redisUtil; + + @Override + public void demo() { + redisUtil.set("name", "zhangsan"); + redisUtil.set("age", 18); + Object name = redisUtil.get("name"); + Object age = redisUtil.get("age"); + System.out.println(name); + System.out.println(age); + + redisUtil.hSet("user:1", "name", "zhangsan"); + redisUtil.hSet("user:1", "age", 18); + Object userName = redisUtil.hGet("user:1", "name"); + System.out.println(userName); + Object userAge = redisUtil.hGet("user:1", "age"); + System.out.println(userAge); + + redisUtil.rPush("mylist", "zhangsan"); + redisUtil.rPush("mylist", "lisi"); + List list = redisUtil.lRange("mylist", 0, -1); + System.out.println(list); + + redisUtil.sAdd("mylist", "zhangsan", "lisi", "wangwu", "zhaoliu"); + Set setList = redisUtil.sMembers("mylist"); + System.out.println(setList); + + redisUtil.zAdd("mylist", "zhangsan", 100); + redisUtil.zAdd("mylist", "lisi", 200); + redisUtil.zAdd("mylist", "wangwu", 300); + redisUtil.zAdd("mylist", "zhaoliu", 400); + Set zList = redisUtil.zRange("mylist", 0, -1); + System.out.println(zList); + + } +} diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/StudentServiceImpl.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/StudentServiceImpl.java index a103610..ab252a7 100644 --- a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/StudentServiceImpl.java +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/service/impl/StudentServiceImpl.java @@ -10,6 +10,7 @@ import com.yngang.springdemo.entity.Student; import com.yngang.springdemo.exception.BusinessException; import com.yngang.springdemo.mapper.StudentMapper; import com.yngang.springdemo.service.StudentService; +import com.yngang.springdemo.util.RedisUtil; import com.yngang.springdemo.vo.StudentVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,6 +24,7 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -36,6 +38,8 @@ public class StudentServiceImpl implements StudentService { private static final int STATUS_ENROLLED = 0; private static final int STATUS_SUSPENDED = 1; private static final int STATUS_GRADUATED = 2; + private static final String STUDENT_CACHE_KEY_PREFIX = "student:detail:"; + private static final long STUDENT_CACHE_TTL_MINUTES = 30L; private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"); @@ -43,12 +47,23 @@ public class StudentServiceImpl implements StudentService { private static final DateTimeFormatter ENROLL_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final StudentMapper studentMapper; + private final RedisUtil redisUtil; @Override @Transactional(readOnly = true) public StudentVO findById(Long id) { validateId(id, "学生ID不能为空"); - return StudentVO.from(requireStudent(id)); + String cacheKey = buildStudentCacheKey(id); + StudentVO cachedStudent = redisUtil.get(cacheKey, StudentVO.class); + if (cachedStudent != null) { + log.info("从Redis缓存中获取学生详情,id={}", id); + return cachedStudent; + } + + StudentVO studentVO = StudentVO.from(requireStudent(id)); + cacheStudent(studentVO); + log.info("学生详情写入Redis缓存,id={}", id); + return studentVO; } @Override @@ -71,8 +86,10 @@ public class StudentServiceImpl implements StudentService { throw new BusinessException(500, "新增学生失败"); } + StudentVO createdStudent = StudentVO.from(requireStudent(student.getId())); + cacheStudent(createdStudent); log.info("创建学生成功,ID:{}", student.getId()); - return StudentVO.from(requireStudent(student.getId())); + return createdStudent; } @Override @@ -109,8 +126,10 @@ public class StudentServiceImpl implements StudentService { throw new BusinessException(500, "更新学生失败"); } + StudentVO updatedStudent = StudentVO.from(requireStudent(student.getId())); + cacheStudent(updatedStudent); log.info("更新学生成功,ID:{}", student.getId()); - return StudentVO.from(requireStudent(student.getId())); + return updatedStudent; } @Override @@ -124,6 +143,7 @@ public class StudentServiceImpl implements StudentService { throw new BusinessException(500, "删除学生失败"); } + evictStudentCache(id); log.info("删除学生成功,ID:{}", id); } @@ -344,6 +364,24 @@ public class StudentServiceImpl implements StudentService { return safeQuery; } + private void cacheStudent(StudentVO studentVO) { + if (studentVO == null || studentVO.getId() == null) { + return; + } + redisUtil.set(buildStudentCacheKey(studentVO.getId()), studentVO, STUDENT_CACHE_TTL_MINUTES, TimeUnit.MINUTES); + } + + private void evictStudentCache(Long id) { + if (id == null) { + return; + } + redisUtil.delete(buildStudentCacheKey(id)); + } + + private String buildStudentCacheKey(Long id) { + return STUDENT_CACHE_KEY_PREFIX + id; + } + private LocalDateTime parseEnrollDate(String enrollDate) { if (!StringUtils.hasText(enrollDate)) { return null; diff --git a/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/util/RedisUtil.java b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/util/RedisUtil.java new file mode 100644 index 0000000..c2d1dfd --- /dev/null +++ b/2026-07-03/spring-demo/src/main/java/com/yngang/springdemo/util/RedisUtil.java @@ -0,0 +1,616 @@ +package com.yngang.springdemo.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Redis工具类 - 使用RedisTemplate实现对Redis各种数据类型的操作 + */ +@Component +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + // ==================== String类型操作 ==================== + + /** + * 设置字符串值 + * + * @param key 键 + * @param value 值 + */ + public void set(String key, Object value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 设置字符串值并指定过期时间 + * + * @param key 键 + * @param value 值 + * @param timeout 过期时间 + * @param timeUnit 时间单位 + */ + public void set(String key, Object value, long timeout, TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 获取字符串值 + * + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + /** + * 获取字符串值并转为指定类型 + * + * @param key 键 + * @param clazz 返回类型 + * @param 泛型 + * @return 值 + */ + @SuppressWarnings("unchecked") + public T get(String key, Class clazz) { + Object value = redisTemplate.opsForValue().get(key); + return value != null ? (T) value : null; + } + + /** + * 删除指定键 + * + * @param key 键 + * @return 是否删除成功 + */ + public Boolean delete(String key) { + return redisTemplate.delete(key); + } + + /** + * 删除多个键 + * + * @param keys 键集合 + * @return 删除的数量 + */ + public Long delete(Collection keys) { + return redisTemplate.delete(keys); + } + + /** + * 判断键是否存在 + * + * @param key 键 + * @return 是否存在 + */ + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + /** + * 设置键的过期时间 + * + * @param key 键 + * @param timeout 过期时间 + * @param timeUnit 时间单位 + * @return 是否设置成功 + */ + public Boolean expire(String key, long timeout, TimeUnit timeUnit) { + return redisTemplate.expire(key, timeout, timeUnit); + } + + /** + * 获取键的过期时间 + * + * @param key 键 + * @return 过期时间(秒),-1表示永不过期,-2表示键不存在 + */ + public Long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 递增操作 + * + * @param key 键 + * @param delta 增量值 + * @return 递增后的值 + */ + public Long increment(String key, long delta) { + return redisTemplate.opsForValue().increment(key, delta); + } + + /** + * 递减操作 + * + * @param key 键 + * @param delta 减量值 + * @return 递减后的值 + */ + public Long decrement(String key, long delta) { + return redisTemplate.opsForValue().decrement(key, delta); + } + + // ==================== Hash类型操作 ==================== + + /** + * 向Hash中设置字段值 + * + * @param key 键 + * @param field 字段 + * @param value 值 + */ + public void hSet(String key, String field, Object value) { + redisTemplate.opsForHash().put(key, field, value); + } + + /** + * 向Hash中批量设置字段值 + * + * @param key 键 + * @param values 字段值Map + */ + public void hSetAll(String key, Map values) { + redisTemplate.opsForHash().putAll(key, values); + } + + /** + * 获取Hash中指定字段的值 + * + * @param key 键 + * @param field 字段 + * @return 值 + */ + public Object hGet(String key, String field) { + return redisTemplate.opsForHash().get(key, field); + } + + /** + * 获取Hash中指定字段的值并转为指定类型 + * + * @param key 键 + * @param field 字段 + * @param clazz 返回类型 + * @param 泛型 + * @return 值 + */ + @SuppressWarnings("unchecked") + public T hGet(String key, String field, Class clazz) { + Object value = redisTemplate.opsForHash().get(key, field); + return value != null ? (T) value : null; + } + + /** + * 获取Hash中所有字段值 + * + * @param key 键 + * @return 所有字段值Map + */ + public Map hGetAll(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 获取Hash中所有字段 + * + * @param key 键 + * @return 字段集合 + */ + public Set hKeys(String key) { + return redisTemplate.opsForHash().keys(key); + } + + /** + * 获取Hash中所有值 + * + * @param key 键 + * @return 值集合 + */ + public List hValues(String key) { + return redisTemplate.opsForHash().values(key); + } + + /** + * 删除Hash中指定字段 + * + * @param key 键 + * @param fields 字段数组 + * @return 删除的数量 + */ + public Long hDelete(String key, Object... fields) { + return redisTemplate.opsForHash().delete(key, fields); + } + + /** + * 判断Hash中是否存在指定字段 + * + * @param key 键 + * @param field 字段 + * @return 是否存在 + */ + public Boolean hHasKey(String key, String field) { + return redisTemplate.opsForHash().hasKey(key, field); + } + + /** + * Hash中字段值递增 + * + * @param key 键 + * @param field 字段 + * @param delta 增量值 + * @return 递增后的值 + */ + public Long hIncrement(String key, String field, long delta) { + return redisTemplate.opsForHash().increment(key, field, delta); + } + + /** + * Hash中字段值递减 + * + * @param key 键 + * @param field 字段 + * @param delta 减量值 + * @return 递减后的值 + */ + public Long hDecrement(String key, String field, long delta) { + return redisTemplate.opsForHash().increment(key, field, -delta); + } + + // ==================== List类型操作 ==================== + + /** + * 从List左侧插入元素 + * + * @param key 键 + * @param value 值 + * @return List长度 + */ + public Long lPush(String key, Object value) { + return redisTemplate.opsForList().leftPush(key, value); + } + + /** + * 从List左侧批量插入元素 + * + * @param key 键 + * @param values 值数组 + * @return List长度 + */ + public Long lPushAll(String key, Object... values) { + return redisTemplate.opsForList().leftPushAll(key, values); + } + + /** + * 从List右侧插入元素 + * + * @param key 键 + * @param value 值 + * @return List长度 + */ + public Long rPush(String key, Object value) { + return redisTemplate.opsForList().rightPush(key, value); + } + + /** + * 从List右侧批量插入元素 + * + * @param key 键 + * @param values 值数组 + * @return List长度 + */ + public Long rPushAll(String key, Object... values) { + return redisTemplate.opsForList().rightPushAll(key, values); + } + + /** + * 从List左侧弹出元素 + * + * @param key 键 + * @return 弹出的元素 + */ + public Object lPop(String key) { + return redisTemplate.opsForList().leftPop(key); + } + + /** + * 从List右侧弹出元素 + * + * @param key 键 + * @return 弹出的元素 + */ + public Object rPop(String key) { + return redisTemplate.opsForList().rightPop(key); + } + + /** + * 获取List指定索引范围的元素 + * + * @param key 键 + * @param start 起始索引 + * @param end 结束索引 + * @return 元素列表 + */ + public List lRange(String key, long start, long end) { + return redisTemplate.opsForList().range(key, start, end); + } + + /** + * 获取List长度 + * + * @param key 键 + * @return List长度 + */ + public Long lSize(String key) { + return redisTemplate.opsForList().size(key); + } + + /** + * 设置List指定索引位置的元素 + * + * @param key 键 + * @param index 索引 + * @param value 值 + */ + public void lSet(String key, long index, Object value) { + redisTemplate.opsForList().set(key, index, value); + } + + /** + * 删除List中指定值的元素 + * + * @param key 键 + * @param count 删除数量(0表示全部,正数表示从左到右,负数表示从右到左) + * @param value 值 + * @return 删除的数量 + */ + public Long lRemove(String key, long count, Object value) { + return redisTemplate.opsForList().remove(key, count, value); + } + + /** + * 获取List指定索引位置的元素 + * + * @param key 键 + * @param index 索引 + * @return 元素 + */ + public Object lIndex(String key, long index) { + return redisTemplate.opsForList().index(key, index); + } + + // ==================== Set类型操作 ==================== + + /** + * 向Set中添加元素 + * + * @param key 键 + * @param values 值数组 + * @return 添加的数量 + */ + public Long sAdd(String key, Object... values) { + return redisTemplate.opsForSet().add(key, values); + } + + /** + * 获取Set中的所有元素 + * + * @param key 键 + * @return 元素集合 + */ + public Set sMembers(String key) { + return redisTemplate.opsForSet().members(key); + } + + /** + * 判断Set中是否存在指定元素 + * + * @param key 键 + * @param value 值 + * @return 是否存在 + */ + public Boolean sIsMember(String key, Object value) { + return redisTemplate.opsForSet().isMember(key, value); + } + + /** + * 获取Set的大小 + * + * @param key 键 + * @return Set大小 + */ + public Long sSize(String key) { + return redisTemplate.opsForSet().size(key); + } + + /** + * 从Set中删除指定元素 + * + * @param key 键 + * @param values 值数组 + * @return 删除的数量 + */ + public Long sRemove(String key, Object... values) { + return redisTemplate.opsForSet().remove(key, values); + } + + /** + * 从Set中随机弹出一个元素 + * + * @param key 键 + * @return 弹出的元素 + */ + public Object sPop(String key) { + return redisTemplate.opsForSet().pop(key); + } + + /** + * 获取两个Set的交集 + * + * @param key1 键1 + * @param key2 键2 + * @return 交集元素集合 + */ + public Set sIntersect(String key1, String key2) { + return redisTemplate.opsForSet().intersect(key1, key2); + } + + /** + * 获取两个Set的并集 + * + * @param key1 键1 + * @param key2 键2 + * @return 并集元素集合 + */ + public Set sUnion(String key1, String key2) { + return redisTemplate.opsForSet().union(key1, key2); + } + + /** + * 获取两个Set的差集 + * + * @param key1 键1 + * @param key2 键2 + * @return 差集元素集合 + */ + public Set sDifference(String key1, String key2) { + return redisTemplate.opsForSet().difference(key1, key2); + } + + // ==================== ZSet(Sorted Set)类型操作 ==================== + + /** + * 向ZSet中添加元素 + * + * @param key 键 + * @param value 值 + * @param score 分数 + * @return 是否添加成功 + */ + public Boolean zAdd(String key, Object value, double score) { + return redisTemplate.opsForZSet().add(key, value, score); + } + + /** + * 获取ZSet中元素的分数 + * + * @param key 键 + * @param value 值 + * @return 分数 + */ + public Double zScore(String key, Object value) { + return redisTemplate.opsForZSet().score(key, value); + } + + /** + * 获取ZSet中元素的排名(从小到大) + * + * @param key 键 + * @param value 值 + * @return 排名(从0开始) + */ + public Long zRank(String key, Object value) { + return redisTemplate.opsForZSet().rank(key, value); + } + + /** + * 获取ZSet中元素的排名(从大到小) + * + * @param key 键 + * @param value 值 + * @return 排名(从0开始) + */ + public Long zReverseRank(String key, Object value) { + return redisTemplate.opsForZSet().reverseRank(key, value); + } + + /** + * 获取ZSet指定排名范围的元素(从小到大) + * + * @param key 键 + * @param start 起始排名 + * @param end 结束排名 + * @return 元素集合 + */ + public Set zRange(String key, long start, long end) { + return redisTemplate.opsForZSet().range(key, start, end); + } + + /** + * 获取ZSet指定排名范围的元素(从大到小) + * + * @param key 键 + * @param start 起始排名 + * @param end 结束排名 + * @return 元素集合 + */ + public Set zReverseRange(String key, long start, long end) { + return redisTemplate.opsForZSet().reverseRange(key, start, end); + } + + /** + * 获取ZSet指定分数范围的元素 + * + * @param key 键 + * @param min 最小分数 + * @param max 最大分数 + * @return 元素集合 + */ + public Set zRangeByScore(String key, double min, double max) { + return redisTemplate.opsForZSet().rangeByScore(key, min, max); + } + + /** + * 获取ZSet的大小 + * + * @param key 键 + * @return ZSet大小 + */ + public Long zSize(String key) { + return redisTemplate.opsForZSet().size(key); + } + + /** + * 获取ZSet指定分数范围内的元素数量 + * + * @param key 键 + * @param min 最小分数 + * @param max 最大分数 + * @return 元素数量 + */ + public Long zCount(String key, double min, double max) { + return redisTemplate.opsForZSet().count(key, min, max); + } + + /** + * 从ZSet中删除指定元素 + * + * @param key 键 + * @param values 值数组 + * @return 删除的数量 + */ + public Long zRemove(String key, Object... values) { + return redisTemplate.opsForZSet().remove(key, values); + } + + /** + * 增加ZSet中元素的分数 + * + * @param key 键 + * @param value 值 + * @param delta 增量值 + * @return 更新后的分数 + */ + public Double zIncrementScore(String key, Object value, double delta) { + return redisTemplate.opsForZSet().incrementScore(key, value, delta); + } +} \ No newline at end of file diff --git a/2026-07-03/spring-demo/src/main/resources/application.yml b/2026-07-03/spring-demo/src/main/resources/application.yml index 1aa7a9e..db9ea9d 100644 --- a/2026-07-03/spring-demo/src/main/resources/application.yml +++ b/2026-07-03/spring-demo/src/main/resources/application.yml @@ -1,6 +1,19 @@ spring: application: name: spring-demo + redis: + host: 192.168.142.130 + port: 6379 + password: 123456 + database: 1 + timeout: 5000ms + connect-timeout: 5000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + max-wait: 1000ms datasource: url: jdbc:mysql://192.168.142.130:3306/spring_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root diff --git a/2026-07-03/spring-demo/src/test/java/com/yngang/springdemo/service/impl/StudentServiceImplTest.java b/2026-07-03/spring-demo/src/test/java/com/yngang/springdemo/service/impl/StudentServiceImplTest.java new file mode 100644 index 0000000..1669e80 --- /dev/null +++ b/2026-07-03/spring-demo/src/test/java/com/yngang/springdemo/service/impl/StudentServiceImplTest.java @@ -0,0 +1,108 @@ +package com.yngang.springdemo.service.impl; + +import com.yngang.springdemo.entity.Student; +import com.yngang.springdemo.mapper.StudentMapper; +import com.yngang.springdemo.util.RedisUtil; +import com.yngang.springdemo.vo.StudentVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StudentServiceImplTest { + + private static final String STUDENT_CACHE_KEY_PREFIX = "student:detail:"; + + @Mock + private StudentMapper studentMapper; + + @Mock + private RedisUtil redisUtil; + + @InjectMocks + private StudentServiceImpl studentService; + + @Test + void findById_shouldReturnCachedStudentWhenCacheHit() { + Long studentId = 1L; + StudentVO cachedStudent = buildStudentVO(studentId, "张三"); + when(redisUtil.get(STUDENT_CACHE_KEY_PREFIX + studentId, StudentVO.class)).thenReturn(cachedStudent); + + StudentVO result = studentService.findById(studentId); + + assertThat(result).isSameAs(cachedStudent); + verify(studentMapper, never()).findById(any(Long.class)); + } + + @Test + void findById_shouldQueryDatabaseAndCacheResultWhenCacheMiss() { + Long studentId = 2L; + Student student = buildStudent(studentId, "李四"); + when(redisUtil.get(STUDENT_CACHE_KEY_PREFIX + studentId, StudentVO.class)).thenReturn(null); + when(studentMapper.findById(studentId)).thenReturn(student); + + StudentVO result = studentService.findById(studentId); + + assertThat(result.getId()).isEqualTo(studentId); + assertThat(result.getName()).isEqualTo("李四"); + verify(redisUtil).set(eq(STUDENT_CACHE_KEY_PREFIX + studentId), eq(result), eq(30L), eq(TimeUnit.MINUTES)); + } + + private Student buildStudent(Long studentId, String name) { + Student student = new Student(); + student.setId(studentId); + student.setStudentNo("2024000001"); + student.setName(name); + student.setGender(1); + student.setAge(20); + student.setGrade("2024"); + student.setClassName("1班"); + student.setMajor("计算机科学"); + student.setCollege("信息工程学院"); + student.setPhone("13812345678"); + student.setEmail("student@example.com"); + student.setHometown("上海"); + student.setDormitory("A101"); + student.setGpa(3.8); + student.setStatus(0); + student.setEnrollDate(LocalDateTime.of(2024, 9, 1, 0, 0)); + student.setCreatedAt(LocalDateTime.of(2024, 9, 1, 8, 0)); + student.setUpdatedAt(LocalDateTime.of(2024, 9, 2, 8, 0)); + return student; + } + + private StudentVO buildStudentVO(Long studentId, String name) { + StudentVO studentVO = new StudentVO(); + studentVO.setId(studentId); + studentVO.setStudentNo("2024000001"); + studentVO.setName(name); + studentVO.setGender(1); + studentVO.setAge(20); + studentVO.setGrade("2024"); + studentVO.setClassName("1班"); + studentVO.setMajor("计算机科学"); + studentVO.setCollege("信息工程学院"); + studentVO.setPhone("13812345678"); + studentVO.setEmail("student@example.com"); + studentVO.setHometown("上海"); + studentVO.setDormitory("A101"); + studentVO.setGpa(3.8); + studentVO.setStatus(0); + studentVO.setEnrollDate("2024-09-01"); + studentVO.setCreatedAt(LocalDateTime.of(2024, 9, 1, 8, 0)); + studentVO.setUpdatedAt(LocalDateTime.of(2024, 9, 2, 8, 0)); + return studentVO; + } +} diff --git a/2026-07-07/rag_agent_mvp.py b/2026-07-07/rag_agent_mvp.py new file mode 100644 index 0000000..b749c95 --- /dev/null +++ b/2026-07-07/rag_agent_mvp.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""单文件版 RAG + Agent MVP 模拟。""" + +from __future__ import annotations + +import argparse +import random +import re +import textwrap +from typing import Dict, List, Optional, Tuple + + +KNOWLEDGE_BASE: List[Dict[str, object]] = [ + { + "topic": "leave", + "title": "年假申请", + "keywords": ["年假", "请假", "休假", "流程", "多少天"], + "content": "年假需提前 3 个工作日提交审批,转正员工每年可享受 10 天年假。", + }, + { + "topic": "overtime", + "title": "加班餐补", + "keywords": ["加班", "餐补", "补贴", "报销", "晚上"], + "content": "工作日加班至 20:30 之后可申请 30 元餐补,需在次日中午前提交凭证。", + }, + { + "topic": "travel", + "title": "差旅报销", + "keywords": ["差旅", "出差", "报销", "机票", "酒店"], + "content": "差旅结束后 5 个工作日内完成报销提单,机票和酒店发票需一并上传。", + }, + { + "topic": "attendance", + "title": "考勤补卡", + "keywords": ["考勤", "补卡", "迟到", "打卡", "忘记"], + "content": "忘记打卡可在当天 18:00 前发起补卡申请,每月最多补卡 3 次。", + }, +] + +WEATHER_DATA: Dict[str, Dict[str, object]] = { + "北京": {"condition": "多云", "temperature": 31, "humidity": 46, "wind": "东北风 2 级"}, + "上海": {"condition": "晴", "temperature": 33, "humidity": 62, "wind": "东南风 3 级"}, + "广州": {"condition": "阵雨", "temperature": 29, "humidity": 80, "wind": "南风 2 级"}, + "深圳": {"condition": "多云", "temperature": 30, "humidity": 74, "wind": "东风 2 级"}, + "杭州": {"condition": "小雨", "temperature": 28, "humidity": 85, "wind": "东南风 2 级"}, +} + +WEATHER_KEYWORDS = {"天气", "气温", "下雨", "热吗", "冷吗", "几度", "温度", "晴吗"} +POLICY_KEYWORDS = { + "政策", + "规定", + "制度", + "流程", + "奖金", + "福利", + "补助", + "津贴", + "年假", + "请假", + "加班", + "餐补", + "报销", + "补卡", + "考勤", + "差旅", +} +CHAT_KEYWORDS = {"你好", "谢谢", "心情", "聊聊", "在吗", "陪我", "开心", "难过"} + +CHAT_FALLBACKS = [ + "我在呢,可以陪你聊,也可以继续帮你查天气或公司政策。", + "收到,我们可以轻松聊聊;如果你想切回办公问题,我也能马上接上。", + "没问题,我可以一边陪聊,一边随时帮你查信息。", +] + + +def normalize_text(text: str) -> str: + return re.sub(r"\s+", "", text.strip().lower()) + + +def extract_city(query: str) -> Optional[str]: + for city in WEATHER_DATA: + if city in query: + return city + match = re.search(r"([\u4e00-\u9fff]{2,6})(?:明天|今天|天气|气温|热吗|冷吗|下雨)", query) + if match: + return match.group(1) + return None + + +def detect_intent(query: str) -> Dict[str, str]: + normalized = normalize_text(query) + city = extract_city(query) + + weather_hits = sum(1 for kw in WEATHER_KEYWORDS if kw in normalized) + (1 if city else 0) + policy_hits = sum(1 for kw in POLICY_KEYWORDS if kw in normalized) + chat_hits = sum(1 for kw in CHAT_KEYWORDS if kw in normalized) + + if weather_hits >= max(policy_hits, chat_hits, 1): + return { + "intent": "Weather", + "kw": city or "天气", + "reason": "命中天气关键词或城市名", + } + if policy_hits >= max(weather_hits, chat_hits, 1): + return { + "intent": "Policy", + "kw": find_best_keyword(query, POLICY_KEYWORDS) or "政策", + "reason": "命中制度/流程类关键词", + } + return { + "intent": "Chat", + "kw": find_best_keyword(query, CHAT_KEYWORDS) or "闲聊", + "reason": "未命中明确工具/知识库信号,走闲聊路由", + } + + +def find_best_keyword(query: str, candidates: set[str]) -> Optional[str]: + for keyword in sorted(candidates, key=len, reverse=True): + if keyword in query: + return keyword + return None + + +def simple_rag_search(query: str) -> Optional[Dict[str, object]]: + scored_docs: List[Tuple[int, Dict[str, object]]] = [] + for doc in KNOWLEDGE_BASE: + keywords = doc["keywords"] + score = sum(2 for kw in keywords if kw in query) + if doc["title"] in query: + score += 1 + if score > 0: + scored_docs.append((score, doc)) + + if not scored_docs: + return None + + scored_docs.sort(key=lambda item: item[0], reverse=True) + return scored_docs[0][1] + + +def answer_policy(query: str) -> str: + doc = simple_rag_search(query) + if doc is None: + return ( + "【防幻觉机制已触发】\n" + "资料未找到,我目前只能回答知识库里已有的政策条目," + "请改问年假、加班餐补、差旅报销或考勤补卡。" + ) + + return ( + f"【RAG 命中】{doc['title']}\n" + f"依据知识库内容:{doc['content']}\n" + "说明:这是基于内部片段的精确回答,没有额外编造。" + ) + + +def get_weather(query: str) -> str: + city = extract_city(query) or "北京" + weather = WEATHER_DATA.get(city) + if weather is None: + supported = "、".join(WEATHER_DATA.keys()) + return f"当前只模拟了 {supported} 的天气数据,你可以换这些城市再试试。" + + trend = random.choice(["体感偏热", "适合外出", "建议备伞", "注意补水"]) + return ( + f"【天气工具结果】\n" + f"{city}:{weather['condition']},{weather['temperature']}°C," + f"湿度 {weather['humidity']}%,{weather['wind']}。\n" + f"建议:{trend}。" + ) + + +def chat_reply(query: str) -> str: + if "你好" in query or "hello" in query.lower(): + return "你好呀,我可以陪你聊天,也可以帮你查天气、查政策。" + if "谢谢" in query: + return "不客气,随时叫我。" + if "心情" in query or "难过" in query: + return "听起来你想找个人说说话,我在这儿。要不要先讲讲今天最让你在意的事?" + return random.choice(CHAT_FALLBACKS) + + +def route_query(query: str) -> Dict[str, str]: + intent_info = detect_intent(query) + intent = intent_info["intent"] + + if intent == "Weather": + answer = get_weather(query) + elif intent == "Policy": + answer = answer_policy(query) + else: + answer = chat_reply(query) + + return { + "intent": intent, + "kw": intent_info["kw"], + "reason": intent_info["reason"], + "answer": answer, + } + + +def run_demo() -> None: + demo_queries = [ + "北京明天天气热吗?", + "年假有多少天?", + "生育奖金发多少?", + "你好,今天心情一般。", + ] + print("===== RAG + Agent MVP 演示 =====") + for query in demo_queries: + show_result(query) + + +def show_result(query: str) -> None: + result = route_query(query) + print(f"\n用户:{query}") + print( + "Agent 路由:" + f"intent={result['intent']}, kw={result['kw']}, reason={result['reason']}" + ) + print(result["answer"]) + + +def interactive_chat() -> None: + intro = """ + 欢迎使用单文件 Python 版 RAG + Agent MVP。 + + 可体验 3 类能力: + 1. 查天气:例如 “北京今天天气怎么样” + 2. 查政策:例如 “年假有多少天” + 3. 友好闲聊:例如 “你好,陪我聊聊” + + 输入 quit / exit 结束。 + """ + print(textwrap.dedent(intro).strip()) + + while True: + query = input("\n你:").strip() + if not query: + print("请输入一点内容,我才能判断要走哪条路由。") + continue + if query.lower() in {"quit", "exit"}: + print("下次见,我会继续在天气、政策和陪聊之间帮你切换。") + break + show_result(query) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="单文件版 RAG + Agent MVP:天气查询 + 政策问答 + 友好闲聊" + ) + parser.add_argument( + "--query", + help="单次提问,例如:--query \"年假有多少天?\"", + ) + parser.add_argument( + "--demo", + action="store_true", + help="运行内置演示场景", + ) + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + if args.demo: + run_demo() + return + + if args.query: + show_result(args.query) + return + + interactive_chat() + + +if __name__ == "__main__": + main() -- Gitee