1.知识要点
1.1数据模型
const formDate = ref({
division: ‘’, //部门
reimbursementTime: null, //报销日期
reimbursementApplicant: ‘’, // 报销人
businessTraveller: ‘’, // 出差人
beOnABusinessTripCause: ‘’, // 出差事由
reimbuursementCountPrice: null, // 报销总额
advanceMoney: null, //预支旅费
advanceTime: ‘’, //预支时间
replenishMoney: null, //不领不足(应补金额)
returnMoney: null, //归还多余(应退金额)
leadSignature: ‘’, //主管签字
accountingFunctionaryUser: ‘’, //财务审核
divisionFunctionaryUser: ‘’, //部门审核
domain: [
{
invoiceTiem: ‘’, // 开票日期
invoiceNumber: ‘’, // 发票号码
invoiceCode: ‘’, // 发票代码
invoiceName: ‘’, //发票项目名称
fileList: [] // 文件数组
}
],
mFileList: []
})
1.2 vue代码
1.2.1 html代码
点击上传
<template v-slot:tip>
<div class="el-upload__tip">只能上传文件,且不超过5M</div>
</template>
</el-upload>
1.2.2文件状态改变功能函数
const handleChange = (uploadFile) => {
formDate.value.mFileList.push(uploadFile.raw)
}
1.2.3提交代码
将表单中的formDate数据中的部分数据添加到djOrderTourDto对象中,后面将djOrderTourDto和fileList(文件数据)转化成FormData()数据
// 提交数据
const submitForm = async () => {
for (let i = 0; i < formDate.value.domain.length; i++) {
console.log(‘当前的:’, formDate.value.domain[i])
}
const fd = new FormData()
// 提取formDate中的domain数据
const invoices = formDate.value.domain.map((m) => ({
invoiceCode: m.invoiceCode,
invoiceName: m.invoiceName,
invoiceNumber: m.invoiceNumber,
invoiceTime: m.invoiceTiem
}))
for (let invoice of invoices) {
console.log(‘invoices的数据:’, invoice)
}
console.log(‘invoices的数据类型:’, Array.isArray(invoices))
// formDate.value.mFileList.push(…formDate.value.domain.map((m) => m.fileList))
console.log(‘当前的文件数据:’, JSON.stringify(formDate.value.mFileList))
// 创建DjOrderTourDto
const djOrderTourDto = {
invoices,
division: formDate.value.division,
reimbursementTime: formDate.value.reimbursementTime,
reimbursementApplicant: formDate.value.reimbursementApplicant,
businessTraveller: formDate.value.businessTraveller,
beOnABusinessTripCause: formDate.value.beOnABusinessTripCause, // 出差事由
reimbuursementCountPrice: formDate.value.reimbuursementCountPrice, // 报销总额
advanceMoney: formDate.value.advanceMoney, //预支旅费
advanceTime: formDate.value.advanceTime, //预支时间
replenishMoney: formDate.value.replenishMoney, //不领不足(应补金额)
returnMoney: formDate.value.returnMoney, //归还多余(应退金额)
leadSignature: formDate.value.leadSignature, //主管签字
accountingFunctionaryUser: formDate.value.accountingFunctionaryUser, //财务审核
divisionFunctionaryUser: formDate.value.divisionFunctionaryUser //部门审核
}
fd.append(‘djOrderTourDto’, JSON.stringify(djOrderTourDto))
// fd.append(‘fileList’, formDate.value.mFileList)
for (let m of formDate.value.mFileList) {
fd.append(‘fileList’, m)
}
loading.value = true
await tourReimbursementInfoAll(fd)
loading.value = false
ElMessage.success(‘提交成功’)
}
1.2.4 api接口
当你使用FormData对象来构造请求体时,不需要在请求头中手动设置Content-Type。FormData对象会自动设置正确的Content-Type,通常是multipart/form-data,这允许它携带多个部分的数据,包括文本和二进制数据(如文件)
import request from ‘@/utils/request.js’
export const tourReimbursementInfoAll = (data) =>
request.post(‘/invoice/upload’, data)
1.2.2 重点:
给数据处理后,给api发送到后端
for (let m of formDate.value.mFileList) {
fd.append(‘fileList’, m)
}
文件数组,那么按照前端格式,就是相同键名,不同的Value值
因为我是同一个数据接收多个文件对象,类型于下面
[
{fileObjiect},
{fileObjiect}
]
1.3 后端Springboot
1.3.1 关键代码
DjOrderTourController.java
如果前端发送的是表单数据(非JSON格式),那么在后端接收时,你应该使用@ModelAttribute注解而不是@RequestBody。
Fastjson本身并不直接处理multipart/form-data格式的数据,因为它设计用于处理JSON格式的数据。在你的场景中,前端发送的multipart/form-data包含了JSON字符串化的djOrderTourDto,这部分数据需要先从multipart/form-data中提取出来,然后才能用Fastjson进行反序列化。
以下是一个步骤说明:
从multipart/form-data中提取JSON字符串: 后端需要从请求中获取djOrderTourDto字段,这通常通过@RequestPart注解完成,因为@RequestParam通常用于处理简单的表单字段,而不是multipart/form-data中的部分。
反序列化JSON字符串: 一旦得到了djOrderTourDto的JSON字符串,可以使用Fastjson的JSON.parseObject()或JSON.toJavaObject()方法将其转换为DjOrderTourDto实体类。
这是一个示例代码片段,展示了如何处理multipart/form-data中的JSON字符串并反序列化为
public R addInvoiceAllInfo(@RequestPart(“djOrderTourDto”)String djOrderTourDtoJson, @RequestPart(“fileList”) MultipartFile[] files) {
// 使用Fastjson反序列化
DjOrderTourDto djOrderTourDto = JSON.parseObject(djOrderTourDtoJson, DjOrderTourDto.class);
Integer tourId = iDjOrderTourService.saveAndGetId(djOrderTour);
DjOrderTourServiceImpl.java
保存一个DjOrderTour对象,并立即获取其自增ID。
直接使用自增ID: 既然您使用的是MyBatis Plus,且假设id字段是自增的,可以在插入后直接使用djOrderTour对象中的ID,因为大多数ORM框架会在执行插入后自动填充自增主键到对象中。这样可以避免额外的查询操作。
确保事务安全: 如果数据库操作需要保证原子性(即保存和获取ID必须同时成功或失败),请确保此方法在一个事务中执行。
在Spring Boot应用中启用事务管理,你需要按照以下步骤操作:
添加依赖:确保你的项目中包含了Spring Data JPA或者MyBatis等持久层框架的依赖,因为Spring的事务管理通常与这些框架一起使用。
配置类启用事务管理:在你的配置类(通常是一个带有@Configuration注解的类)中,添加@EnableTransactionManagement注解,这会启用Spring的声明式事务管理。
启动类加@EnableTransactionManagement注解
@Transactional // 添加事务注解,确保操作的原子性
public Integer saveAndGetId(DjOrderTour djOrderTour) {
djOrderTourMapper.insert(djOrderTour);
/* // 查询最新的ID
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(DjOrderTour::getId);
DjOrderTour one = getOne(queryWrapper, false);
return one.getId();*/
djOrderTourMapper.insert(djOrderTour);
return djOrderTour.getId();
}
2.完整示例代码
2.1 前端 (vue3+element-plus)
2.1.1 页面
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="报销总额">
<el-input v-model="formDate.reimbuursementCountPrice"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预支旅费">
<el-input v-model="formDate.advanceMoney"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="补领不足">
<el-input v-model="formDate.replenishMoney"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="归还多余">
<el-input v-model="formDate.returnMoney"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="主管签字">
<el-input v-model="formDate.leadSignature"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="财务审核">
<el-input v-model="formDate.accountingFunctionaryUser"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="部门审核">
<el-input v-model="formDate.divisionFunctionaryUser"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报销日期">
<el-date-picker
v-model="formDate.reimbursementTime"
type="date"
placeholder="请选择日期"
format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-form
:model=“formDate”
v-for=“(dom, index) in formDate.domain”
:key=“dom.key”
:label=“‘费用项目’ + (index + 1)”
:label-position=“‘top’”
<el-row>
<el-col :span="3">
<el-form-item
label="发票名称"
:prop="'domain' + index + '.invoiceName'"
>
<el-input v-model="dom.invoiceName"></el-input>
</el-form-item>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="4">
<el-form-item
label="发票代码"
:prop="'domain' + index + '.invoiceCode'"
>
<el-input v-model="dom.invoiceCode"></el-input>
</el-form-item>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="4">
<el-form-item
label="发票号码"
:prop="'domain' + index + '.invoiceNumber'"
>
<el-input v-model="dom.invoiceNumber"></el-input>
</el-form-item>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="3">
<el-form-item
label="开票日期"
:prop="'domain' + index + '.invoiceTiem'"
>
<el-date-picker
v-model="dom.invoiceTiem"
type="date"
placeholder="Pick a date"
/>
</el-form-item>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="4">
<el-form-item label="附件上传" :prop="'domain' + index + '.fileList'">
<el-upload
class="upload-demo"
:auto-upload="false"
:on-change="handleChange"
:file-list="dom.fileList"
>
<el-button size="small" type="primary">点击上传</el-button>
<template v-slot:tip>
<div class="el-upload__tip">只能上传文件,且不超过5M</div>
</template>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="2" style="padding: 30px">
<el-form-item>
<el-button type="warning" @click.prevent="removeDomain(dom)"
>删除</el-button
>
</el-form-item>
</el-col>
</el-row>
2.2 api接口
import request from ‘@/utils/request.js’
export const tourReimbursementInfoAll = (data) =>
request.post(‘/invoice/upload’, data)
2.1.2 后端 Springboot
@PostMapping(“/upload”)
public R addInvoiceAllInfo(@RequestPart(“djOrderTourDto”)String djOrderTourDtoJson, @RequestPart(“fileList”) MultipartFile[] files) {
// 使用Fastjson反序列化
DjOrderTourDto djOrderTourDto = JSON.parseObject(djOrderTourDtoJson, DjOrderTourDto.class);
// 通过静态方法获取差旅费报销单对象
/DjOrderTour djOrderTour = getDjOrderTour(djOrderTourDto);/
// 将DTO中的赋值给差旅费报销单DjOrderTour
DjOrderTour djOrderTour = new DjOrderTour();
djOrderTour.setDivision(djOrderTourDto.getDivision());
djOrderTour.setReimbursementTime(djOrderTourDto.getReimbursementTime());
djOrderTour.setReimbursementApplicant(djOrderTourDto.getReimbursementApplicant());
djOrderTour.setBusinessTraveller(djOrderTourDto.getBusinessTraveller());
djOrderTour.setBeOnABusinessTripCause(djOrderTourDto.getBeOnABusinessTripCause());
djOrderTour.setReimbuursementCountPrice(djOrderTourDto.getReimbuursementCountPrice());
djOrderTour.setAdvanceMoney(djOrderTourDto.getAdvanceMoney());
djOrderTour.setReplenishMoney(djOrderTourDto.getReplenishMoney());
djOrderTour.setReturnMoney(djOrderTourDto.getReturnMoney());
djOrderTour.setLeadSignature(djOrderTourDto.getLeadSignature());
djOrderTour.setAccountingFunctionaryUser(djOrderTourDto.getAccountingFunctionaryUser());
djOrderTour.setDivisionFunctionaryUser(djOrderTourDto.getDivisionFunctionaryUser());
Integer tourId = iDjOrderTourService.saveAndGetId(djOrderTour);
//查询发票ID数组
ArrayList<Integer> ids = new ArrayList<>();
// 发票对象
DjInvoice djInvoice = new DjInvoice();
// 遍历前端传递过来的发票数组
for (DjInvoice invoice : djOrderTourDto.getInvoices()) {
djInvoice.setInvoiceCode(invoice.getInvoiceCode());
djInvoice.setInvoiceName(invoice.getInvoiceName());
djInvoice.setInvoiceNumber(invoice.getInvoiceNumber());
// 返回日期数组
log.info(“当前的传递过来的日期数据:{}”,invoice.getInvoiceTime());
djInvoice.setInvoiceTime(invoice.getInvoiceTime());
djInvoice.setFilePath(invoice.getFilePath());
int invoiceId = iDjInvoiceService.saveAndGetId(djInvoice);
ids.add(invoiceId);
}
// 遍历发票id数组,将发票id和差旅费报销单id绑定
DjInvoiceTour djInvoiceTour = new DjInvoiceTour();
for (Integer id : ids) {
djInvoiceTour.setTid(tourId);
djInvoiceTour.setIid(id);
iDjInvoiceTourService.save(djInvoiceTour);
}
//
for (MultipartFile file : files) {
//
String originalFilename = file.getOriginalFilename();
//
String suffixName = originalFilename.substring(originalFilename.lastIndexOf(“.”));
String newFileName = UUID.randomUUID() + suffixName;
File dest = new File(filePath + newFileName);
if ( !dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
log.warn("文件不存在,新建路径");
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
}
return R.success("发票保存成功");
}