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:
wangjia
2026-07-03 09:57:52 +08:00
parent 69890b9935
commit ca7595b113
22 changed files with 423 additions and 225 deletions
+3 -3
View File
@@ -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/ 目录下):
//
+12 -12
View File
@@ -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
View File
@@ -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,