package controllers import ( "encoding/json" "fmt" "io" "regexp" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // PlatformDictController 平台端数据字典:字典列表 + 字典项 // // 表:yz_platform_dict / yz_platform_dict_item(建表 SQL:docs/sql/create_dict.sql)。 // 平台端字典不按租户隔离;租户端字典见 BackendDictController(/backend/dict/*)。 type PlatformDictController struct { beego.Controller } // dictCodeRe 字典编码:字母开头,仅字母/数字/下划线 var dictCodeRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{0,99}$`) // dictTagTypes 字典项标签色(与前端 el-tag 的 type 对齐),空串表示不渲染标签 var dictTagTypes = map[string]bool{ "": true, "primary": true, "success": true, "warning": true, "danger": true, "info": true, } func (c *PlatformDictController) dictClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("认证信息格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("无效的token") } if claims.UserType != "platform" { return nil, fmt.Errorf("无权访问") } return claims, nil } func (c *PlatformDictController) dictErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *PlatformDictController) dictOk(data interface{}, msg string) { if msg == "" { msg = "success" } resp := map[string]interface{}{"code": 200, "msg": msg} if data != nil { resp["data"] = data } c.Data["json"] = resp _ = c.ServeJSON() } // dictPageSize 统一分页参数处理:page 从 1 开始,pageSize 限制在 1~100 func dictPageSize(c *beego.Controller) (page, pageSize, offset 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, (page - 1) * pageSize } type dictTypePayload struct { DictName string `json:"dict_name"` DictCode string `json:"dict_code"` Remark string `json:"remark"` Sort int `json:"sort"` Status *int8 `json:"status"` } type dictItemPayload struct { DictID uint64 `json:"dict_id"` ItemLabel string `json:"item_label"` ItemValue string `json:"item_value"` TagType string `json:"tag_type"` Remark string `json:"remark"` Sort int `json:"sort"` Status *int8 `json:"status"` IsDefault *int8 `json:"is_default"` } func parseDictTypeBody(c *beego.Controller) (*dictTypePayload, error) { body, _ := io.ReadAll(c.Ctx.Request.Body) var p dictTypePayload if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("参数错误") } p.DictName = strings.TrimSpace(p.DictName) p.DictCode = strings.TrimSpace(p.DictCode) p.Remark = strings.TrimSpace(p.Remark) return &p, nil } func parseDictItemBody(c *beego.Controller) (*dictItemPayload, error) { body, _ := io.ReadAll(c.Ctx.Request.Body) var p dictItemPayload if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("参数错误") } p.ItemLabel = strings.TrimSpace(p.ItemLabel) p.ItemValue = strings.TrimSpace(p.ItemValue) p.TagType = strings.TrimSpace(p.TagType) p.Remark = strings.TrimSpace(p.Remark) return &p, nil } func dictPathID(c *beego.Controller) uint64 { id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) return id } /* ---------------- 字典列表 ---------------- */ // ListTypes GET /platform/dict/types // 参数:page / pageSize / keyword(名称或编码模糊)/ status func (c *PlatformDictController) ListTypes() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } page, pageSize, offset := dictPageSize(&c.Controller) keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) cond := orm.NewCondition().And("delete_time__isnull", true) if keyword != "" { cond = cond.AndCond(orm.NewCondition(). Or("dict_name__contains", keyword). Or("dict_code__contains", keyword)) } if status != "" { if v, err := strconv.Atoi(status); err == nil { cond = cond.And("status", v) } } qs := models.Orm.QueryTable(new(models.PlatformDict)).SetCond(cond) total, err := qs.Count() if err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } var list []models.PlatformDict if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } c.dictOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }, "") } // CreateType POST /platform/dict/types func (c *PlatformDictController) CreateType() { claims, err := c.dictClaims() if err != nil { c.dictErr(401, 401, err.Error()) return } p, err := parseDictTypeBody(&c.Controller) if err != nil { c.dictErr(400, 400, err.Error()) return } if p.DictName == "" { c.dictErr(400, 400, "字典名称不能为空") return } if !dictCodeRe.MatchString(p.DictCode) { c.dictErr(400, 400, "字典编码以字母开头,只能包含字母、数字与下划线") return } exist, _ := models.Orm.QueryTable(new(models.PlatformDict)). Filter("dict_code", p.DictCode). Filter("delete_time__isnull", true). Count() if exist > 0 { c.dictErr(400, 400, "字典编码已存在") return } status := int8(1) if p.Status != nil { status = *p.Status } now := time.Now() row := &models.PlatformDict{ DictName: p.DictName, DictCode: p.DictCode, Remark: p.Remark, Sort: p.Sort, Status: status, CreateUserID: strconv.Itoa(claims.UserID), UpdateTime: &now, } id, err := models.Orm.Insert(row) if err != nil { c.dictErr(500, 500, "新增失败: "+err.Error()) return } row.ID = uint64(id) c.dictOk(row, "新增成功") } // UpdateType PUT /platform/dict/types/:id // 编码是业务取值依据,不允许修改。 func (c *PlatformDictController) UpdateType() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } id := dictPathID(&c.Controller) if id == 0 { c.dictErr(400, 400, "无效的ID") return } p, err := parseDictTypeBody(&c.Controller) if err != nil { c.dictErr(400, 400, err.Error()) return } if p.DictName == "" { c.dictErr(400, 400, "字典名称不能为空") return } var row models.PlatformDict if err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.dictErr(404, 404, "字典不存在") return } now := time.Now() row.DictName = p.DictName row.Remark = p.Remark row.Sort = p.Sort if p.Status != nil { row.Status = *p.Status } row.UpdateTime = &now if _, err := models.Orm.Update(&row); err != nil { c.dictErr(500, 500, "保存失败: "+err.Error()) return } c.dictOk(row, "保存成功") } // DeleteType DELETE /platform/dict/types/:id // 字典下还有字典项时拒绝删除,避免留下孤儿数据。 func (c *PlatformDictController) DeleteType() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } id := dictPathID(&c.Controller) if id == 0 { c.dictErr(400, 400, "无效的ID") return } var row models.PlatformDict if err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.dictErr(404, 404, "字典不存在") return } cnt, _ := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("dict_id", id). Filter("delete_time__isnull", true). Count() if cnt > 0 { c.dictErr(400, 400, fmt.Sprintf("该字典下还有 %d 个字典项,请先删除字典项", cnt)) return } now := time.Now() if _, err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("id", id). Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { c.dictErr(500, 500, "删除失败: "+err.Error()) return } c.dictOk(nil, "删除成功") } /* ---------------- 字典项 ---------------- */ // ListItems GET /platform/dict/items // 参数:dict_id(必填)/ page / pageSize / keyword(名称或值模糊)/ status func (c *PlatformDictController) ListItems() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } dictID, _ := c.GetInt64("dict_id") if dictID <= 0 { c.dictErr(400, 400, "dict_id 不能为空") return } page, pageSize, offset := dictPageSize(&c.Controller) keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) cond := orm.NewCondition(). And("delete_time__isnull", true). And("dict_id", dictID) if keyword != "" { cond = cond.AndCond(orm.NewCondition(). Or("item_label__contains", keyword). Or("item_value__contains", keyword)) } if status != "" { if v, err := strconv.Atoi(status); err == nil { cond = cond.And("status", v) } } qs := models.Orm.QueryTable(new(models.PlatformDictItem)).SetCond(cond) total, err := qs.Count() if err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } var list []models.PlatformDictItem if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } c.dictOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }, "") } // CreateItem POST /platform/dict/items func (c *PlatformDictController) CreateItem() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } p, err := parseDictItemBody(&c.Controller) if err != nil { c.dictErr(400, 400, err.Error()) return } if p.DictID == 0 { c.dictErr(400, 400, "dict_id 不能为空") return } if p.ItemLabel == "" { c.dictErr(400, 400, "字典项名称不能为空") return } if p.ItemValue == "" { c.dictErr(400, 400, "字典项值不能为空") return } if !dictTagTypes[p.TagType] { c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") return } var dict models.PlatformDict if err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("id", p.DictID). Filter("delete_time__isnull", true). One(&dict); err != nil { c.dictErr(404, 404, "字典不存在") return } cnt, _ := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("dict_id", p.DictID). Filter("item_value", p.ItemValue). Filter("delete_time__isnull", true). Count() if cnt > 0 { c.dictErr(400, 400, "同一字典下的字典项值不能重复") return } status := int8(1) if p.Status != nil { status = *p.Status } isDefault := int8(0) if p.IsDefault != nil { isDefault = *p.IsDefault } now := time.Now() row := &models.PlatformDictItem{ DictID: p.DictID, ItemLabel: p.ItemLabel, ItemValue: p.ItemValue, TagType: p.TagType, Sort: p.Sort, Status: status, IsDefault: isDefault, Remark: p.Remark, UpdateTime: &now, } id, err := models.Orm.Insert(row) if err != nil { c.dictErr(500, 500, "新增失败: "+err.Error()) return } row.ID = uint64(id) c.dictOk(row, "新增成功") } // UpdateItem PUT /platform/dict/items/:id // 字典项值是业务入库值,不允许修改,避免存量数据对不上。 func (c *PlatformDictController) UpdateItem() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } id := dictPathID(&c.Controller) if id == 0 { c.dictErr(400, 400, "无效的ID") return } p, err := parseDictItemBody(&c.Controller) if err != nil { c.dictErr(400, 400, err.Error()) return } if p.ItemLabel == "" { c.dictErr(400, 400, "字典项名称不能为空") return } if !dictTagTypes[p.TagType] { c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") return } var row models.PlatformDictItem if err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.dictErr(404, 404, "字典项不存在") return } now := time.Now() row.ItemLabel = p.ItemLabel row.TagType = p.TagType row.Sort = p.Sort row.Remark = p.Remark if p.Status != nil { row.Status = *p.Status } if p.IsDefault != nil { row.IsDefault = *p.IsDefault } row.UpdateTime = &now if _, err := models.Orm.Update(&row); err != nil { c.dictErr(500, 500, "保存失败: "+err.Error()) return } c.dictOk(row, "保存成功") } // ToggleItemStatus POST /platform/dict/items/:id/status func (c *PlatformDictController) ToggleItemStatus() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } id := dictPathID(&c.Controller) if id == 0 { c.dictErr(400, 400, "无效的ID") return } body, _ := io.ReadAll(c.Ctx.Request.Body) var p struct { Status *int8 `json:"status"` } if err := json.Unmarshal(body, &p); err != nil || p.Status == nil { c.dictErr(400, 400, "参数错误") return } if *p.Status != 0 && *p.Status != 1 { c.dictErr(400, 400, "状态只能是 0 或 1") return } var row models.PlatformDictItem if err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.dictErr(404, 404, "字典项不存在") return } now := time.Now() if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("id", id). Update(orm.Params{"status": *p.Status, "update_time": now}); err != nil { c.dictErr(500, 500, "操作失败: "+err.Error()) return } c.dictOk(nil, "操作成功") } // DeleteItem DELETE /platform/dict/items/:id func (c *PlatformDictController) DeleteItem() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } id := dictPathID(&c.Controller) if id == 0 { c.dictErr(400, 400, "无效的ID") return } var row models.PlatformDictItem if err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("id", id). Filter("delete_time__isnull", true). One(&row); err != nil { c.dictErr(404, 404, "字典项不存在") return } now := time.Now() if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("id", id). Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { c.dictErr(500, 500, "删除失败: "+err.Error()) return } c.dictOk(nil, "删除成功") } /* ---------------- 业务读取 ---------------- */ // OptionsAll GET /platform/dict/options // 一次取回全部「启用」字典的字典项,供前端登录后预热缓存: // 之后页面按任意 dict_code 都能同步读到,新增字典不用改前端。 func (c *PlatformDictController) OptionsAll() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } result := map[string][]models.PlatformDictItem{} var dicts []models.PlatformDict if _, err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("status", 1). Filter("delete_time__isnull", true). All(&dicts); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } if len(dicts) == 0 { c.dictOk(map[string]interface{}{"dicts": result}, "") return } ids := make([]uint64, 0, len(dicts)) codeByID := make(map[uint64]string, len(dicts)) for _, d := range dicts { ids = append(ids, d.ID) codeByID[d.ID] = d.DictCode result[d.DictCode] = []models.PlatformDictItem{} } var items []models.PlatformDictItem if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("dict_id__in", ids). Filter("status", 1). Filter("delete_time__isnull", true). OrderBy("sort", "id"). All(&items); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } for _, it := range items { code := codeByID[it.DictID] result[code] = append(result[code], it) } c.dictOk(map[string]interface{}{"dicts": result}, "") } // OptionsBatch POST /platform/dict/options // 一次取多个字典的启用项,供前端预加载(body: {"codes":["payment_channel",...]})。 // 编码不存在或已停用时返回空数组,前端不必再单独重试。 // 注意:这里刻意与 GET /options/:code 分开成「同段不同方法」,避免静态段与通配段同级。 func (c *PlatformDictController) OptionsBatch() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } body, _ := io.ReadAll(c.Ctx.Request.Body) var p struct { Codes []string `json:"codes"` } if err := json.Unmarshal(body, &p); err != nil { c.dictErr(400, 400, "参数错误") return } result := map[string][]models.PlatformDictItem{} codes := make([]string, 0, len(p.Codes)) for _, code := range p.Codes { code = strings.TrimSpace(code) if code == "" { continue } if _, ok := result[code]; ok { continue } result[code] = []models.PlatformDictItem{} codes = append(codes, code) } if len(codes) == 0 { c.dictOk(map[string]interface{}{"dicts": result}, "") return } // 先按编码取字典,再按字典ID一次取回全部字典项,避免逐个字典查一轮(N+1) var dicts []models.PlatformDict if _, err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("dict_code__in", codes). Filter("status", 1). Filter("delete_time__isnull", true). All(&dicts); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } if len(dicts) == 0 { c.dictOk(map[string]interface{}{"dicts": result}, "") return } ids := make([]uint64, 0, len(dicts)) codeByID := make(map[uint64]string, len(dicts)) for _, d := range dicts { ids = append(ids, d.ID) codeByID[d.ID] = d.DictCode } var items []models.PlatformDictItem if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("dict_id__in", ids). Filter("status", 1). Filter("delete_time__isnull", true). OrderBy("sort", "id"). All(&items); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } for _, it := range items { code := codeByID[it.DictID] result[code] = append(result[code], it) } c.dictOk(map[string]interface{}{"dicts": result}, "") } // Options GET /platform/dict/options/:code // 业务页面按编码取选项:只返回「字典与字典项都启用」的项,按 sort 升序。 func (c *PlatformDictController) Options() { if _, err := c.dictClaims(); err != nil { c.dictErr(401, 401, err.Error()) return } code := strings.TrimSpace(c.Ctx.Input.Param(":code")) if code == "" { c.dictErr(400, 400, "字典编码不能为空") return } var dict models.PlatformDict if err := models.Orm.QueryTable(new(models.PlatformDict)). Filter("dict_code", code). Filter("status", 1). Filter("delete_time__isnull", true). One(&dict); err != nil { c.dictErr(404, 404, "字典不存在或已停用") return } var list []models.PlatformDictItem if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). Filter("dict_id", dict.ID). Filter("status", 1). Filter("delete_time__isnull", true). OrderBy("sort", "id"). All(&list); err != nil { c.dictErr(500, 500, "查询失败: "+err.Error()) return } c.dictOk(map[string]interface{}{ "dict_id": dict.ID, "dict_code": dict.DictCode, "dict_name": dict.DictName, "list": list, }, "") }