从前端向后端发送multipart/form-data
类型数据(主要用于文件上传或表单提交)
如发送如下信息:
前端代码vue文件:(配置了服务器代理)
<template>
<div class="content">
<el-form :model="form" ref="form" :rules="rules" label-width="100px" class="demo-form">
<div class="infoAdd">
<div class="title">申请信息</div>
<el-form-item label="申请人" prop="proposer_Name">
<el-input v-model="form.proposer_Name" placeholder="请输入申请人姓名"></el-input>
</el-form-item>
<el-form-item label="联系电话" prop="telephone">
<el-input v-model="form.telephone" placeholder="请输入手机号码"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="cardId">
<el-input v-model="form.cardId" placeholder="请输入身份证号"></el-input>
</el-form-item>
<el-form-item label="营业执照" prop="charterName">
<el-input v-model="form.charterName" placeholder="请输入营业执照(名称)"></el-input>
</el-form-item>
<el-form-item label="所在区域" prop="location">
<el-cascader
v-model="form.location"
:options="areaOptions"
:props="{
value: 'name', // 绑定选项的value字段
label: 'name', // 绑定选项的label字段
children: 'children', // 绑定子级字段
expandTrigger: 'hover' }"
clearable
@change="handleAreaChange"
placeholder="请选择所在区域"
style="width: 100%;"
></el-cascader>
</el-form-item>
<el-form-item label="详细地址" prop="address">
<el-input v-model="form.address" placeholder="请输入详细地址"></el-input>
</el-form-item>
<el-form-item label="踏勘时间" prop="expDate">
<el-date-picker
v-model="form.expDate"
type="date"
placeholder="请选择踏勘时间"
value-format="yyyy-MM-dd"
style="width: 100%;"
></el-date-picker>
</el-form-item>
<el-form-item label="申请说明" prop="applyState">
<el-input
type="textarea"
v-model="form.applyState"
placeholder="请输入详细的申请说明"
:rows="4"
></el-input>
</el-form-item>
</div>
<div class="infoAdd">
<div class="title" style="margin-top: 20px;">已上传资料</div>
<div class="items">
<!-- 上传的图片 -->
<div class="itemAdd" v-for="(img, index) in uploadedImages" :key="'img-'+index">
<img :src="img.preview" alt="上传图片">
<div class="cancel" @click="removeUploadedItem('image', index)">×</div>
</div>
<!-- 上传的视频 -->
<div class="itemAdd" v-for="(video, index) in uploadedVideos" :key="'video-'+index">
<video controls width="100" height="80">
<source :src="video.preview" :type="video.type">
您的浏览器不支持视频预览
</video>
<div class="cancel" @click="removeUploadedItem('video', index)">×</div>
</div>
<!-- 上传的文件 -->
<div class="itemAdd file-item" v-for="(file, index) in uploadedDocs" :key="'file-'+index">
<div class="file-icon">
<i class="el-icon-document"></i>
</div>
<div class="file-name">{{ truncateFileName(file.name) }}</div>
<div class="cancel" @click="removeUploadedItem('doc', index)">×</div>
</div>
</div>
</div>
<div class="infoAdd">
<div class="title">上传资料</div>
<div class="upload-container">
<div class="upload-item" @click="triggerUpload('img')">
<div class="upload-icon">+</div>
<div class="upload-text">图片</div>
<input type="file" ref="img" @change="handleImageUpload" hidden accept="image/*" multiple>
</div>
<div class="upload-item" @click="triggerUpload('video')">
<div class="upload-icon">+</div>
<div class="upload-text">录像</div>
<input type="file" ref="video" @change="handleVideoUpload" hidden accept="video/*" multiple>
</div>
<div class="upload-item" @click="triggerUpload('file')">
<div class="upload-icon">+</div>
<div class="upload-text">文件</div>
<input type="file" ref="file" @change="handleFileUpload" hidden multiple>
</div>
</div>
</div>
<div class="sumBtn">
<el-button type="primary" @click="submitForm('form')" size="large">提交</el-button>
</div>
</el-form>
</div>
</template>
<script>
import chinaRegionData from '@/assets/data/china-region.json';
export default {
name: 'addInfo',
data() {
// 手机号校验函数
const validatePhone = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入联系电话'));
}
if (!/^1[3-9]\d{9}$/.test(value)) {
return callback(new Error('请输入正确的手机号码'));
}
callback();
};
// 身份证校验函数
const validateIdCard = (rule, value, callback) => {
if (!value) {
return callback(new Error('请输入身份证号'));
}
// 简单校验18位(实际项目应使用更严格校验)
if (!/(^\d{15}$)|(^\d{17}(\d|X|x)$)/.test(value)) {
return callback(new Error('请输入有效的身份证号'));
}
callback();
};
return {
form: {
proposer_Name: '',
telephone: '',
cardId: '',
charterName: '',
location: [],
address: '',
expDate: '',
applyState: ''
},
areaOptions: chinaRegionData,
uploadedImages: [], // 上传的图片
uploadedVideos: [], // 上传的视频
uploadedDocs: [] , // 上传的文件
rules: {
proposer_Name: [
{ required: true, message: '请输入申请人姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在2到10个字符', trigger: 'blur' }
],
telephone: [
{ required: true, validator: validatePhone, trigger: 'blur' }
],
cardId: [
{ required: true, validator: validateIdCard, trigger: 'blur' }
],
charterName: [
{ required: true, message: '请输入营业执照名称', trigger: 'blur' }
],
location: [
{ type: 'array', required: true, message: '请选择所在区域', trigger: 'change' }
],
address: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ min: 5, max: 100, message: '长度在5到100个字符', trigger: 'blur' }
],
expDate: [
{ required: true, message: '请选择踏勘时间', trigger: 'change' }
],
applyState: [
{ required: true, message: '请输入申请说明', trigger: 'blur' },
{ min: 10, message: '至少输入10个字符', trigger: 'blur' }
]
},
}
},
methods: {
handleAreaChange(value) {
console.log('Area changed:', value);
},
triggerUpload(type) {
this.$refs[type].click();
},
handleImageUpload(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
Array.from(files).forEach(file => {
if (!file.type.match('image.*')) {
this.$message.error(`${file.name} 不是有效的图片文件`);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedImages.push({
file: file,
preview: e.target.result,
type: file.type
});
};
reader.readAsDataURL(file);
});
console.log("uploadedImages:"+this.uploadedImages)
},
handleVideoUpload(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
Array.from(files).forEach(file => {
if (!file.type.match('video.*')) {
this.$message.error(`${file.name} 不是有效的视频文件`);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedVideos.push({
file: file,
preview: e.target.result,
type: file.type
});
};
reader.readAsDataURL(file);
});
console.log("uploadedVideos:"+this.uploadedVideos)
},
handleFileUpload(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
Array.from(files).forEach(file => {
this.uploadedDocs.push({
file: file,
name: file.name,
type: file.type,
size: file.size
});
});
this.$message.success(`已添加 ${files.length} 个文件`);
console.log("uploadedDocs:"+this.uploadedDocs)
},
removeUploadedItem(type, index) {
if (type === 'image') {
this.uploadedImages.splice(index, 1);
} else if (type === 'video') {
this.uploadedVideos.splice(index, 1);
} else {
this.uploadedDocs.splice(index, 1);
}
this.$message.success('已删除');
},
truncateFileName(name) {
if (name.length > 10) {
return name.substring(0, 8) + '...';
}
return name;
},
submitForm(formName) {
this.$refs[formName].validate((valid)=>{
if (!valid) {
this.$message.error('请检查表单填写是否正确');
return false;
}
// 检查是否上传了文件(如果需要)
if (this.uploadedImages.length === 0 &&
this.uploadedVideos.length === 0 &&
this.uploadedDocs.length === 0) {
this.$message.warning('请至少上传一份资料');
return;
}
console.log("from"+this.form)
// Prepare form data
const formData = new FormData();
formData.append('proposer_Name', this.form.proposer_Name);
formData.append('telephone', this.form.telephone);
formData.append('cardId', this.form.cardId);
formData.append('charterName', this.form.charterName);
formData.append('location', this.form.location.join('/'));
formData.append('address', this.form.address);
formData.append('expDate', this.form.expDate);
formData.append('applyState', this.form.applyState);
formData.append('licenseName',this.$route.query.name)
// Add files
this.uploadedImages.forEach(item => formData.append('img', item.file));
this.uploadedVideos.forEach(item => formData.append('video', item.file));
this.uploadedDocs.forEach(item => formData.append('file', item.file));
console.log("formData:"+this.formData)
console.log("uploadedImages:"+this.uploadedImages)
console.log("uploadedVideos:"+this.uploadedVideos)
console.log("uploadedDocs:"+this.uploadedDocs)
// Submit form
this.$axios.post('/api/user/addUser', formData).then(response => {
console.log("response.data"+response.data)
if(response.data==='success'){
this.$message.success('提交成功');
this.$router.push({
path:'/infoList'
})
}
}).catch(error => {
this.$message.error('提交失败');
console.error(error);
});
})
}
}
}
</script>
<style scoped>
.content {
height: 100%;
width: 100%;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.infoAdd {
width: 95%;
margin: 0 auto;
margin-top: 20px;
border: 1px solid #EBEEF5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
border-radius: 4px;
background-color: #fff;
}
.title::before {
content: '';
display: inline-block;
width: 4px;
height: 14px;
background-color: #409EFF;
margin-right: 8px;
vertical-align: middle;
}
.title {
font-size: 16px;
margin-bottom: 20px;
font-weight: bold;
color: #303133;
}
.items {
margin-top: 12px;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
gap: 10px;
}
.itemAdd {
width: 100px;
height: 80px;
background-color: #F5F7FA;
border: 1px dashed #DCDFE6;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.itemAdd:hover {
border-color: #409EFF;
}
.itemAdd img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.upload-container {
display: flex;
justify-content: space-between;
margin-top: 15px;
}
.upload-item {
flex: 1;
margin: 0 5px;
height: 100px;
background-color: #F5F7FA;
border: 1px dashed #DCDFE6;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-item:hover {
border-color: #409EFF;
}
.upload-icon {
font-size: 24px;
width: 40px;
height: 40px;
color: #909399;
border: 1px solid #DCDFE6;
box-sizing: border-box;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8px;
}
.upload-text {
font-size: 14px;
color: #606266;
}
.cancel {
position: absolute;
font-size: 16px;
right: -8px;
top: -8px;
width: 20px;
height: 20px;
background: #f56c6c;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 2;
}
.file-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-icon {
margin-bottom: 5px;
}
.file-icon i {
font-size: 30px;
color: #409EFF;
}
.file-name {
font-size: 12px;
text-align: center;
width: 90px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
video {
background-color: #000;
border-radius: 4px;
}
.sumBtn {
width: 95%;
margin: 30px auto;
text-align: center;
}
.example {
color: #909399;
font-size: 14px;
}
.el-form-item {
margin-bottom: 20px;
}
.el-cascader {
width: 100%;
}
.el-date-editor {
width: 100%;
}
.el-textarea__inner {
min-height: 80px !important;
}
</style>
以下是基于SSM框架的后端实现代码,用于接收前端发送的表单数据和文件,并保存到数据库中的步骤:(在成功创建SSM项目并能启动成功之后)
1.在 pom.xml
中添加文件上传支持库:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
2.在 spring-mvc.xml
中配置 MultipartResolver:
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置编码 -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 最大上传大小(单位:字节) -->
<property name="maxUploadSize" value="10485760"/> <!-- 10MB -->
<!-- 内存缓冲大小 -->
<property name="maxInMemorySize" value="40960"/>
</bean>
还可能需要解决跨域问题:
<!-- 配置CORS(跨域资源共享)的设定。它的作用是允许web应用处理来自不同源的跨域请求-->
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-methods="GET, POST, PUT, DELETE, OPTIONS"
allowed-headers="Content-Type"/>
</mvc:cors>
3.创建实体类,接收表单数据
@Data
public class User {
private int id;
private String proposerName;
private String licenseName;
private String telephone;
private String cardId;
private String charterName;
private String location;
private String address;
private Date expDate;
private String applyState;
}
4.mapper接口
public interface UserMapper {
//添加申请人
void addUser(User user);
//更新申请人
void updateUser(@Param("id") Integer id,@Param("type") String type,@Param("filePath") String fliePath);
}
5.mapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yuankan.mapper.UserMapper">
<resultMap id="userMap" type="com.yuankan.pojo.User">
<!-- 主键字段使用id标签 -->
<id property="id" column="id" />
<!-- 普通字段使用result标签 -->
<result property="proposerName" column="proposer_Name" />
<result property="licenseName" column="licenseName"/>
<result property="telephone" column="telephone" />
<result property="cardId" column="cardId" />
<result property="charterName" column="charterName" />
<result property="location" column="location" />
<result property="address" column="address" />
<result property="expDate" column="expDate" />
<result property="applyState" column="applyState" />
</resultMap>
<insert id="addUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
insert into user values(null,#{proposerName},#{licenseName},#{telephone},
#{cardId},#{charterName},#{location},#{address},#{expDate}, #{applyState},null,null,null)
</insert>
<update id="updateUser">
update user
<set>
<if test="type=='image'">
img = #{filePath},
</if>
<if test="type=='video'">
video = #{filePath},
</if>
<if test="type=='file'">
file = #{filePath},
</if>
</set>
WHERE id = #{id}
</update>
</mapper>
6.service接口
public interface UserService {
//添加申请人
void addUser(User user, List<MultipartFile> images,
List<MultipartFile> videos,
List<MultipartFile> files);
}
7.service接口实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 添加申请人
* @param user
*/
@Override
public void addUser(User user, List<MultipartFile> images,
List<MultipartFile> videos,
List<MultipartFile> files) {
//保存申请人信息到数据库
userMapper.addUser(user);
// 保存上传的文件
try {
saveUploadedFiles(images, "image", user.getId());
saveUploadedFiles(videos, "video", user.getId());
saveUploadedFiles(files, "file", user.getId());
} catch (IOException e) {
throw new RuntimeException("文件保存失败", e);
}
}
/**
* 保存上传的文件
* @param files 上传的文件
* @param type 文件类型
* @param id 申请人id
* @throws IOException
*/
private void saveUploadedFiles(List<MultipartFile> files, String type, Integer id) throws IOException {
// 如果没有上传文件,则返回
if (files == null || files.isEmpty()) return;
// 遍历文件列表,保存文件到服务器
for (MultipartFile file : files) {
String filePath = FileUploadUtil.saveFile(file, type);
// 更新数据库中的文件路径
userMapper.updateUser(id,type,filePath);
}
}
}
FileUploadUtil工具类:
/**
* 文件上传工具类
*/
public class FileUploadUtil {
// 上传目录
private static final String UPLOAD_DIR = "/uploads/";
/**
* 保存上传的文件
* @param file 上传的文件
* @param type 文件类型
* @return
* @throws IOException
*/
public static String saveFile(MultipartFile file, String type) throws IOException {
// 创建上传目录(如果不存在)
File uploadDir = new File(UPLOAD_DIR + type);
if (!uploadDir.exists()) {
// 创建目录
uploadDir.mkdirs();
}
// 生成唯一文件名
String fileName = file.getOriginalFilename();
//文件路径
Path filePath = Paths.get(uploadDir.getAbsolutePath(), fileName);
// 保存文件
Files.copy(file.getInputStream(), filePath);
// 返回文件路径
return filePath.toString();
}
}
8.Controller层(使用日志记录信息)
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//获取Logger对象
private static final Logger logger= LogManager.getLogger(UserController.class);
@PostMapping("/addUser")
public String addUser(
@RequestParam String proposer_Name,
@RequestParam String telephone,
@RequestParam String cardId,
@RequestParam String charterName,
@RequestParam String[] location, // 地区数组
@RequestParam String address,
@RequestParam Date expDate,
@RequestParam String applyState,
@RequestParam String licenseName,
@RequestParam(required = false) MultipartFile[] img,
@RequestParam(required = false) MultipartFile[] video,
@RequestParam(required = false) MultipartFile[] file){
// 创建申请人对象
User user = new User();
user.setProposerName(proposer_Name);
logger.info("申请人姓名:"+proposer_Name);
user.setTelephone(telephone);
logger.info("申请人电话:"+telephone);
user.setCardId(cardId);
logger.info("申请人身份证号:"+cardId);
user.setCharterName(charterName);
logger.info("营业执照名称:"+charterName);
user.setLocation(String.join("/", location));// 将数组转为字符串
logger.info("申请人地址:"+String.join("/", location));
user.setAddress(address);
logger.info("申请人详细地址:"+address);
user.setExpDate(expDate);
logger.info("申请人有效期:"+expDate);
user.setApplyState(applyState);
logger.info("申请说明:"+applyState);
user.setLicenseName(licenseName);
logger.info("许可证名称:"+licenseName);
logger.info("user:"+user);
// 处理上传文件
List<MultipartFile> images = img != null ? Arrays.asList(img) : null;
List<MultipartFile> videos = video != null ? Arrays.asList(video) : null;
List<MultipartFile> files = file != null ? Arrays.asList(file) : null;
// 添加申请人
userService.addUser(user, images, videos, files);
return "success";
}
}
日志使用:
1.导入依赖:
<!-- Log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>
2.编写log4j2.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!-- <RollingFile name="RollingFile" fileName="logs/ssm.log"-->
<!-- filePattern="logs/ssm-%d{yyyy-MM-dd}.log">-->
<!-- <PatternLayout>-->
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>-->
<!-- </PatternLayout>-->
<!-- <Policies>-->
<!-- <TimeBasedTriggeringPolicy interval="1" modulate="true"/>-->
<!-- </Policies>-->
<!-- <DefaultRolloverStrategy max="30"/>-->
<!-- </RollingFile>-->
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<!-- <AppenderRef ref="RollingFile"/>-->
</Root>
<Logger name="com.it.dao" level="debug"/>
</Loggers>
</Configuration>