Files
yunzerwebsiteallinone/go/controllers/backend_erp_base.go
T
2026-10-19 01:13:17 +08:00

241 lines
7.2 KiB
Go

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]
}
}