一、前言
欢迎回到mypet项目实战!🏪 今天我们开启商家端核心功能—— 实现 “商家注册” 模块。平台用户(普通用户)通过提交营业执照、店铺信息等资料,完成商家身份认证后,即可在平台发布宠物用品销售、美容服务等业务,这是连接 “用户需求” 与 “商家服务” 的关键桥梁。
本次实现的核心难点是 “多图上传”(如营业执照 + 店铺门面照),前端通过uni.chooseImage多选图片,循环调用uni.uploadFile上传至后端FileController,后端用 Hutool 工具类校验文件合法性,最终将图片 URL 存入shangjia(商家表)。即使是零基础,也能通过 “分步代码 + 注释解析” 掌握多图上传与商家信息提交的全流程。
📌 学习目标:
- 掌握 Hutool 的Validator工具类,实现图片格式、文件合法性校验;
- 熟练使用uni.chooseImage+uni.uploadFile完成多图选择与循环上传;
- 理解 “多图 URL 拼接存储” 逻辑(适配数据库单字段存储多图);
- 解决 “上传顺序混乱”“文件大小超限”“商家状态初始化” 等实战问题。
二、前置准备
开始编码前,请确认以下内容已就绪,避免开发受阻:
| 项目 | 检查内容 | 注意事项 |
|---|---|---|
| 基础依赖 | 1. Day10 登录态正常(需 Token 验证,避免匿名注册);2. Day11 的FileController上传接口可用;3. Hutool 依赖已引入(用于文件校验) | 若FileController不可用,需先回顾 Day11 代码;Hutool 版本建议与 Day9 一致(4.0.12),避免兼容性问题 |
| 数据库表结构 | shangjia(商家表)需包含以下字段:id(主键)、user_id(关联注册用户 ID)、shangjiamingcheng(商家名称)、zhizhao(营业执照 URL,多图用逗号分隔)、dianpumianmao(店铺门面照 URL,可选)、lianxidianhua(联系电话)、zhuangtai(审核状态:0 - 待审核,1 - 已通过,2 - 已拒绝)、beizhu(备注) | 若表 / 字段缺失,执行建表 SQL:sqlCREATE TABLE shangjia (id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, shangjiamingcheng VARCHAR(100) NOT NULL, zhizhao VARCHAR(500) NOT NULL, lianxidianhua VARCHAR(20) NOT NULL, zhuangtai TINYINT DEFAULT 0 COMMENT '0-待审核,1-已通过,2-已拒绝'); |
| 前端组件 | 1. 已导入 Uni-App 的uni-forms(表单校验)、uni-upload(可选,优化上传体验);2. pages.json配置路由: "pages": [{"path": "pages/shangjia/register","style": {"navigationBarTitleText": "商家注册"}}] | 若需优化上传 UI,可安装uni-upload组件;路由配置确保页面能正常跳转 |
| 文件配置 | 后端config.properties配置多图上传限制:upload.max.size=5242880(单文件最大 5MB)、upload.allow.types=jpg,png,jpeg(允许的图片格式) | 确保配置文件路径正确,后端能读取到限制参数 |
三、商家注册流程图
先通过流程图理清 “多图上传→信息填写→提交审核” 的完整逻辑:
flowchart TD
A[用户进入商家注册页面] --> B[前端加载表单<br>商家名称和电话等输入框]
B --> C[用户点击上传多图<br> 营业执照加店铺照]
C --> D[uni.chooseImage多选图片<br>限制最多2张 仅图片格式]
D --> E[循环调用uni.uploadFile<br>携带Token 调用FileController]
E --> F{所有图片上传成功}
F -- 否 --> G[提示部分图片上传失败 请重试]
F -- 是 --> H[多图URL用逗号拼接<br>如url1 url2]
H --> I[用户填写其他信息<br>商家名称和联系电话]
I --> J[点击提交注册<br>前端表单校验]
J -- 校验失败 --> K[提示商家名称/电话不能为空]
J -- 校验成功 --> L[携带Token调用/shangjia/register接口<br>传递user_id 多图URL 商家信息]
L --> M[后端拦截器验证Token<br>解析用户ID 关联商家]
M -- Token无效 --> N[返回请先登录]
M -- Token有效 --> O[后端校验 <br>1商家名称非空<br>2营业执照URL非空且格式合法<br>3联系电话格式正确]
O -- 校验失败 --> P[返回具体错误如营业执照格式无效]
O -- 校验成功 --> Q[初始化审核状态为0-待审核<br>MP的insert存入shangjia表]
Q --> R[返回注册提交成功 等待审核]
R --> S[前端提示成功<br>跳转至商家中心页面]
四、代码实现
4.1 后端:核心接口开发(2 个关键部分)
4.1.1 1. ShangjiaController:商家注册接口(含多图校验)
路径:src/main/java/com/controller/ShangjiaController.java
核心功能:接收商家信息 + 多图 URL→校验合法性→关联用户 ID→初始化审核状态→存入数据库。
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.Validator;
import com.entity.ShangjiaEntity;
import com.entity.YonghuEntity;
import com.service.ShangjiaService;
import com.service.YonghuService;
import com.utils.R;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@RestController
@RequestMapping("/shangjia")
public class ShangjiaController {
@Autowired
private ShangjiaService shangjiaService;
@Autowired
private YonghuService yonghuService;
// JWT密钥(与Day10一致)
private static final String JWT_SECRET = "mypet-secret-2024";
// 从配置文件读取允许的图片格式(如jpg,png,jpeg)
@Value("${upload.allow.types}")
private String allowImageTypes;
/**
* 商家注册接口(需登录,Token验证)
* @param shangjia 前端传递的商家信息(含多图URL、商家名称等)
* @param request 用于获取Token,解析用户ID
*/
@PostMapping("/register")
public R register(@RequestBody ShangjiaEntity shangjia, HttpServletRequest request) {
// 从Token解析当前登录用户ID(关联商家与用户,避免越权)
String token = request.getHeader("token");
Claims claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody();
Long userId = Long.parseLong(claims.getSubject());
// 校验用户是否存在(避免无效用户注册)
YonghuEntity user = yonghuService.getById(userId);
if (user == null) {
return R.error("注册用户不存在,请重新登录!");
}
// 核心校验:商家基础信息(非空+格式)
// 校验商家名称(非空+长度限制)
if (StrUtil.isBlank(shangjia.getShangjiamingcheng()) || shangjia.getShangjiamingcheng().length() > 100) {
return R.error("商家名称不能为空,且长度不超过100字!");
}
// 校验联系电话(手机号格式,用Hutool的Validator)
if (!Validator.isMobile(shangjia.getLianxidianhua())) {
return R.error("联系电话格式无效,请输入正确的手机号!");
}
// 多图URL校验(营业执照必传,且格式合法)
String zhizhaoUrls = shangjia.getZhizhao();
if (StrUtil.isBlank(zhizhaoUrls)) {
return R.error("营业执照必传,请上传至少1张图片!");
}
// 分割多图URL(逗号分隔),校验每张图片格式
String[] urlArray = zhizhaoUrls.split(",");
String[] allowTypes = allowImageTypes.split(","); // 从配置读取允许的格式
for (String url : urlArray) {
// 提取URL后缀(如"http://xxx/1.jpg" → "jpg")
String suffix = StrUtil.subAfter(url, ".", true);
// 校验后缀是否在允许列表中
if (!Arrays.asList(allowTypes).contains(suffix.toLowerCase())) {
return R.error("营业执照格式无效,仅支持" + allowImageTypes + "格式!");
}
}
// 初始化商家状态(默认“0-待审核”,避免手动设置)
shangjia.setUserId(userId); // 关联当前用户ID
shangjia.setZhuangtai((byte) 0); // 0-待审核
// MP的save()方法:存入商家数据到数据库
boolean registerSuccess = shangjiaService.save(shangjia);
if (registerSuccess) {
return R.ok("商家注册提交成功,等待平台审核(1-3个工作日)!");
} else {
return R.error("注册提交失败,请重试!");
}
}
}
📌 后端关键讲解:
- 多图校验逻辑:将逗号分隔的 URL 拆分为数组,循环校验每张图片的后缀是否在允许列表(如 jpg/png),比单纯校验 “是否包含图片后缀” 更严谨;
- 状态初始化:默认设置zhuangtai=0(待审核),避免前端篡改状态(如直接设置为 “已通过”);
- 用户关联:从 Token 解析userId,而非依赖前端传递,确保商家与注册用户一一对应,防止越权注册。
4.1.2 2. Service 层:无需自定义实现(MP 原生方法)
路径(Service):src/main/java/com/service/ShangjiaService.java
路径(ServiceImpl):src/main/java/com/service/impl/ShangjiaServiceImpl.java
Service 接口(继承 MP 的IService,获取原生save()方法):
import com.entity.ShangjiaEntity;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ShangjiaService extends IService<ShangjiaEntity> {
// 无需添加自定义方法,MP的IService已包含save()(新增)、getById()(单查)等
}
ServiceImpl 实现类(继承 MP 的ServiceImpl,自动实现接口方法):
import com.entity.ShangjiaEntity;
import com.mapper.ShangjiaMapper;
import com.service.ShangjiaService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class ShangjiaServiceImpl extends ServiceImpl<ShangjiaMapper, ShangjiaEntity> implements ShangjiaService {
// 无需重写save(),父类已实现(底层调用mapper的insert())
}
4.2 前端:完整表单 + 多图上传 + 校验逻辑
路径:pages/shangjia/register.vue
核心功能:多图选择与上传、表单校验、提交注册、状态提示。
<template>
<view class="merchant-register-page">
<!-- 表单容器(uni-forms做校验) -->
<uni-forms
ref="registerForm"
:model="form"
labelWidth="160rpx"
@validate="onValidate"
>
<!-- 1. 商家名称 -->
<uni-forms-item
label="商家名称"
name="shangjiamingcheng"
required
:rules="[{ required: true, errorMessage: '商家名称不能为空' }, { maxLength: 100, errorMessage: '长度不超过100字' }]"
>
<input
v-model="form.shangjiamingcheng"
placeholder="请输入商家名称(如“宠爱之家宠物用品店”)"
class="input"
/>
</uni-forms-item>
<!-- 2. 联系电话 -->
<uni-forms-item
label="联系电话"
name="lianxidianhua"
required
:rules="[{ required: true, errorMessage: '联系电话不能为空' }, { pattern: /^1[3-9]\d{9}$/, errorMessage: '请输入正确的手机号' }]"
>
<input
v-model="form.lianxidianhua"
type="number"
placeholder="请输入用于联系的手机号"
class="input"
maxlength="11"
/>
</uni-forms-item>
<!-- 3. 多图上传(营业执照+店铺门面照) -->
<uni-forms-item
label="营业执照上传"
required
:error-message="formErrors.zhizhao"
>
<!-- 上传按钮 -->
<button
class="upload-btn"
type="default"
@click="chooseMultiImages"
>
选择图片(最多2张,支持jpg/png)
</button>
<!-- 已上传图片预览 -->
<view class="image-preview-list" v-if="imageUrls.length > 0">
<view class="image-item" v-for="(url, index) in imageUrls" :key="index">
<image :src="url" mode="widthFix" class="preview-image"></image>
<button class="delete-btn" @click="deleteImage(index)">删除</button>
</view>
</view>
<view class="tip-text">提示:需上传清晰的营业执照照片,用于审核</view>
</uni-forms-item>
<!-- 4. 提交按钮 -->
<uni-forms-item class="submit-btn-item">
<button
type="primary"
class="submit-btn"
@click="submitRegister"
:disabled="!formValid || isUploading" <!-- 上传中/表单无效时禁用 -->
>
{{ isUploading ? '提交中...' : '提交商家注册' }}
</button>
</uni-forms-item>
</uni-forms>
</view>
</template>
<script>
// 导入请求工具和Uni-UI组件
import request from '@/api/request.js';
import uniForms from '@dcloudio/uni-ui/lib/uni-forms/uni-forms';
import uniFormsItem from '@dcloudio/uni-ui/lib/uni-forms-item/uni-forms-item';
export default {
components: { // 注册组件
uniForms,
uniFormsItem
},
data() {
return {
form: { // 商家注册表单数据
shangjiamingcheng: '', // 商家名称
lianxidianhua: '', // 联系电话
zhizhao: '' // 营业执照多图URL(逗号分隔,提交时赋值)
},
imageUrls: [], // 已上传的图片URL列表(前端预览用)
formErrors: {}, // 自定义错误提示(如多图上传错误)
formValid: false, // 表单整体是否校验通过
isUploading: false, // 是否正在上传中(防止重复提交)
uploadCount: 0 // 已完成上传的图片数量(用于判断是否全部上传成功)
};
},
methods: {
// 1. 选择多图(最多2张)
chooseMultiImages() {
// 限制最多上传2张(营业执照至少1张,可选1-2张)
const maxCount = 2;
if (this.imageUrls.length >= maxCount) {
return uni.showToast({ title: `最多只能上传${maxCount}张图片`, icon: 'none' });
}
uni.chooseImage({
count: maxCount - this.imageUrls.length, // 剩余可选择数量
sizeType: ['original', 'compressed'], // 原图/压缩图(压缩图更省流量)
sourceType: ['album', 'camera'], //相册 / 相机选图
success: (chooseRes) => {
// 开始上传选中的图片(循环上传)
this.isUploading = true;
this.uploadCount = 0; // 重置上传计数
const tempPaths = chooseRes.tempFilePaths; // 选中的临时图片路径
tempPaths.forEach((tempPath, index) => {
uni.uploadFile({
url: '<http://localhost:8080/file/upload>', // 后端 FileController 接口
filePath: tempPath, // 临时图片路径
name: 'file', // 与后端 MultipartFile 参数名一致
header: {
'token': uni.getStorageSync ('mypet_token'), // 携带 Token 验证登录
'Content-Type': 'multipart/form-data' // 文件上传必须的 Content-Type
},
// 上传进度回调(可选,提升体验)
progress: (progressRes) => {
console.log (第${index+1}张图上传进度:${progressRes.progress}%);
},
success: (uploadRes) => {
// 解析上传结果(uploadRes.data 默认是字符串,需转 JSON)
const uploadData = JSON.parse (uploadRes.data);
if (uploadData.code === 0) {
// 上传成功:添加 URL 到预览列表
this.imageUrls.push (uploadData.file);
} else {
// 单张上传失败:提示具体错误
uni.showToast ({ title: 第${index+1}张图上传失败:${uploadData.msg}, icon: 'none' });
}
},
fail: (uploadErr) => {
// 网络错误等上传失败
uni.showToast ({ title: 第${index+1}张图上传失败,请重试, icon: 'none' });
console.error (' 图片上传失败:', uploadErr);
},
complete: () => {
// 无论成功 / 失败,都计数并判断是否全部上传完成
this.uploadCount++;
if (this.uploadCount === tempPaths.length) {
this.isUploading = false; // 所有图片上传完成,取消上传中状态
// 提示上传结果
if (this.imageUrls.length > 0) {
uni.showToast ({ title: 成功上传${this.imageUrls.length}张图片, icon: 'success' });
}
}
}
});
});
},
fail: (chooseErr) => {
// 用户取消选图或选图失败
console.log (' 选图失败:', chooseErr);
}
});
},
// 2. 删除已上传的图片
deleteImage (index) {
// 从预览列表中删除指定索引的图片
this.imageUrls.splice (index, 1);
// 若删除后无图片,清除表单中的 zhizhao 字段
if (this.imageUrls.length === 0) {
this.form.zhizhao = '';
} else {
// 重新拼接 URL(逗号分隔)
this.form.zhizhao = this.imageUrls.join (',');
}
// 重新触发表单校验
this.$refs.registerForm.validate ();
},
// 3. 表单校验回调(判断整体是否通过)
onValidate (res) {
//res:uni-forms 自带校验结果(商家名称、联系电话)
let zhizhaoValid = true; // 营业执照上传校验结果
// 校验:至少上传 1 张营业执照图片
if (this.imageUrls.length === 0) {
this.formErrors.zhizhao = ' 请至少上传 1 张营业执照图片 ';
zhizhaoValid = false;
} else {
this.formErrors.zhizhao = '';
// 拼接多图 URL(逗号分隔),赋值给表单字段
this.form.zhizhao = this.imageUrls.join (',');
}
// 表单整体校验通过条件:自带校验通过 + 营业执照校验通过
this.formValid = res.valid && zhizhaoValid;
},
// 4. 提交商家注册
submitRegister () {
// 再次手动校验,避免极端情况
this.$refs.registerForm.validate ();
if (!this.formValid || this.isUploading) {
return;
}
// 防止重复提交
this.isUploading = true;
// 调用后端注册接口
request.post ('/shangjia/register', this.form, {
headers: {'token': uni.getStorageSync ('mypet_token') }
})
.then (res => {
if (res.data.code === 0) {
// 注册成功:提示 + 跳转至商家中心
uni.showToast ({
title: res.data.msg,
icon: 'success',
duration: 2000,
success: () => {
// 跳转后关闭当前页(避免返回重复提交)
uni.redirectTo ({ url: '/pages/shangjia/center' });
}
});
} else {
// 注册失败:提示错误信息
uni.showToast ({ title: res.data.msg, icon: 'none' });
}
})
.catch (err => {
// 网络错误
uni.showToast ({ title: ' 网络异常,请稍后再试 ', icon: 'none' });
console.error (' 商家注册请求失败:', err);
})
.finally (() => {
// 无论成功 / 失败,都取消上传中状态
this.isUploading = false;
});
}
}
};
五、效果验证
按以下步骤验证商家注册与多图上传功能,确保前后端联动正常:
✅ 1. 后端接口测试(Postman)
测试1:多图上传接口(复用Day11的/file/upload)
- 请求地址:
http://localhost:8080/file/upload - 请求方式:POST
- 请求头:
token: 有效登录Token - 请求体:
form-data格式,key=file,value选择2张jpg/png图片(分2次上传,模拟前端循环) - 成功返回(单张):
{ "code": 0, "msg": "上传成功", "file": "http://localhost:8080/mypet/upload/123.jpg" }
测试 2:商家注册接口
- 请求方式:POST
- 请求头:token: 有效登录Token
- 请求体(JSON) :
{
"shangjiamingcheng": "宠爱之家宠物用品店",
"lianxidianhua": "13812345678",
"zhizhao": "http://localhost:8080/mypet/upload/123.jpg,http://localhost:8080/mypet/upload/456.jpg"
}
- 成功返回:
{
"code": 0,
"msg": "商家注册提交成功,等待平台审核(1-3个工作日)!"
}
- 数据库验证:查询shangjia表,新增数据中user_id为 Token 解析的用户 ID,zhizhao字段为逗号分隔的 2 个 URL,zhuangtai=0(待审核)。
✅ 2. 前端功能测试(Uni-App 模拟器 / 真机)
- 登录:通过 Day10 登录页登录,确保mypet_token已存储;
- 进入注册页面:跳转至pages/shangjia/register.vue,表单初始为空;
- 填写基础信息:
-
- 商家名称:输入 “宠爱之家宠物用品店”;
-
- 联系电话:输入 “13812345678”;
- 多图上传:
-
- 点击 “选择图片”→从相册选 2 张图片(营业执照 + 店铺照);
-
- 上传完成后,预览区显示 2 张图片,支持点击删除(删除后预览区同步减少);
- 提交注册:
-
- 点击 “提交商家注册”→按钮显示 “提交中...”(防止重复点击);
-
- 提交成功后,弹出 “等待审核” 提示→自动跳转至商家中心页面;
- 商家中心验证:商家中心显示 “待审核” 状态,与数据库zhuangtai字段同步。
六、常见问题与排查
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 1. 多图上传时部分失败 | 1. 单张图片超过后端大小限制;2. 网络波动导致中断;3. Token 过期 | 1. 检查config.properties的upload.max.size,确保图片不超限(如 5MB);2. 前端添加 “重试上传” 按钮,失败后允许重新上传;3. 上传前校验 Token 有效性,过期则提示重新登录 |
| 2. 提交时提示 “请至少上传 1 张营业执照” | 前端imageUrls为空;或form.zhizhao未拼接 URL | 1. 检查chooseMultiImages是否正确将上传成功的 URL 添加到imageUrls;2. 确认onValidate中this.form.zhizhao = this.imageUrls.join(',')已执行;3. 打印this.imageUrls,确保提交前非空 |
| 3. 后端提示 “营业执照格式无效” | 1. URL 后缀不在允许列表(如.gif);2. 前端拼接 URL 时多逗号 / 空格 | 1. 检查upload.allow.types配置,确保仅允许jpg,png,jpeg;2. 前端拼接 URL 前去除空值(如this.imageUrls.filter(url => url).join(','));3. 打印zhizhaoUrls,确认后缀合法 |
| 4. 数据库zhuangtai字段为 null | 后端未初始化shangjia.setZhuangtai((byte) 0) | 1. 检查ShangjiaController的注册逻辑,确保状态初始化代码执行;2. 确认shangjia实体类zhuangtai字段类型为Byte(匹配数据库 TINYINT) |
| 5. 前端点击提交无反应 | 1. formValid=false(表单未通过校验);2. isUploading=true(上传中) | 1. 检查表单错误提示,确保商家名称、电话、图片均已填写 / 上传;2. 等待上传完成(isUploading变为 false)后再提交;3. 打印this.formValid和this.isUploading,定位禁用原因 |
七、扩展与提升
7.1 功能优化:多图上传进度条与重试机制
当前上传进度仅打印日志,可优化为可视化进度条:
- 前端引入uni-progress组件,为每张上传中的图片显示进度;
- 上传失败时,在预览图上显示 “重试” 按钮,点击后重新上传该张图片;
- 全部上传完成后,提示 “共上传 X 张,成功 X 张,失败 X 张”,清晰展示结果。
7.2 体验优化:图片压缩与格式转换
避免大图片占用过多带宽,前端可在上传前压缩图片:
// 选择图片后压缩(使用uni.getImageInfo+canvas压缩)
uni.getImageInfo({
src: tempPath,
success: (imageInfo) => {
// 压缩逻辑:如宽高超过1000px则等比缩小,质量0.8
const canvasWidth = imageInfo.width > 1000 ? 1000 : imageInfo.width;
const canvasHeight = imageInfo.height * (canvasWidth / imageInfo.width);
// 通过canvas压缩后,再调用uni.uploadFile上传压缩后的临时路径
}
});
7.3 安全优化:营业执照真实性校验
防止用户上传非营业执照图片,可增加基础校验:
- 后端对接第三方 OCR 接口(如阿里云 OCR),提取图片中的 “营业执照编号”“公司名称”;
- 校验 OCR 提取的公司名称与商家注册填写的 “商家名称” 是否一致;
- 若未提取到营业执照关键信息,返回 “请上传清晰的营业执照图片”,提升审核准确性。
八、课堂互动
🙋♂️ 思考题 / 互动:
- 若商家需要上传更多类型图片(如店铺环境照、资质证书),如何扩展表单并区分存储(如dianpu_env字段存环境照 URL)?
- 前端如何实现 “上传中的图片禁止删除”(避免删除正在上传的文件导致异常)?
💡 互动引导:你的多图上传和商家注册功能跑通了吗?如果遇到 “部分图片上传失败” 或 “审核状态不显示” 的问题,欢迎分享你的uploadRes数据或后端日志,我们一起排查!
九、下节预告
👉 明天 Day14:商家服务列表!我们将学习:
- 设计shangjia_service(商家服务表),存储服务名称、价格、描述等;
- 实现商家端 “添加服务” 功能(关联商家 ID,仅自己可见);
- 前端用uni-list展示服务列表,支持 “编辑 / 删除” 服务;
- 后端添加服务状态控制(如 “上架 / 下架”,仅上架服务对用户可见)。
记得提前复习 MP 的 “条件查询”(如按商家 ID 查询服务)用法哦!