f928d5c6a3
- #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>
156 lines
4.6 KiB
Go
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)})
|
|
}
|