Compare commits

...

5 Commits

Author SHA1 Message Date
6606438862 【改进】密码校验 2025-11-12 09:26:59 +08:00
c9332b85db 【新增】增加跨域支持 2025-11-12 09:26:45 +08:00
74d19bb9d6 【新增】新增OSS-SDK 2025-11-12 09:26:35 +08:00
57f97a490a 【改进】固件上传 2025-11-12 09:26:16 +08:00
746e5051cf 【改进】完善教程 2025-11-12 09:25:35 +08:00
13 changed files with 229 additions and 14 deletions

View File

@@ -42,6 +42,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok' // Lombok 注解处理 annotationProcessor 'org.projectlombok:lombok' // Lombok 注解处理
testImplementation 'org.springframework.boot:spring-boot-starter-test' // 测试框架 testImplementation 'org.springframework.boot:spring-boot-starter-test' // 测试框架
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // thymeleaf模版引擎 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // thymeleaf模版引擎
implementation 'com.aliyun.oss:aliyun-sdk-oss:3.15.1' // OSS SDK
} }
tasks.named('test') { tasks.named('test') {

View File

@@ -0,0 +1,39 @@
package com.corewing.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 标记为配置类
public class CorsConfig implements WebMvcConfigurer {
/**
* 配置跨域过滤器,优先级高于所有拦截器
*/
@Bean
public CorsFilter corsFilter() {
// 1. 配置跨域参数
CorsConfiguration config = new CorsConfiguration();
// 允许的前端域名(根据实际环境修改)
config.addAllowedOriginPattern("*");
// 允许携带 Cookie前后端都需要开启
config.setAllowCredentials(true);
// 允许的请求方法(包含预检请求 OPTIONS
config.addAllowedMethod("*");
// 允许的请求头(* 表示所有)
config.addAllowedHeader("*");
// 预检请求缓存时间1小时减少重复验证
config.setMaxAge(3600L);
// 2. 配置路径匹配规则(对所有接口生效)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 3. 返回过滤器(优先级最高)
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,7 @@
package com.corewing.app.dto.api;
import lombok.Data;
@Data
public class TutorialListRequest {
}

View File

@@ -7,6 +7,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 教程分类 * 教程分类
@@ -76,4 +77,7 @@ public class TutorialCategory implements Serializable {
public static final String typeCategory = "category"; public static final String typeCategory = "category";
public static final String typeTag = "tag"; public static final String typeTag = "tag";
@TableField(exist = false)
private List<Tutorial> tutorials;
} }

View File

@@ -93,8 +93,8 @@ public class BizFirmwareController {
*/ */
@PostMapping("/uploadFile") @PostMapping("/uploadFile")
@ResponseBody @ResponseBody
public Result<String> uploadFile(MultipartFile file) { public Result<String> uploadFile(MultipartFile file, Long id) {
return Result.success(); return Result.isBool(firmwareService.uploadFile(file, id));
} }
} }

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory; import com.corewing.app.entity.TutorialCategory;
import com.corewing.app.service.TutorialCategoryService; import com.corewing.app.service.TutorialCategoryService;
@@ -107,5 +108,4 @@ public class AppTutorialController {
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.entity.Firmware; import com.corewing.app.entity.Firmware;
import org.springframework.web.multipart.MultipartFile;
/** /**
* 固件 Service 接口 * 固件 Service 接口
@@ -24,4 +25,12 @@ public interface FirmwareService extends IService<Firmware> {
* @return 固件信息 * @return 固件信息
*/ */
Firmware getByFirmwareName(String firmwareName); Firmware getByFirmwareName(String firmwareName);
/**
* 上传文件
* @param file
* @param id
* @return
*/
boolean uploadFile(MultipartFile file, Long id);
} }

View File

@@ -3,9 +3,13 @@ package com.corewing.app.service;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
public interface TutorialService extends IService<Tutorial> { public interface TutorialService extends IService<Tutorial> {
Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang); Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang);

View File

@@ -7,8 +7,12 @@ import com.corewing.app.common.page.PageContext;
import com.corewing.app.entity.Firmware; import com.corewing.app.entity.Firmware;
import com.corewing.app.mapper.FirmwareMapper; import com.corewing.app.mapper.FirmwareMapper;
import com.corewing.app.service.FirmwareService; import com.corewing.app.service.FirmwareService;
import com.corewing.app.util.OSSUploadUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/** /**
* 固件 Service 实现类 * 固件 Service 实现类
@@ -32,5 +36,18 @@ public class FirmwareServiceImpl extends ServiceImpl<FirmwareMapper, Firmware> i
return this.getOne(wrapper); return this.getOne(wrapper);
} }
@Override
public boolean uploadFile(MultipartFile file, Long id) {
try {
String downloadUrl = OSSUploadUtil.uploadFile(file.getInputStream(), "/");
Firmware firmware = getById(id);
firmware.setDownloadUrl(downloadUrl);
updateById(firmware);
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
} }

View File

@@ -5,16 +5,20 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.common.page.PageContext; import com.corewing.app.common.page.PageContext;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory;
import com.corewing.app.entity.TutorialCategoryRelation; import com.corewing.app.entity.TutorialCategoryRelation;
import com.corewing.app.mapper.TutorialMapper; import com.corewing.app.mapper.TutorialMapper;
import com.corewing.app.service.TutorialCategoryRelationService; import com.corewing.app.service.TutorialCategoryRelationService;
import com.corewing.app.service.TutorialCategoryService;
import com.corewing.app.service.TutorialService; import com.corewing.app.service.TutorialService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
@Service @Service
public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> implements TutorialService { public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> implements TutorialService {
@@ -25,6 +29,9 @@ public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> i
@Resource @Resource
private TutorialCategoryRelationService tutorialCategoryRelationService; private TutorialCategoryRelationService tutorialCategoryRelationService;
@Resource
private TutorialCategoryService tutorialCategoryService;
@Override @Override
public Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang) { public Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang) {
return tutorialMapper.pageList(page, categoryId, tutorialTitle, lang); return tutorialMapper.pageList(page, categoryId, tutorialTitle, lang);
@@ -64,4 +71,5 @@ public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> i
removeById(id); removeById(id);
return tutorialCategoryRelationService.remove(new LambdaQueryWrapper<TutorialCategoryRelation>().eq(TutorialCategoryRelation::getCategoryId, tutorial.getCategoryId())); return tutorialCategoryRelationService.remove(new LambdaQueryWrapper<TutorialCategoryRelation>().eq(TutorialCategoryRelation::getCategoryId, tutorial.getCategoryId()));
} }
} }

View File

@@ -144,9 +144,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
} }
// 验证密码MD5加密 // 验证密码MD5加密
// String encryptPassword = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8)); String encryptPassword = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));
// 客户端已经使用加密 // 客户端已经使用加密
if (!password.equals(user.getPassword())) { if (!encryptPassword.equals(user.getPassword())) {
throw new RuntimeException(I18nUtil.getMessage("error.password.incorrect")); throw new RuntimeException(I18nUtil.getMessage("error.password.incorrect"));
} }

View File

@@ -0,0 +1,86 @@
package com.corewing.app.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.InputStream;
@Component
public class OSSUploadUtil {
private static final String ENDPOINT = "oss-cn-shenzhen.aliyuncs.com"; // 地域节点
private static final String ACCESS_KEY_ID = "your-access-key-id"; // 替换为你的 AccessKey ID
private static final String ACCESS_KEY_SECRET = "your-access-key-secret"; // 替换为你的 AccessKey Secret
private static final String BUCKET_NAME = "corewing-app"; // 替换为你的 Bucket 名称
/**
* 上传文件到 OSS
* @param inputStream 文件输入流
* @param objectName OSS 中存储的文件路径(如 "firmware/20231104/update.bin"
* @return 上传成功后的文件 URL
*/
public static String uploadFile(InputStream inputStream, String objectName) {
// 创建 OSS 客户端实例
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
try {
// 设置文件元数据(可选,如 Content-Type
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(getContentType(objectName)); // 自动识别文件类型
// 上传文件
PutObjectRequest request = new PutObjectRequest(BUCKET_NAME, objectName, inputStream, metadata);
PutObjectResult result = ossClient.putObject(request);
// 生成文件访问 URL公网访问需 Bucket 设为公共读)
return String.format("https://%s.%s/%s", BUCKET_NAME, ENDPOINT, objectName);
} finally {
// 关闭 OSS 客户端,释放资源
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 上传本地文件到 OSS
* @param localFilePath 本地文件路径(如 "D:/firmware/update.bin"
* @param objectName OSS 中存储的文件路径
* @return 上传成功后的文件 URL
*/
public static String uploadLocalFile(String localFilePath, String objectName) {
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
try {
File file = new File(localFilePath);
ossClient.putObject(BUCKET_NAME, objectName, file);
return String.format("https://%s.%s/%s", BUCKET_NAME, ENDPOINT, objectName);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
/**
* 根据文件名获取 Content-Type
* @param fileName 文件名(如 "update.bin"
* @return Content-Type
*/
private static String getContentType(String fileName) {
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
switch (suffix) {
case "bin": return "application/octet-stream";
case "jpg": case "jpeg": return "image/jpeg";
case "png": return "image/png";
case "txt": return "text/plain";
// 其他文件类型可自行扩展
default: return "application/octet-stream";
}
}
}

View File

@@ -219,6 +219,7 @@
id="firmwareFile" id="firmwareFile"
name="file" name="file"
accept=".bin" accept=".bin"
ref="firmwareFileRef"
> >
<div class="form-text text-muted mt-1">支持 .bin 格式,单个文件不超过 100MB</div> <div class="form-text text-muted mt-1">支持 .bin 格式,单个文件不超过 100MB</div>
</div> </div>
@@ -499,20 +500,59 @@
}, },
// 打开文件上传模态框 // 打开文件上传模态框
openUploadModal(item) { openUploadModal(item) {
this.addOrEditDto = item;
this.modalInstances['uploadFileModal'].show(); this.modalInstances['uploadFileModal'].show();
}, },
uploadFile() { async uploadFile() {
request.put("/biz/user/resetPasswordRequest", this.resetPasswordDto) // 1. 获取选中的文件
.then((res) => { const fileInput = this.$refs.firmwareFileRef;
if (res.code === 200) { const file = fileInput.files[0];
$message.success('修改成功!', 500);
this.modalInstances['resetPwdModal'].hide(); // 2. 校验文件(格式、大小)
} else { if (!file) {
alert('修改失败!'); alert("请选择 .bin 格式的固件文件");
return;
}
if (file.type !== "application/octet-stream" && !file.name.endsWith(".bin")) {
alert("仅支持 .bin 格式文件");
return;
}
if (file.size > 100 * 1024 * 1024) { // 100MB
alert("文件大小不能超过 100MB");
return;
}
const formData = new FormData();
formData.append("file", file);
formData.append("id", this.addOrEditDto.id);
// 4. 发送上传请求Axios
try {
const response = await axios({
url: "/biz/firmware/uploadFile",
method: "POST",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: (progressEvent) => {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
console.log(`上传进度:${progress.toFixed(2)}%`);
} }
this.resetPasswordDto = {};
}); });
if (response.data.success) {
$message.success('固件上传成功');
this.$refs.firmwareFileRef.value = "";
this.modalInstances['uploadFileModal'].hide();
} else {
$message.error("上传失败:" + response.data.message);
}
} catch (error) {
console.error("上传出错:", error);
$message.error("网络异常,上传失败");
}
}, },
// 打开新增模态框 // 打开新增模态框
openAddModal() { openAddModal() {