增加租户官网模块
This commit is contained in:
@@ -0,0 +1,857 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendBannerController 官网 Banner 管理(租户端,tid 严格隔离)
|
||||
type BackendBannerController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendFriendlinkController 官网友情链接管理(租户端)
|
||||
type BackendFriendlinkController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendOnepageController 官网单页管理(租户端)
|
||||
type BackendOnepageController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendBannerController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendFriendlinkController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendOnepageController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendBannerController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendFriendlinkController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendOnepageController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func cmsBannerToMap(row models.CmsFrontendBanner) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"title": row.Title,
|
||||
"desc": row.Desc,
|
||||
"image": row.Image,
|
||||
"url": row.Url,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsFriendlinkToMap(row models.CmsFrontendFriendlink) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"link_name": row.LinkName,
|
||||
"link_url": row.LinkUrl,
|
||||
"link_logo": row.LinkLogo,
|
||||
"description": row.Description,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsOnepageToMap(row models.CmsFrontendOnepage) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"title": row.Title,
|
||||
"path": row.Path,
|
||||
"content": row.Content,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Banner ----------------
|
||||
|
||||
type cmsBannerPayload struct {
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Image string `json:"image"`
|
||||
Url string `json:"url"`
|
||||
Sort int `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// readBannerPayload 兼容 JSON 与 multipart/form-data 两种请求体
|
||||
// (前端 createbanner 以 multipart 头发送,实际可能是 JSON 体)
|
||||
func (c *BackendBannerController) readBannerPayload() (*cmsBannerPayload, error) {
|
||||
ct := c.Ctx.Request.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(ct, "multipart/form-data") {
|
||||
// cmsEffectiveTid 已触发 ParseForm,表单字段可直接读取
|
||||
sort, _ := c.GetInt("sort")
|
||||
status, _ := c.GetInt8("status", 1)
|
||||
return &cmsBannerPayload{
|
||||
Title: strings.TrimSpace(c.GetString("title")),
|
||||
Desc: strings.TrimSpace(c.GetString("desc")),
|
||||
Image: strings.TrimSpace(c.GetString("image")),
|
||||
Url: strings.TrimSpace(c.GetString("url")),
|
||||
Sort: sort,
|
||||
Status: status,
|
||||
}, nil
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p cmsBannerPayload
|
||||
p.Status = 1
|
||||
if len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return nil, fmt.Errorf("参数错误")
|
||||
}
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListAll GET /backend/allbanners
|
||||
func (c *BackendBannerController) ListAll() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.CmsFrontendBanner
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendBanner)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取Banner列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsBannerToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Create POST /backend/createbanner
|
||||
func (c *BackendBannerController) Create() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
p, err := c.readBannerPayload()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Title) == "" && strings.TrimSpace(p.Image) == "" {
|
||||
c.cmsJSONErr(400, 400, "标题与图片不能都为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsFrontendBanner{
|
||||
Tid: tid,
|
||||
Title: strings.TrimSpace(p.Title),
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
Image: strings.TrimSpace(p.Image),
|
||||
Url: strings.TrimSpace(p.Url),
|
||||
Sort: p.Sort,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if row.Status == 0 {
|
||||
row.Status = 1
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update POST /backend/editbanner/:id
|
||||
func (c *BackendBannerController) Update() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsBannerPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"title": strings.TrimSpace(p.Title),
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"image": strings.TrimSpace(p.Image),
|
||||
"url": strings.TrimSpace(p.Url),
|
||||
"sort": p.Sort,
|
||||
"update_time": now,
|
||||
}
|
||||
if p.Status > 0 {
|
||||
fields["status"] = p.Status
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendBanner)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "Banner不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deletebanner/:id
|
||||
func (c *BackendBannerController) Delete() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendBanner)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "Banner不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ---------------- 友情链接 ----------------
|
||||
|
||||
// List GET /backend/friendlinks(分页:page/limit/keyword/status)
|
||||
func (c *BackendFriendlinkController) List() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
limit, _ := c.GetInt("limit", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
|
||||
qs = qs.Filter("link_name__icontains", kw)
|
||||
}
|
||||
if st := strings.TrimSpace(c.GetString("status")); st != "" {
|
||||
if v, e := strconv.Atoi(st); e == nil {
|
||||
qs = qs.Filter("status", v)
|
||||
}
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsFrontendFriendlink
|
||||
offset := (page - 1) * limit
|
||||
_, err = qs.OrderBy("sort", "id").Limit(limit, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取友情链接失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsFriendlinkToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ListAll GET /backend/friendlinks/all
|
||||
func (c *BackendFriendlinkController) ListAll() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.CmsFrontendFriendlink
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取友情链接失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsFriendlinkToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsFriendlinkPayload struct {
|
||||
LinkName string `json:"link_name"`
|
||||
LinkUrl string `json:"link_url"`
|
||||
LinkLogo string `json:"link_logo"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// Create POST /backend/friendlinks
|
||||
func (c *BackendFriendlinkController) Create() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsFriendlinkPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.LinkName) == "" {
|
||||
c.cmsJSONErr(400, 400, "链接名称不能为空")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.LinkUrl) == "" {
|
||||
c.cmsJSONErr(400, 400, "链接地址不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsFrontendFriendlink{
|
||||
Tid: tid,
|
||||
LinkName: strings.TrimSpace(p.LinkName),
|
||||
LinkUrl: strings.TrimSpace(p.LinkUrl),
|
||||
LinkLogo: strings.TrimSpace(p.LinkLogo),
|
||||
Description: strings.TrimSpace(p.Description),
|
||||
Sort: p.Sort,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if row.Status == 0 {
|
||||
row.Status = 1
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update PUT /backend/friendlinks/:id(支持只传部分字段,如 {status:1})
|
||||
func (c *BackendFriendlinkController) Update() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
if v, ok := body["link_name"]; ok {
|
||||
fields["link_name"] = strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
if v, ok := body["link_url"]; ok {
|
||||
fields["link_url"] = strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
if v, ok := body["link_logo"]; ok {
|
||||
fields["link_logo"] = strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
if v, ok := body["description"]; ok {
|
||||
fields["description"] = strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
if v, ok := body["sort"]; ok {
|
||||
if n, e := strconv.Atoi(fmt.Sprintf("%.0f", toFloat(v))); e == nil {
|
||||
fields["sort"] = n
|
||||
}
|
||||
}
|
||||
if v, ok := body["status"]; ok {
|
||||
fields["status"] = int8(toFloat(v))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
c.cmsJSONErr(400, 400, "没有可更新的字段")
|
||||
return
|
||||
}
|
||||
fields["update_time"] = time.Now()
|
||||
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "友情链接不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// toFloat 宽松地把 JSON 值转成数字(兼容字符串数字)
|
||||
func toFloat(v interface{}) float64 {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return x
|
||||
case string:
|
||||
if n, err := strconv.ParseFloat(strings.TrimSpace(x), 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/friendlinks/:id
|
||||
func (c *BackendFriendlinkController) Delete() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "友情链接不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// BatchDelete POST /backend/friendlinks/batchdelete
|
||||
func (c *BackendFriendlinkController) BatchDelete() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []interface{} `json:"ids"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil || len(body.IDs) == 0 {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]uint64, 0, len(body.IDs))
|
||||
for _, v := range body.IDs {
|
||||
if n := cmsParseUintArg(v); n > 0 {
|
||||
ids = append(ids, n)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("tid", tid).
|
||||
Filter("id__in", ids).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ---------------- 单页 ----------------
|
||||
|
||||
type cmsOnepagePayload struct {
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Sort int `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// ListAll GET /backend/allonepages
|
||||
func (c *BackendOnepageController) ListAll() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.CmsFrontendOnepage
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取单页列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsOnepageToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Create POST /backend/createonepage
|
||||
func (c *BackendOnepageController) Create() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsOnepagePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
path := strings.TrimSpace(p.Path)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "标题不能为空")
|
||||
return
|
||||
}
|
||||
if path == "" {
|
||||
c.cmsJSONErr(400, 400, "路径标识不能为空")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(path, "/\\ ") {
|
||||
c.cmsJSONErr(400, 400, "路径标识只能包含字母数字与下划线")
|
||||
return
|
||||
}
|
||||
|
||||
// path 租户内唯一
|
||||
cnt, _ := models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("tid", tid).
|
||||
Filter("path", path).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt > 0 {
|
||||
c.cmsJSONErr(400, 400, "该路径标识已存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsFrontendOnepage{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Path: path,
|
||||
Content: p.Content,
|
||||
Sort: p.Sort,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if row.Status == 0 {
|
||||
row.Status = 1
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update POST /backend/editonepage/:id
|
||||
func (c *BackendOnepageController) Update() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsOnepagePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(p.Path)
|
||||
if path != "" && strings.ContainsAny(path, "/\\ ") {
|
||||
c.cmsJSONErr(400, 400, "路径标识只能包含字母数字与下划线")
|
||||
return
|
||||
}
|
||||
if path != "" {
|
||||
dup, _ := models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("tid", tid).
|
||||
Filter("path", path).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("id", id).
|
||||
Count()
|
||||
if dup > 0 {
|
||||
c.cmsJSONErr(400, 400, "该路径标识已存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"title": strings.TrimSpace(p.Title),
|
||||
"content": p.Content,
|
||||
"sort": p.Sort,
|
||||
"update_time": now,
|
||||
}
|
||||
if path != "" {
|
||||
fields["path"] = path
|
||||
}
|
||||
if p.Status > 0 {
|
||||
fields["status"] = p.Status
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "单页不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deleteonepage/:id
|
||||
func (c *BackendOnepageController) Delete() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "单页不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendThemeController 官网模板选用(租户端只读选用,模板管理在 platform 端)
|
||||
type BackendThemeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendThemeController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendThemeController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
var reThemeCode = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`)
|
||||
|
||||
// List GET /backend/theme 仅返回启用中的模板(含内置 default)
|
||||
func (c *BackendThemeController) List() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.CmsFrontendTemplate
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取模板列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows)+1)
|
||||
hasDefault := false
|
||||
for _, r := range rows {
|
||||
if r.Code == "default" {
|
||||
hasDefault = true
|
||||
}
|
||||
list = append(list, map[string]interface{}{
|
||||
"key": r.Code,
|
||||
"name": r.Name,
|
||||
"description": r.Description,
|
||||
"preview": r.Preview,
|
||||
"version": "1.0",
|
||||
"author": "",
|
||||
})
|
||||
}
|
||||
// 内置 default 模板始终可选,即使未登记入库
|
||||
if !hasDefault {
|
||||
list = append([]map[string]interface{}{{
|
||||
"key": "default",
|
||||
"name": "默认模板",
|
||||
"description": "系统内置默认模板",
|
||||
"preview": "/themes/default/static/preview.png",
|
||||
"version": "1.0",
|
||||
"author": "",
|
||||
}}, list...)
|
||||
}
|
||||
|
||||
current := cmsTenantTemplateCode(tid)
|
||||
if current == "" {
|
||||
current = "default"
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "currentTheme": current},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// cmsTenantTemplateCode 读取租户当前选用的模板编码
|
||||
func cmsTenantTemplateCode(tid uint64) string {
|
||||
var row models.TenantSiteSetting
|
||||
err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return row.TemplateCode
|
||||
}
|
||||
|
||||
// Switch POST /backend/theme/switch 切换租户使用的模板(写入 template_code)
|
||||
func (c *BackendThemeController) Switch() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
ThemeKey string `json:"theme_key"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if tid == 0 {
|
||||
tid = body.Tid
|
||||
}
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(body.ThemeKey)
|
||||
if code == "" || !reThemeCode.MatchString(code) {
|
||||
c.cmsJSONErr(400, 400, "模板编码不合法")
|
||||
return
|
||||
}
|
||||
// 非内置 default 模板必须已登记且启用中
|
||||
if code != "default" {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("code", code).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt == 0 {
|
||||
c.cmsJSONErr(400, 400, "模板不存在或已停用")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmsUpsertTenantTemplate(tid, code); err != nil {
|
||||
c.cmsJSONErr(500, 500, "切换失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "切换成功", "data": map[string]interface{}{"currentTheme": code}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// cmsUpsertTenantTemplate 写入租户模板编码(站点设置不存在则创建)
|
||||
func cmsUpsertTenantTemplate(tid uint64, code string) error {
|
||||
now := time.Now()
|
||||
cnt, err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt == 0 {
|
||||
_, err = models.Orm.Insert(&models.TenantSiteSetting{
|
||||
Tid: tid,
|
||||
TemplateCode: code,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"template_code": code, "update_time": now})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetData GET /backend/theme/data 返回站点设置(供模板配置展示)
|
||||
func (c *BackendThemeController) GetData() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.TenantSiteSetting
|
||||
err = models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"tid": tid,
|
||||
"sitename": row.Sitename,
|
||||
"logo": row.Logo,
|
||||
"logow": row.Logow,
|
||||
"ico": row.Ico,
|
||||
"description": row.Description,
|
||||
"copyright": row.Copyright,
|
||||
"companyname": row.Companyname,
|
||||
"icp": row.Icp,
|
||||
"companyintroduction": row.Companyintroduction,
|
||||
"template_code": row.TemplateCode,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveData POST /backend/theme/data 保存站点设置(模板配置用,白名单字段)
|
||||
func (c *BackendThemeController) SaveData() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
allowed := []string{"sitename", "logo", "logow", "ico", "description", "copyright", "companyname", "icp", "companyintroduction"}
|
||||
for _, k := range allowed {
|
||||
if v, ok := body[k]; ok {
|
||||
fields[k] = strings.TrimSpace(toString(v))
|
||||
}
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
c.cmsJSONErr(400, 400, "没有可保存的字段")
|
||||
return
|
||||
}
|
||||
fields["update_time"] = time.Now()
|
||||
|
||||
cnt, err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
now := time.Now()
|
||||
row := &models.TenantSiteSetting{Tid: tid, CreateTime: now, UpdateTime: &now}
|
||||
if v, ok := fields["sitename"]; ok {
|
||||
row.Sitename = v.(string)
|
||||
}
|
||||
if v, ok := fields["logo"]; ok {
|
||||
row.Logo = v.(string)
|
||||
}
|
||||
if v, ok := fields["logow"]; ok {
|
||||
row.Logow = v.(string)
|
||||
}
|
||||
if v, ok := fields["ico"]; ok {
|
||||
row.Ico = v.(string)
|
||||
}
|
||||
if v, ok := fields["description"]; ok {
|
||||
row.Description = v.(string)
|
||||
}
|
||||
if v, ok := fields["copyright"]; ok {
|
||||
row.Copyright = v.(string)
|
||||
}
|
||||
if v, ok := fields["companyname"]; ok {
|
||||
row.Companyname = v.(string)
|
||||
}
|
||||
if v, ok := fields["icp"]; ok {
|
||||
row.Icp = v.(string)
|
||||
}
|
||||
if v, ok := fields["companyintroduction"]; ok {
|
||||
row.Companyintroduction = v.(string)
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.cmsJSONErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields); err != nil {
|
||||
c.cmsJSONErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// toString 宽松地把 JSON 值转成字符串
|
||||
func toString(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/tagengine"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformTemplateController 官网模板管理(平台端专用)
|
||||
// 模板文件存磁盘 themes/{code}/,DB 仅登记元信息;租户端只读选用。
|
||||
type PlatformTemplateController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformTemplateController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// cmsThemesRoot 模板根目录(相对运行目录),渲染与静态路径保持一致
|
||||
func cmsThemesRoot() string {
|
||||
return "themes"
|
||||
}
|
||||
|
||||
// cmsTemplateSafePath 校验 code 与相对路径,返回模板文件的绝对安全路径(防目录穿越)
|
||||
func cmsTemplateSafePath(code, rel string) (string, error) {
|
||||
if !reThemeCode.MatchString(code) {
|
||||
return "", fmt.Errorf("模板编码不合法")
|
||||
}
|
||||
rel = strings.TrimSpace(rel)
|
||||
if rel == "" {
|
||||
return "", fmt.Errorf("文件路径不能为空")
|
||||
}
|
||||
rel = strings.ReplaceAll(rel, "\\", "/")
|
||||
if strings.Contains(rel, "..") {
|
||||
return "", fmt.Errorf("文件路径不合法")
|
||||
}
|
||||
cleaned := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+rel)), "/")
|
||||
if cleaned == "" || cleaned == "." {
|
||||
return "", fmt.Errorf("文件路径不合法")
|
||||
}
|
||||
root, err := filepath.Abs(cmsThemesRoot())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("模板目录解析失败")
|
||||
}
|
||||
full := filepath.Join(root, code, filepath.FromSlash(cleaned))
|
||||
abs, err := filepath.Abs(full)
|
||||
if err != nil || !strings.HasPrefix(abs, root+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("文件路径不合法")
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
// cmsTemplateDirExists 判断模板目录是否存在
|
||||
func cmsTemplateDirExists(code string) bool {
|
||||
if !reThemeCode.MatchString(code) {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(cmsThemesRoot(), code))
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func cmsTemplateToMap(row models.CmsFrontendTemplate) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"code": row.Code,
|
||||
"name": row.Name,
|
||||
"description": row.Description,
|
||||
"preview": row.Preview,
|
||||
"status": row.Status,
|
||||
"sort": row.Sort,
|
||||
"dir_exists": cmsTemplateDirExists(row.Code),
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
// Index GET /platform/template/index 模板列表(DB 登记)
|
||||
func (c *PlatformTemplateController) Index() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.CmsFrontendTemplate
|
||||
_, err := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.jsonErr(500, 500, "获取模板列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsTemplateToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": len(list)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// cmsScanThemeDirs 扫描 themes/ 目录,返回磁盘上的模板编码列表
|
||||
func cmsScanThemeDirs() []string {
|
||||
entries, err := os.ReadDir(cmsThemesRoot())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := []string{}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() && reThemeCode.MatchString(e.Name()) {
|
||||
out = append(out, e.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Scan POST /platform/template/scan 扫描 themes/ 目录,新目录自动登记入库
|
||||
func (c *PlatformTemplateController) Scan() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
codes := cmsScanThemeDirs()
|
||||
added := 0
|
||||
now := time.Now()
|
||||
for _, code := range codes {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("code", code).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt > 0 {
|
||||
continue
|
||||
}
|
||||
name := code
|
||||
if code == "default" {
|
||||
name = "默认模板"
|
||||
}
|
||||
row := models.CmsFrontendTemplate{
|
||||
Code: code,
|
||||
Name: name,
|
||||
Description: "目录扫描自动登记",
|
||||
Preview: "/themes/" + code + "/static/preview.png",
|
||||
Status: 1,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(&row); err == nil {
|
||||
added++
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "扫描完成",
|
||||
"data": map[string]interface{}{"scanned": len(codes), "added": added},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Upload POST /platform/template/upload 上传模板 zip(解压到 themes/{code}/)
|
||||
func (c *PlatformTemplateController) Upload() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
f, header, err := c.GetFile("file")
|
||||
if err != nil || header == nil {
|
||||
c.jsonErr(400, 400, "请上传模板 zip 文件")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
code := strings.TrimSpace(c.GetString("code"))
|
||||
if code == "" {
|
||||
// 未显式指定编码时取上传文件名(去扩展名)
|
||||
code = strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename))
|
||||
}
|
||||
code = strings.ToLower(strings.TrimSpace(code))
|
||||
if !reThemeCode.MatchString(code) {
|
||||
c.jsonErr(400, 400, "模板编码不合法(仅允许字母数字下划线中划线)")
|
||||
return
|
||||
}
|
||||
|
||||
if header.Size > 50<<20 {
|
||||
c.jsonErr(400, 400, "模板包过大(限制 50MB)")
|
||||
return
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "yz_template_*.zip")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "临时文件创建失败")
|
||||
return
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := io.Copy(tmp, f); err != nil {
|
||||
tmp.Close()
|
||||
c.jsonErr(500, 500, "上传失败")
|
||||
return
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := cmsUnzipTemplate(tmpPath, filepath.Join(cmsThemesRoot(), code)); err != nil {
|
||||
c.jsonErr(400, 400, "解压失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 解压成功后自动登记/更新 DB
|
||||
now := time.Now()
|
||||
cnt, _ := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("code", code).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt == 0 {
|
||||
row := models.CmsFrontendTemplate{
|
||||
Code: code,
|
||||
Name: code,
|
||||
Description: "zip 上传自动登记",
|
||||
Preview: "/themes/" + code + "/static/preview.png",
|
||||
Status: 1,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
_, _ = models.Orm.Insert(&row)
|
||||
} else {
|
||||
_, _ = models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("code", code).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"update_time": now})
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "上传成功",
|
||||
"data": map[string]interface{}{"code": code},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// cmsUnzipTemplate 解压模板 zip 到目标目录;
|
||||
// 支持 zip 内含单一顶层目录的情况(自动剥离),并拦截 zip-slip。
|
||||
func cmsUnzipTemplate(zipPath, destDir string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的 zip 文件")
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// 探测是否所有文件都在同一顶层目录下
|
||||
topDir := ""
|
||||
singleTop := true
|
||||
for _, f := range r.File {
|
||||
name := strings.ReplaceAll(f.Name, "\\", "/")
|
||||
if name == "" || strings.Contains(name, "..") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(strings.Trim(name, "/"), "/")
|
||||
if topDir == "" {
|
||||
topDir = parts[0]
|
||||
} else if parts[0] != topDir {
|
||||
singleTop = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(r.File) == 0 {
|
||||
return fmt.Errorf("zip 内容为空")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
return fmt.Errorf("模板目录创建失败")
|
||||
}
|
||||
destAbs, _ := filepath.Abs(destDir)
|
||||
|
||||
totalSize := int64(0)
|
||||
for _, f := range r.File {
|
||||
name := strings.ReplaceAll(f.Name, "\\", "/")
|
||||
if name == "" || strings.Contains(name, "..") {
|
||||
continue
|
||||
}
|
||||
rel := name
|
||||
if singleTop && topDir != "" {
|
||||
parts := strings.Split(strings.Trim(name, "/"), "/")
|
||||
if len(parts) == 1 && f.FileInfo().IsDir() {
|
||||
continue // 顶层目录本身
|
||||
}
|
||||
rel = strings.Join(parts[1:], "/")
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
target := filepath.Join(destAbs, filepath.FromSlash(rel))
|
||||
tabs, _ := filepath.Abs(target)
|
||||
if !strings.HasPrefix(tabs, destAbs+string(filepath.Separator)) {
|
||||
continue // 越界条目直接跳过
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
_ = os.MkdirAll(tabs, 0o755)
|
||||
continue
|
||||
}
|
||||
_ = os.MkdirAll(filepath.Dir(tabs), 0o755)
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 zip 条目失败")
|
||||
}
|
||||
out, err := os.Create(tabs)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return fmt.Errorf("写入模板文件失败")
|
||||
}
|
||||
n, err := io.Copy(out, rc)
|
||||
out.Close()
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入模板文件失败")
|
||||
}
|
||||
totalSize += n
|
||||
if totalSize > 200<<20 {
|
||||
return fmt.Errorf("解压内容过大(限制 200MB)")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Files GET /platform/template/files/:code 列出模板目录下可编辑文件
|
||||
func (c *PlatformTemplateController) Files() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
code := c.GetString(":code")
|
||||
if !cmsTemplateDirExists(code) {
|
||||
c.jsonErr(404, 404, "模板目录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
base := filepath.Join(cmsThemesRoot(), code)
|
||||
files := []map[string]interface{}{}
|
||||
_ = filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".html" && ext != ".css" && ext != ".js" {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(base, path)
|
||||
files = append(files, map[string]interface{}{
|
||||
"path": filepath.ToSlash(rel),
|
||||
"size": info.Size(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i]["path"].(string) < files[j]["path"].(string)
|
||||
})
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"code": code, "files": files},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ReadFile GET /platform/template/file?code=&path= 读取模板文件内容
|
||||
func (c *PlatformTemplateController) ReadFile() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
code := strings.TrimSpace(c.GetString("code"))
|
||||
path := strings.TrimSpace(c.GetString("path"))
|
||||
full, err := cmsTemplateSafePath(code, path)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(full))
|
||||
if ext != ".html" && ext != ".css" && ext != ".js" {
|
||||
c.jsonErr(400, 400, "仅支持读取 html/css/js 文件")
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"code": code, "path": path, "content": string(data)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveFile POST /platform/template/file/save 保存模板文件(路径校验防穿越)
|
||||
func (c *PlatformTemplateController) SaveFile() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Code string `json:"code"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
full, err := cmsTemplateSafePath(strings.TrimSpace(body.Code), body.Path)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(full))
|
||||
if ext != ".html" && ext != ".css" && ext != ".js" {
|
||||
c.jsonErr(400, 400, "仅支持保存 html/css/js 文件")
|
||||
return
|
||||
}
|
||||
if len(body.Content) > 2<<20 {
|
||||
c.jsonErr(400, 400, "文件内容过大(限制 2MB)")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(full, []byte(body.Content), 0o644); err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Status POST /platform/template/status 启用/停用模板
|
||||
func (c *PlatformTemplateController) Status() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint64 `json:"id"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil || body.ID == 0 {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if body.Status != 0 && body.Status != 1 {
|
||||
c.jsonErr(400, 400, "状态值不合法")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("id", body.ID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"status": body.Status, "update_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "操作成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /platform/template/delete/:id 删除登记(可选同时删除磁盘目录)
|
||||
func (c *PlatformTemplateController) Delete() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var body struct {
|
||||
RemoveFiles bool `json:"remove_files"`
|
||||
}
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &body)
|
||||
}
|
||||
|
||||
var row models.CmsFrontendTemplate
|
||||
err := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "模板不存在")
|
||||
return
|
||||
}
|
||||
if row.Code == "default" {
|
||||
c.jsonErr(400, 400, "内置默认模板不可删除")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if body.RemoveFiles && cmsTemplateDirExists(row.Code) {
|
||||
_ = os.RemoveAll(filepath.Join(cmsThemesRoot(), row.Code))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Tags GET /platform/template/tags 标签调用说明数据(由引擎注册表元信息生成)
|
||||
func (c *PlatformTemplateController) Tags() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": tagengine.TagDocs(),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/tagengine"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// TenantSiteController 租户官网前台渲染(Go 直出 HTML,无需登录)
|
||||
// 路由:GET / | /news | /news/:id | /page/:path
|
||||
type TenantSiteController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// siteNotFoundPage 未匹配域名的提示页
|
||||
const siteNotFoundPage = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><title>站点不存在</title>
|
||||
<style>body{font-family:"Microsoft YaHei",sans-serif;background:#f5f6f8;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
||||
.box{background:#fff;padding:48px 64px;border-radius:8px;box-shadow:0 2px 12px rgba(0,0,0,.06);text-align:center}
|
||||
h1{font-size:22px;margin:0 0 12px}p{color:#888;font-size:14px;margin:0}</style></head>
|
||||
<body><div class="box"><h1>站点不存在</h1><p>当前域名未绑定任何租户官网,请检查域名配置。</p></div></body>
|
||||
</html>`
|
||||
|
||||
// renderErrorPage 渲染失败的兜底页
|
||||
const renderErrorPage = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><title>页面异常</title>
|
||||
<style>body{font-family:"Microsoft YaHei",sans-serif;background:#f5f6f8;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
||||
.box{background:#fff;padding:48px 64px;border-radius:8px;box-shadow:0 2px 12px rgba(0,0,0,.06);text-align:center}
|
||||
h1{font-size:22px;margin:0 0 12px}p{color:#888;font-size:14px;margin:0}</style></head>
|
||||
<body><div class="box"><h1>页面暂时无法访问</h1><p>%s</p></div></body>
|
||||
</html>`
|
||||
|
||||
// resolveTid 按访问 Host 识别租户 ID。
|
||||
// 规则:dev 环境支持 X-Tenant-ID 头切换 → localhost/127.0.0.1 默认租户 1
|
||||
// → 平台主域名池命中返回租户 1(平台站点)→ 租户域名表命中返回对应 tid
|
||||
// → 全部未命中返回 0,false(不再默认回落租户 1)
|
||||
func (c *TenantSiteController) resolveTid() (uint64, bool) {
|
||||
runmode, _ := beego.AppConfig.String("runmode")
|
||||
if runmode == "dev" {
|
||||
if h := strings.TrimSpace(c.Ctx.Request.Header.Get("X-Tenant-ID")); h != "" {
|
||||
if decoded, err := url.QueryUnescape(h); err == nil {
|
||||
h = decoded
|
||||
}
|
||||
if n, err := strconv.ParseUint(h, 10, 64); err == nil && n > 0 {
|
||||
return n, true
|
||||
}
|
||||
var tenant models.SystemTenant
|
||||
cond := orm.NewCondition().Or("tenant_name", h).Or("tenant_short_name", h)
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).SetCond(cond).One(&tenant); err == nil {
|
||||
return tenant.ID, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host := c.Ctx.Request.Host
|
||||
if idx := strings.Index(host, ":"); idx > 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
if host == "localhost" || host == "127.0.0.1" {
|
||||
return 1, true
|
||||
}
|
||||
|
||||
// 平台主域名池命中 → 平台站点(tid 1 + 默认模板)
|
||||
poolCnt, _ := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("main_domain", host).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if poolCnt > 0 {
|
||||
return 1, true
|
||||
}
|
||||
|
||||
var td models.SystemTenantDomain
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("full_domain", host).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&td)
|
||||
if err == nil && td.Tid != nil && *td.Tid > 0 {
|
||||
return *td.Tid, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// resolveTemplateCode 解析租户选用模板编码,缺失/停用/目录不存在均回退 default
|
||||
func (c *TenantSiteController) resolveTemplateCode(tid uint64) string {
|
||||
code := strings.TrimSpace(cmsTenantTemplateCode(tid))
|
||||
if code != "" && code != "default" {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.CmsFrontendTemplate)).
|
||||
Filter("code", code).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt == 0 {
|
||||
code = ""
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
code = "default"
|
||||
}
|
||||
if !cmsTemplateDirExists(code) {
|
||||
code = "default"
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// writeHTML 直出 HTML
|
||||
func (c *TenantSiteController) writeHTML(status int, html string) {
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Ctx.WriteString(html)
|
||||
}
|
||||
|
||||
// render 解析并输出模板文件
|
||||
func (c *TenantSiteController) render(file string, mod func(ctx *tagengine.RenderCtx)) {
|
||||
tid, ok := c.resolveTid()
|
||||
if !ok {
|
||||
c.writeHTML(404, siteNotFoundPage)
|
||||
return
|
||||
}
|
||||
|
||||
code := c.resolveTemplateCode(tid)
|
||||
dir := filepath.Join(cmsThemesRoot(), code)
|
||||
if _, err := os.Stat(filepath.Join(dir, file)); err != nil {
|
||||
c.writeHTML(404, fmt.Sprintf(renderErrorPage, "模板页面不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := &tagengine.RenderCtx{Tid: tid, Page: 1, PageSize: 10}
|
||||
if mod != nil {
|
||||
mod(ctx)
|
||||
}
|
||||
out, err := tagengine.Render(dir, file, ctx)
|
||||
if err != nil {
|
||||
c.writeHTML(500, fmt.Sprintf(renderErrorPage, "模板渲染失败"))
|
||||
return
|
||||
}
|
||||
c.writeHTML(200, out)
|
||||
}
|
||||
|
||||
// Index GET / 首页
|
||||
func (c *TenantSiteController) Index() {
|
||||
c.render("index.html", nil)
|
||||
}
|
||||
|
||||
// News GET /news 新闻列表页(?page=N 分页)
|
||||
func (c *TenantSiteController) News() {
|
||||
page, _ := c.GetInt("page", 1)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
c.render("news.html", func(ctx *tagengine.RenderCtx) {
|
||||
ctx.Page = page
|
||||
})
|
||||
}
|
||||
|
||||
// NewsDetail GET /news/:id 文章详情页
|
||||
func (c *TenantSiteController) NewsDetail() {
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.writeHTML(404, fmt.Sprintf(renderErrorPage, "文章不存在"))
|
||||
return
|
||||
}
|
||||
c.render("news_detail.html", func(ctx *tagengine.RenderCtx) {
|
||||
ctx.ArticleID = id
|
||||
})
|
||||
}
|
||||
|
||||
// Page GET /page/:path 单页
|
||||
func (c *TenantSiteController) Page() {
|
||||
path := strings.TrimSpace(c.GetString(":path"))
|
||||
if path == "" || strings.ContainsAny(path, "/\\.") {
|
||||
c.writeHTML(404, fmt.Sprintf(renderErrorPage, "页面不存在"))
|
||||
return
|
||||
}
|
||||
c.render("page.html", func(ctx *tagengine.RenderCtx) {
|
||||
ctx.PagePath = path
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user