AI BI Helper 开发实录 01:文本向量与 Milvus 向量数据库集成
跟着课程《Java转AI高薪领域必备------从0到1打通生产级AI Agent开发》做的实战记录。技术栈:Spring AI Alibaba 1.1.2.0 + Milvus 3.0 + Redis(备选)+ MySQL 8.x。项目代号 ai-bi-helper,这是第 01 期,聚焦文本向量技术,打通文档上传、自定义分割、向量存储全流程。
AI BI Helper 功能分为两大部分:一是文本向量 ,用于文档语义检索;二是Graph 工作流编排,用于实现智能数据分析和决策。本期先搞定第一部分------文本向量,把文档上传、自定义标题分割、向量存储(Redis → Milvus)这套流程跑通。
一. 基础环境搭建
1.1 新建项目 ivy-service-ai-bi-helper-bootstrap
在 ivy-services 中新增 ivy-service-ai-bi-helper-bootstrap 项目。这次项目比较简单,就不再新建 models 模块了,代码都放在一起,结构更紧凑。
1.2 添加 pom 依赖
核心依赖包括:数据库支持、Graph 工作流、阿里百炼大模型。
xml
<dependencies>
<dependency>
<groupId>vip.wayhua.ivy.ai</groupId>
<artifactId>ivy-starter-core-db</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-graph-core</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-starter-model-ollama</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<!-- 阿里百炼大模型服务平台 -->
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
</dependencies>
补充说明:Ivy 项目已经封装了很多常用 starter,比如
ivy-starter-core-db包含了 MySQL 驱动、tk-mybatis、Druid 连接池等,开箱即用。这也是我一直强调的------一定要把常用的东西封装成 starter,避免重复造轮子。
1.3 新建 Spring Boot 启动类
java
package vip.wayhua.ivy.ai.bi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import tk.mybatis.spring.annotation.MapperScan;
import vip.wayhua.ivy.ai.core.utils.StringUtils;
@MapperScan("vip.wayhua.ivy.ai.*.dao")
@SpringBootApplication(scanBasePackages = "vip.wayhua.ivy")
public class BIHelperApplication {
private static final Logger log= LoggerFactory.getLogger(BIHelperApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(BIHelperApplication.class, args);
ConfigurableEnvironment env = run.getEnvironment();
String appName = "AI BI Helper后台接口";
String port = env.getProperty("server.port");
String slog = StringUtils.banner(appName, port, true);
log.error(slog);
}
}
1.4 application.yml 配置
yaml
server:
port: 8910
logging: # SpringBoot日志配置
level:
# ChatClient 监控日志的等级
org.springframework.ai.chat.client.advisor: debug
ivy:
mysql:
ip: 192.168.55.140
port: 3306
driverClassName: com.mysql.cj.jdbc.Driver
database: ai-agent
username: root
password: Ivy@2024
spring:
ai:
dashscope:
# 阿里百炼大模型服务平台 API-Key申请文档
# https://help.aliyun.com/zh/model-studio/get-api-key
api-key: sk-b956cd85b6cd****
chat:
options:
model: qwen3.7-max
# ollama:
# base-url: http://192.168.55.130:11434
# chat:
## model: deepseek-r1:7b
# model: qwen3.5:2b
datasource:
url: jdbc:mysql://${ivy.mysql.ip}:${ivy.mysql.port}/${ivy.mysql.database}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&nullCatalogMeansCurrent=true
username: ${ivy.mysql.username}
password: ${ivy.mysql.password}
db-type: mysql
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: ${ivy.mysql.driverClassName}
druid:
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: true
test-on-return: false
db-type: mysql
mail:
host: smtp.126.com
port: 465
username: wayhua@126.com
password: CDnDp94-***
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
fallback: false
mybatis:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapperLocations: classpath*:mapper/*Mapper.xml
# 开启接口文档
nextdoc4j:
enabled: true
1.5 测试 Controller
java
package vip.wayhua.ivy.ai.bi.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.wayhua.ivy.ai.core.dto.R;
@Tag(name = "测试Controller")
@RestController
@RequestMapping("/test")
public class TestController {
@Operation(summary = "测试", description = "测试")
@GetMapping("/test")
public R test() {
return R.success();
}
}
1.6 运行测试
启动服务后访问接口文档地址:
http://127.0.0.1:8910/doc.html#/document/all/%E6%B5%8B%E8%AF%95Controller/test

1.7 小结
基础环境搭建比较顺利,能复用的都封装过了,直接引入即可。这也是我一直强调的:一定要将常用的东西进行封装,形成自己的 starter 体系,这样新项目才能快速上手。
二. 文本向量 - Redis 版本
2.1 什么是文本向量
先搞清楚核心概念,引用豆包的解释:
文本向量:把自然语言(句子、段落、词语)转换成固定长度的浮点数数组,让计算机能理解文字语义。
文字本身是字符串,机器无法直接计算相似度;转为向量后,就可以用数学方法衡量语义关系。
简单说就是:把文字变成数字数组,方便计算机做语义匹配和相似度计算。
2.2 向量模型选择
本来想使用硅基流动的向量服务(申请了代金券),但一直无法正常使用,最终选择了阿里百炼的文本向量模型:

模型名称:qwen3.7-text-embedding
2.3 配置 Embedding
在 application.yml 中添加向量模型配置:
yaml
spring:
ai:
embedding:
api-key: sk-b956cd85b6**
options:
model: qwen3.7-text-embedding
dimensions: 1024
补充说明:
dimensions: 1024指定向量维度为 1024,这是该模型的默认维度。维度越高,语义表达越精细,但存储和计算成本也越高。
2.4 添加 Redis 向量存储依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
2.5 配置 Redis 向量存储
yaml
spring:
ai:
vectorstore:
redis:
index-name: bi-helper
prefix: bi-helper-prefix
initialize-schema: true
2.6 增加文档上传接口
java
package vip.wayhua.ivy.ai.bi.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import vip.wayhua.ivy.ai.core.dto.R;
@Tag(name = "文档上传Controller")
@RestController
@RequestMapping("/document")
public class DocumentController {
@Autowired
DocumentService documentService;
@PostMapping("/upload")
public R upload(@RequestParam("file") MultipartFile file) {
documentService.saveInVector(file);
return R.success();
}
}
2.7 DocumentServiceImpl 实现
2.7.1 添加 Tika 文档解析依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
补充说明:Tika 是 Apache 的文档解析工具,支持 PDF、Word、Excel 等多种格式,Spring AI 对其做了封装,使用非常方便。
2.7.2 代码实现(基础版本)
java
package vip.wayhua.ivy.ai.bi.service.impl;
import jakarta.annotation.Resource;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import vip.wayhua.ivy.ai.bi.service.DocumentService;
import java.util.List;
@Service
public class DocumentServiceImpl implements DocumentService {
@Resource
VectorStore vectorStore;
/****
* 业务逻辑
* 1.读取文档
* 2.进行分档的拆分,拆成不同的块
* 3.借助向量大模型,将文本转成向量
* 4.入库
* @param file
*/
@Override
public void saveInVector(MultipartFile file) {
// 1.读取文档,2.进行分档的拆分,拆成不同的块
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documents = documentReader.get();
//4.入库
vectorStore.add(documents);
}
}
2.7.3 测试
查看 Redis 中的向量数据:

