Files
wangjia f928d5c6a3 fix(backend): 架构质量改进批次三 (#41-44)
- #41 CheckInventoryAvailability Scan 加错误检查,DB 故障不再误报库存不足
- #42 finance ListRecords 和 inventory Logs 补加 pageSize 上限(500)
- #43 ReconcileInventory 两条 Scan 加错误检查,DB 故障返回 500 而非误导性空结果
- #44 license Info 改用 CountDevices(licenseID) 统计当前 license 绑定数,修复跨 license 计数偏差

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 08:21:42 +08:00

156 lines
4.6 KiB
Go

package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/middleware"
)
type AdminHandler struct{ db *gorm.DB }
func NewAdminHandler(db *gorm.DB) *AdminHandler { return &AdminHandler{db: db} }
// ClearData 清空指定表数据(仅限当前 shop,不影响其他租户)
func (h *AdminHandler) ClearData(c *gin.Context) {
shopID := middleware.GetShopID(c)
var body struct {
Tables []string `json:"tables"`
}
if err := c.ShouldBindJSON(&body); err != nil || len(body.Tables) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "tables is required"})
return
}
// 允许清空的表名白名单(key=前端传入标识,value=DB表名列表)
allowed := map[string][]string{
"stock_in": {"stock_in_items", "stock_in_orders"},
"stock_out": {"stock_out_items", "stock_out_orders"},
"inventory": {"inventory_logs", "inventories"},
"products": {"products"},
"partners": {"partners"},
}
cleared := []string{}
err := h.db.Transaction(func(tx *gorm.DB) error {
for _, t := range body.Tables {
tables, ok := allowed[t]
if !ok {
continue
}
for _, tbl := range tables {
if err := tx.Exec("DELETE FROM "+tbl+" WHERE shop_id = ?", shopID).Error; err != nil {
return err
}
}
cleared = append(cleared, t)
}
return nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"cleared": cleared})
}
// ReconcileInventory 对账:检查 inventories 表当前库存与 inventory_logs 流水之和是否吻合。
// 返回所有差异行,正常情况下 data 为空数组。
// 支持可选 ?shop_id=N 参数(superadmin 可指定任意门店)。
func (h *AdminHandler) ReconcileInventory(c *gin.Context) {
// superadmin 路由,可通过参数指定 shop;不指定则对所有门店执行
var shopFilter *uint64
if s := c.Query("shop_id"); s != "" {
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid shop_id"})
return
}
shopFilter = &v
}
_ = middleware.GetShopID // superadmin 不依赖 JWT shop_id
type row struct {
ShopID uint64 `json:"shop_id"`
WarehouseID uint64 `json:"warehouse_id"`
ProductID uint64 `json:"product_id"`
InvQty float64 `json:"inv_qty"`
LogQty float64 `json:"log_qty"`
Diff float64 `json:"diff"`
}
// inventories 侧:当前各(shop,warehouse,product)库存总量
invQuery := h.db.Table("inventories").
Select("shop_id, warehouse_id, product_id, COALESCE(SUM(quantity),0) AS inv_qty").
Where("deleted_at IS NULL").
Group("shop_id, warehouse_id, product_id")
if shopFilter != nil {
invQuery = invQuery.Where("shop_id = ?", *shopFilter)
}
// inventory_logs 侧:同维度流水净量(in 为正,out 为负)
logQuery := h.db.Table("inventory_logs").
Select("shop_id, warehouse_id, product_id, " +
"COALESCE(SUM(CASE WHEN direction='in' THEN quantity ELSE -quantity END),0) AS log_qty").
Group("shop_id, warehouse_id, product_id")
if shopFilter != nil {
logQuery = logQuery.Where("shop_id = ?", *shopFilter)
}
// 全外连接找差异
type invRow struct {
ShopID uint64 `gorm:"column:shop_id"`
WarehouseID uint64 `gorm:"column:warehouse_id"`
ProductID uint64 `gorm:"column:product_id"`
InvQty float64 `gorm:"column:inv_qty"`
}
type logRow struct {
ShopID uint64 `gorm:"column:shop_id"`
WarehouseID uint64 `gorm:"column:warehouse_id"`
ProductID uint64 `gorm:"column:product_id"`
LogQty float64 `gorm:"column:log_qty"`
}
var invRows []invRow
var logRows []logRow
if err := invQuery.Scan(&invRows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "inventory query failed: " + err.Error()})
return
}
if err := logQuery.Scan(&logRows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "log query failed: " + err.Error()})
return
}
// 合并为 map 再求差
type key struct{ S, W, P uint64 }
m := map[key]*row{}
for _, r := range invRows {
k := key{r.ShopID, r.WarehouseID, r.ProductID}
m[k] = &row{ShopID: r.ShopID, WarehouseID: r.WarehouseID, ProductID: r.ProductID, InvQty: r.InvQty}
}
for _, r := range logRows {
k := key{r.ShopID, r.WarehouseID, r.ProductID}
if _, ok := m[k]; !ok {
m[k] = &row{ShopID: r.ShopID, WarehouseID: r.WarehouseID, ProductID: r.ProductID}
}
m[k].LogQty = r.LogQty
}
diffs := make([]row, 0)
for _, v := range m {
v.Diff = v.InvQty - v.LogQty
if v.Diff < -0.001 || v.Diff > 0.001 {
diffs = append(diffs, *v)
}
}
c.JSON(http.StatusOK, gin.H{"data": diffs, "total": len(diffs)})
}