├── DevOps ├── bruno │ ├── environments │ │ └── 本地开发环境.bru │ ├── bruno.json │ ├── ping │ │ ├── 测试根目录.bru │ │ ├── MySQL 延迟测试.bru │ │ └── Redis 延迟测试.bru │ ├── collection.bru │ ├── debug │ │ ├── 获取时间相关参数.bru │ │ ├── 查看服务器信息.bru │ │ ├── 原样返回请求数据.bru │ │ └── 获取请求详情.bru │ ├── reminder │ │ ├── project │ │ │ ├── 列表.bru │ │ │ ├── 删除.bru │ │ │ ├── 创建.bru │ │ │ ├── 更新.bru │ │ │ └── 详情.bru │ │ └── filter │ │ │ ├── 显示任务数.bru │ │ │ └── 查看未完成任务数.bru │ ├── temp │ │ └── 临时 a.bru │ ├── account │ │ ├── 获取短信验证码.bru │ │ ├── 短信验证码登录.bru │ │ └── 修改用户基础信息.bru │ ├── oss │ │ └── 生成 OSS 直传凭据.bru │ └── README.md ├── pm2 │ ├── pm2-dev.json │ ├── pm2-prod.json │ └── README.md ├── maven │ └── settings.xml ├── nginx │ └── api-local.weutil.com.nginx.conf ├── script │ ├── deploy.sh │ ├── init.sh │ └── build.sh └── mysql │ └── schema │ ├── life-helper-aliyun.sql │ ├── life-helper-common.sql │ └── life-helper-todolist.sql ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── icon.svg └── jarRepositories.xml ├── mybatis-flex.config ├── .editorconfig ├── life-helper-aliyun ├── life-helper-aliyun-sms │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── weutil │ │ │ └── sms │ │ │ ├── exception │ │ │ ├── SmsSentFailureException.java │ │ │ ├── InvalidPhoneNumberException.java │ │ │ └── SmsRateLimitExceededException.java │ │ │ ├── model │ │ │ ├── AliyunSmsClient.java │ │ │ └── SmsRateLimitExceededExceptionResponse.java │ │ │ ├── config │ │ │ ├── AliyunSmsProperties.java │ │ │ ├── AliyunSmsConfig.java │ │ │ └── AliyunSmsExceptionHandler.java │ │ │ ├── entity │ │ │ └── SmsLog.java │ │ │ └── service │ │ │ └── AliyunSmsApiService.java │ └── pom.xml ├── life-helper-aliyun-captcha │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── weutil │ │ │ └── aliyun │ │ │ └── captcha │ │ │ ├── exception │ │ │ └── AliyunCaptchaVerifiedFailureException.java │ │ │ ├── model │ │ │ └── AliyunCaptchaClient.java │ │ │ ├── config │ │ │ ├── AliyunCaptchaProperties.java │ │ │ ├── AliyunCaptchaExceptionHandler.java │ │ │ └── AliyunCaptchaConfig.java │ │ │ └── service │ │ │ ├── AliyunCaptchaApiService.java │ │ │ └── AliyunCaptchaService.java │ └── pom.xml ├── life-helper-aliyun-oss │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── weutil │ │ │ └── oss │ │ │ ├── model │ │ │ ├── GeneratingPostCredentialDTO.java │ │ │ ├── OssDir.java │ │ │ ├── GeneratingPostCredentialOptions.java │ │ │ └── OssPostCredential.java │ │ │ ├── annotation │ │ │ ├── OssResource.java │ │ │ └── OssResourceJsonSerializer.java │ │ │ ├── config │ │ │ ├── OssProperties.java │ │ │ └── OssConfig.java │ │ │ └── controller │ │ │ └── OssController.java │ └── pom.xml └── pom.xml ├── life-helper-external ├── life-helper-wemap │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── weutil │ │ │ └── wemap │ │ │ ├── exception │ │ │ └── WeMapCommonException.java │ │ │ ├── config │ │ │ └── WeMapProperties.java │ │ │ └── model │ │ │ ├── WeMapListRegionResponse.java │ │ │ ├── WeMapLocateIpResponse.java │ │ │ └── WeMapReverseGeocodeResponse.java │ └── pom.xml └── pom.xml ├── life-helper-account ├── src │ └── main │ │ └── java │ │ └── com │ │ └── weutil │ │ └── account │ │ ├── exception │ │ ├── PhoneCodeAttemptExceededException.java │ │ ├── NotSameIpException.java │ │ ├── UserNotExistException.java │ │ └── PhoneCodeNotMatchException.java │ │ ├── model │ │ ├── BaseUserInfoDTO.java │ │ ├── PhoneCodeLoginDTO.java │ │ ├── SendingSmsDTO.java │ │ ├── LoginChannel.java │ │ ├── LoginType.java │ │ ├── Gender.java │ │ └── BaseUserInfoVO.java │ │ ├── event │ │ ├── UserCreatedEvent.java │ │ └── LoginEvent.java │ │ ├── entity │ │ ├── PhoneAccount.java │ │ ├── LoginLog.java │ │ └── User.java │ │ ├── service │ │ ├── PhoneAccountService.java │ │ └── PhoneCodeLoginService.java │ │ ├── controller │ │ ├── BaseUserInfoController.java │ │ └── PhoneCodeLoginController.java │ │ └── config │ │ └── PhoneCodeLoginExceptionHandler.java └── pom.xml ├── life-helper-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── weutil │ │ └── common │ │ ├── validation │ │ └── group │ │ │ ├── CreateGroup.java │ │ │ └── UpdateGroup.java │ │ ├── exception │ │ ├── UnauthorizedAccessException.java │ │ ├── ServerSideTemporaryException.java │ │ ├── UnpredictableException.java │ │ └── ResourceNotFoundException.java │ │ ├── annotation │ │ ├── ClientIp.java │ │ ├── UserPermission.java │ │ ├── UserId.java │ │ └── resolver │ │ │ ├── UserIdMethodArgumentResolver.java │ │ │ └── ClientIpMethodArgumentResolver.java │ │ ├── model │ │ ├── SingleNumberResponse.java │ │ ├── AccessTokenDetail.java │ │ ├── SingleListResponse.java │ │ ├── ClientType.java │ │ ├── ErrorResponse.java │ │ ├── IdentityCertificate.java │ │ ├── Role.java │ │ ├── CustomRequestAttribute.java │ │ ├── CustomHttpHeader.java │ │ ├── CustomRequestContext.java │ │ ├── SimpleAuthentication.java │ │ └── CustomCachingRequestWrapper.java │ │ ├── entity │ │ ├── BaseUserRelatedEntity.java │ │ ├── BaseEntity.java │ │ └── RequestLog.java │ │ ├── startup │ │ └── TimeZoneSettingRunner.java │ │ ├── config │ │ ├── JacksonConfig.java │ │ ├── RestClientConfig.java │ │ ├── SpringRedisConfig.java │ │ ├── SpringSecurityConfig.java │ │ ├── SpringAsyncConfig.java │ │ ├── WebMvcConfig.java │ │ └── MyBatisFlexConfig.java │ │ ├── filter │ │ ├── InitialFilter.java │ │ ├── DebugSleepFilter.java │ │ ├── TraceIdFilter.java │ │ ├── ClientInfoFilter.java │ │ ├── AccessTokenFilter.java │ │ ├── ClientIpFilter.java │ │ └── RequestLogFilter.java │ │ ├── service │ │ ├── IdentityCertificateService.java │ │ ├── AccessTokenService.java │ │ └── RequestLogService.java │ │ ├── util │ │ └── RandomStringUtil.java │ │ └── interceptor │ │ └── LogInterceptor.java └── pom.xml ├── life-helper-todo ├── src │ └── main │ │ └── java │ │ └── com │ │ └── weutil │ │ └── todo │ │ ├── exception │ │ ├── TodoProjectDuplicateNameException.java │ │ ├── TodoTaskNotFoundException.java │ │ ├── TodoProjectNotFoundException.java │ │ └── TodoProjectFailedToDeleteException.java │ │ ├── event │ │ ├── TodoTaskCreatedEvent.java │ │ ├── TodoTaskDeletedEvent.java │ │ ├── TodoTaskCompletedEvent.java │ │ ├── TodoProjectDeltedEvent.java │ │ ├── TodoTaskUncompletedEvent.java │ │ ├── TodoProjectCreatedEvent.java │ │ ├── TodoTaskEvent.java │ │ ├── TodoProjectEvent.java │ │ ├── TodoTaskMovedEvent.java │ │ └── TodoProjectRenamedEvent.java │ │ ├── model │ │ ├── OperateTodoTaskDTO.java │ │ ├── TodoTaskOperation.java │ │ ├── TodoFilter.java │ │ ├── TodoFilterVO.java │ │ ├── TodoTaskListGroup.java │ │ ├── TodoTaskFilter.java │ │ ├── TodoProjectVO.java │ │ ├── CreateTodoTaskDTO.java │ │ ├── TodoProjectDTO.java │ │ ├── Priority.java │ │ ├── TodoTaskVO.java │ │ └── UpdateTodoTaskDTO.java │ │ ├── config │ │ └── TodoExceptionHandler.java │ │ ├── entity │ │ ├── TodoProject.java │ │ └── TodoTask.java │ │ └── controller │ │ └── TodoFilterController.java └── pom.xml ├── life-helper-web ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── weutil │ │ │ └── web │ │ │ └── Application.java │ │ └── resources │ │ ├── application.yml │ │ └── application-demo.yml └── pom.xml ├── life-helper-system ├── src │ └── main │ │ └── java │ │ └── com │ │ └── weutil │ │ └── system │ │ ├── startup │ │ └── LaunchTimeSavingRunner.java │ │ ├── config │ │ └── PipelineProperties.java │ │ ├── controller │ │ ├── PingController.java │ │ └── SystemController.java │ │ ├── model │ │ └── ServerInfo.java │ │ └── service │ │ ├── LaunchTimeService.java │ │ └── DelayTimeService.java └── pom.xml ├── docker-compose.yml ├── .gitignore └── LICENSE /DevOps/bruno/environments/本地开发环境.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | base_url: https://api-local.weutil.com 3 | } 4 | vars:secret [ 5 | token, 6 | phone 7 | ] 8 | -------------------------------------------------------------------------------- /DevOps/bruno/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "小鸣助手项目接口", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /DevOps/bruno/ping/测试根目录.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 测试根目录 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{base_url}}/ping 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/collection.bru: -------------------------------------------------------------------------------- 1 | headers { 2 | x-weutil-access-token: {{token}} 3 | x-weutil-client-info: type=web; id=weutil.com; version=3.0.0 4 | } 5 | 6 | docs { 7 | 「小鸣助手」项目 API 调试 8 | } 9 | -------------------------------------------------------------------------------- /DevOps/bruno/debug/获取时间相关参数.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 获取时间相关参数 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{base_url}}/debug/time 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /DevOps/bruno/debug/查看服务器信息.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 查看服务器信息 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{base_url}}/debug/system/server 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/ping/MySQL 延迟测试.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: MySQL 延迟测试 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/ping/mysql 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/ping/Redis 延迟测试.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Redis 延迟测试 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/ping/redis 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/project/列表.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 列表 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/reminder/projects 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/project/删除.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 删除 3 | type: http 4 | seq: 5 5 | } 6 | 7 | delete { 8 | url: {{base_url}}/reminder/projects/1 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/filter/显示任务数.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 显示任务数 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/reminder/filters/count 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /mybatis-flex.config: -------------------------------------------------------------------------------- 1 | # MyBatis-Flex APT 配置 2 | # https://mybatis-flex.com/zh/others/apt.html 3 | 4 | # 开启 Mapper 自动生成 5 | processor.mapper.generateEnable=true 6 | 7 | # 开启 @Mapper 注解 8 | processor.mapper.annotation=true 9 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/filter/查看未完成任务数.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 查看未完成任务数 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/reminder/filters/all/count-uncompleted 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 官网地址:https://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/project/创建.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 创建 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{base_url}}/reminder/projects 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | {"name":"待办项目111"} 15 | } 16 | -------------------------------------------------------------------------------- /DevOps/bruno/temp/临时 a.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 临时 a 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/temp/a?phone=13100001115 9 | body: none 10 | auth: none 11 | } 12 | 13 | params:query { 14 | phone: 13100001115 15 | } 16 | -------------------------------------------------------------------------------- /DevOps/bruno/account/获取短信验证码.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 获取短信验证码 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{base_url}}/sms/login 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "phone": "{{phone}}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DevOps/bruno/oss/生成 OSS 直传凭据.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 生成 OSS 直传凭据 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{base_url}}/oss/credential 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "extension": "png" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/project/更新.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 更新 3 | type: http 4 | seq: 4 5 | } 6 | 7 | put { 8 | url: {{base_url}}/reminder/projects/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | {"name":"待办项目888","colorCode":2,"favorite":false} 15 | } 16 | -------------------------------------------------------------------------------- /DevOps/bruno/reminder/project/详情.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 详情 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/reminder/projects/2 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | {"name":"待办项目888","colorCode":2,"favorite":false} 15 | } 16 | -------------------------------------------------------------------------------- /DevOps/bruno/account/短信验证码登录.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 短信验证码登录 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{base_url}}/login/sms 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "phone": "{{phone}}", 16 | "code": "123456" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DevOps/bruno/debug/原样返回请求数据.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 原样返回请求数据 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{base_url}}/debug/data 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "name": "mark", 16 | "age": 19, 17 | "isGood": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DevOps/bruno/account/修改用户基础信息.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 修改用户基础信息 3 | type: http 4 | seq: 3 5 | } 6 | 7 | put { 8 | url: {{base_url}}/user-info/base?rand=3 9 | body: json 10 | auth: none 11 | } 12 | 13 | params:query { 14 | rand: 3 15 | } 16 | 17 | body:json { 18 | { 19 | "nickName": "会飞的猪12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/exception/SmsSentFailureException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.exception; 2 | 3 | /** 4 | * 短信发送失败异常 5 | * 6 | * @author inlym 7 | * @date 2024/7/22 8 | * @since 3.0.0 9 | **/ 10 | public class SmsSentFailureException extends RuntimeException {} 11 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/src/main/java/com/weutil/wemap/exception/WeMapCommonException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.wemap.exception; 2 | 3 | /** 4 | * 腾讯位置服务通用异常 5 | * 6 | * @author inlym 7 | * @date 2024/7/16 8 | * @since 3.0.0 9 | **/ 10 | public class WeMapCommonException extends RuntimeException {} 11 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/exception/PhoneCodeAttemptExceededException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.exception; 2 | 3 | /** 4 | * 使用短信验证码尝试次数超出限制异常 5 | * 6 | * @author inlym 7 | * @date 2024/11/4 8 | * @since 3.0.0 9 | **/ 10 | public class PhoneCodeAttemptExceededException extends RuntimeException {} 11 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/exception/NotSameIpException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.exception; 2 | 3 | /** 4 | * 非相同IP异常 5 | * 6 | *

说明 7 | *

主要用途是防“中间人”攻击。 8 | * 9 | * @author inlym 10 | * @date 2024/11/4 11 | * @since 3.0.0 12 | **/ 13 | public class NotSameIpException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /DevOps/pm2/pm2-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "life-helper-server", 3 | "script": "java", 4 | "args": [ 5 | "-jar", 6 | "-Dspring.profiles.active=dev", 7 | "life-helper-web.jar" 8 | ], 9 | "cwd": "/app", 10 | "watch": false, 11 | "out_file": "/app/logs/life-helper-server-dev.log", 12 | "error_file": "/app/logs/life-helper-server-dev-error.log" 13 | } 14 | -------------------------------------------------------------------------------- /DevOps/pm2/pm2-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "life-helper-server", 3 | "script": "java", 4 | "args": [ 5 | "-jar", 6 | "-Dspring.profiles.active=prod", 7 | "life-helper-web.jar" 8 | ], 9 | "cwd": "/app", 10 | "watch": false, 11 | "out_file": "/app/logs/life-helper-server-prod.log", 12 | "error_file": "/app/logs/life-helper-server-prod-error.log" 13 | } 14 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/validation/group/CreateGroup.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.validation.group; 2 | 3 | /** 4 | * 新增情况校验分组 5 | * 6 | *

说明 7 | *

请求数据实体({@code xxxDTO})将「新增」和「更新」情况共用一个数据模型时,使用分组分别校验字段。 8 | * 9 | * @author inlym 10 | * @date 2024/7/15 11 | * @since 3.0.0 12 | **/ 13 | public interface CreateGroup {} 14 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/validation/group/UpdateGroup.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.validation.group; 2 | 3 | /** 4 | * 更新情况校验分组 5 | * 6 | *

说明 7 | *

请求数据实体({@code xxxDTO})将「新增」和「更新」情况共用一个数据模型时,使用分组分别校验字段。 8 | * 9 | * @author inlym 10 | * @date 2024/7/15 11 | * @since 3.0.0 12 | **/ 13 | public interface UpdateGroup {} 14 | -------------------------------------------------------------------------------- /DevOps/bruno/README.md: -------------------------------------------------------------------------------- 1 | # bruno 说明 2 | 3 | ## 介绍 4 | 5 | 本项目使用 [bruno](https://github.com/usebruno/bruno) 作为 API 调试工具,该工具的优点是: 6 | 7 | 1. 接口数据保存在本地,便于 git 管理。 8 | 2. 界面简单,无多余功能。 9 | 10 | ## 客户端下载 11 | 12 | 点此跳转 [客户端下载地址](https://www.usebruno.com/downloads) 。 13 | 14 | ## 其他工具 15 | 16 | 1. [Postman](https://www.postman.com/) 17 | 2. [Apifox](https://apifox.com/) 18 | 3. [Apipost](https://www.apipost.cn/) 19 | -------------------------------------------------------------------------------- /DevOps/bruno/debug/获取请求详情.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 获取请求详情 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{base_url}}/debug?name=mark&age=19&sleep=200&rand=3 9 | body: json 10 | auth: none 11 | } 12 | 13 | params:query { 14 | name: mark 15 | age: 19 16 | sleep: 200 17 | rand: 3 18 | } 19 | 20 | body:json { 21 | { 22 | "isGood": true, 23 | "nickname": "good boy" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/exception/UserNotExistException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.exception; 2 | 3 | /** 4 | * 用户不存在异常 5 | * 6 | *

使用说明 7 | *

