完成了基础的增删改查,接下来我们开发一个新的功能-上传文件
2023/10/25
1.上传一个文件
首先,我们上传文件要新增一个按钮,并且要有一个文件上传框,默认是隐藏的;当我们点击上传按钮时,可以把文件上传框显示出来,这是基本的逻辑,接下来实现:
<button id="uploadbutton" onclick="document.getElementById('fileInput').click();">上传文件</button>)"
<input id="fileInput" th:type="file" style="display: none" onchange="upload()">
这里我们使用按钮的点击事件,当我们点击上传文件时,会自动获取到id为fileinput的输入框,此时它的隐藏状态虽然不会被改变,但是被选中了,所以会弹出来,然后我们接下来处理上传的逻辑,当我们选择好本地文件后,点击打开,需要触发一个change事件,JavaScript定义如下:
<script>
function upload(){
const fileInput = document.getElementById('fileInput');
const formdata = new FormData();
formdata.append('file',fileInput.files[0])
fetch('/upload',{
method:'POST',
body:formdata
}).then(res=>res.json())
.then(res=>{
console.log(res)
})
.catch(err=>{
console.error("上传失败请重试"+err)
})
}
</script>
我们首先获取fileinput元素,然后定义一个formdata元素,这是常用来发送ajax请求的键值对对象,ajax是不用刷新整个页面就能获取服务器数据的好东西,formdata与model类似,我们定义它的file属性为fileinput的第一个文件数据,发送fetch请求(ajax的一种)路径是upload,方法为post,为什么不用get呢,因为get会把发送的东西附加在浏览器地址里,我们是文件不太合适,请求体设为formdata,一般不会变,后面两句是接收返回对象和打印。
前端写好了,我们先去数据库建立一整表,因为我们使用的是文件,字节文件,不能和cai放在一起了,建表语句为:
create table files(
id int auto_increment primary key,
filename varchar(255) not null,
filedata blob )
其中blob代表字节码文件
然后我们去新建一个实体类file,其中在java中我们定义为byte[]类型,对应数据库中的blob字节类型,
@Data
public class File {
private int id;
private String filename;
private byte[] filedata;
}
有了file实体,定义它的mapper,这里我们定义一个sava方法,
@Mapper
public interface FileMapper {
void save(File file);
}
定义service,注意一个点,我们在mapper和service中定义的文件类型都是File对象类型,不再是byte[]了,真正的文件类型只在实体类内部定义
@Service
public interface FileService extends FileMapper {
void save(File file);
}
还是老方法,好习惯,我们使用service实现类定义真正的调用,service和mapper都只定义空方法,在实现接口中,调用mapper的方法,这里注意一个点,我们调用mapper方法,需要先自动注解,让spring可以识别到mapper,如果不写@Autowired 是无法运行的
public class FileServiceImpl implements FileService {
@Autowired
private FileMapper fileMapper;
@Override
public void save(File file) {
fileMapper.save(file);
}
}
处理对应的逻辑,定义@postmapping路径为upload,定义@ResponseBody返回体类型为http响应,否则只能返回视图名称,
这里我们把返回值类型改为ResponseEntity<Map<String, String>>
可以更容易地构建结构化的 JSON 响应,也就是我们最喜欢的键值对类型,一个名字一个值,首先定义response为hashmap键值对类型,接着trycatch,如果文件非空的话,新建一个文件对象,设置它的名字为我们file参数中的文件名,设置它的filedata为file参数中的获取字节数据方法,其中file设置为常用的文件上传MultipartFile
对象类型。设置号了file的数据库字段,我们调用save方法,保存到数据库,设置response的message属性的值,返回ok消息为response,如果不设为response就可以自定义别的,比如直接输出一段话;
如果文件为空,输出消息,返回错误请求
最后catch一个错误,使用log.error输出它,这个以后比较常用,但是注意要加@SLF4J注解才能使用log,返回错误信息。
@PostMapping("/upload")
@ResponseBody
public ResponseEntity<Map<String, String>> upload(@RequestParam("file") MultipartFile file) {
Map<String, String> response = new HashMap<>();
try {
if (!file.isEmpty()) {
// 获取文件名
File file1 = new File(); // 注意这里,建议重新命名您的File类
file1.setFilename(file.getOriginalFilename());
file1.setFiledata(file.getBytes());
fileService.save(file1);
response.put("message", "文件保存成功");
return ResponseEntity.ok(response);
} else {
response.put("message", "上传的文件为空");
return ResponseEntity.badRequest().body(response);
}
} catch (Exception e) {
log.error("Error during file upload:", e);
response.put("message", "上传失败");
return ResponseEntity.badRequest().body(response);
}
}
注意不要忘了自动注解service接口,否则spring找不到它的实现类
@Autowired
private FileService fileService;
response是我们定义的一个hashmap键值对对象,理论上我们可以定义任何键值,常用的有message,error,data(返回数据),id,result(返回查询结果或列表),status(成功或者失败),当然我们也可以给自己定义一个加油打气:
以后,我们会逐渐使用这些大家通用的方法来替代我们原来的基础东西,慢慢成长
2023/10/28
2.上传一个菜品图片并显示
前面的代码中提到,如何上传一个文件到数据库,这里我们已经可以存到数据库了,任何文件都可以上传,我本以为显示会很简单,但是耗费了我好几天才归类出一个显示图片的办法,不同的文件不可以通用一个模块,本次我们首先讲解菜品图片的上传与显示。
首先,我们上传一张图片到files表中,因为它与菜品信息相关,所有我将cai的id设置为files的外键,其实files这里我们只当作caifiles就行了,专为cai而创建的图片文件,创建外键的好处就是当我们删除主表信息,也就是cai时,会自动删除files中对应外键的文件信息,接下来是建表语句:
ALTER TABLE files
ADD cai_id INT,
ADD FOREIGN KEY (cai_id) REFERENCES cai(id) on delete cascade
我们修改一点点前端的代码,让我们上传图片时,传入一个id信息,目前只修改查询的菜品部分,我们将上传按钮放到这里来,并且传输文件的同时,传输一个caiid,注意这里的传送方式,我们需要的是caiid的text,所以不能只使用dom的getelementbyid方法,同时还要获取它的textcontent元素,通过formdata传送controller
<!-- 展示查询菜品数据 -->
<tr th:if="${caiinfo != null}">
<td id="caiid" th:text="${caiinfo.id}"></td>
<td th:text="${caiinfo.name}"></td>
<td th:text="${caiinfo.price}"></td>
<td th:text="${caiinfo.fenlei}"></td>
<td>
<img th:src="${imageURL}" alt="菜品图片" />
</td>
<td>
<form th:action="@{/delete}" method="get">
<input type="hidden" th:field="*{caiinfo.id}">
<button type="submit" id="deletebutton">删除</button>
</form>
</td>
<td>
<button id="uploadbutton" onclick="document.getElementById('fileInput').click()" type="button">上传文件</button>
<input id="fileInput" type="file" onchange="upload()" style="display: none">
</td>
</tr>
</tbody>
</table>
<script>
function upload(){
const fileInput = document.getElementById('fileInput');
const formdata = new FormData();
const element = document.getElementById('caiid')
const cid = element.textContent
formdata.append('file',fileInput.files[0])
formdata.append('id',cid)
fetch('/upload',{
method:'POST',
body:formdata
}).then(res=>res.json())
.then(res=>{
console.log(res)
})
.catch(err=>{
console.error("上传失败请重试"+err)
})
}
</script>
然后修改一下controller的逻辑,我们分为两个方法,第一个方法用来查询菜品信息
@GetMapping("/caiinfo")
public String getCaiInfo(@RequestParam("id")int id,Model model){
Cai cai = caiService.selectById(id);
model.addAttribute("caiinfo",cai);
model.addAttribute("imageURL","/image?id="+id);
return "index";
}
这里我们查询cai的info,同时我们设置一个imageURL属性的值返回给前端,当前端加载image URL时,会自动加载/image?id=#{id}路径,实现了我们使用菜品id查询文件的要求,然后写image路径的方法,来返回图片
@GetMapping("/image")
public ResponseEntity<ByteArrayResource> queryResult(@RequestParam("id") int id) {
File file = fileService.selectfilebyid(id);
ByteArrayResource resource = new ByteArrayResource(file.getFiledata());
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(resource);
}
这里是难点,因为我们存储到数据库中使用的bolb也就是字节文件,此时我们需要设置responseEntity的类型为ByteArrayResource,也就是字节数组资源,这是将字节文件转化为输入流的方式,返回给前端,最后我们返回的时候使用ok的contentype方法,选择文件的类型,响应体为resource对象,最后前端显示图片的效果如下所示:
3.弹窗检查是否id已存在(小功能)
在成功上传图片后,我想到如果我再次上传会怎样,结果图片却无法显示了,检查了一会,发现原来我的外键没有设置唯一性约束
Alter TABLE files ADD UNIQUE (cai_id)
然后我再次上传图片时,发现图片不会消失了,因为有外键约束,无法正常上传,但是页面也没有任何反应,作为用户我肯定不希望出现这样的情况的,所以我想着能不能写一个检查id是否存在于files表中,如果不存在再进行上传操作,存在的话就要提示用户原因;id已存在。实现如下:
我们加入一个existbyid方法,同时生成service,和serviceImpl方法,要求返回一个Boolean类型,要么有,要么没有
#Mapper和Service
Boolean existsbyid(int id);
#ServiceImpl
public Boolean existsbyid(int id) {
return fileMapper.existsbyid(id);
}
然后写sql语句,如果大于0就返回true
<select id="existsbyid" resultType="java.lang.Boolean">
select count(*)>0 from files where cai_id=#{id}
</select>
我们重新处理一下上传到controller逻辑,如果id不存在,就更新文件,如果id存在,就设置message信息提示用户,最后在前端弹窗
controller修改部分
if(!fileService.existsbyid(id)){
fileService.save(file1);
response.put("message", "文件保存成功");
response.put("评价","wa!这也太厉害了");
return ResponseEntity.ok(response);
}
response.put("message","此菜品已存在产品图");
return ResponseEntity.badRequest().body(response);
前端修改部分
fetch('/upload',{
method:'POST',
body:formdata
}).then(res=>res.json())
.then(res=>{
console.log(res)
alert(res.message)
})
效果如下,虽然我们肉眼就能看到有图了,但是哈哈万一忍不住手贱再上传一次,总得有点提示把,反正我目前觉得有用
2023/10/30
4.规范化代码
(解决table显示问题和只能上传第一个id的文件问题)
在上面的代码中,我们成功针对一条菜品上传了文件,并在前端显示出来,后来我发现当我上传第2个id的文件时,仍然传递的是第一个id信息,苦思冥想,发现在我们的删除和上传文件按钮中id为固定值,每次获取到的按钮信息为同一个按钮,同时,之前我们传递菜品id时,使用的也是固定id的方式,因此统统需要改为动态显示,此一部分只涉及前端修改,后端逻辑不变,同时将table的显示也改为更合理一些,前端代码如下:
#//此为table全部菜品部分
<tr th:each="cai : ${caiList}">
<td th:text="${cai.id}" th:id="'cid'+cai.id"></td>
<td th:text="${cai.name}"></td>
<td th:text="${cai.price}"></td>
<td th:text="${cai.fenlei}"></td>
<td>
<img th:src="${imageURL+cai.id}" alt="菜品图片" />
</td>
<td>
<button type="button" th:id="'deletebutton'+${cai.id}" th:data-id="${cai.id}" onclick="deleteCai(this)">删除</button>
</td>
<td>
<button th:id="'uploadbutton'+cai.id"
th:attr="onclick='document.getElementById(\'' + 'fileInput' + ${cai.id} + '\').click()'"
type="button">上传文件</button>
<input th:id="'fileInput' + ${cai.id}" th:data-id="${cai.id}" type="file" onchange="uploadFile(this)" style="display: none">
</td>
</tr>
<script th:inline="javascript">
function deleteCai(button){
const cid = button.getAttribute('data-id');
const formdata = new FormData()
formdata.append('id',cid)
fetch('delete',{
method:'POST',
body:formdata
}).then(res=>res.json())
.then(res=>{alert(res.message)
location.reload();
})
console.log()
}
function uploadFile(inputElement) {
console.log(inputElement);
const caiId = inputElement.getAttribute('data-id');
const formdata = new FormData();
formdata.append('file', inputElement.files[0]);
console.log(caiId)
// 获取菜品ID并添加到表单数据中
formdata.append('id', caiId);
fetch('/upload',{
method:'POST',
body:formdata
}).then(res=>res.json())
.then(res=>{
console.log(res)
alert(res.message)
location.reload();
})
.catch(err=>{
console.error("上传失败请重试"+err)
})
}
</script>
在前端我们尽量已显示全部菜品为主要页面,加入了一个展示菜品的按钮,后端不再以默认路径/来查询全部,更改为了/pageInfo
#前端
<button id="pageinfo" onclick="page()">展示菜品</button>
<script>
function page() {
window.location.href = '/pageInfo';
}
</script>
#后端
@GetMapping ("/pageInfo")
public String page(Model model){
List<Cai> caiList = caiService.selectAll();
model.addAttribute("caiList",caiList);
model.addAttribute("imageURL","/image?id=");
return "index";
}
当我们做了修改后,使用reload函数,重新加载即可,修改后的实现,更加合理化,展示如下:
本章节主要实现以下功能:
1.对应id信息来上传图片
2.前端显示对应id的图片信息
3.检查是否已经存在图片信息并弹窗