package controllers import ( "encoding/json" "fmt" "io" "strconv" "strings" "server/models" "server/pkg/identity" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendErpBaseController 进销存交易/档案类接口的公共底座。 // // 沿用 ERP 家族既有约定:控制器自解 Authorization(不依赖全局中间件), // tenant_id 以字符串形式落库,所有查询都必须带 tenant_id 过滤,避免跨租户串数据。 // 响应口径与 backend_erp_customer.go 一致:{code,msg,data} + 分页 {list,total,page,pageSize}。 type BackendErpBaseController struct { beego.Controller } // claims 解析租户端 JWT,仅允许 backend 身份。 func (c *BackendErpBaseController) claims() (*jwtutil.Claims, error) { return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndBackend) } // tid 当前租户ID的字符串口径(ERP 表统一使用) func (c *BackendErpBaseController) tid(cl *jwtutil.Claims) string { return fmt.Sprintf("%d", cl.TenantId) } func (c *BackendErpBaseController) ok(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } func (c *BackendErpBaseController) listOK(list interface{}, total int64, page, pageSize int) { c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize}) } func (c *BackendErpBaseController) fail(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } // denied 鉴权失败统一出口(返回 true 表示已响应,调用方应 return) func (c *BackendErpBaseController) denied(err error) bool { if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return true } return false } func (c *BackendErpBaseController) pathID() uint64 { id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) return id } // pageArgs 分页参数:默认 20,上限 100 func (c *BackendErpBaseController) pageArgs() (int, int) { page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } return page, pageSize } // bindJSON 解析请求体,失败时已响应 func (c *BackendErpBaseController) bindJSON(v interface{}) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.fail(400, 400, "参数错误") return false } if len(strings.TrimSpace(string(raw))) == 0 { c.fail(400, 400, "请求体为空") return false } if err := json.Unmarshal(raw, v); err != nil { c.fail(400, 400, "参数错误: "+err.Error()) return false } return true } func erpUintParam(v string) uint64 { n, _ := strconv.ParseUint(strings.TrimSpace(v), 10, 64) return n } // erpUserOrgID 发起人所属组织,供审批引擎按「部门主管」规则解析审批人 func erpUserOrgID(tid, uid uint64) uint64 { var tu models.SystemTenantUser if err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("uid", uid).Filter("tid", tid). Filter("delete_time__isnull", true).One(&tu); err != nil { return 0 } return tu.OrgID } // erpDocTypes 合法单据类型白名单,防止未知类型绕过效果引擎 var erpDocTypes = map[string]bool{ models.ErpDocPurchaseOrder: true, models.ErpDocPurchaseIn: true, models.ErpDocPurchaseReturn: true, models.ErpDocSalesOrder: true, models.ErpDocSalesOut: true, models.ErpDocSalesReturn: true, models.ErpDocOtherIn: true, models.ErpDocOtherOut: true, models.ErpDocTransfer: true, models.ErpDocStockTake: true, } func erpPartnerLabel(partnerType string) string { if partnerType == "supplier" { return "供应商" } return "客户" } // erpLoadDoc 按租户取单据(所有单据读写入口都先过这一层,保证租户隔离) func erpLoadDoc(docID uint64, tenantID string) (*models.ErpDocument, error) { var doc models.ErpDocument if err := models.Orm.QueryTable(new(models.ErpDocument)). Filter("id", docID).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&doc); err != nil { return nil, err } return &doc, nil } // erpDocItems 取单据明细(按行序) func erpDocItems(o orm.QueryExecutor, docID uint64) []models.ErpDocumentItem { var items []models.ErpDocumentItem _, _ = o.QueryTable(new(models.ErpDocumentItem)). Filter("doc_id", docID).OrderBy("sort_order", "id").All(&items) return items } // erpFillItemStock 给明细行补商品编码/单位与当前可用库存,供开单界面展示。 // fallbackWh:明细行没落仓库时的归属仓(单据表头仓库),否则结存恒查不到、界面显示 0。 func erpFillItemStock(tenantID string, items []models.ErpDocumentItem, fallbackWh uint64) { if len(items) == 0 { return } ids := make([]uint64, 0, len(items)) for _, it := range items { if it.ProductID > 0 { ids = append(ids, it.ProductID) } } var products []models.ErpProduct _, _ = models.Orm.QueryTable(new(models.ErpProduct)). Filter("id__in", ids). All(&products, "id", "product_code", "base_unit", "spec") pmap := make(map[uint64]models.ErpProduct, len(products)) for _, p := range products { pmap[p.ID] = p } var stocks []models.ErpStock _, _ = models.Orm.QueryTable(new(models.ErpStock)). Filter("tenant_id", tenantID).Filter("product_id__in", ids). All(&stocks, "product_id", "warehouse_id", "quantity") type whKey struct{ productID, warehouseID uint64 } qmap := map[whKey]float64{} for _, s := range stocks { qmap[whKey{s.ProductID, s.WarehouseID}] += s.Quantity } for i := range items { if p, hit := pmap[items[i].ProductID]; hit { items[i].ProductCode = p.ProductCode if items[i].BaseUnit == "" { items[i].BaseUnit = p.BaseUnit } } wh := items[i].WarehouseID if wh == 0 { wh = fallbackWh } items[i].StockQty = qmap[whKey{items[i].ProductID, wh}] } } // erpFillDocNames 给单据补仓库名/来源单号,列表与详情共用 func erpFillDocNames(docs []models.ErpDocument) { if len(docs) == 0 { return } whIDs := make([]uint64, 0, len(docs)*2) srcIDs := make([]uint64, 0, len(docs)) for _, d := range docs { if d.WarehouseID > 0 { whIDs = append(whIDs, d.WarehouseID) } if d.ToWarehouseID > 0 { whIDs = append(whIDs, d.ToWarehouseID) } if d.SourceDocID > 0 { srcIDs = append(srcIDs, d.SourceDocID) } } names := map[uint64]string{} if len(whIDs) > 0 { var whs []models.ErpWarehouse _, _ = models.Orm.QueryTable(new(models.ErpWarehouse)). Filter("id__in", whIDs).All(&whs, "id", "warehouse_name") for _, w := range whs { names[w.ID] = w.WarehouseName } } srcNos := map[uint64]string{} if len(srcIDs) > 0 { var srcs []models.ErpDocument _, _ = models.Orm.QueryTable(new(models.ErpDocument)). Filter("id__in", srcIDs).All(&srcs, "id", "doc_no") for _, s := range srcs { srcNos[s.ID] = s.DocNo } } for i := range docs { docs[i].WarehouseName = names[docs[i].WarehouseID] docs[i].ToWarehouseName = names[docs[i].ToWarehouseID] docs[i].SourceDocNo = srcNos[docs[i].SourceDocID] docs[i].DocTypeLabel = models.ErpDocTypeLabel[docs[i].DocType] } }