├── 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 |
主要用途是防“中间人”攻击。 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 | *
请求数据实体({@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 | *
请求数据实体({@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 | *
当经过层层判断和处理后,假定某处查询的用户必定存在,而实际查询不存在时,抛出该异常。 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 | *
需要提供鉴权信息的 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 | *
检测待发送的手机号不正确,则抛出此异常。 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 | *
创建待办项目时,检测待创建的项目名称已经存在了(非全局,仅针对于同一用户),则抛出此异常。 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 | *
包含以下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 | *
处理特殊操作,包含以下: 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 | *
在一些“偏底层”处理(比如建立连接、使用SDK创建客户端等)时,一般很少出现异常,出现时重试几次往往也能成功,无需客户端做出额外处理。出现此类异常时,则抛出当前类。 8 | * 9 | *
展示服务器给出的“笼统”的提示文案,无需告知真实的错误原因。 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 | *
在控制器方法的参数中注入客户端 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 | *
当响应数据只有一个数据值时,使用当前模型。 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 | *
在回调函数 {@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 | *
用于分支判断中,在认为已包含所有情况处理后,若不匹配已有情况则抛出此异常。 8 | * 9 | *
出现了这个异常表示出现了之前未考虑到的情况,利于在开发阶段提早发现错误。
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 |
情况[1]: 该项目下未完成的任务数不为0 8 | * 9 | *
直接展示对应的错误消息文本 {@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 | *
当响应数据只包含 {@code list} 一个属性时,直接使用当前模型进行封装,避免建立重复数据模型。
12 | *
13 | * @author inlym
14 | * @date 2024/7/14
15 | * @since 3.0.0
16 | **/
17 | @Data
18 | public class SingleListResponse 拦截需要用户登录的控制器方法,未携带有效登录信息则直接报错。
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 | 在控制器方法的参数中注入用户 ID,以便在方法内部快捷获取和使用。
10 | *
11 | * {@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 | * 总体范围: {@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 | * 即下一次可以发送短信距此刻的秒数
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 | * 包含“已完成”和“未完成”等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 管理阿里云短信服务的相关密钥信息。
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 | * 当出现异常情况无法返回正常响应时,返回这个错误响应。
10 | *
11 | * 用于客户端展示使用。
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 | * (1)使用主键 ID 查找资源时,未找到资源(可能是不存在或者已被删除)。
10 | * (2)能够找到对应 ID 的资源,但并不归属于当前用户。
11 | *
12 | * 不要直接使用这个类,而是在各个模块内继承这个类,抛出子类。
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 | * 客户端展示名称为 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 | * 管理阿里云验证码服务的相关密钥信息。
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 | * {@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 | * 在 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 | * 该对象仅用于“新增”和情况。
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 | * 该值为 {@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 | * 即下一次可以发送短信距此刻的秒数
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 | * 在用户鉴权通过后(即完成登录),发放身份证书,用于证明其身份。证书中包含鉴权令牌以及使用方法。
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 | 记录项目的本期启动时间,以便排查项目是否正常启动。
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 | * 通过正常方式登录系统
16 | */
17 | public static final String USER = "ROLE_USER";
18 |
19 | /**
20 | * 开发者
21 | *
22 | * 获取普通用户身份后再二次鉴权
24 | *
25 | * 少量接口可获取系统非公开信息,方便开源学习者进行调试。
27 | */
28 | public static final String DEVELOPER = "ROLE_DEVELOPER";
29 |
30 | /**
31 | * 管理员
32 | *
33 | * 特殊方式或手动生成鉴权信息
35 | *
36 | * 用于调试部分对系统可能有一定破坏性的接口
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 | * 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 | * 用于负载均衡健康检查,只要项目没挂,这个接口就可以正常返回。
23 | *
24 | * 项目内部进行日志记录时,不要记录当前接口,因为负载均衡的健康检查频率为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 | * 通过手机号关联到用户账户实体
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 | 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 | * 该对象同时用于“新增”和“编辑”情况。
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 | * 单字符。
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 | 当前事件是其他登录事件的父类。
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 | * 标记该注解的字段,返回响应时自动将 OSS 中的路径转化为完整可访问的 URL 地址。
16 | *
17 | * 原字段值为 {@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 | 用于客户端将资源直传阿里云的 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 | 说明文本内容
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 | * {@code weutil-central}
26 | */
27 | private String bucketName;
28 |
29 | /**
30 | * 绑定的自定义域名
31 | *
32 | * {@code res.weutil.com}
34 | */
35 | private String customDomain;
36 |
37 | /**
38 | * 访问端口
39 | *
40 | * 外网访问 {@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 | * 放在最前面,用于初始化过滤器链。
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 | * 单字符。
35 | */
36 | private String emoji;
37 |
38 | /**
39 | * 颜色名称
40 | *
41 | * 服务端只存储颜色名称,颜色的呈现均由前端处理。
43 | */
44 | private String color;
45 |
46 | /**
47 | * 收藏时间
48 | *
49 | * 前端只处理“是否收藏”操作。
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 用于客户端展示使用。
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 | * {@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 | 仅处理一些基础资料类,特殊操作放在 {@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 | * 该值为 {@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 原本每种登录方式分别一张表,每张表有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 对访问凭证再做一层封装,用于登录后返回给客户端使用。
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 | * 当前数据表不存储账户关联关系(存于其他“用户账户表”),其他数据表通过关联信息指向对应用户 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 | * (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 | * 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 用于调试慢请求。
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 | * 将所有用到的请求域字段数据存储在当前对象中,以便后续使用。
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 | * 生产环境中会由 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 | * 生产环境中会由 API 网关在请求头中传入,而不是通过连接直接获取。
62 | */
63 | private String clientIp;
64 |
65 | /**
66 | * 用户 ID
67 | *
68 | * 务必在鉴权通过后再存入。
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 | * 用语处理和计算启动时间相关事项
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 | * 说明文本内容
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 计算各个中间件的延迟时间。
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 | * 说明文本内容
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 线上生产环境使用 阿里云 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 | * 该字段为阿里云 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 | * 所有的实体都需要继承当前实体。
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 | * 该字段由 MySQL 的触发器维护。
37 | */
38 | private LocalDateTime createTime;
39 |
40 | /**
41 | * 更新时间
42 | *
43 | * 该字段由 MySQL 的触发器维护。
45 | */
46 | private LocalDateTime updateTime;
47 |
48 | /**
49 | * 删除时间
50 | *
51 | * 该字段为逻辑删除标志。
53 | */
54 | @Column(isLogicDelete = true)
55 | private LocalDateTime deleteTime;
56 |
57 | /**
58 | * 创建时的客户端 IP 地址
59 | *
60 | * 该字段由 MyBatis-Flex 框架监听器维护。
62 | */
63 | private String createClientIp;
64 |
65 | /**
66 | * 最后一次更新时的客户端 IP 地址
67 | *
68 | * 该字段由 MyBatis-Flex 框架监听器维护。
70 | */
71 | private String updateClientIp;
72 |
73 | /**
74 | * 更新次数
75 | *
76 | * (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 | 仅用于封装 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 | * 生产环境中会由 API 网关在请求头中传入,在开发环境需模拟生成该值。
61 | */
62 | private String traceId;
63 |
64 | /**
65 | * 客户端 IP 地址
66 | *
67 | * 生产环境中会由 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 | * 为方便内部计算处理,增加当前冗余字段,处理策略如下:
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 | * 通过鉴权后,将相关信息带入生成这个鉴权凭证,用于在 Spring Security 中使用。
16 | *
17 | * 当前只用到用户 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 对 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 | * 处理成抛出错误而不是直接返回布尔值的原因是:能够中断流程,否则一系列判断很麻烦。
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 | * 当前类如果 {@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 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 为什么不使用 {@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 | * [实际原因] 验证码输入错误
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 | * [实际原因] 验证码输入错误的次数超过了限制(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 | * [实际原因] 获取验证码操作的客户端与“验证”操作的客户端对应的 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 | 封装阿里云短信相关的底层方法。
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 | * 用于获取客户端的 IP 地址。
19 | *
20 | * 由于服务器架构问题(客户端 -> API 网关 -> 负载均衡 -> 应用服务器),服务器无法直接获取客户端的 IP 地址,只能依赖从网关处获取客户端 IP 地址并将其传递给应用服务器。
22 | * 请求传递链中的 API 网关和负载均衡直接使用了阿里云的服务,提供了2种通过请求头传递客户端 IP 地址的方式:
23 | *
24 | * 综合考虑以上优缺点,决定使用从负载均衡传递的 `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 | * 该字段为阿里云负载均衡传入。
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>, {@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 | * [实际原因] 用户输入的手机号格式未通过校验(即不是一个正常的手机号)
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 | * [实际原因] 短信发送超过了频次限制(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 | * [实际原因] 短信验证码发送,由 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 | * 处理使用“短信验证码”进行登录的各个环节。
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 | * 记录请求信息。
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 | * 查看系统的运行状态。
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主要用途
12 | *
主要用途
9 | *
注意事项
12 | *
示例值
20 | *
错误码范围
12 | *
说明
18 | *
说明
14 | *
主要用途
11 | *
主要用途
9 | *
错误码说明
12 | *
说明
12 | *
触发条件
9 | *
使用说明
13 | *
字段说明
32 | *
主要用途
11 | *
示例值
26 | *
说明
13 | *
说明
12 | *
说明
28 | *
说明
19 | *
主要用途
14 | *
说明
13 | *
获取方式
15 | *
获取方式
23 | *
主要用途
26 | *
获取方式
34 | *
主要用途
37 | *
说明
11 | *
主要用途
22 | *
注意事项
25 | *
说明
15 | *
来源
7 | *
命名规范
11 | *
说明
31 | *
说明
12 | *
说明
29 | *
说明
13 | *
主要用途
15 | *
使用效果示例
18 | *
主要用途
12 | *
说明
11 | *
示例
25 | *
示例
33 | *
示例
41 | *
说明
19 | *
说明
34 | *
说明
42 | *
说明
50 | *
> 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
说明
16 | *
错误码范围
17 | *
说明
17 | *
说明
36 | *
说明
19 | *
主要用途
16 | *
说明
19 | *
字段说明
45 | *
说明
17 | *
说明
18 | *
主要用途
14 | *
说明
32 | *
说明
61 | *
说明
69 | *
说明
15 | *
说明
15 | *
说明
17 | *
说明
17 | *
说明
18 | *
说明
31 | *
说明
17 | *
说明
36 | *
说明
44 | *
说明
52 | *
说明
61 | *
说明
69 | *
说明
77 | *
说明
16 | *
说明
60 | *
说明
68 | *
字段说明
48 | *
主要用途
15 | *
注意事项
18 | *
说明
13 | *
说明
29 | *
说明
19 | *
客户端信息
18 | *
说明
18 | *
异常说明
31 | *
异常说明
44 | *
异常说明
57 | *
说明
18 | *
主要用途
18 | *
背景介绍
21 | *
说明
39 | *
错误码范围
20 | *
异常说明
37 | *
异常说明
50 | *
异常说明
65 | *
主要用途
20 | *
主要用途
20 | *
主要用途
27 | *