用HomeBox管宿舍杂物:一个"大三程序员"的库存管理实践
写在前面
大三这学期,我的桌面、抽屉、柜子越来越乱。电子元件、开发板、工具书、各种线缆……每次要找个ESP32开发板,都要翻半天。
我意识到需要一个 inventory management(库存管理)系统。不是那种企业级的庞然大物,而是一个轻量、能自己部署、界面不难看的工具。
搜了一圈,找到了 HomeBox —— 一个用 Go + Vue 写的开源家庭库存管理系统。等等,记忆里我的技术栈是 Python FastAPI + Nuxt?其实 HomeBox 官方是 Go 后端,但我研究它的 API 设计后,自己用 FastAPI 搭了一个简化版,顺便练手。
这篇文章记录的是:如何用 FastAPI + Nuxt 3 从零撸一个迷你库存管理系统,以及踩到的坑。
为什么不用现成的?
HomeBox 其实已经很好用了,但作为练手项目,我想自己实现一遍核心功能:
- 物品增删改查 —— 最基础的 CRUD
- 按位置分类 —— 抽屉A、柜子B、书架C
- 按标签筛选 —— "电子元件"、"工具"、"书籍"
- 二维码标签打印 —— 贴在每个储物位置,扫码快速查看
这四个需求不复杂,但足够覆盖一门《Web应用开发》课程的大作业了。
技术选型(以及为什么)
后端:FastAPI
选 FastAPI 的理由很粗暴:
- 自动生成 OpenAPI 文档(
/docs直接可用) - Pydantic 做数据校验,少写很多 if-else
- 异步支持好,未来接 WebSocket 推送不费劲
# main.py 核心片段
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI(title="DormInventory API", version="0.1.0")
class ItemBase(BaseModel):
name: str
location_id: int
tags: List[str] = []
quantity: int = 1
notes: Optional[str] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
class Config:
orm_mode = True
in_memory_db = {"items": [], "locations": []}
_id_counter = 1
@app.post("/items/", response_model=Item, status_code=201)
def create_item(item: ItemCreate):
global _id_counter
record = {"id": _id_counter, **item.model_dump()}
_id_counter += 1
in_memory_db["items"].append(record)
return record
@app.get("/items/", response_model=List[Item])
def list_items(location_id: Optional[int] = None, tag: Optional[str] = None):
results = in_memory_db["items"]
if location_id:
results = [i for i in results if i["location_id"] == location_id]
if tag:
results = [i for i in results if tag in i["tags"]]
return results
上面这段代码是最小可行版本(内存存储,重启即丢)。真实场景下我会换 SQLite(用 aiosqlite 做异步访问),但作为第一个迭代,内存存储足够验证 API 设计。
前端:Nuxt 3
选 Nuxt 3 的主要原因是文件系统路由和自动导入。写一个 .vue 文件就能自动生成路由,不用手动配置 vue-router。
另一个原因是我想试试 Nuxt 的 useFetch —— 相比手动写 axios,它和 FastAPI 的集成更丝滑:
<!-- pages/items/index.vue -->
<script setup lang="ts">
interface Item {
id: number
name: string
location_id: number
tags: string[]
quantity: number
}
const { data: items, refresh } = await useFetch<Item[]>('/api/items/', {
baseURL: 'http://localhost:8000'
})
const filterTag = ref('')
const filtered = computed(() =>
filterTag.value
? items.value?.filter(i => i.tags.includes(filterTag.value))
: items.value
)
</script>
<template>
<div class="p-4">
<h1 class="text-2xl mb-4">我的库存</h1>
<input v-model="filterTag" placeholder="按标签筛选..." class="border p-2 rounded" />
<div class="grid grid-cols-3 gap-4 mt-4">
<div v-for="item in filtered" :key="item.id"
class="border rounded p-3 hover:shadow-md transition">
<h3 class="font-bold">{{ item.name }}</h3>
<p class="text-sm text-gray-500">数量: {{ item.quantity }}</p>
<div class="flex gap-1 mt-2">
<span v-for="tag in item.tags" :key="tag"
class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</template>
踩坑记录
坑1:CORS 是个绕不过去的门槛
FastAPI 后端跑在 localhost:8000,Nuxt 前端跑在 localhost:3000,浏览器直接报 CORS 错误。
解法是用 FastAPI 的 CORSMiddleware:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # 生产环境要收紧
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
这个坑我第一次踩是大二做课程设计的时候,那时候不懂,直接把前端和后端打包在同一个端口下绕过问题。现在知道了,CORS 是浏览器的安全机制,不是 Bug。
坑2:Nuxt 的 SSR 模式下直接访问浏览器 API 会报错
在 Nuxt 3 的 setup 函数里直接调用 localStorage,服务器端渲染时会报错——因为 Node.js 环境没有 localStorage。
解法是用 process.client 判断,或者把相关逻辑放到 onMounted 里:
onMounted(() => {
const savedTag = localStorage.getItem('filterTag')
if (savedTag) filterTag.value = savedTag
})
坑3:SQLite 并发写入
SQLite 在并发写入时会锁表。我的场景是单用户,问题不大,但如果未来想做成宿舍共用系统,需要换成 PostgreSQL 或者用文件锁。
目前我的解法是加一个写队列(用 asyncio.Queue),把所有写操作串行化。简单粗暴,但够用。
最有价值的一个功能:二维码标签
这是我做得最满意的功能。
每个"位置"(抽屉、柜子)生成一个唯一的二维码,打印出来贴在物理位置上。用手机扫一下,直接打开这个位置的物品列表。
后端生成一个指向前端页面的短链接:
@app.get("/locations/{loc_id}/qrcode")
def get_location_qrcode(loc_id: int):
url = f"http://localhost:3000/locations/{loc_id}"
qr = qrcode.make(url)
buf = BytesIO()
qr.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
前端用 vue-qrcode 组件在网页上直接显示二维码,方便截图打印。
现阶段成果
目前这个迷你系统已经管理了我宿舍里 200+ 件物品,找东西的时间从平均 5 分钟降到 30 秒。
最大的收获不是这个系统本身,而是:
- FastAPI 的依赖注入系统很好用,权限校验可以写成一个
Depends函数 - Nuxt 3 的
useAsyncData比我想的要智能,会自动去重请求 - 做一个"能用的东西"比做一个"完美的东西"更有成就感
下一步
这个暑假打算给系统加两个功能:
- 过期提醒:耗材类物品(比如胶水、电池)设置过期日期,快到期时发飞书消息提醒
- 借用登记:室友借了我的万用表,扫码就能登记借用,避免"消失"
如果效果好,考虑把代码整理一下开源出来。毕竟,每一个"大三练手项目",都可能是下一个靠谱的开源项目的起点。
写于 2026 年夏,宿舍空调房,室外 42°C

浙公网安备 33010602011771号