113 Commit-ok aa9bc29f45 ... b5f1f26aab

Szerző SHA1 Üzenet Dátum
  luoshi b5f1f26aab 修改core-ai,增加自动判分统计时的兼容处理 11 hónapja
  luoshi a488df6032 修改core-ai,更新计算小数位数算法 11 hónapja
  luoshi 6d0c0489f1 修改core-ai,调整自动判分请求对象与返回数据对象中的若干字段类型,适配提示词模版修改 11 hónapja
  luoshi ab9743d26d 修改core-ai,调整自动判分请求对象结构与返回数据结构 11 hónapja
  luoshi 15ed1da8bf 修改core-retrofit,调整默认超时配置,补充初始化超时配置遗漏 11 hónapja
  luoshi 23e09cd249 修改core-ai,增加AutoScoreRequest非空校验注解 1 éve
  luoshi c8338a9267 修改core-ai,增加AiService,整合Api接口逻辑合并为简化后的服务方法 1 éve
  luoshi a7a5a00879 修改core-ai,修改OcrType命名 1 éve
  luoshi 0229eb9ee3 修改core-ai,修改OCR接口路径定义 1 éve
  luoshi 588117274a 修改core-ai,增加OCR相关数据与API接口定义 1 éve
  luoshi 61e7a464b3 修改core-ai,增加AI接口类型与AppType类型;修改MemoryRateLimiter部分实现逻辑,提高并发性能 1 éve
  luoshi 5599391fc1 修改core-ai,修改chat接口通用返回数据结构,增加查询余额接口 1 éve
  luoshi 46b7f1e8ac 修改core-ai,接口形式改为通用chat模型 1 éve
  luoshi eb42e35293 创建core-ai模块,用于提供AI相关服务接口 1 éve
  luoshi 194fd72c55 扩展tools-poi,为ExcelWriter增加设置单元格样式方法 1 éve
  luoshi 5916f14bc7 扩展core-security增加调试模式,方便压测等场景跳过签名密文验证 1 éve
  luoshi d9af9a6945 修改core-models中标准异常类型的注释描述 1 éve
  luoshi 5ed9802aae 修改starter-api中内部错误码定义,并在文档中详细陈列出来 1 éve
  luoshi cd03e86aa1 修改tools-common,ZipReader和ZipWriter挪回原package保持兼容性 1 éve
  luoshi e7d4a43004 扩展core-security,引用jasypt工具,支持配置文件加密,提供通用的加解密服务工具;扩展starter-api,默认依赖core-solar自动判断内置密钥,并提供/api/boot开头的内置接口 1 éve
  luoshi bb4193b6c8 扩展tools-poi,ExcelWriter写入title时自动居中且按表头列数合并单元格 1 éve
  luoshi 63e5f08ac3 扩展tools-poi,增加Excel读取为字符串数组的方法支持;修复xls格式读取最后一行bug;增加单元测试文件及代码 1 éve
  luoshi ed67804fe0 扩展core-solar增加可选配置项com.qmth.solar.module-code,用于配合ops保存模块code 1 éve
  luoshi ca56f129c6 修改PdfFormUtil增加图片变量替换保护性代码 1 éve
  luoshi 4a1126c004 增加tools-pdf,tools-zxing,tools-freemarker三个模块;ByteArray增加新方法;ZipReader和ZipWriter修改package位置 1 éve
  luoshi 7fa79a069e 修改core-solar增加wxapp新服务接口,提供获取登录手机号支持 1 éve
  luoshi ba6982df9d 修改core-concurrent中Lock切面日志配置,并增加com.qmth.lock.log-level配置项默认为INFO 1 éve
  luoshi ba0a7e59c4 修改starter-api切换Aac注解中所有BOOL为boolean[] 1 éve
  luoshi b853738ec5 修改starter-api切换Aac注解中所有BOOL为boolean[] 1 éve
  luoshi f4f3465c71 扩展core-sms,修改动态接口地址为动态服务地址,相对路径自动拼接 1 éve
  luoshi 4dc6effa9a 扩展core-sms增加动态指定发送接口地址的方法支持 1 éve
  luoshi 3ed412fc21 修改AppInfo恢复code字段及其校验逻辑 1 éve
  luoshi c7e7c7a5c1 扩展core-solar中AppControl模型,增加自定义控制参数字段 1 éve
  luoshi 1a452375a4 扩展core-fss增加copy方法 2 éve
  luoshi b86631db9d 补充core-solar解析授权文件绑定版本的逻辑 2 éve
  luoshi a08c2f09aa 补充solar中AppInfo和AppLicense模型的json配置,允许忽略未知属性,兼容后续补充code字段的情况 2 éve
  luoshi a375c74d58 暂时修改solar中的AppInfo对象,兼容1.0.3版本授权文件解析 2 éve
  luoshi 050f510b32 修改core-solar增加app-code配置,以及配套的强制校验逻辑 2 éve
  luoshi 4b0dd3e5e9 修改core-solar增加序列化设置 2 éve
  luoshi 16359ca3b0 修改starter-api增加内置监控页面 2 éve
  luoshi bce0232d5b 修改actuator默认配置 2 éve
  luoshi b134cf2453 修改所有模块ComponengScan包路径配置 2 éve
  luoshi 82a1e34925 修改默认pom设置,调整source插件完善构建产物 2 éve
  luoshi a9bab8b4dc 修改starter-api,将actuator默认配置挪到配置文件中,还原全局限流配置支持 2 éve
  luoshi d82983a679 修改限流相关注释描述 2 éve
  luoshi 3e142bc3ac 优化RequestUtil读取属性方法 2 éve
  luoshi b3b356fe7e 重构限流器框架,调整core-rate-limit,data-redis,starter-api模块相关内容 2 éve
  luoshi 7d2586245d 扩展data-mybatis-plus,增加schema-validate配置,对应启动时的schema校验功能 2 éve
  luoshi 7679627539 修改starter-api中HttpTraceUtil的bug 2 éve
  luoshi 4d7e8badc4 修改starter-api,将actuator默认设置写入ApiAutoConfiguration中,无需应用单独配置 2 éve
  luoshi a52b825cc9 方法重命名 2 éve
  luoshi 087089d97f 修改tools-common,重命名PageSize为PaperSize 2 éve
  luoshi fcda8489bb 修改core-retrofit,支持将签名信息以方法参数形式动态提供,去掉之前临时通过header交换的方法;签名信息提供两个类型的快速创建方法 2 éve
  luoshi 37d55f2b6b 修改requestTrace工具类与配置参数名 2 éve
  luoshi f73d4dc479 starter-api在RequestTrace中增加对MultipartFile类型参数的详情支持 2 éve
  luoshi 31457819f0 修改data-redis默认jackson序列化配置 2 éve
  luoshi 93b5a94264 core-cache开启caffeine的recordStats 2 éve
  luoshi 28652828c7 core-retrofit模块增加针对okhttp的metricss事件监听 2 éve
  luoshi 828ce2397e 移除Aac注解中的权限部分;恢复core-cache默认softValues配置 2 éve
  luoshi 1f46d360c3 移除core-metrics模块,starter-api增加actuator依赖,Aac注解移除metrics参数 2 éve
  luoshi 075395d97c 扩展data-mybatis-plus增加数据库初始化服务,应用实现指定接口并注册到容器即可完成自动初始化 2 éve
  luoshi 547ccc1224 修改core-cache默认缓存策略;增加starter-api鉴权后的日志设置;恢复鉴权接口的hasPermission方法保持向前兼容 2 éve
  luoshi 7b476aa1c3 tools-common增加HtmlToPdf转换工具类,支持自动调用安装包内置的mkhtmltopdf工具 2 éve
  luoshi 74dac2bdd5 starter-api增加接口切面与请求详情日志记录 2 éve
  luoshi c5956ee828 升级版本号至1.0.4;修改core-solar核心对象结构 2 éve
  luoshi 2c9a1fe3e4 扩展core-retrofit增加retry参数status-code,默认配置408,503,504均需要重试,可以通过配置文件自定义修改 2 éve
  luoshi 4af26e359a 扩展core-security的AuthorizationComponent注解,prefix属性支持数组,同时注册为多个uri前缀的鉴权服务 2 éve
  luoshi 8a25950901 扩展AccessEntity增加getLogName方法,方便应用扩展修改日志中的CALLER信息记录内容 2 éve
  luoshi 51db088463 修改data-mybatis-plus组件,增加配置项com.qmth.mybatis-plus.block-attack,默认为true开启防全表更新/删除插件 2 éve
  luoshi a61b57ad2d 修改data-mybatis-plus组件,增加配置项com.qmth.mybatis.block-attack,默认为true开启防全表更新/删除插件 2 éve
  luoshi b7a3821440 去掉core-fss中server配置项非空要求,没有配置时默认为空白字符串,满足本地部署情况下服务IP不固定要求 2 éve
  luoshi 90e894d3d6 增加tools-poi组件中按String[]导出excel的方法;去掉DataMap中有歧义的valus属性;增加单对象迭代器工具 2 éve
  luoshi a7fa558c12 修复PageListIterator的bug 2 éve
  luoshi e51a92dd4e bugfix 2 éve
  luoshi 6866d3ad40 修正Retrofit异常情况下body内容解析问题;增加core-sms启动日志输出 2 éve
  luoshi c954fb3602 扩展core-fss,为FileStore增加delete方法,以及两种存储模式的实现 2 éve
  luoshi 82215dfd8b 增加core-sms组建,提供统一的短信服务调用接口与配置方法 2 éve
  luoshi ee048029b4 修改core-solar中微信服务接口,获取访问令牌额外返回失效时间戳 2 éve
  luoshi ccbc97a392 扩展ExcelReader,在解析为对象时,按照定义注解验证表头,有非空列名缺失时抛出异常并返回缺少的列名 2 éve
  luoshi 068b66b615 扩展ExcelReader,增加单独获取列名的方法 2 éve
  luoshi 8a01120691 扩展core-solar将微信服务接口正式加入到SolarService中 2 éve
  luoshi 6047a679ae 注释core-fss中OssStore单元测试代码;去掉ZipReader中的main函数;core-solar中增加对于微信平台访问的接口,避免密钥信息的泄漏 2 éve
  luoshi f334c14394 增加ZIP工具类指定字符集构造方法;强制指定ZIP格式路径分隔符 2 éve
  luoshi 67bb52b15c 增加ZIP格式文件的读取工具,支持遍历文件/目录,支持指定路径文件内容读取 2 éve
  luoshi 4be8910766 修改IOUtils中纯文件拷贝的实现方式,并附带测试代码 3 éve
  luoshi cb9cf5e492 修改DiskStore中校验写入md5的方法,改为重复计算一次临时文件md5值 3 éve
  luoshi 60c63458b5 修改ZipWriter接口,删除暂时无用的直接输出到byte[]的创建方法 3 éve
  luoshi 7e11df71e2 修改ZipWriter接口,去掉@NotNull注释,增加防空判断,避免打包异常 3 éve
  luoshi e3a19fc78b 修改ZipWriter接口,强制添加本地文件时,必须提供ZIP内部文件名,不再默认取本地文件名 3 éve
  luoshi 659b3ad913 扩展tools-common,提供通用的ZIP格式写入工具ZipWriter 3 éve
  luoshi 41b86c017a 修改datasource默认配置项;增加通用ForbiddenException与API层的异常解析处理 3 éve
  luoshi af2946bf12 修改data-redis,修改RSemaphore默认值设置 3 éve
  luoshi f741851544 扩展core-concurrent的信号量工具,增加判断是否可用的方法 3 éve
  luoshi 6c66c97ae5 修改core-retrofit的ObjectMapper配置,兼容不存在的属性 3 éve
  luoshi 9e9de48fdb 修改core-retrofit的ObjectMapper配置,兼容不存在的属性 3 éve
  luoshi 305567a6ca 增加通用的找不到资源异常,并在api层按照404状态码进行特殊处理 3 éve
  luoshi de779cc0fa 扩展core-concurrent,增加单一许可的信号量获取接口,并兼容JUC和redisson两套实现方案 3 éve
  luoshi b1b4c9c3d7 扩展starter-api增加通用的ParameterException异常类与框架处理逻辑 3 éve
  luoshi e0c0b9f1ef 扩展core-retrofit,增加FormData文件上传辅助构造工具 3 éve
  luoshi b4de19860b 扩展core-retrofit,增加转换器兼容直接返回string等基本类型的接口定义 3 éve
  luoshi 3bdeb4f477 扩展core-retrofit,除注解配置方法外,增加单纯由参数控制的动态鉴权信息 3 éve
  luoshi de8f1b7d4a 模型增加通用鉴权异常,并在api层附带特殊处理状态码 3 éve
  luoshi 10fbae86a8 扩展core-retrofit增加基于ThreadLocal的动态签名信息容器 3 éve
  luoshi e96dba6342 扩展ByteArray根据URL初始化的方法,支持https协议 3 éve
  luoshi 6369b7f25a 增加ByteArray初始化方法,支持输入URL并自动下载 3 éve
  luoshi 7cc161a1f7 增加通用的分页查询结果类,以及mybatis-plus模块中BaseQuery的转换方法 3 éve
  luoshi 228dea2118 修改鉴权框架中,自动注入到RequestAttribute中的访问对象名,由原来的固定名称accessEntity(依然保留但是标记废弃)改为自动获取AccessEntity实现类的名称并首字母转小写 3 éve
  luoshi 3ea12e05cf 修改FileStore接口getTemporaryUrl重命名为getPresignedUrl 3 éve
  luoshi 1c2d1d603f 修改@RetrofitClient中的directReturn配置,默认值改为true,默认接口直接返回数据对象 3 éve
  luoshi 82280f5d1b 修改版本号为1.0.3 3 éve
  luoshi 186e3eed4a 修改ExcelColumn注解中index的注释说明,避免歧义 3 éve
  luoshi 05aa679669 修改DeviceInfo的id生成规则,过滤mac地址中长度不为17的非ipv4地址,忽略所有存储设备标识 3 éve
  luoshi 91af130d50 修改solar中的应用类型名称 3 éve
