一、前言
欢迎回到mypet项目实战!🐾 今天我们正式进入宠物核心模块 ------ 实现 "宠物信息添加" 功能。用户登录后(基于 Day10 的 Token 登录态),可录入自家宠物的分类(如 "狗类""猫类")、品种(如 "拉布拉多""布偶猫")、年龄、照片等信息,这些数据将作为后续 "宠物美容预约""用品推荐" 的基础。
本次实现的核心亮点是 "下拉分类联动":前端通过uni-picker组件加载chongwufenlei(宠物分类表)数据,用户选择分类后提交表单,后端通过 MyBatis-Plus 的insert()方法将数据存入chongwuxinxi(宠物信息表),同时关联当前登录用户 ID(避免越权添加)。即使是零基础,也能通过 "复制代码 + 理解注释" 掌握表单联动与数据新增的全流程。
📌 学习目标:
- 掌握 MP 的insert()方法,实现宠物信息的数据库新增;
- 熟练使用uni-picker组件实现下拉分类选择,并完成接口联动;
- 理解 "用户 ID 关联" 逻辑(从 Token 解析用户 ID,而非 Session);
- 解决 "分类无数据""表单校验""跨端适配" 等实战问题。
二、前置准备
开始编码前,请确认以下内容已就绪,避免开发中卡壳:
项目 | 检查内容 | 注意事项 |
---|---|---|
登录态基础 | Day10 的 Token 登录功能正常,uni.getStorageSync('mypet_token')能获取有效 Token | 若 Token 失效,需重新登录;后端需确保拦截器能通过 Token 解析用户 ID |
数据库表结构 | 需存在 2 张核心表,字段如下:1. chongwufenlei(宠物分类表): id(主键)、fenleimingcheng(分类名称,如 "狗类")、beizhu(备注)2. chongwuxinxi(宠物信息表): id(主键)、user_id(关联用户 ID)、fenlei_id(关联分类 ID)、pinzhong(品种)、nianling(年龄)、chongwumianmao(宠物面貌 URL,可选) | 若表 / 字段缺失,需执行建表语句,例如:sqlCREATE TABLE chongwufenlei (id BIGINT PRIMARY KEY AUTO_INCREMENT, fenleimingcheng VARCHAR(50) NOT NULL);CREATE TABLE chongwuxinxi (id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, fenlei_id BIGINT NOT NULL, pinzhong VARCHAR(50) NOT NULL, nianling INT NOT NULL); |
后端依赖 | 1. 已引入 Hutool(用于字符串校验,同 Day9);2. 已引入 JWT(用于 Token 解析,同 Day10) | 若缺少依赖,需在pom.xml中补充(参考 Day9、Day10 的依赖配置) |
前端组件 | 1. 已导入 Uni-App 的uni-picker(下拉选择)、uni-forms(表单校验)组件;2. 在pages.json中配置页面路由: "pages": [{"path": "pages/chongwuxinxi/add","style": {"navigationBarTitleText": "添加宠物信息"}}] | 组件导入方式:HBuilder X→右键项目→"导入插件"→搜索 "uni-ui" 安装,或手动在pages.json的usingComponents中配置 |
测试数据 | chongwufenlei表已存在测试数据(如 ID=1→"狗类",ID=2→"猫类") | 可通过 SQL 插入:INSERT INTO chongwufenlei (fenleimingcheng) VALUES ('狗类'), ('猫类'); |
三、宠物信息添加流程图
先通过流程图理清 "加载分类→选择分类→填写表单→提交添加" 的完整逻辑:
四、代码实现
4.1 后端:核心接口开发(3 个关键部分)
4.1.1 1. ChongwufenleiController:获取分类列表接口(供前端下拉联动)
路径:src/main/java/com/controller/ChongwufenleiController.java
核心功能:查询chongwufenlei表所有数据,返回分类 ID 和名称,供前端 picker 渲染。
kotlin
import com.entity.ChongwufenleiEntity;
import com.service.ChongwufenleiService;
import com.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/chongwufenlei")
public class ChongwufenleiController {
@Autowired
private ChongwufenleiService chongwufenleiService;
/**
* 获取所有宠物分类(无需登录,公开接口)
*/
@GetMapping("/list")
public R getFenleiList() {
// MP的list()方法:查询所有分类数据
List<ChongwufenleiEntity> fenleiList = chongwufenleiService.list();
// 返回分类ID和名称(前端需要ID关联,名称显示)
return R.ok().put("data", fenleiList);
}
}
4.1.2 2. ChongwuxinxiController:宠物信息添加接口(核心)
路径:src/main/java/com/controller/ChongwuxinxiController.java
核心功能:接收前端表单数据→验证 Token 与用户 ID→校验参数→新增数据到数据库。
kotlin
import cn.hutool.core.util.StrUtil;
import com.entity.ChongwuxinxiEntity;
import com.service.ChongwuxinxiService;
import com.utils.R;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@RequestMapping("/chongwuxinxi")
public class ChongwuxinxiController {
@Autowired
private ChongwuxinxiService chongwuxinxiService;
// JWT密钥(与Day10一致,建议配置在application.properties)
private static final String JWT_SECRET = "mypet-secret-2024";
/**
* 宠物信息添加接口(需登录,Token验证)
* @param chongwu 前端传递的宠物信息(含fenlei_id、pinzhong、nianling等)
* @param request 用于获取Token,解析用户ID
*/
@PostMapping("/add")
public R addChongwu(@RequestBody ChongwuxinxiEntity chongwu, HttpServletRequest request) {
// 从Token解析当前登录用户ID(替代Session,适配移动端)
String token = request.getHeader("token");
Claims claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody();
Long loginUserId = Long.parseLong(claims.getSubject()); // 解析用户ID
// 关联当前用户ID(避免用户添加宠物时篡改user_id)
chongwu.setUserId(loginUserId);
// 参数校验(后端兜底,防止前端跳过验证)
// 校验分类ID:必须存在(前端已选分类,后端二次确认)
if (chongwu.getFenleiId() == null || chongwu.getFenleiId() <= 0) {
return R.error("请选择宠物分类!");
}
// 校验品种:非空且长度不超过50
if (StrUtil.isBlank(chongwu.getPinzhong()) || chongwu.getPinzhong().length() > 50) {
return R.error("宠物品种不能为空,且长度不超过50字!");
}
// 校验年龄:必须为正整数(如1岁、3岁)
if (chongwu.getNianling() == null || chongwu.getNianling() <= 0) {
return R.error("宠物年龄必须为正整数!");
}
// MP的insert()方法:新增宠物信息到数据库
boolean addSuccess = chongwuxinxiService.save(chongwu); // save()等价于insert(),MP推荐用法
if (addSuccess) {
return R.ok("宠物信息添加成功!");
} else {
return R.error("添加失败,请重试!");
}
}
}
📌 后端关键讲解:
- 用户 ID 关联:从 Token 解析loginUserId,而非依赖前端传递或 Session,避免用户篡改 ID(越权添加他人宠物);
- 参数校验:除了非空校验,还增加了 "年龄正整数""品种长度限制",比单纯的空值校验更严谨;
- MP 方法:用save()替代insert(),两者功能一致,但save()是 MP 更通用的新增方法,后续升级兼容性更好。
4.1.3 3. Service 层:无需自定义实现(MP 原生方法)
路径(Service):src/main/java/com/service/ChongwuxinxiService.java
路径(ServiceImpl):src/main/java/com/service/impl/ChongwuxinxiServiceImpl.java
Service 接口(继承 MP 的IService,获取原生方法):
java
import com.entity.ChongwuxinxiEntity;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ChongwuxinxiService extends IService<ChongwuxinxiEntity> {
// 无需添加自定义方法,MP的IService已包含save()(新增)、list()(查询列表)等
}
ServiceImpl 实现类(继承 MP 的ServiceImpl,自动实现接口方法):
scala
import com.entity.ChongwuxinxiEntity;
import com.mapper.ChongwuxinxiMapper;
import com.service.ChongwuxinxiService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class ChongwuxinxiServiceImpl extends ServiceImpl<ChongwuxinxiMapper, ChongwuxinxiEntity> implements ChongwuxinxiService {
// 无需重写save(),父类ServiceImpl已实现(底层调用mapper的insert())
}
4.2 前端:完整表单 + 下拉联动 + 校验逻辑
路径:pages/chongwuxinxi/add.vue
核心功能:加载分类列表→渲染下拉选择→表单校验→提交添加→跳转列表页。
xml
<template>
<view class="pet-add-page">
<!-- 表单容器(用uni-forms做校验) -->
<uni-forms
ref="petForm"
:model="form"
labelWidth="140rpx"
@validate="onValidate"
>
<!-- 1. 宠物分类下拉选择(uni-picker) -->
<uni-forms-item
label="宠物分类"
name="fenleiId"
required
:error-message="formErrors.fenleiId"
>
<uni-picker
@change="onFenleiChange"
:range="fenleiList"
:range-key="'fenleimingcheng'" <!-- 下拉显示的字段(分类名称) -->
placeholder="请选择宠物分类"
>
<view class="picker-input">
{{ form.fenleiId ? (fenleiList.find(item => item.id === form.fenleiId)?.fenleimingcheng || '请选择') : '请选择' }}
</view>
</uni-picker>
</uni-forms-item>
<!-- 2. 宠物品种输入 -->
<uni-forms-item
label="宠物品种"
name="pinzhong"
required
:rules="[{ required: true, errorMessage: '品种不能为空' }, { maxLength: 50, errorMessage: '品种长度不超过50字' }]"
>
<input
v-model="form.pinzhong"
placeholder="请输入宠物品种(如拉布拉多、布偶猫)"
class="input"
/>
</uni-forms-item>
<!-- 3. 宠物年龄输入 -->
<uni-forms-item
label="宠物年龄"
name="nianling"
required
:rules="[{ required: true, errorMessage: '年龄不能为空' }, { pattern: /^[1-9]\d*$/, errorMessage: '年龄必须为正整数' }]"
>
<input
v-model.number="form.nianling" <!-- .number确保值为数字类型 -->
type="number"
placeholder="请输入宠物年龄(如1、3)"
class="input"
min="1" <!-- 限制最小输入1 -->
/>
</uni-forms-item>
<!-- 4. 提交按钮 -->
<uni-forms-item class="submit-btn-item">
<button
type="primary"
class="submit-btn"
@click="submitAdd"
:disabled="!formValid" <!-- 表单校验不通过时禁用按钮 -->
>
提交添加
</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';
import uniPicker from '@dcloudio/uni-ui/lib/uni-picker/uni-picker';
export default {
components: { // 注册组件(必须,否则组件无法渲染)
uniForms,
uniFormsItem,
uniPicker
},
data() {
return {
form: { // 表单数据(关键:存储fenleiId,而非仅名称)
fenleiId: null, // 关联分类ID(提交时需传递给后端)
pinzhong: '', // 宠物品种
nianling: null // 宠物年龄(数字类型)
},
fenleiList: [], // 分类列表(存储ID和名称:[{id:1, fenleimingcheng:'狗类'}, ...])
formErrors: {}, // 表单错误信息(自定义下拉分类的错误提示)
formValid: false // 表单整体是否校验通过(控制按钮禁用)
};
},
onLoad() {
// 页面加载时:获取宠物分类列表(供下拉联动)
this.getFenleiList();
},
methods: {
// 1. 获取宠物分类列表(调用后端/chongwufenlei/list接口)
getFenleiList() {
request.get('/chongwufenlei/list')
.then(res => {
if (res.data.code === 0) {
this.fenleiList = res.data.data; // 存储分类ID和名称
} else {
uni.showToast({ title: res.data.msg, icon: 'none' });
}
})
.catch(err => {
uni.showToast({ title: '分类加载失败,请重试', icon: 'none' });
console.error('分类列表请求失败:', err);
});
},
// 2. 下拉分类选择变化(更新form.fenleiId)
onFenleiChange(e) {
// e.detail.value是选中的分类对象(含id和fenleimingcheng)
this.form.fenleiId = e.detail.value.id;
// 清除分类的错误提示(选择后校验通过)
this.formErrors.fenleiId = '';
// 触发表单整体校验
this.$refs.petForm.validate();
},
// 3. 表单校验回调(判断整体是否通过)
onValidate(res) {
// res是uni-forms的校验结果(仅包含有rules的字段:pinzhong、nianling)
// 额外校验分类(fenleiId是否已选择)
const fenleiValid = this.form.fenleiId !== null;
if (!fenleiValid) {
this.formErrors.fenleiId = '请选择宠物分类';
} else {
this.formErrors.fenleiId = '';
}
// 表单整体校验通过条件:uni-forms校验通过 + 分类已选择
this.formValid = res.valid && fenleiValid;
},
// 4. 提交添加(调用后端/chongwuxinxi/add接口)
submitAdd() {
// 再次手动校验(避免极端情况)
this.$refs.petForm.validate();
if (!this.formValid) {
return;
}
// 调用添加接口(携带Token验证登录)
request.post('/chongwuxinxi/add', this.form, {
headers: { 'token': uni.getStorageSync('mypet_token') }
})
.then(res => {
if (res.data.code === 0) {
// 添加成功:提示+跳转至宠物列表页
uni.showToast({
title: '添加成功',
icon: 'success',
duration: 1500,
success: () => {
// 跳转后关闭当前页(避免返回重复添加)
uni.redirectTo({ url: '/pages/chongwuxinxi/list' });
}
});
} else {
// 添加失败:提示错误信息
uni.showToast({ title: res.data.msg, icon: 'none' });
}
})
.catch(err => {
// 网络错误:提示用户
uni.showToast({ title: '网络异常,请稍后再试', icon: 'none' });
console.error('添加宠物请求失败:', err);
});
}
}
};
</script>
<style scoped>
/* 页面整体样式 */
.pet-add-page {
padding: 30rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
/* 下拉选择框样式 */
.picker-input {
width: 100%;
padding: 20rpx;
border: 1px solid #e5e5e5;
border-radius: 10rpx;
background-color: #fff;
font-size: 28rpx;
color: #333;
}
/* 输入框样式 */
.input {
width: 100%;
padding: 20rpx;
border: 1px solid #e5e5e5;
border-radius: 10rpx;
font-size: 28rpx;
background-color: #fff;
}
/* 提交按钮样式 */
.submit-btn-item {
margin-top: 40rpx;
}
.submit-btn {
width: 100%;
padding: 22rpx 0;
font-size: 30rpx;
border-radius: 10rpx;
}
/* 错误提示样式 */
.uni-forms-item__error {
font-size: 24rpx;
color: #ff4d4f;
margin-top: 10rpx;
}
</style>
📌 前端关键细节讲解:
- 分类联动优化:存储fenleiList为 "ID + 名称" 对象数组(而非仅名称),提交时传递fenleiId,确保后端能关联分类表(之前仅存名称会导致分类关联失效);
- 表单校验:用uni-forms的rules实现品种 / 年龄的校验,同时自定义分类的校验(fenleiId !== null),避免用户跳过必填项;
- 数据类型处理:年龄用v-model.number绑定为数字类型,与后端nianling(INT)字段匹配,避免字符串类型导致的入库错误;
- 用户体验:按钮根据formValid禁用 / 启用,选择分类后清除错误提示,添加成功后跳转并关闭当前页,减少用户误操作。
五、效果验证
按以下步骤验证功能,确保前后端联动正常:
✅ 1. 后端接口测试(Postman)
测试 1:获取分类列表接口
- 请求方式:GET
- 成功返回:
css
{
"code": 0,
"msg": "success",
"data": [
{"id": 1, "fenleimingcheng": "狗类", "beizhu": ""},
{"id": 2, "fenleimingcheng": "猫类", "beizhu": ""}
]
}
测试 2:添加宠物信息接口
- 请求方式:POST
- 请求头:token: 你的有效Token(从 Day10 登录获取)
- 请求体(JSON) :
json
{
"fenleiId": 1, // 对应"狗类"
"pinzhong": "拉布拉多",
"nianling": 2
}
- 成功返回:{"code":0,"msg":"宠物信息添加成功!"}
- 数据库验证:查询chongwuxinxi表,新增一条数据,user_id为 Token 解析的用户 ID,fenlei_id=1,pinzhong=拉布拉多,nianling=2。
✅ 2. 前端功能测试(Uni-App 模拟器 / 真机)
- 登录:通过 Day10 登录页登录,确保mypet_token已存储;
- 进入添加页面:跳转至pages/chongwuxinxi/add.vue,页面加载后下拉分类显示 "狗类""猫类";
- 填写表单:
-
- 选择分类:点击下拉→选择 "狗类";
-
- 输入品种:"拉布拉多";
-
- 输入年龄:"2";
- 提交添加:点击 "提交添加"→弹出 "添加成功" 提示→自动跳转至宠物列表页;
- 列表页验证:宠物列表页显示新增的 "拉布拉多(2 岁,狗类)",数据与数据库同步。
六、常见问题与排查
问题现象 | 可能原因 | 解决方式 |
---|---|---|
1. 下拉分类无数据 | 1. /chongwufenlei/list接口返回空;2. 前端未正确接收数据;3. 分类表无测试数据 | 1. 用 Postman 测试接口,确认返回data非空;2. 检查前端res.data.data是否正确(避免层级错误);3. 往chongwufenlei表插入测试数据(如 "狗类""猫类") |
2. 提交添加时提示 "请选择宠物分类" | 前端form.fenleiId未赋值;或下拉选择后未触发onFenleiChange | 1. 检查uni-picker的@change是否绑定onFenleiChange;2. 确认e.detail.value.id正确赋值给form.fenleiId;3. 打印this.form.fenleiId,确认选择后不为 null |
3. 后端提示 "宠物年龄必须为正整数" | 前端传递的nianling是字符串类型(如 "2");或输入 0 / 负数 | 1. 给年龄输入框加v-model.number,确保传递数字类型;2. 加min="1"限制输入,避免 0 / 负数;3. 前端校验nianling为正整数 |
4. 提示 "Token 无效,请先登录" | 1. Token 过期;2. 提交时未携带 Token;3. Token 格式错误 | 1. 重新登录获取新 Token;2. 检查前端headers是否添加'token': uni.getStorageSync('mypet_token');3. 确认 Token 无空格 / 截断(可打印uni.getStorageSync('mypet_token')查看) |
5. 数据库user_id为 null | 后端未从 Token 解析用户 ID;或chongwu.setUserId(loginUserId)未执行 | 1. 检查 Token 解析逻辑,确认claims.getSubject()能获取用户 ID;2. 确保chongwu.setUserId(loginUserId)在insert()前执行;3. 打印loginUserId,确认不为 null |
6. uni-picker不显示样式 | 1. 未注册uni-picker组件;2. 未安装 uni-ui 插件 | 1. 在components中注册uniPicker;2. HBuilder X→工具→插件市场→搜索 "uni-ui"→安装并导入 |
七、扩展与提升
7.1 功能优化:添加宠物照片上传(类似 Day11 头像)
当前仅添加文字信息,可扩展图片上传功能,让用户上传宠物照片:
- 后端:在chongwuxinxi表添加chongwumianmao(VARCHAR (255),存储图片 URL);
- 前端:添加 "上传宠物照片" 按钮,用uni.chooseImage+uni.uploadFile调用 Day11 的/file/upload接口,获取 URL 后赋值给form.chongwumianmao;
- 后端:在addChongwu方法中接收chongwumianmao字段,一并存入数据库。
7.2 体验优化:分类级联选择(如 "狗类→中型犬→拉布拉多")
若分类有层级(如大类→小类),可实现级联下拉:
- 数据库chongwufenlei表添加parent_id(父分类 ID,如 "狗类" parent_id=0,"中型犬" parent_id=1);
- 前端用uni-picker-cascader组件(uni-ui),先加载大类,选择大类后加载对应小类;
- 提交时传递小类 ID(如 "拉布拉多" 对应的小类 ID),后端关联更精准。
7.3 性能优化:分类数据缓存(减少接口请求)
前端可将分类列表缓存到本地,避免每次进入页面都请求接口:
kotlin
// 获取分类列表时添加缓存
getFenleiList() {
const cacheFenlei = uni.getStorageSync('mypet_fenlei_list');
if (cacheFenlei) {
this.fenleiList = cacheFenlei; // 用缓存数据
return;
}
// 无缓存时请求接口
request.get('/chongwufenlei/list')
.then(res => {
if (res.data.code === 0) {
this.fenleiList = res.data.data;
uni.setStorageSync('mypet_fenlei_list', res.data.data); // 缓存7天(可选)
}
});
}
八、课堂互动
🙋♂️ 思考题 / 互动:
- 如果想让用户添加多只宠物,如何实现 "添加后不跳转,保留表单继续添加"?(提示:清空表单数据)
- 若宠物品种需要跟分类联动(如选 "狗类" 后,品种下拉仅显示狗的品种),该如何设计表结构和前端逻辑?
💡 互动引导:你的下拉分类联动和添加功能跑通了吗?如果遇到 "分类有数据但下拉不显示" 或 "添加后列表不刷新",欢迎分享你的getFenleiList代码或列表页请求逻辑,我们一起排查!
九、下节预告
👉 明天 Day13:商家注册!我们将学习:
- 设计商家表(shangjia)结构(含商家名称、资质照片、联系方式等);
- 实现商家注册表单(含资质照片上传,类似 Day11 的头像上传);
- 后端添加商家审核逻辑(如 "待审核""已通过" 状态);
- 区分 "用户账号" 和 "商家账号" 的权限(用户只能添加宠物,商家可发布服务)。
记得提前复习文件上传(Day11)和表单校验(Day12)的知识点哦!