package controllers import ( "encoding/json" "io" "strconv" "strings" "time" "server/pkg/identity" "server/pkg/jwtutil" beego "github.com/beego/beego/v2/server/web" ) // AppErpBaseController 移动端进销存接口公共底座。 // // 与 /backend/erp/** 的关系(接口隔离规范):两端各有独立 handler 与路由, // 但单据校验、明细装配、库存/往来账联动与报表统计的口径只能有一份实现, // 全部在 erp_*_core.go 里;这里只负责「解移动端令牌 → 组装 erpActor → 调核心 → 输出响应」。 // // 鉴权:/api/** 只被 uniapp 调用,放行 app 与 backend 两类令牌,平台令牌不允许打移动端接口。 // 数据隔离:tenant_id 一律取自 JWT,前端无法指定租户。 type AppErpBaseController struct { beego.Controller } func (c *AppErpBaseController) erpClaims() (*jwtutil.Claims, error) { return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend) } // actor 组装核心逻辑需要的调用者身份 func (c *AppErpBaseController) actor(cl *jwtutil.Claims) erpActor { return erpNewActor(cl) } func (c *AppErpBaseController) erpOk(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } // erpListOK 分页出参形状与 Web 端保持一致:{list,total,page,pageSize,...extra} func (c *AppErpBaseController) erpListOK(list interface{}, total int64, page, pageSize int, extra map[string]interface{}) { data := map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize} for k, v := range extra { data[k] = v } c.erpOk(data) } func (c *AppErpBaseController) erpFail(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } // erpDenied 鉴权失败统一出口(返回 true 表示已响应,调用方应 return) func (c *AppErpBaseController) erpDenied(err error) bool { if err != nil { code := identity.Status(err) c.erpFail(code, code, err.Error()) return true } return false } func (c *AppErpBaseController) erpPathID() uint64 { id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) return id } // erpPage 分页参数:默认 20,上限 100(与 Web 端口径一致) func (c *AppErpBaseController) erpPage() (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 } // erpBody 解析 JSON 请求体;allowEmpty 为 true 时允许空 body(自动核销等场景) func (c *AppErpBaseController) erpBody(v interface{}, allowEmpty bool) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.erpFail(400, 400, "参数错误") return false } if len(strings.TrimSpace(string(raw))) == 0 { if allowEmpty { return true } c.erpFail(400, 400, "请求体为空") return false } if err := json.Unmarshal(raw, v); err != nil { c.erpFail(400, 400, "参数错误: "+err.Error()) return false } return true } func (c *AppErpBaseController) erpQuery(name string) string { return strings.TrimSpace(c.GetString(name)) } func (c *AppErpBaseController) erpUint(name string) uint64 { return erpUintParam(c.GetString(name)) } func (c *AppErpBaseController) erpFlag(name string) bool { return c.erpQuery(name) == "1" } // erpDateRange 报表/流水的日期区间:默认当月,date_to 含当天 func (c *AppErpBaseController) erpDateRange(defMonths int) (time.Time, time.Time) { from, to := erpMonthRange(defMonths) if d := c.erpQuery("date_from"); d != "" { if t, e := time.ParseInLocation("2006-01-02", d, time.Local); e == nil { from = t } } if d := c.erpQuery("date_to"); d != "" { if t, e := time.ParseInLocation("2006-01-02", d, time.Local); e == nil { to = t.AddDate(0, 0, 1) } } return from, to } // ---- 经营看板 / 字典 ---- // AppErpController 移动端进销存首页聚合与全局字典。 type AppErpController struct { AppErpBaseController } // Dashboard GET /api/erp/dashboard 经营看板(与 Web 端同一份统计口径) func (c *AppErpController) Dashboard() { cl, err := c.erpClaims() if c.erpDenied(err) { return } c.erpOk(erpDashboardCore(c.actor(cl), c.erpQuery("period"))) } // Meta GET /api/erp/meta 单据类型、状态、收付款方式等下拉字典 func (c *AppErpController) Meta() { cl, err := c.erpClaims() if c.erpDenied(err) { return } c.erpOk(erpDocMetaDict(c.actor(cl))) }