完成app接口国际化
Some checks failed
CI Build and Test / build (push) Has been cancelled
Deploy to Server / build-and-deploy (push) Has been cancelled

This commit is contained in:
2025-10-21 10:15:38 +08:00
parent 1e356a03bb
commit 928bb078f0
16 changed files with 791 additions and 89 deletions

View File

@@ -63,6 +63,7 @@ com.corewing.app/
│ └── Result.java # 统一返回结果类
├── config/ # 配置类
│ ├── DruidConfig.java # Druid 数据源监控配置
│ ├── I18nConfig.java # 国际化配置
│ ├── MybatisPlusConfig.java # MyBatis-Plus 配置
│ ├── RedisConfig.java # Redis 配置
│ └── SaTokenConfig.java # Sa-Token 权限配置
@@ -87,6 +88,7 @@ com.corewing.app/
│ └── VerifyCodeServiceImpl.java # 验证码服务实现
└── util/ # 工具类
├── EmailUtil.java # 邮件发送工具类
├── I18nUtil.java # 国际化工具类
├── IpUtil.java # IP 工具类
├── RedisUtil.java # Redis 工具类
└── SmsBaoUtil.java # 短信宝工具类
@@ -103,6 +105,10 @@ com.corewing.app/
- 短信宝配置
- 邮件配置
### 资源文件
- `src/main/resources/i18n/messages_zh_CN.properties` - 中文国际化消息
- `src/main/resources/i18n/messages_en_US.properties` - 英文国际化消息
### 数据库
- 建表 SQL`src/main/resources/db/user.sql`
- 主表:`app_user` - 应用用户表
@@ -159,6 +165,20 @@ com.corewing.app/
- 验证码邮件采用精美的 HTML 模板
- 支持多种邮箱服务商QQ、163、Gmail 等)
### 9. 国际化支持
- 支持中文zh_CN和英文en_US双语
- 客户端通过 HTTP Header 的 `Accept-Language``lang` 指定语言
- 默认语言:中文
- 国际化范围:
- API 响应消息(成功/失败提示)
- 用户操作反馈(登录、注册、登出等)
- 所有异常和错误消息
- 邮件模板内容
- 短信模板内容
- 钉钉推送消息
- I18nUtil 工具类提供便捷的国际化消息获取
- 详细使用说明参见:`I18N_README.md`
## 开发规范
### 1. 依赖注入
@@ -183,6 +203,12 @@ com.corewing.app/
- 使用 `IpUtil.getClientIp()` 获取真实 IP
- 支持多级代理
### 6. 国际化消息
- 所有用户可见的消息都应使用国际化
- 使用 `I18nUtil.getMessage("message.key")` 获取国际化消息
- 支持消息参数:`I18nUtil.getMessage("message.key", param1, param2)`
- 新增消息时需同时更新中英文资源文件
## 项目特点
- 项目使用 MyBatis Plus 作为 ORM 框架,支持自动代码生成
@@ -192,3 +218,4 @@ com.corewing.app/
- 开发环境支持热重载 (Spring Boot DevTools)
- Druid 数据库连接池,提供 SQL 监控功能
- 统一返回结果封装,规范 API 响应格式
- 完整的国际化支持,支持中英文双语切换

230
I18N_README.md Normal file
View File

