feat(backend): 出入库汇总近30天滚动窗口 + 跨租户安全加固
- summaryBounds:本月/近30天滚动双口径(stock-in/out Summary ?window=rolling30) - security(SEC-001):新增 ownership.go ensureShopRef 写入侧防线(stock-in/out/finance/ 盘点建单的 warehouse/partner/product 外键归属校验);读取侧全部 Preload 补 shop_id 作用域,finance Summary JOIN 补租户条件;回归测试 CrossTenantRefs - security(SEC-002):release 模式 JWT 密钥为空/默认值时拒绝启动 - gofmt 对齐若干 model/cmd 文件 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
// product_id 指错了。
|
||||
//
|
||||
// 修复(就地、不删数据、幂等):遍历目标门店活跃库存行,对每行:
|
||||
// 1. 按 (shop_id, code=inv.product_code) 找 product;
|
||||
// 2. 找不到就用该行快照新建一个独立 product(保留原始编号);
|
||||
// 3. 把 inv.product_id 重指到它。
|
||||
// 1. 按 (shop_id, code=inv.product_code) 找 product;
|
||||
// 2. 找不到就用该行快照新建一个独立 product(保留原始编号);
|
||||
// 3. 把 inv.product_id 重指到它。
|
||||
//
|
||||
// 用法(在 backend/ 目录下):
|
||||
//
|
||||
|
||||
@@ -4,12 +4,12 @@ import "testing"
|
||||
|
||||
func TestMapStatus(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"已审核": "approved",
|
||||
"待审核": "pending",
|
||||
"被驳回": "rejected",
|
||||
"作废": "rejected",
|
||||
"": "draft",
|
||||
"未知": "draft",
|
||||
"已审核": "approved",
|
||||
"待审核": "pending",
|
||||
"被驳回": "rejected",
|
||||
"作废": "rejected",
|
||||
"": "draft",
|
||||
"未知": "draft",
|
||||
" 已审核 ": "approved",
|
||||
}
|
||||
for in, want := range cases {
|
||||
@@ -21,12 +21,12 @@ func TestMapStatus(t *testing.T) {
|
||||
|
||||
func TestParseFloatLoose(t *testing.T) {
|
||||
cases := map[string]float64{
|
||||
"": 0,
|
||||
" ": 0,
|
||||
"123": 123,
|
||||
"1,234.50": 1234.5,
|
||||
"abc": 0,
|
||||
"-9.9": -9.9,
|
||||
"": 0,
|
||||
" ": 0,
|
||||
"123": 123,
|
||||
"1,234.50": 1234.5,
|
||||
"abc": 0,
|
||||
"-9.9": -9.9,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := parseFloatLoose(in); got != want {
|
||||
|
||||
+24
-24
@@ -142,21 +142,21 @@ func main() {
|
||||
hash := mustHash("password123")
|
||||
admin := upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "admin", func() any {
|
||||
return &model.User{
|
||||
ShopID: shop.ID,
|
||||
ShopID: shop.ID,
|
||||
Username: "admin", PasswordHash: hash, RealName: "张三(管理员)",
|
||||
Phone: "13800000001", Role: "admin", IsActive: true,
|
||||
}
|
||||
}).(model.User)
|
||||
operator := upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "operator", func() any {
|
||||
return &model.User{
|
||||
ShopID: shop.ID,
|
||||
ShopID: shop.ID,
|
||||
Username: "operator", PasswordHash: hash, RealName: "李四(操作员)",
|
||||
Phone: "13800000002", Role: "operator", IsActive: true,
|
||||
}
|
||||
}).(model.User)
|
||||
upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "test", func() any {
|
||||
return &model.User{
|
||||
ShopID: shop.ID,
|
||||
ShopID: shop.ID,
|
||||
Username: "test", PasswordHash: hash, RealName: "王五(只读)",
|
||||
Phone: "13800000003", Role: "readonly", IsActive: true,
|
||||
}
|
||||
@@ -168,13 +168,13 @@ func main() {
|
||||
wh1 := upsert(db, &model.Warehouse{}, "shop_id = ? AND name = ?", shop.ID, "主仓库", func() any {
|
||||
return &model.Warehouse{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Name: "主仓库", Location: "A栋1层东侧", IsDefault: true,
|
||||
Name: "主仓库", Location: "A栋1层东侧", IsDefault: true,
|
||||
}
|
||||
}).(model.Warehouse)
|
||||
wh2 := upsert(db, &model.Warehouse{}, "shop_id = ? AND name = ?", shop.ID, "进口酒专库", func() any {
|
||||
return &model.Warehouse{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Name: "进口酒专库", Location: "B栋2层恒温区", IsDefault: false,
|
||||
Name: "进口酒专库", Location: "B栋2层恒温区", IsDefault: false,
|
||||
}
|
||||
}).(model.Warehouse)
|
||||
|
||||
@@ -259,8 +259,8 @@ func main() {
|
||||
// ═══════════════════════════════════════════════════
|
||||
type partnerSeed struct {
|
||||
code, name, ptype, contact, phone, address, bank string
|
||||
creditLimit float64
|
||||
remark string
|
||||
creditLimit float64
|
||||
remark string
|
||||
}
|
||||
partnerSeeds := []partnerSeed{
|
||||
{"SUP001", "贵州茅台酒股份有限公司", "supplier", "张经理", "0851-22222001",
|
||||
@@ -528,22 +528,22 @@ func (s *seeder) createStockIn(
|
||||
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,
|
||||
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,
|
||||
NamePinyin: full,
|
||||
NameInitials: initials,
|
||||
}
|
||||
s.db.Create(&p)
|
||||
|
||||
@@ -678,8 +678,8 @@ func (s *seeder) createStockOut(
|
||||
Series: p.Series,
|
||||
Spec: p.Spec,
|
||||
Quantity: ln.qty,
|
||||
UnitPrice: p.PurchasePrice, // 成本单价(快照)
|
||||
SalePrice: ln.sale, // 售价
|
||||
UnitPrice: p.PurchasePrice, // 成本单价(快照)
|
||||
SalePrice: ln.sale, // 售价
|
||||
TotalPrice: ln.qty * p.PurchasePrice, // 成本小计
|
||||
BatchNo: p.BatchNo,
|
||||
Remark: ln.remark,
|
||||
|
||||
Reference in New Issue
Block a user