100 módosított fájl, 1804 hozzáadás és 616 törlés
  1. 12 8
      core-ai/pom.xml
  2. 64 0
      core-ai/src/main/java/com/qmth/boot/core/ai/client/LlmApiClient.java
  3. 29 0
      core-ai/src/main/java/com/qmth/boot/core/ai/client/OcrApiClient.java
  4. 22 0
      core-ai/src/main/java/com/qmth/boot/core/ai/config/AiAutoConfiguration.java
  5. 26 0
      core-ai/src/main/java/com/qmth/boot/core/ai/config/LlmApiConfiguration.java
  6. 28 0
      core-ai/src/main/java/com/qmth/boot/core/ai/config/LlmProperties.java
  7. 26 0
      core-ai/src/main/java/com/qmth/boot/core/ai/config/OcrApiConfiguration.java
  8. 28 0
      core-ai/src/main/java/com/qmth/boot/core/ai/config/OcrProperties.java
  9. 18 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/AiConstants.java
  10. 96 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/AutoScoreRequest.java
  11. 24 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/AutoScoreResult.java
  12. 17 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatChoice.java
  13. 27 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatMessage.java
  14. 31 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatRequest.java
  15. 27 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatResult.java
  16. 6 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatRole.java
  17. 36 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/LlmAppBalance.java
  18. 26 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/LlmAppType.java
  19. 33 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/PromptTemplate.java
  20. 33 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/StandardAnswer.java
  21. 26 0
      core-ai/src/main/java/com/qmth/boot/core/ai/model/ocr/OcrType.java
  22. 106 0
      core-ai/src/main/java/com/qmth/boot/core/ai/service/AiService.java
  23. 1 1
      core-ai/src/main/resources/META-INF/spring.factories
  24. 2 2
      core-cache/pom.xml
  25. 3 3
      core-cache/src/main/java/com/qmth/boot/core/cache/config/CacheAutoConfiguration.java
  26. 1 1
      core-concurrent/pom.xml
  27. 1 1
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/ConcurrentAutoConfiguration.java
  28. 18 6
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/LockAspectConfiguration.java
  29. 24 0
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/LockProperties.java
  30. 15 0
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/model/Semaphore.java
  31. 31 0
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/model/StackLock.java
  32. 10 0
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/service/ConcurrentService.java
  33. 41 0
      core-concurrent/src/main/java/com/qmth/boot/core/concurrent/service/impl/MemoryConcurrentService.java
  34. 1 1
      core-fss/pom.xml
  35. 0 1
      core-fss/src/main/java/com/qmth/boot/core/fss/config/FileStoreProperty.java
  36. 36 1
      core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java
  37. 30 7
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/DiskStore.java
  38. 15 3
      core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java
  39. 2 1
      core-fss/src/main/java/com/qmth/boot/core/fss/utils/FileStoreBuilder.java
  40. 2 3
      core-fss/src/test/java/com/qmth/boot/test/core/fss/OssStoreTest.java
  41. 1 1
      core-logging/pom.xml
  42. 1 1
      core-logging/src/main/java/com/qmth/boot/core/logger/config/LoggerAutoConfiguration.java
  43. 1 1
      core-logging/src/main/java/com/qmth/boot/core/logger/context/LoggerContextService.java
  44. 0 19
      core-metrics/src/main/java/com/qmth/boot/core/metrics/config/MetricsAutoConfiguration.java
  45. 0 38
      core-metrics/src/main/java/com/qmth/boot/core/metrics/config/MetricsProperties.java
  46. 0 39
      core-metrics/src/main/java/com/qmth/boot/core/metrics/model/MetricsResult.java
  47. 0 54
      core-metrics/src/main/java/com/qmth/boot/core/metrics/model/TimeRange.java
  48. 0 47
      core-metrics/src/main/java/com/qmth/boot/core/metrics/service/MetricsService.java
  49. 0 65
      core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/MemoryMetricsService.java
  50. 0 60
      core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/MetricsRecorder.java
  51. 0 35
      core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/TimeCounter.java
  52. 0 49
      core-metrics/src/test/java/com/qmth/boot/test/core/metrics/MetricsTest.java
  53. 1 1
      core-models/pom.xml
  54. 59 0
      core-models/src/main/java/com/qmth/boot/core/collection/PageResult.java
  55. 10 0
      core-models/src/main/java/com/qmth/boot/core/exception/CodeNameException.java
  56. 48 0
      core-models/src/main/java/com/qmth/boot/core/exception/ForbiddenException.java
  57. 59 0
      core-models/src/main/java/com/qmth/boot/core/exception/NotFoundException.java
  58. 59 0
      core-models/src/main/java/com/qmth/boot/core/exception/ParameterException.java
  59. 1 1
      core-models/src/main/java/com/qmth/boot/core/exception/ReentrantException.java
  60. 1 1
      core-models/src/main/java/com/qmth/boot/core/exception/StatusException.java
  61. 48 0
      core-models/src/main/java/com/qmth/boot/core/exception/UnauthorizedException.java
  62. 1 1
      core-rate-limit/pom.xml
  63. 45 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/annotation/RateLimit.java
  64. 1 1
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/config/RateLimitAutoConfiguration.java
  65. 7 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitRule.java
  66. 6 2
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitScope.java
  67. 9 2
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitTarget.java
  68. 0 18
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitPolicy.java
  69. 4 5
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitService.java
  70. 16 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimiter.java
  71. 15 49
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimitService.java
  72. 49 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimiter.java
  73. 0 38
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/RateLimitor.java
  74. 6 3
      core-rate-limit/src/test/java/com/qmth/boot/test/core/rateLimit/RateLimitTest.java
  75. 13 1
      core-retrofit/pom.xml
  76. 3 3
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/annotatioin/RetrofitClient.java
  77. 7 1
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetrofitAutoConfiguration.java
  78. 2 2
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetrofitProperties.java
  79. 14 0
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetryProperties.java
  80. 38 7
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/core/RetrofitFactoryBean.java
  81. 6 8
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/ErrorDecodeInterceptor.java
  82. 1 1
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/RetryInterceptor.java
  83. 25 7
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/SignatureInterceptor.java
  84. 59 0
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/utils/SignatureInfo.java
  85. 38 0
      core-retrofit/src/main/java/com/qmth/boot/core/retrofit/utils/UploadFile.java
  86. 1 1
      core-schedule/pom.xml
  87. 1 1
      core-schedule/src/main/java/com/qmth/boot/core/schedule/config/ScheduleAutoConfiguration.java
  88. 5 1
      core-security/pom.xml
  89. 1 1
      core-security/src/main/java/com/qmth/boot/core/security/annotation/AuthorizationComponent.java
  90. 30 1
      core-security/src/main/java/com/qmth/boot/core/security/config/SecurityAutoConfiguration.java
  91. 8 7
      core-security/src/main/java/com/qmth/boot/core/security/config/SecurityContextListener.java
  92. 13 0
      core-security/src/main/java/com/qmth/boot/core/security/config/SecurityProperties.java
  93. 1 1
      core-security/src/main/java/com/qmth/boot/core/security/exception/AuthorizationException.java
  94. 9 0
      core-security/src/main/java/com/qmth/boot/core/security/model/AccessEntity.java
  95. 3 1
      core-security/src/main/java/com/qmth/boot/core/security/service/AuthorizationService.java
  96. 14 0
      core-security/src/main/java/com/qmth/boot/core/security/service/EncryptKeyProvider.java
  97. 31 0
      core-security/src/main/java/com/qmth/boot/core/security/service/EncryptService.java
  98. 2 2
      core-security/src/main/java/com/qmth/boot/core/security/service/impl/BaseAuthorizationSupport.java
  99. 16 0
      core-security/src/main/java/com/qmth/boot/core/security/service/impl/DefaultEncryptKeyProvider.java
  100. 41 0
      core-security/src/main/java/com/qmth/boot/core/security/service/impl/DefaultEncryptService.java

+ 12 - 8
core-metrics/pom.xml → core-ai/pom.xml

@@ -1,20 +1,24 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
-        <groupId>com.qmth.boot</groupId>
         <artifactId>qmth-boot</artifactId>
-        <version>1.0.2</version>
+        <groupId>com.qmth.boot</groupId>
+        <version>1.0.4</version>
     </parent>
-    <artifactId>core-metrics</artifactId>
-    <name>core-metrics</name>
+    <artifactId>core-ai</artifactId>
+    <name>core-ai</name>
 
     <dependencies>
         <dependency>
             <groupId>com.qmth.boot</groupId>
-            <artifactId>core-models</artifactId>
+            <artifactId>tools-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-solar</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -35,4 +39,4 @@
         </dependency>
     </dependencies>
 
-</project>
+</project>

+ 64 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/client/LlmApiClient.java