当经过层层判断和处理后,假定某处查询的用户必定存在,而实际查询不存在时,抛出该异常。 8 | * 9 | * @author inlym 10 | * @date 2024/7/22 11 | * @since 3.0.0 12 | **/ 13 | public class UserNotExistException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/exception/UnauthorizedAccessException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.exception; 2 | 3 | /** 4 | * 未授权访问异常 5 | * 6 | *

说明 7 | *

需要提供鉴权信息的 API,未提供或提供了无效的鉴权信息,则抛出此异常。 8 | * 9 | * @author inlym 10 | * @date 2024/7/14 11 | * @since 3.0.0 12 | **/ 13 | public class UnauthorizedAccessException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/exception/InvalidPhoneNumberException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.exception; 2 | 3 | /** 4 | * 手机号码不可用异常 5 | * 6 | *

说明 7 | *

检测待发送的手机号不正确,则抛出此异常。 8 | * 9 | * @author inlym 10 | * @date 2024/7/22 11 | * @since 3.0.0 12 | **/ 13 | public class InvalidPhoneNumberException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/exception/TodoProjectDuplicateNameException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.exception; 2 | 3 | /** 4 | * 待办项目重名异常 5 | * 6 | *

说明 7 | *

创建待办项目时,检测待创建的项目名称已经存在了(非全局,仅针对于同一用户),则抛出此异常。 8 | * 9 | * @author inlym 10 | * @date 2024/12/25 11 | * @since 3.0.0 12 | **/ 13 | public class TodoProjectDuplicateNameException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/exception/PhoneCodeNotMatchException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.exception; 2 | 3 | /** 4 | * 手机号和验证码不匹配异常 5 | * 6 | *

使用说明 7 | *

包含以下2种情况,都抛出这个异常: 8 | *

1. 发了验证码,但是两者不匹配。 9 | *

2. 没发验证码。 10 | * 11 | * @author inlym 12 | * @date 2024/7/22 13 | * @since 3.0.0 14 | **/ 15 | public class PhoneCodeNotMatchException extends RuntimeException {} 16 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | 5 | /** 6 | * 待办任务创建事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/24 10 | * @since 3.0.0 11 | **/ 12 | public class TodoTaskCreatedEvent extends TodoTaskEvent { 13 | public TodoTaskCreatedEvent(TodoTask task) { 14 | super(task); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | 5 | /** 6 | * 待办任务删除事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/25 10 | * @since 3.0.0 11 | **/ 12 | public class TodoTaskDeletedEvent extends TodoTaskEvent { 13 | public TodoTaskDeletedEvent(TodoTask task) { 14 | super(task); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/BaseUserInfoDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 基础用户信息 请求数据 7 | * 8 | * @author inlym 9 | * @date 2024/7/24 10 | * @since 3.0.0 11 | **/ 12 | @Data 13 | public class BaseUserInfoDTO { 14 | /** 昵称 */ 15 | private String nickName; 16 | 17 | /** 头像资源在 OSS 的存储路径 */ 18 | private String avatarKey; 19 | } 20 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | 5 | /** 6 | * 待办任务被标记为“已完成”事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/26 10 | * @since 3.0.0 11 | **/ 12 | public class TodoTaskCompletedEvent extends TodoTaskEvent { 13 | public TodoTaskCompletedEvent(TodoTask task) { 14 | super(task); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoProjectDeltedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoProject; 4 | 5 | /** 6 | * 待办项目删除事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/25 10 | * @since 3.0.0 11 | **/ 12 | public class TodoProjectDeltedEvent extends TodoProjectEvent { 13 | public TodoProjectDeltedEvent(TodoProject project) { 14 | super(project); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskUncompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | 5 | /** 6 | * 待办任务被标记为“未完成”事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/26 10 | * @since 3.0.0 11 | **/ 12 | public class TodoTaskUncompletedEvent extends TodoTaskEvent { 13 | public TodoTaskUncompletedEvent(TodoTask task) { 14 | super(task); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoProjectCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoProject; 4 | 5 | /** 6 | * 待办项目创建事件 7 | * 8 | * @author inlym 9 | * @date 2024/12/25 10 | * @since 3.0.0 11 | **/ 12 | public class TodoProjectCreatedEvent extends TodoProjectEvent { 13 | public TodoProjectCreatedEvent(TodoProject project) { 14 | super(project); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/OperateTodoTaskDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 操作待办任务的请求数据 7 | * 8 | *

说明 9 | *

处理特殊操作,包含以下: 10 | *

(1)完成 11 | *

(2)取消完成 12 | *

(3)清空截止时间 13 | * 14 | * @author inlym 15 | * @date 2024/12/25 16 | * @since 3.0.0 17 | **/ 18 | @Data 19 | public class OperateTodoTaskDTO { 20 | private TodoTaskOperation operation; 21 | } 22 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/model/AliyunSmsClient.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.model; 2 | 3 | import com.aliyun.teaopenapi.models.Config; 4 | 5 | /** 6 | * 二次封装的阿里云短信客户端 7 | * 8 | * @author inlym 9 | * @date 2024/7/16 10 | * @since 3.0.0 11 | **/ 12 | public class AliyunSmsClient extends com.aliyun.dysmsapi20170525.Client { 13 | public AliyunSmsClient(Config config) throws Exception { 14 | super(config); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/exception/ServerSideTemporaryException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.exception; 2 | 3 | /** 4 | * 服务器端临时性的异常 5 | * 6 | *

异常类说明 7 | *

在一些“偏底层”处理(比如建立连接、使用SDK创建客户端等)时,一般很少出现异常,出现时重试几次往往也能成功,无需客户端做出额外处理。出现此类异常时,则抛出当前类。 8 | * 9 | *

客户端处理策略 10 | *

展示服务器给出的“笼统”的提示文案,无需告知真实的错误原因。 11 | * 12 | * @author inlym 13 | * @date 2024/12/15 14 | * @since 3.0.0 15 | **/ 16 | public class ServerSideTemporaryException extends RuntimeException {} 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/exception/TodoTaskNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.exception; 2 | 3 | import com.weutil.common.exception.ResourceNotFoundException; 4 | 5 | /** 6 | * 待办任务未找到异常 7 | * 8 | * @author inlym 9 | * @date 2024/12/23 10 | * @since 3.0.0 11 | **/ 12 | public class TodoTaskNotFoundException extends ResourceNotFoundException { 13 | public TodoTaskNotFoundException(long pkId, long userId) { 14 | super(pkId, userId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoTaskOperation.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | /** 4 | * 待办任务特殊操作 5 | * 6 | * @author inlym 7 | * @date 2024/12/25 8 | * @since 3.0.0 9 | **/ 10 | public enum TodoTaskOperation { 11 | /** 把待办任务标记为“已完成” */ 12 | COMPLETE, 13 | 14 | /** 把待办任务标记为“未完成” */ 15 | UNCOMPLETE, 16 | 17 | /** 清空截止期限 */ 18 | CLEAR_DUE_DATETIME, 19 | 20 | /** 置顶 */ 21 | PIN, 22 | 23 | /** 取消置顶 */ 24 | UNPIN 25 | } 26 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/annotation/ClientIp.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 客户端 IP 地址注入器 7 | * 8 | *

主要用途 9 | *

在控制器方法的参数中注入客户端 IP 地址,以便在方法内部快捷获取和使用。 10 | * 11 | * @author inlym 12 | * @date 2024/7/14 13 | * @since 3.0.0 14 | **/ 15 | @Target(ElementType.PARAMETER) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Documented 18 | public @interface ClientIp { 19 | String value() default ""; 20 | } 21 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/SingleNumberResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 单个数据项响应数据 7 | * 8 | *

说明 9 | *

当响应数据只有一个数据值时,使用当前模型。 10 | * 11 | * @author inlym 12 | * @date 2025/1/8 13 | * @since 3.0.0 14 | **/ 15 | @Data 16 | public class SingleNumberResponse { 17 | /** 数据值 */ 18 | private Long num; 19 | 20 | public SingleNumberResponse(long num) { 21 | this.num = num; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/exception/AliyunCaptchaVerifiedFailureException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.exception; 2 | 3 | /** 4 | * 阿里云验证码校验不通过异常 5 | * 6 | *

客户端处理策略 7 | *

在回调函数 {@code captchaVerifyCallback} 的参数 {@code captchaResult} 返回结果为 {@code false},不需要展示错误提示文案。 8 | * 9 | * @author inlym 10 | * @date 2024/12/15 11 | * @since 3.0.0 12 | **/ 13 | public class AliyunCaptchaVerifiedFailureException extends RuntimeException {} 14 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/exception/TodoProjectNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.exception; 2 | 3 | import com.weutil.common.exception.ResourceNotFoundException; 4 | 5 | /** 6 | * 待办项目未找到异常 7 | * 8 | * @author inlym 9 | * @date 2024/12/13 10 | * @since 3.0.0 11 | **/ 12 | public class TodoProjectNotFoundException extends ResourceNotFoundException { 13 | public TodoProjectNotFoundException(long pkId, long userId) { 14 | super(pkId, userId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/PhoneCodeLoginDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import lombok.Data; 5 | 6 | /** 7 | * 使用短信验证码方式登录的请求数据 8 | * 9 | * @author inlym 10 | * @date 2024/7/22 11 | * @since 3.0.0 12 | **/ 13 | @Data 14 | public class PhoneCodeLoginDTO { 15 | /** 手机号 */ 16 | @NotEmpty 17 | private String phone; 18 | 19 | /** 短信验证码 */ 20 | @NotEmpty 21 | private String code; 22 | } 23 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/SendingSmsDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import lombok.Data; 5 | 6 | /** 7 | * 发送短信验证码的请求数据 8 | * 9 | * @author inlym 10 | * @date 2024/7/22 11 | * @since 3.0.0 12 | **/ 13 | @Data 14 | public class SendingSmsDTO { 15 | /** 手机号 */ 16 | @NotEmpty 17 | private String phone; 18 | 19 | /** 验证码校验参数 */ 20 | @NotEmpty 21 | private String captchaVerifyParam; 22 | } 23 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/AccessTokenDetail.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 访问凭证详情 10 | * 11 | * @author inlym 12 | * @date 2024/7/14 13 | * @since 3.0.0 14 | **/ 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class AccessTokenDetail { 20 | /** 用户 ID */ 21 | private Long userId; 22 | } 23 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/model/AliyunCaptchaClient.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.model; 2 | 3 | import com.aliyun.captcha20230305.Client; 4 | import com.aliyun.teaopenapi.models.Config; 5 | 6 | /** 7 | * 阿里云验证码服务客户端 8 | * 9 | * @author inlym 10 | * @date 2024/10/16 11 | * @since 3.0.0 12 | **/ 13 | public class AliyunCaptchaClient extends Client { 14 | public AliyunCaptchaClient(Config config) throws Exception { 15 | super(config); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/exception/UnpredictableException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.exception; 2 | 3 | /** 4 | * 意料之外的异常 5 | * 6 | *

使用说明 7 | *

用于分支判断中,在认为已包含所有情况处理后,若不匹配已有情况则抛出此异常。 8 | * 9 | *

主要用途 10 | *

出现了这个异常表示出现了之前未考虑到的情况,利于在开发阶段提早发现错误。 11 | * 12 | * @author inlym 13 | * @date 2024/7/14 14 | * @since 3.0.0 15 | **/ 16 | public class UnpredictableException extends RuntimeException { 17 | public UnpredictableException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DevOps/maven/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | aliyunmaven 9 | * 10 | 阿里云公共仓库 11 | https://maven.aliyun.com/repository/public 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/exception/TodoProjectFailedToDeleteException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.exception; 2 | 3 | /** 4 | * 待办项目不允许被删除异常 5 | * 6 | *

以下情况抛出异常 7 | *

情况[1]: 该项目下未完成的任务数不为0 8 | * 9 | *

前端异常处理 10 | *

直接展示对应的错误消息文本 {@code message}。 11 | * 12 | * @author inlym 13 | * @date 2024/12/13 14 | * @since 3.0.0 15 | **/ 16 | public class TodoProjectFailedToDeleteException extends RuntimeException { 17 | public TodoProjectFailedToDeleteException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/src/main/java/com/weutil/wemap/config/WeMapProperties.java: -------------------------------------------------------------------------------- 1 | package com.weutil.wemap.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 腾讯位置服务配置信息 9 | * 10 | * @author inlym 11 | * @date 2024/7/16 12 | * @since 3.0.0 13 | **/ 14 | @Component 15 | @ConfigurationProperties(prefix = "wemap") 16 | @Data 17 | public class WeMapProperties { 18 | /** 开发者密钥 */ 19 | private String key; 20 | } 21 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/SingleListResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 单个列表响应数据 9 | * 10 | *

说明 11 | *

当响应数据只包含 {@code list} 一个属性时,直接使用当前模型进行封装,避免建立重复数据模型。 12 | * 13 | * @author inlym 14 | * @date 2024/7/14 15 | * @since 3.0.0 16 | **/ 17 | @Data 18 | public class SingleListResponse { 19 | /** 列表数据 */ 20 | private List list; 21 | 22 | public SingleListResponse(List list) { 23 | this.list = list; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | import lombok.Getter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | /** 8 | * 待办任务相关事件 9 | * 10 | * @author inlym 11 | * @date 2024/12/25 12 | * @since 3.0.0 13 | **/ 14 | @Getter 15 | public class TodoTaskEvent extends ApplicationEvent { 16 | private final TodoTask task; 17 | 18 | public TodoTaskEvent(TodoTask task) { 19 | super(task); 20 | this.task = task; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/event/UserCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.event; 2 | 3 | import com.weutil.account.entity.User; 4 | import lombok.Getter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | /** 8 | * 用户创建事件 9 | * 10 | * @author inlym 11 | * @date 2024/7/22 12 | * @since 3.0.0 13 | **/ 14 | @Getter 15 | public class UserCreatedEvent extends ApplicationEvent { 16 | private final User user; 17 | 18 | public UserCreatedEvent(User user) { 19 | super(user); 20 | 21 | this.user = user; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/LoginChannel.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import com.mybatisflex.annotation.EnumValue; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 登录渠道 8 | * 9 | * @author inlym 10 | * @date 2024/8/28 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public enum LoginChannel { 15 | /** Web 网页端(www 主站) */ 16 | WEB(1), 17 | 18 | /** 微信小程序 */ 19 | MINI_PROGRAM(2); 20 | 21 | @EnumValue 22 | private final Integer code; 23 | 24 | LoginChannel(int code) { 25 | this.code = code; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/LoginType.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import com.mybatisflex.annotation.EnumValue; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 登录方式 8 | * 9 | * @author inlym 10 | * @date 2024/8/28 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public enum LoginType { 15 | /** 手机号 + 短信验证码 */ 16 | PHONE_SMS(1), 17 | 18 | /** 手机号 + 密码 */ 19 | PHONE_PASSWORD(2); 20 | 21 | @EnumValue 22 | private final Integer code; 23 | 24 | LoginType(int code) { 25 | this.code = code; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/Gender.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import com.mybatisflex.annotation.EnumValue; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 性别 8 | * 9 | * @author inlym 10 | * @date 2024/12/3 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public enum Gender { 15 | /** 未知 */ 16 | UNKNOWN(0), 17 | 18 | /** 男 */ 19 | MALE(1), 20 | 21 | /** 女 */ 22 | FEMALE(2); 23 | 24 | @EnumValue 25 | private final Integer code; 26 | 27 | Gender(int code) { 28 | this.code = code; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 待办任务过滤器 7 | * 8 | * @author inlym 9 | * @date 2025/1/8 10 | * @since 3.0.0 11 | **/ 12 | @Getter 13 | public enum TodoFilter { 14 | /** 所有 */ 15 | ALL, 16 | 17 | /** 收集箱(即未设置项目的) */ 18 | INBOX, 19 | 20 | /** 今天 */ 21 | TODAY, 22 | 23 | /** 最近7天 */ 24 | NEXT_SEVEN_DAYS, 25 | 26 | /** 已过期 */ 27 | OVERDUE, 28 | 29 | /** 未设置截止日期的 */ 30 | NO_DATE, 31 | 32 | /** 已完成 */ 33 | COMPLETED 34 | } 35 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/annotation/UserPermission.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.annotation; 2 | 3 | import com.weutil.common.model.Role; 4 | import org.springframework.security.access.annotation.Secured; 5 | 6 | import java.lang.annotation.*; 7 | 8 | /** 9 | * 用户登录权限鉴权注解 10 | * 11 | *

主要用途 12 | *

拦截需要用户登录的控制器方法,未携带有效登录信息则直接报错。 13 | * 14 | * @author inlym 15 | * @date 2024/7/14 16 | * @since 3.0.0 17 | **/ 18 | @Target({ElementType.METHOD}) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | @Documented 21 | @Secured({Role.USER}) 22 | public @interface UserPermission {} 23 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoProjectEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoProject; 4 | import lombok.Getter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | /** 8 | * 待办项目相关事件 9 | * 10 | * @author inlym 11 | * @date 2024/12/25 12 | * @since 3.0.0 13 | **/ 14 | @Getter 15 | public class TodoProjectEvent extends ApplicationEvent { 16 | private final TodoProject project; 17 | 18 | public TodoProjectEvent(TodoProject project) { 19 | super(project); 20 | this.project = project; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/ClientType.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import com.mybatisflex.annotation.EnumValue; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 客户端类型 8 | * 9 | * @author inlym 10 | * @date 2024/12/12 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public enum ClientType { 15 | /** 未知 */ 16 | UNKNOWN(0), 17 | 18 | /** Web 网页端 */ 19 | WEB(1), 20 | 21 | /** 微信小程序 */ 22 | MINI_PROGRAM(2); 23 | 24 | @EnumValue 25 | private final Integer code; 26 | 27 | ClientType(int code) { 28 | this.code = code; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoFilterVO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 过滤器视图对象 10 | * 11 | * @author inlym 12 | * @date 2024/12/27 13 | * @since 3.0.0 14 | **/ 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class TodoFilterVO { 20 | /** 名称 */ 21 | private String name; 22 | 23 | /** 类型 */ 24 | private TodoTaskFilter type; 25 | 26 | /** 计数(除“已完成”过滤器外,其余均为未完成任务数) */ 27 | private Long num; 28 | } 29 | -------------------------------------------------------------------------------- /life-helper-web/src/main/java/com/weutil/web/Application.java: -------------------------------------------------------------------------------- 1 | package com.weutil.web; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.web.servlet.ServletComponentScan; 7 | 8 | @SpringBootApplication(scanBasePackages = {"com.weutil.**"}) 9 | @ServletComponentScan(basePackages = {"com.weutil.**"}) 10 | @MapperScan(basePackages = {"com.weutil.**"}) 11 | public class Application { 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /life-helper-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | life-helper-common 13 | 14 | ${project.artifactId} 15 | 通用子模块 16 | 17 | 18 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/annotation/UserId.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 用户 ID 注入器 7 | * 8 | *

主要用途 9 | *

在控制器方法的参数中注入用户 ID,以便在方法内部快捷获取和使用。 10 | * 11 | *

注意事项 12 | *
  • 需要结合 {@link UserPermission @UserPermission} 注解使用(登录鉴权通过才会有用户 ID)。 13 | *
  • 在控制器方法参数注入 {@code @UserId long userId}。 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | @Target(ElementType.PARAMETER) 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Documented 22 | public @interface UserId { 23 | String value() default ""; 24 | } 25 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-external 9 | ${revision} 10 | 11 | 12 | life-helper-wemap 13 | 14 | ${project.artifactId} 15 | 腾讯位置服务 16 | 17 | 18 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/entity/BaseUserRelatedEntity.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | /** 10 | * 用户相关类实体基类 11 | * 12 | * @author inlym 13 | * @date 2024/12/25 14 | * @since 3.0.0 15 | **/ 16 | @Data 17 | @EqualsAndHashCode(callSuper = true) 18 | @SuperBuilder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public abstract class BaseUserRelatedEntity extends BaseEntity { 22 | /** 所属的用户 ID */ 23 | private Long userId; 24 | } 25 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/model/GeneratingPostCredentialDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.model; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import lombok.Data; 6 | 7 | /** 8 | * 生成直传凭据的请求数据 9 | * 10 | * @author inlym 11 | * @date 2024/7/15 12 | * @since 3.0.0 13 | **/ 14 | @Data 15 | public class GeneratingPostCredentialDTO { 16 | /** 17 | * 文件后缀名 18 | * 19 | *

    示例值 20 | *

    {@code png} 21 | */ 22 | @NotBlank 23 | @Pattern(regexp = "^(png|jpg|jpeg|gif|webp)$", message = "请选择正确的图片上传!") 24 | private String extension; 25 | } 26 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/model/OssDir.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.model; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 在 OSS 中使用的目录 7 | * 8 | * @author inlym 9 | * @date 2024/7/15 10 | * @since 3.0.0 11 | **/ 12 | @Getter 13 | public enum OssDir { 14 | /** 用户头像 */ 15 | AVATAR("avatar"), 16 | 17 | /** 微信小程序码 */ 18 | WEACODE("wxacode"), 19 | 20 | /** 用户上传使用 */ 21 | UPLOAD("upload"), 22 | 23 | /** 临时使用的目录,一般仅用于开发阶段调试 */ 24 | TEMP("temp"); 25 | 26 | /** 目录名 */ 27 | private final String dirname; 28 | 29 | OssDir(String dirname) { 30 | this.dirname = dirname; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/config/TodoExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.core.Ordered; 5 | import org.springframework.core.annotation.Order; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | 8 | /** 9 | * 待办清单模块异常捕获器 10 | * 11 | *

    错误码范围 12 | *

    总体范围: {@code 13xxx} 13 | *

    待办项目部分(project): {@code 131xx} 14 | *

    待办任务部分(task): {@code 132xx} 15 | * 16 | * @author inlym 17 | * @date 2024/12/26 18 | * @since 3.0.0 19 | **/ 20 | @RestControllerAdvice 21 | @Slf4j 22 | @Order(Ordered.HIGHEST_PRECEDENCE + 1050) 23 | public class TodoExceptionHandler {} 24 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/exception/SmsRateLimitExceededException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.exception; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 短信发送速率超出限制异常 7 | * 8 | * @author inlym 9 | * @date 2024/7/22 10 | * @since 3.0.0 11 | **/ 12 | @Getter 13 | public class SmsRateLimitExceededException extends RuntimeException { 14 | /** 15 | * 剩余的等待秒数 16 | * 17 | *

    说明 18 | *

    即下一次可以发送短信距此刻的秒数 19 | */ 20 | private final Long remainingSeconds; 21 | 22 | public SmsRateLimitExceededException(long remainingSeconds) { 23 | super(); 24 | 25 | this.remainingSeconds = remainingSeconds; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoTaskListGroup.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 待办任务列表组合 12 | * 13 | *

    说明 14 | *

    包含“已完成”和“未完成”等2份列表。 15 | * 16 | * @author inlym 17 | * @date 2024/12/24 18 | * @since 3.0.0 19 | **/ 20 | @Data 21 | @Builder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public class TodoTaskListGroup { 25 | /** 未完成(进行中)的待办任务列表 */ 26 | private List uncompleted; 27 | 28 | /** 已完成的待办任务列表 */ 29 | private List completed; 30 | } 31 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoTaskMovedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoTask; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 待办任务移动项目事件 8 | * 9 | * @author inlym 10 | * @date 2024/12/25 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public class TodoTaskMovedEvent extends TodoTaskEvent { 15 | /** 移动前的项目 ID */ 16 | private final Long originProjectId; 17 | 18 | /** 移动后的项目 ID */ 19 | private final Long targetProjectId; 20 | 21 | public TodoTaskMovedEvent(TodoTask task, long originProjectId, long targetProjectId) { 22 | super(task); 23 | 24 | this.originProjectId = originProjectId; 25 | this.targetProjectId = targetProjectId; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoTaskFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 待办任务查询过滤器 7 | * 8 | * @author inlym 9 | * @date 2024/12/25 10 | * @since 3.0.0 11 | **/ 12 | @Getter 13 | public enum TodoTaskFilter { 14 | /** 所有待办(未完成) */ 15 | ALL_UNCOMPLETED("所有待办"), 16 | 17 | /** 已完成 */ 18 | ALL_COMPLETED("已完成"), 19 | 20 | /** 今天 */ 21 | TODAY("今天"), 22 | 23 | /** 已过期 */ 24 | EXPIRED("已过期"), 25 | 26 | /** 无期限 */ 27 | UNDATED("无期限"), 28 | 29 | /** 未分类(未选择项目的) */ 30 | UNSPECIFIED("未分类"); 31 | 32 | /** 过滤器名称 */ 33 | private final String name; 34 | 35 | TodoTaskFilter(String name) { 36 | this.name = name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/config/AliyunSmsProperties.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 阿里云短信服务配置信息 9 | * 10 | *

    主要用途 11 | *

    管理阿里云短信服务的相关密钥信息。 12 | * 13 | * @author inlym 14 | * @date 2024/7/16 15 | * @since 3.0.0 16 | **/ 17 | @Component 18 | @ConfigurationProperties(prefix = "aliyun.sms") 19 | @Data 20 | public class AliyunSmsProperties { 21 | /** 访问密钥 ID */ 22 | private String accessKeyId; 23 | 24 | /** 访问密钥口令 */ 25 | private String accessKeySecret; 26 | 27 | /** 短信签名名称 */ 28 | private String signName; 29 | } 30 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 错误响应 7 | * 8 | *

    主要用途 9 | *

    当出现异常情况无法返回正常响应时,返回这个错误响应。 10 | * 11 | *

    错误码说明 12 | *
  • [0]:请求成功,非错误响应。 13 | *
  • [1~9999]:系统级错误,客户端需要在全局拦截器中处理。 14 | *
  • [ >9999 ]:业务级错误,在各个业务代码中单独处理。 15 | * 16 | * @author inlym 17 | * @date 2024/7/14 18 | * @since 3.0.0 19 | **/ 20 | @Data 21 | public class ErrorResponse { 22 | /** 错误码 */ 23 | private Integer errorCode; 24 | 25 | /** 错误消息 */ 26 | private String errorMessage; 27 | 28 | public ErrorResponse(int errorCode, String errorMessage) { 29 | this.errorCode = errorCode; 30 | this.errorMessage = errorMessage; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoProjectVO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 待办项目视图对象 10 | * 11 | *

    说明 12 | *

    用于客户端展示使用。 13 | * 14 | * @author inlym 15 | * @date 2024/12/13 16 | * @since 3.0.0 17 | **/ 18 | @Data 19 | @Builder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class TodoProjectVO { 23 | /** 主键 ID */ 24 | private Long id; 25 | 26 | /** 项目名称 */ 27 | private String name; 28 | 29 | /** emoji 图标 */ 30 | private String emoji; 31 | 32 | /** 颜色名称 */ 33 | private String color; 34 | 35 | /** 是否收藏 */ 36 | private Boolean favorite; 37 | } 38 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.exception; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 资源未找到异常 7 | * 8 | *

    触发条件 9 | *

    (1)使用主键 ID 查找资源时,未找到资源(可能是不存在或者已被删除)。 10 | *

    (2)能够找到对应 ID 的资源,但并不归属于当前用户。 11 | * 12 | *

    使用说明 13 | *

    不要直接使用这个类,而是在各个模块内继承这个类,抛出子类。 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | @Getter 20 | public class ResourceNotFoundException extends RuntimeException { 21 | /** 主键 ID */ 22 | private final Long pkId; 23 | 24 | /** 用户 ID */ 25 | private final Long userId; 26 | 27 | public ResourceNotFoundException(long pkId, long userId) { 28 | this.pkId = pkId; 29 | this.userId = userId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/model/BaseUserInfoVO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.model; 2 | 3 | import com.weutil.oss.annotation.OssResource; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | /** 10 | * 基础用户信息视图对象 11 | * 12 | * @author inlym 13 | * @date 2024/7/22 14 | * @since 3.0.0 15 | **/ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class BaseUserInfoVO { 21 | /** 昵称 */ 22 | private String nickName; 23 | 24 | /** 头像的完整 URL 地址 */ 25 | @OssResource 26 | private String avatarUrl; 27 | 28 | /** 29 | * 账户 ID 30 | * 31 | *

    字段说明 32 | *

    客户端展示名称为 UID 33 | */ 34 | private Long accountId; 35 | } 36 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/config/AliyunCaptchaProperties.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 阿里云验证码服务配置信息 9 | * 10 | *

    主要用途 11 | *

    管理阿里云验证码服务的相关密钥信息。 12 | * 13 | * @author inlym 14 | * @date 2024/10/16 15 | * @since 3.0.0 16 | **/ 17 | @Component 18 | @ConfigurationProperties(prefix = "aliyun.captcha") 19 | @Data 20 | public class AliyunCaptchaProperties { 21 | /** 访问密钥 ID */ 22 | private String accessKeyId; 23 | 24 | /** 访问密钥口令 */ 25 | private String accessKeySecret; 26 | 27 | /** 场景 ID */ 28 | private String sceneId; 29 | } 30 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/event/TodoProjectRenamedEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.event; 2 | 3 | import com.weutil.todo.entity.TodoProject; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 待办项目重命名事件 8 | * 9 | * @author inlym 10 | * @date 2024/12/25 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public class TodoProjectRenamedEvent extends TodoProjectEvent { 15 | /** 改名前的项目名称 */ 16 | private final String originProjectName; 17 | 18 | /** 改名后的项目名称 */ 19 | private final String targetProjectName; 20 | 21 | public TodoProjectRenamedEvent(TodoProject project, String originProjectName, String targetProjectName) { 22 | super(project); 23 | 24 | this.originProjectName = originProjectName; 25 | this.targetProjectName = targetProjectName; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/model/GeneratingPostCredentialOptions.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.Duration; 9 | 10 | /** 11 | * 生成直传凭据的配置项 12 | * 13 | * @author inlym 14 | * @date 2024/7/15 15 | * @since 3.0.0 16 | **/ 17 | @Data 18 | @Builder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class GeneratingPostCredentialOptions { 22 | /** 23 | * 文件后缀名 24 | * 25 | *

    示例值 26 | *

    {@code png} 27 | */ 28 | private String extension; 29 | 30 | /** 限制最大上传文件大小,单位为 MD */ 31 | private Long sizeMB; 32 | 33 | /** 直传凭据有效时长 */ 34 | private Duration duration; 35 | } 36 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/startup/TimeZoneSettingRunner.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.startup; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.CommandLineRunner; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.TimeZone; 8 | 9 | /** 10 | * 时区设置任务 11 | * 12 | *

    说明 13 | *

    在 Docker 中,默认使用了 GMT 时区,而项目运行时需要以 "Asia/Shanghai" 时区运行。 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | @Component 20 | @Slf4j 21 | public class TimeZoneSettingRunner implements CommandLineRunner { 22 | @Override 23 | public void run(String... args) throws Exception { 24 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); 25 | log.trace("[启动时任务] 已将时区设置为 Asia/Shanghai"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/CreateTodoTaskDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import jakarta.validation.constraints.NotNull; 6 | import lombok.Data; 7 | 8 | /** 9 | * 创建待办任务操作的请求数据 10 | * 11 | *

    说明 12 | *

    该对象仅用于“新增”和情况。 13 | * 14 | * @author inlym 15 | * @date 2024/12/13 16 | * @since 3.0.0 17 | **/ 18 | @Data 19 | public class CreateTodoTaskDTO { 20 | /** 任务名称 */ 21 | @NotEmpty(message = "项目名称不能为空!") 22 | private String name; 23 | 24 | /** 25 | * 所属项目 ID 26 | * 27 | *

    说明 28 | *

    该值为 {@code 0} 则表示不从属于任何项目。 29 | */ 30 | @Min(value = 0, message = "你选择的项目不存在,请刷新后重试!") 31 | @NotNull 32 | private Long projectId; 33 | } 34 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/model/SmsRateLimitExceededExceptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.model; 2 | 3 | import com.weutil.common.model.ErrorResponse; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 短信发送速率超出限制异常响应数据 8 | * 9 | * @author inlym 10 | * @date 2024/7/22 11 | * @since 3.0.0 12 | **/ 13 | @Getter 14 | public class SmsRateLimitExceededExceptionResponse extends ErrorResponse { 15 | /** 16 | * 剩余的等待秒数 17 | * 18 | *

    说明 19 | *

    即下一次可以发送短信距此刻的秒数 20 | */ 21 | private final Long remainingSeconds; 22 | 23 | public SmsRateLimitExceededExceptionResponse(int errorCode, String errorMessage, long remainingSeconds) { 24 | super(errorCode, errorMessage); 25 | 26 | this.remainingSeconds = remainingSeconds; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DevOps/pm2/README.md: -------------------------------------------------------------------------------- 1 | # pm2 2 | 3 | ## 说明 4 | 5 | 使用 **pm2** 来部署 *jar* 包,作为一种可选的项目部署方式,具有以下优点: 6 | 7 | 1. 比打包成 Docker 镜像再部署的方式显得更轻量一些,适合作为测试环境的部署方式。 8 | 2. 比原生使用 `nohup` 方式运行 *jar* 包,相对更容易维护。 9 | 10 | ## 前置条件 11 | 12 | **pm2** 是一个 *Node.js* 库,需要安装 *Node.js* 和 *npm* 。之后再全局安装 **pm2**,命令如下: 13 | 14 | ```shell 15 | $ npm install -g pm2 16 | ``` 17 | 18 | ## 配置 19 | 20 | 当前目录下的 **pm2.json** 是运行项目的配置文件,关于各个配置项的含义,可参考 [ECOSYSTEM FILE](https://pm2.keymetrics.io/docs/usage/application-declaration/) 上的说明。 21 | 22 | ## 运行 23 | 24 | 首次运行时,将当前目录的 **pm2.json** 文件和构建产生的 `life-helper-web.jar` 文件复制到部署服务器的 `/app` (在 **pm2.json** 配置)目录下,然后进入目录运行以下命令: 25 | 26 | ```shell 27 | $ pm2 start pm2-dev.json 28 | ``` 29 | 30 | 资源更新后,重启命令为: 31 | 32 | ```shell 33 | $ pm2 reload life-helper-server 34 | ``` 35 | 36 | ## 更多 37 | 38 | 关于 **pm2** 的更多使用方式,可查看 [官网文档](https://pm2.keymetrics.io/docs/usage/quick-start/) 。 39 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/IdentityCertificate.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | /** 11 | * 身份证书 12 | * 13 | *

    主要用途 14 | *

    在用户鉴权通过后(即完成登录),发放身份证书,用于证明其身份。证书中包含鉴权令牌以及使用方法。 15 | * 16 | * @author inlym 17 | * @date 2024/7/14 18 | * @since 3.0.0 19 | **/ 20 | @Data 21 | @Builder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public class IdentityCertificate { 25 | /** 鉴权令牌 */ 26 | private String token; 27 | 28 | /** 发起请求时,携带令牌的请求头名称 */ 29 | private String headerName; 30 | 31 | /** 创建时间 */ 32 | private LocalDateTime createTime; 33 | 34 | /** 过期时间 */ 35 | private LocalDateTime expireTime; 36 | } 37 | -------------------------------------------------------------------------------- /life-helper-todo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | life-helper-todo 13 | 14 | ${project.artifactId} 15 | 待办任务模块 16 | 17 | 18 | 19 | com.weutil 20 | life-helper-common 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/startup/LaunchTimeSavingRunner.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.startup; 2 | 3 | import com.weutil.system.service.LaunchTimeService; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * 项目启动时间记录任务 11 | * 12 | *

    说明 13 | *

    记录项目的本期启动时间,以便排查项目是否正常启动。 14 | * 15 | * @author inlym 16 | * @date 2024/12/3 17 | * @since 3.0.0 18 | **/ 19 | @Component 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | public class LaunchTimeSavingRunner implements CommandLineRunner { 23 | private final LaunchTimeService launchTimeService; 24 | 25 | @Override 26 | public void run(String... args) { 27 | launchTimeService.recordOnStartUp(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /life-helper-web/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | 3 | profiles: 4 | # 启用的环境,目前包含 dev 和 prod 5 | active: dev 6 | 7 | main: 8 | # 关闭启动 Banner 9 | banner-mode: off 10 | # 关闭延迟初始化 11 | lazy-initialization: false 12 | 13 | jackson: 14 | # 时区 15 | time-zone: GMT+8 16 | # 日期格式化 17 | date-format: yyyy-MM-dd HH:mm:ss 18 | # 返回响应移除 null 值字段 19 | default-property-inclusion: non_null 20 | 21 | # 缓存配置 22 | cache: 23 | type: redis 24 | 25 | # Spring Security 配置 26 | security: 27 | filter: 28 | # 鉴权过滤器排序 29 | order: 10000 30 | # 内置的一个账户鉴权方式 31 | user: 32 | name: NOT_USER 33 | password: NOT_USER 34 | roles: 35 | - NOT_USER 36 | 37 | server: 38 | # 优雅关闭 39 | shutdown: graceful 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 本地开发环境使用 2 | 3 | version: "3.9" 4 | 5 | services: 6 | mysql: 7 | image: mysql 8 | environment: 9 | # 设置 root 用户密码 10 | MYSQL_ROOT_PASSWORD: "123456" 11 | # 创建一个初始数据库 12 | MYSQL_DATABASE: "lifehelper_db_dev" 13 | networks: 14 | - backend 15 | ports: 16 | - "3306:3306" 17 | volumes: 18 | - mysql-data:/var/lib/mysql 19 | 20 | redis: 21 | image: redis 22 | environment: 23 | TZ: Asia/Shanghai 24 | 25 | networks: 26 | - backend 27 | ports: 28 | - "6379:6379" 29 | volumes: 30 | - ./data/redis/:/data/ 31 | 32 | 33 | networks: 34 | backend: 35 | driver: bridge 36 | 37 | volumes: 38 | mysql-data: 39 | driver: local 40 | redis-data: 41 | driver: local 42 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/Role.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | /** 4 | * 自定义角色 5 | * 6 | * @author inlym 7 | * @date 2024/7/14 8 | * @since 3.0.0 9 | **/ 10 | public abstract class Role { 11 | /** 12 | * 普通用户 13 | * 14 | *

    获取方式 15 | *

    通过正常方式登录系统 16 | */ 17 | public static final String USER = "ROLE_USER"; 18 | 19 | /** 20 | * 开发者 21 | * 22 | *

    获取方式 23 | *

    获取普通用户身份后再二次鉴权 24 | * 25 | *

    主要用途 26 | *

    少量接口可获取系统非公开信息,方便开源学习者进行调试。 27 | */ 28 | public static final String DEVELOPER = "ROLE_DEVELOPER"; 29 | 30 | /** 31 | * 管理员 32 | * 33 | *

    获取方式 34 | *

    特殊方式或手动生成鉴权信息 35 | * 36 | *

    主要用途 37 | *

    用于调试部分对系统可能有一定破坏性的接口 38 | */ 39 | public static final String ADMIN = "ROLE_ADMIN"; 40 | } 41 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/config/PipelineProperties.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 流水线信息 9 | * 10 | *

    说明 11 | *

    CI/CD 阶段的流水线环境变量信息。 12 | * 13 | * @author inlym 14 | * @date 2024/12/11 15 | * @see 环境变量 16 | * @since 3.0.0 17 | **/ 18 | @Component 19 | @ConfigurationProperties(prefix = "pipeline") 20 | @Data 21 | public class PipelineProperties { 22 | /** 流水线的运行编号,从1开始,按自然数自增 */ 23 | private String buildNumber; 24 | 25 | /** 本次部署代码版本的 commit ID */ 26 | private String commitSha; 27 | 28 | /** 本次部署代码版本的分支名或标签名 */ 29 | private String commitRefName; 30 | } 31 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/controller/PingController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | /** 8 | * 连通性测试控制器 9 | * 10 | * @author inlym 11 | * @date 2024/7/14 12 | * @since 3.0.0 13 | **/ 14 | @RestController 15 | @RequiredArgsConstructor 16 | public class PingController { 17 | 18 | /** 19 | * 接口连通性测试 20 | * 21 | *

    主要用途 22 | *

    用于负载均衡健康检查,只要项目没挂,这个接口就可以正常返回。 23 | * 24 | *

    注意事项 25 | *

    项目内部进行日志记录时,不要记录当前接口,因为负载均衡的健康检查频率为1次/秒,记录日志会造成大量无效日志。 26 | * 27 | * @date 2024/6/30 28 | * @since 2.3.0 29 | */ 30 | @GetMapping("/ping") 31 | public String ping() { 32 | return "pong"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### Bruno ### 36 | /DevOps/bruno/environments/加密开发环境.bru 37 | 38 | ### application-{name}.yml ### 39 | /life-helper-web/src/main/resources/application-dev.yml 40 | /life-helper-web/src/main/resources/application-prod.yml 41 | 42 | ### flatten-maven-plugin 插件生成的文件 ### 43 | .flattened-pom.xml 44 | 45 | ### 临时测试文件 ### 46 | /life-helper-web/src/main/java/com/weutil/web/temp/ 47 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/entity/PhoneAccount.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.entity; 2 | 3 | import com.mybatisflex.annotation.Table; 4 | import com.weutil.common.entity.BaseUserRelatedEntity; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | /** 12 | * 用户手机号账户关联表 13 | * 14 | *

    说明 15 | *

    通过手机号关联到用户账户实体 16 | * 17 | * @author inlym 18 | * @date 2024/7/22 19 | * @since 3.0.0 20 | **/ 21 | 22 | @Table("account_phone") 23 | @Data 24 | @EqualsAndHashCode(callSuper = true) 25 | @SuperBuilder 26 | @NoArgsConstructor 27 | @AllArgsConstructor 28 | public class PhoneAccount extends BaseUserRelatedEntity { 29 | 30 | // ---------- 账户关联表差异项 ---------- 31 | 32 | /** 手机号(仅支持国内手机号) */ 33 | private String phone; 34 | } 35 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/CustomRequestAttribute.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | /** 4 | * 自定义请求域属性 5 | * 6 | * @author inlym 7 | * @date 2024/12/12 8 | * @since 3.0.0 9 | **/ 10 | public abstract class CustomRequestAttribute { 11 | /** 追踪 ID */ 12 | public static final String TRACE_ID = "TRACE_ID"; 13 | 14 | /** 访问令牌 */ 15 | public static final String ACCESS_TOKEN = "ACCESS_TOKEN"; 16 | 17 | /** 客户端 IP 地址 */ 18 | public static final String CLIENT_IP = "CLIENT_IP"; 19 | 20 | /** 用户 ID */ 21 | public static final String USER_ID = "USER_ID"; 22 | 23 | /** 客户端类型 */ 24 | public static final String CLIENT_TYPE = "CLIENT_TYPE"; 25 | 26 | /** 客户端 ID */ 27 | public static final String CLIENT_ID = "CLIENT_ID"; 28 | 29 | /** 客户端版本 */ 30 | public static final String CLIENT_VERSION = "CLIENT_VERSION"; 31 | } 32 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-aliyun 9 | ${revision} 10 | 11 | 12 | life-helper-aliyun-sms 13 | 14 | ${project.artifactId} 15 | 封装阿里云短信(SMS)服务 16 | 17 | 18 | 19 | 20 | com.aliyun 21 | dysmsapi20170525 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DevOps/nginx/api-local.weutil.com.nginx.conf: -------------------------------------------------------------------------------- 1 | # [SSL 证书] 2 | # 将 SSL 证书文件复制到 nginx/conf/cert 目录下,包含 pem 和 key 两份文件。 3 | 4 | # [使用说明] 5 | # 使用方法[1]: 把当前文件的内容复制到在 nginx/conf/nginx.conf 文件的 http 模块内。 6 | # 使用方法[2]: 将当前文件复制到 /etc/nginx/conf.d 目录下。 7 | 8 | 9 | # 本地开发环境 nginx 配置 10 | server { 11 | listen 443 ssl; 12 | http2 on; 13 | server_name api-local.weutil.com; 14 | 15 | ssl_certificate cert/api-local.weutil.com.pem; 16 | ssl_certificate_key cert/api-local.weutil.com.key; 17 | 18 | location / { 19 | proxy_pass http://127.0.0.1:23010; 20 | 21 | # 在生产环境上,以下请求头将由 API 网关自动传递,这里仅用于模拟 22 | proxy_set_header x-forwarded-for 114.114.114.114,1.1.1.1; 23 | proxy_set_header x-ca-request-id $request_id; 24 | } 25 | } 26 | 27 | # 配置 HTTP 重定向 28 | server { 29 | listen 80; 30 | server_name api-local.weutil.com; 31 | 32 | # 重定向所有 HTTP 请求到 HTTPS 33 | return 301 https://$server_name$request_uri; 34 | } 35 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-aliyun 9 | ${revision} 10 | 11 | 12 | life-helper-aliyun-captcha 13 | 14 | ${project.artifactId} 15 | 封装阿里云验证码服务 16 | 17 | 18 | 19 | com.aliyun 20 | captcha20230305 21 | 1.1.2 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /DevOps/script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################### 项目部署脚本 ################################### 4 | 5 | ### 运行说明 6 | # 1. 需要在各个应用服务器(即部署机)上都运行一遍。 7 | 8 | ### 运行环境 9 | # 1. 由“云效”执行自动化脚本,在部署机上运行。 10 | # 2. 部署机即应用服务器,处于 VPC 内。 11 | 12 | ### 文档地址 13 | # 1. 云效-环境变量 -> https://help.aliyun.com/document_detail/153688.html?userCode=lzfqdh6g 14 | # 2. 阿里云-容器镜像服务 -> https://www.aliyun.com/product/acr?userCode=lzfqdh6g 15 | 16 | # 镜像仓库地址 17 | echo "${DOCKER_REPOSITORY_VPC}" 18 | 19 | # 构建使用的 Git Commit 的标签名,例如 `1.0.0` 20 | echo "${CI_COMMIT_REF_NAME}" 21 | 22 | # 镜像名称 23 | IMAGE_NAME="${DOCKER_REPOSITORY_VPC}:${CI_COMMIT_REF_NAME}" 24 | 25 | # 需要先拉取镜像再停止容器,否则中断时间会很久 26 | docker pull "${IMAGE_NAME}" 27 | 28 | # 停止运行并删除旧的容器 29 | docker rm -f lifehelper_server 30 | 31 | # 运行新的容器 32 | # 注意:因为网络模式指定了 `host`,因此 `-p` 参数是无效的,所以去掉了。 33 | docker run -d --name=lifehelper_server --network=host --restart=always "${IMAGE_NAME}" 34 | 35 | # 删除已弃用的镜像 36 | docker image prune -af 37 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/CustomHttpHeader.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | /** 4 | * 自定义的 HTTP 请求头 5 | * 6 | *

    来源 7 | *
  • 1. API 网关层传入 8 | *
  • 2. 项目自定义 9 | * 10 | *

    命名规范 11 | *
  • 为避免冲突,项目自定义的请求头均设定以 {@code x-weutil-} 开头 12 | * 13 | * @author inlym 14 | * @date 2024/7/14 15 | * @since 3.0.0 16 | **/ 17 | public abstract class CustomHttpHeader { 18 | /** 请求 ID,用作全链路追踪 ID */ 19 | public static final String REQUEST_ID = "x-ca-request-id"; 20 | 21 | /** 访问令牌 */ 22 | public static final String ACCESS_TOKEN = "x-weutil-access-token"; 23 | 24 | /** 客户端 IP 地址 */ 25 | public static final String CLIENT_IP = "x-forwarded-for"; 26 | 27 | /** 28 | * 客户端信息 29 | * 30 | *

    说明 31 | *

    1. 组合多项键值对,使用 `; ` 分割。 32 | *

    2. 文本格式示例:`key1=value1; key2=value2; key3=value3` 33 | */ 34 | public static final String CLIENT_INFO = "x-weutil-client-info"; 35 | } 36 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoProjectDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import com.weutil.common.validation.group.CreateGroup; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import jakarta.validation.constraints.Size; 6 | import lombok.Data; 7 | 8 | /** 9 | * 保存待办项目操作的请求数据 10 | * 11 | *

    说明 12 | *

    该对象同时用于“新增”和“编辑”情况。 13 | * 14 | * @author inlym 15 | * @date 2024/12/13 16 | * @since 3.0.0 17 | **/ 18 | @Data 19 | public class TodoProjectDTO { 20 | /** 项目名称 */ 21 | @NotEmpty(message = "项目名称不能为空", groups = {CreateGroup.class}) 22 | @Size(max = 20, message = "项目名称最多20个字") 23 | private String name; 24 | 25 | /** 26 | * emoji 图标 27 | * 28 | *

    说明 29 | *

    单字符。 30 | */ 31 | @Size(max = 1, message = "你最多只能选择1个emoji图标") 32 | private String emoji; 33 | 34 | /** 颜色名称 */ 35 | private String color; 36 | 37 | /** 是否收藏 */ 38 | private Boolean favorite; 39 | } 40 | -------------------------------------------------------------------------------- /life-helper-external/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | 13 | life-helper-wemap 14 | 15 | 16 | pom 17 | 18 | life-helper-external 19 | 20 | ${project.artifactId} 21 | 调用外部 API 服务 22 | 23 | 24 | 25 | com.weutil 26 | life-helper-common 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/event/LoginEvent.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.event; 2 | 3 | import com.weutil.account.entity.LoginLog; 4 | import lombok.Getter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | /** 10 | * 用户登录事件 11 | * 12 | *

    说明 13 | *

    当前事件是其他登录事件的父类。 14 | * 15 | * @author inlym 16 | * @date 2024/7/22 17 | * @since 3.0.0 18 | **/ 19 | @Getter 20 | public class LoginEvent extends ApplicationEvent { 21 | /** 原始登录日志 */ 22 | private final LoginLog loginLog; 23 | 24 | /** 对应的用户 ID */ 25 | private final Long userId; 26 | 27 | /** 客户端 IP 地址 */ 28 | private final String ip; 29 | 30 | /** 登录时间 */ 31 | private final LocalDateTime loginTime; 32 | 33 | public LoginEvent(LoginLog source) { 34 | super(source); 35 | 36 | this.userId = source.getUserId(); 37 | this.ip = source.getIp(); 38 | this.loginTime = source.getLoginTime(); 39 | this.loginLog = source; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DevOps/script/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################## 初始化服务器脚本 ################################### 4 | 5 | ### 运行说明 6 | # 1. 当前脚本用于服务器初始化,仅在服务器重置后(包含新购机器、重装系统等)执行一次即可。 7 | # 2. 在日常 CI/CD 过程中,无需执行当前脚本。 8 | 9 | ### 运行环境 10 | # 1. 由“云效”执行自动化脚本,环境变量也由“云效”注入。 11 | # 2. 部署机(即应用服务器)只需要 Docker 环境即可,Docker 镜像托管在阿里云容器镜像服务。 12 | 13 | ### 文档地址 14 | # 1. 云效-环境变量 -> https://help.aliyun.com/document_detail/153688.html?userCode=lzfqdh6g 15 | # 2. 阿里云-容器镜像服务 -> https://www.aliyun.com/product/acr?userCode=lzfqdh6g 16 | 17 | # 阿里云容器镜像服务的 Docker Registry,为方便运行,从环境变量获取 18 | # 由于服务器处理 VPC 环境内,在获取镜像时,从 VPC 获取更稳定,网速更快 19 | # DOCKER_REGISTRY=registry.cn-hangzhou.aliyuncs.com 20 | # DOCKER_REGISTRY_VPC=registry-vpc.cn-hangzhou.aliyuncs.com 21 | echo "${DOCKER_REGISTRY_VPC}" 22 | 23 | # 在阿里云容器镜像服务中使用的用户名 24 | echo "${DOCKER_USERNAME}" 25 | 26 | # 在阿里云容器镜像服务中使用的密码 27 | echo "${DOCKER_PASSWORD}" 28 | 29 | # 更新软件清单 30 | apt update 31 | 32 | # 安装 Docker 33 | apt install -y docker.io 34 | 35 | # 登录 Docker 36 | echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin "${DOCKER_REGISTRY_VPC}" 37 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/annotation/OssResource.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.annotation; 2 | 3 | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * 阿里云 OSS 资源注解 13 | * 14 | *

    主要用途 15 | *

    标记该注解的字段,返回响应时自动将 OSS 中的路径转化为完整可访问的 URL 地址。 16 | * 17 | *

    使用效果示例 18 | *

    原字段值为 {@code temp/4rqE00X6qXg0.jpg},使用该注解标记后,响应中的值为 {@code https://res.weutil.com/temp/4rqE00X6qXg0 19 | * .jpg?Expires=1720599451&OSSAccessKeyId=LTAI5tKLP9qGYxi3AUwFNaZV&Signature=G4dgAW6tpUZv%2Bmub%2FRJ%2B3nQ9hSo%3D}。 20 | * 21 | * @author inlym 22 | * @date 2024/7/18 23 | * @since 3.0.0 24 | **/ 25 | @Retention(RetentionPolicy.RUNTIME) 26 | @Target(ElementType.FIELD) 27 | @JacksonAnnotationsInside 28 | @JsonSerialize(using = OssResourceJsonSerializer.class) 29 | public @interface OssResource {} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 inlym 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /life-helper-aliyun/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | 13 | life-helper-aliyun-oss 14 | life-helper-aliyun-sms 15 | life-helper-aliyun-captcha 16 | 17 | 18 | 19 | pom 20 | 21 | life-helper-aliyun 22 | 23 | ${project.artifactId} 24 | 封装阿里云服务 25 | 26 | 27 | 28 | com.weutil 29 | life-helper-common 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/JacksonConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; 4 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 5 | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.time.format.DateTimeFormatter; 10 | 11 | /** 12 | * Jackson 配置 13 | * 14 | * @author inlym 15 | * @date 2024/7/14 16 | * @since 3.0.0 17 | **/ 18 | @Configuration 19 | public class JacksonConfig { 20 | @Bean 21 | public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { 22 | return builder -> { 23 | builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); 24 | builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); 25 | builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/Priority.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | import com.mybatisflex.annotation.EnumValue; 6 | 7 | /** 8 | * 优先级 9 | * 10 | * @author inlym 11 | * @date 2025/1/21 12 | * @since 3.0.0 13 | **/ 14 | public enum Priority { 15 | /** 无优先级(默认值) */ 16 | NONE(0), 17 | 18 | /** 低优先级 */ 19 | LOW(1), 20 | 21 | /** 中优先级 */ 22 | MEDIUM(2), 23 | 24 | /** 高优先级 */ 25 | HIGH(3); 26 | 27 | @EnumValue 28 | private final Integer code; 29 | 30 | Priority(int code) { 31 | this.code = code; 32 | } 33 | 34 | @JsonCreator 35 | public static Priority fromCode(int code) { 36 | for (Priority e : Priority.values()) { 37 | if (e.getCode() == code) { 38 | return e; 39 | } 40 | } 41 | throw new IllegalArgumentException("Invalid Priority code: " + code); 42 | } 43 | 44 | @JsonValue 45 | public int getCode() { 46 | return code; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/model/OssPostCredential.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 阿里云 OSS 直传凭据 10 | * 11 | *

    主要用途 12 | *

    用于客户端将资源直传阿里云的 OSS 提供鉴权凭证,客户端获取凭据后对应字段填充至需要的位置即可。 13 | * 14 | * @author inlym 15 | * @date 2024/7/15 16 | * @see 服务端签名直传 17 | * @since 3.0.0 18 | **/ 19 | @Data 20 | @Builder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | public class OssPostCredential { 24 | /** 客户端使用时参数改为 `OSSAccessKeyId` */ 25 | private String accessKeyId; 26 | 27 | /** 上传地址,即绑定的域名,示例值:{@code https://res.weutil.com} */ 28 | private String url; 29 | 30 | /** 上传至 OSS 后的文件路径,示例值:{@code upload/20240608/r7OPIjuso1rR.png} */ 31 | private String key; 32 | 33 | /** 用户表单上传的策略,是经过 Base64 编码过的字符串 */ 34 | private String policy; 35 | 36 | /** 对 policy 签名后的字符串 */ 37 | private String signature; 38 | } 39 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-aliyun 9 | ${revision} 10 | 11 | 12 | life-helper-aliyun-oss 13 | 14 | ${project.artifactId} 15 | 封装阿里云对象存储(OSS)服务 16 | 17 | 18 | 19 | 20 | com.aliyun.oss 21 | aliyun-sdk-oss 22 | 23 | 24 | commons-logging 25 | commons-logging 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/config/OssProperties.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 类名称 9 | * 10 | *

    说明 11 | *

    说明文本内容 12 | * 13 | * @author inlym 14 | * @date 2024/7/15 15 | * @since 3.0.0 16 | **/ 17 | @Component 18 | @ConfigurationProperties(prefix = "aliyun.oss") 19 | @Data 20 | public class OssProperties { 21 | /** 22 | * 存储空间名称 23 | * 24 | *

    示例 25 | *

    {@code weutil-central} 26 | */ 27 | private String bucketName; 28 | 29 | /** 30 | * 绑定的自定义域名 31 | * 32 | *

    示例 33 | *

    {@code res.weutil.com} 34 | */ 35 | private String customDomain; 36 | 37 | /** 38 | * 访问端口 39 | * 40 | *

    示例 41 | *

    外网访问 {@code oss-cn-hangzhou.aliyuncs.com} 42 | *

    VPC 网络访问 {@code oss-cn-hangzhou-internal.aliyuncs.com} 43 | */ 44 | private String endpoint; 45 | 46 | /** 访问密钥 ID */ 47 | private String accessKeyId; 48 | 49 | /** 访问密钥口令 */ 50 | private String accessKeySecret; 51 | } 52 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/InitialFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.model.CustomCachingRequestWrapper; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.springframework.core.Ordered; 9 | import org.springframework.core.annotation.Order; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.filter.OncePerRequestFilter; 12 | 13 | import java.io.IOException; 14 | 15 | /** 16 | * 初始化过滤器 17 | * 18 | *

    说明 19 | *

    放在最前面,用于初始化过滤器链。 20 | * 21 | * @author inlym 22 | * @date 2024/10/15 23 | * @since 3.0.0 24 | **/ 25 | @Component 26 | @Order(Ordered.HIGHEST_PRECEDENCE) 27 | public class InitialFilter extends OncePerRequestFilter { 28 | @Override 29 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 30 | CustomCachingRequestWrapper wrapper = new CustomCachingRequestWrapper(request); 31 | 32 | chain.doFilter(wrapper, response); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/entity/TodoProject.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.entity; 2 | 3 | import com.mybatisflex.annotation.Table; 4 | import com.weutil.common.entity.BaseUserRelatedEntity; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * 待办项目实体 15 | * 16 | * @author inlym 17 | * @date 2024/12/12 18 | * @since 3.0.0 19 | **/ 20 | @Table("reminder_project") 21 | @Data 22 | @EqualsAndHashCode(callSuper = true) 23 | @SuperBuilder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class TodoProject extends BaseUserRelatedEntity { 27 | /** 项目名称 */ 28 | private String name; 29 | 30 | /** 31 | * emoji 图标 32 | * 33 | *

    说明 34 | *

    单字符。 35 | */ 36 | private String emoji; 37 | 38 | /** 39 | * 颜色名称 40 | * 41 | *

    说明 42 | *

    服务端只存储颜色名称,颜色的呈现均由前端处理。 43 | */ 44 | private String color; 45 | 46 | /** 47 | * 收藏时间 48 | * 49 | *

    说明 50 | *

    前端只处理“是否收藏”操作。 51 | */ 52 | private LocalDateTime favoriteTime; 53 | } 54 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/src/main/java/com/weutil/wemap/model/WeMapListRegionResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.wemap.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 获取省市区列表请求的响应数据 10 | * 11 | * @author inlym 12 | * @date 2024/7/16 13 | * @see 获取省市区列表 14 | * @since 3.0.0 15 | **/ 16 | @Data 17 | public class WeMapListRegionResponse { 18 | /** 状态码,0为正常,其它为异常 */ 19 | private Integer status; 20 | 21 | /** 对 status 的描述 */ 22 | private String message; 23 | 24 | /** 数据版本,用于判断更新 */ 25 | @JsonProperty("data_version") 26 | private String dataVersion; 27 | 28 | private List> result; 29 | 30 | @Data 31 | public static class Region { 32 | /** 行政区划唯一标识(adcode) */ 33 | private String id; 34 | 35 | /** 简称 */ 36 | private String name; 37 | 38 | /** 全称 */ 39 | @JsonProperty("fullname") 40 | private String fullName; 41 | 42 | /** 子级行政区划在下级数组中的下标位置 */ 43 | @JsonProperty("cidx") 44 | private List childrenIndex; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/TodoTaskVO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDate; 9 | import java.time.LocalDateTime; 10 | import java.time.LocalTime; 11 | 12 | /** 13 | * 待办任务视图对象 14 | * 15 | *

    说明 16 | *

    用于客户端展示使用。 17 | * 18 | * @author inlym 19 | * @date 2024/12/24 20 | * @since 3.0.0 21 | **/ 22 | @Data 23 | @Builder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class TodoTaskVO { 27 | /** 主键 ID */ 28 | private Long id; 29 | 30 | /** 所属项目 ID */ 31 | private Long projectId; 32 | 33 | /** 任务名称 */ 34 | private String name; 35 | 36 | /** 任务描述内容文本 */ 37 | private String content; 38 | 39 | /** 任务完成时间 */ 40 | private LocalDateTime completeTime; 41 | 42 | /** 截止期限的日期部分(年月日) */ 43 | private LocalDate dueDate; 44 | 45 | /** 截止期限的时间部分(时分秒) */ 46 | private LocalTime dueTime; 47 | 48 | /** 优先级 */ 49 | private Priority priority; 50 | 51 | // ============================ 关联字段数据 ============================ 52 | 53 | /** 所属的项目名称 */ 54 | private String projectName; 55 | } 56 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/config/AliyunCaptchaExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.config; 2 | 3 | import com.weutil.aliyun.captcha.exception.AliyunCaptchaVerifiedFailureException; 4 | import com.weutil.common.model.ErrorResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.Ordered; 7 | import org.springframework.core.annotation.Order; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseStatus; 11 | import org.springframework.web.bind.annotation.RestControllerAdvice; 12 | 13 | /** 14 | * 阿里云验证码模块异常处理器 15 | * 16 | *

    错误码范围 17 | *

    {@code 12101} ~ {@code 12199} 18 | * 19 | * @author inlym 20 | * @date 2024/12/15 21 | * @since 3.0.0 22 | **/ 23 | @RestControllerAdvice 24 | @Slf4j 25 | @Order(Ordered.HIGHEST_PRECEDENCE + 1000) 26 | public class AliyunCaptchaExceptionHandler { 27 | @ResponseStatus(HttpStatus.OK) 28 | @ExceptionHandler(AliyunCaptchaVerifiedFailureException.class) 29 | public ErrorResponse handleAliyunCaptchaVerifiedFailureException() { 30 | return new ErrorResponse(12101, "验证码校验未通过"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/config/AliyunSmsConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.config; 2 | 3 | import com.aliyun.teaopenapi.models.Config; 4 | import com.weutil.common.exception.ServerSideTemporaryException; 5 | import com.weutil.sms.model.AliyunSmsClient; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * 阿里云短信客户端配置 13 | * 14 | * @author inlym 15 | * @date 2024/7/16 16 | * @since 3.0.0 17 | **/ 18 | @Configuration 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | public class AliyunSmsConfig { 22 | private final AliyunSmsProperties properties; 23 | 24 | @Bean 25 | public AliyunSmsClient aliyunSmsClient() { 26 | Config config = new Config() 27 | .setAccessKeyId(properties.getAccessKeyId()) 28 | .setAccessKeySecret(properties.getAccessKeySecret()) 29 | .setEndpoint("dysmsapi.aliyuncs.com"); 30 | 31 | try { 32 | return new AliyunSmsClient(config); 33 | } catch (Exception e) { 34 | log.error("阿里云短信客户端创建错误,错误消息:{}", e.getMessage()); 35 | throw new ServerSideTemporaryException(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /life-helper-system/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | life-helper-system 13 | 14 | ${project.artifactId} 15 | 系统调试服务 16 | 17 | 18 | 19 | com.weutil 20 | life-helper-common 21 | 22 | 23 | 24 | com.weutil 25 | life-helper-aliyun-sms 26 | 27 | 28 | com.weutil 29 | life-helper-wemap 30 | 31 | 32 | com.weutil 33 | life-helper-aliyun-captcha 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /life-helper-account/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | life-helper-account 13 | 14 | ${project.artifactId} 15 | 用户账户模块 16 | 17 | 18 | 19 | com.weutil 20 | life-helper-common 21 | 22 | 23 | 24 | com.weutil 25 | life-helper-aliyun-oss 26 | 27 | 28 | com.weutil 29 | life-helper-aliyun-sms 30 | 31 | 32 | com.weutil 33 | life-helper-aliyun-captcha 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/model/UpdateTodoTaskDTO.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.model; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.Size; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDate; 11 | import java.time.LocalTime; 12 | 13 | /** 14 | * 更新待办任务的请求数据 15 | * 16 | *

    说明 17 | *

    仅处理一些基础资料类,特殊操作放在 {@link OperateTodoTaskDTO} 中。 18 | * 19 | * @author inlym 20 | * @date 2024/12/25 21 | * @since 3.0.0 22 | **/ 23 | @Data 24 | @Builder 25 | @NoArgsConstructor 26 | @AllArgsConstructor 27 | public class UpdateTodoTaskDTO { 28 | /** 任务名称 */ 29 | @Size(max = 50, message = "任务名称最长为50个字") 30 | private String name; 31 | 32 | /** 33 | * 所属项目 ID 34 | * 35 | *

    说明 36 | *

    该值为 {@code 0} 则表示不从属于任何项目。 37 | */ 38 | @Min(value = 0, message = "你选择的项目不存在,请刷新后重试") 39 | private Long projectId; 40 | 41 | /** 任务描述内容文本 */ 42 | private String content; 43 | 44 | /** 截止期限的日期部分(年月日) */ 45 | private LocalDate dueDate; 46 | 47 | /** 截止期限的时间部分(时分秒) */ 48 | private LocalTime dueTime; 49 | 50 | /** 优先级 */ 51 | private Priority priority; 52 | 53 | /** 特定操作 */ 54 | private TodoTaskOperation operation; 55 | } 56 | -------------------------------------------------------------------------------- /DevOps/script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################### 项目构建脚本 ################################### 4 | 5 | ### 运行说明 6 | # 1. 以 git 标签(即版本号)名发起构建。 7 | 8 | ### 运行环境 9 | # 1. 由“云效”执行自动化脚本,在构建机上运行。 10 | # 2. 由于“云效”构建环境与 VPC 隔离,因此推送 Docker 镜像时,推送至仓库的公网地址。 11 | 12 | ### 构建目标 13 | # 输出 Docker 镜像,并上传至阿里云镜像服务。 14 | 15 | ### 文档地址 16 | # 1. 云效-环境变量 -> https://help.aliyun.com/document_detail/153688.html?userCode=lzfqdh6g 17 | # 2. 阿里云-容器镜像服务 -> https://www.aliyun.com/product/acr?userCode=lzfqdh6g 18 | 19 | # 镜像仓库地址 20 | echo "${DOCKER_REPOSITORY}" 21 | 22 | # 在阿里云容器镜像服务中使用的用户名 23 | echo "${DOCKER_USERNAME}" 24 | 25 | # 在阿里云容器镜像服务中使用的密码 26 | echo "${DOCKER_PASSWORD}" 27 | 28 | # 要构建的 Git Commit 的标签名,例如 `1.0.0` 29 | echo "${CI_COMMIT_REF_NAME}" 30 | 31 | # 将代码克隆至本地 32 | git clone https://github.com/inlym/life-helper-server.git --depth=1 --branch "${CI_COMMIT_REF_NAME}" /root/workspace/life-helper-server 33 | 34 | # 进入工作目录 35 | cd /root/workspace/life-helper-server || exit 36 | 37 | # 从 OSS 中下载配置文件(为保密起见,在 github 上的源码不包含配置文件) 38 | ossutil cp -r oss://lifehelper-config/config/application-prod.yml /root/workspace/life-helper-server/src/main/resources/ --update 39 | 40 | # 使用 Google Jib 构建 Docker 镜像并自动推送至阿里云镜像仓库 41 | mvn compile jib:build \ 42 | -Djib.to.image="${DOCKER_REPOSITORY}" \ 43 | -Djib.to.auth.username="${DOCKER_USERNAME}" \ 44 | -Djib.to.auth.password="${DOCKER_PASSWORD}" 45 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/config/AliyunCaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.config; 2 | 3 | import com.aliyun.teaopenapi.models.Config; 4 | import com.weutil.aliyun.captcha.model.AliyunCaptchaClient; 5 | import com.weutil.common.exception.ServerSideTemporaryException; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * 阿里云验证码服务配置 13 | * 14 | * @author inlym 15 | * @date 2024/10/16 16 | * @since 3.0.0 17 | **/ 18 | @Configuration 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | public class AliyunCaptchaConfig { 22 | private final AliyunCaptchaProperties properties; 23 | 24 | @Bean 25 | public AliyunCaptchaClient aliyunCaptchaClient() { 26 | Config config = new Config(); 27 | config.setAccessKeyId(properties.getAccessKeyId()); 28 | config.setAccessKeySecret(properties.getAccessKeySecret()); 29 | config.setEndpoint("captcha.cn-shanghai.aliyuncs.com"); 30 | 31 | try { 32 | return new AliyunCaptchaClient(config); 33 | } catch (Exception e) { 34 | log.error("阿里云验证码服务客户端创建错误,错误消息:{}", e.getMessage()); 35 | throw new ServerSideTemporaryException(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/model/ServerInfo.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.Map; 10 | 11 | /** 12 | * 服务器信息 13 | * 14 | * @author inlym 15 | * @date 2024/12/4 16 | * @since 3.0.0 17 | **/ 18 | @Data 19 | @Builder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class ServerInfo { 23 | /** 最近一次项目启动的时间 */ 24 | private LocalDateTime launchTime; 25 | 26 | /** 服务器的当前时间 */ 27 | private LocalDateTime now; 28 | 29 | /** 本次项目启动后运行时长(单位:秒) */ 30 | private Long duration; 31 | 32 | /** 当前激活的配置文件名称 */ 33 | private String activeProfiles; 34 | 35 | /** 当前使用的端口号 */ 36 | private String serverPort; 37 | 38 | /** 当前使用的 Spring Boot 版本号 */ 39 | private String springBootVersion; 40 | 41 | /** 主机名 */ 42 | private String hostName; 43 | 44 | /** IP 地址 */ 45 | private String ip; 46 | 47 | /** 时区 */ 48 | private String timeZone; 49 | 50 | /** 本次部署代码的 commit ID(SHA-1) */ 51 | private String commitId; 52 | 53 | /** 本次部署代码的 commit 的分支名或标签名 */ 54 | private String commitRefName; 55 | 56 | /** 本次部署的编号 */ 57 | private String buildNumber; 58 | 59 | /** 各中间件延迟时间(单位:毫秒) */ 60 | private Map delay; 61 | } 62 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/entity/LoginLog.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.entity; 2 | 3 | import com.mybatisflex.annotation.Table; 4 | import com.weutil.account.model.LoginChannel; 5 | import com.weutil.account.model.LoginType; 6 | import com.weutil.common.entity.BaseUserRelatedEntity; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import java.time.LocalDateTime; 14 | 15 | /** 16 | * 登录日志 17 | * 18 | *

    说明 19 | *

    原本每种登录方式分别一张表,每张表有80%左右的字段重合,为方便统计和使用,合并到一张表上。 20 | * 21 | * @author inlym 22 | * @date 2024/8/28 23 | * @since 3.0.0 24 | **/ 25 | @Table("login_log") 26 | @Data 27 | @EqualsAndHashCode(callSuper = true) 28 | @SuperBuilder 29 | @NoArgsConstructor 30 | @AllArgsConstructor 31 | public class LoginLog extends BaseUserRelatedEntity { 32 | 33 | // ---------- 各种登录方式通用项 ---------- 34 | 35 | /** 登录方式 */ 36 | private LoginType type; 37 | 38 | /** 登录渠道 */ 39 | private LoginChannel channel; 40 | 41 | /** 发放的鉴权令牌 */ 42 | private String token; 43 | 44 | /** 客户端 IP 地址 */ 45 | private String ip; 46 | 47 | /** 登录时间 */ 48 | private LocalDateTime loginTime; 49 | 50 | // ---------- 各种登录方式差异项 ---------- 51 | 52 | // --- 通过手机号系列方式登录 --- 53 | 54 | /** 关联的用户手机号账户表 ID */ 55 | private Long phoneAccountId; 56 | } 57 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/RestClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 10 | import org.springframework.web.client.RestClient; 11 | 12 | import java.util.function.Consumer; 13 | 14 | /** 15 | * HTTP 请求客户端 16 | * 17 | * @author inlym 18 | * @date 2024/7/15 19 | * @since 3.0.0 20 | **/ 21 | @Configuration 22 | @RequiredArgsConstructor 23 | public class RestClientConfig { 24 | private final ObjectMapper objectMapper; 25 | 26 | @Bean 27 | public RestClient restClient() { 28 | Consumer jsonContentHeaders = headers -> headers.setContentType(MediaType.APPLICATION_JSON); 29 | 30 | return RestClient.builder().messageConverters(converters -> { 31 | // 说明:RestClient 原生的转化器内自建的 objectMapper 不会过滤值为 null 的属性,此处相当于做了个替换 32 | converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance); 33 | converters.add(new MappingJackson2HttpMessageConverter(objectMapper)); 34 | }).defaultHeaders(jsonContentHeaders).build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/service/IdentityCertificateService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.service; 2 | 3 | import com.weutil.common.model.CustomHttpHeader; 4 | import com.weutil.common.model.IdentityCertificate; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.time.Duration; 10 | import java.time.LocalDateTime; 11 | 12 | /** 13 | * 身份证书服务 14 | * 15 | *

    主要用途 16 | *

    对访问凭证再做一层封装,用于登录后返回给客户端使用。 17 | * 18 | * @author inlym 19 | * @date 2024/7/15 20 | * @since 3.0.0 21 | **/ 22 | @Service 23 | @Slf4j 24 | @RequiredArgsConstructor 25 | public class IdentityCertificateService { 26 | /** 默认有效期:10天 */ 27 | private static final Duration timeout = Duration.ofDays(10L); 28 | 29 | private final AccessTokenService accessTokenService; 30 | 31 | /** 32 | * 创建身份证书 33 | * 34 | * @param userId 用户 ID 35 | * 36 | * @date 2024/7/15 37 | * @since 3.0.0 38 | */ 39 | public IdentityCertificate create(long userId) { 40 | String token = accessTokenService.create(userId, timeout); 41 | 42 | return IdentityCertificate.builder() 43 | .token(token) 44 | .headerName(CustomHttpHeader.ACCESS_TOKEN) 45 | .createTime(LocalDateTime.now()) 46 | .expireTime(LocalDateTime.now().plus(timeout)) 47 | .build(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.entity; 2 | 3 | import com.mybatisflex.annotation.Column; 4 | import com.mybatisflex.annotation.Table; 5 | import com.weutil.account.model.Gender; 6 | import com.weutil.common.entity.BaseEntity; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import java.time.LocalDateTime; 14 | 15 | /** 16 | * 用户账户实体 17 | * 18 | *

    说明 19 | *

    当前数据表不存储账户关联关系(存于其他“用户账户表”),其他数据表通过关联信息指向对应用户 ID: 20 | *

    1. {@code WeChatAccount} 微信账户关联表 21 | *

    2. {@code PhoneAccount} 手机号账户关联表 22 | *

    3. {@code GithubAccount} Github账户关联表(当前无) 23 | * 24 | * @author inlym 25 | * @date 2024/7/22 26 | * @since 3.0.0 27 | **/ 28 | @Table("user") 29 | @Data 30 | @EqualsAndHashCode(callSuper = true) 31 | @SuperBuilder 32 | @NoArgsConstructor 33 | @AllArgsConstructor 34 | public class User extends BaseEntity { 35 | /** 昵称 */ 36 | private String nickName; 37 | 38 | /** 头像路径 */ 39 | private String avatarPath; 40 | 41 | /** 42 | * 账户 ID 43 | * 44 | *

    字段说明 45 | *

    (1)用于展示用途,客户端展示名称为 UID 46 | *

    (2)该字段添加“唯一索引” 47 | */ 48 | private Long accountId; 49 | 50 | /** 性别 */ 51 | private Gender gender; 52 | 53 | /** 注册时间 */ 54 | @Column(onInsertValue = "now()") 55 | private LocalDateTime registerTime; 56 | } 57 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/annotation/resolver/UserIdMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.annotation.resolver; 2 | 3 | import com.weutil.common.annotation.UserId; 4 | import com.weutil.common.exception.UnauthorizedAccessException; 5 | import org.springframework.core.MethodParameter; 6 | import org.springframework.web.bind.support.WebDataBinderFactory; 7 | import org.springframework.web.context.request.NativeWebRequest; 8 | import org.springframework.web.context.request.RequestAttributes; 9 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 10 | import org.springframework.web.method.support.ModelAndViewContainer; 11 | 12 | /** 13 | * 用户 ID 注入器注解解析器 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | public class UserIdMethodArgumentResolver implements HandlerMethodArgumentResolver { 20 | @Override 21 | public boolean supportsParameter(MethodParameter parameter) { 22 | return parameter.getParameterType().isAssignableFrom(long.class) && parameter.hasParameterAnnotation(UserId.class); 23 | } 24 | 25 | @Override 26 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory factory) { 27 | Long userId = (Long) webRequest.getAttribute("USER_ID", RequestAttributes.SCOPE_REQUEST); 28 | 29 | if (userId != null && userId > 0) { 30 | return userId; 31 | } 32 | 33 | throw new UnauthorizedAccessException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/entity/SmsLog.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.entity; 2 | 3 | import com.mybatisflex.annotation.Table; 4 | import com.weutil.common.entity.BaseEntity; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * 短信发送记录 15 | * 16 | *

    说明 17 | *

    1. 仅记录短信发送,不处理其他的业务逻辑。 18 | *

    2. 目前只包含“验证码”短信。 19 | * 20 | * @author inlym 21 | * @date 2024/11/4 22 | * @since 3.0.0 23 | **/ 24 | @Table("sms_log") 25 | @Data 26 | @EqualsAndHashCode(callSuper = true) 27 | @SuperBuilder 28 | @NoArgsConstructor 29 | @AllArgsConstructor 30 | public class SmsLog extends BaseEntity { 31 | 32 | /** 手机号 */ 33 | private String phone; 34 | 35 | /** 短信验证码 */ 36 | private String code; 37 | 38 | /** 客户端 IP 地址 */ 39 | private String ip; 40 | 41 | /** 短信发送时间(发送前) */ 42 | private LocalDateTime preSendTime; 43 | 44 | // ---------- 短信发出后,从发送结果反馈获得的的字段 ---------- 45 | // 文档地址:https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms 46 | 47 | /** 请求状态码 */ 48 | private String resCode; 49 | 50 | /** 状态码的描述 */ 51 | private String resMessage; 52 | 53 | /** 发送回执 ID */ 54 | private String resBizId; 55 | 56 | /** 请求 ID */ 57 | private String requestId; 58 | 59 | // ---------- 短信发出后,得到相应后处理的字段 ---------- 60 | 61 | /** 收到响应的时间 */ 62 | private LocalDateTime postSendTime; 63 | } 64 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/annotation/resolver/ClientIpMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.annotation.resolver; 2 | 3 | import com.weutil.common.annotation.ClientIp; 4 | import com.weutil.common.exception.UnpredictableException; 5 | import org.springframework.core.MethodParameter; 6 | import org.springframework.web.bind.support.WebDataBinderFactory; 7 | import org.springframework.web.context.request.NativeWebRequest; 8 | import org.springframework.web.context.request.RequestAttributes; 9 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 10 | import org.springframework.web.method.support.ModelAndViewContainer; 11 | 12 | /** 13 | * 客户端 IP 地址注入器注解解析器 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | public class ClientIpMethodArgumentResolver implements HandlerMethodArgumentResolver { 20 | @Override 21 | public boolean supportsParameter(MethodParameter parameter) { 22 | return parameter.getParameterType().isAssignableFrom(String.class) && parameter.hasParameterAnnotation(ClientIp.class); 23 | } 24 | 25 | @Override 26 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory factory) { 27 | String clientIp = (String) webRequest.getAttribute("CLIENT_IP", RequestAttributes.SCOPE_REQUEST); 28 | 29 | if (clientIp != null) { 30 | return clientIp; 31 | } 32 | 33 | throw new UnpredictableException("未获取到客户端 IP 地址"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/SpringRedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.redis.connection.RedisConnectionFactory; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 10 | import org.springframework.data.redis.serializer.StringRedisSerializer; 11 | 12 | /** 13 | * Spring Redis 配置 14 | * 15 | * @author inlym 16 | * @date 2024/7/14 17 | * @since 3.0.0 18 | **/ 19 | @Configuration 20 | @RequiredArgsConstructor 21 | public class SpringRedisConfig { 22 | private final RedisConnectionFactory redisConnectionFactory; 23 | private final ObjectMapper objectMapper; 24 | 25 | @Bean 26 | public RedisTemplate redisTemplate() { 27 | RedisTemplate template = new RedisTemplate<>(); 28 | template.setConnectionFactory(redisConnectionFactory); 29 | template.setKeySerializer(new StringRedisSerializer()); 30 | template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); 31 | template.setHashKeySerializer(new StringRedisSerializer()); 32 | template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); 33 | template.afterPropertiesSet(); 34 | 35 | return template; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/util/RandomStringUtil.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.util; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.stream.Collectors; 5 | import java.util.stream.IntStream; 6 | 7 | /** 8 | * 随机字符串工具集 9 | * 10 | * @author inlym 11 | * @date 2024/7/14 12 | * @since 3.0.0 13 | **/ 14 | public abstract class RandomStringUtil { 15 | /** 16 | * 生成指定长度的随机字符串(包含大小写字母和数字) 17 | * 18 | * @param length 字符串长度 19 | * 20 | * @return 生成的字符串 21 | * @date 2024/5/31 22 | * @since 2.3.0 23 | */ 24 | public static String generate(int length) { 25 | // 定义字符集,包括大写字母、小写字母和数字 26 | String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 27 | SecureRandom random = new SecureRandom(); 28 | 29 | return IntStream 30 | .range(0, length) 31 | .map(i -> random.nextInt(characters.length())) 32 | .mapToObj(randomIndex -> String.valueOf(characters.charAt(randomIndex))) 33 | .collect(Collectors.joining()); 34 | } 35 | 36 | /** 37 | * 生成数字串(全部由数字组成的字符串) 38 | * 39 | * @param length 字符串长度 40 | * 41 | * @date 2024/6/14 42 | * @since 2.3.0 43 | */ 44 | public static String generateNumericString(int length) { 45 | SecureRandom random = new SecureRandom(); 46 | StringBuilder sb = new StringBuilder(); 47 | for (int i = 0; i < length; i++) { 48 | sb.append(random.nextInt(10)); 49 | } 50 | 51 | return sb.toString(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/DebugSleepFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.filter.OncePerRequestFilter; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * 调试暂停过滤器 16 | * 17 | *

    说明 18 | *

    用于调试慢请求。 19 | * 20 | * @author inlym 21 | * @date 2024/12/23 22 | * @since 3.0.0 23 | **/ 24 | @Component 25 | @Slf4j 26 | public class DebugSleepFilter extends OncePerRequestFilter { 27 | private static final String PARAM_NAME = "sleep"; 28 | 29 | @Override 30 | protected boolean shouldNotFilter(HttpServletRequest request) { 31 | return request.getParameter(PARAM_NAME) == null; 32 | } 33 | 34 | @Override 35 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 36 | String str = request.getParameter(PARAM_NAME); 37 | 38 | if (str != null) { 39 | long timeout = Long.parseLong(str); 40 | 41 | try { 42 | TimeUnit.MILLISECONDS.sleep(timeout); 43 | } catch (InterruptedException e) { 44 | log.debug(e.getMessage()); 45 | } 46 | } 47 | 48 | chain.doFilter(request, response); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/CustomRequestContext.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | /** 11 | * 自定义请求上下文 12 | * 13 | *

    主要用途 14 | *

    将所有用到的请求域字段数据存储在当前对象中,以便后续使用。 15 | * 16 | * @author inlym 17 | * @date 2024/7/14 18 | * @since 3.0.0 19 | **/ 20 | @Data 21 | @Builder 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public class CustomRequestContext { 25 | /** 在请求域属性使用的名称 */ 26 | public static final String NAME = "CUSTOM_REQUEST_CONTEXT"; 27 | 28 | /** 29 | * 请求 ID(追踪 ID) 30 | * 31 | *

    说明 32 | *

    生产环境中会由 API 网关在请求头中传入,在开发环境需模拟生成该值。 33 | */ 34 | private String traceId; 35 | 36 | /** 请求时间 */ 37 | private LocalDateTime requestTime; 38 | 39 | /** 请求方法 */ 40 | private String method; 41 | 42 | /** 请求路径 */ 43 | private String path; 44 | 45 | /** 请求参数 */ 46 | private String querystring; 47 | 48 | /** 请求数据 */ 49 | private String requestBody; 50 | 51 | /** 响应状态码 */ 52 | private Integer status; 53 | 54 | /** 响应数据 */ 55 | private String responseBody; 56 | 57 | /** 58 | * 客户端 IP 地址 59 | * 60 | *

    说明 61 | *

    生产环境中会由 API 网关在请求头中传入,而不是通过连接直接获取。 62 | */ 63 | private String clientIp; 64 | 65 | /** 66 | * 用户 ID 67 | * 68 | *

    说明 69 | *

    务必在鉴权通过后再存入。 70 | */ 71 | private Long userId; 72 | 73 | /** 客户端版本号 */ 74 | private String clientVersion; 75 | } 76 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/service/LaunchTimeService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.service; 2 | 3 | import com.weutil.common.exception.UnpredictableException; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | /** 12 | * 项目启动时间服务 13 | * 14 | *

    说明 15 | *

    用语处理和计算启动时间相关事项 16 | * 17 | * @author inlym 18 | * @date 2024/12/4 19 | * @since 3.0.0 20 | **/ 21 | @Service 22 | @Slf4j 23 | @RequiredArgsConstructor 24 | public class LaunchTimeService { 25 | /** 存储在 Redis 中的键名 */ 26 | private static final String REDIS_KEY = "system:launch-time"; 27 | 28 | private final StringRedisTemplate stringRedisTemplate; 29 | 30 | /** 31 | * 在项目启动时记录 32 | * 33 | * @date 2024/12/4 34 | * @since 3.0.0 35 | */ 36 | public void recordOnStartUp() { 37 | LocalDateTime now = LocalDateTime.now(); 38 | log.debug("[启动时任务] 项目启动时间: {}", now); 39 | stringRedisTemplate.opsForValue().set(REDIS_KEY, now.toString()); 40 | } 41 | 42 | /** 43 | * 获取项目启动时间 44 | * 45 | * @return 项目启动时间 46 | * @date 2024/12/4 47 | * @since 3.0.0 48 | */ 49 | public LocalDateTime getLaunchTime() { 50 | String str = stringRedisTemplate.opsForValue().get(REDIS_KEY); 51 | if (str == null) { 52 | throw new UnpredictableException("未在 Redis 中获取到项目启动时间数据!"); 53 | } 54 | 55 | return LocalDateTime.parse(str); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/SpringSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 8 | import org.springframework.security.config.http.SessionCreationPolicy; 9 | import org.springframework.security.web.SecurityFilterChain; 10 | 11 | /** 12 | * Spring Security 配置 13 | * 14 | *

    说明 15 | *

    说明文本内容 16 | * 17 | * @author inlym 18 | * @date 2024/7/14 19 | * @since 3.0.0 20 | **/ 21 | @Configuration 22 | @RequiredArgsConstructor 23 | public class SpringSecurityConfig { 24 | private final HttpSecurity http; 25 | 26 | @Bean 27 | public SecurityFilterChain securityFilterChain() throws Exception { 28 | // 备注:实际上可以使用 {@code .and()} 来连接各个语句,但笔者觉得使用 {@code http} 看起来更优雅。 29 | 30 | // 关闭 form 表单认证 31 | http.formLogin(AbstractHttpConfigurer::disable); 32 | // 关闭 basic 方式认证 33 | http.httpBasic(AbstractHttpConfigurer::disable); 34 | // 关闭 CSRF 防护 35 | http.csrf(AbstractHttpConfigurer::disable); 36 | http.sessionManagement(registry -> registry.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); 37 | 38 | // 默认所有 API 均免鉴权,需要鉴权的 API 再额外使用 @Secured 注解声明需要的角色 39 | http.authorizeHttpRequests(registry -> registry.anyRequest().permitAll()); 40 | 41 | return http.build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/interceptor/LogInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.slf4j.MDC; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | 10 | import java.util.Map; 11 | 12 | /** 13 | * 日志拦截器 14 | * 15 | * @author inlym 16 | * @date 2024/7/15 17 | * @since 3.0.0 18 | **/ 19 | @Component 20 | @Slf4j 21 | public class LogInterceptor implements HandlerInterceptor { 22 | public final String TRACE_ID = "TRACE_ID"; 23 | public final String CLIENT_IP = "CLIENT_IP"; 24 | public final String USER_ID = "USER_ID"; 25 | 26 | @Override 27 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 28 | String traceId = (String) request.getAttribute(TRACE_ID); 29 | String clientIp = (String) request.getAttribute(CLIENT_IP); 30 | Long userId = (Long) request.getAttribute(USER_ID); 31 | 32 | MDC.put(TRACE_ID, traceId); 33 | MDC.put(CLIENT_IP, clientIp); 34 | MDC.put(USER_ID, String.valueOf(userId)); 35 | 36 | Map map = MDC.getCopyOfContextMap(); 37 | if (map != null) { 38 | log.debug("MDC={}", map); 39 | } 40 | 41 | return true; 42 | } 43 | 44 | @Override 45 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 46 | MDC.clear(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/controller/TodoFilterController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.controller; 2 | 3 | import com.weutil.common.annotation.UserId; 4 | import com.weutil.common.annotation.UserPermission; 5 | import com.weutil.common.model.SingleNumberResponse; 6 | import com.weutil.todo.model.TodoFilter; 7 | import com.weutil.todo.service.TodoFilterService; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.validation.annotation.Validated; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | /** 16 | * 待办任务过滤器控制器 17 | * 18 | * @author inlym 19 | * @date 2024/12/27 20 | * @since 3.0.0 21 | **/ 22 | @RestController 23 | @Validated 24 | @Slf4j 25 | @RequiredArgsConstructor 26 | public class TodoFilterController { 27 | private final TodoFilterService todoFilterService; 28 | 29 | /** 30 | * 查看指定过滤器的未完成任务数 31 | * 32 | * @param userId 用户 ID 33 | * @param filterName 过滤器名称(前端可直接传小写字母) 34 | * 35 | * @date 2025/01/08 36 | * @since 3.0.0 37 | */ 38 | @GetMapping("/todo/filters/{filter_name}/count-uncompleted") 39 | @UserPermission 40 | public SingleNumberResponse countUncompletedTasks(@UserId long userId, @PathVariable("filter_name") String filterName) { 41 | TodoFilter filter = TodoFilter.valueOf(filterName.toUpperCase()); 42 | long num = todoFilterService.countUncompletedTasks(userId, filter); 43 | return new SingleNumberResponse(num); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/controller/OssController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.controller; 2 | 3 | import com.weutil.oss.model.GeneratingPostCredentialDTO; 4 | import com.weutil.oss.model.GeneratingPostCredentialOptions; 5 | import com.weutil.oss.model.OssPostCredential; 6 | import com.weutil.oss.service.OssService; 7 | import jakarta.validation.Valid; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.validation.annotation.Validated; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.time.Duration; 15 | 16 | /** 17 | * 阿里云对象存储管理控制器 18 | * 19 | * @author inlym 20 | * @date 2024/7/15 21 | * @since 3.0.0 22 | **/ 23 | @RestController 24 | @RequiredArgsConstructor 25 | @Validated 26 | public class OssController { 27 | private final OssService ossService; 28 | 29 | /** 30 | * 生成 OSS 直传凭据 31 | * 32 | * @param dto 请求数据 33 | * 34 | * @date 2024/7/15 35 | * @since 3.0.0 36 | */ 37 | @PostMapping("/oss/credential") 38 | public OssPostCredential generatePostCredential(@Valid @RequestBody GeneratingPostCredentialDTO dto) { 39 | String extension = dto.getExtension(); 40 | GeneratingPostCredentialOptions options = GeneratingPostCredentialOptions.builder() 41 | .extension(extension) 42 | .sizeMB(100L) 43 | .duration(Duration.ofHours(2L)) 44 | .build(); 45 | 46 | return ossService.generatePostCredential(options); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/service/PhoneAccountService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.service; 2 | 3 | import com.mybatisflex.core.query.QueryCondition; 4 | import com.weutil.account.entity.PhoneAccount; 5 | import com.weutil.account.entity.table.PhoneAccountTableDef; 6 | import com.weutil.account.mapper.PhoneAccountMapper; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * 手机号账户服务 13 | * 14 | * @author inlym 15 | * @date 2024/7/22 16 | * @since 3.0.0 17 | **/ 18 | @Service 19 | @RequiredArgsConstructor 20 | @Slf4j 21 | public class PhoneAccountService { 22 | private final PhoneAccountMapper phoneAccountMapper; 23 | private final UserService userService; 24 | 25 | /** 26 | * 通过手机号获取关联表账户 27 | * 28 | * @param phone 手机号,示例值:{@code 13111111111} 29 | * 30 | * @date 2024/7/24 31 | * @since 3.0.0 32 | */ 33 | public PhoneAccount getOrCreatePhoneAccount(String phone) { 34 | QueryCondition condition = PhoneAccountTableDef.PHONE_ACCOUNT.PHONE.eq(phone); 35 | PhoneAccount result = phoneAccountMapper.selectOneByCondition(condition); 36 | 37 | // 能找到则直接返回结果 38 | if (result != null) { 39 | return result; 40 | } 41 | 42 | // 未找到则说明:该手机号从未注册过,需要先注册一个新用户,再返回 43 | long userId = userService.createUser().getId(); 44 | PhoneAccount inserted = PhoneAccount.builder().userId(userId).phone(phone).build(); 45 | phoneAccountMapper.insertSelective(inserted); 46 | return phoneAccountMapper.selectOneById(inserted.getId()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/service/DelayTimeService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.service; 2 | 3 | import com.weutil.common.util.RandomStringUtil; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.jdbc.core.JdbcTemplate; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.time.Duration; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * 延迟时间计算服务 15 | * 16 | *

    说明 17 | *

    计算各个中间件的延迟时间。 18 | * 19 | * @author inlym 20 | * @date 2024/12/10 21 | * @since 3.0.0 22 | **/ 23 | @Service 24 | @Slf4j 25 | @RequiredArgsConstructor 26 | public class DelayTimeService { 27 | private final JdbcTemplate jdbcTemplate; 28 | private final StringRedisTemplate stringRedisTemplate; 29 | 30 | /** 31 | * 计算 MySQL 的延迟时间(单位:毫秒) 32 | * 33 | * @date 2024/12/10 34 | * @since 3.0.0 35 | */ 36 | public long calcMysqlDelayTime() { 37 | long startTime = System.currentTimeMillis(); 38 | jdbcTemplate.execute("select 1"); 39 | long endTime = System.currentTimeMillis(); 40 | 41 | return endTime - startTime; 42 | } 43 | 44 | /** 45 | * 计算 Redis 的延迟时间(单位:毫秒) 46 | * 47 | * @date 2024/12/10 48 | * @since 3.0.0 49 | */ 50 | public long calcRedisDelayTime() { 51 | long startTime = System.currentTimeMillis(); 52 | stringRedisTemplate.opsForValue().set("temp:" + RandomStringUtil.generate(12), LocalDateTime.now().toString(), Duration.ofMinutes(1L)); 53 | long endTime = System.currentTimeMillis(); 54 | 55 | return endTime - startTime; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/SpringAsyncConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import org.slf4j.MDC; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.AsyncConfigurer; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | 9 | import java.util.Map; 10 | import java.util.concurrent.Executor; 11 | import java.util.concurrent.ThreadPoolExecutor; 12 | 13 | /** 14 | * 异步方法配置 15 | * 16 | *

    说明 17 | *

    说明文本内容 18 | * 19 | * @author inlym 20 | * @date 2024/7/14 21 | * @since 3.0.0 22 | **/ 23 | @EnableAsync 24 | @Configuration 25 | public class SpringAsyncConfig implements AsyncConfigurer { 26 | @Override 27 | public Executor getAsyncExecutor() { 28 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 29 | executor.setCorePoolSize(10); 30 | executor.setMaxPoolSize(50); 31 | executor.setQueueCapacity(1000); 32 | executor.setKeepAliveSeconds(300); 33 | executor.setThreadNamePrefix("async-thread-"); 34 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 35 | executor.setTaskDecorator(runnable -> { 36 | Map contextMap = MDC.getCopyOfContextMap(); 37 | return () -> { 38 | try { 39 | MDC.setContextMap(contextMap); 40 | runnable.run(); 41 | } finally { 42 | MDC.clear(); 43 | } 44 | }; 45 | }); 46 | 47 | executor.initialize(); 48 | return executor; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/controller/BaseUserInfoController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.controller; 2 | 3 | import com.weutil.account.model.BaseUserInfoDTO; 4 | import com.weutil.account.model.BaseUserInfoVO; 5 | import com.weutil.account.service.UserService; 6 | import com.weutil.common.annotation.UserId; 7 | import com.weutil.common.annotation.UserPermission; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PutMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | /** 15 | * 基础用户信息管理控制器 16 | * 17 | * @author inlym 18 | * @date 2024/7/22 19 | * @since 3.0.0 20 | **/ 21 | @RestController 22 | @RequiredArgsConstructor 23 | public class BaseUserInfoController { 24 | private final UserService userService; 25 | 26 | /** 27 | * 获取用户基础信息 28 | * 29 | * @param userId 用户 ID 30 | * 31 | * @date 2024/6/9 32 | * @since 2.3.0 33 | */ 34 | @GetMapping("/user-info/base") 35 | @UserPermission 36 | public BaseUserInfoVO get(@UserId long userId) { 37 | return userService.getBaseUserInfo(userId); 38 | } 39 | 40 | /** 41 | * 修改用户基础信息 42 | * 43 | * @param userId 用户 ID 44 | * @param dto 请求数据 45 | * 46 | * @date 2024/6/9 47 | * @since 2.3.0 48 | */ 49 | @PutMapping("/user-info/base") 50 | @UserPermission 51 | public BaseUserInfoVO update(@UserId long userId, @RequestBody BaseUserInfoDTO dto) { 52 | userService.updateBaseUserInfo(userId, dto); 53 | return userService.getBaseUserInfo(userId); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/TraceIdFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.model.CustomHttpHeader; 4 | import com.weutil.common.model.CustomRequestAttribute; 5 | import jakarta.servlet.FilterChain; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * 追踪 ID 过滤器 16 | * 17 | *

    说明 18 | *

    线上生产环境使用 阿里云 API 网关 承载 HTTP 请求, 19 | * 会自动生成一个唯一请求 ID 并放置于响应头的 `X-Ca-Request-Id` 字段,项目将该字段用作全链路追踪 ID。 20 | * 21 | * @author inlym 22 | * @date 2024/7/14 23 | * @since 3.0.0 24 | **/ 25 | @Component 26 | public class TraceIdFilter extends OncePerRequestFilter { 27 | /** 28 | * 传递唯一请求 ID 的请求头字段 29 | * 30 | *

    说明 31 | *

    该字段为阿里云 API 网关的系统参数,在编辑 API 时,配置获取 `CaRequestId` 参数传入请求头。 32 | */ 33 | private static final String HEADER_NAME = CustomHttpHeader.REQUEST_ID; 34 | 35 | @Override 36 | protected boolean shouldNotFilter(HttpServletRequest request) { 37 | // 指定请求头为空则不进行任何处理 38 | return request.getHeader(HEADER_NAME) == null; 39 | } 40 | 41 | @Override 42 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 43 | String traceId = request.getHeader(HEADER_NAME); 44 | request.setAttribute(CustomRequestAttribute.TRACE_ID, traceId); 45 | 46 | chain.doFilter(request, response); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.entity; 2 | 3 | import com.mybatisflex.annotation.Column; 4 | import com.mybatisflex.annotation.Id; 5 | import com.mybatisflex.annotation.KeyType; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * 实体基类 15 | * 16 | *

    说明 17 | *

    所有的实体都需要继承当前实体。 18 | * 19 | * @author inlym 20 | * @date 2024/12/25 21 | * @since 3.0.0 22 | **/ 23 | @Data 24 | @SuperBuilder 25 | @NoArgsConstructor 26 | @AllArgsConstructor 27 | public abstract class BaseEntity { 28 | /** 主键 ID */ 29 | @Id(keyType = KeyType.Auto) 30 | private Long id; 31 | 32 | /** 33 | * 创建时间 34 | * 35 | *

    说明 36 | *

    该字段由 MySQL 的触发器维护。 37 | */ 38 | private LocalDateTime createTime; 39 | 40 | /** 41 | * 更新时间 42 | * 43 | *

    说明 44 | *

    该字段由 MySQL 的触发器维护。 45 | */ 46 | private LocalDateTime updateTime; 47 | 48 | /** 49 | * 删除时间 50 | * 51 | *

    说明 52 | *

    该字段为逻辑删除标志。 53 | */ 54 | @Column(isLogicDelete = true) 55 | private LocalDateTime deleteTime; 56 | 57 | /** 58 | * 创建时的客户端 IP 地址 59 | * 60 | *

    说明 61 | *

    该字段由 MyBatis-Flex 框架监听器维护。 62 | */ 63 | private String createClientIp; 64 | 65 | /** 66 | * 最后一次更新时的客户端 IP 地址 67 | * 68 | *

    说明 69 | *

    该字段由 MyBatis-Flex 框架监听器维护。 70 | */ 71 | private String updateClientIp; 72 | 73 | /** 74 | * 更新次数 75 | * 76 | *

    说明 77 | *

    (1)默认值:0 78 | */ 79 | @Column(onUpdateValue = "update_count + 1") 80 | private Long updateCount; 81 | } 82 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/config/OssConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.config; 2 | 3 | import com.aliyun.oss.ClientBuilderConfiguration; 4 | import com.aliyun.oss.OSS; 5 | import com.aliyun.oss.OSSClientBuilder; 6 | import com.aliyun.oss.common.comm.Protocol; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * OSS 客户端配置 13 | * 14 | * @author inlym 15 | * @date 2024/7/15 16 | * @since 3.0.0 17 | **/ 18 | @Configuration 19 | @RequiredArgsConstructor 20 | public class OssConfig { 21 | private final OssProperties ossProperties; 22 | 23 | /** 24 | * 通用 OSS 客户端 25 | * 26 | * @date 2024/7/15 27 | * @since 3.0.0 28 | */ 29 | @Bean 30 | public OSS ossClient() { 31 | String endpoint = ossProperties.getEndpoint(); 32 | String accessKeyId = ossProperties.getAccessKeyId(); 33 | String accessKeySecret = ossProperties.getAccessKeySecret(); 34 | 35 | return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); 36 | } 37 | 38 | /** 39 | * 专用于“预签名”的 OSS 客户端 40 | * 41 | * @date 2024/7/15 42 | * @since 3.0.0 43 | */ 44 | @Bean 45 | public OSS ossClientForPresigning() { 46 | String endpoint = ossProperties.getCustomDomain(); 47 | String accessKeyId = ossProperties.getAccessKeyId(); 48 | String accessKeySecret = ossProperties.getAccessKeySecret(); 49 | 50 | ClientBuilderConfiguration conf = new ClientBuilderConfiguration(); 51 | conf.setProtocol(Protocol.HTTPS); 52 | conf.setSupportCname(true); 53 | 54 | return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret, conf); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/service/AliyunCaptchaApiService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.service; 2 | 3 | import com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaRequest; 4 | import com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaResponse; 5 | import com.weutil.aliyun.captcha.config.AliyunCaptchaProperties; 6 | import com.weutil.aliyun.captcha.model.AliyunCaptchaClient; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.SneakyThrows; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | /** 13 | * 阿里云验证码 API 服务 14 | * 15 | *

    说明 16 | *

    仅用于封装 API 请求,不做任何业务处理。 17 | * 18 | * @author inlym 19 | * @date 2024/10/16 20 | * @see 服务端接入 21 | * @since 3.0.0 22 | **/ 23 | @Service 24 | @Slf4j 25 | @RequiredArgsConstructor 26 | public class AliyunCaptchaApiService { 27 | private final AliyunCaptchaClient aliyunCaptchaClient; 28 | private final AliyunCaptchaProperties properties; 29 | 30 | /** 31 | * 校验验证码参数 32 | * 33 | * @param captchaVerifyParam 由验证码脚本回调的验证参数 34 | * 35 | * @date 2024/10/16 36 | * @see 调用VerifyIntelligentCaptcha接口 37 | * @since 3.0.0 38 | */ 39 | @SneakyThrows 40 | public VerifyIntelligentCaptchaResponse verifyCaptcha(String captchaVerifyParam) { 41 | VerifyIntelligentCaptchaRequest request = new VerifyIntelligentCaptchaRequest(); 42 | request.setCaptchaVerifyParam(captchaVerifyParam); 43 | request.setSceneId(properties.getSceneId()); 44 | 45 | return aliyunCaptchaClient.verifyIntelligentCaptcha(request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/src/main/java/com/weutil/wemap/model/WeMapLocateIpResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.wemap.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | /** 7 | * 腾讯位置服务 IP 定位响应数据 8 | * 9 | * @author inlym 10 | * @date 2024/7/16 11 | * @see IP定位 12 | * @since 3.0.0 13 | **/ 14 | @Data 15 | public class WeMapLocateIpResponse { 16 | /** 状态码,0为正常,其它为异常 */ 17 | private Integer status; 18 | 19 | /** 对 status 的描述 */ 20 | private String message; 21 | 22 | /** IP 定位结果 */ 23 | private Result result; 24 | 25 | @Data 26 | public static class Result { 27 | /** 用于定位的IP地址 */ 28 | private String ip; 29 | 30 | /** 定位坐标 */ 31 | private Location location; 32 | 33 | /** 定位行政区划信息 */ 34 | @JsonProperty("ad_info") 35 | private AddressInfo addressInfo; 36 | } 37 | 38 | /** 经纬度坐标 */ 39 | @Data 40 | public static class Location { 41 | /** 经度 */ 42 | @JsonProperty("lng") 43 | private Double longitude; 44 | 45 | /** 纬度 */ 46 | @JsonProperty("lat") 47 | private Double latitude; 48 | } 49 | 50 | /** 定位行政区划信息 */ 51 | @Data 52 | public static class AddressInfo { 53 | /** 国家 */ 54 | private String nation; 55 | 56 | /** 国家代码(ISO3166标准3位数字码) */ 57 | @JsonProperty("nation_code") 58 | private String nationCode; 59 | 60 | /** 省 */ 61 | private String province; 62 | 63 | /** 市(可能为空) */ 64 | private String city; 65 | 66 | /** 区(可能为空) */ 67 | private String district; 68 | 69 | /** 行政区划代码(非正常情况则返回 -1) */ 70 | private Integer adcode; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DevOps/mysql/schema/life-helper-aliyun.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------------------------------------------------------------------------------------------- 2 | -- 短信发送日志 3 | -- 对应实体: [com.weutil.sms.entity.SmsLog] 4 | -- 创建时间: 2024/11/04 5 | -- ---------------------------------------------------------------------------------------------------------------- 6 | 7 | create table `sms_log` 8 | ( 9 | /* 下方是通用字段 */ 10 | `id` bigint unsigned not null auto_increment comment '主键 ID', 11 | `create_time` datetime not null default current_timestamp comment '创建时间', 12 | `update_time` datetime not null default current_timestamp on update current_timestamp comment '更新时间', 13 | `delete_time` datetime default null comment '删除时间(逻辑删除标志)', 14 | `create_client_ip` varchar(15) not null default '' comment '创建时的客户端 IP 地址', 15 | `update_client_ip` varchar(15) not null default '' comment '最后一次更新时的客户端 IP 地址', 16 | `update_count` bigint unsigned not null default 0 comment '更新次数', 17 | 18 | /* 下方为业务字段 */ 19 | `phone` char(11) not null default '' comment '手机号', 20 | `code` char(6) not null default '' comment '短信验证码', 21 | `ip` char(15) not null default '' comment '客户端 IP 地址', 22 | `pre_send_time` datetime not null comment '短信发送时间(发送前)', 23 | `res_code` varchar(50) not null default '' comment '请求状态码', 24 | `res_message` varchar(200) not null default '' comment '状态码的描述', 25 | `res_biz_id` varchar(50) not null default '' comment '发送回执 ID', 26 | `request_id` varchar(50) not null default '' comment '请求 ID', 27 | `post_send_time` datetime not null comment '收到响应的时间', 28 | 29 | primary key (`id`) 30 | ) engine = InnoDB 31 | default character set = `utf8mb4` comment ='短信发送日志'; 32 | -------------------------------------------------------------------------------- /life-helper-external/life-helper-wemap/src/main/java/com/weutil/wemap/model/WeMapReverseGeocodeResponse.java: -------------------------------------------------------------------------------- 1 | package com.weutil.wemap.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | /** 7 | * 腾讯位置服务逆地址解析响应数据 8 | * 9 | * @author inlym 10 | * @date 2024/7/16 11 | * @since 3.0.0 12 | **/ 13 | @Data 14 | public class WeMapReverseGeocodeResponse { 15 | /** 状态码,0为正常,其它为异常 */ 16 | private Integer status; 17 | 18 | /** 对 status 的描述 */ 19 | private String message; 20 | 21 | /** 本次请求的唯一标识 */ 22 | @JsonProperty("request_id") 23 | private String requestId; 24 | 25 | private ReverseGeocodingResult result; 26 | 27 | @Data 28 | public static class ReverseGeocodingResult { 29 | /** 以行政区划+道路+门牌号等信息组成的标准格式化地址 */ 30 | private String address; 31 | 32 | @JsonProperty("formatted_addresses") 33 | private FormattedAddresses formattedAddresses; 34 | 35 | @JsonProperty("address_component") 36 | private AddressComponent addressComponent; 37 | } 38 | 39 | /** 结合知名地点形成的描述性地址,更具人性化特点 */ 40 | @Data 41 | public static class FormattedAddresses { 42 | /** 推荐使用的地址描述,描述精确性较高 */ 43 | private String recommend; 44 | 45 | /** 粗略位置描述 */ 46 | private String rough; 47 | } 48 | 49 | /** 地址部件 */ 50 | @Data 51 | public static class AddressComponent { 52 | /** 国家 */ 53 | private String nation; 54 | 55 | /** 省 */ 56 | private String province; 57 | 58 | /** 市,如果当前城市为省直辖县级区划,city与district字段均会返回此城市 */ 59 | private String city; 60 | 61 | /** 区,可能为空字串 */ 62 | private String district; 63 | 64 | /** 街道,可能为空字串 */ 65 | private String street; 66 | 67 | /** 门牌,可能为空字串 */ 68 | @JsonProperty("street_number") 69 | private String streetNumber; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/entity/RequestLog.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.entity; 2 | 3 | import com.mybatisflex.annotation.Table; 4 | import com.weutil.common.model.ClientType; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | import lombok.experimental.SuperBuilder; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * 请求日志 15 | * 16 | * @author inlym 17 | * @date 2024/12/12 18 | * @since 3.0.0 19 | **/ 20 | @Table("request_log") 21 | @Data 22 | @EqualsAndHashCode(callSuper = true) 23 | @SuperBuilder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class RequestLog extends BaseUserRelatedEntity { 27 | 28 | // ---------- 原始数据字段 ---------- 29 | 30 | /** 请求方法 */ 31 | private String method; 32 | 33 | /** 请求路径 */ 34 | private String path; 35 | 36 | /** 请求参数 */ 37 | private String querystring; 38 | 39 | /** 响应状态码 */ 40 | private Integer status; 41 | 42 | /** 请求数据 */ 43 | private String requestBody; 44 | 45 | // ---------- 自定义数据处理字段 ---------- 46 | 47 | /** 请求开始时间 */ 48 | private LocalDateTime startTime; 49 | 50 | /** 请求结束时间 */ 51 | private LocalDateTime endTime; 52 | 53 | /** 请求时长(单位:毫秒) */ 54 | private Long duration; 55 | 56 | /** 57 | * 请求 ID(追踪 ID) 58 | * 59 | *

    说明 60 | *

    生产环境中会由 API 网关在请求头中传入,在开发环境需模拟生成该值。 61 | */ 62 | private String traceId; 63 | 64 | /** 65 | * 客户端 IP 地址 66 | * 67 | *

    说明 68 | *

    生产环境中会由 API 网关在请求头中传入,而不是通过连接直接获取。 69 | */ 70 | private String clientIp; 71 | 72 | /** 访问凭证 */ 73 | private String accessToken; 74 | 75 | /** 客户端类型 */ 76 | private ClientType clientType; 77 | 78 | /** 客户端 ID */ 79 | private String clientId; 80 | 81 | /** 客户端版本号 */ 82 | private String clientVersion; 83 | } 84 | -------------------------------------------------------------------------------- /life-helper-todo/src/main/java/com/weutil/todo/entity/TodoTask.java: -------------------------------------------------------------------------------- 1 | package com.weutil.todo.entity; 2 | 3 | import com.mybatisflex.annotation.RelationManyToOne; 4 | import com.mybatisflex.annotation.Table; 5 | import com.weutil.common.entity.BaseUserRelatedEntity; 6 | import com.weutil.todo.model.Priority; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.NoArgsConstructor; 11 | import lombok.experimental.SuperBuilder; 12 | 13 | import java.time.LocalDate; 14 | import java.time.LocalDateTime; 15 | import java.time.LocalTime; 16 | 17 | /** 18 | * 待办任务实体 19 | * 20 | * @author inlym 21 | * @date 2024/12/12 22 | * @since 3.0.0 23 | **/ 24 | @Table("reminder_task") 25 | @Data 26 | @EqualsAndHashCode(callSuper = true) 27 | @SuperBuilder 28 | @NoArgsConstructor 29 | @AllArgsConstructor 30 | public class TodoTask extends BaseUserRelatedEntity { 31 | 32 | /** 所属项目 ID */ 33 | private Long projectId; 34 | 35 | /** 任务名称 */ 36 | private String name; 37 | 38 | /** 任务描述内容文本 */ 39 | private String content; 40 | 41 | /** 任务完成时间 */ 42 | private LocalDateTime completeTime; 43 | 44 | /** 45 | * 截止期限(日期+时间) 46 | * 47 | *

    字段说明 48 | *

    为方便内部计算处理,增加当前冗余字段,处理策略如下: 49 | *

    (1)若 {@code dueDate} 为空,则 {@code dueDateTime} 也为空。 50 | *

    (2)若 {@code dueDate} 不为空,但 {@code dueTime} 为空,则 {@code dueDateTime} 日期部分保持一致,时间部分替换为最大时间值 {@code 23:59:59}。 51 | *

    (3)若 {@code dueDate} 和 {@code dueTime} 均不为空,则 {@code dueDateTime} 填充对应值。 52 | */ 53 | private LocalDateTime dueDateTime; 54 | 55 | /** 截止期限的日期部分(年月日) */ 56 | private LocalDate dueDate; 57 | 58 | /** 截止期限的时间部分(时分秒) */ 59 | private LocalTime dueTime; 60 | 61 | /** 优先级 */ 62 | private Priority priority; 63 | 64 | // ============================ 关联字段 ============================ 65 | 66 | @RelationManyToOne(selfField = "projectId") 67 | private TodoProject project; 68 | } 69 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/SimpleAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | /** 12 | * 自定义简单鉴权凭证 13 | * 14 | *

    主要用途 15 | *

    通过鉴权后,将相关信息带入生成这个鉴权凭证,用于在 Spring Security 中使用。 16 | * 17 | *

    注意事项 18 | *

    当前只用到用户 ID 和用户角色。 19 | * 20 | * @author inlym 21 | * @date 2024/7/14 22 | * @since 3.0.0 23 | **/ 24 | public class SimpleAuthentication implements Authentication { 25 | private final Long userId; 26 | 27 | private final List authorities = new ArrayList<>(); 28 | 29 | private Boolean authenticated; 30 | 31 | public SimpleAuthentication(long userId) { 32 | this.userId = userId; 33 | this.authorities.add(new SimpleGrantedAuthority(Role.USER)); 34 | this.authenticated = true; 35 | } 36 | 37 | public long getUserId() { 38 | return userId; 39 | } 40 | 41 | @Override 42 | public Collection getAuthorities() { 43 | return this.authorities; 44 | } 45 | 46 | @Override 47 | public Object getCredentials() { 48 | return null; 49 | } 50 | 51 | @Override 52 | public Object getDetails() { 53 | return null; 54 | } 55 | 56 | @Override 57 | public Object getPrincipal() { 58 | return this.userId; 59 | } 60 | 61 | @Override 62 | public boolean isAuthenticated() { 63 | return this.authenticated; 64 | } 65 | 66 | @Override 67 | public void setAuthenticated(boolean b) throws IllegalArgumentException { 68 | this.authenticated = b; 69 | } 70 | 71 | @Override 72 | public String getName() { 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-oss/src/main/java/com/weutil/oss/annotation/OssResourceJsonSerializer.java: -------------------------------------------------------------------------------- 1 | package com.weutil.oss.annotation; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.BeanProperty; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 8 | import com.fasterxml.jackson.databind.ser.ContextualSerializer; 9 | import com.fasterxml.jackson.databind.ser.std.StdSerializer; 10 | import com.weutil.oss.service.OssService; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.io.IOException; 15 | 16 | /** 17 | * 阿里云 OSS 资源注解序列化器 18 | * 19 | * @author inlym 20 | * @date 2024/7/18 21 | * @since 3.0.0 22 | **/ 23 | @Component 24 | @JsonSerialize(using = OssResourceJsonSerializer.class) 25 | public class OssResourceJsonSerializer extends StdSerializer implements ContextualSerializer { 26 | private OssService ossService; 27 | 28 | protected OssResourceJsonSerializer() { 29 | super(String.class); 30 | } 31 | 32 | // 备注(2024.06.10) 33 | // [当前类不使用构造器注入的原因] 34 | // 使用构造器注入,则无默认构造器,会有警告提示。 35 | @Autowired 36 | public void setOssService(OssService ossService) { 37 | this.ossService = ossService; 38 | } 39 | 40 | @Override 41 | public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { 42 | jsonGenerator.writeString(ossService.getPresignedUrl(s)); 43 | } 44 | 45 | @Override 46 | public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) { 47 | OssResource annotation = beanProperty.getAnnotation(OssResource.class); 48 | if (annotation != null) { 49 | return this; 50 | } 51 | 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /life-helper-web/src/main/resources/application-demo.yml: -------------------------------------------------------------------------------- 1 | # ========================== 文件说明 ========================== 2 | # 当前文件仅用于演示对应配置文件的格式,请将 xxxxxxxxxxxx 替换为实际的值,并改名为 `application-dev.yml` 或 `application-prod.yml` 3 | 4 | # ======================== 以下是配置内容 ======================== 5 | 6 | server: # Web 监听端口 7 | port: 8080 8 | 9 | spring: 10 | data: # Redis 配置 11 | redis: 12 | host: xxxxxxxxxxxx 13 | database: 0 14 | port: 6379 15 | password: xxxxxxxxxxxx 16 | connect-timeout: 1000 17 | timeout: 20000 18 | 19 | datasource: 20 | url: jdbc:mysql://localhost:3306/lifehelper_db_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai 21 | username: xxxxxxxxxxxx 22 | password: xxxxxxxxxxxx 23 | driver-class-name: com.mysql.cj.jdbc.Driver 24 | 25 | # 日志 26 | logging: 27 | # 日志等级 28 | level: 29 | com.weutil: trace 30 | 31 | # 阿里云资源配置 32 | aliyun: 33 | # OSS 配置 34 | oss: 35 | # 存储空间名称 36 | bucket-name: xxxxxxxxxxxx 37 | # 访问端口 38 | endpoint: "oss-cn-hangzhou.aliyuncs.com" 39 | # 绑定的自定义域名 40 | custom-domain: xxxxxxxxxxxx 41 | # AccessKey ID 42 | access-key-id: xxxxxxxxxxxx 43 | # AccessKey Secret 44 | access-key-secret: xxxxxxxxxxxx 45 | 46 | # 短信服务 47 | sms: 48 | access-key-id: xxxxxxxxxxxx 49 | access-key-secret: xxxxxxxxxxxx 50 | sign-name: xxxxxxxxxxxx 51 | 52 | # 验证码服务 53 | captcha: 54 | access-key-id: xxxxxxxxxxxx 55 | access-key-secret: xxxxxxxxxxxx 56 | scene-id: xxxxxxxxxxxx 57 | 58 | # 腾讯位置服务 59 | wemap: 60 | # 开发者密钥 61 | key: xxxxxxxxxxxx 62 | 63 | # 流水线环境变量 64 | # https://help.aliyun.com/zh/yunxiao/user-guide/environment-variables 65 | pipeline: 66 | # 以下变量将在阿里云云效流水线部署时替换为实际值,不需要手工处理 67 | # https://help.aliyun.com/zh/yunxiao/user-guide/environment-variables#b106db50a15jy 68 | build-number: ${BUILD_NUMBER} 69 | commit-sha: ${CI_COMMIT_SHA} 70 | commit-ref-name: ${CI_COMMIT_REF_NAME} 71 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-captcha/src/main/java/com/weutil/aliyun/captcha/service/AliyunCaptchaService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.aliyun.captcha.service; 2 | 3 | import com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaResponse; 4 | import com.weutil.aliyun.captcha.exception.AliyunCaptchaVerifiedFailureException; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | * 阿里云验证码服务 11 | * 12 | *

    说明 13 | *

    对 API 做进一步封装,用于内部调用。 14 | * 15 | * @author inlym 16 | * @date 2024/10/16 17 | * @since 3.0.0 18 | **/ 19 | @Service 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | public class AliyunCaptchaService { 23 | private final AliyunCaptchaApiService aliyunCaptchaApiService; 24 | 25 | /** 26 | * 检验验证码参数(校验通过或直接抛出异常) 27 | * 28 | *

    说明 29 | *

    处理成抛出错误而不是直接返回布尔值的原因是:能够中断流程,否则一系列判断很麻烦。 30 | * 31 | * @param captchaVerifyParam 由验证码脚本回调的验证参数 32 | * 33 | * @date 2024/12/15 34 | * @since 3.0.0 35 | */ 36 | public void verifyOrThrow(String captchaVerifyParam) { 37 | boolean result = verifyCaptcha(captchaVerifyParam); 38 | if (!result) { 39 | throw new AliyunCaptchaVerifiedFailureException(); 40 | } 41 | } 42 | 43 | /** 44 | * 校验验证码参数 45 | * 46 | * @param captchaVerifyParam 由验证码脚本回调的验证参数 47 | * 48 | * @return 是否验证通过 49 | * @date 2024/10/16 50 | * @since 3.0.0 51 | */ 52 | public boolean verifyCaptcha(String captchaVerifyParam) { 53 | VerifyIntelligentCaptchaResponse response = aliyunCaptchaApiService.verifyCaptcha(captchaVerifyParam); 54 | if (!response.getBody().getSuccess() || !"Success".equalsIgnoreCase(response.getBody().getCode())) { 55 | log.debug("验证码校验失败,响应结果:{}", response.getBody()); 56 | return false; 57 | } 58 | 59 | if (!response.getBody().getResult().verifyResult) { 60 | log.debug("验证码校验失败,原因码:{}", response.getBody().getResult().verifyCode); 61 | return false; 62 | } 63 | 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import com.weutil.common.annotation.resolver.ClientIpMethodArgumentResolver; 4 | import com.weutil.common.annotation.resolver.UserIdMethodArgumentResolver; 5 | import com.weutil.common.interceptor.LogInterceptor; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 9 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 10 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * MVC 配置 17 | *

    18 | *

    说明 19 | *

    当前类如果 {@code extends WebMvcConfigurationSupport} 时,会导致配置文件中的 Jackson 配置无效,因此要用如下实现接口对方式。 20 | * 21 | * @author inlym 22 | * @date 2024/7/14 23 | * @since 3.0.0 24 | **/ 25 | @Configuration 26 | @RequiredArgsConstructor 27 | public class WebMvcConfig implements WebMvcConfigurer { 28 | private final LogInterceptor logInterceptor; 29 | 30 | /** 31 | * 配置拦截器 32 | * 33 | * @date 2024/7/16 34 | * @since 3.0.0 35 | */ 36 | @Override 37 | public void addInterceptors(InterceptorRegistry registry) { 38 | registry.addInterceptor(logInterceptor).order(1).addPathPatterns("/**").excludePathPatterns("/ping"); 39 | } 40 | 41 | /** 42 | * 跨域资源共享配置 43 | * 44 | * @date 2024/7/16 45 | * @since 3.0.0 46 | */ 47 | @Override 48 | public void addCorsMappings(CorsRegistry registry) { 49 | registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*").maxAge(864000L); 50 | } 51 | 52 | /** 53 | * 注解解析器配置 54 | * 55 | * @date 2024/7/16 56 | * @since 3.0.0 57 | */ 58 | @Override 59 | public void addArgumentResolvers(List argumentResolvers) { 60 | argumentResolvers.add(new UserIdMethodArgumentResolver()); 61 | argumentResolvers.add(new ClientIpMethodArgumentResolver()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/ClientInfoFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.model.CustomHttpHeader; 4 | import com.weutil.common.model.CustomRequestAttribute; 5 | import jakarta.servlet.FilterChain; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * 客户端信息过滤器 16 | * 17 | *

    客户端信息 18 | *

    1. [type] -> [web, miniprogram] 19 | *

    2. [id] -> web 为域名地址,miniprogram 为小程序的 appId 20 | *

    3. [version] -> 示例值: 3.0.0 21 | * 22 | * @author inlym 23 | * @date 2024/12/12 24 | * @since 3.0.0 25 | **/ 26 | @Component 27 | public class ClientInfoFilter extends OncePerRequestFilter { 28 | /** 传递客户端信息的请求头字段 */ 29 | private static final String HEADER_NAME = CustomHttpHeader.CLIENT_INFO; 30 | 31 | @Override 32 | protected boolean shouldNotFilter(HttpServletRequest request) { 33 | // 指定请求头为空则不进行任何处理 34 | return request.getHeader(HEADER_NAME) == null; 35 | } 36 | 37 | @Override 38 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 39 | // 文本格式为: `key1=value1; key2=value2; key3=value3` 40 | String str = request.getHeader(HEADER_NAME); 41 | 42 | for (String item : str.split("; ")) { 43 | String[] keypair = item.split("="); 44 | 45 | String name = keypair[0]; 46 | String value = keypair[1]; 47 | 48 | if ("type".equalsIgnoreCase(name)) { 49 | request.setAttribute(CustomRequestAttribute.CLIENT_TYPE, value); 50 | } else if ("id".equalsIgnoreCase(name)) { 51 | request.setAttribute(CustomRequestAttribute.CLIENT_ID, value); 52 | } else if ("version".equalsIgnoreCase(name)) { 53 | request.setAttribute(CustomRequestAttribute.CLIENT_VERSION, value); 54 | } 55 | } 56 | 57 | chain.doFilter(request, response); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/controller/PhoneCodeLoginController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.controller; 2 | 3 | import com.weutil.account.model.PhoneCodeLoginDTO; 4 | import com.weutil.account.model.SendingSmsDTO; 5 | import com.weutil.account.service.PhoneCodeLoginService; 6 | import com.weutil.aliyun.captcha.service.AliyunCaptchaService; 7 | import com.weutil.common.annotation.ClientIp; 8 | import com.weutil.common.model.IdentityCertificate; 9 | import jakarta.validation.Valid; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.Map; 16 | 17 | /** 18 | * 手机短信验证码登录控制器 19 | * 20 | * @author inlym 21 | * @date 2024/7/22 22 | * @since 3.0.0 23 | **/ 24 | @RestController 25 | @RequiredArgsConstructor 26 | public class PhoneCodeLoginController { 27 | private final PhoneCodeLoginService phoneCodeLoginService; 28 | private final AliyunCaptchaService aliyunCaptchaService; 29 | 30 | /** 31 | * 发送登录使用的短信验证码 32 | * 33 | * @param ip 客户端 IP 地址 34 | * @param dto 请求数据 35 | * 36 | * @date 2024/6/23 37 | * @since 2.3.0 38 | */ 39 | @PostMapping("/sms/login") 40 | public Map sendSms(@ClientIp String ip, @Valid @RequestBody SendingSmsDTO dto) { 41 | String phone = dto.getPhone(); 42 | String captchaVerifyParam = dto.getCaptchaVerifyParam(); 43 | 44 | aliyunCaptchaService.verifyOrThrow(captchaVerifyParam); 45 | phoneCodeLoginService.sendSms(phone, ip); 46 | 47 | return Map.of("phone", phone); 48 | } 49 | 50 | /** 51 | * 使用短信验证码登录 52 | * 53 | * @param ip 客户端 IP 地址 54 | * @param dto 请求数据 55 | * 56 | * @date 2024/6/23 57 | * @since 2.3.0 58 | */ 59 | @PostMapping("/login/sms") 60 | public IdentityCertificate loginBySmsCode(@ClientIp String ip, @Valid @RequestBody PhoneCodeLoginDTO dto) { 61 | String phone = dto.getPhone(); 62 | String code = dto.getCode(); 63 | 64 | return phoneCodeLoginService.loginByPhoneCode(phone, code, ip); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/model/CustomCachingRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.model; 2 | 3 | import jakarta.servlet.ReadListener; 4 | import jakarta.servlet.ServletInputStream; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletRequestWrapper; 7 | import org.springframework.util.StreamUtils; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | 14 | /** 15 | * 自定义缓存请求体封装 16 | * 17 | *

    说明 18 | *

    为什么不使用 {@link org.springframework.web.util.ContentCachingRequestWrapper} ? 19 | *

    {@link org.springframework.web.util.ContentCachingRequestWrapper} 没有修改 getInputStream() 和 getReader() 方法,只能在使用 @RequestBody 注解后使用。 20 | * 21 | * @author inlym 22 | * @date 2024/10/15 23 | * @since 3.0.0 24 | **/ 25 | public class CustomCachingRequestWrapper extends HttpServletRequestWrapper { 26 | // 用于缓存请求体内容 27 | private final byte[] content; 28 | 29 | public CustomCachingRequestWrapper(HttpServletRequest request) throws IOException { 30 | super(request); 31 | 32 | // 在构造方法中将请求体内容缓存到内部属性中 33 | this.content = StreamUtils.copyToByteArray(request.getInputStream()); 34 | } 35 | 36 | @Override 37 | public ServletInputStream getInputStream() { 38 | // 将缓存下来的内容转换为字节流 39 | final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content); 40 | 41 | return new ServletInputStream() { 42 | @Override 43 | public boolean isFinished() { 44 | return false; 45 | } 46 | 47 | @Override 48 | public boolean isReady() { 49 | return false; 50 | } 51 | 52 | @Override 53 | public void setReadListener(ReadListener listener) {} 54 | 55 | @Override 56 | public int read() { 57 | return byteArrayInputStream.read(); 58 | } 59 | }; 60 | } 61 | 62 | // 重写 getReader() 方法,这里复用 getInputStream() 的逻辑 63 | @Override 64 | public BufferedReader getReader() { 65 | return new BufferedReader(new InputStreamReader(getInputStream())); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/config/PhoneCodeLoginExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.config; 2 | 3 | import com.weutil.account.exception.NotSameIpException; 4 | import com.weutil.account.exception.PhoneCodeAttemptExceededException; 5 | import com.weutil.account.exception.PhoneCodeNotMatchException; 6 | import com.weutil.common.model.ErrorResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.core.Ordered; 9 | import org.springframework.core.annotation.Order; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | import org.springframework.web.bind.annotation.RestControllerAdvice; 14 | 15 | /** 16 | * 短信验证码模块异常捕获器 17 | * 18 | * @author inlym 19 | * @date 2024/8/28 20 | * @since 3.0.0 21 | **/ 22 | @RestControllerAdvice 23 | @Slf4j 24 | @Order(Ordered.HIGHEST_PRECEDENCE + 1000) 25 | public class PhoneCodeLoginExceptionHandler { 26 | 27 | /** 28 | * 处理验证码不正确问题 29 | * 30 | *

    异常说明 31 | *

    [实际原因] 验证码输入错误 32 | *

    [前端使用] 无需额外处理,直接展示提示文案 33 | */ 34 | @ResponseStatus(HttpStatus.OK) 35 | @ExceptionHandler({PhoneCodeNotMatchException.class}) 36 | public ErrorResponse handlePhoneCodeNotMatchException() { 37 | return new ErrorResponse(11011, "验证码错误,请重新输入"); 38 | } 39 | 40 | /** 41 | * 处理验证码输入错误太多问题 42 | * 43 | *

    异常说明 44 | *

    [实际原因] 验证码输入错误的次数超过了限制(10次) 45 | *

    [前端使用] 无需额外处理,直接展示提示文案 46 | */ 47 | @ResponseStatus(HttpStatus.OK) 48 | @ExceptionHandler({PhoneCodeAttemptExceededException.class}) 49 | public ErrorResponse handlePhoneCodeAttemptExceededException() { 50 | return new ErrorResponse(11013, "你输入的验证码错误次数过多,已限制您的操作,请5分钟后再试"); 51 | } 52 | 53 | /** 54 | * 处理登录设备与获取验证码设备的 IP 地址不一致问题 55 | * 56 | *

    异常说明 57 | *

    [实际原因] 获取验证码操作的客户端与“验证”操作的客户端对应的 IP 地址不一致 58 | *

    [备注] 为安全起见,此项返回了模糊的提示文案,未告知真实的错误原因 59 | */ 60 | @ResponseStatus(HttpStatus.OK) 61 | @ExceptionHandler({NotSameIpException.class}) 62 | public ErrorResponse handleNotSameIpException() { 63 | return new ErrorResponse(11014, "由于网络环境变化,你的验证码已失效,请重新获取验证码"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/AccessTokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.model.AccessTokenDetail; 4 | import com.weutil.common.model.CustomHttpHeader; 5 | import com.weutil.common.model.CustomRequestAttribute; 6 | import com.weutil.common.model.SimpleAuthentication; 7 | import com.weutil.common.service.AccessTokenService; 8 | import jakarta.servlet.FilterChain; 9 | import jakarta.servlet.ServletException; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import jakarta.servlet.http.HttpServletResponse; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.filter.OncePerRequestFilter; 16 | 17 | import java.io.IOException; 18 | 19 | /** 20 | * 访问凭据过滤器 21 | * 22 | * @author inlym 23 | * @date 2024/7/14 24 | * @since 3.0.0 25 | **/ 26 | @Component 27 | @RequiredArgsConstructor 28 | public class AccessTokenFilter extends OncePerRequestFilter { 29 | /** 请求头 */ 30 | private static final String HEADER_NAME = CustomHttpHeader.ACCESS_TOKEN; 31 | 32 | private final AccessTokenService accessTokenService; 33 | 34 | @Override 35 | protected boolean shouldNotFilter(HttpServletRequest request) { 36 | // 指定请求头为空则不进行任何处理 37 | return request.getHeader(HEADER_NAME) == null; 38 | } 39 | 40 | @Override 41 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 42 | String token = request.getHeader(HEADER_NAME); 43 | if (token != null) { 44 | request.setAttribute(CustomRequestAttribute.ACCESS_TOKEN, token); 45 | AccessTokenDetail detail = accessTokenService.parse(token); 46 | if (detail != null) { 47 | SimpleAuthentication authentication = new SimpleAuthentication(detail.getUserId()); 48 | 49 | SecurityContextHolder.getContext().setAuthentication(authentication); 50 | request.setAttribute(CustomRequestAttribute.USER_ID, detail.getUserId()); 51 | } 52 | } 53 | 54 | chain.doFilter(request, response); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/service/AccessTokenService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.weutil.common.model.AccessTokenDetail; 5 | import com.weutil.common.util.RandomStringUtil; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.SneakyThrows; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.data.redis.core.StringRedisTemplate; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.time.Duration; 13 | 14 | /** 15 | * 访问凭证服务 16 | * 17 | * @author inlym 18 | * @date 2024/7/14 19 | * @since 3.0.0 20 | **/ 21 | @Service 22 | @Slf4j 23 | @RequiredArgsConstructor 24 | public class AccessTokenService { 25 | private final StringRedisTemplate stringRedisTemplate; 26 | private final ObjectMapper objectMapper; 27 | 28 | /** 29 | * 创建访问凭证 30 | * 31 | * @param userId 用户 ID 32 | * @param duration 有效时长 33 | * 34 | * @return 访问凭证文本 35 | * @date 2024/7/14 36 | * @since 3.0.0 37 | */ 38 | @SneakyThrows 39 | public String create(long userId, Duration duration) { 40 | String token = RandomStringUtil.generate(32); 41 | AccessTokenDetail accessTokenDetail = AccessTokenDetail.builder().userId(userId).build(); 42 | stringRedisTemplate.opsForValue().set(generateKey(token), objectMapper.writeValueAsString(accessTokenDetail), duration); 43 | log.info("[生成访问凭证] userId={}, token={}", userId, token); 44 | 45 | return token; 46 | } 47 | 48 | /** 49 | * 生成在 Redis 中使用的键名 50 | * 51 | * @param token 访问凭证 52 | * 53 | * @date 2024/7/14 54 | * @since 3.0.0 55 | */ 56 | private static String generateKey(String token) { 57 | return "auth:access-token:" + token; 58 | } 59 | 60 | /** 61 | * 解析访问凭证 62 | * 63 | * @param token 访问凭证 64 | * 65 | * @date 2024/7/14 66 | * @since 3.0.0 67 | */ 68 | @SneakyThrows 69 | public AccessTokenDetail parse(String token) { 70 | String key = generateKey(token); 71 | String value = stringRedisTemplate.opsForValue().get(key); 72 | if (value == null) { 73 | return null; 74 | } 75 | 76 | return objectMapper.readValue(value, AccessTokenDetail.class); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /DevOps/mysql/schema/life-helper-common.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------------------------------------------------------------------------------------------- 2 | -- 请求日志表 3 | -- 对应实体: [com.weutil.common.entity.RequestLog] 4 | -- 创建时间: 2024/12/12 5 | -- ---------------------------------------------------------------------------------------------------------------- 6 | 7 | create table `request_log` 8 | ( 9 | /* 下方是通用字段 */ 10 | `id` bigint unsigned not null auto_increment comment '主键 ID', 11 | `create_time` datetime not null default current_timestamp comment '创建时间', 12 | `update_time` datetime not null default current_timestamp on update current_timestamp comment '更新时间', 13 | `delete_time` datetime default null comment '删除时间(逻辑删除标志)', 14 | `create_client_ip` varchar(15) not null default '' comment '创建时的客户端 IP 地址', 15 | `update_client_ip` varchar(15) not null default '' comment '最后一次更新时的客户端 IP 地址', 16 | `update_count` bigint unsigned not null default 0 comment '更新次数', 17 | 18 | /* 下方为用户相关类数据表通用字段 */ 19 | `user_id` bigint unsigned not null default 0 comment '所属的用户 ID', 20 | 21 | /* 下方为业务字段 */ 22 | `method` varchar(10) not null default '' comment '请求方法', 23 | `path` varchar(100) not null default '' comment '请求路径', 24 | `querystring` varchar(300) not null default '' comment '请求参数', 25 | `status` int not null default 0 comment '响应状态码', 26 | `request_body` varchar(10000) not null default '' comment '请求数据', 27 | 28 | `start_time` datetime not null comment '请求开始时间', 29 | `end_time` datetime not null comment '请求结束时间', 30 | `duration` int unsigned not null default 0 comment '请求时长(单位:毫秒)', 31 | 32 | `trace_id` varchar(50) not null default '' comment '请求 ID(追踪 ID)', 33 | `client_ip` varchar(50) not null default '' comment '客户端 IP 地址', 34 | `access_token` varchar(50) not null default '' comment '访问凭证', 35 | `client_type` int not null default 0 comment '客户端类型(枚举值)', 36 | `client_id` varchar(50) not null default '' comment '客户端 ID', 37 | `client_version` varchar(10) not null default '' comment '客户端版本号', 38 | 39 | primary key (`id`) 40 | ) engine = InnoDB 41 | default character set = `utf8mb4` comment ='请求日志表'; 42 | -------------------------------------------------------------------------------- /life-helper-web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.weutil 8 | life-helper-server 9 | ${revision} 10 | 11 | 12 | life-helper-web 13 | jar 14 | 15 | ${project.artifactId} 16 | Web 应用入口 17 | 18 | 19 | 20 | com.weutil 21 | life-helper-common 22 | 23 | 24 | 25 | com.weutil 26 | life-helper-system 27 | 28 | 29 | 30 | com.weutil 31 | life-helper-aliyun-oss 32 | 33 | 34 | 35 | com.weutil 36 | life-helper-aliyun-sms 37 | 38 | 39 | 40 | com.weutil 41 | life-helper-account 42 | 43 | 44 | 45 | com.weutil 46 | life-helper-todo 47 | 48 | 49 | 50 | 51 | 52 | 53 | ${project.artifactId} 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-maven-plugin 59 | 60 | 61 | 62 | org.projectlombok 63 | lombok 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/service/AliyunSmsApiService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.service; 2 | 3 | import com.aliyun.dysmsapi20170525.models.SendSmsRequest; 4 | import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody; 5 | import com.weutil.sms.config.AliyunSmsProperties; 6 | import com.weutil.sms.exception.SmsSentFailureException; 7 | import com.weutil.sms.model.AliyunSmsClient; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Objects; 13 | 14 | /** 15 | * 阿里云短信 API 服务 16 | * 17 | *

    说明 18 | *

    封装阿里云短信相关的底层方法。 19 | * 20 | * @author inlym 21 | * @date 2024/7/16 22 | * @since 3.0.0 23 | **/ 24 | @Service 25 | @RequiredArgsConstructor 26 | @Slf4j 27 | public class AliyunSmsApiService { 28 | /** 响应数据表达成功的 code 值 */ 29 | private static final String SUCCESS_CODE = "OK"; 30 | 31 | private final AliyunSmsProperties properties; 32 | private final AliyunSmsClient aliyunSmsClient; 33 | 34 | /** 35 | * 发送登录用途的短信验证码 36 | * 37 | * @param phone 手机号,示例值:{@code 13111111111} 38 | * @param code 6位纯数字格式的验证码,示例值:{@code 123456} 39 | * 40 | * @date 2024/6/12 41 | * @since 2.3.0 42 | */ 43 | public SendSmsResponseBody sendPhoneCode(String phone, String code) { 44 | SendSmsRequest sendSmsRequest = new SendSmsRequest() 45 | .setPhoneNumbers(phone) 46 | .setSignName(properties.getSignName()) 47 | .setTemplateCode("SMS_468360281") 48 | .setTemplateParam("{\"code\":\"" + code + "\"}"); 49 | 50 | try { 51 | SendSmsResponseBody result = aliyunSmsClient.sendSms(sendSmsRequest).getBody(); 52 | if (Objects.equals(result.getCode(), SUCCESS_CODE)) { 53 | log.info("[SendSms] 短信发送成功, phone={}, code={}, BizId={}", phone, code, result.getBizId()); 54 | return result; 55 | } else { 56 | log.error("[SendSms] 短信发送失败,phone={}, code={},error_code={}, message={}", phone, code, result.getCode(), result.getMessage()); 57 | throw new SmsSentFailureException(); 58 | } 59 | } catch (Exception e) { 60 | // 其他异常,统一处理成“发送失败” 61 | log.error("[SendSms] 短信发送失败,错误消息:{}", e.getMessage()); 62 | throw new SmsSentFailureException(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/ClientIpFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.model.CustomHttpHeader; 4 | import com.weutil.common.model.CustomRequestAttribute; 5 | import jakarta.servlet.FilterChain; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import java.io.IOException; 13 | 14 | /** 15 | * 客户端 IP 地址过滤器 16 | * 17 | *

    主要用途 18 | *

    用于获取客户端的 IP 地址。 19 | * 20 | *

    背景介绍 21 | *

    由于服务器架构问题(客户端 -> API 网关 -> 负载均衡 -> 应用服务器),服务器无法直接获取客户端的 IP 地址,只能依赖从网关处获取客户端 IP 地址并将其传递给应用服务器。 22 | * 请求传递链中的 API 网关和负载均衡直接使用了阿里云的服务,提供了2种通过请求头传递客户端 IP 地址的方式: 23 | * 24 | *

  • API 网关处可自定义传递客户端 IP 地址的请求头,但是如果客户端伪造请求传递了该请求头,API 网关处将不再传递该值,导致获取不到真实的客户端 IP 地址。 25 | *
  • 负载均衡通过 `X-Forwarded-For` 请求头字段传递客户端 IP 地址,如果客户端伪造请求传递了该请求头,则会将真实的数据加在已有值的后面并用逗号分隔。 26 | * 27 | *

    综合考虑以上优缺点,决定使用从负载均衡传递的 `X-Forwarded-For` 请求头字段获取真实的客户端 IP 地址。 28 | * 29 | * @author inlym 30 | * @date 2024/7/14 31 | * @since 3.0.0 32 | **/ 33 | @Component 34 | public class ClientIpFilter extends OncePerRequestFilter { 35 | /** 36 | * 传递客户端 IP 地址的请求头字段 37 | * 38 | *

    说明 39 | *

    该字段为阿里云负载均衡传入。 40 | * 41 | *

    文档地址:HTTP头字段 42 | */ 43 | private static final String HEADER_NAME = CustomHttpHeader.CLIENT_IP; 44 | 45 | @Override 46 | protected boolean shouldNotFilter(HttpServletRequest request) { 47 | // 指定请求头为空则不进行任何处理 48 | return request.getHeader(HEADER_NAME) == null; 49 | } 50 | 51 | @Override 52 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 53 | // 一般情况下,该请求头的格式为 `<真实的客户端 IP>, `,如果攻击者伪造请求并传递了该字段, 54 | // 格式将变成 `<攻击者定义的值>, <真实的客户端 IP>, `,考虑到以上2种情况,获取其中的 55 | // “真实的客户端 IP”的方式应为:以逗号分隔,取倒数第2段的值。 56 | String ipString = request.getHeader(HEADER_NAME); 57 | 58 | if (ipString != null) { 59 | // 使用 `,` 分割字符串,取最后第2段 60 | String[] ips = ipString.split(","); 61 | String clientIp = ips[ips.length - 2].trim(); 62 | 63 | request.setAttribute(CustomRequestAttribute.CLIENT_IP, clientIp); 64 | } 65 | 66 | chain.doFilter(request, response); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/filter/RequestLogFilter.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.filter; 2 | 3 | import com.weutil.common.entity.RequestLog; 4 | import com.weutil.common.model.CustomRequestAttribute; 5 | import com.weutil.common.service.RequestLogService; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.ServletException; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.core.Ordered; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.filter.OncePerRequestFilter; 15 | 16 | import java.io.IOException; 17 | import java.time.LocalDateTime; 18 | 19 | /** 20 | * 请求日志过滤器 21 | * 22 | * @author inlym 23 | * @date 2024/12/12 24 | * @since 3.0.0 25 | **/ 26 | @Component 27 | @Order(Ordered.HIGHEST_PRECEDENCE + 1) 28 | @RequiredArgsConstructor 29 | public class RequestLogFilter extends OncePerRequestFilter { 30 | private final RequestLogService requestLogService; 31 | 32 | @Override 33 | protected boolean shouldNotFilter(HttpServletRequest request) { 34 | // 该请求路径用于健康检查,不做任何处理 35 | return "/ping".equalsIgnoreCase(request.getServletPath()); 36 | } 37 | 38 | @Override 39 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { 40 | RequestLog requestLog = requestLogService.transform(request); 41 | requestLog.setStartTime(LocalDateTime.now()); 42 | 43 | chain.doFilter(request, response); 44 | 45 | requestLog.setEndTime(LocalDateTime.now()); 46 | requestLog.setTraceId((String) request.getAttribute(CustomRequestAttribute.TRACE_ID)); 47 | requestLog.setClientIp((String) request.getAttribute(CustomRequestAttribute.CLIENT_IP)); 48 | requestLog.setAccessToken((String) request.getAttribute(CustomRequestAttribute.ACCESS_TOKEN)); 49 | requestLog.setUserId((Long) request.getAttribute(CustomRequestAttribute.USER_ID)); 50 | requestLog.setClientType(requestLogService.parseClientType((String) request.getAttribute(CustomRequestAttribute.CLIENT_TYPE))); 51 | requestLog.setClientId((String) request.getAttribute(CustomRequestAttribute.CLIENT_ID)); 52 | requestLog.setClientVersion((String) request.getAttribute(CustomRequestAttribute.CLIENT_VERSION)); 53 | 54 | requestLog.setStatus(response.getStatus()); 55 | 56 | requestLogService.recordAsync(requestLog); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /life-helper-aliyun/life-helper-aliyun-sms/src/main/java/com/weutil/sms/config/AliyunSmsExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.weutil.sms.config; 2 | 3 | import com.weutil.common.model.ErrorResponse; 4 | import com.weutil.sms.exception.InvalidPhoneNumberException; 5 | import com.weutil.sms.exception.SmsRateLimitExceededException; 6 | import com.weutil.sms.exception.SmsSentFailureException; 7 | import com.weutil.sms.model.SmsRateLimitExceededExceptionResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.core.Ordered; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.bind.annotation.RestControllerAdvice; 15 | 16 | /** 17 | * 阿里云短信模块异常处理器 18 | * 19 | *

    错误码范围 20 | *

    {@code 12301} ~ {@code 12399} 21 | * 22 | * @author inlym 23 | * @date 2024/7/16 24 | * @since 3.0.0 25 | **/ 26 | @RestControllerAdvice 27 | @Slf4j 28 | @Order(Ordered.HIGHEST_PRECEDENCE + 1000) 29 | public class AliyunSmsExceptionHandler { 30 | 31 | // ================================================== 发送前校验环节 ================================================== 32 | 33 | /** 34 | * 处理手机号异常问题 35 | * 36 | *

    异常说明 37 | *

    [实际原因] 用户输入的手机号格式未通过校验(即不是一个正常的手机号) 38 | *

    [前端使用] 无需额外处理,直接展示提示文案 39 | */ 40 | @ResponseStatus(HttpStatus.BAD_REQUEST) 41 | @ExceptionHandler({InvalidPhoneNumberException.class}) 42 | public ErrorResponse handleInvalidPhoneNumberException() { 43 | return new ErrorResponse(12301, "你输入的手机号不正确,请重新输入"); 44 | } 45 | 46 | /** 47 | * 处理短信发送超频问题 48 | * 49 | *

    异常说明 50 | *

    [实际原因] 短信发送超过了频次限制(1条/1分钟 & 5条/1小时) 51 | *

    [前端使用] 自行拼接“剩余秒数”字段形成文案展示在按钮上,给出的提示文案作为保底方案 52 | */ 53 | @ResponseStatus(HttpStatus.OK) 54 | @ExceptionHandler({SmsRateLimitExceededException.class}) 55 | public ErrorResponse handleSmsRateLimitExceededException(SmsRateLimitExceededException exception) { 56 | return new SmsRateLimitExceededExceptionResponse(12303, "你获取验证码的次数超过限制,请稍后再试", exception.getRemainingSeconds()); 57 | } 58 | 59 | // ================================================== 发送环节 ================================================== 60 | 61 | /** 62 | * 处理短信发送失败问题 63 | * 64 | *

    异常说明 65 | *

    [实际原因] 短信验证码发送,由 API 返回的结果告知发送失败 66 | *

    [前端使用] 无需额外处理,直接展示提示文案 67 | */ 68 | @ResponseStatus(HttpStatus.OK) 69 | @ExceptionHandler({SmsSentFailureException.class}) 70 | public ErrorResponse handleSmsSentFailureException() { 71 | return new ErrorResponse(12302, "短信发送失败,请稍后再试"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /life-helper-account/src/main/java/com/weutil/account/service/PhoneCodeLoginService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.account.service; 2 | 3 | import com.weutil.account.entity.LoginLog; 4 | import com.weutil.account.entity.PhoneAccount; 5 | import com.weutil.account.mapper.LoginLogMapper; 6 | import com.weutil.account.model.LoginChannel; 7 | import com.weutil.account.model.LoginType; 8 | import com.weutil.common.model.IdentityCertificate; 9 | import com.weutil.common.service.IdentityCertificateService; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.time.LocalDateTime; 15 | 16 | /** 17 | * 手机验证码登录服务 18 | * 19 | *

    主要用途 20 | *

    处理使用“短信验证码”进行登录的各个环节。 21 | * 22 | * @author inlym 23 | * @date 2024/7/23 24 | * @since 3.0.0 25 | **/ 26 | @Service 27 | @Slf4j 28 | @RequiredArgsConstructor 29 | public class PhoneCodeLoginService { 30 | private final PhoneAccountService phoneAccountService; 31 | private final IdentityCertificateService identityCertificateService; 32 | private final LoginLogMapper loginHistoryMapper; 33 | private final PhoneCodeService phoneCodeService; 34 | 35 | /** 36 | * 发送短信验证码 37 | * 38 | * @param phone 手机号,示例值:{@code 13111111111} 39 | * @param ip 客户端 IP 地址,示例值:{@code 114.114.114.114} 40 | * 41 | * @date 2024/6/13 42 | * @since 2.3.0 43 | */ 44 | public void sendSms(String phone, String ip) { 45 | phoneCodeService.send(phone, ip); 46 | } 47 | 48 | /** 49 | * 通过短信验证码登录 50 | * 51 | * @param phone 手机号,示例值:{@code 13111111111} 52 | * @param code 6位纯数字格式的验证码,示例值:{@code 123456} 53 | * @param ip 客户端 IP 地址,示例值:{@code 114.114.114.114} 54 | * 55 | * @date 2024/06/23 56 | * @since 2.3.0 57 | */ 58 | public IdentityCertificate loginByPhoneCode(String phone, String code, String ip) { 59 | phoneCodeService.verifyOnceOrThrow(phone, code, ip); 60 | 61 | // 全部校验通过,登录成功 62 | PhoneAccount phoneAccount = phoneAccountService.getOrCreatePhoneAccount(phone); 63 | 64 | // 生成鉴权凭据 65 | IdentityCertificate identityCertificate = identityCertificateService.create(phoneAccount.getUserId()); 66 | 67 | // 记录到日志 68 | LoginLog history = LoginLog.builder() 69 | .type(LoginType.PHONE_SMS) 70 | .channel(LoginChannel.WEB) 71 | .userId(phoneAccount.getUserId()) 72 | .token(identityCertificate.getToken()) 73 | .ip(ip) 74 | .loginTime(LocalDateTime.now()) 75 | .phoneAccountId(phoneAccount.getId()) 76 | .build(); 77 | 78 | loginHistoryMapper.insertSelective(history); 79 | 80 | return identityCertificate; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/service/RequestLogService.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.service; 2 | 3 | import com.weutil.common.entity.RequestLog; 4 | import com.weutil.common.mapper.RequestLogMapper; 5 | import com.weutil.common.model.ClientType; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.scheduling.annotation.Async; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.io.IOException; 13 | import java.nio.charset.StandardCharsets; 14 | import java.time.Duration; 15 | 16 | /** 17 | * 请求日志服务 18 | * 19 | *

    主要用途 20 | *

    记录请求信息。 21 | * 22 | * @author inlym 23 | * @date 2024/12/12 24 | * @since 3.0.0 25 | **/ 26 | @Service 27 | @Slf4j 28 | @RequiredArgsConstructor 29 | public class RequestLogService { 30 | private final RequestLogMapper requestLogMapper; 31 | 32 | /** 33 | * 转化请求日志实体 34 | * 35 | * @param request 请求 36 | * 37 | * @date 2024/12/12 38 | * @since 3.0.0 39 | */ 40 | public RequestLog transform(HttpServletRequest request) { 41 | RequestLog requestLog = RequestLog.builder() 42 | .method(request.getMethod()) 43 | .path(request.getRequestURI()) 44 | .querystring(request.getQueryString()) 45 | .build(); 46 | 47 | try { 48 | requestLog.setRequestBody(new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8)); 49 | } catch (IOException e) { 50 | log.error("读取请求数据出错"); 51 | } 52 | 53 | return requestLog; 54 | } 55 | 56 | /** 57 | * 解析客户端类型 58 | * 59 | * @param type 类型文本 60 | * 61 | * @date 2024/12/12 62 | * @since 3.0.0 63 | */ 64 | public ClientType parseClientType(String type) { 65 | if ("web".equalsIgnoreCase(type)) { 66 | return ClientType.WEB; 67 | } 68 | 69 | if ("miniprogram".equalsIgnoreCase(type)) { 70 | return ClientType.MINI_PROGRAM; 71 | } 72 | 73 | return ClientType.UNKNOWN; 74 | } 75 | 76 | /** 77 | * 异步记录请求日志 78 | * 79 | * @param requestLog 请求日志实体 80 | * 81 | * @date 2024/12/12 82 | * @since 3.0.0 83 | */ 84 | @Async 85 | public void recordAsync(RequestLog requestLog) { 86 | long duration = Duration.between(requestLog.getStartTime(), requestLog.getEndTime()).toMillis(); 87 | requestLog.setDuration(duration); 88 | 89 | record(requestLog); 90 | } 91 | 92 | /** 93 | * 记录请求日志 94 | * 95 | * @param requestLog 请求日志实体 96 | * 97 | * @date 2024/12/12 98 | * @since 3.0.0 99 | */ 100 | public void record(RequestLog requestLog) { 101 | requestLogMapper.insertSelective(requestLog); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /life-helper-system/src/main/java/com/weutil/system/controller/SystemController.java: -------------------------------------------------------------------------------- 1 | package com.weutil.system.controller; 2 | 3 | import com.weutil.system.config.PipelineProperties; 4 | import com.weutil.system.model.ServerInfo; 5 | import com.weutil.system.service.DelayTimeService; 6 | import com.weutil.system.service.LaunchTimeService; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.boot.SpringBootVersion; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.net.Inet4Address; 15 | import java.net.InetAddress; 16 | import java.net.UnknownHostException; 17 | import java.time.Duration; 18 | import java.time.LocalDateTime; 19 | import java.time.ZoneId; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | /** 24 | * 系统信息控制器 25 | * 26 | *

    主要用途 27 | *

    查看系统的运行状态。 28 | * 29 | * @author inlym 30 | * @date 2024/12/4 31 | * @since 3.0.0 32 | **/ 33 | @RestController 34 | @Slf4j 35 | @RequiredArgsConstructor 36 | public class SystemController { 37 | private final Environment environment; 38 | private final LaunchTimeService launchTimeService; 39 | private final DelayTimeService delayTimeService; 40 | private final PipelineProperties pipelineProperties; 41 | 42 | /** 43 | * 查看系统服务器运行信息 44 | * 45 | * @date 2024/12/4 46 | * @since 3.0.0 47 | */ 48 | @GetMapping("/debug/system/server") 49 | public ServerInfo getServerInfo() { 50 | // 项目启动时间及运行时长 51 | LocalDateTime launchTime = launchTimeService.getLaunchTime(); 52 | LocalDateTime now = LocalDateTime.now(); 53 | long duration = Duration.between(launchTime, now).toSeconds(); 54 | 55 | // 各个中间件延迟时间 56 | Map delay = new HashMap<>(); 57 | delay.put("mysql", delayTimeService.calcMysqlDelayTime()); 58 | delay.put("redis", delayTimeService.calcRedisDelayTime()); 59 | 60 | ServerInfo info = ServerInfo.builder() 61 | .now(now) 62 | .launchTime(launchTime) 63 | .duration(duration) 64 | .activeProfiles(environment.getProperty("spring.profiles.active")) 65 | .serverPort(environment.getProperty("server.port")) 66 | .springBootVersion(SpringBootVersion.getVersion()) 67 | .timeZone(ZoneId.systemDefault().getId()) 68 | .delay(delay) 69 | .commitId(pipelineProperties.getCommitSha()) 70 | .commitRefName(pipelineProperties.getCommitRefName()) 71 | .buildNumber(pipelineProperties.getBuildNumber()) 72 | .build(); 73 | 74 | // 此处使用 `try...catch` 原因说明 75 | // 发生错误时,保证上述信息输出即可,无需由全局异常捕获器接管处理 76 | try { 77 | InetAddress localHost = Inet4Address.getLocalHost(); 78 | info.setHostName(localHost.getHostName()); 79 | info.setIp(localHost.getHostAddress()); 80 | } catch (UnknownHostException e) { 81 | log.error("获取主机信息失败"); 82 | } 83 | 84 | return info; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /life-helper-common/src/main/java/com/weutil/common/config/MyBatisFlexConfig.java: -------------------------------------------------------------------------------- 1 | package com.weutil.common.config; 2 | 3 | import com.mybatisflex.annotation.InsertListener; 4 | import com.mybatisflex.annotation.UpdateListener; 5 | import com.mybatisflex.core.FlexGlobalConfig; 6 | import com.mybatisflex.core.audit.AuditManager; 7 | import com.mybatisflex.core.audit.AuditMessage; 8 | import com.mybatisflex.core.audit.MessageCollector; 9 | import com.mybatisflex.core.logicdelete.LogicDeleteProcessor; 10 | import com.mybatisflex.core.logicdelete.impl.DateTimeLogicDeleteProcessor; 11 | import com.mybatisflex.spring.boot.MyBatisFlexCustomizer; 12 | import com.weutil.common.entity.BaseEntity; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.slf4j.MDC; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | 18 | /** 19 | * MyBatis-Flex 配置 20 | * 21 | * @author inlym 22 | * @date 2024/7/14 23 | * @see MyBatisFlexCustomizer 24 | * @since 3.0.0 25 | **/ 26 | @Configuration 27 | public class MyBatisFlexConfig implements MyBatisFlexCustomizer { 28 | /** 29 | * 配置逻辑删除处理器 30 | * 31 | * @date 2023/10/9 32 | * @see 逻辑删除 33 | * @since 2.0.3 34 | */ 35 | @Bean 36 | public LogicDeleteProcessor logicDeleteProcessor() { 37 | return new DateTimeLogicDeleteProcessor(); 38 | } 39 | 40 | /** 41 | * MyBatis-Flex 初始化配置 42 | * 43 | * @param config 公共配置 44 | * 45 | * @date 2023/10/10 46 | * @see MyBatisFlexCustomizer 47 | * @since 2.0.3 48 | */ 49 | @Override 50 | public void customize(FlexGlobalConfig config) { 51 | // 设置自定义的 SQL 审计日志收集器 52 | AuditManager.setMessageCollector(new LogMessageCollector()); 53 | 54 | // 开启审计功能 55 | AuditManager.setAuditEnable(true); 56 | 57 | // 关闭控制台打印标志 58 | config.setPrintBanner(false); 59 | 60 | ClientIpListener clientIpListener = new ClientIpListener(); 61 | config.registerInsertListener(clientIpListener, BaseEntity.class); 62 | config.registerUpdateListener(clientIpListener, BaseEntity.class); 63 | } 64 | 65 | @Slf4j 66 | static class LogMessageCollector implements MessageCollector { 67 | @Override 68 | public void collect(AuditMessage message) { 69 | log.info("{} <<< {}ms", message.getFullSql(), message.getElapsedTime()); 70 | } 71 | } 72 | 73 | static class ClientIpListener implements InsertListener, UpdateListener { 74 | @Override 75 | public void onInsert(Object o) { 76 | String clientIp = MDC.get("CLIENT_IP"); 77 | if (clientIp != null && o instanceof BaseEntity) { 78 | ((BaseEntity) o).setCreateClientIp(clientIp); 79 | } 80 | } 81 | 82 | @Override 83 | public void onUpdate(Object o) { 84 | String clientIp = MDC.get("CLIENT_IP"); 85 | if (clientIp != null && o instanceof BaseEntity) { 86 | ((BaseEntity) o).setUpdateClientIp(clientIp); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /DevOps/mysql/schema/life-helper-todolist.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------------------------------------------------------- 2 | -- 待办项目表 3 | -- 对应实体: [com.weutil.todolist.entity.TodolistProject] 4 | -- 创建时间: 2024/12/12 5 | -- ---------------------------------------------------------------------------- 6 | 7 | create table `todolist_project` 8 | ( 9 | /* 下方是通用字段 */ 10 | `id` bigint unsigned not null auto_increment comment '主键 ID', 11 | `create_time` datetime not null default current_timestamp comment '创建时间', 12 | `update_time` datetime not null default current_timestamp on update current_timestamp comment '更新时间', 13 | `delete_time` datetime default null comment '删除时间(逻辑删除标志)', 14 | `create_client_ip` varchar(15) not null default '' comment '创建时的客户端 IP 地址', 15 | `update_client_ip` varchar(15) not null default '' comment '最后一次更新时的客户端 IP 地址', 16 | `update_count` bigint unsigned not null default 0 comment '更新次数', 17 | 18 | /* 下方为用户相关类数据表通用字段 */ 19 | `user_id` bigint unsigned not null comment '所属的用户 ID', 20 | 21 | /* 下方为业务字段 */ 22 | `name` varchar(30) not null default '' comment '项目名称', 23 | `emoji` char(1) not null default '' comment 'emoji 图标', 24 | `color` varchar(20) not null default '' comment '颜色名称', 25 | `favorite_time` datetime default null comment '收藏时间', 26 | 27 | primary key (`id`) 28 | ) engine = InnoDB 29 | default character set = `utf8mb4` comment ='待办项目表'; 30 | 31 | -- ---------------------------------------------------------------------------- 32 | -- 待办项目表 33 | -- 对应实体: [com.weutil.todolist.entity.TodolistTask] 34 | -- 创建时间: 2024/12/12 35 | -- ---------------------------------------------------------------------------- 36 | 37 | create table `todolist_task` 38 | ( 39 | /* 下方是通用字段 */ 40 | `id` bigint unsigned not null auto_increment comment '主键 ID', 41 | `create_time` datetime not null default current_timestamp comment '创建时间', 42 | `update_time` datetime not null default current_timestamp on update current_timestamp comment '更新时间', 43 | `delete_time` datetime default null comment '删除时间(逻辑删除标志)', 44 | `create_client_ip` varchar(15) not null default '' comment '创建时的客户端 IP 地址', 45 | `update_client_ip` varchar(15) not null default '' comment '最后一次更新时的客户端 IP 地址', 46 | `update_count` bigint unsigned not null default 0 comment '更新次数', 47 | 48 | /* 下方为用户相关类数据表通用字段 */ 49 | `user_id` bigint unsigned not null comment '所属的用户 ID', 50 | 51 | /* 下方为业务字段 */ 52 | `project_id` bigint unsigned not null default 0 comment '所属项目 ID', 53 | `name` varchar(30) not null default '' comment '任务名称', 54 | `content` varchar(1000) not null default '' comment '任务描述内容文本', 55 | `complete_time` datetime default null comment '任务完成时间', 56 | `due_date_time` datetime default null comment '截止期限(日期+时间)', 57 | `due_date` date default null comment '截止期限的日期部分(年月日)', 58 | `due_time` time default null comment '截止期限的时间部分(时分秒)', 59 | `priority` tinyint not null default 0 comment '任务优先级(枚举值)', 60 | 61 | primary key (`id`) 62 | ) engine = InnoDB 63 | default character set = `utf8mb4` comment ='待办任务表'; 64 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 80 | --------------------------------------------------------------------------------