踩坑记录:使用 Redis Plus 查看向量数据会报错,建议使用 Another Redis Desktop Manager:

2.7.4 问题发现
默认的文本拆分策略是按固定字符数拆分,导致所有文档内容混在一起,语义不连贯。比如一个标题下的内容被拆到了两个块里,后续检索时上下文就不完整了。
2.8 自定义标题分隔器 HeadingBasedSplitter
针对上述问题,自定义一个基于标题的文本分割器,按文档中的数字标题(如 "1."、"1.1"、"2.3.1")进行拆分,确保每个块都是一个完整的章节。
java
package vip.wayhua.ivy.ai.bi.splitter;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 基于标题的文本分割器
* <p>
* 继承 Spring AI 的 {@link org.springframework.ai.transformer.splitter.TextSplitter},通过重写 {@link #splitText(String)} 方法,
* 识别文本中形如 "1."、"1.1"、"2.3.1" 的数字标题行,并按照最低层级标题将文档切分为独立的块。
* <p>
* 主要特性:
* <ul>
* <li>自动识别多级数字标题(例如:1.、1.1、2.1.3)。</li>
* <li>通过选项 {@code prependHeadingPath} 可将当前标题的完整层级路径(如 "1. 概述 > 1.1 背景")
* 前置到每个块的内容开头,帮助后续检索理解上下文。</li>
* <li>可配置最小块大小,过滤掉内容过短的空章节。</li>
* </ul>
* <p>
* 注意:此拆分器仅处理数字编号的标题,不支持中文数字(如"一、")或其他自定义格式,
* 如有需要可自行扩展匹配模式。
**/
public class HeadingBasedSplitter extends TextSplitter {
/**
* 用于匹配标题行的正则表达式。
* <ul>
* <li>{@code ^} ------ 行首</li>
* <li>{@code (\d+(?:\.\d+)*)} ------ 数字编号,如 "1"、"1.1"、"2.3.1"</li>
* <li>{@code \s+} ------ 至少一个空白字符(分隔编号与标题文字)</li>
* <li>{@code (.*)$} ------ 标题文字,直到行尾</li>
* </ul>
* 使用 {@link Pattern#MULTILINE} 模式,使 ^ 和 $ 匹配每一行的开头和结尾。
*/
private static final Pattern HEADING_PATTERN =
Pattern.compile("^(\\d+(?:\\.\\d+)*)\\s+(.*)$", Pattern.MULTILINE);
/**
* 是否将标题路径前置到每个块内容的最前面
*/
private final boolean prependHeadingPath;
/**
* 允许的最小块字符数,小于此值的章节将被忽略
*/
private final int minChunkSize;
/**
* 构造一个标题拆分器。
*
* @param prependHeadingPath 是否在块内容中前置完整的标题路径(例如 "1. 概述 > 1.2 背景")
* @param minChunkSize 最小块大小,低于此大小的标题小节将被丢弃
*/
public HeadingBasedSplitter(boolean prependHeadingPath, int minChunkSize) {
this.prependHeadingPath = prependHeadingPath;
this.minChunkSize = minChunkSize;
}
/**
* 对单段文本进行基于标题的拆分。
* <p>
* 实现逻辑:
* <ol>
* <li>扫描全文,找出所有匹配标题模式的位置。</li>
* <li>若无任何标题,且整个文本长度不少于最小块大小,则原样作为一个块返回。</li>
* <li>若存在标题,则遍历每个标题:
* <ul>
* <li>维护一个路径栈(双端队列),根据当前标题的层级深度调整栈中内容。</li>
* <li>提取从前一个标题结束到下一个标题开始之间的正文部分。</li>
* <li>根据配置决定是否将标题路径前置到正文前。</li>
* <li>过滤掉正文长度小于最小块大小的章节。</li>
* </ul>
* </li>
* </ol>
*
* @param text 待切分的原始文本
* @return 切分后的字符串列表,每个元素对应一个标题下的内容块
*/
@Override
protected List<String> splitText(String text) {
if (!StringUtils.hasText(text)) {
return List.of();
}
// 收集所有标题匹配的位置信息
Matcher matcher = HEADING_PATTERN.matcher(text);
List<HeadingMatch> matches = new ArrayList<>();
while (matcher.find()) {
String numberStr = matcher.group(1); // 如 "1.1"
String titleText = matcher.group(2) != null ? matcher.group(2).trim() : "";
matches.add(new HeadingMatch(matcher.start(), matcher.end(), numberStr, titleText));
}
// 没有标题:整个文本视为一个块(需满足最小长度)
if (matches.isEmpty()) {
return text.trim().length() >= minChunkSize ? List.of(text.trim()) : List.of();
}
List<String> chunks = new ArrayList<>();
// 使用双端队列维护当前标题的完整路径
Deque<String> pathStack = new LinkedList<>();
for (int i = 0; i < matches.size(); i++) {
HeadingMatch match = matches.get(i);
int currentDepth = match.numberStr.split("\\.").length; // 层级深度 = 数字编号中小数点数量 + 1
String headingFull = match.numberStr + " " + match.titleText;
// 1. 更新路径栈:弹出层级大于等于当前深度的旧标题,保证栈顶为直接上级
while (pathStack.size() >= currentDepth) {
pathStack.pollLast();
}
pathStack.offerLast(headingFull);
// 2. 确定正文范围:当前标题结束位置 到 下一个标题开始位置(或文档结尾)
int bodyStart = match.endIndex;
int bodyEnd = (i + 1 < matches.size()) ? matches.get(i + 1).startIndex : text.length();
String body = text.substring(bodyStart, bodyEnd).trim();
// 3. 过滤掉内容过短的章节(如纯标题段)
if (body.length() < minChunkSize) {
// 但仍需保持路径栈已更新(因为它可能作为后续更深层标题的上级)------ 此处通过 continue 可以保留栈状态
continue;
}
// 4. 组装最终块内容
String headingPath = String.join(" > ", pathStack); // 如 "1. 概述 > 1.1 背景"
String chunkContent = prependHeadingPath ? headingPath + "\n" + body : body;
chunks.add(chunkContent);
}
return chunks;
}
/**
* 内部类,记录单个标题匹配的关键信息。
*/
private static class HeadingMatch {
/**
* 标题在原文中的起始索引
*/
final int startIndex;
/**
* 标题结束索引(即正文开始位置)
*/
final int endIndex;
/**
* 纯数字编号,例如 "1.2"
*/
final String numberStr;
/**
* 标题文字部分,不包含编号
*/
final String titleText;
HeadingMatch(int start, int end, String numberStr, String title) {
this.startIndex = start;
this.endIndex = end;
this.numberStr = numberStr;
this.titleText = title;
}
}
}
2.9 集成自定义分隔器
修改 DocumentServiceImpl,引入自定义分割器:
java
public void saveInVector(MultipartFile file) {
// 1.读取文档,2.进行分档的拆分,拆成不同的块
TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
List<Document> documents = documentReader.get();
//3.
HeadingBasedSplitter splitter = new HeadingBasedSplitter(true,50);
List<Document> split = splitter.split(documents);
//4.入库
vectorStore.add(split);
}
注意事项:查看源码发现标题格式要求是 数字 + 空格 + 标题名称(如 "1 概述"、"1.1 背景"),这样才能被正则匹配到。
2.10 测试带分隔的效果



三. 文本向量 - Milvus 数据库版本
Redis 作为向量数据库适合小规模场景,生产环境推荐使用专业的向量数据库。这里选择 Milvus 3.0,它是一款开源的云原生向量数据库,性能优异,社区活跃。
3.1 移除 Redis 向量存储依赖
将 Redis 向量存储的引用注释掉(application.yml 中的配置可以保留,不会产生影响):
xml
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-starter-vector-store-redis</artifactId>-->
<!-- </dependency>-->
3.2 安装 Milvus 3.0
参考官方文档:milvus.io/docs/zh/qui...

3.2.1 下载 docker-compose.yml
bash
mkdir milvus
cd milvus/
yum install wget -y
wget https://github.com/milvus-io/milvus/releases/download/v3.0-beta/milvus-standalone-docker-compose.yml -O docker-compose.yml
踩坑记录:GitHub 有时无法访问,如果下载失败,可直接使用下面的配置内容。
3.2.2 docker-compose.yml 配置
yaml
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.25
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2024-12-18T13-15-44Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v3.0-beta
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
MQ_TYPE: woodpecker
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
networks:
default:
name: milvus
3.2.3 启动 Milvus
bash
docker-compose up -d


3.2.4 权限问题处理
启动后遇到权限错误:

错误信息:
ruby
["failed to mkdir"] [localStoragePath=/var/lib/milvus/data/] [error="mkdir /var/lib/milvus/data/: permission denied"]
解决方案:Milvus 默认以 UID 1000 运行,需要给数据目录设置正确的权限:
bash
# milvus 默认运行uid:1000
mkdir -p /data/milvus
chmod -R 777 /export/server/milvus/volumes/milvus/
# 或者指定uid
chown -R 1000:1000 /export/server/milvus/volumes/milvus/
最好是在当前目录执行:
bash
chmod -R 777 ./volumes/milvus/
# 或者指定uid
chown -R 1000:1000 ./volumes/milvus/

3.2.5 重启 Milvus
bash
docker-compose stop
./autorun.sh
访问 Milvus Web UI:http://192.168.55.130:9091/webui/

3.3 添加 Milvus 向量存储依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>
3.4 配置 Milvus
3.4.1 初始配置
yaml
Spring:
ai:
vectorstore:
milvus:
client:
host: 192.168.55.130
port: 19530
collection-name: bi-helper
database-name: default
3.4.2 踩坑记录:Collection 名称格式错误
启动时报错:
sql
Fail to describe collection 'bi-helper' in database 'default' failed, error code: 1100, reason: Invalid collection name: bi-helper. collection name can only contain numbers, letters and underscores: invalid parameter
[18:22:37.955] ERROR i.m.c.AbstractMilvusGrpcClient - InsertRequest collectionName:bi-helper failed! Exception:{}
io.milvus.exception.ServerException: Invalid collection name: bi-helper. collection name can only contain numbers, letters and underscores: invalid parameter
问题分析 :Milvus 的 collection 名称只能包含数字、字母和下划线,不能包含连字符 -。
解决方案 :将 bi-helper 改为 biHelper,并添加自动建表配置:
yaml
spring:
ai:
vectorstore:
milvus:
client:
host: 192.168.55.130
port: 19530
collection-name: biHelper
database-name: default
id-field-name: id
auto-id: false
embedding-dimension: 1024
initialize-schema: true
补充说明:
embedding-dimension: 1024:与 Embedding 模型的维度保持一致initialize-schema: true:自动创建 collection schema,无需手动建表
3.5 测试 Milvus 向量存储



已成功插入 8 条向量数据!
3.6 安装 Milvus 可视化工具 Attu
Milvus 3.0 默认没有自带图形化管理界面,需要单独安装 Attu:





重要提示:尽可能使用正式版本(如 2.6.5),beta 版本可能会有访问问题。正式版本可以正常查询和管理数据。

四. 数据库准备
为后续的 BI 分析和 Graph 工作流准备业务数据。
4.1 创建数据库
创建数据库 bi-helper:

4.2 创建数据库表
sql
-- ========================================
-- 库存/商品/仓库/销售/调拨管理数据库脚本
-- ========================================
-- 1. 商品表:记录商品的基本信息
DROP TABLE IF EXISTS bb_product;
CREATE TABLE bb_product
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
product_code VARCHAR(50) NOT NULL UNIQUE COMMENT '商品编码',
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
spec VARCHAR(100) DEFAULT NULL COMMENT '规格型号',
unit VARCHAR(20) DEFAULT '件' COMMENT '计量单位',
category VARCHAR(50) DEFAULT NULL COMMENT '分类',
status TINYINT DEFAULT 1 COMMENT '状态(1:启用,0:停用)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
-- 2. 仓库表:存放仓库基本信息
DROP TABLE IF EXISTS bb_warehouse;
CREATE TABLE bb_warehouse
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
warehouse_code VARCHAR(50) NOT NULL UNIQUE COMMENT '仓库编码',
warehouse_name VARCHAR(100) NOT NULL COMMENT '仓库名称',
location VARCHAR(255) DEFAULT NULL COMMENT '仓库位置描述',
manager VARCHAR(50) DEFAULT NULL COMMENT '负责人',
status TINYINT DEFAULT 1 COMMENT '状态(1:启用,0:停用)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库信息表';
-- 3. 库存表:记录每个仓库中每种商品的库存量
DROP TABLE IF EXISTS bb_inventory;
CREATE TABLE bb_inventory
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
quantity DECIMAL(18, 2) DEFAULT 0 COMMENT '当前库存数量',
locked_quantity DECIMAL(18, 2) DEFAULT 0 COMMENT '锁定库存数量(例如调拨中未完成的部分)',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_inventory_wh_prod (warehouse_id, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
-- 4. 销售表
DROP TABLE IF EXISTS bb_sales_record;
CREATE TABLE bb_sales_record
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
sale_date DATE NOT NULL COMMENT '销售日期',
quantity DECIMAL(18, 2) NOT NULL COMMENT '销售数量',
revenue DECIMAL(18, 2) DEFAULT 0 COMMENT '销售金额',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品销售记录表';
-- 5. 调拨单主表
DROP TABLE IF EXISTS bb_transfer_order;
CREATE TABLE bb_transfer_order
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
order_no VARCHAR(50) NOT NULL UNIQUE COMMENT '调拨单号',
source_warehouse_id BIGINT NOT NULL COMMENT '调出仓库ID',
target_warehouse_id BIGINT NOT NULL COMMENT '调入仓库ID',
status TINYINT DEFAULT 0 COMMENT '状态(0:待审核,1:已审核,2:调拨中,3:已完成,4:已驳回)',
transfer_type INT DEFAULT 0 COMMENT '调拨类型:1智能,0人工',
transfer_date DATE DEFAULT NULL COMMENT '调拨日期',
comment TEXT COMMENT '说明',
created_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
approved_by VARCHAR(50) DEFAULT NULL COMMENT '审核人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调拨单主表';
-- 6. 调拨单明细表
DROP TABLE IF EXISTS bb_transfer_order_item;
CREATE TABLE bb_transfer_order_item
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
transfer_order_id BIGINT NOT NULL COMMENT '调拨单ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
transfer_quantity DECIMAL(18, 2) NOT NULL COMMENT '调拨数量',
actual_quantity DECIMAL(18, 2) DEFAULT 0 COMMENT '实际到货数量',
remark VARCHAR(255) DEFAULT NULL COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调拨单明细表';
-- 7. 库存操作日志表(用于追踪库存变动)
DROP TABLE IF EXISTS bb_inventory_log;
CREATE TABLE bb_inventory_log
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
change_type VARCHAR(50) NOT NULL COMMENT '变动类型(IN:入库,OUT:出库,TRANSFER_OUT:调出,TRANSFER_IN:调入)',
quantity_change DECIMAL(18, 2) NOT NULL COMMENT '变动数量(正数增加,负数减少)',
reference_no VARCHAR(50) DEFAULT NULL COMMENT '关联单据号(如调拨单号)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
remark VARCHAR(255) DEFAULT NULL COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作日志表';

4.3 创建数据
sql
-- ========================================
-- 1. 创建仓库样例数据
-- ========================================
INSERT INTO bb_warehouse (warehouse_code, warehouse_name, location, manager)
VALUES
('WH001', '华北仓', '北京', '张三'),
('WH002', '华东仓', '上海', '李四'),
('WH003', '华南仓', '广州', '王五');
-- ========================================
-- 2. 创建商品样例数据
-- ========================================
INSERT INTO bb_product (product_code, product_name, spec, unit, category)
VALUES
('P001', '智能手机', '6.5英寸 128G', '台', '电子产品'),
('P002', '蓝牙耳机', '入耳式', '副', '电子产品'),
('P003', '办公椅', '可调节靠背', '张', '家具'),
('P004', '显示器', '27寸 IPS', '台', '电子产品'),
('P005', '机械键盘', '104键 RGB', '个', '外设');
-- ========================================
-- 3. 初始化库存(每仓库每商品)
-- ========================================
INSERT INTO bb_inventory (warehouse_id, product_id, quantity, locked_quantity)
SELECT
w.id,
p.id,
FLOOR(RAND() * 200 + 100),
0
FROM bb_warehouse w, bb_product p;
-- ========================================
-- 4. 创建过去三年的季度销售数据(2023-2025)
-- 每季度每仓库每商品一条
-- ========================================
SET @start_date = '2023-01-01';
SET @end_date = '2025-12-31';
INSERT INTO bb_sales_record (warehouse_id, product_id, sale_date, quantity, revenue)
SELECT *
FROM (
SELECT
w.id AS warehouse_id,
p.id AS product_id,
DATE_ADD('2023-01-01', INTERVAL (FLOOR(RAND() * 12 * 3)) MONTH) AS sale_date,
ROUND(RAND() * 500 + 50, 2) AS quantity,
ROUND(RAND() * 50000 + 5000, 2) AS revenue
FROM bb_warehouse w,
bb_product p,
(
SELECT 1 FROM dual
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
) q
) t
WHERE t.sale_date BETWEEN @start_date AND @end_date;
-- ========================================
-- 5. 创建调拨单主表(模拟每季度一次调拨)
-- 插入随机调拨单数据(排除相同仓库)
-- ========================================
INSERT INTO bb_transfer_order (
order_no,
source_warehouse_id,
target_warehouse_id,
status,
created_by,
transfer_type,
transfer_date
)
SELECT
t.order_no,
t.source_warehouse_id,
t.target_warehouse_id,
t.status,
t.created_by,
t.transfer_type,
t.transfer_date
FROM (
SELECT
CONCAT('TO', LPAD(FLOOR(RAND() * 99999), 5, '0')) AS order_no,
FLOOR(1 + RAND() * 3) AS source_warehouse_id,
FLOOR(1 + RAND() * 3) AS target_warehouse_id,
3 AS status, -- 已完成
'系统自动' AS created_by,
1 AS transfer_type, -- 假设 1=系统自动
DATE_ADD('2023-01-01', INTERVAL FLOOR(RAND() * 36) MONTH) AS transfer_date
FROM (
SELECT 1 FROM dual
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
UNION SELECT 5
) tmp
) t
WHERE t.source_warehouse_id <> t.target_warehouse_id;
-- ========================================
-- 6. 创建调拨明细数据
-- ========================================
INSERT INTO bb_transfer_order_item (
transfer_order_id,
product_id,
transfer_quantity,
actual_quantity,
remark
)
SELECT
o.id,
p.id,
ROUND(RAND() * 50 + 10, 2),
ROUND(RAND() * 50 + 10, 2),
'季度调拨'
FROM bb_transfer_order o, bb_product p
WHERE o.status = 3
ORDER BY o.id
LIMIT 50;
-- ========================================
-- 7. 为库存操作日志生成随机调拨记录 - 调出
-- ========================================
INSERT INTO bb_inventory_log (
warehouse_id,
product_id,
change_type,
quantity_change,
reference_no,
remark
)
SELECT
t.source_warehouse_id,
item.product_id,
'TRANSFER_OUT',
-item.transfer_quantity,
o.order_no,
'系统模拟调拨出库'
FROM bb_transfer_order o
JOIN bb_transfer_order_item item ON o.id = item.transfer_order_id
JOIN bb_transfer_order t ON o.id = t.id;
-- ========================================
-- 8. 为库存操作日志生成随机调拨记录 - 调入
-- ========================================
INSERT INTO bb_inventory_log (
warehouse_id,
product_id,
change_type,
quantity_change,
reference_no,
remark
)
SELECT
t.target_warehouse_id,
item.product_id,
'TRANSFER_IN',
item.transfer_quantity,
o.order_no,
'系统模拟调拨入库'
FROM bb_transfer_order o
JOIN bb_transfer_order_item item ON o.id = item.transfer_order_id
JOIN bb_transfer_order t ON o.id = t.id;

五. 小结
本期内容主要聚焦于文本向量技术 和Milvus 向量数据库集成,完成了以下工作:
已完成内容
- 基础环境搭建 :新建
ivy-service-ai-bi-helper-bootstrap项目,配置数据库和大模型 - 文本向量核心功能 :
- 集成阿里百炼
qwen3.7-text-embedding向量模型 - 实现文档上传接口,支持 PDF、Word 等多种格式
- 自定义
HeadingBasedSplitter标题分割器,按数字标题拆分文档 - 支持 Redis 和 Milvus 两种向量存储方案
- 集成阿里百炼
- Milvus 3.0 部署 :
- Docker 部署 Milvus standalone 版本
- 解决权限问题(UID 1000)
- 安装 Attu 可视化工具
- 数据库准备:创建商品、仓库、库存、销售、调拨等业务表,并插入测试数据
踩坑记录
| 问题 | 解决方案 |
|---|---|
| Redis Plus 无法查看向量数据 | 使用 Another Redis Desktop Manager |
| Milvus 启动报权限错误 | chown -R 1000:1000 ./volumes/milvus/ |
Milvus Collection 名称包含 - |
改为 biHelper,只能包含数字、字母、下划线 |
| Attu beta 版本无法正常查询 | 使用正式版本 2.6.5 |
下期预告
AI BI Helper 的第二部分------Graph 工作流编排,将实现基于 AI 的智能数据分析和调拨建议生成。
本文是 AI BI Helper 开发实录的第一期,主要完成了文本向量和 Milvus 数据库的集成。感谢阅读!