Files
yunzerwebsiteallinone/go/controllers/platform_dict.go
T
2026-09-28 23:16:26 +08:00

734 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}, "")
}