【Uni-App+SSM 宠物项目实战】Day12:宠物信息添加

一、前言

欢迎回到mypet项目实战!🐾 今天我们正式进入宠物核心模块 ------ 实现 "宠物信息添加" 功能。用户登录后(基于 Day10 的 Token 登录态),可录入自家宠物的分类(如 "狗类""猫类")、品种(如 "拉布拉多""布偶猫")、年龄、照片等信息,这些数据将作为后续 "宠物美容预约""用品推荐" 的基础。

本次实现的核心亮点是 "下拉分类联动":前端通过uni-picker组件加载chongwufenlei(宠物分类表)数据,用户选择分类后提交表单,后端通过 MyBatis-Plus 的insert()方法将数据存入chongwuxinxi(宠物信息表),同时关联当前登录用户 ID(避免越权添加)。即使是零基础,也能通过 "复制代码 + 理解注释" 掌握表单联动与数据新增的全流程。

📌 学习目标

  1. 掌握 MP 的insert()方法,实现宠物信息的数据库新增;
  1. 熟练使用uni-picker组件实现下拉分类选择,并完成接口联动;
  1. 理解 "用户 ID 关联" 逻辑(从 Token 解析用户 ID,而非 Session);
  1. 解决 "分类无数据""表单校验""跨端适配" 等实战问题。

二、前置准备

开始编码前,请确认以下内容已就绪,避免开发中卡壳:

项目 检查内容 注意事项
登录态基础 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>

📌 前端关键细节讲解

  1. 分类联动优化:存储fenleiList为 "ID + 名称" 对象数组(而非仅名称),提交时传递fenleiId,确保后端能关联分类表(之前仅存名称会导致分类关联失效);
  1. 表单校验:用uni-forms的rules实现品种 / 年龄的校验,同时自定义分类的校验(fenleiId !== null),避免用户跳过必填项;
  1. 数据类型处理:年龄用v-model.number绑定为数字类型,与后端nianling(INT)字段匹配,避免字符串类型导致的入库错误;
  1. 用户体验:按钮根据formValid禁用 / 启用,选择分类后清除错误提示,添加成功后跳转并关闭当前页,减少用户误操作。

五、效果验证

按以下步骤验证功能,确保前后端联动正常:

✅ 1. 后端接口测试(Postman)

测试 1:获取分类列表接口

  1. 请求地址http://localhost:8080/chongwufenlei/list
  1. 请求方式:GET
  1. 成功返回
css 复制代码
{
  "code": 0,
  "msg": "success",
  "data": [
    {"id": 1, "fenleimingcheng": "狗类", "beizhu": ""},
    {"id": 2, "fenleimingcheng": "猫类", "beizhu": ""}
  ]
}

测试 2:添加宠物信息接口

  1. 请求地址http://localhost:8080/chongwuxinxi/add
  1. 请求方式:POST
  1. 请求头:token: 你的有效Token(从 Day10 登录获取)
  1. 请求体(JSON)
json 复制代码
{
  "fenleiId": 1,    // 对应"狗类"
  "pinzhong": "拉布拉多",
  "nianling": 2
}
  1. 成功返回:{"code":0,"msg":"宠物信息添加成功!"}
  1. 数据库验证:查询chongwuxinxi表,新增一条数据,user_id为 Token 解析的用户 ID,fenlei_id=1,pinzhong=拉布拉多,nianling=2。

✅ 2. 前端功能测试(Uni-App 模拟器 / 真机)

  1. 登录:通过 Day10 登录页登录,确保mypet_token已存储;
  1. 进入添加页面:跳转至pages/chongwuxinxi/add.vue,页面加载后下拉分类显示 "狗类""猫类";
  1. 填写表单
    • 选择分类:点击下拉→选择 "狗类";
    • 输入品种:"拉布拉多";
    • 输入年龄:"2";
  1. 提交添加:点击 "提交添加"→弹出 "添加成功" 提示→自动跳转至宠物列表页;
  1. 列表页验证:宠物列表页显示新增的 "拉布拉多(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 头像)

当前仅添加文字信息,可扩展图片上传功能,让用户上传宠物照片:

  1. 后端:在chongwuxinxi表添加chongwumianmao(VARCHAR (255),存储图片 URL);
  1. 前端:添加 "上传宠物照片" 按钮,用uni.chooseImage+uni.uploadFile调用 Day11 的/file/upload接口,获取 URL 后赋值给form.chongwumianmao;
  1. 后端:在addChongwu方法中接收chongwumianmao字段,一并存入数据库。

7.2 体验优化:分类级联选择(如 "狗类→中型犬→拉布拉多")

若分类有层级(如大类→小类),可实现级联下拉:

  1. 数据库chongwufenlei表添加parent_id(父分类 ID,如 "狗类" parent_id=0,"中型犬" parent_id=1);
  1. 前端用uni-picker-cascader组件(uni-ui),先加载大类,选择大类后加载对应小类;
  1. 提交时传递小类 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天(可选)
      }
    });
}

八、课堂互动

🙋‍♂️ 思考题 / 互动:

  1. 如果想让用户添加多只宠物,如何实现 "添加后不跳转,保留表单继续添加"?(提示:清空表单数据)
  1. 若宠物品种需要跟分类联动(如选 "狗类" 后,品种下拉仅显示狗的品种),该如何设计表结构和前端逻辑?

💡 互动引导:你的下拉分类联动和添加功能跑通了吗?如果遇到 "分类有数据但下拉不显示" 或 "添加后列表不刷新",欢迎分享你的getFenleiList代码或列表页请求逻辑,我们一起排查!

九、下节预告

👉 明天 Day13:商家注册!我们将学习:

  1. 设计商家表(shangjia)结构(含商家名称、资质照片、联系方式等);
  1. 实现商家注册表单(含资质照片上传,类似 Day11 的头像上传);
  1. 后端添加商家审核逻辑(如 "待审核""已通过" 状态);
  1. 区分 "用户账号" 和 "商家账号" 的权限(用户只能添加宠物,商家可发布服务)。

记得提前复习文件上传(Day11)和表单校验(Day12)的知识点哦!

相关推荐
muchan922 小时前
为什么“它”在业务逻辑上是最简单的?
前端·后端·面试
SimonKing3 小时前
告别繁琐配置!Retrofit-Spring-Boot-Starter让HTTP调用更优雅
java·后端·程序员
kele_z3 小时前
PostgreSQL执行计划的使用与查看
后端
往事随风去3 小时前
别再纠结了!IM场景下WebSocket和MQTT的正确选择姿势,一文讲透!
后端·websocket·架构
咖啡Beans3 小时前
Docker安装ELK(Elasticsearch + Logstash + Kibana)
后端·elasticsearch·docker
过分不让我用liberty3 小时前
在java项目中项目里集成ES
后端
Python私教3 小时前
Django全栈班v1.04 Python基础语法 20250912 下午
后端·python·django
爱读源码的大都督4 小时前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
这里有鱼汤4 小时前
发现一个高性能回测框架,Python + Rust,比 backtrader 快 250 倍?小团队必备!
后端·python