我是一名兼任财务与产品管理的工程公司员工,负责多个工地项目的农民工工资及材料采购管理。每个月我都要手工录入几十张采购发票和销售清单,再对着 Excel 逐条核账——不仅费时,还容易出错。
我开始尝试用 WorkBuddy(一款 AI 编程助手)来帮我开发一套专属进销存系统,最终在约 5天内完成了一套可以实际上线使用的软件。整个过程我几乎没有自己写过一行代码,但我需要非常清楚地告诉 WorkBuddy 我想要什么、为什么要这样设计。
本文是这段经历的完整复盘,希望对同样有"业务强、代码弱"困境的朋友有所帮助。
在动手之前,我先把业务痛点整理成一张清单:
痛点 | 现状 | 期望 |
|---|---|---|
发票录入 | 人工手打,每张约 5 分钟 | OCR 自动识别,1 秒导入 |
库存核对 | Excel 手工加减,易出错 | 系统自动计算,支持负库存提醒 |
多项目管理 | 多个文件夹分公司,容易混淆 | 单系统多公司切换 |
出库成本 | 不清楚每次出库真实成本 | 加权平均法自动计算 |
进销存软件
├── 基础档案
│ ├── 商品管理(含规格/单位)
│ ├── 供应商管理
│ └── 客户管理
├── 采购管理
│ ├── OCR 扫描进项发票
│ └── 发票明细入库
├── 销售管理
│ ├── OCR 扫描销项发票
│ └── 发票明细出库(含出库成本)
├── 库存管理
│ ├── 期初库存导入(Excel)
│ └── 库存台账(数量 + 金额合计)
└── 系统设置
├── 多公司管理
└── 用户权限(JWT 鉴权)我把上面的需求描述告诉 WorkBuddy,然后问它:"给我推荐一个适合 Windows 本地部署、Python 开发者友好、前后端分离的技术栈。"
WorkBuddy 给出了以下方案,并解释了每个选择的理由:
层次 | 技术 | 选择理由 |
|---|---|---|
后端 | Flask (Python) | 轻量、易扩展,适合中小型应用;Python 生态丰富(OCR 库多) |
数据库 | MySQL 8.0 | 事务支持好,ENUM 类型方便状态管理 |
ORM/驱动 | PyMySQL(原生 SQL) | 不用 ORM 避免复杂映射,SQL 清晰可读 |
前端 | Vue 3 + Vite | 组合式 API 上手快,Vite 构建速度快 |
UI 组件库 | Element Plus | 表单/表格组件丰富,契合后台系统风格 |
状态管理 | Pinia | 比 Vuex 更简洁 |
OCR 引擎 | 百度云 OCR + 腾讯云 OCR 双引擎 | 互为备用,识别率更高 |
鉴权 | JWT (JSON Web Token) | 无状态鉴权,前后端分离友好 |
部署 | 本地 Windows 服务,start_backend.py 一键启动 | 无需运维经验 |
WorkBuddy 帮我设计了核心表结构,关键决策包括:
inventory_transactions 表使用 ENUM('purchase','sale','initial','adjustment') 的 reference_type 字段,统一管理所有出入库记录purchase_invoices、sales_invoices),通过外键关联到明细表inventory_transactions 聚合计算,不冗余存储,保证数据一致性核心设计原则(WorkBuddy 建议):库存数量和金额不单独存字段,每次查询都从流水表实时聚合——这样历史数据永远可追溯。
我给 WorkBuddy 发了第一条指令:
帮我在 D:\workbuddy\inventory-system 目录下初始化一个
Flask + Vue3 前后端分离项目,后端端口 5000,前端端口 5173,
MySQL 数据库名 inventory_db。WorkBuddy 在几分钟内完成了:
backend/、frontend/)requirements.txt,包含 Flask、PyMySQL、PyJWT、python-dotenv 等依赖create_tables.py,一键建库建表实际项目目录结构:
inventory-system/
├── backend/
│ ├── app.py # Flask 主入口
│ ├── start_backend.py # 一键启动脚本
│ ├── config.py # 数据库/JWT 配置
│ ├── routes/
│ │ ├── auth.py # 登录鉴权
│ │ ├── inventory.py # 库存管理(含期初导入)
│ │ ├── purchase.py # 采购发票
│ │ ├── sales.py # 销售发票
│ │ ├── products.py # 商品档案
│ │ ├── suppliers.py # 供应商
│ │ └── customers.py # 客户
│ ├── utils/
│ │ ├── baidu_ocr.py # 百度 OCR 封装
│ │ └── tencent_ocr.py # 腾讯 OCR 封装
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── views/ # 14 个页面组件
│ │ ├── router/ # Vue Router 路由配置
│ │ ├── stores/ # Pinia 状态管理
│ │ └── api/ # Axios API 模块
│ └── package.json
└── .workbuddy/ # WorkBuddy 工作目录# routes/auth.py(WorkBuddy 生成)
@auth_bp.route('/api/auth/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')
user = db.execute_one(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, md5(password))
)
if not user:
return jsonify({'code': 401, 'msg': '用户名或密码错误'}), 401
token = jwt.encode({
'user_id': user['id'],
'exp': datetime.utcnow() + timedelta(hours=24)
}, SECRET_KEY, algorithm='HS256')
return jsonify({'code': 0, 'token': token, 'user': user})WorkBuddy 帮我实现了采购发票的"幂等性入库"——同一张发票号不会重复入库:
@purchase_bp.route('/api/purchase/invoices', methods=['POST'])
@require_auth
def create_purchase_invoice():
data = request.json
company_id = g.company_id # 从 JWT 中取多公司 ID
# 幂等检查:发票号唯一
existing = db.execute_one(
"SELECT id FROM purchase_invoices WHERE invoice_no = %s AND company_id = %s",
(data['invoice_no'], company_id)
)
if existing:
return jsonify({'code': 400, 'msg': '该发票号已存在'}), 400
# 事务入库
with db.transaction():
invoice_id = db.insert('purchase_invoices', {...})
for item in data['items']:
db.insert('purchase_invoice_items', {
'invoice_id': invoice_id, ...
})
# 写入库存流水
db.insert('inventory_transactions', {
'reference_type': 'purchase',
'reference_id': invoice_id,
'product_id': item['product_id'],
'quantity': item['quantity'],
'unit_price': item['unit_price'],
'company_id': company_id
})
return jsonify({'code': 0, 'msg': '入库成功'})这是整个项目最有技术含量的部分。我告诉 WorkBuddy:
我需要上传一张发票图片,系统自动识别出:
- 发票号、开票日期
- 供应商名称和税号
- 商品明细列表(名称、规格、数量、单价、金额)
百度 OCR 和腾讯 OCR 各自的 API Key 我已经申请好了。
请帮我封装两个 OCR 函数,优先用百度,失败时自动切换腾讯。WorkBuddy 生成了以下核心逻辑:
# utils/baidu_ocr.py
def recognize_invoice(image_path: str) -> dict:
"""调用百度增值税发票识别 API"""
client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
with open(image_path, 'rb') as f:
image_data = f.read()
result = client.vatInvoice(image_data)
if 'words_result' not in result:
raise OCRException(f"百度 OCR 识别失败: {result}")
# 结构化解析
words = result['words_result']
items = []
for item in words.get('CommodityName', {}).get('word', []):
items.append({
'name': item,
'quantity': ...,
'unit_price': ...,
'amount': ...
})
return {
'invoice_no': words.get('InvoiceNum', {}).get('word', ''),
'date': words.get('InvoiceDate', {}).get('word', ''),
'seller_name': words.get('SellerName', {}).get('word', ''),
'seller_tax_no': words.get('SellerRegisterNum', {}).get('word', ''),
'items': items,
'total_amount': words.get('TotalAmount', {}).get('word', '0')
}双引擎降级策略:
def ocr_invoice_with_fallback(image_path: str) -> dict:
"""优先百度,失败切换腾讯"""
try:
return baidu_ocr.recognize_invoice(image_path)
except Exception as e:
logger.warning(f"百度 OCR 失败,切换腾讯: {e}")
return tencent_ocr.recognize_invoice(image_path)财务系统上线前需要导入历史库存数据。我提供了一个 Excel 模板,WorkBuddy 帮我实现了完整的导入流程:
@inventory_bp.route('/api/inventory/initial-import', methods=['POST'])
@require_auth
def initial_import():
"""Excel 期初库存导入"""
file = request.files.get('file')
if not file:
return jsonify({'code': 400, 'msg': '请上传文件'}), 400
# 检查是否已有期初数据(防重复导入)
existing = db.execute_one(
"SELECT COUNT(*) as cnt FROM inventory_transactions "
"WHERE reference_type = 'initial' AND company_id = %s",
(g.company_id,)
)
if existing['cnt'] > 0:
return jsonify({'code': 400, 'msg': '期初库存已导入,如需重新导入请先清除'}), 400
# 用 openpyxl 解析 Excel
wb = openpyxl.load_workbook(file)
ws = wb.active
success_count = 0
errors = []
with db.transaction():
for row_num, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
product_name, spec, unit, quantity, unit_price = row[:5]
if not product_name or quantity is None:
continue
try:
# 模糊匹配商品档案
product = find_or_create_product(product_name, spec, unit)
db.insert('inventory_transactions', {
'reference_type': 'initial',
'product_id': product['id'],
'quantity': float(quantity),
'unit_price': float(unit_price or 0),
'company_id': g.company_id,
'remark': '期初导入'
})
success_count += 1
except Exception as e:
errors.append(f"第 {row_num} 行:{str(e)}")
return jsonify({
'code': 0,
'success_count': success_count,
'errors': errors
})前端部分我让 WorkBuddy 并行开发,把 14 个页面分批次生成。关键页面包括:
采购发票页面(OCR 核心交互):
<!-- PurchaseForm.vue 核心部分 -->
<template>
<div class="purchase-form">
<!-- 发票上传区域 -->
<el-upload
action="/api/ocr/invoice"
:headers="authHeaders"
:on-success="handleOCRSuccess"
accept=".jpg,.png,.pdf"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽发票图片到此处,或 <em>点击上传</em></div>
</el-upload>
<!-- OCR 识别结果(可编辑) -->
<el-form v-if="invoiceData" :model="invoiceData" label-width="120px">
<el-form-item label="发票号码">
<el-input v-model="invoiceData.invoice_no" />
</el-form-item>
<el-form-item label="供应商">
<el-select v-model="invoiceData.supplier_id" filterable allow-create>
<el-option v-for="s in suppliers" :key="s.id" :label="s.name" :value="s.id" />
</el-select>
</el-form-item>
<!-- 商品明细表格(可编辑) -->
<el-table :data="invoiceData.items" border>
<el-table-column label="商品名称">
<template #default="{ row }">
<el-input v-model="row.product_name" @blur="handleFuzzyMatch(row)" />
</template>
</el-table-column>
<!-- ... 数量、单价、金额列 -->
</el-table>
<el-button type="primary" @click="submitInvoice">确认入库</el-button>
</el-form>
</div>
</template>库存台账页面(带汇总行):
<!-- InventoryLedger.vue 核心查询逻辑 -->
<script setup>
const ledgerData = ref([])
const totalQty = computed(() => ledgerData.value.reduce((s, r) => s + r.quantity, 0))
const totalAmount = computed(() => ledgerData.value.reduce((s, r) => s + r.amount, 0))
// 查询库存台账
const fetchLedger = async () => {
const res = await api.get('/api/inventory/ledger', {
params: { company_id: companyStore.currentCompanyId, ...filters }
})
ledgerData.value = res.data.list
}
</script>这是一个让我印象深刻的细节。OCR 识别出来的商品名称常常和档案里的不完全一致:
"钢筋 HRB400 Φ16mm""螺纹钢筋 HRB400 16"我告诉 WorkBuddy:
商品名称模糊匹配规则:
1. 名称中包含相同核心词(如"钢筋")
2. 规格相同(如"HRB400"、"16mm")
3. 单位相同(如"吨")
以上三个条件满足则视为同一商品,自动匹配并关联WorkBuddy 生成了一个批量模糊匹配 API:
@inventory_bp.route('/api/inventory/batch-fuzzy-match', methods=['POST'])
@require_auth
def batch_fuzzy_match():
"""OCR 识别后批量模糊匹配商品"""
items = request.json.get('items', [])
products = db.execute_many(
"SELECT id, name, spec, unit FROM products WHERE company_id = %s",
(g.company_id,)
)
results = []
for item in items:
matched = None
best_score = 0
for product in products:
score = 0
# 名称包含匹配(核心词提取)
if extract_core_name(item['name']) in product['name']:
score += 2
# 规格完全匹配
if normalize_spec(item.get('spec', '')) == normalize_spec(product['spec']):
score += 2
# 单位匹配
if item.get('unit', '') == product['unit']:
score += 1
if score > best_score and score >= 3:
best_score = score
matched = product
results.append({
'input': item,
'matched_product': matched,
'confidence': best_score / 5
})
return jsonify({'code': 0, 'results': results})在后期新增 initial(期初)类型时,直接 ALTER TABLE 修改 ENUM 失败。
WorkBuddy 的解决方案:
-- 先修改为 VARCHAR,再约束
ALTER TABLE inventory_transactions
MODIFY COLUMN reference_type VARCHAR(20) NOT NULL;
-- 添加 CHECK 约束
ALTER TABLE inventory_transactions
ADD CONSTRAINT chk_ref_type
CHECK (reference_type IN ('purchase', 'sale', 'initial', 'adjustment'));导入了 1,818,830.55 元,但账面显示 1,705,583.95 元,相差约 11 万。
排查过程:
教训: 期初导入一定要在导入后立刻跑对账 SQL 验证总金额。
有些商品因为期初录入不完整,出库时会出现负库存。
我们的策略: 对于工程类材料,允许负库存放行(不阻断业务),但在台账中用红色标注,月底统一核查。
WorkBuddy 在 API 层实现了这个逻辑:
# 负库存放行策略
def check_stock_before_sale(product_id, quantity, company_id):
current_stock = get_current_stock(product_id, company_id)
if current_stock < quantity:
# 不阻断,记录警告
logger.warning(f"商品 {product_id} 库存不足: 当前 {current_stock},出库 {quantity}")
return {'allow': True, 'warning': f'库存不足,当前余量 {current_stock}'}
return {'allow': True, 'warning': None}经过约 10 天的开发与调试,系统实现了以下核心功能:
操作 | 原来耗时 | 现在耗时 | 提升 |
|---|---|---|---|
录入一张采购发票 | 5 分钟 | 30 秒 | 10 倍 |
月度库存核对 | 半天 | 10 分钟 | 30 倍 |
多公司账务切换 | 人工切换文件 | 显著提升 | 显著提升 |
1. 描述问题而不是描述代码
差的指令:
写一个 Python 函数 insert_into_db(data)好的指令:
我需要在采购发票保存时,把每条商品明细同时写入库存流水表,
要求事务原子性,如果任何一条失败则全部回滚,
数据库是 MySQL 8,用 PyMySQL 连接池。2. 给出业务约束而不是技术约束
差的指令:
添加一个 if 判断好的指令:
同一张发票号,在同一个公司下只能录入一次,
如果重复提交要返回友好的错误提示(不是 500 报错)3. 分阶段迭代,不要一次要求太多
先让系统能跑起来,再逐步加功能。每次只描述当前最重要的那一个需求。
# 安装 Python 依赖
pip install flask pymysql pyjwt python-dotenv openpyxl aip
# 创建 MySQL 数据库
mysql -u root -p
CREATE DATABASE inventory_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# 初始化表结构
python backend/create_tables.py# backend/config.py
DB_CONFIG = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'password': 'your_password',
'database': 'inventory_db',
'charset': 'utf8mb4'
}
JWT_SECRET = 'your-secret-key-change-in-production'
BAIDU_OCR_APP_ID = 'your_app_id'
BAIDU_OCR_API_KEY = 'your_api_key'
BAIDU_OCR_SECRET_KEY = 'your_secret_key'# start_backend.py — WorkBuddy 生成的启动脚本
import subprocess
import sys
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
print("启动后端服务...")
process = subprocess.Popen(
[sys.executable, 'app.py'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
print(f"后端服务已启动,PID: {process.pid}")
print("访问地址:http://127.0.0.1:5000")# 启动前端
cd frontend && npm run dev
# 访问系统
open http://localhost:5173这次用 WorkBuddy 开发进销存系统的经历,改变了我对"普通人能不能做软件开发"这个问题的看法。
我的收获:
如果你也有一个"一直想做但不会写代码"的业务系统,不妨试试 WorkBuddy。你需要的不是会写代码,而是清晰地知道自己想要什么。
接口 | 方法 | 说明 |
|---|---|---|
| POST | 用户登录,返回 JWT Token |
| POST | 创建采购发票并入库 |
| POST | 创建销售发票并出库 |
| POST | Excel 期初库存导入 |
| GET | 查询期初导入状态 |
| GET | 下载期初导入模板 |
| GET | 查询库存台账(含合计) |
| POST | OCR 商品批量模糊匹配 |
| GET/POST | 商品档案管理 |
| GET/POST | 供应商管理 |
| GET/POST | 客户管理 |
本文代码均为示意,实际项目中请注意:生产环境 JWT Secret 须使用强随机字符串,数据库密码不要硬编码在代码中,建议使用 .env 文件管理敏感配置。
如有问题欢迎在评论区交流,一起探讨 AI 辅助开发的更多可能。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。