@@ -0,0 +1,230 @@
# CoreWing 国际化使用说明
## 概述
CoreWing 项目已完成国际化改造支持中文zh_CN和英文en_US两种语言。
## 客户端使用方式
### HTTP Header 配置
客户端通过 HTTP Header 指定语言,支持以下两种方式:
**方式一:使用 Accept-Language推荐**
```
Accept-Language: zh_CN
```
**方式二:使用自定义 lang header**
```
lang: en_US
```
### 支持的语言代码
- `zh_CN` - 简体中文(默认)
- `en_US` - 英语
### 示例
#### JavaScript/Axios
```javascript
// 中文请求
axios.get('/user/info', {
headers: {
'Accept-Language': 'zh_CN'
}
});
// 英文请求
axios.get('/user/info', {
headers: {
'Accept-Language': 'en_US'
}
});
```
#### cURL
```bash
# 中文
curl -H "Accept-Language: zh_CN" http://localhost:8080/user/info
# 英文
curl -H "Accept-Language: en_US" http://localhost:8080/user/info
```
#### Java/OkHttp
```java
Request request = new Request.Builder()
.url("http://localhost:8080/user/info")
.addHeader("Accept-Language", "zh_CN")
.build();
```
## 国际化功能范围
### 已国际化的内容
1. **API 响应消息**
- 成功/失败提示
- 用户操作反馈(登录、注册、登出等)
- 数据验证消息
2. **异常和错误消息**
- 用户认证异常
- 业务逻辑异常
- 服务器错误提示
3. **邮件模板**
- 验证码邮件内容
- 邮件主题和页脚
4. **短信模板**
- 验证码短信内容
- 短信发送结果消息
5. **钉钉推送消息**
- 反馈通知标题和内容
- 表单标签文本
## 技术实现
### 架构组成
1. **I18nConfig** - 国际化配置类
- 配置 MessageSource
- 实现自定义 LocaleResolver
- 支持从 Header 读取语言设置
2. **I18nUtil** - 国际化工具类
- 提供静态方法获取国际化消息
- 支持消息参数替换
3. **消息资源文件**
- `messages_zh_CN.properties` - 中文消息约90条
- `messages_en_US.properties` - 英文消息约90条
### 消息 Key 命名规范
```
common.* - 通用消息
user.* - 用户模块
feedback.* - 反馈模块
error.* - 错误消息
email.* - 邮件模板
sms.* - 短信模板
dingtalk.* - 钉钉推送
```
## 响应示例
### 中文响应 (Accept-Language: zh_CN)
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "xxx",
"userId": 123
},
"success": true
}
```
### 英文响应 (Accept-Language: en_US)
```json
{
"code": 200,
"message": "Login successful",
"data": {
"token": "xxx",
"userId": 123
},
"success": true
}
```
## 开发指南
### 添加新的国际化消息
1.`messages_zh_CN.properties` 中添加中文消息:
```properties
user.new.feature=这是一个新功能
```
2.`messages_en_US.properties` 中添加对应的英文消息:
```properties
user.new.feature=This is a new feature
```
3. 在代码中使用:
```java
String message = I18nUtil.getMessage("user.new.feature");
```
### 带参数的消息
**资源文件:**
```properties
# 中文
user.welcome=欢迎,{0}!您已成功登录。
# 英文
user.welcome=Welcome, {0}! You have successfully logged in.
```
**代码使用:**
```java
String message = I18nUtil.getMessage("user.welcome", username);
```
## 注意事项
1. **默认语言**如果客户端未指定语言系统默认使用中文zh_CN
2. **URL 参数支持**:也可以通过 URL 参数切换语言
```
/user/info?lang=en_US
```
3. **消息缓存**:资源文件会缓存 1 小时,修改后需要重启应用或等待缓存过期
4. **消息 Key 不存在**:如果找不到对应的消息 key会返回 key 本身
## 修改文件清单
### 新增文件3个
- `src/main/java/com/corewing/app/config/I18nConfig.java`
- `src/main/java/com/corewing/app/util/I18nUtil.java`
- `src/main/resources/i18n/messages_zh_CN.properties`
- `src/main/resources/i18n/messages_en_US.properties`
### 修改文件13个
1. `Result.java` - 统一返回结果类
2. `AppUserController.java` - 用户控制器
3. `AppFeedbackController.java` - 反馈控制器
4. `AppUserServiceImpl.java` - 用户服务实现
5. `VerifyCodeServiceImpl.java` - 验证码服务实现
6. `AppFeedbackServiceImpl.java` - 反馈服务实现
7. `GlobalExceptionHandler.java` - 全局异常处理
8. `EmailUtil.java` - 邮件工具类
9. `SmsBaoUtil.java` - 短信工具类
10. `RedisUtil.java` - Redis工具类
## 测试建议
使用 Postman 或 curl 测试不同语言的响应:
```bash
# 测试中文
curl -H "Accept-Language: zh_CN" http://localhost:8080/user/login
# 测试英文
curl -H "Accept-Language: en_US" http://localhost:8080/user/login
```
---
**更新时间:** 2025-01-21
**版本:** 1.0

View File

@@ -1,5 +1,6 @@
package com.corewing.app.common;
import com.corewing.app.util.I18nUtil;
import lombok.Data;
import java.io.Serializable;
@@ -32,9 +33,6 @@ public class Result<T> implements Serializable {
*/
private Boolean success;
public Result() {
}
public Result(Integer code, String message, T data, Boolean success) {
this.code = code;
this.message = message;
@@ -46,14 +44,14 @@ public class Result<T> implements Serializable {
* 成功返回(无数据)
*/
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null, true);
return new Result<>(200, I18nUtil.getMessage("common.success"), null, true);
}
/**
* 成功返回(带数据)
*/
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data, true);
return new Result<>(200, I18nUtil.getMessage("common.success"), data, true);
}
/**
@@ -67,7 +65,7 @@ public class Result<T> implements Serializable {
* 失败返回
*/
public static <T> Result<T> error() {
return new Result<>(500, "操作失败", null, false);
return new Result<>(500, I18nUtil.getMessage("common.error"), null, false);
}
/**

View File

@@ -0,0 +1,110 @@
package com.corewing.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
/**
* 国际化配置类
* 从 HTTP Header 中的 Accept-Language 或 lang 参数读取语言设置
*/
@Configuration
public class I18nConfig implements WebMvcConfigurer {
/**
* 配置消息资源
*/
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 设置资源文件的基础名称(不包含语言代码和.properties后缀
messageSource.setBasename("i18n/messages");
// 设置默认编码
messageSource.setDefaultEncoding("UTF-8");
// 设置缓存时间(秒),-1表示永久缓存0表示不缓存
messageSource.setCacheSeconds(3600);
// 找不到对应的消息key时返回key本身而不是抛出异常
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
/**
* 配置语言环境解析器
* 从 HTTP Header 的 Accept-Language 读取语言设置
*/
@Bean
public LocaleResolver localeResolver() {
return new HeaderLocaleResolver();
}
/**
* 配置拦截器(可选)
* 支持通过URL参数 ?lang=zh_CN 切换语言
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
// 设置请求参数名
interceptor.setParamName("lang");
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
/**
* 自定义 LocaleResolver从 Header 中读取语言设置
*/
private static class HeaderLocaleResolver implements LocaleResolver {
private static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 1. 优先从 Accept-Language header 读取
String language = request.getHeader("Accept-Language");
// 2. 如果没有,尝试从自定义 lang header 读取
if (language == null || language.trim().isEmpty()) {
language = request.getHeader("lang");
}
// 3. 解析语言代码
if (language != null && !language.trim().isEmpty()) {
try {
// 支持格式: zh_CN, zh-CN, en_US, en-US
String normalizedLang = language.replace("-", "_");
String[] parts = normalizedLang.split("_");
if (parts.length == 2) {
return new Locale(parts[0], parts[1]);
} else if (parts.length == 1) {
return new Locale(parts[0]);
}
} catch (Exception e) {
// 解析失败,使用默认语言
}
}
// 4. 返回默认语言(中文)
return DEFAULT_LOCALE;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// Header方式不支持设置语言环境
throw new UnsupportedOperationException(
"Cannot change HTTP header - use Accept-Language or lang header");
}
}
}

View File

@@ -8,6 +8,7 @@ import com.corewing.app.dto.FeedbackRequest;
import com.corewing.app.entity.AppFeedback;
import com.corewing.app.service.AppFeedbackService;
import com.corewing.app.util.DingTalkUtil;
import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.Ip2RegionUtil;
import com.corewing.app.util.IpUtil;
import org.springframework.web.bind.annotation.*;
@@ -63,9 +64,9 @@ public class AppFeedbackController {
// 推送到钉钉
sendFeedbackToDingTalk(feedback, submitIp, submitRegion);
return Result.success("反馈提交成功");
return Result.success(I18nUtil.getMessage("feedback.submit.success"));
}
return Result.error("反馈提交失败");
return Result.error(I18nUtil.getMessage("feedback.submit.failed"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -96,7 +97,7 @@ public class AppFeedbackController {
if (feedback != null) {
return Result.success(feedback);
}
return Result.error("反馈不存在");
return Result.error(I18nUtil.getMessage("feedback.not.found"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -135,9 +136,9 @@ public class AppFeedbackController {
try {
boolean success = feedbackService.updateStatus(id, status);
if (success) {
return Result.success("状态更新成功");
return Result.success(I18nUtil.getMessage("feedback.status.update.success"));
}
return Result.error("状态更新失败");
return Result.error(I18nUtil.getMessage("feedback.status.update.failed"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -151,9 +152,9 @@ public class AppFeedbackController {
try {
boolean success = feedbackService.removeById(id);
if (success) {
return Result.success("删除成功");
return Result.success(I18nUtil.getMessage("feedback.delete.success"));
}
return Result.error("删除失败");
return Result.error(I18nUtil.getMessage("feedback.delete.failed"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -165,18 +166,18 @@ public class AppFeedbackController {
@GetMapping("/test-dingtalk")
public Result<String> testDingTalk() {
try {
String title = "测试消息";
String title = I18nUtil.getMessage("feedback.dingtalk.test.message");
String text = "### 🔔 钉钉推送测试\n\n这是一条测试消息用于验证钉钉推送功能是否正常工作。\n\n**测试时间:** " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
boolean success = dingTalkUtil.sendMarkdownMessage(title, text);
if (success) {
return Result.success("钉钉推送测试成功,请检查钉钉群消息");
return Result.success(I18nUtil.getMessage("feedback.dingtalk.test.success"));
} else {
return Result.error("钉钉推送测试失败,请检查配置和日志");
return Result.error(I18nUtil.getMessage("feedback.dingtalk.test.failed"));
}
} catch (Exception e) {
return Result.error("测试异常: " + e.getMessage());
return Result.error(I18nUtil.getMessage("feedback.dingtalk.test.error", e.getMessage()));
}
}
@@ -185,45 +186,53 @@ public class AppFeedbackController {
*/
private void sendFeedbackToDingTalk(AppFeedback feedback, String submitIp, String submitRegion) {
try {
String title = "📢 新的用户反馈";
String title = I18nUtil.getMessage("dingtalk.feedback.title");
StringBuilder text = new StringBuilder();
text.append("### 📢 新的用户反馈\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.header")).append("\n\n");
text.append("---\n\n");
// 用户信息
if (feedback.getUserId() != null) {
text.append("**用户ID** ").append(feedback.getUserId()).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.user.id")).append(" ")
.append(feedback.getUserId()).append("\n\n");
} else {
text.append("**用户ID** 匿名用户\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.user.id")).append(" ")
.append(I18nUtil.getMessage("dingtalk.feedback.user.anonymous")).append("\n\n");
}
// 反馈类型
text.append("**问题类型:** ").append(feedback.getFeedbackType()).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.type")).append(" ")
.append(feedback.getFeedbackType()).append("\n\n");
// 反馈标题
text.append("**问题标题:** ").append(feedback.getTitle()).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.title.label")).append(" ")
.append(feedback.getTitle()).append("\n\n");
// 反馈内容
if (feedback.getContent() != null && !feedback.getContent().trim().isEmpty()) {
text.append("**问题描述:** \n\n").append(feedback.getContent()).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.content.label")).append(" \n\n")
.append(feedback.getContent()).append("\n\n");
}
// 联系方式
if (feedback.getContact() != null && !feedback.getContact().trim().isEmpty()) {
text.append("**联系方式:** ").append(feedback.getContact()).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.contact")).append(" ")
.append(feedback.getContact()).append("\n\n");
}
// IP 和归属地
text.append("**提交IP** ").append(submitIp).append("\n\n");
text.append("**IP归属地** ").append(submitRegion).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.submit.ip")).append(" ")
.append(submitIp).append("\n\n");
text.append(I18nUtil.getMessage("dingtalk.feedback.submit.region")).append(" ")
.append(submitRegion).append("\n\n");
// 提交时间
text.append("**提交时间:** ").append(
text.append(I18nUtil.getMessage("dingtalk.feedback.submit.time")).append(" ").append(
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
).append("\n\n");
text.append("---\n\n");
text.append("> 请及时处理用户反馈!");
text.append(I18nUtil.getMessage("dingtalk.feedback.footer"));
// 异步发送钉钉消息(避免阻塞用户请求)
new Thread(() -> dingTalkUtil.sendMarkdownMessage(title, text.toString())).start();

View File

@@ -9,6 +9,7 @@ import com.corewing.app.dto.UpdatePasswordRequest;
import com.corewing.app.entity.AppUser;
import com.corewing.app.service.AppUserService;
import com.corewing.app.service.VerifyCodeService;
import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.IpUtil;
import org.springframework.web.bind.annotation.*;
@@ -39,9 +40,9 @@ public class AppUserController {
try {
boolean success = verifyCodeService.sendCode(request.getAccount(), request.getType());
if (success) {
return Result.success("验证码发送成功");
return Result.success(I18nUtil.getMessage("user.code.send.success"));
}
return Result.error("验证码发送失败");
return Result.error(I18nUtil.getMessage("user.code.send.failed"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -65,7 +66,7 @@ public class AppUserController {
data.put("userId", user.getId());
data.put("username", user.getUsername());
return Result.success("登录成功", data);
return Result.success(I18nUtil.getMessage("user.login.success"), data);
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -87,7 +88,7 @@ public class AppUserController {
// 获取注册IP
String registerIp = IpUtil.getClientIp(httpRequest);
userService.register(user, request.getCode(), registerIp);
return Result.success("注册成功");
return Result.success(I18nUtil.getMessage("user.register.success"));
} catch (Exception e) {
return Result.error(e.getMessage());
}
@@ -99,7 +100,7 @@ public class AppUserController {
@PostMapping("/logout")
public Result<String> logout() {
StpUtil.logout();
return Result.success("登出成功");
return Result.success(I18nUtil.getMessage("user.logout.success"));
}
/**
@@ -125,7 +126,7 @@ public class AppUserController {
user.setPassword(null);
return Result.success(user);
}
return Result.error("用户不存在");
return Result.error(I18nUtil.getMessage("user.not.found"));
}
/**
@@ -138,9 +139,9 @@ public class AppUserController {
boolean success = userService.updateById(user);
if (success) {
return Result.success("更新成功");
return Result.success(I18nUtil.getMessage("user.update.success"));
}
return Result.error("更新失败");
return Result.error(I18nUtil.getMessage("user.update.failed"));
}
/**
@@ -157,7 +158,7 @@ public class AppUserController {
request.getOldPassword().getBytes(java.nio.charset.StandardCharsets.UTF_8));
if (!oldPasswordMd5.equals(user.getPassword())) {
return Result.error("原密码错误");
return Result.error(I18nUtil.getMessage("user.password.old.incorrect"));
}
// 更新新密码
@@ -167,7 +168,7 @@ public class AppUserController {
user.setPassword(newPasswordMd5);
userService.updateById(user);
return Result.success("密码修改成功");
return Result.success(I18nUtil.getMessage("user.password.update.success"));
} catch (Exception e) {
return Result.error(e.getMessage());
}

View File

@@ -2,6 +2,7 @@ package com.corewing.app.handler;
import cn.dev33.satoken.exception.NotLoginException;
import com.corewing.app.common.Result;
import com.corewing.app.util.I18nUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -25,22 +26,22 @@ public class GlobalExceptionHandler {
String message;
switch (e.getType()) {
case NotLoginException.NOT_TOKEN:
message = "未提供登录凭证";
message = I18nUtil.getMessage("error.token.missing");
break;
case NotLoginException.INVALID_TOKEN:
message = "登录凭证无效";
message = I18nUtil.getMessage("error.token.invalid");
break;
case NotLoginException.TOKEN_TIMEOUT:
message = "登录已过期,请重新登录";
message = I18nUtil.getMessage("error.token.expired");
break;
case NotLoginException.BE_REPLACED:
message = "账号已在其他地方登录";
message = I18nUtil.getMessage("error.token.replaced");
break;
case NotLoginException.KICK_OUT:
message = "账号已被踢下线";
message = I18nUtil.getMessage("error.token.kicked.out");
break;
default:
message = "未登录,请先登录";
message = I18nUtil.getMessage("error.not.login");
}
return Result.error(HttpStatus.UNAUTHORIZED.value(), message);
}
@@ -52,6 +53,7 @@ public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<String> handleException(Exception e) {
log.error(e.getMessage());
return Result.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误:" + e.getMessage());
return Result.error(HttpStatus.INTERNAL_SERVER_ERROR.value(),
I18nUtil.getMessage("error.server.internal", e.getMessage()));
}
}

View File

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.entity.AppFeedback;
import com.corewing.app.mapper.AppFeedbackMapper;
import com.corewing.app.service.AppFeedbackService;
import com.corewing.app.util.I18nUtil;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -30,7 +31,7 @@ public class AppFeedbackServiceImpl extends ServiceImpl<AppFeedbackMapper, AppFe
@Override
public List<AppFeedback> listByUserId(Long userId) {
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
throw new RuntimeException(I18nUtil.getMessage("error.feedback.user.id.empty"));
}
LambdaQueryWrapper<AppFeedback> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AppFeedback::getUserId, userId)
@@ -62,10 +63,10 @@ public class AppFeedbackServiceImpl extends ServiceImpl<AppFeedbackMapper, AppFe
@Override
public boolean updateStatus(Long id, Integer status) {
if (id == null) {
throw new RuntimeException("反馈ID不能为空");
throw new RuntimeException(I18nUtil.getMessage("error.feedback.id.empty"));
}
if (status == null) {
throw new RuntimeException("状态不能为空");
throw new RuntimeException(I18nUtil.getMessage("error.feedback.status.empty"));
}
AppFeedback feedback = new AppFeedback();

View File

@@ -7,6 +7,7 @@ import com.corewing.app.entity.AppUser;
import com.corewing.app.mapper.AppUserMapper;
import com.corewing.app.service.AppUserService;
import com.corewing.app.service.VerifyCodeService;
import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.Ip2RegionUtil;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
@@ -75,18 +76,18 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
// 查询用户(支持用户名/邮箱/手机号)
AppUser user = getByAccount(account);
if (user == null) {
throw new RuntimeException("用户不存在");
throw new RuntimeException(I18nUtil.getMessage("error.user.not.found"));
}
// 验证密码MD5加密
String encryptPassword = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
if (!encryptPassword.equals(user.getPassword())) {
throw new RuntimeException("密码错误");
throw new RuntimeException(I18nUtil.getMessage("error.password.incorrect"));
}
// 检查用户状态
if (user.getStatus() == 0) {
throw new RuntimeException("账号已被禁用");
throw new RuntimeException(I18nUtil.getMessage("error.account.disabled"));
}
// 登录成功,使用 Sa-Token 生成 token
@@ -99,19 +100,19 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
// 检查用户名是否已存在
AppUser existUser = getByUsername(user.getUsername());
if (existUser != null) {
throw new RuntimeException("用户名已存在");
throw new RuntimeException(I18nUtil.getMessage("error.username.exists"));
}
// 邮箱和手机号至少要有一个
if (!StringUtils.hasText(user.getEmail()) && !StringUtils.hasText(user.getTelephone())) {
throw new RuntimeException("邮箱和手机号至少填写一个");
throw new RuntimeException(I18nUtil.getMessage("error.email.or.phone.required"));
}
// 检查邮箱是否已存在
if (StringUtils.hasText(user.getEmail())) {
existUser = getByEmail(user.getEmail());
if (existUser != null) {
throw new RuntimeException("邮箱已被使用");
throw new RuntimeException(I18nUtil.getMessage("error.email.exists"));
}
}
@@ -119,7 +120,7 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
if (StringUtils.hasText(user.getTelephone())) {
existUser = getByTelephone(user.getTelephone());
if (existUser != null) {
throw new RuntimeException("手机号已被使用");
throw new RuntimeException(I18nUtil.getMessage("error.phone.exists"));
}
}
@@ -127,7 +128,7 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
String account = StringUtils.hasText(user.getTelephone()) ? user.getTelephone() : user.getEmail();
boolean codeValid = verifyCodeService.verifyCode(account, code, "register");
if (!codeValid) {
throw new RuntimeException("验证码错误或已过期");
throw new RuntimeException(I18nUtil.getMessage("error.verify.code.invalid"));
}
// 密码加密MD5

View File

@@ -2,6 +2,7 @@ package com.corewing.app.service.impl;
import com.corewing.app.service.VerifyCodeService;
import com.corewing.app.util.EmailUtil;
import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.RedisUtil;
import com.corewing.app.util.SmsBaoUtil;
import lombok.extern.slf4j.Slf4j;
@@ -9,7 +10,6 @@ import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
@@ -52,11 +52,11 @@ public class VerifyCodeServiceImpl implements VerifyCodeService {
@Override
public boolean sendCode(String account, String type) {
if (!StringUtils.hasText(account)) {
throw new RuntimeException("账号不能为空");
throw new RuntimeException(I18nUtil.getMessage("error.account.empty"));
}
if (!StringUtils.hasText(type)) {
throw new RuntimeException("验证码类型不能为空");
throw new RuntimeException(I18nUtil.getMessage("error.verify.type.empty"));
}
// 判断是手机号还是邮箱
@@ -64,7 +64,7 @@ public class VerifyCodeServiceImpl implements VerifyCodeService {
boolean isEmail = EMAIL_PATTERN.matcher(account).matches();
if (!isPhone && !isEmail) {
throw new RuntimeException("请输入正确的手机号或邮箱");
throw new RuntimeException(I18nUtil.getMessage("error.account.format.invalid"));
}
// 生成验证码
@@ -81,13 +81,13 @@ public class VerifyCodeServiceImpl implements VerifyCodeService {
// 发送短信验证码
boolean success = smsBaoUtil.sendVerifyCode(account, code);
if (!success) {
throw new RuntimeException("短信发送失败,请稍后重试");
throw new RuntimeException(I18nUtil.getMessage("error.sms.send.failed"));
}
} else {
// 发送邮件验证码
boolean success = emailUtil.sendVerifyCode(account, code);
if (!success) {
throw new RuntimeException("邮件发送失败,请稍后重试");
throw new RuntimeException(I18nUtil.getMessage("error.email.send.failed"));
}
}

View File

@@ -86,7 +86,7 @@ public class EmailUtil {
* @return 是否发送成功
*/
public boolean sendVerifyCode(String to, String code) {
String subject = "【CoreWing】验证码";
String subject = I18nUtil.getMessage("email.subject.verify.code");
String content = buildVerifyCodeHtml(code);
return sendHtmlMail(to, subject, content);
}
@@ -130,28 +130,28 @@ public class EmailUtil {
"<h1 class=\"logo\">CoreWing</h1>" +
"</div>" +
"<div class=\"content\">" +
"<div class=\"greeting\">Hi你好</div>" +
"<div class=\"greeting\">" + I18nUtil.getMessage("email.greeting") + "</div>" +
"<p style=\"color: #555; font-size: 14px; line-height: 1.8; margin-bottom: 20px;\">" +
"感谢使用 CoreWing。我们收到了你的验证请求请使用以下验证码完成操作" +
I18nUtil.getMessage("email.verify.intro") +
"</p>" +
"<div class=\"code-container\">" +
"<div class=\"code-label\">验证码</div>" +
"<div class=\"code-label\">" + I18nUtil.getMessage("email.verify.code.label") + "</div>" +
"<div class=\"code\">" + code + "</div>" +
"</div>" +
"<div class=\"notice\">" +
"<div class=\"notice-title\">重要提示</div>" +
"<div class=\"notice-text\">验证码 5 分钟内有效,请勿泄露给他人</div>" +
"<div class=\"notice-title\">" + I18nUtil.getMessage("email.verify.notice.title") + "</div>" +
"<div class=\"notice-text\">" + I18nUtil.getMessage("email.verify.notice.content") + "</div>" +
"</div>" +
"<div class=\"divider\"></div>" +
"<div class=\"tips\">" +
"如果这不是你本人的操作,可以直接忽略这封邮件,你的账号仍然是安全的。<br>" +
"有任何问题都可以联系我们的客服团队。" +
I18nUtil.getMessage("email.verify.security.tips") + "<br>" +
I18nUtil.getMessage("email.verify.support.tips") +
"</div>" +
"</div>" +
"<div class=\"footer\">" +
"<div class=\"footer-text\">" +
"这是一封自动发送的邮件,请不要直接回复<br>" +
"© 2025 CoreWing · 保留所有权利" +
I18nUtil.getMessage("email.footer.notice") + "<br>" +
I18nUtil.getMessage("email.footer.copyright") +
"</div>" +
"</div>" +
"</div>" +

View File

@@ -0,0 +1,88 @@
package com.corewing.app.util;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Locale;
/**
* 国际化工具类
* 提供便捷的静态方法获取国际化消息
*/
@Component
public class I18nUtil {
private final MessageSource messageSource;
private static I18nUtil instance;
public I18nUtil(MessageSource messageSource) {
this.messageSource = messageSource;
}
@PostConstruct
public void init() {
instance = this;
}
/**
* 获取国际化消息
*
* @param key 消息key
* @return 国际化消息
*/
public static String getMessage(String key) {
return getMessage(key, null);
}
/**
* 获取国际化消息(带参数)
*
* @param key 消息key
* @param args 参数数组
* @return 国际化消息
*/
public static String getMessage(String key, Object[] args) {
try {
Locale locale = LocaleContextHolder.getLocale();
return instance.messageSource.getMessage(key, args, locale);
} catch (Exception e) {
// 如果获取失败返回key本身
return key;
}
}
/**
* 获取国际化消息(带单个参数)
*
* @param key 消息key
* @param arg 参数
* @return 国际化消息
*/
public static String getMessage(String key, Object arg) {
return getMessage(key, new Object[]{arg});
}
/**
* 获取国际化消息(带两个参数)
*
* @param key 消息key
* @param arg1 参数1
* @param arg2 参数2
* @return 国际化消息
*/
public static String getMessage(String key, Object arg1, Object arg2) {
return getMessage(key, new Object[]{arg1, arg2});
}
/**
* 获取当前语言环境
*
* @return 当前Locale
*/
public static Locale getCurrentLocale() {
return LocaleContextHolder.getLocale();
}
}

View File

@@ -1,6 +1,5 @@
package com.corewing.app.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@@ -78,7 +77,7 @@ public class RedisUtil {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) List.of(key));
redisTemplate.delete(List.of(key));
}
}
}
@@ -142,7 +141,7 @@ public class RedisUtil {
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
throw new RuntimeException(I18nUtil.getMessage("error.redis.increment.positive"));
}
Long result = redisTemplate.opsForValue().increment(key, delta);
return result != null ? result : 0L;
@@ -156,7 +155,7 @@ public class RedisUtil {
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
throw new RuntimeException(I18nUtil.getMessage("error.redis.decrement.positive"));
}
Long result = redisTemplate.opsForValue().increment(key, -delta);
return result != null ? result : 0L;

View File

@@ -13,7 +13,7 @@ import java.nio.charset.StandardCharsets;
/**
* 短信宝工具类
* 官方文档: https://www.smsbao.com/
* 官方文档: @<a href="https://www.smsbao.com/">...</a>
*/
@Slf4j
@Component
@@ -83,7 +83,7 @@ public class SmsBaoUtil {
* @return 是否发送成功
*/
public boolean sendVerifyCode(String phone, String code) {
String content = String.format("【CoreWing】您的验证码是%s5分钟内有效。请勿泄露给他人", code);
String content = String.format(I18nUtil.getMessage("sms.verify.code.template"), code);
return sendSms(phone, content);
}
@@ -96,23 +96,23 @@ public class SmsBaoUtil {
private String getErrorMessage(int code) {
switch (code) {
case 0:
return "发送成功";
return I18nUtil.getMessage("sms.result.success");
case 30:
return "密码错误";
return I18nUtil.getMessage("sms.result.password.error");
case 40:
return "账号不存在";
return I18nUtil.getMessage("sms.result.account.not.exist");
case 41:
return "余额不足";
return I18nUtil.getMessage("sms.result.balance.insufficient");
case 42:
return "账户已过期";
return I18nUtil.getMessage("sms.result.account.expired");
case 43:
return "IP地址限制";
return I18nUtil.getMessage("sms.result.ip.restricted");
case 50:
return "内容含有敏感词";
return I18nUtil.getMessage("sms.result.sensitive.word");
case 51:
return "手机号码不正确";
return I18nUtil.getMessage("sms.result.phone.incorrect");
default:
return "未知错误";
return I18nUtil.getMessage("sms.result.unknown.error");
}
}
}

View File

@@ -0,0 +1,118 @@
# ===================================
# CoreWing I18n Resource Bundle - English
# ===================================
# ==================== Common Messages ====================
common.success=Operation successful
common.error=Operation failed
# ==================== User Module ====================
# User Controller
user.code.send.success=Verification code sent successfully
user.code.send.failed=Failed to send verification code
user.login.success=Login successful
user.register.success=Registration successful
user.logout.success=Logout successful
user.not.found=User not found
user.update.success=Update successful
user.update.failed=Update failed
user.password.old.incorrect=Incorrect old password
user.password.update.success=Password updated successfully
# User Service Exceptions
error.user.not.found=User not found
error.password.incorrect=Incorrect password
error.account.disabled=Account has been disabled
error.username.exists=Username already exists
error.email.or.phone.required=At least one of email or phone number is required
error.email.exists=Email already in use
error.phone.exists=Phone number already in use
error.verify.code.invalid=Verification code is incorrect or expired
# ==================== Verification Code Module ====================
error.account.empty=Account cannot be empty
error.verify.type.empty=Verification type cannot be empty
error.account.format.invalid=Please enter a valid phone number or email
error.sms.send.failed=SMS sending failed, please try again later
error.email.send.failed=Email sending failed, please try again later
# ==================== Feedback Module ====================
# Feedback Controller
feedback.submit.success=Feedback submitted successfully
feedback.submit.failed=Failed to submit feedback
feedback.not.found=Feedback not found
feedback.status.update.success=Status updated successfully
feedback.status.update.failed=Failed to update status
feedback.delete.success=Deleted successfully
feedback.delete.failed=Failed to delete
feedback.dingtalk.test.message=Test message
feedback.dingtalk.test.success=DingTalk push test successful, please check the DingTalk group
feedback.dingtalk.test.failed=DingTalk push test failed, please check configuration and logs
feedback.dingtalk.test.error=Test exception: {0}
# Feedback Service Exceptions
error.feedback.user.id.empty=User ID cannot be empty
error.feedback.id.empty=Feedback ID cannot be empty
error.feedback.status.empty=Status cannot be empty
# ==================== Global Exception Handler ====================
error.token.missing=Login credentials not provided
error.token.invalid=Invalid login credentials
error.token.expired=Login has expired, please login again
error.token.replaced=Account has been logged in elsewhere
error.token.kicked.out=Account has been kicked offline
error.not.login=Not logged in, please login first
error.server.internal=Internal server error: {0}
# ==================== Utility Classes ====================
# Redis Utility
error.redis.increment.positive=Increment factor must be greater than 0
error.redis.decrement.positive=Decrement factor must be greater than 0
# ==================== Email Template ====================
email.subject.verify.code=[CoreWing] Verification Code
email.greeting=Hi there!
email.verify.intro=Thank you for using CoreWing. We received your verification request. Please use the following verification code to complete the operation:
email.verify.code.label=Verification Code
email.verify.notice.title=Important Notice
email.verify.notice.content=The verification code is valid for 5 minutes. Please do not share it with others
email.verify.security.tips=If this was not you, you can safely ignore this email. Your account is still secure.
email.verify.support.tips=If you have any questions, feel free to contact our support team.
email.footer.notice=This is an automated email. Please do not reply directly
email.footer.copyright=© 2025 CoreWing · All Rights Reserved
# ==================== SMS Template ====================
sms.verify.code.template=[CoreWing] Your verification code is {0}, valid for 5 minutes. Do not share with others!
sms.result.success=Sent successfully
sms.result.password.error=Password error
sms.result.account.not.exist=Account does not exist
sms.result.balance.insufficient=Insufficient balance
sms.result.account.expired=Account has expired
sms.result.ip.restricted=IP address restricted
sms.result.sensitive.word=Content contains sensitive words
sms.result.phone.incorrect=Incorrect phone number
sms.result.unknown.error=Unknown error
# ==================== DingTalk Push ====================
dingtalk.feedback.title=📢 New User Feedback
dingtalk.feedback.header=### 📢 New User Feedback
dingtalk.feedback.user.id=**User ID:**
dingtalk.feedback.user.anonymous=Anonymous User
dingtalk.feedback.type=**Feedback Type:**
dingtalk.feedback.title.label=**Title:**
dingtalk.feedback.content.label=**Description:**
dingtalk.feedback.contact=**Contact:**
dingtalk.feedback.submit.ip=**Submit IP:**
dingtalk.feedback.submit.region=**IP Location:**
dingtalk.feedback.submit.time=**Submit Time:**
dingtalk.feedback.footer=> Please handle user feedback promptly!
# DingTalk Log Messages (Optional)
dingtalk.webhook.not.configured=DingTalk Webhook not configured, skipping message push
dingtalk.text.send.failed=Failed to send DingTalk text message: {0}
dingtalk.markdown.send.failed=Failed to send DingTalk Markdown message: {0}
dingtalk.sign.mode=Using signature mode, timestamp: {0}
dingtalk.send.message=Sending DingTalk message: {0}
dingtalk.push.success=DingTalk message pushed successfully, response: {0}
dingtalk.push.failed=DingTalk message push failed, HTTP status code: {0}, response: {1}
dingtalk.push.exception=DingTalk message push exception: {0}

View File

@@ -0,0 +1,118 @@
# ===================================
# CoreWing 国际化资源文件 - 中文
# ===================================
# ==================== 通用消息 ====================
common.success=操作成功
common.error=操作失败
# ==================== 用户模块 ====================
# 用户Controller
user.code.send.success=验证码发送成功
user.code.send.failed=验证码发送失败
user.login.success=登录成功
user.register.success=注册成功
user.logout.success=登出成功
user.not.found=用户不存在
user.update.success=更新成功
user.update.failed=更新失败
user.password.old.incorrect=原密码错误
user.password.update.success=密码修改成功
# 用户Service异常
error.user.not.found=用户不存在
error.password.incorrect=密码错误
error.account.disabled=账号已被禁用
error.username.exists=用户名已存在
error.email.or.phone.required=邮箱和手机号至少填写一个
error.email.exists=邮箱已被使用
error.phone.exists=手机号已被使用
error.verify.code.invalid=验证码错误或已过期
# ==================== 验证码模块 ====================
error.account.empty=账号不能为空
error.verify.type.empty=验证码类型不能为空
error.account.format.invalid=请输入正确的手机号或邮箱
error.sms.send.failed=短信发送失败,请稍后重试
error.email.send.failed=邮件发送失败,请稍后重试
# ==================== 反馈模块 ====================
# 反馈Controller
feedback.submit.success=反馈提交成功
feedback.submit.failed=反馈提交失败
feedback.not.found=反馈不存在
feedback.status.update.success=状态更新成功
feedback.status.update.failed=状态更新失败
feedback.delete.success=删除成功
feedback.delete.failed=删除失败
feedback.dingtalk.test.message=测试消息
feedback.dingtalk.test.success=钉钉推送测试成功,请检查钉钉群消息
feedback.dingtalk.test.failed=钉钉推送测试失败,请检查配置和日志
feedback.dingtalk.test.error=测试异常: {0}
# 反馈Service异常
error.feedback.user.id.empty=用户ID不能为空
error.feedback.id.empty=反馈ID不能为空
error.feedback.status.empty=状态不能为空
# ==================== 全局异常处理 ====================
error.token.missing=未提供登录凭证
error.token.invalid=登录凭证无效
error.token.expired=登录已过期,请重新登录
error.token.replaced=账号已在其他地方登录
error.token.kicked.out=账号已被踢下线
error.not.login=未登录,请先登录
error.server.internal=服务器内部错误:{0}
# ==================== 工具类 ====================
# Redis工具类
error.redis.increment.positive=递增因子必须大于0
error.redis.decrement.positive=递减因子必须大于0
# ==================== 邮件模板 ====================
email.subject.verify.code=【CoreWing】验证码
email.greeting=Hi你好
email.verify.intro=感谢使用 CoreWing。我们收到了你的验证请求请使用以下验证码完成操作
email.verify.code.label=验证码
email.verify.notice.title=重要提示
email.verify.notice.content=验证码 5 分钟内有效,请勿泄露给他人
email.verify.security.tips=如果这不是你本人的操作,可以直接忽略这封邮件,你的账号仍然是安全的。
email.verify.support.tips=有任何问题都可以联系我们的客服团队。
email.footer.notice=这是一封自动发送的邮件,请不要直接回复
email.footer.copyright=© 2025 CoreWing · 保留所有权利
# ==================== 短信模板 ====================
sms.verify.code.template=【CoreWing】您的验证码是{0}5分钟内有效。请勿泄露给他人
sms.result.success=发送成功
sms.result.password.error=密码错误
sms.result.account.not.exist=账号不存在
sms.result.balance.insufficient=余额不足
sms.result.account.expired=账户已过期
sms.result.ip.restricted=IP地址限制
sms.result.sensitive.word=内容含有敏感词
sms.result.phone.incorrect=手机号码不正确
sms.result.unknown.error=未知错误
# ==================== 钉钉推送 ====================
dingtalk.feedback.title=📢 新的用户反馈
dingtalk.feedback.header=### 📢 新的用户反馈
dingtalk.feedback.user.id=**用户ID**
dingtalk.feedback.user.anonymous=匿名用户
dingtalk.feedback.type=**问题类型:**
dingtalk.feedback.title.label=**问题标题:**
dingtalk.feedback.content.label=**问题描述:**
dingtalk.feedback.contact=**联系方式:**
dingtalk.feedback.submit.ip=**提交IP**
dingtalk.feedback.submit.region=**IP归属地**
dingtalk.feedback.submit.time=**提交时间:**
dingtalk.feedback.footer=> 请及时处理用户反馈!
# 钉钉日志消息(可选)
dingtalk.webhook.not.configured=钉钉 Webhook 未配置,跳过消息推送
dingtalk.text.send.failed=发送钉钉文本消息失败: {0}
dingtalk.markdown.send.failed=发送钉钉 Markdown 消息失败: {0}
dingtalk.sign.mode=使用加签模式timestamp: {0}
dingtalk.send.message=发送钉钉消息: {0}
dingtalk.push.success=钉钉消息推送成功, 响应: {0}
dingtalk.push.failed=钉钉消息推送失败, HTTP 状态码: {0}, 响应: {1}
dingtalk.push.exception=钉钉消息推送异常: {0}