@@ -5,6 +5,12 @@
// go run cmd/seed/main.go # 写入数据(已存在则跳过)
// go run cmd/seed/main.go --reset # 删表重建 + 写入数据
// go run cmd/seed/main.go --clear # 清空业务数据 + 重新写入(保留表结构)
//
// 数据遵循「序列号模型」规范(见 CLAUDE.md 数据模型铁律):
// - 入库每录一行 = 新建一个独立 product(唯一商品编号),绝不按名称复用;
// - 入库/出库明细、库存均带快照列(product_code/name/series/spec…);
// - 审核生成库存时关联 stock_in_item_id、拷贝 unit_price/批次/生产日期快照;
// - 出库记录 sale_price(售价),应收(total_amount)按 售价×数量。
package main
import (
@@ -13,6 +19,7 @@ import (
"os"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
@@ -20,6 +27,7 @@ import (
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/internal/util"
)
var allModels = [ ] any {
@@ -111,6 +119,7 @@ func main() {
now := time . Now ( )
today := time . Date ( now . Year ( ) , now . Month ( ) , now . Day ( ) , 0 , 0 , 0 , 0 , now . Location ( ) )
d := func ( daysAgo int ) model . Date { return model . Date { Time : today . AddDate ( 0 , 0 , - daysAgo ) } }
dptr := func ( daysAgo int ) * model . Date { dd := model . Date { Time : today . AddDate ( 0 , 0 , - daysAgo ) } ; return & dd }
ptrT := func ( t time . Time ) * time . Time { return & t }
// ═══════════════════════════════════════════════════
@@ -180,41 +189,8 @@ func main() {
} ) . ( model . ProductCategory )
// ═══════════════════════════════════════════════════
// 商品
// ═══════════════════════════════════════════════════
type prodSeed struct {
code , barcode , name , series , spec , unit , brand string
catID * uint64
purchase , sale float64
minStock int
remark string
}
prodSeeds := [ ] prodSeed {
{ "MT-001" , "6901234567890" , "飞天茅台 53度" , "茅台" , "500ml/瓶" , "瓶" , "贵州茅台" , & catBaijiu . ID , 2350 , 2800 , 10 , "酱香型白酒,53度,飞天系列" } ,
{ "WLY-001" , "6902345678901" , "五粮液 52度" , "五粮液" , "500ml/瓶" , "瓶" , "宜宾五粮液" , & catBaijiu . ID , 950 , 1200 , 6 , "浓香型白酒,52度,普五系列" } ,
{ "YH-001" , "6903456789012" , "洋河梦之蓝 M6" , "洋河" , "500ml/瓶" , "瓶" , "江苏洋河" , & catBaijiu . ID , 560 , 680 , 6 , "浓香型,绵柔苏酒代表" } ,
{ "LZ-001" , "6904567890123" , "泸州老窖 特曲" , "泸州老窖" , "500ml/瓶" , "瓶" , "泸州老窖" , & catBaijiu . ID , 420 , 520 , 6 , "浓香鼻祖,特曲系列" } ,
{ "JNC-001" , "6905678901234" , "剑南春 水晶剑" , "剑南春" , "500ml/瓶" , "瓶" , "剑南春" , & catBaijiu . ID , 390 , 480 , 6 , "浓香型,绵竹名酒" } ,
{ "LJ-001" , "6906789012345" , "郎酒 红花郎10" , "郎酒" , "500ml/瓶" , "瓶" , "古蔺郎酒" , & catBaijiu . ID , 320 , 420 , 6 , "酱香型,赤水河畔酿造" } ,
{ "LF-001" , "3760093550058" , "拉菲古堡 2018" , "波尔多" , "750ml/瓶" , "瓶" , "Château Lafite" , & catImport . ID , 3800 , 5200 , 3 , "波尔多一级名庄,2018年份" } ,
{ "RTM-001" , "3021691010008" , "人头马 VSOP" , "人头马" , "700ml/瓶" , "瓶" , "Rémy Martin" , & catImport . ID , 480 , 680 , 3 , "法国干邑,VSOP级别" } ,
}
var prods [ ] model . Product
for _ , s := range prodSeeds {
p := upsert ( db , & model . Product { } , "shop_id = ? AND code = ?" , shop . ID , s . code , func ( ) any {
return & model . Product {
TenantBase : model . TenantBase { ShopID : shop . ID } ,
Code : s . code , Barcode : s . barcode , Name : s . name ,
Series : s . series , Spec : s . spec , Unit : s . unit , Brand : s . brand ,
CategoryID : s . catID , PurchasePrice : s . purchase , SalePrice : s . sale ,
MinStock : s . minStock , Remark : s . remark ,
}
} ) . ( model . Product )
prods = append ( prods , p )
}
// ═══════════════════════════════════════════════════
// 商品名称/系列/规格选项
// 商品基础数据字典(品牌/型号/版本 = 名称/系列/规格选项)
// 序列号模型下:product 由入库逐行生成,字典只是录入时的可选项来源。
// ═══════════════════════════════════════════════════
type dimSeed struct { code , name string }
@@ -243,7 +219,10 @@ func main() {
}
}
for _ , s := range [ ] struct { code , name string ; quantity int } {
for _ , s := range [ ] struct {
code , name string
quantity int
} {
{ "GG001" , "500ml/瓶" , 1 } , { "GG002" , "750ml/瓶" , 1 } , { "GG003" , "700ml/瓶" , 1 } ,
} {
var opt model . ProductSpecOption
@@ -259,6 +238,22 @@ func main() {
}
}
// 商品模板:仅作为入库逐行生成 product 的来源,本身不直接落 products 表。
cat := func ( c * model . ProductCategory ) * uint64 { id := c . ID ; return & id }
catalog := [ ] goods {
{ "飞天茅台 53度" , "茅台" , "500ml/瓶" , "瓶" , "贵州茅台" , cat ( & catBaijiu ) , 2350 , 2800 , 10 } ,
{ "五粮液 52度" , "五粮液" , "500ml/瓶" , "瓶" , "宜宾五粮液" , cat ( & catBaijiu ) , 950 , 1200 , 6 } ,
{ "洋河梦之蓝 M6" , "洋河" , "500ml/瓶" , "瓶" , "江苏洋河" , cat ( & catBaijiu ) , 560 , 680 , 6 } ,
{ "泸州老窖 特曲" , "泸州老窖" , "500ml/瓶" , "瓶" , "泸州老窖" , cat ( & catBaijiu ) , 420 , 520 , 6 } ,
{ "剑南春 水晶剑" , "剑南春" , "500ml/瓶" , "瓶" , "剑南春" , cat ( & catBaijiu ) , 390 , 480 , 6 } ,
{ "郎酒 红花郎10" , "郎酒" , "500ml/瓶" , "瓶" , "古蔺郎酒" , cat ( & catBaijiu ) , 320 , 420 , 6 } ,
{ "拉菲古堡 2018" , "波尔多" , "750ml/瓶" , "瓶" , "Château Lafite" , cat ( & catImport ) , 3800 , 5200 , 3 } ,
{ "人头马 VSOP" , "人头马" , "700ml/瓶" , "瓶" , "Rémy Martin" , cat ( & catImport ) , 480 , 680 , 3 } ,
}
// 商品编码顺序生成器(序列号:每入库一行发一个唯一编号 P0001、P0002…)
codeSeq := 0
nextCode := func ( ) string { codeSeq ++ ; return fmt . Sprintf ( "P%04d" , codeSeq ) }
// ═══════════════════════════════════════════════════
// 往来单位
// ═══════════════════════════════════════════════════
@@ -317,110 +312,100 @@ func main() {
upsertNumberRule ( db , shop . ID , "stock_out" , "CK" , 3 )
upsertNumberRule ( db , shop . ID , "inventory_check" , "PD" , 1 )
mk := func ( db * gorm . DB ) * seeder {
return & seeder { db : db , shopID : shop . ID , catalog : catalog , nextCode : nextCode , supplier : partners }
}
sd := mk ( db )
// ═══════════════════════════════════════════════════
// 入库单
// 入库单(每行新建独立 product + 快照)
// ═══════════════════════════════════════════════════
// #1 已审核:茅台+五粮液,主仓库,茅台供应商
in1 := createStockInOrder ( db , shop . ID , wh1 . ID , admin . ID , partners [ "SUP001" ] . ID ,
"RK20260401001" , d ( 6 ) , "approved" , "purchase" ,
"茅台系列首批入库,含飞天茅台及五粮液" ,
[ ] inItemSeed {
{ prods [ 0 ] . ID , 120 , 2350 , "BATCH-MT -2024001" , "飞天茅台 2024年第一 批次"} ,
{ prods [ 1 ] . ID , 60 , 950 , "BATCH-WLY-2024001" , "五粮液普五 2024年批次" } ,
in1 := sd . createStockIn ( wh1 . ID , wh1 . Name , admin . ID , partners [ "SUP001" ] . ID ,
"RK20260401001" , d ( 6 ) , "approved" , "purchase" , "茅台系列首批入库,含飞天茅台及五粮液" ,
[ ] inLine {
{ 0 , 120 , 2350 , "BATCH-MT-2024001" , dptr ( 40 ) , "飞天茅台 2024年第一批次" } ,
{ 1 , 60 , 950 , "BATCH-WLY -2024001" , dptr ( 50 ) , "五粮液普五 2024年批次"} ,
} , admin . ID , ptrT ( today . AddDate ( 0 , 0 , - 5 ) ) )
// #2 已审核:洋河+泸州+剑南春,主仓库,洋河供应商
in2 := createStockInOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "SUP002" ] . ID ,
"RK20260403001" , d ( 4 ) , "approved" , "purchase" ,
"浓香型白酒补货入库" ,
[ ] inItemSeed {
{ prods [ 2 ] . ID , 48 , 560 , "BATCH-YH -2024003 " , "洋河梦之蓝 M6 3月 批次"} ,
{ prods [ 3 ] . ID , 36 , 420 , "BATCH-LZ -2024002 " , "泸州老窖特曲 春季批次 "} ,
{ prods [ 4 ] . ID , 24 , 390 , "BATCH-JNC-2024001" , "剑南春水晶剑 首批" } ,
in2 := sd . createStockIn ( wh1 . ID , wh1 . Name , operator . ID , partners [ "SUP002" ] . ID ,
"RK20260403001" , d ( 4 ) , "approved" , "purchase" , "浓香型白酒补货入库" ,
[ ] inLine {
{ 2 , 48 , 560 , "BATCH-YH-2024003" , dptr ( 60 ) , "洋河梦之蓝 M6 3月批次" } ,
{ 3 , 36 , 420 , "BATCH-LZ -2024002 " , dptr ( 70 ) , "泸州老窖特曲 春季 批次"} ,
{ 4 , 24 , 390 , "BATCH-JNC -2024001 " , dptr ( 45 ) , "剑南春水晶剑 首批 "} ,
} , admin . ID , ptrT ( today . AddDate ( 0 , 0 , - 3 ) ) )
// #3 已审核:进口酒,进口酒专库,茅台 供应商(代理进口)
in3 := createStockInOrder ( db , shop . ID , wh2 . ID , operator . ID , partners [ "SUP004" ] . ID ,
"RK20260404001" , d ( 3 ) , "approved" , "purchase" ,
"进口烈酒专库入库,含拉菲及人头马" ,
[ ] inItemSeed {
{ prods [ 6 ] . ID , 24 , 3800 , "BATCH-LF -2018 001" , "拉菲古堡2018,原箱 "} ,
{ prods [ 7 ] . ID , 36 , 480 , "BATCH-RTM-2024001" , "人头马VSOP, 2024年进口" } ,
// #3 已审核:进口酒,进口酒专库,郎酒 供应商(代理进口)
in3 := sd . createStockIn ( wh2 . ID , wh2 . Name , operator . ID , partners [ "SUP004" ] . ID ,
"RK20260404001" , d ( 3 ) , "approved" , "purchase" , "进口烈酒专库入库,含拉菲及人头马" ,
[ ] inLine {
{ 6 , 24 , 3800 , "BATCH-LF-2018001" , dptr ( 400 ) , "拉菲古堡2018,原箱" } ,
{ 7 , 36 , 480 , "BATCH-RTM -2024 001" , dptr ( 90 ) , "人头马VSOP, 2024年进口 "} ,
} , admin . ID , ptrT ( today . AddDate ( 0 , 0 , - 2 ) ) )
// #4 待审核:郎酒补货
in4 := createStockInOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "SUP004" ] . ID ,
"RK20260406001" , d ( 1 ) , "pending" , "purchase" ,
"郎酒红花郎补货,待仓库管理员审核" ,
[ ] inItemSeed {
{ prods [ 5 ] . ID , 60 , 320 , "BATCH-LJ-2024002" , "郎酒红花郎10 第二批次" } ,
// #4 待审核:郎酒补货(仅建单+建 product,不生成库存)
in4 := sd . createStockIn ( wh1 . ID , wh1 . Name , operator . ID , partners [ "SUP004" ] . ID ,
"RK20260406001" , d ( 1 ) , "pending" , "purchase" , "郎酒红花郎补货,待仓库管理员审核" ,
[ ] inLine {
{ 5 , 60 , 320 , "BATCH-LJ-2024002" , dptr ( 30 ) , "郎酒红花郎10 第二批次" } ,
} , 0 , nil )
// #5 草稿:追加茅台
in5 := createStockInOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "SUP001" ] . ID ,
"RK20260407001" , d ( 0 ) , "draft" , "purchase" ,
"茅台追加订货,草稿中" ,
[ ] inItemSeed {
{ prods [ 0 ] . ID , 60 , 2380 , "BATCH-MT-2024002" , "飞天茅台 第二批次" } ,
sd . createStockIn ( wh1 . ID , wh1 . Name , operator . ID , partners [ "SUP001" ] . ID ,
"RK20260407001" , d ( 0 ) , "draft" , "purchase" , "茅台追加订货,草稿中" ,
[ ] inLine {
{ 0 , 60 , 2380 , "BATCH-MT-2024002" , dptr ( 20 ) , "飞天茅台 第二批次" } ,
} , 0 , nil )
_ = in4
_ = in5
// 根据已审核入库单更新库存
updateInventoryBatch ( db , shop . ID , wh1 . ID , admin . ID , in1 )
updateInventoryBatch ( db , shop . ID , wh1 . ID , admin . ID , in2 )
updateInventoryBatch ( db , shop . ID , wh2 . ID , admin . ID , in3 )
// 已审核入库单 → 生成库存(带快照 + 关联 stock_in_item_id)
sd . applyStockInToInventory ( in1 )
sd . applyStockInToInventory ( in2 )
sd . applyStockInToInventory ( in3 )
// ═══════════════════════════════════════════════════
// 出库单
// 出库单(引用入库已建 product,记 sale_price,应收按售价)
// ═══════════════════════════════════════════════════
// #1 已审核:北京君悦大酒店,主仓库
out1 := createStockOutOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "CUS001" ] . ID ,
"CK20260404001" , d ( 3 ) , "approved" , "sale" ,
"北京君悦大酒店 4月份定期供货" ,
[ ] outItemSeed {
{ prods [ 0 ] . ID , 12 , 2800 , "飞天茅台 12瓶,整箱出库 " } ,
{ prods [ 1 ] . ID , 6 , 1200 , "五粮液普五 6 瓶" } ,
{ prods [ 2 ] . ID , 12 , 680 , "洋河梦之蓝 M6 12瓶" } ,
out1 := sd . createStockOut ( wh1 . ID , operator . ID , partners [ "CUS001" ] . ID ,
"CK20260404001" , d ( 3 ) , "approved" , "sale" , "北京君悦大酒店 4月份定期供货" ,
[ ] outLine {
{ in1 . products [ 0 ] , 12 , 2800 , "飞天茅台 12瓶,整箱出库" } ,
{ in1 . products [ 1 ] , 6 , 1200 , "五粮液普五 6瓶 " } ,
{ in2 . products [ 0 ] , 12 , 680 , "洋河梦之蓝 M6 12 瓶" } ,
} , admin . ID , ptrT ( today . AddDate ( 0 , 0 , - 2 ) ) )
// #2 已审核:上海外滩华尔道夫,进口酒专库
out2 := createStockOutOrder ( db , shop . ID , wh2 . ID , operator . ID , partners [ "CUS002" ] . ID ,
"CK20260405001" , d ( 2 ) , "approved" , "sale" ,
"上海外滩华尔道夫 进口酒专属采购" ,
[ ] outItemSeed {
{ prods [ 6 ] . ID , 6 , 5200 , "拉菲古堡2018 6 瓶" } ,
{ prods [ 7 ] . ID , 12 , 680 , "人头马VSOP 12瓶" } ,
out2 := sd . createStockOut ( wh2 . ID , operator . ID , partners [ "CUS002" ] . ID ,
"CK20260405001" , d ( 2 ) , "approved" , "sale" , "上海外滩华尔道夫 进口酒专属采购" ,
[ ] outLine {
{ in3 . products [ 0 ] , 6 , 5200 , "拉菲古堡2018 6瓶" } ,
{ in3 . products [ 1 ] , 12 , 680 , "人头马VSOP 12 瓶" } ,
} , admin . ID , ptrT ( today . AddDate ( 0 , 0 , - 1 ) ) )
// #3 待审核:广州白天鹅宾馆
out3 := createStockOutOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "CUS003" ] . ID ,
"CK20260406001" , d ( 1 ) , "pending" , "sale" ,
"广州白天鹅宾馆 月度供货申请" ,
[ ] outItemSeed {
{ prods [ 3 ] . ID , 24 , 520 , "泸州老窖特曲 24 瓶" } ,
{ prods [ 4 ] . ID , 12 , 480 , "剑南春水晶剑 12瓶" } ,
{ prods [ 5 ] . ID , 12 , 420 , "郎酒红花郎10 12瓶" } ,
sd . createStockOut ( wh1 . ID , operator . ID , partners [ "CUS003" ] . ID ,
"CK20260406001" , d ( 1 ) , "pending" , "sale" , "广州白天鹅宾馆 月度供货申请" ,
[ ] outLine {
{ in2 . products [ 1 ] , 24 , 520 , "泸州老窖特曲 24瓶" } ,
{ in2 . products [ 2 ] , 12 , 480 , "剑南春水晶剑 12 瓶" } ,
{ in4 . products [ 0 ] , 12 , 420 , "郎酒红花郎10 12瓶" } ,
} , 0 , nil )
// #4 草稿:君悦追加
out4 := createStockOutOrder ( db , shop . ID , wh1 . ID , operator . ID , partners [ "CUS001" ] . ID ,
"CK20260407001" , d ( 0 ) , "draft" , "sale" ,
"北京君悦追加订单,草稿中" ,
[ ] outItemSeed {
{ prods [ 0 ] . ID , 6 , 2800 , "飞天茅台 追加 6瓶" } ,
sd . createStockOut ( wh1 . ID , operator . ID , partners [ "CUS001" ] . ID ,
"CK20260407001" , d ( 0 ) , "draft" , "sale" , "北京君悦追加订单,草稿中" ,
[ ] outLine {
{ in1 . products [ 0 ] , 6 , 2800 , "飞天茅台 追加 6瓶" } ,
} , 0 , nil )
_ = out3
_ = out4
// 根据已审核出库单减库存
deductInventoryBatch ( db , shop . ID , wh1 . ID , admin . ID , out1 )
deductInventoryBatch ( db , shop . ID , wh2 . ID , admin . ID , out2 )
// 已审核出库单 → 扣减库存(FIFO)
sd . deductStockOut ( wh1 . ID , admin . ID , out1 )
sd . deductStockOut ( wh2 . ID , admin . ID , out2 )
// ═══════════════════════════════════════════════════
// 财务记录(与已审核出库单对应的应收款)
// 财务记录(与已审核出库单对应的应收款,按售价口径 )
// ═══════════════════════════════════════════════════
createFinanceRecord ( db , shop . ID , partners [ "CUS001" ] . ID , admin . ID ,
"receivable" , out1 . order . TotalAmount , out1 . order . TotalAmount , "stock_out" , out1 . order . ID ,
@@ -446,44 +431,313 @@ func main() {
fmt . Println ( " operator / password123 (操作员)" )
fmt . Println ( " test / password123 (只读)" )
fmt . Println ( )
fmt . Println ( " 数据概览:" )
fmt . Println ( " 数据概览(序列号模型:入库逐行建独立 product) : " )
fmt . Printf ( " 仓库:%s / %s\n" , wh1 . Name , wh2 . Name )
fmt . Printf ( " 商品分类:白酒 / 进口烈酒,共 %d 种商品 \n" , len ( prods ) )
fmt . Printf ( " 入库逐行生成 product:%d 个独立商品编号 \n" , codeSeq )
fmt . Println ( " 往来单位:4 供应商 + 3 客户" )
fmt . Println ( " 入库单:5 张(2 已审核 / 1待审核 / 1草稿 / 1进口专库已审核 ) " )
fmt . Println ( " 出库单:4 张(2已审核 / 1待审核 / 1草稿)" )
fmt . Println ( " 入库单:5 张(3 已审核 / 1待审核 / 1草稿)" )
fmt . Println ( " 出库单:4 张(2已审核 / 1待审核 / 1草稿),记 sale_price,应收按售价 " )
fmt . Println ( " 财务记录:2 应收 + 1 收款" )
fmt . Println ( "═══════════════════════════════════════════" )
}
// ── 辅助类型 ────────────────────────────────────────────
// ── 数据模板 ────────────────────────────────────────────
type inItemSeed struct {
productID uint64
qty float64
price float64
batchNo string
remark string
// goods 商品模板:入库逐行据此 new 独立 product,模板本身不落库。
type goods struct {
name , series , spec , unit , brand string
cat * uint64
purchase , sale float64
minStock int
}
type outItemSeed struct {
productID uint64
qty float64
price float64
// inLine 入库行:g=catalog 下标,qty/price,批次/生产日期/备注。
type inLine struct {
g int
qty , price float64
batch string
prodDate * model . Date
remark string
}
// outLine 出库行:引用入库已建 product, salePrice=售价。
type outLine struct {
product model . Product
qty , sale float64
remark string
}
type stockInResult struct {
order model . StockInOrder
items [ ] inItemSeed
order model . StockInOrder
items [ ] model . StockInItem
products [ ] model . Product
}
type stockOutResult struct {
order model . StockOutOrder
items [ ] outItemSeed
items [ ] model . StockOutItem
}
// ── 核心 upsert ────────────────────────────────────────────
// seeder 收敛公共上下文(db/shop/商品模板/编码序列/供应商)。
type seeder struct {
db * gorm . DB
shopID uint64
catalog [ ] goods
nextCode func ( ) string
supplier map [ string ] model . Partner
}
// ── 入库单(每行新建独立 product + 写快照) ───────────────
func ( s * seeder ) createStockIn (
whID uint64 , whName string , opID , partnerID uint64 ,
orderNo string , date model . Date , status , orderType , remark string ,
lines [ ] inLine , reviewerID uint64 , reviewedAt * time . Time ,
) stockInResult {
var o model . StockInOrder
if s . db . Where ( "shop_id = ? AND order_no = ?" , s . shopID , orderNo ) . First ( & o ) . Error == nil {
fmt . Printf ( "⏭ 入库单:%s\n" , orderNo )
return s . loadStockIn ( o )
}
o = model . StockInOrder {
TenantBase : model . TenantBase { ShopID : s . shopID } ,
OrderNo : orderNo ,
Type : orderType ,
WarehouseID : whID ,
PartnerID : & partnerID ,
OperatorID : opID ,
Status : status ,
OrderDate : date ,
Remark : remark ,
}
if reviewerID > 0 {
o . ReviewerID = & reviewerID
o . ReviewedAt = reviewedAt
}
s . db . Create ( & o )
var (
total float64
items [ ] model . StockInItem
prods [ ] model . Product
)
for _ , ln := range lines {
g := s . catalog [ ln . g ]
code := s . nextCode ( )
full , initials := util . ToPinyin ( g . name )
// 铁律:入库每行 = 新建一个独立 product(唯一编号),不按名称复用
p := model . Product {
TenantBase : model . TenantBase { ShopID : s . shopID } ,
PublicID : uuid . New ( ) . String ( ) ,
Code : code ,
Name : g . name ,
Series : g . series ,
Spec : g . spec ,
Unit : g . unit ,
Brand : g . brand ,
CategoryID : g . cat ,
PurchasePrice : ln . price ,
SalePrice : g . sale ,
MinStock : g . minStock ,
BatchNo : ln . batch ,
ProductionDate : ln . prodDate ,
NamePinyin : full ,
NameInitials : initials ,
}
s . db . Create ( & p )
item := model . StockInItem {
OrderID : o . ID ,
ShopID : s . shopID ,
ProductID : p . ID ,
ProductCode : code ,
ProductName : g . name ,
Series : g . series ,
Spec : g . spec ,
Quantity : ln . qty ,
UnitPrice : ln . price ,
TotalPrice : ln . qty * ln . price ,
BatchNo : ln . batch ,
ProductionDate : ln . prodDate ,
Remark : ln . remark ,
}
s . db . Create ( & item )
total += item . TotalPrice
items = append ( items , item )
prods = append ( prods , p )
}
s . db . Model ( & o ) . Update ( "total_amount" , total )
o . TotalAmount = total
fmt . Printf ( "✅ 入库单:%s [%s] 供应商ID=%d 仓库=%s 行数=%d 金额=%.0f\n" ,
orderNo , status , partnerID , whName , len ( lines ) , total )
return stockInResult { order : o , items : items , products : prods }
}
// loadStockIn 单据已存在时,回读其明细与 product,供出库行引用(保证重复执行可用)。
func ( s * seeder ) loadStockIn ( o model . StockInOrder ) stockInResult {
var items [ ] model . StockInItem
s . db . Where ( "order_id = ?" , o . ID ) . Order ( "id ASC" ) . Find ( & items )
prods := make ( [ ] model . Product , 0 , len ( items ) )
for _ , it := range items {
var p model . Product
s . db . First ( & p , it . ProductID )
prods = append ( prods , p )
}
return stockInResult { order : o , items : items , products : prods }
}
// applyStockInToInventory 已审核入库单 → 逐明细生成库存行(带快照 + 关联 stock_in_item_id)。
func ( s * seeder ) applyStockInToInventory ( r stockInResult ) {
for i := range r . items {
it := r . items [ i ]
var unit string
if i < len ( r . products ) {
unit = r . products [ i ] . Unit
}
var qtyBefore float64
s . db . Model ( & model . Inventory { } ) .
Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL" , s . shopID , r . order . WarehouseID , it . ProductID ) .
Select ( "COALESCE(SUM(quantity), 0)" ) . Scan ( & qtyBefore )
whIDCopy := r . order . WarehouseID
pidCopy := it . ProductID
itemIDCopy := it . ID
price := it . UnitPrice
inv := model . Inventory {
ShopID : s . shopID ,
WarehouseID : & whIDCopy ,
ProductID : & pidCopy ,
StockInItemID : & itemIDCopy ,
Quantity : it . Quantity ,
ProductCode : it . ProductCode ,
ProductName : it . ProductName ,
Series : it . Series ,
Spec : it . Spec ,
Unit : unit ,
UnitPrice : & price ,
BatchNo : it . BatchNo ,
ProductionDate : it . ProductionDate ,
}
s . db . Create ( & inv )
opID := r . order . OperatorID
s . db . Create ( & model . InventoryLog {
ShopID : s . shopID , WarehouseID : r . order . WarehouseID , ProductID : it . ProductID ,
Direction : "in" , Quantity : it . Quantity , QtyBefore : qtyBefore , QtyAfter : qtyBefore + it . Quantity ,
RefType : "stock_in" , RefID : r . order . ID , OperatorID : & opID ,
} )
}
}
// ── 出库单(记 sale_price,应收按售价) ───────────────────
func ( s * seeder ) createStockOut (
whID , opID , partnerID uint64 ,
orderNo string , date model . Date , status , orderType , remark string ,
lines [ ] outLine , reviewerID uint64 , reviewedAt * time . Time ,
) stockOutResult {
var o model . StockOutOrder
if s . db . Where ( "shop_id = ? AND order_no = ?" , s . shopID , orderNo ) . First ( & o ) . Error == nil {
fmt . Printf ( "⏭ 出库单:%s\n" , orderNo )
var items [ ] model . StockOutItem
s . db . Where ( "order_id = ?" , o . ID ) . Order ( "id ASC" ) . Find ( & items )
return stockOutResult { order : o , items : items }
}
o = model . StockOutOrder {
TenantBase : model . TenantBase { ShopID : s . shopID } ,
OrderNo : orderNo ,
Type : orderType ,
WarehouseID : whID ,
PartnerID : & partnerID ,
OperatorID : opID ,
Status : status ,
OrderDate : date ,
Remark : remark ,
}
if reviewerID > 0 {
o . ReviewerID = & reviewerID
o . ReviewedAt = reviewedAt
}
s . db . Create ( & o )
var (
total float64 // 应收 = Σ 售价×数量
items [ ] model . StockOutItem
)
for _ , ln := range lines {
p := ln . product
item := model . StockOutItem {
OrderID : o . ID ,
ShopID : s . shopID ,
ProductID : p . ID ,
ProductCode : p . Code ,
ProductName : p . Name ,
Series : p . Series ,
Spec : p . Spec ,
Quantity : ln . qty ,
UnitPrice : p . PurchasePrice , // 成本单价(快照)
SalePrice : ln . sale , // 售价
TotalPrice : ln . qty * p . PurchasePrice , // 成本小计
BatchNo : p . BatchNo ,
Remark : ln . remark ,
}
s . db . Create ( & item )
total += ln . qty * ln . sale
items = append ( items , item )
}
s . db . Model ( & o ) . Update ( "total_amount" , total )
o . TotalAmount = total
fmt . Printf ( "✅ 出库单:%s [%s] 客户ID=%d 仓库ID=%d 行数=%d 应收=%.0f\n" ,
orderNo , status , partnerID , whID , len ( lines ) , total )
return stockOutResult { order : o , items : items }
}
// deductStockOut 已审核出库单 → FIFO 扣减库存 + 写流水。
func ( s * seeder ) deductStockOut ( whID , opID uint64 , r stockOutResult ) {
now := time . Now ( )
for _ , it := range r . items {
var qtyBefore float64
s . db . Model ( & model . Inventory { } ) .
Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL" , s . shopID , whID , it . ProductID ) .
Select ( "COALESCE(SUM(quantity), 0)" ) . Scan ( & qtyBefore )
var batches [ ] model . Inventory
s . db . Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND quantity > 0 AND deleted_at IS NULL" ,
s . shopID , whID , it . ProductID ) .
Order ( "created_at ASC" ) . Find ( & batches )
remaining := it . Quantity
for i := range batches {
if remaining <= 0 {
break
}
b := & batches [ i ]
if b . Quantity <= remaining {
remaining -= b . Quantity
s . db . Model ( b ) . Updates ( map [ string ] interface { } { "quantity" : 0 , "deleted_at" : now } )
} else {
s . db . Model ( b ) . Update ( "quantity" , gorm . Expr ( "quantity - ?" , remaining ) )
remaining = 0
}
}
after := qtyBefore - it . Quantity
if after < 0 {
after = 0
}
s . db . Create ( & model . InventoryLog {
ShopID : s . shopID , WarehouseID : whID , ProductID : it . ProductID ,
Direction : "out" , Quantity : it . Quantity , QtyBefore : qtyBefore , QtyAfter : after ,
RefType : "stock_out" , RefID : r . order . ID , OperatorID : & opID ,
} )
}
}
// ── 通用 upsert(主数据) ────────────────────────────────
// upsert 通用:查不到就调 factory 建,返回具体值(interface{})
func upsert ( db * gorm . DB , dest any , cond string , args ... any ) any {
@@ -534,16 +788,6 @@ func upsert(db *gorm.DB, dest any, cond string, args ...any) any {
}
fmt . Printf ( "⏭ 分类:%s\n" , v . Name )
return * v
case * model . Product :
found = db . Where ( cond , queryArgs ... ) . First ( v ) . Error == nil
if ! found {
obj := factory ( ) . ( * model . Product )
db . Create ( obj )
fmt . Printf ( "✅ 商品:%s( %s) \n" , obj . Name , obj . Code )
return * obj
}
fmt . Printf ( "⏭ 商品:%s\n" , v . Name )
return * v
case * model . Partner :
found = db . Where ( cond , queryArgs ... ) . First ( v ) . Error == nil
if ! found {
@@ -558,179 +802,6 @@ func upsert(db *gorm.DB, dest any, cond string, args ...any) any {
return nil
}
// ── 入库单 ────────────────────────────────────────────
func createStockInOrder (
db * gorm . DB , shopID , whID , opID , partnerID uint64 ,
orderNo string , date model . Date , status , orderType , remark string ,
items [ ] inItemSeed ,
reviewerID uint64 , reviewedAt * time . Time ,
) stockInResult {
var o model . StockInOrder
if db . Where ( "shop_id = ? AND order_no = ?" , shopID , orderNo ) . First ( & o ) . Error != nil {
o = model . StockInOrder {
TenantBase : model . TenantBase { ShopID : shopID } ,
OrderNo : orderNo ,
Type : orderType ,
WarehouseID : whID ,
PartnerID : & partnerID ,
OperatorID : opID ,
Status : status ,
OrderDate : date ,
Remark : remark ,
}
if reviewerID > 0 {
o . ReviewerID = & reviewerID
o . ReviewedAt = reviewedAt
}
db . Create ( & o )
var total float64
for _ , it := range items {
item := model . StockInItem {
OrderID : o . ID ,
ShopID : shopID ,
ProductID : it . productID ,
Quantity : it . qty ,
UnitPrice : it . price ,
TotalPrice : it . qty * it . price ,
BatchNo : it . batchNo ,
Remark : it . remark ,
}
db . Create ( & item )
total += item . TotalPrice
}
db . Model ( & o ) . Update ( "total_amount" , total )
o . TotalAmount = total
fmt . Printf ( "✅ 入库单:%s [%s] 供应商ID=%d 仓库ID=%d 金额=%.0f\n" ,
orderNo , status , partnerID , whID , total )
} else {
fmt . Printf ( "⏭ 入库单:%s\n" , orderNo )
}
return stockInResult { order : o , items : items }
}
// ── 出库单 ────────────────────────────────────────────
func createStockOutOrder (
db * gorm . DB , shopID , whID , opID , partnerID uint64 ,
orderNo string , date model . Date , status , orderType , remark string ,
items [ ] outItemSeed ,
reviewerID uint64 , reviewedAt * time . Time ,
) stockOutResult {
var o model . StockOutOrder
if db . Where ( "shop_id = ? AND order_no = ?" , shopID , orderNo ) . First ( & o ) . Error != nil {
o = model . StockOutOrder {
TenantBase : model . TenantBase { ShopID : shopID } ,
OrderNo : orderNo ,
Type : orderType ,
WarehouseID : whID ,
PartnerID : & partnerID ,
OperatorID : opID ,
Status : status ,
OrderDate : date ,
Remark : remark ,
}
if reviewerID > 0 {
o . ReviewerID = & reviewerID
o . ReviewedAt = reviewedAt
}
db . Create ( & o )
var total float64
for _ , it := range items {
item := model . StockOutItem {
OrderID : o . ID ,
ShopID : shopID ,
ProductID : it . productID ,
Quantity : it . qty ,
UnitPrice : it . price ,
TotalPrice : it . qty * it . price ,
Remark : it . remark ,
}
db . Create ( & item )
total += item . TotalPrice
}
db . Model ( & o ) . Update ( "total_amount" , total )
o . TotalAmount = total
fmt . Printf ( "✅ 出库单:%s [%s] 客户ID=%d 仓库ID=%d 金额=%.0f\n" ,
orderNo , status , partnerID , whID , total )
} else {
fmt . Printf ( "⏭ 出库单:%s\n" , orderNo )
}
return stockOutResult { order : o , items : items }
}
// ── 库存 ────────────────────────────────────────────
func updateInventoryBatch ( db * gorm . DB , shopID , whID , opID uint64 , r stockInResult ) {
for _ , it := range r . items {
var qtyBefore float64
db . Model ( & model . Inventory { } ) .
Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL" , shopID , whID , it . productID ) .
Select ( "COALESCE(SUM(quantity), 0)" ) . Scan ( & qtyBefore )
productIDCopy := it . productID
whIDCopy := whID
inv := model . Inventory {
ShopID : shopID ,
WarehouseID : & whIDCopy ,
ProductID : & productIDCopy ,
Quantity : it . qty ,
}
db . Create ( & inv )
db . Create ( & model . InventoryLog {
ShopID : shopID , WarehouseID : whID , ProductID : it . productID ,
Direction : "in" , Quantity : it . qty , QtyBefore : qtyBefore , QtyAfter : qtyBefore + it . qty ,
RefType : "stock_in" , RefID : r . order . ID , OperatorID : & opID ,
} )
}
}
func deductInventoryBatch ( db * gorm . DB , shopID , whID , opID uint64 , r stockOutResult ) {
now := time . Now ( )
for _ , it := range r . items {
var qtyBefore float64
db . Model ( & model . Inventory { } ) .
Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL" , shopID , whID , it . productID ) .
Select ( "COALESCE(SUM(quantity), 0)" ) . Scan ( & qtyBefore )
// FIFO deduction
var batches [ ] model . Inventory
db . Where ( "shop_id = ? AND warehouse_id = ? AND product_id = ? AND quantity > 0 AND deleted_at IS NULL" ,
shopID , whID , it . productID ) .
Order ( "created_at ASC" ) . Find ( & batches )
remaining := it . qty
for i := range batches {
if remaining <= 0 {
break
}
b := & batches [ i ]
if b . Quantity <= remaining {
remaining -= b . Quantity
db . Model ( b ) . Updates ( map [ string ] interface { } { "quantity" : 0 , "deleted_at" : now } )
} else {
db . Model ( b ) . Update ( "quantity" , gorm . Expr ( "quantity - ?" , remaining ) )
remaining = 0
}
}
after := qtyBefore - it . qty
if after < 0 {
after = 0
}
db . Create ( & model . InventoryLog {
ShopID : shopID , WarehouseID : whID , ProductID : it . productID ,
Direction : "out" , Quantity : it . qty , QtyBefore : qtyBefore , QtyAfter : after ,
RefType : "stock_out" , RefID : r . order . ID , OperatorID : & opID ,
} )
}
}
// ── 财务记录 ────────────────────────────────────────────
func createFinanceRecord ( db * gorm . DB , shopID , partnerID , opID uint64 ,