@@ -0,0 +1,64 @@
+package com.qmth.boot.core.ai.client;
+
+import com.qmth.boot.core.ai.config.LlmApiConfiguration;
+import com.qmth.boot.core.ai.model.AiConstants;
+import com.qmth.boot.core.ai.model.llm.*;
+import com.qmth.boot.core.retrofit.annotatioin.RetrofitClient;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
+import retrofit2.http.Body;
+import retrofit2.http.Header;
+import retrofit2.http.POST;
+import retrofit2.http.Tag;
+
+/**
+ * 大模型应用服务接口
+ */
+@RetrofitClient(configuration = LlmApiConfiguration.class)
+public interface LlmApiClient {
+
+    /**
+     * 大模型chat类型请求
+     *
+     * @param signature 使用机构AK构造Secret类型签名
+     * @param type      大模型应用类型
+     * @param request   标准chat请求对象
+     * @return
+     */
+    @POST(AiConstants.LLM_CHAT_PATH)
+    ChatResult chat(@Tag SignatureInfo signature, @Header(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type,
+            @Body ChatRequest request);
+
+    /**
+     * 基于Prompt模版的大模型chat类型请求
+     *
+     * @param signature 使用机构AK构造Secret类型签名
+     * @param type      大模型应用类型
+     * @param param     模版变量
+     * @return
+     */
+    @POST(AiConstants.LLM_CHAT_TEMPLATE_PATH)
+    ChatResult chatTemplate(@Tag SignatureInfo signature, @Header(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type,
+            @Body Object param);
+
+    /**
+     * 大模型接口余额查询
+     *
+     * @param signature 使用机构AK构造Secret类型签名
+     * @param type      大模型应用类型
+     * @return
+     */
+    @POST(AiConstants.LLM_BALANCE_PATH)
+    LlmAppBalance getBalance(@Tag SignatureInfo signature, @Header(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type);
+
+    /**
+     * 大模型提示词模版获取
+     *
+     * @param signature 使用机构AK构造Secret类型签名
+     * @param type      大模型应用类型
+     * @return
+     */
+    @POST(AiConstants.LLM_PROMPT_TEMPLATE_PATH)
+    PromptTemplate getPromptTemplate(@Tag SignatureInfo signature,
+            @Header(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type);
+
+}

+ 29 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/client/OcrApiClient.java

@@ -0,0 +1,29 @@
+package com.qmth.boot.core.ai.client;
+
+import com.qmth.boot.core.ai.config.OcrApiConfiguration;
+import com.qmth.boot.core.ai.model.AiConstants;
+import com.qmth.boot.core.ai.model.ocr.OcrType;
+import com.qmth.boot.core.retrofit.annotatioin.RetrofitClient;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
+import okhttp3.MultipartBody;
+import retrofit2.http.*;
+
+/**
+ * OCR应用服务接口
+ */
+@RetrofitClient(configuration = OcrApiConfiguration.class)
+public interface OcrApiClient {
+
+    /**
+     * OCR识别图片
+     *
+     * @param signature 使用机构AK构造Secret类型签名
+     * @param type      识别类型
+     * @param image     待识别图片
+     * @return
+     */
+    @Multipart
+    @POST(AiConstants.OCR_IMAGE_PATH)
+    String forImage(@Tag SignatureInfo signature, @Query("type") OcrType type, @Part MultipartBody.Part image);
+
+}

+ 22 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/config/AiAutoConfiguration.java

@@ -0,0 +1,22 @@
+package com.qmth.boot.core.ai.config;
+
+import com.qmth.boot.core.retrofit.annotatioin.RetrofitScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan("com.qmth.boot.core.ai")
+@RetrofitScan("com.qmth.boot.core.ai.client")
+public class AiAutoConfiguration {
+
+    @Bean
+    public LlmApiConfiguration llmApiConfiguration(LlmProperties llmProperties) {
+        return new LlmApiConfiguration(llmProperties);
+    }
+
+    @Bean
+    public OcrApiConfiguration ocrApiConfiguration(OcrProperties ocrProperties) {
+        return new OcrApiConfiguration(ocrProperties);
+    }
+}

+ 26 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/config/LlmApiConfiguration.java

@@ -0,0 +1,26 @@
+package com.qmth.boot.core.ai.config;
+
+import com.qmth.boot.core.retrofit.interfaces.CustomizeRetrofitConfiguration;
+import com.qmth.boot.core.retrofit.interfaces.SignatureProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LlmApiConfiguration implements CustomizeRetrofitConfiguration {
+
+    private static final Logger log = LoggerFactory.getLogger(LlmApiConfiguration.class);
+
+    private LlmProperties llmProperties;
+
+    public LlmApiConfiguration(LlmProperties llmProperties) {
+        this.llmProperties = llmProperties;
+        log.info("LLM api configuration inited, server=" + llmProperties.getServer());
+    }
+
+    public String getBaseUrl() {
+        return llmProperties.getServer();
+    }
+
+    public SignatureProvider getSignature() {
+        return null;
+    }
+}

+ 28 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/config/LlmProperties.java

@@ -0,0 +1,28 @@
+package com.qmth.boot.core.ai.config;
+
+import com.qmth.boot.core.constant.CoreConstant;
+import com.qmth.boot.core.solar.model.SolarConstants;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+
+@Component
+@ConfigurationProperties(prefix = CoreConstant.CONFIG_PREFIX + ".llm")
+public class LlmProperties {
+
+    /**
+     * 默认直接访问solar地址
+     */
+    @NotNull
+    private String server = SolarConstants.DEFAULT_SERVER;
+
+    public String getServer() {
+        return server;
+    }
+
+    public void setServer(String server) {
+        this.server = server;
+    }
+
+}

+ 26 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/config/OcrApiConfiguration.java

@@ -0,0 +1,26 @@
+package com.qmth.boot.core.ai.config;
+
+import com.qmth.boot.core.retrofit.interfaces.CustomizeRetrofitConfiguration;
+import com.qmth.boot.core.retrofit.interfaces.SignatureProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OcrApiConfiguration implements CustomizeRetrofitConfiguration {
+
+    private static final Logger log = LoggerFactory.getLogger(OcrApiConfiguration.class);
+
+    private OcrProperties ocrProperties;
+
+    public OcrApiConfiguration(OcrProperties ocrProperties) {
+        this.ocrProperties = ocrProperties;
+        log.info("OCR api configuration inited, server=" + ocrProperties.getServer());
+    }
+
+    public String getBaseUrl() {
+        return ocrProperties.getServer();
+    }
+
+    public SignatureProvider getSignature() {
+        return null;
+    }
+}

+ 28 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/config/OcrProperties.java

@@ -0,0 +1,28 @@
+package com.qmth.boot.core.ai.config;
+
+import com.qmth.boot.core.constant.CoreConstant;
+import com.qmth.boot.core.solar.model.SolarConstants;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+
+@Component
+@ConfigurationProperties(prefix = CoreConstant.CONFIG_PREFIX + ".ocr")
+public class OcrProperties {
+
+    /**
+     * 默认直接访问solar地址
+     */
+    @NotNull
+    private String server = SolarConstants.DEFAULT_SERVER;
+
+    public String getServer() {
+        return server;
+    }
+
+    public void setServer(String server) {
+        this.server = server;
+    }
+
+}

+ 18 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/AiConstants.java

@@ -0,0 +1,18 @@
+package com.qmth.boot.core.ai.model;
+
+public interface AiConstants {
+
+    String LLM_APP_TYPE_HEADER = "llm_app_type";
+
+    String API_PREFIX = "/api/ai";
+
+    String LLM_BALANCE_PATH = API_PREFIX + "/llm/balance";
+
+    String LLM_CHAT_PATH = API_PREFIX + "/llm/chat";
+
+    String LLM_CHAT_TEMPLATE_PATH = API_PREFIX + "/llm/chat_template";
+
+    String LLM_PROMPT_TEMPLATE_PATH = API_PREFIX + "/llm/prompt_template";
+
+    String OCR_IMAGE_PATH = API_PREFIX + "/ocr/image";
+}

+ 96 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/AutoScoreRequest.java

@@ -0,0 +1,96 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 自动判分请求参数
+ */
+@Validated
+public class AutoScoreRequest {
+
+    @NotBlank(message = "科目名称不能为空")
+    private String subjectName;
+
+    @NotBlank(message = "试题内容不能为空")
+    private String questionBody;
+
+    @NotNull(message = "标答不能为空")
+    @Size(min = 1, message = "标答不能为空")
+    @Valid
+    private List<StandardAnswer> standardAnswer = new ArrayList<>();
+
+    @NotNull(message = "考生回答不能为空")
+    private String studentAnswer;
+
+    private double totalScore;
+
+    private double intervalScore = 1;
+
+    public String getSubjectName() {
+        return subjectName;
+    }
+
+    public void setSubjectName(String subjectName) {
+        this.subjectName = subjectName;
+    }
+
+    public String getQuestionBody() {
+        return questionBody;
+    }
+
+    public void setQuestionBody(String questionBody) {
+        this.questionBody = questionBody;
+    }
+
+    public List<StandardAnswer> getStandardAnswer() {
+        return standardAnswer;
+    }
+
+    public void setStandardAnswer(List<StandardAnswer> standardAnswer) {
+        this.standardAnswer = standardAnswer;
+    }
+
+    /**
+     * 增加标答内容及分值
+     *
+     * @param content 文本内容
+     * @param score   格式化后的分数字符串
+     */
+    public void addStandardAnswer(@NotNull String content, @NotNull double score) {
+        StandardAnswer answer = new StandardAnswer();
+        answer.setContent(content);
+        answer.setScore(score);
+        this.standardAnswer.add(answer);
+    }
+
+    public String getStudentAnswer() {
+        return studentAnswer;
+    }
+
+    public void setStudentAnswer(String studentAnswer) {
+        this.studentAnswer = studentAnswer;
+    }
+
+    public double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public double getIntervalScore() {
+        return intervalScore;
+    }
+
+    public void setIntervalScore(double intervalScore) {
+        this.intervalScore = intervalScore;
+    }
+}

+ 24 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/AutoScoreResult.java

@@ -0,0 +1,24 @@
+package com.qmth.boot.core.ai.model.llm;
+
+public class AutoScoreResult {
+
+    private double totalScore;
+
+    private double[] stepScore;
+
+    public double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public double[] getStepScore() {
+        return stepScore;
+    }
+
+    public void setStepScore(double[] stepScore) {
+        this.stepScore = stepScore;
+    }
+}

+ 17 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatChoice.java

@@ -0,0 +1,17 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ChatChoice {
+
+    private ChatMessage message;
+
+    public ChatMessage getMessage() {
+        return message;
+    }
+
+    public void setMessage(ChatMessage message) {
+        this.message = message;
+    }
+}

+ 27 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatMessage.java

@@ -0,0 +1,27 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ChatMessage {
+
+    private ChatRole role;
+
+    private String content;
+
+    public ChatRole getRole() {
+        return role;
+    }
+
+    public void setRole(ChatRole role) {
+        this.role = role;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 31 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatRequest.java

@@ -0,0 +1,31 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 大模型通用chat请求
+ */
+public class ChatRequest {
+
+    private List<ChatMessage> messages;
+
+    public ChatRequest() {
+        this.messages = new LinkedList<>();
+    }
+
+    public void addMessage(ChatRole role, String content) {
+        ChatMessage message = new ChatMessage();
+        message.setRole(role);
+        message.setContent(content);
+        this.messages.add(message);
+    }
+
+    public List<ChatMessage> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<ChatMessage> messages) {
+        this.messages = messages;
+    }
+}

+ 27 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatResult.java

@@ -0,0 +1,27 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 大模型通用chat返回结果
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ChatResult {
+
+    private List<ChatChoice> choices;
+
+    public ChatResult() {
+        this.choices = new LinkedList<>();
+    }
+
+    public List<ChatChoice> getChoices() {
+        return choices;
+    }
+
+    public void setChoices(List<ChatChoice> choices) {
+        this.choices = choices;
+    }
+}

+ 6 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/ChatRole.java

@@ -0,0 +1,6 @@
+package com.qmth.boot.core.ai.model.llm;
+
+public enum ChatRole {
+
+    system, user, assistant;
+}

+ 36 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/LlmAppBalance.java

@@ -0,0 +1,36 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * 大模型接口余额
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class LlmAppBalance {
+
+    /**
+     * 累计许可次数
+     */
+    private int permitCount;
+
+    /**
+     * 剩余可用次数
+     */
+    private int leftCount;
+
+    public int getPermitCount() {
+        return permitCount;
+    }
+
+    public void setPermitCount(int permitCount) {
+        this.permitCount = permitCount;
+    }
+
+    public int getLeftCount() {
+        return leftCount;
+    }
+
+    public void setLeftCount(int leftCount) {
+        this.leftCount = leftCount;
+    }
+}

+ 26 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/LlmAppType.java

@@ -0,0 +1,26 @@
+package com.qmth.boot.core.ai.model.llm;
+
+/**
+ * 大模型应用类型
+ */
+public enum LlmAppType {
+
+    /**
+     * 自动命题
+     */
+    AUTO_GENERATE_QUESTION,
+
+    /**
+     * 自动评分
+     */
+    AUTO_SCORE;
+
+    public static LlmAppType find(String text) {
+        for (LlmAppType type : values()) {
+            if (type.toString().equalsIgnoreCase(text)) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 33 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/PromptTemplate.java

@@ -0,0 +1,33 @@
+package com.qmth.boot.core.ai.model.llm;
+
+/**
+ * 提示词模版
+ */
+public class PromptTemplate {
+
+    /**
+     * system角色提示词模版
+     */
+    private String system;
+
+    /**
+     * user角色提示词模版
+     */
+    private String user;
+
+    public String getSystem() {
+        return system;
+    }
+
+    public void setSystem(String system) {
+        this.system = system;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+}

+ 33 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/llm/StandardAnswer.java

@@ -0,0 +1,33 @@
+package com.qmth.boot.core.ai.model.llm;
+
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 自动评分应用中按得分点构造的标答
+ */
+@Validated
+public class StandardAnswer {
+
+    @NotNull(message = "标答不能为空")
+    private String content;
+
+    private double score;
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public double getScore() {
+        return score;
+    }
+
+    public void setScore(double score) {
+        this.score = score;
+    }
+}

+ 26 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/model/ocr/OcrType.java

@@ -0,0 +1,26 @@
+package com.qmth.boot.core.ai.model.ocr;
+
+/**
+ * OCR识别类型
+ */
+public enum OcrType {
+
+    /**
+     * 通用文字识别
+     */
+    GENERAL,
+
+    /**
+     * 手写文字识别
+     */
+    HANDWRITING;
+
+    public static OcrType find(String text) {
+        for (OcrType type : values()) {
+            if (type.toString().equalsIgnoreCase(text)) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 106 - 0
core-ai/src/main/java/com/qmth/boot/core/ai/service/AiService.java

@@ -0,0 +1,106 @@
+package com.qmth.boot.core.ai.service;
+
+import com.qmth.boot.core.ai.client.LlmApiClient;
+import com.qmth.boot.core.ai.model.llm.*;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * AI相关功能服务
+ */
+@Service
+public class AiService {
+
+    private static final Pattern score_pattern = Pattern.compile("[^\\d]*(\\d+)[^\\d]*");
+
+    @Resource
+    private LlmApiClient llmApiClient;
+
+    /**
+     * 获取当前机构大模型接口调用次数余额
+     *
+     * @param appType   大模型接口应用类型
+     * @param signature 使用当前机构AK作为鉴权信息
+     * @return 调用次数余额
+     */
+    public LlmAppBalance getLlmBalance(@NotNull LlmAppType appType, @NotNull SignatureInfo signature) {
+        return llmApiClient.getBalance(signature, appType);
+    }
+
+    /**
+     * 使用自定义提示词进行自动命题
+     *
+     * @param request   大模型通用Chat请求
+     * @param signature 使用当前机构AK作为鉴权信息
+     * @return 大模型返回的文本集合
+     */
+    public List<String> autoGenerateQuestion(@NotNull ChatRequest request, @NotNull SignatureInfo signature) {
+        ChatResult result = llmApiClient.chat(signature, LlmAppType.AUTO_GENERATE_QUESTION, request);
+        return result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).collect(Collectors.toList());
+    }
+
+    /**
+     * 使用预设模版进行自动命题
+     *
+     * @param param     基于预设模版的参数对象,可以使用Map或自定义Object
+     * @param signature 使用当前机构AK作为鉴权信息
+     * @return 大模型返回的文本集合
+     */
+    public List<String> autoGenerateQuestion(@NotNull Object param, @NotNull SignatureInfo signature) {
+        ChatResult result = llmApiClient.chatTemplate(signature, LlmAppType.AUTO_GENERATE_QUESTION, param);
+        return result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).collect(Collectors.toList());
+    }
+
+    /**
+     * 使用预设模版进行自动判分,考生作答为文本
+     *
+     * @param request   自动判分请求参数
+     * @param signature 使用当前机构AK作为鉴权信息
+     * @return 得分率,保留最多三位小数;null表示无法获取判分结果
+     */
+    public AutoScoreResult autoScore(@NotNull @Validated AutoScoreRequest request, @NotNull SignatureInfo signature) {
+        ChatResult result = llmApiClient.chatTemplate(signature, LlmAppType.AUTO_SCORE, request);
+        String text = result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).findFirst().orElse("");
+        try {
+            AutoScoreResult scoreResult = new AutoScoreResult();
+            //依据总分与步骤分计算最大精度
+            int scale = Math
+                    .max(getDecimalPlaces(request.getIntervalScore()), getDecimalPlaces(request.getTotalScore()));
+            int stepCount = request.getStandardAnswer().size();
+            String[] scores = StringUtils.split(text.replaceAll(",", ","), ",");
+            double[] scoreArray = new double[stepCount];
+            for (int i = 0; i < stepCount; i++) {
+                //根据得分率与步骤总分计算实际得分,按最大精度保留小数位数
+                double score = BigDecimal.valueOf(
+                        Math.min(Integer.parseInt(scores[i].trim()), 100) * request.getStandardAnswer().get(i)
+                                .getScore()).divide(BigDecimal.valueOf(100), scale, RoundingMode.HALF_UP).doubleValue();
+                scoreArray[i] = score;
+            }
+            scoreResult.setStepScore(scoreArray);
+            scoreResult.setTotalScore(
+                    Arrays.stream(scoreArray).mapToObj(BigDecimal::new).reduce(BigDecimal.ZERO, BigDecimal::add)
+                            .doubleValue());
+            return scoreResult;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static int getDecimalPlaces(double value) {
+        return Math.max(0, BigDecimal.valueOf(value).stripTrailingZeros().scale());
+    }
+}

+ 1 - 1
core-metrics/src/main/resources/META-INF/spring.factories → core-ai/src/main/resources/META-INF/spring.factories

@@ -1,2 +1,2 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-  com.qmth.boot.core.metrics.config.MetricsAutoConfiguration
+  com.qmth.boot.core.ai.config.AiAutoConfiguration

+ 2 - 2
core-cache/pom.xml

@@ -6,10 +6,10 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-cache</artifactId>
-    <name>core-uid</name>
+    <name>core-cache</name>
 
     <dependencies>
         <dependency>

+ 3 - 3
core-cache/src/main/java/com/qmth/boot/core/cache/config/CacheAutoConfiguration.java

@@ -24,7 +24,7 @@ import java.time.Duration;
  */
 @Configuration
 @EnableCaching
-@ComponentScan("com.qmth.boot.core.cache.*")
+@ComponentScan("com.qmth.boot.core.cache")
 public class CacheAutoConfiguration {
 
     private static final Logger log = LoggerFactory.getLogger(CacheAutoConfiguration.class);
@@ -71,8 +71,8 @@ public class CacheAutoConfiguration {
     }
 
     private Caffeine<Object, Object> buildCaffeineConfig(Duration expireAfterWrite) {
-        //默认开启softValues,避免JVM撑爆
-        Caffeine<Object, Object> caffeine = Caffeine.newBuilder().softValues();
+        //默认开启softValues
+        Caffeine<Object, Object> caffeine = Caffeine.newBuilder().softValues().recordStats();
         //写入失效时长大于0时才启用
         if (expireAfterWrite.toMillis() > 0) {
             caffeine = caffeine.expireAfterWrite(expireAfterWrite);

+ 1 - 1
core-concurrent/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-concurrent</artifactId>
     <name>core-concurrent</name>

+ 1 - 1
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/ConcurrentAutoConfiguration.java

@@ -8,7 +8,7 @@ import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
-@ComponentScan("com.qmth.boot.core.concurrent.*")
+@ComponentScan("com.qmth.boot.core.concurrent")
 public class ConcurrentAutoConfiguration {
 
     @Bean

+ 18 - 6
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/LockAspectConfiguration.java

@@ -1,10 +1,13 @@
 package com.qmth.boot.core.concurrent.configuration;
 
+import ch.qos.logback.classic.Level;
 import com.qmth.boot.core.concurrent.annotation.Lockable;
 import com.qmth.boot.core.concurrent.annotation.Locks;
 import com.qmth.boot.core.concurrent.exception.TryLockFaileException;
 import com.qmth.boot.core.concurrent.model.LockType;
+import com.qmth.boot.core.concurrent.model.StackLock;
 import com.qmth.boot.core.concurrent.service.ConcurrentService;
+import com.qmth.boot.core.logger.context.LoggerContextService;
 import org.apache.commons.lang3.StringUtils;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
@@ -14,13 +17,14 @@ import org.aspectj.lang.reflect.MethodSignature;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 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.Assert;
 
-import javax.annotation.Resource;
 import java.lang.reflect.Method;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -32,15 +36,21 @@ import java.util.concurrent.locks.ReadWriteLock;
  */
 @Aspect
 @Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
 public class LockAspectConfiguration {
 
     protected static final Logger log = LoggerFactory.getLogger(LockAspectConfiguration.class);
 
     private static final String LOCK_KEY_SEPARATOR = "_";
 
-    @Resource
     private ConcurrentService concurrentService;
 
+    public LockAspectConfiguration(ConcurrentService concurrentService, LoggerContextService loggerContextService,
+            LockProperties lockProperties) {
+        this.concurrentService = concurrentService;
+        loggerContextService.setLevel(LockAspectConfiguration.class, Level.toLevel(lockProperties.getLogLevel()));
+    }
+
     /***
      * 定义切入点拦截规则,拦截Lockable注解的业务方法
      */
@@ -87,7 +97,7 @@ public class LockAspectConfiguration {
             list.add(annotation);
         }
         // 已上锁堆栈
-        Deque<Lock> stack = new ArrayDeque<>(count);
+        Deque<StackLock> stack = new ArrayDeque<>(count);
         try {
             // 按顺序循环尝试上锁
             for (Lockable item : list) {
@@ -99,7 +109,7 @@ public class LockAspectConfiguration {
                 }
                 ReadWriteLock readWriteLock = concurrentService.getReadWriteLock(key);
                 Lock lock = item.type() == LockType.READ ? readWriteLock.readLock() : readWriteLock.writeLock();
-                log.info("Start lock: name={}, type={}, timeout={}", key, item.type(), item.timeout());
+                log.debug("Start lock: key={}, type={}, timeout={}", key, item.type(), item.timeout());
                 if (item.timeout() >= 0) {
                     // 定义了超时时间为尝试上锁模式,失败则抛出异常
                     if (!lock.tryLock(item.timeout(), TimeUnit.SECONDS)) {
@@ -108,7 +118,7 @@ public class LockAspectConfiguration {
                 } else {
                     lock.lockInterruptibly();
                 }
-                stack.push(lock);
+                stack.push(new StackLock(lock, key, item.type()));
             }
             return joinPoint.proceed();
         } catch (TryLockFaileException e) {
@@ -118,7 +128,9 @@ public class LockAspectConfiguration {
             throw new RuntimeException(e);
         } finally {
             while (!stack.isEmpty()) {
-                stack.pop().unlock();
+                StackLock lock = stack.pop();
+                lock.getLock().unlock();
+                log.debug("Finish unlock: key={}, type={}", lock.getKey(), lock.getType());
             }
         }
     }

+ 24 - 0
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/configuration/LockProperties.java

@@ -0,0 +1,24 @@
+package com.qmth.boot.core.concurrent.configuration;
+
+import ch.qos.logback.classic.Level;
+import com.qmth.boot.core.constant.CoreConstant;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+
+@Component
+@ConfigurationProperties(prefix = CoreConstant.CONFIG_PREFIX + ".lock")
+public class LockProperties {
+
+    @NotNull
+    private String logLevel = Level.INFO.toString();
+
+    public String getLogLevel() {
+        return logLevel;
+    }
+
+    public void setLogLevel(String logLevel) {
+        this.logLevel = logLevel;
+    }
+}

+ 15 - 0
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/model/Semaphore.java

@@ -0,0 +1,15 @@
+package com.qmth.boot.core.concurrent.model;
+
+/**
+ * 单一许可的抽象信号量,兼容JUC和Redisson等多种实现
+ */
+public interface Semaphore {
+
+    void acquire() throws InterruptedException;
+
+    boolean tryAcquire();
+
+    void release();
+
+    boolean isAvailable();
+}

+ 31 - 0
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/model/StackLock.java

@@ -0,0 +1,31 @@
+package com.qmth.boot.core.concurrent.model;
+
+import java.util.concurrent.locks.Lock;
+
+public class StackLock {
+
+    private Lock lock;
+
+    private String key;
+
+    private LockType type;
+
+    public StackLock(Lock lock, String key, LockType type) {
+        this.lock = lock;
+        this.key = key;
+        this.type = type;
+    }
+
+    public Lock getLock() {
+        return lock;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public LockType getType() {
+        return type;
+    }
+
+}

+ 10 - 0
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/service/ConcurrentService.java

@@ -1,5 +1,7 @@
 package com.qmth.boot.core.concurrent.service;
 
+import com.qmth.boot.core.concurrent.model.Semaphore;
+
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 
@@ -48,4 +50,12 @@ public interface ConcurrentService {
      */
     boolean isWriteLocked(String name);
 
+    /**
+     * 获取抽象信号量
+     *
+     * @param name
+     * @return
+     */
+    Semaphore getSemaphore(String name);
+
 }

+ 41 - 0
core-concurrent/src/main/java/com/qmth/boot/core/concurrent/service/impl/MemoryConcurrentService.java

@@ -1,8 +1,10 @@
 package com.qmth.boot.core.concurrent.service.impl;
 
+import com.qmth.boot.core.concurrent.model.Semaphore;
 import com.qmth.boot.core.concurrent.service.ConcurrentService;
 
 import javax.annotation.PreDestroy;
+import javax.validation.constraints.NotNull;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.locks.Lock;
@@ -16,15 +18,19 @@ public class MemoryConcurrentService implements ConcurrentService {
 
     private final Map<String, ReentrantReadWriteLock> readWriteMap;
 
+    private final Map<String, Semaphore> semaphoreMap;
+
     public MemoryConcurrentService() {
         this.lockMap = new HashMap<>();
         this.readWriteMap = new HashMap<>();
+        this.semaphoreMap = new HashMap<>();
     }
 
     @PreDestroy
     public void close() {
         this.lockMap.clear();
         this.readWriteMap.clear();
+        this.semaphoreMap.clear();
     }
 
     private ReentrantLock lock(String name) {
@@ -71,4 +77,39 @@ public class MemoryConcurrentService implements ConcurrentService {
     public boolean isWriteLocked(String name) {
         return readWriteLock(name).isWriteLocked();
     }
+
+    @Override
+    public Semaphore getSemaphore(@NotNull String name) {
+        Semaphore semaphore = semaphoreMap.get(name);
+        if (semaphore == null) {
+            synchronized (semaphoreMap) {
+                semaphore = semaphoreMap.computeIfAbsent(name, key -> {
+                    java.util.concurrent.Semaphore se = new java.util.concurrent.Semaphore(1, true);
+                    return new Semaphore() {
+
+                        @Override
+                        public void acquire() throws InterruptedException {
+                            se.acquire();
+                        }
+
+                        @Override
+                        public boolean tryAcquire() {
+                            return se.tryAcquire();
+                        }
+
+                        @Override
+                        public void release() {
+                            se.release();
+                        }
+
+                        @Override
+                        public boolean isAvailable() {
+                            return se.availablePermits() > 0;
+                        }
+                    };
+                });
+            }
+        }
+        return semaphore;
+    }
 }

+ 1 - 1
core-fss/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-fss</artifactId>
     <name>core-fss</name>

+ 0 - 1
core-fss/src/main/java/com/qmth/boot/core/fss/config/FileStoreProperty.java

@@ -10,7 +10,6 @@ public class FileStoreProperty {
     @NotNull
     private String config;
 
-    @NotNull
     private String server;
 
     public String getConfig() {

+ 36 - 1
core-fss/src/main/java/com/qmth/boot/core/fss/store/FileStore.java

@@ -18,6 +18,27 @@ public interface FileStore {
     default void close() {
     }
 
+    /**
+     * 将md5内容转换为hexString格式
+     *
+     * @param md5
+     * @return
+     */
+    default String toHexString(String md5) {
+        if (StringUtils.isBlank(md5)) {
+            throw new IllegalArgumentException("Invalid md5 parameter");
+        }
+        //hex格式
+        if (md5.length() == 32) {
+            return md5;
+        }
+        //base64格式
+        else if (md5.length() == 24) {
+            return ByteArray.fromBase64(md5).toHexString();
+        }
+        throw new IllegalArgumentException("Invalid md5 length");
+    }
+
     /**
      * 将md5内容转换为base64格式
      *
@@ -63,7 +84,7 @@ public interface FileStore {
      * @param expireDuration
      * @return
      */
-    default String getTemporaryUrl(String path, Duration expireDuration) {
+    default String getPresignedUrl(String path, Duration expireDuration) {
         return getServer().concat(formatPath(path));
     }
 
@@ -101,4 +122,18 @@ public interface FileStore {
      */
     boolean exist(String path);
 
+    /**
+     * 删除已存在的文件
+     *
+     * @param path
+     */
+    boolean delete(String path);
+
+    /**
+     * 将指定路径的文件复制到目标路径中
+     *
+     * @param source
+     * @param target
+     */
+    void copy(String source, String target) throws Exception;
 }

+ 30 - 7
core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/DiskStore.java

@@ -1,15 +1,15 @@
 package com.qmth.boot.core.fss.store.impl;
 
+import com.qmth.boot.core.exception.StatusException;
 import com.qmth.boot.core.fss.store.FileStore;
-import com.qmth.boot.tools.codec.CodecUtils;
 import com.qmth.boot.tools.io.IOUtils;
+import com.qmth.boot.tools.models.ByteArray;
 import com.qmth.boot.tools.uuid.FastUUID;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.InputStream;
-import java.security.MessageDigest;
 
 /**
  * 本地磁盘文件管理工具
@@ -53,15 +53,20 @@ public class DiskStore implements FileStore {
         //临时目录创建临时文件
         File tempFile = new File(tempDir, FastUUID.get());
         try {
-            final MessageDigest digest = CodecUtils.getMd5();
+            //final MessageDigest digest = CodecUtils.getMd5();
             FileOutputStream ous = new FileOutputStream(tempFile);
-            IOUtils.copy(ins, ous, (buffer, length) -> digest.update(buffer, 0, length));
+            IOUtils.copy(ins, ous);
+            //IOUtils.copy(ins, ous, (buffer, length) -> digest.update(buffer, 0, length));
             //校验写入内容的摘要信息
-            if (!CodecUtils.toBase64(digest.digest()).equalsIgnoreCase(toBase64(md5))) {
-                throw new RuntimeException("Write md5 validate faile");
-            }
+            //if (!CodecUtils.toBase64(digest.digest()).equalsIgnoreCase(toBase64(md5))) {
+            //throw new RuntimeException("Write md5 validate faile");
+            //}
             IOUtils.closeQuietly(ins);
             IOUtils.closeQuietly(ous);
+            //校验写入内容的摘要信息
+            if (!ByteArray.md5(tempFile).toBase64().equalsIgnoreCase(toBase64(md5))) {
+                throw new RuntimeException("Write md5 validate faile");
+            }
             //检查正式文件及目录
             File targetFile = new File(rootDir, path);
             if (targetFile.isDirectory()) {
@@ -93,4 +98,22 @@ public class DiskStore implements FileStore {
         return file.exists() && file.isFile();
     }
 
+    @Override
+    public boolean delete(String path) {
+        path = formatPath(path);
+        File file = new File(rootDir, path);
+        if (file.exists() && file.isFile()) {
+            return file.delete();
+        } else {
+            throw new StatusException(path + " not exist");
+        }
+    }
+
+    @Override
+    public void copy(String source, String target) throws Exception {
+        source = formatPath(source);
+        target = formatPath(target);
+        IOUtils.copy(new File(rootDir, source), new File(rootDir, target));
+    }
+
 }

+ 15 - 3
core-fss/src/main/java/com/qmth/boot/core/fss/store/impl/OssStore.java

@@ -71,20 +71,20 @@ public class OssStore implements FileStore {
     }
 
     @Override
-    public String getTemporaryUrl(String path, Duration expireDuration) {
+    public String getPresignedUrl(String path, Duration expireDuration) {
         return temporaryUrlClient.generatePresignedUrl(bucket, formatPath(path),
                 new Date(System.currentTimeMillis() + expireDuration.toMillis())).toString();
     }
 
     @Override
-    public void write(String path, InputStream ins, String md5) throws Exception {
+    public void write(String path, InputStream ins, String md5) {
         ObjectMetadata metadata = new ObjectMetadata();
         metadata.setContentMD5(toBase64(md5));
         ossClient.putObject(bucket, formatPath(path), ins, metadata);
     }
 
     @Override
-    public InputStream read(String path) throws Exception {
+    public InputStream read(String path) {
         OSSObject ossObject = ossClient.getObject(bucket, formatPath(path));
         return ossObject.getObjectContent();
     }
@@ -98,6 +98,17 @@ public class OssStore implements FileStore {
         }
     }
 
+    @Override
+    public boolean delete(String path) {
+        ossClient.deleteObject(bucket, formatPath(path));
+        return true;
+    }
+
+    @Override
+    public void copy(String source, String target) throws Exception {
+        ossClient.copyObject(bucket, formatPath(source), bucket, formatPath(target));
+    }
+
     @Override
     public void close() {
         if (ossClient != null) {
@@ -107,4 +118,5 @@ public class OssStore implements FileStore {
             temporaryUrlClient.shutdown();
         }
     }
+
 }

+ 2 - 1
core-fss/src/main/java/com/qmth/boot/core/fss/utils/FileStoreBuilder.java

@@ -4,11 +4,12 @@ import com.qmth.boot.core.fss.config.FileStoreProperty;
 import com.qmth.boot.core.fss.store.FileStore;
 import com.qmth.boot.core.fss.store.impl.DiskStore;
 import com.qmth.boot.core.fss.store.impl.OssStore;
+import org.apache.commons.lang3.StringUtils;
 
 public class FileStoreBuilder {
 
     public static FileStore buildFileStore(FileStoreProperty fileStoreProperty) {
-        String server = fileStoreProperty.getServer();
+        String server = StringUtils.trimToEmpty(fileStoreProperty.getServer());
         if (!server.endsWith(FssUtils.FILE_PATH_SEPARATOR)) {
             server = server.concat(FssUtils.FILE_PATH_SEPARATOR);
         }

+ 2 - 3
core-fss/src/test/java/com/qmth/boot/test/core/fss/OssStoreTest.java

@@ -4,7 +4,6 @@ import com.qmth.boot.core.fss.store.impl.OssStore;
 import com.qmth.boot.tools.models.ByteArray;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.Assert;
-import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.time.Duration;
@@ -19,7 +18,7 @@ public class OssStoreTest {
 
     private String config_local = "oss://key:secret@test.oss-api.qmth.com.cn";
 
-    @Test
+    //@Test
     public void test() throws Exception {
         OssStore store = new OssStore(server_local, config_local);
         String content = RandomStringUtils.random(128, true, true);
@@ -27,7 +26,7 @@ public class OssStoreTest {
         store.write("/test/1.txt", new ByteArrayInputStream(data.value()), ByteArray.md5(data.value()).toHexString());
         //Assert.assertTrue(store.exist("test/1.txt"));
         Assert.assertEquals(content, ByteArray.fromInputStream(store.read("test/1.txt")).toString());
-        String url = store.getTemporaryUrl("/test/1.txt", Duration.ofMinutes(5));
+        String url = store.getPresignedUrl("/test/1.txt", Duration.ofMinutes(5));
         //System.out.println(url);
         Assert.assertTrue(url.startsWith(store.getServer()));
         store.close();

+ 1 - 1
core-logging/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-logging</artifactId>
     <name>core-logging</name>

+ 1 - 1
core-logging/src/main/java/com/qmth/boot/core/logger/config/LoggerAutoConfiguration.java

@@ -4,7 +4,7 @@ import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
-@ComponentScan("com.qmth.boot.core.logger.*")
+@ComponentScan("com.qmth.boot.core.logger")
 public class LoggerAutoConfiguration {
 
 }

+ 1 - 1
core-logging/src/main/java/com/qmth/boot/core/logger/context/LoggerContextService.java

@@ -76,7 +76,7 @@ public class LoggerContextService {
         while (iterator.hasNext()) {
             Appender<ILoggingEvent> appender = iterator.next();
             if (appender instanceof ConsoleAppender) {
-                ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
+                ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
                 consoleAppender.setEncoder(encoder);
                 consoleAppender.start();
                 hasConsole = true;

+ 0 - 19
core-metrics/src/main/java/com/qmth/boot/core/metrics/config/MetricsAutoConfiguration.java

@@ -1,19 +0,0 @@
-package com.qmth.boot.core.metrics.config;
-
-import com.qmth.boot.core.metrics.service.MetricsService;
-import com.qmth.boot.core.metrics.service.impl.MemoryMetricsService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@ComponentScan("com.qmth.boot.core.metrics.*")
-public class MetricsAutoConfiguration {
-
-    @Bean
-    @ConditionalOnMissingBean(MetricsService.class)
-    public MetricsService metricsService(MetricsProperties metricsProperties) {
-        return new MemoryMetricsService(metricsProperties);
-    }
-}

+ 0 - 38
core-metrics/src/main/java/com/qmth/boot/core/metrics/config/MetricsProperties.java

@@ -1,38 +0,0 @@
-package com.qmth.boot.core.metrics.config;
-
-import com.qmth.boot.core.constant.CoreConstant;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-import org.springframework.validation.annotation.Validated;
-
-import javax.validation.constraints.NotEmpty;
-import java.time.Duration;
-
-/**
- * 统计工具参数设置
- */
-@Component
-@ConfigurationProperties(prefix = CoreConstant.CONFIG_PREFIX + ".metrics")
-@Validated
-public class MetricsProperties {
-
-    /**
-     * 默认时间分组,按0~10ms,10ms~100ms,100ms~500ms,500ms~1s,1s~10s,>10s分为6个档次
-     */
-    @NotEmpty
-    private Duration[] timeGroup = new Duration[] { Duration.ofMillis(10), Duration.ofMillis(100),
-            Duration.ofMillis(500), Duration.ofSeconds(1), Duration.ofSeconds(10) };
-
-    /**
-     * 获取响应时间分组间隔
-     *
-     * @return
-     */
-    public Duration[] getTimeGroup() {
-        return timeGroup;
-    }
-
-    public void setTimeGroup(Duration[] timeGroup) {
-        this.timeGroup = timeGroup;
-    }
-}

+ 0 - 39
core-metrics/src/main/java/com/qmth/boot/core/metrics/model/MetricsResult.java

@@ -1,39 +0,0 @@
-package com.qmth.boot.core.metrics.model;
-
-import java.util.Map;
-
-/**
- * 单个统计点的统计结果
- */
-public class MetricsResult {
-
-    private String endpoint;
-
-    private Map<Integer, Long> status;
-
-    private Map<TimeRange, Long> time;
-
-    public String getEndpoint() {
-        return endpoint;
-    }
-
-    public void setEndpoint(String endpoint) {
-        this.endpoint = endpoint;
-    }
-
-    public Map<Integer, Long> getStatus() {
-        return status;
-    }
-
-    public void setStatus(Map<Integer, Long> status) {
-        this.status = status;
-    }
-
-    public Map<TimeRange, Long> getTime() {
-        return time;
-    }
-
-    public void setTime(Map<TimeRange, Long> time) {
-        this.time = time;
-    }
-}

+ 0 - 54
core-metrics/src/main/java/com/qmth/boot/core/metrics/model/TimeRange.java

@@ -1,54 +0,0 @@
-package com.qmth.boot.core.metrics.model;
-
-/**
- * 以毫秒为单位的统计时间范围
- */
-public class TimeRange {
-
-    private long ge;
-
-    private long lt;
-
-    public TimeRange(long ge, long lt) {
-        this.ge = ge;
-        this.lt = lt;
-    }
-
-    public long getGe() {
-        return ge;
-    }
-
-    public long getLt() {
-        return lt;
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder();
-        if (ge >= 0) {
-            sb.append(ge).append("-");
-        }
-        if (lt >= 0) {
-            sb.append(lt);
-        }
-        return sb.toString();
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (!(obj instanceof TimeRange)) {
-            return false;
-        }
-        TimeRange other = (TimeRange) obj;
-        return other.ge == this.ge && other.lt == this.lt;
-    }
-
-    @Override
-    public int hashCode() {
-        final int PRIME = 31;
-        int result = 1;
-        result = PRIME * result + Long.hashCode(ge);
-        result = PRIME * result + Long.hashCode(lt);
-        return result;
-    }
-}

+ 0 - 47
core-metrics/src/main/java/com/qmth/boot/core/metrics/service/MetricsService.java

@@ -1,47 +0,0 @@
-package com.qmth.boot.core.metrics.service;
-
-import com.qmth.boot.core.metrics.model.MetricsResult;
-
-import java.util.List;
-
-/**
- * 统计服务接口
- */
-public interface MetricsService {
-
-    /**
-     * 记录单次统计结果
-     *
-     * @param endpoint
-     * @param status
-     * @param timecost
-     */
-    void record(String endpoint, int status, long timecost);
-
-    /**
-     * 获取某个统计点的统计结果
-     *
-     * @param endpoint
-     * @return
-     */
-    MetricsResult result(String endpoint);
-
-    /**
-     * 获取所有统计点的统计结果
-     *
-     * @return
-     */
-    List<MetricsResult> result();
-
-    /**
-     * 重置某个统计点的统计结果
-     *
-     * @param endpoint
-     */
-    void reset(String endpoint);
-
-    /**
-     * 重置所有统计结果
-     */
-    void reset();
-}

+ 0 - 65
core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/MemoryMetricsService.java

@@ -1,65 +0,0 @@
-package com.qmth.boot.core.metrics.service.impl;
-
-import com.qmth.boot.core.metrics.config.MetricsProperties;
-import com.qmth.boot.core.metrics.model.MetricsResult;
-import com.qmth.boot.core.metrics.service.MetricsService;
-
-import javax.annotation.PreDestroy;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class MemoryMetricsService implements MetricsService {
-
-    private Map<String, MetricsRecorder> recorderMap;
-
-    private MetricsProperties metricsProperties;
-
-    public MemoryMetricsService(MetricsProperties metricsProperties) {
-        this.metricsProperties = metricsProperties;
-        this.recorderMap = new HashMap<>();
-    }
-
-    @PreDestroy
-    public void close() {
-        this.recorderMap.clear();
-    }
-
-    @Override
-    public void record(String endpoint, int status, long timecost) {
-        recorderMap.computeIfAbsent(endpoint, key -> new MetricsRecorder(metricsProperties)).record(status, timecost);
-    }
-
-    @Override
-    public MetricsResult result(String endpoint) {
-        MetricsResult result = new MetricsResult();
-        MetricsRecorder recorder = recorderMap.get(endpoint);
-        if (recorder != null) {
-            result = recorder.getResult();
-        }
-        result.setEndpoint(endpoint);
-        return result;
-    }
-
-    @Override
-    public List<MetricsResult> result() {
-        List<MetricsResult> list = new ArrayList<>();
-        for (Map.Entry<String, MetricsRecorder> entry : recorderMap.entrySet()) {
-            MetricsResult result = entry.getValue().getResult();
-            result.setEndpoint(entry.getKey());
-            list.add(result);
-        }
-        return list;
-    }
-
-    @Override
-    public void reset(String endpoint) {
-        recorderMap.remove(endpoint);
-    }
-
-    @Override
-    public void reset() {
-        recorderMap.clear();
-    }
-}

+ 0 - 60
core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/MetricsRecorder.java

@@ -1,60 +0,0 @@
-package com.qmth.boot.core.metrics.service.impl;
-
-import com.qmth.boot.core.metrics.config.MetricsProperties;
-import com.qmth.boot.core.metrics.model.MetricsResult;
-import com.qmth.boot.core.metrics.model.TimeRange;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicLong;
-
-public class MetricsRecorder {
-
-    private Map<Integer, AtomicLong> statusMap;
-
-    private List<TimeCounter> timeList;
-
-    public MetricsRecorder(MetricsProperties properties) {
-        this.statusMap = new HashMap<>();
-        this.timeList = new ArrayList<>();
-        long ge = 0;
-        for (Duration time : properties.getTimeGroup()) {
-            long lt = time.toMillis();
-            this.timeList.add(new TimeCounter(ge, lt));
-            ge = lt;
-        }
-        this.timeList.add(new TimeCounter(ge, -1));
-    }
-
-    public void record(int status, long timecost) {
-        statusMap.computeIfAbsent(status, key -> new AtomicLong(0)).incrementAndGet();
-        for (TimeCounter counter : timeList) {
-            if (counter.accept(timecost)) {
-                break;
-            }
-        }
-    }
-
-    public MetricsResult getResult() {
-        MetricsResult result = new MetricsResult();
-
-        Map<Integer, Long> statusResult = new HashMap<>();
-        for (Map.Entry<Integer, AtomicLong> entry : statusMap.entrySet()) {
-            statusResult.put(entry.getKey(), entry.getValue().longValue());
-        }
-        result.setStatus(statusResult);
-
-        Map<TimeRange, Long> timeResult = new HashMap<>();
-        for (TimeCounter tc : timeList) {
-            timeResult.put(tc.getTimeRage(), tc.count());
-        }
-        result.setTime(timeResult);
-
-        return result;
-    }
-
-}
-

+ 0 - 35
core-metrics/src/main/java/com/qmth/boot/core/metrics/service/impl/TimeCounter.java

@@ -1,35 +0,0 @@
-package com.qmth.boot.core.metrics.service.impl;
-
-import com.qmth.boot.core.metrics.model.TimeRange;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-public class TimeCounter {
-
-    private TimeRange range;
-
-    private AtomicLong count;
-
-    public TimeCounter(long ge, long lt) {
-        this.range = new TimeRange(ge, lt);
-        this.count = new AtomicLong(0);
-    }
-
-    public TimeRange getTimeRage() {
-        return this.range;
-    }
-
-    public boolean accept(long timecost) {
-        if (timecost >= range.getGe() && (range.getLt() < 0 || timecost < range.getLt())) {
-            count.incrementAndGet();
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    public long count() {
-        return count.get();
-    }
-
-}

+ 0 - 49
core-metrics/src/test/java/com/qmth/boot/test/core/metrics/MetricsTest.java

@@ -1,49 +0,0 @@
-package com.qmth.boot.test.core.metrics;
-
-import com.qmth.boot.core.metrics.config.MetricsProperties;
-import com.qmth.boot.core.metrics.model.MetricsResult;
-import com.qmth.boot.core.metrics.model.TimeRange;
-import com.qmth.boot.core.metrics.service.impl.MemoryMetricsService;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import java.util.List;
-
-public class MetricsTest {
-
-    private static MemoryMetricsService service;
-
-    private static MetricsProperties config;
-
-    @BeforeClass
-    public static void prepare() {
-        config = new MetricsProperties();
-        service = new MemoryMetricsService(config);
-    }
-
-    @AfterClass
-    public static void clear() {
-        service.reset();
-    }
-
-    @Test
-    public void testSingleEndpoint() {
-        service.record("/a1", 200, 8);
-        service.record("/a1", 200, 22);
-        service.record("/a1", 200, 130);
-        service.record("/a1", 400, 12);
-        service.record("/a1", 500, 33);
-        service.record("/a1", 401, 330);
-        service.record("/a1", 404, 1209);
-        List<MetricsResult> list = service.result();
-        Assert.assertEquals(1, list.size());
-        MetricsResult result = list.get(0);
-        Assert.assertEquals(5, result.getStatus().size());
-        Assert.assertEquals(3, result.getStatus().get(200).longValue());
-        Assert.assertEquals(3, result.getTime().get(new TimeRange(10, 100)).longValue());
-        Assert.assertEquals(0, result.getTime().get(new TimeRange(10000, -1)).longValue());
-    }
-
-}

+ 1 - 1
core-models/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-models</artifactId>
     <name>core-models</name>

+ 59 - 0
core-models/src/main/java/com/qmth/boot/core/collection/PageResult.java

@@ -0,0 +1,59 @@
+package com.qmth.boot.core.collection;
+
+import java.util.List;
+
+/**
+ * 通用分页查询结果封装类,适用于API层的数据返回
+ */
+public class PageResult<T> {
+
+    private List<T> result;
+
+    private long totalCount;
+
+    private long pageCount;
+
+    private long pageNumber;
+
+    private long pageSize;
+
+    public List<T> getResult() {
+        return result;
+    }
+
+    public void setResult(List<T> result) {
+        this.result = result;
+    }
+
+    public long getTotalCount() {
+        return totalCount;
+    }
+
+    public void setTotalCount(long totalCount) {
+        this.totalCount = totalCount;
+    }
+
+    public long getPageCount() {
+        return pageCount;
+    }
+
+    public void setPageCount(long pageCount) {
+        this.pageCount = pageCount;
+    }
+
+    public long getPageNumber() {
+        return pageNumber;
+    }
+
+    public void setPageNumber(long pageNumber) {
+        this.pageNumber = pageNumber;
+    }
+
+    public long getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(long pageSize) {
+        this.pageSize = pageSize;
+    }
+}

+ 10 - 0
core-models/src/main/java/com/qmth/boot/core/exception/CodeNameException.java

@@ -0,0 +1,10 @@
+package com.qmth.boot.core.exception;
+
+public interface CodeNameException {
+
+    Integer getCode();
+
+    String getName();
+
+    String getMessage();
+}

+ 48 - 0
core-models/src/main/java/com/qmth/boot/core/exception/ForbiddenException.java

@@ -0,0 +1,48 @@
+package com.qmth.boot.core.exception;
+
+/**
+ * 禁止访问的运行时异常,在API层会按403状态码处理
+ */
+public class ForbiddenException extends RuntimeException implements CodeNameException {
+
+    private static final long serialVersionUID = 1014093321734424324L;
+
+    private Integer code;
+
+    private String name;
+
+    public ForbiddenException(String message) {
+        super(message);
+    }
+
+    public ForbiddenException(String message, Throwable t) {
+        super(message, t);
+    }
+
+    /**
+     * 带所有参数的构造方法
+     *
+     * @param code    - 数字标识,范围在100~999
+     * @param name    - 异常详细类型
+     * @param message - 文字提示
+     * @param cause   - 触发来源
+     */
+    public ForbiddenException(Integer code, String name, String message, Throwable cause) {
+        super(message, cause);
+        if (code != null) {
+            if (code < 100 || code > 999) {
+                throw new IllegalArgumentException("ForbiddenException.code should between 100~999");
+            }
+        }
+        this.code = code;
+        this.name = name;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 59 - 0
core-models/src/main/java/com/qmth/boot/core/exception/NotFoundException.java

@@ -0,0 +1,59 @@
+package com.qmth.boot.core.exception;
+
+/**
+ * 通用资源不存在异常,在API层按404状态码处理
+ */
+public class NotFoundException extends RuntimeException implements CodeNameException {
+
+    private static final long serialVersionUID = 648446289813795314L;
+
+    private Integer code;
+
+    private String name;
+
+    /**
+     * 有明确的文字提示
+     *
+     * @param message
+     */
+    public NotFoundException(String message) {
+        super(message);
+    }
+
+    /**
+     * 有明确的文字提示与触发来源
+     *
+     * @param message
+     * @param cause
+     */
+    public NotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * 带所有参数的构造方法
+     *
+     * @param code    - 数字标识,范围在100~999
+     * @param name    - 异常详细类型
+     * @param message - 文字提示
+     * @param cause   - 触发来源
+     */
+    public NotFoundException(Integer code, String name, String message, Throwable cause) {
+        super(message, cause);
+        if (code != null) {
+            if (code < 100 || code > 999) {
+                throw new IllegalArgumentException("NotFoundException.code should between 100~999");
+            }
+        }
+        this.code = code;
+        this.name = name;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 59 - 0
core-models/src/main/java/com/qmth/boot/core/exception/ParameterException.java

@@ -0,0 +1,59 @@
+package com.qmth.boot.core.exception;
+
+/**
+ * 通用请求参数异常,在API层按400状态码处理
+ */
+public class ParameterException extends RuntimeException implements CodeNameException {
+
+    private static final long serialVersionUID = -1045285828688368482L;
+
+    private Integer code;
+
+    private String name;
+
+    /**
+     * 有明确的文字提示
+     *
+     * @param message
+     */
+    public ParameterException(String message) {
+        super(message);
+    }
+
+    /**
+     * 有明确的文字提示与触发来源
+     *
+     * @param message
+     * @param cause
+     */
+    public ParameterException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * 带所有参数的构造方法
+     *
+     * @param code    - 数字标识,范围在100~999
+     * @param name    - 异常详细类型
+     * @param message - 文字提示
+     * @param cause   - 触发来源
+     */
+    public ParameterException(Integer code, String name, String message, Throwable cause) {
+        super(message, cause);
+        if (code != null) {
+            if (code < 100 || code > 999) {
+                throw new IllegalArgumentException("ParameterException.code should between 100~999");
+            }
+        }
+        this.code = code;
+        this.name = name;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 1 - 1
core-models/src/main/java/com/qmth/boot/core/exception/ReentrantException.java

@@ -1,7 +1,7 @@
 package com.qmth.boot.core.exception;
 
 /**
- * 可重试的运行时异常,在API层会进行特殊处理
+ * 可重试的运行时异常,在API层会进行特殊处理,按503状态码返回
  */
 public class ReentrantException extends RuntimeException {
 

+ 1 - 1
core-models/src/main/java/com/qmth/boot/core/exception/StatusException.java

@@ -3,7 +3,7 @@ package com.qmth.boot.core.exception;
 /**
  * 有状态标识的运行时异常,在API层按500状态码处理
  */
-public class StatusException extends RuntimeException {
+public class StatusException extends RuntimeException implements CodeNameException {
 
     private static final long serialVersionUID = -2411329525159341065L;
 

+ 48 - 0
core-models/src/main/java/com/qmth/boot/core/exception/UnauthorizedException.java

@@ -0,0 +1,48 @@
+package com.qmth.boot.core.exception;
+
+/**
+ * 鉴权相关的运行时异常,在API层会按401状态码处理
+ */
+public class UnauthorizedException extends RuntimeException implements CodeNameException {
+
+    private static final long serialVersionUID = 1957446799072827901L;
+
+    private Integer code;
+
+    private String name;
+
+    public UnauthorizedException(String message) {
+        super(message);
+    }
+
+    public UnauthorizedException(String message, Throwable t) {
+        super(message, t);
+    }
+
+    /**
+     * 带所有参数的构造方法
+     *
+     * @param code    - 数字标识,范围在100~999
+     * @param name    - 异常详细类型
+     * @param message - 文字提示
+     * @param cause   - 触发来源
+     */
+    public UnauthorizedException(Integer code, String name, String message, Throwable cause) {
+        super(message, cause);
+        if (code != null) {
+            if (code < 100 || code > 999) {
+                throw new IllegalArgumentException("UnauthorizedException.code should between 100~999");
+            }
+        }
+        this.code = code;
+        this.name = name;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 1 - 1
core-rate-limit/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.qmth.boot</groupId>
         <artifactId>qmth-boot</artifactId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-rate-limit</artifactId>
     <name>core-rate-limit</name>

+ 45 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/annotation/RateLimit.java

@@ -0,0 +1,45 @@
+package com.qmth.boot.core.rateLimit.annotation;
+
+import com.qmth.boot.core.rateLimit.entity.RateLimitScope;
+import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
+
+import java.lang.annotation.*;
+
+/**
+ * 限流设置注解
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Documented
+public @interface RateLimit {
+
+    /**
+     * 数量限制
+     *
+     * @return
+     */
+    int count();
+
+    /**
+     * 作用范围,默认为整个集群
+     *
+     * @return
+     */
+    RateLimitScope scope() default RateLimitScope.CLUSTER;
+
+    /**
+     * 限流对象,默认为所有访问者
+     *
+     * @return
+     */
+    RateLimitTarget target() default RateLimitTarget.ALL;
+
+    /**
+     * 时间范围,单位为毫秒,默认为0;
+     * 为0时表示不针对时间段,而是控制并发数量
+     *
+     * @return
+     */
+    long period() default 0;
+
+}

+ 1 - 1
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/config/RateLimitAutoConfiguration.java

@@ -8,7 +8,7 @@ import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
-@ComponentScan("com.qmth.boot.core.rateLimit.*")
+@ComponentScan("com.qmth.boot.core.rateLimit")
 public class RateLimitAutoConfiguration {
 
     @Bean

+ 7 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitRule.java

@@ -1,5 +1,6 @@
 package com.qmth.boot.core.rateLimit.entity;
 
+import com.qmth.boot.core.rateLimit.annotation.RateLimit;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.convert.DurationStyle;
 
@@ -9,6 +10,7 @@ import java.time.Duration;
  * 限流规则对象,可以基于标准表达式构建
  * 表达式格式为:100/2s,斜杠左边为数量,右边为时间间隔
  * 可选参数格式:100/id/2s,i表示单实例,d表示单设备
+ * 可选参数格式:100/u/0s,u表示单用户,0s表示全局并发控制
  * 未配置情况下,默认参数为集群环境下的所有设备
  */
 public class RateLimitRule {
@@ -23,6 +25,11 @@ public class RateLimitRule {
 
     private Duration period;
 
+    public static RateLimitRule parse(RateLimit annotation) {
+        return new RateLimitRule(annotation.count(), annotation.scope(), annotation.target(),
+                Duration.ofMillis(annotation.period()));
+    }
+
     public static RateLimitRule parse(String expression) {
         String[] values = StringUtils.split(StringUtils.trimToNull(expression), EXPRESSION_SPLIT);
         if (values != null) {

+ 6 - 2
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitScope.java

@@ -2,11 +2,15 @@ package com.qmth.boot.core.rateLimit.entity;
 
 public enum RateLimitScope {
 
-    CLUSTER(""), INSTANCE("i");
+    //作用整个集群
+    CLUSTER(""),
+
+    //仅限当前实例
+    INSTANCE("i");
 
     private String code;
 
-    private RateLimitScope(String code) {
+    RateLimitScope(String code) {
         this.code = code;
     }
 

+ 9 - 2
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitTarget.java

@@ -2,11 +2,18 @@ package com.qmth.boot.core.rateLimit.entity;
 
 public enum RateLimitTarget {
 
-    ALL(""), DEVICE("d");
+    //所有访问者
+    ALL(""),
+
+    //按用户区分访问者
+    USER("u"),
+
+    //按设备区分访问者
+    DEVICE("d");
 
     private String code;
 
-    private RateLimitTarget(String code) {
+    RateLimitTarget(String code) {
         this.code = code;
     }
 

+ 0 - 18
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitPolicy.java

@@ -1,18 +0,0 @@
-package com.qmth.boot.core.rateLimit.service;
-
-import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
-
-/**
- * 动态获取限流规则扩展接口
- */
-public interface RateLimitPolicy {
-
-    /**
-     * 根据API地址动态获取限流规则
-     *
-     * @param endpoint
-     * @return
-     */
-    RateLimitRule[] getRules(String endpoint);
-
-}

+ 4 - 5
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitService.java

@@ -8,14 +8,13 @@ import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 public interface RateLimitService {
 
     /**
-     * 根据控制点、访问设备、规则集合执行限流判定
-     * 多个规则必须全满足才判定通过
+     * 根据控制点、访问对象、限流规则获取限流器
      *
      * @param endpoint
-     * @param device
-     * @param rules
+     * @param target
+     * @param rule
      * @return
      */
-    boolean accept(String endpoint, String device, RateLimitRule... rules);
+    RateLimiter getRateLimiter(String endpoint, String target, RateLimitRule rule);
 
 }

+ 16 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimiter.java

@@ -0,0 +1,16 @@
+package com.qmth.boot.core.rateLimit.service;
+
+public interface RateLimiter {
+
+    /**
+     * 尝试获取限流许可
+     *
+     * @return
+     */
+    boolean acquire();
+
+    /**
+     * 释放已获取的许可
+     */
+    void release();
+}

+ 15 - 49
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimitService.java

@@ -1,68 +1,34 @@
 package com.qmth.boot.core.rateLimit.service.impl;
 
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
-import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
 import com.qmth.boot.core.rateLimit.service.RateLimitService;
-import org.apache.commons.lang3.ArrayUtils;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
 
-import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
 
 public class MemoryRateLimitService implements RateLimitService {
 
-    protected static final String KEY_JOINER = "_";
+    private static final String TARGET_ALL_KEY = "*";
 
-    protected static final int LIMITOR_ARRAY_COUNT = 10;
-
-    private final Map<String, RateLimitor[]> limitorMap = new HashMap<>();
+    private final Map<String, Map<String, MemoryRateLimiter>> limiterMap = new HashMap<>();
 
     @Override
-    public boolean accept(String endpoint, String device, RateLimitRule... rules) {
-        if (ArrayUtils.isEmpty(rules)) {
-            return true;
-        }
-        for (RateLimitRule rule : rules) {
-            if (!accept(endpoint, device, rule)) {
-                return false;
+    public RateLimiter getRateLimiter(String endpoint, String target, RateLimitRule rule) {
+        target = target == null ? TARGET_ALL_KEY : target;
+        Map<String, MemoryRateLimiter> map = limiterMap.get(endpoint);
+        if (map == null) {
+            synchronized (limiterMap) {
+                map = limiterMap.computeIfAbsent(endpoint, k -> new HashMap<>());
             }
         }
-        return true;
-    }
-
-    protected boolean accept(String endpoint, String device, RateLimitRule rule) {
-        String key = endpoint;
-        if (rule.getTarget() == RateLimitTarget.DEVICE) {
-            // 若限流对象是单个设备,则KEY需要包含设备标识
-            key = key.concat(KEY_JOINER).concat(device);
-        }
-        RateLimitor limitor = getLimitor(key, rule.getPeriod());
-        return limitor.count() <= rule.getCount() && limitor.incr() <= rule.getCount();
-    }
-
-    private RateLimitor getLimitor(String key, Duration period) {
-        RateLimitor[] array = limitorMap.get(key);
-        if (array == null) {
-            synchronized (limitorMap) {
-                array = limitorMap.computeIfAbsent(key, k -> initArray());
+        MemoryRateLimiter limiter = map.get(target);
+        if (limiter == null) {
+            synchronized (map) {
+                limiter = map.computeIfAbsent(target,
+                        k -> new MemoryRateLimiter(rule.getCount(), rule.getPeriod().toMillis()));
             }
         }
-        long time = System.currentTimeMillis() / period.toMillis();
-        int index = (int) (time % LIMITOR_ARRAY_COUNT);
-        RateLimitor limitor = array[index];
-        // 判断当前限流器归属的时间窗口,不是当前则重置
-        if (limitor.time() != time) {
-            limitor.reset(time);
-        }
-        return limitor;
-    }
-
-    private RateLimitor[] initArray() {
-        RateLimitor[] array = new RateLimitor[LIMITOR_ARRAY_COUNT];
-        for (int i = 0; i < LIMITOR_ARRAY_COUNT; i++) {
-            array[i] = new RateLimitor(0);
-        }
-        return array;
+        return limiter;
     }
-
 }

+ 49 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimiter.java

@@ -0,0 +1,49 @@
+package com.qmth.boot.core.rateLimit.service.impl;
+
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+
+import java.util.concurrent.Semaphore;
+
+/**
+ * 内存限流控制单元
+ */
+public class MemoryRateLimiter implements RateLimiter {
+
+    private volatile long expireTime;
+
+    private long interval;
+
+    private Semaphore counter;
+
+    private int maxCount;
+
+    public MemoryRateLimiter(int count, long interval) {
+        this.maxCount = count;
+        this.counter = new Semaphore(count);
+        //interval大于0表示需要限流的时间段
+        //为0表示不分时间段,控制绝对并发数
+        this.interval = interval;
+        this.expireTime = interval > 0 ? (System.currentTimeMillis() / interval + 1) * interval : Long.MAX_VALUE;
+    }
+
+    @Override
+    public boolean acquire() {
+        long time = System.currentTimeMillis();
+        while (time > expireTime) {
+            synchronized (this) {
+                if (time > expireTime) {
+                    expireTime = (time / interval + 1) * interval;
+                    counter.drainPermits();
+                    counter.release(maxCount);
+                }
+            }
+        }
+        return counter.tryAcquire();
+    }
+
+    @Override
+    public void release() {
+        this.counter.release();
+    }
+
+}

+ 0 - 38
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/RateLimitor.java

@@ -1,38 +0,0 @@
-package com.qmth.boot.core.rateLimit.service.impl;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * 内存限流控制单元
- */
-public class RateLimitor {
-
-    private long time;
-
-    private AtomicInteger count;
-
-    public RateLimitor(long time) {
-        this.time = time;
-        this.count = new AtomicInteger(0);
-    }
-
-    public synchronized void reset(long time) {
-        if (time != this.time) {
-            this.time = time;
-            this.count.set(0);
-        }
-    }
-
-    public int incr() {
-        return this.count.incrementAndGet();
-    }
-
-    public long time() {
-        return time;
-    }
-
-    public int count() {
-        return count.get();
-    }
-
-}

+ 6 - 3
core-rate-limit/src/test/java/com/qmth/boot/test/core/rateLimit/RateLimitTest.java

@@ -3,6 +3,7 @@ package com.qmth.boot.test.core.rateLimit;
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 import com.qmth.boot.core.rateLimit.entity.RateLimitScope;
 import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
 import com.qmth.boot.core.rateLimit.service.impl.MemoryRateLimitService;
 import org.apache.commons.lang3.RandomUtils;
 import org.junit.Assert;
@@ -37,17 +38,19 @@ public class RateLimitTest {
         Assert.assertEquals(rule.getTarget(), RateLimitTarget.DEVICE);
     }
 
-    @Test
+    //@Test
     public void testSimpleRule() throws InterruptedException {
         RateLimitRule rule = RateLimitRule.parse("1/1s");
         String endpoint = "/api/test";
-        String device = "test";
+        String target = "test";
+        RateLimiter limiter = service.getRateLimiter(endpoint, target, rule);
 
         int loop = 0;
         long time = 0;
         while (loop < 50) {
             long current = System.currentTimeMillis() / rule.getPeriod().toMillis();
-            boolean result = service.accept(endpoint, device, rule);
+            boolean result = limiter.acquire();
+            System.out.println(current + "-" + time + "-" + result);
             if (current != time) {
                 Assert.assertTrue(result);
             } else {

+ 13 - 1
core-retrofit/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-retrofit</artifactId>
     <name>core-retrofit</name>
@@ -40,6 +40,10 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.squareup.retrofit2</groupId>
             <artifactId>retrofit</artifactId>
@@ -48,6 +52,10 @@
             <groupId>com.squareup.retrofit2</groupId>
             <artifactId>converter-jackson</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.squareup.retrofit2</groupId>
+            <artifactId>converter-scalars</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.squareup.okhttp3</groupId>
             <artifactId>okhttp</artifactId>
@@ -56,6 +64,10 @@
             <groupId>com.squareup.okhttp3</groupId>
             <artifactId>logging-interceptor</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+        </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>

+ 3 - 3
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/annotatioin/RetrofitClient.java

@@ -27,14 +27,14 @@ public @interface RetrofitClient {
     LogLevel logLevel() default LogLevel.OFF;
 
     /**
-     * 是否直接返回数据对象而不使用Call包装,默认为false
+     * 是否直接返回数据对象而不使用Call包装,默认为true
      *
      * @return
      */
-    boolean directReturn() default false;
+    boolean directReturn() default true;
 
     /**
-     * 自定义客户端配置
+     * 自定义客户端配置类,使用时会尝试从SpringContext中按Class获取
      *
      * @return
      */

+ 7 - 1
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetrofitAutoConfiguration.java

@@ -1,10 +1,16 @@
 package com.qmth.boot.core.retrofit.config;
 
+import com.qmth.boot.core.retrofit.interfaces.CustomizeRetrofitConfiguration;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
-@ComponentScan("com.qmth.boot.core.retrofit.*")
+@ComponentScan("com.qmth.boot.core.retrofit")
 public class RetrofitAutoConfiguration {
 
+    @Bean
+    CustomizeRetrofitConfiguration.VoidRetrofitConfiguration voidRetrofitConfiguration() {
+        return new CustomizeRetrofitConfiguration.VoidRetrofitConfiguration();
+    }
 }

+ 2 - 2
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetrofitProperties.java

@@ -16,10 +16,10 @@ public class RetrofitProperties {
     private Duration connectTimeout = Duration.ofSeconds(10);
 
     @NotNull
-    private Duration readTimeout = Duration.ofSeconds(10);
+    private Duration readTimeout = Duration.ofSeconds(30);
 
     @NotNull
-    private Duration writeTimeout = Duration.ofSeconds(10);
+    private Duration writeTimeout = Duration.ofSeconds(30);
 
     @NotNull
     private PoolProperties pool = new PoolProperties();

+ 14 - 0
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/config/RetryProperties.java

@@ -4,6 +4,9 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.validation.constraints.NotNull;
 import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
 @Validated
 public class RetryProperties {
@@ -14,6 +17,9 @@ public class RetryProperties {
     @NotNull
     private Duration interval = Duration.ofMillis(300);
 
+    @NotNull
+    private Set<Integer> statusCode = new HashSet<>(Arrays.asList(408, 503, 504));
+
     public Integer getCount() {
         return count;
     }
@@ -29,4 +35,12 @@ public class RetryProperties {
     public void setInterval(Duration interval) {
         this.interval = interval;
     }
+
+    public Set<Integer> getStatusCode() {
+        return statusCode;
+    }
+
+    public void setStatusCode(Set<Integer> statusCode) {
+        this.statusCode = statusCode;
+    }
 }

+ 38 - 7
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/core/RetrofitFactoryBean.java

@@ -1,5 +1,12 @@
 package com.qmth.boot.core.retrofit.core;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.qmth.boot.core.retrofit.annotatioin.RetrofitClient;
 import com.qmth.boot.core.retrofit.config.RetrofitProperties;
 import com.qmth.boot.core.retrofit.interceptor.ErrorDecodeInterceptor;
@@ -7,7 +14,8 @@ import com.qmth.boot.core.retrofit.interceptor.LoggingInterceptor;
 import com.qmth.boot.core.retrofit.interceptor.RetryInterceptor;
 import com.qmth.boot.core.retrofit.interceptor.SignatureInterceptor;
 import com.qmth.boot.core.retrofit.interfaces.CustomizeRetrofitConfiguration;
-import com.qmth.boot.core.retrofit.interfaces.SignatureProvider;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.binder.okhttp3.OkHttpMetricsEventListener;
 import okhttp3.ConnectionPool;
 import okhttp3.OkHttpClient;
 import org.apache.commons.lang3.StringUtils;
@@ -18,6 +26,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import retrofit2.Retrofit;
 import retrofit2.converter.jackson.JacksonConverterFactory;
+import retrofit2.converter.scalars.ScalarsConverterFactory;
 
 import java.lang.reflect.Proxy;
 
@@ -29,8 +38,23 @@ public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContex
 
     private RetrofitProperties retrofitProperties;
 
+    private MeterRegistry meterRegistry;
+
+    private ObjectMapper mapper;
+
     public RetrofitFactoryBean(Class<T> retrofitInterface) {
         this.retrofitInterface = retrofitInterface;
+        // 初始化ObjectMapper
+        ObjectMapper mapper = new ObjectMapper();
+        // 反序列化时,允许有未知字段
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        // 默认不序列化空值
+        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        // 解决jackson2无法反序列化LocalDateTime的问题
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        mapper.registerModule(new JavaTimeModule());
+        this.mapper = mapper;
     }
 
     @Override
@@ -59,7 +83,8 @@ public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContex
 
         OkHttpClient client = getOkHttpClient(retrofitClient, customize);
         Retrofit.Builder retrofitBuilder = new Retrofit.Builder().baseUrl(getBaseUrl(retrofitClient, customize))
-                .addConverterFactory(JacksonConverterFactory.create()).validateEagerly(true).client(client);
+                .addConverterFactory(ScalarsConverterFactory.create())
+                .addConverterFactory(JacksonConverterFactory.create(mapper)).validateEagerly(true).client(client);
         if (retrofitClient.directReturn()) {
             retrofitBuilder.addCallAdapterFactory(new DirectCallAdapterFactory());
         }
@@ -73,13 +98,12 @@ public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContex
      * @return OkHttpClient instance
      */
     private OkHttpClient getOkHttpClient(RetrofitClient retrofitClient, CustomizeRetrofitConfiguration customize) {
-        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder().connectionPool(new ConnectionPool());
+        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder().connectionPool(new ConnectionPool())
+                .connectTimeout(retrofitProperties.getConnectTimeout()).readTimeout(retrofitProperties.getReadTimeout())
+                .writeTimeout(retrofitProperties.getWriteTimeout());
 
         // add signature interceptor
-        SignatureProvider provider = customize.getSignature();
-        if (provider != null) {
-            okHttpClientBuilder.addInterceptor(new SignatureInterceptor(provider));
-        }
+        okHttpClientBuilder.addInterceptor(new SignatureInterceptor(customize.getSignature()));
 
         // add ErrorDecodeInterceptor
         okHttpClientBuilder.addInterceptor(new ErrorDecodeInterceptor());
@@ -93,6 +117,12 @@ public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContex
             okHttpClientBuilder.addNetworkInterceptor(new LoggingInterceptor(logLevel));
         }
 
+        // add metrics event listener
+        if (meterRegistry != null) {
+            okHttpClientBuilder
+                    .eventListener(OkHttpMetricsEventListener.builder(meterRegistry, "okhttp.client").build());
+        }
+
         return okHttpClientBuilder.build();
     }
 
@@ -116,6 +146,7 @@ public class RetrofitFactoryBean<T> implements FactoryBean<T>, ApplicationContex
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         this.applicationContext = applicationContext;
         this.retrofitProperties = applicationContext.getBean(RetrofitProperties.class);
+        this.meterRegistry = applicationContext.getBean(MeterRegistry.class);
     }
 
 }

+ 6 - 8
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/ErrorDecodeInterceptor.java

@@ -5,6 +5,8 @@ import okhttp3.*;
 import okio.Buffer;
 import okio.BufferedSource;
 import okio.GzipSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -12,6 +14,8 @@ import java.nio.charset.StandardCharsets;
 
 public class ErrorDecodeInterceptor implements Interceptor {
 
+    public static final Logger log = LoggerFactory.getLogger(ErrorDecodeInterceptor.class);
+
     public static final String GZIP = "gzip";
 
     public static final String CONTENT_ENCODING = "Content-Encoding";
@@ -44,7 +48,6 @@ public class ErrorDecodeInterceptor implements Interceptor {
             if (responseBody == null) {
                 return null;
             }
-            long contentLength = responseBody.contentLength();
 
             BufferedSource source = responseBody.source();
             // Buffer the entire body.
@@ -62,14 +65,9 @@ public class ErrorDecodeInterceptor implements Interceptor {
             if (contentType != null) {
                 charset = contentType.charset(StandardCharsets.UTF_8);
             }
-
-            if (contentLength > 0) {
-                return buffer.clone().readString(charset);
-            } else {
-                return null;
-            }
+            return buffer.clone().readString(charset);
         } catch (Exception e) {
-            //throw new RuntimeException(e);
+            log.error("Retrofit response decode error", e);
             return null;
         }
     }

+ 1 - 1
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/RetryInterceptor.java

@@ -28,7 +28,7 @@ public class RetryInterceptor implements Interceptor {
     }
 
     private boolean needRetry(Response response, int count) {
-        if (response.isSuccessful() || response.code() != 503) {
+        if (response.isSuccessful() || !retryProperties.getStatusCode().contains(response.code())) {
             return false;
         }
         return count < retryProperties.getCount();

+ 25 - 7
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/interceptor/SignatureInterceptor.java

@@ -1,6 +1,7 @@
 package com.qmth.boot.core.retrofit.interceptor;
 
 import com.qmth.boot.core.retrofit.interfaces.SignatureProvider;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
 import com.qmth.boot.tools.signature.SignatureEntity;
 import okhttp3.Interceptor;
 import okhttp3.Request;
@@ -26,13 +27,30 @@ public class SignatureInterceptor implements Interceptor {
     @Override
     public Response intercept(Chain chain) throws IOException {
         Request request = chain.request();
-        long time = System.currentTimeMillis();
-        String signature = SignatureEntity
-                .build(provider.getType(), request.method(), request.url().uri().getPath(), time,
-                        provider.getIdentity(), provider.getSecret());
-        return chain.proceed(request.newBuilder().removeHeader(HEADER_KEY_TIME).removeHeader(HEADER_KEY_AUTHORIZATION)
-                .addHeader(HEADER_KEY_TIME, String.valueOf(time)).addHeader(HEADER_KEY_AUTHORIZATION, signature)
-                .build());
+        SignatureProvider provider = getSignatureInfo(request);
+        if (provider == null) {
+            provider = this.provider;
+        }
+        if (provider != null) {
+            long time = System.currentTimeMillis();
+            String signature = SignatureEntity
+                    .build(provider.getType(), request.method(), request.url().uri().getPath(), time,
+                            provider.getIdentity(), provider.getSecret());
+            return chain.proceed(
+                    request.newBuilder().removeHeader(HEADER_KEY_TIME).removeHeader(HEADER_KEY_AUTHORIZATION)
+                            .addHeader(HEADER_KEY_TIME, String.valueOf(time))
+                            .addHeader(HEADER_KEY_AUTHORIZATION, signature).build());
+        } else {
+            return chain.proceed(request);
+        }
+    }
+
+    private SignatureInfo getSignatureInfo(Request request) {
+        try {
+            return request.tag(SignatureInfo.class);
+        } catch (Exception e) {
+            return null;
+        }
     }
 
 }

+ 59 - 0
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/utils/SignatureInfo.java

@@ -0,0 +1,59 @@
+package com.qmth.boot.core.retrofit.utils;
+
+import com.qmth.boot.core.retrofit.interfaces.SignatureProvider;
+import com.qmth.boot.tools.signature.SignatureType;
+
+/**
+ * 调用方动态提供的签名信息,替代容器管理的静态配置
+ */
+public class SignatureInfo implements SignatureProvider {
+
+    private SignatureType type;
+
+    private String identity;
+
+    private String secret;
+
+    public SignatureInfo(SignatureType type, String identity, String secret) {
+        this.type = type;
+        this.identity = identity;
+        this.secret = secret;
+    }
+
+    @Override
+    public SignatureType getType() {
+        return type;
+    }
+
+    @Override
+    public String getIdentity() {
+        return identity;
+    }
+
+    @Override
+    public String getSecret() {
+        return secret;
+    }
+
+    /**
+     * 按照密钥类型创建签名信息
+     *
+     * @param accessKey
+     * @param accessSecret
+     * @return
+     */
+    public static SignatureInfo secret(String accessKey, String accessSecret) {
+        return new SignatureInfo(SignatureType.SECRET, accessKey, accessSecret);
+    }
+
+    /**
+     * 按照令牌类型创建签名信息
+     *
+     * @param session
+     * @param token
+     * @return
+     */
+    public static SignatureInfo token(String session, String token) {
+        return new SignatureInfo(SignatureType.TOKEN, session, token);
+    }
+}

+ 38 - 0
core-retrofit/src/main/java/com/qmth/boot/core/retrofit/utils/UploadFile.java

@@ -0,0 +1,38 @@
+package com.qmth.boot.core.retrofit.utils;
+
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
+
+import java.io.File;
+
+/**
+ * 文件上传表单字段辅助构造工具
+ */
+public class UploadFile {
+
+    /**
+     * 根据二进制内容构造FormData字段
+     *
+     * @param field 表单字段
+     * @param name  上传文件名
+     * @param data  二进制内容
+     * @return
+     */
+    public static MultipartBody.Part build(String field, String name, byte[] data) {
+        return MultipartBody.Part.createFormData(field, name, RequestBody.create(MediaType.parse(name), data));
+    }
+
+    /**
+     * 根据本地磁盘文件构造FormData字段
+     *
+     * @param field 表单字段
+     * @param name  上传文件名
+     * @param file  本地文件
+     * @return
+     */
+    public static MultipartBody.Part build(String field, String name, File file) {
+        return MultipartBody.Part.createFormData(field, name, RequestBody.create(MediaType.parse(name), file));
+    }
+
+}

+ 1 - 1
core-schedule/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <artifactId>qmth-boot</artifactId>
         <groupId>com.qmth.boot</groupId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-schedule</artifactId>
     <name>core-schedule</name>

+ 1 - 1
core-schedule/src/main/java/com/qmth/boot/core/schedule/config/ScheduleAutoConfiguration.java

@@ -14,7 +14,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
 @Configuration
 @EnableScheduling
 @EnableAsync
-@ComponentScan("com.qmth.boot.core.schedule.*")
+@ComponentScan("com.qmth.boot.core.schedule")
 public class ScheduleAutoConfiguration {
 
     @Bean(destroyMethod = "shutdown")

+ 5 - 1
core-security/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <groupId>com.qmth.boot</groupId>
         <artifactId>qmth-boot</artifactId>
-        <version>1.0.2</version>
+        <version>1.0.4</version>
     </parent>
     <artifactId>core-security</artifactId>
     <name>core-security</name>
@@ -20,6 +20,10 @@
             <groupId>com.qmth.boot</groupId>
             <artifactId>core-models</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.github.ulisesbocchio</groupId>
+            <artifactId>jasypt-spring-boot-starter</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot</artifactId>

+ 1 - 1
core-security/src/main/java/com/qmth/boot/core/security/annotation/AuthorizationComponent.java

@@ -19,7 +19,7 @@ public @interface AuthorizationComponent {
      *
      * @return
      */
-    String prefix() default "";
+    String[] prefix() default {};
 
     /**
      * 匹配签名类型,可以为空

+ 30 - 1
core-security/src/main/java/com/qmth/boot/core/security/config/SecurityAutoConfiguration.java

@@ -1,10 +1,39 @@
 package com.qmth.boot.core.security.config;
 
+import com.qmth.boot.core.security.service.EncryptKeyProvider;
+import com.qmth.boot.core.security.service.EncryptService;
+import com.qmth.boot.core.security.service.impl.DefaultEncryptKeyProvider;
+import org.jasypt.encryption.StringEncryptor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
+import javax.validation.constraints.NotNull;
+
 @Configuration
-@ComponentScan("com.qmth.boot.core.security.*")
+@ComponentScan("com.qmth.boot.core.security")
 public class SecurityAutoConfiguration {
 
+    @Bean
+    @ConditionalOnMissingBean(EncryptKeyProvider.class)
+    public EncryptKeyProvider encryptKeyProvider() {
+        return new DefaultEncryptKeyProvider();
+    }
+
+    @Bean("jasyptStringEncryptor")
+    public StringEncryptor stringEncryptor(@NotNull EncryptService encryptService) {
+        return new StringEncryptor() {
+
+            @Override
+            public String encrypt(String s) {
+                return encryptService.encrypt(s);
+            }
+
+            @Override
+            public String decrypt(String s) {
+                return encryptService.decrypt(s);
+            }
+        };
+    }
 }

+ 8 - 7
core-security/src/main/java/com/qmth/boot/core/security/config/SecurityContextListener.java

@@ -4,7 +4,6 @@ import com.qmth.boot.core.security.annotation.AuthorizationComponent;
 import com.qmth.boot.core.security.service.AuthorizationService;
 import com.qmth.boot.core.security.service.impl.BaseAuthorizationSupport;
 import com.qmth.boot.tools.signature.SignatureType;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextRefreshedEvent;
@@ -43,12 +42,14 @@ public class SecurityContextListener implements ApplicationListener<ContextRefre
         }
     }
 
-    private void register(AuthorizationService bean, String prefix, SignatureType type) {
-        if (StringUtils.isNotBlank(prefix)) {
-            if (type != null) {
-                baseAuthorizationSupport.getServiceContainer().setPrefix(prefix, type, bean);
-            } else {
-                baseAuthorizationSupport.getServiceContainer().setPrefix(prefix, bean);
+    private void register(AuthorizationService bean, String[] prefixs, SignatureType type) {
+        if (prefixs.length > 0) {
+            for (String prefix : prefixs) {
+                if (type != null) {
+                    baseAuthorizationSupport.getServiceContainer().setPrefix(prefix, type, bean);
+                } else {
+                    baseAuthorizationSupport.getServiceContainer().setPrefix(prefix, bean);
+                }
             }
         } else {
             if (type != null) {

+ 13 - 0
core-security/src/main/java/com/qmth/boot/core/security/config/SecurityProperties.java

@@ -31,6 +31,11 @@ public class SecurityProperties {
     @DurationMin(seconds = 0)
     private Duration timeMaxAhead = Duration.ofSeconds(5);
 
+    /**
+     * 调试模式,默认不开启;开启后不验证密文直接验证通过
+     */
+    private boolean debugMode = false;
+
     public Duration getTimeMaxDelay() {
         return timeMaxDelay;
     }
@@ -46,4 +51,12 @@ public class SecurityProperties {
     public void setTimeMaxAhead(Duration timeMaxAhead) {
         this.timeMaxAhead = timeMaxAhead;
     }
+
+    public boolean isDebugMode() {
+        return debugMode;
+    }
+
+    public void setDebugMode(boolean debugMode) {
+        this.debugMode = debugMode;
+    }
 }

+ 1 - 1
core-security/src/main/java/com/qmth/boot/core/security/exception/AuthorizationException.java

@@ -23,7 +23,7 @@ public class AuthorizationException extends RuntimeException {
 
     public static AuthorizationException NO_PERMISSION = new AuthorizationException(401008, "no permission");
 
-    public static AuthorizationException SERVICE_NOT_FOUND = new AuthorizationException(401010,
+    public static AuthorizationException SERVICE_NOT_FOUND = new AuthorizationException(401009,
             "authorization service not found");
 
     public AuthorizationException(int code, String message) {

+ 9 - 0
core-security/src/main/java/com/qmth/boot/core/security/model/AccessEntity.java

@@ -21,6 +21,15 @@ public interface AccessEntity {
      */
     String getSecret();
 
+    /**
+     * 记录到日志中的访问者信息,默认取identity内容
+     *
+     * @return
+     */
+    default String getLogName() {
+        return getIdentity();
+    }
+
     /**
      * 允许访问的IP
      *

+ 3 - 1
core-security/src/main/java/com/qmth/boot/core/security/service/AuthorizationService.java

@@ -25,6 +25,8 @@ public interface AuthorizationService<T extends AccessEntity> {
      * @param path
      * @return
      */
-    boolean hasPermission(T accessEntity, String path);
+    default boolean hasPermission(T accessEntity, String path) {
+        return true;
+    }
 
 }

+ 14 - 0
core-security/src/main/java/com/qmth/boot/core/security/service/EncryptKeyProvider.java

@@ -0,0 +1,14 @@
+package com.qmth.boot.core.security.service;
+
+/**
+ * 加解密密钥提供接口,可以由上层应用自定义实现
+ */
+public interface EncryptKeyProvider {
+
+    /**
+     * 获取密钥字符串
+     *
+     * @return
+     */
+    String getKey();
+}

+ 31 - 0
core-security/src/main/java/com/qmth/boot/core/security/service/EncryptService.java

@@ -0,0 +1,31 @@
+package com.qmth.boot.core.security.service;
+
+/**
+ * 加解密等安全服务接口
+ */
+public interface EncryptService {
+
+    /**
+     * 使用对称算法进行字符串加密
+     *
+     * @param text 明文
+     * @return
+     */
+    String encrypt(String text);
+
+    /**
+     * 使用对称算法进行字符串解密
+     *
+     * @param text 密文
+     * @return
+     */
+    String decrypt(String text);
+
+    /**
+     * 使用散列算法生成密文,通常用于密码等数据的不可逆加密保存
+     *
+     * @param text
+     * @return
+     */
+    String hash(String text);
+}

+ 2 - 2
core-security/src/main/java/com/qmth/boot/core/security/service/impl/BaseAuthorizationSupport.java

@@ -61,8 +61,8 @@ public class BaseAuthorizationSupport implements AuthorizationSupport {
         if (ae == null) {
             throw AuthorizationException.IDENTITY_NOT_FOUND;
         }
-        // 验证签名的密文部分
-        if (!entity.validate(ae.getSecret())) {
+        // 非调试模式下验证签名的密文部分
+        if (!securityProperties.isDebugMode() && !entity.validate(ae.getSecret())) {
             throw entity.getType() == SignatureType.SECRET ?
                     AuthorizationException.SECRET_ERROR :
                     AuthorizationException.TOKEN_ERROR;

+ 16 - 0
core-security/src/main/java/com/qmth/boot/core/security/service/impl/DefaultEncryptKeyProvider.java

@@ -0,0 +1,16 @@
+package com.qmth.boot.core.security.service.impl;
+
+import com.qmth.boot.core.security.service.EncryptKeyProvider;
+
+/**
+ * 基础密钥提供服务,使用固定字符串
+ */
+public class DefaultEncryptKeyProvider implements EncryptKeyProvider {
+
+    private static final String DEFAULT_KEY = "qmth";
+
+    @Override
+    public String getKey() {
+        return DEFAULT_KEY;
+    }
+}

+ 41 - 0
core-security/src/main/java/com/qmth/boot/core/security/service/impl/DefaultEncryptService.java

@@ -0,0 +1,41 @@
+package com.qmth.boot.core.security.service.impl;
+
+import com.qmth.boot.core.security.service.EncryptKeyProvider;
+import com.qmth.boot.core.security.service.EncryptService;
+import com.qmth.boot.tools.crypto.AES;
+import com.qmth.boot.tools.models.ByteArray;
+import org.springframework.stereotype.Service;
+
+import javax.validation.constraints.NotNull;
+
+@Service
+public class DefaultEncryptService implements EncryptService {
+
+    private String key, vector;
+
+    public DefaultEncryptService(@NotNull EncryptKeyProvider encryptKeyProvider) {
+        String keyDigest = ByteArray.sha1(ByteArray.fromString(encryptKeyProvider.getKey()).toBase64()).toHexString();
+        this.key = keyDigest.substring(0, 16);
+        this.vector = keyDigest.substring(keyDigest.length() - 16);
+    }
+
+    public String encrypt(String text) {
+        try {
+            return AES.encrypt(ByteArray.fromString(text).value(), key, vector).toHexString().toLowerCase();
+        } catch (Exception e) {
+            throw new RuntimeException("string encrypt error", e);
+        }
+    }
+
+    public String decrypt(String text) {
+        try {
+            return AES.decrypt(ByteArray.fromHexString(text).value(), key, vector).toString();
+        } catch (Exception e) {
+            throw new RuntimeException("string decrypt error", e);
+        }
+    }
+
+    public String hash(String text) {
+        return ByteArray.sha1(ByteArray.sha1(text).toHexString() + key + text.length()).toHexString().toLowerCase();
+    }
+}

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott